diff --git a/README.md b/README.md index 14ea0b7..43f0ab9 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - Pre-battle Chapters overview shows story-arc progress with map thumbnail rows, compact status badges, hover details, and can open completed or current battles. - Pre-battle Save menu can write and load one manual campaign checkpoint separate from the automatic save, using compact seal-style slot rows and hover checkpoint details. - The title screen exposes Start, Load, and Settings, and Load now lets the player choose between the automatic record and the manual checkpoint. -- Victory results show compact reward icons for gold, items, and officer roster changes, with hover details for the full reward text. +- Victory results show compact reward icons for gold, items, and officer roster changes, with joined-officer portraits, class crests, Korean join/leave badges, and hover details for the full reward text. - Escort scenarios can include required, non-controllable protected units. - Campaign saves joined officers, future deployments can require prior recruitment, and choices can make officers leave. - Completed saves reconcile officer join/leave rewards on load without replaying gold or items. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 4b7567e..609755c 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -11002,7 +11002,14 @@ func _make_result_reward_item_chip(item_id: String, count: int) -> PanelContaine func _make_result_reward_officer_chip(officer_id: String, joined: bool) -> PanelContainer: var officer_name := _officer_display_name_from_id(officer_id) var action_text := "합류" if joined else "이탈" + var officer_class_name := _officer_class_display_name(officer_id) var tooltip := "%s: %s" % [action_text, officer_name] + if not officer_class_name.is_empty(): + tooltip += "\n병과: %s" % officer_class_name + var officer_def: Dictionary = state.data_catalog.officers.get(officer_id, {}) + var level := int(officer_def.get("level", 0)) + if level > 0: + tooltip += "\n품계: %d" % level var panel := _make_result_reward_chip_shell(tooltip) var stack := Control.new() stack.set_anchors_preset(Control.PRESET_FULL_RECT) @@ -11028,7 +11035,11 @@ func _make_result_reward_officer_chip(officer_id: String, joined: bool) -> Panel _apply_label_style(fallback_label, UI_PARCHMENT_TEXT, 16) stack.add_child(fallback_label) - var badge_text := "+" if joined else "-" + var class_icon := _officer_class_icon_texture(officer_id) + if class_icon != null: + stack.add_child(_make_result_reward_class_badge(class_icon, tooltip)) + + var badge_text := "합" if joined else "이" stack.add_child(_make_result_reward_corner_badge(badge_text, tooltip)) return panel @@ -11046,6 +11057,32 @@ func _make_result_reward_count_badge(count: int, tooltip_text: String) -> Label: return _make_result_reward_corner_badge("x%d" % count, tooltip_text) +func _make_result_reward_class_badge(icon_texture: Texture2D, tooltip_text: String) -> PanelContainer: + var panel := PanelContainer.new() + panel.name = "ResultRewardClassBadge" + panel.position = Vector2(2, 28) + panel.size = Vector2(20, 18) + panel.custom_minimum_size = Vector2(20, 18) + panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + panel.tooltip_text = tooltip_text + _apply_panel_style(panel, "caption") + var stylebox := panel.get_theme_stylebox("panel") + if stylebox != null: + stylebox.content_margin_left = 1 + stylebox.content_margin_right = 1 + stylebox.content_margin_top = 1 + stylebox.content_margin_bottom = 1 + + var icon := TextureRect.new() + icon.set_anchors_preset(Control.PRESET_FULL_RECT) + icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + icon.texture = icon_texture + icon.tooltip_text = tooltip_text + panel.add_child(icon) + return panel + + func _make_result_reward_corner_badge(text: String, tooltip_text: String) -> Label: var badge := Label.new() badge.text = text @@ -11062,6 +11099,39 @@ func _make_result_reward_corner_badge(text: String, tooltip_text: String) -> Lab return badge +func _officer_class_display_name(officer_id: String) -> String: + var officer: Dictionary = state.data_catalog.officers.get(officer_id, {}) + var class_id := str(officer.get("class_id", "")).strip_edges() + if class_id.is_empty(): + return "" + var class_def: Dictionary = state.data_catalog.get_class_def(class_id) + return str(class_def.get("name", _format_identifier_label(class_id))).strip_edges() + + +func _officer_class_icon_texture(officer_id: String) -> Texture2D: + var officer: Dictionary = state.data_catalog.officers.get(officer_id, {}) + var class_id := str(officer.get("class_id", "")).strip_edges() + if class_id.is_empty(): + return _load_class_icon_texture("generic") + return _load_class_icon_texture(_class_icon_key_from_class_id(class_id)) + + +func _class_icon_key_from_class_id(class_id: String) -> String: + match class_id.strip_edges().to_lower(): + "cavalry", "elite_cavalry": + return "cavalry" + "archer", "marksman": + return "ranged" + "strategist", "military_advisor", "hero", "commander": + return "tactic" + "warrior", "champion", "bandit": + return "heavy" + "infantry", "guard_captain": + return "infantry" + _: + return "generic" + + func _format_victory_result_text() -> String: var lines := ["승리"] _append_edict_clause(lines, "승리 조건", "완료", str(state.objectives.get("victory", ""))) diff --git a/tools/smoke_result_summary.gd b/tools/smoke_result_summary.gd index 5f06c9e..936cd63 100644 --- a/tools/smoke_result_summary.gd +++ b/tools/smoke_result_summary.gd @@ -24,7 +24,7 @@ func _init() -> void: "pending_choice": false, "gold": 100, "items": ["bean", "bean"], - "joined_officers": ["dian_wei"], + "joined_officers": ["xiahou_yuan", "cao_ren"], "left_officers": [], "progression_events": [ {"type": "level_up", "name": "조조", "to_level": 2} @@ -38,7 +38,7 @@ func _init() -> void: _check_contains(failures, "reward section", result_text, "전리품") _check_contains(failures, "gold reward", result_text, "군자금 +100냥") _check_contains(failures, "item reward", result_text, "콩 2점") - _check_contains(failures, "roster section", result_text, "합류: 전위") + _check_contains(failures, "roster section", result_text, "합류: 하후연, 조인") _check_contains(failures, "growth section", result_text, "조조 품계 2") _check_contains(failures, "campaign section", result_text, "다음 행군: 사수관 선봉전") _check_contains(failures, "next camp section", result_text, "다음 군막") @@ -51,12 +51,17 @@ func _init() -> void: scene._rebuild_result_reward_strip() if scene.result_reward_strip == null or not scene.result_reward_strip.visible: failures.append("victory result should show a compact reward icon strip") - elif scene.result_reward_strip.get_child_count() < 3: - failures.append("reward icon strip should include gold, item, and officer chips") + elif scene.result_reward_strip.get_child_count() < 4: + failures.append("reward icon strip should include gold, item, and two opening officer chips") var reward_tooltips := _collect_tooltips(scene.result_reward_strip) _check_contains(failures, "reward strip gold tooltip", reward_tooltips, "군자금 +100냥") _check_contains(failures, "reward strip item tooltip", reward_tooltips, "물자: 콩 2점") - _check_contains(failures, "reward strip officer tooltip", reward_tooltips, "합류: 전위") + _check_contains(failures, "reward strip Xiahou Yuan tooltip", reward_tooltips, "합류: 하후연") + _check_contains(failures, "reward strip Cao Ren tooltip", reward_tooltips, "합류: 조인") + _check_contains(failures, "reward strip archer class tooltip", reward_tooltips, "병과: 궁병") + _check_contains(failures, "reward strip infantry class tooltip", reward_tooltips, "병과: 보병") + if _count_named_nodes(scene.result_reward_strip, "ResultRewardClassBadge") < 2: + failures.append("joined officer reward chips should show compact class crests") if not _has_texture_rect(scene.result_reward_strip): failures.append("reward icon strip should reuse item or officer artwork") @@ -161,3 +166,10 @@ func _has_texture_rect(root: Node) -> bool: if _has_texture_rect(child): return true return false + + +func _count_named_nodes(root: Node, node_name: String) -> int: + var count := 1 if root.name == node_name else 0 + for child in root.get_children(): + count += _count_named_nodes(child, node_name) + return count