diff --git a/client/characters/character/fps_camera.gd b/client/characters/character/fps_camera.gd index 16d8343..84377dc 100644 --- a/client/characters/character/fps_camera.gd +++ b/client/characters/character/fps_camera.gd @@ -70,12 +70,12 @@ var _weapon_rest: Transform3D var _base_fov: float = 75.0 var _current_fov_offset: float = 0.0 -## Sway state. +## Sway state (used as both current value and interpolation target). var _sway_current: Vector2 = Vector2.ZERO var _sway_target: Vector2 = Vector2.ZERO -## Sprint state cache (detect transition for FOV kick). -var _was_sprinting: bool = false +## Base eye Y set by parent controller's crouch logic (for additive bob). +var _base_eye_y: float = 0.0 # --------------------------------------------------------------------------- # Lifecycle @@ -100,6 +100,10 @@ func _process(delta: float) -> void: if _controller == null: return + # Capture the eye height set by parent controller's crouch logic + # before we apply view bob offsets (which must be additive). + _base_eye_y = position.y + # FOV management _update_fov(delta) @@ -119,16 +123,15 @@ func _process(delta: float) -> void: func _update_fov(delta: float) -> void: var target_offset: float = 0.0 - # Sprint FOV kick - if _controller.is_sprinting() and not _was_sprinting: + # Sprint FOV kick — sustained while sprinting (not just on transition) + if _controller.is_sprinting(): target_offset = sprint_fov_kick # Shoot FOV kick is triggered externally via trigger_shoot_fov() + # and decays toward the sprint/walk target naturally via move_toward. _current_fov_offset = move_toward(_current_fov_offset, target_offset, fov_recovery_speed * delta) fov = _base_fov + _current_fov_offset - _was_sprinting = _controller.is_sprinting() - # --------------------------------------------------------------------------- # View bobbing @@ -157,7 +160,9 @@ func _update_bob(delta: float) -> void: # Apply bob to camera position (relative to parent's eye offset) position.x = _bob_offset.x - position.y = _bob_offset.y + # Bob is additive on Y so it does NOT overwrite the crouch eye height + # set by the parent FPSCharacterController._update_crouch(). + position.y = _base_eye_y + _bob_offset.y # --------------------------------------------------------------------------- diff --git a/client/characters/character/fps_character_controller.gd b/client/characters/character/fps_character_controller.gd index af73273..48c9a45 100644 --- a/client/characters/character/fps_character_controller.gd +++ b/client/characters/character/fps_character_controller.gd @@ -186,7 +186,7 @@ func _unhandled_input(event: InputEvent) -> void: func _physics_process(delta: float) -> void: # 1. Capture input state var input_dir := _get_move_direction() - var jump_pressed := Input.is_action_just_pressed(&"jump") or Input.is_action_pressed(&"jump") + var jump_pressed := Input.is_action_just_pressed(&"jump") var sprint_pressed := Input.is_action_pressed(&"sprint") var crouch_pressed := Input.is_action_pressed(&"crouch") var shoot_pressed := _mouse_clicked_this_frame or Input.is_action_pressed(&"shoot") @@ -229,18 +229,16 @@ func _physics_process(delta: float) -> void: # 6. Send input to simulation server if _server != null and entity_id >= 0: - # Build input for SimulationServer - _input_dict.merge({ - "move_direction": input_dir, - "look_yaw": rad_to_deg(_yaw), - "look_pitch": rad_to_deg(_pitch), - "jump": jump_pressed, - "crouch": _crouch_active, - "sprint": _sprint_active, - "shoot": shoot_pressed, - "aim": aim_pressed, - "input_sequence": _input_sequence, - }, true) + # Build input for SimulationServer (mutate cached dict to avoid alloc) + _input_dict["move_direction"] = input_dir + _input_dict["look_yaw"] = rad_to_deg(_yaw) + _input_dict["look_pitch"] = rad_to_deg(_pitch) + _input_dict["jump"] = jump_pressed + _input_dict["crouch"] = _crouch_active + _input_dict["sprint"] = _sprint_active + _input_dict["shoot"] = shoot_pressed + _input_dict["aim"] = aim_pressed + _input_dict["input_sequence"] = _input_sequence _server.apply_input(entity_id, _input_dict) @@ -251,14 +249,17 @@ func _physics_process(delta: float) -> void: # 7. Read server state back (for local simulation / listen server) # The SimulationServer.tick() call happens in the game manager / network layer. - # Here we read the entity's canonical position from the server. + # Here we optionally read the entity's canonical position from the server. + # NOTE: On a dedicated client, position comes from the network replication + # layer (server snapshot), not from direct server entity access. + # On a listen server (host+client same process) or in standalone mode, + # reading position directly from the server entity is correct. if _server != null and entity_id >= 0: var entity = _server.get_entity(entity_id) if entity != null and entity.is_alive(): # Server position is ground truth — apply it to the visual body + # (only correct on listen-server; pure clients must use replication) global_position = entity.position - # Note: rotation is already set from yaw/pitch above (client-side) - # On a pure client the server snapshot override is applied differently. else: # Standalone mode: do local CharacterBody3D physics _move_local(input_dir, delta, jump_pressed)