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
+12 -1
View File
@@ -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
+51 -14
View File
@@ -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
+9 -9
View File
@@ -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
+2 -2
View File
@@ -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
+88 -57
View File
@@ -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"
+2 -2
View File
@@ -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