Improve opening battle pacing and objectives UI

This commit is contained in:
2026-06-19 13:32:05 +09:00
parent b9fb2dd79e
commit f69438e964
4 changed files with 352 additions and 56 deletions

View File

@@ -2,14 +2,14 @@
"id": "001_yellow_turbans", "id": "001_yellow_turbans",
"name": "영천 소전", "name": "영천 소전",
"objectives": { "objectives": {
"victory": "황건적 본대를 격파하고 성채를 장악하라.", "victory": "성채를 장악하고 마지막 황건대까지 격파하라.",
"defeat": "조조가 퇴각하거나 제16턴이 시작되면 패한다." "defeat": "조조가 퇴각하거나 제18턴이 시작되면 패한다."
}, },
"conditions": { "conditions": {
"victory": { "victory": {
"type": "all", "type": "all",
"conditions": [ "conditions": [
{ "type": "all_units_defeated", "team": "enemy" }, { "type": "all_units_defeated", "team": "enemy", "after_event": "turn_9_western_reserve" },
{ {
"type": "unit_reaches_tile", "type": "unit_reaches_tile",
"label": "성채 장악", "label": "성채 장악",
@@ -21,7 +21,7 @@
}, },
"defeat": [ "defeat": [
{ "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] }, { "type": "any_officer_defeated", "team": "player", "officer_ids": ["cao_cao"] },
{ "type": "turn_reached", "turn": 16, "team": "player" } { "type": "turn_reached", "turn": 18, "team": "player" }
] ]
}, },
"briefing": { "briefing": {
@@ -30,7 +30,7 @@
"lines": [ "lines": [
"황건의 무리가 영천 근방에 깃발을 세웠다.", "황건의 무리가 영천 근방에 깃발을 세웠다.",
"조조와 하후돈은 난세의 불길이 번지기 전에 그 진형을 꺾어야 한다.", "조조와 하후돈은 난세의 불길이 번지기 전에 그 진형을 꺾어야 한다.",
"마을과 성채를 점거한 부대는 차례가 시작될 때 병력을 회복한다." "적은 마을과 성채에서 병력을 고치며, 늦게 닿는 원군까지 기다리고 있다."
], ],
"camp_dialogue": [ "camp_dialogue": [
{ "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "영천은 마른 풀밭의 불씨와 같다. 결단이 빠르면 조정도 아직 질서가 돌아올 수 있음을 믿을 것이다." }, { "speaker": "Cao Cao", "display_speaker": "조조", "side": "left", "text": "영천은 마른 풀밭의 불씨와 같다. 결단이 빠르면 조정도 아직 질서가 돌아올 수 있음을 믿을 것이다." },
@@ -196,6 +196,42 @@
"level": 1, "level": 1,
"pos": [10, 6], "pos": [10, 6],
"base": { "hp": 32, "atk": 11, "def": 5 } "base": { "hp": 32, "atk": 11, "def": 5 }
},
{
"unit_id": "yellow_turban_9",
"name": "황건 보병",
"class_id": "infantry",
"team": "enemy",
"level": 1,
"pos": [5, 4],
"base": { "hp": 34, "atk": 9, "def": 6 }
},
{
"unit_id": "yellow_turban_10",
"name": "황건 궁수",
"class_id": "archer",
"team": "enemy",
"level": 1,
"pos": [8, 2],
"base": { "hp": 26, "atk": 9, "def": 4 }
},
{
"unit_id": "yellow_turban_11",
"name": "황건 유격병",
"class_id": "bandit",
"team": "enemy",
"level": 1,
"pos": [6, 6],
"base": { "hp": 32, "atk": 11, "def": 5 }
},
{
"unit_id": "yellow_turban_12",
"name": "북숲 초병",
"class_id": "infantry",
"team": "enemy",
"level": 1,
"pos": [4, 2],
"base": { "hp": 34, "atk": 9, "def": 6 }
} }
], ],
"events": [ "events": [
@@ -312,6 +348,52 @@
"text": "남쪽 약탈대가 조조의 본진을 노린다." "text": "남쪽 약탈대가 조조의 본진을 노린다."
} }
] ]
},
{
"id": "turn_9_western_reserve",
"once": true,
"when": { "type": "turn_start", "team": "enemy", "turn": 9 },
"actions": [
{ "type": "log", "text": "서쪽 숲길에서 마지막 황건대가 전장에 닿았다." },
{
"type": "spawn_deployments",
"deployments": [
{
"unit_id": "yellow_turban_reinforcement_5",
"name": "황건 보병",
"class_id": "infantry",
"team": "enemy",
"level": 1,
"pos": [5, 8],
"base": { "hp": 34, "atk": 10, "def": 6 }
},
{
"unit_id": "yellow_turban_reinforcement_6",
"name": "황건 궁수",
"class_id": "archer",
"team": "enemy",
"level": 1,
"pos": [6, 8],
"base": { "hp": 26, "atk": 9, "def": 4 }
},
{
"unit_id": "yellow_turban_reinforcement_7",
"name": "황건 유격병",
"class_id": "bandit",
"team": "enemy",
"level": 1,
"pos": [3, 6],
"base": { "hp": 32, "atk": 11, "def": 5 }
}
]
},
{
"type": "set_ai_target_priority",
"unit_id": "cao_cao",
"priority": 12,
"text": "서쪽 황건대가 조조의 깃발을 노린다."
}
]
} }
], ],
"rewards": { "rewards": {

View File

@@ -48,7 +48,7 @@ const FLOATING_TEXT_LIFETIME := 1.0
const FLOATING_TEXT_RISE := 42.0 const FLOATING_TEXT_RISE := 42.0
const OBJECTIVE_NOTICE_DURATION := 2.6 const OBJECTIVE_NOTICE_DURATION := 2.6
const UNIT_MOVE_ANIMATION_DURATION := 0.18 const UNIT_MOVE_ANIMATION_DURATION := 0.18
const TARGET_PREVIEW_BADGE_SIZE := Vector2(92, 22) const TARGET_PREVIEW_BADGE_SIZE := Vector2(104, 22)
const LOW_HP_WARNING_RATIO := 0.35 const LOW_HP_WARNING_RATIO := 0.35
const UNIT_STATUS_MARKER_RADIUS := 6.0 const UNIT_STATUS_MARKER_RADIUS := 6.0
const UNIT_STATUS_MARKER_GAP := 14.0 const UNIT_STATUS_MARKER_GAP := 14.0
@@ -204,6 +204,8 @@ var pending_move_unit_id := ""
var pending_move_from_cell := Vector2i(-1, -1) var pending_move_from_cell := Vector2i(-1, -1)
var pending_move_to_cell := Vector2i(-1, -1) var pending_move_to_cell := Vector2i(-1, -1)
var show_threat_overlay := false var show_threat_overlay := false
var mission_detail_collapsed := false
var briefing_objective_collapsed := false
var active_scenario_id := "" var active_scenario_id := ""
var battle_started := false var battle_started := false
var battle_result_applied := false var battle_result_applied := false
@@ -215,6 +217,7 @@ var status_label: Label
var objective_label: Label var objective_label: Label
var campaign_status_label: Label var campaign_status_label: Label
var mission_title_label: Label var mission_title_label: Label
var mission_toggle_button: Button
var mission_detail_panel: PanelContainer var mission_detail_panel: PanelContainer
var mission_detail_label: Label var mission_detail_label: Label
var objective_notice_panel: PanelContainer var objective_notice_panel: PanelContainer
@@ -234,6 +237,7 @@ var briefing_location_label: Label
var briefing_seal_ribbon: HBoxContainer var briefing_seal_ribbon: HBoxContainer
var briefing_objective_panel: PanelContainer var briefing_objective_panel: PanelContainer
var briefing_objective_label: Label var briefing_objective_label: Label
var briefing_objective_toggle_button: Button
var briefing_camp_overview_panel: PanelContainer var briefing_camp_overview_panel: PanelContainer
var briefing_camp_overview_row: HBoxContainer var briefing_camp_overview_row: HBoxContainer
var briefing_camp_overview_texture: TextureRect var briefing_camp_overview_texture: TextureRect
@@ -878,10 +882,10 @@ func _make_map_lattice_overlay(size: Vector2) -> Control:
overlay.add_child(horizontal) overlay.add_child(horizontal)
var corner_specs := [ var corner_specs := [
{"text": "", "pos": Vector2(4, 4)}, {"text": "", "pos": Vector2(4, 4)},
{"text": "", "pos": Vector2(size.x - 24, 4)}, {"text": "", "pos": Vector2(size.x - 24, 4)},
{"text": "", "pos": Vector2(4, size.y - 24)}, {"text": "", "pos": Vector2(4, size.y - 24)},
{"text": "", "pos": Vector2(size.x - 24, size.y - 24)} {"text": "", "pos": Vector2(size.x - 24, size.y - 24)}
] ]
for spec in corner_specs: for spec in corner_specs:
var seal := _make_seal_tile(str(spec["text"]), 20) var seal := _make_seal_tile(str(spec["text"]), 20)
@@ -1161,11 +1165,24 @@ func _create_hud() -> void:
_apply_label_style(selected_label, UI_PARCHMENT_TEXT) _apply_label_style(selected_label, UI_PARCHMENT_TEXT)
selected_row.add_child(selected_label) selected_row.add_child(selected_label)
var mission_title_row := HBoxContainer.new()
mission_title_row.add_theme_constant_override("separation", 8)
side_column.add_child(mission_title_row)
mission_title_label = Label.new() mission_title_label = Label.new()
mission_title_label.text = "전투 목표" mission_title_label.text = "전투 목표"
mission_title_label.custom_minimum_size = Vector2(296, 28)
mission_title_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
mission_title_label.add_theme_font_size_override("font_size", 14) mission_title_label.add_theme_font_size_override("font_size", 14)
_apply_label_style(mission_title_label, UI_OLD_BRONZE) _apply_label_style(mission_title_label, UI_OLD_BRONZE)
side_column.add_child(mission_title_label) mission_title_row.add_child(mission_title_label)
mission_toggle_button = Button.new()
mission_toggle_button.text = "접기"
mission_toggle_button.tooltip_text = "전투 목표 판을 접거나 펼칩니다."
mission_toggle_button.custom_minimum_size = Vector2(78, 28)
mission_toggle_button.pressed.connect(_on_mission_toggle_pressed)
mission_title_row.add_child(mission_toggle_button)
mission_detail_panel = PanelContainer.new() mission_detail_panel = PanelContainer.new()
mission_detail_panel.custom_minimum_size = Vector2(420, 134) mission_detail_panel.custom_minimum_size = Vector2(420, 134)
@@ -1178,7 +1195,7 @@ func _create_hud() -> void:
mission_detail_panel.add_child(mission_detail_row) mission_detail_panel.add_child(mission_detail_row)
mission_detail_row.add_child(_make_bamboo_gutter(12, 112)) 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 = Label.new()
mission_detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART mission_detail_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
@@ -1186,7 +1203,7 @@ func _create_hud() -> void:
mission_detail_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL mission_detail_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_apply_label_style(mission_detail_label, UI_PARCHMENT_TEXT) _apply_label_style(mission_detail_label, UI_PARCHMENT_TEXT)
mission_detail_row.add_child(mission_detail_label) 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)) mission_detail_row.add_child(_make_bamboo_gutter(12, 112))
cell_info_label = Label.new() cell_info_label = Label.new()
@@ -1459,7 +1476,7 @@ func _create_hud() -> void:
briefing_shell.add_child(briefing_column) briefing_shell.add_child(briefing_column)
briefing_shell.add_child(_make_bamboo_gutter(22, 632)) briefing_shell.add_child(_make_bamboo_gutter(22, 632))
briefing_column.add_child(_make_scroll_rod(680, 6)) 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)) briefing_column.add_child(_make_tablet_binding(680, 16))
var briefing_title_panel := PanelContainer.new() var briefing_title_panel := PanelContainer.new()
@@ -1486,12 +1503,23 @@ func _create_hud() -> void:
briefing_column.add_child(briefing_location_label) briefing_column.add_child(briefing_location_label)
briefing_start_button = Button.new() briefing_start_button = Button.new()
briefing_start_button.text = "닫고 전투 시작" briefing_start_button.text = "전투 시작"
briefing_start_button.tooltip_text = "브리핑을 닫고 전투를 시작합니다."
briefing_start_button.pressed.connect(_on_begin_battle_pressed) briefing_start_button.pressed.connect(_on_begin_battle_pressed)
briefing_column.add_child(briefing_start_button) briefing_column.add_child(briefing_start_button)
briefing_column.add_child(_make_scroll_rod(680, 5)) briefing_column.add_child(_make_scroll_rod(680, 5))
briefing_column.add_child(_make_section_caption("승리 조건과 패배 조건", 680)) var briefing_objective_header := HBoxContainer.new()
briefing_objective_header.add_theme_constant_override("separation", 8)
briefing_column.add_child(briefing_objective_header)
briefing_objective_header.add_child(_make_section_caption("승리와 패배", 548))
briefing_objective_toggle_button = Button.new()
briefing_objective_toggle_button.text = "조건 접기"
briefing_objective_toggle_button.tooltip_text = "승리와 패배 조건을 접거나 펼칩니다."
briefing_objective_toggle_button.custom_minimum_size = Vector2(124, 28)
briefing_objective_toggle_button.pressed.connect(_on_briefing_objective_toggle_pressed)
briefing_objective_header.add_child(briefing_objective_toggle_button)
briefing_objective_panel = PanelContainer.new() briefing_objective_panel = PanelContainer.new()
briefing_objective_panel.custom_minimum_size = Vector2(680, 104) briefing_objective_panel.custom_minimum_size = Vector2(680, 104)
@@ -1503,7 +1531,7 @@ func _create_hud() -> void:
briefing_objective_panel.add_child(briefing_objective_row) briefing_objective_panel.add_child(briefing_objective_row)
briefing_objective_row.add_child(_make_bamboo_gutter(18, 84)) 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 = Label.new()
briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART briefing_objective_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
@@ -1511,7 +1539,7 @@ func _create_hud() -> void:
briefing_objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL briefing_objective_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_apply_label_style(briefing_objective_label, UI_AGED_INK, 15) _apply_label_style(briefing_objective_label, UI_AGED_INK, 15)
briefing_objective_row.add_child(briefing_objective_label) 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_objective_row.add_child(_make_bamboo_gutter(18, 84))
briefing_column.add_child(_make_ink_wash_rule(680, 8)) briefing_column.add_child(_make_ink_wash_rule(680, 8))
@@ -1525,7 +1553,7 @@ func _create_hud() -> void:
var briefing_camp_overview_shell := HBoxContainer.new() var briefing_camp_overview_shell := HBoxContainer.new()
briefing_camp_overview_shell.add_theme_constant_override("separation", 10) briefing_camp_overview_shell.add_theme_constant_override("separation", 10)
briefing_camp_overview_panel.add_child(briefing_camp_overview_shell) 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 = HBoxContainer.new()
briefing_camp_overview_row.custom_minimum_size = Vector2(580, 92) briefing_camp_overview_row.custom_minimum_size = Vector2(580, 92)
@@ -1579,7 +1607,7 @@ func _create_hud() -> void:
briefing_scroll.add_child(briefing_label) briefing_scroll.add_child(briefing_label)
briefing_column.add_child(_make_tablet_binding(680, 14)) 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() var prep_button_row := HBoxContainer.new()
prep_button_row.add_theme_constant_override("separation", 8) prep_button_row.add_theme_constant_override("separation", 8)
@@ -1827,7 +1855,7 @@ func _create_hud() -> void:
dialogue_row.add_child(dialogue_right_tassel) dialogue_row.add_child(dialogue_right_tassel)
dialogue_column.add_child(_make_tablet_binding(DIALOGUE_TEXT_SIZE.x, 13)) 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_seal_ribbon = _make_seal_ribbon(DIALOGUE_TEXT_SIZE.x, 18)
dialogue_column.add_child(dialogue_seal_ribbon) dialogue_column.add_child(dialogue_seal_ribbon)
@@ -1840,7 +1868,7 @@ func _create_hud() -> void:
var dialogue_speaker_row := HBoxContainer.new() var dialogue_speaker_row := HBoxContainer.new()
dialogue_speaker_row.add_theme_constant_override("separation", 8) dialogue_speaker_row.add_theme_constant_override("separation", 8)
dialogue_speaker_panel.add_child(dialogue_speaker_row) 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 = Label.new()
dialogue_speaker_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 88, 24) dialogue_speaker_label.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x - 88, 24)
@@ -1848,7 +1876,7 @@ func _create_hud() -> void:
dialogue_speaker_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER dialogue_speaker_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_apply_label_style(dialogue_speaker_label, UI_PARCHMENT_TEXT, 18) _apply_label_style(dialogue_speaker_label, UI_PARCHMENT_TEXT, 18)
dialogue_speaker_row.add_child(dialogue_speaker_label) 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 = PanelContainer.new()
dialogue_text_panel.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 104) dialogue_text_panel.custom_minimum_size = Vector2(DIALOGUE_TEXT_SIZE.x, 104)
@@ -1860,7 +1888,7 @@ func _create_hud() -> void:
dialogue_text_panel.add_child(dialogue_text_row) 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_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 = Label.new()
dialogue_text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART dialogue_text_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
@@ -1869,7 +1897,7 @@ func _create_hud() -> void:
dialogue_text_label.size_flags_vertical = Control.SIZE_EXPAND_FILL dialogue_text_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
_apply_label_style(dialogue_text_label, UI_AGED_INK, 17) _apply_label_style(dialogue_text_label, UI_AGED_INK, 17)
dialogue_text_row.add_child(dialogue_text_label) 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_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)) dialogue_column.add_child(_make_scroll_rod(DIALOGUE_TEXT_SIZE.x, 3))
@@ -1925,7 +1953,7 @@ func _create_hud() -> void:
var result_body_row := HBoxContainer.new() var result_body_row := HBoxContainer.new()
result_body_row.add_theme_constant_override("separation", 8) result_body_row.add_theme_constant_override("separation", 8)
result_body_panel.add_child(result_body_row) 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 = Label.new()
result_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT result_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
@@ -1936,7 +1964,7 @@ func _create_hud() -> void:
result_label.size_flags_vertical = Control.SIZE_EXPAND_FILL result_label.size_flags_vertical = Control.SIZE_EXPAND_FILL
_apply_label_style(result_label, UI_AGED_INK, 16) _apply_label_style(result_label, UI_AGED_INK, 16)
result_body_row.add_child(result_label) 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)) result_column.add_child(_make_scroll_rod(520, 3))
@@ -1961,6 +1989,7 @@ func _create_hud() -> void:
tactic_button, tactic_button,
item_button, item_button,
equip_button, equip_button,
mission_toggle_button,
threat_button, threat_button,
end_turn_button, end_turn_button,
post_move_attack_button, post_move_attack_button,
@@ -1980,6 +2009,7 @@ func _create_hud() -> void:
formation_button, formation_button,
save_button, save_button,
briefing_start_button, briefing_start_button,
briefing_objective_toggle_button,
shop_buy_button, shop_buy_button,
shop_sell_button, shop_sell_button,
manual_save_button, manual_save_button,
@@ -2358,6 +2388,9 @@ func _handle_key(event: InputEventKey) -> void:
elif save_menu != null and save_menu.visible: elif save_menu != null and save_menu.visible:
_hide_save_menu() _hide_save_menu()
elif briefing_panel != null and briefing_panel.visible: elif briefing_panel != null and briefing_panel.visible:
if _set_briefing_objective_collapsed(true):
_play_ui_cancel()
return
_on_begin_battle_pressed() _on_begin_battle_pressed()
elif post_move_picker_panel != null and post_move_picker_panel.visible: elif post_move_picker_panel != null and post_move_picker_panel.visible:
_on_post_move_picker_back_pressed() _on_post_move_picker_back_pressed()
@@ -3041,7 +3074,7 @@ func _attack_target_marker_entries() -> Array[Dictionary]:
"cell": cell, "cell": cell,
"target_id": str(target.get("id", "")), "target_id": str(target.get("id", "")),
"text": "%s %d%%" % [result_text, hit_chance], "text": "%s %d%%" % [result_text, hit_chance],
"kind": "damage" "kind": _attack_preview_badge_kind(preview)
}) })
return result return result
@@ -3718,7 +3751,24 @@ func _attack_target_preview_badge(selected: Dictionary, target: Dictionary) -> D
var hit_chance := int(preview.get("hit_chance", 100)) var hit_chance := int(preview.get("hit_chance", 100))
var result_text := "퇴각" if bool(preview.get("would_defeat", false)) else "-%d" % int(preview.get("damage", 0)) var result_text := "퇴각" if bool(preview.get("would_defeat", false)) else "-%d" % int(preview.get("damage", 0))
return _make_target_preview_badge("%s %d%%" % [result_text, hit_chance], "damage") var counter_text := _format_attack_counter_badge_suffix(preview)
return _make_target_preview_badge("%s %d%%%s" % [result_text, hit_chance, counter_text], _attack_preview_badge_kind(preview))
func _format_attack_counter_badge_suffix(preview: Dictionary) -> String:
if not bool(preview.get("counter_in_range", false)):
return ""
if bool(preview.get("counter_would_defeat", false)):
return " 반격!"
return "%d" % int(preview.get("counter_damage", 0))
func _attack_preview_badge_kind(preview: Dictionary) -> String:
if bool(preview.get("counter_would_defeat", false)):
return "danger"
if bool(preview.get("counter_in_range", false)):
return "counter"
return "damage"
func _skill_target_preview_badge(selected: Dictionary, target: Dictionary) -> Dictionary: func _skill_target_preview_badge(selected: Dictionary, target: Dictionary) -> Dictionary:
@@ -3882,6 +3932,10 @@ func _support_preview_badge_kind(effect_text: String) -> String:
func _target_preview_badge_color(kind: String) -> Color: func _target_preview_badge_color(kind: String) -> Color:
if kind == "damage": if kind == "damage":
return Color(1.0, 0.38, 0.26) return Color(1.0, 0.38, 0.26)
if kind == "counter":
return Color(1.0, 0.64, 0.22)
if kind == "danger":
return Color(1.0, 0.18, 0.14)
if kind == "move": if kind == "move":
return Color(0.38, 0.70, 1.0) return Color(0.38, 0.70, 1.0)
if kind == "select": if kind == "select":
@@ -4015,7 +4069,7 @@ func _format_objective_hud_text() -> String:
if not defeat.is_empty(): if not defeat.is_empty():
lines.append("패배: %s" % defeat) lines.append("패배: %s" % defeat)
if lines.is_empty(): if lines.is_empty():
return "승리 조건 대기" return "목표 대기"
return _join_strings(lines, "\n") return _join_strings(lines, "\n")
@@ -4206,7 +4260,11 @@ func _unit_current_terrain_text(unit: Dictionary) -> String:
func _update_mission_panel() -> void: func _update_mission_panel() -> void:
if mission_detail_label == null: if mission_detail_panel != null:
mission_detail_panel.visible = not mission_detail_collapsed
if mission_toggle_button != null:
mission_toggle_button.text = "보기" if mission_detail_collapsed else "접기"
if mission_detail_label == null or mission_detail_collapsed:
return return
mission_detail_label.text = _format_mission_panel_text() mission_detail_label.text = _format_mission_panel_text()
@@ -4215,18 +4273,18 @@ func _format_mission_panel_text() -> String:
var lines := [] var lines := []
var victory := str(state.objectives.get("victory", "")) var victory := str(state.objectives.get("victory", ""))
if not victory.is_empty(): if not victory.is_empty():
_append_edict_clause(lines, "승리 조건", "목표", victory) lines.append("승리: %s" % victory)
var defeat := str(state.objectives.get("defeat", "")) var defeat := str(state.objectives.get("defeat", ""))
if not defeat.is_empty(): if not defeat.is_empty():
_append_edict_clause(lines, "패배 조건", "주의", defeat) lines.append("패배: %s" % defeat)
var progress_lines := state.get_objective_progress_lines(false, false) var progress_lines := state.get_objective_progress_lines(false, false)
if not progress_lines.is_empty(): if not progress_lines.is_empty():
_append_edict_clause(lines, "진행", "현황", _join_strings(progress_lines, " · ")) lines.append("진행: %s" % _join_strings(progress_lines, " · "))
var risk_lines := state.get_defeat_progress_lines() var risk_lines := state.get_defeat_progress_lines()
if not risk_lines.is_empty(): if not risk_lines.is_empty():
_append_edict_clause(lines, "위험", "경고", _join_strings(risk_lines, " · ")) lines.append("주의: %s" % _join_strings(risk_lines, " · "))
if lines.is_empty(): if lines.is_empty():
return "군령\n 대기|아직 열린 군령이 없습니다." return "열린 목표 없음"
return _join_strings(lines, "\n") return _join_strings(lines, "\n")
@@ -4699,9 +4757,9 @@ func _format_hanja_number(value: int) -> String:
func _on_objective_updated(victory: String, defeat: String) -> void: func _on_objective_updated(victory: String, defeat: String) -> void:
var notice_lines := ["군령 갱신"] var notice_lines := ["군령 갱신"]
if not victory.is_empty(): if not victory.is_empty():
_append_edict_clause(notice_lines, "승리 조건", "목표", victory) notice_lines.append("목표: %s" % victory)
if not defeat.is_empty(): if not defeat.is_empty():
_append_edict_clause(notice_lines, "패배 조건", "주의", defeat) notice_lines.append("주의: %s" % defeat)
if objective_notice_label != null: if objective_notice_label != null:
objective_notice_label.text = "\n".join(notice_lines) objective_notice_label.text = "\n".join(notice_lines)
if objective_notice_panel != null: if objective_notice_panel != null:
@@ -5323,6 +5381,12 @@ func _on_threat_pressed() -> void:
queue_redraw() queue_redraw()
func _on_mission_toggle_pressed() -> void:
_play_ui_click()
mission_detail_collapsed = not mission_detail_collapsed
_update_mission_panel()
func _on_restart_pressed() -> void: func _on_restart_pressed() -> void:
if campaign_complete_screen: if campaign_complete_screen:
return return
@@ -5465,16 +5529,13 @@ func _format_briefing_objectives() -> String:
var lines := [] var lines := []
var victory := str(state.objectives.get("victory", "")) var victory := str(state.objectives.get("victory", ""))
if not victory.is_empty(): if not victory.is_empty():
lines.append("승리 조건") lines.append("승리: %s" % victory)
lines.append(" %s" % victory)
var defeat := str(state.objectives.get("defeat", "")) var defeat := str(state.objectives.get("defeat", ""))
if not defeat.is_empty(): if not defeat.is_empty():
lines.append("패배 조건") lines.append("패배: %s" % defeat)
lines.append(" %s" % defeat)
var rewards_text := _format_briefing_rewards() var rewards_text := _format_briefing_rewards()
if not rewards_text.is_empty(): if not rewards_text.is_empty():
lines.append("보상") lines.append("보상: %s" % rewards_text)
lines.append(" %s" % rewards_text)
return _join_strings(lines, "\n") return _join_strings(lines, "\n")
@@ -5682,8 +5743,10 @@ func _show_briefing() -> void:
briefing_title_label.text = _format_briefing_title(briefing) briefing_title_label.text = _format_briefing_title(briefing)
briefing_location_label.text = _format_briefing_location(briefing) briefing_location_label.text = _format_briefing_location(briefing)
briefing_location_label.visible = not briefing_location_label.text.is_empty() briefing_location_label.visible = not briefing_location_label.text.is_empty()
briefing_objective_collapsed = false
briefing_objective_label.text = _format_briefing_objectives() briefing_objective_label.text = _format_briefing_objectives()
briefing_objective_label.visible = not briefing_objective_label.text.is_empty() briefing_objective_label.visible = not briefing_objective_label.text.is_empty()
_update_briefing_objective_visibility()
briefing_label.text = body briefing_label.text = body
briefing_panel.visible = true briefing_panel.visible = true
_refresh_screen_backdrop_visibility() _refresh_screen_backdrop_visibility()
@@ -5714,6 +5777,30 @@ func _show_briefing() -> void:
save_button.disabled = prep_locked save_button.disabled = prep_locked
func _on_briefing_objective_toggle_pressed() -> void:
_play_ui_click()
_set_briefing_objective_collapsed(not briefing_objective_collapsed)
func _set_briefing_objective_collapsed(collapsed: bool) -> bool:
if briefing_objective_label == null or briefing_objective_label.text.strip_edges().is_empty():
return false
if briefing_objective_collapsed == collapsed:
return false
briefing_objective_collapsed = collapsed
_update_briefing_objective_visibility()
return true
func _update_briefing_objective_visibility() -> void:
var has_text := briefing_objective_label != null and not briefing_objective_label.text.strip_edges().is_empty()
if briefing_objective_panel != null:
briefing_objective_panel.visible = has_text and not briefing_objective_collapsed
if briefing_objective_toggle_button != null:
briefing_objective_toggle_button.visible = has_text
briefing_objective_toggle_button.text = "조건 보기" if briefing_objective_collapsed else "조건 접기"
func _on_begin_battle_pressed() -> void: func _on_begin_battle_pressed() -> void:
_play_ui_confirm() _play_ui_confirm()
_start_battle_from_briefing() _start_battle_from_briefing()

