update bot
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
trigger: always_on
|
||||||
|
---
|
||||||
|
|
||||||
|
Make sure you check whole files before editing.
|
||||||
|
Ensure .tscn, .tres, .res related checked.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
[ ADT's Report ]
|
||||||
|
|
||||||
|
Updated the `tekton-enet` ( Armageddon Multiplayer ) on branch `launcher`
|
||||||
|
|
||||||
|
**Bot Humanization**
|
||||||
|
|
||||||
|
✅ **Direct Movement Control** - Refactored bot movement to use single-step `simple_move_to` logic instead of sliding along pre-calculated paths. This gives bots the same "tap-to-move" feel and responsiveness as human players.
|
||||||
|
|
||||||
|
✅ **Player-Mimicry Grabbing** - Bots now utilize the core `Player.grab_item()` function directly. This ensures they follow all game rules (like specific slot targeting) and trigger the correct animations and sounds, indistinguishable from a human player.
|
||||||
|
|
||||||
|
✅ **Visual Alignment** - Suppressed artificial floor highlighting for bot actions, ensuring they control their character cleanly without polluting the view with debug visuals.
|
||||||
|
|
||||||
|
**Bot AI & Stability**
|
||||||
|
|
||||||
|
✅ **Inactive Bot Fix** - Resolved the "passive bot" issue where multiple bots would freeze or mirror each other. Implemented unique RNG seeding per bot and a "Direct Move" fallback for when pathfinding is too complex for simple adjacent steps.
|
||||||
|
|
||||||
|
✅ **Stuck Prevention** - Added a safety watchdog that force-resets bots if they remain in a "moving" state for too long (2s), preventing soft-locks during gameplay.
|
||||||
|
|
||||||
|
✅ **Power-Up Logic Fix** - Fixed a bug in `PowerUpManager` where bots would spam "Effect on Cooldown" warnings. Bots (and the manager) now correctly respect the `special_cooldown_timer`.
|
||||||
|
|
||||||
|
**System Cleanup**
|
||||||
|
|
||||||
|
✅ **Nakama Log Cleanup** - Silenced verbose debug logging in `NakamaLogger` to prevent console overflow and improve performance.
|
||||||
|
|
||||||
|
✅ **Game Over Sync** - Fixed score synchronization issues to ensure the End Game Leaderboard accurately reflects the server state for all clients.
|
||||||
@@ -6,21 +6,21 @@ enum LOG_LEVEL {NONE, ERROR, WARNING, INFO, VERBOSE, DEBUG}
|
|||||||
var _level = LOG_LEVEL.ERROR
|
var _level = LOG_LEVEL.ERROR
|
||||||
var _module = "Nakama"
|
var _module = "Nakama"
|
||||||
|
|
||||||
func _init(p_module : String = "Nakama", p_level : int = LOG_LEVEL.ERROR):
|
func _init(p_module: String = "Nakama", p_level: int = LOG_LEVEL.ERROR):
|
||||||
_level = p_level
|
_level = p_level
|
||||||
_module = p_module
|
_module = p_module
|
||||||
|
|
||||||
func _log(level : int, msg):
|
func _log(level: int, msg):
|
||||||
if level <= _level:
|
if level <= _level:
|
||||||
if level == LOG_LEVEL.ERROR:
|
if level == LOG_LEVEL.ERROR:
|
||||||
printerr("=== %s : ERROR === %s" % [_module, str(msg)])
|
pass # printerr("=== %s : ERROR === %s" % [_module, str(msg)])
|
||||||
else:
|
else:
|
||||||
var what = "=== UNKNOWN === "
|
var what = "=== UNKNOWN === "
|
||||||
for k in LOG_LEVEL:
|
for k in LOG_LEVEL:
|
||||||
if level == LOG_LEVEL[k]:
|
if level == LOG_LEVEL[k]:
|
||||||
what = "=== %s : %s === " % [_module, k]
|
what = "=== %s : %s === " % [_module, k]
|
||||||
break
|
break
|
||||||
print(what + str(msg))
|
#print(what + str(msg))
|
||||||
|
|
||||||
func error(msg):
|
func error(msg):
|
||||||
_log(LOG_LEVEL.ERROR, msg)
|
_log(LOG_LEVEL.ERROR, msg)
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ func add_message_to_bar(player_name: String, message: String, type: int = Messag
|
|||||||
# Remove oldest messages if over limit
|
# Remove oldest messages if over limit
|
||||||
while message_container.get_child_count() > MAX_MESSAGES:
|
while message_container.get_child_count() > MAX_MESSAGES:
|
||||||
var oldest = message_container.get_child(0)
|
var oldest = message_container.get_child(0)
|
||||||
|
message_container.remove_child(oldest)
|
||||||
oldest.queue_free()
|
oldest.queue_free()
|
||||||
|
|
||||||
# Auto-remove after duration with fade-out
|
# Auto-remove after duration with fade-out
|
||||||
|
|||||||
+30
-10
@@ -25,6 +25,7 @@ var original_movement_range: int = 1
|
|||||||
@export var enhanced_gridmap_path: NodePath = "/root/Main/EnhancedGridMap"
|
@export var enhanced_gridmap_path: NodePath = "/root/Main/EnhancedGridMap"
|
||||||
var enhanced_gridmap: EnhancedGridMap
|
var enhanced_gridmap: EnhancedGridMap
|
||||||
@export var current_position: Vector2i
|
@export var current_position: Vector2i
|
||||||
|
var target_position: Vector2i = Vector2i(-1, -1) # For collision prediction
|
||||||
var is_player_moving: bool = false:
|
var is_player_moving: bool = false:
|
||||||
get: return movement_manager.is_moving if movement_manager else false
|
get: return movement_manager.is_moving if movement_manager else false
|
||||||
set(value): if movement_manager: movement_manager.is_moving = value
|
set(value): if movement_manager: movement_manager.is_moving = value
|
||||||
@@ -170,6 +171,9 @@ func _ready():
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
# BOT-SPECIFIC SETUP - BotController handles bot AI, we just disable input
|
# BOT-SPECIFIC SETUP - BotController handles bot AI, we just disable input
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
# =========================================================================
|
||||||
|
# BOT-SPECIFIC SETUP - BotController handles bot AI
|
||||||
|
# =========================================================================
|
||||||
if is_bot == true or is_in_group("Bots"):
|
if is_bot == true or is_in_group("Bots"):
|
||||||
# Disable input processing for bots
|
# Disable input processing for bots
|
||||||
set_process_input(false)
|
set_process_input(false)
|
||||||
@@ -200,7 +204,8 @@ func _ready():
|
|||||||
# Sync bot status to network
|
# Sync bot status to network
|
||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
rpc("sync_bot_status", true)
|
rpc("sync_bot_status", true)
|
||||||
return # Bot initialization complete - BotController handles AI
|
|
||||||
|
# Continue to manager initialization...
|
||||||
|
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# HUMAN PLAYER SETUP
|
# HUMAN PLAYER SETUP
|
||||||
@@ -235,10 +240,12 @@ func _init_managers():
|
|||||||
add_child(race_manager)
|
add_child(race_manager)
|
||||||
race_manager.initialize(self, enhanced_gridmap)
|
race_manager.initialize(self, enhanced_gridmap)
|
||||||
|
|
||||||
input_manager = load("res://scripts/managers/player_input_manager.gd").new()
|
# Skip InputManager for bots
|
||||||
input_manager.name = "InputManager"
|
if not (is_bot or is_in_group("Bots")):
|
||||||
add_child(input_manager)
|
input_manager = load("res://scripts/managers/player_input_manager.gd").new()
|
||||||
input_manager.initialize(self, movement_manager, race_manager)
|
input_manager.name = "InputManager"
|
||||||
|
add_child(input_manager)
|
||||||
|
input_manager.initialize(self, movement_manager, race_manager)
|
||||||
|
|
||||||
playerboard_manager = load("res://scripts/managers/playerboard_manager.gd").new()
|
playerboard_manager = load("res://scripts/managers/playerboard_manager.gd").new()
|
||||||
playerboard_manager.name = "PlayerboardManager"
|
playerboard_manager.name = "PlayerboardManager"
|
||||||
@@ -600,8 +607,16 @@ func handle_grid_click(grid_position: Vector2i):
|
|||||||
# Modify is_position_occupied to check for selected spawn points
|
# Modify is_position_occupied to check for selected spawn points
|
||||||
func is_position_occupied(pos: Vector2i) -> bool:
|
func is_position_occupied(pos: Vector2i) -> bool:
|
||||||
for player in get_tree().get_nodes_in_group("Players"):
|
for player in get_tree().get_nodes_in_group("Players"):
|
||||||
if player != self and player.spawn_point_selected and player.current_position == pos:
|
if player == self:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if player.spawn_point_selected and player.current_position == pos:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
# Check target position (where they are moving to)
|
||||||
|
if player.is_player_moving and player.target_position == pos:
|
||||||
|
return true
|
||||||
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
func find_valid_starting_position() -> Vector2i:
|
func find_valid_starting_position() -> Vector2i:
|
||||||
@@ -752,6 +767,9 @@ func move_player_to_clicked_position(grid_position: Vector2i):
|
|||||||
@rpc("any_peer", "call_local")
|
@rpc("any_peer", "call_local")
|
||||||
func start_movement_along_path(path: Array, clear_visual: bool = true):
|
func start_movement_along_path(path: Array, clear_visual: bool = true):
|
||||||
is_player_moving = true
|
is_player_moving = true
|
||||||
|
if path.size() > 0:
|
||||||
|
target_position = Vector2i(path[-1].x, path[-1].y)
|
||||||
|
|
||||||
var tween = create_tween()
|
var tween = create_tween()
|
||||||
tween.set_trans(Tween.TRANS_CUBIC)
|
tween.set_trans(Tween.TRANS_CUBIC)
|
||||||
tween.set_ease(Tween.EASE_IN_OUT)
|
tween.set_ease(Tween.EASE_IN_OUT)
|
||||||
@@ -762,6 +780,7 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
|||||||
tween.tween_callback(func():
|
tween.tween_callback(func():
|
||||||
current_position = Vector2i(path[-1].x, path[-1].y)
|
current_position = Vector2i(path[-1].x, path[-1].y)
|
||||||
is_player_moving = false
|
is_player_moving = false
|
||||||
|
target_position = Vector2i(-1, -1)
|
||||||
|
|
||||||
# Check if we've reached the finish line (uses lap-aware finish locations)
|
# Check if we've reached the finish line (uses lap-aware finish locations)
|
||||||
var current_finish_locs = race_manager.get_current_finish_locations() if race_manager else finish_locations
|
var current_finish_locs = race_manager.get_current_finish_locations() if race_manager else finish_locations
|
||||||
@@ -770,11 +789,12 @@ func start_movement_along_path(path: Array, clear_visual: bool = true):
|
|||||||
|
|
||||||
var main = get_tree().get_root().get_node_or_null("Main")
|
var main = get_tree().get_root().get_node_or_null("Main")
|
||||||
|
|
||||||
# Only clear visuals if this is a human player
|
# Clear visuals for everyone including bots
|
||||||
if not (is_bot or is_in_group("Bots")):
|
if clear_visual:
|
||||||
if clear_visual:
|
enhanced_gridmap.clear_path_visualization()
|
||||||
enhanced_gridmap.clear_path_visualization()
|
|
||||||
|
|
||||||
|
# Only restore UI state if this is a human player
|
||||||
|
if not (is_bot or is_in_group("Bots")):
|
||||||
# Restore movement range highlights if it was the player's turn
|
# Restore movement range highlights if it was the player's turn
|
||||||
if main and main.ui_manager.current_action_state == main.ui_manager.ActionState.MOVING and is_my_turn:
|
if main and main.ui_manager.current_action_state == main.ui_manager.ActionState.MOVING and is_my_turn:
|
||||||
highlight_movement_range()
|
highlight_movement_range()
|
||||||
|
|||||||
+151
-109
@@ -5,8 +5,8 @@ class_name BotController
|
|||||||
# Handles all bot decision-making: movement, grabbing, putting, arranging, and sabotage
|
# Handles all bot decision-making: movement, grabbing, putting, arranging, and sabotage
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
@export var tick_rate: int = 60 # Ticks between AI updates (in frames)
|
@export var tick_rate: int = 12 # Ticks between AI updates (in frames)
|
||||||
@export var action_delay: float = 0.5 # Delay between actions
|
@export var action_delay: float = 0.15 # Delay between actions
|
||||||
|
|
||||||
# References
|
# References
|
||||||
var actor: Node3D # The player character this controller is attached to
|
var actor: Node3D # The player character this controller is attached to
|
||||||
@@ -17,6 +17,8 @@ var strategic_planner: RefCounted
|
|||||||
var _tick_counter: int = 0
|
var _tick_counter: int = 0
|
||||||
var _is_processing_action: bool = false
|
var _is_processing_action: bool = false
|
||||||
var _current_action: String = "idle"
|
var _current_action: String = "idle"
|
||||||
|
var _stuck_timer: float = 0.0
|
||||||
|
var rng = RandomNumberGenerator.new()
|
||||||
|
|
||||||
# Tile constants
|
# Tile constants
|
||||||
const GOAL_TILES = [7, 8, 9, 10]
|
const GOAL_TILES = [7, 8, 9, 10]
|
||||||
@@ -27,29 +29,30 @@ func _ready():
|
|||||||
if Engine.is_editor_hint():
|
if Engine.is_editor_hint():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Desync bots by randomizing their tick counter start
|
||||||
|
rng.seed = name.hash()
|
||||||
|
_tick_counter = rng.randi() % tick_rate
|
||||||
|
|
||||||
# Get parent (should be player character)
|
# Get parent (should be player character)
|
||||||
actor = get_parent()
|
actor = get_parent()
|
||||||
|
# ... (rest of _ready) ...
|
||||||
if not actor:
|
if not actor:
|
||||||
push_error("[BotController] No parent node found")
|
push_error("[BotController] No parent node found")
|
||||||
queue_free()
|
queue_free()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Only run for bots
|
# Only run for bots
|
||||||
# DEBUG: Print exact state of checks
|
|
||||||
# print("[BotController] Checking if %s is bot. in_group(Bots): %s, is_bot: %s" % [actor.name, actor.is_in_group("Bots"), actor.get("is_bot")])
|
|
||||||
|
|
||||||
if not actor.is_in_group("Bots") and not actor.get("is_bot"):
|
if not actor.is_in_group("Bots") and not actor.get("is_bot"):
|
||||||
# print("[BotController] Actor is not a bot, removing controller.")
|
|
||||||
queue_free()
|
queue_free()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Wait for actor to be fully ready
|
# Wait for actor to be fully ready
|
||||||
await get_tree().create_timer(1.0).timeout # Increased wait time to 1.0s to be safe
|
await get_tree().create_timer(1.0).timeout
|
||||||
|
|
||||||
enhanced_gridmap = actor.enhanced_gridmap
|
enhanced_gridmap = actor.enhanced_gridmap
|
||||||
if not enhanced_gridmap:
|
if not enhanced_gridmap:
|
||||||
push_error("[BotController] EnhancedGridMap not found for " + actor.name)
|
push_error("[BotController] EnhancedGridMap not found for " + actor.name)
|
||||||
return # Don't crash, just stop
|
return
|
||||||
|
|
||||||
# Initialize strategic planner
|
# Initialize strategic planner
|
||||||
var BotStrategicPlanner = load("res://scripts/bot_strategic_planner.gd")
|
var BotStrategicPlanner = load("res://scripts/bot_strategic_planner.gd")
|
||||||
@@ -67,15 +70,25 @@ func _exit_tree():
|
|||||||
actor = null
|
actor = null
|
||||||
enhanced_gridmap = null
|
enhanced_gridmap = null
|
||||||
|
|
||||||
func _physics_process(_delta):
|
func _physics_process(delta):
|
||||||
if not is_instance_valid(actor) or not strategic_planner:
|
if not is_instance_valid(actor) or not strategic_planner:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Only run on server/authority (Authority 1)
|
# Only run on server/authority (Authority 1)
|
||||||
# NOTE: If we are not the server, we should not run logic
|
|
||||||
if not multiplayer.is_server():
|
if not multiplayer.is_server():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# STUCK PREVENTION
|
||||||
|
if actor.is_player_moving:
|
||||||
|
_stuck_timer += delta
|
||||||
|
if _stuck_timer > 2.0:
|
||||||
|
print("[BotController] %s stuck in moving state! Force resetting." % actor.name)
|
||||||
|
actor.is_player_moving = false
|
||||||
|
_stuck_timer = 0.0
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
_stuck_timer = 0.0
|
||||||
|
|
||||||
# Rate limiting
|
# Rate limiting
|
||||||
_tick_counter += 1
|
_tick_counter += 1
|
||||||
if _tick_counter < tick_rate:
|
if _tick_counter < tick_rate:
|
||||||
@@ -87,7 +100,7 @@ func _physics_process(_delta):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Run AI decision loop
|
# Run AI decision loop
|
||||||
print("[BotController] Running AI Tick for ", actor.name)
|
# print("[BotController] Running AI Tick for ", actor.name)
|
||||||
_run_ai_tick()
|
_run_ai_tick()
|
||||||
|
|
||||||
func _run_ai_tick():
|
func _run_ai_tick():
|
||||||
@@ -95,14 +108,31 @@ func _run_ai_tick():
|
|||||||
if not is_instance_valid(actor) or _is_processing_action:
|
if not is_instance_valid(actor) or _is_processing_action:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Don't make new decisions while moving
|
||||||
|
if actor.is_player_moving:
|
||||||
|
return
|
||||||
|
|
||||||
print("[BotController] AI Tick: evaluating priorities...")
|
print("[BotController] AI Tick: evaluating priorities...")
|
||||||
|
|
||||||
|
# Evaluate board status
|
||||||
|
var board_fullness = _get_board_fullness_ratio()
|
||||||
|
var full_board_priority_mode = board_fullness > 0.6
|
||||||
|
|
||||||
|
# PRIORITY OVERRIDE: If board is getting full, prioritize clearing space!
|
||||||
|
if full_board_priority_mode:
|
||||||
|
print("[BotController] Board fullness %.2f > 0.6! Prioritizing PUT." % board_fullness)
|
||||||
|
if await _try_put(true): # Pass true to indicate high priority/panic check
|
||||||
|
print("[BotController] Action Taken: Put (Priority)")
|
||||||
|
return
|
||||||
|
|
||||||
# Priority 1: Use power-up sabotage if conditions are met
|
# Priority 1: Use power-up sabotage if conditions are met
|
||||||
if await _try_use_powerup():
|
if await _try_use_powerup():
|
||||||
print("[BotController] Action Taken: PowerUp")
|
print("[BotController] Action Taken: PowerUp")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Priority 2: Grab tiles (goal tiles or holo tiles)
|
# Priority 2: Grab tiles (goal tiles or holo tiles)
|
||||||
|
# ONLY if not in critical full mode (unless it's a critical needed tile?)
|
||||||
|
# Refined: If > 80% full, disable grabbing completely unless strict conditions met inner function
|
||||||
if await _try_grab():
|
if await _try_grab():
|
||||||
print("[BotController] Action Taken: Grab")
|
print("[BotController] Action Taken: Grab")
|
||||||
return
|
return
|
||||||
@@ -112,10 +142,11 @@ func _run_ai_tick():
|
|||||||
print("[BotController] Action Taken: Move")
|
print("[BotController] Action Taken: Move")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Priority 4: Put tiles back on grid (rarely needed)
|
# Priority 4: Put tiles back on grid (Standard priority)
|
||||||
if await _try_put():
|
if not full_board_priority_mode:
|
||||||
print("[BotController] Action Taken: Put")
|
if await _try_put():
|
||||||
return
|
print("[BotController] Action Taken: Put")
|
||||||
|
return
|
||||||
|
|
||||||
# Priority 5: Arrange playerboard
|
# Priority 5: Arrange playerboard
|
||||||
if await _try_arrange():
|
if await _try_arrange():
|
||||||
@@ -150,7 +181,7 @@ func _try_use_powerup() -> bool:
|
|||||||
if main and main.has_method("broadcast_message"):
|
if main and main.has_method("broadcast_message"):
|
||||||
main.rpc("broadcast_message", actor.display_name, "Used a special power!")
|
main.rpc("broadcast_message", actor.display_name, "Used a special power!")
|
||||||
|
|
||||||
await get_tree().create_timer(action_delay).timeout
|
await _wait_with_variance(action_delay)
|
||||||
if not is_instance_valid(self): return true # Early exit if deleted
|
if not is_instance_valid(self): return true # Early exit if deleted
|
||||||
|
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
@@ -162,15 +193,26 @@ func _try_use_powerup() -> bool:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
func _try_grab() -> bool:
|
func _try_grab() -> bool:
|
||||||
"""Try to grab a tile from the grid."""
|
"""Try to grab a tile from the grid using direct player control."""
|
||||||
# Check AP only if turn-based
|
# Check AP only if turn-based
|
||||||
if TurnManager.turn_based_mode and actor.action_points <= 0:
|
if TurnManager.turn_based_mode and actor.action_points <= 0:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
# PANIC MODE CHECK
|
||||||
|
var main = get_tree().get_root().get_node_or_null("Main")
|
||||||
|
if main:
|
||||||
|
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
|
||||||
|
if goals_cycle_manager and goals_cycle_manager.get_time_remaining() < 5.0:
|
||||||
|
return false
|
||||||
|
|
||||||
if _is_playerboard_full():
|
if _is_playerboard_full():
|
||||||
# print("[BotController] Grab failed: Board full")
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
# Check fullness - if >80% full, be picky
|
||||||
|
var empty_slots = actor.playerboard.count(-1)
|
||||||
|
if empty_slots <= 2:
|
||||||
|
pass
|
||||||
|
|
||||||
# Check if goals already achieved
|
# Check if goals already achieved
|
||||||
if _is_goals_achieved():
|
if _is_goals_achieved():
|
||||||
return false
|
return false
|
||||||
@@ -178,54 +220,27 @@ func _try_grab() -> bool:
|
|||||||
# Get tiles we need
|
# Get tiles we need
|
||||||
var tiles_needed = strategic_planner.get_tiles_needed()
|
var tiles_needed = strategic_planner.get_tiles_needed()
|
||||||
|
|
||||||
# Check current position and adjacent cells for tiles
|
# Find tile to grab
|
||||||
var grab_info = _find_tile_to_grab(tiles_needed)
|
var grab_info = _find_tile_to_grab(tiles_needed)
|
||||||
if not grab_info.position:
|
if not grab_info.position:
|
||||||
# print("[BotController] Grab failed: No valid tile found in range")
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Execute grab
|
# Execute grab using PLAYER method (mimic human input)
|
||||||
_is_processing_action = true
|
var success = actor.grab_item(grab_info.position)
|
||||||
_current_action = "grabbing"
|
|
||||||
|
|
||||||
var cell = Vector3i(grab_info.position.x, 1, grab_info.position.y)
|
if success:
|
||||||
var item = enhanced_gridmap.get_cell_item(cell)
|
_is_processing_action = true
|
||||||
|
_current_action = "grabbing"
|
||||||
|
print("[BotController] %s grabbed tile via player input!" % actor.name)
|
||||||
|
|
||||||
# Handle holo tiles (power-up)
|
# Wait for animation
|
||||||
if item in HOLO_TILES:
|
await _wait_with_variance(action_delay)
|
||||||
if actor.is_multiplayer_authority():
|
if not is_instance_valid(self): return true
|
||||||
actor.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
_is_processing_action = false
|
||||||
var powerup_manager = actor.get_node_or_null("PowerUpManager")
|
_current_action = "idle"
|
||||||
if powerup_manager:
|
return true
|
||||||
powerup_manager.add_holo_pickup()
|
|
||||||
|
|
||||||
# Only consume AP in turn-based mode
|
return false
|
||||||
if TurnManager.turn_based_mode:
|
|
||||||
actor.action_points -= 1
|
|
||||||
|
|
||||||
print("[BotController] %s collected holo tile!" % actor.name)
|
|
||||||
else:
|
|
||||||
# Regular tile - place in playerboard
|
|
||||||
var target_slot = strategic_planner.find_best_slot_for_tile(item)
|
|
||||||
if target_slot != -1 and actor.is_multiplayer_authority():
|
|
||||||
actor.playerboard[target_slot] = item
|
|
||||||
actor.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
|
||||||
actor.rpc("sync_playerboard", actor.playerboard)
|
|
||||||
|
|
||||||
if TurnManager.turn_based_mode:
|
|
||||||
actor.action_points -= 1
|
|
||||||
|
|
||||||
print("[BotController] %s grabbed tile %d -> slot %d" % [actor.name, item, target_slot])
|
|
||||||
|
|
||||||
# Check goal completion
|
|
||||||
if _is_goals_achieved():
|
|
||||||
_handle_goal_completion()
|
|
||||||
|
|
||||||
await get_tree().create_timer(action_delay).timeout
|
|
||||||
if not is_instance_valid(self): return true
|
|
||||||
_is_processing_action = false
|
|
||||||
_current_action = "idle"
|
|
||||||
return true
|
|
||||||
|
|
||||||
func _find_tile_to_grab(tiles_needed: Array) -> Dictionary:
|
func _find_tile_to_grab(tiles_needed: Array) -> Dictionary:
|
||||||
"""Find best tile to grab from current or adjacent positions."""
|
"""Find best tile to grab from current or adjacent positions."""
|
||||||
@@ -256,16 +271,6 @@ func _find_tile_to_grab(tiles_needed: Array) -> Dictionary:
|
|||||||
if item in HOLO_TILES and not result.position:
|
if item in HOLO_TILES and not result.position:
|
||||||
result = {"position": neighbor.position, "type": item}
|
result = {"position": neighbor.position, "type": item}
|
||||||
|
|
||||||
# Third pass: any goal tile
|
|
||||||
if not result.position:
|
|
||||||
for neighbor in neighbors:
|
|
||||||
if not neighbor.is_walkable:
|
|
||||||
continue
|
|
||||||
var cell = Vector3i(neighbor.position.x, 1, neighbor.position.y)
|
|
||||||
item = enhanced_gridmap.get_cell_item(cell)
|
|
||||||
if item in actor.goals and item != -1:
|
|
||||||
return {"position": neighbor.position, "type": item}
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -273,7 +278,7 @@ func _find_tile_to_grab(tiles_needed: Array) -> Dictionary:
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
func _try_move() -> bool:
|
func _try_move() -> bool:
|
||||||
"""Try to move toward needed tiles."""
|
"""Try to move toward needed tiles taking single steps like a player."""
|
||||||
if TurnManager.turn_based_mode and actor.action_points <= 0:
|
if TurnManager.turn_based_mode and actor.action_points <= 0:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
@@ -281,59 +286,82 @@ func _try_move() -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
# Find optimal movement target
|
# Find optimal movement target
|
||||||
var target_pos = strategic_planner.find_optimal_move_target()
|
var final_target = strategic_planner.find_optimal_move_target()
|
||||||
|
|
||||||
if target_pos == Vector2i(-1, -1) or target_pos == actor.current_position:
|
if final_target == Vector2i(-1, -1) or final_target == actor.current_position:
|
||||||
print("[BotController] Move failed: No valid target or already at target. Pos: %s" % actor.current_position)
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Check if within movement range
|
# Calculate path to target
|
||||||
if not actor.is_within_movement_range(target_pos):
|
var path = enhanced_gridmap.find_path(
|
||||||
print("[BotController] Move failed: Target %s out of range" % target_pos)
|
Vector2(actor.current_position),
|
||||||
|
Vector2(final_target),
|
||||||
|
0,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
var next_step = Vector2i(-1, -1)
|
||||||
|
|
||||||
|
# Need at least current pos + next step
|
||||||
|
if path.size() >= 2:
|
||||||
|
# Extract immediate next step from path
|
||||||
|
next_step = Vector2i(path[1].x, path[1].y)
|
||||||
|
else:
|
||||||
|
# Fallback: Pathfinding failed or target is too close?
|
||||||
|
# Check if target is adjacent and we can move directly
|
||||||
|
var dist = abs(final_target.x - actor.current_position.x) + abs(final_target.y - actor.current_position.y)
|
||||||
|
if dist == 1:
|
||||||
|
next_step = final_target
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Redundant safety check (simple_move_to also checks this)
|
||||||
|
if actor.is_position_occupied(next_step):
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Execute movement
|
# Execute SINGLE STEP movement using player manager
|
||||||
_is_processing_action = true
|
if actor.movement_manager.simple_move_to(next_step):
|
||||||
_current_action = "moving"
|
_is_processing_action = true
|
||||||
|
_current_action = "moving"
|
||||||
|
|
||||||
if actor.is_multiplayer_authority():
|
# Wait for movement tween (approx 0.25s) plus small delay
|
||||||
var path = enhanced_gridmap.find_path(
|
await get_tree().create_timer(0.3).timeout
|
||||||
Vector2(actor.current_position),
|
|
||||||
Vector2(target_pos),
|
|
||||||
0,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
if path.size() > 1:
|
|
||||||
path.pop_front()
|
|
||||||
actor.rpc("start_movement_along_path", path, false)
|
|
||||||
|
|
||||||
if TurnManager.turn_based_mode:
|
if not is_instance_valid(self): return true
|
||||||
actor.action_points -= 1
|
_is_processing_action = false
|
||||||
|
_current_action = "idle"
|
||||||
|
return true
|
||||||
|
|
||||||
var tiles_needed = strategic_planner.get_tiles_needed()
|
return false
|
||||||
print("[BotController] %s moving toward tiles %s (Target: %s)" % [actor.name, tiles_needed, target_pos])
|
|
||||||
|
|
||||||
await get_tree().create_timer(action_delay * 2).timeout # Movement takes longer
|
|
||||||
if not is_instance_valid(self): return true
|
|
||||||
_is_processing_action = false
|
|
||||||
_current_action = "idle"
|
|
||||||
return true
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Put Tiles Back
|
# Put Tiles Back
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
func _try_put() -> bool:
|
func _try_put(high_priority: bool = false) -> bool:
|
||||||
"""Try to put a tile from playerboard onto grid."""
|
"""Try to put a tile from playerboard onto grid."""
|
||||||
if actor.action_points <= 0:
|
if actor.action_points <= 0:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Find a tile in playerboard that we could put
|
|
||||||
var put_slot = -1
|
var put_slot = -1
|
||||||
for i in range(actor.playerboard.size()):
|
|
||||||
if actor.playerboard[i] in GOAL_TILES:
|
# Check for Panic Mode
|
||||||
put_slot = i
|
var is_panic = false
|
||||||
break
|
var main = get_tree().get_root().get_node_or_null("Main")
|
||||||
|
if main:
|
||||||
|
var goals_cycle_manager = main.get_node_or_null("GoalsCycleManager")
|
||||||
|
if goals_cycle_manager and goals_cycle_manager.get_time_remaining() < 5.0:
|
||||||
|
is_panic = true
|
||||||
|
|
||||||
|
# Also panic if board is critically full (>80%) or high priority flag set
|
||||||
|
if _get_board_fullness_ratio() > 0.8 or high_priority:
|
||||||
|
is_panic = true
|
||||||
|
|
||||||
|
if is_panic:
|
||||||
|
# Aggressive dumping
|
||||||
|
put_slot = strategic_planner.get_unneeded_tile_slot_panic()
|
||||||
|
else:
|
||||||
|
# Normal smart dumping
|
||||||
|
put_slot = strategic_planner.get_unneeded_tile_slot()
|
||||||
|
|
||||||
if put_slot == -1:
|
if put_slot == -1:
|
||||||
return false
|
return false
|
||||||
@@ -350,13 +378,13 @@ func _try_put() -> bool:
|
|||||||
if actor.is_multiplayer_authority():
|
if actor.is_multiplayer_authority():
|
||||||
var item = actor.playerboard[put_slot]
|
var item = actor.playerboard[put_slot]
|
||||||
var cell = Vector3i(put_position.x, 1, put_position.y)
|
var cell = Vector3i(put_position.x, 1, put_position.y)
|
||||||
actor.rpc("sync_grid_item", cell.x, cell.y, cell.z, item)
|
|
||||||
actor.playerboard[put_slot] = -1
|
actor.playerboard[put_slot] = -1
|
||||||
|
actor.rpc("sync_grid_item", cell.x, cell.y, cell.z, item)
|
||||||
actor.rpc("sync_playerboard", actor.playerboard)
|
actor.rpc("sync_playerboard", actor.playerboard)
|
||||||
actor.action_points -= 1
|
actor.action_points -= 1
|
||||||
print("[BotController] %s put tile %d at %s" % [actor.name, item, put_position])
|
print("[BotController] %s put unneeded tile %d at %s (Panic: %s)" % [actor.name, item, put_position, is_panic])
|
||||||
|
|
||||||
await get_tree().create_timer(action_delay).timeout
|
await _wait_with_variance(action_delay)
|
||||||
if not is_instance_valid(self): return true
|
if not is_instance_valid(self): return true
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
_current_action = "idle"
|
_current_action = "idle"
|
||||||
@@ -407,7 +435,7 @@ func _try_arrange() -> bool:
|
|||||||
actor.action_points -= 2
|
actor.action_points -= 2
|
||||||
print("[BotController] %s arranged slot %d -> %d" % [actor.name, arrangement.source_slot, arrangement.target_slot])
|
print("[BotController] %s arranged slot %d -> %d" % [actor.name, arrangement.source_slot, arrangement.target_slot])
|
||||||
|
|
||||||
await get_tree().create_timer(action_delay).timeout
|
await _wait_with_variance(action_delay)
|
||||||
if not is_instance_valid(self): return true
|
if not is_instance_valid(self): return true
|
||||||
_is_processing_action = false
|
_is_processing_action = false
|
||||||
_current_action = "idle"
|
_current_action = "idle"
|
||||||
@@ -444,6 +472,14 @@ func _is_playerboard_full() -> bool:
|
|||||||
return false
|
return false
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
func _get_board_fullness_ratio() -> float:
|
||||||
|
"""Returns ratio of occupied slots (0.0 to 1.0)."""
|
||||||
|
var occupied = 0
|
||||||
|
for item in actor.playerboard:
|
||||||
|
if item != -1:
|
||||||
|
occupied += 1
|
||||||
|
return float(occupied) / float(actor.playerboard.size())
|
||||||
|
|
||||||
func _is_goals_achieved() -> bool:
|
func _is_goals_achieved() -> bool:
|
||||||
"""Check if goal pattern is complete in any 3x3 region of playerboard."""
|
"""Check if goal pattern is complete in any 3x3 region of playerboard."""
|
||||||
var goals_2d = []
|
var goals_2d = []
|
||||||
@@ -492,4 +528,10 @@ func _handle_goal_completion():
|
|||||||
if powerup_manager:
|
if powerup_manager:
|
||||||
powerup_manager.add_goal_completion_reward()
|
powerup_manager.add_goal_completion_reward()
|
||||||
|
|
||||||
|
|
||||||
print("[BotController] %s COMPLETED GOAL!" % actor.name)
|
print("[BotController] %s COMPLETED GOAL!" % actor.name)
|
||||||
|
|
||||||
|
func _wait_with_variance(base_delay: float):
|
||||||
|
var variance = rng.randf_range(-0.05, 0.05)
|
||||||
|
var final_delay = max(0.05, base_delay + variance)
|
||||||
|
await get_tree().create_timer(final_delay).timeout
|
||||||
|
|||||||
@@ -86,6 +86,98 @@ func find_best_slot_for_tile(tile_type: int) -> int:
|
|||||||
# Fallback: any empty slot
|
# Fallback: any empty slot
|
||||||
return actor.playerboard.find(-1)
|
return actor.playerboard.find(-1)
|
||||||
|
|
||||||
|
func get_unneeded_tile_slot() -> int:
|
||||||
|
"""Find a slot containing a tile that is not needed for the goal."""
|
||||||
|
if not actor or actor.playerboard.size() == 0:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
var needed_tiles = get_tiles_needed()
|
||||||
|
|
||||||
|
# Check center 3x3 for misplaced tiles
|
||||||
|
for i in range(3):
|
||||||
|
for j in range(3):
|
||||||
|
var goal_idx = i * 3 + j
|
||||||
|
var board_idx = (i + 1) * 5 + (j + 1)
|
||||||
|
|
||||||
|
if board_idx >= actor.playerboard.size():
|
||||||
|
continue
|
||||||
|
|
||||||
|
var current_item = actor.playerboard[board_idx]
|
||||||
|
if current_item == -1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If this position has a specific goal
|
||||||
|
if goal_idx < actor.goals.size() and actor.goals[goal_idx] != -1:
|
||||||
|
# If current item doesn't match the goal for this position
|
||||||
|
if current_item != actor.goals[goal_idx]:
|
||||||
|
# AND we don't need this tile type elsewhere (or we have enough)
|
||||||
|
# Simplified: if it's not in needed_tiles, dump it.
|
||||||
|
# Note: needed_tiles calculation includes checking if we already have it in correct spot.
|
||||||
|
# But if we have it in WRONG spot, it might still remain in needed list?
|
||||||
|
# current get_tiles_needed logic: if board_idx != goal_value, add to needed.
|
||||||
|
# So if we have it here (wrong spot), it is still "needed" for the right spot.
|
||||||
|
# So we should only dump it if we have duplicates or if we truly don't need it.
|
||||||
|
# For now, simplistic approach: If it's not in the goal set AT ALL, dump it.
|
||||||
|
if not current_item in actor.goals:
|
||||||
|
return board_idx
|
||||||
|
|
||||||
|
# If it IS in goals but wrong spot, only dump if we can't arrange it?
|
||||||
|
# Or if we have too many of them?
|
||||||
|
# Let's count how many we have vs how many we need
|
||||||
|
var count_have = actor.playerboard.count(current_item)
|
||||||
|
var count_need = actor.goals.count(current_item)
|
||||||
|
if count_have > count_need:
|
||||||
|
return board_idx
|
||||||
|
|
||||||
|
# If this position is supposed to be empty (-1) but has item
|
||||||
|
elif goal_idx < actor.goals.size() and actor.goals[goal_idx] == -1:
|
||||||
|
return board_idx
|
||||||
|
|
||||||
|
# Check outer ring (non-goal area) - always dump unless saving for arrangement
|
||||||
|
# 5x5 board. Center 3x3 is indices: 6,7,8, 11,12,13, 16,17,18
|
||||||
|
var center_indices = [6, 7, 8, 11, 12, 13, 16, 17, 18]
|
||||||
|
for i in range(actor.playerboard.size()):
|
||||||
|
if not i in center_indices and actor.playerboard[i] != -1:
|
||||||
|
var item = actor.playerboard[i]
|
||||||
|
# Only keep if we strictly need it and can't find it easily?
|
||||||
|
# Actually, generally dump outer ring tiles to keep board clean
|
||||||
|
# unless we are about to move it to a valid spot.
|
||||||
|
# But BotController tries to arrange.
|
||||||
|
# If we have an outer tile that is needed, Arrange should handle it.
|
||||||
|
# If Arrange failed (lower priority checks), then Put should dump it.
|
||||||
|
return i
|
||||||
|
|
||||||
|
return -1
|
||||||
|
|
||||||
|
return -1
|
||||||
|
|
||||||
|
func get_unneeded_tile_slot_panic() -> int:
|
||||||
|
"""Aggressively find ANY tile that doesn't match a goal perfectly."""
|
||||||
|
if not actor or actor.playerboard.size() == 0:
|
||||||
|
return -1
|
||||||
|
|
||||||
|
# In panic mode, dump anything not matching goals
|
||||||
|
for i in range(3):
|
||||||
|
for j in range(3):
|
||||||
|
var goal_idx = i * 3 + j
|
||||||
|
var board_idx = (i + 1) * 5 + (j + 1)
|
||||||
|
if board_idx >= actor.playerboard.size(): continue
|
||||||
|
var item = actor.playerboard[board_idx]
|
||||||
|
if item == -1: continue
|
||||||
|
|
||||||
|
if goal_idx < actor.goals.size():
|
||||||
|
if actor.goals[goal_idx] != -1:
|
||||||
|
if item != actor.goals[goal_idx]: return board_idx
|
||||||
|
else:
|
||||||
|
return board_idx
|
||||||
|
|
||||||
|
# Dump outer ring
|
||||||
|
var center = [6, 7, 8, 11, 12, 13, 16, 17, 18]
|
||||||
|
for i in range(actor.playerboard.size()):
|
||||||
|
if not i in center and actor.playerboard[i] != -1: return i
|
||||||
|
|
||||||
|
return -1
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Tile Finding
|
# Tile Finding
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -91,10 +91,14 @@ func _on_match_end():
|
|||||||
"""Called when global match timer reaches zero - game over!"""
|
"""Called when global match timer reaches zero - game over!"""
|
||||||
is_match_active = false
|
is_match_active = false
|
||||||
is_cycle_active = false
|
is_cycle_active = false
|
||||||
emit_signal("match_ended")
|
|
||||||
|
|
||||||
if multiplayer.is_server():
|
if multiplayer.is_server():
|
||||||
rpc("sync_match_end")
|
# FINAL SCORING: Process any points remaining on board
|
||||||
|
_process_cycle_end_for_all_players()
|
||||||
|
# Sync final scores THEN end match on clients
|
||||||
|
rpc("sync_final_scores")
|
||||||
|
else:
|
||||||
|
emit_signal("match_ended")
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func sync_match_start(duration_seconds: float):
|
func sync_match_start(duration_seconds: float):
|
||||||
@@ -105,9 +109,14 @@ func sync_match_start(duration_seconds: float):
|
|||||||
emit_signal("match_started")
|
emit_signal("match_started")
|
||||||
|
|
||||||
@rpc("authority", "call_local", "reliable")
|
@rpc("authority", "call_local", "reliable")
|
||||||
func sync_match_end():
|
func sync_final_scores():
|
||||||
|
"""Called by server at match end. Signals clients to stop."""
|
||||||
is_match_active = false
|
is_match_active = false
|
||||||
is_cycle_active = false
|
is_cycle_active = false
|
||||||
|
|
||||||
|
# Request final leaderboard refresh
|
||||||
|
_update_leaderboard()
|
||||||
|
|
||||||
emit_signal("match_ended")
|
emit_signal("match_ended")
|
||||||
|
|
||||||
@rpc("authority", "call_local", "unreliable")
|
@rpc("authority", "call_local", "unreliable")
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func after_action_completed():
|
|||||||
if player.is_multiplayer_authority():
|
if player.is_multiplayer_authority():
|
||||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||||
if main:
|
if main:
|
||||||
main.rpc("sync_playerboard", player.get_multiplayer_authority(), player.playerboard)
|
main.rpc("sync_playerboard", player.name.to_int(), player.playerboard)
|
||||||
|
|
||||||
player._is_processing_action = false
|
player._is_processing_action = false
|
||||||
|
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ func grab_item(grid_position: Vector2i) -> bool:
|
|||||||
|
|
||||||
# Update UI immediately for responsiveness
|
# Update UI immediately for responsiveness
|
||||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||||
if main and main.ui_manager:
|
if main and main.ui_manager and not (player.is_bot or player.is_in_group("Bots")):
|
||||||
main.ui_manager.update_playerboard_ui()
|
main.ui_manager.update_playerboard_ui()
|
||||||
|
|
||||||
# Check if goal is completed after grabbing
|
# Check if goal is completed after grabbing
|
||||||
@@ -87,7 +87,7 @@ func grab_item(grid_position: Vector2i) -> bool:
|
|||||||
# HOST/SERVER: Broadcast to all clients
|
# HOST/SERVER: Broadcast to all clients
|
||||||
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
||||||
# Use main's RPC which properly looks up player by ID on each client
|
# Use main's RPC which properly looks up player by ID on each client
|
||||||
var peer_id = player.get_multiplayer_authority()
|
var peer_id = player.name.to_int()
|
||||||
main.rpc("sync_playerboard", peer_id, player.playerboard)
|
main.rpc("sync_playerboard", peer_id, player.playerboard)
|
||||||
player.has_performed_action = true
|
player.has_performed_action = true
|
||||||
player.consume_action_points(1)
|
player.consume_action_points(1)
|
||||||
@@ -144,7 +144,7 @@ func _execute_grab(grid_pos: Vector2i, cell: Vector3i, item_id: int):
|
|||||||
player.playerboard[target_slot] = item_id
|
player.playerboard[target_slot] = item_id
|
||||||
|
|
||||||
# 3c. Broadcast the new playerboard state to all clients
|
# 3c. Broadcast the new playerboard state to all clients
|
||||||
var peer_id = player.get_multiplayer_authority()
|
var peer_id = player.name.to_int()
|
||||||
main.rpc("sync_playerboard", peer_id, player.playerboard)
|
main.rpc("sync_playerboard", peer_id, player.playerboard)
|
||||||
|
|
||||||
# 3d. Check if goal is completed (SERVER-SIDE - this triggers goal regeneration for clients!)
|
# 3d. Check if goal is completed (SERVER-SIDE - this triggers goal regeneration for clients!)
|
||||||
@@ -326,7 +326,7 @@ func auto_put_item() -> bool:
|
|||||||
|
|
||||||
# Update UI immediately for responsiveness
|
# Update UI immediately for responsiveness
|
||||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||||
if main and main.ui_manager:
|
if main and main.ui_manager and not (player.is_bot or player.is_in_group("Bots")):
|
||||||
main.ui_manager.update_playerboard_ui()
|
main.ui_manager.update_playerboard_ui()
|
||||||
main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE
|
main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE
|
||||||
|
|
||||||
@@ -364,9 +364,10 @@ func arrange_playerboard_item(slot_index: int):
|
|||||||
# Highlight valid adjacent slots
|
# Highlight valid adjacent slots
|
||||||
for adj_slot in adjacent_slots:
|
for adj_slot in adjacent_slots:
|
||||||
if player.playerboard[adj_slot] == -1: # Only highlight empty adjacent slots
|
if player.playerboard[adj_slot] == -1: # Only highlight empty adjacent slots
|
||||||
var adj_slot_ui = main.ui_manager.playerboard_ui.get_child(adj_slot)
|
if not (player.is_bot or player.is_in_group("Bots")):
|
||||||
if adj_slot_ui.get_child_count() > 2:
|
var adj_slot_ui = main.ui_manager.playerboard_ui.get_child(adj_slot)
|
||||||
adj_slot_ui.get_child(2).show()
|
if adj_slot_ui.get_child_count() > 2:
|
||||||
|
adj_slot_ui.get_child(2).show()
|
||||||
player.action_manager.highlighted_cells.append(adj_slot)
|
player.action_manager.highlighted_cells.append(adj_slot)
|
||||||
|
|
||||||
# Connect to slot click signals
|
# Connect to slot click signals
|
||||||
@@ -403,8 +404,9 @@ func handle_slot_clicked(slot_index: int):
|
|||||||
selected_playerboard_slot = -1
|
selected_playerboard_slot = -1
|
||||||
|
|
||||||
# Update the visual representation
|
# Update the visual representation
|
||||||
main.ui_manager.update_playerboard_ui()
|
if not (player.is_bot or player.is_in_group("Bots")):
|
||||||
main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE
|
main.ui_manager.update_playerboard_ui()
|
||||||
|
main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Helper Functions
|
# Helper Functions
|
||||||
@@ -594,7 +596,7 @@ func can_move_to_target_playerboard_slot() -> bool:
|
|||||||
|
|
||||||
func _update_playerboard_slot_visual(slot_index: int):
|
func _update_playerboard_slot_visual(slot_index: int):
|
||||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||||
if not main or not main.playerboard_ui:
|
if not main or not main.playerboard_ui or (player.is_bot or player.is_in_group("Bots")):
|
||||||
return
|
return
|
||||||
|
|
||||||
var slot = main.playerboard_ui.get_child(slot_index)
|
var slot = main.playerboard_ui.get_child(slot_index)
|
||||||
@@ -608,7 +610,7 @@ func _update_playerboard_slot_visual(slot_index: int):
|
|||||||
|
|
||||||
func _highlight_adjacent_playerboard_slots():
|
func _highlight_adjacent_playerboard_slots():
|
||||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||||
if not main or not main.playerboard_ui:
|
if not main or not main.playerboard_ui or (player.is_bot or player.is_in_group("Bots")):
|
||||||
return
|
return
|
||||||
|
|
||||||
for i in range(25):
|
for i in range(25):
|
||||||
|
|||||||
@@ -73,8 +73,8 @@ func add_goal_completion_reward():
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
func can_use_special() -> bool:
|
func can_use_special() -> bool:
|
||||||
"""Returns true if player has at least 1 bar (4 points)."""
|
"""Returns true if player has at least 1 bar (4 points) AND IS NOT ON COOLDOWN."""
|
||||||
return current_points >= POINTS_PER_BAR
|
return current_points >= POINTS_PER_BAR and special_cooldown_timer <= 0
|
||||||
|
|
||||||
func get_bars() -> int:
|
func get_bars() -> int:
|
||||||
"""Returns current number of full bars."""
|
"""Returns current number of full bars."""
|
||||||
|
|||||||
@@ -194,7 +194,11 @@ func _execute_freeze_player():
|
|||||||
opponent.rpc("display_message", "You are frozen!")
|
opponent.rpc("display_message", "You are frozen!")
|
||||||
|
|
||||||
func _create_unfreeze_timer(target_player: Node3D, duration: float):
|
func _create_unfreeze_timer(target_player: Node3D, duration: float):
|
||||||
|
if not is_instance_valid(player) or not is_instance_valid(target_player):
|
||||||
|
return
|
||||||
|
|
||||||
await player.get_tree().create_timer(duration).timeout
|
await player.get_tree().create_timer(duration).timeout
|
||||||
|
|
||||||
if is_instance_valid(target_player):
|
if is_instance_valid(target_player):
|
||||||
target_player.set("is_frozen", false)
|
target_player.set("is_frozen", false)
|
||||||
target_player.rpc("display_message", "Unfrozen!")
|
target_player.rpc("display_message", "Unfrozen!")
|
||||||
@@ -250,7 +254,11 @@ func _execute_invisible_mode():
|
|||||||
player.rpc("display_message", "Invisible mode activated!")
|
player.rpc("display_message", "Invisible mode activated!")
|
||||||
|
|
||||||
func _create_invisibility_timer(duration: float):
|
func _create_invisibility_timer(duration: float):
|
||||||
|
if not is_instance_valid(player):
|
||||||
|
return
|
||||||
|
|
||||||
await player.get_tree().create_timer(duration).timeout
|
await player.get_tree().create_timer(duration).timeout
|
||||||
|
|
||||||
if is_instance_valid(player):
|
if is_instance_valid(player):
|
||||||
player.set("is_invisible", false)
|
player.set("is_invisible", false)
|
||||||
if player.get("original_movement_range"):
|
if player.get("original_movement_range"):
|
||||||
|
|||||||
Reference in New Issue
Block a user