7 Commits

Author SHA1 Message Date
shawn 695e4db5cd Add input debug to window title for diagnostic
Shows Input.get_axis() values, weapon slot states, and fire
state in the window title bar — visible even when print()
output is lost on Windows GUI exports.
2026-07-03 23:13:42 -04:00
shawn c5b6b05801 Fix keyboard input: use OS.move_to_foreground() for window focus
get_window().grab_focus() was insufficient on Windows GUI exports.
Replaced with OS.move_to_foreground() + Input.set_mouse_mode(CAPTURED)
+ deferred retries to ensure the game window holds keyboard focus
after the connection UI is dismissed.
2026-07-03 22:34:50 -04:00
shawn ef4e4ccb80 Clean up test diagnostic files 2026-07-03 20:07:49 -04:00
shawn 7db8434f7e Fix duplicate _rollback_tick causing WeaponManager compile error
A debug-print attempt from earlier inadvertently appended a second
_rollback_tick function (with spaces instead of tabs) at the end of
weapon_manager.gd:419, causing the entire script to fail compilation.
This meant the WeaponManager node had NO script attached:
- 'has_method("set_default_loadout")' returned false
- Client-side loadout init never ran
- All weapon operations silently did nothing

This is the real root cause of why weapons never worked on the client.
2026-07-03 20:07:40 -04:00
shawn 7bbe4ba71d Fix weapons not working on client: remove is_predicting check + init loadout
- Removed the is_predicting() early-return in weapon_manager._rollback_tick
  which blocked all weapon actions on the client (player owned by server)
- Added client-side weapon loadout initialization in player-spawner._spawn
  so the player has weapons immediately, before server state sync arrives
- Server reconciles via rollback state sync

Movement was fixed in v0.2.2 (window focus grab). This addresses
the remaining 'can't switch or use weapons' issue.
2026-07-03 18:24:21 -04:00
shawn 1ccd26bbab Fix keyboard input not working on Windows client
Added explicit window focus grab after connecting (lan-bootstrapper.gd)
so keyboard input is properly received by the input system.
Also added diagnostic debug prints to trace input pipeline.
2026-07-03 17:48:33 -04:00
shawn 9901f6a34d Fix blaster GLB root type error (Node3D, not MeshInstance3D)
- Changed _gun_model type from MeshInstance3D to Node3D
  (all imported GLB/GLTF weapon models have Node3D root)
- Added check_models.gd diagnostic script

