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: match item_id: 11: return SpecialEffect.FASTER_SPEED 12: return SpecialEffect.AREA_FREEZE 13: 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 (Turn Floor Blue - Item 12 on Layer 0) if player.is_multiplayer_authority(): # Sync Icy Floor (Layer 0) 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): var main = player.get_tree().get_root().get_node_or_null("Main") if main: # CHECK: Don't overwrite Wall Block (Item 4) var current_item = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y)) if current_item != 4: # 4 is Wall Block # Use Item 12 (Blue Freeze Tile) on Layer 0 (Floor) if multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: main.rpc("sync_grid_item", pos.x, 0, pos.y, 12) # Cleanup visual timer (managed locally by author) get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func(): 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): var main = player.get_tree().get_root().get_node_or_null("Main") if main: # CHECK: Only restore if it is STILL Ice (Item 12) # This prevents removing a Wall that was placed AFTER the freeze started var current_check = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y)) if current_check == 12: # Restore to Item 0 (Standard Floor) main.rpc("sync_grid_item", pos.x, 0, pos.y, 0) ) 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]) for n in neighbors: var pos = n.position # PHYSICS CHECK if _is_position_blocked_by_stand(pos): continue var block_pos = Vector3i(pos.x, 0, pos.y) # Check current item first var current_item = enhanced_gridmap.get_cell_item(block_pos) # Skip if already a wall or immutable # We assume Item 4 is the wall/stand. # Also check enhanced_gridmap.immutable_items if available 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: # Don't overwrite existing walls/stands, and don't schedule them for "restoration" (deletion) 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", block_pos.x, block_pos.y, block_pos.z, 4) # Record for restoration blocked_tiles.append({ "position": block_pos, "original_item": current_item, # Restore the ACTUAL item that was there (e.g. ground 0 or maybe a dropped item?) "timer": BLOCK_DURATION }) # 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, 12, 14].pick_random() else: item_id = rng.randi_range(11, 14) var cell = Vector3i(pos.x, 1, pos.y) 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 repeatedly # We use a short duration so it expires quickly if they leave 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 == 15: _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 == 15: 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