feat(candy-survival): toggle-and-walk knock mode, unified animations, score rewards, RPC fix
- Changed knock from instant-ambush to toggle-and-walk: press Q to toggle Knock Mode ON, walk into target to execute, consumed on use - Unified knock animation with other game modes (attack_mode SFX, bump, 3-tile knockback, stagger) - Added +100 score rewards for completing goals, KNOCK, and GHOST - Fixed RPC unknown peer ID crash for AI bots in ghost/candy stack sync - Bots auto-toggle is_charged_strike when they have knock charges - Fixed profile panel username update on connected peers - Added SafeNakamaMultiplayerBridge for match state crash prevention
This commit is contained in:
@@ -283,6 +283,7 @@ func on_blueprint_completed(pid: int, primary_tile_id: int, off_color: bool) ->
|
||||
break
|
||||
|
||||
_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
|
||||
if main_scene and player_node:
|
||||
@@ -403,23 +404,12 @@ func try_knock(attacker: int, target: int) -> bool:
|
||||
|
||||
var attacker_node = _find_player_node(attacker)
|
||||
var target_node = _find_player_node(target)
|
||||
|
||||
if attacker_node and target_node:
|
||||
if attacker_node.has_method("can_rpc") and attacker_node.can_rpc():
|
||||
attacker_node.rpc("sync_bump", target_node.current_position)
|
||||
|
||||
if target_candies == 0:
|
||||
# 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 knocked down briefly (backfire)
|
||||
if attacker_node:
|
||||
if attacker_node.has_method("can_rpc") and attacker_node.can_rpc():
|
||||
attacker_node.rpc("apply_stagger", 2.0)
|
||||
elif attacker_node.has_method("apply_stagger"):
|
||||
attacker_node.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()
|
||||
@@ -452,14 +442,7 @@ func try_knock(attacker: int, target: int) -> bool:
|
||||
rpc("sync_knock_charge_count", attacker, player_knocks[attacker])
|
||||
player_last_knocked_by[target] = attacker
|
||||
|
||||
# Target is knocked down / staggered on successful knock
|
||||
if target_node:
|
||||
if target_node.has_method("can_rpc") and target_node.can_rpc():
|
||||
target_node.rpc("apply_stagger", 2.0)
|
||||
elif target_node.has_method("apply_stagger"):
|
||||
target_node.apply_stagger(2.0)
|
||||
|
||||
_add_score(attacker, target_candies * 100)
|
||||
_add_score(attacker, target_candies * 100 + 100) # Award +100 points for using KNOCK + 100 per candy stolen
|
||||
_update_candy_badge(attacker)
|
||||
_update_candy_badge(target)
|
||||
|
||||
@@ -493,6 +476,7 @@ func try_activate_ghost(pid: int) -> bool:
|
||||
player.sync_modulate(Color(1.0, 1.0, 1.0, 0.4))
|
||||
break
|
||||
|
||||
_add_score(pid, 100) # Award +100 points for tactically using GHOST
|
||||
return true
|
||||
|
||||
func is_ghost_active(pid: int) -> bool:
|
||||
@@ -716,6 +700,63 @@ func sync_knock_result(attacker: int, target: int, candies: int) -> void:
|
||||
if hud_knock_label and player_knocks.has(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")
|
||||
func sync_ghost_active(pid: int, active_on: bool) -> void:
|
||||
if not active:
|
||||
@@ -759,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()
|
||||
if curr_pid == pid:
|
||||
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
|
||||
|
||||
func _update_special_inventory_for_ghosts(pid: int) -> void:
|
||||
@@ -771,10 +815,16 @@ func _update_special_inventory_for_ghosts(pid: int) -> void:
|
||||
var st_manager = player_node.get_node_or_null("SpecialTilesManager")
|
||||
if st_manager:
|
||||
var has_charges = (player_ghosts.get(pid, 0) > 0)
|
||||
if has_charges:
|
||||
st_manager.rpc_id(pid, "sync_inventory_add", st_manager.SpecialEffect.INVISIBLE_MODE, 8)
|
||||
if pid in multiplayer.get_peers():
|
||||
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:
|
||||
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")
|
||||
func add_ghost_charge(pid: int) -> void:
|
||||
|
||||
Reference in New Issue
Block a user