Skip to main content

pinch_points/app/
mod.rs

1//! The Bevy shell: windowing, rendering, input, and the fixed-timestep bridge
2//! into the headless simulation. Nothing in `crate::sim` may depend on this.
3//!
4//! This file is the shell's shared vocabulary: the resources, the states,
5//! the messages. When each of them runs is [`schedule`]'s business.
6
7mod achievements;
8pub(crate) mod announce;
9mod art;
10mod audio;
11mod binds;
12mod board_render;
13mod boot;
14mod campaign;
15mod clock;
16mod codes;
17mod controls;
18mod creatures;
19mod cursor;
20mod cycle;
21mod daily;
22mod dev;
23mod editor;
24mod effects;
25mod embedded;
26mod gamepad;
27mod hint;
28mod hud;
29pub(crate) mod i18n;
30mod language;
31pub mod layout;
32mod lobby;
33mod match_setup;
34mod menu_scene;
35mod menu_ui;
36pub mod net;
37pub mod palette;
38mod paths;
39mod pause;
40mod progress;
41mod replays;
42mod results;
43mod schedule;
44mod session;
45mod settings;
46mod side_panels;
47mod sim_events;
48mod stage_select;
49mod suspend;
50mod teams;
51mod tournament;
52mod update;
53
54pub use campaign::{Campaign, CampaignKind};
55pub use daily::Daily;
56pub use schedule::run;
57
58use crate::app::i18n::fill;
59use crate::sim::{
60    Board, BotLevel, Level, MAX_PLAYERS, PlayerAction, PuzzleOutcome, Replay, bot_action,
61    castle_spots, classic_arena, classic_arena_seeded, generate_arena,
62};
63use bevy::prelude::*;
64use bevy::render::RenderPlugin;
65use bevy::render::settings::{InstanceFlags, RenderCreation, WgpuSettings};
66
67/// Where the last finished versus round's replay is written (spec §7.7).
68pub fn replay_path() -> std::path::PathBuf {
69    replays::library_dir().join("last.txt")
70}
71/// Where that round's highlight reel lands (see [`crate::highlight`]).
72pub fn highlight_path() -> std::path::PathBuf {
73    replays::library_dir().join("highlight.gif")
74}
75
76/// Where the finished round's highlight reel was written, so the results
77/// card can say so. Cleared when a round has no replay to build one from.
78#[derive(Resource, Default)]
79pub struct Highlight(pub Option<String>);
80
81/// The authoritative simulation, wrapped for Bevy. Systems read it freely;
82/// mutation happens in `advance_sim` (ticks) and, during puzzle setup only,
83/// in the placement input system.
84#[derive(Resource)]
85pub struct Sim(pub Board);
86
87/// Player actions accumulated from input since the last fixed tick, in the
88/// shape rollback netcode will feed. Taken (and reset) by `advance_sim`
89/// each tick.
90#[derive(Resource, Default)]
91pub struct PendingActions(pub [PlayerAction; MAX_PLAYERS]);
92
93#[derive(Resource, Default)]
94pub struct Paused(pub bool);
95
96/// Records the running versus round for the replay file (spec §7.7).
97#[derive(Resource, Default)]
98pub struct Recorder(pub Option<Replay>);
99
100/// Which seats are bot-driven this round, and at what difficulty.
101#[derive(Resource, Default)]
102pub struct Bots(pub [Option<BotLevel>; MAX_PLAYERS]);
103
104/// A loaded replay being watched, and the next input index to feed.
105#[derive(Resource, Default)]
106pub struct Playback(pub Option<(Replay, usize)>);
107
108/// Top-level mode select.
109#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
110pub enum Screen {
111    #[default]
112    Menu,
113    /// Tide Pool (spec §5.1).
114    Puzzle,
115    /// Turf War (spec §5.3).
116    Versus,
117    /// Driftwood (spec §5.4).
118    Editor,
119    /// LAN matchmaking for online Turf War.
120    Lobby,
121    Settings,
122    /// Per-key rebinding, reached from Settings.
123    Controls,
124    /// Local versus configuration (players, bots, map, ...).
125    MatchSetup,
126    /// Lifetime stats and trophies.
127    Achievements,
128    /// The stage list a puzzle campaign is entered through.
129    StageSelect,
130    /// The kept rounds, and which one to watch.
131    Replays,
132    /// Between-rounds breather in a best-of-5 series.
133    Interlude,
134    /// The very first screen of the very first run: which language the
135    /// game should speak. Never reached again once a settings file
136    /// exists, since the dial in Settings takes over from there.
137    Language,
138    /// A newer release is out: its notes and one question. Reached from
139    /// the menu when the start-up check comes back with one, and left
140    /// for the menu either way.
141    NewVersion,
142}
143
144/// Fixed interface a board must not slide under, in unscaled pixels.
145///
146/// Three floats in a row, two of them the same unit and one of them not,
147/// is a thing to get the wrong way round: the camera reads `top` and
148/// `bottom` to centre the board on the gap between the bars, and swapping
149/// them moves every board a few pixels the wrong way on every screen.
150#[derive(Clone, Copy)]
151pub struct Chrome {
152    /// The sidebars, both of them together.
153    pub width: f32,
154    /// The header bar.
155    pub top: f32,
156    /// The prompt line, which runs to two rows in the wordier languages,
157    /// and the crab legend above it.
158    pub bottom: f32,
159}
160
161impl Chrome {
162    const fn of(width: f32, top: f32, bottom: f32) -> Chrome {
163        Chrome { width, top, bottom }
164    }
165}
166
167impl Screen {
168    /// Every screen, so anything that must cover all of them can iterate
169    /// rather than be remembered.
170    pub const ALL: [Screen; 14] = [
171        Screen::Menu,
172        Screen::Puzzle,
173        Screen::Versus,
174        Screen::Editor,
175        Screen::Lobby,
176        Screen::Settings,
177        Screen::Controls,
178        Screen::MatchSetup,
179        Screen::Achievements,
180        Screen::StageSelect,
181        Screen::Replays,
182        Screen::Interlude,
183        Screen::Language,
184        Screen::NewVersion,
185    ];
186
187    /// The fixed interface this screen puts around a board.
188    ///
189    /// Top and bottom are separate because they are not equal: the header
190    /// is one line and the prompt runs to two in the wordier languages. A
191    /// single height would fit the board and then centre it on the window
192    /// rather than on the gap, which is how the editor's bottom wall rail
193    /// ended up under the prompt.
194    ///
195    /// Lives here rather than in the camera system because it is a fact
196    /// about the screen, and because a new screen should have to answer
197    /// this question at the point it is declared.
198    fn chrome(self) -> Chrome {
199        match self {
200            // The menu is a full-bleed postcard laid out 1:1.
201            Screen::Menu => Chrome::of(0.0, 0.0, 0.0),
202            // Versus flanks the board with the two score panels.
203            Screen::Versus => Chrome::of(2.0 * side_panels::SIDEBAR_W + 60.0, 60.0, 104.0),
204            Screen::Puzzle
205            | Screen::Editor
206            | Screen::Lobby
207            | Screen::Settings
208            | Screen::Controls
209            | Screen::MatchSetup
210            | Screen::Achievements
211            | Screen::StageSelect
212            | Screen::Replays
213            | Screen::Interlude
214            | Screen::Language
215            | Screen::NewVersion => Chrome::of(40.0, 60.0, 104.0),
216        }
217    }
218}
219
220/// Puzzle-mode round phases (spec §5.1: place, run, win or lose, retry).
221#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
222pub enum Phase {
223    #[default]
224    Setup,
225    Running,
226    Won,
227    Lost,
228}
229
230/// Versus round flow: play until the tide, then results.
231#[derive(States, Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
232pub enum VersusPhase {
233    #[default]
234    Running,
235    Over,
236}
237
238/// Dev sandbox (`PINCH_SANDBOX=1`): skips the menu straight into a versus
239/// arena with preloaded castle tiers.
240#[derive(Resource)]
241pub struct Sandbox(pub bool);
242
243/// Rebuild the board from the campaign's current level. With `keep_posts`,
244/// signposts standing on the old board are re-placed: the fast retry loop.
245#[derive(Message)]
246pub struct LoadLevel {
247    pub keep_posts: bool,
248}
249
250/// A player's placement was rejected (occupied tile, rival post, or spent
251/// inventory): drives the denied sound and cursor flash.
252#[derive(Message)]
253pub struct PlacementDenied {
254    pub player: u8,
255}
256
257/// The editor wrote a level to disk. A message rather than a direct call so
258/// the editor need not know that anyone is keeping score.
259#[derive(Message)]
260pub struct LevelSaved;
261
262/// How many players a versus round seats (2-4). Drives castles, cursors,
263/// and HUD chips.
264#[derive(Resource, Default)]
265pub struct Seats(pub u8);
266
267/// What each seat is called this round, resolved once at round load:
268/// online, the handshake's agreed table (never the local couch names,
269/// which would label rivals with leftovers); offline, the settings names.
270/// Empty entries fall back to the localized seat label.
271#[derive(Resource, Default)]
272pub struct SeatNames(pub [String; MAX_PLAYERS]);
273
274impl SeatNames {
275    /// The name to show for `seat`, or the localized "P{n}" fallback.
276    pub fn label(&self, tr: &i18n::Tr, seat: u8) -> String {
277        match self.0.get(usize::from(seat)) {
278            Some(name) if !name.is_empty() => name.clone(),
279            _ => seat_label(tr, seat),
280        }
281    }
282}
283
284/// The localized "P{n}" label for a seat, off-by-one included: seats count
285/// from 0, players from 1. The screens that talk about a seat with no name
286/// to consult (bindings, match setup) say it this way too.
287pub fn seat_label(tr: &i18n::Tr, seat: u8) -> String {
288    fill(tr.player_label, &[("p", &(seat + 1).to_string())])
289}
290
291/// A round picked back up, from the save slot or from a pasted code,
292/// waiting for [`Screen::Versus`] to seat it. Taken by `load_versus`, which
293/// is the one place that decides what board a round starts from.
294#[derive(Resource, Default)]
295pub struct Resuming(pub Option<suspend::Suspended>);
296
297/// What the menu has to say about the round you just put down, copied, or
298/// failed to. Shown in the menu's status slot; a save that fails must not
299/// disappear in silence, since what it loses is the round you were playing.
300#[derive(Resource, Default)]
301pub struct RoundNotice(pub String);