extends SceneTree const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd") ## Command-backed UI acceptance. Incidents are disclosed threat fixtures; ## combat progression uses the ordinary game clock. var _checks := 0 var _failures := 0 var _main: Control var _runtime: Variant var _clock: Variant var _panel: DefenseManagementPanel var _facility_id: StringName = &"" var _path := "user://tests/defense_management_%d/slot" % OS.get_process_id() var _slots: Array[String] = [] func _initialize() -> void: call_deferred("_run") func _expect(ok: bool, message: String) -> void: _checks += 1 if not ok: _failures += 1 push_error(message) func _run() -> void: root.gui_embed_subwindows = true root.size = Vector2i(1440, 810) _runtime = root.get_node("SimulationRuntime") _clock = root.get_node("GameClock") _clock.reset(1, 6, 0) root.get_node("GameState").reset() _runtime.initialize(_clock, root.get_node("GameState"), 42540) _clock.set_paused(true) _main = load("res://scenes/main/main.tscn").instantiate() _main.set("manual_save_path", _path) root.add_child(_main) _runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false}) await process_frame await process_frame _main.call("_set_management_panel_open", true) _main.find_child("DefenseManagementButton", true, false).pressed.emit() _panel = _main.get("defense_manager").find_child("DefenseManagementPanel", true, false) _expect(_main.get("defense_manager").visible and _panel.selected_raid_id.is_empty(), "ordinary management button opens empty defense screen") _expect("ありません" in _panel.summary_label.text and _panel.start_button.disabled, "no incident has no actionable start") # Paid normal construction and hiring establish real target/defender links. _main.get("defense_manager").hide() _main.call("_select_build_definition", &"reception") _main.call("_on_map_cell_clicked", Vector3i(-8, 0, 0)) _main.call("_clear_build_selection") _clock.advance_minutes(120) for facility: FacilityInstance in _runtime.facility_service.get_instances(): if facility.definition_id == &"reception": _facility_id = facility.instance_id _expect(not _facility_id.is_empty(), "normal paid reception construction provides a real target") _main.get("staff_candidate_toggle_button").pressed.emit() _main.get("staff_candidate_buttons")[&"staff_klaus"].pressed.emit() _expect(_runtime.staff_service.has_staff(&"staff_klaus"), "ordinary recruitment hires guard with existing combat data") if _facility_id.is_empty() or not _runtime.staff_service.has_staff(&"staff_klaus"): _finish() return _expect(_create_incident(&"ui.defense.fixture").is_success(), "disclosed threat fixture creates an existing preparing incident") _main.find_child("DefenseManagementButton", true, false).pressed.emit() _expect(_panel.selected_raid_id == &"ui.defense.fixture" and "準備中" in _panel.summary_label.text, "real popup observes Runtime incident publication") _expect(_panel.find_child("DefenseCancelPreparation", true, false) == null and &"cancel_defense_raid" not in DefenseManagementPanel.COMMANDS, "preparation has no free incident-cancellation button or allowed UI command") var preparing_before: Dictionary = _runtime.create_snapshot() _panel.call("_submit", &"cancel_defense_raid") _panel.retreat_button.pressed.emit() _expect(not _panel.confirmation.visible and (_panel.get("_confirmation_context") as Dictionary).is_empty(), "preparation cannot open a withdrawal confirmation even through a stale hidden button signal") _expect(preparing_before == _runtime.create_snapshot(), "forbidden preparation cancellation and withdrawal do not enqueue commands or mutate any Runtime state") var before: Dictionary = _runtime.create_snapshot() var report: Dictionary = _runtime.get_defense_management_report(&"ui.defense.fixture") _expect(before == _runtime.create_snapshot(), "management report does not mutate gameplay or RNG") report[&"selected"][&"objective_ids"].clear() report[&"staff"][0][&"combat"] = 999 _expect(before == _runtime.create_snapshot(), "compact projections are detached from Defense and Staff") _expect(not _panel.register_staff_button.disabled and _panel.start_button.disabled, "unregistered objective explains and prevents start") _panel.register_staff_button.pressed.emit() var raid: Dictionary = _runtime.defense_service.get_raid(&"ui.defense.fixture") var guard: StaffRuntime = _runtime.staff_service.get_staff(&"staff_klaus") _expect(raid[&"combatants"].has(&"staff_klaus"), "real participation button registers defender through Runtime command") _expect(raid[&"combatants"][&"staff_klaus"][&"combat"] == guard.skills[&"combat"] and guard.state == StaffRuntime.State.UNAVAILABLE, "defender uses actual stored skill and becomes unavailable for work") _panel.register_objective_button.pressed.emit() raid = _runtime.defense_service.get_raid(&"ui.defense.fixture") _expect(raid[&"assets"].has(_facility_id) and not _panel.start_button.disabled, "real target button registers existing facility and enables start") _expect(raid[&"assets"][_facility_id][&"integrity"] == _runtime.facility_service.get_instance(_facility_id).durability, "objective reflects current facility durability") # Policy edits survive publication until explicitly applied, and do not # change current policy before the player presses the apply button. _panel.policy_option.select(2) _panel.policy_option.item_selected.emit(2) _panel.threshold_spin.value = 7 _panel.refresh() _expect(_panel.policy_option.selected == 2 and _panel.threshold_spin.value == 7, "uncommitted retreat draft survives UI refresh") _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"retreat_policy"] == &"HOLD", "editing a draft alone does not alter defense") _panel.policy_button.pressed.emit() _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"retreat_policy"] == &"THRESHOLD" and _runtime.defense_service.get_raid(&"ui.defense.fixture")[&"defender_retreat_threshold"] == 7, "apply button updates existing retreat policy contract") _main.call("_save_manual") _slots.append(_path) var package := SaveService.load_snapshot(_path) _expect(package.get(&"ok", false) and package.get(&"manifest", {}).get("interface_selection", {}).get("defense_id") == "ui.defense.fixture" and package.get(&"manifest", {}).get("interface_selection", {}).get("defense_open") == true, "normal save captures selected incident and open popup") # Change a real policy after saving so the following read proves restoration. _panel.policy_option.select(0) _panel.policy_option.item_selected.emit(0) _panel.policy_button.pressed.emit() _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"retreat_policy"] == &"HOLD", "ordinary policy button changes live preparation after the saved checkpoint") _main.get("defense_manager").hide() _main.call("_load_manual", true) _expect(_main.get("defense_manager").visible and _panel.selected_raid_id == &"ui.defense.fixture", "manual load restores actual saved incident and visibility") var restored_preparing: Dictionary = _runtime.defense_service.get_raid(&"ui.defense.fixture") _expect(restored_preparing[&"state"] == DefenseService.RaidState.PREPARING and _runtime.staff_service.get_staff(&"staff_klaus").state == StaffRuntime.State.UNAVAILABLE, "saved preparation remains a live threat and keeps its actual defender locked") _expect(restored_preparing[&"retreat_policy"] == &"THRESHOLD" and restored_preparing[&"defender_retreat_threshold"] == 7, "manual load restores the saved policy instead of the later live edit") _clock.set_paused(false) _panel.start_button.pressed.emit() _panel.start_button.pressed.emit() var queued := 0 for command: Dictionary in _runtime.command_queue.create_snapshot().get("pending", []): if command.get("command_type", "") == "start_defense_raid": queued += 1 _expect(queued == 1 and _panel.start_button.disabled, "playing simulation accepts a single pending start despite repeated button activation") _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.PREPARING, "accepted running command has not mutated the incident before its tick") _clock.advance_minutes(1) _clock.set_paused(true) _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.ACTIVE and _panel.retreat_button.visible, "start button enters actual battle and exposes withdrawal") # Stale start invocation is rejected by Runtime and never restarts combat. var active: Dictionary = _runtime.defense_service.create_snapshot() _panel.start_button.pressed.emit() _expect(active == _runtime.defense_service.create_snapshot(), "invalid repeated start preserves active battle state") # Withdrawal is available only after the existing incident starts. Backing # out changes presentation alone and retains the threat and defender locks. var active_before_return: Dictionary = _runtime.create_snapshot() _panel.retreat_button.pressed.emit() _expect(_panel.confirmation.visible and _panel.confirmation.title == "撤退の確認", "active battle opens only a withdrawal confirmation") _panel.confirmation.canceled.emit() _panel.confirmation.hide() _expect((_panel.get("_confirmation_context") as Dictionary).is_empty() and active_before_return == _runtime.create_snapshot(), "returning from active withdrawal clears confirmation without changing gameplay or queued commands") # Save the ACTIVE incident, then replace its queue through an actual load. _main.call("_save_manual") var active_package := SaveService.load_snapshot(_path) _expect(active_package.get(&"ok", false), "normal save writes the active battle before stale-context testing") _panel.retreat_button.pressed.emit() var context: Dictionary = _panel.get("_confirmation_context").duplicate(true) _expect(_panel.confirmation.visible and not context.is_empty(), "active withdrawal captures the current command queue before load") _main.call("_load_manual", true) _expect(_main.get("build_status_label").text == "手動セーブを読み込みました。" and _main.get("defense_manager").visible and _panel.selected_raid_id == &"ui.defense.fixture", "successful strict read restores active incident selection and popup") _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.ACTIVE, "active save restores actual battle state") _expect(not _panel.confirmation.visible and (_panel.get("_confirmation_context") as Dictionary).is_empty(), "successful active load dismisses and invalidates the old withdrawal confirmation") _expect(_runtime.command_queue.get_instance_id() != int(context.get(&"queue_instance_id", 0)), "successful load uses a different command queue than the pending confirmation") var loaded_before_replay: Dictionary = _runtime.create_snapshot() _panel.set("_confirmation_context", context) _panel.confirmation.confirmed.emit() _expect(loaded_before_replay == _runtime.create_snapshot(), "replayed old-queue withdrawal context leaves every loaded Runtime subsystem and command unchanged") _expect("対象が変わりました" in _panel.result_label.text and _runtime.staff_service.get_staff(&"staff_klaus").state == StaffRuntime.State.UNAVAILABLE, "stale withdrawal reports rejection and leaves the loaded defender assigned") # The disclosed fixture creates only the threat. Combat now progresses from # ordinary game minutes without a round button or a synthetic player command. var interval := int(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"round_minutes"]) _clock.advance_minutes(interval - 1) _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"current_round"] == 0, "ordinary battle waits for its persisted round interval") _clock.advance_minutes(1) _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"current_round"] == 1, "ordinary clock automatically resolves the first attack") _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.BETWEEN_WAVES and _panel.next_wave_button.visible, "service wave outcome exposes ordinary next-wave control") _panel.next_wave_button.pressed.emit() _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.ACTIVE, "player next-wave button advances existing incident through Runtime") _panel.retreat_button.pressed.emit() _panel.confirmation.confirmed.emit() _panel.confirmation.hide() _expect(_runtime.defense_service.get_raid(&"ui.defense.fixture")[&"state"] == DefenseService.RaidState.DEFENDERS_RETREATED, "confirmed withdrawal terminates battle through Runtime") _expect(_runtime.staff_service.get_staff(&"staff_klaus").state == StaffRuntime.State.FLEEING and _panel.policy_button.disabled, "retreat reconciles staff and terminal UI disables commands") # Failed gameplay loads retain current selection. Optional bad presentation # must not invalidate a valid gameplay load or reopen this inspector. var selected := _panel.selected_raid_id _expect(not _runtime.load_from_path(_path + "_absent").get(&"ok", true), "missing package load fails explicitly") _expect(_panel.selected_raid_id == selected and _main.get("defense_manager").visible, "failed load preserves visible defense selection") for entry: Array in [["types", []], ["missing", "missing.raid"], ["long", "x".repeat(257)]]: var slot: String = _path + "_" + entry[0] _slots.append(slot) _expect(SaveService.save_snapshot(slot, _runtime.create_snapshot(), {&"interface_selection": {&"version": 1, &"defense_id": entry[1], &"defense_open": true}}).get(&"ok", false), "write malformed optional UI metadata fixture " + entry[0]) _expect(_runtime.load_from_path(slot).get(&"ok", false), "valid gameplay still loads with optional invalid UI metadata " + entry[0]) _expect(not _main.get("defense_manager").visible, "invalid incident selection does not reopen popup " + entry[0]) _main.find_child("DefenseManagementButton", true, false).pressed.emit() await _test_incident_presentation() var capture := OS.get_environment("THE_TREE_DEFENSE_MANAGEMENT_CAPTURE") if not capture.is_empty(): for frame: int in 4: await process_frame await RenderingServer.frame_post_draw _expect(root.get_texture().get_image().save_png(capture) == OK, "capture actual defense popup after source-label and saved-selection checks") _finish() func _create_incident(id: StringName) -> CommandResult: return _runtime.submit_command(&"create_defense_raid", {&"raid_id": id, &"definition": { &"entry_points": [&"fixture.gate"], &"objective_ids": [_facility_id], &"waves": [ {&"wave_id": &"fixture.wave.one", &"threat_strength": 5, &"max_rounds": 1}, {&"wave_id": &"fixture.wave.two", &"threat_strength": 5, &"max_rounds": 1}, ], }}, &"test.fixture") func _finish() -> void: if is_instance_valid(_main): var _audio_cleanup_final: Dictionary = await FixtureCleanup.free_owner_and_wait(self, _main) if not bool(_audio_cleanup_final.get(&"passed", false)): push_error("Ordinary fixture audio cleanup failed: %s" % str(_audio_cleanup_final)) quit(96) return for path: String in _slots: SaveService.delete_save(path) if _failures == 0: print("PASS: %d checks" % _checks) else: push_error("FAIL: %d of %d checks failed" % [_failures, _checks]) quit(_failures) ## Display acceptance uses explicit disclosed causes through the actual Event ## queue. These cases do not claim normal threat generation or approve balance. func _test_incident_presentation() -> void: var expected := {"THIEF": "盗賊の襲撃", "BANKRUPTCY_RIOT": "客の暴動", "BANK_DELINQUENCY": "銀行による取り立て", "AUTHORITY_ESCALATION": "監査による摘発"} var event_ids: Dictionary = {} for kind: String in expected: var event_id := StringName("ui.presentation.event." + kind) var instance_id := StringName("ui.presentation.instance." + kind) var action_id := &"ui.presentation.threat" var action := EventAction.new().configure(action_id, EventAction.Type.CREATE_DEFENSE_RAID, { &"template_id": &"ui.presentation.disclosed.template", &"source_kind": kind, &"definition": _presentation_raid_definition(), }) var registered: CommandResult = _runtime.submit_command(&"register_event_definition", {&"definition": { &"event_id": event_id, &"category": &"DEFENSE", &"foreground": false, &"transaction_policy": &"ROLLBACK_ALL", &"automatic_actions": [action.create_snapshot()], }}, &"test.fixture") _expect(registered.is_success(), "disclosed presentation source registers " + kind) var queued: CommandResult = _runtime.submit_command(&"enqueue_event", { &"event_id": event_id, &"actor_ids": [], &"options": { &"event_instance_id": instance_id, &"context": {&"threat_source_id": StringName("ui.presentation.cause." + kind)}, }, }, &"test.fixture") _expect(queued.is_success(), "disclosed presentation cause enters the real event queue " + kind) _clock.advance_minutes(1) var raid_id := StringName("event_defense_raid." + JSON.stringify([String(instance_id), String(action_id)], "", true, true).sha256_text()) event_ids[kind] = raid_id var raid: Dictionary = _runtime.defense_service.get_raid(raid_id) _expect(not raid.is_empty() and raid.get(&"event_source", {}).get("source_kind", "") == kind, "ordinary event processing supplies explicit source provenance " + kind) # The two manual incidents deliberately resemble a source enum. They have # no event provenance and must remain generic defense records. var manual_one := &"THIEF" var manual_two := &"ui.presentation.manual.thief.source" for id: StringName in [manual_one, manual_two]: var definition := _presentation_raid_definition() definition[&"source_id"] = &"THIEF" var created: CommandResult = _runtime.submit_command(&"create_defense_raid", {&"raid_id": id, &"definition": definition}, &"test.fixture") _expect(created.is_success(), "manual same-minute presentation incident is disclosed and created") _main.find_child("DefenseManagementButton", true, false).pressed.emit() _panel.refresh() var before: Dictionary = _runtime.create_snapshot() var report: Dictionary = _runtime.get_defense_management_report(event_ids["THIEF"]) var selected: Dictionary = report.get(&"selected", {}) _expect(selected.get(&"source_kind", "") == "THIEF" and typeof(selected.get(&"created_at")) == TYPE_INT, "compact management report exposes only explicit cause and persisted creation time") selected[&"source_kind"] = "BANK_DELINQUENCY" selected[&"created_at"] = -1 _expect(_runtime.create_snapshot() == before, "display projection fields remain owned and cannot mutate gameplay, source or RNG") for kind: String in expected: var raid_id := StringName(event_ids[kind]) _select_presentation_option(raid_id) var index := _panel.raid_option.selected _expect(_panel.selected_raid_id == raid_id and _panel.raid_option.get_item_metadata(index) == raid_id, "cause label never replaces internal selection metadata " + kind) _expect(expected[kind] in _panel.raid_option.get_item_text(index) and "日目" in _panel.raid_option.get_item_text(index), "option shows readable cause and occurrence time " + kind) _expect(expected[kind] in _panel.summary_label.text and String(raid_id) not in _panel.summary_label.text and "event_defense_raid." not in _panel.raid_option.get_item_text(index), "summary and option keep the SHA out of user text " + kind) var again: Dictionary = _runtime.create_snapshot() _panel.refresh() _expect(_runtime.create_snapshot() == again and _panel.selected_raid_id == raid_id, "repeated label refresh preserves every gameplay system and selected record " + kind) var manual_labels: Array[String] = [] for id: StringName in [manual_one, manual_two]: _select_presentation_option(id) manual_labels.append(_panel.raid_option.get_item_text(_panel.raid_option.selected)) _expect(_panel.selected_raid_id == id and "防衛戦" in _panel.summary_label.text and "盗賊" not in _panel.summary_label.text, "manual ID and source_id never imply a thief cause") _expect(manual_labels[0] != manual_labels[1] and "(1)" in manual_labels[0] and "(2)" in manual_labels[1], "same cause/time manual incidents remain separately readable") _expect(_panel.call("_raid_base_label", {&"raid_id": &"THIEF", &"source_id": &"THIEF"}) == "防衛戦 · 時刻不明", "legacy projection without cause/time never fabricates a source or midnight timestamp") _expect("防衛戦" in String(_panel.call("_raid_base_label", {&"source_kind": "FUTURE_KIND", &"created_at": 0})), "unknown cause text receives a neutral label") var saved_id := StringName(event_ids["THIEF"]) _select_presentation_option(saved_id) var saved_text := _panel.summary_label.text _panel.policy_option.select(2) _panel.policy_option.item_selected.emit(2) _panel.threshold_spin.value = 17 _panel.policy_button.pressed.emit() _expect(_runtime.defense_service.get_raid(saved_id)[&"retreat_policy"] == &"THRESHOLD" and _runtime.defense_service.get_raid(saved_id)[&"defender_retreat_threshold"] == 17, "readable event selection sends the ordinary policy command to its actual internal raid") _expect(_runtime.defense_service.get_raid(event_ids["BANK_DELINQUENCY"])[&"retreat_policy"] == &"HOLD", "selected policy action never affects another similarly displayed incident") _main.call("_save_manual") var package := SaveService.load_snapshot(_path) _expect(package.get(&"ok", false) and package.get(&"manifest", {}).get("interface_selection", {}).get("defense_id") == String(saved_id), "normal UI save retains the full internal event raid ID") _select_presentation_option(manual_two) _main.get("defense_manager").hide() _main.call("_load_manual", true) _expect(_main.get("build_status_label").text == "手動セーブを読み込みました。" and _main.get("defense_manager").visible, "strict normal load restores the saved readable defense panel") _expect(_panel.selected_raid_id == saved_id and _panel.summary_label.text == saved_text, "cause, creation time, state and exact selected event raid survive normal save/load") _expect(_runtime.defense_service.get_raid(saved_id)[&"event_source"]["source_kind"] == "THIEF", "display labels never overwrite restored event provenance") _expect(_runtime.defense_service.get_raid(saved_id)[&"retreat_policy"] == &"THRESHOLD" and _panel.threshold_spin.value == 17, "saved user command and policy controls restore on the same readable incident") var restored_labels: Array[String] = [] for id: StringName in [manual_one, manual_two]: _select_presentation_option(id) restored_labels.append(_panel.raid_option.get_item_text(_panel.raid_option.selected)) _expect(restored_labels == manual_labels, "duplicate occurrence labels retain the same order after restoration") _select_presentation_option(saved_id) var title_before_failed_load := _panel.summary_label.text _expect(not _runtime.load_from_path(_path + "_presentation_absent").get(&"ok", true), "failed load remains explicit during source-label acceptance") _expect(_panel.selected_raid_id == saved_id and _panel.summary_label.text == title_before_failed_load, "failed read preserves readable selection and title") for frame: int in 4: await process_frame _expect(not _panel.raid_option.fit_to_longest_item and _panel.raid_option.clip_text, "raid selector cannot force window width from an internal or future long label") _expect(_main.get("defense_manager").size.x <= 520 and _panel.raid_option.get_global_rect().size.x <= _main.get("defense_manager").size.x, "actual 520px defense dialog and selector stay within their intended width") _expect(_panel.raid_option.get_item_tooltip(_panel.raid_option.selected) == _panel.raid_option.get_item_text(_panel.raid_option.selected), "clipped selector offers the full human-readable text instead of a technical ID") func _select_presentation_option(raid_id: StringName) -> void: for index: int in _panel.raid_option.item_count: if _panel.raid_option.get_item_metadata(index) == raid_id: _panel.raid_option.select(index) _panel.raid_option.item_selected.emit(index) return _expect(false, "requested presentation raid exists in actual option metadata") func _presentation_raid_definition() -> Dictionary: return { &"entry_points": [&"ui.presentation.disclosed.gate"], &"objective_ids": [_facility_id], &"round_minutes": 5, &"max_duration_minutes": 120, &"retreat_policy": &"HOLD", &"defender_retreat_threshold": 0, &"terrain_bonus": 0, &"command_bonus": 0, &"surprise_penalty": 0, &"multiple_entry_penalty": 0, &"waves": [{&"wave_id": &"ui.presentation.disclosed.wave", &"threat_strength": 5, &"max_rounds": 1, &"retreat_threshold": 0}], }