Files
tekton/scripts/managers/special_tiles_manager.gd
T

693 lines
26 KiB
GDScript

extends Node
# SpecialTilesManager - Handles special effects triggered by holo tile pickups
# Holo tile indices (11-14) trigger special effects
const HOLO_TILES = [11, 12, 13, 14]
enum SpecialEffect {
FASTER_SPEED, # ID 11
AREA_FREEZE, # ID 12
BLOCK_FLOOR, # ID 13
INVISIBLE_MODE # ID 14
}
# Levels & Cooldowns
var powerup_levels: Dictionary = {} # EffectEnum -> int (1 to 8)
var powerup_cooldowns: Dictionary = {} # EffectEnum -> float (Time Remaining)
var active_buffs: Dictionary = {} # EffectEnum -> float (Duration Running)
# Cooldown Constants (Level 1 / Level 8)
const COOLDOWN_L1 = 15.0
const COOLDOWN_L8 = 5.0
const FASTER_DURATION = 5.0
const FREEZE_SLOW_DURATION = 3.0
signal cooldown_updated(effect: int, time_left: float, max_time: float)
signal powerup_unlocked(effect: int, level: int)
var wall_orientation_horizontal: bool = false # False = Vertical, True = Horizontal
# New Helper functions for Targeting and Preview
func get_skill_affected_area(effect: int, center_pos: Vector2i) -> Array[Vector2i]:
var area: Array[Vector2i] = []
match effect:
SpecialEffect.AREA_FREEZE:
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
var radius = 1
if current_lvl >= 5:
radius = 2
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center_pos + Vector2i(x, y)
# Validate bounds
if enhanced_gridmap.is_position_valid(pos):
area.append(pos)
SpecialEffect.BLOCK_FLOOR:
# Logic: Based on toggled orientation state
var is_horizontal = wall_orientation_horizontal
if is_horizontal:
for x in range(enhanced_gridmap.columns):
area.append(Vector2i(x, center_pos.y))
else:
for z in range(enhanced_gridmap.rows):
area.append(Vector2i(center_pos.x, z))
return area
func execute_targeted_effect(effect: int, target_pos: Vector2i):
# Apply Cooldown NOW
var level = powerup_levels.get(effect, 1)
var cooldown_time = COOLDOWN_L1 + ((level - 1) * (COOLDOWN_L8 - COOLDOWN_L1) / 7.0)
powerup_cooldowns[effect] = cooldown_time
emit_signal("cooldown_updated", effect, cooldown_time, cooldown_time)
print("[SpecialTiles] Executing Targeted Effect %s at %s" % [SpecialEffect.keys()[effect], target_pos])
match effect:
SpecialEffect.AREA_FREEZE:
_execute_area_freeze(target_pos)
SpecialEffect.BLOCK_FLOOR:
_execute_block_floor(target_pos)
# Animation / Shake
if player.is_multiplayer_authority():
player.rpc("trigger_screen_shake", "light")
# Also reset action loop? (ONLY for human players)
if not (player.is_bot or player.is_in_group("Bots")):
var main = player.get_tree().get_root().get_node_or_null("Main")
if main and main.ui_manager:
main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE
# Random shape patterns for 3x3 area (relative offsets from center)
const PATTERNS = {
"T": [Vector2i(0, -1), Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0)],
"L": [Vector2i(0, -1), Vector2i(0, 0), Vector2i(0, 1), Vector2i(1, 1)],
"I_H": [Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0)],
"I_V": [Vector2i(0, -1), Vector2i(0, 0), Vector2i(0, 1)],
"PLUS": [Vector2i(0, -1), Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0), Vector2i(0, 1)],
"CORNER": [Vector2i(-1, -1), Vector2i(0, -1), Vector2i(-1, 0), Vector2i(0, 0)],
"FULL": [Vector2i(-1, -1), Vector2i(0, -1), Vector2i(1, -1), Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0), Vector2i(-1, 1), Vector2i(0, 1)],
"DOT3": [Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0)]
}
var player: Node3D
var enhanced_gridmap: Node
var rng: RandomNumberGenerator
# Effect durations
const FREEZE_ZONE_DURATION = 15.0
const FREEZE_SLOW_MULTIPLIER = 0.2 # Super slow down
const BLOCK_DURATION = 9.0
const INVISIBLE_DURATION = 6.0
# Active effect tracking
var blocked_tiles: Array[Dictionary] = [] # {position: Vector3i, original_item: int, timer: float}
var freeze_zones: Array[Dictionary] = [] # {position: Vector2i, timer: float}
var active_freeze_zones: Array = [] # Array of {center, radius, timer}
var invisible_timer: float = 0.0
# INVENTORY SYSTEM
# Stores count of each power-up type. Max 1 per type as per user request?
# "player can store 1 of each different power up"
var inventory = {
SpecialEffect.FASTER_SPEED: false,
SpecialEffect.AREA_FREEZE: false,
SpecialEffect.BLOCK_FLOOR: false,
SpecialEffect.INVISIBLE_MODE: false
}
# Signal for UI
signal inventory_updated(inventory_data: Dictionary)
func initialize(p_player: Node3D, p_gridmap: Node):
player = p_player
enhanced_gridmap = p_gridmap
rng = RandomNumberGenerator.new()
rng.randomize()
# =============================================================================
# Helper: Item ID to Effect Enum
# =============================================================================
func get_effect_from_item(item_id: int) -> int:
var mode = LobbyManager.get_game_mode()
var is_restricted = GameMode.is_restricted(mode)
match item_id:
11: return SpecialEffect.FASTER_SPEED
12:
if is_restricted: return -1
return SpecialEffect.AREA_FREEZE
13:
if is_restricted: return -1
return SpecialEffect.BLOCK_FLOOR
14: return SpecialEffect.INVISIBLE_MODE
_: return -1
func add_powerup_from_item(item_id: int):
var effect = get_effect_from_item(item_id)
if effect == -1: return
# Unlock or Level Up
if not inventory.get(effect, false):
# New Unlock
inventory[effect] = true
powerup_levels[effect] = 1
emit_signal("inventory_updated", inventory)
emit_signal("powerup_unlocked", effect, 1)
print("Player %s unlocked powerup %s (Lvl 1)" % [player.name, SpecialEffect.keys()[effect]])
else:
# Level Up
var current_lvl = powerup_levels.get(effect, 1)
if current_lvl < 8:
powerup_levels[effect] = current_lvl + 1
emit_signal("powerup_unlocked", effect, powerup_levels[effect])
print("Player %s leveled up %s to Lvl %d" % [player.name, SpecialEffect.keys()[effect], powerup_levels[effect]])
if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
rpc("sync_inventory_add", effect, powerup_levels[effect])
@rpc("any_peer", "call_local", "reliable")
func sync_inventory_add(effect: int, level: int):
inventory[effect] = true
powerup_levels[effect] = level
emit_signal("inventory_updated", inventory)
emit_signal("powerup_unlocked", effect, level)
func remove_powerup(effect: int):
# We DO NOT remove item from inventory on use anymore (for reusable leveling system)?
# Wait, user request: "cooldown is decarese and max is level 8"
# Does "activate it" consume the item?
# User says "it will cooldown agian for 15 seconds".
# Implies PERMANENT UNLOCK once picked up, reusable with cooldown.
# So we NEVER set inventory[effect] = false unless reset.
pass
@rpc("any_peer", "call_local", "reliable")
func sync_inventory_remove(effect: int):
# Deprecated for new system, but kept for compatibility if needed
pass
# =============================================================================
# Activate Effect (Explicit Target)
# =============================================================================
func activate_effect(effect: int, target_player: Node3D = null):
# Validation
if not inventory.get(effect, false):
print("PowerUp %s not found in inventory or false. Inventory: %s" % [effect, inventory])
return
# Check Cooldown
if powerup_cooldowns.get(effect, 0.0) > 0:
print("PowerUp %s on cooldown." % SpecialEffect.keys()[effect])
return
# Check Carrying Restriction
if player.get("is_carrying_tekton") and effect != SpecialEffect.FASTER_SPEED:
NotificationManager.send_message(player, "Cannot use this power while carrying a Tekton!", NotificationManager.MessageType.WARNING)
return
# Calculate Cooldown based on Level
var level = powerup_levels.get(effect, 1)
# Linear Interp: Lvl 1 = 15s, Lvl 8 = 5s
# Slope = (5 - 15) / (8 - 1) = -10 / 7 = -1.428...
var cooldown_time = COOLDOWN_L1 + ((level - 1) * (COOLDOWN_L8 - COOLDOWN_L1) / 7.0)
powerup_cooldowns[effect] = cooldown_time
emit_signal("cooldown_updated", effect, cooldown_time, cooldown_time)
print("[SpecialTiles] Player %s activated %s (Lvl %d). Cooldown: %.1fs" % [player.name, SpecialEffect.keys()[effect], level, cooldown_time])
match effect:
SpecialEffect.FASTER_SPEED:
_execute_faster_speed()
SpecialEffect.AREA_FREEZE, SpecialEffect.BLOCK_FLOOR:
# Enter Targeting Mode instead of executing immediately (ONLY for human players)
if not (player.is_bot or player.is_in_group("Bots")):
var main = player.get_tree().get_root().get_node_or_null("Main")
if main and main.ui_manager:
# Toggle Logic for Wall Block
if main.ui_manager.current_action_state == main.ui_manager.ActionState.TARGETING and main.ui_manager.pending_skill_id == effect:
if effect == SpecialEffect.BLOCK_FLOOR:
toggle_wall_orientation()
powerup_cooldowns[effect] = 0.0 # Revert cooldown
emit_signal("cooldown_updated", effect, 0.0, 0.0)
return
main.ui_manager.current_action_state = main.ui_manager.ActionState.TARGETING
main.ui_manager.pending_skill_id = effect
var msg = "Select a target area..."
if effect == SpecialEffect.BLOCK_FLOOR:
msg = "Click again to toggle Vertical/Horizontal"
NotificationManager.send_message(player, msg, NotificationManager.MessageType.NORMAL)
# Do NOT set cooldown yet. Cooldown sets on execution.
# Revert the cooldown set above (hacky but handles the split flow)
powerup_cooldowns[effect] = 0.0
emit_signal("cooldown_updated", effect, 0.0, 0.0)
print("[SpecialTiles] Entered Targeting Mode for %s" % SpecialEffect.keys()[effect])
return # Exit, wait for input
SpecialEffect.INVISIBLE_MODE:
_execute_invisible_mode(player)
# Play generic cast animation or sound?
if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
player.rpc("trigger_screen_shake", "light")
# Sync cooldown to others not strictly needed unless UI shows it?
# Probably local UI only.
# =============================================================================
# Check if item is a holo tile
# =============================================================================
func is_holo_tile(item_id: int) -> bool:
return item_id in HOLO_TILES
# =============================================================================
# Effect Implementations (New)
# =============================================================================
func _execute_faster_speed():
if player.movement_manager:
player.movement_manager.set_speed_multiplier(1.5) # 50% faster
active_buffs[SpecialEffect.FASTER_SPEED] = FASTER_DURATION
NotificationManager.send_message(player, "Speed Boost! (5s)", NotificationManager.MessageType.POWERUP)
func _execute_area_freeze(center_pos: Vector2i = Vector2i.ZERO):
if center_pos == Vector2i.ZERO:
# Fallback to old behavior if no target provided (or error)
return
# 3. Determine Radius based on Level
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
var radius = 1
if current_lvl >= 5:
radius = 2 # Bigger area at high levels
print("Player %s executing Area Freeze at %s (Lvl %d, Rad %d)" % [player.name, center_pos, current_lvl, radius])
# Register Zone for persistence
active_freeze_zones.append({
"center": center_pos,
"radius": radius,
"timer": FREEZE_SLOW_DURATION
})
# Initial Check (Instant Feedback)
var all_players = player.get_tree().get_nodes_in_group("Players")
var hit_count = 0
for p in all_players:
# Check distance (Chebyshev distance for square area)
var dx = abs(p.current_position.x - center_pos.x)
var dy = abs(p.current_position.y - center_pos.y)
# If inside square radius
if dx <= radius and dy <= radius:
if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
p.rpc("apply_slow_effect", FREEZE_SLOW_DURATION)
NotificationManager.send_message(p, "Caught in Freeze Zone!", NotificationManager.MessageType.WARNING)
if p != player: # Don't score for freezing self (unless desired?) - Assuming enemies
hit_count += 1
if hit_count > 0 and player.is_multiplayer_authority():
var is_sng = LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO)
if not is_sng:
var points = hit_count * 50
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
var gcm = main.get_node_or_null("GoalsCycleManager")
if gcm:
gcm.rpc("request_add_score", points)
NotificationManager.send_message(player, "Hit %d Players! +%d Pts" % [hit_count, points], NotificationManager.MessageType.GOAL)
else:
NotificationManager.send_message(player, "Hit %d Players!" % hit_count, NotificationManager.MessageType.GOAL)
# Visual Feedback (Overlay Layer 2)
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main and main.has_method("sync_grid_items_batch"):
var batch_data = []
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center_pos + Vector2i(x, y)
if enhanced_gridmap.is_position_valid(pos):
# Use Item 5 (Freeze Floor) on Layer 2
batch_data.append({"x": pos.x, "y": 2, "z": pos.y, "item": 5})
if not batch_data.is_empty():
main.rpc("sync_grid_items_batch", batch_data)
# Cleanup visual timer (managed locally by author)
get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func():
var restore_batch = []
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center_pos + Vector2i(x, y)
if enhanced_gridmap.is_position_valid(pos):
# Check if it is STILL Freeze Overlay
var current_check = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 2, pos.y))
if current_check == 5:
restore_batch.append({"x": pos.x, "y": 2, "z": pos.y, "item": -1})
if not restore_batch.is_empty():
main.rpc("sync_grid_items_batch", restore_batch)
)
func toggle_wall_orientation():
wall_orientation_horizontal = !wall_orientation_horizontal
var mode_str = "HORIZONTAL" if wall_orientation_horizontal else "VERTICAL"
NotificationManager.send_message(player, "Wall Mode: " + mode_str, NotificationManager.MessageType.NORMAL)
func _execute_block_floor(target_pos: Vector2i):
# "Wall Block"
var is_horizontal = wall_orientation_horizontal
var neighbors = []
if is_horizontal:
# Block entire Row (Fixed Z, iterate all X)
var row_z = target_pos.y
for x in range(enhanced_gridmap.columns):
neighbors.append({"position": Vector2i(x, row_z)})
print("Player %s activated Wall Block: HORIZONTAL ROW (Z=%d)" % [player.name, row_z])
else:
# Block entire Column (Fixed X, iterate all Z)
var col_x = target_pos.x
for z in range(enhanced_gridmap.rows):
neighbors.append({"position": Vector2i(col_x, z)})
print("Player %s activated Wall Block: VERTICAL COLUMN (X=%d)" % [player.name, col_x])
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main and main.has_method("sync_grid_items_batch"):
var batch_data = []
for n in neighbors:
var pos = n.position
if _is_position_blocked_by_stand(pos): continue
var block_pos = Vector3i(pos.x, 0, pos.y)
var current_item = enhanced_gridmap.get_cell_item(block_pos)
var is_immutable = false
if "immutable_items" in enhanced_gridmap:
if current_item in enhanced_gridmap.immutable_items:
is_immutable = true
if current_item == 4 or is_immutable: continue
batch_data.append({"x": block_pos.x, "y": 0, "z": block_pos.z, "item": 4})
# Record for restoration
blocked_tiles.append({
"position": block_pos,
"original_item": current_item,
"timer": BLOCK_DURATION
})
if not batch_data.is_empty():
main.rpc("sync_grid_items_batch", batch_data)
# Notify
var all_players = player.get_tree().get_nodes_in_group("Players")
for p in all_players:
if p.current_position == target_pos:
NotificationManager.send_message(p, "Wall Block Created!", NotificationManager.MessageType.POWERUP)
func _execute_invisible_mode(target: Node3D):
target.is_invisible = true
invisible_timer = INVISIBLE_DURATION
# Visual Feedback: Ghost Mode (Low Alpha)
if target.has_method("sync_modulate"):
target.rpc("sync_modulate", Color(1.0, 1.0, 1.0, 0.4)) # 40% Opacity
NotificationManager.send_message(target, "Invisible Mode!", NotificationManager.MessageType.POWERUP)
# =============================================================================
# Helper: Spawn Powerups (For Super Push)
# =============================================================================
func spawn_powerups_around(center: Vector2i, force_powerups: bool = true):
# "spawn / replace your nearby tiles into power up ( special tiles )"
# New PowerUp Tiles are 11, 12, 13, 14
var radius = 2
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center + Vector2i(x, y)
if _is_position_blocked_by_stand(pos):
continue
if enhanced_gridmap.is_position_valid(pos):
# Random chance to spawn ANYTHING at this spot (keep density reasonable)
if rng.randf() > 0.5: continue
var item_id: int
# 70% Chance for Normal Tile (7-10)
if rng.randf() < 0.7:
item_id = rng.randi_range(7, 10)
else:
# 30% Chance for PowerUp (Speed 11, Freeze 12, Ghost 14 - Exclude Wall 13 in restricted modes)
var mode = LobbyManager.get_game_mode()
var is_restricted = GameMode.is_restricted(mode)
if is_restricted:
item_id = [11, 14].pick_random()
else:
item_id = rng.randi_range(11, 14)
var cell = Vector3i(pos.x, 1, pos.y)
# PREVENT SPAWNING ON FROZEN FLOORS (Visual/Lag Fix)
var is_frozen = false
if enhanced_gridmap:
# Check Layer 2 for Freeze Overlay (ID 5)
if enhanced_gridmap.get_cell_item(Vector3i(pos.x, 2, pos.y)) == 5:
is_frozen = true
if is_frozen:
continue
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, item_id)
func _update_freeze_zones(delta: float):
# Only the authority of this manager (the caster) handles the timers and cleanup
if not active_freeze_zones.is_empty():
var zones_to_remove = []
for i in range(active_freeze_zones.size()):
var zone = active_freeze_zones[i]
zone.timer -= delta
# Check for players inside this zone (Trap Logic)
var all_players = player.get_tree().get_nodes_in_group("Players")
for p in all_players:
# Invisible Immunity (Passive)
if p.get("is_invisible"): continue
var dx = abs(p.current_position.x - zone.center.x)
var dy = abs(p.current_position.y - zone.center.y)
# If inside zone
if dx <= zone.radius and dy <= zone.radius:
# Apply slow effect via RPC only IF not already slowed to prevent network flood
if "movement_manager" in p and p.movement_manager and p.movement_manager.speed_multiplier >= 1.0:
p.rpc("apply_slow_effect", 0.5)
if zone.timer <= 0:
zones_to_remove.append(i)
# Cleanup expired zones
zones_to_remove.reverse()
for idx in zones_to_remove:
active_freeze_zones.remove_at(idx)
func _check_for_icy_floor():
# Every player checks if they are standing on an icy floor (item 15 on layer 2)
# This ensures slow-mo works even if zones were cast by another player.
if not player.is_multiplayer_authority():
return
# Invisible Immunity (Passive)
if player.is_invisible:
return
if not enhanced_gridmap:
return
var current_item = enhanced_gridmap.get_cell_item(Vector3i(player.current_position.x, 2, player.current_position.y))
if current_item == 5: # Freeze Floor
_apply_slow_mo(player)
elif player.movement_manager and player.movement_manager.speed_multiplier < 1.0:
# Check if we should restore speed
# In this case, we'll let _apply_slow_mo's timer handle it,
# OR we can explicitly reset here if NOT on item 15.
pass
func _process(delta):
# Update Cooldowns
for effect in powerup_cooldowns.keys():
if powerup_cooldowns[effect] > 0:
powerup_cooldowns[effect] -= delta
# Emit signal occasionally or only on change? Every frame might be too much for UI?
# Optimization: Emit every 0.1s or if diff is significant?
# For snappy UI text, frame sync is okay for local player.
emit_signal("cooldown_updated", effect, powerup_cooldowns[effect], 0.0) # max unused for tick
if powerup_cooldowns[effect] <= 0:
powerup_cooldowns[effect] = 0
emit_signal("cooldown_updated", effect, 0, 0)
# Update Active Buffs (Speed)
if active_buffs.has(SpecialEffect.FASTER_SPEED):
active_buffs[SpecialEffect.FASTER_SPEED] -= delta
if active_buffs[SpecialEffect.FASTER_SPEED] <= 0:
active_buffs.erase(SpecialEffect.FASTER_SPEED)
if player.movement_manager:
player.movement_manager.set_speed_multiplier(1.0) # Reset
NotificationManager.send_message(player, "Speed Boost Ended", NotificationManager.MessageType.NORMAL)
_update_blocked_tiles(delta)
_update_invisible_timer(delta)
_update_freeze_zones(delta)
_check_for_icy_floor()
func _apply_slow_mo(target_player: Node3D):
if target_player.has_method("apply_stagger") and target_player.is_frozen:
return # Already fully frozen/staggered
if target_player.movement_manager:
target_player.movement_manager.set_speed_multiplier(FREEZE_SLOW_MULTIPLIER)
# Visual tint
if target_player.has_method("sync_modulate"):
target_player.rpc("sync_modulate", Color(0.6, 0.8, 1.0)) # Icy blue
# Reset speed after a short delay if they leave
_create_restore_speed_timer(target_player, 0.2)
func _create_restore_speed_timer(target_player: Node3D, duration: float):
# We use a short timer to reset speed. If they are still in the zone,
# _process will re-apply it next frame.
await player.get_tree().create_timer(duration).timeout
if is_instance_valid(target_player) and target_player.movement_manager:
# Check if they are still on an icy floor
var still_in_zone = false
if enhanced_gridmap:
var item = enhanced_gridmap.get_cell_item(Vector3i(target_player.current_position.x, 2, target_player.current_position.y))
if item == 5: # Freeze Floor
still_in_zone = true
if not still_in_zone:
target_player.movement_manager.set_speed_multiplier(1.0)
if target_player.has_method("sync_modulate"):
target_player.rpc("sync_modulate", Color.WHITE)
func _update_invisible_timer(delta: float):
if invisible_timer > 0:
invisible_timer -= delta
if invisible_timer <= 0:
invisible_timer = 0
if is_instance_valid(player):
player.is_invisible = false
# Reset Visuals
if player.has_method("sync_modulate"):
player.rpc("sync_modulate", Color.WHITE)
NotificationManager.send_message(player, "Invisibility Ended", NotificationManager.MessageType.NORMAL)
# =============================================================================
# Helper Functions
# =============================================================================
func _get_empty_neighbors_recursive(center: Vector2i, radius: int) -> Array[Vector2i]:
var result: Array[Vector2i] = []
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
var pos = center + Vector2i(x, y)
if enhanced_gridmap.is_position_valid(pos):
if enhanced_gridmap.get_cell_item(Vector3i(pos.x, 1, pos.y)) == -1:
result.append(pos)
return result
func _update_blocked_tiles(delta: float):
var tiles_to_restore: Array[int] = []
for i in range(blocked_tiles.size()):
blocked_tiles[i].timer -= delta
if blocked_tiles[i].timer <= 0:
tiles_to_restore.append(i)
# Restore tiles in reverse order to maintain indices
tiles_to_restore.reverse()
for idx in tiles_to_restore:
var tile_data = blocked_tiles[idx]
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
main.rpc("sync_grid_item", tile_data.position.x, tile_data.position.y, tile_data.position.z, tile_data.original_item)
blocked_tiles.remove_at(idx)
if tiles_to_restore.size() > 0:
enhanced_gridmap.initialize_astar()
# =============================================================================
# Shield Check (for Invisible Mode)
# =============================================================================
func check_shield_and_cancel_effect() -> bool:
"""Returns true if player has shield (invisible mode) and cancels the incoming effect."""
if player.get("is_invisible"):
player.set("is_invisible", false)
invisible_timer = 0 # Cancel timer
if player.get("original_movement_range"):
player.movement_range = player.original_movement_range
NotificationManager.send_message(player, NotificationManager.MESSAGES.SHIELD_BLOCKED, NotificationManager.MessageType.POWERUP)
return true
return false
func _create_unfreeze_timer(target_player: Node3D, duration: float):
await player.get_tree().create_timer(duration).timeout
if is_instance_valid(target_player):
target_player.set("is_frozen", false)
# Reset visuals
if target_player.has_method("sync_modulate"):
target_player.rpc("sync_modulate", Color.WHITE)
NotificationManager.send_message(target_player, "You are no longer frozen!", NotificationManager.MessageType.NORMAL)
func _is_position_blocked_by_stand(target_pos: Vector2i) -> bool:
if not player: return false
# Raycast check for Static Tekton Stand
# Check CENTER of tile (x+0.5) at LOW height (0.3)
var from = Vector3(target_pos.x + 0.5, 0.3, target_pos.y + 0.5) + Vector3.UP * 2.0
var to = Vector3(target_pos.x + 0.5, 0.3, target_pos.y + 0.5) + Vector3.DOWN * 2.0
var query = PhysicsRayQueryParameters3D.create(from, to)
query.collide_with_areas = false
query.collide_with_bodies = true
var space_state = player.get_world_3d().direct_space_state
var result = space_state.intersect_ray(query)
if result:
if result.collider != player:
return true
return false