View File

@@ -29,7 +29,13 @@ func _init() -> void:
failures, failures,
"001 turn pressure risk", "001 turn pressure risk",
_defeat_text("res://data/scenarios/001_yellow_turbans.json"), _defeat_text("res://data/scenarios/001_yellow_turbans.json"),
"잔여 군령 15" "잔여 군령 17"
)
_check_contains(
failures,
"001 late reserve gate",
_progress_text("res://data/scenarios/001_yellow_turbans.json", false),
"제9군령에 전공 갱신"
) )
_check_contains( _check_contains(
failures, failures,
@@ -69,6 +75,7 @@ func _init() -> void:
_check_objective_notice(failures) _check_objective_notice(failures)
_check_terrain_recovery(failures) _check_terrain_recovery(failures)
_check_briefing_battlefield_overview(failures) _check_briefing_battlefield_overview(failures)
_check_opening_battle_extended_pressure(failures)
_check_opening_board_avoids_side_panel(failures) _check_opening_board_avoids_side_panel(failures)
if failures.is_empty(): if failures.is_empty():
@@ -227,8 +234,14 @@ func _check_opening_battle_requires_castle_capture(failures: Array[String]) -> v
var cao_cao: Dictionary = state.get_unit("cao_cao") var cao_cao: Dictionary = state.get_unit("cao_cao")
cao_cao["pos"] = Vector2i(12, 1) cao_cao["pos"] = Vector2i(12, 1)
state._check_battle_status() state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_ACTIVE:
failures.append("opening battle should wait for late reserve before victory, got %s" % str(state.battle_status))
state._run_events("turn_start", BattleStateScript.TEAM_ENEMY, 9)
for enemy in state.get_living_units(BattleStateScript.TEAM_ENEMY):
enemy["alive"] = false
state._check_battle_status()
if str(state.battle_status) != BattleStateScript.STATUS_VICTORY: if str(state.battle_status) != BattleStateScript.STATUS_VICTORY:
failures.append("opening battle should end after enemies fall and castle is captured, got %s" % str(state.battle_status)) failures.append("opening battle should end after late reserve falls and castle is captured, got %s" % str(state.battle_status))
func _check_log_contains(failures: Array[String], logs: Array, expected: String) -> void: func _check_log_contains(failures: Array[String], logs: Array, expected: String) -> void:
@@ -264,13 +277,33 @@ func _check_briefing_battlefield_overview(failures: Array[String]) -> void:
return return
var overview := scene._format_briefing_battlefield_overview_text() var overview := scene._format_briefing_battlefield_overview_text()
_check_contains(failures, "001 briefing map size", overview, "14칸 x 10칸") _check_contains(failures, "001 briefing map size", overview, "14칸 x 10칸")
_check_contains(failures, "001 briefing enemy count", overview, "적세 8") _check_contains(failures, "001 briefing enemy count", overview, "적세 12")
_check_contains(failures, "001 briefing enemy classes", overview, "보병") _check_contains(failures, "001 briefing enemy classes", overview, "보병")
_check_contains(failures, "001 briefing village recovery", overview, "마을 4칸 +6") _check_contains(failures, "001 briefing village recovery", overview, "마을 4칸 +6")
_check_contains(failures, "001 briefing castle recovery", overview, "성채 4칸 +8") _check_contains(failures, "001 briefing castle recovery", overview, "성채 4칸 +8")
scene.free() scene.free()
func _check_opening_battle_extended_pressure(failures: Array[String]) -> void:
var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("opening battle pressure smoke could not load scenario")
return
if state.get_turn_limit() != 17:
failures.append("opening battle should allow play through turn 17, got limit %d" % state.get_turn_limit())
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 12:
failures.append("opening battle should start with 12 enemies, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
if state._condition_gate_open({"after_event": "turn_9_western_reserve"}):
failures.append("opening battle final defeat-all condition should be gated until turn 9 reserve")
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 not state._condition_gate_open({"after_event": "turn_9_western_reserve"}):
failures.append("opening battle final gate should open after turn 9 reserve event")
if state.get_living_units(BattleStateScript.TEAM_ENEMY).size() != 19:
failures.append("opening battle should field 19 enemies after late reserve, got %d" % state.get_living_units(BattleStateScript.TEAM_ENEMY).size())
func _check_opening_board_avoids_side_panel(failures: Array[String]) -> void: func _check_opening_board_avoids_side_panel(failures: Array[String]) -> void:
var state = BattleStateScript.new() var state = BattleStateScript.new()
if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"): if not state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
@@ -293,12 +326,12 @@ func _check_objective_notice(failures: Array[String]) -> void:
scene._on_objective_updated("Defeat the new vanguard.", "") scene._on_objective_updated("Defeat the new vanguard.", "")
if scene.objective_notice_panel == null or not scene.objective_notice_panel.visible: if scene.objective_notice_panel == null or not scene.objective_notice_panel.visible:
failures.append("objective notice panel should be visible after objective update") 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."): 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) failures.append("objective notice label should use Korean order wording: %s" % scene.objective_notice_label.text)
scene._on_objective_updated("", "Supply train is lost.") scene._on_objective_updated("", "Supply train is lost.")
if scene.objective_notice_panel == null or not scene.objective_notice_panel.visible: if scene.objective_notice_panel == null or not scene.objective_notice_panel.visible:
failures.append("objective notice panel should be visible after defeat update") 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."): 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) 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.": 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") failures.append("objective log display should use Korean order wording")

View File

@@ -95,6 +95,7 @@ func _init() -> void:
_check_hud_focus_text(failures) _check_hud_focus_text(failures)
_check_ancient_ui_theme(failures) _check_ancient_ui_theme(failures)
_check_hover_intent_badges(failures) _check_hover_intent_badges(failures)
_check_counter_attack_badge_variants(failures)
_check_action_button_disabled_reasons(failures) _check_action_button_disabled_reasons(failures)
_check_terrain_and_unit_presentation(failures) _check_terrain_and_unit_presentation(failures)
_check_objective_and_status_marker_helpers(failures) _check_objective_and_status_marker_helpers(failures)
@@ -3085,19 +3086,24 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
scene.free() scene.free()
return return
var briefing_text := scene._format_briefing_objectives() var briefing_text := scene._format_briefing_objectives()
for expected in ["승리 조건", "패배 조건", "보상"]: scene.briefing_objective_label.text = briefing_text
scene._update_briefing_objective_visibility()
for expected in ["승리:", "패배:", "보상:"]:
if not briefing_text.contains(expected): if not briefing_text.contains(expected):
failures.append("briefing objective parchment should use Korean heading `%s`: %s" % [expected, briefing_text]) failures.append("briefing objective parchment should use Korean heading `%s`: %s" % [expected, briefing_text])
for clutter in ["진행", "위험"]: for clutter in ["승리 조건", "패배 조건", "진행", "위험"]:
if briefing_text.contains(clutter): if briefing_text.contains(clutter):
failures.append("briefing objective parchment should stay concise without `%s`: %s" % [clutter, briefing_text]) failures.append("briefing objective parchment should stay concise without `%s`: %s" % [clutter, briefing_text])
var hud_objective := scene._format_objective_hud_text() var hud_objective := scene._format_objective_hud_text()
if not hud_objective.contains("승리:") or not hud_objective.contains("패배:"): if not hud_objective.contains("승리:") or not hud_objective.contains("패배:"):
failures.append("HUD objective should use concise Korean labels: %s" % hud_objective) failures.append("HUD objective should use concise Korean labels: %s" % hud_objective)
var mission_text := scene._format_mission_panel_text() var mission_text := scene._format_mission_panel_text()
for expected in ["승리 조건", "패배 조건", "진행", "위험", "목표|", "주의|", "현황|", "경고|"]: for expected in ["승리:", "패배:", "진행:", "주의:"]:
if not mission_text.contains(expected): if not mission_text.contains(expected):
failures.append("mission panel should use Korean campaign wording `%s`: %s" % [expected, mission_text]) failures.append("mission panel should use Korean campaign wording `%s`: %s" % [expected, mission_text])
for clutter in ["목표|", "주의|", "현황|", "경고|"]:
if mission_text.contains(clutter):
failures.append("mission panel should avoid old verbose marker `%s`: %s" % [clutter, mission_text])
_check_panel_style_fill(failures, scene.briefing_panel, "briefing panel", Color(0.54, 0.36, 0.14, 0.997)) _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_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)) _check_panel_style_fill(failures, scene.briefing_objective_panel, "briefing objective edict panel", Color(0.67, 0.49, 0.24, 0.998))
@@ -3182,8 +3188,34 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
failures.append("dialogue continue button should use the project serif font override") failures.append("dialogue continue button should use the project serif font override")
if scene.mission_title_label == null or scene.mission_title_label.text != "전투 목표": if scene.mission_title_label == null or scene.mission_title_label.text != "전투 목표":
failures.append("mission panel title should use clear Korean wording") failures.append("mission panel title should use clear Korean wording")
if scene.briefing_start_button == null or scene.briefing_start_button.text != "닫고 전투 시작": if scene.mission_toggle_button == null or scene.mission_toggle_button.text != "접기":
failures.append("briefing start button should clearly close the briefing") failures.append("mission objective panel should expose a close control")
else:
scene._on_mission_toggle_pressed()
if scene.mission_detail_panel == null or scene.mission_detail_panel.visible or scene.mission_toggle_button.text != "보기":
failures.append("mission objective panel should collapse cleanly")
scene._on_mission_toggle_pressed()
if scene.mission_detail_panel == null or not scene.mission_detail_panel.visible or scene.mission_toggle_button.text != "접기":
failures.append("mission objective panel should reopen cleanly")
if scene.briefing_start_button == null or scene.briefing_start_button.text != "전투 시작":
failures.append("briefing start button should clearly start after briefing")
if scene.briefing_objective_toggle_button == null or scene.briefing_objective_toggle_button.text != "조건 접기":
failures.append("briefing objective panel should expose a collapse button")
else:
scene._set_briefing_objective_collapsed(true)
if scene.briefing_objective_panel == null or scene.briefing_objective_panel.visible or scene.briefing_objective_toggle_button.text != "조건 보기":
failures.append("briefing objective panel should collapse cleanly")
scene._set_briefing_objective_collapsed(false)
if scene.briefing_objective_panel == null or not scene.briefing_objective_panel.visible or scene.briefing_objective_toggle_button.text != "조건 접기":
failures.append("briefing objective panel should reopen cleanly")
scene.briefing_panel.visible = true
scene.battle_started = false
var escape_event := InputEventKey.new()
escape_event.keycode = KEY_ESCAPE
scene._handle_key(escape_event)
if not scene.briefing_objective_collapsed or scene.battle_started:
failures.append("briefing Escape should collapse conditions before starting battle")
scene.briefing_panel.visible = false
if scene.shop_button == null or scene.shop_button.text != "군상": if scene.shop_button == null or scene.shop_button.text != "군상":
failures.append("briefing shop button should use sutler wording") failures.append("briefing shop button should use sutler wording")
if scene.talk_button == null or scene.talk_button.text != "군막 회의": if scene.talk_button == null or scene.talk_button.text != "군막 회의":
@@ -3212,8 +3244,8 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
failures.append("targeting hint title should use mark foe wording") failures.append("targeting hint title should use mark foe wording")
if scene.targeting_hint_back_button == null or scene.targeting_hint_back_button.text != "죽간 거두기": 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") failures.append("targeting hint back button should use bamboo-slip wording")
scene._on_objective_updated("동문에 조서를 꽂아라.", "조조가 퇴각하면 패전이다.") 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("주의"): 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") failures.append("objective update notice should use Korean annotation wording")
scene._update_briefing_camp_overview(scene.state.get_briefing(), false) 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 != "비단 전장도": if scene.briefing_camp_overview_fallback_label == null or scene.briefing_camp_overview_fallback_label.text != "비단 전장도":
@@ -3492,6 +3524,10 @@ func _check_hover_intent_badges(failures: Array[String]) -> void:
var attack_badge := scene._target_preview_badge() 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", "")) == "행군": 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)) failures.append("enemy hover should keep attack badge priority: %s" % str(attack_badge))
if not str(attack_badge.get("text", "")).contains(""):
failures.append("adjacent attack badge should expose counterattack risk: %s" % str(attack_badge))
if str(attack_badge.get("kind", "")) != "counter" and str(attack_badge.get("kind", "")) != "danger":
failures.append("counterable attack badge should use a counter danger color kind: %s" % str(attack_badge))
var cao_cao: Dictionary = scene.state.get_unit("cao_cao") var cao_cao: Dictionary = scene.state.get_unit("cao_cao")
cao_cao["moved"] = true cao_cao["moved"] = true
@@ -3506,6 +3542,10 @@ func _check_hover_intent_badges(failures: Array[String]) -> void:
failures.append("MOVE badge should use a distinct color") failures.append("MOVE badge should use a distinct color")
if scene._target_preview_badge_color("select") == default_color: if scene._target_preview_badge_color("select") == default_color:
failures.append("SELECT badge should use a distinct color") failures.append("SELECT badge should use a distinct color")
if scene._target_preview_badge_color("counter") == default_color:
failures.append("COUNTER badge should use a distinct color")
if scene._target_preview_badge_color("danger") == default_color:
failures.append("DANGER badge should use a distinct color")
var board_rect: Rect2 = scene._board_rect() var board_rect: Rect2 = scene._board_rect()
var edge_badge_rect: Rect2 = scene._target_preview_badge_rect(Vector2i(13, 9)) var edge_badge_rect: Rect2 = scene._target_preview_badge_rect(Vector2i(13, 9))
if edge_badge_rect.position.x < board_rect.position.x or edge_badge_rect.position.y < board_rect.position.y: if edge_badge_rect.position.x < board_rect.position.x or edge_badge_rect.position.y < board_rect.position.y:
@@ -3517,6 +3557,60 @@ func _check_hover_intent_badges(failures: Array[String]) -> void:
scene.free() scene.free()
func _check_counter_attack_badge_variants(failures: Array[String]) -> void:
var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):
failures.append("could not load battle scene state for counter badge variants")
scene.free()
return
scene.battle_started = true
scene.state.select_unit("cao_cao")
var cao_cao: Dictionary = scene.state.get_unit("cao_cao")
var infantry: Dictionary = scene.state.get_unit("yellow_turban_2")
var archer: Dictionary = scene.state.get_unit("yellow_turban_3")
if cao_cao.is_empty() or infantry.is_empty() or archer.is_empty():
failures.append("counter badge variant smoke missing units")
scene.free()
return
infantry["pos"] = Vector2i(2, 3)
infantry["hp"] = 99
infantry["max_hp"] = 99
archer["pos"] = Vector2i(10, 8)
scene.hover_cell = Vector2i(2, 3)
var counter_badge := scene._target_preview_badge()
if str(counter_badge.get("kind", "")) != "counter" or not str(counter_badge.get("text", "")).contains(""):
failures.append("melee target should expose counter badge: %s" % str(counter_badge))
scene.basic_attack_targeting = true
scene._refresh_ranges()
var marker_entries := scene._attack_target_marker_entries()
if marker_entries.is_empty() or str(marker_entries[0].get("kind", "")) != "counter":
failures.append("attack target marker should carry counter kind: %s" % str(marker_entries))
scene.basic_attack_targeting = false
infantry["pos"] = Vector2i(10, 8)
archer["pos"] = Vector2i(2, 3)
archer["hp"] = 99
archer["max_hp"] = 99
scene.hover_cell = Vector2i(2, 3)
var no_counter_badge := scene._target_preview_badge()
if str(no_counter_badge.get("kind", "")) != "damage" or str(no_counter_badge.get("text", "")).contains(""):
failures.append("adjacent archer target should not expose counter badge: %s" % str(no_counter_badge))
archer["pos"] = Vector2i(10, 8)
infantry["pos"] = Vector2i(2, 3)
infantry["hp"] = 99
infantry["max_hp"] = 99
infantry["atk"] = 999
cao_cao["hp"] = 1
scene.hover_cell = Vector2i(2, 3)
var danger_badge := scene._target_preview_badge()
if str(danger_badge.get("kind", "")) != "danger" or not str(danger_badge.get("text", "")).contains("반격!"):
failures.append("lethal counter should expose danger badge: %s" % str(danger_badge))
scene.free()
func _check_terrain_and_unit_presentation(failures: Array[String]) -> void: func _check_terrain_and_unit_presentation(failures: Array[String]) -> void:
var scene = BattleSceneScript.new() var scene = BattleSceneScript.new()
if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"): if not scene.state.load_battle("res://data/scenarios/001_yellow_turbans.json"):