196 lines
7.1 KiB
Rust
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);
|
|
}
|
|
}
|