Skip to main content

shuyi_ui/grid/
skin.rs

1//! A grid skin: how a [`MapScene`] looks (game ADR-0038). The skin paints
2//! the ground, the ranges, the path, the cursor and the threat arrows; the
3//! game's [`UnitPainter`] paints its units, between the skin's layers.
4//! What the skin paints with besides the scene (the palette, the game's
5//! terrains, the lights) it is given as [`Looks`].
6//!
7//! # A game with its own painters and several looks
8//!
9//! A game keeps its skins as `Rc<dyn GridSkin<U>>` and needs no skin
10//! trait of its own:
11//!
12//! - **The painter is built each frame.** [`GridSkin::paint`] takes the
13//!   [`UnitPainter`] per call, so a screen builds it in its own `draw`
14//!   from that frame's context (its palette, its unit pictures) and passes
15//!   a reference. The painter is told the skin's [`Grid`], whose
16//!   [`zoom`](Grid::zoom) and [`art_tile`](Grid::art_tile) say how big to
17//!   paint.
18//! - **What the painter needs is kept beside the skin.** Which painter
19//!   goes with which skin (a tileset of the game's unit pictures, a
20//!   one-bit painter) is the game's: a small struct or enum of its own
21//!   holding the skin and that, not a trait.
22//! - **Zoom** is the skin's: [`zoom_levels`](GridSkin::zoom_levels),
23//!   [`zoom`](GridSkin::zoom) and [`at_zoom`](GridSkin::at_zoom), none
24//!   for a skin that can't.
25//! - **Walk speeds** are the skin's too: [`WalkSpeeds`], with a speed for
26//!   while a key is held to hurry another side's turn.
27//! - **Another look of the same tiles** (a one-bit screen's) is the
28//!   glyph skin with a [`GlyphStyle`](super::glyph::style::GlyphStyle).
29//!
30//! ```
31//! use std::rc::Rc;
32//!
33//! use shuyi_ui::frame::glyph_buffer::{GlyphBuffer, Rect};
34//! use shuyi_ui::grid::{Grid, GridSkin, MapScene, UnitPainter};
35//! use shuyi_ui::toy::grid::{GLYPH, Kit, Piece};
36//!
37//! /// The game's painter, borrowing what one frame paints with.
38//! struct Pieces<'a> {
39//!     kit: &'a Kit,
40//! }
41//!
42//! impl UnitPainter<Piece> for Pieces<'_> {
43//!     fn paint(&self, scene: &MapScene<Piece>, grid: &Grid, buf: &mut GlyphBuffer) {
44//!         // Paints `scene.units` in `self.kit`'s colors and pictures, at
45//!         // `grid.zoom()`.
46//!         let _ = (self.kit, scene, grid, buf);
47//!     }
48//! }
49//!
50//! /// The game's look: a skin, and whatever its painter needs.
51//! struct MapLook {
52//!     skin: Rc<dyn GridSkin<Piece>>,
53//! }
54//!
55//! impl MapLook {
56//!     /// What the screen's `draw` does each frame.
57//!     fn draw(&self, kit: &Kit, scene: &MapScene<Piece>, buf: &mut GlyphBuffer) {
58//!         let painter = Pieces { kit };
59//!         let area = Rect::new(0, 0, 40, 20);
60//!         self.skin.paint(&kit.looks(), scene, area, &painter, buf);
61//!     }
62//!
63//!     /// The same look at `zoom`, if its skin zooms to it.
64//!     fn at_zoom(&self, zoom: i32) -> Option<Self> {
65//!         Some(Self { skin: self.skin.at_zoom(zoom)? })
66//!     }
67//! }
68//!
69//! let kit = Kit::new();
70//! let look = MapLook { skin: Rc::new(GLYPH) };
71//! let black = shuyi_ui::frame::Rgb::new(0, 0, 0);
72//! let mut buf = GlyphBuffer::new(40, 20, shuyi_ui::frame::Cell::new(' ', black, black));
73//! look.draw(&kit, &MapScene::new(shuyi_core::Pos::new(0, 0), (4, 4)), &mut buf);
74//! // The glyph skin's tile is two font cells: it doesn't zoom.
75//! assert!(look.at_zoom(2).is_none());
76//! ```
77
78use alloc::rc::Rc;
79
80use shuyi_assets::lighting::Lighting;
81use shuyi_core::{Pos, TerrainId};
82
83use super::layout::{CELL_PX, Grid};
84use super::scene::{MapScene, RangeName};
85use super::threat;
86use crate::frame::color::{EngineColor, Palette, Rgb};
87use crate::frame::glyph_buffer::{GlyphBuffer, PxRect, Rect};
88
89/// How the glyph skin draws one terrain: its two glyphs, in the palette's
90/// colors of these names. Build it with [`TerrainGlyphs::new`]: a struct
91/// literal doesn't compile outside this crate, so a new field never breaks
92/// a game (ADR-0039).
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94#[non_exhaustive]
95pub struct TerrainGlyphs<'a> {
96    /// The tile's two glyphs, left then right.
97    pub glyphs: [char; 2],
98    /// The palette's name of the glyphs' color.
99    pub fg: &'a str,
100    /// The palette's name of the tile's background.
101    pub bg: &'a str,
102}
103
104impl<'a> TerrainGlyphs<'a> {
105    /// The glyphs `glyphs`, in the palette's color `fg` on its `bg`.
106    #[must_use]
107    pub const fn new(glyphs: [char; 2], fg: &'a str, bg: &'a str) -> Self {
108        Self { glyphs, fg, bg }
109    }
110}
111
112/// A game's terrains, as far as its map's look goes: what each is called,
113/// and how the glyph skin draws it. The game's table, by [`TerrainId`].
114pub trait Terrains {
115    /// The name of terrain `id` (`"water"`), if the game has it.
116    fn name(&self, id: TerrainId) -> Option<&str>;
117
118    /// How the glyph skin draws terrain `id`, if the game has it.
119    fn glyphs(&self, id: TerrainId) -> Option<TerrainGlyphs<'_>>;
120}
121
122/// The palette's names of the colors a grid skin paints the path, the
123/// cursor and the threat arrows in: what the game's palette calls them.
124/// The ranges are painted in the colors of their own names; a threat's
125/// marks are the engine's highlight color, and every dark edge (and what
126/// the cursor fades toward) its background color. Build it with
127/// [`MapColors::new`] (ADR-0039).
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129#[non_exhaustive]
130pub struct MapColors {
131    /// The path's line and arrowhead.
132    pub path: &'static str,
133    /// The cursor.
134    pub cursor: &'static str,
135    /// The threat arrows: the color of whoever threatens.
136    pub threat: &'static str,
137}
138
139impl MapColors {
140    /// The path in the palette's color `path`, the cursor in `cursor` and
141    /// the threat arrows in `threat`.
142    #[must_use]
143    pub const fn new(path: &'static str, cursor: &'static str, threat: &'static str) -> Self {
144        Self {
145            path,
146            cursor,
147            threat,
148        }
149    }
150}
151
152/// What a skin paints a scene with besides the scene itself: the palette,
153/// the game's terrains and the lights a map may be in. Build it with
154/// [`Looks::new`] (ADR-0039).
155#[derive(Clone, Copy)]
156#[non_exhaustive]
157pub struct Looks<'a> {
158    /// The colors.
159    pub palette: &'a Palette,
160    /// The game's terrains.
161    pub terrains: &'a dyn Terrains,
162    /// The lights, by the name a map's look gives
163    /// ([`MapLook::light`](shuyi_assets::grid_map::MapLook::light)).
164    pub lights: &'a Lighting,
165}
166
167impl core::fmt::Debug for Looks<'_> {
168    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
169        f.debug_struct("Looks")
170            .field("palette", self.palette)
171            .field("lights", self.lights)
172            .finish_non_exhaustive()
173    }
174}
175
176impl<'a> Looks<'a> {
177    /// The looks of `palette`'s colors, the game's `terrains` and its
178    /// `lights`.
179    #[must_use]
180    pub const fn new(
181        palette: &'a Palette,
182        terrains: &'a dyn Terrains,
183        lights: &'a Lighting,
184    ) -> Self {
185        Self {
186            palette,
187            terrains,
188            lights,
189        }
190    }
191
192    /// The palette's color called `name`, or the engine's `fallback`
193    /// (a palette checked against the game's colors has every name the
194    /// game uses, so this only guards against a bug).
195    pub fn named(&self, name: &str, fallback: EngineColor) -> Rgb {
196        self.palette
197            .lookup(name)
198            .unwrap_or_else(|| self.palette.engine(fallback))
199    }
200
201    /// The color a range of `kind` tints its tiles with: the palette's of
202    /// its name.
203    pub fn range(&self, kind: RangeName) -> Rgb {
204        self.named(kind.0, EngineColor::Text)
205    }
206
207    /// The colors of `colors`' names.
208    pub fn path(&self, colors: &MapColors) -> Rgb {
209        self.named(colors.path, EngineColor::Text)
210    }
211
212    /// The cursor's color, at full brightness.
213    pub fn cursor(&self, colors: &MapColors) -> Rgb {
214        self.named(colors.cursor, EngineColor::Text)
215    }
216
217    /// The colors of the threat arrows and marks: the arrows in
218    /// `colors`' threat color, the marks in the engine's highlight color,
219    /// both edged in its background color.
220    pub fn threat(&self, colors: &MapColors) -> threat::Colors {
221        threat::Colors {
222            arrow: self.named(colors.threat, EngineColor::Text),
223            mark: self.palette.engine(EngineColor::Highlight),
224            edge: self.palette.engine(EngineColor::Background),
225        }
226    }
227
228    /// The engine's background color: every dark edge, and what the
229    /// cursor fades toward.
230    pub fn dark(&self) -> Rgb {
231        self.palette.engine(EngineColor::Background)
232    }
233}
234
235/// How fast a unit's walk is shown, in tiles per second: how long the
236/// player watches a move, never what the move does. A skin's setting.
237#[derive(Debug, Clone, Copy, PartialEq)]
238pub struct WalkSpeeds {
239    /// As a rule.
240    pub normal: f32,
241    /// With the player's fast animations on.
242    pub fast: f32,
243    /// While the player holds a key to hurry another side's turn along.
244    pub held: f32,
245}
246
247impl WalkSpeeds {
248    /// `normal`, with fast animations the same, and four times it while
249    /// a turn is hurried.
250    pub const fn of(normal: f32) -> Self {
251        Self {
252            normal,
253            fast: normal,
254            held: 4.0 * normal,
255        }
256    }
257}
258
259/// Paints a game's units on a grid skin's tiles: its letters, bars,
260/// marks or pictures (game ADR-0038). A skin calls it between its own
261/// layers.
262pub trait UnitPainter<U> {
263    /// What lies on the ground under the units (a light square under a
264    /// unit picked out): painted after the ground and the ranges, before
265    /// the path's line. Nothing unless the painter says.
266    fn paint_under(&self, _scene: &MapScene<U>, _grid: &Grid, _buf: &mut GlyphBuffer) {}
267
268    /// The units of `scene` on `grid`'s tiles: after the path's line (on
269    /// the glyph skin, after the ground), before its arrowhead and the
270    /// cursor.
271    fn paint(&self, scene: &MapScene<U>, grid: &Grid, buf: &mut GlyphBuffer);
272
273    /// What lies over the units and under the cursor (marks at the edges
274    /// the map goes on past, [`MapScene::more`]): painted after the units
275    /// and the path, before the cursor and the threat arrows. Nothing
276    /// unless the painter says.
277    fn paint_over(&self, _scene: &MapScene<U>, _grid: &Grid, _buf: &mut GlyphBuffer) {}
278}
279
280/// Paints no units: a map shown with nobody on it, or the ground alone.
281#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
282pub struct NoUnits;
283
284impl<U> UnitPainter<U> for NoUnits {
285    fn paint(&self, _: &MapScene<U>, _: &Grid, _: &mut GlyphBuffer) {}
286}
287
288/// Paints grid maps of units `U`. The tile size is the skin's: screens ask
289/// it how many tiles fit and where a tile is. A skin never changes the
290/// game: only the frame, how many tiles are on screen, and how fast a
291/// walk is shown.
292pub trait GridSkin<U>: core::fmt::Debug {
293    /// A stable `snake_case` name for the kind of skin, e.g. `"glyph"`.
294    fn name(&self) -> &'static str;
295
296    /// A map tile's size on screen, across × down, in console pixels.
297    fn tile_size(&self) -> (i32, i32);
298
299    /// How many tiles, across × down, fit in a map area of `area` cells.
300    fn view_tiles(&self, area: Rect) -> (i32, i32);
301
302    /// Where `scene`'s tiles go when it is painted into `area`.
303    fn grid(&self, scene: &MapScene<U>, area: Rect) -> Grid;
304
305    /// How fast a unit's walk is shown under this skin.
306    fn walk(&self) -> WalkSpeeds;
307
308    /// The zooms it can draw the map at, smallest first: console pixels
309    /// per pixel of its art. None if it can't zoom (the glyph skin: its
310    /// tile is two font cells).
311    fn zoom_levels(&self) -> &[i32] {
312        &[]
313    }
314
315    /// The zoom it draws at, or `None` if it can't zoom.
316    fn zoom(&self) -> Option<i32> {
317        None
318    }
319
320    /// The same skin drawing at `zoom`, or `None` if `zoom` isn't one of
321    /// its [`zoom_levels`](Self::zoom_levels).
322    fn at_zoom(&self, _zoom: i32) -> Option<Rc<dyn GridSkin<U>>> {
323        None
324    }
325
326    /// Paints `scene` into the cells of `area`, its units by `units`: a
327    /// painter the caller may build each frame, from what that frame paints
328    /// with (see the [module's docs](self)). Touches nothing outside it.
329    fn paint(
330        &self,
331        looks: &Looks,
332        scene: &MapScene<U>,
333        area: Rect,
334        units: &dyn UnitPainter<U>,
335        buf: &mut GlyphBuffer,
336    );
337
338    /// The console pixels of `tile` when `scene` is painted into `area`, or
339    /// `None` if it isn't visible. Menus and popups go beside it.
340    fn tile_px(&self, scene: &MapScene<U>, area: Rect, tile: Pos) -> Option<PxRect> {
341        self.grid(scene, area).rect(tile)
342    }
343
344    /// The cells [`tile_px`](Self::tile_px) touches.
345    fn tile_cells(&self, scene: &MapScene<U>, area: Rect, tile: Pos) -> Option<Rect> {
346        let px = self.tile_px(scene, area, tile)?;
347        Some(cells_of(px))
348    }
349}
350
351/// The cells the pixels `px` touch.
352pub fn cells_of(px: PxRect) -> Rect {
353    let (cw, ch) = CELL_PX;
354    let (x, y) = (px.x.div_euclid(cw), px.y.div_euclid(ch));
355    let right = (px.x + px.w + cw - 1).div_euclid(cw);
356    let bottom = (px.y + px.h + ch - 1).div_euclid(ch);
357    Rect::new(x, y, right - x, bottom - y)
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use crate::prelude::*;
364    use crate::toy::grid::{Kit, Piece};
365
366    /// A skin whose every tile is the pixel rectangle it was built with.
367    #[derive(Debug)]
368    struct Fixed(Option<PxRect>);
369
370    impl GridSkin<Piece> for Fixed {
371        fn name(&self) -> &'static str {
372            "fixed"
373        }
374        fn tile_size(&self) -> (i32, i32) {
375            (16, 16)
376        }
377        fn view_tiles(&self, _: Rect) -> (i32, i32) {
378            (1, 1)
379        }
380        fn grid(&self, scene: &MapScene<Piece>, _: Rect) -> Grid {
381            Grid::top_down(scene.origin, (0, 0), (16, 16), (1, 1), 1)
382        }
383        fn walk(&self) -> WalkSpeeds {
384            WalkSpeeds::of(3.0)
385        }
386        fn paint(
387            &self,
388            _: &Looks,
389            _: &MapScene<Piece>,
390            _: Rect,
391            _: &dyn UnitPainter<Piece>,
392            _: &mut GlyphBuffer,
393        ) {
394        }
395        fn tile_px(&self, _: &MapScene<Piece>, _: Rect, _: Pos) -> Option<PxRect> {
396            self.0
397        }
398    }
399
400    fn cells(px: Option<PxRect>) -> Option<Rect> {
401        let scene = MapScene::<Piece>::new(Pos::new(0, 0), (1, 1));
402        Fixed(px).tile_cells(&scene, Rect::new(0, 0, 2, 1), Pos::new(0, 0))
403    }
404
405    #[test]
406    fn tile_cells_are_the_cells_the_tile_touches() {
407        let r = Rect::new;
408        // Cells are 8 × 16 px. A glyph tile: two whole cells.
409        assert_eq!(cells(Some(r(160, 176, 16, 16))), Some(r(20, 11, 2, 1)));
410        // One pixel into the next cell, each way.
411        assert_eq!(cells(Some(r(160, 176, 17, 17))), Some(r(20, 11, 3, 2)));
412        assert_eq!(cells(Some(r(159, 175, 16, 16))), Some(r(19, 10, 3, 2)));
413        // A 32 px tile: four cells by two.
414        assert_eq!(cells(Some(r(32, 32, 32, 32))), Some(r(4, 2, 4, 2)));
415        // Left of and above the console.
416        assert_eq!(cells(Some(r(-16, -16, 16, 16))), Some(r(-2, -1, 2, 1)));
417        assert_eq!(cells(Some(r(-1, -1, 2, 2))), Some(r(-1, -1, 2, 2)));
418        assert_eq!(cells(None), None);
419        // A skin can't zoom unless it says it can.
420        let fixed: &dyn GridSkin<Piece> = &Fixed(None);
421        let found = fixed.zoom_levels();
422        assert!(found.is_empty(), "{found:?}");
423        assert_eq!(fixed.zoom(), None);
424        assert!(fixed.at_zoom(1).is_none());
425    }
426
427    #[test]
428    fn a_skins_tiles_are_where_its_grid_puts_them_unless_it_says() {
429        #[derive(Debug)]
430        struct Plain;
431        impl GridSkin<Piece> for Plain {
432            fn name(&self) -> &'static str {
433                "plain_grid"
434            }
435            fn tile_size(&self) -> (i32, i32) {
436                (16, 16)
437            }
438            fn view_tiles(&self, _: Rect) -> (i32, i32) {
439                (2, 2)
440            }
441            fn grid(&self, scene: &MapScene<Piece>, _: Rect) -> Grid {
442                Grid::top_down(scene.origin, (8, 32), (16, 16), (2, 2), 1)
443            }
444            fn walk(&self) -> WalkSpeeds {
445                WalkSpeeds::of(3.0)
446            }
447            fn paint(
448                &self,
449                _: &Looks,
450                _: &MapScene<Piece>,
451                _: Rect,
452                _: &dyn UnitPainter<Piece>,
453                _: &mut GlyphBuffer,
454            ) {
455            }
456        }
457        let scene = MapScene::<Piece>::new(Pos::new(4, 4), (2, 2));
458        let area = Rect::new(0, 0, 4, 4);
459        assert_eq!(
460            Plain.tile_px(&scene, area, Pos::new(5, 4)),
461            Some(Rect::new(24, 32, 16, 16))
462        );
463        assert_eq!(Plain.tile_px(&scene, area, Pos::new(6, 4)), None);
464        assert_eq!(
465            Plain.tile_cells(&scene, area, Pos::new(5, 5)),
466            Some(Rect::new(3, 3, 2, 1))
467        );
468        assert_eq!(Plain.walk(), WalkSpeeds::of(3.0));
469    }
470
471    #[test]
472    fn walk_speeds_of_a_pace_are_the_same_fast_and_four_times_held() {
473        let w = WalkSpeeds::of(5.0);
474        assert_eq!((w.normal, w.fast, w.held), (5.0, 5.0, 20.0));
475    }
476
477    #[test]
478    fn looks_name_the_palettes_colors_and_fall_back_to_the_engines() {
479        let kit = Kit::new();
480        let palette = &kit.palette;
481        let looks = kit.looks();
482        let lookup = |name| palette.lookup(name).unwrap();
483        assert_eq!(
484            looks.named("grass_bg", EngineColor::Text),
485            lookup("grass_bg")
486        );
487        let text = palette.engine(EngineColor::Text);
488        assert_eq!(looks.named("no_such_color", EngineColor::Text), text);
489        let warning = palette.engine(EngineColor::Warning);
490        assert_eq!(looks.named("no_such_color", EngineColor::Warning), warning);
491        let colors = crate::toy::grid::COLORS;
492        assert_eq!(looks.range(RangeName("move")), lookup("move"));
493        assert_eq!(looks.range(RangeName("nothing")), text);
494        assert_eq!(looks.path(&colors), lookup("path"));
495        assert_eq!(looks.cursor(&colors), lookup("cursor"));
496        let threat = looks.threat(&colors);
497        assert_eq!(threat.arrow, lookup("foe"));
498        assert_eq!(threat.mark, palette.engine(EngineColor::Highlight));
499        assert_eq!(threat.edge, palette.engine(EngineColor::Background));
500        assert_eq!(looks.dark(), palette.engine(EngineColor::Background));
501        // A painter that paints nothing under or over the ground.
502        let scene = MapScene::<Piece>::new(Pos::new(0, 0), (2, 2));
503        let grid = Grid::top_down(scene.origin, (0, 0), (16, 16), (2, 2), 1);
504        let mut buf = GlyphBuffer::new(8, 4, crate::frame::Cell::new('x', text, text));
505        let before = buf.clone();
506        NoUnits.paint_under(&scene, &grid, &mut buf);
507        NoUnits.paint(&scene, &grid, &mut buf);
508        assert_eq!(buf, before);
509        assert!(format!("{looks:?}").starts_with("Looks"));
510    }
511
512    /// [`MapColors`] as games build theirs, in a `const` item.
513    const COLORS: MapColors = MapColors::new("path", "cursor", "threat");
514
515    #[test]
516    fn the_constructors_give_the_literals() {
517        assert_eq!(
518            TerrainGlyphs::new(['~', '~'], "water_fg", "water_bg"),
519            TerrainGlyphs {
520                glyphs: ['~', '~'],
521                fg: "water_fg",
522                bg: "water_bg",
523            }
524        );
525        assert_eq!(
526            COLORS,
527            MapColors {
528                path: "path",
529                cursor: "cursor",
530                threat: "threat",
531            }
532        );
533        let kit = Kit::new();
534        let literal = kit.looks();
535        let looks = Looks::new(literal.palette, literal.terrains, literal.lights);
536        assert!(core::ptr::eq(looks.palette, literal.palette));
537        assert!(core::ptr::eq(looks.lights, literal.lights));
538        assert!(core::ptr::addr_eq(looks.terrains, literal.terrains));
539    }
540}