782 lines
29 KiB
GDScript
782 lines
29 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 GLOBAL_COOLDOWN_MAX = 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 global_cooldown_updated(time_left: float, max_time: float)
|
|
signal powerup_unlocked(effect: int, level: int)
|
|
|
|
|
|
# New Helper functions for Targeting and Preview
|
|
|
|
func get_skill_affected_area(effect: int, center_pos: Vector2i) -> Array[Vector2i]:
|
|
var area: Array[Vector2i] = []
|
|
|
|
if effect == SpecialEffect.AREA_FREEZE:
|
|
var radius = 2 # 5x5 area for freeze
|
|
for x in range(-radius, radius + 1):
|
|
for z in range(-radius, radius + 1):
|
|
area.append(center_pos + Vector2i(x, z))
|
|
elif effect == SpecialEffect.BLOCK_FLOOR:
|
|
# Wall logic: project full line based on player orientation
|
|
var last_dir = player.movement_manager.last_move_direction if (player and player.movement_manager) else Vector2i(0, 1)
|
|
if last_dir.x != 0:
|
|
# Vertical Wall
|
|
for z in range(enhanced_gridmap.rows):
|
|
area.append(Vector2i(center_pos.x, z))
|
|
else:
|
|
# Horizontal Wall
|
|
for x in range(enhanced_gridmap.columns):
|
|
area.append(Vector2i(x, center_pos.y))
|
|
|
|
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)
|
|
|
|
# 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
|
|
var global_cooldown_timer: float = 0.0
|
|
|
|
# Targeting Mode
|
|
var is_targeting_mode: bool = false
|
|
var targeting_effect: int = -1
|
|
var target_indicator_pos: Vector2i = Vector2i.ZERO
|
|
|
|
# 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()
|
|
|
|
# Ensure Powerup Tiles (11-14) are NOT in the non-walkable list
|
|
if enhanced_gridmap and "non_walkable_items" in enhanced_gridmap:
|
|
for id in HOLO_TILES:
|
|
if id in enhanced_gridmap.non_walkable_items:
|
|
enhanced_gridmap.non_walkable_items.erase(id)
|
|
|
|
# =============================================================================
|
|
# 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: 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
|
|
|
|
# 1-PowerUp Rule: If this is a DIFFERENT power-up, clear the old one
|
|
var is_different = not inventory.get(effect, false)
|
|
var already_has_any = false
|
|
for e in inventory:
|
|
if inventory[e]:
|
|
already_has_any = true
|
|
break
|
|
|
|
if is_different and already_has_any:
|
|
print("Player %s replacing existing powerup with %s" % [player.name, SpecialEffect.keys()[effect]])
|
|
for e in inventory:
|
|
inventory[e] = false
|
|
powerup_levels[e] = 1 # Reset levels of discarded powerups
|
|
|
|
# Instant Level 8 on pickup (User Request)
|
|
inventory[effect] = true
|
|
powerup_levels[effect] = 8
|
|
emit_signal("inventory_updated", inventory)
|
|
emit_signal("powerup_unlocked", effect, 8)
|
|
print("[SpecialTiles] SUCCESS: Player %s picked up %s. Force Level 8 applied." % [player.name, SpecialEffect.keys()[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, 8)
|
|
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func sync_inventory_add(effect: int, level: int):
|
|
# Clear others on sync too to maintain 1-powerup rule
|
|
for e in inventory:
|
|
inventory[e] = false
|
|
powerup_levels[e] = 1 # Reset levels of discarded powerups
|
|
|
|
inventory[effect] = true
|
|
powerup_levels[effect] = level
|
|
emit_signal("inventory_updated", inventory)
|
|
emit_signal("powerup_unlocked", effect, level)
|
|
|
|
func remove_powerup(effect: int):
|
|
# User Request: "If the power up already use it will remain empty"
|
|
if inventory.get(effect, false):
|
|
inventory[effect] = false
|
|
powerup_levels[effect] = 1
|
|
emit_signal("inventory_updated", inventory)
|
|
print("[SpecialTiles] Player %s consumed powerup %s" % [player.name, SpecialEffect.keys()[effect]])
|
|
|
|
if player.is_multiplayer_authority() and multiplayer.has_multiplayer_peer() and multiplayer.multiplayer_peer.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
|
rpc("sync_inventory_remove", effect)
|
|
|
|
@rpc("any_peer", "call_local", "reliable")
|
|
func sync_inventory_remove(effect: int):
|
|
inventory[effect] = false
|
|
powerup_levels[effect] = 1
|
|
emit_signal("inventory_updated", inventory)
|
|
|
|
# =============================================================================
|
|
# 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 global_cooldown_timer > 0:
|
|
NotificationManager.send_message(player, "Skill in Cooldown! (%.1fs)" % global_cooldown_timer, NotificationManager.MessageType.WARNING)
|
|
return
|
|
|
|
# Check Attack Mode Restriction
|
|
if player.get("is_attack_mode") and effect == SpecialEffect.INVISIBLE_MODE:
|
|
NotificationManager.send_message(player, "Cannot enter Ghost mode while in Attack Mode!", NotificationManager.MessageType.WARNING)
|
|
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
|
|
|
|
var level = powerup_levels.get(effect, 1)
|
|
print("[SpecialTiles] Player %s activated %s (Lvl %d)." % [player.name, SpecialEffect.keys()[effect], level])
|
|
|
|
match effect:
|
|
SpecialEffect.FASTER_SPEED:
|
|
_execute_faster_speed()
|
|
SpecialEffect.AREA_FREEZE, SpecialEffect.BLOCK_FLOOR:
|
|
# TWO-CLICK LOGIC: First click starts Targeting Mode (Projection), second click executes.
|
|
if is_targeting_mode and targeting_effect == effect:
|
|
# SECOND CLICK: Execute at current indicator position
|
|
_execute_targeted_effect_v2(effect, target_indicator_pos)
|
|
_exit_targeting_mode()
|
|
else:
|
|
# FIRST CLICK: Start targeting
|
|
_enter_targeting_mode(effect)
|
|
return # Don't apply cooldown or generic effects yet
|
|
SpecialEffect.INVISIBLE_MODE:
|
|
_execute_invisible_mode(player)
|
|
|
|
# Apply 5s cooldown globally
|
|
global_cooldown_timer = 5.0
|
|
emit_signal("global_cooldown_updated", global_cooldown_timer, GLOBAL_COOLDOWN_MAX)
|
|
|
|
# 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")
|
|
|
|
# Consumable: Remove after use (User Request)
|
|
print("[SpecialTiles] Consuming %s after successful activation." % SpecialEffect.keys()[effect])
|
|
remove_powerup(effect)
|
|
|
|
# =============================================================================
|
|
# Targeting Mode Helpers
|
|
# =============================================================================
|
|
|
|
func _enter_targeting_mode(effect: int):
|
|
is_targeting_mode = true
|
|
targeting_effect = effect
|
|
print("[SpecialTiles] Entered Targeting Mode for %s" % SpecialEffect.keys()[effect])
|
|
# Visual feedback if needed
|
|
|
|
func _exit_targeting_mode():
|
|
if is_targeting_mode:
|
|
is_targeting_mode = false
|
|
targeting_effect = -1
|
|
if player.action_manager:
|
|
player.action_manager.clear_highlights()
|
|
print("[SpecialTiles] Exited Targeting Mode.")
|
|
|
|
func _update_targeting_preview():
|
|
if not player.is_multiplayer_authority(): return
|
|
|
|
# Logic for projection:
|
|
# Freeze: 4 tiles ahead
|
|
# Wall: Always behind player
|
|
var distance = 4
|
|
if targeting_effect == SpecialEffect.BLOCK_FLOOR:
|
|
distance = -1 # 1 tile behind
|
|
|
|
var move_dir = Vector2i.ZERO
|
|
if player.movement_manager:
|
|
move_dir = player.movement_manager.current_move_direction
|
|
if move_dir == Vector2i.ZERO:
|
|
move_dir = player.movement_manager.last_move_direction
|
|
|
|
if move_dir == Vector2i.ZERO:
|
|
move_dir = Vector2i(0, 1) # Fallback
|
|
|
|
var center_pos = player.current_position + (move_dir * distance)
|
|
target_indicator_pos = center_pos
|
|
|
|
# Show highlights via ActionManager
|
|
var area = get_skill_affected_area(targeting_effect, center_pos)
|
|
|
|
# User Request: Use yellow/orange like hover (ID 1 in EnhancedGridMap)
|
|
var indicator_id = 1
|
|
if player.action_manager:
|
|
player.action_manager.highlight_cells_if_authorized(area, indicator_id)
|
|
|
|
func _execute_targeted_effect_v2(effect: int, target_pos: Vector2i):
|
|
match effect:
|
|
SpecialEffect.AREA_FREEZE:
|
|
_execute_area_freeze(target_pos)
|
|
SpecialEffect.BLOCK_FLOOR:
|
|
_execute_block_floor(target_pos)
|
|
|
|
|
|
# =============================================================================
|
|
# 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():
|
|
# VFX: show speed initiator on all peers
|
|
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
|
player.rpc("play_skill_vfx", "skill_speed")
|
|
elif player.has_method("play_skill_vfx"):
|
|
player.play_skill_vfx("skill_speed")
|
|
if player.movement_manager:
|
|
player.movement_manager.set_speed_multiplier(1.5) # 50% faster
|
|
active_buffs[SpecialEffect.FASTER_SPEED] = FASTER_DURATION
|
|
SfxManager.rpc("play_rpc", "speed")
|
|
NotificationManager.send_message(player, "Speed Boost! (5s)", NotificationManager.MessageType.POWERUP)
|
|
|
|
func _execute_area_freeze(target_pos: Vector2i = Vector2i(-9999, -9999)):
|
|
# VFX: show freeze initiator on all peers
|
|
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
|
player.rpc("play_skill_vfx", "skill_freeze")
|
|
elif player.has_method("play_skill_vfx"):
|
|
player.play_skill_vfx("skill_freeze")
|
|
|
|
# Determine Position: if passed Sentinel, fallback to projection ahead
|
|
var center_pos = target_pos
|
|
if center_pos.x == -9999:
|
|
var last_dir = player.movement_manager.last_move_direction if (player and player.movement_manager) else Vector2i(0, 1)
|
|
center_pos = player.current_position + (last_dir * 4)
|
|
|
|
# Determine Level/Radius
|
|
var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1)
|
|
var radius = 2 # Always 5x5 (radius 2)
|
|
|
|
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
|
|
|
|
SfxManager.rpc("play_rpc", "freeze")
|
|
|
|
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 (Layer 2 - Overlay Level)
|
|
if player.is_multiplayer_authority():
|
|
var main_node = get_node_or_null("/root/Main")
|
|
if main_node and main_node.has_method("sync_grid_items_batch"):
|
|
var batch_data = []
|
|
for rx in range(-radius, radius + 1):
|
|
for rz in range(-radius, radius + 1):
|
|
var cell_x = center_pos.x + rx
|
|
var cell_z = center_pos.y + rz
|
|
# Put back on Layer 2 (Overlay) as requested
|
|
batch_data.append({"x": cell_x, "y": 2, "z": cell_z, "item": 5})
|
|
|
|
if not batch_data.is_empty():
|
|
main_node.rpc("sync_grid_items_batch", batch_data)
|
|
|
|
# Removal timer
|
|
get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func():
|
|
var cl_node = get_node_or_null("/root/Main")
|
|
if not cl_node: return
|
|
var clear_batch = []
|
|
for rx in range(-radius, radius + 1):
|
|
for rz in range(-radius, radius + 1):
|
|
var cx = center_pos.x + rx
|
|
var cz = center_pos.y + rz
|
|
# Check if it is STILL Freeze Overlay on Layer 2
|
|
if enhanced_gridmap.get_cell_item(Vector3i(cx, 2, cz)) == 5:
|
|
# Remove Item (back to -1)
|
|
clear_batch.append({"x": cx, "y": 2, "z": cz, "item": -1})
|
|
if not clear_batch.is_empty():
|
|
cl_node.rpc("sync_grid_items_batch", clear_batch)
|
|
)
|
|
|
|
|
|
func _execute_block_floor(target_pos: Vector2i = Vector2i(-999, -999)):
|
|
# VFX: show wall initiator on all peers
|
|
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
|
player.rpc("play_skill_vfx", "skill_wall")
|
|
elif player.has_method("play_skill_vfx"):
|
|
player.play_skill_vfx("skill_wall")
|
|
# "Wall Block" - Spawn line behind player
|
|
var behind_pos = target_pos
|
|
var last_dir = player.movement_manager.last_move_direction if player.movement_manager else Vector2i(0, 1)
|
|
|
|
if behind_pos == Vector2i(-999, -999):
|
|
behind_pos = player.current_position - last_dir
|
|
|
|
if not enhanced_gridmap.is_position_valid(behind_pos):
|
|
return
|
|
|
|
# Rollback: Should be vertical or horizontal based on direction
|
|
var neighbors = []
|
|
if last_dir.x != 0:
|
|
# Moving on X-axis (Columns) -> Vertical Wall (Fixed X, all Z)
|
|
for z in range(enhanced_gridmap.rows):
|
|
neighbors.append({"position": Vector2i(behind_pos.x, z)})
|
|
else:
|
|
# Moving on Z-axis (Rows) -> Horizontal Wall (Fixed Z, all X)
|
|
for x in range(enhanced_gridmap.columns):
|
|
neighbors.append({"position": Vector2i(x, behind_pos.y)})
|
|
|
|
if player.is_multiplayer_authority():
|
|
var main = get_node_or_null("/root/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 original_item = enhanced_gridmap.get_cell_item(block_pos)
|
|
|
|
# PROTECTED FLOOR CHECK: avoid overwriting Start (1), Safe (2), Finish (3), or Wall (4)
|
|
var is_immutable = false
|
|
if "immutable_items" in enhanced_gridmap:
|
|
if original_item in enhanced_gridmap.immutable_items:
|
|
is_immutable = true
|
|
if original_item in [1, 2, 3, 4, 15, 16] 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": original_item,
|
|
"timer": BLOCK_DURATION
|
|
})
|
|
|
|
if not batch_data.is_empty():
|
|
main.rpc("sync_grid_items_batch", batch_data)
|
|
|
|
# Notify
|
|
SfxManager.rpc("play_rpc", "wall")
|
|
NotificationManager.send_message(player, "Defensive Wall Deployed!", NotificationManager.MessageType.POWERUP)
|
|
|
|
func _execute_invisible_mode(target: Node3D):
|
|
# VFX: show ghost initiator on all peers (on the caster/player, not the target)
|
|
if player.is_multiplayer_authority() and player.has_method("can_rpc") and player.can_rpc():
|
|
player.rpc("play_skill_vfx", "skill_ghost")
|
|
elif player.has_method("play_skill_vfx"):
|
|
player.play_skill_vfx("skill_ghost")
|
|
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
|
|
|
|
SfxManager.rpc("play_rpc", "ghost")
|
|
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, only_common: bool = false, full_density: bool = false):
|
|
# "spawn / replace your nearby tiles into power up ( special tiles )"
|
|
# New PowerUp Tiles are 11, 12, 13, 14
|
|
SfxManager.rpc("play_rpc", "generate_tile")
|
|
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 not full_density and rng.randf() > 0.5: continue
|
|
|
|
# PROTECTED FLOOR CHECK: Don't spawn on existing walls or void
|
|
var f0 = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 0, pos.y))
|
|
var f1 = enhanced_gridmap.get_cell_item(Vector3i(pos.x, 1, pos.y))
|
|
|
|
# Stop n Go: Don't overwrite static powerup spawns (ID 15 floor)
|
|
if LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO) and f0 == 15:
|
|
continue
|
|
|
|
if f0 in [4, -1] or f1 in [4, 16]:
|
|
continue
|
|
|
|
var item_id: int
|
|
var mode = LobbyManager.get_game_mode()
|
|
|
|
if only_common or LobbyManager.is_game_mode(GameMode.Mode.STOP_N_GO):
|
|
# Spawn ONLY common tiles (7-10) in Stop n Go mode (User Request)
|
|
item_id = rng.randi_range(7, 10)
|
|
else:
|
|
# Other modes: 80% Chance for Common Tile (7-10), 20% for PowerUp
|
|
if rng.randf() < 0.8:
|
|
item_id = rng.randi_range(7, 10)
|
|
else:
|
|
# 20% Chance for PowerUp
|
|
if LobbyManager.is_game_mode(GameMode.Mode.TEKTON_DOORS):
|
|
# Restrict to Speed (11) and Ghost (14) for Tekton Doors
|
|
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 Global Cooldown
|
|
if global_cooldown_timer > 0:
|
|
global_cooldown_timer -= delta
|
|
if global_cooldown_timer <= 0:
|
|
global_cooldown_timer = 0
|
|
emit_signal("global_cooldown_updated", global_cooldown_timer, GLOBAL_COOLDOWN_MAX)
|
|
|
|
# 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()
|
|
|
|
if is_targeting_mode:
|
|
_update_targeting_preview()
|
|
|
|
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
|