diff --git a/README.md b/README.md index ba79c71..b124a05 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, 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. - 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 ce74dd4..8839227 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -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_ICON_SIZE := Vector2(22, 22) 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_IMAGE_SIZE := Vector2(112, 42) 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_fallback_label: Label var briefing_camp_overview_label: Label +var briefing_force_preview_overlay: Control var briefing_tactical_marker_list: VBoxContainer var briefing_label: Label 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(_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.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART 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: briefing_camp_overview_fallback_label.text = "비단 전장도" briefing_camp_overview_fallback_label.visible = not has_texture + _rebuild_briefing_force_preview_overlay() _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: if briefing_tactical_marker_list == null: return diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index f38a342..97c2ea3 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -3102,6 +3102,16 @@ func _check_scene_texture_loading(failures: Array[String]) -> void: 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 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_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점"): @@ -3586,6 +3596,17 @@ 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_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: failures.append("briefing should include a tactical marker card list") elif scene.briefing_tactical_marker_list.get_child_count() < 3: