From bee9c30f0e574c2024f6dd535d9ed8d69d707f5f Mon Sep 17 00:00:00 2001 From: adtpdn Date: Wed, 14 Jan 2026 00:20:20 +0800 Subject: [PATCH] bugfix, desync, and add UI function --- _daily_basis/report_2026-01-14.md | 33 +++++ assets/models/meshes/tiles.res | Bin 1937 -> 4197 bytes assets/models/meshes/tiles_armagedon_a4.res | Bin 7472 -> 14581 bytes scenes/main.gd | 68 ++++++--- scenes/main.tscn | 90 +++++++++++- scenes/player.gd | 32 ++++- scripts/bot_controller.gd | 13 +- scripts/bot_strategic_planner.gd | 65 +++++++-- scripts/managers/auth_manager.gd | 18 +-- scripts/managers/lobby_manager.gd | 4 +- scripts/managers/touch_controls.gd | 145 ++++++++++++-------- scripts/managers/user_profile_manager.gd | 4 +- server/docker-compose.yaml | 2 - tiles_armagedon_a1.res | Bin 0 -> 11257 bytes 14 files changed, 362 insertions(+), 112 deletions(-) create mode 100644 _daily_basis/report_2026-01-14.md create mode 100644 tiles_armagedon_a1.res diff --git a/_daily_basis/report_2026-01-14.md b/_daily_basis/report_2026-01-14.md new file mode 100644 index 0000000..e2a906c --- /dev/null +++ b/_daily_basis/report_2026-01-14.md @@ -0,0 +1,33 @@ +[ ADT's Report ] + +Updated the `tekton-enet` ( Armageddon Multiplayer ) on branch `launcher` + +**Network & Desync Fixes** + +✅ **Visual Interpolation** - Fixed player position desynchronization. +* Implemented client-side smoothing using `target_visual_position` to fix jitter and snapping. +* Resolved conflicts between unreliable RPC network updates and grid-based logic. + +✅ **Bot Synchronization** - Eliminated "Node not found" RPC errors on client join. +* **Fix**: Modified `main.gd` to pre-spawn potential bot nodes (IDs 2-4) on the client before receiving full sync. +* **Result**: Prevents crash/error spam when Host sends updates for bots before the Client has fully processed the player list. + +**Bot AI Improvements** + +✅ **Stuck Prevention** - Fixed bots getting stuck in "Idle" loops. +* **Problem**: Bots would sometimes enter a state where they had AP (Action Points) but couldn't pathfind or act, effectively freezing the game in turn-based mode. +* **Fix**: Added logic to `BotController` to detect this state and automatically skip the turn (consume remaining AP) to keep the game flow moving. +* **Logging**: Added bot identification to logs for better diagnosis. + +**Touch Controls Refactoring** + +✅ **Scene-Based Instantiation** - Refactored `TouchControls` for easier editing. +* **Logic**: Updated `touch_controls.gd` to look for existing UI nodes (`VirtualJoystick`, `GrabBtn`, etc.) instead of forcing programmatic creation. +* **Implementation**: Moved the entire Touch Control node hierarchy directly into `main.tscn`. +* **Benefit**: Allows visual editing and customization of touch controls directly in the Godot Editor. + +**UI & Lobby Enhancements** + +✅ **Optional Timer Display** - Tuned HUD based on lobby settings. +* **Feature**: The "Global Match Timer" (GoalsTimer) in the main game is now hidden if "Enable Timer Check" is disabled in the Lobby. +* **Default**: Changed "Enable Timer Check" default to `false` in `LobbyManager`. diff --git a/assets/models/meshes/tiles.res b/assets/models/meshes/tiles.res index 6897925d4d839def5c4441b0d8570a9fd12447e3..b877432728e55c2469cef3f334c70fe85e45be42 100644 GIT binary patch literal 4197 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 diff --git a/assets/models/meshes/tiles_armagedon_a4.res b/assets/models/meshes/tiles_armagedon_a4.res index 1bd617eaef309b2eb415f1634cc65e544acbd762..6345c378f01cabb42f44a90707bcd4b8f212f223 100644 GIT binary patch literal 14581 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 literal 7472 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 diff --git a/scenes/main.gd b/scenes/main.gd index afb0ae7..f3b9ff5 100644 --- a/scenes/main.gd +++ b/scenes/main.gd @@ -59,9 +59,14 @@ func _init_managers(): screen_shake_manager.initialize($Camera3D) # Touch controls for mobile - touch_controls = load("res://scripts/managers/touch_controls.gd").new() - touch_controls.name = "TouchControls" - add_child(touch_controls) + # Touch controls for mobile + touch_controls = get_node_or_null("TouchControls") + if not touch_controls: + print("TouchControls node not found in scene, creating instance...") + touch_controls = load("res://scripts/managers/touch_controls.gd").new() + touch_controls.name = "TouchControls" + add_child(touch_controls) + touch_controls.initialize(self) # Connect signals for UI updates @@ -180,8 +185,16 @@ func broadcast_message(player_name: String, message: String): func _setup_global_match_timer_ui(): """Create the global match timer display at the top of the screen.""" + # Check if timer check is enabled in lobby settings + if not LobbyManager.enable_cycle_timer: + var existing = get_node_or_null("GlobalMatchTimer") + if existing: + existing.visible = false + return + var existing = get_node_or_null("GlobalMatchTimer") if existing: + existing.visible = true return # Create timer panel @@ -345,21 +358,40 @@ func _setup_client_game(): var my_id = multiplayer.get_unique_id() print("Client setup - my peer ID: ", my_id) - # Create local player immediately - if not has_node(str(my_id)): - var player_character = PlayerManager.add_player_character(my_id) - add_child(player_character) - player_character.add_to_group("Players", true) - GameStateManager.add_player(my_id) - GameStateManager.local_player_character = player_character - ui_manager.set_local_player(player_character) - if touch_controls: - touch_controls.set_player(player_character) - ui_manager.update_button_states() - print("Created local player for client: ", my_id) + # Pre-spawn ALL players known from LobbyManager (including Host ID 1) + # This ensures nodes exist to receive RPCs (like 'set_spawn_position') that might arrive before full sync + var lobby_players = LobbyManager.get_players() + for player_data in lobby_players: + var p_id = player_data.get("id", 0) + if p_id != 0: + add_player_character(p_id) + print("Client: Pre-spawned player ", p_id) - # Wait for host to be ready, then request full sync - await get_tree().create_timer(2.0).timeout + # Pre-spawn potential bots (IDs 2 to MaxPlayers) to prevent RPC "Node not found" errors + # Bots use small integer IDs (2, 3, 4...) while clients use large unique IDs + if GameStateManager.enable_bots: + for i in range(2, GameStateManager.max_players + 1): + # Only spawn if not already existing (e.g. if a human somehow got this ID, though unlikely) + if not has_node(str(i)): + add_player_character(i) + get_node(str(i)).is_bot = true # Assume bot initially + get_node(str(i)).add_to_group("Bots", true) + print("Client: Pre-spawned potential bot ", i) + + # Ensure local player setup (UI, controls) is verified + var player_character = get_node_or_null(str(my_id)) + if player_character: + # If we just spawned it above, we need to set these locally too + if GameStateManager.local_player_character != player_character: + GameStateManager.local_player_character = player_character + ui_manager.set_local_player(player_character) + if touch_controls: + touch_controls.set_player(player_character) + ui_manager.update_button_states() + print("Client: Configured local player ", my_id) + + # Wait shorter time for host to be ready, then request full sync to correct positions/state + await get_tree().create_timer(1.0).timeout rpc_id(1, "request_full_player_sync", my_id) func _auto_start_from_lobby(): @@ -483,7 +515,7 @@ func add_player_character(peer_id: int): func _on_peer_connected(new_peer_id: int): if multiplayer.is_server(): - await get_tree().create_timer(1.5).timeout + await get_tree().create_timer(0.1).timeout add_player_character(new_peer_id) rpc("add_newly_connected_player_character", new_peer_id) diff --git a/scenes/main.tscn b/scenes/main.tscn index 58e1982..71815eb 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=27 format=3 uid="uid://dxn87yj8qnfpp"] +[gd_scene load_steps=29 format=3 uid="uid://dxn87yj8qnfpp"] [ext_resource type="MeshLibrary" uid="uid://kcv6ans86ug7" path="res://addons/enhanced_gridmap/meshlibrary/default.tres" id="1_110wo"] [ext_resource type="Script" uid="uid://co1ads72by6na" path="res://scenes/main.gd" id="1_xcpe3"] @@ -22,6 +22,8 @@ [ext_resource type="StyleBox" uid="uid://d3ruc8gytoovx" path="res://assets/styles/ribbon_selected_gui.tres" id="18_u5x6e"] [ext_resource type="StyleBox" uid="uid://cdhnwvcklbyl8" path="res://assets/styles/ribbon_hovered_gui.tres" id="19_w1rqq"] [ext_resource type="StyleBox" uid="uid://3yog1weaqhxb" path="res://assets/styles/ribbon_unselected_gui.tres" id="20_q6bc1"] +[ext_resource type="Script" uid="uid://b54tfa0n6kogi" path="res://scripts/managers/touch_controls.gd" id="touch_manager"] +[ext_resource type="Script" uid="uid://djiml4sh61dc1" path="res://scripts/ui/virtual_joystick.gd" id="virtual_joystick"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerboard"] content_margin_left = 8.0 @@ -9699,6 +9701,92 @@ custom_minimum_size = Vector2(0, 40) layout_mode = 2 text = "Back" +[node name="TouchControls" type="CanvasLayer" parent="."] +layer = 10 +script = ExtResource("touch_manager") + +[node name="TouchControls" type="Control" parent="TouchControls"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = 318.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 1 + +[node name="VirtualJoystick" type="Control" parent="TouchControls/TouchControls"] +layout_mode = 1 +anchors_preset = 2 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = 120.0 +offset_top = -280.0 +offset_right = 280.0 +offset_bottom = -120.0 +grow_vertical = 0 +script = ExtResource("virtual_joystick") + +[node name="GrabBtn" type="Button" parent="TouchControls/TouchControls"] +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -232.0 +offset_top = -286.0 +offset_right = -162.0 +offset_bottom = -216.0 +grow_horizontal = 0 +grow_vertical = 0 +text = "👋" + +[node name="PutBtn" type="Button" parent="TouchControls/TouchControls"] +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -152.0 +offset_top = -206.0 +offset_right = -82.0 +offset_bottom = -136.0 +grow_horizontal = 0 +grow_vertical = 0 +text = "📦" + +[node name="SpecialBtn" type="Button" parent="TouchControls/TouchControls"] +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -232.0 +offset_top = -126.0 +offset_right = -162.0 +offset_bottom = -56.0 +grow_horizontal = 0 +grow_vertical = 0 +text = "⚡" + +[node name="SettingsBtn" type="Button" parent="TouchControls/TouchControls"] +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -69.0 +offset_top = -69.0 +offset_right = -19.0 +offset_bottom = -19.0 +grow_horizontal = 0 +grow_vertical = 0 +text = "⚙" + [connection signal="pressed" from="Menu/Host" to="." method="_on_host_pressed"] [connection signal="pressed" from="Menu/Join" to="." method="_on_join_pressed"] [connection signal="text_submitted" from="MessageInput" to="." method="_on_message_input_text_submitted"] diff --git a/scenes/player.gd b/scenes/player.gd index 628734a..355e14d 100644 --- a/scenes/player.gd +++ b/scenes/player.gd @@ -78,6 +78,8 @@ var spawn_locations = [ var finish_locations: Array: get: return race_manager.finish_locations if race_manager else [] +var target_visual_position: Vector3 = Vector3.ZERO # For client-side smoothing + var spawn_point_selected = false # Action for hilighter @@ -145,8 +147,8 @@ func _ready(): if is_multiplayer_authority(): rpc("sync_display_name", display_name) - # Wait briefly to ensure proper scene setup - await get_tree().create_timer(0.1).timeout + # Wait briefly to ensure proper scene setup and server recognition + await get_tree().create_timer(0.5).timeout # More robust way to get the main scene var main_scene = get_tree().get_root().get_node_or_null("Main") @@ -226,8 +228,11 @@ func _ready(): 1.0, current_position.y * cell_size.z + cell_size.z * 0.5 ) + target_visual_position = global_position if is_multiplayer_authority(): rpc("sync_position", current_position) + else: + target_visual_position = global_position func _init_managers(): movement_manager = load("res://scripts/managers/player_movement_manager.gd").new() @@ -609,6 +614,11 @@ func _process(delta): if _verify_timer >= 3.0: _verify_timer = 0.0 rpc("ping_existence") + else: + # Client-side visual smoothing + # Interpolate towards the target position received from authority + if global_position.distance_squared_to(target_visual_position) > 0.001: + global_position = global_position.lerp(target_visual_position, delta * 15.0) # Delegate rotation to movement manager if movement_manager: @@ -942,7 +952,8 @@ static func reset_race_stats(): @rpc("any_peer", "call_local", "unreliable") func remote_set_position(authority_position): - global_position = authority_position + # Don't snap directly, update target for interpolation + target_visual_position = authority_position @rpc("any_peer", "call_local") func display_message(message, type: int = 0): @@ -1371,11 +1382,14 @@ func bot_arrange_item(from_slot: int, to_slot: int): func update_visual_position(): # Ensure proper grid-aligned positioning - global_position = Vector3( + var new_pos = Vector3( current_position.x * cell_size.x + cell_size.x * 0.5, 1.0, current_position.y * cell_size.z + cell_size.z * 0.5 ) + global_position = new_pos + target_visual_position = new_pos # Snap target too + if is_multiplayer_authority(): rpc("sync_position", current_position) @@ -1383,11 +1397,14 @@ func update_visual_position(): func sync_position(pos: Vector2i): current_position = pos # Always update the visual position after position sync - global_position = Vector3( + var new_pos = Vector3( current_position.x * cell_size.x + cell_size.x * 0.5, cell_size.y, current_position.y * cell_size.z + cell_size.z * 0.5 ) + cell_offset + + global_position = new_pos + target_visual_position = new_pos # Reset smoothing target to prevent fighting @rpc("any_peer", "call_local", "reliable") func set_spawn_position(pos: Vector2i): @@ -1397,11 +1414,14 @@ func set_spawn_position(pos: Vector2i): # Clear any spawn highlights clear_spawn_highlights() # Update visual position - global_position = Vector3( + var new_pos = Vector3( current_position.x * cell_size.x + cell_size.x * 0.5, cell_size.y, current_position.y * cell_size.z + cell_size.z * 0.5 ) + cell_offset + + global_position = new_pos + target_visual_position = new_pos @rpc("any_peer", "call_local", "reliable") diff --git a/scripts/bot_controller.gd b/scripts/bot_controller.gd index 2169255..6a54aa9 100644 --- a/scripts/bot_controller.gd +++ b/scripts/bot_controller.gd @@ -33,6 +33,11 @@ func _ready(): rng.seed = name.hash() _tick_counter = rng.randi() % tick_rate + # Mobile Optimization: Throttling + if OS.has_feature("mobile") or OS.has_feature("android") or OS.has_feature("ios"): + tick_rate = int(tick_rate * 1.5) # 50% slower updates on mobile + print("[BotController] Mobile detected! Throttling tick rate to: ", tick_rate) + # Get parent (should be player character) actor = get_parent() # ... (rest of _ready) ... @@ -153,7 +158,13 @@ func _run_ai_tick(): print("[BotController] Action Taken: Arrange") return - print("[BotController] No action taken (Idle). AP: %d, GoalsAchieved: %s" % [actor.action_points, _is_goals_achieved()]) + print("[BotController] %s - No action taken (Idle). AP: %d, GoalsAchieved: %s" % [actor.name, actor.action_points, _is_goals_achieved()]) + + # STALL PREVENTION: If we have AP but couldn't do anything, we are stuck. + # Skip turn to prevent game freeze in turn-based mode. + if TurnManager.turn_based_mode and actor.action_points > 0: + print("[BotController] %s is STUCK with AP %d! Skipping turn to proceed flow." % [actor.name, actor.action_points]) + actor.consume_action_points(actor.action_points) # ============================================================================= # Power-Up / Sabotage diff --git a/scripts/bot_strategic_planner.gd b/scripts/bot_strategic_planner.gd index 0b4bdb0..aab5ec2 100644 --- a/scripts/bot_strategic_planner.gd +++ b/scripts/bot_strategic_planner.gd @@ -223,27 +223,64 @@ func find_best_tile_to_grab() -> Dictionary: return best_tile func find_nearest_tile_of_type(tile_types: Array) -> Vector2i: - """Find nearest tile matching any type in array.""" + """Find nearest tile matching any type in array using optimized spiral search.""" var current_pos = actor.current_position - var nearest_pos = Vector2i(-1, -1) - var nearest_dist = 999999 if not enhanced_gridmap: - return nearest_pos + return Vector2i(-1, -1) - for x in range(enhanced_gridmap.columns): - for z in range(enhanced_gridmap.rows): - var pos = Vector2i(x, z) - var cell = Vector3i(x, 1, z) - var item = enhanced_gridmap.get_cell_item(cell) + # Optimization: Start check at simple radius + # If we find something in the spiral, it is guaranteed to be one of the nearest (by Chebyshev distance logic broadly, or just good enough) + + var max_radius = 25 # Limit search range to prevent full map scans on huge maps + if OS.has_feature("mobile"): + max_radius = 15 # Stricter limit on mobile + + # Check center first + var center_cell = Vector3i(current_pos.x, 1, current_pos.y) + if enhanced_gridmap.get_cell_item(center_cell) in tile_types: + return current_pos + + for r in range(1, max_radius + 1): + # Spiral perimeter: + # Top row: (x-r, y-r) to (x+r, y-r) + # Bottom row: (x-r, y+r) to (x+r, y+r) + # Left col: (x-r, y-r+1) to (x-r, y+r-1) + # Right col: (x+r, y-r+1) to (x+r, y+r-1) + var found_in_layer = [] + + # We'll check the ring. Note: Manhattan distance might be better metric for "nearest" + # but layer-by-layer is efficient for finding "close enough" quickly. + + for x_off in range(-r, r + 1): + _check_spiral_cell(current_pos.x + x_off, current_pos.y - r, tile_types, found_in_layer) # Top + _check_spiral_cell(current_pos.x + x_off, current_pos.y + r, tile_types, found_in_layer) # Bottom - if item in tile_types: + for y_off in range(-r + 1, r): + _check_spiral_cell(current_pos.x - r, current_pos.y + y_off, tile_types, found_in_layer) # Left + _check_spiral_cell(current_pos.x + r, current_pos.y + y_off, tile_types, found_in_layer) # Right + + if found_in_layer.size() > 0: + # If we found candidates in this layer, pick the physically closest one (Euclidean/Manhattan refinement) + var nearest_in_layer = found_in_layer[0] + var min_dist = 999999 + for pos in found_in_layer: var dist = abs(pos.x - current_pos.x) + abs(pos.y - current_pos.y) - if dist < nearest_dist: - nearest_dist = dist - nearest_pos = pos + if dist < min_dist: + min_dist = dist + nearest_in_layer = pos + return nearest_in_layer - return nearest_pos + return Vector2i(-1, -1) + +func _check_spiral_cell(x: int, z: int, tile_types: Array, result_array: Array): + if x < 0 or z < 0 or x >= enhanced_gridmap.columns or z >= enhanced_gridmap.rows: + return + + var cell = Vector3i(x, 1, z) + var item = enhanced_gridmap.get_cell_item(cell) + if item in tile_types: + result_array.append(Vector2i(x, z)) # ============================================================================= # Movement Strategy diff --git a/scripts/managers/auth_manager.gd b/scripts/managers/auth_manager.gd index e14d3ef..68bd020 100644 --- a/scripts/managers/auth_manager.gd +++ b/scripts/managers/auth_manager.gd @@ -64,7 +64,7 @@ func _try_restore_session() -> void: if session.is_expired(): # Try to refresh if refresh_token: - var refreshed := await NakamaManager.client.session_refresh_async(session) + var refreshed: NakamaSession = await NakamaManager.client.session_refresh_async(session) if not refreshed.is_exception(): session = refreshed _save_session(session, saved_auth_mode) @@ -118,7 +118,7 @@ func login_as_guest() -> bool: var device_id := _get_device_id() print("[AuthManager] Guest login with device: ", device_id.substr(0, 8) + "...") - var session := await NakamaManager.client.authenticate_device_async(device_id, null, true) + var session: NakamaSession = await NakamaManager.client.authenticate_device_async(device_id, null, true) if session.is_exception(): var error: String = session.get_exception().message @@ -174,7 +174,7 @@ func login_with_email(email: String, password: String, remember: bool = true) -> print("[AuthManager] Email login: ", email) - var session := await NakamaManager.client.authenticate_email_async(email, password, null, false) + var session: NakamaSession = await NakamaManager.client.authenticate_email_async(email, password, null, false) if session.is_exception(): var error: String = session.get_exception().message @@ -205,7 +205,7 @@ func register_with_email(email: String, password: String, username: String = "") print("[AuthManager] Registering: ", email) # Create account (true = create if not exists) - var session := await NakamaManager.client.authenticate_email_async(email, password, username, true) + var session: NakamaSession = await NakamaManager.client.authenticate_email_async(email, password, username, true) if session.is_exception(): var error: String = session.get_exception().message @@ -237,7 +237,7 @@ func login_with_google(id_token: String) -> bool: print("[AuthManager] Google login...") - var session := await NakamaManager.client.authenticate_google_async(id_token, null, true) + var session: NakamaSession = await NakamaManager.client.authenticate_google_async(id_token, null, true) if session.is_exception(): var error: String = session.get_exception().message @@ -265,7 +265,7 @@ func login_with_apple(id_token: String) -> bool: print("[AuthManager] Apple login...") - var session := await NakamaManager.client.authenticate_apple_async(id_token, null, true) + var session: NakamaSession = await NakamaManager.client.authenticate_apple_async(id_token, null, true) if session.is_exception(): var error: String = session.get_exception().message @@ -293,7 +293,7 @@ func login_with_facebook(access_token: String) -> bool: print("[AuthManager] Facebook login...") - var session := await NakamaManager.client.authenticate_facebook_async(access_token, null, true) + var session: NakamaSession = await NakamaManager.client.authenticate_facebook_async(access_token, null, true) if session.is_exception(): var error: String = session.get_exception().message @@ -324,7 +324,7 @@ func link_email(email: String, password: String) -> bool: print("[AuthManager] Linking email to guest account...") - var result := await NakamaManager.client.link_email_async(NakamaManager.session, email, password) + var result: NakamaAsyncResult = await NakamaManager.client.link_email_async(NakamaManager.session, email, password) if result.is_exception(): push_error("[AuthManager] Link failed: " + result.get_exception().message) @@ -341,7 +341,7 @@ func link_google(id_token: String) -> bool: if not is_authenticated or not NakamaManager.session: return false - var result := await NakamaManager.client.link_google_async(NakamaManager.session, id_token) + var result: NakamaAsyncResult = await NakamaManager.client.link_google_async(NakamaManager.session, id_token) if result.is_exception(): return false diff --git a/scripts/managers/lobby_manager.gd b/scripts/managers/lobby_manager.gd index 540a6ef..6abb8d6 100644 --- a/scripts/managers/lobby_manager.gd +++ b/scripts/managers/lobby_manager.gd @@ -31,7 +31,7 @@ var match_duration: int = 180 # Default 3 minutes var randomize_spawn: bool = true # Default enabled # Timer setting -var enable_cycle_timer: bool = true # Default enabled +var enable_cycle_timer: bool = false # Default disabled signal enable_cycle_timer_changed(enabled: bool) # Character and area selection @@ -419,4 +419,4 @@ func reset() -> void: match_duration = 180 # Reset to default 3 minutes selected_area = "Desert" local_character_index = 0 - enable_cycle_timer = true + enable_cycle_timer = false diff --git a/scripts/managers/touch_controls.gd b/scripts/managers/touch_controls.gd index b56afcc..1acc447 100644 --- a/scripts/managers/touch_controls.gd +++ b/scripts/managers/touch_controls.gd @@ -40,77 +40,108 @@ func set_player(p_player: Node3D): local_player = p_player func _create_touch_ui(): - print("[TouchControls] Creating touch UI...") + print("[TouchControls] Creating/Finding touch UI...") # Use layer 10 - above regular UI but below pause menu layer = 10 - # Create main container - var container = Control.new() - container.name = "TouchControls" - container.set_anchors_preset(Control.PRESET_FULL_RECT) - container.mouse_filter = Control.MOUSE_FILTER_PASS # Pass input to children - add_child(container) + # Check if container already exists (added in scene) + var container = get_node_or_null("TouchControls") - # Create virtual joystick (bottom-left) - var joystick_script = load("res://scripts/ui/virtual_joystick.gd") - virtual_joystick = Control.new() - virtual_joystick.set_script(joystick_script) - virtual_joystick.name = "VirtualJoystick" - virtual_joystick.set_anchors_preset(Control.PRESET_BOTTOM_LEFT) + if not container: + # Create main container if missing + container = Control.new() + container.name = "TouchControls" + container.set_anchors_preset(Control.PRESET_FULL_RECT) + container.mouse_filter = Control.MOUSE_FILTER_PASS # Pass input to children + add_child(container) + else: + print("[TouchControls] Found existing TouchControls container") + + # Helper to find or create control + var find_or_create_joystick = func(): + var joy = container.get_node_or_null("VirtualJoystick") + if joy: + print("[TouchControls] Found existing VirtualJoystick") + return joy + + var joystick_script = load("res://scripts/ui/virtual_joystick.gd") + joy = Control.new() + joy.set_script(joystick_script) + joy.name = "VirtualJoystick" + joy.set_anchors_preset(Control.PRESET_BOTTOM_LEFT) + + # Use standard size from joystick script defaults (radius 60 -> size 160) + var joy_size = Vector2(160, 160) + joy.custom_minimum_size = joy_size + joy.size = joy_size + + joy.offset_left = 120 + joy.offset_top = -280 + joy.offset_right = 280 + joy.offset_bottom = -120 + + container.add_child(joy) + return joy + + virtual_joystick = find_or_create_joystick.call() + if not virtual_joystick.direction_changed.is_connected(_on_joystick_direction): + virtual_joystick.direction_changed.connect(_on_joystick_direction) - # Use standard size from joystick script defaults (radius 60 -> size 160) - var joy_size = Vector2(160, 160) - virtual_joystick.custom_minimum_size = joy_size - virtual_joystick.size = joy_size - - # Position relative to Bottom-Left anchor - # joystick_position (120, -120) interpreted as margin from anchor - # x=120 (right from left edge), y=-120 (up from bottom edge - implies bottom margin) - # We want the *center* or *bottom-left* corner? - # Assuming (120, -120) is top-left corner of the control relative to anchor? - # Let's align bottom-left corner of control to (120, -120) from screen bottom-left - # Screen Bottom-Left is (0, 1) in normalized anchors. - # offset_left = 120 - # offset_bottom = -120 (120px up from bottom) - # offset_top = -120 - 160 = -280 - # offset_right = 120 + 160 = 280 - - virtual_joystick.offset_left = 120 - virtual_joystick.offset_top = -280 - virtual_joystick.offset_right = 280 - virtual_joystick.offset_bottom = -120 - - virtual_joystick.direction_changed.connect(_on_joystick_direction) - container.add_child(virtual_joystick) + # Helper to find or create button logic moved to function _find_or_create_action_button # Create action buttons (bottom-right) - grab_button = _create_action_button("Grab", "👋", button_positions.grab) - put_button = _create_action_button("Put", "📦", button_positions.put) - special_button = _create_action_button("Special", "⚡", button_positions.special) - - container.add_child(grab_button) - container.add_child(put_button) - container.add_child(special_button) + grab_button = _find_or_create_action_button(container, "Grab", "👋", button_positions.grab) + put_button = _find_or_create_action_button(container, "Put", "📦", button_positions.put) + special_button = _find_or_create_action_button(container, "Special", "⚡", button_positions.special) # Create settings button (top-right corner) - settings_button = Button.new() - settings_button.name = "SettingsBtn" - settings_button.text = "⚙" - settings_button.set_anchors_preset(Control.PRESET_TOP_RIGHT) - settings_button.offset_left = -70 # Use offsets instead of position for anchored controls - settings_button.offset_right = -20 - settings_button.offset_top = 70 - settings_button.offset_bottom = 120 - settings_button.custom_minimum_size = Vector2(50, 50) - settings_button.mouse_filter = Control.MOUSE_FILTER_STOP # Ensure it receives input - settings_button.pressed.connect(_on_settings_pressed) - _style_button(settings_button, 0.5) - container.add_child(settings_button) + settings_button = container.get_node_or_null("SettingsBtn") + if not settings_button: + settings_button = Button.new() + settings_button.name = "SettingsBtn" + settings_button.text = "⚙" + settings_button.set_anchors_preset(Control.PRESET_TOP_RIGHT) + settings_button.offset_left = -70 + settings_button.offset_right = -20 + settings_button.offset_top = 70 + settings_button.offset_bottom = 120 + settings_button.custom_minimum_size = Vector2(50, 50) + settings_button.mouse_filter = Control.MOUSE_FILTER_STOP + _style_button(settings_button, 0.5) + container.add_child(settings_button) + if not settings_button.pressed.is_connected(_on_settings_pressed): + settings_button.pressed.connect(_on_settings_pressed) + # Always visible now - controlled by settings toggle # Can be hidden via settings if user doesn't want touch controls on desktop visible = true +func _find_or_create_action_button(container: Control, button_name: String, icon: String, pos: Vector2) -> Button: + var btn = container.get_node_or_null(button_name + "Btn") + if btn: + print("[TouchControls] Found existing %s button" % button_name) + # Style it and connect + _style_button(btn, button_opacity) + # Avoid duplicate signal connections + if not btn.button_down.is_connected(_on_button_pressed): # Wait, cannot check lambda easily + # Disconnect all to be safe if previously connected + for conn in btn.button_down.get_connections(): + if conn["callable"].get_object() == self: + btn.button_down.disconnect(conn["callable"]) + for conn in btn.button_up.get_connections(): + if conn["callable"].get_object() == self: + btn.button_up.disconnect(conn["callable"]) + + btn.button_down.connect(func(): _on_button_pressed(button_name)) + btn.button_up.connect(func(): _on_button_released(button_name)) + return btn + + # Create new + var new_btn = _create_action_button(button_name, icon, pos) + container.add_child(new_btn) + return new_btn + func _create_action_button(button_name: String, icon: String, pos: Vector2) -> Button: var btn = Button.new() btn.name = button_name + "Btn" diff --git a/scripts/managers/user_profile_manager.gd b/scripts/managers/user_profile_manager.gd index f3de754..61eb2e4 100644 --- a/scripts/managers/user_profile_manager.gd +++ b/scripts/managers/user_profile_manager.gd @@ -91,7 +91,7 @@ func load_stats() -> Dictionary: if not NakamaManager.session: return {} - var user_id := NakamaManager.session.user_id + var user_id: String = NakamaManager.session.user_id var storage_result = await NakamaManager.client.read_storage_objects_async( NakamaManager.session, @@ -132,7 +132,7 @@ func update_display_name(new_name: String) -> bool: emit_signal("profile_update_failed", "Display name too long (max 50 characters)") return false - var result := await NakamaManager.client.update_account_async( + var result: NakamaAsyncResult = await NakamaManager.client.update_account_async( NakamaManager.session, null, # username (don't change) new_name # display_name diff --git a/server/docker-compose.yaml b/server/docker-compose.yaml index 8b9e11c..96a0766 100644 --- a/server/docker-compose.yaml +++ b/server/docker-compose.yaml @@ -1,5 +1,3 @@ -version: '3.8' - services: postgres: container_name: nakama-postgres diff --git a/tiles_armagedon_a1.res b/tiles_armagedon_a1.res new file mode 100644 index 0000000000000000000000000000000000000000..c25cf1ec482b79ca0d8be687915d085728ab366b GIT binary patch literal 11257 zcmeHNd3Y36wy%&tNJ1cqkbta#xF8O)J`m`%D8rla`!eHaP(Vj&s=KQ@m2|qA>h3HI zo8ZDQvP2{>fS)>`vgi{>b|eW12x0WeAU+rdqv8WY2rg{0rQZ47+qb(zI)V<)U$6SB zx^>Sz_uO0e{?2xjhjSl(Knh-QRN`($cOu;*4MQo-QGI#z&aYT1W)Hh#(8{HwoPu^E zWq_DE(ru_dJz&^X*{$0Zx9rztpI!B+QrpNjk5Z^gDU9NAsD|t{G~LkrrEE?fwZt!b z6`xN^CJw)$czj;PP(AiiDTxR+x9V}og}Os!-1dOm&1;b7P`&;<*9lxMIp-eJjLTt-E83vJ>bgo zsI*Ox8mh@c0H)i78j^r9)k4kZ({zulda$$(HdjGpODQVEp&G7IxiH}NYhJge8f?%| zv+h+5#jhI-Q(>w{4ipVwpBJKu*x=~0F6{7_us(LTsu*@%@rSxX{X$d;+ZU?Ip><=Q8K#SA+(O)nNAu}^L-&@3 zL_$5nWa%0%r&}*BRJ=@y6g|`lYC~0qXIXJNRl8rXh~Y3*VxgVFdNQ%P*yqrW;w$r1 zXax<@NP3vc*A-?WOmnEJm(<-LgeoOcMx!>TTl30(#p5E4W7Z|yjWyXczCUc4!fj&S z(S!x}1at5OY`%cuBm<@R{bYp%5CHva+kAeqKrVkC)50c=1)7IjifCfRY;KPtmRYw# z%}?J*iYz9ObzM$oT%_wu6k}gsA~TWF_4#bNPF56~ZBy(895rIzz)6UD4Ud=!&||1h zL$UiKRUh^sg3N((4X+RZBOAdRB!_?q)pn4@)55CF%rqwzD!u|XwIEA`JK+w5bqo~A zZl#pW3>-+1GibvjC_(Vyvjau_fg`dSP(svnr_-nUnE+J%1{p5JqZkawjVLntBg4x~ zDa-)cigm;7i0H$I7|>{l0gZ>AOt14Ux z9U6CqhH6)}BJw}vPW58$2lzYOzV21*aOse1)7);Gt{6eT4L2Aj6~F-SwQETYE<-P7 zeibj|ZuJ1^J{Hxfm8cHcr%mFP8J^XvlfC66a70wuLV`mv{2CchS_%S$kJ%Tmp_gc+ zbfvOWafG!Fi*`*DeJRsS?jp&Z%3hj;Tc()Ar7{%3$I5!Z@76rD5SSZ4EBf&PSgZoj zUPC6MAt=n-iAu1SQW9>Cr`|q0LCDYKGqr=6#fWkqoDPPSIhbh=ysC^C%f~#m3@fMD zsRk{JKDdn}fk%cIKa*}2)D64mo?KcM2kmE$LO9VB_rOuI zT{DVQX(IN4uIn&Q92l^@uR z-YaXed;EH3_A+B(wj@orLNa0vx~v|0K+caEW->i8D>z_VJ?}K*)RXs%Ivj|Ex$R#430X?@yFL?ryO3J{jBt2v%|n% zhaeh9u3QX1wFt`m`i{M8;yx z^OCfpw%9^pvG?!B}0y<>8G|x0YAmtWgo|(txV(w6+ zP>KZGj*kN7(TJNFA$W~DKd7l_o3_s~W=l=QFJF+YpJnUAd!CoAV8Zf|ljd$?09CHN zzy$N6QG!W^RIE}7g9^pcGnN!9h6V;Djy61=?$-27pfZN;1lorf_Do`YF|_(g zbfZrj`kJ<^-j-8NIQoFLb_{v#82W%bATI3(F!vuc9=-#26wD3yd2G~2G+KEl)7<$r zz0j3eFG<5vOf&DeBps!k0`>z}O+LNP5D8Ii+amD{L70<{jt!kUxz4C}e>L zZMa&_FwONuzc7W@cYe*sg$&3?U6hayx{wQbkcF#$4wqk7&g;!$3R)|cIAqgYnNlTC znn4CIU=xixw9|7lwyLYY~Ae*fSt2j_?F@wPd!eHPw;` zO$mglL_!h}#cf$TqNh?z8r?t?>=o}TULJS&~<_Vmo4GK0#F^z0~gVjmvO*8M7C zQKH~(;LnFUb0){c(KM4TjxUZ8(-eCxFvD?Wn%Hx2i@oP{fd!7225JXxaG?}v=hXZ?jZa&g(xOcd%swcO6&Hx~aGW#t@=%uiBDO z2X6|b!Fq9f2DAZb9D_;p4zwjO;ZZ0sXc?R z*pY6aF5{=;ZqUzzyK!tM7j$@$+{L{|9(xs~B=PdtW2Sle5n8kLyghC`V-7v0-@)6r z|G_kW@hs(S<4tof#ZjiY<$EBY+5Jb<)5Pw;`wCw+v}m^F(|0N?E6wr$k}rb~;l2!> z3_Bn8WtafGSvhR^AWasNk76GNO9n9jYPduult}O4XdB%z8I(B6 zL!Lz^@DyUZ=nUQ?KM=5~a=(5na}F7wo{en-YJ&i--QF_wmr*Ud`JV?(kgs``o4G z&}lX1#%ce3cE-}T&KzEB{kHq$ekZmT-}$96>E6$tyKl$2mQVG$I(N#wC)$^Iuf9F? zua|dDf9#w*rAvc3x$MZ}51#wxl=4OGPRG0p?<^i(f4ErtzUWT{rw0^cXOih|k_OZp47dJCK3E zI<{u^GVG;ONgv^t`S^Pn3zWBONlR{7ixhwklYeC@R^Xna<{Qho;5*LrZTw zJsCyO%#Te^BGZt_6r;JcB{Efs6+3 zU8F^SU2Qupb-Xy}z>@ds{?_4lgL4*bK3&|t?U0gJ?yXB}x9+a{cU2x4)AdX*^@Ezj zgS+qOxo*`u3PjAxstqf?MR{z?Yo|~?J$~peDy{tw?Jd@w!+Kk< zkl&A6W`|6TA2C$b1nW#?mj285h_(oV?&b43-a(0&E@PL0GwD`XMVx zW|ofUwy9*>cqEAIqa#IxoyZn-re_x_@lnwt3`8wxi)!mc+yI6swvMO8&D**jKh4$+ zrD1tm{3Z(mGwZi*=4o;DR-_50IsGk~5TzHRcJQ>=vVo_?OR9KUGx-gk7Tb3rognwi z_>ZRb-OJPBHS2j=JiQumLuUQkn|b;Xv!AEMJ^#kj;?FD{s$EKXl0!e{sva{q+Tt)_!|(@;(2% zDzENfVSf6F2Yo$H3@twN&u1rIKD*)%S>J3c*>h#Vq|H~hO~Ot^)o+o0H>2Nq)DN*l z{`lo}h$FsH9yyyd_WZkuD-cs`pPzdH@xkKCHD@cw=gdWnfjFV>p4O)xv8{O;al_qZ zEoLI-_`Le}+Yo;s-k7=Pow|A)V89Q>-dUVuKTESIs8b1&6)M*M-$?>1ha~MUHYQ& zZ_L5yzqqcAf2;3`{<~V;_%~t0%X=?Y#BHY?tc+iOp*m)8jm_AQmDJ?OIGiqAcr@o|6@qo^!0TBP%NopMkrP4f;7xgz&c&BoH{+hL0CH4bC6A zZ%?E}NA`k~NBH(6%3ATTizIxCzlgge_FZ%bD zp3-ep-Y(rie@(kx>P;m+I$9j?E25(?2h`SH@S8ZI9tu4Oe` z?y?%_LFBL1#pS+-PsRlPdFA}1gD1!7_nbSI|HjD=^E)5^1;TI7wDimE@*8#Fef zKlsBJ$MLbwz%Vcip9#MTzX>1fET6FO*0M*-*De^g3brZo7CRKebYv2A^G~_N={m5? y&o;$hYQiU5B4$NS&c8hx_QWYyRO~@v#Fs_EUgIM$KQW6$9i7CH*u(TszyAj_G=lB` literal 0 HcmV?d00001