bd8533e189
Der Kommentar an dig_at sagte, der Kern prüfe alles außer der Abklingzeit. Das stimmt nicht: Der Kern prüft Abklingzeit, Feld, Gelände, Werkzeugstufe und freien Platz, aber nicht die Reichweite. Die prüft nur die Hülle (in _update_marker und _try_dig), weil die Figuren außerhalb des Gleichschritts laufen und der Kern ihren Ort nicht kennt. Der Plan für M1c darf sich nicht darauf verlassen. Der Kommentar nennt jetzt, was der Aufrufer und was der Kern prüft. AUTOSAVE_TICKS wiederholte mit 2400 die Taktrate von 20 pro Sekunde. Die Konstante wird jetzt als zwei Minuten aus GwSession.TICKS_PER_SECOND abgeleitet; GDScript nimmt das in einer const an und rechnet 2400. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
178 lines
5.2 KiB
GDScript
178 lines
5.2 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 := 2 * 60 * GwSession.TICKS_PER_SECOND
|
|
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 caller must
|
|
## check the reach: figures move outside the lockstep, so the core does not
|
|
## know where the figure is. The core checks the cooldown, the tile, its
|
|
## terrain, the tool tier and the free space in the inventory.
|
|
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())
|