extends CharacterBody3D ## PlayerNode - Server-side player entity. ## ## Represents a connected player on the server. Handles movement, ## input processing, and state synchronization. class_name PlayerNode # --- Signals --- signal health_changed(new_health: int) signal player_died(player_node: PlayerNode) # --- Constants --- const WALK_SPEED := 5.0 const SPRINT_MULTIPLIER := 1.6 const JUMP_VELOCITY := 4.5 const GRAVITY := 9.8 const MAX_FALL_SPEED := 20.0 const FRICTION := 8.0 const AIR_CONTROL := 0.3 # --- Configurable --- @export var walk_speed: float = WALK_SPEED @export var sprint_multiplier: float = SPRINT_MULTIPLIER @export var jump_velocity: float = JUMP_VELOCITY @export var max_fall_speed: float = MAX_FALL_SPEED # --- State --- @export var max_health: int = 100 var current_health: int = 0 var is_alive: bool = true var is_sprinting: bool = false var peer_id: int = -1 var player_name: String = "" var entity_id: int = 0 # --- Input buffer --- var _input_queue: Array[Dictionary] = [] var _last_processed_seq: int = -1 # --- Movement --- var _velocity: Vector3 = Vector3.ZERO func _ready() -> void: current_health = max_health velocity = _velocity # Start alive is_alive = true func _physics_process(delta: float) -> void: if not is_alive: return # Process input from queue _process_input(delta) # Apply gravity if not is_on_floor(): velocity.y -= GRAVITY * delta velocity.y = clampf(velocity.y, -max_fall_speed, max_fall_speed) # Apply friction on ground if is_on_floor(): var move_dir = velocity.xz if move_dir.length() > 0.01: var friction_factor = clampf(FRICTION * delta, 0.0, 1.0) velocity.xz = move_dir.normalized() * move_dir.length() * (1.0 - friction_factor) else: velocity.xz = Vector2.ZERO # Move and slide velocity.y = clampf(velocity.y, -max_fall_speed, max_fall_speed) move_and_slide() # Update visual position (for remote clients via RPC) _update_visual_state() func _process_input(delta: float) -> void: """Process buffered input from the server's input queue.""" if _input_queue.is_empty(): return var input_data: Dictionary = _input_queue.pop_front() var input_state: Dictionary = input_data.get("input_state", {}) # Calculate movement direction var direction := Vector3.ZERO direction.x = input_state.get("move_x", 0.0) direction.z = input_state.get("move_z", 0.0) # Sprint check is_sprinting = input_state.get("sprint", false) if direction.length() > 0.01: direction = direction.normalized() var speed = walk_speed * sprint_multiplier if is_sprinting else walk_speed velocity.x = direction.x * speed velocity.z = direction.z * speed else: # On ground, apply friction (handled in physics_process) if not is_on_floor(): velocity.x *= (1.0 - AIR_CONTROL * delta * 5.0) velocity.z *= (1.0 - AIR_CONTROL * delta * 5.0) # Jump if input_state.get("jump", false) and is_on_floor(): velocity.y = jump_velocity func take_damage(amount: int, source_id: int) -> Dictionary: """Take damage and return result dict.""" if not is_alive: return {"success": false, "reason": "already_dead"} current_health = max(0, current_health - amount) health_changed.emit(current_health) if current_health <= 0: die() return {"success": true, "health": 0, "dead": true} return {"success": true, "health": current_health} func die() -> void: """Handle player death.""" is_alive = false velocity = Vector3.ZERO player_died.emit(self) # Respawn after delay await get_tree().create_timer(3.0).timeout respawn() func respawn() -> void: """Respawn the player at spawn point.""" current_health = max_health is_alive = true velocity = Vector3.ZERO health_changed.emit(current_health) # Move to spawn point (default: origin) position = Vector3.ZERO func set_input_queue(inputs: Array[Dictionary]) -> void: """Set the input queue from the server.""" _input_queue = inputs func get_state() -> Dictionary: """Get current player state for replication.""" return { "entity_id": entity_id, "peer_id": peer_id, "position": position, "rotation": rotation, "health": current_health, "max_health": max_health, "is_alive": is_alive, "is_sprinting": is_sprinting, } func _update_visual_state() -> void: """Update visual state (for remote interpolation).""" # In a full implementation, this would update a remote player's # visual representation. For now, the node itself is the authority. pass # --- RPCs --- @rpc("any_peer", "call_remote", "reliable") func rpc_request_input(input_state: Dictionary, seq: int) -> void: """Receive input from client.""" if not is_alive: return # Store for processing in _physics_process _input_queue.append({ "input_state": input_state, "seq": seq, "timestamp": Time.get_ticks_msec(), }) @rpc("any_peer", "call_remote", "reliable") func rpc_request_interact(target_id: int) -> void: """Handle interaction request from client.""" if not is_alive: return # TODO: Implement interaction logic print("Player %d interacted with entity %d" % [peer_id, target_id]) @rpc("any_peer", "call_remote", "reliable") func rpc_request_attack(target_id: int, aim_dir: Vector3) -> void: """Handle attack request from client.""" if not is_alive: return # TODO: Implement attack logic print("Player %d attacked entity %d with aim %s" % [peer_id, target_id, aim_dir]) @rpc("authority", "call_remote", "reliable") func rpc_set_position(pos: Vector3) -> void: """Server authoritative position update.""" position = pos @rpc("authority", "call_remote", "reliable") func rpc_set_rotation(rot: Vector3) -> void: """Server authoritative rotation update.""" rotation = rot