Skip to main content

shuyi_ui/debug/
camera.rs

1//! The scene camera (the game's ticket 0228, moved here in 1154): a
2//! picture (a whole map, [`MapPicture`](crate::grid::MapPicture)) as a
3//! backdrop (game ADR-0048) behind a window of the console, panned by the pixel
4//! and zoomed in whole steps, with a caption and a text box over it. For
5//! checking by eye that a renderer keeps the scene sharp and seamless.
6//!
7//! The game gives the picture, where the window and the box go, and what
8//! to say when it has no picture. The screen says what it shows as a
9//! [`SceneCameraView`] and [`glyph::paint_camera`](super::glyph::paint_camera)
10//! draws it.
11
12use crate::prelude::*;
13use alloc::rc::Rc;
14
15use shuyi_assets::keymap::{ActionSet, UiAction};
16
17use crate::audio::MenuSound;
18use crate::cinematic::PanFrame;
19use crate::ctx::{Ctx, GameData};
20use crate::frame::cinema::{self, MAX_ZOOM};
21use crate::frame::console::ConsoleSpec;
22use crate::frame::glyph_buffer::{GlyphBuffer, Rect};
23use crate::grid::picture::pixel_size;
24use crate::input::help_keys::{cursor_keys_name, help_line, key_name};
25use crate::screen::{FrameInput, Screen, Transition};
26
27/// How fast a held cursor key pans, in scene pixels a second.
28pub const PAN_SPEED: f32 = 60.0;
29
30/// The lines in the text box.
31// check-text: not player text (debug tools are English only)
32pub const BOX_TEXT: [&str; 3] = ["A text box drawn", "over the scene", "hides it."];
33
34/// Where the scene camera's window and text box go on the console.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct CameraLayout {
37    /// The cells the scene shows through.
38    pub window: Rect,
39    /// The text box drawn over part of the window: at least 20 cells
40    /// across and 5 down, for [`BOX_TEXT`].
41    pub text_box: Rect,
42}
43
44/// What the scene camera shows.
45#[derive(Debug, Clone, PartialEq)]
46pub struct SceneCameraView {
47    /// Its title.
48    pub title: String,
49    /// The line under it: the zoom and where the window starts, or what
50    /// is missing.
51    pub info: String,
52    /// The help line.
53    pub help: String,
54    /// Where the window and the text box go.
55    pub layout: CameraLayout,
56    /// The picture behind the window, and where it is; `None` without
57    /// one, and then no window is drawn.
58    pub shown: Option<(Rc<GlyphBuffer>, PanFrame)>,
59    /// The text over the scene, with no box behind it.
60    pub caption: String,
61    /// The lines in the text box.
62    pub box_text: Vec<String>,
63}
64
65/// Shows a picture through a window: the cursor actions pan (held),
66/// Confirm steps the zoom 1× to [`MAX_ZOOM`] and round again, Cancel
67/// closes.
68#[derive(Debug, Clone)]
69pub struct SceneCameraScreen {
70    /// The picture; `None` if the game has none to show.
71    picture: Option<Rc<GlyphBuffer>>,
72    /// What to say without it.
73    missing: String,
74    /// Where the window and the text box go.
75    layout: CameraLayout,
76    /// The window's size in console pixels.
77    window_px: (u32, u32),
78    /// The scene pixel in the middle of the window.
79    centre: (f32, f32),
80    /// Console pixels per scene pixel.
81    zoom: u8,
82}
83
84impl SceneCameraScreen {
85    /// Name reported by [`Screen::name`].
86    pub const NAME: &'static str = "scene_camera";
87
88    /// The camera at 1× over the middle of `picture`, with `layout` on a
89    /// console of `console`'s cells; `missing` is what it says without a
90    /// picture.
91    pub fn new(
92        console: ConsoleSpec,
93        picture: Option<Rc<GlyphBuffer>>,
94        layout: CameraLayout,
95        missing: String,
96    ) -> Self {
97        let (w, h) = picture.as_deref().map_or((0, 0), pixel_size);
98        #[allow(clippy::cast_precision_loss)] // pixel sizes are small
99        let centre = (w as f32 / 2.0, h as f32 / 2.0);
100        let px = |cells: i32, cell: u16| u32::try_from(cells).unwrap_or(0) * u32::from(cell);
101        Self {
102            picture,
103            missing,
104            layout,
105            window_px: (
106                px(layout.window.w, console.cell_w),
107                px(layout.window.h, console.cell_h),
108            ),
109            centre,
110            zoom: 1,
111        }
112    }
113
114    /// The picture shown, if there is one.
115    pub fn picture(&self) -> Option<&Rc<GlyphBuffer>> {
116        self.picture.as_ref()
117    }
118
119    /// The window's size in console pixels.
120    pub fn window_px(&self) -> (u32, u32) {
121        self.window_px
122    }
123
124    /// The scene pixel in the middle of the window.
125    pub fn centre(&self) -> (f32, f32) {
126        self.centre
127    }
128
129    /// Console pixels per scene pixel: 1 to [`MAX_ZOOM`].
130    pub fn zoom(&self) -> u8 {
131        self.zoom
132    }
133
134    /// The scene pixel at the window's top-left ([`cinema::view`]).
135    pub fn origin_px(&self) -> (f32, f32) {
136        let scene = self.picture.as_deref().map_or((0, 0), pixel_size);
137        cinema::view(scene, self.window_px, self.centre, self.zoom)
138    }
139
140    /// Moves the centre by `(dx, dy)` scene pixels, kept to the middle of
141    /// what the window can show: holding a key at an edge doesn't run the
142    /// centre away from it. Along an axis the whole scene fits in, that is
143    /// the scene's middle.
144    fn pan(&mut self, dx: f32, dy: f32) {
145        let scene = self.picture.as_deref().map_or((0, 0), pixel_size);
146        let clip = self.window_px;
147        let zoom = f32::from(self.zoom);
148        #[allow(clippy::cast_precision_loss)] // pixel sizes are small
149        let axis = |centre: f32, scene: u32, clip: u32| {
150            let (half, middle) = (clip as f32 / zoom / 2.0, scene as f32 / 2.0);
151            centre.clamp(half.min(middle), (scene as f32 - half).max(middle))
152        };
153        self.centre = (
154            axis(self.centre.0 + dx, scene.0, clip.0),
155            axis(self.centre.1 + dy, scene.1, clip.1),
156        );
157    }
158
159    /// What the camera shows, naming the keys of the device used last.
160    pub fn view<G: GameData>(&self, ctx: &Ctx<G>) -> SceneCameraView {
161        let km = ctx.help_keys();
162        let key = |ui| G::Action::for_ui(ui).map(|a| key_name(km, a));
163        // check-text: not player text (debug tools are English only)
164        let help = help_line(&[
165            (Some(cursor_keys_name(km)), "pan (hold)"),
166            (key(UiAction::Confirm), "zoom"),
167            (key(UiAction::Cancel), "back"),
168        ]);
169        let (x, y) = self.origin_px();
170        let frame = PanFrame {
171            origin_px: (x, y),
172            zoom: self.zoom,
173        };
174        let info = match self.picture {
175            // check-text: not player text (debug tools are English only)
176            Some(_) => format!("zoom {}x   window starts at {x:.1},{y:.1}", self.zoom),
177            None => self.missing.clone(),
178        };
179        SceneCameraView {
180            // check-text: not player text (debug tools are English only)
181            title: "Scene camera (debug)".to_owned(),
182            info,
183            help,
184            layout: self.layout,
185            shown: self.picture.as_ref().map(|p| (Rc::clone(p), frame)),
186            // check-text: not player text (debug tools are English only)
187            caption: "Text over the scene".to_owned(),
188            box_text: BOX_TEXT.map(str::to_owned).to_vec(),
189        }
190    }
191}
192
193impl<G: GameData> Screen<G> for SceneCameraScreen {
194    fn name(&self) -> &'static str {
195        Self::NAME
196    }
197
198    /// Confirm zooms, Cancel closes, the held directions pan.
199    fn actions(&self) -> Option<Vec<G::Action>> {
200        Some(crate::input::ui_actions(crate::input::NAVIGATION))
201    }
202
203    fn update(&mut self, ctx: &mut Ctx<G>, input: &FrameInput<G::Action>) -> Transition<G> {
204        for &action in &input.actions {
205            match action.ui() {
206                Some(UiAction::Cancel) => {
207                    ctx.audio.menu(MenuSound::Cancel);
208                    return Transition::Pop;
209                }
210                Some(UiAction::Confirm) => {
211                    ctx.audio.menu(MenuSound::Select);
212                    self.zoom = self.zoom % MAX_ZOOM + 1;
213                    self.pan(0.0, 0.0);
214                }
215                _ => {}
216            }
217        }
218        let step = PAN_SPEED * input.dt();
219        let held = |ui| {
220            let down = G::Action::for_ui(ui).is_some_and(|a| input.is_held(a));
221            f32::from(u8::from(down))
222        };
223        let dx = held(UiAction::Right) - held(UiAction::Left);
224        let dy = held(UiAction::Down) - held(UiAction::Up);
225        self.pan(dx * step, dy * step);
226        Transition::None
227    }
228
229    fn draw(&self, ctx: &Ctx<G>, buf: &mut GlyphBuffer) {
230        super::glyph::paint_camera(&ctx.palette, &self.view(ctx), buf);
231    }
232
233    fn as_any(&self) -> Option<&dyn core::any::Any> {
234        Some(self)
235    }
236}
237
238#[cfg(test)]
239mod tests;