diff --git a/README.md b/README.md index 7028881..563580f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/src/main.gd b/src/main.gd index 3b6101d..c5195c0 100644 --- a/src/main.gd +++ b/src/main.gd @@ -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 - pause_overlay.visible = 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: diff --git a/src/persistence/demo_snapshot.gd b/src/persistence/demo_snapshot.gd new file mode 100644 index 0000000..60d59b4 --- /dev/null +++ b/src/persistence/demo_snapshot.gd @@ -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 diff --git a/src/simulation/world_simulation.gd b/src/simulation/world_simulation.gd index 10a3266..3a3cc85 100644 --- a/src/simulation/world_simulation.gd +++ b/src/simulation/world_simulation.gd @@ -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: diff --git a/tasks/plan.md b/tasks/plan.md index 8613b14..2cea814 100644 --- a/tasks/plan.md +++ b/tasks/plan.md @@ -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 diff --git a/tasks/todo.md b/tasks/todo.md index 2969c8e..cc7851e 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -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 diff --git a/tests/snapshot_smoke.gd b/tests/snapshot_smoke.gd new file mode 100644 index 0000000..c37243b --- /dev/null +++ b/tests/snapshot_smoke.gd @@ -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