Add icons to target preview markers

This commit is contained in:
2026-06-20 05:48:22 +09:00
parent 059f79b0e4
commit 03e0837071
4 changed files with 103 additions and 9 deletions

View File

@@ -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.

View File

@@ -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(","):

View File

@@ -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":

View File

@@ -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 = ""