diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 543a4a6..1d54fa8 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -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." diff --git a/tools/smoke_post_move_action_flow.gd b/tools/smoke_post_move_action_flow.gd index 50be6d6..dcdfdbd 100644 --- a/tools/smoke_post_move_action_flow.gd +++ b/tools/smoke_post_move_action_flow.gd @@ -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")