extends SceneTree const FixtureCleanup := preload("res://tests/helpers/ui_audio_fixture_cleanup.gd") const OperationFeedbackScript := preload("res://scripts/ui/operation_feedback.gd") const OperationsOSObserverScript := preload("res://tests/ui/fixtures/operations_os_observer.gd") const ManualSaveSlotsDialogScript := preload("res://scripts/ui/manual_save_slots_dialog.gd") var _checks := 0 var _failures := 0 func _initialize() -> void: call_deferred("_run") func _expect(ok: bool, message: String) -> void: _checks += 1 if not ok: _failures += 1 push_error("FAIL: " + message) func _run() -> void: var observer := OperationsOSObserverScript.new() var runtime: Variant = root.get_node("SimulationRuntime") var clock: Variant = root.get_node("GameClock") var state: Variant = root.get_node("GameState") clock.reset(1, 6, 0) state.reset() runtime.initialize(clock, state, 42534) clock.set_paused(true) root.size = Vector2i(1440, 810) var main: Control = load("res://scenes/main/main.tscn").instantiate() root.add_child(main) runtime.configure_save_policy({&"enabled": false, &"daily_checkpoint_enabled": false}) await process_frame await process_frame var preview: WorldPreview = main.get("world_preview") _expect(preview != null, "WorldPreview exists in Main") var env_report: Dictionary = observer.capture_environment(self, main) _expect(env_report[&"pid"] > 0, "Observer captured actual process PID") # ========================================================================= # Condition 1: placement_and_invalid_drop (Grid snap, warnings, invalid rejection) # ========================================================================= var def_registry: Variant = runtime.get("definition_registry") var reception_def: Variant = def_registry.get_facility(&"reception") if def_registry != null else null _expect(reception_def != null, "Reception facility definition exists") # Valid placement commits once var valid_cell := Vector3i(-8, 0, 0) var build_result: Variant = runtime.submit_command(&"build_facility", { &"definition_id": &"reception", &"origin": valid_cell, &"rotation": 0 }) _expect(build_result.is_success(), "Valid facility construction commits once") # Advance time for facility to finish construction clock.advance_minutes(120) var facilities: Array = runtime.construction_service.get_facilities() _expect(not facilities.is_empty(), "Facility construction completed in world") var facility_id := StringName(facilities[0][&"instance_id"]) # Connection warning on preview preview.set_build_definition(reception_def, 0) var connection_preview: Dictionary = preview.get_build_preview_report(Vector3i(10, 10, 0)) _expect(connection_preview.has(&"connection"), "Build preview includes connection warning report") # Invalid placement rejection explanation var invalid_preview: Dictionary = preview.get_build_preview_report(Vector3i(999, 999, 0)) var rejection_text := OperationFeedbackScript.get_placement_rejection_text(invalid_preview) _expect(not rejection_text.is_empty(), "Invalid placement explains actual rejection reason: " + rejection_text) preview.clear_build_definition() # Staff assignment via drop var staff_panel: StaffManagementPanel = main.get("staff_management_panel") _expect(staff_panel != null, "StaffManagementPanel exists") _expect(runtime.staff_service.create_staff(&"op_worker_1", &"cleaner", {}).is_success(), "Create test employee") staff_panel.refresh() var drop_payload := { &"kind": &"staff_assignment", &"staff_id": &"op_worker_1", &"template_id": &"DAY", &"queue_instance_id": int(staff_panel.get("_queue_instance_id")) } staff_panel.assign_from_drop(drop_payload, facility_id) _expect(staff_panel.selected_staff_id == &"op_worker_1", "Drop assignment preserves employee selection") # Invalid assignment rejection explains actual reason and leaves model intact var before_model: Dictionary = runtime.create_snapshot() var invalid_drop := { &"kind": &"staff_assignment", &"staff_id": &"non_existent_staff", &"template_id": &"DAY", &"queue_instance_id": int(staff_panel.get("_queue_instance_id")) } staff_panel.assign_from_drop(invalid_drop, facility_id) var after_model: Dictionary = runtime.create_snapshot() _expect(SimulationDigest.compute(before_model) == SimulationDigest.compute(after_model), "Invalid drop leaves simulation model completely intact") # Click-based assignment alternative _expect(main.has_method(&"assign_staff_by_click"), "Main provides click-based assignment alternative") staff_panel.select_staff(&"op_worker_1") main.call(&"assign_staff_by_click", facility_id, &"op_worker_1") _expect(staff_panel.selected_staff_id == &"op_worker_1", "Click-based assignment succeeds and leaves selection intact") # ========================================================================= # Condition 2: modal_and_pause_resume (Modal input block, pause freezes simulation) # ========================================================================= var save_dialog: ConfirmationDialog = main.call(&"_ensure_manual_save_slots") _expect(save_dialog != null, "Save slots dialog is available") var initial_manual_path: String = main.manual_save_path save_dialog.show_new_save("user://saves/manual_ui") await process_frame _expect(preview.modal_input_blocked, "Behind-dialog input is blocked while modal is open") # Behind-dialog drop assignment is rejected while modal is open var drop_test_payload := { &"kind": &"staff_assignment", &"staff_id": &"op_worker_1", &"template_id": &"DAY", } _expect(not preview._can_drop_data(Vector2.ZERO, drop_test_payload), "Modal suppresses behind-dialog staff assignment drop") # Cancel modal preserves save identity and unblocks behind-dialog input save_dialog.hide() save_dialog.emit_signal(&"canceled") await process_frame _expect(not preview.modal_input_blocked, "Modal close unblocks behind-dialog input") _expect(main.manual_save_path == initial_manual_path, "Modal cancellation preserves existing save identity") # Defense dialog uses the ordinary visibility callback and real model. main.call(&"_show_defense_manager") await process_frame _expect(preview.modal_input_blocked, "Defense dialog blocks map input on open") preview.set_build_definition(reception_def, 0) var defense_before: Dictionary = runtime.create_snapshot() var defense_selection := [preview.selected_entity_id, staff_panel.selected_staff_id, main.get("_selected_entity_id"), main.get("_selected_entity_kind")] var behind_click := InputEventMouseButton.new() behind_click.button_index = MOUSE_BUTTON_LEFT behind_click.pressed = true behind_click.position = Vector2(200, 200) preview._gui_input(behind_click) preview._drop_data(Vector2.ZERO, drop_payload) _expect(SimulationDigest.compute(defense_before) == SimulationDigest.compute(runtime.create_snapshot()), "Defense modal build/drop attempts preserve complete model including economy ledger") _expect(defense_selection == [preview.selected_entity_id, staff_panel.selected_staff_id, main.get("_selected_entity_id"), main.get("_selected_entity_kind")], "Defense modal build/drop attempts preserve map and staff selections") preview.clear_build_definition() var defense_dialog: AcceptDialog = main.get("defense_manager") defense_dialog.hide() await process_frame _expect(not preview.modal_input_blocked, "Defense dialog close unblocks map input") # Pause freezes simulation via process_real_delta/accumulator; ordinary resume continues exactly once var clock_paused_before: bool = bool(clock.get("paused")) clock.set_paused(true) var min_paused: int = int(clock.get("absolute_minute")) # Passing non-zero real delta while paused returns 0 ticks and does not advance simulation minute var paused_ticks: int = clock.process_real_delta(SimulationKernel.REAL_SECONDS_PER_GAME_MINUTE * 2.0) _expect(paused_ticks == 0, "Paused clock returns 0 ticks on non-zero real process delta") _expect(int(clock.get("absolute_minute")) == min_paused, "Paused clock freezes minute advancement on real process delta") # Ordinary resume continues exactly once clock.set_paused(false) var resumed_ticks: int = clock.process_real_delta(SimulationKernel.REAL_SECONDS_PER_GAME_MINUTE) _expect(resumed_ticks == 1, "Resumed clock on real process delta advances exactly one tick") _expect(int(clock.get("absolute_minute")) == min_paused + 1, "Resumed clock advances simulation minute exactly once") clock.set_paused(clock_paused_before) # ========================================================================= # Condition 3: unit_bubbles (Zzz, anger, heart from real states) # ========================================================================= var test_staff: StaffRuntime = runtime.staff_service.get_staff(&"op_worker_1") _expect(test_staff != null, "Worker runtime exists for bubble test") # SLEEPING / INCAPACITATED -> Zzz test_staff.state = StaffRuntime.State.SLEEPING var bubble_zzz: StringName = OperationFeedbackScript.resolve_staff_bubble(test_staff) _expect(bubble_zzz == OperationFeedbackScript.BUBBLE_ZZZ, "Sleeping staff resolves to Zzz bubble") observer.record_bubble(&"op_worker_1", bubble_zzz, test_staff.state) # STRIKE / FLEEING -> anger (no unapproved morale thresholds) test_staff.state = StaffRuntime.State.STRIKE var bubble_anger: StringName = OperationFeedbackScript.resolve_staff_bubble(test_staff) _expect(bubble_anger == OperationFeedbackScript.BUBBLE_ANGER, "Strike staff resolves to anger bubble") observer.record_bubble(&"op_worker_1", bubble_anger, test_staff.state) # Authoritative RelationshipState / RouteLock -> heart (no unapproved loyalty/morale thresholds) var rel_state := RelationshipState.new() rel_state.configure(&"rel_op_1", &"heroine_anri", &"op_worker_1", { &"shield_max": 100, &"shield_current": 100, &"route_lock": RelationshipState.RouteLock.EXCLUSIVE_PENDING, &"route_id": &"pure_love", &"metrics": {&"affection": 80} }) test_staff.state = StaffRuntime.State.AVAILABLE var bubble_heart: StringName = OperationFeedbackScript.resolve_staff_bubble(test_staff, rel_state) _expect(bubble_heart == OperationFeedbackScript.BUBBLE_HEART, "Authoritative relationship model resolves to heart bubble") observer.record_bubble(&"op_worker_1", bubble_heart, test_staff.state) # Switching state back to normal working -> clears bubble without stale symbol test_staff.state = StaffRuntime.State.WORKING var bubble_work: StringName = OperationFeedbackScript.resolve_staff_bubble(test_staff) _expect(bubble_work == OperationFeedbackScript.BUBBLE_NONE, "Normal working staff clears bubble without stale symbol") var neutral_rel := RelationshipState.new() neutral_rel.configure(&"rel_op_neutral", &"heroine_anri", &"op_worker_1", { &"shield_max": 100, &"shield_current": 0, &"route_lock": RelationshipState.RouteLock.NONE, &"route_id": &"", &"metrics": {&"affection": 0} }) var bubble_cleared: StringName = OperationFeedbackScript.resolve_staff_bubble(test_staff, neutral_rel) _expect(bubble_cleared == OperationFeedbackScript.BUBBLE_NONE, "Neutral relationship model clears heart bubble") observer.record_bubble(&"op_worker_1", bubble_cleared, test_staff.state) _expect(observer.get_latest_bubble(&"op_worker_1").get(&"kind", &"") == OperationFeedbackScript.BUBBLE_NONE, "Observer confirms stale bubble symbol is removed on transition to normal") # Guest bubble: Incapacitated -> Zzz var test_guest := GuestRuntime.new() test_guest.guest_id = &"bubble_guest" test_guest.state = GuestRuntime.State.INCAPACITATED _expect(OperationFeedbackScript.resolve_guest_bubble(test_guest) == OperationFeedbackScript.BUBBLE_ZZZ, "Incapacitated guest resolves to Zzz bubble") # Guest bubble: Abandoning -> anger test_guest.state = GuestRuntime.State.ABANDONING _expect(OperationFeedbackScript.resolve_guest_bubble(test_guest) == OperationFeedbackScript.BUBBLE_ANGER, "Abandoning guest resolves to anger bubble") # Guest bubble: Normal moving -> no bubble test_guest.state = GuestRuntime.State.MOVING _expect(OperationFeedbackScript.resolve_guest_bubble(test_guest) == OperationFeedbackScript.BUBBLE_NONE, "Normal moving guest clears bubble without stale symbol") # ========================================================================= # Condition 4: event_log (Bounded 256, formatted Japanese, deduplicated) # ========================================================================= var event_dialog: EventLogDialog = main.get("event_log_dialog") _expect(event_dialog != null, "EventLogDialog exists in Main") var event_rows: Array[Dictionary] = event_dialog.get_event_rows() _expect(event_rows.size() <= 256, "Event rows bounded to recent 256 entries") var sample_event := { "event_id": "evt_test_001", "event_type": "DEFENSE_RAID_CREATED", "absolute_minute": 360, "entity_ids": [], "amount": -500, "reason": "RAID_START" } var presentation: Dictionary = event_dialog.get_event_presentation(sample_event) _expect(presentation[&"title"] == "襲撃が発生", "Event type resolves to readable Japanese title") _expect("G-500" in presentation[&"extra"] or "500" in presentation[&"extra"], "Event presentation includes amount details") # ========================================================================= # Condition 5: important_warning_cutin (Invasion / Duel / Defense start) # ========================================================================= _expect(main.has_method(&"show_warning_cutin"), "Main provides show_warning_cutin") main.call(&"show_warning_cutin", OperationFeedbackScript.CUTIN_DEFENSE, "防衛戦開始演出", "敵の襲撃が発生しました!") var cutin: OperationFeedback.ImportantWarningCutinDialog = main.get("important_warning_cutin") _expect(cutin != null and cutin.visible, "Important warning cut-in is displayed") observer.record_cutin(OperationFeedbackScript.CUTIN_DEFENSE, "防衛戦開始演出", "敵の襲撃が発生しました!") cutin.hide() cutin.emit_signal(&"confirmed") await process_frame _expect(not cutin.visible, "Important warning cut-in dismisses cleanly") # Real domain event triggers: DEFENSE_RAID_CREATED and RELATIONSHIP_SHIELD_CHANGED break main.call(&"_on_domain_event_published", { &"event_type": &"DEFENSE_RAID_CREATED", &"payload": {&"raid_id": &"raid_test_01"} }) _expect(cutin != null and cutin.visible, "DEFENSE_RAID_CREATED domain event triggers defense cutin") cutin.hide() cutin.emit_signal(&"confirmed") await process_frame main.call(&"_on_domain_event_published", { &"event_type": &"RELATIONSHIP_SHIELD_CHANGED", &"payload": {&"old_value": 50, &"new_value": 0} }) _expect(cutin != null and cutin.visible, "Shield vanishing to 0 triggers love shield warning cutin") cutin.hide() cutin.emit_signal(&"confirmed") await process_frame # Generic INCIDENT_CREATED does not falsely trigger warning cutin main.call(&"_on_domain_event_published", { &"event_type": &"INCIDENT_CREATED", &"payload": {&"incident_type": &"illegal_facility"} }) _expect(not cutin.visible, "Generic INCIDENT_CREATED does not trigger warning cutin") # ========================================================================= # Condition 6: click_alternatives (OperationsCameraControls click equivalents) # ========================================================================= var camera_ctrl: OperationsCameraControls = main.get("operations_camera_controls") _expect(camera_ctrl != null, "OperationsCameraControls exists in Main") var initial_zoom: float = preview.zoom_level camera_ctrl.zoom_in_button.emit_signal(&"pressed") _expect(preview.zoom_level > initial_zoom, "Click zoom in increments zoom level") camera_ctrl.zoom_out_button.emit_signal(&"pressed") _expect(is_equal_approx(preview.zoom_level, initial_zoom), "Click zoom out restores zoom level") var initial_pan: Vector2 = preview.pan_offset camera_ctrl.pan_up_button.emit_signal(&"pressed") _expect(preview.pan_offset != initial_pan, "Click pan moves pan offset") camera_ctrl.reset_button.emit_signal(&"pressed") _expect(preview.zoom_level >= 0.5 and preview.zoom_level <= 4.0, "Click reset maintains valid viewport framing") var grid_before: bool = preview.show_grid camera_ctrl.grid_button.emit_signal(&"pressed") _expect(preview.show_grid != grid_before, "Click grid button toggles grid display") camera_ctrl.grid_button.emit_signal(&"pressed") # ========================================================================= # Condition 7: japanese_input_and_scale (Japanese save label, cryptographic path) # ========================================================================= var label_edit: LineEdit = save_dialog.find_child("ManualSaveLabelEdit", true, false) as LineEdit _expect(label_edit != null, "Manual save slots dialog contains LineEdit for Japanese label input") save_dialog.show_new_save("user://saves/manual_ui") _expect(not label_edit.text.is_empty(), "Save label has valid Japanese default text: " + label_edit.text) # User inputs Japanese text var custom_label := "世界樹第壱号拠点・本番セーブ" label_edit.text = custom_label label_edit.emit_signal(&"text_changed", custom_label) _expect(save_dialog.get_ok_button().disabled == false, "Valid Japanese text allows save confirmation") # Clear / cancel semantics save_dialog.hide() save_dialog.emit_signal(&"canceled") _expect(label_edit.text.is_empty(), "Dialog cancel clears input label") # Cryptographic owned path check _expect(ManualSaveSlotsDialogScript.is_owned_slot_name("manual_ui_0123456789abcdef0123456789abcdef"), "Cryptographic slot path regex matches owned specification") _expect(not ManualSaveSlotsDialogScript.is_owned_slot_name("user_custom_folder_name"), "User custom folder name rejected from cryptographic slot rule") # ========================================================================= # Condition 8: final_validation_review (All 8 conditions checked, ordinary cleanup) # ========================================================================= var observer_summary: Dictionary = observer.get_summary() _expect(observer_summary[&"pid"] > 0, "Observer tracks non-zero PID") _expect(observer_summary[&"bubbles_tracked"] >= 3, "Observer tracked at least 3 unit bubble events") _expect(observer_summary[&"unique_actors_tracked"] >= 1, "Observer maintains latest state map for tracked actors") var cleanup: Dictionary = await FixtureCleanup.free_owner_and_wait(self, main) _expect(bool(cleanup.get(&"passed", false)), "Ordinary Main fixture and audio cleanup succeeds") print("Operations notification: %d checks, %d failures" % [_checks, _failures]) quit(1 if _failures else 0)