feat: update
This commit is contained in:
@@ -442,28 +442,44 @@ func logout() -> void:
|
||||
# =============================================================================
|
||||
|
||||
func _connect_socket() -> bool:
|
||||
if not NakamaManager.session:
|
||||
push_error("[AuthManager] Socket connection failed: no Nakama session")
|
||||
return false
|
||||
|
||||
if NakamaManager.socket and NakamaManager.socket.is_connected_to_host():
|
||||
if not multiplayer.has_multiplayer_peer() and NakamaManager.bridge:
|
||||
multiplayer.set_multiplayer_peer(NakamaManager.bridge.multiplayer_peer)
|
||||
NakamaManager.connected_to_nakama.emit()
|
||||
return true
|
||||
|
||||
|
||||
if NakamaManager.socket:
|
||||
NakamaManager.socket.close()
|
||||
NakamaManager.socket = null
|
||||
|
||||
NakamaManager.socket = Nakama.create_socket_from(NakamaManager.client)
|
||||
var result = await NakamaManager.socket.connect_async(NakamaManager.session)
|
||||
|
||||
|
||||
if result.is_exception():
|
||||
push_error("[AuthManager] Socket connection failed: " + result.get_exception().message)
|
||||
var exception = result.get_exception()
|
||||
var error_message = "Socket connection failed"
|
||||
if exception and not exception.message.is_empty():
|
||||
error_message = exception.message
|
||||
elif exception and exception.status_code >= 0:
|
||||
error_message = "Socket connection failed with error code %s" % exception.status_code
|
||||
push_error("[AuthManager] " + error_message)
|
||||
NakamaManager.socket.close()
|
||||
NakamaManager.socket = null
|
||||
return false
|
||||
|
||||
|
||||
# Initialize multiplayer bridge
|
||||
NakamaManager.bridge = NakamaMultiplayerBridge.new(NakamaManager.socket)
|
||||
NakamaManager.bridge.match_joined.connect(NakamaManager._on_bridge_match_joined)
|
||||
NakamaManager.bridge.match_join_error.connect(NakamaManager._on_bridge_match_join_error)
|
||||
multiplayer.set_multiplayer_peer(NakamaManager.bridge.multiplayer_peer)
|
||||
|
||||
|
||||
# Notify other systems that Nakama socket is ready
|
||||
NakamaManager.connected_to_nakama.emit()
|
||||
|
||||
|
||||
return true
|
||||
|
||||
func _load_user_profile() -> void:
|
||||
|
||||
@@ -99,6 +99,10 @@ const SMACK_CHARGE_WINDOW: float = 3.0
|
||||
|
||||
var player_mission_completions: Dictionary = {} # player_id → int
|
||||
var player_cleansers: Dictionary = {} # player_id → int (0 or 1)
|
||||
var cleanser_active: Dictionary = {} # player_id → true when immunity active
|
||||
var cleanser_cells_left: Dictionary = {} # player_id → int (cells remaining)
|
||||
const CLEANSER_MAX_CELLS: int = 5
|
||||
const CLEANSER_ACTIVATION_DELAY: float = 0.3
|
||||
|
||||
# =============================================================================
|
||||
# Trapped Players
|
||||
@@ -131,6 +135,7 @@ var phase_label: Label
|
||||
var cleanser_label: Label
|
||||
var cleanser_icon: TextureRect
|
||||
var cleanser_count: int = 0
|
||||
var slowmo_label: Label
|
||||
var _gauntlet_hud_scene: PackedScene = preload("res://scenes/gauntlet_hud.tscn")
|
||||
|
||||
# =============================================================================
|
||||
@@ -763,13 +768,13 @@ func clear_sticky_cell(pos: Vector2i) -> void:
|
||||
main_scene.rpc("sync_grid_item", pos.x, 2, pos.y, -1)
|
||||
|
||||
func _try_use_cleanser() -> void:
|
||||
"""Local player attempts to use Cleanser on adjacent sticky cells."""
|
||||
"""Local player attempts to activate Cleanser for 5-cell sticky immunity."""
|
||||
var local_pid = multiplayer.get_unique_id()
|
||||
var count = player_cleansers.get(local_pid, 0)
|
||||
if count <= 0:
|
||||
return
|
||||
|
||||
# Find local player
|
||||
# Block activation during stun
|
||||
var all_players = get_tree().get_nodes_in_group("Players")
|
||||
var local_player = null
|
||||
for p in all_players:
|
||||
@@ -777,41 +782,75 @@ func _try_use_cleanser() -> void:
|
||||
if pid == local_pid:
|
||||
local_player = p
|
||||
break
|
||||
if not local_player or not gridmap:
|
||||
if not local_player:
|
||||
return
|
||||
if local_player.get("is_frozen") or local_player.get("is_stop_frozen"):
|
||||
return
|
||||
# Already active
|
||||
if cleanser_active.has(local_pid):
|
||||
return
|
||||
|
||||
# Get player grid position
|
||||
var player_pos = local_player.global_position
|
||||
var grid_pos = Vector2i(int(player_pos.x), int(player_pos.z))
|
||||
# 0.3s activation delay
|
||||
await get_tree().create_timer(CLEANSER_ACTIVATION_DELAY).timeout
|
||||
|
||||
# Clear sticky cells in 3x3 area around player
|
||||
var cleared_any = false
|
||||
for dx in range(-1, 2):
|
||||
for dz in range(-1, 2):
|
||||
var check_pos = grid_pos + Vector2i(dx, dz)
|
||||
if sticky_cells.has(check_pos):
|
||||
if multiplayer.is_server():
|
||||
clear_sticky_cell(check_pos)
|
||||
else:
|
||||
rpc("rpc_use_cleanser", check_pos)
|
||||
cleared_any = true
|
||||
# Re-validate after delay
|
||||
if not is_instance_valid(local_player):
|
||||
return
|
||||
if local_player.get("is_frozen") or local_player.get("is_stop_frozen"):
|
||||
return
|
||||
|
||||
if cleared_any:
|
||||
# Consume cleanser
|
||||
player_cleansers[local_pid] = 0
|
||||
update_cleanser_ui(0)
|
||||
# Trigger slow-mo for dramatic effect
|
||||
if multiplayer.is_server():
|
||||
trigger_slowmo()
|
||||
else:
|
||||
rpc("rpc_trigger_slowmo")
|
||||
# Notify server if we're a client
|
||||
if not multiplayer.is_server() and _can_rpc():
|
||||
rpc("rpc_consume_cleanser", local_pid)
|
||||
elif multiplayer.is_server():
|
||||
# Sync to all clients
|
||||
# Activate cleanser immunity
|
||||
cleanser_active[local_pid] = true
|
||||
cleanser_cells_left[local_pid] = CLEANSER_MAX_CELLS
|
||||
|
||||
# Consume cleanser from inventory
|
||||
player_cleansers[local_pid] = 0
|
||||
update_cleanser_ui(0)
|
||||
|
||||
# Sync to server/clients
|
||||
if not multiplayer.is_server() and _can_rpc():
|
||||
rpc("rpc_activate_cleanser", local_pid)
|
||||
elif multiplayer.is_server():
|
||||
if _can_rpc():
|
||||
rpc("sync_cleanser_count", local_pid, 0)
|
||||
|
||||
# Trigger slow-mo for dramatic effect
|
||||
if multiplayer.is_server():
|
||||
trigger_slowmo()
|
||||
else:
|
||||
rpc("rpc_trigger_slowmo")
|
||||
|
||||
NotificationManager.send_message(local_player, "Cleanser Active! (5 cells)", NotificationManager.MessageType.POWERUP)
|
||||
|
||||
func deactivate_cleanser(player_id: int) -> void:
|
||||
"""Deactivate cleanser immunity for a player."""
|
||||
cleanser_active.erase(player_id)
|
||||
cleanser_cells_left.erase(player_id)
|
||||
|
||||
func is_cleanser_active(player_id: int) -> bool:
|
||||
"""Check if a player has active cleanser immunity."""
|
||||
return cleanser_active.has(player_id)
|
||||
|
||||
func use_cleanser_cell(player_id: int) -> bool:
|
||||
"""Use one cleanser cell. Returns true if still active, false if exhausted."""
|
||||
if not cleanser_active.has(player_id):
|
||||
return false
|
||||
cleanser_cells_left[player_id] -= 1
|
||||
if cleanser_cells_left[player_id] <= 0:
|
||||
deactivate_cleanser(player_id)
|
||||
return false
|
||||
return true
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func rpc_activate_cleanser(pid: int) -> void:
|
||||
"""RPC for clients to activate cleanser on server."""
|
||||
if multiplayer.is_server():
|
||||
if not cleanser_active.has(pid):
|
||||
cleanser_active[pid] = true
|
||||
cleanser_cells_left[pid] = CLEANSER_MAX_CELLS
|
||||
player_cleansers[pid] = 0
|
||||
if _can_rpc():
|
||||
rpc("sync_cleanser_count", local_pid, 0)
|
||||
rpc("sync_cleanser_count", pid, 0)
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func rpc_use_cleanser(pos: Vector2i) -> void:
|
||||
@@ -848,6 +887,9 @@ func trigger_slowmo(duration: float = 4.0) -> void:
|
||||
# Show visual overlay
|
||||
if main_scene and main_scene.has_node("Camera3D200"):
|
||||
_show_slowmo_overlay()
|
||||
# Show slow-mo HUD label
|
||||
if slowmo_label:
|
||||
slowmo_label.visible = true
|
||||
if _can_rpc():
|
||||
rpc("sync_slowmo_start", duration)
|
||||
|
||||
@@ -855,6 +897,9 @@ func _end_slowmo() -> void:
|
||||
slowmo_active = false
|
||||
Engine.time_scale = 1.0
|
||||
_hide_slowmo_overlay()
|
||||
# Hide slow-mo HUD label
|
||||
if slowmo_label:
|
||||
slowmo_label.visible = false
|
||||
if _can_rpc():
|
||||
rpc("sync_slowmo_end")
|
||||
|
||||
@@ -890,6 +935,8 @@ func sync_slowmo_start(duration: float) -> void:
|
||||
slowmo_timer = duration
|
||||
Engine.time_scale = SLOWMO_SCALE
|
||||
_show_slowmo_overlay()
|
||||
if slowmo_label:
|
||||
slowmo_label.visible = true
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_slowmo_end() -> void:
|
||||
@@ -907,6 +954,7 @@ func _setup_hud() -> void:
|
||||
phase_label = hud_layer.get_node("TopContainer/PhaseLabel")
|
||||
cleanser_icon = hud_layer.get_node("BottomContainer/CleanserHBox/CleanserIcon")
|
||||
cleanser_label = hud_layer.get_node("BottomContainer/CleanserHBox/CleanserLabel")
|
||||
slowmo_label = hud_layer.get_node_or_null("TopContainer/SlowMoLabel")
|
||||
_generate_cleanser_icon()
|
||||
|
||||
func _generate_cleanser_icon() -> void:
|
||||
@@ -995,6 +1043,7 @@ func _on_goal_count_updated(peer_id: int, count: int) -> void:
|
||||
player_cleansers[peer_id] = 0
|
||||
if player_cleansers[peer_id] < 1:
|
||||
player_cleansers[peer_id] = 1
|
||||
emit_signal("cleanser_granted", peer_id)
|
||||
print("[Gauntlet] Player %d granted Cleanser (mission %d)" % [peer_id, completions])
|
||||
|
||||
# Respawn mission tiles in non-sticky locations
|
||||
|
||||
@@ -725,16 +725,40 @@ func set_game_mode(mode: String) -> void:
|
||||
rpc("sync_game_mode", mode)
|
||||
|
||||
_update_available_areas(mode)
|
||||
|
||||
# Only force switch the area if the selected area is NOT valid for this mode
|
||||
if selected_area not in available_areas:
|
||||
set_area(available_areas[0])
|
||||
else:
|
||||
# Important: even if the area is technically in the list, if they just clicked Free Mode
|
||||
# we should default them to Free Mode Area if they were on Stop n Go Area before.
|
||||
if mode == "Free Mode" and "Free Mode Area" in available_areas:
|
||||
set_area("Free Mode Area")
|
||||
elif mode == "Stop n Go" and "Stop n Go Area" in available_areas:
|
||||
set_area("Stop n Go Area")
|
||||
elif mode == "Tekton Doors" and "Tekton Doors Area" in available_areas:
|
||||
set_area("Tekton Doors Area")
|
||||
elif mode == "Gauntlet" and "Candy Pump Arena" in available_areas:
|
||||
set_area("Candy Pump Arena")
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func sync_game_mode(mode: String) -> void:
|
||||
"""Sync game mode selection from host to clients."""
|
||||
game_mode = mode
|
||||
_update_available_areas(mode)
|
||||
if selected_area not in available_areas:
|
||||
|
||||
# Try to smart-match the client's local area to the mode as well so their UI matches
|
||||
if mode == "Free Mode" and "Free Mode Area" in available_areas:
|
||||
selected_area = "Free Mode Area"
|
||||
elif mode == "Stop n Go" and "Stop n Go Area" in available_areas:
|
||||
selected_area = "Stop n Go Area"
|
||||
elif mode == "Tekton Doors" and "Tekton Doors Area" in available_areas:
|
||||
selected_area = "Tekton Doors Area"
|
||||
elif mode == "Gauntlet" and "Candy Pump Arena" in available_areas:
|
||||
selected_area = "Candy Pump Arena"
|
||||
elif selected_area not in available_areas:
|
||||
selected_area = available_areas[0]
|
||||
|
||||
emit_signal("game_mode_changed", mode)
|
||||
|
||||
func start_game(force: bool = false) -> void:
|
||||
|
||||
@@ -143,9 +143,9 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
print("[Move] Failed: Player is trapped in a sticky cell")
|
||||
return false
|
||||
|
||||
# Check for Tekton interaction (Knock Mode)
|
||||
# If moving into a Tekton's space while in Knock Mode, trigger knock
|
||||
if player.get("is_knock_mode"):
|
||||
# Check for Tekton interaction (Charged Strike Mode)
|
||||
# If moving into a Tekton's space while Charged, trigger knock
|
||||
if player.get("is_charged_strike"):
|
||||
# Find Tekton at grid_position
|
||||
var tektons = player.get_tree().get_nodes_in_group("Tektons")
|
||||
for t in tektons:
|
||||
@@ -154,12 +154,19 @@ func simple_move_to(grid_position: Vector2i) -> bool:
|
||||
player.knock_tekton()
|
||||
return false # Don't move into the tile, just knock
|
||||
|
||||
# If moving into a sticky cell, trigger trap
|
||||
# If moving into a sticky cell, trigger trap (unless cleanser active)
|
||||
if gm and gm.is_active and gm.is_sticky_cell(grid_position):
|
||||
print("[Move] Player stepping into sticky cell at %s" % grid_position)
|
||||
movement_queue.clear()
|
||||
if player.is_multiplayer_authority() or multiplayer.is_server():
|
||||
gm._trap_player(player)
|
||||
var pid = player.get("peer_id") if "peer_id" in player else -1
|
||||
if pid != -1 and gm.is_cleanser_active(pid):
|
||||
# Cleanser immunity: clear sticky cell, use one cell, don't trap
|
||||
gm.clear_sticky_cell(grid_position)
|
||||
gm.use_cleanser_cell(pid)
|
||||
print("[Move] Cleanser cleared sticky cell at %s (%d cells left)" % [grid_position, gm.cleanser_cells_left.get(pid, 0)])
|
||||
else:
|
||||
print("[Move] Player stepping into sticky cell at %s" % grid_position)
|
||||
movement_queue.clear()
|
||||
if player.is_multiplayer_authority() or multiplayer.is_server():
|
||||
gm._trap_player(player)
|
||||
|
||||
rotate_towards_target(grid_position)
|
||||
|
||||
@@ -212,9 +219,9 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
||||
var att_pid = player.get("peer_id") if "peer_id" in player else player.name.to_int()
|
||||
has_smack = gm_for_smack.has_smack_charged(att_pid)
|
||||
|
||||
if (not player.get("is_attack_mode") and not has_smack) or player.get("is_invisible"):
|
||||
if (not player.get("is_charged_strike") and not has_smack) or player.get("is_invisible"):
|
||||
# Standard bumping effect (Visual only)
|
||||
print("[Move] Push blocked: Not in attack mode 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():
|
||||
player.rpc("sync_bump", target_pos, true) # Soft bump
|
||||
elif player.has_method("sync_bump"):
|
||||
@@ -334,9 +341,16 @@ func try_push(target_pos: Vector2i, direction: Vector2i) -> bool:
|
||||
if main_sticky and main_sticky.get("gauntlet_manager"):
|
||||
var gm_sticky = main_sticky.gauntlet_manager
|
||||
if gm_sticky.is_active and gm_sticky.is_sticky_cell(pushed_to_pos):
|
||||
print("[Move] Player pushed into sticky cell at %s" % pushed_to_pos)
|
||||
if multiplayer.is_server() or other_player.is_multiplayer_authority():
|
||||
gm_sticky._trap_player(other_player)
|
||||
var push_pid = other_player.get("peer_id") if "peer_id" in other_player else -1
|
||||
if push_pid != -1 and gm_sticky.is_cleanser_active(push_pid):
|
||||
# Cleanser immunity: clear sticky cell, use one cell
|
||||
gm_sticky.clear_sticky_cell(pushed_to_pos)
|
||||
gm_sticky.use_cleanser_cell(push_pid)
|
||||
print("[Move] Cleanser cleared push-into-sticky at %s" % pushed_to_pos)
|
||||
else:
|
||||
print("[Move] Player pushed into sticky cell at %s" % pushed_to_pos)
|
||||
if multiplayer.is_server() or other_player.is_multiplayer_authority():
|
||||
gm_sticky._trap_player(other_player)
|
||||
|
||||
# 2. Apply freeze/stun effect
|
||||
var stun_duration = 1.0 if (gm_push and gm_push.is_active) else 1.5
|
||||
|
||||
@@ -159,18 +159,18 @@ func use_special_effect() -> bool:
|
||||
|
||||
# Restriction: Cannot use attack mode while carrying a Tekton or in Ghost mode
|
||||
if player.is_carrying_tekton:
|
||||
NotificationManager.send_message(player, "Cannot enter Attack Mode while carrying a Tekton!", NotificationManager.MessageType.WARNING)
|
||||
NotificationManager.send_message(player, "Cannot use Charged Strike while carrying a Tekton!", NotificationManager.MessageType.WARNING)
|
||||
return false
|
||||
|
||||
if player.get("is_invisible"):
|
||||
NotificationManager.send_message(player, "Cannot enter Attack Mode while in Ghost mode!", NotificationManager.MessageType.WARNING)
|
||||
NotificationManager.send_message(player, "Cannot use Charged Strike while in Ghost mode!", NotificationManager.MessageType.WARNING)
|
||||
return false
|
||||
|
||||
# Enable Attack Mode explicitly
|
||||
player.is_attack_mode = true
|
||||
# Do NOT consume boost here. Boost acts as "fuel" for the attacks.
|
||||
# Notification check handled by caller or signal
|
||||
return true
|
||||
if player.has_method("enter_charged_strike"):
|
||||
player.enter_charged_strike()
|
||||
return true
|
||||
|
||||
return false
|
||||
|
||||
func consume_boost(amount: float):
|
||||
"""Consume a specific amount of boost."""
|
||||
|
||||
@@ -289,7 +289,9 @@ func get_action_display(action_key: String) -> String:
|
||||
"grab": "ctrl_grab",
|
||||
"use_powerup": "ctrl_use_powerup",
|
||||
"tekton_grab": "ctrl_tekton_grab",
|
||||
"action_grab_tekton": "ctrl_tekton_grab",
|
||||
"attack_mode": "ctrl_attack_mode",
|
||||
"action_knock_tekton": "ctrl_attack_mode",
|
||||
}
|
||||
if ctrl_key_map.has(action_key):
|
||||
return get_controller_binding_text(ctrl_key_map[action_key])
|
||||
@@ -315,7 +317,16 @@ func is_controller_button_used(button_index: int) -> String:
|
||||
return ""
|
||||
|
||||
func get_control_keycode(action_name: String) -> int:
|
||||
return settings.controls.get(action_name, -1)
|
||||
# Map friendly names to their internal settings.controls keys
|
||||
var mapped_name = action_name
|
||||
if action_name == "tekton_grab":
|
||||
mapped_name = "action_grab_tekton"
|
||||
elif action_name == "attack_mode":
|
||||
mapped_name = "action_knock_tekton"
|
||||
elif action_name == "grab":
|
||||
mapped_name = "action_grab"
|
||||
|
||||
return settings.controls.get(mapped_name, -1)
|
||||
|
||||
func get_control_text(action_name: String) -> String:
|
||||
var code = get_control_keycode(action_name)
|
||||
|
||||
@@ -354,9 +354,9 @@ func _run_tutorial() -> void:
|
||||
if spawned_bot.get("is_knock_mode") == true:
|
||||
break
|
||||
|
||||
# Fallback: if player's attack mode deactivated early despite having timer left, they likely successfully rammed
|
||||
var t = local_player.get("attack_mode_timer")
|
||||
if local_player.get("is_attack_mode") == false and t != null and t > 0.0 and t < 3.0:
|
||||
# Fallback: if player's charged strike deactivated early despite having timer left, they likely successfully rammed
|
||||
var t = local_player.get("charged_strike_timer")
|
||||
if local_player.get("is_charged_strike") == false and t != null and t > 0.0 and t < 3.0:
|
||||
break
|
||||
|
||||
await get_tree().process_frame
|
||||
|
||||
@@ -123,8 +123,16 @@ func connect_to_nakama_async(email: String = "", password: String = "") -> bool:
|
||||
return false
|
||||
elif socket_result.is_exception():
|
||||
var err = socket_result.get_exception()
|
||||
printerr("[NakamaManager] Socket Error: %s (Code: %s)" % [err.message, err.status_code])
|
||||
emit_signal("connection_failed", err.message)
|
||||
var err_msg = "Socket connection failed"
|
||||
if err and not err.message.is_empty():
|
||||
err_msg = err.message
|
||||
elif err and err.status_code >= 0:
|
||||
err_msg = "Socket connection failed with code %s" % err.status_code
|
||||
printerr("[NakamaManager] Socket Error: %s (Code: %s)" % [err_msg, err.status_code if err else -1])
|
||||
emit_signal("connection_failed", err_msg)
|
||||
if socket:
|
||||
socket.close()
|
||||
socket = null
|
||||
return false
|
||||
|
||||
# 3. Initialize Multiplayer Bridge
|
||||
|
||||
+16
-1
@@ -414,7 +414,16 @@ func spawn_tiles_around(count: int = 4):
|
||||
# FIX 1: Make tekton look/rotate toward a random spawning direction
|
||||
if not is_carried and not is_thrown:
|
||||
var random_angle = rng.randf_range(0, TAU)
|
||||
rotation.y = random_angle
|
||||
|
||||
# If it's a static turret, make it face the target tile it's about to spawn instead
|
||||
if is_static_turret:
|
||||
# We don't have a specific target yet, but we can pick an average direction
|
||||
# Or just let it throw randomly like the others. Wait, the user wants:
|
||||
# "static tekton, should facing toward where they're going to thrown the tiles"
|
||||
# We'll calculate rotation inside the spawning loop for static turrets.
|
||||
pass
|
||||
else:
|
||||
rotation.y = random_angle
|
||||
|
||||
# Play throw animation
|
||||
if not is_carried and not is_thrown:
|
||||
@@ -439,6 +448,12 @@ func spawn_tiles_around(count: int = 4):
|
||||
|
||||
var pos = current_position + Vector2i(x, y)
|
||||
|
||||
# For static turret, update rotation to face the exact tile being thrown
|
||||
if is_static_turret and not is_carried and not is_thrown:
|
||||
var throw_dir = Vector3(x, 0, y).normalized()
|
||||
if throw_dir.length_squared() > 0.01:
|
||||
rotation.y = atan2(throw_dir.x, throw_dir.z)
|
||||
|
||||
# Don't overwrite the Tekton's own cell? Or do?
|
||||
# Maybe avoid center.
|
||||
if x == 0 and y == 0: continue
|
||||
|
||||
Reference in New Issue
Block a user