diff --git a/scenes/player.gd b/scenes/player.gd index 23e892c..f79f7a8 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -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: diff --git a/scripts/managers/auth_manager.gd b/scripts/managers/auth_manager.gd index 849cf65..1be36c4 100644 --- a/scripts/managers/auth_manager.gd +++ b/scripts/managers/auth_manager.gd @@ -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: diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 3fe50af..acf83f9 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -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) diff --git a/scripts/managers/settings_manager.gd b/scripts/managers/settings_manager.gd index 64401be..f601cec 100644 --- a/scripts/managers/settings_manager.gd +++ b/scripts/managers/settings_manager.gd @@ -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) diff --git a/scripts/nakama_manager.gd b/scripts/nakama_manager.gd index bde30f0..8cba8e7 100644 --- a/scripts/nakama_manager.gd +++ b/scripts/nakama_manager.gd @@ -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 --- diff --git a/scripts/tekton.gd b/scripts/tekton.gd index f052d3a..dafbb3b 100644 --- a/scripts/tekton.gd +++ b/scripts/tekton.gd @@ -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