1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226
//! Track scoring of rounds / tournament
use crate::{prelude::*, ui::scoring::ScoringMenuState};
/// Timer tracking how long until round is scored once one or fewer players are alive
#[derive(HasSchema, Clone, Default)]
pub struct RoundScoringState {
/// Timer used to count down to scoring, or to round transition post scoring.
/// Is `None` if round is in progress.
pub timer: Option<Timer>,
/// If true: round has been scored, timer counts down to round transition.
pub round_scored: bool,
/// Save MapPool state to transitin with when determining round end.
pub next_maps: Option<MapPool>,
/// Save the frame round was marked to transition on in network play.
/// Transition does not execute until this is confirmed by remote players.
pub network_round_end_frame: Option<i32>,
}
impl RoundScoringState {
/// Are timers for round scoring + linger before transition complete?
pub fn transition_timers_done(&self) -> bool {
if let Some(timer) = self.timer.as_ref() {
return timer.finished() && self.round_scored;
}
false
}
/// Is scoring timer complete but round not yet scored?
pub fn should_score_round(&self) -> bool {
if let Some(timer) = self.timer.as_ref() {
return timer.finished() && !self.round_scored;
}
false
}
}
/// Store player's match score's (rounds won)
#[derive(HasSchema, Clone, Default, Debug)]
pub struct MatchScore {
/// Map player to score, if no entry is 0.
player_score: HashMap<PlayerIdx, u32>,
/// How many rounds have completed this match
rounds_completed: u32,
}
impl MatchScore {
/// Get player's score
pub fn score(&self, player: PlayerIdx) -> u32 {
self.player_score.get(&player).map_or(0, |s| *s)
}
/// Mark round as completed and increment score of winner. None should be provided
/// on a draw.
pub fn complete_round(&mut self, winner: Option<PlayerIdx>) {
self.rounds_completed += 1;
// Increment winner's score if not a draw
if let Some(winner) = winner {
if let Some(score) = self.player_score.get_mut(&winner) {
*score += 1;
} else {
self.player_score.insert(winner, 1);
}
}
}
/// How many rounds have been played in this match
pub fn rounds_completed(&self) -> u32 {
self.rounds_completed
}
}
pub fn session_plugin(session: &mut Session) {
session
.stages
.add_system_to_stage(CoreStage::PostUpdate, round_end);
}
pub fn round_end(
mut commands: Commands,
meta: Root<GameMeta>,
entities: Res<Entities>,
rng: Res<GlobalRng>,
map_pool: Res<MapPool>,
mut score: ResMutInit<MatchScore>,
mut sessions: ResMut<Sessions>,
mut session_options: ResMut<SessionOptions>,
time: Res<Time>,
mut state: ResMutInit<RoundScoringState>,
mut scoring_menu: ResMut<ScoringMenuState>,
killed_players: Comp<PlayerKilled>,
player_indices: Comp<PlayerIdx>,
#[cfg(not(target_arch = "wasm32"))] syncing_info: Option<Res<SyncingInfo>>,
) {
// Count players so we can avoid ending round if it's a one player match
let mut player_count = 0;
// Is Some if one player left, or none if all players dead.
// Exits function if >= 2 players left: otherwise we handle continue to handle
// round scoring.
let last_player_or_draw: Option<(PlayerIdx, Entity)> = {
let mut last_player: Option<(PlayerIdx, Entity)> = None;
for (ent, (player_idx, killed)) in
entities.iter_with((&player_indices, &Optional(&killed_players)))
{
player_count += 1;
if killed.is_none() {
if last_player.is_some() {
// At least two players alive, not the round end.
return;
}
last_player = Some((*player_idx, ent));
}
}
// We either found only one player or None.
last_player
};
if player_count == 1 {
// Single player match - don't end round.
return;
}
// Tick any round end timer we have
if let Some(timer) = state.timer.as_mut() {
timer.tick(time.delta());
}
// There are one or fewer players alive if we have not already returned from function
// Ready to score the round?
if state.should_score_round() {
state.round_scored = true;
score.complete_round(last_player_or_draw.map(|x| x.0));
if let Some((_, winner_ent)) = last_player_or_draw {
// commands.add(PlayerCommand::won_round(winner));
commands.add(spawn_win_indicator(winner_ent));
}
// Start the post-score linger timer before next round
state.timer = Some(Timer::new(
meta.core.config.round_end_post_score_linger_time,
TimerMode::Once,
));
} else if state.transition_timers_done() {
// post-score linger timer complete, go to next round if all players confirmed transition
// Is round transition sycnrhonized on all clients in network play?
// Will evaluate to true in local play.
#[allow(unused_assignments)]
let mut round_transition_synchronized = false;
// If in network play and determined a prev frame round should end on:
#[allow(unused_variables)]
if let Some(end_net_frame) = state.network_round_end_frame {
// check if this frame is confirmed by all players.
#[cfg(not(target_arch = "wasm32"))]
{
round_transition_synchronized = match syncing_info {
Some(syncing_info) => end_net_frame <= syncing_info.last_confirmed_frame(),
None => true,
};
}
} else {
// Network frame for round end not yet recorded (or in local only)
// Randomize map and save MapPool to be used for transition
let mut map_pool = map_pool.clone();
map_pool.randomize_current_map(&rng);
state.next_maps = Some(map_pool);
// Save current predicted frame for round end.
// Will not follow through with transition until this frame is confirmed
// by all players in network play.
#[cfg(not(target_arch = "wasm32"))]
if let Some(syncing_info) = syncing_info {
state.network_round_end_frame = Some(syncing_info.current_frame());
} else {
// No sync info - must be local and sychronized.
round_transition_synchronized = true;
}
}
// Wasm32 is always local, can transition now.
#[cfg(target_arch = "wasm32")]
{
round_transition_synchronized = true;
}
if round_transition_synchronized {
if score.rounds_completed % meta.core.config.rounds_between_intermission == 0 {
scoring_menu.active = true;
scoring_menu.match_score = score.clone();
scoring_menu.next_maps = state.next_maps.clone();
session_options.active = false;
} else {
// Not at intermission, tranisition immediately
// Use maps originally determined on synchronized transition frame
let next_maps = state.next_maps.clone().unwrap();
sessions.add_command(Box::new(|sessions: &mut Sessions| {
sessions.restart_game(Some(next_maps), false);
}));
}
}
} else if state.timer.is_none() {
// Scoring timer does not exist, start a new one
state.timer = Some(Timer::new(
meta.core.config.round_end_score_time,
TimerMode::Once,
));
state.round_scored = false;
} else {
// Timer still ticking
}
}