initiative_core/world/npc/
view.rs

1use super::{Age, Gender, NpcData, NpcRelations, Uuid};
2use std::fmt;
3
4pub struct SummaryView<'a>(&'a NpcData);
5
6pub struct DescriptionView<'a>(&'a NpcData);
7
8pub struct DetailsView<'a> {
9    npc: &'a NpcData,
10    uuid: Uuid,
11    relations: NpcRelations,
12}
13
14fn write_summary_details(npc: &NpcData, f: &mut fmt::Formatter) -> fmt::Result {
15    if let Some(age) = npc.age.value() {
16        age.fmt_with_species_ethnicity(npc.species.value(), npc.ethnicity.value(), f)?;
17    } else if let Some(species) = npc.species.value() {
18        write!(f, "{}", species)?;
19    } else if let Some(ethnicity) = npc.ethnicity.value() {
20        write!(f, "{} person", ethnicity)?;
21    } else {
22        write!(f, "person")?;
23    }
24
25    if let Some(gender) = npc.gender.value() {
26        write!(f, ", {}", gender.pronouns())?;
27    }
28
29    Ok(())
30}
31
32impl<'a> SummaryView<'a> {
33    pub fn new(npc: &'a NpcData) -> Self {
34        Self(npc)
35    }
36}
37
38impl<'a> DescriptionView<'a> {
39    pub fn new(npc: &'a NpcData) -> Self {
40        Self(npc)
41    }
42}
43
44impl<'a> DetailsView<'a> {
45    pub fn new(npc: &'a NpcData, uuid: Uuid, relations: NpcRelations) -> Self {
46        Self {
47            npc,
48            uuid,
49            relations,
50        }
51    }
52}
53
54impl fmt::Display for SummaryView<'_> {
55    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
56        let npc = self.0;
57        let has_details = npc.age.is_some()
58            || npc.ethnicity.is_some()
59            || npc.gender.is_some()
60            || npc.species.is_some();
61
62        write!(
63            f,
64            "{} ",
65            match (npc.age.value(), npc.gender.value()) {
66                (Some(Age::Infant), _) => '\u{1f476}',
67                (Some(Age::Child | Age::Adolescent), Some(Gender::Feminine)) => '\u{1f467}',
68                (Some(Age::Child | Age::Adolescent), Some(Gender::Masculine)) => '\u{1f466}',
69                (Some(Age::Child | Age::Adolescent), _) => '\u{1f9d2}',
70                (Some(Age::Elderly | Age::Geriatric), Some(Gender::Feminine)) => '\u{1f475}',
71                (Some(Age::Elderly | Age::Geriatric), Some(Gender::Masculine)) => '\u{1f474}',
72                (Some(Age::Elderly | Age::Geriatric), _) => '\u{1f9d3}',
73                (_, Some(Gender::Feminine)) => '\u{1f469}',
74                (_, Some(Gender::Masculine)) => '\u{1f468}',
75                _ => '\u{1f9d1}',
76            },
77        )?;
78
79        if let Some(name) = npc.name.value() {
80            if has_details {
81                write!(f, "`{}` (", name)?;
82                write_summary_details(npc, f)?;
83                write!(f, ")")?;
84            } else {
85                write!(f, "`{}`", name)?;
86            }
87        } else {
88            write_summary_details(npc, f)?;
89        }
90
91        Ok(())
92    }
93}
94
95impl fmt::Display for DescriptionView<'_> {
96    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
97        write_summary_details(self.0, f)
98    }
99}
100
101impl fmt::Display for DetailsView<'_> {
102    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
103        let Self {
104            npc,
105            uuid,
106            relations,
107        } = self;
108
109        writeln!(f, "<div class=\"thing-box npc\" data-uuid=\"{}\">\n", uuid)?;
110
111        npc.name
112            .value()
113            .map(|name| write!(f, "# {}", name))
114            .unwrap_or_else(|| write!(f, "# Unnamed NPC"))?;
115
116        write!(f, "\n*")?;
117        write_summary_details(npc, f)?;
118        write!(f, "*")?;
119
120        match (npc.species.value(), npc.ethnicity.value()) {
121            (Some(species), Some(ethnicity)) if ethnicity != &species.default_ethnicity() => {
122                write!(f, "\n\n**Species:** {} ({})", species, ethnicity)?
123            }
124            (Some(species), _) => write!(f, "\n\n**Species:** {}", species)?,
125            (None, Some(ethnicity)) => write!(f, "\n\n**Ethnicity:** {}", ethnicity)?,
126            (None, None) => write!(f, "\n\n**Species:** N/A")?,
127        }
128
129        npc.gender
130            .value()
131            .map(|gender| write!(f, "\\\n**Gender:** {}", gender.name()))
132            .transpose()?;
133        npc.age_years
134            .value()
135            .map(|age_years| write!(f, "\\\n**Age:** {} years", age_years))
136            .transpose()?;
137        npc.size
138            .value()
139            .map(|size| write!(f, "\\\n**Size:** {}", size))
140            .transpose()?;
141
142        relations
143            .location
144            .as_ref()
145            .map(|(parent, grandparent)| {
146                if let Some(grandparent) = grandparent {
147                    write!(
148                        f,
149                        "\\\n**Location:** {}, {}",
150                        parent.display_name(),
151                        grandparent.display_name(),
152                    )
153                } else {
154                    write!(f, "\\\n**Location:** {}", parent.display_summary())
155                }
156            })
157            .transpose()?;
158
159        write!(f, "\n\n</div>")?;
160
161        Ok(())
162    }
163}
164
165#[cfg(test)]
166mod test {
167    use super::*;
168    use crate::test_utils as test;
169    use crate::world::npc::{Age, Ethnicity, Gender, Npc, Species};
170
171    const NAME: u8 = 0b1;
172    const AGE: u8 = 0b10;
173    const SPECIES: u8 = 0b100;
174    const GENDER: u8 = 0b1000;
175    const ETHNICITY: u8 = 0b10000;
176
177    #[test]
178    fn summary_view_test() {
179        let (expected, actual): (Vec<String>, Vec<String>) = [
180            "🧑 person",
181            "🧑 `Potato Johnson`",
182            "🧓 elderly person",
183            "🧓 `Potato Johnson` (elderly person)",
184            "🧑 human",
185            "🧑 `Potato Johnson` (human)",
186            "🧓 elderly human",
187            "🧓 `Potato Johnson` (elderly human)",
188            "👨 person, he/him",
189            "👨 `Potato Johnson` (person, he/him)",
190            "👴 elderly person, he/him",
191            "👴 `Potato Johnson` (elderly person, he/him)",
192            "👨 human, he/him",
193            "👨 `Potato Johnson` (human, he/him)",
194            "👴 elderly human, he/him",
195            "👴 `Potato Johnson` (elderly human, he/him)",
196            "🧑 elvish person",
197            "🧑 `Potato Johnson` (elvish person)",
198            "🧓 elderly elvish person",
199            "🧓 `Potato Johnson` (elderly elvish person)",
200            "🧑 human",
201            "🧑 `Potato Johnson` (human)",
202            "🧓 elderly human",
203            "🧓 `Potato Johnson` (elderly human)",
204            "👨 elvish person, he/him",
205            "👨 `Potato Johnson` (elvish person, he/him)",
206            "👴 elderly elvish person, he/him",
207            "👴 `Potato Johnson` (elderly elvish person, he/him)",
208            "👨 human, he/him",
209            "👨 `Potato Johnson` (human, he/him)",
210            "👴 elderly human, he/him",
211            "👴 `Potato Johnson` (elderly human, he/him)",
212        ]
213        .iter()
214        .enumerate()
215        .map(|(i, s)| {
216            (
217                s.to_string(),
218                gen_npc(i as u8).display_summary().to_string(),
219            )
220        })
221        .unzip();
222
223        assert_eq!(expected, actual);
224    }
225
226    #[test]
227    fn details_view_test_filled() {
228        assert_eq!(
229            r#"<div class="thing-box npc" data-uuid="00000000-0000-0000-0000-000000000011">
230
231# Odysseus
232*middle-aged human, he/him*
233
234**Species:** human\
235**Gender:** masculine\
236**Age:** 50 years\
237**Size:** 6'0", 180 lbs (medium)
238
239</div>"#,
240            test::npc::odysseus::data()
241                .display_details(test::npc::odysseus::UUID, NpcRelations::default())
242                .to_string(),
243        );
244    }
245
246    #[test]
247    fn details_view_test_species_ethnicity() {
248        assert_eq!(
249            r#"<div class="thing-box npc" data-uuid="00000000-0000-0000-0000-000000000004">
250
251# Unnamed NPC
252*human*
253
254**Species:** human
255
256</div>"#,
257            gen_npc(SPECIES)
258                .display_details(NpcRelations::default())
259                .to_string(),
260        );
261        assert_eq!(
262            r#"<div class="thing-box npc" data-uuid="00000000-0000-0000-0000-000000000010">
263
264# Unnamed NPC
265*elvish person*
266
267**Ethnicity:** elvish
268
269</div>"#,
270            gen_npc(ETHNICITY)
271                .display_details(NpcRelations::default())
272                .to_string(),
273        );
274        assert_eq!(
275            r#"<div class="thing-box npc" data-uuid="00000000-0000-0000-0000-000000000014">
276
277# Unnamed NPC
278*human*
279
280**Species:** human (elvish)
281
282</div>"#,
283            gen_npc(ETHNICITY | SPECIES)
284                .display_details(NpcRelations::default())
285                .to_string(),
286        );
287    }
288
289    #[test]
290    fn details_view_test_empty() {
291        assert_eq!(
292            r#"<div class="thing-box npc" data-uuid="00000000-0000-0000-0000-000000000000">
293
294# Unnamed NPC
295*person*
296
297**Species:** N/A
298
299</div>"#,
300            NpcData::default()
301                .display_details(Uuid::nil(), NpcRelations::default())
302                .to_string(),
303        );
304    }
305
306    #[test]
307    fn details_view_test_with_parent_location() {
308        assert_eq!(
309            r#"<div class="thing-box npc" data-uuid="00000000-0000-0000-0000-000000000011">
310
311# Odysseus
312*person*
313
314**Species:** N/A\
315**Location:** 🏞 `Styx` (river)
316
317</div>"#,
318            DetailsView::new(
319                &test::npc().name("Odysseus").build(),
320                test::npc::odysseus::UUID,
321                test::npc::odysseus::relations(),
322            )
323            .to_string(),
324        );
325    }
326
327    #[test]
328    fn details_view_test_with_grandparent_location() {
329        assert_eq!(
330            r#"<div class="thing-box npc" data-uuid="00000000-0000-0000-0000-000000000012">
331
332# Penelope
333*person*
334
335**Species:** N/A\
336**Location:** 🏝 `Ithaca`, 👑 `Greece`
337
338</div>"#,
339            DetailsView::new(
340                &test::npc().name("Penelope").build(),
341                test::npc::penelope::UUID,
342                test::npc::penelope::relations(),
343            )
344            .to_string(),
345        );
346    }
347
348    fn gen_npc(bitmask: u8) -> Npc {
349        let mut builder = test::npc();
350
351        if bitmask & NAME > 0 {
352            builder = builder.name("Potato Johnson");
353        }
354        if bitmask & AGE > 0 {
355            builder = builder.age(Age::Elderly).age_years(60);
356        }
357        if bitmask & SPECIES > 0 {
358            builder = builder.species(Species::Human);
359        }
360        if bitmask & GENDER > 0 {
361            builder = builder.gender(Gender::Masculine);
362        }
363        if bitmask & ETHNICITY > 0 {
364            builder = builder.ethnicity(Ethnicity::Elvish);
365        }
366
367        builder.build_with_uuid(Uuid::from_u128(bitmask.into()))
368    }
369}