diff --git a/README.md b/README.md index fb48b60..0caa461 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Godot 4 tactical RPG prototype inspired by classic turn-based Romance of the Thr - 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. - Post-move tactic and item pickers use generated command/item icons, compact two-line option rows, and hover details instead of dense single-line text. +- Target preview badges and battlefield target markers include generated action icons for move, attack, threat, support, and item outcomes. - 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. - Core UI panels now prefer generated high-resolution lacquer, scroll, jade, and command-seal frame textures under `art/ui/panels`, with the calmer ink/wood/jade flat styles kept as fallbacks instead of the earlier yellow, hard-edged frame treatment. diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 8c368c8..7f44260 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -99,7 +99,8 @@ const BATTLE_PRESENTATION_STEP_GAP := 0.08 const BATTLE_PRESENTATION_FEEDBACK_LAG := 0.10 const UNIT_MOVE_ANIMATION_DURATION := 0.18 const UNIT_IDLE_PHASE_SPEED := 2.4 -const TARGET_PREVIEW_BADGE_SIZE := Vector2(104, 22) +const TARGET_PREVIEW_BADGE_SIZE := Vector2(122, 24) +const TARGET_PREVIEW_BADGE_ICON_SIZE := Vector2(18, 18) const HOVER_INFO_BADGE_MAX_WIDTH := 196.0 const HOVER_INFO_BADGE_MIN_WIDTH := 126.0 const LOW_HP_WARNING_RATIO := 0.35 @@ -5865,7 +5866,7 @@ func _draw_target_selection_markers() -> void: _draw_tile_marker("target", rect, Color(1.0, 1.0, 1.0, 0.72)) draw_rect(marker_rect, Color(0.025, 0.028, 0.034, 0.88)) draw_rect(marker_rect, color, false, 1.4) - _draw_ink_text(font, marker_rect.position + Vector2(0, 13), str(marker.get("text", "표")), HORIZONTAL_ALIGNMENT_CENTER, marker_rect.size.x, 12, color) + _draw_target_marker_label(font, marker_rect, str(marker.get("text", "표")), color, str(marker.get("icon", ""))) func _draw_auto_attack_origin_marker(marker: Dictionary, target_color: Color, font: Font) -> void: @@ -5896,7 +5897,28 @@ func _draw_auto_attack_origin_marker(marker: Dictionary, target_color: Color, fo _draw_tile_marker("move", origin_rect, Color(1.0, 1.0, 1.0, 0.56)) draw_rect(marker_rect, Color(0.025, 0.028, 0.034, 0.86)) draw_rect(marker_rect, move_color, false, 1.2) - _draw_ink_text(font, marker_rect.position + Vector2(0, 13), "진", HORIZONTAL_ALIGNMENT_CENTER, marker_rect.size.x, 12, move_color) + _draw_target_marker_label(font, marker_rect, "진", move_color, "move") + + +func _draw_target_marker_label(font: Font, marker_rect: Rect2, text: String, color: Color, icon_kind: String = "") -> void: + var resolved_icon := icon_kind + if resolved_icon.is_empty(): + resolved_icon = _target_preview_badge_icon_kind("damage") + var icon_texture := _make_toolbar_icon(resolved_icon) + var icon_size := Vector2(13.0, 13.0) + var icon_space := icon_size.x + 4.0 if icon_texture != null else 0.0 + if icon_texture != null: + var icon_rect := Rect2(marker_rect.position + Vector2(3.0, (marker_rect.size.y - icon_size.y) * 0.5), icon_size) + draw_texture_rect(icon_texture, icon_rect, false, Color(1.0, 1.0, 1.0, 0.92)) + _draw_ink_text( + font, + marker_rect.position + Vector2(icon_space, 13.0), + text, + HORIZONTAL_ALIGNMENT_LEFT if icon_texture != null else HORIZONTAL_ALIGNMENT_CENTER, + marker_rect.size.x - icon_space - 2.0, + 12, + color + ) func _auto_attack_origin_marker_rect(origin: Vector2i) -> Rect2: @@ -5958,11 +5980,13 @@ func _attack_target_marker_entries() -> Array[Dictionary]: var result_text := "퇴각" if bool(preview.get("would_defeat", false)) else "-%d" % int(preview.get("damage", 0)) var directional_text := _format_directional_badge_suffix(preview) var marker_text := "%s%s" % [result_text, directional_text] if not directional_text.is_empty() else "%s %d%%" % [result_text, hit_chance] + var marker_kind := _attack_preview_badge_kind(preview) result.append({ "cell": cell, "target_id": str(target.get("id", "")), "text": marker_text, - "kind": _attack_preview_badge_kind(preview) + "kind": marker_kind, + "icon": _target_preview_badge_icon_kind(marker_kind) }) if not basic_attack_targeting and not _has_pending_move(): _append_auto_attack_target_marker_entries(result, selected) @@ -5990,11 +6014,13 @@ func _append_auto_attack_target_marker_entries(result: Array[Dictionary], select var preview := _auto_attack_preview_for_origin(selected, target, attack_origin) if preview.is_empty(): continue + var marker_kind := _auto_attack_preview_badge_kind(preview) result.append({ "cell": target_cell, "target_id": target_id, "text": "행공", - "kind": _auto_attack_preview_badge_kind(preview), + "kind": marker_kind, + "icon": _target_preview_badge_icon_kind(marker_kind), "origin": attack_origin, "requires_move": true, "damage": int(preview.get("damage", 0)), @@ -6023,11 +6049,13 @@ func _skill_target_marker_entries() -> Array[Dictionary]: var badge := _skill_target_preview_badge_for_cell(selected, target, cell) if badge.is_empty() or str(badge.get("kind", "")) == "invalid": continue + var marker_kind := str(badge.get("kind", "support")) result.append({ "cell": cell, "target_id": str(target.get("id", "")) if not target.is_empty() else "", "text": str(badge.get("text", "표")), - "kind": str(badge.get("kind", "support")) + "kind": marker_kind, + "icon": str(badge.get("icon", _target_preview_badge_icon_kind(marker_kind))) }) return result @@ -6044,11 +6072,13 @@ func _item_target_marker_entries() -> Array[Dictionary]: var badge := _item_target_preview_badge_for_cell(selected, target, cell) if badge.is_empty() or str(badge.get("kind", "")) == "invalid": continue + var marker_kind := str(badge.get("kind", "support")) result.append({ "cell": cell, "target_id": str(target.get("id", "")) if not target.is_empty() else "", "text": str(badge.get("text", "표")), - "kind": str(badge.get("kind", "support")) + "kind": marker_kind, + "icon": str(badge.get("icon", _target_preview_badge_icon_kind(marker_kind))) }) return result @@ -7132,10 +7162,26 @@ func _draw_target_preview_badge() -> void: var background := Color(0.025, 0.028, 0.034, 0.88) var font := _draw_ui_font(true) var text := str(badge.get("text", "")) + var icon_texture := _target_preview_badge_icon_texture(kind) + var icon_space := TARGET_PREVIEW_BADGE_ICON_SIZE.x + 8.0 if icon_texture != null else 0.0 draw_rect(badge_rect, background) draw_rect(badge_rect, color, false, 2.0) - _draw_ink_text(font, badge_rect.position + Vector2(0, 16), text, HORIZONTAL_ALIGNMENT_CENTER, badge_rect.size.x, 14, color) + if icon_texture != null: + var icon_rect := Rect2( + badge_rect.position + Vector2(5.0, (badge_rect.size.y - TARGET_PREVIEW_BADGE_ICON_SIZE.y) * 0.5), + TARGET_PREVIEW_BADGE_ICON_SIZE + ) + draw_texture_rect(icon_texture, icon_rect, false, Color(1.0, 1.0, 1.0, 0.96)) + _draw_ink_text( + font, + badge_rect.position + Vector2(icon_space, 17.0), + text, + HORIZONTAL_ALIGNMENT_LEFT if icon_texture != null else HORIZONTAL_ALIGNMENT_CENTER, + badge_rect.size.x - icon_space - 4.0, + 14, + color + ) func _draw_hover_info_badge() -> void: @@ -7590,10 +7636,39 @@ func _make_target_preview_badge_at(text: String, kind: String, cell: Vector2i) - return { "text": text, "kind": kind, - "cell": cell + "cell": cell, + "icon": _target_preview_badge_icon_kind(kind) } +func _target_preview_badge_icon_kind(kind: String) -> String: + var normalized := kind.strip_edges().to_lower() + if normalized == "move": + return "move" + if normalized == "select": + return "status" + if normalized == "heal": + return "item" + if normalized == "mp": + return "tactic" + if normalized == "support" or normalized == "debuff" or normalized == "poison" or normalized == "seal" or normalized == "snare": + return "status" + if normalized == "counter" or normalized == "danger": + return "threat" + if normalized == "invalid": + return "cancel" + if normalized == "advance": + return "move" + return "attack" + + +func _target_preview_badge_icon_texture(kind: String) -> Texture2D: + var icon_kind := _target_preview_badge_icon_kind(kind) + if icon_kind.is_empty(): + return null + return _make_toolbar_icon(icon_kind) + + func _compact_support_preview_text(effect_text: String) -> String: var parts := [] for raw_part in effect_text.split(","): diff --git a/tools/smoke_post_move_action_flow.gd b/tools/smoke_post_move_action_flow.gd index ee5a9b5..c80968b 100644 --- a/tools/smoke_post_move_action_flow.gd +++ b/tools/smoke_post_move_action_flow.gd @@ -500,6 +500,8 @@ func _check_scene_post_move_attack_targeting(failures: Array[String]) -> void: var markers := scene._attack_target_marker_entries() if markers.size() != 1 or markers[0].get("cell", Vector2i.ZERO) != Vector2i(3, 3): failures.append("attack target markers should include only the reachable enemy: %s" % str(markers)) + elif str(markers[0].get("icon", "")).strip_edges().is_empty(): + failures.append("attack target markers should carry generated icon hints: %s" % str(markers)) scene._on_targeting_back_pressed() cao_cao = scene.state.get_unit("cao_cao") @@ -579,6 +581,8 @@ func _check_scene_enemy_click_auto_attack(failures: Array[String]) -> void: failures.append("move+attack marker origin should differ from the current cell: %s" % str(marker)) if not bool(marker.get("requires_move", false)): failures.append("move+attack marker should flag that movement is required: %s" % str(marker)) + if str(marker.get("icon", "")).strip_edges().is_empty(): + failures.append("move+attack marker should carry a generated icon hint: %s" % str(marker)) if not found_auto_marker: failures.append("selected unit should mark enemies reachable by move+attack: %s" % str(auto_markers)) scene.hover_cell = Vector2i(4, 3) @@ -587,6 +591,8 @@ func _check_scene_enemy_click_auto_attack(failures: Array[String]) -> void: failures.append("hovering a move+attack target should explain the combined action: %s" % str(auto_badge)) if not ["advance", "counter", "danger"].has(str(auto_badge.get("kind", ""))): failures.append("move+attack hover badge should use an attack-risk color kind: %s" % str(auto_badge)) + if str(auto_badge.get("icon", "")).strip_edges().is_empty(): + failures.append("move+attack hover badge should carry a generated icon hint: %s" % str(auto_badge)) cao_cao["status_effects"] = [{ "type": "action_lock", @@ -859,6 +865,8 @@ func _check_scene_post_move_tactic_picker_flow(failures: Array[String]) -> void: if tactic_markers.size() != 1 or tactic_markers[0].get("cell", Vector2i.ZERO) != Vector2i(3, 3): failures.append("tactic target markers should include only the Spark target: %s" % str(tactic_markers)) else: + if str(tactic_markers[0].get("icon", "")).strip_edges().is_empty(): + failures.append("tactic target marker should carry a generated icon hint: %s" % str(tactic_markers[0])) if str(tactic_markers[0].get("text", "")) != "퇴각": failures.append("tactic target marker should preview the deterministic KO: %s" % str(tactic_markers[0])) if str(tactic_markers[0].get("kind", "")) != "damage": @@ -957,6 +965,8 @@ func _check_scene_post_move_item_picker_flow(failures: Array[String]) -> void: if item_markers.size() != 1 or item_markers[0].get("cell", Vector2i.ZERO) != Vector2i(2, 4): failures.append("item target markers should include only the Bean target: %s" % str(item_markers)) else: + if str(item_markers[0].get("icon", "")).strip_edges().is_empty(): + failures.append("item target marker should carry a generated icon hint: %s" % str(item_markers[0])) if str(item_markers[0].get("text", "")) != "+20 병력": failures.append("item target marker should preview healing: %s" % str(item_markers[0])) if str(item_markers[0].get("kind", "")) != "heal": diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index a7ac04d..36e79d4 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -4260,6 +4260,8 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: var move_badge := scene._target_preview_badge() if str(move_badge.get("text", "")) != "행군" or str(move_badge.get("kind", "")) != "move": failures.append("reachable empty tile should show MOVE badge: %s" % str(move_badge)) + if str(move_badge.get("icon", "")) != "move": + failures.append("MOVE badge should carry a generated icon hint: %s" % str(move_badge)) if not scene._hover_info_badge().is_empty(): failures.append("hover info badge should yield to active move intent badge") @@ -4267,6 +4269,8 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: var select_badge := scene._target_preview_badge() if str(select_badge.get("text", "")) != "선택" or str(select_badge.get("kind", "")) != "select": failures.append("friendly selectable unit should show SELECT badge: %s" % str(select_badge)) + if str(select_badge.get("icon", "")).strip_edges().is_empty(): + failures.append("SELECT badge should carry a generated icon hint: %s" % str(select_badge)) scene.hover_cell = Vector2i(1, 5) var attack_badge := scene._target_preview_badge() @@ -4276,6 +4280,8 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: failures.append("adjacent attack badge should expose counterattack risk: %s" % str(attack_badge)) if str(attack_badge.get("kind", "")) != "counter" and str(attack_badge.get("kind", "")) != "danger": failures.append("counterable attack badge should use a counter danger color kind: %s" % str(attack_badge)) + if str(attack_badge.get("icon", "")) != "threat": + failures.append("counterable attack badge should carry a threat icon hint: %s" % str(attack_badge)) var cao_cao: Dictionary = scene.state.get_unit("cao_cao") cao_cao["moved"] = true @@ -4302,6 +4308,8 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: failures.append("edge target preview badge should stay inside visible board maximum: %s / %s" % [str(edge_badge_rect), str(board_rect)]) if edge_badge_rect.size.x <= BattleSceneScript.TILE_SIZE: failures.append("target preview badge should remain wide enough for Korean tactical text") + if edge_badge_rect.size.x < 120.0: + failures.append("target preview badge should reserve room for icon and Korean text") scene.state.clear_selection() scene.selected_skill_id = ""