feat: half update
This commit is contained in:
@@ -210,6 +210,11 @@ func _run_ai_tick():
|
||||
print("[BotController] Action Taken: Attack Pursuit")
|
||||
return
|
||||
|
||||
# Priority 0.5: Gauntlet (#075) — burn Cleanser if boxed in
|
||||
if await _try_activate_cleanser():
|
||||
print("[BotController] Action Taken: Cleanser (trapped)")
|
||||
return
|
||||
|
||||
# Priority 1: Tekton Management (Grab Tekton if full boost, or spawn if carrying)
|
||||
# Spawning while carrying is high priority; Hunting is medium priority.
|
||||
if await _try_tekton_action():
|
||||
@@ -254,6 +259,49 @@ func _run_ai_tick():
|
||||
elif not is_sng:
|
||||
return
|
||||
|
||||
# =============================================================================
|
||||
# Gauntlet (#075) — Cleanser + Sticky Avoidance wiring
|
||||
# =============================================================================
|
||||
|
||||
func _try_activate_cleanser() -> bool:
|
||||
"""Activate Cleanser when the planner reports imminent danger.
|
||||
|
||||
Server-authoritative RPC; we only request it. Returns true if the request
|
||||
was sent successfully (not a guarantee it landed on a sticky cell)."""
|
||||
if not strategic_planner or not strategic_planner.is_gauntlet_mode():
|
||||
return false
|
||||
if not strategic_planner.should_activate_cleanser_now():
|
||||
return false
|
||||
var gm = strategic_planner._get_gauntlet_manager()
|
||||
if not gm:
|
||||
return false
|
||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||
if pid == null or pid < 0:
|
||||
return false
|
||||
if gm.has_method("rpc_activate_cleanser"):
|
||||
gm.rpc_activate_cleanser(pid)
|
||||
print("[BotController] %s requested Cleanser activation (trapped)" % actor.name)
|
||||
return true
|
||||
return false
|
||||
|
||||
func _on_step_onto_unsafe() -> bool:
|
||||
"""Refuse to step onto a sticky/telegraphed cell and re-plan. Returns true
|
||||
if the bot had to abort the planned move."""
|
||||
if not strategic_planner:
|
||||
return false
|
||||
var here = actor.current_position if "current_position" in actor else Vector2i(-1, -1)
|
||||
if here == Vector2i(-1, -1):
|
||||
return false
|
||||
# Post-move guard: if we somehow landed on a sticky without cleanser active,
|
||||
# burn Cleanser to clear ourselves out next tick.
|
||||
if strategic_planner.is_gauntlet_mode() and strategic_planner._is_overlay_unsafe(here):
|
||||
if not strategic_planner._is_bot_cleanser_active():
|
||||
var gm = strategic_planner._get_gauntlet_manager()
|
||||
if gm and gm.has_method("is_sticky_cell") and gm.is_sticky_cell(here):
|
||||
print("[BotController] %s stepped onto sticky at %s — burning Cleanser" % [actor.name, here])
|
||||
return _try_activate_cleanser()
|
||||
return false
|
||||
|
||||
# =============================================================================
|
||||
# Power-Up / Sabotage
|
||||
# =============================================================================
|
||||
|
||||
@@ -8,11 +8,121 @@ class_name BotStrategicPlanner
|
||||
|
||||
var actor: Node3D
|
||||
var enhanced_gridmap: Node
|
||||
# Optional explicit gauntlet_manager binding (set by tests to avoid scene-tree
|
||||
# traversal collisions; production code uses _get_gauntlet_manager() instead).
|
||||
var gauntlet_manager_override: Node = null
|
||||
|
||||
# Tile type constants
|
||||
const GOAL_TILES = [7, 8, 9, 10] # Heart, Diamond, Star, Coin
|
||||
const HOLO_TILES = [11, 12, 13, 14] # Power-up holo tiles
|
||||
|
||||
# Gauntlet overlay layer (v2 ground-growth model — sticky/telegraph on layer 2).
|
||||
# Bots must avoid these cells or burn a Cleanser charge to cross.
|
||||
const GAUNTLET_OVERLAY_LAYER: int = 2
|
||||
const TILE_STICKY: int = 17
|
||||
const TILE_TELEGRAPH: int = 18
|
||||
|
||||
# =============================================================================
|
||||
# Gauntlet mode helpers (#075 — Bot AI: Sticky Avoidance & Pathfinding)
|
||||
# =============================================================================
|
||||
|
||||
func is_gauntlet_mode() -> bool:
|
||||
return LobbyManager and LobbyManager.is_game_mode(GameMode.Mode.GAUNTLET)
|
||||
|
||||
func _get_gauntlet_manager() -> Node:
|
||||
"""Resolve the active GauntletManager.
|
||||
|
||||
Order of resolution:
|
||||
1. Explicit `gauntlet_manager_override` (used by tests).
|
||||
2. Walk actor's ancestors for any node containing a GauntletManager child
|
||||
(production path — robust to non-standard scene trees).
|
||||
3. Fallback: scan /root children for a GauntletManager.
|
||||
"""
|
||||
if gauntlet_manager_override and is_instance_valid(gauntlet_manager_override):
|
||||
return gauntlet_manager_override
|
||||
var root: Node = null
|
||||
if actor and actor.is_inside_tree():
|
||||
root = actor.get_tree().get_root()
|
||||
# Walk actor's ancestors (handles production scenes where the bot is
|
||||
# nested under Main → Arena → Player).
|
||||
var n: Node = actor.get_parent()
|
||||
while n:
|
||||
var gm = n.get_node_or_null("GauntletManager")
|
||||
if gm:
|
||||
return gm
|
||||
n = n.get_parent()
|
||||
if not root:
|
||||
return null
|
||||
# Last-resort scan of root children (helps in unusual scene trees).
|
||||
for child in root.get_children():
|
||||
if child.name.begins_with("Main") or child.name.begins_with("BotTestMain"):
|
||||
var gm2 = child.get_node_or_null("GauntletManager")
|
||||
if gm2:
|
||||
return gm2
|
||||
return null
|
||||
|
||||
func _bot_has_cleanser_charge() -> bool:
|
||||
var gm = _get_gauntlet_manager()
|
||||
if not gm or not "player_cleansers" in gm:
|
||||
return false
|
||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||
if pid == null or pid < 0:
|
||||
return false
|
||||
return gm.player_cleansers.get(pid, 0) > 0
|
||||
|
||||
func _is_bot_cleanser_active() -> bool:
|
||||
var gm = _get_gauntlet_manager()
|
||||
if not gm:
|
||||
return false
|
||||
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||
if pid == null or pid < 0:
|
||||
return false
|
||||
return gm.is_cleanser_active(pid)
|
||||
|
||||
func _is_overlay_unsafe(pos: Vector2i) -> bool:
|
||||
"""True if the cell carries a sticky or telegraphed overlay on layer 2."""
|
||||
if not enhanced_gridmap:
|
||||
return false
|
||||
var item = enhanced_gridmap.get_cell_item(Vector3i(pos.x, GAUNTLET_OVERLAY_LAYER, pos.y))
|
||||
return item == TILE_STICKY or item == TILE_TELEGRAPH
|
||||
|
||||
func _is_cell_unsafe_in_gauntlet(pos: Vector2i) -> bool:
|
||||
"""Cell is unsafe in Gauntlet if it's sticky/telegraphed — unless the bot's
|
||||
Cleanser is active (grants temporary immunity)."""
|
||||
if not is_gauntlet_mode():
|
||||
return false
|
||||
if _is_bot_cleanser_active():
|
||||
return false
|
||||
var gm = _get_gauntlet_manager()
|
||||
if gm and gm.has_method("is_sticky_cell") and gm.is_sticky_cell(pos):
|
||||
return true
|
||||
return _is_overlay_unsafe(pos)
|
||||
|
||||
func _count_unsafe_neighbors(pos: Vector2i) -> int:
|
||||
"""Count 4-neighbors of `pos` that are sticky/telegraphed."""
|
||||
var count := 0
|
||||
for d in [Vector2i(1, 0), Vector2i(-1, 0), Vector2i(0, 1), Vector2i(0, -1)]:
|
||||
if _is_overlay_unsafe(pos + d):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
func should_activate_cleanser_now() -> bool:
|
||||
"""True if the bot is boxed in / about to be sealed and should burn Cleanser."""
|
||||
if not is_gauntlet_mode():
|
||||
return false
|
||||
if not _bot_has_cleanser_charge():
|
||||
return false
|
||||
if _is_bot_cleanser_active():
|
||||
return false
|
||||
var here = actor.current_position if actor and "current_position" in actor else Vector2i(-1, -1)
|
||||
if here == Vector2i(-1, -1):
|
||||
return false
|
||||
if _is_overlay_unsafe(here) and _count_unsafe_neighbors(here) >= 3:
|
||||
return true
|
||||
if _count_unsafe_neighbors(here) == 4:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _init(p_actor: Node3D, p_gridmap: Node):
|
||||
actor = p_actor
|
||||
enhanced_gridmap = p_gridmap
|
||||
@@ -509,6 +619,14 @@ func _is_valid_move_target(pos: Vector2i, ignore_players: bool = false) -> bool:
|
||||
|
||||
if not ignore_players and actor.is_position_occupied(pos):
|
||||
return false
|
||||
|
||||
# Gauntlet mode (#075): reject cells that are sticky or telegraphed —
|
||||
# stepping onto them either traps the bot or strands it within 1s.
|
||||
# Safety applies even when ignore_players is true (a sticky cell is unsafe
|
||||
# regardless of whether another player is on it). Cleanser-active bots are
|
||||
# exempt via the helper.
|
||||
if _is_cell_unsafe_in_gauntlet(pos):
|
||||
return false
|
||||
return true
|
||||
|
||||
func _get_random_valid_position() -> Vector2i:
|
||||
|
||||
Reference in New Issue
Block a user