91b878f347
- 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)
202 lines
6.4 KiB
GDScript
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
|