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
-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