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}