Hall of the fallen: kills and causes of death, in the game and over all games in the profile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-06 13:48:06 +01:00
1 parent 065fe80218
commit c52fc4c4ae
28 files changed
+534 -5

No files matched your search

+28 -1
View File
@@ -63,8 +63,13 @@ pub struct Profile {
pub totals: BTreeMap<String, f64>,
#[serde(default)]
pub unlocked: BTreeMap<String, u64>, // achievement id ("heroes.2", "flawless") -> unix time
#[serde(default)]
pub hall: Vec<crate::names::Fallen>, // the hall of the fallen over all games (newest last)
}
// the hall of the fallen keeps this many
pub const HALL_MAX: usize = 200;
impl Profile {
pub fn total(&self, id: &str) -> f64 {
self.totals.get(id).copied().unwrap_or(0.0)
@@ -80,6 +85,15 @@ impl Profile {
let e = self.unlocked.entry(k.clone()).or_insert(*t);
*e = (*e).min(*t);
}
// the fallen of both, once each, in the order they fell
for f in &other.hall {
if !self.hall.iter().any(|h| h.name == f.name && h.kind == f.kind && h.when == f.when) {
self.hall.push(f.clone());
}
}
self.hall.sort_by_key(|h| h.when);
let extra = self.hall.len().saturating_sub(HALL_MAX);
self.hall.drain(..extra);
}
// unlocks what the totals have reached; returns the new ones
@@ -117,6 +131,7 @@ pub struct Context {
pub struct Tracker {
pub profile: Profile,
flushed: Vec<f32>,
hall_seen: usize, // entries of the game's hall already in the profile
last_time: f32,
ended: bool,
t: f32,
@@ -126,12 +141,13 @@ pub struct Tracker {
impl Tracker {
pub fn new(profile: Profile) -> Tracker {
Tracker { profile, flushed: vec![0.0; k::COUNTERS], last_time: 0.0, ended: false, t: 0.0, dirty: false, upload: false }
Tracker { profile, flushed: vec![0.0; k::COUNTERS], hall_seen: 0, last_time: 0.0, ended: false, t: 0.0, dirty: false, upload: false }
}
// a different game started (new, loaded): nothing of it has been counted yet
fn restart(&mut self, g: &Game) {
self.flushed = (0..k::COUNTERS).map(|i| g.stats.get(i)).collect();
self.hall_seen = g.hall.len();
self.ended = g.state != 0;
}
@@ -154,6 +170,17 @@ impl Tracker {
let played = self.t;
self.t = 0.0;
let mut new = Vec::new();
// the newly fallen go into the profile's hall
if g.hall.len() > self.hall_seen {
for f in &g.hall[self.hall_seen..] {
self.profile.hall.push(crate::names::Fallen { when: now, ..f.clone() });
}
self.hall_seen = g.hall.len();
let extra = self.profile.hall.len().saturating_sub(HALL_MAX);
self.profile.hall.drain(..extra);
self.dirty = true;
self.upload = true;
}
// the counters of the game
for i in 0..k::COUNTERS {
let cur = g.stats.get(i);
+25
View File
@@ -303,6 +303,10 @@ pub struct Ent {
#[serde(default)]
pub name: u32, // seed of the creature's name (names.rs), 0: none (heroes, siege engines)
#[serde(default)]
pub kills: u32, // heroes this creature has slain (names.rs: the hall of the fallen)
#[serde(default)]
pub bane: u8, // what hurt it last: sprite index + 1 of a unit kind, or names::BANE_*
#[serde(default)]
pub ab_cd: f32, // cooldown of the learned ability
#[serde(default)]
pub mood_t: f32, // seconds until the next mood check (likes and hatreds)
@@ -393,6 +397,8 @@ impl Ent {
invis: 0.0,
plague: 0.0,
name: 0,
kills: 0,
bane: 0,
ab_cd: 0.0,
mood_t: 0.0,
guard: false,
@@ -652,6 +658,8 @@ pub struct Game {
#[serde(default)]
pub memorials: Vec<crate::names::Memorial>, // stones for fallen creatures (names.rs)
#[serde(default)]
pub hall: Vec<crate::names::Fallen>, // the local master's fallen of this game (names.rs)
#[serde(default)]
pub trading: crate::traders::Trading, // the visiting trader (traders.rs)
#[serde(default)]
pub happenings: Option<crate::events::Events>, // random events (free games and versus), see events.rs
@@ -1009,6 +1017,7 @@ impl Game {
bridgework: Default::default(),
weather: Default::default(),
memorials: Vec::new(),
hall: Vec::new(),
trading: Default::default(),
happenings: None,
snares: Vec::new(),
@@ -2791,6 +2800,11 @@ impl Game {
true
}
#[cfg(test)]
pub(crate) fn hit_for_test(&mut self, e: &mut Ent, j: usize) {
self.hit(e, j);
}
fn hit(&mut self, e: &mut Ent, j: usize) {
// the Warlord talent of the attacker's master
let boost = if !e.hero && Owner::MASTER_SIDES.contains(&e.side) { self.talents_of(e.side).damage() } else { 1.0 };
@@ -2910,6 +2924,10 @@ impl Game {
let t = &mut self.ents[j];
t.hp -= if t.shield > 0.0 { dmg * 0.5 } else { dmg } * armour;
t.flash = 0.15;
t.bane = crate::gfx::sprite_index(e.kind) as u8 + 1;
if t.hp <= 0.0 && t.hero && t.captive == 0 {
e.kills += 1;
}
if t.creature() && !matches!(t.job, Job::Attack(_)) {
t.job = Job::Attack(my_id);
t.path.clear();
@@ -3025,6 +3043,13 @@ impl Game {
}
self.release_all(id);
self.hand.retain(|&h| h != id);
// a named creature lost in lava or water is remembered too
if !hero {
let mut dead = self.ents[j].clone();
let lava = self.map.get(x as i32, y as i32).map_or(false, |t| t.river() == Some(Kind::Lava));
dead.bane = if lava { crate::names::BANE_LAVA } else { crate::names::BANE_WATER };
self.raise_memorial(&dead);
}
self.fx(3, x, y, 0.0, 0.0, 1.0, 0xff2060ff);
self.fx(2, x, y, 0.0, 0.0, 0.5, 0xff30a0ff);
self.sfx(Sfx::Death, x, y);
+10 -1
View File
@@ -141,7 +141,16 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32,
(Tool::Dig | Tool::Room(_) | Tool::Sell | Tool::Bridge | Tool::Steel | Tool::Drawbridge | Tool::Wall, Some(t)) => inp.ldown = Some(t),
(Tool::Hand, _) => {
let j = r3d.pick_ent(&app.game, &app.cam, mx, my, false);
let Some(j) = j else { return };
let Some(j) = j else {
// a memorial stone: the hall of the fallen opens
let (gx, gy) = (wx.floor() as i32, wy.floor() as i32);
if map::inb(gx, gy) && app.game.memorial_at(map::idx(gx, gy)).is_some() {
ui.show_overview = true;
ui.ov_fallen = true;
ui.ov_scroll = 0;
}
return;
};
let (id, hero, dwarf) = (app.game.ents[j].id, app.game.ents[j].hero, app.game.ents[j].dwarf());
if dwarf {
return;
+8
View File
@@ -303,6 +303,7 @@ enum Btn {
MatchChat,
Bestiary,
Achievements,
AchHall, // achievements page: switch to the hall of the fallen and back
Tutorial,
BestTab(usize),
BestSel(i32),
@@ -454,6 +455,7 @@ pub struct Menu {
scroll: usize,
rows_visible: usize,
pub profile: crate::achievements::Profile, // shown on the achievements page
ach_hall: bool, // the achievements page shows the hall of the fallen
buttons: Vec<(Rect, Btn)>,
lang_open: Option<Rect>, // the language dropdown is open below/above this field
lang_scroll: usize, // first language shown in the open dropdown
@@ -497,6 +499,7 @@ impl Menu {
focus: 0,
error: None,
match_ui: MatchUi::default(),
ach_hall: false,
coop_mission: crate::campaign::coop_mission_indexes().first().copied().unwrap_or(0),
match_size: 128,
coop_editor: false,
@@ -939,6 +942,11 @@ impl Menu {
}
MenuAction::Consumed
}
Some(Btn::AchHall) => {
self.ach_hall = !self.ach_hall;
self.scroll = 0;
MenuAction::Consumed
}
Some(Btn::Achievements) => {
self.page = Some(Page::Achievements { from_title });
self.scroll = 0;
+69 -2
View File
@@ -15,7 +15,13 @@ impl Menu {
let rows = 40.min((sh / CH()) as i32 - 2);
let x = grid_center(sw, cols as f32 * CW()).max(0.0);
let y = grid_center_y(sh, rows as f32 * CH()).max(0.0);
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &tr("menu.achievements"), FRAME_HI);
let title = if self.ach_hall { tr("hall.title") } else { tr("menu.achievements") };
gfx.window_titled(x, y, cols, rows, S(), true, FRAME_HI, Color::new(0.05, 0.03, 0.04, 0.97), &title, FRAME_HI);
if self.ach_hall {
self.draw_hall_page(gfx, x, y, cols, rows);
self.ach_buttons(gfx, x, y, rows);
return;
}
let list = all();
let done = list.iter().filter(|a| self.profile.unlocked.contains_key(&a.0)).count();
let head = trf("ach.count", &[done.to_string(), list.len().to_string(), crate::achievements::fmt_count(self.profile.total("minutes"))]);
@@ -57,14 +63,75 @@ impl Menu {
}
}
// scrolling and back
let _ = ix;
self.ach_buttons(gfx, x, y, rows);
}
// scrolling, the switch between achievements and the hall, back
fn ach_buttons(&mut self, gfx: &Gfx, x: f32, y: f32, rows: i32) {
let ix = x + CW() * 3.0;
let by = y + (rows - 3) as f32 * CH();
let (mx, my) = mouse();
let bw = 14.0 * CW();
let items = [(Btn::Scroll(-1), "▲".to_string()), (Btn::Scroll(1), "▼".to_string()), (Btn::Back, tr("menu.back"))];
let switch = if self.ach_hall { tr("menu.achievements") } else { tr("hall.title") };
let items = [(Btn::Scroll(-1), "▲".to_string()), (Btn::Scroll(1), "▼".to_string()), (Btn::AchHall, switch), (Btn::Back, tr("menu.back"))];
let bw = bw.min((sw() - ix - CW() * 4.0) / 4.0 - CW());
for (k, (b, label)) in items.iter().enumerate() {
let r = Rect::new(ix + k as f32 * (bw + CW()), by, bw, CH() * 2.0);
button(gfx, r, label, r.contains(vec2(mx, my)), FRAME);
self.buttons.push((r, *b));
}
}
// the hall of the fallen over all games: newest first, two lines each
fn draw_hall_page(&mut self, gfx: &Gfx, x: f32, y: f32, cols: i32, rows: i32) {
let hall = &self.profile.hall;
gfx.text(&trf("hall.count", &[hall.len().to_string()]), x + CW() * 3.0, y + CH() * 1.6, S(), GOLD);
let top = y + CH() * 3.5;
let bottom = y + (rows - 4) as f32 * CH();
let row_h = CH() * 2.4;
self.rows_visible = (((bottom - top) / row_h).floor() as usize).max(1);
self.scroll = self.scroll.min(hall.len().saturating_sub(self.rows_visible));
let ix = x + CW() * 3.0;
let maxc = (cols - 6) as usize;
if hall.is_empty() {
gfx.text(&tr("hall.empty"), ix, top, S(), DIM);
}
for (k, f) in hall.iter().rev().enumerate().skip(self.scroll).take(self.rows_visible) {
let ry = top + (k - self.scroll) as f32 * row_h;
let name = format!("{} ({})", crate::names::full_name(f.kind, f.name, f.level), trf("info.level", &[tr(crate::game::st(f.kind).name), f.level.to_string()]));
gfx.text(&name.chars().take(maxc).collect::<String>(), ix, ry, S(), FRAME_HI);
let kills = if f.kills > 0 { format!(" · {}", trf("hall.kills", &[f.kills.to_string()])) } else { String::new() };
let line = format!("{}{} · {}", crate::names::cause(f.bane), kills, date(f.when));
gfx.text(&line.chars().take(maxc * 2).collect::<String>(), ix + CW() * 2.0, ry + CH(), SMALL(), DIM);
}
}
}
// a unix time as a date (year-month-day)
fn date(unix: u64) -> String {
if unix == 0 {
return String::new();
}
// days since 1970-01-01 to a civil date (Howard Hinnant's algorithm)
let z = (unix / 86400) as i64 + 719468;
let era = z.div_euclid(146097);
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };
format!("{:04}-{:02}-{:02}", y, m, d)
}
#[cfg(test)]
mod tests {
#[test]
fn dates_are_civil() {
assert_eq!(super::date(0), "");
assert_eq!(super::date(1_798_675_200), "2026-12-31");
assert_eq!(super::date(1_709_164_800), "2024-02-29");
}
}
+82
View File
@@ -12,6 +12,40 @@ use serde::{Deserialize, Serialize};
pub const MEMORIAL_LEVEL: u32 = 3;
pub const MAX_MEMORIALS: usize = 24;
// what killed a creature besides another unit (Ent::bane)
pub const BANE_SNARE: u8 = 250;
pub const BANE_LIGHTNING: u8 = 251;
pub const BANE_LAVA: u8 = 252;
pub const BANE_WATER: u8 = 253;
// an entry of the hall of the fallen (in the game and in the player's profile)
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Fallen {
pub kind: EK,
pub name: u32,
pub level: u32,
pub kills: u32,
pub bane: u8,
pub time: f32, // game seconds when it fell
#[serde(default)]
pub when: u64, // unix time (set when it goes into the profile)
}
// how it fell, in words
pub fn cause(bane: u8) -> String {
match bane {
BANE_SNARE => tr("cause.snare"),
BANE_LIGHTNING => tr("cause.lightning"),
BANE_LAVA => tr("cause.lava"),
BANE_WATER => tr("cause.water"),
0 => tr("cause.fell"),
k => match crate::gfx::SPRITE_KINDS.get(k as usize - 1) {
Some(&kind) => trf("cause.slain", &[tr(st(kind).name)]),
None => tr("cause.fell"),
},
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Memorial {
pub tile: usize,
@@ -103,6 +137,10 @@ impl Game {
if e.hero || e.name == 0 || e.level < MEMORIAL_LEVEL || !Owner::MASTER_SIDES.contains(&e.side) {
return false;
}
// the hall of the fallen keeps the local master's (also without room for a stone)
if e.side == self.local_side {
self.hall.push(Fallen { kind: e.kind, name: e.name, level: e.level, kills: e.kills, bane: e.bane, time: self.time, when: 0 });
}
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();
@@ -168,6 +206,50 @@ mod tests {
assert_eq!(g.ents.iter().find(|e| e.id == h).unwrap().name, 0);
}
#[test]
fn the_hall_remembers_kills_and_how_they_fell() {
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 + 6 {
let t = &mut g.map.t[idx(x, y)];
t.kind = Kind::Floor;
t.owner = Owner::Player;
}
}
let mut t = crate::achievements::Tracker::new(Default::default());
let ctx = crate::achievements::Context { coop: false, editor_test: false };
g.time = 1.0;
t.update(&g, &ctx, 6.0, 1_800_000_000);
let ogre = g.spawn(EK::Ogre, false, cx as f32 + 1.5, cy as f32 + 5.5, 5);
let knight = g.spawn(EK::Knight, true, cx as f32 + 2.5, cy as f32 + 5.5, 1);
// the ogre slays the knight
let (oj, kj) = (g.ents.iter().position(|e| e.id == ogre).unwrap(), g.ents.iter().position(|e| e.id == knight).unwrap());
g.ents[kj].hp = 1.0;
let mut o = g.ents[oj].clone();
g.hit_for_test(&mut o, kj);
assert_eq!(o.kills, 1, "a hero slain");
g.ents[oj] = o;
// a second knight brings the ogre down
let k2 = g.spawn(EK::Knight, true, cx as f32 + 2.5, cy as f32 + 6.5, 1);
let k2j = g.ents.iter().position(|e| e.id == k2).unwrap();
let oj = g.ents.iter().position(|e| e.id == ogre).unwrap();
g.ents[oj].hp = 1.0;
let mut k = g.ents[k2j].clone();
g.hit_for_test(&mut k, oj);
assert_eq!(g.hall.len(), 1);
let f = &g.hall[0];
assert_eq!((f.kind, f.kills), (EK::Ogre, 1));
assert_eq!(f.bane as u32, crate::gfx::sprite_index(EK::Knight) + 1);
assert!(cause(f.bane).contains(&crate::i18n::tr("unit.knight")));
// into the profile, once
g.time = 100.0;
t.update(&g, &ctx, 6.0, 1_800_000_100);
t.update(&g, &ctx, 6.0, 1_800_000_200);
assert_eq!(t.profile.hall.len(), 1);
assert_eq!(t.profile.hall[0].when, 1_800_000_100);
}
#[test]
fn the_fallen_get_a_memorial_stone() {
let mut g = Game::new(9, 64, 64);
+1
View File
@@ -193,6 +193,7 @@ impl Game {
let e = &mut self.ents[j];
e.stun = e.stun.max(SNARE_STUN);
e.hp -= SNARE_DAMAGE;
e.bane = crate::names::BANE_SNARE;
e.flash = 0.2;
let (x, y, side, kind, dead) = (e.x, e.y, e.side, e.kind, e.hp <= 0.0);
self.fx(2, x, y, 0.0, 0.0, 0.4, 0xff4040ff);
+54 -1
View File
@@ -461,6 +461,7 @@ enum TBtn {
Learn(u8), // spend a point on a branch
OvRow(u32),
OvScroll(i32),
OvFallen, // switch the overview between the living and the fallen
GoldTo, // the next ally to receive gold
Gold(u32), // send that much gold to him
}
@@ -491,7 +492,8 @@ pub struct Ui {
pub show_talents: bool, // the master's talents
pub coop_end: Option<(bool, bool, bool)>, // end of an editor co-op mission: (host, a next mission, asked for it)
gold_to: usize, // the ally chosen in the creature overview
ov_scroll: usize,
pub ov_scroll: usize,
pub ov_fallen: bool, // the overview shows the hall of the fallen
ov_rect: Option<Rect>, // where the creature overview is (mouse wheel, swipe)
pub show_langsel: bool,
pub log: Vec<(Msg, f64)>,
@@ -603,6 +605,7 @@ impl Ui {
coop_end: None,
gold_to: 0,
ov_scroll: 0,
ov_fallen: false,
ov_rect: None,
show_langsel: false,
log: Vec::new(),
@@ -702,6 +705,11 @@ impl Ui {
UiAction::Consumed
}
TBtn::Learn(b) => UiAction::LearnTalent(b),
TBtn::OvFallen => {
self.ov_fallen = !self.ov_fallen;
self.ov_scroll = 0;
UiAction::Consumed
}
TBtn::OvScroll(d) => {
self.ov_scroll = (self.ov_scroll as i32 + d).max(0) as usize;
UiAction::Consumed
@@ -1343,6 +1351,36 @@ impl Ui {
}
}
// the hall of the fallen of this game: name, kind, level, heroes slain, how it fell
fn draw_hall(&mut self, g: &Game, gfx: &Gfx, x: f32, y: f32, cols: i32, rows: i32) {
gfx.text(&tr("hall.title"), x + CW() * 2.0, y + CH(), S(), GOLD);
let mut lines: Vec<(String, Color)> = Vec::new();
for f in g.hall.iter().rev() {
lines.push((format!("{:>2} {}", f.level, crate::names::full_name(f.kind, f.name, f.level)), FRAME_HI));
let kills = if f.kills > 0 { format!(" · {}", trf("hall.kills", &[f.kills.to_string()])) } else { String::new() };
lines.push((format!(" {} · {}{} · {}", tr(st(f.kind).name), crate::names::cause(f.bane), kills, mmss(f.time)), DIM));
}
if lines.is_empty() {
lines.push((tr("hall.empty"), DIM));
}
let first = y + CH() * 2.6;
let visible = (rows - 4).max(1) as usize;
self.ov_scroll = self.ov_scroll.min(lines.len().saturating_sub(visible));
let maxc = (cols - 4) as usize;
for (n, (text, col)) in lines.iter().enumerate().skip(self.ov_scroll).take(visible) {
let t: String = text.chars().take(maxc).collect();
gfx.text(&t, x + CW() * 2.0, first + (n - self.ov_scroll) as f32 * CH(), S(), *col);
}
if lines.len() > visible {
let up = Rect::new(x + (cols - 4) as f32 * CW(), y + CH() * 2.4, CW() * 2.0, CH() * 1.5);
let down = Rect::new(x + (cols - 4) as f32 * CW(), y + (rows - 2) as f32 * CH(), CW() * 2.0, CH() * 1.5);
gfx.text("\u{25b2}", up.x + CW() * 0.5, up.y, S(), FRAME_HI);
gfx.text("\u{25bc}", down.x + CW() * 0.5, down.y, S(), FRAME_HI);
self.touch_btns.push((up, TBtn::OvScroll(-2)));
self.touch_btns.push((down, TBtn::OvScroll(2)));
}
}
fn draw_overview(&mut self, g: &Game, gfx: &Gfx, bottom: f32) {
let (sw, _) = (sw(), sh());
let (mx, my) = mouse();
@@ -1357,6 +1395,21 @@ impl Ui {
let mine: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
let wage: f32 = mine.iter().map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum();
let mood = if mine.is_empty() { 0.0 } else { mine.iter().map(|e| e.happy.clamp(0.0, 1.0)).sum::<f32>() / mine.len() as f32 };
// the switch to the hall of the fallen (top right)
{
let label = if self.ov_fallen { tr("ov.living") } else { trf("ov.fallen", &[g.hall.len().to_string()]) };
let bw = gfx.measure(&label, SMALL()) + CW() * 2.0;
let r = Rect::new(x + (cols - 3) as f32 * CW() - bw, y + CH() * 0.8, bw, CH() * 1.4);
let hov = r.contains(vec2(mx, my));
draw_rectangle(r.x, r.y, r.w, r.h, if hov { Color::new(0.35, 0.22, 0.1, 1.0) } else { Color::new(0.15, 0.1, 0.08, 1.0) });
draw_rectangle_lines(r.x, r.y, r.w, r.h, 2.0, FRAME_HI);
gfx.text(&label, r.x + CW(), r.y + (r.h - GH * SMALL()) * 0.5, SMALL(), FRAME_HI);
self.touch_btns.push((r, TBtn::OvFallen));
}
if self.ov_fallen {
self.draw_hall(g, gfx, x, y, cols, rows);
return;
}
let head = trf("ov.summary", &[mine.len().to_string(), (wage as i32).to_string(), ((mood * 100.0) as i32).to_string()]);
gfx.text(&head, x + CW() * 2.0, y + CH(), S(), GOLD);
// a match with allies: a row to send them gold (the name switches between the allies)
+1
View File
@@ -145,6 +145,7 @@ impl Game {
let e = &mut self.ents[j];
e.hp -= BOLT_DAMAGE;
e.flash = 0.3;
e.bane = crate::names::BANE_LIGHTNING;
let (x, y, side, kind, hero, dead) = (e.x, e.y, e.side, e.kind, e.hero, e.hp <= 0.0);
if !hero && side == self.local_side {
self.tell_local("msg.lightning_unit", vec![format!("@{}", st(kind).name)], None);