Skip to main content

shuyi_ui/grid/
sprite.rs

1//! The sprite skin (game ADR-0038, game ADR-0049, game ADR-0052): a grid map's
2//! ground painted from a tileset's terrain tiles ([`TerrainTiles`]).
3//!
4//! - **With terrain tiles** it paints the whole ground: a tile is the
5//!   tiles' `tile_px` times the zoom, a size known only here, behind
6//!   [`SpriteSkin::tile_size`]. The ground is each tile's own picture and
7//!   the layers over them, in the look the map asks for and in its light
8//!   (see `ground`). The cursor is corner marks with a dark edge, inside
9//!   its tile (game ticket 0444).
10//! - **Without** them the ground, the ranges and the cursor are the glyph
11//!   skin's, on its tiles: for a game whose units are pictures on glyph
12//!   ground (its unit painter takes the terrain's glyphs off a unit's
13//!   tile).
14//!
15//! Either way the path's line goes under the units and its arrowhead over
16//! them, then the unit painter's [`paint_over`](UnitPainter::paint_over)
17//! under the cursor, and the threat arrows start in the middle of a unit's
18//! tile, over its picture.
19
20mod ground;
21
22use crate::prelude::*;
23use alloc::rc::Rc;
24
25use shuyi_assets::image::{ImageRect, Picture};
26use shuyi_assets::tileset::TerrainTiles;
27
28use super::glyph::{self, GlyphSkin};
29use super::layout::{Grid, px_rect};
30use super::path;
31use super::scene::{CursorStyle, MapScene};
32use super::skin::{GridSkin, Looks, UnitPainter, WalkSpeeds};
33use super::threat;
34use crate::frame::glyph_buffer::{GlyphBuffer, Layer, Overlay, PxRect, Rect, Sprite};
35
36/// A grid map's ground painted from terrain tiles.
37#[derive(Debug, Clone, PartialEq)]
38pub struct SpriteSkin {
39    /// The tiles; `None` = the glyph skin's ground.
40    terrain: Option<TerrainTiles>,
41    /// How big the art is drawn, when the tiles paint the ground: at
42    /// least 1.
43    zoom: i32,
44    /// The zooms a player may pick from, smallest first: none unless
45    /// given ([`with_zooms`](Self::with_zooms)).
46    zooms: Vec<i32>,
47    /// The glyph skin: its colors, and its ground without terrain tiles.
48    glyph: GlyphSkin,
49    /// How fast a walk is shown.
50    walk: WalkSpeeds,
51}
52
53impl SpriteSkin {
54    /// The skin of `terrain` at the art's own size, with `glyph`'s colors
55    /// (and its ground, without terrain tiles), showing a walk at `walk`.
56    pub const fn new(terrain: Option<TerrainTiles>, glyph: GlyphSkin, walk: WalkSpeeds) -> Self {
57        Self {
58            terrain,
59            zoom: 1,
60            zooms: Vec::new(),
61            glyph,
62            walk,
63        }
64    }
65
66    /// The same skin, offering `levels` as its
67    /// [`zoom_levels`](GridSkin::zoom_levels) when its terrain tiles paint
68    /// the ground: sorted, each once, those below 1 left out.
69    #[must_use]
70    pub fn with_zooms(mut self, levels: &[i32]) -> Self {
71        let mut zooms: Vec<i32> = levels.iter().copied().filter(|&z| z >= 1).collect();
72        zooms.sort_unstable();
73        zooms.dedup();
74        self.zooms = zooms;
75        self
76    }
77
78    /// The zooms it offers: its levels with terrain tiles, none on the
79    /// glyph skin's tiles (font cells).
80    fn levels(&self) -> &[i32] {
81        match self.terrain {
82            Some(_) => &self.zooms,
83            None => &[],
84        }
85    }
86
87    /// The same skin drawn `zoom` times the art's size (at least once).
88    #[must_use]
89    pub fn zoomed(mut self, zoom: i32) -> Self {
90        self.zoom = zoom.max(1);
91        self
92    }
93
94    /// Its terrain tiles, if it has them.
95    pub const fn terrain(&self) -> Option<&TerrainTiles> {
96        self.terrain.as_ref()
97    }
98
99    /// The zoom it was given: console pixels per pixel of the art, when it
100    /// has terrain tiles.
101    pub const fn zoom(&self) -> i32 {
102        self.zoom
103    }
104
105    /// Console pixels per pixel of the art: the zoom with terrain tiles;
106    /// always 1 on the glyph skin's tiles, which are font cells.
107    fn scale(&self) -> i32 {
108        match self.terrain {
109            Some(_) => self.zoom,
110            None => 1,
111        }
112    }
113
114    /// A map tile's size on screen, across × down, in console pixels: the
115    /// tiles' times the zoom, or the glyph skin's without terrain tiles.
116    /// Everything that depends on the tile size goes through this.
117    pub fn tile_size(&self) -> (i32, i32) {
118        let zoomed = |px: u32| side(px).saturating_mul(self.scale());
119        match &self.terrain {
120            Some(terrain) => (zoomed(terrain.tile_px.0), zoomed(terrain.tile_px.1)),
121            None => glyph::tile_px(),
122        }
123    }
124
125    /// The area's pixels divided by the tile size, rounded down; without
126    /// terrain tiles, what the glyph skin fits.
127    pub fn view_tiles(&self, area: Rect) -> (i32, i32) {
128        if self.terrain.is_none() {
129            return glyph::view_tiles(area);
130        }
131        let px = px_rect(area);
132        let (w, h) = self.tile_size();
133        (px.w.max(0) / w, px.h.max(0) / h)
134    }
135
136    /// Where `scene`'s tiles go in `area`: as many as fit, the pixels left
137    /// over split evenly around them; without terrain tiles, where the
138    /// glyph skin puts them.
139    pub fn grid<U>(&self, scene: &MapScene<U>, area: Rect) -> Grid {
140        if self.terrain.is_none() {
141            return glyph::Layout::new(scene, area).grid();
142        }
143        let px = px_rect(area);
144        let tile = self.tile_size();
145        let fit = self.view_tiles(area);
146        let margin = |len: i32, n: i32, side: i32| (len.max(0) - n * side) / 2;
147        Grid::top_down(
148            scene.origin,
149            (
150                px.x + margin(px.w, fit.0, tile.0),
151                px.y + margin(px.h, fit.1, tile.1),
152            ),
153            tile,
154            (scene.size.0.clamp(0, fit.0), scene.size.1.clamp(0, fit.1)),
155            self.scale(),
156        )
157    }
158
159    /// The cursor: corner marks in the cursor's color with a dark edge
160    /// round them, inside its tile, in every style (pictures have no tile
161    /// glow: game ticket 0444). The color pulses; the edge doesn't.
162    fn paint_own_cursor<U>(
163        &self,
164        looks: &Looks,
165        scene: &MapScene<U>,
166        grid: &Grid,
167        buf: &mut GlyphBuffer,
168    ) {
169        if let Some(c) = &scene.cursor
170            && let Some(tile) = grid.rect(c.pos)
171        {
172            let edge = looks.dark();
173            let color = looks.cursor(&self.glyph.colors).scale(c.brightness);
174            let marks = CursorMarks::of(c.style, grid.zoom());
175            let art = grid.art_tile();
176            let edges = corner_marks(art, 0, marks.arm + 2, marks.thick + 2);
177            let arms = corner_marks(art, 1, marks.arm, marks.thick);
178            for (rects, color) in [(edges, edge), (arms, color)] {
179                for r in rects {
180                    let r = grid.zoomed((tile.x, tile.y), r);
181                    buf.add_overlay(Overlay::new(r, color, Layer::Over));
182                }
183            }
184        }
185    }
186}
187
188/// The size of the cursor's corner marks on picture tiles, in pixels of
189/// the art (Nick, 2026-10-13, game ticket 0444).
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191struct CursorMarks {
192    /// How long each arm of a corner is.
193    arm: i32,
194    /// How thick.
195    thick: i32,
196}
197
198impl CursorMarks {
199    /// At the art's own size, thick marks (2 px, 4 px long); zoomed in,
200    /// thin ones (1 px of the art, 3 long), which the zoom thickens.
201    /// Large corners are a pixel longer; the glow style has no look of
202    /// its own here and is the corners.
203    const fn of(style: CursorStyle, zoom: i32) -> Self {
204        let (arm, thick) = if zoom == 1 { (4, 2) } else { (3, 1) };
205        let longer = match style {
206            CursorStyle::Corners | CursorStyle::TileGlow => 0,
207            CursorStyle::LargeCorners => 1,
208        };
209        Self {
210            arm: arm + longer,
211            thick,
212        }
213    }
214}
215
216impl<U> GridSkin<U> for SpriteSkin {
217    /// `"sprite"` when the tiles paint the ground, `"sprite_units"` on the
218    /// glyph skin's ground.
219    fn name(&self) -> &'static str {
220        match self.terrain {
221            Some(_) => "sprite",
222            None => "sprite_units",
223        }
224    }
225
226    fn tile_size(&self) -> (i32, i32) {
227        SpriteSkin::tile_size(self)
228    }
229
230    fn view_tiles(&self, area: Rect) -> (i32, i32) {
231        SpriteSkin::view_tiles(self, area)
232    }
233
234    fn grid(&self, scene: &MapScene<U>, area: Rect) -> Grid {
235        SpriteSkin::grid(self, scene, area)
236    }
237
238    fn walk(&self) -> WalkSpeeds {
239        self.walk
240    }
241
242    /// The levels it was given ([`SpriteSkin::with_zooms`]), with terrain
243    /// tiles; none without.
244    fn zoom_levels(&self) -> &[i32] {
245        self.levels()
246    }
247
248    fn zoom(&self) -> Option<i32> {
249        (!self.levels().is_empty()).then_some(self.zoom)
250    }
251
252    fn at_zoom(&self, zoom: i32) -> Option<Rc<dyn GridSkin<U>>> {
253        if !self.levels().contains(&zoom) {
254            return None;
255        }
256        Some(Rc::new(self.clone().zoomed(zoom)))
257    }
258
259    /// The ground (terrain tinted by flashes and ranges), what lies under
260    /// the units ([`UnitPainter::paint_under`]), the path's line, the
261    /// units, the path's arrowhead, what lies over them
262    /// ([`UnitPainter::paint_over`]), then the cursor; over them all, the
263    /// threat arrows and marks.
264    fn paint(
265        &self,
266        looks: &Looks,
267        scene: &MapScene<U>,
268        area: Rect,
269        units: &dyn UnitPainter<U>,
270        buf: &mut GlyphBuffer,
271    ) {
272        let grid = SpriteSkin::grid(self, scene, area);
273        let layout = glyph::Layout::new(scene, area);
274        match &self.terrain {
275            Some(terrain) => ground::paint(looks, terrain, scene, &grid, buf),
276            None => glyph::draw_tiles(looks, buf, scene, &layout),
277        }
278        units.paint_under(scene, &grid, buf);
279        let color = looks.path(&self.glyph.colors);
280        let (line, arrowhead): (Vec<Overlay>, Vec<Overlay>) =
281            path::path_overlays(&scene.path, &grid, color)
282                .into_iter()
283                .partition(|o| o.layer == Layer::Under);
284        for overlay in line {
285            buf.add_overlay(overlay);
286        }
287        units.paint(scene, &grid, buf);
288        for overlay in arrowhead {
289            buf.add_overlay(overlay);
290        }
291        units.paint_over(scene, &grid, buf);
292        if self.terrain.is_some() {
293            self.paint_own_cursor(looks, scene, &grid, buf);
294        } else {
295            self.glyph.draw_cursor(looks, buf, scene, &layout);
296        }
297        let colors = looks.threat(&self.glyph.colors);
298        threat::paint(scene, &grid, threat::Start::Middle, colors, buf);
299    }
300}
301
302/// A size of the art as console pixels.
303pub fn side(px: u32) -> i32 {
304    i32::try_from(px).unwrap_or(i32::MAX)
305}
306
307/// `rect` of an image as a sprite's `src`.
308pub fn src_rect(rect: ImageRect) -> PxRect {
309    Rect::new(side(rect.x), side(rect.y), side(rect.w), side(rect.h))
310}
311
312/// A solid sprite of the tile `picture` on the pixels `dest`.
313pub fn tile_sprite(picture: Picture, dest: PxRect) -> Sprite {
314    Sprite::new(picture.image, src_rect(picture.rect), dest, Layer::Under)
315}
316
317/// The eight arms, each `arm` px long and `thick` px thick, of corner
318/// marks `inset` px inside the corners of the pixels `tile`.
319fn corner_marks(tile: PxRect, inset: i32, arm: i32, thick: i32) -> [Rect; 8] {
320    let (left, top) = (tile.x + inset, tile.y + inset);
321    let (right, bottom) = (tile.x + tile.w - inset, tile.y + tile.h - inset);
322    let across = |x0, y0| Rect::new(x0, y0, arm, thick);
323    let down = |x0, y0| Rect::new(x0, y0, thick, arm);
324    [
325        across(left, top),
326        down(left, top),
327        across(right - arm, top),
328        down(right - thick, top),
329        across(left, bottom - thick),
330        down(left, bottom - arm),
331        across(right - arm, bottom - thick),
332        down(right - thick, bottom - arm),
333    ]
334}
335
336#[cfg(test)]
337pub(crate) mod tests;