754d7f1e92
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>
176 lines
5.0 KiB
GDScript
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())
|