From a039ca6037eb96875314ce6baf6f5ad2a4e71a4a Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 13:01:14 +0900 Subject: [PATCH] Preview enemy reactions on move cells --- scripts/scenes/battle_scene.gd | 49 +++++++++++++++++++++++++++++++++- tools/smoke_visual_assets.gd | 29 +++++++++++++++++++- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 8b0659c..e6cf175 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -6542,6 +6542,14 @@ func _ai_zone_cells_for_unit(unit: Dictionary) -> Array[Vector2i]: return cells +func _ai_zone_contains_cell(unit: Dictionary, cell: Vector2i) -> bool: + var radius := _ai_zone_radius_for_unit(unit) + var anchor := _ai_zone_anchor_for_unit(unit) + if radius < 0 or not state.is_inside(anchor) or not state.is_inside(cell): + return false + return absi(cell.x - anchor.x) + absi(cell.y - anchor.y) <= radius + + func _ai_zone_anchor_for_unit(unit: Dictionary) -> Vector2i: var behavior := str(unit.get("ai_behavior", "")).strip_edges().to_lower() var fallback: Vector2i = unit.get("pos", Vector2i(-1, -1)) @@ -8618,10 +8626,49 @@ func _hover_intent_preview_badge(selected: Dictionary, target: Dictionary) -> Di return _make_target_preview_badge("선택", "select") return {} if move_cells.has(hover_cell): - return _make_target_preview_badge("행군", "move") + return _move_cell_preview_badge(hover_cell) return {} +func _move_cell_preview_badge(cell: Vector2i) -> Dictionary: + var sentry_names := _ai_reaction_unit_names_for_cell(cell, "sentry") + if not sentry_names.is_empty(): + var lure_badge := _make_target_preview_badge_at("유인", "advance", cell) + lure_badge["tooltip"] = "초병 반응권: %s" % _compact_name_list(sentry_names, 2) + return lure_badge + var guard_names := _ai_reaction_unit_names_for_cell(cell, "guard") + if not guard_names.is_empty(): + var guard_badge := _make_target_preview_badge_at("경계", "select", cell) + guard_badge["tooltip"] = "거점 경계권: %s" % _compact_name_list(guard_names, 2) + return guard_badge + var threat_names := state.get_threatening_unit_names(cell, BattleState.TEAM_ENEMY) + if not threat_names.is_empty(): + var danger_badge := _make_target_preview_badge_at("위험", "danger", cell) + danger_badge["tooltip"] = "현재 적 공격권: %s" % _compact_name_list(threat_names, 2) + return danger_badge + return _make_target_preview_badge_at("행군", "move", cell) + + +func _ai_reaction_unit_names_for_cell(cell: Vector2i, behavior: String) -> Array[String]: + var names: Array[String] = [] + var normalized_behavior := behavior.strip_edges().to_lower() + if normalized_behavior.is_empty() or not state.is_inside(cell): + return names + for unit in state.get_living_units(BattleState.TEAM_ENEMY): + if not bool(unit.get("deployed", true)): + continue + if str(unit.get("ai_behavior", "")).strip_edges().to_lower() != normalized_behavior: + continue + if normalized_behavior == "sentry" and bool(unit.get("ai_awake", false)): + continue + if not _ai_zone_contains_cell(unit, cell): + continue + var unit_name := str(unit.get("name", unit.get("id", "부대"))) + if not names.has(unit_name): + names.append(unit_name) + return names + + func _make_target_preview_badge(text: String, kind: String) -> Dictionary: return _make_target_preview_badge_at(text, kind, hover_cell) diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index 84c9d48..c4693db 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -4743,10 +4743,14 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: return scene.battle_started = true var enemy: Dictionary = scene.state.get_unit("yellow_turban_1") - enemy["pos"] = Vector2i(1, 5) scene.state.select_unit("cao_cao") scene._refresh_ranges() + var enemy_alive_states := {} + for foe in scene.state.get_living_units(BattleStateScript.TEAM_ENEMY): + enemy_alive_states[str(foe.get("id", ""))] = bool(foe.get("alive", false)) + foe["alive"] = false + scene._refresh_ranges() scene.hover_cell = Vector2i(2, 5) var move_badge := scene._target_preview_badge() if str(move_badge.get("text", "")) != "행군" or str(move_badge.get("kind", "")) != "move": @@ -4757,6 +4761,19 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: failures.append("MOVE badge should use generated jade panel metadata: %s" % str(move_badge)) if not scene._hover_info_badge().is_empty(): failures.append("hover info badge should yield to active move intent badge") + for unit_id in enemy_alive_states.keys(): + var restored_enemy: Dictionary = scene.state.get_unit(str(unit_id)) + if not restored_enemy.is_empty(): + restored_enemy["alive"] = bool(enemy_alive_states[unit_id]) + scene._refresh_ranges() + enemy["pos"] = Vector2i(1, 5) + scene._refresh_ranges() + scene.hover_cell = Vector2i(2, 5) + var danger_move_badge := scene._target_preview_badge() + if str(danger_move_badge.get("text", "")) != "위험" or str(danger_move_badge.get("kind", "")) != "danger": + failures.append("moving into current enemy attack range should show DANGER badge: %s" % str(danger_move_badge)) + if not str(danger_move_badge.get("tooltip", "")).contains("현재 적 공격권"): + failures.append("danger move badge should explain current enemy attack range: %s" % str(danger_move_badge)) scene.hover_cell = Vector2i(1, 7) var select_badge := scene._target_preview_badge() @@ -4830,6 +4847,11 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: failures.append("sleeping sentry hover badge should expose lure timing compactly: %s" % str(sentry_hover_badge)) if not sentry_hover_text.contains("유인 밖 4칸"): failures.append("sleeping sentry hover badge should expose current lure distance: %s" % str(sentry_hover_badge)) + var lure_move_badge := scene._move_cell_preview_badge(Vector2i(5, 6)) + if str(lure_move_badge.get("text", "")) != "유인" or str(lure_move_badge.get("kind", "")) != "advance": + failures.append("move preview should mark cells that wake sleeping sentries: %s" % str(lure_move_badge)) + if not str(lure_move_badge.get("tooltip", "")).contains("초병 반응권"): + failures.append("lure move badge should explain sentry reaction zone: %s" % str(lure_move_badge)) central_sentry["ai_awake"] = true var awake_sentry_hover_badge := scene._hover_info_badge() if not str(awake_sentry_hover_badge.get("lines", [])).contains("초병 경계"): @@ -4856,6 +4878,11 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: var guard_hover_text := str(guard_hover_badge.get("lines", [])) if not guard_hover_text.contains("수비") or not guard_hover_text.contains("거점 밖 4칸"): failures.append("guard hover badge should expose current guard-zone distance: %s" % str(guard_hover_badge)) + var guard_move_badge := scene._move_cell_preview_badge(Vector2i(4, 5)) + if str(guard_move_badge.get("text", "")) != "경계" or str(guard_move_badge.get("kind", "")) != "select": + failures.append("move preview should mark guard reaction cells: %s" % str(guard_move_badge)) + if not str(guard_move_badge.get("tooltip", "")).contains("거점 경계권"): + failures.append("guard move badge should explain guard reaction zone: %s" % str(guard_move_badge)) var hovered_guard: Dictionary = scene._hovered_ai_zone_unit() if str(hovered_guard.get("id", "")) != "yellow_turban_4": failures.append("hovered guard should expose a drawable AI zone: %s" % str(hovered_guard))