1use 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
27pub const PAN_SPEED: f32 = 60.0;
29
30pub const BOX_TEXT: [&str; 3] = ["A text box drawn", "over the scene", "hides it."];
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub struct CameraLayout {
37 pub window: Rect,
39 pub text_box: Rect,
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub struct SceneCameraView {
47 pub title: String,
49 pub info: String,
52 pub help: String,
54 pub layout: CameraLayout,
56 pub shown: Option<(Rc<GlyphBuffer>, PanFrame)>,
59 pub caption: String,
61 pub box_text: Vec<String>,
63}
64
65#[derive(Debug, Clone)]
69pub struct SceneCameraScreen {
70 picture: Option<Rc<GlyphBuffer>>,
72 missing: String,
74 layout: CameraLayout,
76 window_px: (u32, u32),
78 centre: (f32, f32),
80 zoom: u8,
82}
83
84impl SceneCameraScreen {
85 pub const NAME: &'static str = "scene_camera";
87
88 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)] 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 pub fn picture(&self) -> Option<&Rc<GlyphBuffer>> {
116 self.picture.as_ref()
117 }
118
119 pub fn window_px(&self) -> (u32, u32) {
121 self.window_px
122 }
123
124 pub fn centre(&self) -> (f32, f32) {
126 self.centre
127 }
128
129 pub fn zoom(&self) -> u8 {
131 self.zoom
132 }
133
134 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 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)] 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 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 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 Some(_) => format!("zoom {}x window starts at {x:.1},{y:.1}", self.zoom),
177 None => self.missing.clone(),
178 };
179 SceneCameraView {
180 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 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 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;