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)
211 lines
6.7 KiB
GDScript
211 lines
6.7 KiB
GDScript
## 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])
|