221 lines
5.6 KiB
GDScript
221 lines
5.6 KiB
GDScript
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
|