use crate::app::cursor::{Cursor, FLASH_SECS, keymap, seat_keymap};
use crate::app::settings::GameSettings;
use crate::app::{Campaign, LoadLevel, PendingActions, Phase, PlacementDenied, Screen, Sim};
use crate::sim::{Board, Direction, PlayerAction};
use bevy::prelude::*;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum SeatCommand {
Place(Direction),
Remove,
ClearAll,
Start,
}
pub fn apply_setup(
command: SeatCommand,
hand: &mut Cursor,
sim: &mut Sim,
next_phase: &mut NextState<Phase>,
denied: &mut MessageWriter<PlacementDenied>,
) {
let seat = crate::app::cursor::stage_seat(&sim.0, hand);
match command {
SeatCommand::Place(dir) => {
let spent = sim.0.out_of_signposts(seat, hand.x, hand.y);
if !sim.0.place_signpost(seat, hand.x, hand.y, dir) {
hand.flash = FLASH_SECS;
denied.write(PlacementDenied {
player: seat,
out_of_signposts: spent,
});
}
}
SeatCommand::Remove => {
let _ = sim.0.remove_signpost(seat, hand.x, hand.y);
}
SeatCommand::ClearAll => clear_setup(sim, seat),
SeatCommand::Start => next_phase.set(Phase::Running),
}
}
pub fn queue_versus(
command: SeatCommand,
cursor: &mut Cursor,
board: &Board,
pending: &mut PendingActions,
denied: &mut MessageWriter<PlacementDenied>,
) {
let seat = cursor.player;
let (x, y) = (cursor.x, cursor.y);
let action = match command {
SeatCommand::Place(dir) => {
if !board.can_place_signpost(seat, x, y) {
cursor.flash = FLASH_SECS;
denied.write(PlacementDenied {
player: seat,
out_of_signposts: board.out_of_signposts(seat, x, y),
});
return;
}
PlayerAction::Place { x, y, dir }
}
SeatCommand::Remove => PlayerAction::Remove { x, y },
SeatCommand::ClearAll => match board.first_signpost_of(seat) {
Some((x, y)) => PlayerAction::Remove { x, y },
None => return,
},
SeatCommand::Start => return,
};
pending.0[usize::from(seat)] = action;
}
pub fn setup_input(
keys: Res<ButtonInput<KeyCode>>,
settings: Res<GameSettings>,
mut sim: ResMut<Sim>,
mut next_phase: ResMut<NextState<Phase>>,
mut denied: MessageWriter<PlacementDenied>,
mut cursors: Query<&mut Cursor>,
) {
let commit = settings.commit_in_play(crate::app::cursor::has_partner(cursors.iter()));
for mut hand in cursors.iter_mut().filter(|c| c.player < 2) {
let map = keymap(&settings, hand.player, commit);
let places = map
.places
.into_iter()
.filter(|(key, _)| keys.just_pressed(*key));
let commands = places
.map(|(_, dir)| SeatCommand::Place(dir))
.chain(keys.just_pressed(map.remove).then_some(SeatCommand::Remove))
.chain(
keys.just_pressed(map.clear_all)
.then_some(SeatCommand::ClearAll),
);
for command in commands {
apply_setup(command, &mut hand, &mut sim, &mut next_phase, &mut denied);
}
}
if crate::app::menu_ui::enter(&keys)
&& let Some(mut hand) = cursors.iter_mut().find(|c| c.player == 0)
{
let start = SeatCommand::Start;
apply_setup(start, &mut hand, &mut sim, &mut next_phase, &mut denied);
}
}
pub fn clear_setup(sim: &mut Sim, seat: u8) {
while let Some((x, y)) = sim.0.first_signpost_of(seat) {
if !sim.0.remove_signpost(seat, x, y) {
break;
}
}
}
pub fn browse_levels(
keyboard: crate::app::keycaps::Keyboard,
pads: crate::app::gamepad::face::PadsOnFace,
mut ladder: crate::app::progress::Ladder,
mut load: MessageWriter<LoadLevel>,
mut denied: MessageWriter<PlacementDenied>,
mut cursors: Query<&mut Cursor>,
) {
let partner = crate::app::Coop(crate::app::cursor::has_partner(cursors.iter()));
let Some(mut cursor) = cursors.iter_mut().find(|c| c.player == 0) else {
return;
};
let at = ladder.campaign.index;
let section = ladder.campaign.section(ladder.campaign.section_of(at));
let (start, end) = (section.start, section.end);
let len = end - start;
use crate::app::gamepad::scheme::Does;
let asked = if keyboard.global('N') || pads.pressed(Does::NextLevel) {
start + (at - start + 1) % len
} else if keyboard.global('P') || pads.pressed(Does::PrevLevel) {
start + (at - start + len - 1) % len
} else {
return;
};
if asked == at {
return;
}
if !ladder.campaign.needs_partner(asked, partner) && ladder.climb_to(asked) {
load.write(LoadLevel { keep_posts: false });
} else {
cursor.flash = FLASH_SECS;
denied.write(PlacementDenied {
player: 0,
out_of_signposts: false,
});
}
}
pub fn running_input(
keyboard: crate::app::keycaps::Keyboard,
pads: crate::app::gamepad::face::PadsOnFace,
sim: Res<Sim>,
campaign: Res<Campaign>,
mut load: MessageWriter<LoadLevel>,
) {
let settled = campaign.current().outcome(&sim.0) != crate::sim::PuzzleOutcome::Running;
let reset = keyboard.global('R') || pads.pressed(crate::app::gamepad::scheme::Does::Reset);
if reset && !settled {
load.write(LoadLevel { keep_posts: true });
}
}
pub fn done_input(
keyboard: crate::app::keycaps::Keyboard,
phase: Res<State<Phase>>,
coop: Res<crate::app::Coop>,
mut campaign: ResMut<Campaign>,
mut next_screen: ResMut<NextState<Screen>>,
mut load: MessageWriter<LoadLevel>,
) {
if keyboard.keys.just_pressed(KeyCode::Escape) {
next_screen.set(Screen::StageSelect);
return;
}
if crate::app::menu_ui::enter(&keyboard.keys) {
if *phase.get() != Phase::Won {
load.write(LoadLevel { keep_posts: true });
return;
}
if campaign.is_last() {
next_screen.set(Screen::Menu);
return;
}
if campaign.needs_partner(campaign.index + 1, *coop) {
next_screen.set(Screen::StageSelect);
return;
}
campaign.index += 1;
load.write(LoadLevel { keep_posts: false });
return;
}
if keyboard.global('R') {
load.write(LoadLevel { keep_posts: true });
}
}
pub fn versus_input(
keys: Res<ButtonInput<KeyCode>>,
sim: Res<Sim>,
settings: Res<GameSettings>,
online: Res<crate::app::net::Online>,
mut pending: ResMut<PendingActions>,
mut denied: MessageWriter<PlacementDenied>,
mut cursors: Query<&mut Cursor>,
) {
let board = &sim.0;
let commit = settings.commit_in_play(true);
for mut cursor in &mut cursors {
let Some(map) = seat_keymap(&settings, cursor.player, online.0.is_some(), commit) else {
continue;
};
let places = map
.places
.into_iter()
.filter(|(key, _)| keys.just_pressed(*key));
let commands = places
.map(|(_, dir)| SeatCommand::Place(dir))
.chain(keys.just_pressed(map.remove).then_some(SeatCommand::Remove))
.chain(keys.pressed(map.clear_all).then_some(SeatCommand::ClearAll));
for command in commands {
queue_versus(command, &mut cursor, board, &mut pending, &mut denied);
}
}
}
pub fn versus_over_input(
mut keyboard: crate::app::keycaps::KeyboardMut,
(pause, time, mut next_map): (Res<crate::app::pause::PauseMenu>, Res<Time>, NextMapPage),
mut tournament: ResMut<crate::app::tournament::Tournament>,
mut online: ResMut<crate::app::net::Online>,
mut homecoming: ResMut<crate::app::lobby::Homecoming>,
mut next_screen: ResMut<NextState<Screen>>,
mut again: crate::app::rematch::PlayAgain,
) {
let series_on = tournament.is_running();
if online.0.as_ref().is_some_and(|session| session.next_round) {
next_screen.set(Screen::Interlude);
return;
}
if next_map.page.is_up()
&& let Some(session) = &mut online.0
{
let empty = crate::app::match_setup::CustomBeaches::default();
let beaches = again.beaches.as_deref().unwrap_or(&empty);
*next_map.held += 1;
let mut go = ButtonInput::<KeyCode>::default();
if crate::app::dev::lobby_pick_go() && *next_map.held == 30 {
go.press(KeyCode::Enter);
}
let keys = match go.get_pressed().next() {
Some(_) => &go,
None => &*keyboard.keys,
};
let press = crate::app::next_map::next_map_keys(
keys,
&mut next_map.page,
session,
&mut again.config,
&mut next_map.menu,
beaches,
tournament.standing(),
);
if let crate::app::next_map::NextMapPress::Called(standing) = press {
if let Some(crate::transport::SeriesStanding { round, wins }) = standing {
tournament.round = round;
tournament.wins = wins;
}
next_screen.set(Screen::Interlude);
}
if !next_map.page.is_up() {
*next_map.held = 0;
crate::app::menu_ui::spend(&mut keyboard.keys, &crate::app::menu_ui::CLOSERS);
}
return;
}
if pause.is_open() {
return;
}
let keys = &*keyboard.keys;
let now = time.elapsed_secs();
let r = keyboard.global('R');
let other_key = !r && keys.get_just_pressed().next().is_some();
crate::app::rematch::settle_rematch_ask(&mut again.table, other_key, now);
let hook = after_round(&online, series_on) == AfterRound::NextMap
&& crate::app::dev::lobby_pick()
&& !std::mem::replace(&mut *next_map.hooked, true);
if !crate::app::menu_ui::enter(keys) && !hook {
let rematch = crate::app::rematch::rematch(&again.table, &online, &tournament);
if let Some(rematch) = rematch.filter(|_| r) {
let pressed =
crate::app::rematch::press_rematch(&mut again.table, rematch, &tournament, now);
if let Some(rematch) = pressed {
let rematch = crate::app::rematch::Again::Rematch(rematch);
again.ask(rematch, &mut tournament, &mut next_screen);
}
}
return;
}
match after_round(&online, series_on) {
AfterRound::NextRound => next_screen.set(Screen::Interlude),
AfterRound::NextMap => {
if let Some(session) = &mut online.0 {
let empty = crate::app::match_setup::CustomBeaches::default();
let beaches = again.beaches.as_deref().unwrap_or(&empty);
crate::app::next_map::open_next_map(
&mut next_map.page,
session,
&mut again.config,
&mut next_map.menu,
beaches,
);
}
}
AfterRound::Menu => next_screen.set(Screen::Menu),
AfterRound::Lobby => {
let session = online.0.take().expect("the lobby door needs a session");
homecoming.0 = Some(session.back_to_the_lobby());
next_screen.set(Screen::Lobby);
}
}
}
#[derive(bevy::ecs::system::SystemParam)]
pub struct NextMapPage<'w, 's> {
page: ResMut<'w, crate::app::next_map::NextMap>,
menu: ResMut<'w, crate::app::match_setup::MatchMenu>,
hooked: Local<'s, bool>,
held: Local<'s, u32>,
}
pub fn enter_door(
tr: &crate::app::i18n::Tr,
online: &crate::app::net::Online,
series_on: bool,
) -> &'static str {
match after_round(online, series_on) {
AfterRound::NextRound => tr.tour_next,
AfterRound::NextMap => tr.tour_next_map,
AfterRound::Lobby => tr.prompt_enter_lobby,
AfterRound::Menu => tr.prompt_enter_menu,
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum AfterRound {
NextRound,
NextMap,
Lobby,
Menu,
}
pub fn after_round(online: &crate::app::net::Online, series_on: bool) -> AfterRound {
let Some(session) = &online.0 else {
return match series_on {
true => AfterRound::NextRound,
false => AfterRound::Menu,
};
};
if session.is_host() && series_on {
return AfterRound::NextMap;
}
match series_on || !session.home.from_lobby {
true => AfterRound::Menu,
false => AfterRound::Lobby,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::progress::Progress;
use crate::sim::{Goal, Level, campaign_levels};
#[test]
fn a_local_series_carries_enter_on_to_the_next_round() {
let alone = crate::app::net::Online::default();
assert_eq!(after_round(&alone, true), AfterRound::NextRound);
assert_eq!(after_round(&alone, false), AfterRound::Menu);
}
#[test]
fn clear_all_empties_a_puzzle_setup() {
let level = campaign_levels()
.into_iter()
.find(|level| level.posts >= 2)
.expect("a stage with two arrows");
let mut sim = Sim(level.board());
let mut placed = 0;
for (x, y, _) in sim.0.tiles().collect::<Vec<_>>() {
if placed < 2 && sim.0.place_signpost(0, x, y, Direction::Up) {
placed += 1;
}
}
assert_eq!(sim.0.signpost_count(0), 2);
clear_setup(&mut sim, 0);
assert_eq!(sim.0.signpost_count(0), 0);
}
fn campaign_with(index: usize, mine: bool) -> Campaign {
let mut levels: Vec<Level> = campaign_levels().into_iter().take(3).collect();
if mine {
let mut mine = levels[0].clone();
mine.name = "Mine".into();
levels.push(mine);
}
Campaign {
index,
builtins: 3,
..Campaign::tide_pool_of(levels)
}
}
fn campaign(index: usize) -> Campaign {
campaign_with(index, true)
}
fn tap(app: &mut App, key: KeyCode) {
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>();
keys.reset_all();
keys.press(key);
app.update();
app.world_mut()
.resource_mut::<ButtonInput<KeyCode>>()
.reset_all();
}
fn won(index: usize) -> App {
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Screen>();
app.init_state::<Phase>();
app.insert_resource(State::new(Screen::Puzzle));
app.insert_resource(State::new(Phase::Won));
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<crate::app::gamepad::PadFace>();
app.init_resource::<crate::app::gamepad::LastDevice>();
app.init_resource::<crate::app::keycaps::KeyCaps>();
app.init_resource::<crate::app::cursor::LiveSeatKeys>();
app.insert_resource(campaign(index));
app.init_resource::<crate::app::Coop>();
app.add_message::<LoadLevel>();
app.add_systems(Update, done_input);
app
}
fn loads(app: &App) -> usize {
app.world()
.resource::<Messages<LoadLevel>>()
.iter_current_update_messages()
.count()
}
#[test]
fn r_on_the_frame_a_puzzle_is_won_does_not_reload_it() {
for (goal, reloads) in [(Goal::Bank(0), 0), (Goal::AllCrabs, 1)] {
let mut app = App::new();
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<crate::app::gamepad::PadFace>();
app.init_resource::<crate::app::gamepad::LastDevice>();
app.init_resource::<crate::app::keycaps::KeyCaps>();
app.init_resource::<crate::app::cursor::LiveSeatKeys>();
let mut campaign = campaign(0);
campaign.levels[0].goal = goal;
app.insert_resource(Sim(campaign.current().board()));
app.insert_resource(campaign);
app.add_message::<LoadLevel>();
app.add_systems(Update, running_input);
tap(&mut app, KeyCode::KeyR);
assert_eq!(loads(&app), reloads, "{goal:?}");
}
}
#[test]
fn enter_after_the_last_shipped_stage_goes_to_the_menu() {
let mut app = won(2);
tap(&mut app, KeyCode::Enter);
assert_eq!(loads(&app), 0, "nothing loaded");
app.update();
assert_eq!(*app.world().resource::<State<Screen>>().get(), Screen::Menu);
assert_eq!(
app.world().resource::<Campaign>().index,
2,
"the list stays put"
);
}
#[test]
fn enter_walks_the_list_and_stops_at_its_end() {
let mut app = won(0);
tap(&mut app, KeyCode::Enter);
assert_eq!(loads(&app), 1, "the next stage is loaded");
assert_eq!(app.world().resource::<Campaign>().index, 1);
app.update();
assert_eq!(
*app.world().resource::<State<Screen>>().get(),
Screen::Puzzle
);
let mut app = won(3);
tap(&mut app, KeyCode::Enter);
assert_eq!(loads(&app), 0);
app.update();
assert_eq!(*app.world().resource::<State<Screen>>().get(), Screen::Menu);
}
#[test]
fn num_enter_takes_the_won_cards_door() {
let mut app = won(0);
tap(&mut app, KeyCode::NumpadEnter);
assert_eq!(loads(&app), 1, "the next stage is loaded");
assert_eq!(app.world().resource::<Campaign>().index, 1);
}
#[test]
fn browsing_from_setup_honours_the_ladder_both_ways() {
let mut app = browsing_app(campaign_with(0, true));
tap(&mut app, KeyCode::KeyP);
assert_eq!(
app.world().resource::<Campaign>().index,
0,
"the end is locked"
);
assert_eq!(denials(&app), 1, "and refused out loud");
tap(&mut app, KeyCode::KeyN);
assert_eq!(
app.world().resource::<Campaign>().index,
0,
"so is the second"
);
assert_eq!(denials(&app), 1);
let first = app.world().resource::<Campaign>().levels[0].name.clone();
app.world_mut().resource_mut::<Progress>().mark(&first);
tap(&mut app, KeyCode::KeyN);
assert_eq!(
app.world().resource::<Campaign>().index,
1,
"cleared, so open"
);
assert_eq!(loads(&app), 1);
tap(&mut app, KeyCode::KeyP);
assert_eq!(app.world().resource::<Campaign>().index, 0, "and back");
assert_eq!(denials(&app), 0);
}
fn browsing_app(campaign: Campaign) -> App {
let mut app = App::new();
app.add_plugins(bevy::state::app::StatesPlugin);
app.init_state::<Phase>();
app.insert_resource(State::new(Phase::Setup));
app.init_resource::<ButtonInput<KeyCode>>();
app.init_resource::<crate::app::gamepad::PadFace>();
app.init_resource::<crate::app::gamepad::LastDevice>();
app.init_resource::<crate::app::keycaps::KeyCaps>();
app.init_resource::<crate::app::cursor::LiveSeatKeys>();
app.insert_resource(GameSettings::default());
app.init_resource::<Progress>();
app.insert_resource(Sim(campaign.current().board()));
app.insert_resource(campaign);
app.add_message::<LoadLevel>();
app.add_message::<PlacementDenied>();
app.add_systems(Update, browse_levels);
let mut cursor = Cursor::seated(0);
(cursor.x, cursor.y) = (1, 1);
app.world_mut().spawn((cursor, Transform::default()));
app
}
fn denials(app: &App) -> usize {
app.world()
.resource::<Messages<PlacementDenied>>()
.iter_current_update_messages()
.count()
}
#[test]
fn browsing_a_shelf_of_one_keeps_the_setup() {
let mut app = browsing_app(campaign_with(3, true));
for key in [KeyCode::KeyN, KeyCode::KeyP] {
tap(&mut app, key);
assert_eq!(app.world().resource::<Campaign>().index, 3, "{key:?}");
assert_eq!(loads(&app), 0, "{key:?} reloads nothing");
assert_eq!(denials(&app), 0, "{key:?} refuses nothing");
}
}
}