2 Commits

Author SHA1 Message Date
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
7 changed files with 40 additions and 6 deletions
@@ -75,12 +75,16 @@ func _input(event: InputEvent) -> void:
func _gather(): func _gather():
if !is_setup: if !is_setup:
print("[INPUT DEBUG] _gather first call - setting up")
setup() setup()
# Movement (continuous) # Movement (continuous)
var mx = Input.get_axis("move_west", "move_east") var mx = Input.get_axis("move_west", "move_east")
var mz = Input.get_axis("move_north", "move_south") var mz = Input.get_axis("move_north", "move_south")
var old_mv = movement
movement = Vector3(mx, 0, mz) 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") jump = Input.is_action_pressed("move_jump")
+11 -2
View File
@@ -123,21 +123,30 @@ func _after_tick_loop():
func _process(_delta: float): func _process(_delta: float):
if not _is_local: if not _is_local:
if input.is_multiplayer_authority(): if input.is_multiplayer_authority():
print("[PLAYER DEBUG] First _process — calling _setup_local_camera (authority: %d)" % multiplayer.get_unique_id())
_setup_local_camera() _setup_local_camera()
else: else:
print("[PLAYER DEBUG] _process skipping — input not authority (unique_id=%d, input_auth=%d)" % [multiplayer.get_unique_id(), input.get_multiplayer_authority()])
return return
# Smooth position interpolation between ticks # Smooth position interpolation between ticks
var f := NetworkTime.tick_factor var f := NetworkTime.tick_factor
global_position = _lerp_from_pos.lerp(_lerp_to_pos, f) global_position = _lerp_from_pos.lerp(_lerp_to_pos, f)
# Frame-rate mouse look (recoil only affects raycast, not camera) # Frame-rate mouse look (recoil only affects raycast, not camera)
var old_yaw = _sim_yaw
rotation.y = _sim_yaw + (-input.mouse_rotation.y) 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.x = clamp(_sim_pitch + (-input.mouse_rotation.x), -1.57, 1.57)
head.rotation.y = 0 head.rotation.y = 0
head.rotation.z = 0 head.rotation.z = 0
func _rollback_tick(delta: float, tick: int, is_fresh: bool) -> void: 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 # Handle round respawn teleport inside rollback
if respawn_tick >= 0 and tick == respawn_tick: if respawn_tick >= 0 and tick == respawn_tick:
global_position = respawn_position global_position = respawn_position
@@ -26,7 +26,7 @@ var _pending_grenades: Array[Dictionary] = [] # Buffered throws, consumed in _a
# Weapon models (loaded at runtime) # Weapon models (loaded at runtime)
var _knife_model: Node3D var _knife_model: Node3D
var _gun_model: MeshInstance3D var _gun_model: Node3D
var _grenade_model: Node3D var _grenade_model: Node3D
var _current_visible_slot: int = -1 var _current_visible_slot: int = -1
var _knife_sound: AudioStreamPlayer3D var _knife_sound: AudioStreamPlayer3D
@@ -161,6 +161,10 @@ func _rollback_tick(delta: float, tick: int, _is_fresh: bool):
if rollback_synchronizer.is_predicting(): if rollback_synchronizer.is_predicting():
return return
# DEBUG: weapon switching
if input and (input.slot_1 or input.slot_2 or input.slot_3 or input.slot_4):
print("[WEAPON DEBUG] _rollback_tick tick=%d slots=[s1=%s s2=%s s3=%s s4=%s] active=%d inv=%s" % [tick, input.slot_1, input.slot_2, input.slot_3, input.slot_4, active_slot, inventory])
# Skip if player dead # Skip if player dead
if player.death_tick >= 0 and tick >= player.death_tick: if player.death_tick >= 0 and tick >= player.death_tick:
return return
@@ -406,3 +410,8 @@ func _spawn_grenade(origin: Vector3, direction: Vector3, owner_id: int, type_idx
get_tree().current_scene.add_child(grenade) get_tree().current_scene.add_child(grenade)
var gtype: Grenade.GrenadeType = Grenade.GrenadeType.FLASH if type_idx == 0 else Grenade.GrenadeType.SMOKE var gtype: Grenade.GrenadeType = Grenade.GrenadeType.FLASH if type_idx == 0 else Grenade.GrenadeType.SMOKE
grenade.setup(origin, direction, owner_id, gtype) grenade.setup(origin, direction, owner_id, gtype)
func _rollback_tick(delta: float, tick: int, _is_fresh: bool):
# DEBUG: Log when this is called with non-default input
if input and (input.slot_1 or input.slot_2 or input.slot_3 or input.slot_4):
print("[WEAPON DEBUG] _rollback_tick tick=%d slots=[%s %s %s %s]" % [tick, input.slot_1, input.slot_2, input.slot_3, input.slot_4])
+11 -2
View File
@@ -72,6 +72,9 @@ func join():
print("Client started") print("Client started")
connect_ui.hide() connect_ui.hide()
# Ensure game window has keyboard focus for input
get_window().grab_focus()
# Only start manually if NetworkEvents is not handling it (e.g. multiplayer-simple) # Only start manually if NetworkEvents is not handling it (e.g. multiplayer-simple)
if not NetworkEvents.enabled: if not NetworkEvents.enabled:
NetworkTime.start() NetworkTime.start()
@@ -80,8 +83,14 @@ func _enter_tree():
# Hide and show UI as appropriate # Hide and show UI as appropriate
# These handlers are necessary, since the game could have started via # These handlers are necessary, since the game could have started via
# autoconnect, or any other method # autoconnect, or any other method
NetworkEvents.on_client_start.connect(func(__): connect_ui.hide()) NetworkEvents.on_client_start.connect(func(__):
NetworkEvents.on_server_start.connect(func(): connect_ui.hide()) connect_ui.hide()
get_window().grab_focus()
)
NetworkEvents.on_server_start.connect(func():
connect_ui.hide()
get_window().grab_focus()
)
NetworkEvents.on_client_stop.connect(func(): connect_ui.show()) NetworkEvents.on_client_stop.connect(func(): connect_ui.show())
NetworkEvents.on_server_stop.connect(func(): connect_ui.show()) NetworkEvents.on_server_stop.connect(func(): connect_ui.show())
+1
View File
@@ -0,0 +1 @@
uid://4mlr8ca8nlg7
+1
View File
@@ -0,0 +1 @@
uid://h5lroui7prfv
+1
View File
@@ -0,0 +1 @@
uid://c2m50xd561do8