bugfix, desync, and add UI function
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
container_name: nakama-postgres
|
container_name: nakama-postgres
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user