Add briefing force preview markers

This commit is contained in:
2026-06-20 05:26:42 +09:00
parent 2723cdb8d7
commit 7b78c8255c
3 changed files with 142 additions and 1 deletions

View File

@@ -33,7 +33,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr
- Pre-battle Armory equipment changes, including unequipping gear back into stock, save immediately and sync roster equipment plus inventory stock, with portrait and item-icon rows plus hover change details. - Pre-battle Armory equipment changes, including unequipping gear back into stock, save immediately and sync roster equipment plus inventory stock, with portrait and item-icon rows plus hover change details.
- Pre-battle Roster can move optional officers between sortie and reserve within scenario limits, using portrait rows, compact status badges, and hover details. - Pre-battle Roster can move optional officers between sortie and reserve within scenario limits, using portrait rows, compact status badges, and hover details.
- Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells, with portrait rows, readable zone badges, and hover placement instructions. - Pre-battle Formation lets deployed officers swap starting positions inside scenario-defined cells, with portrait rows, readable zone badges, and hover placement instructions.
- Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, first-clear reward preview, generated battlefield thumbnail, and image-first tactical marker cards for lure lines and supply points. - Pre-battle briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, first-clear reward preview, generated battlefield thumbnail, allied portrait/enemy unit force markers, and image-first tactical marker cards for lure lines and supply points.
- Pre-battle prep commands use compact icon buttons with active selection state and Korean hover labels for chapters, shop, talks, armory, roster, formation, and save records. - Pre-battle prep commands use compact icon buttons with active selection state and Korean hover labels for chapters, shop, talks, armory, roster, formation, and save records.
- Battle, top-toolbar, and pre-battle command buttons now prefer generated high-resolution bronze/parchment icon art under `art/ui/icons` and generated lacquer/jade/cinnabar button surfaces under `art/ui/buttons`, with procedural icon drawing and flat button styles kept only as fallbacks. - Battle, top-toolbar, and pre-battle command buttons now prefer generated high-resolution bronze/parchment icon art under `art/ui/icons` and generated lacquer/jade/cinnabar button surfaces under `art/ui/buttons`, with procedural icon drawing and flat button styles kept only as fallbacks.
- Battle maps blend generated terrain texture tiles for plains, forests, hills, wasteland, roads, water, villages, and castles over the high-resolution battlefield backdrop, with generated transparent feature overlays for road connections, water, villages, castles, and wasteland accents. - Battle maps blend generated terrain texture tiles for plains, forests, hills, wasteland, roads, water, villages, and castles over the high-resolution battlefield backdrop, with generated transparent feature overlays for road connections, water, villages, castles, and wasteland accents.

View File

