Files
tactical-shooter/examples/multiplayer-fps/scripts/round-manager.gd
T
shawn 91b878f347 Quaternius weapons, input fixes, sound & path fixes
- Added 6 Quaternius weapon models (Pistol, Revolver, Rifle, Shotgun, P90, SniperRifle)
- Created weapon resource files with balanced stats
- Wired Pistol + Rifle into player.tscn replacing Kenney blasters
- Fixed WASD input (matched input map action names)
- Fixed Escape key toggles mouse capture
- Added Audio autoload singleton
- Fixed broken sound asset paths across all scripts
- Fixed all scene ext_resource text paths (nested under assets/kenney-fps/)
- Fixed all .import source_file paths
- Deleted stale .import files (will be regenerated by editor on open)
2026-07-05 22:53:27 -04:00

202 lines
6.4 KiB
GDScript

extends Node
class_name RoundManager
enum RoundState { WARMUP, FREEZE_TIME, ACTIVE, ROUND_END }
signal round_state_changed(new_state: RoundState)
signal round_started(round_number: int)
signal round_ended(winner: TeamManager.Team)
signal player_killed(victim_id: int, killer_id: int, weapon_id: int, kill_tick: int)
@export var freeze_time: float = 0.0
@export var round_time: float = 115.0
@export var round_end_time: float = 5.0
@export var max_rounds: int = 24
@export var rounds_to_win: int = 13
var state: RoundState = RoundState.WARMUP
var timer: float = 0.0
var round_number: int = 0
var score: Dictionary = { TeamManager.Team.T: 0, TeamManager.Team.CT: 0 }
var freeze_end_tick: int = -1
var active_end_tick: int = -1
var round_end_tick: int = -1
# Tracking alive players per round (synced to clients via RPC)
var _alive_players: Dictionary = {} # peer_id -> bool
var _last_winner: TeamManager.Team = TeamManager.Team.NONE
var team_manager: TeamManager
var bomb: Bomb
func _ready():
team_manager = $"../TeamManager"
bomb = get_tree().current_scene.get_node_or_null("Network/Bomb")
set_process(false)
NetworkEvents.on_peer_leave.connect(_handle_peer_leave)
func start_match():
if not multiplayer.is_server():
return
round_number = 0
score = { TeamManager.Team.T: 0, TeamManager.Team.CT: 0 }
_sync_score.rpc(score[TeamManager.Team.T], score[TeamManager.Team.CT])
start_round()
func start_round():
if not multiplayer.is_server():
return
round_number += 1
freeze_end_tick = NetworkTime.tick + int(freeze_time * float(NetworkTime.tickrate))
active_end_tick = freeze_end_tick + int(round_time * float(NetworkTime.tickrate))
round_end_tick = -1
_mark_all_alive()
_change_state(RoundState.FREEZE_TIME, freeze_time)
_sync_round_start.rpc(round_number, freeze_end_tick, active_end_tick)
func _process(delta: float):
# Local timer countdown for HUD display only
if timer > 0:
timer -= delta
# Only server handles state transitions
if not multiplayer.is_server():
return
var current_tick := NetworkTime.tick
# Tick-based transitions for gameplay-critical boundaries
if state == RoundState.FREEZE_TIME and current_tick >= freeze_end_tick:
_change_state(RoundState.ACTIVE, round_time)
elif state == RoundState.ACTIVE and current_tick >= active_end_tick:
# Time ran out — CTs win (only if bomb not planted)
if bomb and (bomb.state == Bomb.BombState.PLANTED or bomb.state == Bomb.BombState.DEFUSING):
pass # Round continues — bomb is ticking
else:
end_round(TeamManager.Team.CT)
elif state == RoundState.ROUND_END and timer <= 0:
if _check_match_over():
return
start_round()
func report_kill(victim_id: int, killer_id: int, weapon_id: int, kill_tick: int = -1):
if not multiplayer.is_server():
return
var check_tick := kill_tick if kill_tick >= 0 else NetworkTime.tick
if round_end_tick >= 0 and check_tick > round_end_tick:
return
if check_tick < freeze_end_tick:
return
if check_tick >= active_end_tick:
return
_alive_players[victim_id] = false
_sync_player_alive.rpc(victim_id, false)
# Emit kill event for economy, stats, kill-feed
player_killed.emit(victim_id, killer_id, weapon_id, check_tick)
# Check if a whole team is eliminated
var t_alive := _count_alive(TeamManager.Team.T)
var ct_alive := _count_alive(TeamManager.Team.CT)
var bomb_planted := bomb and (bomb.state == Bomb.BombState.PLANTED or bomb.state == Bomb.BombState.DEFUSING)
if ct_alive == 0:
end_round(TeamManager.Team.T)
elif t_alive == 0 and not bomb_planted:
# All Ts dead + bomb NOT planted → CT win
end_round(TeamManager.Team.CT)
elif t_alive == 0 and bomb_planted:
pass # Round continues — bomb is planted
func end_round(winner: TeamManager.Team):
if not multiplayer.is_server():
return
if round_end_tick >= 0:
return
round_end_tick = NetworkTime.tick
_last_winner = winner
score[winner] += 1
_sync_score.rpc(score[TeamManager.Team.T], score[TeamManager.Team.CT])
_sync_round_end.rpc(winner, round_end_tick)
_change_state(RoundState.ROUND_END, round_end_time)
round_ended.emit(winner)
func is_player_alive(peer_id: int) -> bool:
return _alive_players.get(peer_id, false)
func sync_to_peer(peer_id: int):
_sync_state.rpc_id(peer_id, state, timer)
_sync_score.rpc_id(peer_id, score[TeamManager.Team.T], score[TeamManager.Team.CT])
_sync_round_start.rpc_id(peer_id, round_number, freeze_end_tick, active_end_tick)
for pid in _alive_players:
_sync_player_alive.rpc_id(peer_id, pid, _alive_players[pid])
if state == RoundState.ROUND_END and _last_winner != TeamManager.Team.NONE:
_sync_round_end.rpc_id(peer_id, _last_winner, round_end_tick)
func _count_alive(team: TeamManager.Team) -> int:
var count := 0
for peer_id in team_manager.get_team_members(team):
if _alive_players.get(peer_id, false):
count += 1
return count
func _mark_all_alive():
_alive_players.clear()
for peer_id in team_manager.assignments:
if team_manager.get_team(peer_id) != TeamManager.Team.NONE:
_alive_players[peer_id] = true
for pid in _alive_players:
_sync_player_alive.rpc(pid, _alive_players[pid])
func _change_state(new_state: RoundState, duration: float):
state = new_state
timer = duration
set_process(true)
_sync_state.rpc(new_state, duration)
round_state_changed.emit(new_state)
func _handle_peer_leave(peer_id: int) -> void:
_alive_players.erase(peer_id)
func _check_match_over() -> bool:
if score[TeamManager.Team.T] >= rounds_to_win or score[TeamManager.Team.CT] >= rounds_to_win:
return true
if round_number >= max_rounds:
return true
return false
# --- RPCs: Server -> All Clients ---
@rpc("authority", "call_local", "reliable")
func _sync_state(new_state: int, duration: float) -> void:
state = new_state as RoundState
timer = duration
set_process(true)
round_state_changed.emit(state)
@rpc("authority", "call_local", "reliable")
func _sync_score(t_score: int, ct_score: int) -> void:
score[TeamManager.Team.T] = t_score
score[TeamManager.Team.CT] = ct_score
@rpc("authority", "call_local", "reliable")
func _sync_round_start(round_num: int, _freeze_end: int, _active_end: int) -> void:
round_number = round_num
freeze_end_tick = _freeze_end
active_end_tick = _active_end
round_end_tick = -1
round_started.emit(round_number)
@rpc("authority", "call_local", "reliable")
func _sync_round_end(winner: int, _round_end_tick: int) -> void:
round_end_tick = _round_end_tick
round_ended.emit(winner as TeamManager.Team)
@rpc("authority", "call_local", "reliable")
func _sync_player_alive(peer_id: int, alive: bool) -> void:
_alive_players[peer_id] = alive