Darstellung und Hauptszene
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>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
class_name Hud
|
||||
extends CanvasLayer
|
||||
## Text over the game: the stone count, short messages and a help line.
|
||||
|
||||
const MESSAGE_SECONDS := 2.5
|
||||
|
||||
var _stone := Label.new()
|
||||
var _message := Label.new()
|
||||
var _help := Label.new()
|
||||
var _message_left := 0.0
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_stone.position = Vector2(16, 12)
|
||||
_stone.add_theme_font_size_override("font_size", 22)
|
||||
_message.position = Vector2(16, 48)
|
||||
_message.add_theme_font_size_override("font_size", 18)
|
||||
_help.text = tr("HELP")
|
||||
_help.add_theme_font_size_override("font_size", 14)
|
||||
for label: Label in [_stone, _message, _help]:
|
||||
label.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||
label.add_theme_constant_override("outline_size", 4)
|
||||
add_child(label)
|
||||
_help.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_LEFT, Control.PRESET_MODE_MINSIZE, 16)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _message_left <= 0.0:
|
||||
return
|
||||
_message_left -= delta
|
||||
if _message_left <= 0.0:
|
||||
_message.text = ""
|
||||
|
||||
|
||||
func show_stone(count: int) -> void:
|
||||
_stone.text = tr("STONE_COUNT") % count
|
||||
|
||||
|
||||
## Shows the text for `key` for a few seconds.
|
||||
func flash(key: String) -> void:
|
||||
_message.text = tr(key)
|
||||
_message_left = MESSAGE_SECONDS
|
||||
|
||||
|
||||
## Shows or hides the pause notice. It stays until the game continues.
|
||||
func show_paused(paused: bool) -> void:
|
||||
_message.text = tr("PAUSED") if paused else ""
|
||||
_message_left = 0.0
|
||||
|
||||
|
||||
## The text currently shown as a message. Tests read it.
|
||||
func message() -> String:
|
||||
return _message.text
|
||||
|
||||
|
||||
## The text currently shown as the stone count. Tests read it.
|
||||
func stone_text() -> String:
|
||||
return _stone.text
|
||||
@@ -0,0 +1 @@
|
||||
uid://cosq3mrn0ve5v
|
||||
@@ -0,0 +1,162 @@
|
||||
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())
|
||||
@@ -0,0 +1 @@
|
||||
uid://by56f6vi542a1
|
||||
@@ -0,0 +1,48 @@
|
||||
class_name MapView
|
||||
extends Node2D
|
||||
## Draws the terrain of the shared world. The colours are placeholders until
|
||||
## a style is chosen from the samples of plan M1d.
|
||||
|
||||
const CRACK_COLOR := Color(0.1, 0.08, 0.06, 0.8)
|
||||
|
||||
var session: GwSession
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
if session == null or not session.is_running():
|
||||
return
|
||||
var width := session.map_width()
|
||||
var height := session.map_height()
|
||||
var terrain := session.terrain_codes()
|
||||
var damage := session.damage_codes()
|
||||
var size := GridMotion.TILE_SIZE
|
||||
for y in height:
|
||||
for x in width:
|
||||
var index := y * width + x
|
||||
var rect := Rect2(x * size, y * size, size, size)
|
||||
draw_rect(rect, color_of(terrain[index]))
|
||||
if damage[index] > 0:
|
||||
_draw_cracks(rect, damage[index])
|
||||
|
||||
|
||||
## The placeholder colour of a terrain code.
|
||||
static func color_of(terrain: int) -> Color:
|
||||
if terrain == GwSession.TERRAIN_GROUND:
|
||||
return Color(0.42, 0.36, 0.28)
|
||||
if terrain == GwSession.TERRAIN_SOFT_ROCK:
|
||||
return Color(0.56, 0.55, 0.52)
|
||||
if terrain == GwSession.TERRAIN_HARD_ROCK:
|
||||
return Color(0.33, 0.33, 0.37)
|
||||
if terrain == GwSession.TERRAIN_BEDROCK:
|
||||
return Color(0.12, 0.12, 0.14)
|
||||
return Color.MAGENTA
|
||||
|
||||
|
||||
## One crack line for every hit the tile has taken.
|
||||
func _draw_cracks(rect: Rect2, hits: int) -> void:
|
||||
var inset := rect.grow(-3.0)
|
||||
for line in hits:
|
||||
var share := (line + 1.0) / (hits + 1.0)
|
||||
var from := Vector2(inset.position.x, inset.position.y + inset.size.y * share)
|
||||
var to := Vector2(inset.end.x, inset.position.y + inset.size.y * (1.0 - share))
|
||||
draw_line(from, to, CRACK_COLOR, 1.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dasl7vpyifxob
|
||||
@@ -0,0 +1,25 @@
|
||||
class_name TileMarker
|
||||
extends Node2D
|
||||
## Outlines the tile under the mouse: green when the figure can reach it,
|
||||
## red when it is too far away.
|
||||
|
||||
const REACHABLE_COLOR := Color(0.4, 1.0, 0.4)
|
||||
const UNREACHABLE_COLOR := Color(1.0, 0.4, 0.4)
|
||||
|
||||
var tile := Vector2i(-1, -1)
|
||||
var in_reach := false
|
||||
|
||||
|
||||
## Moves the outline to `new_tile`. Redraws only when something changed.
|
||||
func show_tile(new_tile: Vector2i, reachable: bool) -> void:
|
||||
if new_tile == tile and reachable == in_reach:
|
||||
return
|
||||
tile = new_tile
|
||||
in_reach = reachable
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var size := GridMotion.TILE_SIZE
|
||||
var color := REACHABLE_COLOR if in_reach else UNREACHABLE_COLOR
|
||||
draw_rect(Rect2(Vector2(tile) * size, Vector2(size, size)), color, false, 1.0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cjnucbgwr65ud
|
||||
Reference in New Issue
Block a user