edit special power up

This commit is contained in:
2026-01-09 22:41:00 +08:00
parent 6aede0a382
commit 6948a4aed1
11 changed files with 326 additions and 468 deletions
+41
View File
@@ -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.
+11 -198
View File
@@ -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
View File
@@ -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
# =============================================================================
-1
View File
@@ -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
View File
@@ -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):
-56
View File
@@ -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)
+1 -32
View File
@@ -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():
+1 -3
View File
@@ -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:
+5 -15
View File
@@ -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
+217 -110
View File
@@ -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!")
-1
View File
@@ -29,7 +29,6 @@ enum ActionState {
PUTTING,
RANDOMIZING,
ARRANGING,
PLACING_OBSTACLE
}
var current_action_state = ActionState.NONE