Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b4c9d9dcb | |||
| 00c9e85527 | |||
| 203175178f |
@@ -1,4 +1,18 @@
|
|||||||
## [NEXT]
|
## [NEXT]
|
||||||
|
- Changed Candy Survival knock from instant-ambush to **toggle-and-walk mode** like other game modes: press Q to toggle Knock Mode ON (red glow), then walk into a target to execute the knock. Press Q again to cancel. Consumed on use. Bots auto-toggle when they have charges.
|
||||||
|
- Fixed `Attempt to call RPC with unknown peer ID: 5` crash in Candy Survival when updating ghost inventory or candy stacks for AI bots or unverified peers.
|
||||||
|
- Added +100 score rewards in Candy Survival when completing goals by collecting tiles, successfully landing a KNOCK, or tactically activating GHOST.
|
||||||
|
- Updated Candy Survival knocking animation to match other game modes (`attack_mode` SFX, bump animation, 3-tile knockback along path, and stun stagger) while keeping the Candy Survival ruleset: successful knock steals all target's candies and knocks target back; knocking a target with 0 candies backfires, knocking the attacker back 3 tiles and transferring attacker's candies to target.
|
||||||
|
- Fixed profile panel (`SaveNameBtn`) failing when updating username on connected peers by passing `null` for login username so Nakama only updates `display_name` without unique username collisions.
|
||||||
|
- Preserved detailed profile update failure messages in ProfilePanel UI status label and persisted display name changes to storage.
|
||||||
|
|
||||||
|
## [2.4.6] — 2026-07-09
|
||||||
|
- Fixed NakamaMultiplayerBridge dictionary lookup crash when receiving match state before user presence is populated by creating a `SafeNakamaMultiplayerBridge` subclass.
|
||||||
|
|
||||||
|
## [2.4.5] — 2026-07-09
|
||||||
|
- Fixed missing `player_goals` dictionary declaration in GoalsCycleManager.
|
||||||
|
- Improved Candy Survival manager sync and LobbyManager initialization.
|
||||||
|
- Updated EnhancedGridMap and Main scene synchronization.
|
||||||
|
|
||||||
## [2.4.4] — 2026-07-09
|
## [2.4.4] — 2026-07-09
|
||||||
- Fixed Candy Survival Knock mechanic: pressing Q now properly knocks adjacent players diagonally and cardinally.
|
- Fixed Candy Survival Knock mechanic: pressing Q now properly knocks adjacent players diagonally and cardinally.
|
||||||
|
|||||||
@@ -144,3 +144,13 @@ This document serves as an exhaustive, technical record of all modifications, re
|
|||||||
- [x] Baked terrain height shifts directly into `.tscn` files to match the reference tile perfectly.
|
- [x] Baked terrain height shifts directly into `.tscn` files to match the reference tile perfectly.
|
||||||
- [x] Scaled red `non-walkable` blocks (Tile 4) to 0 so they act as invisible collision walls in 3D arenas.
|
- [x] Scaled red `non-walkable` blocks (Tile 4) to 0 so they act as invisible collision walls in 3D arenas.
|
||||||
- [x] Fixed Stop N Go safe zone clipping by adjusting render priorities (`1` for safe zone, `-1` for baked terrain shadows).
|
- [x] Fixed Stop N Go safe zone clipping by adjusting render priorities (`1` for safe zone, `-1` for baked terrain shadows).
|
||||||
|
- [x] **Candy Survival Multiplayer Client Arena & NPC Desync Fixes**
|
||||||
|
- [x] Fixed client-side arena desync where `CandySurvivalManager._apply_arena_setup()` was empty (`pass`), leaving clients without the 18x18 Floor 0 grid or sticky boundary walls.
|
||||||
|
- [x] Added `@rpc("authority", "call_remote", "reliable") func sync_arena_setup()` and updated `_apply_arena_setup()` to construct the 18x18 arena floor, apply sticky walls, and deterministically spawn `CandySurvivalNpc` (`mekton_node`) across both host and clients.
|
||||||
|
- [x] Synced `sync_arena_setup` during `sync_state_to_player(peer_id)` so late-joining or resyncing clients receive the full arena structure and NPC node before incoming state RPCs.
|
||||||
|
- [x] Added `can_rpc()` connectivity checks before sending RPCs (`set_face_color_rpc`, `sync_bump`, `apply_stagger`) to prevent remote packet errors or client drops.
|
||||||
|
- [x] **Game Mode Switching Lag & Arena Desync Fixes**
|
||||||
|
- [x] Fixed `Engine.time_scale` persistence: Ensured `Engine.time_scale = 1.0` is restored in `Main._ready()`, `CandySurvivalManager._exit_tree()`, and `LobbyManager.leave_room()` so high-speed scales (`SR_SPEED`) do not carry over across scene transitions or cause physics stutter when switching game modes.
|
||||||
|
- [x] Fixed `EnhancedGridMap` auto-randomization conflict: Updated safety check so `EnhancedGridMap._ready()` skips auto-randomizing when `LobbyManager.game_mode` is either `"Stop n Go"` or `"Candy Survival"`, avoiding unnecessary 14x14 generation before game mode managers set up custom arena bounds.
|
||||||
|
- [x] Fixed `sync_full_grid_data()` client-side arena setup: Added `"Candy Survival"` check so clients reapply `candy_survival_manager._apply_arena_setup()` prior to syncing Floor 1 items instead of overwriting Floor 0 with 14x14 Freemode tiles.
|
||||||
|
- [x] Fixed `GoalsCycleManager` process persistence: Added `reset()` to stop `_process` execution and clean up active cycle/match state when leaving rooms or returning to lobby.
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func _ready():
|
|||||||
# Safety check: Don't auto-randomize if game mode manages its own arena
|
# Safety check: Don't auto-randomize if game mode manages its own arena
|
||||||
if not (ResourceLoader.exists("res://scripts/managers/lobby_manager.gd") \
|
if not (ResourceLoader.exists("res://scripts/managers/lobby_manager.gd") \
|
||||||
and get_node_or_null("/root/LobbyManager") \
|
and get_node_or_null("/root/LobbyManager") \
|
||||||
and (get_node("/root/LobbyManager").game_mode == "Stop n Go")):
|
and (get_node("/root/LobbyManager").game_mode in ["Stop n Go", "Candy Survival"])):
|
||||||
randomize_grid()
|
randomize_grid()
|
||||||
validate_item_indices()
|
validate_item_indices()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,27 @@
|
|||||||
{
|
{
|
||||||
"latest_version": "2.4.4",
|
"latest_version": "2.4.6",
|
||||||
"minimum_app_version": "2.1.0",
|
"minimum_app_version": "2.1.0",
|
||||||
"releases": [
|
"releases": [
|
||||||
|
{
|
||||||
|
"version": "2.4.6",
|
||||||
|
"date": "2026-07-09",
|
||||||
|
"pck_url": "https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck",
|
||||||
|
"pck_size": 0,
|
||||||
|
"changelog": [
|
||||||
|
"Fixed NakamaMultiplayerBridge dictionary lookup crash when receiving match state before user presence is populated by creating a `SafeNakamaMultiplayerBridge` subclass."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "2.4.5",
|
||||||
|
"date": "2026-07-09",
|
||||||
|
"pck_url": "https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck",
|
||||||
|
"pck_size": 0,
|
||||||
|
"changelog": [
|
||||||
|
"Fixed missing `player_goals` dictionary declaration in GoalsCycleManager.",
|
||||||
|
"Improved Candy Survival manager sync and LobbyManager initialization.",
|
||||||
|
"Updated EnhancedGridMap and Main scene synchronization."
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "2.4.4",
|
"version": "2.4.4",
|
||||||
"date": "2026-07-09",
|
"date": "2026-07-09",
|
||||||
|
|||||||
+4
-4
@@ -14,7 +14,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/windows/tekton_armageddon_v2.4.4.exe"
|
export_path="build/windows/tekton_armageddon_v2.4.6.exe"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -85,7 +85,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/tekton-dash-armageddon-v.2.4.4.apk"
|
export_path="build/tekton-dash-armageddon-v.2.4.6.apk"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -314,7 +314,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/tekton_armageddon_v2.4.4.zip"
|
export_path="build/tekton_armageddon_v2.4.6.zip"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
@@ -589,7 +589,7 @@ custom_features=""
|
|||||||
export_filter="all_resources"
|
export_filter="all_resources"
|
||||||
include_filter=""
|
include_filter=""
|
||||||
exclude_filter=""
|
exclude_filter=""
|
||||||
export_path="build/linux/tekton_armageddon_v2.4.4.x86_64"
|
export_path="build/linux/tekton_armageddon_v2.4.6.x86_64"
|
||||||
patches=PackedStringArray()
|
patches=PackedStringArray()
|
||||||
patch_delta_encoding=false
|
patch_delta_encoding=false
|
||||||
patch_delta_compression_level_zstd=19
|
patch_delta_compression_level_zstd=19
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
|
|||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Tekton Dash Armageddon"
|
config/name="Tekton Dash Armageddon"
|
||||||
config/version="2.4.4"
|
config/version="2.4.6"
|
||||||
run/main_scene="res://scenes/ui/boot_screen.tscn"
|
run/main_scene="res://scenes/ui/boot_screen.tscn"
|
||||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||||
boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1)
|
boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1)
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ func _can_rpc() -> bool:
|
|||||||
return true
|
return true
|
||||||
|
|
||||||
func _ready():
|
func _ready():
|
||||||
|
# Always ensure time scale is normal when entering Main
|
||||||
|
Engine.time_scale = 1.0
|
||||||
# Initialize scene managers
|
# Initialize scene managers
|
||||||
_init_managers()
|
_init_managers()
|
||||||
|
|
||||||
@@ -2078,6 +2080,13 @@ func sync_full_grid_data(data: PackedInt32Array):
|
|||||||
stop_n_go_manager.name = "StopNGoManager"
|
stop_n_go_manager.name = "StopNGoManager"
|
||||||
add_child(stop_n_go_manager)
|
add_child(stop_n_go_manager)
|
||||||
stop_n_go_manager._apply_arena_setup()
|
stop_n_go_manager._apply_arena_setup()
|
||||||
|
elif LobbyManager.game_mode == "Candy Survival":
|
||||||
|
if not candy_survival_manager:
|
||||||
|
candy_survival_manager = load("res://scripts/managers/candy_survival_manager.gd").new()
|
||||||
|
candy_survival_manager.name = "CandySurvivalManager"
|
||||||
|
add_child(candy_survival_manager)
|
||||||
|
candy_survival_manager.initialize(self, enhanced_gridmap)
|
||||||
|
candy_survival_manager._apply_arena_setup()
|
||||||
else:
|
else:
|
||||||
# Freemode: Ensure Floor 0 is entirely walkable (reset stale state from previous modes)
|
# Freemode: Ensure Floor 0 is entirely walkable (reset stale state from previous modes)
|
||||||
for x in range(enhanced_gridmap.columns):
|
for x in range(enhanced_gridmap.columns):
|
||||||
|
|||||||
@@ -210,6 +210,17 @@ func _run_ai_tick():
|
|||||||
print("[BotController] Action Taken: Attack Pursuit")
|
print("[BotController] Action Taken: Attack Pursuit")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Priority 0.3: Candy Survival — auto-toggle knock mode when bot has charges
|
||||||
|
if strategic_planner and strategic_planner.is_candy_survival_mode():
|
||||||
|
var gm = strategic_planner._get_candy_survival_manager()
|
||||||
|
if gm and gm.active:
|
||||||
|
var pid = actor.get("peer_id") if "peer_id" in actor else actor.name.to_int()
|
||||||
|
var charges = gm.player_knocks.get(pid, 0)
|
||||||
|
if charges > 0:
|
||||||
|
actor.is_charged_strike = true
|
||||||
|
else:
|
||||||
|
actor.is_charged_strike = false
|
||||||
|
|
||||||
# Priority 0.5: Candy Survival (#075) — use Ghost powerup if boxed in
|
# Priority 0.5: Candy Survival (#075) — use Ghost powerup if boxed in
|
||||||
if await _try_activate_ghost():
|
if await _try_activate_ghost():
|
||||||
print("[BotController] Action Taken: Ghost (trapped)")
|
print("[BotController] Action Taken: Ghost (trapped)")
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ extends Node
|
|||||||
## AuthManager - Centralized authentication handling for Nakama
|
## AuthManager - Centralized authentication handling for Nakama
|
||||||
## Supports: Guest (device ID), Email/Password, Social (Google, Apple, Facebook)
|
## Supports: Guest (device ID), Email/Password, Social (Google, Apple, Facebook)
|
||||||
|
|
||||||
|
const SafeNakamaMultiplayerBridge = preload("res://scripts/services/safe_nakama_multiplayer_bridge.gd")
|
||||||
|
|
||||||
# Signals
|
# Signals
|
||||||
signal auth_started
|
signal auth_started
|
||||||
signal auth_completed(success: bool, user_data: Dictionary)
|
signal auth_completed(success: bool, user_data: Dictionary)
|
||||||
@@ -472,7 +474,7 @@ func _connect_socket() -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
# Initialize multiplayer bridge
|
# Initialize multiplayer bridge
|
||||||
NakamaManager.bridge = NakamaMultiplayerBridge.new(NakamaManager.socket)
|
NakamaManager.bridge = SafeNakamaMultiplayerBridge.new(NakamaManager.socket)
|
||||||
NakamaManager.bridge.match_joined.connect(NakamaManager._on_bridge_match_joined)
|
NakamaManager.bridge.match_joined.connect(NakamaManager._on_bridge_match_joined)
|
||||||
NakamaManager.bridge.match_join_error.connect(NakamaManager._on_bridge_match_join_error)
|
NakamaManager.bridge.match_join_error.connect(NakamaManager._on_bridge_match_join_error)
|
||||||
multiplayer.set_multiplayer_peer(NakamaManager.bridge.multiplayer_peer)
|
multiplayer.set_multiplayer_peer(NakamaManager.bridge.multiplayer_peer)
|
||||||
|
|||||||
@@ -64,6 +64,9 @@ func _ready():
|
|||||||
set_process(false)
|
set_process(false)
|
||||||
_setup_hud()
|
_setup_hud()
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
Engine.time_scale = 1.0
|
||||||
|
|
||||||
func _setup_hud() -> void:
|
func _setup_hud() -> void:
|
||||||
var hud_instance = _candy_survival_hud_scene.instantiate()
|
var hud_instance = _candy_survival_hud_scene.instantiate()
|
||||||
hud_layer = hud_instance
|
hud_layer = hud_instance
|
||||||
@@ -203,8 +206,11 @@ func _process(delta: float) -> void:
|
|||||||
current_face = CandyColor.values()[(current_face + 1) % CandyColor.size()]
|
current_face = CandyColor.values()[(current_face + 1) % CandyColor.size()]
|
||||||
|
|
||||||
# Sync color to the Mekton NPC
|
# Sync color to the Mekton NPC
|
||||||
if mekton_node and mekton_node.has_method("set_face_color_rpc"):
|
if mekton_node and is_instance_valid(mekton_node) and mekton_node.has_method("set_face_color_rpc"):
|
||||||
mekton_node.rpc("set_face_color_rpc", current_face)
|
if multiplayer.is_server() and can_rpc():
|
||||||
|
mekton_node.rpc("set_face_color_rpc", current_face)
|
||||||
|
else:
|
||||||
|
mekton_node.set_face_color_rpc(current_face)
|
||||||
|
|
||||||
# Update delivery indicators for all players based on new face
|
# Update delivery indicators for all players based on new face
|
||||||
for pid in player_candies:
|
for pid in player_candies:
|
||||||
@@ -277,6 +283,7 @@ func on_blueprint_completed(pid: int, primary_tile_id: int, off_color: bool) ->
|
|||||||
break
|
break
|
||||||
|
|
||||||
_give_candy(pid, candy_color)
|
_give_candy(pid, candy_color)
|
||||||
|
_add_score(pid, 100) # Award +100 points when completing goals by collecting tiles
|
||||||
|
|
||||||
# Clear the board now that it's matched and converted to a candy
|
# Clear the board now that it's matched and converted to a candy
|
||||||
if main_scene and player_node:
|
if main_scene and player_node:
|
||||||
@@ -397,19 +404,12 @@ func try_knock(attacker: int, target: int) -> bool:
|
|||||||
|
|
||||||
var attacker_node = _find_player_node(attacker)
|
var attacker_node = _find_player_node(attacker)
|
||||||
var target_node = _find_player_node(target)
|
var target_node = _find_player_node(target)
|
||||||
|
|
||||||
if attacker_node and target_node:
|
|
||||||
attacker_node.rpc("sync_bump", target_node.current_position)
|
|
||||||
|
|
||||||
if target_candies == 0:
|
if target_candies == 0:
|
||||||
# Backfire: attacker loses a charge & gets staggered
|
# Backfire: attacker loses a charge & gets staggered
|
||||||
player_knocks[attacker] = player_knocks.get(attacker, START_KNOCK) - 1
|
player_knocks[attacker] = player_knocks.get(attacker, START_KNOCK) - 1
|
||||||
rpc("sync_knock_charge_count", attacker, player_knocks[attacker])
|
rpc("sync_knock_charge_count", attacker, player_knocks[attacker])
|
||||||
|
|
||||||
# Attacker is knocked down briefly (backfire)
|
|
||||||
if attacker_node:
|
|
||||||
attacker_node.rpc("apply_stagger", 2.0)
|
|
||||||
|
|
||||||
# Backfire penalty: Attacker's candy stack transfers to Target!
|
# Backfire penalty: Attacker's candy stack transfers to Target!
|
||||||
var attacker_colors = player_candy_colors.get(attacker, [])
|
var attacker_colors = player_candy_colors.get(attacker, [])
|
||||||
var attacker_candies = attacker_colors.size()
|
var attacker_candies = attacker_colors.size()
|
||||||
@@ -442,11 +442,7 @@ func try_knock(attacker: int, target: int) -> bool:
|
|||||||
rpc("sync_knock_charge_count", attacker, player_knocks[attacker])
|
rpc("sync_knock_charge_count", attacker, player_knocks[attacker])
|
||||||
player_last_knocked_by[target] = attacker
|
player_last_knocked_by[target] = attacker
|
||||||
|
|
||||||
# Target is knocked down / staggered on successful knock
|
_add_score(attacker, target_candies * 100 + 100) # Award +100 points for using KNOCK + 100 per candy stolen
|
||||||
if target_node:
|
|
||||||
target_node.rpc("apply_stagger", 2.0)
|
|
||||||
|
|
||||||
_add_score(attacker, target_candies * 100)
|
|
||||||
_update_candy_badge(attacker)
|
_update_candy_badge(attacker)
|
||||||
_update_candy_badge(target)
|
_update_candy_badge(target)
|
||||||
|
|
||||||
@@ -480,14 +476,44 @@ func try_activate_ghost(pid: int) -> bool:
|
|||||||
player.sync_modulate(Color(1.0, 1.0, 1.0, 0.4))
|
player.sync_modulate(Color(1.0, 1.0, 1.0, 0.4))
|
||||||
break
|
break
|
||||||
|
|
||||||
|
_add_score(pid, 100) # Award +100 points for tactically using GHOST
|
||||||
return true
|
return true
|
||||||
|
|
||||||
func is_ghost_active(pid: int) -> bool:
|
func is_ghost_active(pid: int) -> bool:
|
||||||
return player_ghost_active.get(pid, false)
|
return player_ghost_active.get(pid, false)
|
||||||
|
|
||||||
|
func can_rpc() -> bool:
|
||||||
|
if not multiplayer.has_multiplayer_peer() or multiplayer.multiplayer_peer.get_connection_status() != MultiplayerPeer.CONNECTION_CONNECTED:
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
# ── Arena ──
|
# ── Arena ──
|
||||||
|
|
||||||
func _setup_arena() -> void:
|
func _setup_arena() -> void:
|
||||||
|
if not gridmap:
|
||||||
|
return
|
||||||
|
if can_rpc():
|
||||||
|
rpc("sync_arena_setup")
|
||||||
|
_apply_arena_setup()
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable")
|
||||||
|
func sync_arena_setup() -> void:
|
||||||
|
print("[CandySurvival] Client: Syncing Arena Setup (18x18)...")
|
||||||
|
_apply_arena_setup()
|
||||||
|
|
||||||
|
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.name = "CandySurvivalNpc"
|
||||||
|
npc.position = Vector3(center.x + 1, 0, center.y + 1)
|
||||||
|
add_child(npc, true)
|
||||||
|
if npc.has_method("set_face_color_rpc"):
|
||||||
|
npc.set_face_color_rpc(current_face)
|
||||||
|
return npc
|
||||||
|
return null
|
||||||
|
|
||||||
|
func _apply_arena_setup() -> void:
|
||||||
if not gridmap:
|
if not gridmap:
|
||||||
return
|
return
|
||||||
gridmap.set("columns", ARENA_COLS)
|
gridmap.set("columns", ARENA_COLS)
|
||||||
@@ -504,24 +530,10 @@ func _setup_arena() -> void:
|
|||||||
gridmap.set_cell_item(Vector3i(pos.x, 2, pos.y), 14)
|
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 and is_instance_valid(mekton_node):
|
||||||
mekton_node.queue_free()
|
mekton_node.queue_free()
|
||||||
mekton_node = _spawn_mekton_npc(NPC_CENTER)
|
mekton_node = _spawn_mekton_npc(NPC_CENTER)
|
||||||
print("[CandySurvival] Arena setup 18x18")
|
print("[CandySurvival] Arena setup applied 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
|
|
||||||
|
|
||||||
func _apply_arena_setup() -> void:
|
|
||||||
pass
|
|
||||||
|
|
||||||
func setup_mission_tiles() -> void:
|
func setup_mission_tiles() -> void:
|
||||||
if not gridmap:
|
if not gridmap:
|
||||||
@@ -688,6 +700,63 @@ func sync_knock_result(attacker: int, target: int, candies: int) -> void:
|
|||||||
if hud_knock_label and player_knocks.has(local):
|
if hud_knock_label and player_knocks.has(local):
|
||||||
hud_knock_label.text = "🥊 Knocks: %d" % player_knocks[local]
|
hud_knock_label.text = "🥊 Knocks: %d" % player_knocks[local]
|
||||||
|
|
||||||
|
var attacker_node = _find_player_node(attacker)
|
||||||
|
var target_node = _find_player_node(target)
|
||||||
|
if not attacker_node or not target_node:
|
||||||
|
return
|
||||||
|
|
||||||
|
# SFX: Play attack_mode sound like other game modes
|
||||||
|
var sfx = Engine.get_main_loop().root.get_node_or_null("SfxManager")
|
||||||
|
if sfx and sfx.has_method("play"):
|
||||||
|
sfx.play("attack_mode")
|
||||||
|
|
||||||
|
# Attacker bump animation toward target
|
||||||
|
if attacker_node.has_method("sync_bump"):
|
||||||
|
attacker_node.sync_bump(target_node.current_position, false)
|
||||||
|
|
||||||
|
# Calculate push direction from attacker to target
|
||||||
|
var diff: Vector2i = target_node.current_position - attacker_node.current_position
|
||||||
|
var push_dir = Vector2i(clamp(diff.x, -1, 1), clamp(diff.y, -1, 1))
|
||||||
|
if push_dir == Vector2i.ZERO:
|
||||||
|
push_dir = Vector2i(1, 0)
|
||||||
|
|
||||||
|
if candies > 0:
|
||||||
|
# SUCCESSFUL STEAL: target has candies
|
||||||
|
# Target is knocked back up to 3 tiles & stunned
|
||||||
|
_apply_knock_animation_to_victim(target_node, push_dir, 2.0)
|
||||||
|
else:
|
||||||
|
# BACKFIRE: target had 0 candies
|
||||||
|
# Target is NOT stunned ("whoever doesn't hold candy, will not stunned")
|
||||||
|
# Attacker backfires: knocked back up to 3 tiles away from target & stunned
|
||||||
|
_apply_knock_animation_to_victim(attacker_node, -push_dir, 2.0)
|
||||||
|
|
||||||
|
func _apply_knock_animation_to_victim(victim_node: Node, push_dir: Vector2i, stagger_duration: float) -> void:
|
||||||
|
if not is_instance_valid(victim_node):
|
||||||
|
return
|
||||||
|
|
||||||
|
var pushed_to_pos: Vector2i = victim_node.current_position
|
||||||
|
var push_path: Array = []
|
||||||
|
for i in range(3):
|
||||||
|
var next_back: Vector2i = pushed_to_pos + push_dir
|
||||||
|
var can_push: bool = false
|
||||||
|
if victim_node.get("movement_manager") and victim_node.movement_manager.has_method("_can_push_to"):
|
||||||
|
can_push = victim_node.movement_manager._can_push_to(next_back)
|
||||||
|
if can_push:
|
||||||
|
pushed_to_pos = next_back
|
||||||
|
push_path.append(Vector2(pushed_to_pos.x, pushed_to_pos.y))
|
||||||
|
if is_sticky_cell(pushed_to_pos):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if push_path.size() > 0:
|
||||||
|
if victim_node.has_method("start_movement_along_path"):
|
||||||
|
victim_node.start_movement_along_path(push_path, false, true)
|
||||||
|
victim_node.target_position = pushed_to_pos
|
||||||
|
|
||||||
|
if victim_node.has_method("apply_stagger"):
|
||||||
|
victim_node.apply_stagger(stagger_duration)
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "unreliable")
|
@rpc("authority", "call_remote", "unreliable")
|
||||||
func sync_ghost_active(pid: int, active_on: bool) -> void:
|
func sync_ghost_active(pid: int, active_on: bool) -> void:
|
||||||
if not active:
|
if not active:
|
||||||
@@ -697,9 +766,14 @@ func sync_ghost_active(pid: int, active_on: bool) -> void:
|
|||||||
hud_ghost_label.text = "👻 Ghosts: %d" % player_ghosts[pid]
|
hud_ghost_label.text = "👻 Ghosts: %d" % player_ghosts[pid]
|
||||||
|
|
||||||
func sync_state_to_player(peer_id: int) -> void:
|
func sync_state_to_player(peer_id: int) -> void:
|
||||||
|
# Ensure the client gets the arena setup before state
|
||||||
|
if can_rpc():
|
||||||
|
rpc_id(peer_id, "sync_arena_setup")
|
||||||
|
|
||||||
# Sync Mekton's current face color
|
# Sync Mekton's current face color
|
||||||
if mekton_node and mekton_node.has_method("set_face_color_rpc"):
|
if mekton_node and is_instance_valid(mekton_node) and mekton_node.has_method("set_face_color_rpc"):
|
||||||
mekton_node.rpc_id(peer_id, "set_face_color_rpc", current_face)
|
if can_rpc():
|
||||||
|
mekton_node.rpc_id(peer_id, "set_face_color_rpc", current_face)
|
||||||
|
|
||||||
# Sync Ghost inventory to make the HUD button visible on player load
|
# Sync Ghost inventory to make the HUD button visible on player load
|
||||||
_update_special_inventory_for_ghosts(peer_id)
|
_update_special_inventory_for_ghosts(peer_id)
|
||||||
@@ -726,7 +800,10 @@ func sync_state_to_player(peer_id: int) -> void:
|
|||||||
var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
var curr_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||||
if curr_pid == pid:
|
if curr_pid == pid:
|
||||||
if player.has_method("sync_candy_stack"):
|
if player.has_method("sync_candy_stack"):
|
||||||
player.rpc_id(peer_id, "sync_candy_stack", colors)
|
if peer_id in multiplayer.get_peers():
|
||||||
|
player.rpc_id(peer_id, "sync_candy_stack", colors)
|
||||||
|
else:
|
||||||
|
player.sync_candy_stack(colors)
|
||||||
break
|
break
|
||||||
|
|
||||||
func _update_special_inventory_for_ghosts(pid: int) -> void:
|
func _update_special_inventory_for_ghosts(pid: int) -> void:
|
||||||
@@ -738,10 +815,16 @@ func _update_special_inventory_for_ghosts(pid: int) -> void:
|
|||||||
var st_manager = player_node.get_node_or_null("SpecialTilesManager")
|
var st_manager = player_node.get_node_or_null("SpecialTilesManager")
|
||||||
if st_manager:
|
if st_manager:
|
||||||
var has_charges = (player_ghosts.get(pid, 0) > 0)
|
var has_charges = (player_ghosts.get(pid, 0) > 0)
|
||||||
if has_charges:
|
if pid in multiplayer.get_peers():
|
||||||
st_manager.rpc_id(pid, "sync_inventory_add", st_manager.SpecialEffect.INVISIBLE_MODE, 8)
|
if has_charges:
|
||||||
|
st_manager.rpc_id(pid, "sync_inventory_add", st_manager.SpecialEffect.INVISIBLE_MODE, 8)
|
||||||
|
else:
|
||||||
|
st_manager.rpc_id(pid, "sync_inventory_remove", st_manager.SpecialEffect.INVISIBLE_MODE)
|
||||||
else:
|
else:
|
||||||
st_manager.rpc_id(pid, "sync_inventory_remove", st_manager.SpecialEffect.INVISIBLE_MODE)
|
if has_charges:
|
||||||
|
st_manager.sync_inventory_add(st_manager.SpecialEffect.INVISIBLE_MODE, 8)
|
||||||
|
else:
|
||||||
|
st_manager.sync_inventory_remove(st_manager.SpecialEffect.INVISIBLE_MODE)
|
||||||
|
|
||||||
@rpc("any_peer", "call_local", "reliable")
|
@rpc("any_peer", "call_local", "reliable")
|
||||||
func add_ghost_charge(pid: int) -> void:
|
func add_ghost_charge(pid: int) -> void:
|
||||||
|
|||||||
@@ -309,6 +309,9 @@ func leave_room() -> void:
|
|||||||
kick_all_clients.rpc()
|
kick_all_clients.rpc()
|
||||||
|
|
||||||
# Important: Reset all lobby settings and player lists first
|
# Important: Reset all lobby settings and player lists first
|
||||||
|
Engine.time_scale = 1.0
|
||||||
|
if get_node_or_null("/root/GoalsCycleManager"):
|
||||||
|
get_node("/root/GoalsCycleManager").reset()
|
||||||
reset()
|
reset()
|
||||||
_stop_lan_broadcast()
|
_stop_lan_broadcast()
|
||||||
|
|
||||||
|
|||||||
@@ -120,39 +120,28 @@ func handle_unhandled_input(event):
|
|||||||
var gm = player.get_node_or_null("/root/Main/CandySurvivalManager")
|
var gm = player.get_node_or_null("/root/Main/CandySurvivalManager")
|
||||||
if gm and gm.active:
|
if gm and gm.active:
|
||||||
var att_id = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
var att_id = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||||
|
|
||||||
|
# Toggle mode: if already in knock mode, turn it off
|
||||||
|
if player.get("is_charged_strike"):
|
||||||
|
player.is_charged_strike = false
|
||||||
|
NotificationManager.send_message(player, "Knock Mode OFF", NotificationManager.MessageType.WARNING)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check charges before toggling on
|
||||||
if gm.player_knocks.get(att_id, 5) <= 0:
|
if gm.player_knocks.get(att_id, 5) <= 0:
|
||||||
NotificationManager.send_message(player, "No Knock charges left!", NotificationManager.MessageType.WARNING)
|
NotificationManager.send_message(player, "No Knock charges left!", NotificationManager.MessageType.WARNING)
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Find a target to knock (adjacent player: cardinal or diagonal distance <= 1)
|
# Toggle knock mode ON (reuses charged strike visuals)
|
||||||
var p_pos = player.current_position
|
player.is_charged_strike = true
|
||||||
var all_players = get_tree().get_nodes_in_group("Players")
|
NotificationManager.send_message(player, "🥊 Knock Mode ON — Walk into a target!", NotificationManager.MessageType.POWERUP)
|
||||||
var target_found = null
|
player.update_active_player_indicator()
|
||||||
var min_dist = 999
|
|
||||||
for other in all_players:
|
|
||||||
if other == player or not is_instance_valid(other): continue
|
|
||||||
var other_pos = other.current_position
|
|
||||||
var dist = max(abs(p_pos.x - other_pos.x), abs(p_pos.y - other_pos.y))
|
|
||||||
if dist <= 1 and dist < min_dist:
|
|
||||||
target_found = other
|
|
||||||
min_dist = dist
|
|
||||||
if dist == 1:
|
|
||||||
break
|
|
||||||
|
|
||||||
if target_found:
|
|
||||||
var tgt_id = target_found.get("peer_id") if "peer_id" in target_found else target_found.name.to_int()
|
|
||||||
if multiplayer.is_server():
|
|
||||||
gm.try_knock(att_id, tgt_id)
|
|
||||||
else:
|
|
||||||
gm.rpc_id(1, "try_knock", att_id, tgt_id)
|
|
||||||
else:
|
|
||||||
# No target, but play the bump animation anyway in place
|
|
||||||
player.rpc("sync_bump", p_pos)
|
|
||||||
|
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|
||||||
if player.powerup_manager:
|
if player.powerup_manager:
|
||||||
player.powerup_manager.use_special_effect()
|
player.powerup_manager.use_special_effect()
|
||||||
if player.get("is_attack_mode") and player.has_method("enter_attack_mode"):
|
if player.get("is_attack_mode") and player.has_method("enter_attack_mode"):
|
||||||
|
|||||||
@@ -215,16 +215,7 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
|||||||
NotificationManager.send_message(player, "Target is Immune!", NotificationManager.MessageType.WARNING)
|
NotificationManager.send_message(player, "Target is Immune!", NotificationManager.MessageType.WARNING)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# Candy Survival: check if attacker has knock charges
|
if not player.get("is_charged_strike") or player.get("is_invisible"):
|
||||||
var has_knock = false
|
|
||||||
var main_for_knock = player.get_tree().root.get_node_or_null("Main")
|
|
||||||
var gm_for_knock = main_for_knock.get("candy_survival_manager") if main_for_knock else null
|
|
||||||
if gm_for_knock and gm_for_knock.active:
|
|
||||||
var att_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
|
||||||
var charges = gm_for_knock.player_knocks.get(att_pid, 0) if gm_for_knock.player_knocks else 0
|
|
||||||
has_knock = charges > 0
|
|
||||||
|
|
||||||
if (not player.get("is_charged_strike") and not has_knock) or player.get("is_invisible"):
|
|
||||||
# Standard bumping effect (Visual only)
|
# Standard bumping effect (Visual only)
|
||||||
print("[Move] Push blocked: Not charged or is Ghost (%s trying to push %s)" % [player.name, other_player.name])
|
print("[Move] Push blocked: Not charged or is Ghost (%s trying to push %s)" % [player.name, other_player.name])
|
||||||
if _can_rpc():
|
if _can_rpc():
|
||||||
@@ -258,12 +249,12 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
|||||||
if gm and gm.active and gm.has_method("try_knock"):
|
if gm and gm.active and gm.has_method("try_knock"):
|
||||||
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
var pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||||
var other_pid = other_player.get("peer_id") if "peer_id" in other_player else other_player.name.to_int()
|
var other_pid = other_player.get("peer_id") if "peer_id" in other_player else other_player.name.to_int()
|
||||||
if gm.try_knock(pid, other_pid):
|
if multiplayer.is_server():
|
||||||
print("[CandySurvival] Player %d knocked %d, stole candies!" % [pid, other_pid])
|
gm.try_knock(pid, other_pid)
|
||||||
else:
|
else:
|
||||||
print("[CandySurvival] Player %d tried to knock %d (backfire or no charges)" % [pid, other_pid])
|
gm.rpc_id(1, "try_knock", pid, other_pid)
|
||||||
if _can_rpc():
|
# Consume knock mode after use (toggle off)
|
||||||
player.rpc("sync_bump", target_pos, false)
|
player.is_charged_strike = false
|
||||||
return false
|
return false
|
||||||
|
|
||||||
# === SUPER PUSH (Attack Mode) ===
|
# === SUPER PUSH (Attack Mode) ===
|
||||||
|
|||||||
@@ -216,10 +216,9 @@ func update_display_name(new_name: String) -> bool:
|
|||||||
emit_signal("profile_updated")
|
emit_signal("profile_updated")
|
||||||
return true
|
return true
|
||||||
|
|
||||||
var formatted_username = new_name.replace(" ", "_").to_lower()
|
|
||||||
var result: NakamaAsyncResult = await NakamaManager.client.update_account_async(
|
var result: NakamaAsyncResult = await NakamaManager.client.update_account_async(
|
||||||
NakamaManager.session,
|
NakamaManager.session,
|
||||||
formatted_username, # username (sync to display name)
|
null, # username (keep existing unique username unchanged)
|
||||||
new_name # display_name
|
new_name # display_name
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -228,6 +227,7 @@ func update_display_name(new_name: String) -> bool:
|
|||||||
return false
|
return false
|
||||||
|
|
||||||
profile["display_name"] = new_name
|
profile["display_name"] = new_name
|
||||||
|
await _save_profile_data()
|
||||||
emit_signal("profile_updated")
|
emit_signal("profile_updated")
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
|
const SafeNakamaMultiplayerBridge = preload("res://scripts/services/safe_nakama_multiplayer_bridge.gd")
|
||||||
|
|
||||||
# Standard Nakama Configuration
|
# Standard Nakama Configuration
|
||||||
var nakama_server_key = OS.get_environment("NAKAMA_SERVER_KEY") if OS.has_environment("NAKAMA_SERVER_KEY") else ProjectSettings.get_setting("network/nakama/server_key", "defaultkey")
|
var nakama_server_key = OS.get_environment("NAKAMA_SERVER_KEY") if OS.has_environment("NAKAMA_SERVER_KEY") else ProjectSettings.get_setting("network/nakama/server_key", "defaultkey")
|
||||||
var nakama_host = OS.get_environment("NAKAMA_HOST") if OS.has_environment("NAKAMA_HOST") else ProjectSettings.get_setting("network/nakama/host", "tektondash.vps.webdock.cloud")
|
var nakama_host = OS.get_environment("NAKAMA_HOST") if OS.has_environment("NAKAMA_HOST") else ProjectSettings.get_setting("network/nakama/host", "tektondash.vps.webdock.cloud")
|
||||||
@@ -141,7 +143,7 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool:
|
|||||||
bridge.leave()
|
bridge.leave()
|
||||||
bridge = null
|
bridge = null
|
||||||
|
|
||||||
bridge = NakamaMultiplayerBridge.new(socket)
|
bridge = SafeNakamaMultiplayerBridge.new(socket)
|
||||||
|
|
||||||
# Connect bridge signals
|
# Connect bridge signals
|
||||||
bridge.match_joined.connect(_on_bridge_match_joined)
|
bridge.match_joined.connect(_on_bridge_match_joined)
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
class_name SafeNakamaMultiplayerBridge
|
||||||
|
extends NakamaMultiplayerBridge
|
||||||
|
|
||||||
|
func _init(p_nakama_socket: NakamaSocket) -> void:
|
||||||
|
super._init(p_nakama_socket)
|
||||||
|
|
||||||
|
func _map_id_to_session(peer_id: int, session_id: String) -> void:
|
||||||
|
_id_map[peer_id] = session_id
|
||||||
|
if not _users.has(session_id):
|
||||||
|
_users[session_id] = NakamaMultiplayerBridge.User.new(null)
|
||||||
|
_users[session_id].peer_id = peer_id
|
||||||
|
|
||||||
|
func _on_multiplayer_peer_packet_generated(peer_id: int, buffer: PackedByteArray) -> void:
|
||||||
|
if match_state == MatchState.CONNECTED:
|
||||||
|
var target_presences = null
|
||||||
|
if peer_id > 0:
|
||||||
|
if not _id_map.has(peer_id) or not _users.has(_id_map[peer_id]) or _users[_id_map[peer_id]].presence == null:
|
||||||
|
push_error("Attempting to send RPC to unknown or uninitialized peer id: %s" % peer_id)
|
||||||
|
return
|
||||||
|
target_presences = [ _users[_id_map[peer_id]].presence ]
|
||||||
|
_nakama_socket.send_match_state_raw_async(_match_id, rpc_op_code, buffer, target_presences)
|
||||||
|
else:
|
||||||
|
push_error("RPC sent while the NakamaMultiplayerBridge isn't connected!")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dfyyc34ekv3yw
|
||||||
@@ -607,7 +607,7 @@ func _on_save_name_pressed() -> void:
|
|||||||
emit_signal("profile_updated")
|
emit_signal("profile_updated")
|
||||||
await get_tree().create_timer(3.0).timeout
|
await get_tree().create_timer(3.0).timeout
|
||||||
status_label.text = ""
|
status_label.text = ""
|
||||||
else:
|
elif status_label.text == "Saving...":
|
||||||
_set_status("Failed to update name.", Color.RED)
|
_set_status("Failed to update name.", Color.RED)
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user