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}