feat: add player core, movement, and playerboard managers, special tile effects system, and lobby scene.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
[gd_scene load_steps=7 format=3 uid="uid://b7nxt2hc4kqp8"]
|
[gd_scene load_steps=7 format=3 uid="uid://b7nxt2hc4kqp8"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://b5q6yekyk0tld" path="res://scenes/lobby.gd" id="1_lobby"]
|
[ext_resource type="Script" uid="uid://b5q6yekyk0tld" path="res://scenes/lobby.gd" id="1_lp6xi"]
|
||||||
|
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
|
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_panel"]
|
||||||
content_margin_left = 24.0
|
content_margin_left = 24.0
|
||||||
@@ -89,7 +89,7 @@ anchor_right = 1.0
|
|||||||
anchor_bottom = 1.0
|
anchor_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("1_lobby")
|
script = ExtResource("1_lp6xi")
|
||||||
|
|
||||||
[node name="Background" type="ColorRect" parent="."]
|
[node name="Background" type="ColorRect" parent="."]
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
|
|||||||
@@ -6,6 +6,12 @@ var race_manager
|
|||||||
var input_manager
|
var input_manager
|
||||||
var playerboard_manager
|
var playerboard_manager
|
||||||
var action_manager
|
var action_manager
|
||||||
|
var special_tiles_manager
|
||||||
|
|
||||||
|
# Special effect states
|
||||||
|
var is_frozen: bool = false
|
||||||
|
var is_invisible: bool = false
|
||||||
|
var original_movement_range: int = 1
|
||||||
|
|
||||||
@export var is_bot: bool = false
|
@export var is_bot: bool = false
|
||||||
|
|
||||||
@@ -213,6 +219,11 @@ func _init_managers():
|
|||||||
add_child(action_manager)
|
add_child(action_manager)
|
||||||
action_manager.initialize(self, enhanced_gridmap)
|
action_manager.initialize(self, enhanced_gridmap)
|
||||||
|
|
||||||
|
special_tiles_manager = load("res://scripts/managers/special_tiles_manager.gd").new()
|
||||||
|
special_tiles_manager.name = "SpecialTilesManager"
|
||||||
|
add_child(special_tiles_manager)
|
||||||
|
special_tiles_manager.initialize(self, enhanced_gridmap)
|
||||||
|
|
||||||
# Add function to check if position is at finish line
|
# Add function to check if position is at finish line
|
||||||
func is_at_finish_line() -> bool:
|
func is_at_finish_line() -> bool:
|
||||||
return race_manager.is_at_finish_line()
|
return race_manager.is_at_finish_line()
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
if not player.is_multiplayer_authority() or is_moving:
|
if not player.is_multiplayer_authority() or is_moving:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
# Check if player is frozen
|
||||||
|
if player.get("is_frozen"):
|
||||||
|
return false
|
||||||
|
|
||||||
# Check if target is within 1-tile range
|
# Check if target is within 1-tile range
|
||||||
var distance: int
|
var distance: int
|
||||||
if use_diagonal_movement:
|
if use_diagonal_movement:
|
||||||
@@ -79,6 +83,10 @@ func move_to_clicked_position(grid_position: Vector2i) -> bool:
|
|||||||
if not player.is_multiplayer_authority() or is_moving or player.action_points <= 0:
|
if not player.is_multiplayer_authority() or is_moving or player.action_points <= 0:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
# Check if player is frozen
|
||||||
|
if player.get("is_frozen"):
|
||||||
|
return false
|
||||||
|
|
||||||
# Validate grid position is within bounds
|
# Validate grid position is within bounds
|
||||||
if not enhanced_gridmap.is_position_valid(grid_position):
|
if not enhanced_gridmap.is_position_valid(grid_position):
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ func grab_item(grid_position: Vector2i) -> bool:
|
|||||||
# === Optimistic Local Update (immediate visual feedback) ===
|
# === Optimistic Local Update (immediate visual feedback) ===
|
||||||
# Apply changes locally first, server will validate/sync
|
# Apply changes locally first, server will validate/sync
|
||||||
enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately
|
enhanced_gridmap.set_cell_item(cell, -1) # Remove item visually immediately
|
||||||
|
|
||||||
|
# Check if grabbed item is a holo tile (11-14) and trigger special effect
|
||||||
|
var is_holo = item >= 11 and item <= 14
|
||||||
|
if is_holo:
|
||||||
|
var special_tiles_manager = player.get_node_or_null("SpecialTilesManager")
|
||||||
|
if special_tiles_manager:
|
||||||
|
special_tiles_manager.trigger_random_effect()
|
||||||
|
# Convert holo tile to normal tile (11->7, 12->8, 13->9, 14->10)
|
||||||
|
item = item - 4
|
||||||
|
|
||||||
player.playerboard[target_slot] = item # Add to playerboard immediately
|
player.playerboard[target_slot] = item # Add to playerboard immediately
|
||||||
|
|
||||||
# Update UI immediately for responsiveness
|
# Update UI immediately for responsiveness
|
||||||
@@ -141,6 +151,12 @@ func bot_try_grab_item() -> bool:
|
|||||||
var empty_slot = player.playerboard.find(-1)
|
var empty_slot = player.playerboard.find(-1)
|
||||||
if empty_slot != -1:
|
if empty_slot != -1:
|
||||||
if player.is_multiplayer_authority():
|
if player.is_multiplayer_authority():
|
||||||
|
# Check if grabbed item is a holo tile (11-14)
|
||||||
|
if item >= 11 and item <= 14:
|
||||||
|
var special_tiles_manager = player.get_node_or_null("SpecialTilesManager")
|
||||||
|
if special_tiles_manager:
|
||||||
|
special_tiles_manager.trigger_random_effect()
|
||||||
|
item = item - 4 # Convert to normal tile
|
||||||
player.playerboard[empty_slot] = item
|
player.playerboard[empty_slot] = item
|
||||||
player.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1)
|
player.rpc("sync_grid_item", current_cell.x, current_cell.y, current_cell.z, -1)
|
||||||
player.rpc("sync_playerboard", player.playerboard)
|
player.rpc("sync_playerboard", player.playerboard)
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
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 {
|
||||||
|
BURN_TILES, # Remove 3x3 pattern tiles on random opponent
|
||||||
|
SPAWN_TILES, # Spawn 3x3 pattern tiles around activating player
|
||||||
|
FREEZE_PLAYER, # Freeze random opponent for 3 seconds
|
||||||
|
BLOCK_FLOOR, # Make nearby tile non-walkable for 9 seconds
|
||||||
|
INVISIBLE_MODE # Speed boost + auto-grab + shield for 6 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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_DURATION = 3.0
|
||||||
|
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}
|
||||||
|
|
||||||
|
func initialize(p_player: Node3D, p_gridmap: Node):
|
||||||
|
player = p_player
|
||||||
|
enhanced_gridmap = p_gridmap
|
||||||
|
rng = RandomNumberGenerator.new()
|
||||||
|
rng.randomize()
|
||||||
|
|
||||||
|
func _process(delta):
|
||||||
|
_update_blocked_tiles(delta)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Check if item is a holo tile
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
func is_holo_tile(item_id: int) -> bool:
|
||||||
|
return item_id in HOLO_TILES
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Trigger random special effect
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
func trigger_random_effect():
|
||||||
|
var effect = rng.randi() % SpecialEffect.size()
|
||||||
|
|
||||||
|
print("[SpecialTiles] Player %s triggered effect: %s" % [player.name, SpecialEffect.keys()[effect]])
|
||||||
|
|
||||||
|
match effect:
|
||||||
|
SpecialEffect.BURN_TILES:
|
||||||
|
_execute_burn_tiles()
|
||||||
|
SpecialEffect.SPAWN_TILES:
|
||||||
|
_execute_spawn_tiles()
|
||||||
|
SpecialEffect.FREEZE_PLAYER:
|
||||||
|
_execute_freeze_player()
|
||||||
|
SpecialEffect.BLOCK_FLOOR:
|
||||||
|
_execute_block_floor()
|
||||||
|
SpecialEffect.INVISIBLE_MODE:
|
||||||
|
_execute_invisible_mode()
|
||||||
|
|
||||||
|
# Sync effect to all clients
|
||||||
|
if player.is_multiplayer_authority():
|
||||||
|
rpc("sync_effect_triggered", effect)
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func sync_effect_triggered(effect: int):
|
||||||
|
print("[SpecialTiles] Synced effect %s for player %s" % [SpecialEffect.keys()[effect], player.name])
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Pattern Generation
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
func _get_random_pattern() -> Array[Vector2i]:
|
||||||
|
var pattern_keys = PATTERNS.keys()
|
||||||
|
var selected_pattern = pattern_keys[rng.randi() % pattern_keys.size()]
|
||||||
|
var base_pattern = PATTERNS[selected_pattern].duplicate()
|
||||||
|
|
||||||
|
# Randomly rotate pattern (0, 90, 180, 270 degrees)
|
||||||
|
var rotations = rng.randi() % 4
|
||||||
|
for i in range(rotations):
|
||||||
|
for j in range(base_pattern.size()):
|
||||||
|
var p = base_pattern[j]
|
||||||
|
base_pattern[j] = Vector2i(-p.y, p.x) # 90 degree rotation
|
||||||
|
|
||||||
|
# Ensure pattern has 3-8 cells
|
||||||
|
var result: Array[Vector2i] = []
|
||||||
|
for offset in base_pattern:
|
||||||
|
result.append(offset)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
func _get_valid_pattern_positions(center: Vector2i, pattern: Array[Vector2i]) -> Array[Vector2i]:
|
||||||
|
var valid_positions: Array[Vector2i] = []
|
||||||
|
|
||||||
|
for offset in pattern:
|
||||||
|
var pos = center + offset
|
||||||
|
if enhanced_gridmap.is_position_valid(pos):
|
||||||
|
valid_positions.append(pos)
|
||||||
|
|
||||||
|
return valid_positions
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Effect Implementations
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
func _execute_burn_tiles():
|
||||||
|
# Find random opponent
|
||||||
|
var opponent = _get_random_opponent()
|
||||||
|
if not opponent:
|
||||||
|
print("[SpecialTiles] No opponent found for BURN_TILES")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Get pattern around opponent
|
||||||
|
var pattern = _get_random_pattern()
|
||||||
|
var positions = _get_valid_pattern_positions(opponent.current_position, pattern)
|
||||||
|
|
||||||
|
# Remove tiles at these positions
|
||||||
|
for pos in positions:
|
||||||
|
var cell = Vector3i(pos.x, 1, pos.y)
|
||||||
|
if enhanced_gridmap.get_cell_item(cell) != -1:
|
||||||
|
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, -1)
|
||||||
|
|
||||||
|
print("[SpecialTiles] BURN_TILES: Removed %d tiles around %s" % [positions.size(), opponent.name])
|
||||||
|
player.rpc("display_message", "Burned tiles near opponent!")
|
||||||
|
|
||||||
|
func _execute_spawn_tiles():
|
||||||
|
# Get pattern around activating player
|
||||||
|
var pattern = _get_random_pattern()
|
||||||
|
var positions = _get_valid_pattern_positions(player.current_position, pattern)
|
||||||
|
|
||||||
|
# Spawn random tiles at empty positions
|
||||||
|
var spawned_count = 0
|
||||||
|
for pos in positions:
|
||||||
|
var cell = Vector3i(pos.x, 1, pos.y)
|
||||||
|
if enhanced_gridmap.get_cell_item(cell) == -1:
|
||||||
|
var new_tile = rng.randi_range(7, 10) # Random normal tile
|
||||||
|
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)
|
||||||
|
spawned_count += 1
|
||||||
|
|
||||||
|
print("[SpecialTiles] SPAWN_TILES: Spawned %d tiles around %s" % [spawned_count, player.name])
|
||||||
|
player.rpc("display_message", "Spawned new tiles!")
|
||||||
|
|
||||||
|
func _execute_freeze_player():
|
||||||
|
# Find random opponent
|
||||||
|
var opponent = _get_random_opponent()
|
||||||
|
if not opponent:
|
||||||
|
print("[SpecialTiles] No opponent found for FREEZE_PLAYER")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Freeze the opponent
|
||||||
|
if opponent.has_method("apply_freeze"):
|
||||||
|
opponent.apply_freeze(FREEZE_DURATION)
|
||||||
|
else:
|
||||||
|
# Fallback: directly set frozen state
|
||||||
|
opponent.set("is_frozen", true)
|
||||||
|
_create_unfreeze_timer(opponent, FREEZE_DURATION)
|
||||||
|
|
||||||
|
print("[SpecialTiles] FREEZE_PLAYER: Froze %s for %ds" % [opponent.name, FREEZE_DURATION])
|
||||||
|
player.rpc("display_message", "Froze an opponent!")
|
||||||
|
opponent.rpc("display_message", "You are frozen!")
|
||||||
|
|
||||||
|
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)
|
||||||
|
target_player.rpc("display_message", "Unfrozen!")
|
||||||
|
|
||||||
|
func _execute_block_floor():
|
||||||
|
# Find valid tile near player to block
|
||||||
|
var neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0)
|
||||||
|
var valid_neighbors = neighbors.filter(func(n): return n.is_walkable)
|
||||||
|
|
||||||
|
if valid_neighbors.is_empty():
|
||||||
|
print("[SpecialTiles] No valid tile to block")
|
||||||
|
return
|
||||||
|
|
||||||
|
var target_neighbor = valid_neighbors[rng.randi() % valid_neighbors.size()]
|
||||||
|
var block_pos = Vector3i(target_neighbor.position.x, 0, target_neighbor.position.y)
|
||||||
|
var original_item = enhanced_gridmap.get_cell_item(block_pos)
|
||||||
|
|
||||||
|
# Make tile non-walkable (use a blocked item index)
|
||||||
|
var blocked_item = 4 # Using non_walkable_items[0] typically
|
||||||
|
if enhanced_gridmap.non_walkable_items.size() > 0:
|
||||||
|
blocked_item = enhanced_gridmap.non_walkable_items[0]
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
# Track blocked tile for restoration
|
||||||
|
blocked_tiles.append({
|
||||||
|
"position": block_pos,
|
||||||
|
"original_item": original_item,
|
||||||
|
"timer": BLOCK_DURATION
|
||||||
|
})
|
||||||
|
|
||||||
|
# Re-initialize pathfinding
|
||||||
|
enhanced_gridmap.initialize_astar()
|
||||||
|
|
||||||
|
print("[SpecialTiles] BLOCK_FLOOR: Blocked tile at %s for %ds" % [target_neighbor.position, BLOCK_DURATION])
|
||||||
|
player.rpc("display_message", "Blocked a floor tile!")
|
||||||
|
|
||||||
|
func _execute_invisible_mode():
|
||||||
|
# Set invisible mode on player
|
||||||
|
if player.has_method("apply_invisible_mode"):
|
||||||
|
player.apply_invisible_mode(INVISIBLE_DURATION)
|
||||||
|
else:
|
||||||
|
# Fallback: directly set invisible state
|
||||||
|
player.set("is_invisible", true)
|
||||||
|
player.set("original_movement_range", player.movement_range)
|
||||||
|
player.movement_range = player.movement_range + 2 # Speed boost
|
||||||
|
_create_invisibility_timer(INVISIBLE_DURATION)
|
||||||
|
|
||||||
|
print("[SpecialTiles] INVISIBLE_MODE: %s is now invisible for %ds" % [player.name, INVISIBLE_DURATION])
|
||||||
|
player.rpc("display_message", "Invisible mode activated!")
|
||||||
|
|
||||||
|
func _create_invisibility_timer(duration: float):
|
||||||
|
await player.get_tree().create_timer(duration).timeout
|
||||||
|
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!")
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Helper Functions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
func _get_random_opponent() -> Node3D:
|
||||||
|
var all_players = player.get_tree().get_nodes_in_group("Players")
|
||||||
|
var opponents = all_players.filter(func(p): return p != player)
|
||||||
|
|
||||||
|
if opponents.is_empty():
|
||||||
|
return null
|
||||||
|
|
||||||
|
return opponents[rng.randi() % opponents.size()]
|
||||||
|
|
||||||
|
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)
|
||||||
|
if player.get("original_movement_range"):
|
||||||
|
player.movement_range = player.original_movement_range
|
||||||
|
player.rpc("display_message", "Shield blocked an attack!")
|
||||||
|
return true
|
||||||
|
return false
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dw1euu2uxkggg
|
||||||
Reference in New Issue
Block a user