Files
tekton/scripts/managers/candy_survival_manager.gd
T

476 lines
16 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
extends Node
class_name CandySurvivalManager
# Candy Survival — Sugar Rush Chaos
# Speed Building Sabotage (blueprints + power-ups)
signal sugar_rush_started(pid)
signal sugar_rush_ended(pid)
signal mekton_face_changed(color_index)
# ── Arena ──
const ARENA_COLS: int = 18
const ARENA_ROWS: int = 18
const NPC_CENTER: Vector2i = Vector2i(8, 8)
const NPC_CELLS: int = 3 # 3x3 Mekton zone
# ── Blueprint ──
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
# ── Sugar Rush ──
const SR_BASE_TIME: float = 2.0
const SR_PER_CANDY: float = 0.4 # extra seconds per delivered candy
const SR_SPEED: float = 2.0
# ── Knock / Ghost ──
const START_KNOCK: int = 5
const START_GHOST: int = 5
const GHOST_DURATION: float = 4.0
# ── Mekton ──
const COLOR_CYCLE_TIME: float = 5.0 # seconds between face color changes
enum CandyColor { HEART, DIAMOND, STAR, COIN }
# ── State ──
var active: bool = false
var main_scene: Node
var gridmap: Node
var mekton_node: Node
var current_face: CandyColor = CandyColor.HEART
var face_timer: float = 0.0
var game_elapsed: float = 0.0
# ── HUD ──
var hud_layer: CanvasLayer
var hud_knock_label: Label
var hud_ghost_label: Label
var hud_rush_bar: ProgressBar
var hud_rush_label: Label
var hud_stack_badge: Label
var hud_points_label: Label
var _candy_survival_hud_scene: PackedScene = preload("res://scenes/candy_survival_hud.tscn")
func _ready():
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")
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()
func activate_client_side() -> void:
active = true
if hud_layer:
hud_layer.visible = true
set_process(true)
# 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_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_sugar_rush: Dictionary = {} # pid -> float (remaining)
var player_last_knocked_by: Dictionary = {} # pid -> int
# 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")
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
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()
# 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()]
# 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
# 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:
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:
return not _grid_has_color_tiles(primary_color)
func _grid_has_color_tiles(color: int) -> bool:
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)
func _update_candy_badge(pid: int) -> void:
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
# ── Mekton Delivery ──
func try_deliver(pid: int) -> bool:
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)
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
# ── 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
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
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
func is_ghost_active(pid: int) -> bool:
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 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:
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)
func get_spawn_points(count: int) -> Array:
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}
func get_arena_bounds() -> Dictionary:
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)
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
# ── 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)
# ── Helpers ──
func _get_player_ids() -> Array:
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
func get_score(pid: int) -> int:
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) -> void:
if pid == multiplayer.get_unique_id():
if hud_stack_badge:
hud_stack_badge.text = "Stack: %d (x%.1f)" % [count, mult]
@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]
@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]