@@ -139,6 +139,9 @@ const BRIEFING_CAMP_THUMBNAIL_SIZE := Vector2(188, 86)
const BRIEFING_TACTICAL_MARKER_CHIP_SIZE := Vector2(122, 26) const BRIEFING_TACTICAL_MARKER_CHIP_SIZE := Vector2(122, 26)
const BRIEFING_TACTICAL_MARKER_ICON_SIZE := Vector2(22, 22) const BRIEFING_TACTICAL_MARKER_ICON_SIZE := Vector2(22, 22)
const BRIEFING_TACTICAL_MARKER_MAX_COUNT := 3 const BRIEFING_TACTICAL_MARKER_MAX_COUNT := 3
const BRIEFING_FORCE_PREVIEW_ICON_SIZE := Vector2(32, 32)
const BRIEFING_FORCE_PREVIEW_MAX_ALLIES := 2
const BRIEFING_FORCE_PREVIEW_MAX_ENEMY_TYPES := 4
const OPENING_STORY_BEAT_CARD_SIZE := Vector2(122, 66) const OPENING_STORY_BEAT_CARD_SIZE := Vector2(122, 66)
const OPENING_STORY_BEAT_IMAGE_SIZE := Vector2(112, 42) const OPENING_STORY_BEAT_IMAGE_SIZE := Vector2(112, 42)
const CHAPTER_SEAL_BADGE_SIZE := Vector2(54, 54) const CHAPTER_SEAL_BADGE_SIZE := Vector2(54, 54)
@@ -409,6 +412,7 @@ var briefing_camp_overview_row: HBoxContainer
var briefing_camp_overview_texture: TextureRect var briefing_camp_overview_texture: TextureRect
var briefing_camp_overview_fallback_label: Label var briefing_camp_overview_fallback_label: Label
var briefing_camp_overview_label: Label var briefing_camp_overview_label: Label
var briefing_force_preview_overlay: Control
var briefing_tactical_marker_list: VBoxContainer var briefing_tactical_marker_list: VBoxContainer
var briefing_label: Label var briefing_label: Label
var briefing_start_button: Button var briefing_start_button: Button
@@ -3270,6 +3274,13 @@ func _create_hud() -> void:
camp_thumbnail_stack.add_child(briefing_camp_overview_fallback_label) camp_thumbnail_stack.add_child(briefing_camp_overview_fallback_label)
camp_thumbnail_stack.add_child(_make_map_lattice_overlay(BRIEFING_CAMP_THUMBNAIL_SIZE)) camp_thumbnail_stack.add_child(_make_map_lattice_overlay(BRIEFING_CAMP_THUMBNAIL_SIZE))
briefing_force_preview_overlay = Control.new()
briefing_force_preview_overlay.name = "BriefingForcePreviewOverlay"
briefing_force_preview_overlay.set_anchors_preset(Control.PRESET_FULL_RECT)
briefing_force_preview_overlay.custom_minimum_size = BRIEFING_CAMP_THUMBNAIL_SIZE
briefing_force_preview_overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
camp_thumbnail_stack.add_child(briefing_force_preview_overlay)
briefing_camp_overview_label = Label.new() briefing_camp_overview_label = Label.new()
briefing_camp_overview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART briefing_camp_overview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
briefing_camp_overview_label.custom_minimum_size = Vector2(238, 86) briefing_camp_overview_label.custom_minimum_size = Vector2(238, 86)
@@ -10346,9 +10357,118 @@ func _update_briefing_camp_overview(briefing: Dictionary, prep_locked: bool) ->
if briefing_camp_overview_fallback_label != null: if briefing_camp_overview_fallback_label != null:
briefing_camp_overview_fallback_label.text = "비단 전장도" briefing_camp_overview_fallback_label.text = "비단 전장도"
briefing_camp_overview_fallback_label.visible = not has_texture briefing_camp_overview_fallback_label.visible = not has_texture
_rebuild_briefing_force_preview_overlay()
_rebuild_briefing_tactical_marker_list() _rebuild_briefing_tactical_marker_list()
func _rebuild_briefing_force_preview_overlay() -> void:
if briefing_force_preview_overlay == null:
return
for child in briefing_force_preview_overlay.get_children():
briefing_force_preview_overlay.remove_child(child)
child.queue_free()
var allies := _briefing_force_preview_allies()
for index in range(allies.size()):
var ally: Dictionary = allies[index]
var icon := _make_briefing_force_preview_icon(ally, "ally", _format_briefing_force_unit_tooltip(ally, "아군"))
icon.name = "BriefingForcePreviewAllyIcon%d" % (index + 1)
icon.position = Vector2(8.0 + float(index) * 34.0, 46.0)
briefing_force_preview_overlay.add_child(icon)
var enemies := _briefing_force_preview_enemy_types()
for index in range(enemies.size()):
var enemy: Dictionary = enemies[index]
var icon := _make_briefing_force_preview_icon(enemy, "enemy", _format_briefing_force_unit_tooltip(enemy, "적세"))
icon.name = "BriefingForcePreviewEnemyIcon%d" % (index + 1)
icon.position = Vector2(148.0 - float(index) * 30.0, 8.0)
briefing_force_preview_overlay.add_child(icon)
func _briefing_force_preview_allies() -> Array[Dictionary]:
var result: Array[Dictionary] = []
for unit in state.get_controllable_player_units():
result.append(unit)
if result.size() >= BRIEFING_FORCE_PREVIEW_MAX_ALLIES:
break
return result
func _briefing_force_preview_enemy_types() -> Array[Dictionary]:
var result: Array[Dictionary] = []
var seen_classes := {}
for unit in state.get_living_units(BattleState.TEAM_ENEMY):
var class_key := str(unit.get("class_id", _unit_class_display_name(unit))).strip_edges()
if class_key.is_empty():
class_key = str(unit.get("name", "enemy"))
if seen_classes.has(class_key):
continue
seen_classes[class_key] = true
result.append(unit)
if result.size() >= BRIEFING_FORCE_PREVIEW_MAX_ENEMY_TYPES:
break
return result
func _make_briefing_force_preview_icon(unit: Dictionary, side: String, tooltip_text: String) -> PanelContainer:
var panel := PanelContainer.new()
panel.custom_minimum_size = BRIEFING_FORCE_PREVIEW_ICON_SIZE
panel.tooltip_text = tooltip_text
panel.set_meta("briefing_force_side", side)
_apply_panel_style(panel, "portrait")
var panel_style := panel.get_theme_stylebox("panel")
if panel_style != null:
panel_style.content_margin_left = 2
panel_style.content_margin_right = 2
panel_style.content_margin_top = 2
panel_style.content_margin_bottom = 2
var stack := Control.new()
stack.custom_minimum_size = BRIEFING_FORCE_PREVIEW_ICON_SIZE
stack.tooltip_text = tooltip_text
panel.add_child(stack)
var texture_rect := TextureRect.new()
texture_rect.name = "BriefingForcePreviewImage"
texture_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
texture_rect.texture = _briefing_force_preview_texture(unit, side)
texture_rect.visible = texture_rect.texture != null
texture_rect.tooltip_text = tooltip_text
stack.add_child(texture_rect)
var fallback_label := Label.new()
fallback_label.set_anchors_preset(Control.PRESET_FULL_RECT)
fallback_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
fallback_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
fallback_label.text = _dialogue_portrait_initials(str(unit.get("name", "")))
fallback_label.visible = texture_rect.texture == null
fallback_label.tooltip_text = tooltip_text
_apply_label_style(fallback_label, UI_PARCHMENT_TEXT, 11)
stack.add_child(fallback_label)
_set_control_tree_tooltip(panel, tooltip_text)
return panel
func _briefing_force_preview_texture(unit: Dictionary, side: String) -> Texture2D:
var texture: Texture2D = null
if side == "ally":
texture = _load_portrait_texture(str(unit.get("portrait", "")))
if texture == null:
texture = _load_art_texture(str(unit.get("sprite", "")))
if texture == null:
texture = _load_unit_class_icon_texture(unit)
return texture
func _format_briefing_force_unit_tooltip(unit: Dictionary, side_label: String) -> String:
return "%s · %s\n%s · 품계 %d" % [
side_label,
str(unit.get("name", "부대")),
_unit_class_display_name(unit),
int(unit.get("level", 1))
]
func _rebuild_briefing_tactical_marker_list() -> void: func _rebuild_briefing_tactical_marker_list() -> void:
if briefing_tactical_marker_list == null: if briefing_tactical_marker_list == null:
return return

