From 77db064d03d29a39a065365f771507f788fc0c96 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Fri, 19 Jun 2026 12:33:37 +0900 Subject: [PATCH] Refine Korean briefing and expand first battle --- data/defs/terrain.json | 16 ++ data/scenarios/001_yellow_turbans.json | 154 +++++++++++++++--- scripts/core/battle_state.gd | 55 ++++++- scripts/core/data_catalog.gd | 1 + scripts/scenes/battle_scene.gd | 213 ++++++++++++++----------- tools/smoke_event_ai_priority.gd | 2 +- tools/smoke_objective_progress.gd | 47 ++++-- tools/smoke_visual_assets.gd | 81 ++++++---- tools/validate_data.ps1 | 4 + 9 files changed, 403 insertions(+), 170 deletions(-) diff --git a/data/defs/terrain.json b/data/defs/terrain.json index 555865d..f2076dd 100644 --- a/data/defs/terrain.json +++ b/data/defs/terrain.json @@ -5,6 +5,7 @@ "color": "#6d9e4f", "defense": 0, "avoid": 0, + "heal": 0, "move_cost": { "foot": 1, "mounted": 1, "archer": 1, "water": 99 } }, "F": { @@ -13,6 +14,7 @@ "color": "#33733a", "defense": 2, "avoid": 10, + "heal": 0, "move_cost": { "foot": 2, "mounted": 3, "archer": 2, "water": 99 } }, "H": { @@ -21,6 +23,7 @@ "color": "#8c8057", "defense": 1, "avoid": 5, + "heal": 0, "move_cost": { "foot": 2, "mounted": 3, "archer": 2, "water": 99 } }, "D": { @@ -29,6 +32,7 @@ "color": "#96734a", "defense": 0, "avoid": 0, + "heal": 0, "move_cost": { "foot": 1, "mounted": 2, "archer": 1, "water": 99 } }, "R": { @@ -37,6 +41,7 @@ "color": "#a38c6b", "defense": 0, "avoid": 0, + "heal": 0, "move_cost": { "foot": 1, "mounted": 1, "archer": 1, "water": 99 } }, "W": { @@ -45,14 +50,25 @@ "color": "#2e5c9e", "defense": 0, "avoid": 0, + "heal": 0, "move_cost": { "foot": 99, "mounted": 99, "archer": 99, "water": 1 } }, + "T": { + "id": "town", + "name": "마을", + "color": "#b68a54", + "defense": 1, + "avoid": 5, + "heal": 6, + "move_cost": { "foot": 1, "mounted": 2, "archer": 1, "water": 99 } + }, "C": { "id": "castle", "name": "성채", "color": "#7f7a70", "defense": 3, "avoid": 0, + "heal": 8, "move_cost": { "foot": 1, "mounted": 2, "archer": 1, "water": 99 } } } diff --git a/data/scenarios/001_yellow_turbans.json b/data/scenarios/001_yellow_turbans.json index b31e050..f74a468 100644 --- a/data/scenarios/001_yellow_turbans.json +++ b/data/scenarios/001_yellow_turbans.json @@ -2,14 +2,14 @@ "id": "001_yellow_turbans", "name": "영천 소전", "objectives": { - "victory": "황건적 전군을 격파하라.", - "defeat": "조조가 퇴각하거나 제8턴에 이르면 패한다." + "victory": "황건적 본대를 격파하고 성채를 장악하라.", + "defeat": "조조가 퇴각하거나 제16턴이 시작되면 패한다." }, "conditions": { "victory": { "type": "all_units_defeated", "team": "enemy" }, "defeat": [ { "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] }, - { "type": "turn_reached", "turn": 8, "team": "player" } + { "type": "turn_reached", "turn": 16, "team": "player" } ] }, "briefing": { @@ -17,7 +17,8 @@ "location": "영천, 중평 원년", "lines": [ "황건의 무리가 영천 근방에 깃발을 세웠다.", - "조조와 하후돈은 난세의 불길이 번지기 전에 그 진형을 꺾어야 한다." + "조조와 하후돈은 난세의 불길이 번지기 전에 그 진형을 꺾어야 한다.", + "마을과 성채를 점거한 부대는 차례가 시작될 때 병력을 회복한다." ], "camp_dialogue": [ { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "영천은 마른 풀밭의 불씨와 같다. 결단이 빠르면 조정도 아직 질서가 돌아올 수 있음을 믿을 것이다." }, @@ -83,18 +84,20 @@ "cells": [[1, 3], [1, 4], [2, 3], [2, 4]] }, "map": { - "width": 10, - "height": 8, + "width": 14, + "height": 10, "background": "res://art/backgrounds/battlefield_frontier.png", "terrain": [ - "GGGGGGGGCC", - "GGGFFFFGCC", - "GGGFFFGGGG", - "GGGRRGGGGG", - "GGGRRGGHHG", - "GGGGGGGHHG", - "GGGWWGGGGG", - "GGGWWGGGGG" + "GGGGGGGGGGGTCC", + "GGGFFFFGGGGTCC", + "GGGFFFFGGGGGGG", + "GGGRRRRRRRGGGG", + "GGGRGGGGGRGGHG", + "GGGRGGTTGRGGHG", + "GGGGGGGGGRGGGG", + "GGGWWGGGGRGGGG", + "GGGWWGGGGGGGGG", + "GGGGGGGGGGGGGG" ] }, "deployments": [ @@ -115,18 +118,18 @@ "name": "장만성", "class_id": "bandit", "team": "enemy", - "level": 1, - "pos": [7, 2], - "base": { "hp": 34, "atk": 11, "def": 5 } + "level": 2, + "pos": [12, 1], + "base": { "hp": 46, "atk": 12, "def": 7 } }, { "unit_id": "yellow_turban_2", - "name": "농민 반군", + "name": "황건 창병", "class_id": "infantry", "team": "enemy", "level": 1, - "pos": [8, 5], - "base": { "hp": 30, "atk": 10, "def": 5 } + "pos": [9, 4], + "base": { "hp": 34, "atk": 10, "def": 6 } }, { "unit_id": "yellow_turban_3", @@ -134,8 +137,53 @@ "class_id": "archer", "team": "enemy", "level": 1, - "pos": [7, 6], + "pos": [11, 2], "base": { "hp": 26, "atk": 9, "def": 4 } + }, + { + "unit_id": "yellow_turban_4", + "name": "마을 방패병", + "class_id": "infantry", + "team": "enemy", + "level": 1, + "pos": [6, 5], + "base": { "hp": 36, "atk": 9, "def": 7 } + }, + { + "unit_id": "yellow_turban_5", + "name": "황건 유격병", + "class_id": "bandit", + "team": "enemy", + "level": 1, + "pos": [7, 3], + "base": { "hp": 32, "atk": 11, "def": 5 } + }, + { + "unit_id": "yellow_turban_6", + "name": "황건 궁수", + "class_id": "archer", + "team": "enemy", + "level": 1, + "pos": [11, 7], + "base": { "hp": 26, "atk": 9, "def": 4 } + }, + { + "unit_id": "yellow_turban_7", + "name": "성채 수비병", + "class_id": "infantry", + "team": "enemy", + "level": 1, + "pos": [11, 1], + "base": { "hp": 38, "atk": 10, "def": 7 } + }, + { + "unit_id": "yellow_turban_8", + "name": "황건 별동대", + "class_id": "bandit", + "team": "enemy", + "level": 1, + "pos": [10, 6], + "base": { "hp": 32, "atk": 11, "def": 5 } } ], "events": [ @@ -176,7 +224,69 @@ "once": true, "when": { "type": "turn_start", "team": "enemy", "turn": 2 }, "actions": [ - { "type": "log", "text": "황건적이 빈틈을 엿보고 있다." } + { "type": "log", "text": "황건적이 마을과 성채에 기대어 전열을 고친다." } + ] + }, + { + "id": "turn_4_eastern_reserve", + "once": true, + "when": { "type": "turn_start", "team": "enemy", "turn": 4 }, + "actions": [ + { "type": "log", "text": "동쪽 들판에서 황건 잔병이 합류했다." }, + { + "type": "spawn_deployments", + "deployments": [ + { + "unit_id": "yellow_turban_reinforcement_1", + "name": "황건 창병", + "class_id": "infantry", + "team": "enemy", + "level": 1, + "pos": [13, 4], + "base": { "hp": 32, "atk": 10, "def": 5 } + }, + { + "unit_id": "yellow_turban_reinforcement_2", + "name": "황건 궁수", + "class_id": "archer", + "team": "enemy", + "level": 1, + "pos": [13, 5], + "base": { "hp": 24, "atk": 9, "def": 4 } + } + ] + } + ] + }, + { + "id": "turn_7_southern_raiders", + "once": true, + "when": { "type": "turn_start", "team": "enemy", "turn": 7 }, + "actions": [ + { "type": "log", "text": "남쪽 샛길에서 약탈대가 뒤늦게 모습을 드러냈다." }, + { + "type": "spawn_deployments", + "deployments": [ + { + "unit_id": "yellow_turban_reinforcement_3", + "name": "황건 약탈병", + "class_id": "bandit", + "team": "enemy", + "level": 1, + "pos": [8, 8], + "base": { "hp": 30, "atk": 10, "def": 5 } + }, + { + "unit_id": "yellow_turban_reinforcement_4", + "name": "황건 궁수", + "class_id": "archer", + "team": "enemy", + "level": 1, + "pos": [9, 8], + "base": { "hp": 24, "atk": 9, "def": 4 } + } + ] + } ] } ], diff --git a/scripts/core/battle_state.gd b/scripts/core/battle_state.gd index add8c00..2ff9e76 100644 --- a/scripts/core/battle_state.gd +++ b/scripts/core/battle_state.gd @@ -50,6 +50,7 @@ const DEFAULT_TERRAIN_DEFS := { "move_cost": 1, "defense": 0, "avoid": 0, + "heal": 0, "color": Color(0.43, 0.62, 0.31) }, "F": { @@ -57,6 +58,7 @@ const DEFAULT_TERRAIN_DEFS := { "move_cost": 2, "defense": 2, "avoid": 10, + "heal": 0, "color": Color(0.20, 0.45, 0.23) }, "H": { @@ -64,6 +66,7 @@ const DEFAULT_TERRAIN_DEFS := { "move_cost": 2, "defense": 1, "avoid": 5, + "heal": 0, "color": Color(0.55, 0.50, 0.34) }, "D": { @@ -71,6 +74,7 @@ const DEFAULT_TERRAIN_DEFS := { "move_cost": {"foot": 1, "mounted": 2, "archer": 1, "water": 99}, "defense": 0, "avoid": 0, + "heal": 0, "color": Color(0.59, 0.45, 0.29) }, "R": { @@ -78,6 +82,7 @@ const DEFAULT_TERRAIN_DEFS := { "move_cost": 1, "defense": 0, "avoid": 0, + "heal": 0, "color": Color(0.64, 0.55, 0.42) }, "W": { @@ -85,7 +90,24 @@ const DEFAULT_TERRAIN_DEFS := { "move_cost": 99, "defense": 0, "avoid": 0, + "heal": 0, "color": Color(0.18, 0.36, 0.62) + }, + "T": { + "name": "마을", + "move_cost": {"foot": 1, "mounted": 2, "archer": 1, "water": 99}, + "defense": 1, + "avoid": 5, + "heal": 6, + "color": Color(0.71, 0.54, 0.33) + }, + "C": { + "name": "성채", + "move_cost": {"foot": 1, "mounted": 2, "archer": 1, "water": 99}, + "defense": 3, + "avoid": 0, + "heal": 8, + "color": Color(0.50, 0.48, 0.44) } } @@ -2028,6 +2050,10 @@ func get_terrain_avoid(cell: Vector2i) -> int: return int(terrain_defs.get(get_terrain_key(cell), DEFAULT_TERRAIN_DEFS["G"]).get("avoid", 0)) +func get_terrain_heal(cell: Vector2i) -> int: + return int(terrain_defs.get(get_terrain_key(cell), DEFAULT_TERRAIN_DEFS["G"]).get("heal", 0)) + + func get_cell_summary(cell: Vector2i) -> Dictionary: if not is_inside(cell): return {} @@ -2039,6 +2065,7 @@ func get_cell_summary(cell: Vector2i) -> Dictionary: "move_cost": get_move_cost(cell), "defense": get_terrain_defense(cell), "avoid": get_terrain_avoid(cell), + "heal": get_terrain_heal(cell), "unit": unit } @@ -4169,7 +4196,7 @@ func _emit_ai_target_priority_feedback(unit: Dictionary, previous_priority: int, text = "%s는 여전히 적의 표적입니다." % unit_name _emit_log(text) if bool(unit.get("alive", false)) and bool(unit.get("deployed", true)): - _emit_combat_feedback(unit, "標的", "priority") + _emit_combat_feedback(unit, "표적", "priority") func _apply_objective_event(action: Dictionary) -> void: @@ -4187,9 +4214,9 @@ func _apply_objective_event(action: Dictionary) -> void: if not updated: return if action.has("victory") and not updated_victory.is_empty(): - _emit_log("軍令 개정: %s" % updated_victory) + _emit_log("군령 갱신: %s" % updated_victory) if action.has("defeat") and not updated_defeat.is_empty(): - _emit_log("敗兆 개정: %s" % updated_defeat) + _emit_log("패배 조건 갱신: %s" % updated_defeat) objective_updated.emit(updated_victory, updated_defeat) _notify_changed() @@ -4296,6 +4323,7 @@ func _reset_team_actions(team: String) -> void: _check_battle_status() if battle_status != STATUS_ACTIVE: return + _apply_terrain_recovery_for_team(team) _expire_status_effects_for_team(team) for unit in units: if unit.get("team", "") == team and unit.get("alive", false): @@ -4326,6 +4354,27 @@ func _apply_phase_status_effects_for_team(team: String) -> void: break +func _apply_terrain_recovery_for_team(team: String) -> void: + for unit in units: + if unit.get("team", "") != team or not unit.get("alive", false) or not unit.get("deployed", true): + continue + var cell: Vector2i = unit.get("pos", Vector2i(-1, -1)) + if not is_inside(cell): + continue + var heal := get_terrain_heal(cell) + if heal <= 0: + continue + var old_hp := int(unit.get("hp", 0)) + var max_hp := int(unit.get("max_hp", 1)) + var new_hp: int = mini(max_hp, old_hp + heal) + if new_hp <= old_hp: + continue + unit["hp"] = new_hp + var recovered := new_hp - old_hp + _emit_log("%s, %s에서 병력 %d 회복." % [unit["name"], get_terrain_name(cell), recovered]) + _emit_combat_feedback(unit, "+%d" % recovered, "heal") + + func _expire_status_effects_for_team(team: String) -> void: for unit in units: if unit.get("team", "") != team: diff --git a/scripts/core/data_catalog.gd b/scripts/core/data_catalog.gd index 9ebada6..76a1ebc 100644 --- a/scripts/core/data_catalog.gd +++ b/scripts/core/data_catalog.gd @@ -103,6 +103,7 @@ func get_runtime_terrain_defs(default_defs: Dictionary) -> Dictionary: runtime["name"] = str(source.get("name", runtime.get("name", key))) runtime["defense"] = int(source.get("defense", runtime.get("defense", 0))) runtime["avoid"] = int(source.get("avoid", runtime.get("avoid", 0))) + runtime["heal"] = int(source.get("heal", runtime.get("heal", 0))) runtime["move_cost"] = source.get("move_cost", runtime.get("move_cost", 1)) runtime["color"] = _parse_color(source.get("color", runtime.get("color", Color.WHITE)), runtime.get("color", Color.WHITE)) result[key] = runtime diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 0b2a0af..776b870 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -238,6 +238,7 @@ var briefing_camp_overview_texture: TextureRect var briefing_camp_overview_fallback_label: Label var briefing_camp_overview_label: Label var briefing_label: Label +var briefing_start_button: Button var chapter_button: Button var chapter_menu: VBoxContainer var chapter_list: VBoxContainer @@ -866,10 +867,10 @@ func _make_map_lattice_overlay(size: Vector2) -> Control: overlay.add_child(horizontal) var corner_specs := [ - {"text": "魏", "pos": Vector2(4, 4)}, - {"text": "令", "pos": Vector2(size.x - 24, 4)}, - {"text": "圖", "pos": Vector2(4, size.y - 24)}, - {"text": "印", "pos": Vector2(size.x - 24, size.y - 24)} + {"text": "위", "pos": Vector2(4, 4)}, + {"text": "명", "pos": Vector2(size.x - 24, 4)}, + {"text": "도", "pos": Vector2(4, size.y - 24)}, + {"text": "인", "pos": Vector2(size.x - 24, size.y - 24)} ] for spec in corner_specs: var seal := _make_seal_tile(str(spec["text"]), 20) @@ -1150,7 +1151,7 @@ func _create_hud() -> void: selected_row.add_child(selected_label) mission_title_label = Label.new() - mission_title_label.text = "軍令 목패" + mission_title_label.text = "전투 목표" mission_title_label.add_theme_font_size_override("font_size", 14) _apply_label_style(mission_title_label, UI_OLD_BRONZE) side_column.add_child(mission_title_label) @@ -1166,7 +1167,7 @@ func _create_hud() -> void: mission_detail_panel.add_child(mission_detail_row) mission_detail_row.add_child(_make_bamboo_gutter(12, 112)) - mission_detail_row.add_child(_make_edict_marker_stack(["令", "勝", "敗"], 112)) + mission_detail_row.add_child(_make_edict_marker_stack(["명", "승", "패"], 112)) mission_detail_label = Label.new() mission_detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART @@ -1174,7 +1175,7 @@ func _create_hud() -> void: mission_detail_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL _apply_label_style(mission_detail_label, UI_PARCHMENT_TEXT) mission_detail_row.add_child(mission_detail_label) - mission_detail_row.add_child(_make_edict_marker_stack(["功", "兆", "封"], 112)) + mission_detail_row.add_child(_make_edict_marker_stack(["공", "징", "봉"], 112)) mission_detail_row.add_child(_make_bamboo_gutter(12, 112)) cell_info_label = Label.new() @@ -1447,7 +1448,7 @@ func _create_hud() -> void: briefing_shell.add_child(briefing_column) briefing_shell.add_child(_make_bamboo_gutter(22, 632)) briefing_column.add_child(_make_scroll_rod(680, 6)) - briefing_column.add_child(_make_edict_title_strip(["魏", "軍", "令"], 680, 24)) + briefing_column.add_child(_make_edict_title_strip(["위", "군", "명"], 680, 24)) briefing_column.add_child(_make_tablet_binding(680, 16)) var briefing_title_panel := PanelContainer.new() @@ -1473,8 +1474,13 @@ func _create_hud() -> void: _apply_label_style(briefing_location_label, UI_INK_WASH, 14) briefing_column.add_child(briefing_location_label) + briefing_start_button = Button.new() + briefing_start_button.text = "닫고 전투 시작" + briefing_start_button.pressed.connect(_on_begin_battle_pressed) + briefing_column.add_child(briefing_start_button) + briefing_column.add_child(_make_scroll_rod(680, 5)) - briefing_column.add_child(_make_section_caption("軍令 · 勝敗條目", 680)) + briefing_column.add_child(_make_section_caption("승리 조건과 패배 조건", 680)) briefing_objective_panel = PanelContainer.new() briefing_objective_panel.custom_minimum_size = Vector2(680, 104) @@ -1486,7 +1492,7 @@ func _create_hud() -> void: briefing_objective_panel.add_child(briefing_objective_row) briefing_objective_row.add_child(_make_bamboo_gutter(18, 84)) - briefing_objective_row.add_child(_make_edict_marker_stack(["令", "勝", "敗"], 84)) + briefing_objective_row.add_child(_make_edict_marker_stack(["명", "승", "패"], 84)) briefing_objective_label = Label.new() briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART @@ -1494,11 +1500,11 @@ func _create_hud() -> void: briefing_objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL _apply_label_style(briefing_objective_label, UI_AGED_INK, 15) briefing_objective_row.add_child(briefing_objective_label) - briefing_objective_row.add_child(_make_edict_marker_stack(["軍", "功", "封"], 84)) + briefing_objective_row.add_child(_make_edict_marker_stack(["공", "보", "상"], 84)) briefing_objective_row.add_child(_make_bamboo_gutter(18, 84)) briefing_column.add_child(_make_ink_wash_rule(680, 8)) - briefing_column.add_child(_make_section_caption("戰圖 · 軍幕奏報", 680)) + briefing_column.add_child(_make_section_caption("전장도와 군막 보고", 680)) briefing_camp_overview_panel = PanelContainer.new() briefing_camp_overview_panel.custom_minimum_size = Vector2(680, 108) @@ -1508,7 +1514,7 @@ func _create_hud() -> void: var briefing_camp_overview_shell := HBoxContainer.new() briefing_camp_overview_shell.add_theme_constant_override("separation", 10) briefing_camp_overview_panel.add_child(briefing_camp_overview_shell) - briefing_camp_overview_shell.add_child(_make_edict_marker_stack(["圖", "地"], 86)) + briefing_camp_overview_shell.add_child(_make_edict_marker_stack(["도", "지"], 86)) briefing_camp_overview_row = HBoxContainer.new() briefing_camp_overview_row.custom_minimum_size = Vector2(580, 92) @@ -1562,7 +1568,7 @@ func _create_hud() -> void: briefing_scroll.add_child(briefing_label) briefing_column.add_child(_make_tablet_binding(680, 14)) - briefing_column.add_child(_make_edict_title_strip(["印", "兵", "發"], 680, 22)) + briefing_column.add_child(_make_edict_title_strip(["인", "출", "진"], 680, 22)) var prep_button_row := HBoxContainer.new() prep_button_row.add_theme_constant_override("separation", 8) @@ -1762,11 +1768,6 @@ func _create_hud() -> void: manual_load_button.pressed.connect(_on_manual_load_pressed) save_action_row.add_child(manual_load_button) - var begin_button := Button.new() - begin_button.text = "전장도 개봉" - begin_button.pressed.connect(_on_begin_battle_pressed) - briefing_column.add_child(begin_button) - dialogue_panel = PanelContainer.new() dialogue_panel.visible = false dialogue_panel.position = DIALOGUE_PANEL_POSITION @@ -1815,7 +1816,7 @@ func _create_hud() -> void: dialogue_row.add_child(dialogue_right_tassel) dialogue_column.add_child(_make_tablet_binding(DIALOGUE_TEXT_SIZE.x, 13)) - dialogue_column.add_child(_make_edict_title_strip(["軍", "談", "錄"], DIALOGUE_TEXT_SIZE.x, 22)) + dialogue_column.add_child(_make_edict_title_strip(["군", "담", "록"], DIALOGUE_TEXT_SIZE.x, 22)) dialogue_seal_ribbon = _make_seal_ribbon(DIALOGUE_TEXT_SIZE.x, 18) dialogue_column.add_child(dialogue_seal_ribbon) @@ -1828,7 +1829,7 @@ func _create_hud() -> void: var dialogue_speaker_row := HBoxContainer.new() dialogue_speaker_row.add_theme_constant_override("separation", 8) dialogue_speaker_panel.add_child(dialogue_speaker_row) - dialogue_speaker_row.add_child(_make_seal_tile("名", 24)) + dialogue_speaker_row.add_child(_make_seal_tile("명", 24)) dialogue_speaker_label = Label.new() dialogue_speaker_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 88, 24) @@ -1836,7 +1837,7 @@ func _create_hud() -> void: dialogue_speaker_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _apply_label_style(dialogue_speaker_label, UI_PARCHMENT_TEXT, 18) dialogue_speaker_row.add_child(dialogue_speaker_label) - dialogue_speaker_row.add_child(_make_seal_tile("語", 24)) + dialogue_speaker_row.add_child(_make_seal_tile("말", 24)) dialogue_text_panel = PanelContainer.new() dialogue_text_panel.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 104) @@ -1848,7 +1849,7 @@ func _create_hud() -> void: dialogue_text_panel.add_child(dialogue_text_row) dialogue_text_row.add_child(_make_bamboo_gutter(18, DIALOGUE_TEXT_SIZE.y - 20)) - dialogue_text_row.add_child(_make_edict_marker_stack(["言", "問", "答"], DIALOGUE_TEXT_SIZE.y - 20)) + dialogue_text_row.add_child(_make_edict_marker_stack(["말", "문", "답"], DIALOGUE_TEXT_SIZE.y - 20)) dialogue_text_label = Label.new() dialogue_text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART @@ -1857,7 +1858,7 @@ func _create_hud() -> void: dialogue_text_label.size_flags_vertical = Control.SIZE_EXPAND_FILL _apply_label_style(dialogue_text_label, UI_AGED_INK, 17) dialogue_text_row.add_child(dialogue_text_label) - dialogue_text_row.add_child(_make_edict_marker_stack(["錄", "軍", "印"], DIALOGUE_TEXT_SIZE.y - 20)) + dialogue_text_row.add_child(_make_edict_marker_stack(["록", "군", "인"], DIALOGUE_TEXT_SIZE.y - 20)) dialogue_text_row.add_child(_make_bamboo_gutter(18, DIALOGUE_TEXT_SIZE.y - 20)) dialogue_column.add_child(_make_scroll_rod(DIALOGUE_TEXT_SIZE.x, 3)) @@ -1875,14 +1876,14 @@ func _create_hud() -> void: dialogue_control_row.add_child(dialogue_progress_label) dialogue_previous_button = Button.new() - dialogue_previous_button.text = "前簡" + dialogue_previous_button.text = "이전" dialogue_previous_button.disabled = true dialogue_previous_button.custom_minimum_size = Vector2(88, 32) dialogue_previous_button.pressed.connect(_retreat_dialogue) dialogue_control_row.add_child(dialogue_previous_button) dialogue_continue_button = Button.new() - dialogue_continue_button.text = "次簡" + dialogue_continue_button.text = "다음" dialogue_continue_button.custom_minimum_size = Vector2(120, 32) dialogue_continue_button.pressed.connect(_advance_dialogue) dialogue_control_row.add_child(dialogue_continue_button) @@ -1903,7 +1904,7 @@ func _create_hud() -> void: result_seal_ribbon = _make_seal_ribbon(520, 18) result_column.add_child(result_seal_ribbon) - result_column.add_child(_make_section_caption("軍功 · 勝敗奏牘", 520)) + result_column.add_child(_make_section_caption("전공과 승패 기록", 520)) var result_body_panel := PanelContainer.new() result_body_panel.custom_minimum_size = Vector2(520, 236) @@ -1913,7 +1914,7 @@ func _create_hud() -> void: var result_body_row := HBoxContainer.new() result_body_row.add_theme_constant_override("separation", 8) result_body_panel.add_child(result_body_row) - result_body_row.add_child(_make_edict_marker_stack(["功", "賞", "命"], 198)) + result_body_row.add_child(_make_edict_marker_stack(["공", "상", "명"], 198)) result_label = Label.new() result_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT @@ -1924,7 +1925,7 @@ func _create_hud() -> void: result_label.size_flags_vertical = Control.SIZE_EXPAND_FILL _apply_label_style(result_label, UI_AGED_INK, 16) result_body_row.add_child(result_label) - result_body_row.add_child(_make_edict_marker_stack(["封", "牘", "印"], 198)) + result_body_row.add_child(_make_edict_marker_stack(["기", "록", "인"], 198)) result_column.add_child(_make_scroll_rod(520, 3)) @@ -1967,6 +1968,7 @@ func _create_hud() -> void: roster_button, formation_button, save_button, + briefing_start_button, shop_buy_button, shop_sell_button, manual_save_button, @@ -1977,7 +1979,7 @@ func _create_hud() -> void: next_battle_button ]: _apply_button_style(hud_button) - _apply_button_style(begin_button, true) + _apply_button_style(briefing_start_button, true) func _create_audio() -> void: @@ -2084,7 +2086,7 @@ func _play_ui_cancel() -> void: func _play_log_sfx(message: String) -> void: if message.contains(" but misses.") or message.contains("헛쳤다"): _play_sfx("guard_block") - elif message.begins_with("Objective updated:") or message.begins_with("Defeat condition updated:") or message.begins_with("軍令 개정:") or message.begins_with("敗兆 개정:") or message.begins_with("군령 갱신:") or message.begins_with("패전 군령 갱신:"): + elif message.begins_with("Objective updated:") or message.begins_with("Defeat condition updated:") or message.begins_with("군령 갱신:") or message.begins_with("패배 조건 갱신:"): _play_ui_confirm() elif message.ends_with(" withdraws from the battlefield.") or message.ends_with("전장을 물러났다."): _play_ui_confirm() @@ -2344,6 +2346,8 @@ func _handle_key(event: InputEventKey) -> void: _hide_formation_menu() elif save_menu != null and save_menu.visible: _hide_save_menu() + elif briefing_panel != null and briefing_panel.visible: + _on_begin_battle_pressed() elif post_move_picker_panel != null and post_move_picker_panel.visible: _on_post_move_picker_back_pressed() elif tactic_menu != null and tactic_menu.visible: @@ -2553,7 +2557,7 @@ func _terrain_fill_color(cell: Vector2i, terrain_key: String, has_background: bo var alpha := 0.13 if has_background else 0.34 if terrain_key == "W": alpha += 0.04 - elif terrain_key == "C" or terrain_key == "H": + elif terrain_key == "C" or terrain_key == "H" or terrain_key == "T": alpha += 0.02 elif terrain_key == "D": alpha += 0.015 @@ -2597,6 +2601,8 @@ func _draw_terrain_detail(cell: Vector2i, rect: Rect2, terrain_key: String) -> v _draw_water_shoreline(cell, rect) elif terrain_key == "C": _draw_castle_detail(rect) + elif terrain_key == "T": + _draw_town_detail(rect) else: for index in range(2): var point := _terrain_detail_point(cell, rect, index + 15) @@ -2664,6 +2670,24 @@ func _draw_castle_detail(rect: Rect2) -> void: draw_rect(notch, Color(0.84, 0.80, 0.72, 0.28)) +func _draw_town_detail(rect: Rect2) -> void: + var wall := Color(0.20, 0.12, 0.055, 0.30) + var roof := Color(0.58, 0.22, 0.10, 0.34) + var straw := Color(0.88, 0.70, 0.34, 0.28) + for index in range(2): + var base := rect.position + Vector2(12.0 + float(index) * 24.0, 31.0) + var body := Rect2(base, Vector2(18.0, 13.0)) + var roof_points := PackedVector2Array([ + base + Vector2(-3.0, 1.0), + base + Vector2(9.0, -9.0), + base + Vector2(21.0, 1.0) + ]) + draw_colored_polygon(roof_points, roof) + draw_rect(body, straw) + draw_rect(body, wall, false, 1.1) + draw_line(rect.position + Vector2(8, 50), rect.position + Vector2(rect.size.x - 8, 48), Color(0.26, 0.16, 0.08, 0.28), 1.4) + + func _draw_terrain_edge_blend(cell: Vector2i, rect: Rect2, terrain_key: String) -> void: _draw_terrain_edge(cell, rect, terrain_key, Vector2i(0, -1)) _draw_terrain_edge(cell, rect, terrain_key, Vector2i(1, 0)) @@ -2710,6 +2734,8 @@ func _terrain_edge_priority(terrain_key: String) -> int: return 60 if terrain_key == "C": return 50 + if terrain_key == "T": + return 45 if terrain_key == "F": return 40 if terrain_key == "H": @@ -2732,6 +2758,8 @@ func _terrain_edge_color(terrain_key: String, neighbor_key: String) -> Color: return Color(0.72, 0.90, 1.0, 0.28) if terrain_key == "C": return Color(0.92, 0.86, 0.72, 0.24) + if terrain_key == "T": + return Color(0.92, 0.70, 0.40, 0.26) if neighbor_key == "W": return Color(0.08, 0.18, 0.20, 0.18) return Color(0.96, 0.92, 0.62, 0.12) @@ -2742,6 +2770,8 @@ func _terrain_edge_width(terrain_key: String, neighbor_key: String) -> float: return 2.4 if terrain_key == "C": return 2.0 + if terrain_key == "T": + return 1.8 return 1.4 @@ -2920,7 +2950,7 @@ func _draw_target_selection_markers() -> void: draw_rect(rect.grow(-6.0), color, false, 2.2) draw_rect(marker_rect, Color(0.025, 0.028, 0.034, 0.88)) draw_rect(marker_rect, color, false, 1.4) - draw_string(font, marker_rect.position + Vector2(0, 13), str(marker.get("text", "標")), HORIZONTAL_ALIGNMENT_CENTER, marker_rect.size.x, 12, color) + draw_string(font, marker_rect.position + Vector2(0, 13), str(marker.get("text", "표")), HORIZONTAL_ALIGNMENT_CENTER, marker_rect.size.x, 12, color) func _target_selection_marker_entries() -> Array[Dictionary]: @@ -2973,7 +3003,7 @@ func _skill_target_marker_entries() -> Array[Dictionary]: result.append({ "cell": cell, "target_id": str(target.get("id", "")) if not target.is_empty() else "", - "text": str(badge.get("text", "標")), + "text": str(badge.get("text", "표")), "kind": str(badge.get("kind", "support")) }) return result @@ -2994,7 +3024,7 @@ func _item_target_marker_entries() -> Array[Dictionary]: result.append({ "cell": cell, "target_id": str(target.get("id", "")) if not target.is_empty() else "", - "text": str(badge.get("text", "標")), + "text": str(badge.get("text", "표")), "kind": str(badge.get("kind", "support")) }) return result @@ -3107,14 +3137,14 @@ func _draw_unit_class_emblem(rect: Rect2, center: Vector2, unit: Dictionary, tea func _unit_class_abbrev(unit: Dictionary) -> String: var family := _unit_class_family(unit) if family == "cavalry": - return "騎" + return "기" if family == "ranged": - return "弓" + return "궁" if family == "tactic": - return "策" + return "책" if family == "heavy": - return "武" - return "步" + return "무" + return "보" func _unit_class_family(unit: Dictionary) -> String: @@ -3875,12 +3905,12 @@ func _format_objective_hud_text() -> String: var lines := [] var victory := String(state.objectives.get("victory", "적군을 모두 격파하라.")) if not victory.is_empty(): - lines.append("勝令|朱批 %s" % victory) + lines.append("승리: %s" % victory) var defeat := String(state.objectives.get("defeat", "")) if not defeat.is_empty(): - lines.append("敗兆|墨戒 %s" % defeat) + lines.append("패배: %s" % defeat) if lines.is_empty(): - return "勝令|待詔" + return "승리 조건 대기" return _join_strings(lines, "\n") @@ -4056,7 +4086,7 @@ func _unit_current_terrain_text(unit: Dictionary) -> String: var cell: Vector2i = unit.get("pos", Vector2i(-1, -1)) if not state.is_inside(cell): return "" - return "지형 %d,%d %s 행군 %d 방비 +%d 회피 +%d%%" % [ + var text := "지형 %d,%d %s 행군 %d 방비 +%d 회피 +%d%%" % [ cell.x + 1, cell.y + 1, _terrain_display_name(state.get_terrain_name(cell)), @@ -4064,6 +4094,10 @@ func _unit_current_terrain_text(unit: Dictionary) -> String: state.get_terrain_defense(cell), state.get_terrain_avoid(cell) ] + var heal := state.get_terrain_heal(cell) + if heal > 0: + text += " 회복 +%d" % heal + return text func _update_mission_panel() -> void: @@ -4076,18 +4110,18 @@ func _format_mission_panel_text() -> String: var lines := [] var victory := str(state.objectives.get("victory", "")) if not victory.is_empty(): - _append_edict_clause(lines, "勝令", "令牌", victory) + _append_edict_clause(lines, "승리 조건", "목표", victory) var defeat := str(state.objectives.get("defeat", "")) if not defeat.is_empty(): - _append_edict_clause(lines, "敗兆", "戒牌", defeat) + _append_edict_clause(lines, "패배 조건", "주의", defeat) var progress_lines := state.get_objective_progress_lines(false, false) if not progress_lines.is_empty(): - _append_edict_clause(lines, "戰功", "功簿", _join_strings(progress_lines, " · ")) + _append_edict_clause(lines, "진행", "현황", _join_strings(progress_lines, " · ")) var risk_lines := state.get_defeat_progress_lines() if not risk_lines.is_empty(): - _append_edict_clause(lines, "凶兆", "凶簡", _join_strings(risk_lines, " · ")) + _append_edict_clause(lines, "위험", "경고", _join_strings(risk_lines, " · ")) if lines.is_empty(): - return "軍令\n 待詔|아직 봉인된 군령이 없습니다." + return "군령\n 대기|아직 열린 군령이 없습니다." return _join_strings(lines, "\n") @@ -4133,6 +4167,9 @@ func _update_cell_info() -> void: summary["defense"], summary.get("avoid", 0) ] + var heal := int(summary.get("heal", 0)) + if heal > 0: + text += " 회복 +%d" % heal if state.get_objective_cells().has(cell): text += "\n조서 표식" if show_threat_overlay: @@ -4509,8 +4546,8 @@ func _update_result_panel() -> void: func _format_victory_result_text() -> String: - var lines := ["凱旋 勝牘"] - _append_edict_clause(lines, "受命 勝令", "朱批", str(state.objectives.get("victory", ""))) + var lines := ["승리"] + _append_edict_clause(lines, "승리 조건", "완료", str(state.objectives.get("victory", ""))) var summary := _format_battle_result_summary() if not summary.is_empty(): lines.append(summary) @@ -4518,8 +4555,8 @@ func _format_victory_result_text() -> String: func _format_defeat_result_text() -> String: - var lines := ["敗軍 碑牘"] - _append_edict_clause(lines, "戒懼 敗兆", "墨戒", str(state.objectives.get("defeat", ""))) + var lines := ["패배"] + _append_edict_clause(lines, "패배 조건", "확인", str(state.objectives.get("defeat", ""))) return _join_strings(lines, "\n") @@ -4543,34 +4580,22 @@ func _on_log_added(message: String) -> void: func _format_log_entry_text(message: String) -> String: if message.begins_with("Objective updated:"): - return "軍令 개정:%s" % message.substr("Objective updated:".length()) + return "군령 갱신:%s" % message.substr("Objective updated:".length()) if message.begins_with("Defeat condition updated:"): - return "敗兆 개정:%s" % message.substr("Defeat condition updated:".length()) + return "패배 조건 갱신:%s" % message.substr("Defeat condition updated:".length()) return message func _format_hanja_number(value: int) -> String: - if value <= 0: - return str(value) - var digits := ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"] - if value < 10: - return digits[value] - if value < 20: - var ones := value % 10 - return "十" if ones == 0 else "十%s" % digits[ones] - if value < 100: - var tens := int(value / 10) - var ones := value % 10 - return "%s十" % digits[tens] if ones == 0 else "%s十%s" % [digits[tens], digits[ones]] return str(value) func _on_objective_updated(victory: String, defeat: String) -> void: - var notice_lines := ["急詔 軍令改封"] + var notice_lines := ["군령 갱신"] if not victory.is_empty(): - _append_edict_clause(notice_lines, "勝令", "朱批", victory) + _append_edict_clause(notice_lines, "승리 조건", "목표", victory) if not defeat.is_empty(): - _append_edict_clause(notice_lines, "敗兆", "墨戒", defeat) + _append_edict_clause(notice_lines, "패배 조건", "주의", defeat) if objective_notice_label != null: objective_notice_label.text = "\n".join(notice_lines) if objective_notice_panel != null: @@ -4908,7 +4933,7 @@ func _render_dialogue_line() -> void: var display_speaker := _localized_speaker_name(str(line.get("display_speaker", speaker))) var side := _normalized_dialogue_side(line.get("side", "left")) _apply_dialogue_side(side) - dialogue_speaker_label.text = "%s 曰" % display_speaker if not display_speaker.is_empty() else "" + dialogue_speaker_label.text = display_speaker if not display_speaker.is_empty() else "" dialogue_speaker_label.visible = not display_speaker.is_empty() dialogue_text_label.text = _format_dialogue_body_text(str(line.get("text", ""))) _update_dialogue_portrait(display_speaker, str(line.get("portrait", ""))) @@ -4923,19 +4948,16 @@ func _format_dialogue_body_text(text: String) -> String: if stripped.is_empty(): return "" if stripped.begins_with("「") or stripped.begins_with("『"): - return "墨書\n %s" % stripped - return "墨書\n 「%s」" % stripped + return stripped + return "「%s」" % stripped func _update_dialogue_controls() -> void: if dialogue_progress_label != null: - dialogue_progress_label.text = "죽간 %s之%s" % [ - _format_hanja_number(active_dialogue_index + 1), - _format_hanja_number(active_dialogue_lines.size()) - ] + dialogue_progress_label.text = "대화 %d/%d" % [active_dialogue_index + 1, active_dialogue_lines.size()] if dialogue_previous_button != null: dialogue_previous_button.disabled = active_dialogue_index <= 0 - dialogue_continue_button.text = "封緘" if active_dialogue_index >= active_dialogue_lines.size() - 1 else "次簡" + dialogue_continue_button.text = "닫기" if active_dialogue_index >= active_dialogue_lines.size() - 1 else "다음" func _apply_dialogue_side(side: String) -> void: @@ -5288,12 +5310,12 @@ func _format_briefing_title(briefing: Dictionary) -> String: var chapter_position := campaign_state.get_chapter_position_for_scenario(active_scenario_id) var chapter_count := campaign_state.get_chapter_count() if chapter_position > 0 and chapter_count > 0: - return "兵書 제%s책 · %s" % [_format_hanja_number(chapter_position), chapter_title] + return "장 %s/%s · %s" % [chapter_position, chapter_count, chapter_title] return chapter_title var position := campaign_state.get_scenario_position(active_scenario_id) var count := campaign_state.get_scenario_count() if position > 0 and count > 0: - return "戰場 제%s폭 · %s" % [_format_hanja_number(position), title] + return "전장 %s/%s · %s" % [position, count, title] return title @@ -5321,7 +5343,7 @@ func _format_briefing_chapter_battle_text(briefing: Dictionary) -> String: var position := campaign_state.get_scenario_position_in_chapter(active_scenario_id) var count := campaign_state.get_scenario_count_in_chapter(active_scenario_id) if position > 0 and count > 0: - return "戰場 제%s폭 · %s" % [_format_hanja_number(position), title] + return "전장 %s/%s · %s" % [position, count, title] return title @@ -5337,19 +5359,16 @@ func _format_briefing_objectives() -> String: var lines := [] var victory := str(state.objectives.get("victory", "")) if not victory.is_empty(): - _append_edict_clause(lines, "受命 勝令", "朱批", victory) + lines.append("승리 조건") + lines.append(" %s" % victory) var defeat := str(state.objectives.get("defeat", "")) if not defeat.is_empty(): - _append_edict_clause(lines, "戒懼 敗兆", "墨戒", defeat) - var progress_lines := state.get_objective_progress_lines(false, false) - if not progress_lines.is_empty(): - _append_edict_clause(lines, "驗功 戰功", "功簿", _join_strings(progress_lines, " · ")) - var risk_lines := state.get_defeat_progress_lines() - if not risk_lines.is_empty(): - _append_edict_clause(lines, "不祥 凶兆", "凶簡", _join_strings(risk_lines, " · ")) + lines.append("패배 조건") + lines.append(" %s" % defeat) var rewards_text := _format_briefing_rewards() if not rewards_text.is_empty(): - _append_edict_clause(lines, "封賞 戰利", "封賞", rewards_text) + lines.append("보상") + lines.append(" %s" % rewards_text) return _join_strings(lines, "\n") @@ -5398,8 +5417,8 @@ func _format_briefing_camp_overview_text(briefing: Dictionary, prep_locked: bool var lines := [] var location := str(briefing.get("location", "")).strip_edges() if not location.is_empty(): - lines.append("地勢|%s" % location) - var camp_parts := ["軍資 %d냥" % campaign_state.gold] + lines.append("위치: %s" % location) + var camp_parts := ["군자금 %d냥" % campaign_state.gold] var talk_text := _format_briefing_talk_overview_text() if not talk_text.is_empty(): camp_parts.append(talk_text) @@ -5407,7 +5426,7 @@ func _format_briefing_camp_overview_text(briefing: Dictionary, prep_locked: bool if not shop_text.is_empty(): camp_parts.append(shop_text) if not camp_parts.is_empty(): - lines.append("軍幕|%s" % _join_strings(camp_parts, " · ")) + lines.append("군막: %s" % _join_strings(camp_parts, " · ")) var prep_parts := [] var roster_text := _format_briefing_roster_overview_text() if not roster_text.is_empty(): @@ -5416,11 +5435,11 @@ func _format_briefing_camp_overview_text(briefing: Dictionary, prep_locked: bool if not formation_text.is_empty(): prep_parts.append(formation_text) if not state.get_controllable_player_units().is_empty(): - prep_parts.append("兵藏 비축") + prep_parts.append("비축 물자") if not prep_parts.is_empty(): - lines.append("軍備|%s" % _join_strings(prep_parts, " · ")) + lines.append("준비: %s" % _join_strings(prep_parts, " · ")) if prep_locked: - lines.append("封印|회상 두루마리 봉인") + lines.append("회상 전투") return _join_strings(lines, "\n") @@ -8808,6 +8827,8 @@ func _terrain_display_name(terrain_name: String) -> String: return "길" if normalized == "castle": return "성채" + if normalized == "town" or normalized == "village": + return "마을" return terrain_name diff --git a/tools/smoke_event_ai_priority.gd b/tools/smoke_event_ai_priority.gd index 6159fcb..3cc1151 100644 --- a/tools/smoke_event_ai_priority.gd +++ b/tools/smoke_event_ai_priority.gd @@ -46,7 +46,7 @@ func _check_priority_action(state, failures: Array[String]) -> void: failures.append("AI target priority did not update on event action") if not logs.has("하후돈에게 적 시선이 몰립니다."): failures.append("AI priority event log missing") - if not _feedback_has(feedback, "xiahou_dun", "標的", "priority"): + if not _feedback_has(feedback, "xiahou_dun", "표적", "priority"): failures.append("AI priority feedback missing") diff --git a/tools/smoke_objective_progress.gd b/tools/smoke_objective_progress.gd index 167c8f2..048b339 100644 --- a/tools/smoke_objective_progress.gd +++ b/tools/smoke_objective_progress.gd @@ -23,7 +23,7 @@ func _init() -> void: failures, "001 turn pressure risk", _defeat_text("res://data/scenarios/001_yellow_turbans.json"), - "잔여 군령 7" + "잔여 군령 15" ) _check_contains( failures, @@ -60,6 +60,7 @@ func _init() -> void: _check_turn_gated_objective_does_not_create_marker(failures) _check_objective_update(failures) _check_objective_notice(failures) + _check_terrain_recovery(failures) if failures.is_empty(): print("objective progress smoke ok") @@ -111,8 +112,8 @@ func _check_objective_update(failures: Array[String]) -> void: failures.append("objective update signal victory mismatch: %s" % str(signal_value.get("victory", ""))) if str(signal_value.get("defeat", "")) != "Cao Cao is defeated.": failures.append("objective update signal defeat mismatch: %s" % str(signal_value.get("defeat", ""))) - _check_log_contains(failures, logs, "軍令 개정: Defeat the new vanguard.") - _check_log_contains(failures, logs, "敗兆 개정: Cao Cao is defeated.") + _check_log_contains(failures, logs, "군령 갱신: Defeat the new vanguard.") + _check_log_contains(failures, logs, "패배 조건 갱신: Cao Cao is defeated.") signal_values.clear() logs.clear() @@ -125,7 +126,7 @@ func _check_objective_update(failures: Array[String]) -> void: failures.append("defeat-only objective signal should not include victory: %s" % str(defeat_signal_value.get("victory", ""))) if str(defeat_signal_value.get("defeat", "")) != "Supply train is lost.": failures.append("defeat-only objective signal mismatch: %s" % str(defeat_signal_value.get("defeat", ""))) - _check_log_contains(failures, logs, "敗兆 개정: Supply train is lost.") + _check_log_contains(failures, logs, "패배 조건 갱신: Supply train is lost.") func _check_event_gated_objective_markers(failures: Array[String]) -> void: @@ -205,26 +206,44 @@ func _check_log_contains(failures: Array[String], logs: Array, expected: String) failures.append("expected log `%s` in `%s`" % [expected, logs]) +func _check_terrain_recovery(failures: Array[String]) -> void: + var state = BattleStateScript.new() + if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): + failures.append("terrain recovery smoke could not load scenario") + return + var cao_cao: Dictionary = state.get_unit("cao_cao") + cao_cao["pos"] = Vector2i(6, 5) + cao_cao["hp"] = 10 + state._reset_team_actions(BattleStateScript.TEAM_PLAYER) + if int(cao_cao.get("hp", 0)) != 16: + failures.append("village terrain should recover 6 hp at phase start, got %d" % int(cao_cao.get("hp", 0))) + var boss: Dictionary = state.get_unit("yellow_turban_1") + boss["hp"] = 20 + state._reset_team_actions(BattleStateScript.TEAM_ENEMY) + if int(boss.get("hp", 0)) != 28: + failures.append("castle terrain should recover 8 hp at phase start, got %d" % int(boss.get("hp", 0))) + + func _check_objective_notice(failures: Array[String]) -> void: var scene = BattleSceneScript.new() scene._create_hud() scene._on_objective_updated("Defeat the new vanguard.", "") if scene.objective_notice_panel == null or not scene.objective_notice_panel.visible: failures.append("objective notice panel should be visible after objective update") - elif not scene.objective_notice_label.text.contains("急詔 軍令改封") or not scene.objective_notice_label.text.contains("朱批|Defeat the new vanguard."): - failures.append("objective notice label should use sealed edict wording: %s" % scene.objective_notice_label.text) + elif not scene.objective_notice_label.text.contains("군령 갱신") or not scene.objective_notice_label.text.contains("목표|Defeat the new vanguard."): + failures.append("objective notice label should use Korean order wording: %s" % scene.objective_notice_label.text) scene._on_objective_updated("", "Supply train is lost.") if scene.objective_notice_panel == null or not scene.objective_notice_panel.visible: failures.append("objective notice panel should be visible after defeat update") - elif not scene.objective_notice_label.text.contains("急詔 軍令改封") or not scene.objective_notice_label.text.contains("墨戒|Supply train is lost."): - failures.append("objective notice label should use omen wording: %s" % scene.objective_notice_label.text) - if scene._format_log_entry_text("Objective updated: Defeat the new vanguard.") != "軍令 개정: Defeat the new vanguard.": - failures.append("objective log display should use edict wording") - if scene._format_log_entry_text("Defeat condition updated: Supply train is lost.") != "敗兆 개정: Supply train is lost.": - failures.append("defeat log display should use omen wording") - if scene._format_log_entry_text("軍令 개정: Defeat the new vanguard.") != "軍令 개정: Defeat the new vanguard.": + elif not scene.objective_notice_label.text.contains("군령 갱신") or not scene.objective_notice_label.text.contains("주의|Supply train is lost."): + failures.append("objective notice label should use Korean risk wording: %s" % scene.objective_notice_label.text) + if scene._format_log_entry_text("Objective updated: Defeat the new vanguard.") != "군령 갱신: Defeat the new vanguard.": + failures.append("objective log display should use Korean order wording") + if scene._format_log_entry_text("Defeat condition updated: Supply train is lost.") != "패배 조건 갱신: Supply train is lost.": + failures.append("defeat log display should use Korean risk wording") + if scene._format_log_entry_text("군령 갱신: Defeat the new vanguard.") != "군령 갱신: Defeat the new vanguard.": failures.append("localized objective log should pass through") - if scene._format_log_entry_text("敗兆 개정: Supply train is lost.") != "敗兆 개정: Supply train is lost.": + if scene._format_log_entry_text("패배 조건 갱신: Supply train is lost.") != "패배 조건 갱신: Supply train is lost.": failures.append("localized defeat log should pass through") if scene.objective_notice_timer <= 0.0: failures.append("objective notice timer should be active") diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 3818bff..546abe7 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -2964,7 +2964,7 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: if not merchant_notice.contains("군막 상인") or not merchant_notice.contains("길가의 물자"): failures.append("shop merchant notice should include name and flavor lines") var camp_overview := scene._format_briefing_camp_overview_text(scene.state.get_briefing(), false) - for expected in ["地勢|영천, 중평 원년", "軍幕|", "軍備|", "軍資 0냥", "군막 회의 · 3건", "보급 대기 1건", "군상 물품 · 2종", "출진 명부 2명 중 2명", "진형 배치 4칸", "兵藏 비축"]: + for expected in ["위치: 영천, 중평 원년", "군막:", "준비:", "군자금 0냥", "군막 회의 · 3건", "보급 대기 1건", "군상 물품 · 2종", "출진 명부 2명 중 2명", "진형 배치 4칸", "비축 물자"]: if not camp_overview.contains(expected): failures.append("camp overview should include %s: %s" % [expected, camp_overview]) var bean: Dictionary = scene.state.get_item_def("bean") @@ -3052,11 +3052,11 @@ func _check_hud_focus_text(failures: Array[String]) -> void: failures.append("Enemy archer HUD focus should describe enemy control: %s" % archer_text) if not scene._unit_current_terrain_text(archer).contains("평원"): failures.append("Archer terrain text should reflect its current plain tile") - _check_unit_class_mark(failures, scene, "cao_cao", "tactic", "策") - _check_unit_class_mark(failures, scene, "xiahou_dun", "cavalry", "騎") - _check_unit_class_mark(failures, scene, "yellow_turban_2", "infantry", "步") - _check_unit_class_mark(failures, scene, "yellow_turban_3", "ranged", "弓") - _check_unit_class_mark(failures, scene, "yellow_turban_1", "heavy", "武") + _check_unit_class_mark(failures, scene, "cao_cao", "tactic", "책") + _check_unit_class_mark(failures, scene, "xiahou_dun", "cavalry", "기") + _check_unit_class_mark(failures, scene, "yellow_turban_2", "infantry", "보") + _check_unit_class_mark(failures, scene, "yellow_turban_3", "ranged", "궁") + _check_unit_class_mark(failures, scene, "yellow_turban_1", "heavy", "무") scene.free() @@ -3084,18 +3084,19 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: scene.free() return var briefing_text := scene._format_briefing_objectives() - for expected in ["受命 勝令", "戒懼 敗兆", "驗功 戰功", "不祥 凶兆", "封賞 戰利"]: + for expected in ["승리 조건", "패배 조건", "보상"]: if not briefing_text.contains(expected): - failures.append("briefing objective parchment should use ancient heading `%s`: %s" % [expected, briefing_text]) + failures.append("briefing objective parchment should use Korean heading `%s`: %s" % [expected, briefing_text]) + for clutter in ["진행", "위험"]: + if briefing_text.contains(clutter): + failures.append("briefing objective parchment should stay concise without `%s`: %s" % [clutter, briefing_text]) var hud_objective := scene._format_objective_hud_text() - if not hud_objective.contains("勝令|") or not hud_objective.contains("敗兆|"): - failures.append("HUD objective should read as paired edict and omen text: %s" % hud_objective) - if not hud_objective.contains("朱批") or not hud_objective.contains("墨戒"): - failures.append("HUD objective should use brush annotation wording: %s" % hud_objective) + if not hud_objective.contains("승리:") or not hud_objective.contains("패배:"): + failures.append("HUD objective should use concise Korean labels: %s" % hud_objective) var mission_text := scene._format_mission_panel_text() - for expected in ["勝令", "敗兆", "戰功", "凶兆", "令牌|", "戒牌|", "功簿|", "凶簡|"]: + for expected in ["승리 조건", "패배 조건", "진행", "위험", "목표|", "주의|", "현황|", "경고|"]: if not mission_text.contains(expected): - failures.append("mission panel should use old campaign wording `%s`: %s" % [expected, mission_text]) + failures.append("mission panel should use Korean campaign wording `%s`: %s" % [expected, mission_text]) _check_panel_style_fill(failures, scene.briefing_panel, "briefing panel", Color(0.54, 0.36, 0.14, 0.997)) _check_panel_style_frame(failures, scene.briefing_panel, "briefing panel", Color(0.035, 0.014, 0.008, 1.0), 16) _check_panel_style_fill(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.67, 0.49, 0.24, 0.998)) @@ -3136,10 +3137,10 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: _check_hanging_tassel(failures, scene.dialogue_left_tassel, "dialogue left tassel") _check_hanging_tassel(failures, scene.dialogue_right_tassel, "dialogue right tassel") _check_dialogue_column_budget(failures, scene) - if scene.dialogue_continue_button == null or scene.dialogue_continue_button.text != "次簡": - failures.append("dialogue continue button should use ancient slip text") - if scene.dialogue_previous_button == null or scene.dialogue_previous_button.text != "前簡": - failures.append("dialogue previous button should use ancient slip text") + if scene.dialogue_continue_button == null or scene.dialogue_continue_button.text != "다음": + failures.append("dialogue continue button should use Korean text") + if scene.dialogue_previous_button == null or scene.dialogue_previous_button.text != "이전": + failures.append("dialogue previous button should use Korean text") var localized_lines := scene._normalized_dialogue_lines([{"speaker": "Cao Cao", "display_speaker": "조조", "text": "군령을 전하라."}]) if localized_lines.is_empty() or str((localized_lines[0] as Dictionary).get("speaker", "")) != "조조": failures.append("dialogue normalization should preserve display_speaker for localized names") @@ -3147,10 +3148,10 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: scene.active_dialogue_lines = localized_lines scene.active_dialogue_index = 0 scene._render_dialogue_line() - if scene.dialogue_speaker_label == null or scene.dialogue_speaker_label.text != "조조 曰": + if scene.dialogue_speaker_label == null or scene.dialogue_speaker_label.text != "조조": failures.append("dialogue speaker label should render localized display_speaker") - if scene.dialogue_text_label == null or scene.dialogue_text_label.text != "墨書\n 「군령을 전하라.」": - failures.append("dialogue text should render as an old quotation scroll") + if scene.dialogue_text_label == null or scene.dialogue_text_label.text != "「군령을 전하라.」": + failures.append("dialogue text should render as a concise quotation scroll") scene._hide_dialogue_panel() var enemy_lines := scene._normalized_dialogue_lines([{"speaker": "Wen Chou", "text": "전진!"}]) if enemy_lines.is_empty() or str((enemy_lines[0] as Dictionary).get("speaker", "")) != "문추": @@ -3178,8 +3179,10 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: failures.append("dialogue text should use the project serif font override") if scene.dialogue_continue_button == null or not scene.dialogue_continue_button.has_theme_font_override("font"): failures.append("dialogue continue button should use the project serif font override") - if scene.mission_title_label == null or scene.mission_title_label.text != "軍令 목패": - failures.append("mission panel title should use bronze tablet wording") + if scene.mission_title_label == null or scene.mission_title_label.text != "전투 목표": + failures.append("mission panel title should use clear Korean wording") + if scene.briefing_start_button == null or scene.briefing_start_button.text != "닫고 전투 시작": + failures.append("briefing start button should clearly close the briefing") if scene.shop_button == null or scene.shop_button.text != "군상": failures.append("briefing shop button should use sutler wording") if scene.talk_button == null or scene.talk_button.text != "군막 회의": @@ -3209,8 +3212,8 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: if scene.targeting_hint_back_button == null or scene.targeting_hint_back_button.text != "죽간 거두기": failures.append("targeting hint back button should use bamboo-slip wording") scene._on_objective_updated("동문에 조서를 꽂아라.", "조조가 퇴각하면 패전이다.") - if scene.objective_notice_label == null or not scene.objective_notice_label.text.contains("朱批") or not scene.objective_notice_label.text.contains("墨戒"): - failures.append("objective update notice should use brush annotation wording") + if scene.objective_notice_label == null or not scene.objective_notice_label.text.contains("목표|") or not scene.objective_notice_label.text.contains("주의|"): + failures.append("objective update notice should use Korean annotation wording") scene._update_briefing_camp_overview(scene.state.get_briefing(), false) if scene.briefing_camp_overview_fallback_label == null or scene.briefing_camp_overview_fallback_label.text != "비단 전장도": failures.append("briefing map fallback should read as an old campaign map") @@ -3218,10 +3221,10 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: _check_panel_style_fill(failures, scene.result_panel, "victory result panel", Color(0.54, 0.36, 0.15, 0.997)) scene._apply_panel_style(scene.result_panel, "result_defeat_tablet") _check_panel_style_fill(failures, scene.result_panel, "defeat result panel", Color(0.13, 0.032, 0.014, 0.997)) - if not scene._format_victory_result_text().contains("凱旋 勝牘"): - failures.append("victory result should read like a bronze proclamation") - if not scene._format_defeat_result_text().contains("敗軍 碑牘"): - failures.append("defeat result should read like a memorial tablet") + if not scene._format_victory_result_text().contains("승리"): + failures.append("victory result should use Korean title") + if not scene._format_defeat_result_text().contains("패배"): + failures.append("defeat result should use Korean title") var scroll_rod := scene._make_scroll_rod(120, 4) if scroll_rod.get_child_count() != 5: failures.append("scroll rod ornament should use five visual strips") @@ -3469,6 +3472,8 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: scene.free() return scene.battle_started = true + var enemy: Dictionary = scene.state.get_unit("yellow_turban_1") + enemy["pos"] = Vector2i(1, 2) scene.state.select_unit("cao_cao") scene._refresh_ranges() @@ -3482,7 +3487,7 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: if str(select_badge.get("text", "")) != "선택" or str(select_badge.get("kind", "")) != "select": failures.append("friendly selectable unit should show SELECT badge: %s" % str(select_badge)) - scene.hover_cell = Vector2i(7, 2) + scene.hover_cell = Vector2i(1, 2) var attack_badge := scene._target_preview_badge() if attack_badge.is_empty() or str(attack_badge.get("kind", "")) == "move" or str(attack_badge.get("text", "")) == "행군": failures.append("enemy hover should keep attack badge priority: %s" % str(attack_badge)) @@ -3511,8 +3516,14 @@ func _check_terrain_and_unit_presentation(failures: Array[String]) -> void: return if scene._terrain_key_at(Vector2i(3, 1)) != "F": failures.append("terrain helper should read forest cells") + if scene._terrain_key_at(Vector2i(6, 5)) != "T": + failures.append("terrain helper should read village cells") if not scene._terrain_key_at(Vector2i(-1, 0)).is_empty(): failures.append("terrain helper should return empty outside the board") + if scene.state.get_terrain_name(Vector2i(6, 5)) != "마을" or scene.state.get_terrain_heal(Vector2i(6, 5)) != 6: + failures.append("village terrain should expose Korean name and healing") + if scene.state.get_terrain_heal(Vector2i(12, 1)) != 8: + failures.append("castle terrain should expose stronger healing") var background_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "G", true) var fallback_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "G", false) if background_fill.a >= 0.20: @@ -3531,6 +3542,8 @@ func _check_terrain_and_unit_presentation(failures: Array[String]) -> void: failures.append("matching terrain should not draw edge blends") if scene._terrain_edge_width("W", "G") <= scene._terrain_edge_width("G", "F"): failures.append("water shoreline edge should be wider than grass blend") + if scene._terrain_edge_priority("T") <= scene._terrain_edge_priority("F"): + failures.append("village should draw above forest/grass terrain blends") var wasteland_fill: Color = scene._terrain_fill_color(Vector2i(0, 0), "D", true) if wasteland_fill.a <= background_fill.a: failures.append("wasteland fill should sit slightly above grass over background art") @@ -3584,9 +3597,9 @@ func _check_attack_motion_profiles(failures: Array[String]) -> void: return _check_attack_profile(failures, scene, "cao_cao", Vector2i(1, 3), Vector2i(2, 3), "command_strike") _check_attack_profile(failures, scene, "xiahou_dun", Vector2i(1, 4), Vector2i(2, 4), "cavalry_charge") - _check_attack_profile(failures, scene, "yellow_turban_1", Vector2i(7, 2), Vector2i(6, 2), "heavy_strike") - _check_attack_profile(failures, scene, "yellow_turban_2", Vector2i(8, 5), Vector2i(7, 5), "infantry_strike") - _check_attack_profile(failures, scene, "yellow_turban_3", Vector2i(7, 6), Vector2i(7, 4), "arrow") + _check_attack_profile(failures, scene, "yellow_turban_1", Vector2i(12, 1), Vector2i(11, 1), "heavy_strike") + _check_attack_profile(failures, scene, "yellow_turban_2", Vector2i(9, 4), Vector2i(8, 4), "infantry_strike") + _check_attack_profile(failures, scene, "yellow_turban_3", Vector2i(11, 2), Vector2i(11, 0), "arrow") _check_synthetic_attack_profile(failures, scene, "elite_cavalry", "cavalry_charge") _check_synthetic_attack_profile(failures, scene, "guard_captain", "infantry_strike") _check_synthetic_attack_profile(failures, scene, "marksman", "arrow", 2) @@ -3599,7 +3612,7 @@ func _check_attack_motion_profiles(failures: Array[String]) -> void: _check_skill_action_profile(failures, scene, "skill_support", "tactic_support", "order_banner") scene._on_unit_action_motion_requested("xiahou_dun", Vector2i(1, 4), Vector2i(2, 4), "attack") _check_stored_attack_motion(failures, scene, "xiahou_dun", "cavalry_charge", "melee", "attack", "charge_dust") - scene._on_unit_action_motion_requested("yellow_turban_3", Vector2i(7, 6), Vector2i(7, 4), "attack") + scene._on_unit_action_motion_requested("yellow_turban_3", Vector2i(11, 2), Vector2i(11, 0), "attack") _check_stored_attack_motion(failures, scene, "yellow_turban_3", "arrow", "ranged", "attack", "piercing_trace") scene._on_unit_action_motion_requested("cao_cao", Vector2i(1, 3), Vector2i(3, 3), "skill_damage") _check_stored_attack_motion(failures, scene, "cao_cao", "tactic_damage", "tactic", "skill_damage", "tactic_flare") diff --git a/tools/validate_data.ps1 b/tools/validate_data.ps1 index 1a0b147..c00dbd0 100644 --- a/tools/validate_data.ps1 +++ b/tools/validate_data.ps1 @@ -1000,6 +1000,10 @@ function Check-Terrain-Definitions() { if ($avoid -lt 0 -or $avoid -gt 100) { Fail "Terrain $terrainKey avoid must be between 0 and 100." } + $heal = [int](Get-Prop $terrainDef "heal" 0) + if ($heal -lt 0 -or $heal -gt 99) { + Fail "Terrain $terrainKey heal must be between 0 and 99." + } } }