## PlayerNetfox — netfox rollback-aware player for the tactical-shooter client. ## ## Extends the base Player with netfox RollbackSynchronizer, TickInterpolator, ## BaseNetInput, and WeaponManager for deterministic first-person combat. ## ## This script is ONLY loaded from client scenes where netfox is active. ## The headless server uses the base player.gd which has no netfox deps. extends "res://scripts/network/player.gd" # --------------------------------------------------------------------------- # Netfox node references # --------------------------------------------------------------------------- @onready var _tick_interpolator := $TickInterpolator as TickInterpolator @onready var _input := $PlayerNetInput as PlayerNetInput @onready var _weapon_manager := $WeaponManager as WeaponManager @onready var _weapon_anchor := $WeaponAnchor as Node3D var _rs: RollbackSynchronizer # --------------------------------------------------------------------------- # State # --------------------------------------------------------------------------- var _gravity: float = ProjectSettings.get_setting(&"physics/3d/default_gravity", 9.8) var _is_local: bool = false var _camera: Camera3D var _mouse_captured: bool = true # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- func _ready() -> void: super._ready() if Engine.has_singleton(&"NetworkTime") and Engine.has_singleton(&"NetworkRollback"): _setup_rollback() _connect_netfox_signals() print("[PlayerNetfox] Rollback initialised for %s" % name) else: push_warning("[PlayerNetfox] netfox not available — running without rollback") set_process(false) set_physics_process(false) _camera = get_viewport().get_camera_3d() if get_viewport() else null # --------------------------------------------------------------------------- # Rollback setup # --------------------------------------------------------------------------- func _setup_rollback() -> void: _rs = RollbackSynchronizer.new() _rs.name = "RollbackSynchronizer" _rs.state_properties = PackedStringArray([ "global_position", "is_alive", "team_id", "is_spectating", "spectate_target_id", ]) _rs.input_properties = PackedStringArray([ "PlayerNetInput/move_direction", "PlayerNetInput/look_yaw", "PlayerNetInput/look_pitch", "PlayerNetInput/jump", "PlayerNetInput/sprint", "PlayerNetInput/crouch", "PlayerNetInput/shoot", "PlayerNetInput/aim", "PlayerNetInput/input_sequence", "PlayerNetInput/weapon_slot", "PlayerNetInput/reload", "PlayerNetInput/use", ]) add_child(_rs, true) _rs.rollback_ticks.connect(_rollback_tick) func _connect_netfox_signals() -> void: var nt = Engine.get_singleton(&"NetworkTime") if nt: nt.before_tick_loop.connect(_before_tick_loop) nt.after_tick_loop.connect(_after_tick_loop) # --------------------------------------------------------------------------- # Rollback hooks # --------------------------------------------------------------------------- func _before_tick_loop() -> void: if _tick_interpolator and _tick_interpolator.enabled: _tick_interpolator.save_transform() func _after_tick_loop() -> void: if _tick_interpolator and _tick_interpolator.enabled: _tick_interpolator.restore_transform() func _rollback_tick(delta: float, tick: int, is_fresh: bool) -> void: if not is_alive: return var input := _input # Look rotation rotate_object_local(Vector3(0, 1, 0), input.look_yaw) # Gravity + jump _force_update_is_on_floor() if is_on_floor(): if input.jump: velocity.y = ServerConfig.movement_jump_velocity if ServerConfig else 6.0 else: velocity.y -= _gravity * delta # Speed var speed := 5.0 if ServerConfig: speed = ServerConfig.movement_walk_speed if input.sprint: speed = ServerConfig.movement_sprint_speed if ServerConfig else 7.0 if input.crouch: speed = ServerConfig.movement_crouch_speed if ServerConfig else 2.5 # Horizontal movement var dir := (transform.basis * input.move_direction).normalized() if dir.length() > 0.0: velocity.x = dir.x * speed velocity.z = dir.z * speed else: velocity.x = move_toward(velocity.x, 0.0, speed * 2.0) velocity.z = move_toward(velocity.z, 0.0, speed * 2.0) move_and_slide() func _force_update_is_on_floor() -> void: var old := velocity velocity = Vector3.ZERO move_and_slide() velocity = old # --------------------------------------------------------------------------- # Weapon system # --------------------------------------------------------------------------- ## Give starting weapons (called by spawner). func give_default_weapons(team_id: int) -> void: if _weapon_manager == null: return # Always give knife _weapon_manager.add_weapon(WeaponRegistry.get_by_id(WeaponRegistry.ID_KNIFE)) # Give team-default sidearm var default_wep := WeaponRegistry.get_default_weapon(team_id) _weapon_manager.add_weapon(default_wep) ## Handle weapon slot switching from input. func _handle_weapon_input(input: PlayerNetInput) -> void: if _weapon_manager == null: return if input.reload: _weapon_manager.reload() if input.weapon_slot >= 0: _weapon_manager.switch_to_slot(input.weapon_slot) # Reset so it doesn't keep switching input.weapon_slot = -1 if input.shoot: _weapon_manager.fire() # --------------------------------------------------------------------------- # Input handling (frame-rate) # --------------------------------------------------------------------------- func _input(event: InputEvent) -> void: if not _is_local or not _mouse_captured: return if _input == null: return if event is InputEventMouseMotion: _input.look_yaw -= event.relative.x * 0.005 _input.look_pitch = clamp(_input.look_pitch - event.relative.y * 0.005 * 0.8, -1.57, 1.57) if event is InputEventMouseButton and event.pressed: if event.button_index == MOUSE_BUTTON_LEFT: _input.shoot = true func _unhandled_key_input(event: InputEvent) -> void: if not _is_local: return if event.is_action_pressed(&"reload"): _input.reload = true elif event.is_action_pressed(&"shoot"): _input.shoot = true # Weapon slots 1-4 for i in range(4): if event.is_action_pressed(&"weapon_slot_%d" % (i + 1)): _input.weapon_slot = i # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- func mark_local() -> void: _is_local = true if _tick_interpolator: _tick_interpolator.enabled = false if _input: _input.set_process(true) Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) func apply_damage(amount: int, attacker_id: int = -1, weapon_id: int = 0) -> void: health -= amount if health <= 0: health = 0 is_alive = false print("[PlayerNetfox] %s killed by peer %d with weapon %d" % [name, attacker_id, weapon_id])