|
|
|
@@ -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]
|
|
|
|
|