From 48160decbbe9b54fb6f764f2ba6cd464ae04f734 Mon Sep 17 00:00:00 2001 From: Wickedness Date: Sat, 20 Jun 2026 12:51:08 +0900 Subject: [PATCH] Show enemy AI reaction zones --- scripts/scenes/battle_scene.gd | 113 +++++++++++++++++++++++++++++++++ tools/smoke_visual_assets.gd | 29 +++++++++ 2 files changed, 142 insertions(+) diff --git a/scripts/scenes/battle_scene.gd b/scripts/scenes/battle_scene.gd index 7d40ebe..8b0659c 100644 --- a/scripts/scenes/battle_scene.gd +++ b/scripts/scenes/battle_scene.gd @@ -6428,6 +6428,8 @@ func _draw_overlays() -> void: _draw_objective_seal_marker(objective_rect) _draw_tactical_tile_overlay("objective", objective_rect, OBJECTIVE_OVERLAY_COLOR, UI_SEAL_RED, 0.56, 0.30, 6.0) + _draw_hovered_ai_zone_overlay() + for cell in threat_cells: var threat_rect := _rect_for_cell(cell) _draw_tactical_tile_overlay("attack", threat_rect, THREAT_OVERLAY_COLOR, THREAT_BORDER_COLOR, 0.30, 0.42, 6.0) @@ -6484,6 +6486,117 @@ func _draw_overlays() -> void: _draw_tactical_tile_overlay(hover_marker, hover_rect, hover_color, hover_color, 0.44, 0.22, 4.0) +func _draw_hovered_ai_zone_overlay() -> void: + var unit := _hovered_ai_zone_unit() + if unit.is_empty(): + return + var cells := _ai_zone_cells_for_unit(unit) + if cells.is_empty(): + return + var anchor := _ai_zone_anchor_for_unit(unit) + var radius := _ai_zone_radius_for_unit(unit) + var marker_key := _ai_zone_overlay_marker_key(unit) + var fill := _ai_zone_overlay_fill_color(unit) + var border := _ai_zone_overlay_border_color(unit) + var fade_base := float(maxi(1, radius)) + for cell in cells: + if not _cell_is_in_visible_bounds(cell): + continue + var distance := absi(cell.x - anchor.x) + absi(cell.y - anchor.y) + var fade := 1.0 - clampf(float(distance) / fade_base, 0.0, 1.0) * 0.24 + var cell_fill := fill + cell_fill.a *= fade + var cell_border := border + cell_border.a *= fade + _draw_tactical_tile_overlay(marker_key, _rect_for_cell(cell), cell_fill, cell_border, 0.13 * fade, 0.36, 8.0) + if state.is_inside(anchor) and _cell_is_in_visible_bounds(anchor): + _draw_tactical_tile_overlay("target", _rect_for_cell(anchor), _ai_zone_anchor_fill_color(unit), _ai_zone_overlay_border_color(unit), 0.25, 0.40, 6.0) + + +func _hovered_ai_zone_unit() -> Dictionary: + if _is_input_locked() or not battle_started or not state.is_inside(hover_cell): + return {} + var unit := state.get_unit_at(hover_cell) + if unit.is_empty() or str(unit.get("team", "")) != BattleState.TEAM_ENEMY: + return {} + if _ai_zone_radius_for_unit(unit) < 0: + return {} + if not state.is_inside(_ai_zone_anchor_for_unit(unit)): + return {} + return unit + + +func _ai_zone_cells_for_unit(unit: Dictionary) -> Array[Vector2i]: + var cells: Array[Vector2i] = [] + 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): + return cells + for y in range(anchor.y - radius, anchor.y + radius + 1): + for x in range(anchor.x - radius, anchor.x + radius + 1): + var cell := Vector2i(x, y) + if not state.is_inside(cell): + continue + if absi(cell.x - anchor.x) + absi(cell.y - anchor.y) <= radius: + cells.append(cell) + return cells + + +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)) + var anchor: Vector2i = fallback + if behavior == "guard": + anchor = unit.get("ai_guard_anchor", fallback) + elif behavior == "sentry": + anchor = unit.get("ai_sentry_anchor", fallback) + if not state.is_inside(anchor): + return fallback + return anchor + + +func _ai_zone_radius_for_unit(unit: Dictionary) -> int: + var behavior := str(unit.get("ai_behavior", "")).strip_edges().to_lower() + if behavior == "guard": + return max(0, int(unit.get("ai_guard_radius", 0))) + if behavior == "sentry": + return max(0, int(unit.get("ai_sentry_radius", 0))) + return -1 + + +func _ai_zone_overlay_marker_key(unit: Dictionary) -> String: + var behavior := str(unit.get("ai_behavior", "")).strip_edges().to_lower() + if behavior == "guard": + return "select" + if behavior == "sentry" and bool(unit.get("ai_awake", false)): + return "attack" + return "move" + + +func _ai_zone_overlay_fill_color(unit: Dictionary) -> Color: + var behavior := str(unit.get("ai_behavior", "")).strip_edges().to_lower() + if behavior == "guard": + return Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.20) + if behavior == "sentry" and bool(unit.get("ai_awake", false)): + return Color(UI_SEAL_RED.r, UI_SEAL_RED.g, UI_SEAL_RED.b, 0.19) + return Color(UI_MUTED_JADE.r, UI_MUTED_JADE.g, UI_MUTED_JADE.b, 0.20) + + +func _ai_zone_anchor_fill_color(unit: Dictionary) -> Color: + var color := _ai_zone_overlay_fill_color(unit) + color.a = minf(color.a + 0.10, 0.34) + return color + + +func _ai_zone_overlay_border_color(unit: Dictionary) -> Color: + var behavior := str(unit.get("ai_behavior", "")).strip_edges().to_lower() + if behavior == "guard": + return Color(UI_OLD_BRONZE.r, UI_OLD_BRONZE.g, UI_OLD_BRONZE.b, 0.42) + if behavior == "sentry" and bool(unit.get("ai_awake", false)): + return Color(UI_SEAL_RED.r, UI_SEAL_RED.g, UI_SEAL_RED.b, 0.38) + return Color(UI_MUTED_JADE.r, UI_MUTED_JADE.g, UI_MUTED_JADE.b, 0.40) + + func _draw_units() -> void: for unit in state.units: if not unit.get("alive", false) or not unit.get("deployed", true): diff --git a/tools/smoke_visual_assets.gd b/tools/smoke_visual_assets.gd index c7c7a1f..84c9d48 100644 --- a/tools/smoke_visual_assets.gd +++ b/tools/smoke_visual_assets.gd @@ -4834,12 +4834,41 @@ func _check_hover_intent_badges(failures: Array[String]) -> void: var awake_sentry_hover_badge := scene._hover_info_badge() if not str(awake_sentry_hover_badge.get("lines", [])).contains("초병 경계"): failures.append("awake sentry hover badge should show the changed AI state: %s" % str(awake_sentry_hover_badge)) + var hovered_sentry: Dictionary = scene._hovered_ai_zone_unit() + if str(hovered_sentry.get("id", "")) != "yellow_turban_5": + failures.append("hovered sentry should expose a drawable AI zone: %s" % str(hovered_sentry)) + var sentry_zone_cells: Array[Vector2i] = scene._ai_zone_cells_for_unit(central_sentry) + if sentry_zone_cells.size() != 61: + failures.append("central sentry lure zone should be a radius-5 diamond, got %d cells" % sentry_zone_cells.size()) + if not sentry_zone_cells.has(Vector2i(10, 6)) or not sentry_zone_cells.has(Vector2i(5, 6)) or sentry_zone_cells.has(Vector2i(4, 6)): + failures.append("central sentry lure zone should include only cells within radius 5") + if scene._ai_zone_overlay_marker_key(central_sentry) != "attack": + failures.append("awake sentry AI zone should use attack-tinted marker artwork") + central_sentry["ai_awake"] = false + if scene._ai_zone_overlay_marker_key(central_sentry) != "move": + failures.append("sleeping sentry AI zone should use movement-tinted marker artwork") + var sentry_fill := scene._ai_zone_overlay_fill_color(central_sentry) + if sentry_fill.a > 0.24: + failures.append("sentry AI zone fill should stay subtle on top of terrain: %.2f" % sentry_fill.a) var village_guard: Dictionary = scene.state.get_unit("yellow_turban_4") scene.hover_cell = village_guard.get("pos", Vector2i(7, 5)) var guard_hover_badge := scene._hover_info_badge() 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 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)) + var guard_zone_cells: Array[Vector2i] = scene._ai_zone_cells_for_unit(village_guard) + if guard_zone_cells.size() != 25: + failures.append("village guard zone should be a radius-3 diamond, got %d cells" % guard_zone_cells.size()) + if not guard_zone_cells.has(Vector2i(7, 5)) or not guard_zone_cells.has(Vector2i(4, 5)) or guard_zone_cells.has(Vector2i(3, 5)): + failures.append("village guard zone should include only cells within radius 3") + if scene._ai_zone_overlay_marker_key(village_guard) != "select": + failures.append("guard AI zone should use select-tinted marker artwork") + var guard_fill := scene._ai_zone_overlay_fill_color(village_guard) + if guard_fill.a > 0.24: + failures.append("guard AI zone fill should stay subtle on top of terrain: %.2f" % guard_fill.a) scene.hover_cell = Vector2i(4, 2) var event_hover_badge := scene._hover_info_badge() if str(event_hover_badge.get("kind", "")) != "event" or not str(event_hover_badge.get("lines", [])).contains("표식 북숲 보급고"):