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 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: save_game() _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() push_warning("save not usable (%s), kept as %s" % [loaded.last_error(), aside]) hud.flash("LOAD_FAILED") return 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: save_game() 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. func save_game() -> bool: return store.write_save(session.save()) func _write_recording() -> void: store.write_recording(session.recording_bytes())