Add one-slot demo snapshot save and load.
This commit is contained in:
@@ -30,8 +30,11 @@ The demo is the physical substrate for a magical-law factory game, not the final
|
||||
- `WASD`: move camera
|
||||
- Mouse wheel: zoom
|
||||
- `N`: restart the demo site
|
||||
- `Esc`: pause, autosave, and open the snapshot menu
|
||||
- `F1`: toggle developer statistics
|
||||
|
||||
One-slot snapshots live in the Godot user data folder as `mana_foundry_slot1.json`. The title screen **继续构筑** button restores construction, belts, machines, mana, items, and objective progress.
|
||||
|
||||
## Running
|
||||
|
||||
Open `project.godot` with Godot 4.7.1 stable, or run the workspace-local engine after Task 1 has completed.
|
||||
|
||||
+147
-5
@@ -1,6 +1,7 @@
|
||||
extends Node3D
|
||||
|
||||
const WorldSimulation = preload("res://src/simulation/world_simulation.gd")
|
||||
const DemoSnapshot = preload("res://src/persistence/demo_snapshot.gd")
|
||||
|
||||
const CELL_SIZE := 2.0
|
||||
const MAP_HALF_WIDTH := 10
|
||||
@@ -113,6 +114,8 @@ var bridge_button: Button
|
||||
var victory_shown := false
|
||||
var selected_building_id := -1
|
||||
var charger_machine_id := -1
|
||||
var continue_button: Button
|
||||
var save_status_label: Label
|
||||
var stats_visible := true
|
||||
var stats_refresh := 0.0
|
||||
var last_tick_microseconds := 0
|
||||
@@ -1068,6 +1071,7 @@ func _create_ui() -> void:
|
||||
start_overlay = _create_start_overlay(canvas)
|
||||
pause_overlay = _create_pause_overlay(canvas)
|
||||
victory_overlay = _create_victory_overlay(canvas)
|
||||
_refresh_continue_button()
|
||||
|
||||
|
||||
func _add_tool_button(parent: Control, text: String, kind: StringName) -> void:
|
||||
@@ -1093,7 +1097,7 @@ func _create_start_overlay(canvas: CanvasLayer) -> Control:
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
shade.add_child(center)
|
||||
var panel := PanelContainer.new()
|
||||
panel.custom_minimum_size = Vector2(600.0, 370.0)
|
||||
panel.custom_minimum_size = Vector2(600.0, 430.0)
|
||||
panel.add_theme_stylebox_override("panel", _panel_style(Color("#0d222df2"), COLOR_CYAN, 2))
|
||||
center.add_child(panel)
|
||||
var box := VBoxContainer.new()
|
||||
@@ -1120,8 +1124,14 @@ func _create_start_overlay(canvas: CanvasLayer) -> Control:
|
||||
start_button.text = "开始构筑"
|
||||
start_button.custom_minimum_size = Vector2(220.0, 48.0)
|
||||
start_button.add_theme_font_size_override("font_size", 18)
|
||||
start_button.pressed.connect(_start_game)
|
||||
start_button.pressed.connect(_start_new_game)
|
||||
box.add_child(start_button)
|
||||
continue_button = Button.new()
|
||||
continue_button.text = "继续构筑"
|
||||
continue_button.custom_minimum_size = Vector2(220.0, 44.0)
|
||||
continue_button.add_theme_font_size_override("font_size", 16)
|
||||
continue_button.pressed.connect(_continue_game)
|
||||
box.add_child(continue_button)
|
||||
var note := _label("第一目标:接通魔力,再按 B 分别为两台机器指定取货与放货点", 12, COLOR_MUTED)
|
||||
note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(note)
|
||||
@@ -1140,12 +1150,12 @@ func _create_pause_overlay(canvas: CanvasLayer) -> Control:
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
shade.add_child(center)
|
||||
var panel := PanelContainer.new()
|
||||
panel.custom_minimum_size = Vector2(360.0, 190.0)
|
||||
panel.custom_minimum_size = Vector2(360.0, 320.0)
|
||||
panel.add_theme_stylebox_override("panel", _panel_style(COLOR_PANEL, COLOR_VIOLET, 2))
|
||||
center.add_child(panel)
|
||||
var box := VBoxContainer.new()
|
||||
box.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
box.add_theme_constant_override("separation", 16)
|
||||
box.add_theme_constant_override("separation", 10)
|
||||
panel.add_child(box)
|
||||
var title := _label("工坊已暂停", 26, COLOR_TEXT)
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
@@ -1155,6 +1165,24 @@ func _create_pause_overlay(canvas: CanvasLayer) -> Control:
|
||||
resume.custom_minimum_size = Vector2(180.0, 42.0)
|
||||
resume.pressed.connect(_set_paused.bind(false))
|
||||
box.add_child(resume)
|
||||
var save_button := Button.new()
|
||||
save_button.text = "保存工坊"
|
||||
save_button.custom_minimum_size = Vector2(180.0, 36.0)
|
||||
save_button.pressed.connect(_save_demo_snapshot)
|
||||
box.add_child(save_button)
|
||||
var title_button := Button.new()
|
||||
title_button.text = "回到标题"
|
||||
title_button.custom_minimum_size = Vector2(180.0, 36.0)
|
||||
title_button.pressed.connect(_return_to_title)
|
||||
box.add_child(title_button)
|
||||
var quit_button := Button.new()
|
||||
quit_button.text = "退出"
|
||||
quit_button.custom_minimum_size = Vector2(180.0, 36.0)
|
||||
quit_button.pressed.connect(_quit_game)
|
||||
box.add_child(quit_button)
|
||||
save_status_label = _label("", 12, COLOR_MUTED)
|
||||
save_status_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
box.add_child(save_status_label)
|
||||
return shade
|
||||
|
||||
|
||||
@@ -1197,12 +1225,126 @@ func _create_victory_overlay(canvas: CanvasLayer) -> Control:
|
||||
func _start_game() -> void:
|
||||
start_overlay.visible = false
|
||||
simulation_enabled = true
|
||||
game_paused = false
|
||||
_set_paused(false)
|
||||
|
||||
|
||||
func _start_new_game() -> void:
|
||||
_reset_demo()
|
||||
_start_game()
|
||||
|
||||
|
||||
func _continue_game() -> void:
|
||||
if not _load_demo_snapshot():
|
||||
_reset_demo()
|
||||
_start_game()
|
||||
|
||||
|
||||
func _set_paused(value: bool) -> void:
|
||||
game_paused = value
|
||||
if pause_overlay != null:
|
||||
pause_overlay.visible = value
|
||||
if value:
|
||||
_save_demo_snapshot()
|
||||
|
||||
|
||||
func _return_to_title() -> void:
|
||||
_save_demo_snapshot()
|
||||
simulation_enabled = false
|
||||
_set_paused(false)
|
||||
if start_overlay != null:
|
||||
start_overlay.visible = true
|
||||
_refresh_continue_button()
|
||||
|
||||
|
||||
func _quit_game() -> void:
|
||||
_save_demo_snapshot()
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func _refresh_continue_button() -> void:
|
||||
if continue_button == null:
|
||||
return
|
||||
continue_button.disabled = not DemoSnapshot.exists()
|
||||
continue_button.modulate = Color.WHITE if not continue_button.disabled else COLOR_MUTED
|
||||
|
||||
|
||||
func collect_demo_snapshot() -> Dictionary:
|
||||
return {
|
||||
"schema": DemoSnapshot.SCHEMA,
|
||||
"saved_at": Time.get_datetime_string_from_system(true, true),
|
||||
"victory_shown": victory_shown,
|
||||
"selected_kind": String(selected_kind),
|
||||
"selected_rotation": selected_rotation,
|
||||
"selected_building_id": selected_building_id,
|
||||
"anchor_machine_id": anchor_machine_id,
|
||||
"charger_machine_id": charger_machine_id,
|
||||
"camera_focus": {
|
||||
"x": camera_focus.x,
|
||||
"y": camera_focus.y,
|
||||
"z": camera_focus.z,
|
||||
},
|
||||
"camera_distance": camera_distance,
|
||||
"simulation": simulation.to_snapshot(),
|
||||
}
|
||||
|
||||
|
||||
func apply_demo_snapshot(payload: Dictionary) -> bool:
|
||||
if payload.is_empty() or int(payload.get("schema", 0)) != DemoSnapshot.SCHEMA:
|
||||
return false
|
||||
var simulation_payload: Dictionary = payload.get("simulation", {})
|
||||
if not simulation.apply_snapshot(simulation_payload):
|
||||
return false
|
||||
victory_shown = bool(payload.get("victory_shown", false))
|
||||
selected_kind = StringName(String(payload.get("selected_kind", String(WorldSimulation.KIND_BELT))))
|
||||
selected_rotation = int(payload.get("selected_rotation", 0))
|
||||
selected_building_id = int(payload.get("selected_building_id", -1))
|
||||
anchor_machine_id = int(payload.get("anchor_machine_id", -1))
|
||||
charger_machine_id = int(payload.get("charger_machine_id", -1))
|
||||
var focus: Dictionary = payload.get("camera_focus", {})
|
||||
camera_focus = Vector3(
|
||||
float(focus.get("x", 0.0)),
|
||||
float(focus.get("y", 0.0)),
|
||||
float(focus.get("z", 0.0))
|
||||
)
|
||||
camera_distance = float(payload.get("camera_distance", 28.0))
|
||||
_relink_locked_machines()
|
||||
_sync_building_visuals()
|
||||
_capture_item_positions_after_tick()
|
||||
previous_item_positions = current_item_positions.duplicate()
|
||||
simulation_accumulator = 0.0
|
||||
if victory_overlay != null:
|
||||
victory_overlay.visible = false
|
||||
_update_camera_transform()
|
||||
_update_tool_ui()
|
||||
_update_objective()
|
||||
_update_inspect_panel()
|
||||
return true
|
||||
|
||||
|
||||
func _save_demo_snapshot() -> bool:
|
||||
var ok: bool = DemoSnapshot.save_payload(collect_demo_snapshot())
|
||||
if save_status_label != null:
|
||||
save_status_label.text = "已写入单槽存档。" if ok else "保存失败。"
|
||||
_refresh_continue_button()
|
||||
return ok
|
||||
|
||||
|
||||
func _load_demo_snapshot() -> bool:
|
||||
return apply_demo_snapshot(DemoSnapshot.load_payload())
|
||||
|
||||
|
||||
func _relink_locked_machines() -> void:
|
||||
if simulation.buildings.has(anchor_machine_id) and simulation.buildings.has(charger_machine_id):
|
||||
return
|
||||
for id_variant in simulation.get_building_ids():
|
||||
var id := int(id_variant)
|
||||
var building: Dictionary = simulation.buildings[id]
|
||||
if not bool(building.get("locked", false)):
|
||||
continue
|
||||
if building["kind"] == WorldSimulation.KIND_MACHINE:
|
||||
anchor_machine_id = id
|
||||
elif building["kind"] == WorldSimulation.KIND_CHARGER:
|
||||
charger_machine_id = id
|
||||
|
||||
|
||||
func _select_tool(kind: StringName) -> void:
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
class_name DemoSnapshot
|
||||
extends RefCounted
|
||||
|
||||
const SCHEMA := 1
|
||||
const DEFAULT_PATH := "user://mana_foundry_slot1.json"
|
||||
|
||||
static var path_override := ""
|
||||
|
||||
|
||||
static func slot_path() -> String:
|
||||
if not path_override.is_empty():
|
||||
return path_override
|
||||
return DEFAULT_PATH
|
||||
|
||||
|
||||
static func exists() -> bool:
|
||||
return FileAccess.file_exists(slot_path())
|
||||
|
||||
|
||||
static func save_payload(payload: Dictionary) -> bool:
|
||||
var path := slot_path()
|
||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return false
|
||||
file.store_string(JSON.stringify(payload, "\t"))
|
||||
return true
|
||||
|
||||
|
||||
static func load_payload() -> Dictionary:
|
||||
if not exists():
|
||||
return {}
|
||||
var file := FileAccess.open(slot_path(), FileAccess.READ)
|
||||
if file == null:
|
||||
return {}
|
||||
var parsed: Variant = JSON.parse_string(file.get_as_text())
|
||||
if typeof(parsed) != TYPE_DICTIONARY:
|
||||
return {}
|
||||
var payload: Dictionary = parsed
|
||||
if int(payload.get("schema", 0)) != SCHEMA:
|
||||
return {}
|
||||
return payload
|
||||
@@ -462,6 +462,196 @@ func state_fingerprint() -> String:
|
||||
return str(hash("|".join(parts)))
|
||||
|
||||
|
||||
func to_snapshot() -> Dictionary:
|
||||
var building_rows: Array = []
|
||||
for id in get_building_ids():
|
||||
building_rows.append(_encode_value(buildings[id]))
|
||||
var item_rows: Array = []
|
||||
for id in get_item_ids():
|
||||
item_rows.append(_encode_value(items[id]))
|
||||
var wake_ids: Array = []
|
||||
for id_variant in _wake_queue.keys():
|
||||
wake_ids.append(int(id_variant))
|
||||
wake_ids.sort()
|
||||
var event_rows: Array = []
|
||||
for event in _event_heap:
|
||||
event_rows.append(_encode_value(event))
|
||||
var network_rows: Array = []
|
||||
for network in mana_networks:
|
||||
network_rows.append(_encode_value(network))
|
||||
var charged_ticks: Array = []
|
||||
for tick_variant in _charged_intake_ticks:
|
||||
charged_ticks.append(int(tick_variant))
|
||||
return {
|
||||
"schema": 1,
|
||||
"tick_index": tick_index,
|
||||
"next_building_id": next_building_id,
|
||||
"next_item_id": next_item_id,
|
||||
"buildings": building_rows,
|
||||
"items": item_rows,
|
||||
"sink_inventory": {
|
||||
String(ITEM_RAW): int(sink_inventory.get(ITEM_RAW, 0)),
|
||||
String(ITEM_REFINED): int(sink_inventory.get(ITEM_REFINED, 0)),
|
||||
String(ITEM_CHARGED): int(sink_inventory.get(ITEM_CHARGED, 0)),
|
||||
},
|
||||
"event_heap": event_rows,
|
||||
"wake_queue": wake_ids,
|
||||
"charged_intake_ticks": charged_ticks,
|
||||
"mana_networks": network_rows,
|
||||
}
|
||||
|
||||
|
||||
func apply_snapshot(snapshot: Dictionary) -> bool:
|
||||
if snapshot.is_empty() or int(snapshot.get("schema", 0)) != 1:
|
||||
return false
|
||||
if not snapshot.has("buildings") or not snapshot.has("items"):
|
||||
return false
|
||||
reset()
|
||||
tick_index = int(snapshot.get("tick_index", 0))
|
||||
next_building_id = int(snapshot.get("next_building_id", 1))
|
||||
next_item_id = int(snapshot.get("next_item_id", 1))
|
||||
var sink: Dictionary = snapshot.get("sink_inventory", {})
|
||||
sink_inventory = {
|
||||
ITEM_RAW: int(sink.get(String(ITEM_RAW), sink.get("raw_crystal", 0))),
|
||||
ITEM_REFINED: int(sink.get(String(ITEM_REFINED), sink.get("refined_crystal", 0))),
|
||||
ITEM_CHARGED: int(sink.get(String(ITEM_CHARGED), sink.get("charged_crystal", 0))),
|
||||
}
|
||||
for row_variant in snapshot.get("buildings", []):
|
||||
if typeof(row_variant) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var building: Dictionary = _decode_building(row_variant)
|
||||
var id := int(building["id"])
|
||||
buildings[id] = building
|
||||
grid[Vector2i(building["cell"])] = id
|
||||
for row_variant in snapshot.get("items", []):
|
||||
if typeof(row_variant) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var item: Dictionary = _decode_item(row_variant)
|
||||
var id := int(item["id"])
|
||||
items[id] = item
|
||||
item_by_cell[Vector2i(item["cell"])] = id
|
||||
for event_variant in snapshot.get("event_heap", []):
|
||||
if typeof(event_variant) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var event: Dictionary = event_variant
|
||||
_push_event({
|
||||
"tick": int(event.get("tick", 0)),
|
||||
"type": StringName(String(event.get("type", ""))),
|
||||
"id": int(event.get("id", 0)),
|
||||
})
|
||||
for id_variant in snapshot.get("wake_queue", []):
|
||||
_wake_queue[int(id_variant)] = true
|
||||
for tick_variant in snapshot.get("charged_intake_ticks", []):
|
||||
_charged_intake_ticks.append(int(tick_variant))
|
||||
for network_variant in snapshot.get("mana_networks", []):
|
||||
if typeof(network_variant) != TYPE_DICTIONARY:
|
||||
continue
|
||||
mana_networks.append(_decode_mana_network(network_variant))
|
||||
_rebuild_watchers()
|
||||
return true
|
||||
|
||||
|
||||
func _rebuild_watchers() -> void:
|
||||
_output_watchers.clear()
|
||||
_input_watchers.clear()
|
||||
for id_variant in get_building_ids():
|
||||
var id := int(id_variant)
|
||||
var kind: StringName = buildings[id]["kind"]
|
||||
if kind == KIND_SOURCE or is_processor(kind):
|
||||
_register_output_watcher(id)
|
||||
if is_processor(kind):
|
||||
_register_input_watcher(id)
|
||||
|
||||
|
||||
func _encode_value(value: Variant) -> Variant:
|
||||
match typeof(value):
|
||||
TYPE_VECTOR2I:
|
||||
var cell := Vector2i(value)
|
||||
return {"x": cell.x, "y": cell.y}
|
||||
TYPE_STRING_NAME:
|
||||
return String(value)
|
||||
TYPE_ARRAY:
|
||||
var encoded_array: Array = []
|
||||
for entry in value:
|
||||
encoded_array.append(_encode_value(entry))
|
||||
return encoded_array
|
||||
TYPE_DICTIONARY:
|
||||
var encoded: Dictionary = {}
|
||||
for key in value.keys():
|
||||
encoded[str(key)] = _encode_value(value[key])
|
||||
return encoded
|
||||
return value
|
||||
|
||||
|
||||
func _decode_building(row: Dictionary) -> Dictionary:
|
||||
var building := row.duplicate(true)
|
||||
building["id"] = int(row.get("id", 0))
|
||||
building["kind"] = StringName(String(row.get("kind", "")))
|
||||
building["cell"] = _decode_cell(row.get("cell", {}))
|
||||
building["rotation"] = int(row.get("rotation", 0))
|
||||
building["locked"] = bool(row.get("locked", false))
|
||||
building["status"] = StringName(String(row.get("status", "idle")))
|
||||
if is_processor(building["kind"]):
|
||||
building["recipe"] = StringName(String(row.get("recipe", String(RECIPE_REFINE))))
|
||||
building["input_kind"] = StringName(String(row.get("input_kind", String(ITEM_RAW))))
|
||||
building["output_kind"] = StringName(String(row.get("output_kind", String(ITEM_REFINED))))
|
||||
building["input_count"] = int(row.get("input_count", 0))
|
||||
building["output_buffer"] = int(row.get("output_buffer", 0))
|
||||
building["completion_tick"] = int(row.get("completion_tick", -1))
|
||||
building["remaining_ticks"] = int(row.get("remaining_ticks", 0))
|
||||
building["completed"] = int(row.get("completed", 0))
|
||||
building["mana_powered"] = bool(row.get("mana_powered", false))
|
||||
building["mana_network"] = int(row.get("mana_network", -1))
|
||||
building["input_cell"] = _decode_cell(row.get("input_cell", {}))
|
||||
building["output_cell"] = _decode_cell(row.get("output_cell", {}))
|
||||
building["bridge_configured"] = bool(row.get("bridge_configured", false))
|
||||
if building["kind"] == KIND_SOURCE:
|
||||
building["next_ready_tick"] = int(row.get("next_ready_tick", 0))
|
||||
building["scheduled_tick"] = int(row.get("scheduled_tick", -1))
|
||||
building["produced"] = int(row.get("produced", 0))
|
||||
if building["kind"] == KIND_SINK:
|
||||
building["accepted"] = int(row.get("accepted", 0))
|
||||
if building["kind"] == KIND_MANA_SOURCE:
|
||||
building["capacity"] = int(row.get("capacity", MANA_SOURCE_CAPACITY))
|
||||
building["mana_network"] = int(row.get("mana_network", -1))
|
||||
if building["kind"] == KIND_MANA_PIPE:
|
||||
building["mana_network"] = int(row.get("mana_network", -1))
|
||||
return building
|
||||
|
||||
|
||||
func _decode_item(row: Dictionary) -> Dictionary:
|
||||
return {
|
||||
"id": int(row.get("id", 0)),
|
||||
"kind": StringName(String(row.get("kind", String(ITEM_RAW)))),
|
||||
"cell": _decode_cell(row.get("cell", {})),
|
||||
"progress": float(row.get("progress", 0.0)),
|
||||
}
|
||||
|
||||
|
||||
func _decode_mana_network(row: Dictionary) -> Dictionary:
|
||||
var infrastructure: Array = []
|
||||
for id_variant in row.get("infrastructure", []):
|
||||
infrastructure.append(int(id_variant))
|
||||
var machines: Array = []
|
||||
for id_variant in row.get("machines", []):
|
||||
machines.append(int(id_variant))
|
||||
return {
|
||||
"id": int(row.get("id", 0)),
|
||||
"capacity": int(row.get("capacity", 0)),
|
||||
"used": int(row.get("used", 0)),
|
||||
"infrastructure": infrastructure,
|
||||
"machines": machines,
|
||||
}
|
||||
|
||||
|
||||
func _decode_cell(value: Variant) -> Vector2i:
|
||||
if value is Vector2i:
|
||||
return value
|
||||
if typeof(value) == TYPE_DICTIONARY:
|
||||
return Vector2i(int(value.get("x", 0)), int(value.get("y", 0)))
|
||||
return Vector2i.ZERO
|
||||
|
||||
|
||||
func _move_items() -> void:
|
||||
var ids := get_item_ids()
|
||||
for id_variant in ids:
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ stress scene + release check
|
||||
|
||||
### Phase 3: Presentation and scale guardrails
|
||||
|
||||
- [ ] Task 9: Add menu, save snapshot, audio hooks, and visual polish
|
||||
- [x] Task 9: Add menu, save snapshot, audio hooks, and visual polish
|
||||
- [ ] Task 10: Add deterministic stress generation and metrics
|
||||
- [ ] Task 11: Package a Windows demo build
|
||||
|
||||
|
||||
+4
-4
@@ -142,12 +142,12 @@
|
||||
**Description:** Add title/pause screens, coherent materials and mana effects, audio hooks, and one-slot world snapshot persistence.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] New game, continue, pause, restart, and quit flows work.
|
||||
- [ ] Snapshot restores construction, transport, production, and objectives.
|
||||
- [ ] Industrial assets read as magical machinery through a unified material pass.
|
||||
- [x] New game, continue, pause, restart, and quit flows work.
|
||||
- [x] Snapshot restores construction, transport, production, and objectives.
|
||||
- [x] Industrial assets read as magical machinery through a unified material pass.
|
||||
|
||||
**Verification:**
|
||||
- [ ] Save-then-run and run-from-save produce matching state hashes.
|
||||
- [x] Save-then-run and run-from-save produce matching state hashes.
|
||||
- [ ] Manual UI flow works at 1080p and a smaller window.
|
||||
|
||||
**Dependencies:** Task 8
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
extends SceneTree
|
||||
|
||||
const WorldSimulation = preload("res://src/simulation/world_simulation.gd")
|
||||
const DemoSnapshot = preload("res://src/persistence/demo_snapshot.gd")
|
||||
const TEST_PATH := "user://mana_foundry_snapshot_test.json"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
DemoSnapshot.path_override = TEST_PATH
|
||||
if FileAccess.file_exists(TEST_PATH):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(TEST_PATH))
|
||||
|
||||
var failures: Array[String] = []
|
||||
var live := _make_two_stage()
|
||||
for _tick in range(400):
|
||||
live.step()
|
||||
var before := live.state_fingerprint()
|
||||
var charged_before := live.get_charged_count()
|
||||
var snapshot := live.to_snapshot()
|
||||
|
||||
var restored := WorldSimulation.new()
|
||||
if not restored.apply_snapshot(snapshot):
|
||||
failures.append("apply_snapshot rejected a live two-stage snapshot.")
|
||||
elif restored.state_fingerprint() != before:
|
||||
failures.append("Restored simulation fingerprint did not match the live factory.")
|
||||
elif restored.get_charged_count() != charged_before:
|
||||
failures.append("Restored charged count did not match.")
|
||||
|
||||
if failures.is_empty():
|
||||
for _tick in range(200):
|
||||
live.step()
|
||||
restored.step()
|
||||
if live.state_fingerprint() != restored.state_fingerprint():
|
||||
failures.append("Live and restored factories diverged after more ticks.")
|
||||
|
||||
if not DemoSnapshot.save_payload({"schema": DemoSnapshot.SCHEMA, "simulation": snapshot}):
|
||||
failures.append("DemoSnapshot could not write the test slot.")
|
||||
else:
|
||||
var loaded := DemoSnapshot.load_payload()
|
||||
var from_disk := WorldSimulation.new()
|
||||
if not from_disk.apply_snapshot(loaded.get("simulation", {})):
|
||||
failures.append("Disk snapshot failed to apply.")
|
||||
elif from_disk.state_fingerprint() != before:
|
||||
failures.append("Disk snapshot fingerprint did not match the pre-save factory.")
|
||||
|
||||
var empty := WorldSimulation.new()
|
||||
if empty.is_demo_complete():
|
||||
failures.append("Empty factory reported demo complete.")
|
||||
var first_item := _make_two_stage()
|
||||
var saw_item := false
|
||||
for _tick in range(2000):
|
||||
first_item.step()
|
||||
if first_item.get_charged_count() >= 1:
|
||||
saw_item = true
|
||||
break
|
||||
if not saw_item:
|
||||
failures.append("Two-stage factory never produced a charged crystal for the one-item check.")
|
||||
elif first_item.is_demo_complete():
|
||||
failures.append("Demo completed after a single charged crystal.")
|
||||
else:
|
||||
var mid := WorldSimulation.new()
|
||||
if not mid.apply_snapshot(first_item.to_snapshot()):
|
||||
failures.append("Could not restore the one-item factory.")
|
||||
elif mid.is_demo_complete():
|
||||
failures.append("Loaded one-item factory reported complete.")
|
||||
|
||||
var packed := load("res://scenes/main.tscn") as PackedScene
|
||||
var demo := packed.instantiate()
|
||||
root.add_child.call_deferred(demo)
|
||||
await process_frame
|
||||
await process_frame
|
||||
demo.call("_start_new_game")
|
||||
var sim: WorldSimulation = demo.get("simulation")
|
||||
for x in range(-5, -1):
|
||||
sim.place_building(WorldSimulation.KIND_BELT, Vector2i(x, 0), 0)
|
||||
sim.place_building(WorldSimulation.KIND_BELT, Vector2i(0, 0), 0)
|
||||
for x in range(2, 6):
|
||||
sim.place_building(WorldSimulation.KIND_BELT, Vector2i(x, 0), 0)
|
||||
for z in range(1, 5):
|
||||
sim.place_building(WorldSimulation.KIND_MANA_PIPE, Vector2i(0, z), 1)
|
||||
sim.configure_machine_bridge(int(demo.get("anchor_machine_id")), Vector2i(-2, 0), Vector2i(0, 0))
|
||||
sim.configure_machine_bridge(int(demo.get("charger_machine_id")), Vector2i(0, 0), Vector2i(2, 0))
|
||||
for _tick in range(240):
|
||||
sim.step()
|
||||
var scene_before := sim.state_fingerprint()
|
||||
var payload: Dictionary = demo.call("collect_demo_snapshot")
|
||||
if not bool(demo.call("_save_demo_snapshot")):
|
||||
failures.append("Scene save helper failed.")
|
||||
demo.call("_reset_demo")
|
||||
if sim.state_fingerprint() == scene_before:
|
||||
failures.append("Reset after save did not change simulation state.")
|
||||
if not bool(demo.call("_load_demo_snapshot")):
|
||||
failures.append("Scene load helper failed.")
|
||||
elif sim.state_fingerprint() != scene_before:
|
||||
failures.append("Scene save/load did not restore the simulation fingerprint.")
|
||||
|
||||
if FileAccess.file_exists(TEST_PATH):
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(TEST_PATH))
|
||||
DemoSnapshot.path_override = ""
|
||||
|
||||
if failures.is_empty():
|
||||
print("SNAPSHOT_SMOKE_OK charged=%d fingerprint=%s" % [charged_before, before])
|
||||
quit(0)
|
||||
else:
|
||||
for failure in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
|
||||
|
||||
func _make_two_stage() -> WorldSimulation:
|
||||
var simulation := WorldSimulation.new()
|
||||
simulation.place_building(WorldSimulation.KIND_SOURCE, Vector2i(-6, 0), 0)
|
||||
for x in range(-5, -1):
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(x, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(0, 0), 0)
|
||||
for x in range(2, 6):
|
||||
simulation.place_building(WorldSimulation.KIND_BELT, Vector2i(x, 0), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_SINK, Vector2i(6, 0), 0)
|
||||
var refine_id := simulation.place_building(WorldSimulation.KIND_MACHINE, Vector2i(-1, 1), 0)
|
||||
var charger_id := simulation.place_building(WorldSimulation.KIND_CHARGER, Vector2i(1, 1), 0)
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_SOURCE, Vector2i(0, 5), 1)
|
||||
for z in range(1, 5):
|
||||
simulation.place_building(WorldSimulation.KIND_MANA_PIPE, Vector2i(0, z), 1)
|
||||
simulation.configure_machine_bridge(refine_id, Vector2i(-2, 0), Vector2i(0, 0))
|
||||
simulation.configure_machine_bridge(charger_id, Vector2i(0, 0), Vector2i(2, 0))
|
||||
return simulation
|
||||
Reference in New Issue
Block a user