68483c648e
Die Hauptszene verbindet Kern, Karte, Figur, Anzeige und Dateien: fester Takt, Laufen, Stein schlagen in Reichweite, Speichern mit F5, Laden mit F9, Pause mit Esc, automatisches Speichern alle zwei Minuten und beim Schließen. Die Karte erscheint in Platzhalterfarben, bis in M1d ein Stil gewählt ist. Ein Werkzeug macht Bildschirmfotos ohne Handarbeit. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
163 lines
4.4 KiB
GDScript
163 lines
4.4 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
|
|
|
|
|
|
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())
|