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 grid_updated
|
||||
|
||||
@export var columns: int = 10 : set = set_columns
|
||||
@export var rows: int = 10 : set = set_rows
|
||||
@export var floors: int = 3 : set = set_floors
|
||||
@export var auto_generate: bool = false : set = set_auto_generate
|
||||
@export var columns: int = 10: set = set_columns
|
||||
@export var rows: int = 10: set = set_rows
|
||||
@export var floors: int = 3: set = set_floors
|
||||
@export var auto_generate: bool = false: set = set_auto_generate
|
||||
|
||||
@export var normal_items: Array[int] = [0]
|
||||
@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 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
|
||||
enum Direction {
|
||||
NORTHWEST, NORTH, NORTHEAST,
|
||||
WEST, CENTER, EAST,
|
||||
SOUTHWEST, SOUTH, SOUTHEAST,
|
||||
BLOCKED_NORTH = 10,
|
||||
BLOCKED_EAST = 11,
|
||||
BLOCKED_SOUTH = 12,
|
||||
BLOCKED_WEST = 13
|
||||
SOUTHWEST, SOUTH, SOUTHEAST
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
# 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):
|
||||
is_walkable = false
|
||||
# if not is_diagonal_direction(dir) and is_movement_blocked(current_pos, neighbor_pos, floor_index):
|
||||
# is_walkable = false
|
||||
|
||||
if is_diagonal_direction(dir):
|
||||
# For diagonal movement, check if both orthogonal paths are blocked
|
||||
var mid1 = Vector2i(neighbor_pos.x, current_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:
|
||||
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
|
||||
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]
|
||||
|
||||
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
|
||||
|
||||
# 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.set_point_weight_scale(neighbor_id, weight)
|
||||
|
||||
@@ -488,124 +472,10 @@ func update_grid_data():
|
||||
grid_data.append(floor_data)
|
||||
emit_signal("grid_updated")
|
||||
|
||||
# Check the obstacle on a cell
|
||||
func has_obstacle_at(pos: Vector3i) -> bool:
|
||||
var item = get_cell_item(pos)
|
||||
return item in obstacle_items
|
||||
|
||||
# Get orientation ( rotation )
|
||||
# Orientation helper
|
||||
func get_cell_orientation(pos: Vector3i) -> int:
|
||||
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
|
||||
func get_cell_rotation(position: Vector3i) -> int:
|
||||
return get_cell_item_orientation(position)
|
||||
@@ -649,60 +519,3 @@ func _emit_grid_updated():
|
||||
func set_diagonal_movement(enable: bool):
|
||||
diagonal_movement = enable
|
||||
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
|
||||
var ui_manager
|
||||
var obstacle_manager
|
||||
var goals_cycle_manager
|
||||
var screen_shake_manager
|
||||
var touch_controls
|
||||
@@ -31,7 +30,6 @@ func _ready():
|
||||
ui_manager.setup_timer_labels(self)
|
||||
ui_manager.setup_leaderboard_ui(self)
|
||||
ui_manager.setup_powerup_bar_ui(self)
|
||||
_setup_obstacle_ui()
|
||||
# GlobalMatchTimer is now static in main.tscn - no setup needed
|
||||
# NetworkPanel is visible during gameplay
|
||||
|
||||
@@ -48,11 +46,6 @@ func _init_managers():
|
||||
add_child(ui_manager)
|
||||
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 = load("res://scripts/managers/goals_cycle_manager.gd").new()
|
||||
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):
|
||||
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():
|
||||
"""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:
|
||||
_show_arrangement_ui()
|
||||
local_player.highlight_occupied_playerboard_slots()
|
||||
ui_manager.ActionState.PLACING_OBSTACLE:
|
||||
local_player.highlight_valid_obstacle_cells()
|
||||
|
||||
|
||||
func _show_arrangement_ui():
|
||||
if ui_manager.playerboard_ui:
|
||||
@@ -626,28 +599,6 @@ func _on_playerboard_slot_clicked(event, slot_index):
|
||||
ui_manager.ActionState.ARRANGING:
|
||||
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
|
||||
# =============================================================================
|
||||
|
||||
@@ -59,7 +59,6 @@ script = ExtResource("2_hbe1v")
|
||||
columns = 14
|
||||
rows = 14
|
||||
floors = 2
|
||||
obstacle_items = Array[int]([12])
|
||||
metadata/_editor_floor_ = Vector3(0, 1, 0)
|
||||
|
||||
[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."""
|
||||
display_name = new_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):
|
||||
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
|
||||
) + cell_offset
|
||||
|
||||
func highlight_valid_obstacle_cells():
|
||||
action_manager.highlight_valid_obstacle_cells()
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
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
|
||||
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():
|
||||
# Never allow bots to clear highlights for human players
|
||||
@@ -220,7 +189,7 @@ func clear_highlights():
|
||||
child.hide()
|
||||
|
||||
# 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()
|
||||
|
||||
func clear_playerboard_highlights():
|
||||
|
||||
@@ -110,9 +110,7 @@ func handle_grid_click(grid_position: Vector2i):
|
||||
main.ui_manager.ActionState.RANDOMIZING:
|
||||
if grid_position in player.highlighted_cells:
|
||||
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:
|
||||
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):
|
||||
return false
|
||||
|
||||
if enhanced_gridmap.is_blocked_by_obstacle(player.current_position, grid_position, 3):
|
||||
return false
|
||||
|
||||
# All checks passed, perform move
|
||||
rotate_towards_target(grid_position)
|
||||
@@ -165,15 +163,7 @@ func highlight_movement_range():
|
||||
# First, identify all cells that are blocked by obstacles
|
||||
var blocked_cells = []
|
||||
|
||||
# Check all cells for obstacles and get their blocked cells
|
||||
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))
|
||||
# Obstacle blocking logic removed
|
||||
|
||||
# Now highlight all cells within movement range that aren't blocked
|
||||
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
|
||||
|
||||
# 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):
|
||||
continue
|
||||
# if not is_diagonal_direction(dir) and enhanced_gridmap.is_movement_blocked(current, next_pos, 3):
|
||||
# continue
|
||||
|
||||
# For diagonal movement, check if both orthogonal paths are blocked
|
||||
if is_diagonal_direction(dir):
|
||||
var mid1 = Vector2i(next_pos.x, current.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 path2_blocked = mid2 in blocked_cells or enhanced_gridmap.is_movement_blocked(current, mid2, 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)
|
||||
|
||||
if path1_blocked and path2_blocked:
|
||||
continue
|
||||
|
||||
@@ -34,8 +34,11 @@ 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}
|
||||
var invisible_timer: float = 0.0
|
||||
|
||||
|
||||
func initialize(p_player: Node3D, p_gridmap: Node):
|
||||
player = p_player
|
||||
@@ -43,8 +46,6 @@ func initialize(p_player: Node3D, p_gridmap: Node):
|
||||
rng = RandomNumberGenerator.new()
|
||||
rng.randomize()
|
||||
|
||||
func _process(delta):
|
||||
_update_blocked_tiles(delta)
|
||||
|
||||
# =============================================================================
|
||||
# Check if item is a holo tile
|
||||
@@ -82,91 +83,113 @@ func trigger_random_effect():
|
||||
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():
|
||||
# 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
|
||||
|
||||
# Get pattern around opponent
|
||||
var pattern = _get_random_pattern()
|
||||
var positions = _get_valid_pattern_positions(opponent.current_position, pattern)
|
||||
# Get opponent's playerboard items
|
||||
var board_indices = []
|
||||
for i in range(opponent.playerboard.size()):
|
||||
if opponent.playerboard[i] != -1:
|
||||
board_indices.append(i)
|
||||
|
||||
# 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 board_indices.is_empty():
|
||||
return # Nothing to burn
|
||||
|
||||
# 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():
|
||||
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)
|
||||
|
||||
# Trigger screen shake on the targeted 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] BURN_TILES: Removed %d tiles around %s" % [positions.size(), opponent.name])
|
||||
player.rpc("display_message", "Burned tiles near opponent!")
|
||||
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")
|
||||
|
||||
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():
|
||||
# Get pattern around activating player
|
||||
var pattern = _get_random_pattern()
|
||||
var positions = _get_valid_pattern_positions(player.current_position, pattern)
|
||||
# NEW LOGIC: Spawn more in neighbor space (radius 2)
|
||||
var radius = 2
|
||||
var candidates = []
|
||||
|
||||
# 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
|
||||
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)
|
||||
|
||||
print("[SpecialTiles] SPAWN_TILES: Spawned %d tiles around %s" % [spawned_count, player.name])
|
||||
player.rpc("display_message", "Spawned new tiles!")
|
||||
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!")
|
||||
|
||||
func _execute_freeze_player():
|
||||
# Find random opponent
|
||||
@@ -190,8 +213,52 @@ func _execute_freeze_player():
|
||||
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 an opponent!")
|
||||
opponent.rpc("display_message", "You are frozen!")
|
||||
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):
|
||||
@@ -201,69 +268,98 @@ func _create_unfreeze_timer(target_player: Node3D, duration: float):
|
||||
|
||||
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!")
|
||||
|
||||
|
||||
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 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)
|
||||
var start_neighbor = valid_neighbors[rng.randi() % valid_neighbors.size()]
|
||||
var start_pos = start_neighbor.position
|
||||
|
||||
# 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]
|
||||
# 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)
|
||||
|
||||
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)
|
||||
var valid_block_count = 0
|
||||
|
||||
# Track blocked tile for restoration
|
||||
blocked_tiles.append({
|
||||
"position": block_pos,
|
||||
"original_item": original_item,
|
||||
"timer": BLOCK_DURATION
|
||||
})
|
||||
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)
|
||||
|
||||
# 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
|
||||
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!")
|
||||
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!")
|
||||
|
||||
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:
|
||||
# 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!")
|
||||
player.movement_range = player.movement_range + 2
|
||||
invisible_timer = INVISIBLE_DURATION
|
||||
|
||||
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):
|
||||
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!")
|
||||
print("[SpecialTiles] INVISIBLE_MODE: Activated")
|
||||
player.rpc("display_message", "Invisible Mode Active!")
|
||||
|
||||
|
||||
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
|
||||
@@ -278,6 +374,16 @@ func _get_random_opponent() -> Node3D:
|
||||
|
||||
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):
|
||||
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."""
|
||||
if player.get("is_invisible"):
|
||||
player.set("is_invisible", false)
|
||||
invisible_timer = 0 # Cancel timer
|
||||
if player.get("original_movement_range"):
|
||||
player.movement_range = player.original_movement_range
|
||||
player.rpc("display_message", "Shield blocked an attack!")
|
||||
|
||||
@@ -29,7 +29,6 @@ enum ActionState {
|
||||
PUTTING,
|
||||
RANDOMIZING,
|
||||
ARRANGING,
|
||||
PLACING_OBSTACLE
|
||||
}
|
||||
|
||||
var current_action_state = ActionState.NONE
|
||||
|
||||
Reference in New Issue
Block a user