diff --git a/_daily_basis/report_2026-01-09.md b/_daily_basis/report_2026-01-09.md new file mode 100644 index 0000000..193903e --- /dev/null +++ b/_daily_basis/report_2026-01-09.md @@ -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. diff --git a/addons/enhanced_gridmap/enhanced_gridmap.gd b/addons/enhanced_gridmap/enhanced_gridmap.gd index d7a1ed5..757e3e6 100644 --- a/addons/enhanced_gridmap/enhanced_gridmap.gd +++ b/addons/enhanced_gridmap/enhanced_gridmap.gd @@ -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 diff --git a/scenes/main.gd b/scenes/main.gd index ec3bbb8..afb0ae7 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -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 # ============================================================================= diff --git a/scenes/main.tscn b/scenes/main.tscn index 7039cc5..58e1982 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -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="."] diff --git a/scenes/player.gd b/scenes/player.gd index 1ca43c6..628734a 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -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): diff --git a/scripts/managers/obstacle_manager.gd b/scripts/managers/obstacle_manager.gd deleted file mode 100644 index 462a56d..0000000 --- a/scripts/managers/obstacle_manager.gd +++ /dev/null @@ -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) diff --git a/scripts/managers/player_action_manager.gd b/scripts/managers/player_action_manager.gd index 4bb57f4..f189cd3 100644 --- a/scripts/managers/player_action_manager.gd +++ b/scripts/managers/player_action_manager.gd @@ -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(): diff --git a/scripts/managers/player_input_manager.gd b/scripts/managers/player_input_manager.gd index cea79c2..6e4cfd9 100644 --- a/scripts/managers/player_input_manager.gd +++ b/scripts/managers/player_input_manager.gd @@ -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: diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index 3396c1e..e33ee64 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -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 diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index eca5b5a..7ff771a 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -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!") diff --git a/scripts/managers/ui_manager.gd b/scripts/managers/ui_manager.gd index ee6257c..31c9592 100644 --- a/scripts/managers/ui_manager.gd +++ b/scripts/managers/ui_manager.gd @@ -29,7 +29,6 @@ enum ActionState { PUTTING, RANDOMIZING, ARRANGING, - PLACING_OBSTACLE } var current_action_state = ActionState.NONE