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 6897925..b877432 100644 Binary files a/assets/models/meshes/tiles.res and b/assets/models/meshes/tiles.res differ diff --git a/assets/models/meshes/tiles_armagedon_a4.res b/assets/models/meshes/tiles_armagedon_a4.res index 1bd617e..6345c37 100644 Binary files a/assets/models/meshes/tiles_armagedon_a4.res and b/assets/models/meshes/tiles_armagedon_a4.res differ 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 0000000..c25cf1e Binary files /dev/null and b/tiles_armagedon_a1.res differ