edit special power up
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
[ ADT's Report ]
|
||||||
|
|
||||||
|
Updated the `tekton-enet` ( Armageddon Multiplayer ) on branch `launcher`
|
||||||
|
|
||||||
|
**Special Tile Enhancements**
|
||||||
|
|
||||||
|
✅ **Block Floor Effect** - Upgraded to create a "wall" or line of blocked tiles instead of a single tile.
|
||||||
|
* **Behavior**: Generates a line of 3 to 9 blocked tiles.
|
||||||
|
* **Direction**: Randomly chooses Horizontal, Vertical, or Diagonal.
|
||||||
|
* **Origin**: Starts propagation from a random adjacent cell.
|
||||||
|
|
||||||
|
✅ **Spawn Tiles Effect** - Enhanced to spawn tiles in a wider area.
|
||||||
|
* **Area**: Increased radius to 2 (covering a 5x5 zone).
|
||||||
|
* **Density**: Randomly fills 3 to 8 empty spots within this radius.
|
||||||
|
* **Logic**: Prioritizes immediate neighbor spaces for better accessibility.
|
||||||
|
|
||||||
|
✅ **Burn Tiles Effect** - Reworked into a "Knockback / Disarm" mechanic.
|
||||||
|
* **Target**: Selects a random opponent.
|
||||||
|
* **Action**: Strips 3-6 random tiles from their playerboard.
|
||||||
|
* **Result**: Scatters them back onto the grid near the opponent (radius 2). If no space exists, it forces a replacement of existing nearby tiles.
|
||||||
|
|
||||||
|
✅ **Invisible Mode** - Tweaked for balance and stability.
|
||||||
|
* **Balance**: Removed "Auto-Grab" mechanic; it was deemed too powerful.
|
||||||
|
* **Buff**: Retains invisibility and the +2 tile movement speed boost.
|
||||||
|
* **Stability**: Refactored timer logic to prevent runtime errors during hot-reloading.
|
||||||
|
|
||||||
|
✅ **Visual Feedback** - Improved clarity for combat and special effects.
|
||||||
|
* **Notifications**: Messages now explicitly state WHO used an ability and WHO was targeted (including usernames).
|
||||||
|
* **Freeze Visual**: Frozen players are now tinted **Ice Blue** for the duration of the effect.
|
||||||
|
* **Implementation**: Added recursion logic to apply material overlays to 3D character meshes since `modulate` is not supported on Node3D.
|
||||||
|
|
||||||
|
**Refactoring & Cleanup**
|
||||||
|
|
||||||
|
✅ **Obstacle Manager Removal** - Completely removed the unused `ObstacleManager` system to streamline the codebase.
|
||||||
|
* Deleted `obstacle_manager.gd`.
|
||||||
|
* Stripped all references and logic from `Main.gd`, `EnhancedGridMap.gd`, `UIManager.gd`.
|
||||||
|
* Cleaned up Player-related managers (`Movement`, `Input`, `Action`).
|
||||||
|
|
||||||
|
**Bug Fixes**
|
||||||
|
|
||||||
|
✅ **Special Tiles Parser Fix** - Resolved a critical parser error in `special_tiles_manager.gd` caused by a duplicate `_process` function declaration, ensuring correct compilation.
|
||||||
@@ -5,10 +5,10 @@ extends GridMap
|
|||||||
signal mesh_library_changed
|
signal mesh_library_changed
|
||||||
signal grid_updated
|
signal grid_updated
|
||||||
|
|
||||||
@export var columns: int = 10 : set = set_columns
|
@export var columns: int = 10: set = set_columns
|
||||||
@export var rows: int = 10 : set = set_rows
|
@export var rows: int = 10: set = set_rows
|
||||||
@export var floors: int = 3 : set = set_floors
|
@export var floors: int = 3: set = set_floors
|
||||||
@export var auto_generate: bool = false : set = set_auto_generate
|
@export var auto_generate: bool = false: set = set_auto_generate
|
||||||
|
|
||||||
@export var normal_items: Array[int] = [0]
|
@export var normal_items: Array[int] = [0]
|
||||||
@export var non_walkable_items: Array[int] = [4]
|
@export var non_walkable_items: Array[int] = [4]
|
||||||
@@ -23,23 +23,12 @@ var grid_data: Array = [] # 3D array [floor][row][column]
|
|||||||
var astar_by_floor = {} # Dictionary of AStar2D instances per floor
|
var astar_by_floor = {} # Dictionary of AStar2D instances per floor
|
||||||
var path = []
|
var path = []
|
||||||
|
|
||||||
# Update the obstacle items array to use your specified item indices
|
|
||||||
@export var obstacle_items: Array[int] = [12, 13, 14, 15] # Obstacle items in mesh library
|
|
||||||
@export var obstacle_directions: Dictionary = {} # Store direction for each placed obstacle: {Vector3i position: Direction}
|
|
||||||
|
|
||||||
# Dictionary to store obstacle information: {cell_pos: orientation}
|
|
||||||
# orientation: 0=North, 1=East, 2=South, 3=West
|
|
||||||
var obstacles = {}
|
|
||||||
|
|
||||||
# Direction and movement systems
|
# Direction and movement systems
|
||||||
enum Direction {
|
enum Direction {
|
||||||
NORTHWEST, NORTH, NORTHEAST,
|
NORTHWEST, NORTH, NORTHEAST,
|
||||||
WEST, CENTER, EAST,
|
WEST, CENTER, EAST,
|
||||||
SOUTHWEST, SOUTH, SOUTHEAST,
|
SOUTHWEST, SOUTH, SOUTHEAST
|
||||||
BLOCKED_NORTH = 10,
|
|
||||||
BLOCKED_EAST = 11,
|
|
||||||
BLOCKED_SOUTH = 12,
|
|
||||||
BLOCKED_WEST = 13
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var diagonal_movement: bool = false
|
var diagonal_movement: bool = false
|
||||||
@@ -353,20 +342,15 @@ func get_neighbors(current_pos: Vector2i, floor_index: int) -> Array[NeighborInf
|
|||||||
var is_walkable = is_cell_walkable(neighbor_pos, floor_index)
|
var is_walkable = is_cell_walkable(neighbor_pos, floor_index)
|
||||||
|
|
||||||
# Check if movement to this neighbor is blocked by obstacles
|
# Check if movement to this neighbor is blocked by obstacles
|
||||||
if not is_diagonal_direction(dir) and is_movement_blocked(current_pos, neighbor_pos, floor_index):
|
# if not is_diagonal_direction(dir) and is_movement_blocked(current_pos, neighbor_pos, floor_index):
|
||||||
is_walkable = false
|
# is_walkable = false
|
||||||
|
|
||||||
if is_diagonal_direction(dir):
|
if is_diagonal_direction(dir):
|
||||||
# For diagonal movement, check if both orthogonal paths are blocked
|
# For diagonal movement, check if both orthogonal paths are blocked
|
||||||
var mid1 = Vector2i(neighbor_pos.x, current_pos.y)
|
var mid1 = Vector2i(neighbor_pos.x, current_pos.y)
|
||||||
var mid2 = Vector2i(current_pos.x, neighbor_pos.y)
|
var mid2 = Vector2i(current_pos.x, neighbor_pos.y)
|
||||||
|
|
||||||
var path1_blocked = is_movement_blocked(current_pos, mid1, floor_index)
|
|
||||||
var path2_blocked = is_movement_blocked(current_pos, mid2, floor_index)
|
|
||||||
|
|
||||||
if path1_blocked and path2_blocked:
|
|
||||||
is_walkable = false
|
|
||||||
|
|
||||||
if is_walkable:
|
if is_walkable:
|
||||||
neighbors.append(NeighborInfo.new(neighbor_pos, dir, is_walkable))
|
neighbors.append(NeighborInfo.new(neighbor_pos, dir, is_walkable))
|
||||||
|
|
||||||
@@ -374,7 +358,7 @@ func get_neighbors(current_pos: Vector2i, floor_index: int) -> Array[NeighborInf
|
|||||||
|
|
||||||
# Helper functions for neighbor checking
|
# Helper functions for neighbor checking
|
||||||
func is_diagonal_direction(direction: Direction) -> bool:
|
func is_diagonal_direction(direction: Direction) -> bool:
|
||||||
return direction in [Direction.NORTHWEST, Direction.NORTHEAST,
|
return direction in [Direction.NORTHWEST, Direction.NORTHEAST,
|
||||||
Direction.SOUTHWEST, Direction.SOUTHEAST]
|
Direction.SOUTHWEST, Direction.SOUTHEAST]
|
||||||
|
|
||||||
func is_position_valid(pos: Vector2i) -> bool:
|
func is_position_valid(pos: Vector2i) -> bool:
|
||||||
@@ -414,7 +398,7 @@ func initialize_astar():
|
|||||||
var weight = 1.0 if not is_diagonal_direction(neighbor.direction) else 1.4142
|
var weight = 1.0 if not is_diagonal_direction(neighbor.direction) else 1.4142
|
||||||
|
|
||||||
# Check if movement is allowed by obstacles
|
# Check if movement is allowed by obstacles
|
||||||
if not is_blocked_by_obstacle(current_pos, neighbor.position, 3):
|
if true: # Obstacle check removed
|
||||||
astar.connect_points(current_point_id, neighbor_id, true)
|
astar.connect_points(current_point_id, neighbor_id, true)
|
||||||
astar.set_point_weight_scale(neighbor_id, weight)
|
astar.set_point_weight_scale(neighbor_id, weight)
|
||||||
|
|
||||||
@@ -488,124 +472,10 @@ func update_grid_data():
|
|||||||
grid_data.append(floor_data)
|
grid_data.append(floor_data)
|
||||||
emit_signal("grid_updated")
|
emit_signal("grid_updated")
|
||||||
|
|
||||||
# Check the obstacle on a cell
|
# Orientation helper
|
||||||
func has_obstacle_at(pos: Vector3i) -> bool:
|
|
||||||
var item = get_cell_item(pos)
|
|
||||||
return item in obstacle_items
|
|
||||||
|
|
||||||
# Get orientation ( rotation )
|
|
||||||
func get_cell_orientation(pos: Vector3i) -> int:
|
func get_cell_orientation(pos: Vector3i) -> int:
|
||||||
return get_cell_item_orientation(pos)
|
return get_cell_item_orientation(pos)
|
||||||
|
|
||||||
# Get obstacle direction
|
|
||||||
# Get the direction of an obstacle at a specific position
|
|
||||||
func get_obstacle_direction(pos: Vector3i) -> Direction:
|
|
||||||
if obstacle_directions.has(pos):
|
|
||||||
return obstacle_directions[pos]
|
|
||||||
return Direction.CENTER
|
|
||||||
|
|
||||||
func get_obstacle_orientation(pos: Vector3i) -> int:
|
|
||||||
return get_cell_item_orientation(pos)
|
|
||||||
|
|
||||||
func is_movement_blocked(from_pos: Vector2i, to_pos: Vector2i, floor_index: int = 3) -> bool:
|
|
||||||
# Must be adjacent cells for direct blocking check
|
|
||||||
if abs(from_pos.x - to_pos.x) + abs(from_pos.y - to_pos.y) != 1:
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Get 3D positions for the cells
|
|
||||||
var from_pos3d = Vector3i(from_pos.x, floor_index, from_pos.y)
|
|
||||||
var to_pos3d = Vector3i(to_pos.x, floor_index, to_pos.y)
|
|
||||||
|
|
||||||
# Check if the starting cell has an obstacle
|
|
||||||
if has_obstacle_at(from_pos3d):
|
|
||||||
var orientation = get_obstacle_orientation(from_pos3d)
|
|
||||||
|
|
||||||
# Check if the obstacle is blocking the requested movement direction
|
|
||||||
if from_pos.y > to_pos.y and orientation == 0: # Moving NORTH, obstacle faces NORTH
|
|
||||||
return true
|
|
||||||
elif from_pos.x < to_pos.x and orientation == 1: # Moving EAST, obstacle faces EAST
|
|
||||||
return true
|
|
||||||
elif from_pos.y < to_pos.y and orientation == 2: # Moving SOUTH, obstacle faces SOUTH
|
|
||||||
return true
|
|
||||||
elif from_pos.x > to_pos.x and orientation == 3: # Moving WEST, obstacle faces WEST
|
|
||||||
return true
|
|
||||||
|
|
||||||
# Check if the destination cell has an obstacle blocking entry
|
|
||||||
if has_obstacle_at(to_pos3d):
|
|
||||||
var orientation = get_obstacle_orientation(to_pos3d)
|
|
||||||
|
|
||||||
# Check if the obstacle is blocking entry from the requested direction
|
|
||||||
if to_pos.y < from_pos.y and orientation == 2: # Coming from SOUTH, obstacle faces SOUTH
|
|
||||||
return true
|
|
||||||
elif to_pos.x > from_pos.x and orientation == 3: # Coming from WEST, obstacle faces WEST
|
|
||||||
return true
|
|
||||||
elif to_pos.y > from_pos.y and orientation == 0: # Coming from NORTH, obstacle faces NORTH
|
|
||||||
return true
|
|
||||||
elif to_pos.x < from_pos.x and orientation == 1: # Coming from EAST, obstacle faces EAST
|
|
||||||
return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Function to check if a cell is blocked by any obstacles in its vicinity
|
|
||||||
func is_cell_blocked_by_obstacles(pos: Vector2i, floor_index: int = 3) -> bool:
|
|
||||||
var pos3d = Vector3i(pos.x, floor_index, pos.y)
|
|
||||||
|
|
||||||
# Check if this cell itself has an obstacle
|
|
||||||
if has_obstacle_at(pos3d):
|
|
||||||
return true
|
|
||||||
|
|
||||||
# Check all adjacent cells for obstacles that might block this cell
|
|
||||||
var adjacent_positions = [
|
|
||||||
Vector2i(pos.x, pos.y - 1), # North
|
|
||||||
Vector2i(pos.x + 1, pos.y), # East
|
|
||||||
Vector2i(pos.x, pos.y + 1), # South
|
|
||||||
Vector2i(pos.x - 1, pos.y), # West
|
|
||||||
]
|
|
||||||
|
|
||||||
for adj_pos in adjacent_positions:
|
|
||||||
var adj_pos3d = Vector3i(adj_pos.x, floor_index, adj_pos.y)
|
|
||||||
|
|
||||||
# Check if position is valid
|
|
||||||
if is_position_valid(adj_pos) and has_obstacle_at(adj_pos3d):
|
|
||||||
var orientation = get_obstacle_orientation(adj_pos3d)
|
|
||||||
|
|
||||||
# Check if the obstacle is blocking this cell
|
|
||||||
if adj_pos.y < pos.y and orientation == 0: # Obstacle to NORTH facing NORTH
|
|
||||||
return true
|
|
||||||
elif adj_pos.x > pos.x and orientation == 1: # Obstacle to EAST facing EAST
|
|
||||||
return true
|
|
||||||
elif adj_pos.y > pos.y and orientation == 2: # Obstacle to SOUTH facing SOUTH
|
|
||||||
return true
|
|
||||||
elif adj_pos.x < pos.x and orientation == 3: # Obstacle to WEST facing WEST
|
|
||||||
return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Function to get all cells blocked by an obstacle at a specific position
|
|
||||||
func get_cells_blocked_by_obstacle(obstacle_pos: Vector2i, orientation: int, floor_index: int = 3) -> Array:
|
|
||||||
var blocked_cells = []
|
|
||||||
|
|
||||||
# Determine which cells are blocked based on orientation
|
|
||||||
match orientation:
|
|
||||||
0: # NORTH - blocks the row above
|
|
||||||
for x in range(max(0, obstacle_pos.x - 1), min(columns, obstacle_pos.x + 2)):
|
|
||||||
blocked_cells.append(Vector2i(x, obstacle_pos.y - 1))
|
|
||||||
|
|
||||||
1: # EAST - blocks the column to the right
|
|
||||||
for y in range(max(0, obstacle_pos.y - 1), min(rows, obstacle_pos.y + 2)):
|
|
||||||
blocked_cells.append(Vector2i(obstacle_pos.x + 1, y))
|
|
||||||
|
|
||||||
2: # SOUTH - blocks the row below
|
|
||||||
for x in range(max(0, obstacle_pos.x - 1), min(columns, obstacle_pos.x + 2)):
|
|
||||||
blocked_cells.append(Vector2i(x, obstacle_pos.y + 1))
|
|
||||||
|
|
||||||
3: # WEST - blocks the column to the left
|
|
||||||
for y in range(max(0, obstacle_pos.y - 1), min(rows, obstacle_pos.y + 2)):
|
|
||||||
blocked_cells.append(Vector2i(obstacle_pos.x - 1, y))
|
|
||||||
|
|
||||||
# Filter out invalid positions
|
|
||||||
return blocked_cells.filter(func(pos): return is_position_valid(pos))
|
|
||||||
|
|
||||||
# Cell rotation handling
|
# Cell rotation handling
|
||||||
func get_cell_rotation(position: Vector3i) -> int:
|
func get_cell_rotation(position: Vector3i) -> int:
|
||||||
return get_cell_item_orientation(position)
|
return get_cell_item_orientation(position)
|
||||||
@@ -649,60 +519,3 @@ func _emit_grid_updated():
|
|||||||
func set_diagonal_movement(enable: bool):
|
func set_diagonal_movement(enable: bool):
|
||||||
diagonal_movement = enable
|
diagonal_movement = enable
|
||||||
initialize_astar()
|
initialize_astar()
|
||||||
|
|
||||||
func is_blocked_by_obstacle(from_pos: Vector2i, to_pos: Vector2i, floor_index: int = 3) -> bool:
|
|
||||||
# For direct orthogonal movement (up, down, left, right)
|
|
||||||
if (from_pos.x == to_pos.x and abs(from_pos.y - to_pos.y) == 1) or (from_pos.y == to_pos.y and abs(from_pos.x - to_pos.x) == 1):
|
|
||||||
return is_movement_blocked(from_pos, to_pos, floor_index)
|
|
||||||
|
|
||||||
# For diagonal or longer distances, build a path and check each step
|
|
||||||
var path = []
|
|
||||||
|
|
||||||
# Simple path planning for orthogonal movement
|
|
||||||
if from_pos.x == to_pos.x or from_pos.y == to_pos.y:
|
|
||||||
var dx = sign(to_pos.x - from_pos.x)
|
|
||||||
var dy = sign(to_pos.y - from_pos.y)
|
|
||||||
var current = from_pos
|
|
||||||
|
|
||||||
while current != to_pos:
|
|
||||||
var next = Vector2i(current.x + dx, current.y + dy)
|
|
||||||
path.append([current, next])
|
|
||||||
current = next
|
|
||||||
else:
|
|
||||||
# For diagonal movement, check both possible paths
|
|
||||||
# Path 1: Move horizontally first, then vertically
|
|
||||||
var mid1 = Vector2i(to_pos.x, from_pos.y)
|
|
||||||
var path1_blocked = is_blocked_by_obstacle(from_pos, mid1, floor_index) or is_blocked_by_obstacle(mid1, to_pos, floor_index)
|
|
||||||
|
|
||||||
# Path 2: Move vertically first, then horizontally
|
|
||||||
var mid2 = Vector2i(from_pos.x, to_pos.y)
|
|
||||||
var path2_blocked = is_blocked_by_obstacle(from_pos, mid2, floor_index) or is_blocked_by_obstacle(mid2, to_pos, floor_index)
|
|
||||||
|
|
||||||
# Movement is blocked if both paths are blocked
|
|
||||||
return path1_blocked and path2_blocked
|
|
||||||
|
|
||||||
# Check each step in the path
|
|
||||||
for step in path:
|
|
||||||
if is_movement_blocked(step[0], step[1], floor_index):
|
|
||||||
return true
|
|
||||||
|
|
||||||
return false
|
|
||||||
|
|
||||||
# Place an obstacle at the specified position with a specific orientation
|
|
||||||
func place_obstacle(pos: Vector3i, obstacle_item: int, orientation: int) -> bool:
|
|
||||||
# Always place on floor 3
|
|
||||||
pos.y = 3
|
|
||||||
|
|
||||||
if get_cell_item(pos) != -1:
|
|
||||||
return false # Cell is already occupied
|
|
||||||
|
|
||||||
# Set the obstacle item with the specified orientation
|
|
||||||
set_cell_item(pos, obstacle_item, orientation)
|
|
||||||
|
|
||||||
# Store the obstacle information
|
|
||||||
obstacles[pos] = orientation
|
|
||||||
|
|
||||||
# Re-initialize A* pathfinding to account for the new obstacle
|
|
||||||
initialize_astar()
|
|
||||||
|
|
||||||
return true
|
|
||||||
|
|||||||
+1
-50
@@ -6,7 +6,6 @@ extends Node3D
|
|||||||
|
|
||||||
# Manager references
|
# Manager references
|
||||||
var ui_manager
|
var ui_manager
|
||||||
var obstacle_manager
|
|
||||||
var goals_cycle_manager
|
var goals_cycle_manager
|
||||||
var screen_shake_manager
|
var screen_shake_manager
|
||||||
var touch_controls
|
var touch_controls
|
||||||
@@ -31,7 +30,6 @@ func _ready():
|
|||||||
ui_manager.setup_timer_labels(self)
|
ui_manager.setup_timer_labels(self)
|
||||||
ui_manager.setup_leaderboard_ui(self)
|
ui_manager.setup_leaderboard_ui(self)
|
||||||
ui_manager.setup_powerup_bar_ui(self)
|
ui_manager.setup_powerup_bar_ui(self)
|
||||||
_setup_obstacle_ui()
|
|
||||||
# GlobalMatchTimer is now static in main.tscn - no setup needed
|
# GlobalMatchTimer is now static in main.tscn - no setup needed
|
||||||
# NetworkPanel is visible during gameplay
|
# NetworkPanel is visible during gameplay
|
||||||
|
|
||||||
@@ -48,11 +46,6 @@ func _init_managers():
|
|||||||
add_child(ui_manager)
|
add_child(ui_manager)
|
||||||
ui_manager.initialize(self)
|
ui_manager.initialize(self)
|
||||||
|
|
||||||
obstacle_manager = load("res://scripts/managers/obstacle_manager.gd").new()
|
|
||||||
obstacle_manager.name = "ObstacleManager"
|
|
||||||
add_child(obstacle_manager)
|
|
||||||
obstacle_manager.initialize($EnhancedGridMap)
|
|
||||||
|
|
||||||
# Goals cycle manager for 60-second timer and scoring
|
# Goals cycle manager for 60-second timer and scoring
|
||||||
goals_cycle_manager = load("res://scripts/managers/goals_cycle_manager.gd").new()
|
goals_cycle_manager = load("res://scripts/managers/goals_cycle_manager.gd").new()
|
||||||
goals_cycle_manager.name = "GoalsCycleManager"
|
goals_cycle_manager.name = "GoalsCycleManager"
|
||||||
@@ -184,25 +177,6 @@ func add_message_to_bar(player_name: String, message: String, type: int = Messag
|
|||||||
func broadcast_message(player_name: String, message: String):
|
func broadcast_message(player_name: String, message: String):
|
||||||
add_message_to_bar(player_name, message)
|
add_message_to_bar(player_name, message)
|
||||||
|
|
||||||
func _setup_obstacle_ui():
|
|
||||||
var obstacle_button = Button.new()
|
|
||||||
obstacle_button.text = "Place Obstacle"
|
|
||||||
obstacle_button.pressed.connect(func(): _set_action_state(ui_manager.ActionState.PLACING_OBSTACLE))
|
|
||||||
$ActionMenu/ActionButtonContainer.add_child(obstacle_button)
|
|
||||||
|
|
||||||
var orientation_button = Button.new()
|
|
||||||
orientation_button.text = "Direction: North"
|
|
||||||
orientation_button.pressed.connect(func():
|
|
||||||
orientation_button.text = obstacle_manager.cycle_obstacle_orientation()
|
|
||||||
)
|
|
||||||
$ActionMenu/ActionButtonContainer.add_child(orientation_button)
|
|
||||||
|
|
||||||
var type_button = Button.new()
|
|
||||||
type_button.text = "Type: 1"
|
|
||||||
type_button.pressed.connect(func():
|
|
||||||
type_button.text = obstacle_manager.cycle_obstacle_type()
|
|
||||||
)
|
|
||||||
$ActionMenu/ActionButtonContainer.add_child(type_button)
|
|
||||||
|
|
||||||
func _setup_global_match_timer_ui():
|
func _setup_global_match_timer_ui():
|
||||||
"""Create the global match timer display at the top of the screen."""
|
"""Create the global match timer display at the top of the screen."""
|
||||||
@@ -608,8 +582,7 @@ func _set_action_state(new_state):
|
|||||||
ui_manager.ActionState.ARRANGING:
|
ui_manager.ActionState.ARRANGING:
|
||||||
_show_arrangement_ui()
|
_show_arrangement_ui()
|
||||||
local_player.highlight_occupied_playerboard_slots()
|
local_player.highlight_occupied_playerboard_slots()
|
||||||
ui_manager.ActionState.PLACING_OBSTACLE:
|
|
||||||
local_player.highlight_valid_obstacle_cells()
|
|
||||||
|
|
||||||
func _show_arrangement_ui():
|
func _show_arrangement_ui():
|
||||||
if ui_manager.playerboard_ui:
|
if ui_manager.playerboard_ui:
|
||||||
@@ -626,28 +599,6 @@ func _on_playerboard_slot_clicked(event, slot_index):
|
|||||||
ui_manager.ActionState.ARRANGING:
|
ui_manager.ActionState.ARRANGING:
|
||||||
local_player.arrange_playerboard_item(slot_index)
|
local_player.arrange_playerboard_item(slot_index)
|
||||||
|
|
||||||
# =============================================================================
|
|
||||||
# Obstacle Management
|
|
||||||
# =============================================================================
|
|
||||||
|
|
||||||
func place_obstacle(grid_position: Vector2i) -> bool:
|
|
||||||
var local_player = GameStateManager.local_player_character
|
|
||||||
var success = obstacle_manager.place_obstacle(grid_position, local_player)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
local_player.clear_highlights()
|
|
||||||
_set_action_state(ui_manager.ActionState.NONE)
|
|
||||||
|
|
||||||
if is_multiplayer_authority():
|
|
||||||
rpc("sync_place_obstacle", grid_position.x, grid_position.y, 3,
|
|
||||||
obstacle_manager.current_obstacle_item, obstacle_manager.current_obstacle_orientation)
|
|
||||||
|
|
||||||
return success
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_local")
|
|
||||||
func sync_place_obstacle(x: int, y: int, floor_index: int, item_index: int, orientation: int):
|
|
||||||
$EnhancedGridMap.place_obstacle(Vector3i(x, floor_index, y), item_index, orientation)
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Goal & Playerboard Sync
|
# Goal & Playerboard Sync
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ script = ExtResource("2_hbe1v")
|
|||||||
columns = 14
|
columns = 14
|
||||||
rows = 14
|
rows = 14
|
||||||
floors = 2
|
floors = 2
|
||||||
obstacle_items = Array[int]([12])
|
|
||||||
metadata/_editor_floor_ = Vector3(0, 1, 0)
|
metadata/_editor_floor_ = Vector3(0, 1, 0)
|
||||||
|
|
||||||
[node name="Camera3D" type="Camera3D" parent="."]
|
[node name="Camera3D" type="Camera3D" parent="."]
|
||||||
|
|||||||
+49
-2
@@ -549,6 +549,55 @@ func sync_display_name(new_name: String) -> void:
|
|||||||
"""Sync display name across network."""
|
"""Sync display name across network."""
|
||||||
display_name = new_name
|
display_name = new_name
|
||||||
$Name.text = display_name
|
$Name.text = display_name
|
||||||
|
|
||||||
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
|
func sync_modulate(color: Color) -> void:
|
||||||
|
"""Sync visual modulation (e.g. freeze effect) across network."""
|
||||||
|
# Node3D doesn't have modulate. We must tint the active character meshes.
|
||||||
|
var active_char_node = null
|
||||||
|
|
||||||
|
match selected_character:
|
||||||
|
"Bob": active_char_node = character_bob
|
||||||
|
"Masbro": active_char_node = character_masbro
|
||||||
|
"Gatot": active_char_node = character_gatot
|
||||||
|
"Oldpop": active_char_node = character_oldpop
|
||||||
|
|
||||||
|
if active_char_node:
|
||||||
|
_apply_tint_recursive(active_char_node, color)
|
||||||
|
|
||||||
|
func _apply_tint_recursive(node: Node, color: Color):
|
||||||
|
if node is MeshInstance3D:
|
||||||
|
# Simple way to tint: use material_overlay with an unshaded material if possible,
|
||||||
|
# or transparency. But changing albedo on shared materials affects all instances!
|
||||||
|
# Using transparency/modulate equivalent in 3D is tricky without unique materials.
|
||||||
|
# A common trick is to use `instance_shader_parameter` if supported, or modifying visibility range? No.
|
||||||
|
# Best safe visual cue for "Help me create an ice effect" without shaders:
|
||||||
|
# Set `transparency` (alpha) if we want ghost, or `material_overlay`.
|
||||||
|
# For this quick fix, let's create a standard material overlay on the fly if needed
|
||||||
|
# or just rely on a debug geometry.
|
||||||
|
# Actually, geometry_instance_3d has `material_overlay`.
|
||||||
|
# Better approach for "Freeze": Just add a visible "Ice Block" mesh to the player
|
||||||
|
# instead of trying to tint the gltf model which might have complex materials.
|
||||||
|
# But user asked for "modulate". The closest 3D equivalent is material_overlay with a color.
|
||||||
|
var mat = StandardMaterial3D.new()
|
||||||
|
mat.albedo_color = color
|
||||||
|
mat.blend_mode = BaseMaterial3D.BLEND_MODE_MIX
|
||||||
|
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||||
|
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
|
||||||
|
# If color is WHITE (reset), clear the overlay
|
||||||
|
if color == Color.WHITE:
|
||||||
|
node.material_overlay = null
|
||||||
|
else:
|
||||||
|
# If color is Blue (frozen), make it semi-transparent overlay
|
||||||
|
mat.albedo_color = color
|
||||||
|
mat.albedo_color.a = 0.5 # Semi-transparent
|
||||||
|
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
node.material_overlay = mat
|
||||||
|
|
||||||
|
for child in node.get_children():
|
||||||
|
_apply_tint_recursive(child, color)
|
||||||
|
|
||||||
|
|
||||||
func _process(delta):
|
func _process(delta):
|
||||||
if is_multiplayer_authority():
|
if is_multiplayer_authority():
|
||||||
@@ -1354,8 +1403,6 @@ func set_spawn_position(pos: Vector2i):
|
|||||||
current_position.y * cell_size.z + cell_size.z * 0.5
|
current_position.y * cell_size.z + cell_size.z * 0.5
|
||||||
) + cell_offset
|
) + cell_offset
|
||||||
|
|
||||||
func highlight_valid_obstacle_cells():
|
|
||||||
action_manager.highlight_valid_obstacle_cells()
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func complete_race(final_position: int):
|
func complete_race(final_position: int):
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
extends Node
|
|
||||||
|
|
||||||
# ObstacleManager - Handles obstacle placement and management
|
|
||||||
|
|
||||||
enum ObstacleOrientation {
|
|
||||||
NORTH = 0,
|
|
||||||
EAST = 1,
|
|
||||||
SOUTH = 2,
|
|
||||||
WEST = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
var current_obstacle_orientation = ObstacleOrientation.NORTH
|
|
||||||
var current_obstacle_item = 12
|
|
||||||
|
|
||||||
var gridmap_ref # Reference to EnhancedGridMap
|
|
||||||
|
|
||||||
func initialize(gridmap):
|
|
||||||
gridmap_ref = gridmap
|
|
||||||
|
|
||||||
func place_obstacle(grid_position: Vector2i, local_player) -> bool:
|
|
||||||
if not local_player or local_player.action_points < 1:
|
|
||||||
return false
|
|
||||||
|
|
||||||
var floor_index = 3 # Always place on floor 3
|
|
||||||
|
|
||||||
var success = gridmap_ref.place_obstacle(
|
|
||||||
Vector3i(grid_position.x, floor_index, grid_position.y),
|
|
||||||
current_obstacle_item,
|
|
||||||
current_obstacle_orientation
|
|
||||||
)
|
|
||||||
|
|
||||||
if success:
|
|
||||||
local_player.action_points -= 1
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
|
|
||||||
func cycle_obstacle_orientation() -> String:
|
|
||||||
var orientations = [
|
|
||||||
ObstacleOrientation.NORTH,
|
|
||||||
ObstacleOrientation.EAST,
|
|
||||||
ObstacleOrientation.SOUTH,
|
|
||||||
ObstacleOrientation.WEST
|
|
||||||
]
|
|
||||||
var current_index = orientations.find(current_obstacle_orientation)
|
|
||||||
current_index = (current_index + 1) % orientations.size()
|
|
||||||
current_obstacle_orientation = orientations[current_index]
|
|
||||||
|
|
||||||
var direction_names = ["North", "East", "South", "West"]
|
|
||||||
return "Direction: " + direction_names[current_index]
|
|
||||||
|
|
||||||
func cycle_obstacle_type() -> String:
|
|
||||||
var obstacle_types = [12, 13, 14, 15]
|
|
||||||
var current_index = obstacle_types.find(current_obstacle_item)
|
|
||||||
current_index = (current_index + 1) % obstacle_types.size()
|
|
||||||
current_obstacle_item = obstacle_types[current_index]
|
|
||||||
return "Type: " + str(current_index + 1)
|
|
||||||
@@ -163,37 +163,6 @@ func highlight_occupied_playerboard_slots():
|
|||||||
# Update the UI to reflect changes
|
# Update the UI to reflect changes
|
||||||
main.ui_manager.update_playerboard_ui()
|
main.ui_manager.update_playerboard_ui()
|
||||||
|
|
||||||
func highlight_valid_obstacle_cells():
|
|
||||||
if not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"):
|
|
||||||
return
|
|
||||||
|
|
||||||
clear_highlights()
|
|
||||||
|
|
||||||
var cells_to_highlight = []
|
|
||||||
|
|
||||||
# Highlight all empty cells on the grid except those occupied by players or obstacles
|
|
||||||
for x in range(enhanced_gridmap.columns):
|
|
||||||
for z in range(enhanced_gridmap.rows):
|
|
||||||
var pos = Vector2i(x, z)
|
|
||||||
var cell = Vector3i(x, 3, z) # Check floor 3 for occupancy
|
|
||||||
var occupied_by_player = false
|
|
||||||
var occupied_by_obstacle = false
|
|
||||||
|
|
||||||
# Check if cell is occupied by any player
|
|
||||||
for p in player.get_tree().get_nodes_in_group("Players"):
|
|
||||||
if p.current_position == pos:
|
|
||||||
occupied_by_player = true
|
|
||||||
break
|
|
||||||
|
|
||||||
# Check if cell is occupied by an obstacle
|
|
||||||
if enhanced_gridmap.get_cell_item(cell) in enhanced_gridmap.obstacle_items:
|
|
||||||
occupied_by_obstacle = true
|
|
||||||
|
|
||||||
# Only add to highlights if not occupied by player or obstacle
|
|
||||||
if not occupied_by_player and not occupied_by_obstacle:
|
|
||||||
cells_to_highlight.append(pos)
|
|
||||||
|
|
||||||
highlight_cells_if_authorized(cells_to_highlight)
|
|
||||||
|
|
||||||
func clear_highlights():
|
func clear_highlights():
|
||||||
# Never allow bots to clear highlights for human players
|
# Never allow bots to clear highlights for human players
|
||||||
@@ -220,7 +189,7 @@ func clear_highlights():
|
|||||||
child.hide()
|
child.hide()
|
||||||
|
|
||||||
# Restore highlights based on current action state
|
# Restore highlights based on current action state
|
||||||
if main and current_state == main.ui_manager.ActionState.MOVING and player.is_my_turn and current_state != main.ui_manager.ActionState.PLACING_OBSTACLE:
|
if main and current_state == main.ui_manager.ActionState.MOVING and player.is_my_turn:
|
||||||
player.highlight_movement_range()
|
player.highlight_movement_range()
|
||||||
|
|
||||||
func clear_playerboard_highlights():
|
func clear_playerboard_highlights():
|
||||||
|
|||||||
@@ -110,9 +110,7 @@ func handle_grid_click(grid_position: Vector2i):
|
|||||||
main.ui_manager.ActionState.RANDOMIZING:
|
main.ui_manager.ActionState.RANDOMIZING:
|
||||||
if grid_position in player.highlighted_cells:
|
if grid_position in player.highlighted_cells:
|
||||||
main.randomize_item_at_position(grid_position)
|
main.randomize_item_at_position(grid_position)
|
||||||
main.ui_manager.ActionState.PLACING_OBSTACLE:
|
|
||||||
if grid_position in player.highlighted_cells:
|
|
||||||
main.place_obstacle(grid_position)
|
|
||||||
|
|
||||||
func handle_slot_gui_input(event, slot_index, slot_ui) -> int:
|
func handle_slot_gui_input(event, slot_index, slot_ui) -> int:
|
||||||
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
|
||||||
|
|||||||
@@ -84,8 +84,6 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
|||||||
if cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items or player.is_position_occupied(grid_position):
|
if cell_item == -1 or cell_item in enhanced_gridmap.non_walkable_items or player.is_position_occupied(grid_position):
|
||||||
return false
|
return false
|
||||||
|
|
||||||
if enhanced_gridmap.is_blocked_by_obstacle(player.current_position, grid_position, 3):
|
|
||||||
return false
|
|
||||||
|
|
||||||
# All checks passed, perform move
|
# All checks passed, perform move
|
||||||
rotate_towards_target(grid_position)
|
rotate_towards_target(grid_position)
|
||||||
@@ -165,15 +163,7 @@ func highlight_movement_range():
|
|||||||
# First, identify all cells that are blocked by obstacles
|
# First, identify all cells that are blocked by obstacles
|
||||||
var blocked_cells = []
|
var blocked_cells = []
|
||||||
|
|
||||||
# Check all cells for obstacles and get their blocked cells
|
# Obstacle blocking logic removed
|
||||||
for x in range(enhanced_gridmap.columns):
|
|
||||||
for z in range(enhanced_gridmap.rows):
|
|
||||||
var cell_pos = Vector2i(x, z)
|
|
||||||
var cell_pos3d = Vector3i(x, 3, z)
|
|
||||||
|
|
||||||
if enhanced_gridmap.has_obstacle_at(cell_pos3d):
|
|
||||||
var orientation = enhanced_gridmap.get_obstacle_orientation(cell_pos3d)
|
|
||||||
blocked_cells.append_array(enhanced_gridmap.get_cells_blocked_by_obstacle(cell_pos, orientation, 3))
|
|
||||||
|
|
||||||
# Now highlight all cells within movement range that aren't blocked
|
# Now highlight all cells within movement range that aren't blocked
|
||||||
for x in range(max(0, player.current_position.x - movement_range),
|
for x in range(max(0, player.current_position.x - movement_range),
|
||||||
@@ -252,16 +242,16 @@ func can_reach_cell(target_pos: Vector2i, blocked_cells: Array) -> bool:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if movement between cells is blocked by an obstacle
|
# Check if movement between cells is blocked by an obstacle
|
||||||
if not is_diagonal_direction(dir) and enhanced_gridmap.is_movement_blocked(current, next_pos, 3):
|
# if not is_diagonal_direction(dir) and enhanced_gridmap.is_movement_blocked(current, next_pos, 3):
|
||||||
continue
|
# continue
|
||||||
|
|
||||||
# For diagonal movement, check if both orthogonal paths are blocked
|
# For diagonal movement, check if both orthogonal paths are blocked
|
||||||
if is_diagonal_direction(dir):
|
if is_diagonal_direction(dir):
|
||||||
var mid1 = Vector2i(next_pos.x, current.y)
|
var mid1 = Vector2i(next_pos.x, current.y)
|
||||||
var mid2 = Vector2i(current.x, next_pos.y)
|
var mid2 = Vector2i(current.x, next_pos.y)
|
||||||
|
|
||||||
var path1_blocked = mid1 in blocked_cells or enhanced_gridmap.is_movement_blocked(current, mid1, 3)
|
var path1_blocked = mid1 in blocked_cells # or enhanced_gridmap.is_movement_blocked(current, mid1, 3)
|
||||||
var path2_blocked = mid2 in blocked_cells or enhanced_gridmap.is_movement_blocked(current, mid2, 3)
|
var path2_blocked = mid2 in blocked_cells # or enhanced_gridmap.is_movement_blocked(current, mid2, 3)
|
||||||
|
|
||||||
if path1_blocked and path2_blocked:
|
if path1_blocked and path2_blocked:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -34,8 +34,11 @@ const FREEZE_DURATION = 3.0
|
|||||||
const BLOCK_DURATION = 9.0
|
const BLOCK_DURATION = 9.0
|
||||||
const INVISIBLE_DURATION = 6.0
|
const INVISIBLE_DURATION = 6.0
|
||||||
|
|
||||||
|
|
||||||
# Active effect tracking
|
# Active effect tracking
|
||||||
var blocked_tiles: Array[Dictionary] = [] # {position: Vector3i, original_item: int, timer: float}
|
var blocked_tiles: Array[Dictionary] = [] # {position: Vector3i, original_item: int, timer: float}
|
||||||
|
var invisible_timer: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
func initialize(p_player: Node3D, p_gridmap: Node):
|
func initialize(p_player: Node3D, p_gridmap: Node):
|
||||||
player = p_player
|
player = p_player
|
||||||
@@ -43,8 +46,6 @@ func initialize(p_player: Node3D, p_gridmap: Node):
|
|||||||
rng = RandomNumberGenerator.new()
|
rng = RandomNumberGenerator.new()
|
||||||
rng.randomize()
|
rng.randomize()
|
||||||
|
|
||||||
func _process(delta):
|
|
||||||
_update_blocked_tiles(delta)
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Check if item is a holo tile
|
# Check if item is a holo tile
|
||||||
@@ -82,91 +83,113 @@ func trigger_random_effect():
|
|||||||
func sync_effect_triggered(effect: int):
|
func sync_effect_triggered(effect: int):
|
||||||
print("[SpecialTiles] Synced effect %s for player %s" % [SpecialEffect.keys()[effect], player.name])
|
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
|
# Effect Implementations
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
func _execute_burn_tiles():
|
func _execute_burn_tiles():
|
||||||
|
# NEW LOGIC: Put back random target tiles from their playerboard to their position nearest
|
||||||
# Find random opponent
|
# Find random opponent
|
||||||
var opponent = _get_random_opponent()
|
var opponent = _get_random_opponent()
|
||||||
if not opponent:
|
if not opponent:
|
||||||
print("[SpecialTiles] No opponent found for BURN_TILES")
|
print("[SpecialTiles] No opponent found for BURN_TILES")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get pattern around opponent
|
# Get opponent's playerboard items
|
||||||
var pattern = _get_random_pattern()
|
var board_indices = []
|
||||||
var positions = _get_valid_pattern_positions(opponent.current_position, pattern)
|
for i in range(opponent.playerboard.size()):
|
||||||
|
if opponent.playerboard[i] != -1:
|
||||||
|
board_indices.append(i)
|
||||||
|
|
||||||
# Remove tiles at these positions
|
if board_indices.is_empty():
|
||||||
for pos in positions:
|
return # Nothing to burn
|
||||||
var cell = Vector3i(pos.x, 1, pos.y)
|
|
||||||
if enhanced_gridmap.get_cell_item(cell) != -1:
|
# 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()
|
||||||
|
|
||||||
|
for i in range(min(burn_count, board_indices.size())):
|
||||||
|
var slot_idx = board_indices[i]
|
||||||
|
var item = opponent.playerboard[slot_idx]
|
||||||
|
|
||||||
|
# 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():
|
if player.is_multiplayer_authority():
|
||||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
var main = player.get_tree().get_root().get_node_or_null("Main")
|
||||||
if main:
|
if main:
|
||||||
main.rpc("sync_grid_item", cell.x, cell.y, cell.z, -1)
|
main.rpc("sync_grid_item", target_pos.x, target_pos.y, target_pos.z, target_item)
|
||||||
|
# Sync opponent board change
|
||||||
# Trigger screen shake on the targeted opponent
|
main.rpc("sync_playerboard", opponent.name.to_int(), opponent.playerboard)
|
||||||
if opponent.is_multiplayer_authority():
|
tiles_burned += 1
|
||||||
opponent.rpc("trigger_screen_shake", "targeted")
|
|
||||||
else:
|
if tiles_burned > 0:
|
||||||
opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted")
|
# Trigger screen shake
|
||||||
|
if opponent.is_multiplayer_authority():
|
||||||
print("[SpecialTiles] BURN_TILES: Removed %d tiles around %s" % [positions.size(), opponent.name])
|
opponent.rpc("trigger_screen_shake", "targeted")
|
||||||
player.rpc("display_message", "Burned tiles near opponent!")
|
else:
|
||||||
|
opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted")
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
func _execute_spawn_tiles():
|
func _execute_spawn_tiles():
|
||||||
# Get pattern around activating player
|
# NEW LOGIC: Spawn more in neighbor space (radius 2)
|
||||||
var pattern = _get_random_pattern()
|
var radius = 2
|
||||||
var positions = _get_valid_pattern_positions(player.current_position, pattern)
|
var candidates = []
|
||||||
|
|
||||||
# Spawn random tiles at empty positions
|
for x in range(-radius, radius + 1):
|
||||||
var spawned_count = 0
|
for y in range(-radius, radius + 1):
|
||||||
for pos in positions:
|
if x == 0 and y == 0: continue
|
||||||
var cell = Vector3i(pos.x, 1, pos.y)
|
var pos = player.current_position + Vector2i(x, y)
|
||||||
if enhanced_gridmap.get_cell_item(cell) == -1:
|
if enhanced_gridmap.is_position_valid(pos):
|
||||||
var new_tile = rng.randi_range(7, 10) # Random normal tile
|
var cell = Vector3i(pos.x, 1, pos.y)
|
||||||
if player.is_multiplayer_authority():
|
if enhanced_gridmap.get_cell_item(cell) == -1:
|
||||||
var main = player.get_tree().get_root().get_node_or_null("Main")
|
candidates.append(cell)
|
||||||
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])
|
var spawn_count = rng.randi_range(3, 8) # Spawn a bunch
|
||||||
player.rpc("display_message", "Spawned new tiles!")
|
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!")
|
||||||
|
|
||||||
func _execute_freeze_player():
|
func _execute_freeze_player():
|
||||||
# Find random opponent
|
# Find random opponent
|
||||||
@@ -190,8 +213,52 @@ func _execute_freeze_player():
|
|||||||
opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted")
|
opponent.rpc_id(opponent.get_multiplayer_authority(), "trigger_screen_shake", "targeted")
|
||||||
|
|
||||||
print("[SpecialTiles] FREEZE_PLAYER: Froze %s for %ds" % [opponent.name, FREEZE_DURATION])
|
print("[SpecialTiles] FREEZE_PLAYER: Froze %s for %ds" % [opponent.name, FREEZE_DURATION])
|
||||||
player.rpc("display_message", "Froze an opponent!")
|
player.rpc("display_message", "Froze %s!" % opponent.display_name)
|
||||||
opponent.rpc("display_message", "You are frozen!")
|
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):
|
func _create_unfreeze_timer(target_player: Node3D, duration: float):
|
||||||
if not is_instance_valid(player) or not is_instance_valid(target_player):
|
if not is_instance_valid(player) or not is_instance_valid(target_player):
|
||||||
@@ -201,69 +268,98 @@ func _create_unfreeze_timer(target_player: Node3D, duration: float):
|
|||||||
|
|
||||||
if is_instance_valid(target_player):
|
if is_instance_valid(target_player):
|
||||||
target_player.set("is_frozen", false)
|
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_player.rpc("display_message", "Unfrozen!")
|
||||||
|
|
||||||
|
|
||||||
func _execute_block_floor():
|
func _execute_block_floor():
|
||||||
# Find valid tile near player to block
|
# 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 neighbors = enhanced_gridmap.get_neighbors(player.current_position, 0)
|
||||||
var valid_neighbors = neighbors.filter(func(n): return n.is_walkable)
|
var valid_neighbors = neighbors.filter(func(n): return n.is_walkable)
|
||||||
|
|
||||||
if valid_neighbors.is_empty():
|
if valid_neighbors.is_empty():
|
||||||
print("[SpecialTiles] No valid tile to block")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
var target_neighbor = valid_neighbors[rng.randi() % valid_neighbors.size()]
|
var start_neighbor = valid_neighbors[rng.randi() % valid_neighbors.size()]
|
||||||
var block_pos = Vector3i(target_neighbor.position.x, 0, target_neighbor.position.y)
|
var start_pos = start_neighbor.position
|
||||||
var original_item = enhanced_gridmap.get_cell_item(block_pos)
|
|
||||||
|
|
||||||
# Make tile non-walkable (use a blocked item index)
|
# Random direction: H, V, D1, D2
|
||||||
var blocked_item = 4 # Using non_walkable_items[0] typically
|
var directions = [
|
||||||
if enhanced_gridmap.non_walkable_items.size() > 0:
|
Vector2i(1, 0), Vector2i(-1, 0), # Horizontal
|
||||||
blocked_item = enhanced_gridmap.non_walkable_items[0]
|
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)
|
||||||
|
|
||||||
if player.is_multiplayer_authority():
|
var valid_block_count = 0
|
||||||
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
|
for i in range(count):
|
||||||
blocked_tiles.append({
|
var target_pos_2d = start_pos + (dir * i)
|
||||||
"position": block_pos,
|
# Check if valid grid position
|
||||||
"original_item": original_item,
|
if not enhanced_gridmap.is_position_valid(target_pos_2d):
|
||||||
"timer": BLOCK_DURATION
|
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)
|
||||||
|
|
||||||
|
# Make tile non-walkable
|
||||||
|
var blocked_item = 4
|
||||||
|
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)
|
||||||
|
|
||||||
|
blocked_tiles.append({
|
||||||
|
"position": block_pos,
|
||||||
|
"original_item": original_item,
|
||||||
|
"timer": BLOCK_DURATION
|
||||||
|
})
|
||||||
|
valid_block_count += 1
|
||||||
|
|
||||||
# Re-initialize pathfinding
|
if valid_block_count > 0:
|
||||||
enhanced_gridmap.initialize_astar()
|
enhanced_gridmap.initialize_astar()
|
||||||
|
print("[SpecialTiles] BLOCK_FLOOR: Blocked line of %d tiles" % valid_block_count)
|
||||||
print("[SpecialTiles] BLOCK_FLOOR: Blocked tile at %s for %ds" % [target_neighbor.position, BLOCK_DURATION])
|
player.rpc("display_message", "Blocked a wall of tiles!")
|
||||||
player.rpc("display_message", "Blocked a floor tile!")
|
|
||||||
|
|
||||||
func _execute_invisible_mode():
|
func _execute_invisible_mode():
|
||||||
# Set invisible mode on player
|
# Set invisible mode on player
|
||||||
|
# NEW LOGIC: Also enables auto-grab in _process
|
||||||
if player.has_method("apply_invisible_mode"):
|
if player.has_method("apply_invisible_mode"):
|
||||||
player.apply_invisible_mode(INVISIBLE_DURATION)
|
player.apply_invisible_mode(INVISIBLE_DURATION)
|
||||||
else:
|
else:
|
||||||
# Fallback: directly set invisible state
|
|
||||||
player.set("is_invisible", true)
|
player.set("is_invisible", true)
|
||||||
player.set("original_movement_range", player.movement_range)
|
player.set("original_movement_range", player.movement_range)
|
||||||
player.movement_range = player.movement_range + 2 # Speed boost
|
player.movement_range = player.movement_range + 2
|
||||||
_create_invisibility_timer(INVISIBLE_DURATION)
|
invisible_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):
|
|
||||||
if not is_instance_valid(player):
|
|
||||||
return
|
|
||||||
|
|
||||||
await player.get_tree().create_timer(duration).timeout
|
|
||||||
|
|
||||||
if is_instance_valid(player):
|
print("[SpecialTiles] INVISIBLE_MODE: Activated")
|
||||||
player.set("is_invisible", false)
|
player.rpc("display_message", "Invisible Mode Active!")
|
||||||
if player.get("original_movement_range"):
|
|
||||||
player.movement_range = player.original_movement_range
|
|
||||||
player.rpc("display_message", "Invisible mode ended!")
|
func _process(delta):
|
||||||
|
_update_blocked_tiles(delta)
|
||||||
|
_update_invisible_timer(delta)
|
||||||
|
|
||||||
|
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.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
|
# Helper Functions
|
||||||
@@ -278,6 +374,16 @@ func _get_random_opponent() -> Node3D:
|
|||||||
|
|
||||||
return opponents[rng.randi() % opponents.size()]
|
return opponents[rng.randi() % opponents.size()]
|
||||||
|
|
||||||
|
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):
|
func _update_blocked_tiles(delta: float):
|
||||||
var tiles_to_restore: Array[int] = []
|
var tiles_to_restore: Array[int] = []
|
||||||
|
|
||||||
@@ -307,6 +413,7 @@ func check_shield_and_cancel_effect() -> bool:
|
|||||||
"""Returns true if player has shield (invisible mode) and cancels the incoming effect."""
|
"""Returns true if player has shield (invisible mode) and cancels the incoming effect."""
|
||||||
if player.get("is_invisible"):
|
if player.get("is_invisible"):
|
||||||
player.set("is_invisible", false)
|
player.set("is_invisible", false)
|
||||||
|
invisible_timer = 0 # Cancel timer
|
||||||
if player.get("original_movement_range"):
|
if player.get("original_movement_range"):
|
||||||
player.movement_range = player.original_movement_range
|
player.movement_range = player.original_movement_range
|
||||||
player.rpc("display_message", "Shield blocked an attack!")
|
player.rpc("display_message", "Shield blocked an attack!")
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ enum ActionState {
|
|||||||
PUTTING,
|
PUTTING,
|
||||||
RANDOMIZING,
|
RANDOMIZING,
|
||||||
ARRANGING,
|
ARRANGING,
|
||||||
PLACING_OBSTACLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var current_action_state = ActionState.NONE
|
var current_action_state = ActionState.NONE
|
||||||
|
|||||||
Reference in New Issue
Block a user