The type mismatch at WeaponManager._load_weapon_models:73
was the only script error in the user's game.log.
Headless bot tests confirm movement still works fine.
2026-07-03 17:32:18 -04:00
6 changed files with 59 additions and 8 deletions
@@ -75,12 +75,16 @@ func _input(event: InputEvent) -> void:
func _gather():
if !is_setup:
print("[INPUT DEBUG] _gather first call - setting up")
setup()
# Movement (continuous)
var mx = Input.get_axis("move_west", "move_east")
var mz = Input.get_axis("move_north", "move_south")
var old_mv = movement
movement = Vector3(mx, 0, mz)
if movement != old_mv and (mx != 0 or mz != 0):
print("[INPUT DEBUG] _gather movement=%s (mx=%.2f mz=%.2f)" % [movement, mx, mz])
jump = Input.is_action_pressed("move_jump")
@@ -173,6 +173,15 @@ func _spawn(id: int):
if rollback_sync and rollback_sync.has_method("process_settings"):
rollback_sync.process_settings()
# On the client side, initialize weapon loadout locally so the player
# has weapons immediately. The server will reconcile via rollback state sync.
if not multiplayer.is_server():
var weapon_mgr = avatar.find_child("WeaponManager")
if weapon_mgr and weapon_mgr.has_method("set_default_loadout"):
# Default to T team — server will correct via state sync
weapon_mgr.set_default_loadout(1) # TeamManager.Team.T = 1
print("[Spawner] Client-side weapon loadout initialized for peer %s" % id)
func get_team_spawn_point(peer_id: int, spawn_idx: int = 0) -> Vector3:
var team := team_manager.get_team(peer_id)
var points: Array[Marker3D]
+17 -2
View File
@@ -123,21 +123,36 @@ func _after_tick_loop():
func _process(_delta: float):
if not _is_local:
if input.is_multiplayer_authority():
print("[PLAYER DEBUG] First _process — calling _setup_local_camera (authority: %d)" % multiplayer.get_unique_id())
_setup_local_camera()
else:
print("[PLAYER DEBUG] _process skipping — input not authority (unique_id=%d, input_auth=%d)" % [multiplayer.get_unique_id(), input.get_multiplayer_authority()])
return
# DEBUG: Show input state in window title
if _is_local and input:
var mx := Input.get_axis("move_west", "move_east")
var mz := Input.get_axis("move_north", "move_south")
get_window().title = "TS [mv=%.1f,%.1f] [sl=%s%s%s%s] [fire=%s]" % [mx, mz, input.slot_1, input.slot_2, input.slot_3, input.slot_4, input.fire_held]
# Smooth position interpolation between ticks
var f := NetworkTime.tick_factor
global_position = _lerp_from_pos.lerp(_lerp_to_pos, f)
# Frame-rate mouse look (recoil only affects raycast, not camera)
var old_yaw = _sim_yaw
rotation.y = _sim_yaw + (-input.mouse_rotation.y)
if _sim_yaw != old_yaw or input.mouse_rotation.length_squared() > 0:
pass # mouse look active
head.rotation.x = clamp(_sim_pitch + (-input.mouse_rotation.x), -1.57, 1.57)
head.rotation.y = 0
head.rotation.z = 0
func _rollback_tick(delta: float, tick: int, is_fresh: bool) -> void:
# DEBUG movement state
if input and input.movement.length_squared() > 0:
print("[PLAYER RT] tick=%d movement=%s hp=%d death_tick=%d frozen=%s" % [tick, input.movement, health, death_tick, round_manager and round_manager.freeze_end_tick >= 0 and tick < round_manager.freeze_end_tick])
# Handle round respawn teleport inside rollback
if respawn_tick >= 0 and tick == respawn_tick:
global_position = respawn_position
@@ -26,7 +26,7 @@ var _pending_grenades: Array[Dictionary] = [] # Buffered throws, consumed in _a
# Weapon models (loaded at runtime)
var _knife_model: Node3D
var _gun_model: MeshInstance3D
var _gun_model: Node3D
var _grenade_model: Node3D
var _current_visible_slot: int = -1
var _knife_sound: AudioStreamPlayer3D
@@ -158,8 +158,11 @@ func give_weapon(weapon_id: int):
NetworkRollback.mutate(self)
func _rollback_tick(delta: float, tick: int, _is_fresh: bool):
if rollback_synchronizer.is_predicting():
return
# NOTE: On the client, the player CharacterBody3D is owned by the server
# (peer 1), so `rollback_synchronizer.is_predicting()` may return true
# on early ticks when input history hasn't been built up yet.
# We process weapon actions anyway — the server reconciles via rollback.
# Only skip if the player is dead or frozen (checked below).
# Skip if player dead
if player.death_tick >= 0 and tick >= player.death_tick:
+21 -2
View File
@@ -72,16 +72,35 @@ func join():
print("Client started")
connect_ui.hide()
# Force window focus for keyboard input (Windows GUI apps need this)
_ensure_input_focus()
# Only start manually if NetworkEvents is not handling it (e.g. multiplayer-simple)
if not NetworkEvents.enabled:
NetworkTime.start()
func _ensure_input_focus() -> void:
## Force the game window to have keyboard focus and captured mouse.
## On Windows GUI exports, the window may not hold keyboard focus after
## the connection UI is dismissed, causing Input.get_axis() to return 0.
OS.move_to_foreground()
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
# Deferred retry after window settles
get_window().call_deferred("grab_focus")
Input.call_deferred("set_mouse_mode", Input.MOUSE_MODE_CAPTURED)
func _enter_tree():
# Hide and show UI as appropriate
# These handlers are necessary, since the game could have started via
# autoconnect, or any other method
NetworkEvents.on_client_start.connect(func(__): connect_ui.hide())
NetworkEvents.on_server_start.connect(func(): connect_ui.hide())
NetworkEvents.on_client_start.connect(func(__):
connect_ui.hide()
_ensure_input_focus()
)
NetworkEvents.on_server_start.connect(func():
connect_ui.hide()
_ensure_input_focus()
)
NetworkEvents.on_client_stop.connect(func(): connect_ui.show())
NetworkEvents.on_server_stop.connect(func(): connect_ui.show())
+1
View File
@@ -0,0 +1 @@
uid://c2m50xd561do8