feat: directly retrieve files to restore missing functions from commit

This commit is contained in:
2026-07-14 15:22:28 +08:00
parent 5ac4256972
commit b01ccf88b0
6 changed files with 246 additions and 114 deletions
+88 -67
View File
@@ -70,25 +70,23 @@ var is_carrying_tekton: bool = false:
emit_signal("tekton_carried_changed", value)
# Visual/Logic side effects if any
var is_attack_mode: bool = false:
var is_charged_strike: bool = false:
set(value):
if is_attack_mode == value:
return # Prevent infinite recursion / redundant updates
if is_charged_strike == value:
return
is_attack_mode = value
if is_attack_mode:
attack_mode_timer = MAX_ATTACK_MODE_TIME
is_charged_strike = value
if is_charged_strike:
charged_strike_timer = MAX_CHARGED_STRIKE_TIME
_refresh_player_visuals()
# Sync to others if we are the authority
if is_multiplayer_authority() and can_rpc():
rpc("sync_attack_mode", is_attack_mode)
rpc("sync_charged_strike", is_charged_strike)
@rpc("any_peer", "call_local", "reliable")
func sync_attack_mode(state: bool):
# We WANT to trigger the setter to apply visuals on clients
# Using self.var triggers setter in GDScript
is_attack_mode = state
func sync_charged_strike(state: bool):
is_charged_strike = state
@export var is_bot: bool = false
@@ -248,6 +246,9 @@ func _ready():
# Character Pointer Visibility
# Visible to all human players. Green for local player, Red for others.
var pointer = get_node_or_null("CharacterPointer")
# === Dynamically load new Dasher animations ===
_load_dasher_animations()
if pointer:
pointer.visible = true
@@ -372,6 +373,47 @@ func _init_floor_spawn_anchor():
if floor_spawn_top:
floor_spawn_top.reparent(floor_spawn_anchor, false)
func _load_dasher_animations():
"""Dynamically loads dasher animations from GLB files and adds them to the AnimationPlayer."""
if not anim_player: return
var anim_library = anim_player.get_animation_library("animation-pack")
if not anim_library:
anim_library = AnimationLibrary.new()
anim_player.add_animation_library("animation-pack", anim_library)
var dasher_files = [
{"path": "res://assets/characters/dashers/dasher_getting_hit.glb", "name": "dasher_getting_hit"},
{"path": "res://assets/characters/dashers/dasher_hit.glb", "name": "dasher_hit"},
{"path": "res://assets/characters/dashers/dasher_hold.glb", "name": "dasher_hold"},
{"path": "res://assets/characters/dashers/dasher_put.glb", "name": "dasher_put"},
{"path": "res://assets/characters/dashers/dasher_stun.glb", "name": "dasher_stun"},
{"path": "res://assets/characters/dashers/dasher_take.glb", "name": "dasher_take"}
]
for file_data in dasher_files:
var gltf_doc = GLTFDocument.new()
var gltf_state = GLTFState.new()
var error = gltf_doc.append_from_file(file_data.path, gltf_state)
if error == OK:
var anim_player_node = gltf_state.get_animation_player(0)
# Godot's GLTF importer creates an AnimationPlayer inside the scene
var scene = gltf_doc.generate_scene(gltf_state)
if scene:
var scene_anim_player = scene.find_child("AnimationPlayer", true, false)
if scene_anim_player:
var libs = scene_anim_player.get_animation_library_list()
for lib_name in libs:
var temp_lib = scene_anim_player.get_animation_library(lib_name)
for anim_name in temp_lib.get_animation_list():
var anim = temp_lib.get_animation(anim_name)
if not anim_library.has_animation(file_data.name):
anim_library.add_animation(file_data.name, anim)
scene.queue_free()
print("[Player] Dasher animations loaded into 'animation-pack'.")
@onready var floor_spawn_bot: AnimatedSprite3D = $floor_spawn_bot
@onready var floor_spawn_top: AnimatedSprite3D = $floor_spawn_top
@onready var vfx_scatter_knock: AnimatedSprite3D = $scatter_knock
@@ -874,10 +916,10 @@ func _refresh_player_visuals():
color_to_apply = Color.CYAN # Stop n Go Freeze
elif is_slowed:
color_to_apply = Color(0.6, 0.8, 1.0) # Slowed / Icy Blue
elif is_attack_mode:
color_to_apply = Color(1.0, 0.5, 0.5) # Attack Mode (Red Tint)
elif is_carrying_tekton or is_knock_mode:
color_to_apply = Color(1.0, 1.0, 0.0) # Carrying or Knocking (Yellow)
elif is_charged_strike:
color_to_apply = Color(1.0, 0.5, 0.5) # Charged Strike (Red Tint)
elif is_carrying_tekton:
color_to_apply = Color(1.0, 1.0, 0.0) # Carrying (Yellow)
alpha_to_apply = 0.5 # 50% opacity when carrying Tekton
elif immunity_timer > 0:
color_to_apply = Color(0.5, 1.0, 0.5) # Immunity (Light Green)
@@ -905,7 +947,7 @@ func update_rank_visuals(rank: int):
if not pos_label:
return
if rank <= 3:
if rank <= 4:
pos_label.visible = true
if race_manager:
pos_label.text = race_manager.get_ordinal_string(rank)
@@ -913,9 +955,10 @@ func update_rank_visuals(rank: int):
pos_label.text = str(rank)
match rank:
1: pos_label.modulate = Color(0.85, 0.0, 0.0) # Red
2: pos_label.modulate = Color(0.0, 0.0, 1.0) # Blue
3: pos_label.modulate = Color(1.0, 0.9, 0.0) # Yellow
1: pos_label.modulate = Color(1.0, 0.84, 0.0) # Gold
2: pos_label.modulate = Color(0.75, 0.75, 0.75) # Silver
3: pos_label.modulate = Color(0.8, 0.5, 0.2) # Bronze
4: pos_label.modulate = Color(0.5, 0.5, 0.5) # Grey
else:
pos_label.visible = false
@@ -941,8 +984,8 @@ var slow_timer: float = 0.0
var tekton_carry_timer: float = 0.0
const MAX_TEKTON_CARRY_TIME: float = 3.0
var attack_mode_timer: float = 0.0
const MAX_ATTACK_MODE_TIME: float = 5.0
var charged_strike_timer: float = 0.0
const MAX_CHARGED_STRIKE_TIME: float = 5.0
@rpc("any_peer", "call_local")
func apply_stagger(duration: float = 1.5):
@@ -1220,7 +1263,7 @@ func attempt_target_action(target_index: int):
inventory_ui.deselect()
func activate_powerup(effect_id: int):
if is_carrying_tekton or is_knock_mode or is_attack_mode:
if is_carrying_tekton or is_charged_strike:
NotificationManager.send_message(self, "Cannot use Power-Up right now!", NotificationManager.MessageType.WARNING)
return
@@ -1253,7 +1296,7 @@ func activate_powerup(effect_id: int):
func activate_held_powerup():
"""Finds whichever powerup is currently held and activates it."""
if is_carrying_tekton or is_knock_mode or is_attack_mode:
if is_carrying_tekton or is_charged_strike:
NotificationManager.send_message(self, "Cannot use Power-Up right now!", NotificationManager.MessageType.WARNING)
return
@@ -1299,15 +1342,14 @@ func _process(delta):
if movement_manager:
movement_manager._process(delta)
# Attack/Knock Mode Expiration Timer
if is_multiplayer_authority() and (is_attack_mode or is_knock_mode):
if attack_mode_timer > 0:
attack_mode_timer -= delta
if attack_mode_timer <= 0:
attack_mode_timer = 0.0
is_attack_mode = false
is_knock_mode = false
NotificationManager.send_message(self, "Knock Mode Expired!", NotificationManager.MessageType.WARNING)
# Charged Strike Expiration Timer
if is_multiplayer_authority() and is_charged_strike:
if charged_strike_timer > 0:
charged_strike_timer -= delta
if charged_strike_timer <= 0:
charged_strike_timer = 0.0
is_charged_strike = false
NotificationManager.send_message(self, "Charged Strike Expired!", NotificationManager.MessageType.WARNING)
if powerup_manager:
powerup_manager.reset_boost()
@@ -2393,8 +2435,8 @@ func sync_snatch_tekton(carrier_path: NodePath, tekton_path: NodePath):
tekton_carry_timer = 0.0
# Visual/Logic side effects
if is_attack_mode:
is_attack_mode = false
if is_charged_strike:
is_charged_strike = false
SfxManager.play("pick_up_tekton_roaming")
play_pickup_animation()
@@ -2414,9 +2456,9 @@ func sync_grab_tekton(tekton_path: NodePath):
self.is_carrying_tekton = true
tekton.set_carried(true, self )
# Disposed of AttackMode upon grab
if is_attack_mode:
is_attack_mode = false
# Disposed of Charged Strike upon grab
if is_charged_strike:
is_charged_strike = false
SfxManager.play("pick_up_tekton_roaming")
play_pickup_animation()
@@ -2558,38 +2600,17 @@ func sync_drop_tekton():
print("[Player %s] Dropped Tekton at %s" % [name, current_position])
# is_attack_mode is already declared at top of file (or inherited?)
# Keeping is_knock_mode here for now or moving it up would be better, but let's just fix the error first.
var is_knock_mode: bool = false:
set(value):
if is_knock_mode == value: return
is_knock_mode = value
if is_knock_mode:
attack_mode_timer = MAX_ATTACK_MODE_TIME
_refresh_player_visuals()
func enter_attack_mode():
func enter_charged_strike():
if not is_multiplayer_authority(): return
if is_invisible:
NotificationManager.send_message(self , "Cannot enter Attack Mode while in Ghost mode!", NotificationManager.MessageType.WARNING)
NotificationManager.send_message(self , "Cannot use Charged Strike while in Ghost mode!", NotificationManager.MessageType.WARNING)
return
is_attack_mode = true
is_knock_mode = false # Mutually exclusive
NotificationManager.send_message(self , "Attack Mode ACTIVATED (Red)", NotificationManager.MessageType.POWERUP)
update_active_player_indicator()
func enter_knock_mode():
if not is_multiplayer_authority(): return
if is_invisible:
NotificationManager.send_message(self , "Cannot enter Knock Mode while in Ghost mode!", NotificationManager.MessageType.WARNING)
return
is_knock_mode = true
is_attack_mode = false # Mutually exclusive
NotificationManager.send_message(self , "Knock Mode ACTIVATED (Yellow)", NotificationManager.MessageType.POWERUP)
is_charged_strike = true
NotificationManager.send_message(self , "Charged Strike ACTIVATED (Red)", NotificationManager.MessageType.POWERUP)
update_active_player_indicator()
func update_active_player_indicator():
@@ -2619,8 +2640,8 @@ func knock_tekton():
if not is_multiplayer_authority() or is_frozen or is_stop_frozen or is_invisible:
return
# Requirement: Full Powerup Bar (or we are already in knock mode)
if not is_knock_mode and (not powerup_manager or not powerup_manager.can_use_special()):
# Requirement: Full Powerup Bar (or we are already charged)
if not is_charged_strike and (not powerup_manager or not powerup_manager.can_use_special()):
NotificationManager.send_message(self , "Need Full Boost to Knock!", NotificationManager.MessageType.WARNING)
return
@@ -2633,8 +2654,8 @@ func knock_tekton():
if is_multiplayer_authority():
rpc("sync_knock_tekton", tekton.get_path())
# Reset Knock Mode after successful hit
is_knock_mode = false
# Reset Charged Strike Mode after successful hit
is_charged_strike = false
NotificationManager.send_message(self , "Knock Successful!", NotificationManager.MessageType.POWERUP)
update_active_player_indicator()
else:
+28 -25
View File
@@ -22,8 +22,8 @@ var auth_mode: AuthMode = AuthMode.GUEST
const SESSION_FILE := "user://auth_session.dat"
const CREDENTIALS_FILE := "user://auth_credentials.dat"
# Encryption key for session storage (replace with your own!)
const ENCRYPTION_KEY := "tekton_secret_key_change_me_123"
# Encryption key for session storage (device-specific)
var ENCRYPTION_KEY: String = OS.get_unique_id().sha256_text()
func _ready() -> void:
# Try to restore session on startup
@@ -40,8 +40,7 @@ func _try_restore_session() -> void:
var file := FileAccess.open_encrypted_with_pass(SESSION_FILE, FileAccess.READ, ENCRYPTION_KEY)
if not file:
print("[AuthManager] Could not open session file. Corrupt or wrong key. Deleting.")
DirAccess.remove_absolute(SESSION_FILE)
print("[AuthManager] Could not open session file")
return
var session_data = file.get_var()
@@ -369,7 +368,7 @@ func login_with_steam() -> bool:
return true
func _authenticate_steam_with_fallback(steamworks: Node) -> NakamaSession:
# Try proper Steam ticket auth first
# Proper Steam ticket auth
var auth_ticket = steamworks.get_auth_session_ticket()
if not auth_ticket.is_empty():
print("[AuthManager] Got Steam auth ticket, authenticating with Nakama...")
@@ -377,22 +376,10 @@ func _authenticate_steam_with_fallback(steamworks: Node) -> NakamaSession:
if not session.is_exception():
return session
print("[AuthManager] Steam ticket auth failed: %s" % session.get_exception().message)
print("[AuthManager] Falling back to Steam ID custom auth (dev mode)...")
# Fallback: use Steam ID + username to create an email-style account (works without publisher key)
var steam_id = str(steamworks.get_steam_user_id())
var steam_name = steamworks.get_steam_user_name()
if steam_id == "0" or steam_id.is_empty():
return null
# Derive email and password from Steam credentials
var email = steam_name.to_lower().replace(" ", "_") + "@steam.local"
var password = steam_name # Default password = Steam username
var username = steam_name
print("[AuthManager] Using Steam email auth: %s (%s)" % [email, username])
var fallback_session: NakamaSession = await NakamaManager.client.authenticate_email_async(email, password, username, true)
return fallback_session
print("[AuthManager] Steam auth ticket is empty.")
return null
# =============================================================================
# Account Linking (Convert Guest to Full Account)
@@ -455,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:
+77 -1
View File
@@ -31,6 +31,11 @@ signal doors_swap_time_changed(time: int)
signal doors_refresh_time_changed(time: int)
signal doors_required_goals_changed(goals: int)
# Gauntlet settings signals
signal gauntlet_round_duration_changed(duration: int)
signal gauntlet_cannon_interval_changed(interval: int)
signal gauntlet_volley_size_changed(size: int)
# Room data structure
var current_room: Dictionary = {}
var players_in_room: Array = [] # [{id, name, is_ready}]
@@ -74,13 +79,18 @@ var doors_swap_time: int = 15
var doors_refresh_time: int = 25
var doors_required_goals: int = 8
# Gauntlet settings
var gauntlet_round_duration: int = 180
var gauntlet_cannon_interval: int = 5
var gauntlet_volley_size: int = 5
# Rematch tracking
var rematch_votes: Array = [] # [player_id, ...]
# Character and area selection
var available_characters: Array[String] = ["Copper", "Dabro", "Gatot", "Pip", "Random"]
var available_areas: Array[String] = []
var available_game_modes: Array[String] = ["Freemode", "Stop n Go"]
var available_game_modes: Array[String] = ["Freemode", "Stop n Go", "Candy Cannon Survival"]
var selected_area: String = "Freemode Arena" # Host-controlled
var game_mode: String = "Freemode" # Host-controlled
var local_character_index: int = 0 # Local player's character index
@@ -135,6 +145,8 @@ func _update_available_areas(mode: String) -> void:
available_areas = ["Freemode Arena", "Classic", "Colloseum"]
"Stop n Go":
available_areas = ["Stop N Go Arena"]
"Candy Cannon Survival":
available_areas = ["Gauntlet Arena"]
_:
available_areas = ["Classic"]
@@ -537,6 +549,37 @@ func sync_doors_required_goals(goals: int) -> void:
doors_required_goals = goals
emit_signal("doors_required_goals_changed", goals)
# =============================================================================
# Gauntlet Settings
# =============================================================================
func set_gauntlet_round_duration(duration: int) -> void:
gauntlet_round_duration = duration
if is_host: rpc("sync_gauntlet_round_duration", duration)
@rpc("authority", "call_local", "reliable")
func sync_gauntlet_round_duration(duration: int) -> void:
gauntlet_round_duration = duration
emit_signal("gauntlet_round_duration_changed", duration)
func set_gauntlet_cannon_interval(interval: int) -> void:
gauntlet_cannon_interval = interval
if is_host: rpc("sync_gauntlet_cannon_interval", interval)
@rpc("authority", "call_local", "reliable")
func sync_gauntlet_cannon_interval(interval: int) -> void:
gauntlet_cannon_interval = interval
emit_signal("gauntlet_cannon_interval_changed", interval)
func set_gauntlet_volley_size(size: int) -> void:
gauntlet_volley_size = size
if is_host: rpc("sync_gauntlet_volley_size", size)
@rpc("authority", "call_local", "reliable")
func sync_gauntlet_volley_size(size: int) -> void:
gauntlet_volley_size = size
emit_signal("gauntlet_volley_size_changed", size)
# =============================================================================
# Character Selection
# =============================================================================
@@ -682,14 +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)
# 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:
@@ -715,6 +784,10 @@ func start_game(force: bool = false) -> void:
rpc("sync_doors_swap_time", doors_swap_time)
rpc("sync_doors_refresh_time", doors_refresh_time)
rpc("sync_doors_required_goals", doors_required_goals)
# Sync gauntlet settings
rpc("sync_gauntlet_round_duration", gauntlet_round_duration)
rpc("sync_gauntlet_cannon_interval", gauntlet_cannon_interval)
rpc("sync_gauntlet_volley_size", gauntlet_volley_size)
# Sync game mode
rpc("sync_game_mode", game_mode)
@@ -790,6 +863,9 @@ func request_room_info(requester_id: int, requester_name: String, requester_char
rpc_id(requester_id, "sync_doors_swap_time", doors_swap_time)
rpc_id(requester_id, "sync_doors_refresh_time", doors_refresh_time)
rpc_id(requester_id, "sync_doors_required_goals", doors_required_goals)
rpc_id(requester_id, "sync_gauntlet_round_duration", gauntlet_round_duration)
rpc_id(requester_id, "sync_gauntlet_cannon_interval", gauntlet_cannon_interval)
rpc_id(requester_id, "sync_gauntlet_volley_size", gauntlet_volley_size)
rpc_id(requester_id, "sync_game_mode", game_mode)
rpc_id(requester_id, "sync_area", selected_area)
+12 -1
View File
@@ -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)
+22 -19
View File
@@ -1,11 +1,10 @@
extends Node
# Standard Nakama Configuration
var nakama_server_key = "defaultkey"
var nakama_host = "thunderobot.tapir-atria.ts.net"
var nakama_port = 7350
var nakama_scheme = "https"
var nakama_port_funnel = 443
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_port = OS.get_environment("NAKAMA_PORT").to_int() if OS.has_environment("NAKAMA_PORT") else ProjectSettings.get_setting("network/nakama/port", 7350)
var nakama_scheme = OS.get_environment("NAKAMA_SCHEME") if OS.has_environment("NAKAMA_SCHEME") else ProjectSettings.get_setting("network/nakama/scheme", "http")
# Core Nakama Variables
var client: NakamaClient
@@ -30,10 +29,7 @@ func _ready():
set_process(true)
func _init_client():
if nakama_host == "thunderobot.tapir-atria.ts.net":
client = Nakama.create_client(nakama_server_key, nakama_host, 443, "https")
else:
client = Nakama.create_client(nakama_server_key, nakama_host, nakama_port, nakama_scheme)
client = Nakama.create_client(nakama_server_key, nakama_host, nakama_port, nakama_scheme)
func set_server(host: String, port: int = 7350):
# Clean up the host string
@@ -127,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
@@ -184,8 +188,10 @@ func host_game(room_meta: Dictionary = {}):
printerr("Cannot host: Bridge not initialized")
return
print("Hosting match via Nakama Bridge...")
await bridge.create_match()
# Errors are emitted via bridge's match_join_error signal
var result = await bridge.create_match()
if result and result.is_exception():
emit_signal("match_join_error", result.get_exception().message)
return
# Store room metadata in Nakama storage so other players can see it in listings
if session and current_match_id and room_meta.size() > 0:
var meta_json = JSON.stringify(room_meta)
@@ -211,13 +217,10 @@ func join_game(match_id: String):
# Wait a bit for cleanup
await get_tree().create_timer(0.2).timeout
if not bridge:
printerr("Cannot join: Bridge became null while waiting")
return
print("Joining match: ", match_id)
await bridge.join_match(match_id)
# Errors are emitted via bridge's match_join_error signal
var result = await bridge.join_match(match_id)
if result and result.is_exception():
emit_signal("match_join_error", result.get_exception().message)
# --- Callbacks ---
+19 -1
View File
@@ -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
@@ -472,6 +487,9 @@ func spawn_tiles_around(count: int = 4):
if roll < 0.6 or (LobbyManager and LobbyManager.game_mode == "Stop n Go"):
# 60% Normal Tile (7-10) OR 100% if Stop n Go (User Request)
item_id = rng.randi_range(7, 10)
elif LobbyManager and LobbyManager.get_game_mode() == GameMode.Mode.GAUNTLET:
# Gauntlet mode: No power-up spawns from Tekton grabs
item_id = rng.randi_range(7, 10)
else:
# 40% PowerUp (11-14)
var mode = GameMode.Mode.FREEMODE