use super::*;
use std::sync::{Arc, OnceLock};
pub(in crate::app) fn bot_seats(
config: &match_setup::MatchConfig,
) -> [Option<BotLevel>; MAX_PLAYERS] {
let mut bots = [None; MAX_PLAYERS];
if config.armed {
for seat in (config.seats - config.bots)..config.seats {
bots[seat as usize] = Some(config.bot_levels[seat as usize]);
}
}
bots
}
pub(super) enum RoundOrigin<'a> {
Resumed(Box<suspend::Suspended>),
Replay(&'a Replay),
Online(&'a net::OnlineSession),
Configured(&'a match_setup::MatchConfig),
Unconfigured,
}
impl RoundOrigin<'_> {
pub(super) fn board(
&self,
daily: bool,
beaches: &match_setup::CustomBeaches,
sandbox: bool,
pads: u8,
) -> Board {
match self {
RoundOrigin::Resumed(round) => round.board.clone(),
RoundOrigin::Replay(replay) => replay.level.board(),
RoundOrigin::Online(session) => match &session.catch_up.board {
Some(board) => board.clone(),
None => match_setup::board_from(&session.terms, session.seats, &session.beach),
},
RoundOrigin::Configured(config) => {
let seed = if daily {
Daily::seed()
} else {
clock::fresh_seed()
};
let (w, h) = config.map.size();
let mut board = if config.map == match_setup::MapChoice::Custom {
beaches
.fitting(config.seats)
.get(config.custom)
.map_or_else(
|| generate_arena(seed, config.seats, w, h),
|beach| beach.level.board(),
)
} else if config.map == match_setup::MapChoice::Classic {
classic_arena_seeded(seed, false, config.seats)
} else {
generate_arena(seed, config.seats, w, h)
};
board.set_gull_period(config.gulls.period());
board.set_round_length(Some(config.round.ticks()));
board
}
RoundOrigin::Unconfigured => classic_arena(sandbox, pads.max(2)),
}
}
pub(super) fn table(
&self,
config: &match_setup::MatchConfig,
board: &Board,
pads: u8,
) -> ([Option<BotLevel>; MAX_PLAYERS], u8) {
let bots = match self {
RoundOrigin::Resumed(round) => return (round.bots, round.seats),
RoundOrigin::Online(session) => {
match_setup::bot_seats_from(&session.terms, session.seats)
}
RoundOrigin::Replay(_) | RoundOrigin::Configured(_) | RoundOrigin::Unconfigured => {
bot_seats(config)
}
};
(bots, clamp_seats(self.asked_seats(board, pads)))
}
pub(super) fn asked_seats(&self, board: &Board, pads: u8) -> u8 {
match self {
RoundOrigin::Resumed(round) => round.seats,
RoundOrigin::Replay(_) => board.castle_owners().max().unwrap_or(1).saturating_add(1),
RoundOrigin::Online(session) => session.seats,
RoundOrigin::Configured(config) => config.seats,
RoundOrigin::Unconfigured => pads.max(2),
}
}
pub(super) fn recorded(&self) -> bool {
match self {
RoundOrigin::Resumed(_) | RoundOrigin::Replay(_) => false,
RoundOrigin::Online(session) => session.catch_up.board.is_none(),
RoundOrigin::Configured(_) | RoundOrigin::Unconfigured => true,
}
}
}
pub(in crate::app) fn clamp_seats(asked: u8) -> u8 {
asked.clamp(2, MAX_PLAYERS as u8)
}
#[derive(bevy::ecs::system::SystemParam)]
pub(in crate::app) struct RoundSource<'w> {
sandbox: Res<'w, Sandbox>,
online: Res<'w, net::Online>,
playback: Res<'w, Playback>,
config: Res<'w, match_setup::MatchConfig>,
beaches: Res<'w, match_setup::CustomBeaches>,
daily: Res<'w, Daily>,
resuming: ResMut<'w, Resuming>,
}
#[allow(clippy::too_many_arguments)]
pub(in crate::app) fn load_versus(
mut commands: Commands,
art: Res<art::Art>,
mut source: RoundSource,
mut bots: ResMut<Bots>,
pads: Query<&Gamepad>,
mut seats: ResMut<Seats>,
mut recorder: ResMut<Recorder>,
mut sim: ResMut<Sim>,
mut next_vphase: ResMut<NextState<VersusPhase>>,
mut paused: ResMut<Paused>,
mut pending: ResMut<PendingActions>,
mut cursors: Query<(&mut cursor::Cursor, &mut Transform)>,
) {
let RoundSource {
sandbox,
online,
playback,
config,
beaches,
daily,
resuming,
} = &mut source;
let daily_config = match_setup::MatchConfig::daily();
let config: &match_setup::MatchConfig = if daily.active { &daily_config } else { config };
let pad_count = pads.iter().count() as u8;
let origin = if let Some(round) = resuming.0.take() {
RoundOrigin::Resumed(Box::new(round))
} else if let Some((replay, _)) = &playback.0 {
RoundOrigin::Replay(replay)
} else if let Some(session) = &online.0 {
RoundOrigin::Online(session)
} else if config.armed {
RoundOrigin::Configured(config)
} else {
RoundOrigin::Unconfigured
};
sim.0 = origin.board(daily.active, beaches, sandbox.0, pad_count);
(bots.0, seats.0) = origin.table(config, &sim.0, pad_count);
debug_assert!(
(2..=MAX_PLAYERS as u8).contains(&seats.0),
"a table of {} seats",
seats.0
);
recorder.0 = origin
.recorded()
.then(|| Replay::new(Level::from_board("Turf War", 3, sim.0.clone())));
board_render::spawn_static_board(&mut commands, &sim.0, &art);
board_render::spawn_waterline(&mut commands);
board_render::spawn_water_foam(&mut commands, &art);
pending.0 = [PlayerAction::None; MAX_PLAYERS];
for (mut cur, mut transform) in &mut cursors {
(cur.x, cur.y) = cursor_home(&sim.0, cur.player);
transform.translation = layout::tile_center(&sim.0, cur.x, cur.y).extend(layout::z::CURSOR);
}
paused.0 = false;
next_vphase.set(VersusPhase::Running);
}
#[allow(clippy::too_many_arguments)]
pub(in crate::app) fn end_versus(
mut online: ResMut<net::Online>,
mut playback: ResMut<Playback>,
mut recorder: ResMut<Recorder>,
mut reel_thread: ResMut<ReelThread>,
mut pending: ResMut<PendingActions>,
mut config: ResMut<match_setup::MatchConfig>,
mut bots: ResMut<Bots>,
mut next_vphase: ResMut<NextState<VersusPhase>>,
) {
if !online.0.as_ref().is_some_and(|session| session.next_round) {
online.0 = None;
}
playback.0 = None;
recorder.0 = None;
reel_thread.0 = None;
config.armed = false;
bots.0 = [None; MAX_PLAYERS];
pending.0 = [PlayerAction::None; MAX_PLAYERS];
next_vphase.set(VersusPhase::Running);
}
pub(in crate::app) fn resolve_seat_names(
online: Res<net::Online>,
playback: Res<Playback>,
settings: Res<settings::GameSettings>,
mut recorder: ResMut<Recorder>,
mut names: ResMut<SeatNames>,
) {
names.0 = match (&playback.0, &online.0) {
(Some((replay, _)), _) => replay.names.clone(),
(None, Some(session)) => session.names.clone(),
(None, None) => settings.names.clone(),
};
if let Some(replay) = &mut recorder.0 {
replay.names = names.0.clone();
}
}
#[derive(Resource, Default)]
pub struct ReelThread(Option<Arc<OnceLock<Option<String>>>>);
pub(in crate::app) fn poll_reel(
mut reel_thread: ResMut<ReelThread>,
mut highlight: ResMut<Highlight>,
) {
let Some(answer) = reel_thread.0.as_ref().and_then(|slot| slot.get().cloned()) else {
return;
};
reel_thread.0 = None;
if answer.is_some() {
highlight.0 = answer;
}
}
pub(super) fn winner_name(
sim: &Sim,
seats: &Seats,
settings: &settings::GameSettings,
online: &net::Online,
names: &SeatNames,
) -> String {
let mode = teams::in_play(settings, online, seats.0);
let leaders = side_panels::leading_seats(sim.0.scores(), seats.0, mode);
match leaders.iter().position(|&led| led) {
Some(seat) => names.label(settings.tr(), seat as u8),
None => "draw".to_string(),
}
}
#[allow(clippy::too_many_arguments)]
pub(in crate::app) fn check_versus_over(
sim: Res<Sim>,
seats: Res<Seats>,
settings: Res<settings::GameSettings>,
online: Res<net::Online>,
seat_names: Res<SeatNames>,
mut recorder: ResMut<Recorder>,
mut highlight: ResMut<Highlight>,
mut reel_thread: ResMut<ReelThread>,
mut next_vphase: ResMut<NextState<VersusPhase>>,
) {
if sim.0.round_over() {
highlight.0 = None;
reel_thread.0 = None;
if let Some(replay) = recorder.0.take() {
let library = replays::library_dir();
let text = replay.to_text();
let last = replay_path();
match paths::write_atomic(&last, &text) {
Ok(()) => info!("replay saved to {}", last.display()),
Err(e) => warn!("could not save replay: {e}"),
}
let stamp = clock::now_secs();
let winner = winner_name(&sim, &seats, &settings, &online, &seat_names);
let kept = library.join(replays::file_name(stamp, &winner));
if let Err(e) = paths::write_atomic(&kept, &text) {
warn!("could not file the replay: {e}");
}
replays::prune(settings.replay_cap);
let reel = highlight_path();
let answer = Arc::new(OnceLock::new());
reel_thread.0 = Some(Arc::clone(&answer));
std::thread::spawn(move || {
let saved = match crate::highlight::reel(&replay) {
Some(bytes) => match paths::write_atomic(&reel, bytes) {
Ok(()) => {
info!("highlight reel saved to {}", reel.display());
Some(reel.display().to_string())
}
Err(e) => {
warn!("could not save the highlight reel: {e}");
None
}
},
None => {
warn!("the round was too short for a highlight reel");
None
}
};
let _ = answer.set(saved);
});
}
next_vphase.set(VersusPhase::Over);
}
}