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)
156 lines
5.6 KiB
GDScript
156 lines
5.6 KiB
GDScript
## RoundReplicator — Client-side round state synced from RoundManager Self-RPCs.
|
|
##
|
|
## The server's round_manager signals arrive on clients via netfox Self-RPC
|
|
## (@rpc with call_local). This replicator listens to those signals and
|
|
## maintains a local round state snapshot for rollback-accessible use.
|
|
##
|
|
## This script is ONLY loaded from client scenes.
|
|
|
|
extends Node
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Signals (mirrors server RoundManager for client-side consumers)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Round state (mirrors server RoundManager)
|
|
# ---------------------------------------------------------------------------
|
|
enum RoundState {
|
|
INACTIVE = 0,
|
|
WAITING_FOR_PLAYERS = 1,
|
|
WARMUP = 2,
|
|
LIVE = 3,
|
|
POST_ROUND = 4,
|
|
MATCH_END = 5,
|
|
}
|
|
|
|
# Current round state (populated from server broadcasts)
|
|
var round_state: int = RoundState.INACTIVE
|
|
var round_number: int = 0
|
|
var team_a_score: int = 0
|
|
var team_b_score: int = 0
|
|
var time_remaining: float = 0.0
|
|
var round_time_seconds: float = 600.0
|
|
var win_limit: int = 3
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Rollback-friendly state (used by rollback_tick in player scripts)
|
|
# ---------------------------------------------------------------------------
|
|
## Whether the round is in freeze time (before round start).
|
|
var is_frozen: bool = false
|
|
## The network tick when freeze ends (for rollback checks).
|
|
var freeze_end_tick: int = -1
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Lifecycle
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _ready() -> void:
|
|
# Connect to RoundManager signals (which now arrive via Self-RPC)
|
|
if Engine.has_singleton(&"RoundManager"):
|
|
var rm = Engine.get_singleton(&"RoundManager")
|
|
if rm.round_started.is_connected(_on_round_start):
|
|
return # Already connected
|
|
rm.round_started.connect(_on_round_start)
|
|
rm.round_ended.connect(_on_round_end)
|
|
rm.match_ended.connect(_on_match_end)
|
|
rm.score_changed.connect(_on_score_change)
|
|
else:
|
|
push_warning("[RoundReplicator] RoundManager not available")
|
|
|
|
func _on_connected() -> void:
|
|
print("[RoundReplicator] Connected to server — awaiting round state")
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Signal handlers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
func _on_round_start(round_num: int) -> void:
|
|
round_number = round_num
|
|
# Read round_time_seconds from RoundManager (replicated via _rpc_round_started)
|
|
if Engine.has_singleton(&"RoundManager"):
|
|
var rm = Engine.get_singleton(&"RoundManager")
|
|
round_time_seconds = rm.round_time_seconds if rm.round_time_seconds > 0 else 600
|
|
time_remaining = round_time_seconds
|
|
round_state = RoundState.LIVE
|
|
|
|
# Freeze time handling (10 seconds for first round, 5 for subsequent)
|
|
var freeze_time: float = 10.0 if round_num <= 1 else 5.0
|
|
is_frozen = true
|
|
var nt = Engine.get_singleton(&"NetworkTime")
|
|
if nt:
|
|
freeze_end_tick = nt.tick + int(freeze_time * 64.0) # 64 tick-rate assumption
|
|
else:
|
|
freeze_end_tick = -1
|
|
|
|
print("[RoundReplicator] Round %d started (%.0fs, frozen for %.0fs)" % [round_num, round_time_seconds, freeze_time])
|
|
|
|
func _on_round_end(winner_team: int, reason: String) -> void:
|
|
# Read scores from RoundManager (replicated by Self-RPC)
|
|
if Engine.has_singleton(&"RoundManager"):
|
|
var rm = Engine.get_singleton(&"RoundManager")
|
|
team_a_score = rm.team_a_score
|
|
team_b_score = rm.team_b_score
|
|
is_frozen = false
|
|
round_state = RoundState.POST_ROUND
|
|
print("[RoundReplicator] Round ended: %s wins (%s). Score: %d-%d" % [_team_name(winner_team), reason, team_a_score, team_b_score])
|
|
|
|
func _on_match_end(winner_team: int) -> void:
|
|
# Read scores from RoundManager
|
|
if Engine.has_singleton(&"RoundManager"):
|
|
var rm = Engine.get_singleton(&"RoundManager")
|
|
team_a_score = rm.team_a_score
|
|
team_b_score = rm.team_b_score
|
|
round_state = RoundState.MATCH_END
|
|
print("[RoundReplicator] MATCH OVER: %s wins %d-%d" % [_team_name(winner_team), team_a_score, team_b_score])
|
|
|
|
func _on_score_change(a_score: int, b_score: int) -> void:
|
|
team_a_score = a_score
|
|
team_b_score = b_score
|
|
print("[RoundReplicator] Score updated: %d-%d" % [a_score, b_score])
|
|
|
|
func _process(delta: float) -> void:
|
|
# Update time remaining (for HUD display)
|
|
if round_state == RoundState.LIVE:
|
|
time_remaining -= delta
|
|
if time_remaining < 0.0:
|
|
time_remaining = 0.0
|
|
|
|
# Update freeze state
|
|
if is_frozen and freeze_end_tick >= 0:
|
|
var nt = Engine.get_singleton(&"NetworkTime")
|
|
if nt:
|
|
if nt.tick >= freeze_end_tick:
|
|
is_frozen = false
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
## Get a snapshot of the current round state (for HUD/UI).
|
|
func get_snapshot() -> Dictionary:
|
|
return {
|
|
"state": round_state,
|
|
"round_number": round_number,
|
|
"team_a_score": team_a_score,
|
|
"team_b_score": team_b_score,
|
|
"time_remaining": time_remaining,
|
|
"is_frozen": is_frozen,
|
|
}
|
|
|
|
## Check if the player can move (not frozen, not dead, round is LIVE).
|
|
func can_player_move(player_is_alive: bool) -> bool:
|
|
return player_is_alive and round_state == RoundState.LIVE and not is_frozen
|
|
|
|
## Check if the round is currently active (LIVE or WARMUP).
|
|
func is_round_active() -> bool:
|
|
return round_state == RoundState.LIVE or round_state == RoundState.WARMUP
|
|
|
|
## Get the human-readable team name.
|
|
static func _team_name(team: int) -> String:
|
|
match team:
|
|
0: return "Draw"
|
|
1: return "Team A"
|
|
2: return "Team B"
|
|
_: return "Unknown"
|