This commit is contained in:
2026-01-29 03:04:24 +08:00
parent 6949e20a1f
commit e66ba7542c
12 changed files with 687 additions and 549 deletions
+188 -229
View File
@@ -39,6 +39,21 @@ const INVISIBLE_DURATION = 6.0
var blocked_tiles: Array[Dictionary] = [] # {position: Vector3i, original_item: int, timer: float}
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.BURN_TILES: false, # Coin
SpecialEffect.SPAWN_TILES: false, # (Merged with Coin or deprecated? User said "coin : random between two")
# Let's map Items 7-10 to Effects
# 7=Heart=Block, 8=Diamond=Freeze, 9=Star=Invisible, 10=Coin=Burn/Spawn
SpecialEffect.BLOCK_FLOOR: false,
SpecialEffect.FREEZE_PLAYER: 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
@@ -46,6 +61,83 @@ func initialize(p_player: Node3D, p_gridmap: Node):
rng = RandomNumberGenerator.new()
rng.randomize()
# =============================================================================
# Helper: Item ID to Effect Enum
# =============================================================================
func get_effect_from_item(item_id: int) -> int:
match item_id:
7: return SpecialEffect.BLOCK_FLOOR # Heart
8: return SpecialEffect.FREEZE_PLAYER # Diamond
9: return SpecialEffect.INVISIBLE_MODE # Star
10: return SpecialEffect.BURN_TILES # Coin (Handles Burn or Spawn)
_: return -1
func add_powerup_from_item(item_id: int):
var effect = get_effect_from_item(item_id)
if effect != -1:
inventory[effect] = true
emit_signal("inventory_updated", inventory)
print("Player %s picked up powerup for effect %s" % [player.name, SpecialEffect.keys()[effect]])
if player.is_multiplayer_authority():
rpc("sync_inventory_add", effect)
@rpc("any_peer", "call_local", "reliable")
func sync_inventory_add(effect: int):
inventory[effect] = true
emit_signal("inventory_updated", inventory)
func remove_powerup(effect: int):
inventory[effect] = false
emit_signal("inventory_updated", inventory)
if player.is_multiplayer_authority():
rpc("sync_inventory_remove", effect)
@rpc("any_peer", "call_local", "reliable")
func sync_inventory_remove(effect: int):
inventory[effect] = false
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):
return # Start/Client mismatch
# Consume
remove_powerup(effect)
print("[SpecialTiles] Player %s activated %s on %s" % [player.name, SpecialEffect.keys()[effect], target_player.name if target_player else "Self"])
match effect:
SpecialEffect.BURN_TILES:
# Coin: Random between Burn or Spawn
# "coin : random between two... make it not use directly" -> When activated, it does one of them.
if rng.randf() < 0.5:
_execute_burn_tiles(target_player)
else:
# Spawn tiles around SELF (as per user request "around activating player")
_execute_spawn_tiles(player)
SpecialEffect.BLOCK_FLOOR:
if target_player:
_execute_block_floor(target_player)
SpecialEffect.FREEZE_PLAYER:
if target_player:
_execute_freeze_player(target_player)
SpecialEffect.INVISIBLE_MODE:
# Always self
_execute_invisible_mode(player)
# Play generic cast animation or sound?
if player.is_multiplayer_authority():
player.rpc("trigger_screen_shake", "light")
# =============================================================================
# Check if item is a holo tile
@@ -65,15 +157,15 @@ func trigger_random_effect():
match effect:
SpecialEffect.BURN_TILES:
_execute_burn_tiles()
_execute_burn_tiles(_get_random_opponent())
SpecialEffect.SPAWN_TILES:
_execute_spawn_tiles()
_execute_spawn_tiles(player)
SpecialEffect.FREEZE_PLAYER:
_execute_freeze_player()
_execute_freeze_player(_get_random_opponent())
SpecialEffect.BLOCK_FLOOR:
_execute_block_floor()
_execute_block_floor(player)
SpecialEffect.INVISIBLE_MODE:
_execute_invisible_mode()
_execute_invisible_mode(player)
# Sync effect to all clients
if player.is_multiplayer_authority():
@@ -87,262 +179,122 @@ func sync_effect_triggered(effect: int):
# Effect Implementations
# =============================================================================
func _execute_burn_tiles():
# NEW LOGIC: Put back random target tiles from their playerboard to their position nearest
# Find random opponent
var opponent = _get_random_opponent()
if not opponent:
print("[SpecialTiles] No opponent found for BURN_TILES")
return
func _execute_burn_tiles(target: Node3D):
if not target: return
# Get opponent's playerboard items
# Knock tiles from target's board
var board_indices = []
for i in range(opponent.playerboard.size()):
if opponent.playerboard[i] != -1:
for i in range(target.playerboard.size()):
if target.playerboard[i] != -1:
board_indices.append(i)
if board_indices.is_empty():
return # Nothing to burn
return
# Pick random 1x (3x3 equivalent = ~3-4 tiles) or 2x amount
# Let's say we burn 3 to 6 tiles
var burn_count = rng.randi_range(3, 6)
board_indices.shuffle()
var tiles_burned = 0
# Get valid empty spots near opponent to dump tiles
var empty_spots = _get_empty_neighbors_recursive(opponent.current_position, 2)
empty_spots.shuffle()
# Drop logic similar to burn but we just destroy them or scatter?
# "BURN_TILES, # Remove 3x3 pattern tiles" -> User request says Remove pattern.
# Original code did burn.
# Let's just remove them.
for i in range(min(burn_count, board_indices.size())):
var slot_idx = board_indices[i]
var item = opponent.playerboard[slot_idx]
target.playerboard[board_indices[i]] = -1
# Remove from opponent board
opponent.playerboard[slot_idx] = -1
# Determine where to put it
var target_pos = Vector3i.ZERO
var target_item = item
if not empty_spots.is_empty():
# Place on empty spot
var pos_2d = empty_spots.pop_back()
target_pos = Vector3i(pos_2d.x, 1, pos_2d.y)
else:
# No empty spots? "Replace it with new one" at a random nearby non-empty spot?
# Or just find ANY nearby spot and overwrite
var neighbors = enhanced_gridmap.get_neighbors(opponent.current_position, 1)
if not neighbors.is_empty():
var rand_n = neighbors[rng.randi() % neighbors.size()]
target_pos = Vector3i(rand_n.position.x, 1, rand_n.position.y)
# If we are overwriting or essentially "spawning" a new one to replace it
target_item = rng.randi_range(7, 10) # As per request "replace it with new one" if floor not empty
if target_pos != Vector3i.ZERO:
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
main.rpc("sync_grid_item", target_pos.x, target_pos.y, target_pos.z, target_item)
# Sync opponent board change
main.rpc("sync_playerboard", opponent.name.to_int(), opponent.playerboard)
tiles_burned += 1
if tiles_burned > 0:
# Trigger screen shake
if opponent.is_multiplayer_authority():
opponent.rpc("trigger_screen_shake", "targeted")
else:
opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted")
if player.is_multiplayer_authority():
# Sync the change on the target
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
main.rpc("sync_playerboard", target.name.to_int(), target.playerboard)
print("[SpecialTiles] BURN_TILES: Knocked %d tiles from %s" % [tiles_burned, opponent.name])
player.rpc("display_message", "Knocked tiles from %s!" % opponent.display_name)
opponent.rpc("display_message", "%s knocked tiles out of your bag!" % player.display_name)
target.rpc("display_message", "Burned by %s!" % player.display_name, 3)
func _execute_spawn_tiles():
# NEW LOGIC: Spawn more in neighbor space (radius 2)
var radius = 2
var candidates = []
func _execute_spawn_tiles(target: Node3D):
# Spawn tiles around TARGET (usually Self for Coin)
spawn_powerups_around(target.current_position, false) # False = normal tiles? User says "Spawn 3x3 pattern tiles"
# Okay "SPAWN_TILES" usually means useful numbered tiles.
# But "spawn / replace your nearby tiles into power up" is for Headbutt.
# For Coin->Spawn_Tiles: "Spawn 3x3 pattern tiles around activating player ( self )".
# So random number tiles (7-10 are powerups, 1-6 are normal? No, 7-10 are patterns in this game).
# "Spawn 3x3 pattern tiles" -> Tiles with ID 7,8,9,10 are the goal tiles.
for x in range(-radius, radius + 1):
for y in range(-radius, radius + 1):
if x == 0 and y == 0: continue
var pos = player.current_position + Vector2i(x, y)
if enhanced_gridmap.is_position_valid(pos):
var cell = Vector3i(pos.x, 1, pos.y)
if enhanced_gridmap.get_cell_item(cell) == -1:
candidates.append(cell)
var spawn_count = rng.randi_range(3, 8) # Spawn a bunch
candidates.shuffle()
var actual_spawned = 0
for i in range(min(spawn_count, candidates.size())):
var cell = candidates[i]
var new_tile = rng.randi_range(7, 10)
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, new_tile)
actual_spawned += 1
print("[SpecialTiles] SPAWN_TILES: Spawned %d tiles around %s" % [actual_spawned, player.name])
player.rpc("display_message", "Spawned tiles nearby!")
target.rpc("display_message", "Tiles Spawned!", 2)
func _execute_freeze_player():
# Find random opponent
var opponent = _get_random_opponent()
if not opponent:
func _execute_freeze_player(target: Node3D):
if not target:
print("[SpecialTiles] No opponent found for FREEZE_PLAYER")
return
# Freeze the opponent
if opponent.has_method("apply_freeze"):
opponent.apply_freeze(FREEZE_DURATION)
if target.has_method("apply_stagger"): # Stagger = Freeze roughly
target.rpc("apply_stagger", FREEZE_DURATION)
else:
# Fallback: directly set frozen state
opponent.set("is_frozen", true)
_create_unfreeze_timer(opponent, FREEZE_DURATION)
# Trigger screen shake on the frozen opponent
if opponent.is_multiplayer_authority():
opponent.rpc("trigger_screen_shake", "targeted")
else:
opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted")
print("[SpecialTiles] FREEZE_PLAYER: Froze %s for %ds" % [opponent.name, FREEZE_DURATION])
player.rpc("display_message", "Froze %s!" % opponent.display_name)
opponent.rpc("display_message", "%s froze you!" % player.display_name)
# Visual effect: Ice Blue
# Use RPC to sync visual effect to everyone (call_local handles our screen)
if opponent.has_method("sync_modulate"):
opponent.rpc("sync_modulate", Color(0.5, 0.8, 1.0))
# Standard players sync via network transform but modulation might not sync automatically unless handled.
# Let's hope basic property sync or local effect handles it enough for now,
# but ideally we should RPC a visual update method on the player.
# Checking player.gd again, there isn't a sync_modulate.
# We can just set it locally and rely on the RPCs below for syncing the EFFECT STATUS,
# but we should probably RPC the color change to be sure everyone sees it.
# Actually, since we don't have a generic sync_proeprty, we will just set it locally on the authority
# and rely on the target itself to perhaps propogate it? No, that won't work traversing network.
# We need a way to tell clients "Painter this player blue".
# The simplest safe way without modifying Player.gd extensively is to rpc a method call if available,
# or just set it on the proxy if we are the server.
# But special_tiles is running on the player who TRIGGERED it.
# If I am client A, targeting client B. I am authority of ME. B is authority of B.
# I can't set properties on B directly and expect them to sync.
# I must RPC B to freeze himself.
# The _execute_freeze_player logic calls opponent.apply_freeze or sets is_frozen.
# If opponent has authority, they will run their own logic?
# Wait, special_tiles_manager runs on the client who picked up the tile?
# "if player.is_multiplayer_authority(): rpc(...)" implies we are the authority of the player who picked it up.
# We find an opponent (which is a proxy version on our machine).
# We call methods on that proxy.
# "opponent.rpc(...)" sends a message to the authority of that opponent.
# So we should validly call an RPC on opponent to change color.
# But Player.gd doesn't have "set_modulate_rpc".
# Use "set" works locally.
# We need to add visual sync support to Player.gd or just rely on what we have.
# Given constraints, I'll add the modulate locally and maybe the opponent-side logic should handle it?
# _create_unfreeze_timer runs on OUR machine mostly? No, "await player.get_tree()..."
# If we are A, targeting B.
# We call opponent.apply_freeze(). If B has that method, good.
# If B lacks it, we set is_frozen on B's proxy and run a timer on A's machine?
# That only freezes B on A's screen if logic relies on is_frozen?
# Actually, `opponent.rpc("display_message", ...)` works.
# Let's add a `sync_visual_effect` to Player.gd if needed, or just standard property setting if supported.
# For now, I will just set it and see if I can add a dedicated RPC in Player.gd in the next step if this is insufficient,
# OR better: I'll use `opponent.rpc("sync_modulate", ...)` and add that method to Player.gd in a separate tool call.
# For this tool call, I'll update the text and set local modulate.
func _create_unfreeze_timer(target_player: Node3D, duration: float):
if not is_instance_valid(player) or not is_instance_valid(target_player):
return
target.set("is_frozen", true)
_create_unfreeze_timer(target, FREEZE_DURATION)
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)
target_player.rpc("display_message", "Unfrozen!")
target.rpc("display_message", "Frozen by %s!" % player.display_name, 3)
func _execute_block_floor():
# NEW LOGIC: Block 3 to 9 tiles in a line (Horizontal/Vertical/Diagonal)
# Find valid start neighbor
var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0)
var valid_neighbors = neighbors.filter(func(n): return n.is_walkable)
func _execute_block_floor(target: Node3D):
# Make nearby tile non-walkable for 9 seconds
# Target the floor UNDER or NEAR the target?
# "block ( other player ) spawn BLOCK_FLOOR Make nearby tile non-walkable"
# Let's block the tile they are standing on + neighbors?
var center = target.current_position
var neighbors = enhanced_gridmap.get_neighbors(center, 1) # Include diagonals
neighbors.append({"position": center}) # Add center
if valid_neighbors.is_empty():
return
var start_neighbor = valid_neighbors[rng.randi() % valid_neighbors.size()]
var start_pos = start_neighbor.position
# Random direction: H, V, D1, D2
var directions = [
Vector2i(1, 0), Vector2i(-1, 0), # Horizontal
Vector2i(0, 1), Vector2i(0, -1), # Vertical
Vector2i(1, 1), Vector2i(-1, -1), # Diagonal
Vector2i(1, -1), Vector2i(-1, 1)
]
var dir = directions[rng.randi() % directions.size()]
var count = rng.randi_range(3, 9)
var valid_block_count = 0
for i in range(count):
var target_pos_2d = start_pos + (dir * i)
# Check if valid grid position
if not enhanced_gridmap.is_position_valid(target_pos_2d):
break # Stop if we hit edge of map
var block_pos = Vector3i(target_pos_2d.x, 0, target_pos_2d.y)
var original_item = enhanced_gridmap.get_cell_item(block_pos)
for n in neighbors:
var pos = n.position
var block_pos = Vector3i(pos.x, 0, pos.y)
# Make tile non-walkable
var blocked_item = 4
if enhanced_gridmap.non_walkable_items.size() > 0:
blocked_item = enhanced_gridmap.non_walkable_items[0]
# Block it
if player.is_multiplayer_authority():
var main = player.get_tree().get_root().get_node_or_null("Main")
if main:
main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, blocked_item)
# 4 = Blocked Tile ID usually
main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, 4)
var original_item = enhanced_gridmap.get_cell_item(block_pos)
blocked_tiles.append({
"position": block_pos,
"original_item": original_item,
"original_item": original_item if original_item != 4 else 0, # Restore to 0 (floor) if confused
"timer": BLOCK_DURATION
})
valid_block_count += 1
if valid_block_count > 0:
enhanced_gridmap.initialize_astar()
print("[SpecialTiles] BLOCK_FLOOR: Blocked line of %d tiles" % valid_block_count)
player.rpc("display_message", "Blocked a wall of tiles!")
target.rpc("display_message", "Floor Blocked!", 3)
func _execute_invisible_mode():
# Set invisible mode on player
# NEW LOGIC: Also enables auto-grab in _process
if player.has_method("apply_invisible_mode"):
player.apply_invisible_mode(INVISIBLE_DURATION)
else:
player.set("is_invisible", true)
player.set("original_movement_range", player.movement_range)
player.movement_range = player.movement_range + 2
invisible_timer = INVISIBLE_DURATION
func _execute_invisible_mode(target: Node3D):
target.is_invisible = true
# Auto-disable after duration handled in Player._process or here?
# SpecialTilesManager seems to handle effect timers.
invisible_timer = INVISIBLE_DURATION
target.rpc("display_message", "Invisible!", 2)
# =============================================================================
# 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 )"
# PowerUp Tiles are 7, 8, 9, 10 (Heart, Diamond, Star, Coin)
print("[SpecialTiles] INVISIBLE_MODE: Activated")
player.rpc("display_message", "Invisible Mode Active!")
var radius = 2
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):
# Random chance
if rng.randf() > 0.4: continue
var item_id = rng.randi_range(7, 10) # 7-10 are the special tiles
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:
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, item_id)
func _process(delta):
@@ -355,10 +307,8 @@ func _update_invisible_timer(delta: float):
if invisible_timer <= 0:
invisible_timer = 0
if is_instance_valid(player):
player.set("is_invisible", false)
if player.get("original_movement_range"):
player.movement_range = player.original_movement_range
player.rpc("display_message", "Invisible mode ended!")
player.is_invisible = false
player.rpc("display_message", "Invisibility Ended")
# =============================================================================
@@ -419,3 +369,12 @@ func check_shield_and_cancel_effect() -> bool:
player.rpc("display_message", "Shield blocked an attack!")
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)
target_player.rpc("display_message", "Unfrozen!")