diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index ac41f2e..403ed78 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -27,22 +27,22 @@ jobs: - name: Setup Godot (Cached) run: | apt-get update -qq && apt-get install -y -qq unzip curl - if [ ! -f /cache/godot_4.7 ]; then - echo "Downloading Godot 4.7..." - curl -sL -o /tmp/godot.zip "https://github.com/godotengine/godot-builds/releases/download/4.7-stable/Godot_v4.7-stable_linux.x86_64.zip" + if [ ! -f /cache/godot_4.6.3 ]; then + echo "Downloading Godot 4.6.3..." + curl -sL -o /tmp/godot.zip "https://github.com/godotengine/godot-builds/releases/download/4.6.3-stable/Godot_v4.6.3-stable_linux.x86_64.zip" unzip -q -o /tmp/godot.zip -d /cache/ - mv /cache/Godot_v4.7-stable_linux.x86_64 /cache/godot_4.7 + mv /cache/Godot_v4.6.3-stable_linux.x86_64 /cache/godot_4.6.3 fi - cp /cache/godot_4.7 /usr/local/bin/godot + cp /cache/godot_4.6.3 /usr/local/bin/godot chmod +x /usr/local/bin/godot - mkdir -p ~/.local/share/godot/export_templates/4.7.stable - if [ ! -f /cache/Godot_v4.7-stable_export_templates.tpz ]; then + mkdir -p ~/.local/share/godot/export_templates/4.6.3.stable + if [ ! -f /cache/Godot_v4.6.3-stable_export_templates.tpz ]; then echo "Downloading templates..." - curl -sL -o /cache/Godot_v4.7-stable_export_templates.tpz \ - "https://github.com/godotengine/godot-builds/releases/download/4.7-stable/Godot_v4.7-stable_export_templates.tpz" + curl -sL -o /cache/Godot_v4.6.3-stable_export_templates.tpz \ + "https://github.com/godotengine/godot-builds/releases/download/4.6.3-stable/Godot_v4.6.3-stable_export_templates.tpz" fi - cd ~/.local/share/godot/export_templates/4.7.stable - unzip -q -o /cache/Godot_v4.7-stable_export_templates.tpz + cd ~/.local/share/godot/export_templates/4.6.3.stable + unzip -q -o /cache/Godot_v4.6.3-stable_export_templates.tpz mv templates/* . rm -rf templates cd $GITHUB_WORKSPACE diff --git a/.gitea/workflows/deploy_patch.yml b/.gitea/workflows/deploy_patch.yml index 0d43bda..6aadd0f 100644 --- a/.gitea/workflows/deploy_patch.yml +++ b/.gitea/workflows/deploy_patch.yml @@ -28,14 +28,14 @@ jobs: - name: Setup Godot (Cached) run: | - if [ ! -f /cache/godot_4.7 ]; then - echo "Downloading Godot 4.7..." + if [ ! -f /cache/godot_4.6.3 ]; then + echo "Downloading Godot 4.6.3..." apt-get update -qq && apt-get install -y -qq unzip curl - curl -sL -o /tmp/godot.zip "https://github.com/godotengine/godot-builds/releases/download/4.7-stable/Godot_v4.7-stable_linux.x86_64.zip" + curl -sL -o /tmp/godot.zip "https://github.com/godotengine/godot-builds/releases/download/4.6.3-stable/Godot_v4.6.3-stable_linux.x86_64.zip" unzip -q -o /tmp/godot.zip -d /cache/ - mv /cache/Godot_v4.7-stable_linux.x86_64 /cache/godot_4.7 + mv /cache/Godot_v4.6.3-stable_linux.x86_64 /cache/godot_4.6.3 fi - cp /cache/godot_4.7 /usr/local/bin/godot + cp /cache/godot_4.6.3 /usr/local/bin/godot chmod +x /usr/local/bin/godot mkdir -p build diff --git a/project.godot b/project.godot index d5e2610..19b44df 100644 --- a/project.godot +++ b/project.godot @@ -17,7 +17,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true config/name="Tekton Dash Armageddon" config/version="2.4.3" run/main_scene="res://scenes/ui/boot_screen.tscn" -config/features=PackedStringArray("4.7", "Forward Plus") +config/features=PackedStringArray("4.6", "Forward Plus") boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1) boot_splash/stretch_mode=0 boot_splash/image="uid://b10e6kr508642" diff --git a/scenes/main.gd b/scenes/main.gd index 3f0f39a..e4b1c24 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -204,7 +204,8 @@ func _setup_effect_elevation(): print("[Main] MeshLibrary elevation applied: Wall(4) and Freeze(5) at Y=0.8") # Force gridmap cell size to match player logic (1, 0.05, 1) - >0.001 to avoid errors - var em = $EnhancedGridMap + if not em: + em = get_node_or_null("EnhancedGridMap") if em: em.cell_size = Vector3(1, 0.05, 1) diff --git a/scripts/bot_controller.gd b/scripts/bot_controller.gd index 1aea9ff..27f9dd6 100644 --- a/scripts/bot_controller.gd +++ b/scripts/bot_controller.gd @@ -218,17 +218,27 @@ func _run_ai_tick(): # Priority 0.7: Candy Survival — Deliver candies to Mekton NPC if strategic_planner and strategic_planner.is_candy_survival_mode(): var gm = strategic_planner._get_candy_survival_manager() - if gm and gm.is_active: + if gm and gm.active: var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int() var held_color = gm.player_candy_color.get(pid, -1) if held_color != -1 and gm.player_candies.get(pid, 0) > 0: if held_color == gm.current_face: # Face matches! Run to the NPC Center! - var path = strategic_planner.find_path(actor.current_position, Vector2i(8, 8)) - if path.size() > 0: - _execute_move(path[0]) - print("[BotController] Action Taken: Delivering candies to Mekton!") - return + var path = enhanced_gridmap.find_path( + Vector2(actor.current_position), + Vector2(8, 8), + 0, false, false + ) + if path.size() >= 2: + var next_step = Vector2i(path[1].x, path[1].y) + if actor.movement_manager.simple_move_to(next_step): + _is_processing_action = true + _current_action = "delivering_candies" + while is_instance_valid(actor) and actor.is_player_moving: + await get_tree().process_frame + _is_processing_action = false + print("[BotController] Action Taken: Delivering candies to Mekton!") + return # Priority 1: Tekton Management (Grab Tekton if full boost, or spawn if carrying) # Spawning while carrying is high priority; Hunting is medium priority. diff --git a/scripts/managers/candy_survival_manager.gd b/scripts/managers/candy_survival_manager.gd index 5da8886..ba82393 100644 --- a/scripts/managers/candy_survival_manager.gd +++ b/scripts/managers/candy_survival_manager.gd @@ -56,38 +56,34 @@ var hud_delivery_indicator: Label var _candy_survival_hud_scene: PackedScene = preload("res://scenes/candy_survival_hud.tscn") func _ready(): - set_process(false) - _setup_hud() + set_process(false) + _setup_hud() func _setup_hud() -> void: - var hud_instance = _candy_survival_hud_scene.instantiate() - hud_layer = hud_instance - hud_layer.visible = false - add_child(hud_layer) - - var bot_box = hud_layer.get_node_or_null("BottomContainer/HBoxContainer") - if bot_box: - hud_knock_label = bot_box.get_node_or_null("SabotagePanel/KnockChargesLabel") - hud_ghost_label = bot_box.get_node_or_null("SabotagePanel/GhostChargesLabel") - hud_stack_badge = bot_box.get_node_or_null("ScorePanel/CandyStackBadge") - hud_points_label = bot_box.get_node_or_null("ScorePanel/PointsLabel") - hud_delivery_indicator = bot_box.get_node_or_null("ScorePanel/DeliveryIndicator") - - var top_box = hud_layer.get_node_or_null("TopContainer") - if top_box: - hud_rush_bar = top_box.get_node_or_null("SugarRushBar") - if hud_rush_bar: - hud_rush_label = hud_rush_bar.get_node_or_null("SugarRushLabel") - -func start_game_mode() -> void: - if multiplayer.is_server(): - activate_client_side() + var hud_instance = _candy_survival_hud_scene.instantiate() + hud_layer = hud_instance + hud_layer.visible = false + add_child(hud_layer) + + var bot_box = hud_layer.get_node_or_null("BottomContainer/HBoxContainer") + if bot_box: + hud_knock_label = bot_box.get_node_or_null("SabotagePanel/KnockChargesLabel") + hud_ghost_label = bot_box.get_node_or_null("SabotagePanel/GhostChargesLabel") + hud_stack_badge = bot_box.get_node_or_null("ScorePanel/CandyStackBadge") + hud_points_label = bot_box.get_node_or_null("ScorePanel/PointsLabel") + hud_delivery_indicator = bot_box.get_node_or_null("ScorePanel/DeliveryIndicator") + + var top_box = hud_layer.get_node_or_null("TopContainer") + if top_box: + hud_rush_bar = top_box.get_node_or_null("SugarRushBar") + if hud_rush_bar: + hud_rush_label = hud_rush_bar.get_node_or_null("SugarRushLabel") func activate_client_side() -> void: - active = true - if hud_layer: - hud_layer.visible = true - set_process(true) + active = true + if hud_layer: + hud_layer.visible = true + set_process(true) # Per-player state var player_candies: Dictionary = {} # pid -> int (stack count) @@ -110,410 +106,417 @@ const TILE_IDS: Dictionary = {CandyColor.HEART: 7, CandyColor.DIAMOND: 8, CandyC # ── Initialization ── func initialize(main, grid) -> void: - main_scene = main - gridmap = grid - _define_sticky_walls() - print("[CandySurvival] Initialized") + main_scene = main + gridmap = grid + _define_sticky_walls() + print("[CandySurvival] Initialized") func _define_sticky_walls() -> void: - # Arena boundary walls (rows/cols 0 and 17) - for x in range(ARENA_COLS): - sticky_cells[Vector2i(x, 0)] = true - sticky_cells[Vector2i(x, ARENA_ROWS - 1)] = true - for y in range(ARENA_ROWS): - sticky_cells[Vector2i(0, y)] = true - sticky_cells[Vector2i(ARENA_COLS - 1, y)] = true - # Mekton NPC zone (3x3 center) - for dx in range(NPC_CELLS): - for dy in range(NPC_CELLS): - sticky_cells[Vector2i(NPC_CENTER.x + dx, NPC_CENTER.y + dy)] = true + # Arena boundary walls (rows/cols 0 and 17) + for x in range(ARENA_COLS): + sticky_cells[Vector2i(x, 0)] = true + sticky_cells[Vector2i(x, ARENA_ROWS - 1)] = true + for y in range(ARENA_ROWS): + sticky_cells[Vector2i(0, y)] = true + sticky_cells[Vector2i(ARENA_COLS - 1, y)] = true + # Mekton NPC zone (3x3 center) + for dx in range(NPC_CELLS): + for dy in range(NPC_CELLS): + sticky_cells[Vector2i(NPC_CENTER.x + dx, NPC_CENTER.y + dy)] = true func start_game_mode() -> void: - active = true - current_face = CandyColor.HEART - face_timer = 0.0 - game_elapsed = 0.0 - player_candies.clear() - player_candy_color.clear() - player_knocks.clear() - player_ghosts.clear() - player_ghost_active.clear() - player_ghost_timer.clear() - player_score.clear() - player_blueprints.clear() - player_sugar_rush.clear() - player_last_knocked_by.clear() + if multiplayer.is_server(): + activate_client_side() + active = true + current_face = CandyColor.HEART + face_timer = 0.0 + game_elapsed = 0.0 + player_candies.clear() + player_candy_color.clear() + player_knocks.clear() + player_ghosts.clear() + player_ghost_active.clear() + player_ghost_timer.clear() + player_score.clear() + player_blueprints.clear() + player_sugar_rush.clear() + player_last_knocked_by.clear() - # Init players from lobby - var pids = _get_player_ids() - for pid in pids: - player_candies[pid] = 0 - player_candy_color[pid] = -1 - player_knocks[pid] = START_KNOCK - player_ghosts[pid] = START_GHOST - player_ghost_active[pid] = false - player_ghost_timer[pid] = 0.0 - player_score[pid] = 0 - player_blueprints[pid] = 0 - player_sugar_rush[pid] = 0.0 - player_last_knocked_by[pid] = 0 + # Init players from lobby + var pids = _get_player_ids() + for pid in pids: + player_candies[pid] = 0 + player_candy_color[pid] = -1 + player_knocks[pid] = START_KNOCK + player_ghosts[pid] = START_GHOST + player_ghost_active[pid] = false + player_ghost_timer[pid] = 0.0 + player_score[pid] = 0 + player_blueprints[pid] = 0 + player_sugar_rush[pid] = 0.0 + player_last_knocked_by[pid] = 0 - print("[CandySurvival] Started with %d players" % pids.size()) + print("[CandySurvival] Started with %d players" % pids.size()) func _enter_tree() -> void: - set_process(true) + set_process(true) func _process(delta: float) -> void: - if not active: - return - game_elapsed += delta + if not active: + return + game_elapsed += delta - # Mekton face color cycle - face_timer += delta - if face_timer >= COLOR_CYCLE_TIME: - face_timer = 0.0 - current_face = CandyColor.values()[(current_face + 1) % CandyColor.size()] - - # Sync color to the Mekton NPC - if mekton_node and mekton_node.has_method("set_face_color_rpc"): - mekton_node.rpc("set_face_color_rpc", current_face) - - # Update delivery indicators for all players based on new face - for pid in player_candies: - _update_candy_badge(pid) + # Mekton face color cycle + face_timer += delta + if face_timer >= COLOR_CYCLE_TIME: + face_timer = 0.0 + current_face = CandyColor.values()[(current_face + 1) % CandyColor.size()] + + # Sync color to the Mekton NPC + if mekton_node and mekton_node.has_method("set_face_color_rpc"): + mekton_node.rpc("set_face_color_rpc", current_face) + + # Update delivery indicators for all players based on new face + for pid in player_candies: + _update_candy_badge(pid) - # Sugar Rush timers - var any_rush = false - for pid in player_sugar_rush.keys(): - if player_sugar_rush[pid] > 0: - player_sugar_rush[pid] -= delta - if player_sugar_rush[pid] <= 0: - player_sugar_rush[pid] = 0.0 - _restore_time_scale() - sugar_rush_ended.emit(pid) - if player_sugar_rush[pid] > 0: - any_rush = true - if any_rush: - Engine.time_scale = SR_SPEED + # Sugar Rush timers + var any_rush = false + for pid in player_sugar_rush.keys(): + if player_sugar_rush[pid] > 0: + player_sugar_rush[pid] -= delta + if player_sugar_rush[pid] <= 0: + player_sugar_rush[pid] = 0.0 + _restore_time_scale() + sugar_rush_ended.emit(pid) + if player_sugar_rush[pid] > 0: + any_rush = true + if any_rush: + Engine.time_scale = SR_SPEED - # Ghost timers - for pid in player_ghost_active.keys(): - if player_ghost_active[pid]: - player_ghost_timer[pid] -= delta - if player_ghost_timer[pid] <= 0: - player_ghost_active[pid] = false - player_ghost_timer[pid] = 0.0 - rpc("sync_ghost_active", pid, false) - - # Turn off ghost visual/state - var all_players = get_tree().get_nodes_in_group("Players") - for player in all_players: - var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() - if curr_pid == pid: - player.set("is_invisible", false) - if player.has_method("sync_modulate"): - if multiplayer.is_server() and player.has_method("can_rpc") and player.can_rpc(): - player.rpc("sync_modulate", Color.WHITE) - else: - player.sync_modulate(Color.WHITE) - break + # Ghost timers + for pid in player_ghost_active.keys(): + if player_ghost_active[pid]: + player_ghost_timer[pid] -= delta + if player_ghost_timer[pid] <= 0: + player_ghost_active[pid] = false + player_ghost_timer[pid] = 0.0 + rpc("sync_ghost_active", pid, false) + + # Turn off ghost visual/state + var all_players = get_tree().get_nodes_in_group("Players") + for player in all_players: + var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() + if curr_pid == pid: + player.set("is_invisible", false) + if player.has_method("sync_modulate"): + if multiplayer.is_server() and player.has_method("can_rpc") and player.can_rpc(): + player.rpc("sync_modulate", Color.WHITE) + else: + player.sync_modulate(Color.WHITE) + break - # Candy stack: points per second per candy - for pid in player_candies.keys(): - var count = player_candies.get(pid, 0) - if count > 0: - var points = count * CANDY_PPS * delta - _add_score(pid, int(points)) + # Candy stack: points per second per candy + for pid in player_candies.keys(): + var count = player_candies.get(pid, 0) + if count > 0: + var points = count * CANDY_PPS * delta + _add_score(pid, int(points)) # ── Blueprint ── func on_blueprint_completed(pid: int, primary_tile_id: int, off_color: bool) -> void: - var base_points = 1000 - var points = base_points if not off_color else int(base_points * OFF_COLOR_PENALTY) - _add_score(pid, points) - player_blueprints[pid] = player_blueprints.get(pid, 0) + 1 - - var candy_color = CandyColor.HEART - for key in TILE_IDS: - if TILE_IDS[key] == primary_tile_id: - candy_color = key - break - - _give_candy(pid, candy_color) + var base_points = 1000 + var points = base_points if not off_color else int(base_points * OFF_COLOR_PENALTY) + _add_score(pid, points) + player_blueprints[pid] = player_blueprints.get(pid, 0) + 1 + + var candy_color = CandyColor.HEART + for key in TILE_IDS: + if TILE_IDS[key] == primary_tile_id: + candy_color = key + break + + _give_candy(pid, candy_color) func can_finish_with_off_color(pid: int, primary_tile_id: int) -> bool: - return not _grid_has_color_tiles(primary_tile_id) + return not _grid_has_color_tiles(primary_tile_id) func _grid_has_color_tiles(target_tile_id: int) -> bool: - if not gridmap: - return true - for x in range(1, ARENA_COLS - 1): - for y in range(1, ARENA_ROWS - 1): - var v = Vector3i(x, 1, y) - if gridmap.get_cell_item(v) == target_tile_id: - return true - return false + if not gridmap: + return true + for x in range(1, ARENA_COLS - 1): + for y in range(1, ARENA_ROWS - 1): + var v = Vector3i(x, 1, y) + if gridmap.get_cell_item(v) == target_tile_id: + return true + return false # ── Candy Stack ── func _give_candy(pid: int, color: int) -> void: - player_candies[pid] = player_candies.get(pid, 0) + 1 - player_candy_color[pid] = color - _update_candy_badge(pid) + player_candies[pid] = player_candies.get(pid, 0) + 1 + player_candy_color[pid] = color + _update_candy_badge(pid) func _update_candy_badge(pid: int) -> void: - var count = player_candies.get(pid, 0) - var mult = 1.0 + count * MULTI_STEP - var color = player_candy_color.get(pid, -1) - var face_match = (color != -1 and color == current_face) - rpc("sync_candy_badge", pid, count, mult, face_match) - - # Update the 3D meshes on the player's head across all clients - var all_players = get_tree().get_nodes_in_group("Players") - for player in all_players: - var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() - if curr_pid == pid: - if player.has_method("sync_candy_stack"): - player.rpc("sync_candy_stack", count, color) - break + var count = player_candies.get(pid, 0) + var mult = 1.0 + count * MULTI_STEP + var color = player_candy_color.get(pid, -1) + var face_match = (color != -1 and color == current_face) + rpc("sync_candy_badge", pid, count, mult, face_match) + + # Update the 3D meshes on the player's head across all clients + var all_players = get_tree().get_nodes_in_group("Players") + for player in all_players: + var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() + if curr_pid == pid: + if player.has_method("sync_candy_stack"): + player.rpc("sync_candy_stack", count, color) + break func get_multiplier(pid: int) -> float: - var count = player_candies.get(pid, 0) - return 1.0 + count * MULTI_STEP + var count = player_candies.get(pid, 0) + return 1.0 + count * MULTI_STEP # ── Mekton Delivery ── @rpc("any_peer", "call_local", "reliable") func try_deliver(pid: int) -> bool: - if not multiplayer.is_server(): - return false - - var color = player_candy_color.get(pid, -1) - if color == -1 or player_candies.get(pid, 0) == 0: - return false - if color != current_face: - return false + if not multiplayer.is_server(): + return false + + var color = player_candy_color.get(pid, -1) + if color == -1 or player_candies.get(pid, 0) == 0: + return false + if color != current_face: + return false - var count = player_candies[pid] - var mult = get_multiplier(pid) - _add_score(pid, count * 500) + var count = player_candies[pid] + var mult = get_multiplier(pid) + _add_score(pid, count * 500) - player_candies[pid] = 0 - player_candy_color[pid] = -1 - _update_candy_badge(pid) + player_candies[pid] = 0 + player_candy_color[pid] = -1 + _update_candy_badge(pid) - _trigger_sugar_rush(pid, count, mult) - return true + _trigger_sugar_rush(pid, count, mult) + return true func _trigger_sugar_rush(pid: int, candies: int, mult: float) -> void: - var duration = SR_BASE_TIME + candies * SR_PER_CANDY * mult - player_sugar_rush[pid] = duration - Engine.time_scale = SR_SPEED - sugar_rush_started.emit(pid) + var duration = SR_BASE_TIME + candies * SR_PER_CANDY * mult + player_sugar_rush[pid] = duration + Engine.time_scale = SR_SPEED + sugar_rush_started.emit(pid) func _restore_time_scale() -> void: - var any_rush = false - for pid in player_sugar_rush.keys(): - if player_sugar_rush[pid] > 0: - any_rush = true - break - if not any_rush: - Engine.time_scale = 1.0 + var any_rush = false + for pid in player_sugar_rush.keys(): + if player_sugar_rush[pid] > 0: + any_rush = true + break + if not any_rush: + Engine.time_scale = 1.0 # ── Knock / Ghost ── @rpc("any_peer", "call_local", "reliable") func try_knock(attacker: int, target: int) -> bool: - if not multiplayer.is_server(): - return false - if player_ghost_active.get(target, false): - return false - if player_knocks.get(attacker, 0) <= 0: - return false + if not multiplayer.is_server(): + return false + if player_ghost_active.get(target, false): + return false + if player_knocks.get(attacker, 0) <= 0: + return false - var target_candies = player_candies.get(target, 0) + var target_candies = player_candies.get(target, 0) - if target_candies == 0: - # Backfire: both lose a charge - player_knocks[attacker] = player_knocks.get(attacker, 0) - 1 - # Attacker is also knocked down briefly (visual handled by caller) - rpc("sync_knock_result", attacker, target, 0) - return false + if target_candies == 0: + # Backfire: both lose a charge + player_knocks[attacker] = player_knocks.get(attacker, 0) - 1 + # Attacker is also knocked down briefly (visual handled by caller) + rpc("sync_knock_result", attacker, target, 0) + return false - # Steal all candies - player_candies[attacker] = player_candies.get(attacker, 0) + target_candies - player_candies[target] = 0 - player_candy_color[target] = -1 - player_candy_color[attacker] = player_candy_color.get(target, -1) + # Steal all candies + player_candies[attacker] = player_candies.get(attacker, 0) + target_candies + player_candies[target] = 0 + player_candy_color[target] = -1 + player_candy_color[attacker] = player_candy_color.get(target, -1) - player_knocks[attacker] = player_knocks.get(attacker, 0) - 1 - player_last_knocked_by[target] = attacker + player_knocks[attacker] = player_knocks.get(attacker, 0) - 1 + player_last_knocked_by[target] = attacker - _add_score(attacker, target_candies * 100) - _update_candy_badge(attacker) - _update_candy_badge(target) + _add_score(attacker, target_candies * 100) + _update_candy_badge(attacker) + _update_candy_badge(target) - rpc("sync_knock_result", attacker, target, target_candies) - return true + rpc("sync_knock_result", attacker, target, target_candies) + return true func try_activate_ghost(pid: int) -> bool: - if player_ghost_active.get(pid, false): - return false - if player_ghosts.get(pid, 0) <= 0: - return false + if player_ghost_active.get(pid, false): + return false + if player_ghosts.get(pid, 0) <= 0: + return false - player_ghosts[pid] = player_ghosts.get(pid, 0) - 1 - player_ghost_active[pid] = true - player_ghost_timer[pid] = GHOST_DURATION - rpc("sync_ghost_active", pid, true) - - # Core powerup integration: set is_invisible on the player node - var all_players = get_tree().get_nodes_in_group("Players") - for player in all_players: - var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() - if curr_pid == pid: - player.set("is_invisible", true) - if player.has_method("sync_modulate"): - if multiplayer.is_server() and player.has_method("can_rpc") and player.can_rpc(): - player.rpc("sync_modulate", Color(1.0, 1.0, 1.0, 0.4)) - else: - player.sync_modulate(Color(1.0, 1.0, 1.0, 0.4)) - break - - return true + player_ghosts[pid] = player_ghosts.get(pid, 0) - 1 + player_ghost_active[pid] = true + player_ghost_timer[pid] = GHOST_DURATION + rpc("sync_ghost_active", pid, true) + + # Core powerup integration: set is_invisible on the player node + var all_players = get_tree().get_nodes_in_group("Players") + for player in all_players: + var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int() + if curr_pid == pid: + player.set("is_invisible", true) + if player.has_method("sync_modulate"): + if multiplayer.is_server() and player.has_method("can_rpc") and player.can_rpc(): + player.rpc("sync_modulate", Color(1.0, 1.0, 1.0, 0.4)) + else: + player.sync_modulate(Color(1.0, 1.0, 1.0, 0.4)) + break + + return true func is_ghost_active(pid: int) -> bool: - return player_ghost_active.get(pid, false) + return player_ghost_active.get(pid, false) # ── Arena ── func _setup_arena() -> void: - if not gridmap: - return - gridmap.clear_layer(0) - gridmap.clear_layer(1) - gridmap.clear_layer(2) + if not gridmap: + return + gridmap.set("columns", ARENA_COLS) + gridmap.set("rows", ARENA_ROWS) + gridmap.clear() - # Build floor - for x in range(ARENA_COLS): - for y in range(ARENA_ROWS): - gridmap.set_cell_item(Vector3i(x, 0, y), 0) + # Build floor + for x in range(ARENA_COLS): + for y in range(ARENA_ROWS): + gridmap.set_cell_item(Vector3i(x, 0, y), 0) - # Apply sticky walls on layer 2 - for pos in sticky_cells.keys(): - gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), 14) + # Apply sticky walls on layer 2 + for pos in sticky_cells.keys(): + gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), 14) - # Spawn Mekton NPC at center - if mekton_node: - mekton_node.queue_free() - mekton_node = _spawn_mekton_npc(NPC_CENTER) - print("[CandySurvival] Arena setup 18x18") + # Spawn Mekton NPC at center + if mekton_node: + mekton_node.queue_free() + mekton_node = _spawn_mekton_npc(NPC_CENTER) + print("[CandySurvival] Arena setup 18x18") func _spawn_mekton_npc(center: Vector2i) -> Node: - var path = "res://scenes/candy_survival_npc.tscn" - if ResourceLoader.exists(path): - var npc = load(path).instantiate() - npc.position = Vector3(center.x + 1, 0, center.y + 1) - add_child(npc) - if npc.has_method("set_face_color_rpc"): - npc.set_face_color_rpc(current_face) - return npc - return null + var path = "res://scenes/candy_survival_npc.tscn" + if ResourceLoader.exists(path): + var npc = load(path).instantiate() + npc.position = Vector3(center.x + 1, 0, center.y + 1) + add_child(npc) + if npc.has_method("set_face_color_rpc"): + npc.set_face_color_rpc(current_face) + return npc + return null func _apply_arena_setup() -> void: - pass + pass func setup_mission_tiles() -> void: - if not gridmap: - return - randomize() - var tile_ids = [7, 8, 9, 10] - var count = 0 - for x in range(1, ARENA_COLS - 1): - for y in range(1, ARENA_ROWS - 1): - if sticky_cells.has(Vector2i(x, y)): - continue - if randf() < 0.6: - var tile = tile_ids[randi() % tile_ids.size()] - gridmap.set_cell_item(Vector3i(x, 1, y), tile) - count += 1 - print("[CandySurvival] Spawned %d mission tiles" % count) + if not gridmap: + return + randomize() + var tile_ids = [7, 8, 9, 10] + var count = 0 + for x in range(1, ARENA_COLS - 1): + for y in range(1, ARENA_ROWS - 1): + if sticky_cells.has(Vector2i(x, y)): + continue + if randf() < 0.6: + var tile = tile_ids[randi() % tile_ids.size()] + gridmap.set_cell_item(Vector3i(x, 1, y), tile) + count += 1 + print("[CandySurvival] Spawned %d mission tiles" % count) func get_spawn_points(count: int) -> Array: - var points = [] - var corners = [ - Vector2i(2, 2), Vector2i(ARENA_COLS - 2, 2), - Vector2i(2, ARENA_ROWS - 2), Vector2i(ARENA_COLS - 2, ARENA_ROWS - 2) - ] - for i in range(count): - points.append(corners[i % corners.size()]) - return points + var points = [] + var corners = [ + Vector2i(2, 2), Vector2i(ARENA_COLS - 2, 2), + Vector2i(2, ARENA_ROWS - 2), Vector2i(ARENA_COLS - 2, ARENA_ROWS - 2) + ] + for i in range(count): + points.append(corners[i % corners.size()]) + return points func get_arena_positions() -> Dictionary: - return {"columns": ARENA_COLS, "rows": ARENA_ROWS} + return {"columns": ARENA_COLS, "rows": ARENA_ROWS} func get_arena_bounds() -> Dictionary: - return {"min_x": -1.0, "max_x": float(ARENA_COLS), "min_z": -1.0, "max_z": float(ARENA_ROWS)} + return {"min_x": -1.0, "max_x": float(ARENA_COLS), "min_z": -1.0, "max_z": float(ARENA_ROWS)} # ── Sticky (pure wall collision) ── func is_sticky_cell(pos: Vector2i) -> bool: - return sticky_cells.has(pos) + return sticky_cells.has(pos) func _is_npc_zone(pos: Vector2i) -> bool: - var cx = NPC_CENTER.x - var cy = NPC_CENTER.y - return pos.x >= cx and pos.x < cx + NPC_CELLS and pos.y >= cy and pos.y < cy + NPC_CELLS + var cx = NPC_CENTER.x + var cy = NPC_CENTER.y + return pos.x >= cx and pos.x < cx + NPC_CELLS and pos.y >= cy and pos.y < cy + NPC_CELLS # ── Scoring ── func _add_score(pid: int, points: int) -> void: - player_score[pid] = player_score.get(pid, 0) + points - if main_scene and main_scene.has_method("add_points"): - main_scene.add_points(pid, points) + player_score[pid] = player_score.get(pid, 0) + points + if main_scene and main_scene.has_method("add_points"): + main_scene.add_points(pid, points) # ── Helpers ── func _get_player_ids() -> Array: - if main_scene and main_scene.has_method("get_player_ids"): - return main_scene.get_player_ids() - return [] + if main_scene and main_scene.has_method("get_player_ids"): + return main_scene.get_player_ids() + return [] func candy_survival_round_duration() -> int: - return 180 + return 180 func get_score(pid: int) -> int: - return player_score.get(pid, 0) + return player_score.get(pid, 0) # ── RPCs (stubs — HUD client handles visuals) ── @rpc("authority", "call_remote", "unreliable") func sync_candy_badge(pid: int, count: int, mult: float, face_match: bool) -> void: - if pid == multiplayer.get_unique_id(): - if hud_stack_badge: - hud_stack_badge.text = "Stack: %d (x%.1f)" % [count, mult] - if hud_delivery_indicator: - if count > 0: - if face_match: - hud_delivery_indicator.text = "READY TO DELIVER!" - hud_delivery_indicator.add_theme_color_override("font_color", Color(0.4, 1.0, 0.4)) - else: - hud_delivery_indicator.text = "Waiting for match..." - hud_delivery_indicator.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4)) - else: - hud_delivery_indicator.text = "" + if not active: + activate_client_side() + if pid == multiplayer.get_unique_id(): + if hud_stack_badge: + hud_stack_badge.text = "Stack: %d (x%.1f)" % [count, mult] + if hud_delivery_indicator: + if count > 0: + if face_match: + hud_delivery_indicator.text = "READY TO DELIVER!" + hud_delivery_indicator.add_theme_color_override("font_color", Color(0.4, 1.0, 0.4)) + else: + hud_delivery_indicator.text = "Waiting for match..." + hud_delivery_indicator.add_theme_color_override("font_color", Color(1.0, 0.4, 0.4)) + else: + hud_delivery_indicator.text = "" @rpc("authority", "call_remote", "unreliable") func sync_knock_result(attacker: int, target: int, candies: int) -> void: - var local = multiplayer.get_unique_id() - if attacker == local or target == local: - if hud_knock_label and player_knocks.has(local): - hud_knock_label.text = "🥊 Knocks: %d" % player_knocks[local] + if not active: + activate_client_side() + var local = multiplayer.get_unique_id() + if attacker == local or target == local: + if hud_knock_label and player_knocks.has(local): + hud_knock_label.text = "🥊 Knocks: %d" % player_knocks[local] @rpc("authority", "call_remote", "unreliable") func sync_ghost_active(pid: int, active_on: bool) -> void: - if pid == multiplayer.get_unique_id(): - if hud_ghost_label and player_ghosts.has(pid): - hud_ghost_label.text = "👻 Ghosts: %d" % player_ghosts[pid] - + if not active: + activate_client_side() + if pid == multiplayer.get_unique_id(): + if hud_ghost_label and player_ghosts.has(pid): + hud_ghost_label.text = "👻 Ghosts: %d" % player_ghosts[pid] diff --git a/scripts/managers/player_movement_manager.gd b/scripts/managers/player_movement_manager.gd index bd654dc..910b3de 100644 --- a/scripts/managers/player_movement_manager.gd +++ b/scripts/managers/player_movement_manager.gd @@ -122,7 +122,7 @@ func simple_move_to(grid_position: Vector2i) -> bool: return false # Candy Survival explicit wall overrides (since we visually removed the wall blocks) - if gm and gm.is_active: + if gm and gm.active: if gm._is_npc_zone(grid_position): print("[Move] Failed: Blocked by Candy Survival NPC center at %s" % grid_position) return false diff --git a/scripts/managers/special_tiles_manager.gd b/scripts/managers/special_tiles_manager.gd index 26da95b..76c6364 100644 --- a/scripts/managers/special_tiles_manager.gd +++ b/scripts/managers/special_tiles_manager.gd @@ -558,7 +558,7 @@ func spawn_powerups_around(center: Vector2i, force_powerups: bool = true, only_c item_id = rng.randi_range(7, 10) else: # 20% Chance for PowerUp - item_id = rng.randi_range(11, 14) + item_id = rng.randi_range(11, 14) var cell = Vector3i(pos.x, 1, pos.y)