e7299b17e9
Stack installed: - netfox v1.35.3 (core + extras + noray + internals) - godot-jolt v0.16.0-stable Architecture: - Server: ENet transport (works headless, no netfox deps) - Client/Editor: netfox rollback (RollbackSynchronizer, TickInterpolator) New/modified: - docs/migration-netfox-plan.md — migration architecture - scripts/network/network_manager.gd — netfox-aware ENet fallback - scripts/network/player.gd — clean base player - client/characters/player_netfox.gd — rollback player w/ WeaponManager - client/characters/input/player_net_input.gd — BaseNetInput subclass - client/characters/character/fps_character_controller.gd — netfox input feed - client/weapons/ — weapon data, registry, TacticalWeaponHitscan, WeaponManager - client/scripts/round_replicator.gd — client-side round state bridge - server/scripts/round_manager.gd — improved state machine - server/scripts/plugin_api/plugin_manager.gd — refined plugin system - config: enemy_tag, ally_tag for meatball targeting Removed: old C++ SimulationServer GDExtension (replaced by netfox rollback)
221 lines
7.0 KiB
GDScript
221 lines
7.0 KiB
GDScript
## FpsCamera — first-person camera view effects for tactical FPS.
|
|
##
|
|
## Manages view bobbing (head bob from movement), weapon sway, and FOV kick.
|
|
## The parent FPSCharacterController handles mouse look (yaw/pitch).
|
|
##
|
|
## Place as a Camera3D child of FPSCharacterController.
|
|
class_name FpsCamera
|
|
extends Camera3D
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Exports
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Enable view bobbing (head bob when walking/running).
|
|
@export var view_bob_enabled: bool = true
|
|
|
|
## How much the camera bobs horizontally (units).
|
|
@export_range(0.0, 0.5) var bob_amplitude_h: float = 0.04
|
|
|
|
## How much the camera bobs vertically.
|
|
@export_range(0.0, 0.5) var bob_amplitude_v: float = 0.04
|
|
|
|
## Frequency multiplier for bobbing (higher = faster).
|
|
@export_range(0.5, 5.0) var bob_frequency: float = 2.5
|
|
|
|
## Sprint bob multiplier (more aggressive).
|
|
@export_range(1.0, 3.0) var bob_sprint_mult: float = 1.6
|
|
|
|
## Crouch bob multiplier (less movement).
|
|
@export_range(0.1, 1.0) var bob_crouch_mult: float = 0.4
|
|
|
|
## FOV kick on sprint (tactical FOV increase).
|
|
@export_range(0.0, 10.0) var sprint_fov_kick: float = 3.0
|
|
|
|
## FOV kick when firing weapon.
|
|
@export_range(-5.0, 10.0) var shoot_fov_kick: float = 0.5
|
|
|
|
## How fast FOV recovers.
|
|
@export var fov_recovery_speed: float = 6.0
|
|
|
|
## Enable weapon sway (subtle rotation from movement).
|
|
@export var weapon_sway_enabled: bool = true
|
|
|
|
## Maximum weapon sway rotation (degrees).
|
|
@export_range(0.0, 5.0) var sway_max_rotation: float = 1.5
|
|
|
|
## Sway responsiveness (higher = snappier).
|
|
@export_range(1.0, 20.0) var sway_response: float = 8.0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Reference to parent controller (duck-typed — uses has_method instead of class_name).
|
|
var _controller: Node = null
|
|
|
|
## Bob time accumulator.
|
|
var _bob_time: float = 0.0
|
|
|
|
## Current bob offset.
|
|
var _bob_offset: Vector2 = Vector2.ZERO
|
|
|
|
## Current weapon node (child named "Weapon" or first MeshInstance3D).
|
|
var _weapon: Node3D = null
|
|
|
|
## Weapon rest position (local transform when no sway).
|
|
var _weapon_rest: Transform3D
|
|
|
|
## FOV state.
|
|
var _base_fov: float = 75.0
|
|
var _current_fov_offset: float = 0.0
|
|
|
|
## Sway state (used as both current value and interpolation target).
|
|
var _sway_current: Vector2 = Vector2.ZERO
|
|
var _sway_target: Vector2 = Vector2.ZERO
|
|
|
|
## Base eye Y set by parent controller's crouch logic (for additive bob).
|
|
var _base_eye_y: float = 0.0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lifecycle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _ready() -> void:
|
|
_controller = get_parent()
|
|
if _controller == null or not _controller.has_method(&"is_sprinting"):
|
|
push_warning("FpsCamera: Parent has no is_sprinting() — view features disabled.")
|
|
|
|
_base_fov = fov
|
|
|
|
# Find weapon node (for sway)
|
|
for child in get_children():
|
|
if child is Node3D:
|
|
_weapon = child
|
|
_weapon_rest = child.transform
|
|
break
|
|
|
|
|
|
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)
|
|
|
|
# View bobbing
|
|
if view_bob_enabled:
|
|
_update_bob(delta)
|
|
|
|
# Weapon sway
|
|
if weapon_sway_enabled and _weapon:
|
|
_update_sway(delta)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# FOV
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _update_fov(delta: float) -> void:
|
|
var target_offset: float = 0.0
|
|
|
|
# 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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# View bobbing
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _update_bob(delta: float) -> void:
|
|
var crouch_amount: float = _controller.get_crouch_amount()
|
|
var sprinting: bool = _controller.is_sprinting()
|
|
|
|
# Calculate bob speed multiplier from controller state
|
|
var speed_mult: float = 1.0
|
|
if sprinting:
|
|
speed_mult = bob_sprint_mult
|
|
elif crouch_amount > 0.0:
|
|
speed_mult = lerpf(1.0, bob_crouch_mult, crouch_amount)
|
|
|
|
if speed_mult < 0.01:
|
|
# Stationary — reset to zero
|
|
_bob_offset = _bob_offset.move_toward(Vector2.ZERO, delta * 10.0)
|
|
else:
|
|
_bob_time += delta * bob_frequency * speed_mult
|
|
_bob_offset = Vector2(
|
|
sin(_bob_time * 2.0) * bob_amplitude_h * speed_mult,
|
|
sin(_bob_time) * bob_amplitude_v * speed_mult
|
|
)
|
|
|
|
# Apply bob to camera position (relative to parent's eye offset)
|
|
position.x = _bob_offset.x
|
|
# 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
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Weapon sway
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _update_sway(delta: float) -> void:
|
|
# Sway follows mouse movement indirectly via controller yaw/pitch changes
|
|
# Use velocity for physics-based sway: recentre over time
|
|
var vel := _controller.velocity as Vector3
|
|
var sway_h: float = clamp(vel.x * 0.003, -sway_max_rotation, sway_max_rotation)
|
|
var sway_v: float = clamp(vel.z * 0.003, -sway_max_rotation, sway_max_rotation)
|
|
# Add a tiny amount from pitch/yaw delta (not mouse, from movement direction change)
|
|
_sway_target = Vector2(
|
|
move_toward(_sway_target.x, sway_h, sway_response * delta),
|
|
move_toward(_sway_target.y, sway_v, sway_response * delta)
|
|
)
|
|
|
|
# Apply to weapon node as small rotation offsets
|
|
if _weapon:
|
|
var target := Transform3D(
|
|
Basis.from_euler(Vector3(
|
|
deg_to_rad(_sway_target.y),
|
|
deg_to_rad(_sway_target.x),
|
|
0.0
|
|
)),
|
|
_weapon_rest.origin
|
|
)
|
|
_weapon.transform = _weapon.transform.interpolate_with(target, sway_response * delta * 0.5)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Public API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Trigger a shoot FOV kick. Call from weapon script.
|
|
func trigger_shoot_fov() -> void:
|
|
_current_fov_offset += shoot_fov_kick
|
|
|
|
|
|
## Reset view to resting state (for respawn).
|
|
func reset_view() -> void:
|
|
_bob_time = 0.0
|
|
_bob_offset = Vector2.ZERO
|
|
_current_fov_offset = 0.0
|
|
_sway_current = Vector2.ZERO
|
|
_sway_target = Vector2.ZERO
|
|
fov = _base_fov
|
|
if _weapon:
|
|
_weapon.transform = _weapon_rest
|
|
|
|
|
|
## Get the current bob offset for external effects.
|
|
func get_bob_offset() -> Vector2:
|
|
return _bob_offset
|