Files
Grovewright/game/scripts/main.gd
T
Steven Wroblewski 754d7f1e92 Speichern meldet Fehler und schützt unbrauchbare Stände
Lässt sich ein unbrauchbarer Spielstand nicht beiseitelegen, bleibt das
Speichern gesperrt, bis ein Stand erfolgreich geladen wird; so wird er
nie überschrieben. Ein gescheitertes automatisches Speichern zeigt
SAVE_FAILED, und jedes gescheiterte Speichern wird protokolliert.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-10-01 12:15:05 +02:00

176 lines
5.0 KiB
GDScript

extends Node2D
## Composition root of a solo session: connects the simulation core with the
## map view, the figure, the HUD and the files on disk.
## Seed of a new world until the slice ships its chosen seed.
const NEW_WORLD_SEED := 2026
const PLAYER_NAME := "Spieler"
## How far the pickaxe reaches, in tiles from the figure to the tile centre.
const REACH_TILES := 2.5
## Ticks between two automatic saves: two minutes.
const AUTOSAVE_TICKS := 2400
const CAMERA_ZOOM := Vector2(3.0, 3.0)
## Folder for the player key, the save and the recordings. Tests use their
## own folder.
@export var storage_dir := "user://"
var session: GwSession
var store: SaveStore
var clock: TickClock
var map_view: MapView
var marker: TileMarker
var avatar: Avatar
var hud: Hud
var _player_key: PackedByteArray
var _next_dig_tick := 0
## True while an unusable save could not be set aside. Saving is refused
## then, so that the unusable save is never overwritten.
var _save_blocked := false
func _ready() -> void:
I18n.install()
InputSetup.install()
store = SaveStore.new(storage_dir)
_player_key = store.player_key()
clock = TickClock.new(GwSession.TICKS_PER_SECOND)
map_view = MapView.new()
add_child(map_view)
marker = TileMarker.new()
add_child(marker)
avatar = Avatar.new()
add_child(avatar)
var camera := Camera2D.new()
camera.zoom = CAMERA_ZOOM
avatar.add_child(camera)
hud = Hud.new()
add_child(hud)
if not _start_from_save():
_start_new_world()
func _process(delta: float) -> void:
if session == null or session.is_broken():
return
_update_marker()
if Input.is_action_pressed("dig") and not clock.paused:
_try_dig()
for i in clock.advance(delta):
_run_tick()
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("save_game"):
hud.flash("SAVED" if save_game() else "SAVE_FAILED")
elif event.is_action_pressed("load_game"):
_write_recording()
hud.flash("LOADED" if _start_from_save() else "LOAD_FAILED")
elif event.is_action_pressed("toggle_pause"):
clock.paused = not clock.paused
avatar.set_process(not clock.paused)
hud.show_paused(clock.paused)
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_CLOSE_REQUEST and session != null:
if not save_game():
push_warning("the game could not be saved on close")
_write_recording()
func _start_new_world() -> void:
var fresh := GwSession.new()
fresh.start_new(NEW_WORLD_SEED)
_enter(fresh)
## Continues the saved world. Returns false if there is no usable save. An
## unusable save is set aside, so that the next save does not overwrite it.
func _start_from_save() -> bool:
if not store.has_save():
return false
var loaded := GwSession.new()
if not loaded.start_loaded(store.read_save()):
var aside := store.set_aside_save()
if aside.is_empty():
_save_blocked = true
push_warning("save not usable (%s) and could not be set aside; saving is off" % loaded.last_error())
else:
push_warning("save not usable (%s), kept as %s" % [loaded.last_error(), aside])
hud.flash("LOAD_FAILED")
return false
_save_blocked = false
_enter(loaded)
return true
## Makes `next` the running session and lets the local player join it.
func _enter(next: GwSession) -> void:
session = next
session.join(_player_key, PLAYER_NAME)
_run_tick()
_next_dig_tick = 0
map_view.session = session
map_view.queue_redraw()
avatar.is_walkable = session.is_walkable
avatar.position = GridMotion.tile_center(session.spawn_point())
hud.show_stone(session.item_count(GwSession.ITEM_STONE))
func _run_tick() -> void:
_apply(session.step())
if session.tick() % AUTOSAVE_TICKS == 0 and not save_game():
hud.flash("SAVE_FAILED")
push_warning("the automatic save failed")
func _apply(events: Array[Dictionary]) -> void:
var local := session.local_player()
for event in events:
match event["type"]:
"tile_hit", "tile_changed":
map_view.queue_redraw()
"item_gained":
if event["player"] == local:
hud.show_stone(session.item_count(GwSession.ITEM_STONE))
"inventory_full":
if event["player"] == local:
hud.flash("INVENTORY_FULL")
if session.is_broken():
hud.flash("SIMULATION_STOPPED")
push_error(session.last_error())
func _update_marker() -> void:
var tile := GridMotion.tile_of(get_global_mouse_position())
var distance := avatar.position.distance_to(GridMotion.tile_center(tile))
marker.show_tile(tile, distance <= REACH_TILES * GridMotion.TILE_SIZE)
func _try_dig() -> void:
if marker.in_reach:
dig_at(marker.tile)
## Queues a pickaxe hit on `tile`, at most once per cooldown. The core checks
## everything else.
func dig_at(tile: Vector2i) -> void:
if session.tick() < _next_dig_tick:
return
session.dig(tile.x, tile.y)
_next_dig_tick = session.tick() + GwSession.DIG_COOLDOWN_TICKS
## Writes the running world as the save. Returns false if nothing was
## written, also while an unusable save could not be set aside.
func save_game() -> bool:
if _save_blocked:
return false
return store.write_save(session.save())
func _write_recording() -> void:
store.write_recording(session.recording_bytes())