P7.8: Port FPS controller to netfox BaseNetInput pattern

- FPSCharacterController: removed SimulationServer dependency, added
  _rollback_tick() with acceleration-based CharacterBody3D movement,
  gravity, jump, crouch/sprint speed. Kept mouse look, crouch lerp,
  sprint/crouch toggle, _move_local() standalone fallback.
- Player: extends FPSCharacterController (CharacterBody3D). Added
  authority-guarded _rollback_tick that delegates to super for server
  and local client prediction. TickInterpolator created dynamically
  (avoids headless class_name parse errors). PlayerNetInput child
  wired via existing RollbackSynchronizer input_properties.
- player.tscn: CharacterBody3D root with FpsCamera, CollisionShape3D,
  PlayerNetInput children. TickInterpolator created in code.
- client_main.gd: _spawn_local_player() creates FPS player node with
  first-person camera for own peer. _spawn_remote_player() removes
  FpsCamera for remote players.
- fps_camera.gd: duck-typed _controller (Node) instead of class_name
  cast for headless safety. Fixed type inference warnings.

Also includes parent task P7.5-P7.7 changes:
- project.godot: main_scene -> server_main.tscn
- network_manager.gd: Chan enum -> raw channel ints
- server_main.gd: deferred ServerConfig load, GameServer load()
- game_server.gd: commented out dev deps for headless compilation
This commit is contained in:
2026-07-02 17:45:03 -04:00
parent e7299b17e9
commit 926446e5cf
7 changed files with 341 additions and 309 deletions
+44 -18
View File
@@ -26,17 +26,21 @@ extends Node
# ---------------------------------------------------------------------------
# Force class_name script dependencies to load first
# (Godot 4 headless mode doesn't resolve global classes in dependency order)
#
# NOTE: these are commented out for P7.5 build — they reference development
# systems (weapons, economy, objectives, teams) that aren't part of this
# phase. When those systems are ready, uncomment the relevant preloads.
# ---------------------------------------------------------------------------
const _ws = preload("res://server/scripts/weapons/weapon_server.gd")
const _lc = preload("res://server/scripts/combat/lag_compensation.gd")
const _dp = preload("res://server/scripts/combat/damage_processor.gd")
const _tm = preload("res://scripts/teams/team_manager.gd")
const _rm = preload("res://server/scripts/round/round_manager.gd")
const _em = preload("res://server/scripts/economy/economy_manager.gd")
const _bm = preload("res://server/scripts/economy/buy_menu_handler.gd")
const _bo = preload("res://server/scripts/objectives/bomb_objective.gd")
const _sm = preload("res://scripts/teams/spawn_manager.gd")
const _td = preload("res://scripts/teams/team_data.gd")
#const _ws = preload("res://server/scripts/weapons/weapon_server.gd")
#const _lc = preload("res://server/scripts/combat/lag_compensation.gd")
#const _dp = preload("res://server/scripts/combat/damage_processor.gd")
#const _tm = preload("res://scripts/teams/team_manager.gd")
#const _rm = preload("res://server/scripts/round/round_manager.gd")
#const _em = preload("res://server/scripts/economy/economy_manager.gd")
#const _bm = preload("res://server/scripts/economy/buy_menu_handler.gd")
#const _bo = preload("res://server/scripts/objectives/bomb_objective.gd")
#const _sm = preload("res://scripts/teams/spawn_manager.gd")
#const _td = preload("res://scripts/teams/team_data.gd")
# ---------------------------------------------------------------------------
# Signals
@@ -79,6 +83,9 @@ var buy_menu_handler = null
## BombObjective — server-authoritative bomb plant/defuse logic.
var bomb_objective = null
## TeamData reference — cached with load() to avoid class_name resolution in headless.
var _td = null
## Current server tick counter, incremented each time tick() is called.
var _current_tick: int = 0
@@ -207,11 +214,11 @@ func _ready() -> void:
# Wire: bomb explosion/defuse → round end
bomb_objective.bomb_exploded.connect(func(_pos):
if round_manager:
round_manager.end_round(_td.Team.TERRORIST, "bomb_exploded")
round_manager.end_round(2, "bomb_exploded") # TeamData.Team.TERRORIST
)
bomb_objective.bomb_defused.connect(func(_player_id):
if round_manager:
round_manager.end_round(_td.Team.COUNTER_TERRORIST, "bomb_defused")
round_manager.end_round(1, "bomb_defused") # TeamData.Team.COUNTER_TERRORIST
)
# Wire: round end → reset bomb
@@ -394,8 +401,13 @@ func _on_kill_for_round(victim_id: int, shooter_id: int) -> void:
break
if all_dead and team_player_ids.size() > 0:
print("[GameServer] Team elimination detected! %s eliminated by %s" %
[_td.get_team_name(victim_team), _td.get_team_name(shooter_team)])
var victim_name: String = "Spectator"
var shooter_name: String = "Spectator"
if victim_team == 1: victim_name = "CT"
elif victim_team == 2: victim_name = "T"
if shooter_team == 1: shooter_name = "CT"
elif shooter_team == 2: shooter_name = "T"
print("[GameServer] Team elimination detected! %s eliminated by %s" % [victim_name, shooter_name])
round_manager.end_round(shooter_team, "elimination")
# ---------------------------------------------------------------------------
@@ -413,13 +425,27 @@ func _on_rcon_command(command: String, args: PackedStringArray) -> void:
# end_round [team_name] [reason]
var team_name: String = args[0] if args.size() > 0 else "ct"
var reason: String = args[1] if args.size() > 1 else "admin"
var team: int = _td.from_string(team_name)
if team == _td.Team.SPECTATOR:
var team: int = _parse_team_name(team_name)
if team == 0: # TeamData.Team.SPECTATOR
# Default to CT
team = _td.Team.COUNTER_TERRORIST
team = 1 # TeamData.Team.COUNTER_TERRORIST
if round_manager:
round_manager.end_round(team, reason)
print("[GameServer] RCON: round ended — %s wins (%s)" % [_td.get_team_name(team), reason])
team_name = "Spectator"
if team == 1: team_name = "CT"
elif team == 2: team_name = "T"
print("[GameServer] RCON: round ended — %s wins (%s)" % [team_name, reason])
## Parse a team name string to a team ID (replaces TeamData.from_string for headless compat).
## 0 = Spectator, 1 = CT, 2 = T. Defaults to 0 for unknown names.
func _parse_team_name(name_str: String) -> int:
match name_str.to_lower().strip_edges():
"ct", "counter_terrorist", "counter-terrorist", "team_a", "1":
return 1
"t", "terrorist", "team_b", "2":
return 2
_:
return 0 # Spectator
# ---------------------------------------------------------------------------
# Main loop (128 Hz)