From 5ec559c4c90293cdc484b1ed839860e02ab097ba Mon Sep 17 00:00:00 2001 From: adtpdn Date: Thu, 12 Feb 2026 06:53:26 +0800 Subject: [PATCH] update --- project.godot | 10 ++ scenes/player.gd | 151 ++++++++++++++++++---- scripts/managers/player_input_manager.gd | 12 +- scripts/managers/special_tiles_manager.gd | 18 ++- scripts/tekton.gd | 147 +++++++++++++-------- 5 files changed, 254 insertions(+), 84 deletions(-) diff --git a/project.godot b/project.godot index fcb184d..8526ac2 100644 --- a/project.godot +++ b/project.godot @@ -120,3 +120,13 @@ use_powerup={ "events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null) ] } +action_grab_tekton={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":71,"physical_keycode":0,"key_label":0,"unicode":103,"location":0,"echo":false,"script":null) +] +} +action_knock_tekton={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":66,"physical_keycode":0,"key_label":0,"unicode":98,"location":0,"echo":false,"script":null) +] +} diff --git a/scenes/player.gd b/scenes/player.gd index 87f110b..b50402a 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -33,6 +33,15 @@ var is_frozen: bool = false var is_invisible: bool = false var original_movement_range: int = 1 +# Tekton Interaction +var carried_tekton: Node3D = null +var is_carrying_tekton: bool: + get: return is_carried_tekton + set(value): + is_carried_tekton = value + # Visual/Logic side effects if any + +var is_carried_tekton: bool = false var is_attack_mode: bool = false: set(value): if is_attack_mode == value: @@ -41,9 +50,9 @@ var is_attack_mode: bool = false: is_attack_mode = value # Visual feedback for attack mode (Red Tint) if is_attack_mode: - _apply_tint_recursive(self, Color(1.0, 0.5, 0.5)) + _apply_tint_recursive(self , Color(1.0, 0.5, 0.5)) else: - _apply_tint_recursive(self, Color.WHITE) + _apply_tint_recursive(self , Color.WHITE) # Sync to others if we are the authority if is_multiplayer_authority(): @@ -148,7 +157,7 @@ const AVAILABLE_CHARACTERS: Array[String] = ["Bob", "Masbro", "Gatot", "Oldpop"] set(value): is_my_turn = value if is_my_turn and is_multiplayer_authority(): - NotificationManager.send_message(self, NotificationManager.MESSAGES.TURN_START, NotificationManager.MessageType.NORMAL) + NotificationManager.send_message(self , NotificationManager.MESSAGES.TURN_START, NotificationManager.MessageType.NORMAL) @export var has_moved_this_turn = false @@ -287,39 +296,39 @@ func _init_managers(): movement_manager = load("res://scripts/managers/player_movement_manager.gd").new() movement_manager.name = "MovementManager" add_child(movement_manager) - movement_manager.initialize(self, enhanced_gridmap) + movement_manager.initialize(self , enhanced_gridmap) race_manager = load("res://scripts/managers/player_race_manager.gd").new() race_manager.name = "RaceManager" add_child(race_manager) - race_manager.initialize(self, enhanced_gridmap) + race_manager.initialize(self , enhanced_gridmap) # Skip InputManager for bots if not (is_bot or is_in_group("Bots")): input_manager = load("res://scripts/managers/player_input_manager.gd").new() input_manager.name = "InputManager" add_child(input_manager) - input_manager.initialize(self, movement_manager, race_manager) + input_manager.initialize(self , movement_manager, race_manager) playerboard_manager = load("res://scripts/managers/playerboard_manager.gd").new() playerboard_manager.name = "PlayerboardManager" add_child(playerboard_manager) - playerboard_manager.initialize(self, enhanced_gridmap) + playerboard_manager.initialize(self , enhanced_gridmap) action_manager = load("res://scripts/managers/player_action_manager.gd").new() action_manager.name = "ActionManager" add_child(action_manager) - action_manager.initialize(self, enhanced_gridmap) + action_manager.initialize(self , enhanced_gridmap) special_tiles_manager = load("res://scripts/managers/special_tiles_manager.gd").new() special_tiles_manager.name = "SpecialTilesManager" add_child(special_tiles_manager) - special_tiles_manager.initialize(self, enhanced_gridmap) + special_tiles_manager.initialize(self , enhanced_gridmap) powerup_manager = load("res://scripts/managers/powerup_manager.gd").new() powerup_manager.name = "PowerUpManager" add_child(powerup_manager) - powerup_manager.initialize(self, enhanced_gridmap) + powerup_manager.initialize(self , enhanced_gridmap) # ============================================================================= # Character Selection @@ -689,7 +698,7 @@ func apply_stagger(duration: float = 1.5): return # Already staggered is_frozen = true - _apply_tint_recursive(self, Color.BLUE) # Visual feedback + _apply_tint_recursive(self , Color.BLUE) # Visual feedback # Set immunity (3 seconds as requested) immunity_timer = 3.0 @@ -697,7 +706,7 @@ func apply_stagger(duration: float = 1.5): print("Player %s staggered for %.1f seconds" % [name, duration]) if is_multiplayer_authority(): - NotificationManager.send_message(self, NotificationManager.MESSAGES.CRUSHED, NotificationManager.MessageType.WARNING) + NotificationManager.send_message(self , NotificationManager.MESSAGES.CRUSHED, NotificationManager.MessageType.WARNING) drop_random_item() # Grant "Smashed" Bonus (1 bar, max 2) @@ -709,20 +718,20 @@ func apply_stagger(duration: float = 1.5): is_frozen = false # If still immune, show immunity tint (Green?), otherwise White if immunity_timer > 0: - _apply_tint_recursive(self, Color(0.5, 1.0, 0.5)) # Light Green for immunity + _apply_tint_recursive(self , Color(0.5, 1.0, 0.5)) # Light Green for immunity else: - _apply_tint_recursive(self, Color.WHITE) # Remove tint + _apply_tint_recursive(self , Color.WHITE) # Remove tint @rpc("any_peer", "call_local") func apply_slow_effect(duration: float = 3.0): # "area with blue like wall... Player who cross on that area will got slowed and freeze effect" # Visual: Blue Tint - _apply_tint_recursive(self, Color(0.6, 0.8, 1.0)) # Icy Blue + _apply_tint_recursive(self , Color(0.6, 0.8, 1.0)) # Icy Blue # Logic: Slow Movement speed if movement_manager: # Use 0.2 multipliers to match "slowed" request (20% speed) - movement_manager.set_speed_multiplier(0.2) + movement_manager.set_speed_multiplier(0.2) print("Player %s is slowed for %.1f seconds" % [name, duration]) @@ -740,9 +749,9 @@ func apply_slow_effect(duration: float = 3.0): movement_manager.set_speed_multiplier(1.0) if immunity_timer > 0: - _apply_tint_recursive(self, Color(0.5, 1.0, 0.5)) + _apply_tint_recursive(self , Color(0.5, 1.0, 0.5)) else: - _apply_tint_recursive(self, Color.WHITE) + _apply_tint_recursive(self , Color.WHITE) func playerboard_is_empty() -> bool: for item in playerboard: @@ -779,7 +788,7 @@ func drop_random_item(): var cell = Vector3i(drop_pos.x, 0, drop_pos.y) rpc("sync_grid_item", cell.x, cell.y, cell.z, item_id) - NotificationManager.send_message(self, NotificationManager.MESSAGES.DROPPED_ITEM, NotificationManager.MessageType.WARNING) + NotificationManager.send_message(self , NotificationManager.MESSAGES.DROPPED_ITEM, NotificationManager.MessageType.WARNING) print("Player %s dropped item %d at %s" % [name, item_id, drop_pos]) @@ -805,7 +814,7 @@ func drop_all_tiles(): if dropped_count > 0: rpc("sync_playerboard", playerboard) rpc("trigger_screen_shake", "targeted") - NotificationManager.send_message(self, NotificationManager.MESSAGES.CRITICALLY_HIT, NotificationManager.MessageType.WARNING) + NotificationManager.send_message(self , NotificationManager.MESSAGES.CRITICALLY_HIT, NotificationManager.MessageType.WARNING) print("Player %s dropped %d tiles due to Super Push" % [name, dropped_count]) func _find_valid_drop_position() -> Vector2i: @@ -865,7 +874,7 @@ func attempt_target_action(target_index: int): if target_player == self and effect != 4: # 4 = INVISIBLE (Self) # Trying to target self with harmful effect? - NotificationManager.send_message(self, NotificationManager.MESSAGES.CANT_TARGET_SELF, NotificationManager.MessageType.WARNING) + NotificationManager.send_message(self , NotificationManager.MESSAGES.CANT_TARGET_SELF, NotificationManager.MessageType.WARNING) return # 3. Activate Effect @@ -941,7 +950,7 @@ func _process(delta): immunity_timer -= delta if immunity_timer <= 0: immunity_timer = 0 - _apply_tint_recursive(self, Color.WHITE) # Remove immunity tint + _apply_tint_recursive(self , Color.WHITE) # Remove immunity tint @rpc("any_peer", "call_local") func ping_existence(): @@ -1179,13 +1188,16 @@ func start_movement_along_path(path: Array, clear_visual: bool = true): for point in path: # Use global_position for consistency - tween.tween_property(self, "global_position", grid_to_world(Vector2i(point.x, point.y)), step_duration) + tween.tween_property(self , "global_position", grid_to_world(Vector2i(point.x, point.y)), step_duration) tween.tween_callback(func(): current_position = Vector2i(path[-1].x, path[-1].y) is_player_moving = false target_position = Vector2i(-1, -1) + if is_carrying_tekton and is_instance_valid(carried_tekton): + carried_tekton.current_position = current_position + # FORCE SNAP: Update target visual position to the perfect grid center # This ensures that when interpolation resumes (in _process), it pulls to the correct spot target_visual_position = grid_to_world(current_position) @@ -1797,3 +1809,96 @@ func set_spawn_position(pos: Vector2i): func complete_race(final_position: int): if race_manager: race_manager.on_race_completed(final_position) + +# ============================================================================= +# Tekton Interaction Logic +# ============================================================================= + +func grab_tekton(): + if not is_multiplayer_authority() or is_carrying_tekton or is_frozen: + return + + # Find nearby Tekton + var tekton = _find_nearby_tekton() + if tekton: + if is_multiplayer_authority(): + rpc("sync_grab_tekton", tekton.get_path()) + +@rpc("any_peer", "call_local", "reliable") +func sync_grab_tekton(tekton_path: NodePath): + var tekton = get_node_or_null(tekton_path) + if tekton: + carried_tekton = tekton + is_carrying_tekton = true + tekton.set_carried(true, self ) + print("[Player %s] Grabbed Tekton %s" % [name, tekton.name]) + +func throw_tekton(): + if not is_multiplayer_authority() or not is_carrying_tekton: + return + + # Determine throw direction (where player is facing) + # For simplicity, we use the player's current rotation to find the target tile + var forward = - global_transform.basis.z.normalized() + var throw_dir = Vector2i(round(forward.x), round(forward.z)) + if throw_dir == Vector2i.ZERO: throw_dir = Vector2i(1, 0) # Fallback + + var target_pos = current_position + throw_dir + + if is_multiplayer_authority(): + rpc("sync_throw_tekton", target_pos) + +@rpc("any_peer", "call_local", "reliable") +func sync_throw_tekton(target_pos: Vector2i): + if carried_tekton: + var tekton = carried_tekton + carried_tekton = null + is_carrying_tekton = false + tekton.set_carried(false) + + # Move Tekton to target pos + tekton.current_position = target_pos + + # Intensity 0.5 for throw (drops 50% tiles) + tekton.on_hit(self , 0.5) + + print("[Player %s] Threw Tekton to %s" % [name, target_pos]) + +func knock_tekton(): + if not is_multiplayer_authority() or is_frozen: + return + + # Requirement: Full Powerup Bar + if not powerup_manager or not powerup_manager.can_use_special(): + NotificationManager.send_message(self , "Need Full Boost to Knock!", NotificationManager.MessageType.WARNING) + return + + var tekton = _find_nearby_tekton() + if tekton: + # Consume Boost + powerup_manager.consume_boost(100.0) + + if is_multiplayer_authority(): + rpc("sync_knock_tekton", tekton.get_path()) + +@rpc("any_peer", "call_local", "reliable") +func sync_knock_tekton(tekton_path: NodePath): + var tekton = get_node_or_null(tekton_path) + if tekton: + # Intensity 2.0 for knock (drops 200% tiles) + tekton.on_hit(self , 2.0) + print("[Player %s] Knocked Tekton %s" % [name, tekton.name]) + + # Visual feedback (Juice) + if is_multiplayer_authority(): + rpc("trigger_screen_shake", "heavy") + +func _find_nearby_tekton() -> Node3D: + var tektons = get_tree().get_nodes_in_group("Tektons") + for tekton in tektons: + if tekton.is_carried: continue + + var dist = (tekton.current_position - current_position).length() + if dist <= 1.5: # Adjacent (1.0 or 1.41) + return tekton + return null diff --git a/scripts/managers/player_input_manager.gd b/scripts/managers/player_input_manager.gd index a431220..d691f70 100644 --- a/scripts/managers/player_input_manager.gd +++ b/scripts/managers/player_input_manager.gd @@ -47,7 +47,6 @@ func _process(delta): movement_manager.simple_move_to(target_position) - # 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: @@ -69,7 +68,7 @@ func _process(delta): # Choose highlight color/mesh based on skill # User Request: Use default hover item (1) - var highlight_id = 1 + var highlight_id = 1 player.highlight_cells_if_authorized(area, highlight_id) @@ -104,7 +103,7 @@ func handle_unhandled_input(event): if player.powerup_manager: # Attack Mode (formerly Special) player.powerup_manager.use_special_effect() - KEY_E: + KEY_E: if player.powerup_manager: # Spawn Boost if player.powerup_manager.has_method("spawn_boost_reward"): @@ -112,6 +111,13 @@ func handle_unhandled_input(event): else: # Fallback if method missing player.powerup_manager.use_special_effect() + KEY_G: + if player.is_carrying_tekton: + player.throw_tekton() + else: + player.grab_tekton() + KEY_B: + player.knock_tekton() # Handle spawn point selection if not yet selected diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index ac38b30..e535d25 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -7,15 +7,15 @@ const HOLO_TILES = [11, 12, 13, 14] enum SpecialEffect { FASTER_SPEED, # ID 11 - AREA_FREEZE, # ID 12 - BLOCK_FLOOR, # ID 13 + AREA_FREEZE, # ID 12 + BLOCK_FLOOR, # ID 13 INVISIBLE_MODE # ID 14 } # Levels & Cooldowns -var powerup_levels: Dictionary = {} # EffectEnum -> int (1 to 8) +var powerup_levels: Dictionary = {} # EffectEnum -> int (1 to 8) var powerup_cooldowns: Dictionary = {} # EffectEnum -> float (Time Remaining) -var active_buffs: Dictionary = {} # EffectEnum -> float (Duration Running) +var active_buffs: Dictionary = {} # EffectEnum -> float (Duration Running) # Cooldown Constants (Level 1 / Level 8) const COOLDOWN_L1 = 15.0 @@ -203,6 +203,11 @@ func activate_effect(effect: int, target_player: Node3D = null): if powerup_cooldowns.get(effect, 0.0) > 0: print("PowerUp %s on cooldown." % SpecialEffect.keys()[effect]) return + + # Check Carrying Restriction + if player.get("is_carrying_tekton") and effect != SpecialEffect.FASTER_SPEED: + NotificationManager.send_message(player, "Cannot use this power while carrying a Tekton!", NotificationManager.MessageType.WARNING) + return # Calculate Cooldown based on Level var level = powerup_levels.get(effect, 1) @@ -241,7 +246,7 @@ func activate_effect(effect: int, target_player: Node3D = null): NotificationManager.send_message(player, msg, 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 + 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 @@ -406,7 +411,6 @@ func _execute_invisible_mode(target: Node3D): NotificationManager.send_message(target, "Invisible Mode!", NotificationManager.MessageType.POWERUP) - # ============================================================================= # Helper: Spawn Powerups (For Super Push) # ============================================================================= @@ -460,7 +464,7 @@ func _update_freeze_zones(delta: float): if dx <= zone.radius and dy <= zone.radius: # Apply slow effect repeatedly # We use a short duration so it expires quickly if they leave - p.rpc("apply_slow_effect", 0.5) + p.rpc("apply_slow_effect", 0.5) if zone.timer <= 0: zones_to_remove.append(i) diff --git a/scripts/tekton.gd b/scripts/tekton.gd index 19cf038..1c83ac9 100644 --- a/scripts/tekton.gd +++ b/scripts/tekton.gd @@ -11,48 +11,60 @@ signal movement_finished var enhanced_gridmap: Node var is_moving: bool = false +var is_carried: bool = false +var carrier: Node3D = null var tween: Tween +const SIDE_OFFSET = 0.35 # Distance from center + func initialize(start_pos: Vector2i, p_gridmap: Node): current_position = start_pos enhanced_gridmap = p_gridmap - # Snap to grid visual (Center on tile) - # User wanted Z=1.0. X should ideally be 0.5 (center). - # Ensuring consistent offset across Init, Sync, and Move. - position = Vector3(current_position.x + 0.5, 1.0, current_position.y + 1.0) - - # NO RPC HERE - Spawn RPC handles initial position sync + # Grounded and Side-positioned + # We use a consistent side offset (e.g. North-West corner of the tile) + update_visual_position() @rpc("any_peer", "call_local", "reliable") func sync_position(pos: Vector2i): current_position = pos - position = Vector3(current_position.x + 0.5, 1.0, current_position.y + 1.0) + update_visual_position() + +func update_visual_position(): + if is_carried: return + + # Align with floor height (matching player's typical grounded height) + var floor_y = 0.05 + if enhanced_gridmap and "cell_size" in enhanced_gridmap: + floor_y = enhanced_gridmap.cell_size.y + + # Side offset: place it near the edge + # Using NW corner (+0.2, +0.2) instead of center (+0.5, +0.5) + position = Vector3(current_position.x + 0.2, floor_y, current_position.y + 0.2) func move_to(target_pos: Vector2i): - if is_moving: return + if is_moving or is_carried: return # Validate if not enhanced_gridmap.is_position_valid(target_pos): return - # Check simple collision (optional, can be expanded) - # For now, Tekton walks through things or we check elsewhere? - # Controller should check validity. - is_moving = true - var world_pos = Vector3(target_pos.x + 0.5, 1.0, target_pos.y + 1.0) + + var floor_y = 0.05 + if enhanced_gridmap and "cell_size" in enhanced_gridmap: + floor_y = enhanced_gridmap.cell_size.y + + var target_world_pos = Vector3(target_pos.x + 0.2, floor_y, target_pos.y + 0.2) # Rotation - var dir = world_pos - position - if dir.length_squared() > 0.1: + var dir = target_world_pos - position + if dir.length_squared() > 0.01: var target_rot = atan2(dir.x, dir.z) rotation.y = target_rot - # Tween Movement (Match Z+0.5 offset for symmetry during movement as requested) - var final_world_pos = Vector3(target_pos.x + 0.5, 1.0, target_pos.y + 0.5) tween = create_tween() - tween.tween_property(self, "position", final_world_pos, movement_speed).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) + tween.tween_property(self , "position", target_world_pos, movement_speed).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT) tween.tween_callback(func(): current_position = target_pos is_moving = false @@ -69,16 +81,43 @@ func sync_movement(target_pos: Vector2i): # --- COMBAT / INTERACTION --- -func on_hit(attacker: Node = null): - """Called when hit by a player attack.""" - print("[Tekton] Hit by %s!" % (attacker.name if attacker else "Unknown")) +func on_hit(attacker: Node = null, intensity: float = 1.0): + """Called when hit by a player attack or knock. + Intensity: 0.5 for throw, 1.0+ for knock.""" + print("[Tekton] Hit by %s! Intensity: %.1f" % [attacker.name if attacker else "Unknown", intensity]) # Visual Reaction (Flash red) _flash_damage() # Spawn Tiles if is_multiplayer_authority(): - spawn_tiles_around() + var tile_count = int(8 * intensity) # Base 8 tiles for 1.0 intensity + spawn_tiles_around(tile_count) + +@rpc("any_peer", "call_local") +func set_carried(state: bool, p_carrier: Node3D = null): + is_carried = state + carrier = p_carrier + is_moving = false + if tween: tween.kill() + + # Disable/Enable controller + var controller = get_node_or_null("TektonController") + if controller: + controller.set_physics_process(not state) + if state: + controller.get_node("Timer").stop() + else: + controller.call("_start_timer") + + if not state: + update_visual_position() + +func _process(delta): + if is_carried and is_instance_valid(carrier): + # Carry on head: offset Y by approx carrier height (e.g. 1.25) + global_position = carrier.global_position + Vector3(0, 1.5, 0) + rotation = carrier.rotation func _flash_damage(): var meshes = find_children("*", "MeshInstance3D", true) @@ -90,7 +129,7 @@ func _flash_damage(): t.tween_property(mesh, "scale", Vector3(1.2, 1.2, 1.2), 0.1) t.tween_property(mesh, "scale", Vector3(1.0, 1.0, 1.0), 0.1) -func spawn_tiles_around(): +func spawn_tiles_around(count: int = 4): """Spawns a mix of normal and special tiles in a radius.""" if not enhanced_gridmap: return @@ -98,35 +137,41 @@ func spawn_tiles_around(): var rng = RandomNumberGenerator.new() rng.randomize() - print("[Tekton] Spawning tiles around %s" % current_position) + print("[Tekton] Spawning %d tiles around %s" % [count, current_position]) - for x in range(-radius, radius + 1): - for y in range(-radius, radius + 1): - var pos = current_position + Vector2i(x, y) + var spawned = 0 + var attempts = 0 + while spawned < count and attempts < 25: + attempts += 1 + var x = rng.randi_range(-radius, radius) + var y = rng.randi_range(-radius, radius) + + var pos = current_position + Vector2i(x, y) + + # Don't overwrite the Tekton's own cell? Or do? + # Maybe avoid center. + if x == 0 and y == 0: continue + + if enhanced_gridmap.is_position_valid(pos): + # 50% chance to spawn something + if rng.randf() > 0.5: continue - # Don't overwrite the Tekton's own cell? Or do? - # Maybe avoid center. - if x == 0 and y == 0: continue + # Determine Type + var item_id: int + var roll = rng.randf() - if enhanced_gridmap.is_position_valid(pos): - # 50% chance to spawn something - if rng.randf() > 0.5: continue + if roll < 0.6: + # 60% Normal Tile (7-10) + item_id = rng.randi_range(7, 10) + elif roll < 0.9: + # 30% PowerUp (11-14) + item_id = rng.randi_range(11, 14) + else: + # 10% Obstacle/Trap (optional) + item_id = -1 # Clear? - # Determine Type - var item_id: int - var roll = rng.randf() - - if roll < 0.6: - # 60% Normal Tile (7-10) - item_id = rng.randi_range(7, 10) - elif roll < 0.9: - # 30% PowerUp (11-14) - item_id = rng.randi_range(11, 14) - else: - # 10% Obstacle/Trap (optional) - item_id = -1 # Clear? - - if item_id != -1: - var main = get_tree().get_root().get_node_or_null("Main") - if main: - main.rpc("sync_grid_item", pos.x, 1, pos.y, item_id) + if item_id != -1: + var main = get_tree().get_root().get_node_or_null("Main") + if main: + main.rpc("sync_grid_item", pos.x, 1, pos.y, item_id) + spawned += 1