fix Candy Survival bugs: self-contained Mekton spawn, dead code, clean sticky definition

This commit is contained in:
god
2026-07-06 01:39:23 +08:00
parent 528e22875d
commit 357d84e2ce
+68 -79
View File
@@ -2,10 +2,6 @@ extends Node
class_name CandySurvivalManager class_name CandySurvivalManager
# Candy Survival — Sugar Rush Chaos # Candy Survival — Sugar Rush Chaos
# Speed Building Sabotage (blueprints + power-ups) # Speed Building Sabotage (blueprints + power-ups)
# ponytail: This replaces the old gauntlet ground-growth/sticky/smack system entirely.
# Legacy smack/cleanser/sticky_growth methods deleted — all movement blocking
# is now pure-collision sticky walls. Add back growth if boss requests a
# "rising sticky tide" mechanic later.
signal sugar_rush_started(pid) signal sugar_rush_started(pid)
signal sugar_rush_ended(pid) signal sugar_rush_ended(pid)
@@ -41,8 +37,6 @@ enum CandyColor { HEART, DIAMOND, STAR, COIN }
# ── State ── # ── State ──
var active: bool = false var active: bool = false
var arena_width: int = ARENA_COLS
var arena_height: int = ARENA_ROWS
var main_scene: Node var main_scene: Node
var gridmap: Node var gridmap: Node
var mekton_node: Node var mekton_node: Node
@@ -59,14 +53,16 @@ var player_ghost_active: Dictionary = {} # pid -> bool
var player_ghost_timer: Dictionary = {} # pid -> float var player_ghost_timer: Dictionary = {} # pid -> float
var player_score: Dictionary = {} # pid -> int var player_score: Dictionary = {} # pid -> int
var player_blueprints: Dictionary = {} # pid -> int (completed count) var player_blueprints: Dictionary = {} # pid -> int (completed count)
var player_position: Dictionary = {} # pid -> Vector2i var player_sugar_rush: Dictionary = {} # pid -> float (remaining)
var player_sugar_rush: Dictionary = {} # pid -> float (remaining, 0=none)
var player_last_knocked_by: Dictionary = {} # pid -> int var player_last_knocked_by: Dictionary = {} # pid -> int
# Sticky wall cells (pre-placed collision) # Sticky wall cells (pre-placed collision only)
var sticky_cells: Dictionary = {} # Vector2i -> bool var sticky_cells: Dictionary = {} # Vector2i -> bool
# Pre-placed sticky positions for 18x18 arena
var preplaced_sticky: Array = [] # Tile ID mapping
const TILE_IDS: Dictionary = {CandyColor.HEART: 7, CandyColor.DIAMOND: 8, CandyColor.STAR: 9, CandyColor.COIN: 10}
# ── Initialization ──
func initialize(main, grid) -> void: func initialize(main, grid) -> void:
main_scene = main main_scene = main
@@ -75,23 +71,17 @@ func initialize(main, grid) -> void:
print("[CandySurvival] Initialized") print("[CandySurvival] Initialized")
func _define_sticky_walls() -> void: func _define_sticky_walls() -> void:
# Block Mekton NPC zone (3x3 center) # Arena boundary walls (rows/cols 0 and 17)
var cx = NPC_CENTER.x for x in range(ARENA_COLS):
var cy = NPC_CENTER.y 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 dx in range(NPC_CELLS):
for dy in range(NPC_CELLS): for dy in range(NPC_CELLS):
preplaced_sticky.append(Vector2i(cx + dx, cy + dy)) sticky_cells[Vector2i(NPC_CENTER.x + dx, NPC_CENTER.y + dy)] = true
# Block arena outer boundary walls
for x in range(ARENA_COLS):
preplaced_sticky.append(Vector2i(x, 0))
preplaced_sticky.append(Vector2i(x, ARENA_ROWS - 1))
for y in range(ARENA_ROWS):
preplaced_sticky.append(Vector2i(0, y))
preplaced_sticky.append(Vector2i(ARENA_COLS - 1, y))
# Lobby-zone rows (rows 0-1 and rows 16-17 outside play field)
for x in range(2, ARENA_COLS - 2):
for y in [1, ARENA_ROWS - 2]:
preplaced_sticky.append(Vector2i(x, y))
func start_game_mode() -> void: func start_game_mode() -> void:
active = true active = true
@@ -106,14 +96,8 @@ func start_game_mode() -> void:
player_ghost_timer.clear() player_ghost_timer.clear()
player_score.clear() player_score.clear()
player_blueprints.clear() player_blueprints.clear()
player_position.clear()
player_sugar_rush.clear() player_sugar_rush.clear()
player_last_knocked_by.clear() player_last_knocked_by.clear()
sticky_cells.clear()
# Mark pre-placed sticky as active
for pos in preplaced_sticky:
sticky_cells[pos] = true
# Init players from lobby # Init players from lobby
var pids = _get_player_ids() var pids = _get_player_ids()
@@ -131,6 +115,9 @@ func start_game_mode() -> void:
print("[CandySurvival] Started with %d players" % pids.size()) print("[CandySurvival] Started with %d players" % pids.size())
func _enter_tree() -> void:
set_process(true)
func _process(delta: float) -> void: func _process(delta: float) -> void:
if not active: if not active:
return return
@@ -141,9 +128,9 @@ func _process(delta: float) -> void:
if face_timer >= COLOR_CYCLE_TIME: if face_timer >= COLOR_CYCLE_TIME:
face_timer = 0.0 face_timer = 0.0
current_face = CandyColor.values()[(current_face + 1) % CandyColor.size()] current_face = CandyColor.values()[(current_face + 1) % CandyColor.size()]
mekton_face_changed.emit(current_face)
# Sugar Rush timers # Sugar Rush timers
var any_rush = false
for pid in player_sugar_rush.keys(): for pid in player_sugar_rush.keys():
if player_sugar_rush[pid] > 0: if player_sugar_rush[pid] > 0:
player_sugar_rush[pid] -= delta player_sugar_rush[pid] -= delta
@@ -151,6 +138,10 @@ func _process(delta: float) -> void:
player_sugar_rush[pid] = 0.0 player_sugar_rush[pid] = 0.0
_restore_time_scale() _restore_time_scale()
sugar_rush_ended.emit(pid) sugar_rush_ended.emit(pid)
if player_sugar_rush[pid] > 0:
any_rush = true
if any_rush:
Engine.time_scale = SR_SPEED
# Ghost timers # Ghost timers
for pid in player_ghost_active.keys(): for pid in player_ghost_active.keys():
@@ -168,26 +159,21 @@ func _process(delta: float) -> void:
_add_score(pid, int(points)) _add_score(pid, int(points))
# ── Blueprint ── # ── Blueprint ──
func on_blueprint_completed(pid: int, primary_color: int, off_color: bool) -> void: func on_blueprint_completed(pid: int, primary_color: int, off_color: bool) -> void:
# Called when a 3x3 pattern is detected as complete
var base_points = 1000 var base_points = 1000
var points = base_points if not off_color else int(base_points * OFF_COLOR_PENALTY) var points = base_points if not off_color else int(base_points * OFF_COLOR_PENALTY)
_add_score(pid, points) _add_score(pid, points)
player_blueprints[pid] = player_blueprints.get(pid, 0) + 1 player_blueprints[pid] = player_blueprints.get(pid, 0) + 1
# Award candy on head (matching blueprint primary color)
_give_candy(pid, primary_color) _give_candy(pid, primary_color)
func can_finish_with_off_color(pid: int, primary_color: int) -> bool: func can_finish_with_off_color(pid: int, primary_color: int) -> bool:
# Returns true if primary color tiles are exhausted on the grid
return not _grid_has_color_tiles(primary_color) return not _grid_has_color_tiles(primary_color)
func _grid_has_color_tiles(color: int) -> bool: func _grid_has_color_tiles(color: int) -> bool:
# Check if any tile of given color exists on gridmap layer 1
if not gridmap: if not gridmap:
return true return true
var tile_ids = {0: 7, 1: 8, 2: 9, 3: 10} # Heart/Diamond/Star/Coin var target = TILE_IDS.get(color, 7)
var target = tile_ids.get(color, 7)
for x in range(1, ARENA_COLS - 1): for x in range(1, ARENA_COLS - 1):
for y in range(1, ARENA_ROWS - 1): for y in range(1, ARENA_ROWS - 1):
var v = Vector3i(x, 1, y) var v = Vector3i(x, 1, y)
@@ -196,6 +182,7 @@ func _grid_has_color_tiles(color: int) -> bool:
return false return false
# ── Candy Stack ── # ── Candy Stack ──
func _give_candy(pid: int, color: int) -> void: func _give_candy(pid: int, color: int) -> void:
player_candies[pid] = player_candies.get(pid, 0) + 1 player_candies[pid] = player_candies.get(pid, 0) + 1
player_candy_color[pid] = color player_candy_color[pid] = color
@@ -204,7 +191,6 @@ func _give_candy(pid: int, color: int) -> void:
func _update_candy_badge(pid: int) -> void: func _update_candy_badge(pid: int) -> void:
var count = player_candies.get(pid, 0) var count = player_candies.get(pid, 0)
var mult = 1.0 + count * MULTI_STEP var mult = 1.0 + count * MULTI_STEP
# RPC to update HUD
rpc("sync_candy_badge", pid, count, mult) rpc("sync_candy_badge", pid, count, mult)
func get_multiplier(pid: int) -> float: func get_multiplier(pid: int) -> float:
@@ -212,28 +198,23 @@ func get_multiplier(pid: int) -> float:
return 1.0 + count * MULTI_STEP return 1.0 + count * MULTI_STEP
# ── Mekton Delivery ── # ── Mekton Delivery ──
func try_deliver(pid: int) -> bool: func try_deliver(pid: int) -> bool:
# Player at Mekton zone tries to deliver matching-color candies
var color = player_candy_color.get(pid, -1) var color = player_candy_color.get(pid, -1)
if color == -1 or player_candies.get(pid, 0) == 0: if color == -1 or player_candies.get(pid, 0) == 0:
return false # No candies to deliver return false
if color != current_face: if color != current_face:
return false # Color mismatch return false
# Successful delivery: consume all candies of held color
var count = player_candies[pid] var count = player_candies[pid]
var mult = get_multiplier(pid) var mult = get_multiplier(pid)
var points = count * 500 # base delivery points _add_score(pid, count * 500)
_add_score(pid, points)
# Clear candy stack
player_candies[pid] = 0 player_candies[pid] = 0
player_candy_color[pid] = -1 player_candy_color[pid] = -1
_update_candy_badge(pid) _update_candy_badge(pid)
# Trigger Sugar Rush
_trigger_sugar_rush(pid, count, mult) _trigger_sugar_rush(pid, count, mult)
return true return true
func _trigger_sugar_rush(pid: int, candies: int, mult: float) -> void: func _trigger_sugar_rush(pid: int, candies: int, mult: float) -> void:
@@ -243,7 +224,6 @@ func _trigger_sugar_rush(pid: int, candies: int, mult: float) -> void:
sugar_rush_started.emit(pid) sugar_rush_started.emit(pid)
func _restore_time_scale() -> void: func _restore_time_scale() -> void:
# Only restore if no other player is in rush
var any_rush = false var any_rush = false
for pid in player_sugar_rush.keys(): for pid in player_sugar_rush.keys():
if player_sugar_rush[pid] > 0: if player_sugar_rush[pid] > 0:
@@ -253,18 +233,20 @@ func _restore_time_scale() -> void:
Engine.time_scale = 1.0 Engine.time_scale = 1.0
# ── Knock / Ghost ── # ── Knock / Ghost ──
func try_knock(attacker: int, target: int) -> bool: func try_knock(attacker: int, target: int) -> bool:
if player_ghost_active.get(target, false): if player_ghost_active.get(target, false):
return false # Ghost immunity return false
if player_knocks.get(attacker, 0) <= 0: if player_knocks.get(attacker, 0) <= 0:
return false # No charges return false
var target_candies = player_candies.get(target, 0) var target_candies = player_candies.get(target, 0)
if target_candies == 0: if target_candies == 0:
# Backfire: attacker also knocked # Backfire: both lose a charge
player_knocks[attacker] = player_knocks.get(attacker, 0) - 1 player_knocks[attacker] = player_knocks.get(attacker, 0) - 1
# Both knocked (stunned briefly) # Attacker is also knocked down briefly (visual handled by caller)
rpc("sync_knock_result", attacker, target, 0)
return false return false
# Steal all candies # Steal all candies
@@ -285,9 +267,9 @@ func try_knock(attacker: int, target: int) -> bool:
func try_activate_ghost(pid: int) -> bool: func try_activate_ghost(pid: int) -> bool:
if player_ghost_active.get(pid, false): if player_ghost_active.get(pid, false):
return false # Already ghosted return false
if player_ghosts.get(pid, 0) <= 0: if player_ghosts.get(pid, 0) <= 0:
return false # No charges left return false
player_ghosts[pid] = player_ghosts.get(pid, 0) - 1 player_ghosts[pid] = player_ghosts.get(pid, 0) - 1
player_ghost_active[pid] = true player_ghost_active[pid] = true
@@ -299,6 +281,7 @@ func is_ghost_active(pid: int) -> bool:
return player_ghost_active.get(pid, false) return player_ghost_active.get(pid, false)
# ── Arena ── # ── Arena ──
func _setup_arena() -> void: func _setup_arena() -> void:
if not gridmap: if not gridmap:
return return
@@ -311,28 +294,33 @@ func _setup_arena() -> void:
for y in range(ARENA_ROWS): for y in range(ARENA_ROWS):
gridmap.set_cell_item(Vector3i(x, 0, y), 0) gridmap.set_cell_item(Vector3i(x, 0, y), 0)
# Apply sticky cells on layer 2 # Apply sticky walls on layer 2
for pos in sticky_cells.keys(): for pos in sticky_cells.keys():
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), 14) # sticky overlay id gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), 14)
# Spawn Mekton NPC at center # Spawn Mekton NPC at center
if mekton_node: if mekton_node:
mekton_node.queue_free() mekton_node.queue_free()
if main_scene and main_scene.has_method("_spawn_mekton_npc"): mekton_node = _spawn_mekton_npc(NPC_CENTER)
mekton_node = main_scene._spawn_mekton_npc(NPC_CENTER) print("[CandySurvival] Arena setup 18x18")
print("[CandySurvival] Arena 18x18 setup with Mekton at center") 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)
return npc
return null
func _apply_arena_setup() -> void: func _apply_arena_setup() -> void:
# Client-side visual sync
pass pass
func setup_mission_tiles() -> void: func setup_mission_tiles() -> void:
# Spawn tiles on layer 1 at 60% density, avoiding NPC zone + sticky
if not gridmap: if not gridmap:
return return
randomize() randomize()
var tile_ids = [7, 8, 9, 10] # Heart, Diamond, Star, Coin var tile_ids = [7, 8, 9, 10]
var count = 0 var count = 0
for x in range(1, ARENA_COLS - 1): for x in range(1, ARENA_COLS - 1):
for y in range(1, ARENA_ROWS - 1): for y in range(1, ARENA_ROWS - 1):
@@ -345,16 +333,13 @@ func setup_mission_tiles() -> void:
print("[CandySurvival] Spawned %d mission tiles" % count) print("[CandySurvival] Spawned %d mission tiles" % count)
func get_spawn_points(count: int) -> Array: func get_spawn_points(count: int) -> Array:
# Return spawn points in quadrants avoiding NPC zone
var points = [] var points = []
var quadrants = [ var corners = [
[Vector2i(2, 2), Vector2i(4, 2), Vector2i(2, 4)], Vector2i(2, 2), Vector2i(ARENA_COLS - 3, 2),
[Vector2i(ARENA_COLS - 3, 2), Vector2i(ARENA_COLS - 5, 2), Vector2i(ARENA_COLS - 3, 4)], Vector2i(2, ARENA_ROWS - 3), Vector2i(ARENA_COLS - 3, ARENA_ROWS - 3)
[Vector2i(2, ARENA_ROWS - 3), Vector2i(4, ARENA_ROWS - 3), Vector2i(2, ARENA_ROWS - 5)],
[Vector2i(ARENA_COLS - 3, ARENA_ROWS - 3), Vector2i(ARENA_COLS - 5, ARENA_ROWS - 3), Vector2i(ARENA_COLS - 3, ARENA_ROWS - 5)]
] ]
for i in range(min(count, quadrants.size())): for i in range(count):
points.append(quadrants[i][0]) points.append(corners[i % corners.size()])
return points return points
func get_arena_positions() -> Dictionary: func get_arena_positions() -> Dictionary:
@@ -363,7 +348,8 @@ func get_arena_positions() -> Dictionary:
func get_arena_bounds() -> Dictionary: 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 (now pure wall collision) ── # ── Sticky (pure wall collision) ──
func is_sticky_cell(pos: Vector2i) -> bool: func is_sticky_cell(pos: Vector2i) -> bool:
return sticky_cells.has(pos) return sticky_cells.has(pos)
@@ -372,13 +358,15 @@ func _is_npc_zone(pos: Vector2i) -> bool:
var cy = NPC_CENTER.y 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 return pos.x >= cx and pos.x < cx + NPC_CELLS and pos.y >= cy and pos.y < cy + NPC_CELLS
# ── Scorring ── # ── Scoring ──
func _add_score(pid: int, points: int) -> void: func _add_score(pid: int, points: int) -> void:
player_score[pid] = player_score.get(pid, 0) + points player_score[pid] = player_score.get(pid, 0) + points
if main_scene and main_scene.has_method("add_points"): if main_scene and main_scene.has_method("add_points"):
main_scene.add_points(pid, points) main_scene.add_points(pid, points)
# ── Helpers ── # ── Helpers ──
func _get_player_ids() -> Array: func _get_player_ids() -> Array:
if main_scene and main_scene.has_method("get_player_ids"): if main_scene and main_scene.has_method("get_player_ids"):
return main_scene.get_player_ids() return main_scene.get_player_ids()
@@ -390,12 +378,13 @@ func candy_survival_round_duration() -> int:
func get_score(pid: int) -> int: func get_score(pid: int) -> int:
return player_score.get(pid, 0) return player_score.get(pid, 0)
# ── RPCs ── # ── RPCs (stubs — HUD client handles visuals) ──
func sync_candy_badge(pid: int, count: int, mult: float) -> void: func sync_candy_badge(pid: int, count: int, mult: float) -> void:
pass # Client handles HUD update pass
func sync_knock_result(attacker: int, target: int, candies: int) -> void: func sync_knock_result(attacker: int, target: int, candies: int) -> void:
pass # Client handles animation pass
func sync_ghost_active(pid: int, active_on: bool) -> void: func sync_ghost_active(pid: int, active_on: bool) -> void:
pass # Client handles visual pass