Release 2.4.4: Candy Survival Knock and backfire fixes
Release / Build & Release (push) Successful in 7m54s

This commit is contained in:
2026-07-09 15:05:15 +08:00
parent 85cdd5e9d4
commit 6506e9f416
9 changed files with 116 additions and 35 deletions
+5
View File
@@ -1,4 +1,9 @@
## [NEXT]
## [2.4.4] — 2026-07-09
- Fixed Candy Survival Knock mechanic: pressing Q now properly knocks adjacent players diagonally and cardinally.
- Implemented Candy Survival Knock backfire mechanic: knocking a player with 0 candies transfers your candy stack to them and staggers you.
- Fixed Candy Survival candy stack 3D mesh visual desync and client-side stack synchronization.
- Fixed playerboard desync where host-side board changes weren't reflected on remote clients in multiplayer sessions.
- Fixed punch/smack SFX playing repeatedly when quickly attacking another player.
- Fixed currency split between Gacha and Shop — wallet balance now updates immediately after purchase without needing to reopen the panel.
+1 -1
View File
@@ -78,7 +78,7 @@ This document serves as an exhaustive, technical record of all modifications, re
- Fires `try_knock(attacker, target)` when adjacent to an opponent.
- Consumes 1 of 5 starting charges (`player_knocks`).
- *Steal*: If target has candies, attacker adopts target's full stack and color; target is zeroed.
- *Backfire*: If target has 0 candies, attacker loses a charge and both suffer the knock animation state (handled by caller).
- *Backfire*: If target has 0 candies, attacker loses a charge and gets staggered, while any candy stack held by the attacker is transferred to the target.
- **Ghost (Phase-Through)**:
- Input bound natively to `action_grab_tekton` inside `player_input_manager.gd` (bypasses default grab behavior in this mode).
- Consumes 1 of 5 starting charges (`player_ghosts`).
+21 -1
View File
@@ -1,7 +1,27 @@
{
"latest_version": "2.4.3",
"latest_version": "2.4.4",
"minimum_app_version": "2.1.0",
"releases": [
{
"version": "2.4.4",
"date": "2026-07-09",
"pck_url": "https://git.klud.top/danchie/tekton/raw/branch/patches/patch.pck",
"pck_size": 0,
"changelog": [
"Fixed Candy Survival Knock mechanic: pressing Q now properly knocks adjacent players diagonally and cardinally.",
"Implemented Candy Survival Knock backfire mechanic: knocking a player with 0 candies transfers your candy stack to them and staggers you.",
"Fixed Candy Survival candy stack 3D mesh visual desync and client-side stack synchronization.",
"Fixed playerboard desync where host-side board changes weren't reflected on remote clients in multiplayer sessions.",
"Fixed punch/smack SFX playing repeatedly when quickly attacking another player.",
"Fixed currency split between Gacha and Shop \u2014 wallet balance now updates immediately after purchase without needing to reopen the panel.",
"Fixed fatal crash in Multiplayer Gauntlet caused by missing `has_method` check on smack cooldown timers on remote peers.",
"Upgraded engine from Godot 4.6 to 4.7 stable for better performance and stability.",
"Migrated patch delivery from GitHub Pages to Gitea native raw endpoint \u2014 no more external dependencies for game updates.",
"CI: optimized build cache for Godot binary, eliminating repeated 140MB downloads on every workflow run.",
"CI: shallow repository checkout (--depth 1) for faster clone times.",
"CI: removed unnecessary export template download from patch deployment workflow (--export-pack doesn't need them)."
]
},
{
"version": "2.4.3",
"date": "2026-07-04",
+4 -4
View File
@@ -14,7 +14,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/windows/tekton_armageddon_v2.4.3.exe"
export_path="build/windows/tekton_armageddon_v2.4.4.exe"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -85,7 +85,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/tekton-dash-armageddon-v.2.4.3.apk"
export_path="build/tekton-dash-armageddon-v.2.4.4.apk"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -314,7 +314,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/tekton_armageddon_v2.4.3.zip"
export_path="build/tekton_armageddon_v2.4.4.zip"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
@@ -589,7 +589,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="build/linux/tekton_armageddon_v2.4.3.x86_64"
export_path="build/linux/tekton_armageddon_v2.4.4.x86_64"
patches=PackedStringArray()
patch_delta_encoding=false
patch_delta_compression_level_zstd=19
+1 -1
View File
@@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true
[application]
config/name="Tekton Dash Armageddon"
config/version="2.4.3"
config/version="2.4.4"
run/main_scene="res://scenes/ui/boot_screen.tscn"
config/features=PackedStringArray("4.6", "Forward Plus")
boot_splash/bg_color=Color(0.16470589, 0.6745098, 0.9372549, 1)
+16
View File
@@ -1536,6 +1536,13 @@ func sync_game_start(player_list: Array, is_turn_based: bool):
stop_n_go_manager.name = "StopNGoManager"
add_child(stop_n_go_manager)
stop_n_go_manager.activate_client_side()
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, $EnhancedGridMap)
candy_survival_manager.activate_client_side()
# Initialize leaderboard for all peers (after a delay to ensure players loaded)
call_deferred("_deferred_init_leaderboard")
@@ -2791,3 +2798,12 @@ func display_message(message: String, type: int = 0):
if player.has_method("display_message"):
player.display_message(message, type)
break
func get_player_ids() -> Array:
var pids: Array = []
for p in get_tree().get_nodes_in_group("Players"):
if not is_instance_valid(p): continue
var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
if pid != 0 and not pids.has(pid):
pids.append(pid)
return pids
+1
View File
@@ -611,6 +611,7 @@ var _candy_scene: PackedScene = preload("res://scenes/candy.tscn")
func sync_candy_stack(colors: Array) -> void:
var old_stack = get_node_or_null("CandyStack")
if old_stack:
old_stack.name = "CandyStack_Old_" + str(randi())
old_stack.queue_free()
if colors.is_empty() or not _candy_scene: return
+57 -22
View File
@@ -148,8 +148,6 @@ func _define_sticky_walls() -> void:
sticky_cells[Vector2i(NPC_CENTER.x + dx, NPC_CENTER.y + dy)] = true
func start_game_mode() -> void:
if multiplayer.is_server():
activate_client_side()
active = true
current_face = CandyColor.HEART
face_timer = 0.0
@@ -183,6 +181,9 @@ func start_game_mode() -> void:
rpc("sync_knock_charge_count", pid, START_KNOCK)
rpc("sync_ghost_charge_count", pid, START_GHOST)
if multiplayer.is_server():
activate_client_side()
print("[CandySurvival] Started with %d players" % pids.size())
func _enter_tree() -> void:
@@ -330,15 +331,6 @@ func _update_candy_badge(pid: int) -> void:
var face_match = (color != -1)
var score = player_score.get(pid, 0)
rpc("sync_candy_badge", pid, count, mult, face_match, score, colors)
# 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", colors)
break
func get_multiplier(pid: int) -> float:
var count = player_candies.get(pid, 0)
@@ -395,31 +387,49 @@ func try_knock(attacker: int, target: int) -> bool:
return false
if player_ghost_active.get(target, false):
return false
if player_knocks.get(attacker, 0) <= 0:
if not player_knocks.has(attacker):
player_knocks[attacker] = START_KNOCK
if player_knocks[attacker] <= 0:
return false
var target_colors = player_candy_colors.get(target, [])
var target_candies = target_colors.size()
var attacker_node = main_scene.get_node_or_null(str(attacker))
var target_node = main_scene.get_node_or_null(str(target))
var attacker_node = _find_player_node(attacker)
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:
# Backfire: both lose a charge
player_knocks[attacker] = player_knocks.get(attacker, 0) - 1
# Backfire: attacker loses a charge & gets staggered
player_knocks[attacker] = player_knocks.get(attacker, START_KNOCK) - 1
rpc("sync_knock_charge_count", attacker, player_knocks[attacker])
# Attacker is also knocked down briefly (backfire)
# 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!
var attacker_colors = player_candy_colors.get(attacker, [])
var attacker_candies = attacker_colors.size()
if attacker_candies > 0:
if not player_candy_colors.has(target):
player_candy_colors[target] = []
player_candy_colors[target].append_array(attacker_colors)
player_candy_colors[attacker] = []
player_candies[target] = player_candy_colors[target].size()
player_candies[attacker] = 0
_add_score(target, attacker_candies * 100)
_update_candy_badge(attacker)
_update_candy_badge(target)
rpc("sync_knock_result", attacker, target, 0)
return false
# Steal all candies
# Steal all candies (Successful knock)
if not player_candy_colors.has(attacker):
player_candy_colors[attacker] = []
player_candy_colors[attacker].append_array(target_colors)
@@ -428,7 +438,7 @@ func try_knock(attacker: int, target: int) -> bool:
player_candies[attacker] = player_candy_colors[attacker].size()
player_candies[target] = 0
player_knocks[attacker] = player_knocks.get(attacker, 0) - 1
player_knocks[attacker] = player_knocks.get(attacker, START_KNOCK) - 1
rpc("sync_knock_charge_count", attacker, player_knocks[attacker])
player_last_knocked_by[target] = attacker
@@ -567,7 +577,24 @@ func _add_score(pid: int, points: int) -> void:
func _get_player_ids() -> Array:
if main_scene and main_scene.has_method("get_player_ids"):
return main_scene.get_player_ids()
return []
var pids: Array = []
for p in get_tree().get_nodes_in_group("Players"):
if not is_instance_valid(p): continue
var pid = p.get("peer_id") if "peer_id" in p else p.name.to_int()
if pid != 0 and not pids.has(pid):
pids.append(pid)
return pids
func _find_player_node(pid: int) -> Node:
if main_scene:
var n = main_scene.get_node_or_null(str(pid))
if n: return n
for p in get_tree().get_nodes_in_group("Players"):
if not is_instance_valid(p): continue
var p_id = p.get("peer_id") if "peer_id" in p else p.name.to_int()
if p_id == pid:
return p
return null
func candy_survival_round_duration() -> int:
return 180
@@ -577,10 +604,18 @@ func get_score(pid: int) -> int:
# ── RPCs (stubs — HUD client handles visuals) ──
@rpc("authority", "call_remote", "unreliable")
@rpc("authority", "call_local", "reliable")
func sync_candy_badge(pid: int, count: int, mult: float, face_match: bool, score: int, colors: Array) -> void:
if not active:
activate_client_side()
player_candies[pid] = count
player_candy_colors[pid] = colors
player_score[pid] = score
var p_node = _find_player_node(pid)
if p_node and p_node.has_method("sync_candy_stack"):
p_node.sync_candy_stack(colors)
if pid == multiplayer.get_unique_id():
_last_badge_count = count
_last_badge_mult = mult
@@ -644,7 +679,7 @@ func _restore_delivery_indicator() -> void:
else:
hud_delivery_indicator.text = ""
@rpc("authority", "call_remote", "unreliable")
@rpc("authority", "call_local", "reliable")
func sync_knock_result(attacker: int, target: int, candies: int) -> void:
if not active:
activate_client_side()
+10 -6
View File
@@ -111,7 +111,7 @@ func handle_unhandled_input(event):
# 2. Action Buttons (Remappable via InputMap / KEY_Q override for Candy Survival)
var is_knock = false
if LobbyManager.game_mode == "Candy Survival":
is_knock = (event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_Q) or event.is_action_pressed("action_knock_tekton")
is_knock = (event is InputEventKey and event.pressed and not event.echo and (event.keycode == KEY_Q or event.physical_keycode == KEY_Q)) or event.is_action_pressed("action_knock_tekton")
else:
is_knock = event.is_action_pressed("action_knock_tekton")
@@ -120,21 +120,25 @@ func handle_unhandled_input(event):
var gm = player.get_node_or_null("/root/Main/CandySurvivalManager")
if gm and gm.active:
var att_id = player.get("peer_id") if "peer_id" in player else player.name.to_int()
if gm.player_knocks.get(att_id, 0) <= 0:
if gm.player_knocks.get(att_id, 5) <= 0:
NotificationManager.send_message(player, "No Knock charges left!", NotificationManager.MessageType.WARNING)
get_viewport().set_input_as_handled()
return
# Find a target to knock (first adjacent player)
# Find a target to knock (adjacent player: cardinal or diagonal distance <= 1)
var p_pos = player.current_position
var all_players = get_tree().get_nodes_in_group("Players")
var target_found = null
var min_dist = 999
for other in all_players:
if other == player: continue
if other == player or not is_instance_valid(other): continue
var other_pos = other.current_position
if abs(p_pos.x - other_pos.x) + abs(p_pos.y - other_pos.y) == 1:
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
break
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()