Skip to main content

shuyi_assets/
ink.rs

1//! One-bit inks and their palette (the Playdate's, game ADR-0074): a palette
2//! file of inks gives each colour name an [`Ink`] instead of an RGB value.
3//! It loads as a [`PaletteDef`] like any other, each colour being its
4//! ink's reserved [`code`](Ink::code), so screens and skins draw with it
5//! unchanged and a one-bit renderer reads the codes back as inks.
6use crate::prelude::*;
7
8use alloc::collections::BTreeMap;
9
10use crate::error::ContentError;
11use crate::palette::{PaletteDef, line_of_key};
12use crate::ron::parse_ron;
13
14/// How a one-bit pixel is coloured (game ADR-0074). The patterns are fixed to the
15/// screen's pixels, so neighbouring tiles line up.
16#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
17pub enum Ink {
18    /// White: the ground.
19    White,
20    /// Black.
21    Black,
22    /// Black, and on a glyph a white edge one pixel wide around it, so it
23    /// reads over a pattern. Anything else in it is plain black.
24    BlackEdged,
25    /// A 50% checker (enemies' tiles).
26    Grey,
27    /// Light dots, one pixel in eight (the move range).
28    Dots,
29    /// Diagonal lines, one pixel in four (the attack range).
30    Diagonals,
31    /// Dashes two pixels long (the danger zone's edge): a 1-pixel line of
32    /// it is a dashed line.
33    Dashes,
34    /// Flips the pixels under it (the cursor's corner marks). Only a
35    /// rectangle item flips; anything else in it is plain black.
36    Invert,
37}
38
39impl Ink {
40    /// Every ink.
41    pub const ALL: [Self; 8] = [
42        Self::White,
43        Self::Black,
44        Self::BlackEdged,
45        Self::Grey,
46        Self::Dots,
47        Self::Diagonals,
48        Self::Dashes,
49        Self::Invert,
50    ];
51
52    /// Its name in a palette file of inks.
53    pub const fn name(self) -> &'static str {
54        match self {
55            Self::White => "white",
56            Self::Black => "black",
57            Self::BlackEdged => "black_edged",
58            Self::Grey => "grey",
59            Self::Dots => "dots",
60            Self::Diagonals => "diagonals",
61            Self::Dashes => "dashes",
62            Self::Invert => "invert",
63        }
64    }
65
66    /// The ink named `name`.
67    pub fn parse(name: &str) -> Option<Self> {
68        Self::ALL.into_iter().find(|ink| ink.name() == name)
69    }
70
71    /// The colour reserved for it: a cell or item in this colour is drawn
72    /// in this ink. Each is about as dark as the ink, so the same frame
73    /// drawn in colour still reads.
74    pub const fn code(self) -> [u8; 3] {
75        match self {
76            Self::White => [0xff, 0xff, 0xff],
77            Self::Black => [0x00, 0x00, 0x00],
78            Self::BlackEdged => [0x01, 0x01, 0x01],
79            Self::Grey => [0x80, 0x80, 0x80],
80            Self::Dots => [0xe0, 0xe0, 0xe0],
81            Self::Diagonals => [0xc0, 0xc0, 0xc0],
82            Self::Dashes => [0x40, 0x40, 0x40],
83            Self::Invert => [0xff, 0x00, 0xff],
84        }
85    }
86
87    /// The ink whose [`code`](Self::code) is `rgb`, if any.
88    pub fn from_code(rgb: [u8; 3]) -> Option<Self> {
89        Self::ALL.into_iter().find(|ink| ink.code() == rgb)
90    }
91}
92
93impl PaletteDef {
94    /// Parses palette `source` of ink names (errors attributed to `file`)
95    /// into each ink's code. Reports every unknown ink and every colour of
96    /// `base` it lacks.
97    pub fn from_ink_source(
98        file: &str,
99        source: &str,
100        base: &PaletteDef,
101    ) -> Result<Self, Vec<ContentError>> {
102        let raw: BTreeMap<String, String> = parse_ron(file, source).map_err(|e| vec![e])?;
103        let mut errors = Vec::new();
104        let mut colors = BTreeMap::new();
105        for (name, value) in &raw {
106            if let Some(ink) = Ink::parse(value) {
107                colors.insert(name.clone(), ink.code());
108            } else {
109                let inks: Vec<&str> = Ink::ALL.iter().map(|ink| ink.name()).collect();
110                let mut e = ContentError::new(
111                    file,
112                    format!(
113                        "colour \"{name}\": no ink \"{value}\"; the inks are {}",
114                        inks.join(", ")
115                    ),
116                );
117                if let Some(line) = line_of_key(source, name) {
118                    e = e.at(line, None);
119                }
120                errors.push(e);
121            }
122        }
123        for name in base.colors.keys().filter(|n| !raw.contains_key(*n)) {
124            errors.push(ContentError::new(
125                file,
126                format!("missing colour \"{name}\""),
127            ));
128        }
129        if errors.is_empty() {
130            Ok(Self { colors })
131        } else {
132            Err(errors)
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    fn base(names: &[&str]) -> PaletteDef {
142        PaletteDef {
143            colors: names.iter().map(|&n| (n.to_owned(), [9, 9, 9])).collect(),
144        }
145    }
146
147    #[test]
148    fn every_ink_has_its_own_name_and_code() {
149        for ink in Ink::ALL {
150            assert_eq!(Ink::parse(ink.name()), Some(ink));
151            assert_eq!(Ink::from_code(ink.code()), Some(ink));
152        }
153        assert_eq!(Ink::parse("gray"), None);
154        assert_eq!(Ink::from_code([0x80, 0x80, 0x81]), None);
155        assert_eq!(Ink::Grey.code(), [0x80; 3]);
156        assert_eq!(Ink::Invert.name(), "invert");
157    }
158
159    #[test]
160    fn a_palette_of_inks_loads_as_their_codes() {
161        let src =
162            "{\n    \"text\": \"black\",\n    \"enemy\": \"grey\",\n    \"own\": \"dots\",\n}";
163        let p = PaletteDef::from_ink_source("p.ron", src, &base(&["text", "enemy"]));
164        let p = p.unwrap_or_default();
165        assert_eq!(p.get("text"), Some(Ink::Black.code()));
166        assert_eq!(p.get("enemy"), Some(Ink::Grey.code()));
167        // A name of its own, for the Playdate's skins.
168        assert_eq!(p.get("own"), Some(Ink::Dots.code()));
169        assert_eq!(p.colors.len(), 3);
170    }
171
172    #[test]
173    fn an_unknown_ink_and_a_missing_colour_are_errors() {
174        let src = "{\n    \"text\": \"black\",\n    \"enemy\": \"#808080\",\n}";
175        let errs = PaletteDef::from_ink_source("p.ron", src, &base(&["text", "enemy", "cursor"]))
176            .err()
177            .unwrap_or_default();
178        let shown: Vec<String> = errs.iter().map(ToString::to_string).collect();
179        assert_eq!(shown.len(), 2, "{shown:?}");
180        assert_eq!(errs[0].line, Some(3));
181        assert!(
182            shown[0].contains("colour \"enemy\": no ink \"#808080\"; the inks are white, black,"),
183            "{}",
184            shown[0]
185        );
186        assert_eq!(shown[1], "p.ron: missing colour \"cursor\"");
187        assert!(PaletteDef::from_ink_source("p.ron", "nope", &base(&[])).is_err());
188    }
189}