Add tactic and item target markers

This commit is contained in:
2026-06-18 22:26:55 +09:00
parent 493496fc6f
commit 3273ec33ec
2 changed files with 131 additions and 31 deletions

View File

@@ -259,7 +259,7 @@ func _draw() -> void:
_draw_map()
_draw_overlays()
_draw_units()
_draw_attack_target_markers()
_draw_target_selection_markers()
_draw_attack_effects()
_draw_target_preview_badge()
_draw_floating_texts()
@@ -1728,24 +1728,34 @@ func _draw_units() -> void:
draw_arc(center, 29, 0.0, TAU, 48, Color(1.0, 0.92, 0.25), 3.0)
func _draw_attack_target_markers() -> void:
if not basic_attack_targeting:
func _draw_target_selection_markers() -> void:
if not _is_targeting_mode():
return
var font := ThemeDB.fallback_font
for marker in _attack_target_marker_entries():
for marker in _target_selection_marker_entries():
var cell: Vector2i = marker.get("cell", Vector2i(-1, -1))
if not state.is_inside(cell):
continue
var rect := _rect_for_cell(cell)
var marker_rect := Rect2(rect.position + Vector2(7.0, TILE_SIZE - 24.0), Vector2(TILE_SIZE - 14.0, 18.0))
var color := _target_preview_badge_color(str(marker.get("kind", "damage")))
draw_rect(rect.grow(-3.0), Color(1.0, 0.22, 0.16, 0.18))
draw_rect(rect.grow(-3.0), Color(color.r, color.g, color.b, 0.18))
draw_rect(rect.grow(-6.0), color, false, 2.2)
draw_rect(marker_rect, Color(0.025, 0.028, 0.034, 0.88))
draw_rect(marker_rect, color, false, 1.4)
draw_string(font, marker_rect.position + Vector2(0, 13), str(marker.get("text", "TARGET")), HORIZONTAL_ALIGNMENT_CENTER, marker_rect.size.x, 12, color)
func _target_selection_marker_entries() -> Array[Dictionary]:
if basic_attack_targeting:
return _attack_target_marker_entries()
if not selected_skill_id.is_empty():
return _skill_target_marker_entries()
if not selected_item_id.is_empty():
return _item_target_marker_entries()
return []
func _attack_target_marker_entries() -> Array[Dictionary]:
var result: Array[Dictionary] = []
if not basic_attack_targeting:
@@ -1771,6 +1781,48 @@ func _attack_target_marker_entries() -> Array[Dictionary]:
return result
func _skill_target_marker_entries() -> Array[Dictionary]:
var result: Array[Dictionary] = []
if selected_skill_id.is_empty():
return result
var selected := state.get_selected_unit()
if selected.is_empty():
return result
for cell in skill_cells:
var target := state.get_unit_at(cell)
var badge := _skill_target_preview_badge_for_cell(selected, target, cell)
if badge.is_empty() or str(badge.get("kind", "")) == "invalid":
continue
result.append({
"cell": cell,
"target_id": str(target.get("id", "")) if not target.is_empty() else "",
"text": str(badge.get("text", "TARGET")),
"kind": str(badge.get("kind", "support"))
})
return result
func _item_target_marker_entries() -> Array[Dictionary]:
var result: Array[Dictionary] = []
if selected_item_id.is_empty():
return result
var selected := state.get_selected_unit()
if selected.is_empty():
return result
for cell in item_cells:
var target := state.get_unit_at(cell)
var badge := _item_target_preview_badge_for_cell(selected, target, cell)
if badge.is_empty() or str(badge.get("kind", "")) == "invalid":
continue
result.append({
"cell": cell,
"target_id": str(target.get("id", "")) if not target.is_empty() else "",
"text": str(badge.get("text", "TARGET")),
"kind": str(badge.get("kind", "support"))
})
return result
func _has_basic_attack_target(unit_id: String) -> bool:
var unit := state.get_unit(unit_id)
if unit.is_empty():
@@ -2235,18 +2287,22 @@ func _attack_target_preview_badge(selected: Dictionary, target: Dictionary) -> D
func _skill_target_preview_badge(selected: Dictionary, target: Dictionary) -> Dictionary:
var preview := state.get_skill_preview(selected["id"], selected_skill_id, hover_cell)
return _skill_target_preview_badge_for_cell(selected, target, hover_cell)
func _skill_target_preview_badge_for_cell(selected: Dictionary, target: Dictionary, cell: Vector2i) -> Dictionary:
var preview := state.get_skill_preview(selected["id"], selected_skill_id, cell)
if preview.is_empty():
return {}
if bool(preview.get("skill_locked", false)):
return _make_target_preview_badge("SEALED", "seal")
return _make_target_preview_badge_at("SEALED", "seal", cell)
if not bool(preview.get("in_range", false)):
return _make_target_preview_badge("OUT", "invalid")
return _make_target_preview_badge_at("OUT", "invalid", cell)
if not bool(preview.get("has_mp", false)):
return _make_target_preview_badge("NO MP", "invalid")
return _make_target_preview_badge_at("NO MP", "invalid", cell)
if not bool(preview.get("valid_target", false)):
return _make_target_preview_badge("NO TGT" if bool(preview.get("has_area", false)) else "INVALID", "invalid")
return _make_target_preview_badge_at("NO TGT" if bool(preview.get("has_area", false)) else "INVALID", "invalid", cell)
if target.is_empty() and not bool(preview.get("has_area", false)):
return {}
@@ -2256,50 +2312,54 @@ func _skill_target_preview_badge(selected: Dictionary, target: Dictionary) -> Di
var heal := int(preview.get("heal", 0))
var total_heal := int(preview.get("total_heal", heal))
if total_heal <= 0:
return _make_target_preview_badge("FULL", "invalid")
return _make_target_preview_badge_at("FULL", "invalid", cell)
if bool(preview.get("has_area", false)):
return _make_target_preview_badge("+%d x%d" % [total_heal, target_count], "heal")
return _make_target_preview_badge("+%d HP" % heal, "heal")
return _make_target_preview_badge_at("+%d x%d" % [total_heal, target_count], "heal", cell)
return _make_target_preview_badge_at("+%d HP" % heal, "heal", cell)
if kind == "support":
var effect_text := str(preview.get("effect_text", "Support"))
var badge_kind := "disarm" if effect_text.contains("Disarm") else ("snare" if effect_text.contains("Snare") else ("seal" if effect_text.contains("Seal") else ("poison" if effect_text.contains("Poison") else ("debuff" if effect_text.contains("-") else "support"))))
var badge_kind := _support_preview_badge_kind(effect_text)
if bool(preview.get("has_area", false)):
return _make_target_preview_badge("%s x%d" % [_compact_support_preview_text(effect_text), target_count], badge_kind)
return _make_target_preview_badge(_compact_support_preview_text(effect_text), badge_kind)
return _make_target_preview_badge_at("%s x%d" % [_compact_support_preview_text(effect_text), target_count], badge_kind, cell)
return _make_target_preview_badge_at(_compact_support_preview_text(effect_text), badge_kind, cell)
if bool(preview.get("has_area", false)):
var defeat_count := int(preview.get("defeat_count", 0))
if defeat_count > 0:
return _make_target_preview_badge("KO x%d" % defeat_count, "damage")
return _make_target_preview_badge("-%d x%d" % [int(preview.get("total_damage", 0)), target_count], "damage")
return _make_target_preview_badge_at("KO x%d" % defeat_count, "damage", cell)
return _make_target_preview_badge_at("-%d x%d" % [int(preview.get("total_damage", 0)), target_count], "damage", cell)
var result_text := "KO" if bool(preview.get("would_defeat", false)) else "-%d" % int(preview.get("damage", 0))
return _make_target_preview_badge(result_text, "damage")
return _make_target_preview_badge_at(result_text, "damage", cell)
func _item_target_preview_badge(selected: Dictionary, target: Dictionary) -> Dictionary:
var preview := state.get_item_preview(selected["id"], selected_item_id, hover_cell)
return _item_target_preview_badge_for_cell(selected, target, hover_cell)
func _item_target_preview_badge_for_cell(selected: Dictionary, target: Dictionary, cell: Vector2i) -> Dictionary:
var preview := state.get_item_preview(selected["id"], selected_item_id, cell)
if preview.is_empty() or target.is_empty():
return {}
if not bool(preview.get("valid_target", false)):
return _make_target_preview_badge("INVALID", "invalid")
return _make_target_preview_badge_at("INVALID", "invalid", cell)
if not bool(preview.get("in_range", false)):
return _make_target_preview_badge("OUT", "invalid")
return _make_target_preview_badge_at("OUT", "invalid", cell)
if int(preview.get("count", 0)) <= 0:
return _make_target_preview_badge("NONE", "invalid")
return _make_target_preview_badge_at("NONE", "invalid", cell)
var hp_heal := int(preview.get("heal", 0))
var mp_heal := int(preview.get("mp_heal", 0))
var cure_statuses: Array = preview.get("cure_statuses", [])
if hp_heal <= 0 and mp_heal <= 0 and cure_statuses.is_empty():
return _make_target_preview_badge("NO FX", "invalid")
return _make_target_preview_badge_at("NO FX", "invalid", cell)
if not cure_statuses.is_empty():
return _make_target_preview_badge("CURE", "support")
return _make_target_preview_badge_at("CURE", "support", cell)
if hp_heal > 0 and mp_heal > 0:
return _make_target_preview_badge("+%d/%d" % [hp_heal, mp_heal], "heal")
return _make_target_preview_badge_at("+%d/%d" % [hp_heal, mp_heal], "heal", cell)
if hp_heal > 0:
return _make_target_preview_badge("+%d HP" % hp_heal, "heal")
return _make_target_preview_badge("+%d MP" % mp_heal, "mp")
return _make_target_preview_badge_at("+%d HP" % hp_heal, "heal", cell)
return _make_target_preview_badge_at("+%d MP" % mp_heal, "mp", cell)
func _hover_intent_preview_badge(selected: Dictionary, target: Dictionary) -> Dictionary:
@@ -2321,12 +2381,16 @@ func _hover_intent_preview_badge(selected: Dictionary, target: Dictionary) -> Di
func _make_target_preview_badge(text: String, kind: String) -> Dictionary:
return _make_target_preview_badge_at(text, kind, hover_cell)
func _make_target_preview_badge_at(text: String, kind: String, cell: Vector2i) -> Dictionary:
if text.is_empty():
return {}
return {
"text": text,
"kind": kind,
"cell": hover_cell
"cell": cell
}
@@ -2349,6 +2413,20 @@ func _compact_support_preview_text(effect_text: String) -> String:
return str(parts[0])
func _support_preview_badge_kind(effect_text: String) -> String:
if effect_text.contains("Disarm"):
return "disarm"
if effect_text.contains("Snare"):
return "snare"
if effect_text.contains("Seal"):
return "seal"
if effect_text.contains("Poison"):
return "poison"
if effect_text.contains("-"):
return "debuff"
return "support"
func _target_preview_badge_color(kind: String) -> Color:
if kind == "damage":
return Color(1.0, 0.38, 0.26)
@@ -5766,9 +5844,15 @@ func _targeting_hint_detail_text() -> String:
return "No enemy is in range."
return "%d enemy target%s. Click a marked enemy." % [markers.size(), "" if markers.size() == 1 else "s"]
if not selected_skill_id.is_empty():
return "Click a highlighted tactic cell."
var markers := _skill_target_marker_entries()
if markers.is_empty():
return "No valid tactic target."
return "%d tactic target%s. Click a marked cell." % [markers.size(), "" if markers.size() == 1 else "s"]
if not selected_item_id.is_empty():
return "Click a highlighted item target."
var markers := _item_target_marker_entries()
if markers.is_empty():
return "No valid item target."
return "%d item target%s. Click a marked unit." % [markers.size(), "" if markers.size() == 1 else "s"]
return "Choose a highlighted target."

