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

@@ -48,7 +48,7 @@ const FLOATING_TEXT_LIFETIME := 1.0
const FLOATING_TEXT_RISE := 42.0
const OBJECTIVE_NOTICE_DURATION := 2.6
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 UNIT_STATUS_MARKER_RADIUS := 6.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_to_cell := Vector2i(-1, -1)
var show_threat_overlay := false
var mission_detail_collapsed := false
var briefing_objective_collapsed := false
var active_scenario_id := ""
var battle_started := false
var battle_result_applied := false
@@ -215,6 +217,7 @@ var status_label: Label
var objective_label: Label
var campaign_status_label: Label
var mission_title_label: Label
var mission_toggle_button: Button
var mission_detail_panel: PanelContainer
var mission_detail_label: Label
var objective_notice_panel: PanelContainer
@@ -234,6 +237,7 @@ var briefing_location_label: Label
var briefing_seal_ribbon: HBoxContainer
var briefing_objective_panel: PanelContainer
var briefing_objective_label: Label
var briefing_objective_toggle_button: Button
var briefing_camp_overview_panel: PanelContainer
var briefing_camp_overview_row: HBoxContainer
var briefing_camp_overview_texture: TextureRect
@@ -878,10 +882,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)
@@ -1161,11 +1165,24 @@ func _create_hud() -> void:
_apply_label_style(selected_label, UI_PARCHMENT_TEXT)
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.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)
_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.custom_minimum_size = Vector2(420, 134)
@@ -1178,7 +1195,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
@@ -1186,7 +1203,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()
@@ -1459,7 +1476,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()
@@ -1486,12 +1503,23 @@ func _create_hud() -> void:
briefing_column.add_child(briefing_location_label)
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_column.add_child(briefing_start_button)
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.custom_minimum_size = Vector2(680, 104)
@@ -1503,7 +1531,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
@@ -1511,7 +1539,7 @@ 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))
@@ -1525,7 +1553,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)
@@ -1579,7 +1607,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)
@@ -1827,7 +1855,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)
@@ -1840,7 +1868,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)
@@ -1848,7 +1876,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)
@@ -1860,7 +1888,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
@@ -1869,7 +1897,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))
@@ -1925,7 +1953,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
@@ -1936,7 +1964,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))
@@ -1961,6 +1989,7 @@ func _create_hud() -> void:
tactic_button,
item_button,
equip_button,
mission_toggle_button,
threat_button,
end_turn_button,
post_move_attack_button,
@@ -1980,6 +2009,7 @@ func _create_hud() -> void:
formation_button,
save_button,
briefing_start_button,
briefing_objective_toggle_button,
shop_buy_button,
shop_sell_button,
manual_save_button,
@@ -2358,6 +2388,9 @@ func _handle_key(event: InputEventKey) -> void:
elif save_menu != null and save_menu.visible:
_hide_save_menu()
elif briefing_panel != null and briefing_panel.visible:
if _set_briefing_objective_collapsed(true):
_play_ui_cancel()
return
_on_begin_battle_pressed()
elif post_move_picker_panel != null and post_move_picker_panel.visible:
_on_post_move_picker_back_pressed()
@@ -3041,7 +3074,7 @@ func _attack_target_marker_entries() -> Array[Dictionary]:
"cell": cell,
"target_id": str(target.get("id", "")),
"text": "%s %d%%" % [result_text, hit_chance],
"kind": "damage"
"kind": _attack_preview_badge_kind(preview)
})
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 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:
@@ -3882,6 +3932,10 @@ func _support_preview_badge_kind(effect_text: String) -> String:
func _target_preview_badge_color(kind: String) -> Color:
if kind == "damage":
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":
return Color(0.38, 0.70, 1.0)
if kind == "select":
@@ -4015,7 +4069,7 @@ func _format_objective_hud_text() -> String:
if not defeat.is_empty():
lines.append("패배: %s" % defeat)
if lines.is_empty():
return "승리 조건 대기"
return "목표 대기"
return _join_strings(lines, "\n")
@@ -4206,7 +4260,11 @@ func _unit_current_terrain_text(unit: Dictionary) -> String:
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
mission_detail_label.text = _format_mission_panel_text()
@@ -4215,18 +4273,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)
lines.append("승리: %s" % victory)
var defeat := str(state.objectives.get("defeat", ""))
if not defeat.is_empty():
_append_edict_clause(lines, "패배 조건", "주의", defeat)
lines.append("패배: %s" % 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, " · "))
lines.append("진행: %s" % _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("주의: %s" % _join_strings(risk_lines, " · "))
if lines.is_empty():
return "군령\n 대기|아직 열린 군령이 없습니다."
return "열린 목표 없음"
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:
var notice_lines := ["군령 갱신"]
if not victory.is_empty():
_append_edict_clause(notice_lines, "승리 조건", "목표", victory)
notice_lines.append("목표: %s" % victory)
if not defeat.is_empty():
_append_edict_clause(notice_lines, "패배 조건", "주의", defeat)
notice_lines.append("주의: %s" % defeat)
if objective_notice_label != null:
objective_notice_label.text = "\n".join(notice_lines)
if objective_notice_panel != null:
@@ -5323,6 +5381,12 @@ func _on_threat_pressed() -> void:
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:
if campaign_complete_screen:
return
@@ -5465,16 +5529,13 @@ func _format_briefing_objectives() -> String:
var lines := []
var victory := str(state.objectives.get("victory", ""))
if not victory.is_empty():
lines.append("승리 조건")
lines.append(" %s" % victory)
lines.append("승리: %s" % victory)
var defeat := str(state.objectives.get("defeat", ""))
if not defeat.is_empty():
lines.append("패배 조건")
lines.append(" %s" % defeat)
lines.append("패배: %s" % defeat)
var rewards_text := _format_briefing_rewards()
if not rewards_text.is_empty():
lines.append("보상")
lines.append(" %s" % rewards_text)
lines.append("보상: %s" % rewards_text)
return _join_strings(lines, "\n")
@@ -5682,8 +5743,10 @@ func _show_briefing() -> void:
briefing_title_label.text = _format_briefing_title(briefing)
briefing_location_label.text = _format_briefing_location(briefing)
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.visible = not briefing_objective_label.text.is_empty()
_update_briefing_objective_visibility()
briefing_label.text = body
briefing_panel.visible = true
_refresh_screen_backdrop_visibility()
@@ -5714,6 +5777,30 @@ func _show_briefing() -> void:
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:
_play_ui_confirm()
_start_battle_from_briefing()