bugfix, desync, and add UI function

This commit is contained in:
2026-01-14 00:20:20 +08:00
parent 6948a4aed1
commit bee9c30f0e
14 changed files with 362 additions and 112 deletions
+33
View File
@@ -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`.
Binary file not shown.
Binary file not shown.
+50 -18
View File
@@ -59,9 +59,14 @@ func _init_managers():
screen_shake_manager.initialize($Camera3D) screen_shake_manager.initialize($Camera3D)
# Touch controls for mobile # Touch controls for mobile
touch_controls = load("res://scripts/managers/touch_controls.gd").new() # Touch controls for mobile
touch_controls.name = "TouchControls" touch_controls = get_node_or_null("TouchControls")
add_child(touch_controls) 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) touch_controls.initialize(self)
# Connect signals for UI updates # Connect signals for UI updates
@@ -180,8 +185,16 @@ func broadcast_message(player_name: String, message: String):
func _setup_global_match_timer_ui(): func _setup_global_match_timer_ui():
"""Create the global match timer display at the top of the screen.""" """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") var existing = get_node_or_null("GlobalMatchTimer")
if existing: if existing:
existing.visible = true
return return
# Create timer panel # Create timer panel
@@ -345,21 +358,40 @@ func _setup_client_game():
var my_id = multiplayer.get_unique_id() var my_id = multiplayer.get_unique_id()
print("Client setup - my peer ID: ", my_id) print("Client setup - my peer ID: ", my_id)
# Create local player immediately # Pre-spawn ALL players known from LobbyManager (including Host ID 1)
if not has_node(str(my_id)): # This ensures nodes exist to receive RPCs (like 'set_spawn_position') that might arrive before full sync
var player_character = PlayerManager.add_player_character(my_id) var lobby_players = LobbyManager.get_players()
add_child(player_character) for player_data in lobby_players:
player_character.add_to_group("Players", true) var p_id = player_data.get("id", 0)
GameStateManager.add_player(my_id) if p_id != 0:
GameStateManager.local_player_character = player_character add_player_character(p_id)
ui_manager.set_local_player(player_character) print("Client: Pre-spawned player ", p_id)
if touch_controls:
touch_controls.set_player(player_character)
ui_manager.update_button_states()
print("Created local player for client: ", my_id)
# Wait for host to be ready, then request full sync # Pre-spawn potential bots (IDs 2 to MaxPlayers) to prevent RPC "Node not found" errors
await get_tree().create_timer(2.0).timeout # 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) rpc_id(1, "request_full_player_sync", my_id)
func _auto_start_from_lobby(): func _auto_start_from_lobby():
@@ -483,7 +515,7 @@ func add_player_character(peer_id: int):
func _on_peer_connected(new_peer_id: int): func _on_peer_connected(new_peer_id: int):
if multiplayer.is_server(): 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) add_player_character(new_peer_id)
rpc("add_newly_connected_player_character", new_peer_id) rpc("add_newly_connected_player_character", new_peer_id)
+89 -1
View File
@@ -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="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"] [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://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://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="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"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_playerboard"]
content_margin_left = 8.0 content_margin_left = 8.0
@@ -9699,6 +9701,92 @@ custom_minimum_size = Vector2(0, 40)
layout_mode = 2 layout_mode = 2
text = "Back" 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/Host" to="." method="_on_host_pressed"]
[connection signal="pressed" from="Menu/Join" to="." method="_on_join_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"] [connection signal="text_submitted" from="MessageInput" to="." method="_on_message_input_text_submitted"]
+26 -6
View File
@@ -78,6 +78,8 @@ var spawn_locations = [
var finish_locations: Array: var finish_locations: Array:
get: return race_manager.finish_locations if race_manager else [] 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 var spawn_point_selected = false
# Action for hilighter # Action for hilighter
@@ -145,8 +147,8 @@ func _ready():
if is_multiplayer_authority(): if is_multiplayer_authority():
rpc("sync_display_name", display_name) rpc("sync_display_name", display_name)
# Wait briefly to ensure proper scene setup # Wait briefly to ensure proper scene setup and server recognition
await get_tree().create_timer(0.1).timeout await get_tree().create_timer(0.5).timeout
# More robust way to get the main scene # More robust way to get the main scene
var main_scene = get_tree().get_root().get_node_or_null("Main") var main_scene = get_tree().get_root().get_node_or_null("Main")
@@ -226,8 +228,11 @@ func _ready():
1.0, 1.0,
current_position.y * cell_size.z + cell_size.z * 0.5 current_position.y * cell_size.z + cell_size.z * 0.5
) )
target_visual_position = global_position
if is_multiplayer_authority(): if is_multiplayer_authority():
rpc("sync_position", current_position) rpc("sync_position", current_position)
else:
target_visual_position = global_position
func _init_managers(): func _init_managers():
movement_manager = load("res://scripts/managers/player_movement_manager.gd").new() movement_manager = load("res://scripts/managers/player_movement_manager.gd").new()
@@ -609,6 +614,11 @@ func _process(delta):
if _verify_timer >= 3.0: if _verify_timer >= 3.0:
_verify_timer = 0.0 _verify_timer = 0.0
rpc("ping_existence") 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 # Delegate rotation to movement manager
if movement_manager: if movement_manager:
@@ -942,7 +952,8 @@ static func reset_race_stats():
@rpc("any_peer", "call_local", "unreliable") @rpc("any_peer", "call_local", "unreliable")
func remote_set_position(authority_position): 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") @rpc("any_peer", "call_local")
func display_message(message, type: int = 0): 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(): func update_visual_position():
# Ensure proper grid-aligned positioning # Ensure proper grid-aligned positioning
global_position = Vector3( var new_pos = Vector3(
current_position.x * cell_size.x + cell_size.x * 0.5, current_position.x * cell_size.x + cell_size.x * 0.5,
1.0, 1.0,
current_position.y * cell_size.z + cell_size.z * 0.5 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(): if is_multiplayer_authority():
rpc("sync_position", current_position) rpc("sync_position", current_position)
@@ -1383,11 +1397,14 @@ func update_visual_position():
func sync_position(pos: Vector2i): func sync_position(pos: Vector2i):
current_position = pos current_position = pos
# Always update the visual position after position sync # 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, current_position.x * cell_size.x + cell_size.x * 0.5,
cell_size.y, cell_size.y,
current_position.y * cell_size.z + cell_size.z * 0.5 current_position.y * cell_size.z + cell_size.z * 0.5
) + cell_offset ) + cell_offset
global_position = new_pos
target_visual_position = new_pos # Reset smoothing target to prevent fighting
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
func set_spawn_position(pos: Vector2i): func set_spawn_position(pos: Vector2i):
@@ -1397,11 +1414,14 @@ func set_spawn_position(pos: Vector2i):
# Clear any spawn highlights # Clear any spawn highlights
clear_spawn_highlights() clear_spawn_highlights()
# Update visual position # Update visual position
global_position = Vector3( var new_pos = Vector3(
current_position.x * cell_size.x + cell_size.x * 0.5, current_position.x * cell_size.x + cell_size.x * 0.5,
cell_size.y, cell_size.y,
current_position.y * cell_size.z + cell_size.z * 0.5 current_position.y * cell_size.z + cell_size.z * 0.5
) + cell_offset ) + cell_offset
global_position = new_pos
target_visual_position = new_pos
@rpc("any_peer", "call_local", "reliable") @rpc("any_peer", "call_local", "reliable")
+12 -1
View File
@@ -33,6 +33,11 @@ func _ready():
rng.seed = name.hash() rng.seed = name.hash()
_tick_counter = rng.randi() % tick_rate _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) # Get parent (should be player character)
actor = get_parent() actor = get_parent()
# ... (rest of _ready) ... # ... (rest of _ready) ...
@@ -153,7 +158,13 @@ func _run_ai_tick():
print("[BotController] Action Taken: Arrange") print("[BotController] Action Taken: Arrange")
return 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 # Power-Up / Sabotage
+51 -14
View File
@@ -223,27 +223,64 @@ func find_best_tile_to_grab() -> Dictionary:
return best_tile return best_tile
func find_nearest_tile_of_type(tile_types: Array) -> Vector2i: 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 current_pos = actor.current_position
var nearest_pos = Vector2i(-1, -1)
var nearest_dist = 999999
if not enhanced_gridmap: if not enhanced_gridmap:
return nearest_pos return Vector2i(-1, -1)
for x in range(enhanced_gridmap.columns): # Optimization: Start check at simple radius
for z in range(enhanced_gridmap.rows): # 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 pos = Vector2i(x, z)
var cell = Vector3i(x, 1, z) var max_radius = 25 # Limit search range to prevent full map scans on huge maps
var item = enhanced_gridmap.get_cell_item(cell) 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) var dist = abs(pos.x - current_pos.x) + abs(pos.y - current_pos.y)
if dist < nearest_dist: if dist < min_dist:
nearest_dist = dist min_dist = dist
nearest_pos = pos 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 # Movement Strategy
+9 -9
View File
@@ -64,7 +64,7 @@ func _try_restore_session() -> void:
if session.is_expired(): if session.is_expired():
# Try to refresh # Try to refresh
if refresh_token: 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(): if not refreshed.is_exception():
session = refreshed session = refreshed
_save_session(session, saved_auth_mode) _save_session(session, saved_auth_mode)
@@ -118,7 +118,7 @@ func login_as_guest() -> bool:
var device_id := _get_device_id() var device_id := _get_device_id()
print("[AuthManager] Guest login with device: ", device_id.substr(0, 8) + "...") 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(): if session.is_exception():
var error: String = session.get_exception().message 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) 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(): if session.is_exception():
var error: String = session.get_exception().message 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) print("[AuthManager] Registering: ", email)
# Create account (true = create if not exists) # 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(): if session.is_exception():
var error: String = session.get_exception().message var error: String = session.get_exception().message
@@ -237,7 +237,7 @@ func login_with_google(id_token: String) -> bool:
print("[AuthManager] Google login...") 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(): if session.is_exception():
var error: String = session.get_exception().message var error: String = session.get_exception().message
@@ -265,7 +265,7 @@ func login_with_apple(id_token: String) -> bool:
print("[AuthManager] Apple login...") 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(): if session.is_exception():
var error: String = session.get_exception().message var error: String = session.get_exception().message
@@ -293,7 +293,7 @@ func login_with_facebook(access_token: String) -> bool:
print("[AuthManager] Facebook login...") 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(): if session.is_exception():
var error: String = session.get_exception().message 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...") 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(): if result.is_exception():
push_error("[AuthManager] Link failed: " + result.get_exception().message) 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: if not is_authenticated or not NakamaManager.session:
return false 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(): if result.is_exception():
return false return false
+2 -2
View File
@@ -31,7 +31,7 @@ var match_duration: int = 180 # Default 3 minutes
var randomize_spawn: bool = true # Default enabled var randomize_spawn: bool = true # Default enabled
# Timer setting # 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) signal enable_cycle_timer_changed(enabled: bool)
# Character and area selection # Character and area selection
@@ -419,4 +419,4 @@ func reset() -> void:
match_duration = 180 # Reset to default 3 minutes match_duration = 180 # Reset to default 3 minutes
selected_area = "Desert" selected_area = "Desert"
local_character_index = 0 local_character_index = 0
enable_cycle_timer = true enable_cycle_timer = false
+88 -57
View File
@@ -40,77 +40,108 @@ func set_player(p_player: Node3D):
local_player = p_player local_player = p_player
func _create_touch_ui(): 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 # Use layer 10 - above regular UI but below pause menu
layer = 10 layer = 10
# Create main container # Check if container already exists (added in scene)
var container = Control.new() var container = get_node_or_null("TouchControls")
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)
# Create virtual joystick (bottom-left) if not container:
var joystick_script = load("res://scripts/ui/virtual_joystick.gd") # Create main container if missing
virtual_joystick = Control.new() container = Control.new()
virtual_joystick.set_script(joystick_script) container.name = "TouchControls"
virtual_joystick.name = "VirtualJoystick" container.set_anchors_preset(Control.PRESET_FULL_RECT)
virtual_joystick.set_anchors_preset(Control.PRESET_BOTTOM_LEFT) 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) # Helper to find or create button logic moved to function _find_or_create_action_button
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)
# Create action buttons (bottom-right) # Create action buttons (bottom-right)
grab_button = _create_action_button("Grab", "👋", button_positions.grab) grab_button = _find_or_create_action_button(container, "Grab", "👋", button_positions.grab)
put_button = _create_action_button("Put", "📦", button_positions.put) put_button = _find_or_create_action_button(container, "Put", "📦", button_positions.put)
special_button = _create_action_button("Special", "", button_positions.special) special_button = _find_or_create_action_button(container, "Special", "", button_positions.special)
container.add_child(grab_button)
container.add_child(put_button)
container.add_child(special_button)
# Create settings button (top-right corner) # Create settings button (top-right corner)
settings_button = Button.new() settings_button = container.get_node_or_null("SettingsBtn")
settings_button.name = "SettingsBtn" if not settings_button:
settings_button.text = "" settings_button = Button.new()
settings_button.set_anchors_preset(Control.PRESET_TOP_RIGHT) settings_button.name = "SettingsBtn"
settings_button.offset_left = -70 # Use offsets instead of position for anchored controls settings_button.text = ""
settings_button.offset_right = -20 settings_button.set_anchors_preset(Control.PRESET_TOP_RIGHT)
settings_button.offset_top = 70 settings_button.offset_left = -70
settings_button.offset_bottom = 120 settings_button.offset_right = -20
settings_button.custom_minimum_size = Vector2(50, 50) settings_button.offset_top = 70
settings_button.mouse_filter = Control.MOUSE_FILTER_STOP # Ensure it receives input settings_button.offset_bottom = 120
settings_button.pressed.connect(_on_settings_pressed) settings_button.custom_minimum_size = Vector2(50, 50)
_style_button(settings_button, 0.5) settings_button.mouse_filter = Control.MOUSE_FILTER_STOP
container.add_child(settings_button) _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 # Always visible now - controlled by settings toggle
# Can be hidden via settings if user doesn't want touch controls on desktop # Can be hidden via settings if user doesn't want touch controls on desktop
visible = true 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: func _create_action_button(button_name: String, icon: String, pos: Vector2) -> Button:
var btn = Button.new() var btn = Button.new()
btn.name = button_name + "Btn" btn.name = button_name + "Btn"
+2 -2
View File
@@ -91,7 +91,7 @@ func load_stats() -> Dictionary:
if not NakamaManager.session: if not NakamaManager.session:
return {} 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( var storage_result = await NakamaManager.client.read_storage_objects_async(
NakamaManager.session, 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)") emit_signal("profile_update_failed", "Display name too long (max 50 characters)")
return false return false
var result := await NakamaManager.client.update_account_async( var result: NakamaAsyncResult = await NakamaManager.client.update_account_async(
NakamaManager.session, NakamaManager.session,
null, # username (don't change) null, # username (don't change)
new_name # display_name new_name # display_name
-2
View File
@@ -1,5 +1,3 @@
version: '3.8'
services: services:
postgres: postgres:
container_name: nakama-postgres container_name: nakama-postgres
Binary file not shown.