use crate::app::cursor::{Cursor, FLASH_SECS, keymap};
use crate::app::settings::{CommitScheme, GameSettings};
use crate::app::{Campaign, LoadLevel, PendingActions, Phase, PlacementDenied, Screen, Sim};
use crate::sim::PlayerAction;
use bevy::prelude::*;
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 hands = cursors.iter().filter(|c| c.player < 2).count();
let commit = if hands > 1 {
CommitScheme::Arrows
} else {
settings.commit
};
for mut hand in cursors.iter_mut().filter(|c| c.player < 2) {
let map = keymap(&settings, hand.player, commit);
for (key, dir) in map.places {
if keys.just_pressed(key) {
let spent = sim.0.out_of_signposts(0, hand.x, hand.y);
if !sim.0.place_signpost(0, hand.x, hand.y, dir) {
hand.flash = FLASH_SECS;
denied.write(PlacementDenied {
player: 0,
out_of_signposts: spent,
});
}
}
}
if keys.just_pressed(map.remove) {
let _ = sim.0.remove_signpost(0, hand.x, hand.y);
}
if keys.just_pressed(map.clear_all) {
clear_setup(&mut sim);
}
}
if keys.just_pressed(KeyCode::Enter) && cursors.iter().any(|c| c.player == 0) {
next_phase.set(Phase::Running);
}
}
pub fn clear_setup(sim: &mut Sim) {
while let Some((x, y)) = sim.0.first_signpost_of(0) {
if !sim.0.remove_signpost(0, x, y) {
break;
}
}
}
pub fn browse_levels(
keyboard: crate::app::keycaps::Keyboard,
mut ladder: crate::app::progress::Ladder,
mut load: MessageWriter<LoadLevel>,
mut denied: MessageWriter<PlacementDenied>,
mut cursors: Query<&mut Cursor>,
) {
let Some(mut cursor) = cursors.iter_mut().find(|c| c.player == 0) else {
return;
};
let at = ladder.campaign.index;
let builtins = ladder.campaign.builtins;
let (start, end) = match at < builtins {
true => (0, builtins),
false => (builtins, ladder.campaign.levels.len()),
};
let len = end - start;
let asked = if keyboard.caps.just_pressed(&keyboard.keys, 'N') {
start + (at - start + 1) % len
} else if keyboard.caps.just_pressed(&keyboard.keys, 'P') {
start + (at - start + len - 1) % len
} else {
return;
};
if 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(
keys: Res<ButtonInput<KeyCode>>,
caps: Res<crate::app::keycaps::KeyCaps>,
mut load: MessageWriter<LoadLevel>,
) {
if caps.just_pressed(&keys, 'R') {
load.write(LoadLevel { keep_posts: true });
}
}
pub fn done_input(
keys: Res<ButtonInput<KeyCode>>,
caps: Res<crate::app::keycaps::KeyCaps>,
phase: Res<State<Phase>>,
mut campaign: ResMut<Campaign>,
mut next_screen: ResMut<NextState<Screen>>,
mut load: MessageWriter<LoadLevel>,
) {
if keys.just_pressed(KeyCode::Escape) {
next_screen.set(Screen::StageSelect);
return;
}
if keys.just_pressed(KeyCode::Enter) {
if *phase.get() != Phase::Won {
load.write(LoadLevel { keep_posts: true });
return;
}
if campaign.is_last() {
next_screen.set(Screen::Menu);
return;
}
campaign.index += 1;
load.write(LoadLevel { keep_posts: false });
return;
}
if caps.just_pressed(&keys, '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;
for mut cursor in &mut cursors {
let p = cursor.player as usize;
let map = if online.0.is_some() {
keymap(&settings, 0, CommitScheme::Arrows)
} else if cursor.player < 2 {
keymap(&settings, cursor.player, CommitScheme::Arrows)
} else {
continue;
};
let seat = cursor.player;
for (key, dir) in map.places {
if keys.just_pressed(key) {
if !board.can_place_signpost(seat, cursor.x, cursor.y) {
cursor.flash = FLASH_SECS;
denied.write(PlacementDenied {
player: seat,
out_of_signposts: board.out_of_signposts(seat, cursor.x, cursor.y),
});
continue;
}
pending.0[p] = PlayerAction::Place {
x: cursor.x,
y: cursor.y,
dir,
};
}
}
if keys.just_pressed(map.remove) {
pending.0[p] = PlayerAction::Remove {
x: cursor.x,
y: cursor.y,
};
}
if keys.pressed(map.clear_all)
&& let Some((x, y)) = board.first_signpost_of(cursor.player)
{
pending.0[p] = PlayerAction::Remove { x, y };
}
}
}
pub fn versus_over_input(
keys: Res<ButtonInput<KeyCode>>,
pause: Res<crate::app::pause::PauseMenu>,
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>>,
) {
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 pause.open || !crate::app::menu_ui::enter(&keys) {
return;
}
match after_round(&online, series_on) {
AfterRound::NextRound => {
if let Some(session) = &mut online.0 {
let standing = session.call_next_round(
crate::app::match_setup::next_round_terms(
session.terms,
session.seats,
crate::app::clock::fresh_seed(),
),
tournament.standing(),
);
if let Some(crate::transport::SeriesStanding { round, wins }) = standing {
tournament.round = round;
tournament.wins = wins;
}
}
next_screen.set(Screen::Interlude);
}
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);
}
}
}
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::Lobby => tr.prompt_enter_lobby,
AfterRound::Menu => tr.prompt_enter_menu,
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum AfterRound {
NextRound,
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::NextRound;
}
match series_on || !session.home.from_lobby {
true => AfterRound::Menu,
false => AfterRound::Lobby,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::campaign::CampaignKind;
use crate::app::progress::Progress;
use crate::sim::{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, crate::sim::Direction::Up) {
placed += 1;
}
}
assert_eq!(sim.0.signpost_count(0), 2);
clear_setup(&mut sim);
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 {
kind: CampaignKind::TidePool,
levels,
index,
builtins: 3,
}
}
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::keycaps::KeyCaps>();
app.insert_resource(campaign(index));
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 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 browsing_from_setup_honours_the_ladder_both_ways() {
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::keycaps::KeyCaps>();
app.insert_resource(GameSettings::default());
app.init_resource::<Progress>();
app.insert_resource(Sim(campaign_with(0, true).levels[0].board()));
app.insert_resource(campaign_with(0, true));
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()));
let denials = |app: &App| {
app.world()
.resource::<Messages<PlacementDenied>>()
.iter_current_update_messages()
.count()
};
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(CampaignKind::TidePool, &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);
}
}