Files
dungeoncraft/src/names.rs
T

196 lines
7.1 KiB
Rust

// 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<String> {
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<u32> = 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<usize> = 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);
}
}