fix Candy Survival bugs: self-contained Mekton spawn, dead code, clean sticky definition
This commit is contained in:
@@ -2,10 +2,6 @@ extends Node
|
||||
class_name CandySurvivalManager
|
||||
# Candy Survival — Sugar Rush Chaos
|
||||
# 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_ended(pid)
|
||||
@@ -22,12 +18,12 @@ const BLUEPRINT_SIZE: int = 3
|
||||
const OFF_COLOR_PENALTY: float = 0.5
|
||||
|
||||
# ── Candy ──
|
||||
const CANDY_PPS: float = 1.0 # points per second per candy
|
||||
const MULTI_STEP: float = 0.1 # extra multiplier per candy
|
||||
const CANDY_PPS: float = 1.0 # points per second per candy
|
||||
const MULTI_STEP: float = 0.1 # extra multiplier per candy
|
||||
|
||||
# ── Sugar Rush ──
|
||||
const SR_BASE_TIME: float = 2.0
|
||||
const SR_PER_CANDY: float = 0.4 # extra seconds per delivered candy
|
||||
const SR_PER_CANDY: float = 0.4 # extra seconds per delivered candy
|
||||
const SR_SPEED: float = 2.0
|
||||
|
||||
# ── Knock / Ghost ──
|
||||
@@ -36,13 +32,11 @@ const START_GHOST: int = 5
|
||||
const GHOST_DURATION: float = 4.0
|
||||
|
||||
# ── Mekton ──
|
||||
const COLOR_CYCLE_TIME: float = 5.0 # seconds between face color changes
|
||||
const COLOR_CYCLE_TIME: float = 5.0 # seconds between face color changes
|
||||
enum CandyColor { HEART, DIAMOND, STAR, COIN }
|
||||
|
||||
# ── State ──
|
||||
var active: bool = false
|
||||
var arena_width: int = ARENA_COLS
|
||||
var arena_height: int = ARENA_ROWS
|
||||
var main_scene: Node
|
||||
var gridmap: Node
|
||||
var mekton_node: Node
|
||||
@@ -51,351 +45,346 @@ var face_timer: float = 0.0
|
||||
var game_elapsed: float = 0.0
|
||||
|
||||
# Per-player state
|
||||
var player_candies: Dictionary = {} # pid -> int (stack count)
|
||||
var player_candy_color: Dictionary = {} # pid -> CandyColor (-1 if none)
|
||||
var player_knocks: Dictionary = {} # pid -> int (charges left)
|
||||
var player_ghosts: Dictionary = {} # pid -> int (charges left)
|
||||
var player_candies: Dictionary = {} # pid -> int (stack count)
|
||||
var player_candy_color: Dictionary = {} # pid -> CandyColor (-1 if none)
|
||||
var player_knocks: Dictionary = {} # pid -> int (charges left)
|
||||
var player_ghosts: Dictionary = {} # pid -> int (charges left)
|
||||
var player_ghost_active: Dictionary = {} # pid -> bool
|
||||
var player_ghost_timer: Dictionary = {} # pid -> float
|
||||
var player_score: Dictionary = {} # pid -> int
|
||||
var player_blueprints: Dictionary = {} # pid -> int (completed count)
|
||||
var player_position: Dictionary = {} # pid -> Vector2i
|
||||
var player_sugar_rush: Dictionary = {} # pid -> float (remaining, 0=none)
|
||||
var player_ghost_timer: Dictionary = {} # pid -> float
|
||||
var player_score: Dictionary = {} # pid -> int
|
||||
var player_blueprints: Dictionary = {} # pid -> int (completed count)
|
||||
var player_sugar_rush: Dictionary = {} # pid -> float (remaining)
|
||||
var player_last_knocked_by: Dictionary = {} # pid -> int
|
||||
|
||||
# Sticky wall cells (pre-placed collision)
|
||||
var sticky_cells: Dictionary = {} # Vector2i -> bool
|
||||
# Pre-placed sticky positions for 18x18 arena
|
||||
var preplaced_sticky: Array = []
|
||||
# Sticky wall cells (pre-placed collision only)
|
||||
var sticky_cells: Dictionary = {} # Vector2i -> bool
|
||||
|
||||
# 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:
|
||||
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:
|
||||
# Block Mekton NPC zone (3x3 center)
|
||||
var cx = NPC_CENTER.x
|
||||
var cy = NPC_CENTER.y
|
||||
for dx in range(NPC_CELLS):
|
||||
for dy in range(NPC_CELLS):
|
||||
preplaced_sticky.append(Vector2i(cx + dx, cy + dy))
|
||||
# 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))
|
||||
# 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_position.clear()
|
||||
player_sugar_rush.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
|
||||
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())
|
||||
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
|
||||
|
||||
print("[CandySurvival] Started with %d players" % pids.size())
|
||||
|
||||
func _enter_tree() -> void:
|
||||
set_process(true)
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
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()]
|
||||
mekton_face_changed.emit(current_face)
|
||||
|
||||
# Sugar Rush timers
|
||||
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)
|
||||
|
||||
# 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
|
||||
|
||||
# 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))
|
||||
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()]
|
||||
|
||||
# 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
|
||||
|
||||
# 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_color: int, off_color: bool) -> void:
|
||||
# Called when a 3x3 pattern is detected as complete
|
||||
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
|
||||
|
||||
# Award candy on head (matching blueprint primary color)
|
||||
_give_candy(pid, primary_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
|
||||
_give_candy(pid, primary_color)
|
||||
|
||||
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:
|
||||
# Check if any tile of given color exists on gridmap layer 1
|
||||
if not gridmap:
|
||||
return true
|
||||
var tile_ids = {0: 7, 1: 8, 2: 9, 3: 10} # Heart/Diamond/Star/Coin
|
||||
var target = tile_ids.get(color, 7)
|
||||
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:
|
||||
return true
|
||||
return false
|
||||
if not gridmap:
|
||||
return true
|
||||
var target = TILE_IDS.get(color, 7)
|
||||
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:
|
||||
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
|
||||
# RPC to update HUD
|
||||
rpc("sync_candy_badge", pid, count, mult)
|
||||
var count = player_candies.get(pid, 0)
|
||||
var mult = 1.0 + count * MULTI_STEP
|
||||
rpc("sync_candy_badge", pid, count, mult)
|
||||
|
||||
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 ──
|
||||
|
||||
func try_deliver(pid: int) -> bool:
|
||||
# Player at Mekton zone tries to deliver matching-color candies
|
||||
var color = player_candy_color.get(pid, -1)
|
||||
if color == -1 or player_candies.get(pid, 0) == 0:
|
||||
return false # No candies to deliver
|
||||
if color != current_face:
|
||||
return false # Color mismatch
|
||||
|
||||
# Successful delivery: consume all candies of held color
|
||||
var count = player_candies[pid]
|
||||
var mult = get_multiplier(pid)
|
||||
var points = count * 500 # base delivery points
|
||||
_add_score(pid, points)
|
||||
|
||||
# Clear candy stack
|
||||
player_candies[pid] = 0
|
||||
player_candy_color[pid] = -1
|
||||
_update_candy_badge(pid)
|
||||
|
||||
# Trigger Sugar Rush
|
||||
_trigger_sugar_rush(pid, count, mult)
|
||||
|
||||
return true
|
||||
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)
|
||||
|
||||
player_candies[pid] = 0
|
||||
player_candy_color[pid] = -1
|
||||
_update_candy_badge(pid)
|
||||
|
||||
_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:
|
||||
# Only restore if no other player is in rush
|
||||
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 ──
|
||||
|
||||
func try_knock(attacker: int, target: int) -> bool:
|
||||
if player_ghost_active.get(target, false):
|
||||
return false # Ghost immunity
|
||||
if player_knocks.get(attacker, 0) <= 0:
|
||||
return false # No charges
|
||||
|
||||
var target_candies = player_candies.get(target, 0)
|
||||
|
||||
if target_candies == 0:
|
||||
# Backfire: attacker also knocked
|
||||
player_knocks[attacker] = player_knocks.get(attacker, 0) - 1
|
||||
# Both knocked (stunned briefly)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
rpc("sync_knock_result", attacker, target, target_candies)
|
||||
return true
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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 # Already ghosted
|
||||
if player_ghosts.get(pid, 0) <= 0:
|
||||
return false # No charges left
|
||||
|
||||
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)
|
||||
return true
|
||||
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)
|
||||
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)
|
||||
|
||||
# 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 cells on layer 2
|
||||
for pos in sticky_cells.keys():
|
||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), 14) # sticky overlay id
|
||||
|
||||
# Spawn Mekton NPC at center
|
||||
if mekton_node:
|
||||
mekton_node.queue_free()
|
||||
if main_scene and main_scene.has_method("_spawn_mekton_npc"):
|
||||
mekton_node = main_scene._spawn_mekton_npc(NPC_CENTER)
|
||||
|
||||
print("[CandySurvival] Arena 18x18 setup with Mekton at center")
|
||||
if not gridmap:
|
||||
return
|
||||
gridmap.clear_layer(0)
|
||||
gridmap.clear_layer(1)
|
||||
gridmap.clear_layer(2)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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)
|
||||
return npc
|
||||
return null
|
||||
|
||||
func _apply_arena_setup() -> void:
|
||||
# Client-side visual sync
|
||||
pass
|
||||
pass
|
||||
|
||||
func setup_mission_tiles() -> void:
|
||||
# Spawn tiles on layer 1 at 60% density, avoiding NPC zone + sticky
|
||||
if not gridmap:
|
||||
return
|
||||
randomize()
|
||||
var tile_ids = [7, 8, 9, 10] # Heart, Diamond, Star, Coin
|
||||
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:
|
||||
# Return spawn points in quadrants avoiding NPC zone
|
||||
var points = []
|
||||
var quadrants = [
|
||||
[Vector2i(2, 2), Vector2i(4, 2), Vector2i(2, 4)],
|
||||
[Vector2i(ARENA_COLS - 3, 2), Vector2i(ARENA_COLS - 5, 2), Vector2i(ARENA_COLS - 3, 4)],
|
||||
[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())):
|
||||
points.append(quadrants[i][0])
|
||||
return points
|
||||
var points = []
|
||||
var corners = [
|
||||
Vector2i(2, 2), Vector2i(ARENA_COLS - 3, 2),
|
||||
Vector2i(2, ARENA_ROWS - 3), Vector2i(ARENA_COLS - 3, ARENA_ROWS - 3)
|
||||
]
|
||||
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) ──
|
||||
|
||||
# ── Sticky (now 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 ──
|
||||
|
||||
# ── Scorring ──
|
||||
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) ──
|
||||
|
||||
# ── RPCs ──
|
||||
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:
|
||||
pass # Client handles animation
|
||||
pass
|
||||
|
||||
func sync_ghost_active(pid: int, active_on: bool) -> void:
|
||||
pass # Client handles visual
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user