// Named creatures: every creature of a master carries a name seed from its birth. The name // comes from the list of its kind in the language files (so it follows a language switch); // from level 5 on it earns a humorous epithet, from level 8 a grand one. A creature of level // 3 or more that falls leaves a memorial stone with its name on its master's ground. use crate::game::*; use crate::i18n::{tr, trf}; use crate::map::*; use serde::{Deserialize, Serialize}; // the fallen get a stone from this level on; at most MAX_MEMORIALS stones (the oldest go) pub const MEMORIAL_LEVEL: u32 = 3; pub const MAX_MEMORIALS: usize = 24; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Memorial { pub tile: usize, pub kind: EK, pub name: u32, pub level: u32, } // the name list of a kind of creature pub fn group(k: EK) -> &'static str { match k { EK::Worker => "imp", EK::Kobold => "kobold", EK::Spider => "spider", EK::Mage => "mage", EK::Ogre => "ogre", EK::Jailer => "jailer", EK::Priestess => "priestess", EK::Chainbreaker => "chainbreaker", EK::Bat => "bat", EK::Gargoyle => "gargoyle", EK::Owl => "owl", EK::Wyrm => "wyrm", EK::Cogling => "cogling", EK::Revenant => "revenant", EK::CaveBear | EK::Beetle => "beast", _ => "human", } } // the seed of a new creature's name (from its id: the same on every computer of a match) pub fn seed_for(id: u32) -> u32 { (id.wrapping_mul(2654435761) ^ 0x5bd1_e995) | 1 } fn pick(key: &str, seed: u32) -> Option { let list = tr(key); if list == key { return None; // no list in this language } let items: Vec<&str> = list.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect(); (!items.is_empty()).then(|| items[seed as usize % items.len()].to_string()) } // the name with its epithet (if the level has earned one); "" if the creature has no name pub fn full_name(kind: EK, seed: u32, level: u32) -> String { if seed == 0 { return String::new(); } let Some(base) = pick(&format!("names.{}", group(kind)), seed) else { return String::new() }; let list = if level >= 8 { "epithets.high" } else { "epithets.mid" }; match pick(list, seed >> 7).filter(|_| level >= 5) { Some(ep) => trf("name.epithet", &[base, ep]), None => base, } } // the name of a unit, or its kind's name if it has none pub fn display_name(e: &Ent) -> String { let n = full_name(e.kind, e.name, e.level); if n.is_empty() { tr(st(e.kind).name) } else { n } } // a message argument that is turned into the name when the message is shown (i18n::trf) pub fn arg(e: &Ent) -> String { if e.name == 0 { return format!("@{}", st(e.kind).name); } format!("%{}.{}.{}", crate::gfx::sprite_index(e.kind), e.name, e.level) } // the counterpart of `arg` pub fn from_arg(a: &str) -> String { let p: Vec = a.split('.').filter_map(|s| s.parse().ok()).collect(); match p.as_slice() { [k, seed, level] => { let kind = crate::gfx::SPRITE_KINDS.get(*k as usize).copied().unwrap_or(EK::Kobold); let n = full_name(kind, *seed, *level); if n.is_empty() { tr(st(kind).name) } else { n } } _ => a.to_string(), } } impl Game { // a named creature of some standing fell: a stone with its name on its master's ground, as // near to where it fell as there is plain floor pub(crate) fn raise_memorial(&mut self, e: &Ent) -> bool { if e.hero || e.name == 0 || e.level < MEMORIAL_LEVEL || !Owner::MASTER_SIDES.contains(&e.side) { return false; } let owner = Owner::of_side(e.side); let (x, y) = (e.x as i32, e.y as i32); let taken: Vec = self.memorials.iter().map(|m| m.tile).collect(); let mut best: Option<(usize, i32)> = None; for dy in -6..=6 { for dx in -6..=6 { let (tx, ty) = (x + dx, y + dy); if !inb(tx, ty) { continue; } let i = idx(tx, ty); let t = &self.map.t[i]; if t.kind == Kind::Floor && t.owner == owner && !taken.contains(&i) { let d = dx * dx + dy * dy; if best.map_or(true, |b| d < b.1) { best = Some((i, d)); } } } } let Some((tile, _)) = best else { return false }; if self.memorials.len() >= MAX_MEMORIALS { self.memorials.remove(0); } self.memorials.push(Memorial { tile, kind: e.kind, name: e.name, level: e.level }); if e.side == self.local_side { self.tell_local("msg.creature_fallen", vec![arg(e)], None); } true } // stones on ground that was lost or built over are taken away pub(crate) fn tidy_memorials(&mut self) { let map = &self.map; self.memorials.retain(|m| map.t[m.tile].kind == Kind::Floor && Owner::MASTER_SIDES.contains(&map.t[m.tile].owner.side())); } pub fn memorial_at(&self, i: usize) -> Option<&Memorial> { self.memorials.iter().find(|m| m.tile == i) } } #[cfg(test)] mod tests { use super::*; #[test] fn creatures_have_names_and_earn_epithets() { let mut g = Game::new(9, 64, 64); let (cx, cy) = g.map.core; let id = g.spawn(EK::Ogre, false, cx as f32 + 0.5, cy as f32 + 4.5, 1); let e = g.ents.iter().find(|e| e.id == id).unwrap().clone(); assert!(e.name != 0); let n1 = full_name(e.kind, e.name, 1); assert!(!n1.is_empty() && !n1.contains("names."), "{}", n1); let n5 = full_name(e.kind, e.name, 5); let n9 = full_name(e.kind, e.name, 9); assert!(n5.starts_with(&n1) || n5.ends_with(&n1), "{} / {}", n1, n5); assert!(n5.len() > n1.len() && n9 != n5, "{} {} {}", n1, n5, n9); assert_eq!(from_arg(arg(&e).trim_start_matches('%')), display_name(&e)); // heroes have none let h = g.spawn(EK::Knight, true, cx as f32 + 0.5, cy as f32 + 5.5, 1); assert_eq!(g.ents.iter().find(|e| e.id == h).unwrap().name, 0); } #[test] fn the_fallen_get_a_memorial_stone() { let mut g = Game::new(9, 64, 64); let (cx, cy) = g.map.core; for y in cy + 4..cy + 8 { for x in cx..cx + 4 { let t = &mut g.map.t[idx(x, y)]; t.kind = Kind::Floor; t.owner = Owner::Player; } } let id = g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 4); let j = g.ents.iter().position(|e| e.id == id).unwrap(); g.kill(j); assert_eq!(g.memorials.len(), 1); let m = &g.memorials[0]; assert_eq!((m.kind, m.level), (EK::Ogre, 4)); assert!(g.memorial_at(m.tile).is_some()); // a level 1 kobold is not remembered let k = g.spawn(EK::Kobold, false, cx as f32 + 2.5, cy as f32 + 5.5, 1); let j = g.ents.iter().position(|e| e.id == k).unwrap(); g.kill(j); assert_eq!(g.memorials.len(), 1); } }