extends SceneTree const BattleStateScript := preload("res://scripts/core/battle_state.gd") const BattleSceneScript := preload("res://scripts/scenes/battle_scene.gd") const CHAPTER_ONE_SCENARIOS := [ "res://data/scenarios/001_yellow_turbans.json", "res://data/scenarios/002_sishui_gate.json", "res://data/scenarios/003_xingyang_ambush.json", "res://data/scenarios/004_qingzhou_campaign.json", "res://data/scenarios/005_puyang_raid.json", "res://data/scenarios/006_dingtao_counterattack.json", "res://data/scenarios/007_xian_emperor_escort.json", "res://data/scenarios/008_wan_castle_escape.json", "res://data/scenarios/009_xiapi_siege.json" ] func _init() -> void: var failures: Array[String] = [] _check_chapter_one_scenarios_have_visual_grounding(failures) _check_edge_scroll_layout_contract(failures) _check_readability_contract(failures) _check_dialogue_localization_and_side(failures) _check_event_dialogue_side_passthrough(failures) _check_opening_battle_event_dialogue_structure(failures) _check_sishui_gate_event_dialogue_structure(failures) _check_opening_battle_story_beats(failures) _check_sishui_gate_story_beats(failures) _check_pixel_unit_helpers(failures) if failures.is_empty(): print("chapter one polish smoke ok") quit(0) return for failure in failures: push_error(failure) quit(1) func _check_chapter_one_scenarios_have_visual_grounding(failures: Array[String]) -> void: var scene = BattleSceneScript.new() for scenario_path in CHAPTER_ONE_SCENARIOS: if not scene.state.load_battle(scenario_path): failures.append("could not load chapter one scenario: %s" % scenario_path) continue var background_path: String = scene.state.get_map_background_path() if background_path.is_empty(): failures.append("scenario is missing a map background: %s" % scenario_path) continue if not FileAccess.file_exists(background_path): failures.append("scenario background does not exist: %s -> %s" % [scenario_path, background_path]) scene.free() func _check_edge_scroll_layout_contract(failures: Array[String]) -> void: var scene = BattleSceneScript.new() if not scene.state.load_battle("res://data/scenarios/009_xiapi_siege.json"): failures.append("could not load large chapter one map for edge scrolling") scene.free() return var view_rect := scene._map_view_rect() var board_size := scene._board_size() if view_rect.size.x < 160.0 or view_rect.size.y < 160.0: failures.append("map view rect should keep a usable minimum size") if board_size.x <= view_rect.size.x and board_size.y <= view_rect.size.y: failures.append("chapter one should include at least one map large enough to exercise edge scrolling") scene._reset_board_scroll() if scene.board_scroll_offset.x > 0.0 or scene.board_scroll_offset.y > 0.0: failures.append("reset board scroll should never push the map past the top-left origin") var far_offset := scene._clamped_board_scroll_offset(Vector2(-9999.0, -9999.0)) var min_x := minf(0.0, view_rect.size.x - board_size.x) var min_y := minf(0.0, view_rect.size.y - board_size.y) if far_offset.x < min_x - 0.01 or far_offset.y < min_y - 0.01: failures.append("edge scroll clamp should stay inside the visible map bounds") if far_offset.x > 0.01 or far_offset.y > 0.01: failures.append("edge scroll clamp should not create positive board offsets") scene.board_scroll_offset = far_offset var first_cell_screen := scene._rect_for_cell(Vector2i(0, 0)).position + Vector2(4.0, 4.0) if scene._cell_from_screen(first_cell_screen) != Vector2i(0, 0): failures.append("screen-to-cell mapping should stay stable after scrolling") scene.free() func _check_readability_contract(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() if scene.dialogue_text_panel.custom_minimum_size.y < 160.0: failures.append("dialogue text panel should be tall enough for multi-line story text") if scene.dialogue_text_label.custom_minimum_size.y < 130.0: failures.append("dialogue label should reserve enough height to avoid clipping") if scene.dialogue_text_label.autowrap_mode != TextServer.AUTOWRAP_WORD_SMART: failures.append("dialogue text should use smart Korean-friendly wrapping") if scene.briefing_objective_panel.custom_minimum_size.y < 120.0: failures.append("briefing objective panel should reserve enough height for victory and defeat text") if scene.briefing_objective_toggle_button == null: failures.append("briefing objective section should have a close/open button") elif scene.briefing_objective_toggle_button.text != "닫기": failures.append("briefing objective button should start with a clear close label") scene.free() func _check_dialogue_localization_and_side(failures: Array[String]) -> void: var scene = BattleSceneScript.new() var lines := scene._normalized_dialogue_lines([ {"speaker": "Cao Cao", "text": "전열을 세워라."}, {"speaker": "Lu Bu", "text": "조조를 짓밟아라."}, {"speaker": "Qingzhou Chief", "text": "마을 곡식을 내놓아라."}, {"speaker": "Camp Merchant", "text": "군막에 물자가 남았습니다."}, {"speaker": "Hua Xiong", "text": "사수관을 지켜라."} ]) if lines.size() != 5: failures.append("dialogue normalization should keep all valid chapter one lines") scene.free() return if lines[0].get("speaker", "") != "조조" or lines[0].get("side", "") != "left": failures.append("Cao Cao dialogue should localize to Korean on the left") if lines[1].get("speaker", "") != "여포" or lines[1].get("side", "") != "right": failures.append("enemy officer dialogue should localize to Korean on the right") if lines[2].get("speaker", "") != "청주 두령" or lines[2].get("side", "") != "right": failures.append("chapter one named enemy dialogue should be localized and right-sided") if lines[3].get("speaker", "") != "군막 상인" or lines[3].get("side", "") != "right": failures.append("camp merchant dialogue should be localized and right-sided by default") if lines[4].get("speaker", "") != "화웅" or lines[4].get("side", "") != "right": failures.append("Sishui Gate enemy commander dialogue should localize to Korean on the right") scene.free() func _check_event_dialogue_side_passthrough(failures: Array[String]) -> void: var state = BattleStateScript.new() var scene = BattleSceneScript.new() var raw_lines := state._dialogue_lines_from_action({ "type": "dialogue", "lines": [ {"speaker": "Zhang Mancheng", "text": "성채를 지켜라."}, {"speaker": "Cao Cao", "text": "길을 열어라."} ] }) if raw_lines.size() != 2: failures.append("battle events should preserve valid dialogue lines") scene.free() return if str((raw_lines[0] as Dictionary).get("side", "")) != "": failures.append("battle state should not force event dialogue without an explicit side to the left") var normalized := scene._normalized_dialogue_lines(raw_lines) if normalized.size() != 2: failures.append("scene should normalize battle event dialogue lines") elif normalized[0].get("speaker", "") != "장만성" or normalized[0].get("side", "") != "right": failures.append("event dialogue should localize Zhang Mancheng and infer right-side layout") scene.free() func _check_opening_battle_event_dialogue_structure(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not load opening battle for event dialogue structure") return for event_id in [ "opening_dialogue", "northern_woods_cache", "turn_2_warning", "turn_4_eastern_reserve", "turn_7_southern_raiders", "turn_9_western_reserve", "zhang_mancheng_falls" ]: var event := _event_by_id(state, event_id) if event.is_empty(): failures.append("opening battle missing event: %s" % event_id) continue if not _event_has_dialogue_action(event): failures.append("opening battle event should include contextual dialogue: %s" % event_id) var boss_event := _event_by_id(state, "zhang_mancheng_falls") if not boss_event.is_empty(): var when: Dictionary = boss_event.get("when", {}) var unit_ids: Array = when.get("unit_ids", []) if not unit_ids.has("yellow_turban_1"): failures.append("Zhang Mancheng defeat event should watch yellow_turban_1") func _check_sishui_gate_event_dialogue_structure(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/002_sishui_gate.json"): failures.append("could not load Sishui Gate for event dialogue structure") return for event_id in [ "opening_dialogue", "turn_2_warning", "turn_5_ridge_patrol", "turn_8_gate_reserve", "hua_xiong_vanguard_falls" ]: var event := _event_by_id(state, event_id) if event.is_empty(): failures.append("Sishui Gate missing event: %s" % event_id) continue if not _event_has_dialogue_action(event): failures.append("Sishui Gate event should include contextual dialogue: %s" % event_id) var vanguard_event := _event_by_id(state, "hua_xiong_vanguard_falls") if not vanguard_event.is_empty(): var when: Dictionary = vanguard_event.get("when", {}) var unit_ids: Array = when.get("unit_ids", []) if not unit_ids.has("hua_xiong_vanguard"): failures.append("Hua Xiong vanguard defeat event should watch hua_xiong_vanguard") if not _event_has_set_objective_action(vanguard_event): failures.append("Hua Xiong vanguard defeat event should clarify the next objective") func _check_opening_battle_story_beats(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): failures.append("could not load opening battle for story beat checks") return var dialogue_batches := [] state.dialogue_requested.connect(func(lines: Array) -> void: dialogue_batches.append(lines) ) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 4) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 7) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9) if dialogue_batches.size() < 4: failures.append("opening battle should add dialogue beats for each major reinforcement phase") var victory_text := str(state.objectives.get("victory", "")) if not victory_text.contains("마지막 황건대"): failures.append("opening battle turn 9 event should update the victory objective after the final reserve arrives") if not state._condition_gate_open({"after_event": "turn_9_western_reserve"}): failures.append("opening battle final objective gate should open after the turn 9 story beat") func _check_sishui_gate_story_beats(failures: Array[String]) -> void: var state = BattleStateScript.new() if not state.load_battle("res://data/scenarios/002_sishui_gate.json"): failures.append("could not load Sishui Gate for story beat checks") return var dialogue_batches := [] state.dialogue_requested.connect(func(lines: Array) -> void: dialogue_batches.append(lines) ) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 2) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 5) state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 8) if dialogue_batches.size() < 3: failures.append("Sishui Gate should add dialogue beats for each major reserve phase") var victory_text := str(state.objectives.get("victory", "")) if not victory_text.contains("마지막 예비대"): failures.append("Sishui Gate turn 8 event should update the victory objective after the final reserve arrives") if not state._condition_gate_open({"after_event": "turn_8_gate_reserve"}): failures.append("Sishui Gate final objective gate should open after the turn 8 story beat") func _event_by_id(state, event_id: String) -> Dictionary: for event in state.battle_events: if typeof(event) == TYPE_DICTIONARY and str(event.get("id", "")) == event_id: return event return {} func _event_has_dialogue_action(event: Dictionary) -> bool: var actions: Array = event.get("actions", []) for action in actions: if typeof(action) == TYPE_DICTIONARY and str(action.get("type", "")) == "dialogue": return true return false func _event_has_set_objective_action(event: Dictionary) -> bool: var actions: Array = event.get("actions", []) for action in actions: if typeof(action) == TYPE_DICTIONARY and str(action.get("type", "")) == "set_objective": return true return false func _check_pixel_unit_helpers(failures: Array[String]) -> void: var scene = BattleSceneScript.new() for method_name in [ "_draw_pixel_unit_sprite", "_draw_pixel_infantry", "_draw_pixel_archer", "_draw_pixel_cavalry", "_draw_pixel_strategist", "_draw_pixel_heavy" ]: if not scene.has_method(method_name): failures.append("missing pixel-map unit helper: %s" % method_name) scene.free()