View File

@@ -3102,6 +3102,16 @@ func _check_scene_texture_loading(failures: Array[String]) -> void:
for expected_marker in ["유인선", "북숲 보급고", "마을 보급"]: for expected_marker in ["유인선", "북숲 보급고", "마을 보급"]:
if not briefing_marker_labels.has(expected_marker): if not briefing_marker_labels.has(expected_marker):
failures.append("briefing tactical markers should include %s: %s" % [expected_marker, str(briefing_marker_labels)]) failures.append("briefing tactical markers should include %s: %s" % [expected_marker, str(briefing_marker_labels)])
var briefing_allies := scene._briefing_force_preview_allies()
if briefing_allies.size() < 2:
failures.append("briefing force preview should expose Cao Cao and Xiahou Dun")
var briefing_enemy_types := scene._briefing_force_preview_enemy_types()
if briefing_enemy_types.size() < 3:
failures.append("briefing force preview should expose representative enemy unit art")
for preview_unit in briefing_allies + briefing_enemy_types:
var preview_side := "ally" if str(preview_unit.get("team", "")) == BattleStateScript.TEAM_PLAYER else "enemy"
if scene._briefing_force_preview_texture(preview_unit, preview_side) == null:
failures.append("briefing force preview unit should resolve portrait or unit art: %s" % str(preview_unit.get("name", "")))
var bean: Dictionary = scene.state.get_item_def("bean") var bean: Dictionary = scene.state.get_item_def("bean")
var bean_buy_detail := scene._format_shop_buy_item_detail_text("bean", bean, int(bean.get("price", 0)), 0) var bean_buy_detail := scene._format_shop_buy_item_detail_text("bean", bean, int(bean.get("price", 0)), 0)
if not bean_buy_detail.contains("병력 +20") or not bean_buy_detail.contains("보유 0점"): if not bean_buy_detail.contains("병력 +20") or not bean_buy_detail.contains("보유 0점"):
@@ -3586,6 +3596,17 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void:
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 != "비단 전장도":
failures.append("briefing map fallback should read as an old campaign map") failures.append("briefing map fallback should read as an old campaign map")
if scene.briefing_force_preview_overlay == null:
failures.append("briefing map thumbnail should include a force preview overlay")
else:
var ally_icons: Array = scene.briefing_force_preview_overlay.find_children("BriefingForcePreviewAllyIcon*", "", false, false)
var enemy_icons: Array = scene.briefing_force_preview_overlay.find_children("BriefingForcePreviewEnemyIcon*", "", false, false)
if ally_icons.size() < 2:
failures.append("briefing force preview should show allied portrait icons")
if enemy_icons.size() < 3:
failures.append("briefing force preview should show representative enemy unit icons")
if not _has_descendant_texture(scene.briefing_force_preview_overlay):
failures.append("briefing force preview should render generated portrait/unit textures")
if scene.briefing_tactical_marker_list == null: if scene.briefing_tactical_marker_list == null:
failures.append("briefing should include a tactical marker card list") failures.append("briefing should include a tactical marker card list")
elif scene.briefing_tactical_marker_list.get_child_count() < 3: elif scene.briefing_tactical_marker_list.get_child_count() < 3: