Release 2.4.4: Candy Survival Knock and backfire fixes
Release / Build & Release (push) Successful in 7m54s
Release / Build & Release (push) Successful in 7m54s
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user