Skip to main content

shuyi_ui/grid/
scene.rs

1//! What is on the visible part of a grid map, as plain data (game
2//! game ADR-0038): terrain (flashing where it just changed, or marked with what
3//! it would turn into), ranges, units, the cursor, the path and those that
4//! threaten a tile. No colors, glyphs, cells or pixels: a
5//! [`GridSkin`](super::GridSkin) turns a scene into those. The units are
6//! the game's own view of them (`U`), which only the game's
7//! [`UnitPainter`](super::UnitPainter) paints.
8
9use crate::prelude::*;
10use core::fmt::Write as _;
11
12use serde::{Deserialize, Serialize};
13use shuyi_assets::grid_map::MapLook;
14use shuyi_core::{Pos, TerrainId};
15
16use super::skin::Terrains;
17
18/// The visible part of a grid map, with units of the game's kind `U`.
19/// Build it with [`MapScene::new`] and assign the rest: a struct literal
20/// doesn't compile outside this crate, so a new field never breaks a game
21/// (ADR-0039).
22#[derive(Debug, Clone, PartialEq)]
23#[non_exhaustive]
24pub struct MapScene<U> {
25    /// The map tile at the view's top-left (negative, or past the map, when
26    /// the map is smaller than the view).
27    pub origin: Pos,
28    /// Visible tiles, across × down.
29    pub size: (i32, i32),
30    /// The sides of the view the map goes on past: for a game's marks at
31    /// those edges. None unless set ([`MapEdges::of_view`]).
32    pub more: MapEdges,
33    /// The visible tiles, row by row: `size.0 × size.1` of them.
34    pub tiles: Vec<TileView>,
35    /// The ranges shown, in the order laid on: on a tile in two, the
36    /// second is laid over the first.
37    pub ranges: Vec<RangeLayer>,
38    /// The terrain of the ring of tiles just outside the view (the row
39    /// above it, then the two ends of each of its rows, then the row
40    /// below), for a skin whose picture of a tile depends on its
41    /// neighbours. `None` = off the map, or not given. Read it with
42    /// [`terrain_near`](Self::terrain_near).
43    pub rim: Vec<Option<TerrainId>>,
44    /// How the map looks, as far as its file says: which set of pictures
45    /// a skin that has pictures paints its terrain with, and which light
46    /// it shades them by (the light's name: the skin says how it looks).
47    pub look: MapLook,
48    /// The units on visible tiles, in the game's order.
49    pub units: Vec<U>,
50    /// The cursor, if it is shown and on a visible tile.
51    pub cursor: Option<CursorView>,
52    /// The selected unit's path, its own tile first; empty = none. Its
53    /// tiles may lie outside the view.
54    pub path: Vec<Pos>,
55    /// Those that could attack a tile, if the game shows them; `None` =
56    /// nothing to show.
57    pub threat: Option<ThreatView>,
58    /// The screen's animation clock, in milliseconds: what a skin or a
59    /// unit painter moves its marks by. It only ever goes forward.
60    pub clock_ms: u64,
61}
62
63/// The sides of a view the map goes on past: there is more map to its
64/// left, to its right, above it or below it.
65#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
66pub struct MapEdges {
67    /// Across: more map left of the view (`before`), right of it
68    /// (`after`).
69    pub x: Ends,
70    /// Down: more map above the view (`before`), below it (`after`).
71    pub y: Ends,
72}
73
74/// Whether the map goes on before and after a view, along one axis.
75#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
76pub struct Ends {
77    /// More map before the view's first tile (left, or above).
78    pub before: bool,
79    /// More map after its last (right, or below).
80    pub after: bool,
81}
82
83impl Ends {
84    /// Neither end.
85    pub const NONE: Self = Self {
86        before: false,
87        after: false,
88    };
89
90    /// The ends a view `len` tiles long from `start` leaves map past, on a
91    /// map `map_len` tiles long from 0.
92    fn of_view(start: i32, len: i32, map_len: i32) -> Self {
93        let end = i64::from(start) + i64::from(len.max(0));
94        Self {
95            before: start > 0,
96            after: end < i64::from(map_len),
97        }
98    }
99}
100
101impl MapEdges {
102    /// The map goes on past no side: it all fits in the view.
103    pub const NONE: Self = Self {
104        x: Ends::NONE,
105        y: Ends::NONE,
106    };
107
108    /// The sides a view of `size` tiles from `origin` leaves map past, on
109    /// a map of `map_size` tiles (across × down) from `(0, 0)`: left when
110    /// the view starts right of the map's first column, right when it
111    /// ends before its last, and the same up and down. A map smaller than
112    /// the view (centred in it, from a negative origin) goes on past none.
113    pub fn of_view(origin: Pos, size: (i32, i32), map_size: (i32, i32)) -> Self {
114        Self {
115            x: Ends::of_view(origin.x, size.0, map_size.0),
116            y: Ends::of_view(origin.y, size.1, map_size.1),
117        }
118    }
119
120    /// More map left of the view.
121    pub const fn left(self) -> bool {
122        self.x.before
123    }
124
125    /// More map right of the view.
126    pub const fn right(self) -> bool {
127        self.x.after
128    }
129
130    /// More map above the view.
131    pub const fn up(self) -> bool {
132        self.y.before
133    }
134
135    /// More map below the view.
136    pub const fn down(self) -> bool {
137        self.y.after
138    }
139
140    /// Whether the map goes on past any side.
141    pub const fn any(self) -> bool {
142        self.left() || self.right() || self.up() || self.down()
143    }
144
145    /// The sides set, by name, in the order left, right, up, down.
146    pub fn names(self) -> Vec<&'static str> {
147        let sides = [
148            (self.left(), "left"),
149            (self.right(), "right"),
150            (self.up(), "up"),
151            (self.down(), "down"),
152        ];
153        sides
154            .into_iter()
155            .filter_map(|(set, name)| set.then_some(name))
156            .collect()
157    }
158}
159
160/// One visible tile.
161#[derive(Debug, Clone, PartialEq, Default)]
162pub struct TileView {
163    /// Its terrain; `None` = off the map.
164    pub terrain: Option<TerrainId>,
165    /// How strongly it flashes after its terrain changed: `1` at the
166    /// change, fading to `0`. One entry per change still flashing; usually
167    /// none.
168    pub flashes: Vec<f32>,
169    /// The terrain it would turn into (a spell being aimed at it): shown
170    /// in place of its own, so the tile reads as terrain, not as a target.
171    pub becomes: Option<TerrainId>,
172}
173
174/// A kind of range a map shows (where a unit can move, what it can
175/// attack), by its name: the game's own list, of which the engine knows
176/// nothing. A skin tints a range's tiles with the palette's color of that
177/// name, and the scene's text marks a tile with its first letter.
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
179pub struct RangeName(pub &'static str);
180
181impl RangeName {
182    /// Its letter in [`MapScene::to_text`]: its name's first, `?` for an
183    /// empty name.
184    pub fn mark(self) -> char {
185        self.0.chars().next().unwrap_or('?')
186    }
187}
188
189/// A range shown on the map: its kind, and the visible tiles in it in the
190/// order given.
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct RangeLayer {
193    /// Which range.
194    pub kind: RangeName,
195    /// Its visible tiles.
196    pub tiles: Vec<Pos>,
197}
198
199/// The ranges each visible tile of a scene is in, in the order laid on:
200/// what a skin tints a tile by ([`MapScene::tints`]).
201#[derive(Debug, Clone, PartialEq, Eq)]
202pub struct Tints {
203    /// Tiles across.
204    across: i32,
205    /// Row by row, as [`MapScene::tiles`].
206    tiles: Vec<Vec<RangeName>>,
207}
208
209impl Tints {
210    /// The ranges the tile `dx` right of and `dy` below the view's first
211    /// is in, in the order laid on; none for a tile outside the view.
212    pub fn at(&self, dx: i32, dy: i32) -> &[RangeName] {
213        let index = dy
214            .checked_mul(self.across)
215            .and_then(|row| row.checked_add(dx))
216            .filter(|_| (0..self.across).contains(&dx))
217            .and_then(|i| usize::try_from(i).ok());
218        index
219            .and_then(|i| self.tiles.get(i))
220            .map_or(&[], Vec::as_slice)
221    }
222}
223
224/// How the cursor is drawn: corner marks by default; bigger corners and
225/// the tile glow are for players who need them.
226#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
227pub enum CursorStyle {
228    /// Corner marks.
229    #[default]
230    Corners,
231    /// Longer corner marks.
232    LargeCorners,
233    /// The tile's background tinted towards the cursor color.
234    TileGlow,
235}
236
237impl CursorStyle {
238    /// Its name in [`MapScene::to_text`].
239    pub const fn name(self) -> &'static str {
240        match self {
241            CursorStyle::Corners => "corners",
242            CursorStyle::LargeCorners => "large-corners",
243            CursorStyle::TileGlow => "glow",
244        }
245    }
246}
247
248/// The cursor as shown on the map. Build it with [`CursorView::new`]
249/// (ADR-0039).
250#[derive(Debug, Clone, Copy, PartialEq)]
251#[non_exhaustive]
252pub struct CursorView {
253    /// The tile it is on.
254    pub pos: Pos,
255    /// Where it is in its pulse: `1` = full brightness, down to about half.
256    pub brightness: f32,
257    /// The player's cursor style.
258    pub style: CursorStyle,
259}
260
261impl CursorView {
262    /// The cursor on tile `pos`, at `brightness` in its pulse, drawn in
263    /// `style`.
264    #[must_use]
265    pub const fn new(pos: Pos, brightness: f32, style: CursorStyle) -> Self {
266        Self {
267            pos,
268            brightness,
269            style,
270        }
271    }
272}
273
274/// Those that could attack a tile in their next turn: an arrow from each
275/// to the tile, and a mark over each.
276#[derive(Debug, Clone, PartialEq, Eq)]
277pub struct ThreatView {
278    /// The tile they could attack.
279    pub tile: Pos,
280    /// The tiles they stand on, in the game's order; never empty. They may
281    /// lie outside the view.
282    pub enemies: Vec<Pos>,
283}
284
285/// What a scene needs of a game's view of a unit: which unit it is, the
286/// tile it is shown on, and its line in the scene's text.
287pub trait MapUnit {
288    /// What tells units apart.
289    type Id: Copy + PartialEq;
290
291    /// Which unit it is.
292    fn id(&self) -> Self::Id;
293
294    /// The tile it is shown on.
295    fn pos(&self) -> Pos;
296
297    /// Its line in [`MapScene::to_text`]: everything about it a test or a
298    /// bug report needs, the same for the same unit.
299    fn text(&self) -> String;
300}
301
302impl<U> MapScene<U> {
303    /// A view of `size` tiles from `origin` with nothing on it: every tile
304    /// off the map. A negative size counts as zero.
305    pub fn new(origin: Pos, size: (i32, i32)) -> Self {
306        let size = (size.0.max(0), size.1.max(0));
307        let count = usize::try_from(size.0).unwrap_or(0) * usize::try_from(size.1).unwrap_or(0);
308        Self {
309            origin,
310            size,
311            more: MapEdges::NONE,
312            tiles: vec![TileView::default(); count],
313            ranges: Vec::new(),
314            rim: vec![None; rim_len(size)],
315            look: MapLook::default(),
316            units: Vec::new(),
317            cursor: None,
318            path: Vec::new(),
319            threat: None,
320            clock_ms: 0,
321        }
322    }
323
324    /// Where `pos` is in the view, as tiles right of and below
325    /// [`origin`](Self::origin), or `None` if it isn't visible.
326    pub fn offset(&self, pos: Pos) -> Option<(i32, i32)> {
327        let dx = pos.x.checked_sub(self.origin.x)?;
328        let dy = pos.y.checked_sub(self.origin.y)?;
329        let inside = (0..self.size.0).contains(&dx) && (0..self.size.1).contains(&dy);
330        inside.then_some((dx, dy))
331    }
332
333    /// Whether `pos` is a visible tile.
334    pub fn contains(&self, pos: Pos) -> bool {
335        self.offset(pos).is_some()
336    }
337
338    /// Index in [`tiles`](Self::tiles) of the tile `dx` right of and `dy`
339    /// below the origin.
340    fn index(&self, dx: i32, dy: i32) -> Option<usize> {
341        if !(0..self.size.0).contains(&dx) || !(0..self.size.1).contains(&dy) {
342            return None;
343        }
344        let (dx, dy) = (usize::try_from(dx).ok()?, usize::try_from(dy).ok()?);
345        Some(dy * usize::try_from(self.size.0).ok()? + dx)
346    }
347
348    /// The tile `dx` right of and `dy` below the origin, if in the view.
349    pub fn tile_at(&self, dx: i32, dy: i32) -> Option<&TileView> {
350        self.tiles.get(self.index(dx, dy)?)
351    }
352
353    /// The tile at map position `pos`, if visible.
354    pub fn tile(&self, pos: Pos) -> Option<&TileView> {
355        let (dx, dy) = self.offset(pos)?;
356        self.tile_at(dx, dy)
357    }
358
359    /// Mutable [`tile`](Self::tile).
360    pub fn tile_mut(&mut self, pos: Pos) -> Option<&mut TileView> {
361        let (dx, dy) = self.offset(pos)?;
362        let index = self.index(dx, dy)?;
363        self.tiles.get_mut(index)
364    }
365
366    /// Index in [`rim`](Self::rim) of the tile `dx` right of and `dy` below
367    /// the origin, if it is in the ring just outside the view.
368    fn rim_index(&self, dx: i32, dy: i32) -> Option<usize> {
369        let (w, h) = self.size;
370        if !(-1..=w).contains(&dx) || !(-1..=h).contains(&dy) {
371            return None;
372        }
373        let across = w + 2;
374        let index = if dy == -1 {
375            dx + 1
376        } else if dy == h {
377            across + 2 * h + dx + 1
378        } else if dx == -1 {
379            across + 2 * dy
380        } else if dx == w {
381            across + 2 * dy + 1
382        } else {
383            return None;
384        };
385        usize::try_from(index).ok()
386    }
387
388    /// The terrain of the tile `dx` right of and `dy` below the origin: a
389    /// tile of the view, or of the ring just outside it. `None` off the
390    /// map, further out, or where the ring wasn't given.
391    pub fn terrain_near(&self, dx: i32, dy: i32) -> Option<TerrainId> {
392        match self.tile_at(dx, dy) {
393            Some(tile) => tile.terrain,
394            None => self.rim.get(self.rim_index(dx, dy)?).copied().flatten(),
395        }
396    }
397
398    /// Sets the terrain of every tile of the view, and of the ring just
399    /// outside it, to what `terrain` gives for its map position.
400    pub fn set_terrain(&mut self, terrain: impl Fn(Pos) -> Option<TerrainId>) {
401        let (w, h) = self.size;
402        let origin = self.origin;
403        for (dx, dy) in (-1..=h).flat_map(|dy| (-1..=w).map(move |dx| (dx, dy))) {
404            let id = terrain(Pos::new(origin.x + dx, origin.y + dy));
405            let tile = self.index(dx, dy).and_then(|i| self.tiles.get_mut(i));
406            if let Some(tile) = tile {
407                tile.terrain = id;
408            } else if let Some(slot) = self.rim_index(dx, dy).and_then(|i| self.rim.get_mut(i)) {
409                *slot = id;
410            }
411        }
412    }
413
414    /// Lays the range `kind` on each of `tiles` that is visible: a new
415    /// layer over those laid before, if any of them is.
416    pub fn tint(&mut self, tiles: impl IntoIterator<Item = Pos>, kind: impl Into<RangeName>) {
417        let tiles: Vec<Pos> = tiles
418            .into_iter()
419            .filter(|&pos| self.tile(pos).is_some())
420            .collect();
421        if !tiles.is_empty() {
422            let kind = kind.into();
423            self.ranges.push(RangeLayer { kind, tiles });
424        }
425    }
426
427    /// The ranges every visible tile is in, in the order laid on (a tile
428    /// listed twice in a layer is in it twice).
429    pub fn tints(&self) -> Tints {
430        let mut tiles = vec![Vec::new(); self.tiles.len()];
431        for layer in &self.ranges {
432            for &pos in &layer.tiles {
433                let at = self.offset(pos).and_then(|(dx, dy)| self.index(dx, dy));
434                if let Some(list) = at.and_then(|i| tiles.get_mut(i)) {
435                    list.push(layer.kind);
436                }
437            }
438        }
439        Tints {
440            across: self.size.0,
441            tiles,
442        }
443    }
444
445    /// The visible tiles in the range `kind`, row by row, each once.
446    pub fn tinted(&self, kind: impl Into<RangeName>) -> Vec<Pos> {
447        let kind = kind.into();
448        let tints = self.tints();
449        let (w, h) = self.size;
450        let offsets = (0..h).flat_map(|dy| (0..w).map(move |dx| (dx, dy)));
451        offsets
452            .filter(|&(dx, dy)| tints.at(dx, dy).contains(&kind))
453            .map(|(dx, dy)| Pos::new(self.origin.x + dx, self.origin.y + dy))
454            .collect()
455    }
456
457    /// The ranges `pos` is in, in the order laid on; none if it isn't
458    /// visible.
459    pub fn tints_at(&self, pos: Pos) -> Vec<RangeName> {
460        match self.offset(pos) {
461            Some((dx, dy)) => self.tints().at(dx, dy).to_vec(),
462            None => Vec::new(),
463        }
464    }
465
466    /// The terrain shown on `pos`, if it is a visible map tile.
467    pub fn terrain_at(&self, pos: Pos) -> Option<TerrainId> {
468        self.tile(pos).and_then(|t| t.terrain)
469    }
470
471    /// The tile the cursor is on, if it is shown.
472    pub fn cursor_tile(&self) -> Option<Pos> {
473        self.cursor.map(|c| c.pos)
474    }
475
476    /// Row `dy` of the view in [`to_text`](Self::to_text): its tiles' names,
477    /// equal neighbours counted.
478    fn row_text(&self, terrains: &dyn Terrains, tints: &Tints, dy: i32) -> String {
479        let mut runs: Vec<(String, usize)> = Vec::new();
480        for dx in 0..self.size.0 {
481            let name = self.tile_at(dx, dy).map_or_else(
482                || "!".to_owned(),
483                |t| tile_text(terrains, t, tints.at(dx, dy)),
484            );
485            match runs.last_mut() {
486                Some((last, n)) if *last == name => *n += 1,
487                _ => runs.push((name, 1)),
488            }
489        }
490        let words: Vec<String> = runs
491            .into_iter()
492            .map(|(name, n)| if n > 1 { format!("{name}*{n}") } else { name })
493            .collect();
494        words.join(" ")
495    }
496}
497
498impl<U: MapUnit> MapScene<U> {
499    /// Adds `unit` if it is on a visible tile.
500    pub fn push_unit(&mut self, unit: U) {
501        if self.contains(unit.pos()) {
502            self.units.push(unit);
503        }
504    }
505
506    /// The unit `id`, if it is shown.
507    pub fn unit(&self, id: U::Id) -> Option<&U> {
508        self.units.iter().find(|u| u.id() == id)
509    }
510
511    /// The unit shown on `pos`, if any (the last one, drawn on top, if two
512    /// share it).
513    pub fn unit_at(&self, pos: Pos) -> Option<&U> {
514        self.units.iter().rev().find(|u| u.pos() == pos)
515    }
516
517    /// The scene as text, for tests and bug reports, with the terrains'
518    /// names from `terrains`. The same scene always gives the same text.
519    /// The clock isn't in it (nothing happens by it), nor the ring of
520    /// terrain round the view (it is the map's, one tile further out). A
521    /// look other than a map's usual one ends the first line: `look
522    /// indoor`; so does a light other than the usual: `light dusk`. The
523    /// sides the map goes on past follow it in a line of their own, only
524    /// when there are some: `more right down`.
525    ///
526    /// ```text
527    /// origin (-10,-11) size 35x30
528    ///  -11: -*35
529    ///    0: -*10 water*2 grass+m*3 sand+mm grass! grass>water -*18
530    /// units 1
531    ///   #4 (7,3)
532    /// cursor (3,5) corners 1.00
533    /// path (3,5) (4,5)
534    /// ```
535    ///
536    /// One line per row of tiles, headed by its map row: terrain names
537    /// (`-` = off the map, `?n` for an id the game lacks), `+` and a letter
538    /// per range on the tile in the order laid on (each range's
539    /// [mark](RangeName::mark)), and `*n` for `n` such tiles in a row. `!`
540    /// marks a tile flashing after its terrain changed, and `>` is
541    /// followed by the terrain it would turn into. Each unit's line is its
542    /// own ([`MapUnit::text`]). The cursor's number is its brightness. A
543    /// last line, `threat (5,3) from (6,3) (9,1)`, is there only when a
544    /// tile is threatened: the tile, then where each threat stands.
545    // check-text: not player text (the scene as text, for tests)
546    pub fn to_text(&self, terrains: &dyn Terrains) -> String {
547        let mut out = String::new();
548        let Pos { x, y } = self.origin;
549        let (w, h) = self.size;
550        // Writing to a `String` can't fail.
551        let _ = write!(out, "origin ({x},{y}) size {w}x{h}");
552        let usual = MapLook::default();
553        if self.look.tiles != usual.tiles {
554            let _ = write!(out, " look {}", self.look.tiles);
555        }
556        if self.look.light != usual.light {
557            let _ = write!(out, " light {}", self.look.light);
558        }
559        out.push('\n');
560        if self.more.any() {
561            let _ = writeln!(out, "more {}", self.more.names().join(" "));
562        }
563        let tints = self.tints();
564        for dy in 0..h {
565            let row = self.row_text(terrains, &tints, dy);
566            let _ = writeln!(out, "{:>4}: {row}", i64::from(y) + i64::from(dy));
567        }
568        let _ = writeln!(out, "units {}", self.units.len());
569        for u in &self.units {
570            let _ = writeln!(out, "  {}", u.text());
571        }
572        match &self.cursor {
573            Some(c) => {
574                let Pos { x, y } = c.pos;
575                let (style, bright) = (c.style.name(), c.brightness);
576                let _ = writeln!(out, "cursor ({x},{y}) {style} {bright:.2}");
577            }
578            None => out.push_str("cursor -\n"),
579        }
580        out.push_str("path");
581        if self.path.is_empty() {
582            out.push_str(" -");
583        }
584        for p in &self.path {
585            let _ = write!(out, " ({},{})", p.x, p.y);
586        }
587        out.push('\n');
588        if let Some(threat) = &self.threat {
589            let Pos { x, y } = threat.tile;
590            let _ = write!(out, "threat ({x},{y}) from");
591            for p in &threat.enemies {
592                let _ = write!(out, " ({},{})", p.x, p.y);
593            }
594            out.push('\n');
595        }
596        out
597    }
598}
599
600/// How many tiles the ring just outside a view of `size` tiles has.
601fn rim_len(size: (i32, i32)) -> usize {
602    let count = |tiles: i32| usize::try_from(tiles).unwrap_or(0);
603    2 * (count(size.0) + 2) + 2 * count(size.1)
604}
605
606/// A tile in [`MapScene::to_text`]: its terrain's name (`-` off the map,
607/// `?n` for an id the game lacks), `!` if it flashes, `+` and its ranges'
608/// letters, then `>` and the terrain it would become.
609fn tile_text(terrains: &dyn Terrains, tile: &TileView, ranges: &[RangeName]) -> String {
610    let name = |id: TerrainId| match terrains.name(id) {
611        Some(name) => name.to_owned(),
612        None => format!("?{}", id.0),
613    };
614    let mut text = tile.terrain.map_or_else(|| "-".to_owned(), name);
615    if !tile.flashes.is_empty() {
616        text.push('!');
617    }
618    if !ranges.is_empty() {
619        text.push('+');
620        text.extend(ranges.iter().map(|k| k.mark()));
621    }
622    if let Some(id) = tile.becomes {
623        text.push('>');
624        text.push_str(&name(id));
625    }
626    text
627}
628
629#[cfg(test)]
630mod tests {
631    use super::*;
632    use crate::toy::grid::{GRASS, Piece, SAND, Terrain, WATER};
633
634    fn p(x: i32, y: i32) -> Pos {
635        Pos::new(x, y)
636    }
637
638    fn piece(id: u32, pos: Pos) -> Piece {
639        Piece::new(id, 'k', pos)
640    }
641
642    const MOVE: RangeName = RangeName("move");
643    const DANGER: RangeName = RangeName("danger");
644    const ATTACK: RangeName = RangeName("attack");
645    const HEAL: RangeName = RangeName("heal");
646
647    #[test]
648    fn a_new_scene_is_empty_tiles_off_the_map() {
649        let s = MapScene::<Piece>::new(p(-2, 3), (4, 2));
650        assert_eq!((s.origin, s.size), (p(-2, 3), (4, 2)));
651        assert_eq!(s.tiles, vec![TileView::default(); 8]);
652        assert_eq!(s.tiles[0].terrain, None);
653        assert!(s.units.is_empty() && s.path.is_empty() && s.cursor.is_none());
654        assert!(s.ranges.is_empty(), "{:?}", s.ranges);
655        assert_eq!(s.threat, None);
656        assert_eq!(s.clock_ms, 0);
657        // The ring round a 4 × 2 view: 6 above, 6 below, 2 at each side.
658        assert_eq!(s.rim, vec![None; 16]);
659        assert_eq!(s.look, MapLook::default());
660        assert_eq!(s.more, MapEdges::NONE);
661        // A negative size is an empty view.
662        let none = MapScene::<Piece>::new(p(0, 0), (-3, 5));
663        assert_eq!((none.size, none.tiles.len()), ((0, 5), 0));
664        let none = MapScene::<Piece>::new(p(0, 0), (3, -5));
665        assert_eq!((none.size, none.tiles.len()), ((3, 0), 0));
666        assert!(!none.contains(p(0, 0)));
667    }
668
669    #[test]
670    fn tiles_are_found_by_map_position_inside_the_view_only() {
671        let mut s = MapScene::<Piece>::new(p(-2, 3), (4, 2));
672        for (i, t) in (0..).zip(&mut s.tiles) {
673            t.terrain = Some(TerrainId(i));
674        }
675        let id = |pos| s.tile(pos).and_then(|t| t.terrain).map(|t| t.0);
676        let last_row = (id(p(-2, 4)), id(p(1, 4)));
677        assert_eq!(id(p(-2, 3)), Some(0));
678        assert_eq!(id(p(1, 3)), Some(3));
679        assert_eq!(last_row, (Some(4), Some(7)));
680        for out in [p(-3, 3), p(2, 3), p(-2, 2), p(-2, 5), p(2, 4)] {
681            assert_eq!(s.tile(out), None, "{out:?}");
682            assert!(!s.contains(out), "{out:?}");
683        }
684        assert_eq!(s.offset(p(1, 4)), Some((3, 1)));
685        assert_eq!(s.offset(p(i32::MIN, 3)), None);
686        assert_eq!(s.offset(p(0, i32::MAX)), None);
687        assert_eq!(s.tile_at(3, 1).and_then(|t| t.terrain), Some(TerrainId(7)));
688        for (dx, dy) in [(4, 0), (-1, 0), (0, 2), (0, -1)] {
689            assert_eq!(s.tile_at(dx, dy), None, "({dx}, {dy})");
690        }
691        // A scene short of tiles (not built by `new`) has none there.
692        s.tiles.truncate(5);
693        assert_eq!(s.tile(p(-2, 4)).and_then(|t| t.terrain), Some(TerrainId(4)));
694        assert_eq!(s.tile(p(-1, 4)), None);
695        s.tile_mut(p(-2, 4)).unwrap().terrain = None;
696        assert_eq!(s.terrain_at(p(-2, 4)), None);
697        assert!(s.tile_mut(p(-1, 4)).is_none());
698    }
699
700    #[test]
701    fn the_ring_round_the_view_holds_the_terrain_one_tile_out() {
702        let mut s = MapScene::<Piece>::new(p(5, -2), (3, 2));
703        assert_eq!((rim_len((3, 2)), rim_len((0, 0))), (14, 4));
704        assert_eq!((rim_len((-3, 2)), rim_len((3, -2))), (8, 10));
705        // Row above, the ends of each row, row below: each place once.
706        let ring = [
707            (-1, -1),
708            (0, -1),
709            (1, -1),
710            (2, -1),
711            (3, -1),
712            (-1, 0),
713            (3, 0),
714            (-1, 1),
715            (3, 1),
716            (-1, 2),
717            (0, 2),
718            (1, 2),
719            (2, 2),
720            (3, 2),
721        ];
722        for (i, (dx, dy)) in ring.into_iter().enumerate() {
723            assert_eq!(s.rim_index(dx, dy), Some(i), "({dx}, {dy})");
724        }
725        // Inside the view, or further out: not in the ring. Further out
726        // along one of the ring's own rows or columns too.
727        let outside = [
728            (0, 0),
729            (2, 1),
730            (-2, 0),
731            (4, 0),
732            (0, -2),
733            (0, 3),
734            (-2, -1),
735            (4, -1),
736            (4, 2),
737            (-1, 3),
738            (3, -2),
739        ];
740        for (dx, dy) in outside {
741            assert_eq!(s.rim_index(dx, dy), None, "({dx}, {dy})");
742            assert_eq!(s.terrain_near(dx, dy), None, "({dx}, {dy})");
743        }
744        // The terrain of each map position: its column and row in one id.
745        let id = |pos: Pos| u16::try_from(10 * pos.x + pos.y + 2).ok().map(TerrainId);
746        s.set_terrain(|pos| id(pos).filter(|_| pos != p(6, -1) && pos != p(8, 0)));
747        assert_eq!(s.terrain_at(p(5, -2)), Some(TerrainId(50)));
748        assert_eq!(s.terrain_at(p(7, -1)), Some(TerrainId(71)));
749        // Off the map, in the view and in the ring.
750        assert_eq!(s.terrain_at(p(6, -1)), None);
751        assert_eq!(s.terrain_near(1, 1), None);
752        assert_eq!(s.terrain_near(3, 2), None);
753        for (dx, dy) in ring {
754            let pos = p(5 + dx, -2 + dy);
755            let expect = id(pos).filter(|_| pos != p(8, 0));
756            assert_eq!(s.terrain_near(dx, dy), expect, "({dx}, {dy})");
757        }
758        assert_eq!(s.terrain_near(0, 0), Some(TerrainId(50)));
759        assert_eq!(s.terrain_near(2, 1), Some(TerrainId(71)));
760        assert_eq!(s.terrain_near(-1, -1), Some(TerrainId(39)));
761        assert_eq!(s.terrain_near(3, 1), Some(TerrainId(81)));
762        // A ring short of tiles (not built by `new`) has none there.
763        s.rim.truncate(3);
764        assert_eq!(s.terrain_near(1, -1), Some(TerrainId(59)));
765        assert_eq!(s.terrain_near(2, -1), None);
766        s.set_terrain(|_| Some(TerrainId(1)));
767        assert_eq!(s.rim, vec![Some(TerrainId(1)); 3]);
768    }
769
770    #[test]
771    fn the_map_goes_on_past_the_sides_the_view_leaves_map_beyond() {
772        let edges = |left, right, up, down| MapEdges {
773            x: Ends {
774                before: left,
775                after: right,
776            },
777            y: Ends {
778                before: up,
779                after: down,
780            },
781        };
782        // A map 20 × 4 seen 8 × 4 at a time: at its left edge, in the
783        // middle and at its right edge.
784        let wide = |x| MapEdges::of_view(p(x, 0), (8, 4), (20, 4));
785        assert_eq!(wide(0), edges(false, true, false, false));
786        assert_eq!(wide(6), edges(true, true, false, false));
787        assert_eq!(wide(12), edges(true, false, false, false));
788        // The same down a map 5 × 30 seen 5 × 10 at a time.
789        let tall = |y| MapEdges::of_view(p(0, y), (5, 10), (5, 30));
790        assert_eq!(tall(0), edges(false, false, false, true));
791        assert_eq!(tall(10), edges(false, false, true, true));
792        assert_eq!(tall(20), edges(false, false, true, false));
793        // Both ways at once.
794        let both = MapEdges::of_view(p(3, 2), (4, 4), (10, 10));
795        assert_eq!(both, edges(true, true, true, true));
796        // A map smaller than the view, centred in it: none.
797        let small = MapEdges::of_view(p(-3, -2), (12, 8), (6, 4));
798        assert_eq!(small, MapEdges::NONE);
799        assert!(!small.any());
800        assert_eq!(MapEdges::default(), MapEdges::NONE);
801        // Far out, nothing overflows; a negative size is an empty view.
802        let far = MapEdges::of_view(p(i32::MAX, i32::MAX), (i32::MAX, i32::MAX), (1, 1));
803        assert_eq!(far, edges(true, false, true, false));
804        let empty = MapEdges::of_view(p(0, 0), (-4, -4), (1, 1));
805        assert_eq!(empty, edges(false, true, false, true));
806        // Each side counts towards any, and is named.
807        for one in [
808            edges(true, false, false, false),
809            edges(false, true, false, false),
810            edges(false, false, true, false),
811            edges(false, false, false, true),
812        ] {
813            assert!(one.any(), "{one:?}");
814            assert_eq!(one.names().len(), 1, "{one:?}");
815        }
816        let all = edges(true, true, true, true).names();
817        assert_eq!(all, ["left", "right", "up", "down"]);
818    }
819
820    #[test]
821    fn the_text_lists_the_sides_the_map_goes_on_past_only_when_there_are_some() {
822        let mut s = MapScene::<Piece>::new(p(0, -1), (2, 1));
823        let plain = s.to_text(&Terrain);
824        assert!(!plain.contains("more"), "{plain}");
825        // A map that fits: no line.
826        s.more = MapEdges::of_view(p(0, 0), s.size, (2, 1));
827        assert_eq!(s.to_text(&Terrain), plain);
828        let after = Ends {
829            before: false,
830            after: true,
831        };
832        s.more = MapEdges { x: after, y: after };
833        assert_eq!(
834            s.to_text(&Terrain),
835            "origin (0,-1) size 2x1\nmore right down\n  -1: -*2\nunits 0\ncursor -\npath -\n"
836        );
837    }
838
839    #[test]
840    fn the_text_names_a_look_other_than_the_usual_one() {
841        let mut s = MapScene::<Piece>::new(p(0, -1), (2, 1));
842        let text = s.to_text(&Terrain);
843        assert!(text.starts_with("origin (0,-1) size 2x1\n"), "{text}");
844        s.look.tiles = "indoor".to_owned();
845        let text = s.to_text(&Terrain);
846        let first = "origin (0,-1) size 2x1 look indoor\n";
847        assert!(text.starts_with(first), "{text}");
848        // A light other than noon, after it or alone.
849        s.look.light = "dusk".to_owned();
850        let text = s.to_text(&Terrain);
851        let first = "origin (0,-1) size 2x1 look indoor light dusk\n";
852        assert!(text.starts_with(first), "{text}");
853        s.look.tiles = "outdoor".to_owned();
854        let text = s.to_text(&Terrain);
855        assert!(
856            text.starts_with("origin (0,-1) size 2x1 light dusk\n"),
857            "{text}"
858        );
859    }
860
861    #[test]
862    fn tints_go_on_visible_tiles_in_the_order_laid_on() {
863        let mut s = MapScene::<Piece>::new(p(1, 1), (3, 2));
864        s.tint([p(1, 1), p(3, 2), p(0, 0), p(4, 1)], DANGER);
865        s.tint([p(3, 2), p(2, 1)], MOVE);
866        assert_eq!(s.tints_at(p(1, 1)), [DANGER]);
867        assert_eq!(s.tints_at(p(3, 2)), [DANGER, MOVE]);
868        let found = s.tints_at(p(2, 2));
869        assert!(found.is_empty(), "{found:?}");
870        let found = s.tints_at(p(0, 0));
871        assert!(found.is_empty(), "{found:?}");
872        assert_eq!(s.tinted(DANGER), [p(1, 1), p(3, 2)]);
873        assert_eq!(s.tinted(MOVE), [p(2, 1), p(3, 2)]);
874        let found = s.tinted(ATTACK);
875        assert!(found.is_empty(), "{found:?}");
876        // Each layer holds only its visible tiles, as given.
877        let layer = |kind, tiles: &[Pos]| RangeLayer {
878            kind,
879            tiles: tiles.to_vec(),
880        };
881        assert_eq!(
882            s.ranges,
883            [
884                layer(DANGER, &[p(1, 1), p(3, 2)]),
885                layer(MOVE, &[p(3, 2), p(2, 1)]),
886            ]
887        );
888        // A range with no visible tile adds nothing.
889        let before = s.clone();
890        s.tint([p(0, 0), p(9, 9)], HEAL);
891        s.tint([], HEAL);
892        assert_eq!(s, before);
893        // A tile given twice is in the range twice; a range laid twice
894        // tints twice.
895        s.tint([p(2, 2), p(2, 2)], HEAL);
896        s.tint([p(2, 2)], DANGER);
897        assert_eq!(s.tints_at(p(2, 2)), [HEAL, HEAL, DANGER]);
898        assert_eq!(s.tinted(HEAL), [p(2, 2)]);
899        // Row by row, whatever the order laid on.
900        assert_eq!(s.tinted(DANGER), [p(1, 1), p(2, 2), p(3, 2)]);
901    }
902
903    #[test]
904    fn tints_are_read_by_the_tiles_place_in_the_view() {
905        let mut s = MapScene::<Piece>::new(p(1, 1), (3, 2));
906        s.tint([p(3, 1), p(1, 2)], MOVE);
907        s.tint([p(1, 2)], HEAL);
908        let tints = s.tints();
909        assert_eq!(tints.at(2, 0), [MOVE]);
910        assert_eq!(tints.at(0, 1), [MOVE, HEAL]);
911        let found = tints.at(0, 0);
912        assert!(found.is_empty(), "{found:?}");
913        // Outside the view: none, never the next row's.
914        let far = [
915            (3, 0),
916            (-1, 1),
917            (0, 2),
918            (0, -1),
919            (i32::MAX, 0),
920            (0, i32::MAX),
921            (0, i32::MIN),
922        ];
923        for (dx, dy) in far {
924            assert!(tints.at(dx, dy).is_empty(), "({dx}, {dy})");
925        }
926        // A layer naming a tile outside the view (not laid by `tint`):
927        // not on any tile.
928        s.ranges.push(RangeLayer {
929            kind: DANGER,
930            tiles: vec![p(0, 1), p(4, 1)],
931        });
932        assert_eq!(s.tints(), tints);
933        // A scene short of tiles: those it lacks are in no range.
934        s.tiles.truncate(3);
935        let short = s.tints();
936        assert_eq!(short.at(2, 0), [MOVE]);
937        let found = short.at(0, 1);
938        assert!(found.is_empty(), "{found:?}");
939        s.tint([p(1, 2)], DANGER);
940        let found = s.tints_at(p(1, 2));
941        assert!(found.is_empty(), "{found:?}");
942    }
943
944    #[test]
945    fn a_ranges_mark_is_its_names_first_letter() {
946        assert_eq!(MOVE.mark(), 'm');
947        assert_eq!(RangeName("Zone").mark(), 'Z');
948        assert_eq!(RangeName("").mark(), '?');
949    }
950
951    #[test]
952    fn only_units_on_visible_tiles_are_kept() {
953        let mut s = MapScene::new(p(1, 1), (3, 2));
954        s.push_unit(piece(4, p(3, 2)));
955        s.push_unit(piece(5, p(4, 2)));
956        s.push_unit(piece(6, p(1, 1)));
957        let ids: Vec<u32> = s.units.iter().map(|u| u.id).collect();
958        assert_eq!(ids, [4, 6]);
959        assert_eq!(s.unit(6).map(|u| u.pos), Some(p(1, 1)));
960        assert_eq!(s.unit(5), None);
961    }
962
963    #[test]
964    fn tiles_answer_what_is_on_them() {
965        let mut s = MapScene::new(p(1, 1), (3, 2));
966        s.tile_mut(p(2, 1)).unwrap().terrain = Some(TerrainId(4));
967        s.tint([p(2, 1)], DANGER);
968        s.tint([p(2, 1)], MOVE);
969        s.push_unit(piece(4, p(2, 1)));
970        s.push_unit(piece(6, p(3, 2)));
971        s.push_unit(piece(7, p(3, 2)));
972        assert_eq!(s.unit_at(p(2, 1)).map(|u| u.id), Some(4));
973        assert_eq!(s.unit_at(p(3, 2)).map(|u| u.id), Some(7));
974        assert_eq!(s.unit_at(p(1, 1)), None);
975        assert_eq!(s.tints_at(p(2, 1)), [DANGER, MOVE]);
976        let found = s.tints_at(p(1, 1));
977        assert!(found.is_empty(), "{found:?}");
978        let found = s.tints_at(p(0, 0));
979        assert!(found.is_empty(), "{found:?}");
980        assert_eq!(s.terrain_at(p(2, 1)), Some(TerrainId(4)));
981        assert_eq!(s.terrain_at(p(1, 1)), None);
982        assert_eq!(s.terrain_at(p(9, 9)), None);
983        assert_eq!(s.cursor_tile(), None);
984        s.cursor = Some(CursorView {
985            pos: p(3, 2),
986            brightness: 1.0,
987            style: CursorStyle::Corners,
988        });
989        assert_eq!(s.cursor_tile(), Some(p(3, 2)));
990    }
991
992    #[test]
993    fn to_text_lists_tiles_units_cursor_and_path() {
994        let mut s = MapScene::new(p(-1, 2), (5, 3));
995        for pos in [p(0, 2), p(1, 2), p(2, 2), p(0, 3), p(1, 3)] {
996            s.tile_mut(pos).unwrap().terrain = Some(GRASS);
997        }
998        s.tile_mut(p(3, 2)).unwrap().terrain = Some(SAND);
999        s.tile_mut(p(2, 3)).unwrap().terrain = Some(TerrainId(999));
1000        s.tint([p(1, 2), p(2, 2)], MOVE);
1001        s.tint([p(3, 2)], DANGER);
1002        s.tint([p(3, 2)], ATTACK);
1003        s.tint([p(0, 3)], HEAL);
1004        s.tile_mut(p(1, 3)).unwrap().flashes.push(0.5);
1005        s.tile_mut(p(0, 2)).unwrap().becomes = Some(WATER);
1006        s.tile_mut(p(2, 2)).unwrap().becomes = Some(TerrainId(998));
1007        s.push_unit(piece(4, p(0, 2)));
1008        s.push_unit(Piece::new(1, 'a', p(1, 3)));
1009        s.cursor = Some(CursorView {
1010            pos: p(1, 3),
1011            brightness: 0.756,
1012            style: CursorStyle::Corners,
1013        });
1014        s.path = vec![p(1, 3), p(1, 2)];
1015        assert_eq!(
1016            s.to_text(&Terrain),
1017            "origin (-1,2) size 5x3\n\
1018             \x20  2: - grass>water grass+m grass+m>?998 sand+da\n\
1019             \x20  3: - grass+h grass! ?999 -\n\
1020             \x20  4: -*5\n\
1021             units 2\n\
1022             \x20 #4 k (0,2)\n\
1023             \x20 #1 a (1,3)\n\
1024             cursor (1,3) corners 0.76\n\
1025             path (1,3) (1,2)\n"
1026        );
1027        // A threatened tile adds a line: the tile, then the threats'.
1028        s.threat = Some(ThreatView {
1029            tile: p(1, 2),
1030            enemies: vec![p(0, 2), p(9, -4)],
1031        });
1032        let text = s.to_text(&Terrain);
1033        let end = "path (1,3) (1,2)\nthreat (1,2) from (0,2) (9,-4)\n";
1034        assert!(text.ends_with(end), "{text}");
1035    }
1036
1037    #[test]
1038    fn to_text_of_an_empty_scene_and_every_cursor_style() {
1039        let mut s = MapScene::<Piece>::new(p(0, -1), (2, 1));
1040        assert_eq!(
1041            s.to_text(&Terrain),
1042            "origin (0,-1) size 2x1\n  -1: -*2\nunits 0\ncursor -\npath -\n"
1043        );
1044        // A scene short of tiles marks them.
1045        s.tiles.truncate(1);
1046        assert!(s.to_text(&Terrain).contains("  -1: - !\n"));
1047        let styles = [
1048            (CursorStyle::Corners, "corners"),
1049            (CursorStyle::LargeCorners, "large-corners"),
1050            (CursorStyle::TileGlow, "glow"),
1051        ];
1052        for (style, name) in styles {
1053            assert_eq!(style.name(), name);
1054            s.cursor = Some(CursorView {
1055                pos: p(1, -1),
1056                brightness: 1.0,
1057                style,
1058            });
1059            let line = format!("cursor (1,-1) {name} 1.00\n");
1060            assert!(s.to_text(&Terrain).contains(&line), "{name}");
1061        }
1062        assert_eq!(CursorStyle::default(), CursorStyle::Corners);
1063    }
1064
1065    #[test]
1066    fn a_new_cursor_is_the_literal() {
1067        assert_eq!(
1068            CursorView::new(Pos::new(2, 3), 0.5, CursorStyle::TileGlow),
1069            CursorView {
1070                pos: Pos::new(2, 3),
1071                brightness: 0.5,
1072                style: CursorStyle::TileGlow,
1073            }
1074        );
1075    }
1076}