View File

@@ -281,6 +281,14 @@ func _check_scene_post_move_tactic_picker_flow(failures: Array[String]) -> void:
failures.append("targeting hint panel should be visible during tactic targeting")
if not scene.skill_cells.has(Vector2i(3, 3)):
failures.append("Spark targeting should highlight the adjacent enemy cell")
var tactic_markers := scene._skill_target_marker_entries()
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("text", "")) != "KO":
failures.append("tactic target marker should preview the deterministic KO: %s" % str(tactic_markers[0]))
if str(tactic_markers[0].get("kind", "")) != "damage":
failures.append("tactic target marker should use damage color: %s" % str(tactic_markers[0]))
scene._on_targeting_back_pressed()
cao_cao = scene.state.get_unit("cao_cao")
@@ -368,6 +376,14 @@ func _check_scene_post_move_item_picker_flow(failures: Array[String]) -> void:
failures.append("targeting hint panel should be visible during item targeting")
if not scene.item_cells.has(Vector2i(2, 4)):
failures.append("Bean targeting should highlight the adjacent injured ally")
var item_markers := scene._item_target_marker_entries()
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("text", "")) != "+20 HP":
failures.append("item target marker should preview healing: %s" % str(item_markers[0]))
if str(item_markers[0].get("kind", "")) != "heal":
failures.append("item target marker should use heal color: %s" % str(item_markers[0]))
scene._on_targeting_back_pressed()
cao_cao = scene.state.get_unit("cao_cao")