Skip to main content

shuyi_ui/grid/
glyph.rs

1//! The glyph skin: a grid map drawn with colored glyphs (game ADR-0018,
2//! game ADR-0024, game ADR-0029). A tile is two 8 × 16 cells: a 16 × 16 pixel
3//! square. That size is known only here. A [`GlyphStyle`] changes how
4//! ranges, flashes, the cursor and the threat marks show on those tiles.
5
6pub mod cursor;
7pub mod style;
8
9use shuyi_core::{Pos, TerrainId};
10
11use super::layout::{CELL_PX, Grid};
12use super::path;
13use super::scene::MapScene;
14use super::skin::{GridSkin, Looks, MapColors, UnitPainter, WalkSpeeds};
15use super::threat;
16use crate::frame::color::EngineColor;
17use crate::frame::glyph_buffer::{Cell, GlyphBuffer, Rect};
18
19pub use style::{CursorLook, FlashLook, GlyphStyle, RangeLook, ThreatLook};
20
21/// Cells per map tile, across (a tile is two glyphs wide, game ADR-0018).
22pub const TILE_W_CELLS: i32 = 2;
23
24/// A map tile in console pixels, across × down: for a skin that paints
25/// its units over this skin's ground (game ADR-0049).
26pub const fn tile_px() -> (i32, i32) {
27    (TILE_W_CELLS * CELL_PX.0, CELL_PX.1)
28}
29
30/// How far ranges tint a tile's background toward their color (about
31/// 75%). *Tunable.*
32pub const OVERLAY_BLEND: f32 = 0.75;
33
34/// A grid map as colored glyphs: each tile its terrain's two glyphs, the
35/// ranges tinting their background (in its [plain style](GlyphStyle::PLAIN)).
36#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct GlyphSkin {
38    /// The palette's names of the path's, the cursor's and the threat
39    /// arrows' colors.
40    pub colors: MapColors,
41    /// How fast a walk is shown.
42    pub walk: WalkSpeeds,
43    /// How many pixels at the foot of a tile the units' own marks use (a
44    /// game's HP bar): the cursor's lower arms stay above them.
45    pub foot_px: i32,
46    /// How ranges, flashes, the cursor and the threat marks show, and the
47    /// cells left at the sides. (The sprite skin's glyph ground is always
48    /// in the plain style.)
49    pub style: GlyphStyle,
50}
51
52impl<U> GridSkin<U> for GlyphSkin {
53    fn name(&self) -> &'static str {
54        "glyph"
55    }
56
57    fn tile_size(&self) -> (i32, i32) {
58        tile_px()
59    }
60
61    fn view_tiles(&self, area: Rect) -> (i32, i32) {
62        self.style.view_tiles(area)
63    }
64
65    fn grid(&self, scene: &MapScene<U>, area: Rect) -> Grid {
66        self.style.layout(scene, area).grid()
67    }
68
69    fn walk(&self) -> WalkSpeeds {
70        self.walk
71    }
72
73    /// Terrain, then the ranges on it, then what lies under the units
74    /// ([`UnitPainter::paint_under`]), the units, the path, what lies over
75    /// them ([`UnitPainter::paint_over`]) and the cursor; over them all,
76    /// the threat arrows and marks.
77    fn paint(
78        &self,
79        looks: &Looks,
80        scene: &MapScene<U>,
81        area: Rect,
82        units: &dyn UnitPainter<U>,
83        buf: &mut GlyphBuffer,
84    ) {
85        let style = &self.style;
86        let layout = style.layout(scene, area);
87        let grid = layout.grid();
88        style.draw_tiles(looks, buf, scene, &layout);
89        units.paint_under(scene, &grid, buf);
90        units.paint(scene, &grid, buf);
91        let color = looks.path(&self.colors);
92        for overlay in path::path_overlays(&scene.path, &grid, color) {
93            buf.add_overlay(overlay);
94        }
95        units.paint_over(scene, &grid, buf);
96        style.draw_cursor(self.cursor_ink(looks), buf, scene, &layout, area);
97        // From the edge of each enemy's tile: never over its letters.
98        let colors = style.threat_colors(looks, &self.colors);
99        threat::paint(scene, &grid, threat::Start::Edge, colors, buf);
100    }
101}
102
103impl GlyphSkin {
104    /// The skin of `colors` in the plain style, showing a walk at `walk`,
105    /// its cursor's lower arms above the `foot_px` pixels at a tile's foot.
106    pub const fn new(colors: MapColors, walk: WalkSpeeds, foot_px: i32) -> Self {
107        Self {
108            colors,
109            walk,
110            foot_px,
111            style: GlyphStyle::PLAIN,
112        }
113    }
114
115    /// The same skin in `style`.
116    #[must_use]
117    pub const fn styled(mut self, style: GlyphStyle) -> Self {
118        self.style = style;
119        self
120    }
121
122    /// How its cursor looks in `looks`' colors.
123    pub fn cursor_ink(&self, looks: &Looks) -> cursor::CursorInk {
124        cursor::CursorInk {
125            color: looks.cursor(&self.colors),
126            dark: looks.dark(),
127            foot_px: self.foot_px,
128        }
129    }
130
131    /// Draws the scene's cursor, if it is on a tile of `layout`, as the
132    /// plain style does.
133    pub fn draw_cursor<U>(
134        &self,
135        looks: &Looks,
136        buf: &mut GlyphBuffer,
137        scene: &MapScene<U>,
138        layout: &Layout,
139    ) {
140        if let Some(c) = &scene.cursor
141            && let Some(at) = tile_to_cell(c.pos, layout)
142        {
143            cursor::draw_cursor(buf, self.cursor_ink(looks), c, layout, at);
144        }
145    }
146}
147
148/// How many tiles, across × down, fit in an area of `area` cells: two
149/// cells to a tile across, one down. An odd cell is left over.
150pub fn view_tiles(area: Rect) -> (i32, i32) {
151    (area.w.max(0) / TILE_W_CELLS, area.h.max(0))
152}
153
154/// Where a scene's tiles go in an area of cells.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct Layout {
157    /// The map tile in the area's top-left corner.
158    origin: Pos,
159    /// The top-left cell.
160    cell: (i32, i32),
161    /// Tiles drawn, across × down: the scene's, or as many as fit.
162    tiles: (i32, i32),
163}
164
165impl Layout {
166    /// The layout of `scene` in `area`.
167    pub fn new<U>(scene: &MapScene<U>, area: Rect) -> Self {
168        Self::fitted(scene, (area.x, area.y), view_tiles(area))
169    }
170
171    /// The layout of `scene` from the cell `corner`, drawing no more than
172    /// `fit` tiles: for a skin of two-cell tiles that places them its own
173    /// way (centred in its area).
174    pub fn fitted<U>(scene: &MapScene<U>, corner: (i32, i32), fit: (i32, i32)) -> Self {
175        Self {
176            origin: scene.origin,
177            cell: corner,
178            tiles: (scene.size.0.clamp(0, fit.0), scene.size.1.clamp(0, fit.1)),
179        }
180    }
181
182    /// The cells the tiles cover.
183    pub fn cells(&self) -> Rect {
184        let (x, y) = self.cell;
185        Rect::new(x, y, TILE_W_CELLS * self.tiles.0, self.tiles.1)
186    }
187
188    /// The tiles in console pixels: 16 × 16 each.
189    pub fn grid(&self) -> Grid {
190        let (cw, ch) = CELL_PX;
191        Grid::top_down(
192            self.origin,
193            (self.cell.0 * cw, self.cell.1 * ch),
194            (TILE_W_CELLS * cw, ch),
195            self.tiles,
196            1,
197        )
198    }
199}
200
201/// The console cell of the left glyph of `tile`, or `None` if the tile is
202/// outside the layout. The only place the "two cells per tile" rule lives
203/// (game ADR-0018).
204pub fn tile_to_cell(tile: Pos, layout: &Layout) -> Option<(i32, i32)> {
205    let dx = tile.x.checked_sub(layout.origin.x)?;
206    let dy = tile.y.checked_sub(layout.origin.y)?;
207    let inside = (0..layout.tiles.0).contains(&dx) && (0..layout.tiles.1).contains(&dy);
208    inside.then(|| (layout.cell.0 + TILE_W_CELLS * dx, layout.cell.1 + dy))
209}
210
211/// The console cell of the left glyph of `tile` on `grid`, a grid of this
212/// skin's tiles ([`Layout::grid`]), or `None` if the tile isn't drawn: where
213/// a game's unit painter writes a unit's glyphs.
214pub fn cell_of(grid: &Grid, tile: Pos) -> Option<(i32, i32)> {
215    let px = grid.rect(tile)?;
216    let (cw, ch) = CELL_PX;
217    Some((px.x.div_euclid(cw), px.y.div_euclid(ch)))
218}
219
220/// Draws terrain `id` on the tile whose left cell is `(x, y)`: its two
221/// glyphs in its colors. Nothing for a terrain the game lacks.
222pub fn draw_terrain(looks: &Looks, buf: &mut GlyphBuffer, id: TerrainId, (x, y): (i32, i32)) {
223    let Some(t) = looks.terrains.glyphs(id) else {
224        return;
225    };
226    let fg = looks.named(t.fg, EngineColor::Text);
227    let bg = looks.named(t.bg, EngineColor::Background);
228    for (i, glyph) in (0..).zip(t.glyphs) {
229        buf.set(x + i, y, Cell::new(glyph, fg, bg));
230    }
231}
232
233/// Draws every tile in the plain style ([`GlyphStyle::draw_tiles`]): its
234/// terrain (tiles off the map are left as they are, blank); a tile whose
235/// terrain just changed flashes, its background tinted towards its glyph
236/// color, fading out; then its ranges tint it, the first laid on
237/// undermost; and a tile that would change is drawn as the terrain it would
238/// become instead.
239pub fn draw_tiles<U>(looks: &Looks, buf: &mut GlyphBuffer, scene: &MapScene<U>, layout: &Layout) {
240    GlyphStyle::PLAIN.draw_tiles(looks, buf, scene, layout);
241}
242
243#[cfg(test)]
244pub(crate) mod tests {
245    use proptest::prelude::*;
246
247    use super::*;
248    use crate::frame::color::Rgb;
249    use crate::frame::glyph_buffer::{Layer, Overlay};
250    use crate::grid::layout::px_rect;
251    use crate::grid::scene::{CursorStyle, CursorView, RangeName, ThreatView};
252    use crate::grid::skin::NoUnits;
253    use crate::grid::testing::{any_area, any_scene};
254    use crate::prelude::*;
255    use crate::toy::grid::{GLYPH, GRASS, Kit, Letters, Piece, SAND, WATER};
256
257    /// A 70 × 30-cell map area: 35 × 30 of this skin's tiles.
258    pub(crate) const AREA: Rect = Rect::new(0, 0, 70, 30);
259
260    /// [`AREA`] with the view's top-left on `origin`.
261    pub(crate) fn layout_at(origin: Pos) -> Layout {
262        let size = view_tiles(AREA);
263        Layout::new(&MapScene::<Piece>::new(origin, size), AREA)
264    }
265
266    fn p(x: i32, y: i32) -> Pos {
267        Pos::new(x, y)
268    }
269
270    const MOVE: RangeName = RangeName("move");
271    const DANGER: RangeName = RangeName("danger");
272    const HEAL: RangeName = RangeName("heal");
273
274    /// A buffer the size of a 100 × 32 console, every cell `fill()`.
275    fn blank() -> GlyphBuffer {
276        GlyphBuffer::new(100, 32, fill())
277    }
278
279    fn fill() -> Cell {
280        Cell::new('x', Rgb::new(1, 2, 3), Rgb::new(4, 5, 6))
281    }
282
283    fn paint(kit: &Kit, scene: &MapScene<Piece>, area: Rect) -> GlyphBuffer {
284        let mut buf = blank();
285        GLYPH.paint(&kit.looks(), scene, area, &Letters, &mut buf);
286        buf
287    }
288
289    #[test]
290    fn a_70_by_30_cell_area_shows_35_by_30_tiles() {
291        let skin: &dyn GridSkin<Piece> = &GLYPH;
292        assert_eq!(skin.name(), "glyph");
293        assert_eq!(skin.view_tiles(AREA), (35, 30));
294        assert_eq!(skin.tile_size(), (16, 16));
295        assert_eq!(skin.walk(), GLYPH.walk);
296        // An odd cell is left over; an empty area shows nothing.
297        assert_eq!(skin.view_tiles(Rect::new(3, 4, 7, 5)), (3, 5));
298        assert_eq!(skin.view_tiles(Rect::new(0, 0, -4, -1)), (0, 0));
299    }
300
301    #[test]
302    fn tile_to_cell_maps_two_cells_per_tile() {
303        let c = layout_at(p(5, 2));
304        assert_eq!(tile_to_cell(Pos::new(5, 2), &c), Some((0, 0)));
305        assert_eq!(tile_to_cell(Pos::new(6, 3), &c), Some((2, 1)));
306        assert_eq!(tile_to_cell(Pos::new(39, 31), &c), Some((68, 29)));
307        assert_eq!(tile_to_cell(Pos::new(40, 2), &c), None);
308        assert_eq!(tile_to_cell(Pos::new(5, 32), &c), None);
309        assert_eq!(tile_to_cell(Pos::new(4, 2), &c), None);
310        assert_eq!(tile_to_cell(Pos::new(5, 1), &c), None);
311        let small = layout_at(p(-10, -11));
312        assert_eq!(tile_to_cell(Pos::new(0, 0), &small), Some((20, 11)));
313        assert_eq!(tile_to_cell(Pos::new(i32::MIN, 0), &c), None);
314        assert_eq!(tile_to_cell(Pos::new(0, i32::MIN), &c), None);
315        // The same cells from the layout's grid.
316        let grid = c.grid();
317        for tile in [p(5, 2), p(6, 3), p(39, 31), p(40, 2), p(4, 2), p(5, 1)] {
318            assert_eq!(cell_of(&grid, tile), tile_to_cell(tile, &c), "{tile:?}");
319        }
320        // Left of and above the console too.
321        let off = Layout::new(
322            &MapScene::<Piece>::new(p(0, 0), (3, 3)),
323            Rect::new(-6, -2, 6, 3),
324        );
325        assert_eq!(cell_of(&off.grid(), p(0, 0)), Some((-6, -2)));
326        assert_eq!(tile_to_cell(p(1, 1), &off), Some((-4, -1)));
327        assert_eq!(cell_of(&off.grid(), p(1, 1)), Some((-4, -1)));
328    }
329
330    #[test]
331    fn a_layout_starts_at_its_area_and_draws_no_more_tiles_than_fit() {
332        let area = Rect::new(4, 3, 7, 2);
333        // The scene is bigger than the area: 3 × 2 tiles fit.
334        let big = Layout::new(&MapScene::<Piece>::new(p(1, 1), (9, 9)), area);
335        assert_eq!(big.cells(), Rect::new(4, 3, 6, 2));
336        assert_eq!(tile_to_cell(p(1, 1), &big), Some((4, 3)));
337        assert_eq!(tile_to_cell(p(3, 2), &big), Some((8, 4)));
338        assert_eq!(tile_to_cell(p(4, 2), &big), None);
339        assert_eq!(tile_to_cell(p(3, 3), &big), None);
340        assert_eq!(big.grid().tile_px(p(1, 1)), (32, 48));
341        assert_eq!(big.grid().tile_px(p(0, 3)), (16, 80));
342        assert_eq!(big.grid().bounds(), px_rect(big.cells()));
343        assert_eq!(big.grid().tile, (16, 16));
344        assert_eq!(tile_px(), (16, 16));
345        // The scene is smaller: only its tiles.
346        let small = Layout::new(&MapScene::<Piece>::new(p(1, 1), (2, 1)), area);
347        assert_eq!(small.cells(), Rect::new(4, 3, 4, 1));
348        assert_eq!(tile_to_cell(p(2, 1), &small), Some((6, 3)));
349        assert_eq!(tile_to_cell(p(3, 1), &small), None);
350        assert_eq!(tile_to_cell(p(1, 2), &small), None);
351        // Fitted from a corner of its own, with room for fewer.
352        let fitted = Layout::fitted(&MapScene::<Piece>::new(p(1, 1), (9, 9)), (5, 0), (2, 3));
353        assert_eq!(fitted.cells(), Rect::new(5, 0, 4, 3));
354        assert_eq!(layout_at(p(0, 0)).cells(), AREA);
355    }
356
357    #[test]
358    fn tile_px_is_the_16_px_square_of_a_visible_tile() {
359        let scene = MapScene::<Piece>::new(p(-10, -11), view_tiles(AREA));
360        let px = |tile| GLYPH.tile_px(&scene, AREA, tile);
361        assert_eq!(px(p(0, 0)), Some(Rect::new(160, 176, 16, 16)));
362        assert_eq!(px(p(24, 18)), Some(Rect::new(544, 464, 16, 16)));
363        assert_eq!(px(p(25, 18)), None);
364        assert_eq!(px(p(-11, 0)), None);
365        assert_eq!(
366            GLYPH.tile_cells(&scene, AREA, p(3, 5)),
367            Some(Rect::new(26, 16, 2, 1))
368        );
369        // In another area: from its corner.
370        let area = Rect::new(4, 3, 7, 2);
371        assert_eq!(
372            GLYPH.tile_px(&scene, area, p(-9, -10)),
373            Some(Rect::new(48, 64, 16, 16))
374        );
375    }
376
377    /// The threat arrows and marks are the last things painted, over the
378    /// units, the path and the cursor, on the skin's 16 px tiles.
379    #[test]
380    fn threat_arrows_and_marks_are_painted_last_over_everything() {
381        let kit = Kit::new();
382        let looks = kit.looks();
383        let area = Rect::new(10, 5, 8, 3);
384        let mut scene = MapScene::new(p(2, 2), (4, 3));
385        scene.push_unit(Piece::new(4, 'f', p(5, 3)));
386        scene.path = vec![p(2, 2), p(3, 2)];
387        scene.cursor = Some(CursorView {
388            pos: p(3, 2),
389            brightness: 1.0,
390            style: CursorStyle::Corners,
391        });
392        let buf = paint(&kit, &scene, area);
393        let before = buf.overlays();
394        let threat = ThreatView {
395            tile: p(3, 2),
396            enemies: vec![p(5, 3)],
397        };
398        scene.threat = Some(threat.clone());
399        let with = paint(&kit, &scene, area);
400        // No cell changes: the arrows are drawn over the glyphs.
401        for y in 0..32 {
402            for x in 0..100 {
403                assert_eq!(with.get(x, y), buf.get(x, y), "({x}, {y})");
404            }
405        }
406        let colors = looks.threat(&GLYPH.colors);
407        let grid = Layout::new(&scene, area).grid();
408        let expect = threat::threat_overlays(&threat, &grid, threat::Start::Edge, colors);
409        let after = with.overlays();
410        assert_eq!(&after[..before.len()], before);
411        assert_eq!(&after[before.len()..], expect);
412        // The arrow's line starts just off the foe's tile (cells (16, 6)
413        // and (17, 6): pixels 128..144 × 96..112), on its way up and left
414        // from the tile's middle, and no arrow is on the tile; the mark is
415        // above that tile, 3 px clear of it.
416        let arrows: Vec<_> = expect.iter().filter(|o| o.color == colors.arrow).collect();
417        assert_eq!(arrows[0].rect, Rect::new(127, 100, 1, 1));
418        let tile = Rect::new(128, 96, 16, 16);
419        assert!(arrows.iter().all(|o| o.rect.intersect(&tile).is_none()));
420        assert!(arrows.iter().all(|o| o.layer == Layer::Over));
421        let marks: Vec<_> = expect.iter().filter(|o| o.color == colors.mark).collect();
422        assert_eq!(marks[0].rect, Rect::new(134, 84, 3, 6));
423        assert_eq!(marks[1].rect, Rect::new(134, 91, 3, 2));
424        assert_eq!(marks.len(), 2);
425    }
426
427    #[test]
428    fn paints_terrain_then_tints_then_units_path_and_cursor() {
429        let kit = Kit::new();
430        let looks = kit.looks();
431        let pal = &kit.palette;
432        let grass = looks.terrains.glyphs(GRASS).unwrap();
433        let (fg, bg) = (pal.lookup(grass.fg).unwrap(), pal.lookup(grass.bg).unwrap());
434        let area = Rect::new(10, 5, 8, 3);
435        let mut scene = MapScene::new(p(2, 2), (4, 3));
436        for pos in [p(2, 2), p(3, 2), p(4, 2), p(5, 2), p(2, 3)] {
437            scene.tile_mut(pos).unwrap().terrain = Some(GRASS);
438        }
439        // A terrain the game lacks is left blank, as off the map.
440        scene.tile_mut(p(3, 3)).unwrap().terrain = Some(TerrainId(999));
441        scene.tint([p(3, 2)], DANGER);
442        scene.tint([p(3, 2), p(4, 2)], MOVE);
443        // A range off the map still tints.
444        scene.tint([p(4, 4)], HEAL);
445        scene.push_unit(Piece::new(4, 'f', p(4, 2)));
446        scene.path = vec![p(2, 2), p(3, 2)];
447        scene.cursor = Some(CursorView {
448            pos: p(5, 2),
449            brightness: 1.0,
450            style: CursorStyle::Corners,
451        });
452        let buf = paint(&kit, &scene, area);
453        let cell = |x, y| *buf.get(x, y).unwrap();
454        // Tile (2, 2) at cells (10, 5) and (11, 5): grass.
455        assert_eq!(cell(10, 5), Cell::new(grass.glyphs[0], fg, bg));
456        assert_eq!(cell(11, 5), Cell::new(grass.glyphs[1], fg, bg));
457        // (3, 2): danger under move.
458        let blend = |bg: Rgb, kind| bg.lerp(looks.range(kind), OVERLAY_BLEND);
459        let both = blend(blend(bg, DANGER), MOVE);
460        assert_eq!(cell(12, 5), Cell::new(grass.glyphs[0], fg, both));
461        assert_eq!(cell(13, 5).bg, both);
462        // (4, 2): the piece's letter on the tinted tile.
463        let moved = blend(bg, MOVE);
464        let piece = pal.lookup("piece").unwrap();
465        assert_eq!(cell(14, 5), Cell::new('f', piece, moved));
466        assert_eq!(cell(15, 5), Cell::new(grass.glyphs[1], fg, moved));
467        // (3, 3) unknown and (4, 3) off the map: untouched; (4, 4) off the
468        // map but tinted.
469        assert_eq!(cell(12, 6), fill());
470        assert_eq!(cell(14, 6), fill());
471        let healed = Cell {
472            bg: blend(fill().bg, HEAL),
473            ..fill()
474        };
475        assert_eq!((cell(14, 7), cell(15, 7)), (healed, healed));
476        assert_eq!(cell(16, 7), fill());
477        // Overlays in order: the piece's own mark, the path, the cursor.
478        let colors: Vec<Rgb> = buf.overlays().iter().map(|o| o.color).collect();
479        let path = looks.path(&GLYPH.colors);
480        let mut expect = vec![piece, path];
481        expect.extend([path; 6]);
482        expect.extend([looks.cursor(&GLYPH.colors); 8]);
483        assert_eq!(colors, expect);
484        // The piece's mark along the foot of its tile (pixels 112..128 ×
485        // 80..96), a pixel in from each side.
486        assert_eq!(buf.overlays()[0].rect, Rect::new(113, 94, 14, 2));
487    }
488
489    #[test]
490    fn a_changed_tile_flashes_and_a_tile_that_would_change_shows_what_it_becomes() {
491        let kit = Kit::new();
492        let looks = kit.looks();
493        let pal = &kit.palette;
494        let look = |id| {
495            let t = looks.terrains.glyphs(id).unwrap();
496            (
497                t.glyphs,
498                pal.lookup(t.fg).unwrap(),
499                pal.lookup(t.bg).unwrap(),
500            )
501        };
502        let (grass, water) = (look(GRASS), look(WATER));
503        let mut scene = MapScene::new(p(0, 0), (5, 1));
504        for tile in &mut scene.tiles {
505            tile.terrain = Some(GRASS);
506        }
507        // Half a flash; a full flash under a range; two flashes at once.
508        scene.tile_mut(p(0, 0)).unwrap().flashes = vec![0.5];
509        scene.tile_mut(p(1, 0)).unwrap().flashes = vec![1.0];
510        scene.tile_mut(p(4, 0)).unwrap().flashes = vec![1.0, 0.5];
511        scene.tint([p(1, 0), p(2, 0), p(3, 0)], RangeName("attack"));
512        // Water to be, and a terrain the game lacks.
513        scene.tile_mut(p(2, 0)).unwrap().becomes = Some(WATER);
514        scene.tile_mut(p(3, 0)).unwrap().becomes = Some(TerrainId(999));
515        let buf = paint(&kit, &scene, Rect::new(0, 0, 10, 1));
516        let cell = |x| *buf.get(x, 0).unwrap();
517        let (glyphs, fg, bg) = grass;
518        let attack = looks.range(RangeName("attack"));
519        // Towards the tile's own glyph color, by the flash's strength.
520        let half = bg.lerp(fg, OVERLAY_BLEND * 0.5);
521        assert_eq!(cell(0), Cell::new(glyphs[0], fg, half));
522        assert_eq!(cell(1), Cell::new(glyphs[1], fg, half));
523        // The range goes over the flash.
524        let full = bg.lerp(fg, OVERLAY_BLEND);
525        assert_eq!(cell(2).bg, full.lerp(attack, OVERLAY_BLEND));
526        // What it would become replaces the tile, range and all…
527        assert_eq!(cell(4), Cell::new(water.0[0], water.1, water.2));
528        assert_eq!(cell(5), Cell::new(water.0[1], water.1, water.2));
529        // …unless the game lacks it.
530        assert_eq!(
531            cell(6),
532            Cell::new(glyphs[0], fg, bg.lerp(attack, OVERLAY_BLEND))
533        );
534        // Each flash tints in turn.
535        assert_eq!(cell(8).bg, full.lerp(fg, OVERLAY_BLEND * 0.5));
536        assert_ne!(water, grass);
537    }
538
539    #[test]
540    fn a_terrain_whose_colors_the_palette_lacks_is_drawn_in_the_engines() {
541        let kit = Kit::new();
542        let looks = kit.looks();
543        let mut scene = MapScene::<Piece>::new(p(0, 0), (1, 1));
544        scene.tiles[0].terrain = Some(SAND);
545        let mut buf = blank();
546        GLYPH.paint(&looks, &scene, Rect::new(0, 0, 2, 1), &NoUnits, &mut buf);
547        let sand = looks.terrains.glyphs(SAND).unwrap();
548        let text = kit.palette.engine(EngineColor::Text);
549        let background = kit.palette.engine(EngineColor::Background);
550        assert!(kit.palette.lookup(sand.fg).is_none());
551        assert!(kit.palette.lookup(sand.bg).is_none());
552        assert_eq!(
553            buf.get(0, 0),
554            Some(&Cell::new(sand.glyphs[0], text, background))
555        );
556        assert_eq!(
557            buf.get(1, 0),
558            Some(&Cell::new(sand.glyphs[1], text, background))
559        );
560    }
561
562    /// The pieces' letters, and over them a mark: a `^` on each piece's
563    /// left cell and a rectangle of [`MARK`] on its tile.
564    struct Marked;
565
566    /// The color of [`Marked`]'s rectangles.
567    const MARK: Rgb = Rgb::new(9, 8, 7);
568
569    impl UnitPainter<Piece> for Marked {
570        fn paint(&self, scene: &MapScene<Piece>, grid: &Grid, buf: &mut GlyphBuffer) {
571            Letters.paint(scene, grid, buf);
572        }
573
574        fn paint_over(&self, scene: &MapScene<Piece>, grid: &Grid, buf: &mut GlyphBuffer) {
575            for piece in &scene.units {
576                if let (Some((x, y)), Some(tile)) = (cell_of(grid, piece.pos), grid.rect(piece.pos))
577                    && let Some(&cell) = buf.get(x, y)
578                {
579                    buf.set(x, y, Cell { glyph: '^', ..cell });
580                    buf.add_overlay(Overlay::new(tile, MARK, Layer::Over));
581                }
582            }
583        }
584    }
585
586    /// What the painter lays over the units goes over them and the path,
587    /// and under the cursor.
588    #[test]
589    fn what_lies_over_the_units_is_painted_under_the_cursor() {
590        let kit = Kit::new();
591        let looks = kit.looks();
592        let area = Rect::new(10, 5, 8, 3);
593        let mut scene = MapScene::new(p(2, 2), (4, 3));
594        scene.push_unit(Piece::new(4, 'f', p(4, 2)));
595        scene.path = vec![p(2, 2), p(3, 2)];
596        let paint = |scene: &MapScene<Piece>, units: &dyn UnitPainter<Piece>| {
597            let mut buf = blank();
598            GLYPH.paint(&looks, scene, area, units, &mut buf);
599            buf
600        };
601        let bare = paint(&scene, &Marked);
602        // Over the piece's letter.
603        assert_eq!(bare.get(14, 5).map(|c| c.glyph), Some('^'));
604        // Its rectangle last: after the piece's bar and the path.
605        let colors: Vec<Rgb> = bare.overlays().iter().map(|o| o.color).collect();
606        let path = looks.path(&GLYPH.colors);
607        let piece = kit.palette.lookup("piece").unwrap();
608        let mut expect = vec![piece];
609        expect.extend([path; 7]);
610        expect.push(MARK);
611        assert_eq!(colors, expect);
612        // The cursor comes after it.
613        scene.cursor = Some(CursorView {
614            pos: p(4, 2),
615            brightness: 1.0,
616            style: CursorStyle::Corners,
617        });
618        let with = paint(&scene, &Marked);
619        let n = bare.overlays().len();
620        assert_eq!(&with.overlays()[..n], bare.overlays());
621        let cursor = looks.cursor(&GLYPH.colors);
622        assert_eq!(with.overlays()[n..].len(), 8);
623        assert!(with.overlays()[n..].iter().all(|o| o.color == cursor));
624        // A painter that lays nothing over the units: the letter, and no
625        // mark.
626        let letters = paint(&scene, &Letters);
627        assert_eq!(letters.overlays().len(), n + 8 - 1);
628        assert_eq!(letters.get(14, 5).map(|c| c.glyph), Some('f'));
629    }
630
631    #[test]
632    fn units_and_the_cursor_off_the_view_are_not_drawn() {
633        let kit = Kit::new();
634        let area = Rect::new(0, 0, 4, 1);
635        let mut scene = MapScene::new(p(0, 0), (2, 1));
636        // Not through `push_unit`: a scene built by hand.
637        scene.units.push(Piece::new(4, 'f', p(2, 0)));
638        scene.cursor = Some(CursorView {
639            pos: p(0, 1),
640            brightness: 1.0,
641            style: CursorStyle::TileGlow,
642        });
643        assert_eq!(paint(&kit, &scene, area), blank());
644    }
645
646    proptest! {
647        /// Whatever the scene and the area (even one partly off the
648        /// console), the skin changes no cell outside the area and adds no
649        /// item (rectangle or sprite) outside its pixels.
650        #[test]
651        fn glyph_skin_paints_only_the_area(scene in any_scene(), area in any_area()) {
652            let kit = Kit::new();
653            let buf = paint(&kit, &scene, area);
654            for y in 0..32 {
655                for x in 0..100 {
656                    if !area.contains(x, y) {
657                        prop_assert_eq!(buf.get(x, y), Some(&fill()), "cell ({}, {})", x, y);
658                    }
659                }
660            }
661            let px = px_rect(area);
662            for item in buf.items() {
663                let seen = item.visible();
664                prop_assert_eq!(seen.intersect(&px), Some(seen), "{:?} in {:?}", item, area);
665            }
666        }
667    }
668}