## PlayerNetInput — netfox-compatible input gathering for tactical-shooter. ## ## Extends Node directly (avoids headless class_name issues with BaseNetInput) ## but follows the same pattern: connects to NetworkTime.before_tick_loop to ## call _gather() on each tick. ## ## RollbackSynchronizer references the exported properties as input_properties. ## ## Usage: ## Place as a child of the Player (Node3D). The parent node's ## RollbackSynchronizer will pick up the input_properties paths. ## ## Set hitscan_manager_ref to the RollbackHitscanManager child so that ## _gather() can call record_fire() for RewindableAction-based fire tracking. extends Node # --------------------------------------------------------------------------- # Input state — read by RollbackSynchronizer as input_properties # --------------------------------------------------------------------------- ## Movement direction in local space (normalized, or zero). @export var move_direction: Vector3 = Vector3.ZERO ## Mouse look: yaw (horizontal, radians). @export var look_yaw: float = 0.0 ## Mouse look: pitch (vertical, radians). @export var look_pitch: float = 0.0 ## Jump pressed this tick. @export var jump: bool = false ## Sprint toggle active. @export var sprint: bool = false ## Crouch toggle active. @export var crouch: bool = false ## Shoot pressed this tick. @export var shoot: bool = false ## Aim-down-sights pressed. @export var aim: bool = false # --------------------------------------------------------------------------- # RollbackHitscanManager reference (set by player.gd) # --------------------------------------------------------------------------- ## Reference to the RollbackHitscanManager for RewindableAction fire tracking. var hitscan_mgr_ref: Node = null # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- func _ready() -> void: # Same pattern as netfox's BaseNetInput: # connect to NetworkTime.before_tick_loop to gather input each tick # Use Engine.get_singleton instead of direct NetworkTime reference to # avoid headless parse errors (NetworkTime is an editor-plugin type). var nt = Engine.get_singleton(&"NetworkTime") if Engine.has_singleton(&"NetworkTime") else null if nt: nt.before_tick_loop.connect(_on_before_tick_loop) else: push_warning("[PlayerNetInput] netfox NetworkTime not available — " + "input gathering disabled") func _on_before_tick_loop() -> void: if is_multiplayer_authority(): _gather() # --------------------------------------------------------------------------- # Input gathering (called before each tick on the authority peer) # --------------------------------------------------------------------------- func _gather() -> void: # Movement direction var dir := Vector3.ZERO if Input.is_action_pressed(&"move_forward") or Input.is_action_pressed(&"move_up"): dir.z -= 1.0 if Input.is_action_pressed(&"move_backward") or Input.is_action_pressed(&"move_down"): dir.z += 1.0 if Input.is_action_pressed(&"move_left"): dir.x -= 1.0 if Input.is_action_pressed(&"move_right"): dir.x += 1.0 if dir.length_squared() > 0.0: dir = dir.normalized() move_direction = dir # Read look direction from parent's FPSCharacterController if available var parent := get_parent() if parent and parent.has_method(&"get_current_yaw"): look_yaw = parent.get_current_yaw() if parent and parent.has_method(&"get_current_pitch"): look_pitch = parent.get_current_pitch() # Action inputs jump = Input.is_action_just_pressed(&"jump") sprint = Input.is_action_pressed(&"sprint") crouch = Input.is_action_pressed(&"crouch") shoot = Input.is_action_pressed(&"shoot") aim = Input.is_action_pressed(&"aim") # ----------------------------------------------------------------------- # Fire recording for RewindableAction rollback sync # ----------------------------------------------------------------------- # When the client fires, record the shot in the RollbackHitscanManager's # RewindableAction. This propagates to the server via netfox sync so # the server can process the lag-compensated raycast at the correct tick. # # We compute the eye position and look direction at gather time for # the RewindableAction context. The server also reconstructs these from # the synced state (position + yaw/pitch) for authoritative hit detection. if shoot and hitscan_mgr_ref != null and hitscan_mgr_ref.has_method(&"record_fire"): var current_tick: int =_get_current_tick() var eye_pos: Vector3 = parent.global_position + Vector3(0, 1.7, 0) if parent else Vector3.ZERO var look_dir: Vector3 = _yaw_pitch_to_dir(look_yaw, look_pitch) hitscan_mgr_ref.record_fire(current_tick, eye_pos, look_dir) ## Get the current network tick, or 0 if NetworkTime is not available. func _get_current_tick() -> int: var nt = Engine.get_singleton(&"NetworkTime") if Engine.has_singleton(&"NetworkTime") else null if nt != null and nt.has_method(&"get_tick"): return nt.get_tick() if nt != null: return nt.tick if nt.get("tick") != null else 0 return 0 ## Convert yaw/pitch to a forward direction vector. static func _yaw_pitch_to_dir(yaw: float, pitch: float) -> Vector3: var cp := cos(pitch) return Vector3(cp * sin(yaw), sin(pitch), cp * cos(yaw))