feat: half update

This commit is contained in:
2026-06-26 18:31:17 +08:00
parent 798189d81b
commit f0ba6c2b54
5 changed files with 712 additions and 0 deletions
+48
View File
@@ -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
# =============================================================================
+118
View File
@@ -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: