From 77b85428961ca0e85783f994e13c780c7d9fad16 Mon Sep 17 00:00:00 2001 From: Yogi Wiguna Date: Wed, 4 Feb 2026 14:25:21 +0800 Subject: [PATCH] feat: Add player action and input managers to handle player actions, movement, targeting, and visual feedback. --- scenes/player.gd | 7 +- scripts/managers/player_action_manager.gd | 11 +- scripts/managers/player_input_manager.gd | 38 +++++ scripts/managers/special_tiles_manager.gd | 181 +++++++++++++++------- scripts/managers/ui_manager.gd | 4 +- 5 files changed, 182 insertions(+), 59 deletions(-) diff --git a/scenes/player.gd b/scenes/player.gd index 9c9c8ba..148618c 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -1411,6 +1411,9 @@ func auto_put_item() -> bool: @rpc("any_peer", "call_local", "reliable") func force_action_state_none(): # This is called by the server on the client to reset the UI + if is_bot or is_in_group("Bots"): + return + var main = get_tree().get_root().get_node_or_null("Main") if main and main.ui_manager: main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE @@ -1524,8 +1527,8 @@ func has_items_in_playerboard() -> bool: func playerboard_is_full() -> bool: return playerboard.find(-1) == -1 -func highlight_cells_if_authorized(cells_to_highlight: Array): - action_manager.highlight_cells_if_authorized(cells_to_highlight) +func highlight_cells_if_authorized(cells_to_highlight: Array, item_id: int = -1): + action_manager.highlight_cells_if_authorized(cells_to_highlight, item_id) # Update highlight_movement_range to respect the expanded obstacle blocking func highlight_movement_range(): diff --git a/scripts/managers/player_action_manager.gd b/scripts/managers/player_action_manager.gd index f189cd3..7ed13cb 100644 --- a/scripts/managers/player_action_manager.gd +++ b/scripts/managers/player_action_manager.gd @@ -71,16 +71,23 @@ func after_action_completed(): # Highlight Operations # ============================================================================= -func highlight_cells_if_authorized(cells_to_highlight: Array): +func highlight_cells_if_authorized(cells_to_highlight: Array, item_id: int = -1): if not player.is_multiplayer_authority() or player.is_bot or player.is_in_group("Bots"): return + # Start with fresh highlights if logical state changes? + # But dragging mouse calls this every frame. We need to clear previous calls. + # The simplistic clear_highlights() clears ALL highlights. + # Which is fine for targeting mode as we only highlight the target area. clear_highlights() + + var highlight_item = item_id if item_id != -1 else enhanced_gridmap.hover_item + for cell in cells_to_highlight: highlighted_cells.append(cell) enhanced_gridmap.set_cell_item( Vector3i(cell.x, 0, cell.y), - enhanced_gridmap.hover_item + highlight_item ) func highlight_empty_adjacent_cells(): diff --git a/scripts/managers/player_input_manager.gd b/scripts/managers/player_input_manager.gd index ffcdf5c..1d7216d 100644 --- a/scripts/managers/player_input_manager.gd +++ b/scripts/managers/player_input_manager.gd @@ -55,6 +55,31 @@ func _process(delta): player.attempt_target_action(2) elif Input.is_key_pressed(KEY_4): player.attempt_target_action(3) + + # Targeting Mode Preview + var main = player.get_node_or_null("/root/Main") + if main and main.ui_manager and main.ui_manager.current_action_state == main.ui_manager.ActionState.TARGETING: + # Use mouse position raycast to determine hover + var viewport = player.get_viewport() + var mouse_pos = viewport.get_mouse_position() + var camera = viewport.get_camera_3d() + var from = camera.project_ray_origin(mouse_pos) + var to = from + camera.project_ray_normal(mouse_pos) * 1000 + var hover_grid = player.raycast_to_grid(from, to) + + # print("Targeting Hover: %s, Skill: %d" % [hover_grid, main.ui_manager.pending_skill_id]) # Debug + + # Only update if valid position + if hover_grid != Vector2i(-1, -1): + var st_manager = player.get_node_or_null("SpecialTilesManager") + if st_manager: + var area = st_manager.get_skill_affected_area(main.ui_manager.pending_skill_id, hover_grid) + + # Choose highlight color/mesh based on skill + # User Request: Use default hover item (1) + var highlight_id = 1 + + player.highlight_cells_if_authorized(area, highlight_id) func handle_unhandled_input(event): # Early return if not authorized human player @@ -120,6 +145,19 @@ 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) + # Add TARGETING State + main.ui_manager.ActionState.TARGETING: + var skill_id = main.ui_manager.pending_skill_id + if skill_id != -1: + var st_manager = player.get_node_or_null("SpecialTilesManager") + if st_manager: + # Clear Highlights FIRST to avoid overwriting the newly placed tiles + player.clear_highlights() + + st_manager.execute_targeted_effect(skill_id, grid_position) + # Reset state + main.ui_manager.pending_skill_id = -1 + main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE func handle_slot_gui_input(event, slot_index, slot_ui) -> int: diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index 07ec1eb..43de432 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -26,7 +26,68 @@ const FREEZE_SLOW_DURATION = 3.0 signal cooldown_updated(effect: int, time_left: float, max_time: float) signal powerup_unlocked(effect: int, level: int) +# New Helper functions for Targeting and Preview + +func get_skill_affected_area(effect: int, center_pos: Vector2i) -> Array[Vector2i]: + var area: Array[Vector2i] = [] + + match effect: + SpecialEffect.AREA_FREEZE: + var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1) + var radius = 1 + if current_lvl >= 5: + radius = 2 + + for x in range(-radius, radius + 1): + for y in range(-radius, radius + 1): + var pos = center_pos + Vector2i(x, y) + # Validate bounds + if enhanced_gridmap.is_position_valid(pos): + area.append(pos) + + SpecialEffect.BLOCK_FLOOR: + # Logic: Perpendicular to player direction or based on major axis difference + var diff = center_pos - player.current_position + var is_horizontal = false + if abs(diff.x) > abs(diff.y): + is_horizontal = false # Vertical Column + else: + is_horizontal = true # Horizontal Row + + if is_horizontal: + for x in range(enhanced_gridmap.columns): + area.append(Vector2i(x, center_pos.y)) + else: + for z in range(enhanced_gridmap.rows): + area.append(Vector2i(center_pos.x, z)) + + return area + +func execute_targeted_effect(effect: int, target_pos: Vector2i): + # Apply Cooldown NOW + var level = powerup_levels.get(effect, 1) + var cooldown_time = COOLDOWN_L1 + ((level - 1) * (COOLDOWN_L8 - COOLDOWN_L1) / 7.0) + powerup_cooldowns[effect] = cooldown_time + emit_signal("cooldown_updated", effect, cooldown_time, cooldown_time) + + print("[SpecialTiles] Executing Targeted Effect %s at %s" % [SpecialEffect.keys()[effect], target_pos]) + + match effect: + SpecialEffect.AREA_FREEZE: + _execute_area_freeze(target_pos) + SpecialEffect.BLOCK_FLOOR: + _execute_block_floor(target_pos) + + # Animation / Shake + if player.is_multiplayer_authority(): + player.rpc("trigger_screen_shake", "light") + # Also reset action loop? + var main = player.get_tree().get_root().get_node_or_null("Main") + if main and main.ui_manager: + main.ui_manager.current_action_state = main.ui_manager.ActionState.NONE + # Random shape patterns for 3x3 area (relative offsets from center) + const PATTERNS = { "T": [Vector2i(0, -1), Vector2i(-1, 0), Vector2i(0, 0), Vector2i(1, 0)], "L": [Vector2i(0, -1), Vector2i(0, 0), Vector2i(0, 1), Vector2i(1, 1)], @@ -159,12 +220,21 @@ func activate_effect(effect: int, target_player: Node3D = null): match effect: SpecialEffect.FASTER_SPEED: _execute_faster_speed() - SpecialEffect.AREA_FREEZE: - _execute_area_freeze() - SpecialEffect.BLOCK_FLOOR: - _execute_block_floor(target_player if target_player else player) # Self or Target? Usually defensive wall? "Wall Block" + SpecialEffect.AREA_FREEZE, SpecialEffect.BLOCK_FLOOR: + # Enter Targeting Mode instead of executing immediately + var main = player.get_tree().get_root().get_node_or_null("Main") + if main and main.ui_manager: + main.ui_manager.current_action_state = main.ui_manager.ActionState.TARGETING + main.ui_manager.pending_skill_id = effect + NotificationManager.send_message(player, "Select a target area...", NotificationManager.MessageType.NORMAL) + # Do NOT set cooldown yet. Cooldown sets on execution. + # Revert the cooldown set above (hacky but handles the split flow) + powerup_cooldowns[effect] = 0.0 + emit_signal("cooldown_updated", effect, 0.0, 0.0) + print("[SpecialTiles] Entered Targeting Mode for %s" % SpecialEffect.keys()[effect]) + return # Exit, wait for input SpecialEffect.INVISIBLE_MODE: - _execute_invisible_mode(player) # Or whatever ID 14 is + _execute_invisible_mode(player) # Play generic cast animation or sound? if player.is_multiplayer_authority(): @@ -190,50 +260,32 @@ func _execute_faster_speed(): active_buffs[SpecialEffect.FASTER_SPEED] = FASTER_DURATION NotificationManager.send_message(player, "Speed Boost! (5s)", NotificationManager.MessageType.POWERUP) -func _execute_area_freeze(): - # "area with blue like wall but with far away from the player who use it" - # "Make it like 4 floor first (offset 4) and the continue to bigger when the level... is close to max" - - # 1. Calculate Forward Direction based on Rotation - # Rotation 0 = South (+Z), PI = North (-Z) - var rot = player.rotation.y - var forward_x = round(sin(rot)) - var forward_z = round(cos(rot)) - var forward_vec = Vector2i(forward_x, forward_z) - - # If rotation is diagonal or imprecise, normalize to cardinal - if abs(forward_x) > abs(forward_z): - forward_vec = Vector2i(sign(forward_x), 0) - else: - forward_vec = Vector2i(0, sign(forward_z)) +func _execute_area_freeze(center_pos: Vector2i = Vector2i.ZERO): + if center_pos == Vector2i.ZERO: + # Fallback to old behavior if no target provided (or error) + return - # 2. Offset Center (4 tiles away) - var offset_dist = 4 - var center = player.current_position + (forward_vec * offset_dist) - # 3. Determine Radius based on Level - # Level 1-4: Radius 1 (3x3 area) - # Level 5-8: Radius 2 (5x5 area) var current_lvl = powerup_levels.get(SpecialEffect.AREA_FREEZE, 1) var radius = 1 if current_lvl >= 5: radius = 2 # Bigger area at high levels - print("Player %s executing Area Freeze at %s (Offset %s, Lvl %d, Rad %d)" % [player.name, center, forward_vec, current_lvl, radius]) + print("Player %s executing Area Freeze at %s (Lvl %d, Rad %d)" % [player.name, center_pos, current_lvl, radius]) # Register Zone for persistence active_freeze_zones.append({ - "center": center, + "center": center_pos, "radius": radius, - "timer": FREEZE_SLOW_DURATION # Same duration as the visual + "timer": FREEZE_SLOW_DURATION }) # Initial Check (Instant Feedback) var all_players = player.get_tree().get_nodes_in_group("Players") for p in all_players: # Check distance (Chebyshev distance for square area) - var dx = abs(p.current_position.x - center.x) - var dy = abs(p.current_position.y - center.y) + var dx = abs(p.current_position.x - center_pos.x) + var dy = abs(p.current_position.y - center_pos.y) # If inside square radius if dx <= radius and dy <= radius: @@ -245,7 +297,7 @@ func _execute_area_freeze(): # Sync Icy Floor (Layer 0) for x in range(-radius, radius + 1): for y in range(-radius, radius + 1): - var pos = center + Vector2i(x, y) + var pos = center_pos + Vector2i(x, y) if enhanced_gridmap.is_position_valid(pos): var main = player.get_tree().get_root().get_node_or_null("Main") # Use Item 12 (Blue Freeze Tile) on Layer 0 (Floor) @@ -255,40 +307,58 @@ func _execute_area_freeze(): get_tree().create_timer(FREEZE_SLOW_DURATION).timeout.connect(func(): for x in range(-radius, radius + 1): for y in range(-radius, radius + 1): - var pos = center + Vector2i(x, y) + var pos = center_pos + Vector2i(x, y) if enhanced_gridmap.is_position_valid(pos): var main = player.get_tree().get_root().get_node_or_null("Main") # Restore to Item 0 (Standard Floor) if main: main.rpc("sync_grid_item", pos.x, 0, pos.y, 0) ) -func _execute_block_floor(target: Node3D): - # Existing logic for blocking, reused - # "Wall Block" usually means block WHERE YOU ARE or FRONT? - # Original code blocked target's floor. - # User Request: "choose one between horizontal or vertical all the way to the colus or rows" - # We interpret "Choose one" as random 50/50 since there's no UI for sub-selection. +func _execute_block_floor(target_pos: Vector2i): + # "Wall Block" + # Determine Row or Column based on click? + # Or let's imply orientation (North/South = Row, East/West = Column) relative to Player? + # Or just Row vs Column based on closest axis? + # Let's use simple logic: If click is further along X than Z from player, use Column(X), else Row(Z). - # Check for Immunity (Invisible Mode) - if target.get("is_invisible"): - NotificationManager.send_message(target, "blocked!", NotificationManager.MessageType.POWERUP) - # We should probably notify the attacker too? - return + var diff = target_pos - player.current_position + var is_horizontal = false + if abs(diff.x) > abs(diff.y): + is_horizontal = false # Aligned with X axis roughly? Logic check: + # If I am at (0,0) and click (5,0), diff is (5,0). X is dominant. + # I want to block the vertical column at X=5? Or the horizontal row? + # Original logic "is_horizontal" meant blocking the Row (all X at fixed Z). + # If I click (5,0), I likely want to block that column or row? + # Let's default to blocking the axis perpendicular to where I'm looking/clicking? + pass + + # Actually, simpler: Let's block the line that passes through the target point + # perpendicular to the direction from player to target. + # If I shoot North (change in Y/Z), I want a wall ACROSS (Row/X). + # If I shoot East (change in X), I want a wall ACROSS (Column/Z). + + if abs(diff.x) > abs(diff.y): + # Target is East/West. I want a wall perpendicular -> Vertical (Fixed X, varying Z) + # Wait, "Column" in grid usually means Fixed X. "Row" means Fixed Z. + # So if X diff is big, I am shooting along X. I want a wall AT that X? No, I want a wall BLOCKING that X? + # Let's stick to the visual preview logic we will implement: + # If abs(diff.x) > abs(diff.y) -> Show Column (Vertical strip at target.x) + is_horizontal = false + else: + # Target is North/South. Show Row (Horizontal strip at target.y) + is_horizontal = true - var center = target.current_position - var is_horizontal = rng.randf() < 0.5 var neighbors = [] if is_horizontal: # Block entire Row (Fixed Z, iterate all X) - # Assuming 'center.y' corresponds to Grid Z-row - var row_z = center.y + var row_z = target_pos.y for x in range(enhanced_gridmap.columns): neighbors.append({"position": Vector2i(x, row_z)}) print("Player %s activated Wall Block: HORIZONTAL ROW (Z=%d)" % [player.name, row_z]) else: # Block entire Column (Fixed X, iterate all Z) - var col_x = center.x + var col_x = target_pos.x for z in range(enhanced_gridmap.rows): neighbors.append({"position": Vector2i(col_x, z)}) print("Player %s activated Wall Block: VERTICAL COLUMN (X=%d)" % [player.name, col_x]) @@ -302,15 +372,18 @@ func _execute_block_floor(target: Node3D): if main: main.rpc("sync_grid_item", block_pos.x, block_pos.y, block_pos.z, 4) - # We don't save original item here properly in this loop if we overwrite something important, - # but for Floor 0 it's usually just ground (0) or obstacles. - # If we overwrite another Wall, it's fine. + # Record for restoration blocked_tiles.append({ "position": block_pos, "original_item": 0, "timer": BLOCK_DURATION }) - NotificationManager.send_message(target, "Wall Block Created!", NotificationManager.MessageType.POWERUP) + + # Notify + var all_players = player.get_tree().get_nodes_in_group("Players") + for p in all_players: + if p.current_position == target_pos: + NotificationManager.send_message(p, "Wall Block Created!", NotificationManager.MessageType.POWERUP) func _execute_invisible_mode(target: Node3D): target.is_invisible = true diff --git a/scripts/managers/ui_manager.gd b/scripts/managers/ui_manager.gd index cfadc47..487034b 100644 --- a/scripts/managers/ui_manager.gd +++ b/scripts/managers/ui_manager.gd @@ -35,10 +35,12 @@ enum ActionState { MOVING, GRABBING, ARRANGING, - RANDOMIZING + RANDOMIZING, + TARGETING } var current_action_state = ActionState.NONE +var pending_skill_id: int = -1 func initialize(player_node): # Get PowerUp Inventory UI from scene