From ed13f9bac67eed2bccff1f5a54b432552e41d6a5 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 05:16:16 +0900 Subject: [PATCH] Add image-first briefing tactical markers --- README.md | 2 +- scripts/scenes/battle_scene.gd | 154 ++++++++++++++++++++++++++++++++- tools/smoke_visual_assets.gd | 19 ++++ 3 files changed, 171 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index afc0abb..d81ebb6 100644 --- a/README.md +++ b/README.md @@ -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 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 briefing shows the campaign chapter, chapter battle number, location, victory/defeat summary, and first-clear reward preview. +- 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 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 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. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index f65f6a6..b71db8e 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -136,6 +136,9 @@ const LOCAL_COMMAND_TITLE_MIN_FONT_SIZE := 10 const LOCAL_COMMAND_DETAIL_FONT_SIZE := 12 const LOCAL_COMMAND_DETAIL_MIN_FONT_SIZE := 10 const BRIEFING_CAMP_THUMBNAIL_SIZE := Vector2(188, 86) +const BRIEFING_TACTICAL_MARKER_CHIP_SIZE := Vector2(122, 26) +const BRIEFING_TACTICAL_MARKER_ICON_SIZE := Vector2(22, 22) +const BRIEFING_TACTICAL_MARKER_MAX_COUNT := 3 const CHAPTER_SEAL_BADGE_SIZE := Vector2(54, 54) const CHAPTER_SCENARIO_THUMBNAIL_SIZE := Vector2(72, 50) const CHAPTER_INFO_BADGE_SIZE := Vector2(86, 22) @@ -404,6 +407,7 @@ var briefing_camp_overview_row: HBoxContainer var briefing_camp_overview_texture: TextureRect var briefing_camp_overview_fallback_label: Label var briefing_camp_overview_label: Label +var briefing_tactical_marker_list: VBoxContainer var briefing_label: Label var briefing_start_button: Button var chapter_button: Button @@ -3188,10 +3192,17 @@ func _create_hud() -> void: briefing_camp_overview_label = Label.new() briefing_camp_overview_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART - briefing_camp_overview_label.custom_minimum_size = Vector2(360, 86) + briefing_camp_overview_label.custom_minimum_size = Vector2(238, 86) briefing_camp_overview_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL _apply_label_style(briefing_camp_overview_label, UI_AGED_INK, 14) briefing_camp_overview_row.add_child(briefing_camp_overview_label) + + briefing_tactical_marker_list = VBoxContainer.new() + briefing_tactical_marker_list.name = "BriefingTacticalMarkerList" + briefing_tactical_marker_list.custom_minimum_size = Vector2(BRIEFING_TACTICAL_MARKER_CHIP_SIZE.x, 86) + briefing_tactical_marker_list.size_flags_vertical = Control.SIZE_SHRINK_CENTER + briefing_tactical_marker_list.add_theme_constant_override("separation", 4) + briefing_camp_overview_row.add_child(briefing_tactical_marker_list) briefing_camp_overview_shell.add_child(_make_bamboo_gutter(14, 86)) briefing_column.add_child(_make_scroll_rod(680, 4)) @@ -10240,9 +10251,11 @@ func _update_briefing_camp_overview(briefing: Dictionary, prep_locked: bool) -> if briefing_camp_overview_row == null: return var overview_text := _format_briefing_camp_overview_text(briefing, prep_locked) - briefing_camp_overview_row.visible = not overview_text.is_empty() + var has_tactical_markers := not _briefing_tactical_marker_entries().is_empty() + var has_overview := not overview_text.is_empty() or has_tactical_markers + briefing_camp_overview_row.visible = has_overview if briefing_camp_overview_panel != null: - briefing_camp_overview_panel.visible = not overview_text.is_empty() + briefing_camp_overview_panel.visible = has_overview if briefing_camp_overview_label != null: briefing_camp_overview_label.text = overview_text var texture := _current_battle_background_texture() @@ -10253,6 +10266,141 @@ func _update_briefing_camp_overview(briefing: Dictionary, prep_locked: bool) -> if briefing_camp_overview_fallback_label != null: briefing_camp_overview_fallback_label.text = "비단 전장도" briefing_camp_overview_fallback_label.visible = not has_texture + _rebuild_briefing_tactical_marker_list() + + +func _rebuild_briefing_tactical_marker_list() -> void: + if briefing_tactical_marker_list == null: + return + for child in briefing_tactical_marker_list.get_children(): + briefing_tactical_marker_list.remove_child(child) + child.queue_free() + var entries := _briefing_tactical_marker_entries() + briefing_tactical_marker_list.visible = not entries.is_empty() + for entry in entries: + briefing_tactical_marker_list.add_child(_make_briefing_tactical_marker_chip(entry)) + + +func _briefing_tactical_marker_entries() -> Array[Dictionary]: + var result: Array[Dictionary] = [] + var seen := {} + for marker in state.get_event_markers(): + var label := str(marker.get("label", "")).strip_edges() + var kind := str(marker.get("kind", "event")).strip_edges().to_lower() + if label.is_empty(): + label = "전장 표식" + var key := "%s/%s" % [label, kind] + if seen.has(key): + continue + seen[key] = true + var cells = marker.get("cells", []) + var cell_count: int = 0 + if typeof(cells) == TYPE_ARRAY: + var marker_cells: Array = cells + cell_count = marker_cells.size() + var badge_kind := _event_marker_badge_kind(label, kind) + result.append({ + "label": label, + "kind": kind, + "badge": badge_kind, + "tile": _event_marker_tile_marker_key(label, kind), + "summary": _briefing_tactical_marker_summary(label, badge_kind, cell_count) + }) + if result.size() >= BRIEFING_TACTICAL_MARKER_MAX_COUNT: + break + return result + + +func _make_briefing_tactical_marker_chip(entry: Dictionary) -> PanelContainer: + var label_text := str(entry.get("label", "전장 표식")) + var summary_text := str(entry.get("summary", "전장 표식")) + var tooltip := "%s\n%s" % [label_text, summary_text] + var chip := PanelContainer.new() + chip.custom_minimum_size = BRIEFING_TACTICAL_MARKER_CHIP_SIZE + _apply_panel_style(chip, "caption") + var chip_style := chip.get_theme_stylebox("panel") + if chip_style != null: + chip_style.content_margin_left = 4 + chip_style.content_margin_right = 4 + chip_style.content_margin_top = 2 + chip_style.content_margin_bottom = 2 + + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 5) + row.tooltip_text = tooltip + chip.add_child(row) + + row.add_child(_make_briefing_tactical_marker_icon(entry, tooltip)) + + var text_column := VBoxContainer.new() + text_column.custom_minimum_size = Vector2(82, 22) + text_column.size_flags_horizontal = Control.SIZE_EXPAND_FILL + text_column.add_theme_constant_override("separation", -2) + text_column.tooltip_text = tooltip + row.add_child(text_column) + + var title_label := Label.new() + title_label.custom_minimum_size = Vector2(78, 13) + title_label.clip_text = true + title_label.text = label_text + title_label.tooltip_text = tooltip + _apply_label_style(title_label, UI_PARCHMENT_TEXT, 12) + text_column.add_child(title_label) + + var detail_label := Label.new() + detail_label.custom_minimum_size = Vector2(78, 10) + detail_label.clip_text = true + detail_label.text = summary_text + detail_label.tooltip_text = tooltip + _apply_label_style(detail_label, UI_OLD_BRONZE, 9) + text_column.add_child(detail_label) + + _set_control_tree_tooltip(chip, tooltip) + return chip + + +func _make_briefing_tactical_marker_icon(entry: Dictionary, tooltip: String) -> Control: + var icon_stack := Control.new() + icon_stack.custom_minimum_size = BRIEFING_TACTICAL_MARKER_ICON_SIZE + icon_stack.tooltip_text = tooltip + var tile_texture := _load_tile_marker_texture(str(entry.get("tile", "select"))) + if tile_texture != null: + var tile_rect := TextureRect.new() + tile_rect.set_anchors_preset(Control.PRESET_FULL_RECT) + tile_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + tile_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + tile_rect.modulate = Color(1.0, 1.0, 1.0, 0.42) + tile_rect.texture = tile_texture + tile_rect.tooltip_text = tooltip + icon_stack.add_child(tile_rect) + var badge_texture := _load_map_badge_texture(str(entry.get("badge", "event"))) + if badge_texture != null: + var badge_rect := TextureRect.new() + badge_rect.set_anchors_preset(Control.PRESET_FULL_RECT) + badge_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + badge_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + badge_rect.texture = badge_texture + badge_rect.tooltip_text = tooltip + icon_stack.add_child(badge_rect) + return icon_stack + + +func _briefing_tactical_marker_summary(label: String, badge_kind: String, cell_count: int) -> String: + if label.contains("유인"): + return "적을 끌어냄" + if label.contains("마을"): + return "회복 거점" + if label.contains("숲"): + return "숨은 보급" + if badge_kind == "supply": + return "보급 확보" + if badge_kind == "gold": + return "군자금" + if badge_kind == "tactic": + return "전술 지점" + if cell_count > 1: + return "%d칸 표식" % cell_count + return "전장 표식" func _format_briefing_camp_overview_text(briefing: Dictionary, prep_locked: bool) -> String: diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index aaca0f5..f38a342 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -3089,6 +3089,19 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: for expected in ["위치: 영천, 중평 원년", "군막:", "준비:", "군자금 0냥", "군막 회의 · 4건", "보급 대기 1건", "군상 물품 · 2종", "출진 명부 2명 중 2명", "진형 배치 6칸", "비축 물자"]: if not camp_overview.contains(expected): failures.append("camp overview should include %s: %s" % [expected, camp_overview]) + var briefing_marker_entries := scene._briefing_tactical_marker_entries() + if briefing_marker_entries.size() < 3: + failures.append("briefing should expose image-first tactical marker entries") + var briefing_marker_labels := [] + for marker_entry in briefing_marker_entries: + briefing_marker_labels.append(str(marker_entry.get("label", ""))) + if scene._load_map_badge_texture(str(marker_entry.get("badge", ""))) == null: + failures.append("briefing tactical marker should resolve generated badge art: %s" % str(marker_entry)) + if scene._load_tile_marker_texture(str(marker_entry.get("tile", ""))) == null: + failures.append("briefing tactical marker should resolve generated tile art: %s" % str(marker_entry)) + for expected_marker in ["유인선", "북숲 보급고", "마을 보급"]: + if not briefing_marker_labels.has(expected_marker): + failures.append("briefing tactical markers should include %s: %s" % [expected_marker, str(briefing_marker_labels)]) 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) if not bean_buy_detail.contains("병력 +20") or not bean_buy_detail.contains("보유 0점"): @@ -3573,6 +3586,12 @@ func _check_ancient_ui_theme(failures: Array[String]) -> void: 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") + if scene.briefing_tactical_marker_list == null: + failures.append("briefing should include a tactical marker card list") + elif scene.briefing_tactical_marker_list.get_child_count() < 3: + failures.append("briefing tactical marker card list should expose three image-first markers") + elif not _has_descendant_texture(scene.briefing_tactical_marker_list): + failures.append("briefing tactical marker cards should render generated badge/tile textures") scene._apply_panel_style(scene.result_panel, "result_victory_tablet") _check_panel_style_fill(failures, scene.result_panel, "victory result panel", Color(0.27, 0.34, 0.28, 0.997)) _check_panel_uses_generated_texture(failures, scene.result_panel, "victory result generated frame")