Creatures report at the portal and set up their bed; payday wages are fetched from the treasury

- New creatures report at the portal (2 s), walk to the lair (flyers: aerie), set up their
  bed (4 s) and only then do as they like; without a reachable lair they start right away
- Payday: the training bill is paid at once; every creature goes for its wage at a random
  moment within about a minute, to the nearest treasury tile holding treasure (or the core),
  and takes it out of that tile; no gold = an angry creature; unreachable = paid anyway
- Tests for the arrival and the walking payday

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 13:49:13 +01:00
1 parent f13ab1a629
commit 52c4d1caf8
3 files changed
+211 -26

No files matched your search

+5
View File
@@ -571,3 +571,8 @@ account.users_changed = Benutzer aktualisiert.
standalone.running = Die Standalone-Version wird vorbereitet ... standalone.running = Die Standalone-Version wird vorbereitet ...
standalone.done = DungeonCraft.html heruntergeladen: In jedem Browser öffnen, kein Server nötig. Deine heruntergeladenen Spielstände lassen sich dort auch laden. standalone.done = DungeonCraft.html heruntergeladen: In jedem Browser öffnen, kein Server nötig. Deine heruntergeladenen Spielstände lassen sich dort auch laden.
standalone.failed = Die Standalone-Version konnte nicht heruntergeladen werden. standalone.failed = Die Standalone-Version konnte nicht heruntergeladen werden.
job.register = Meldet sich am Portal
job.make_bed = Richtet sein Bett ein
job.fetch_wage = Holt sich seinen Lohn
msg.payday_walk = Zahltag! {0} Kreaturen holen sich ihren Lohn aus der Schatzkammer ({1} Gold inkl. Training).
msg.wage_unpaid = Dein {0} hat kein Gold für seinen Lohn gefunden und ist wütend!
+5
View File
@@ -572,3 +572,8 @@ account.users_changed = Users updated.
standalone.running = Preparing the standalone version ... standalone.running = Preparing the standalone version ...
standalone.done = DungeonCraft.html downloaded: open it in any browser, no server needed. Your downloaded savegames load there too. standalone.done = DungeonCraft.html downloaded: open it in any browser, no server needed. Your downloaded savegames load there too.
standalone.failed = The standalone version could not be downloaded. standalone.failed = The standalone version could not be downloaded.
job.register = Reporting at the portal
job.make_bed = Setting up its bed
job.fetch_wage = Fetching its wage
msg.payday_walk = Payday! {0} creatures fetch their wages from the treasury ({1} gold incl. training).
msg.wage_unpaid = Your {0} found no gold for its wage and is furious!
+201 -26
View File
@@ -171,6 +171,9 @@ pub enum Job {
TearBridge(usize), // knight tears down a bridge of the player TearBridge(usize), // knight tears down a bridge of the player
Rest(usize), // flying creature recovers in the aerie (flees when badly hurt) Rest(usize), // flying creature recovers in the aerie (flees when badly hurt)
Torment(u32), // the jailer works on a prisoner in the torture chamber Torment(u32), // the jailer works on a prisoner in the torture chamber
Register, // new creature: reports at the portal
MakeBed(usize), // new creature: sets up its bed
FetchWage(usize), // payday: walks to the treasury (or the core) for the wage
Help(usize), // imp with nothing else to do helps another imp at this tile Help(usize), // imp with nothing else to do helps another imp at this tile
} }
@@ -228,6 +231,16 @@ pub struct Ent {
pub patrol: Vec<(i32, i32)>, // fortress patrol: route of tiles, walked in a loop pub patrol: Vec<(i32, i32)>, // fortress patrol: route of tiles, walked in a loop
#[serde(default)] #[serde(default)]
pub patrol_k: usize, // next point of the route pub patrol_k: usize, // next point of the route
#[serde(default = "yes")]
pub settled: bool, // creatures: reported at the portal and set up their bed
#[serde(default)]
pub pay_due: f32, // wage of the last payday not fetched yet
#[serde(default)]
pub pay_t: f32, // seconds until the creature goes for its wage
}
fn yes() -> bool {
true
} }
impl Ent { impl Ent {
@@ -273,6 +286,9 @@ impl Ent {
home: None, home: None,
patrol: Vec::new(), patrol: Vec::new(),
patrol_k: 0, patrol_k: 0,
settled: true,
pay_due: 0.0,
pay_t: 0.0,
} }
} }
pub fn tile(&self) -> usize { pub fn tile(&self) -> usize {
@@ -1147,44 +1163,140 @@ impl Game {
let owned: Vec<usize> = (0..self.map.portals.len()).filter(|&k| self.map.portal_owned(k)).collect(); let owned: Vec<usize> = (0..self.map.portals.len()).filter(|&k| self.map.portal_owned(k)).collect();
let pk = owned[(self.rng.next() as usize) % owned.len()]; let pk = owned[(self.rng.next() as usize) % owned.len()];
let (px, py) = self.map.portals[pk]; let (px, py) = self.map.portals[pk];
self.spawn(kind, false, px as f32 + 1.5, py as f32 + 1.5, 1); let id = self.spawn(kind, false, px as f32 + 1.5, py as f32 + 1.5, 1);
// new creatures report at the portal and set up their bed first
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
e.settled = false;
e.job = Job::Register;
e.timer = 2.0;
}
self.fx(3, px as f32 + 1.5, py as f32 + 1.5, 0.0, 0.0, 0.8, 0xffcc44ff); self.fx(3, px as f32 + 1.5, py as f32 + 1.5, 0.0, 0.0, 0.8, 0xffcc44ff);
self.sfx(Sfx::PortalArrive, px as f32 + 1.5, py as f32 + 1.5); self.sfx(Sfx::PortalArrive, px as f32 + 1.5, py as f32 + 1.5);
self.msg("msg.creature_arrived", vec![format!("@{}", st(kind).name)]); self.msg("msg.creature_arrived", vec![format!("@{}", st(kind).name)]);
} }
// Payday: the training bill is paid at once; every creature fetches its wage from a
// treasury itself, at a random moment within about a minute
fn do_payday(&mut self) { fn do_payday(&mut self) {
let mut paid = 0; let bill = self.training_bill.min(self.gold).max(0.0);
let mut unpaid = 0; self.gold -= bill;
let mut total = 0.0; self.training_bill = 0.0;
let mut n = 0;
let mut total = bill;
for i in 0..self.ents.len() { for i in 0..self.ents.len() {
if !self.ents[i].alive || !self.ents[i].creature() { if !self.ents[i].alive || !self.ents[i].creature() {
continue; continue;
} }
let e = &self.ents[i]; let r = self.rng.f();
let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32);
if self.gold >= w {
self.gold -= w;
total += w;
paid += 1;
let e = &mut self.ents[i]; let e = &mut self.ents[i];
e.happy = (e.happy + 0.2).min(1.0); let w = st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32);
} else { e.pay_due += w;
unpaid += 1; e.pay_t = r * 55.0;
self.ents[i].happy -= 0.45; n += 1;
total += w;
} }
} if n > 0 || bill > 0.0 {
// training costs collected since the last payday
let bill = self.training_bill.min(self.gold).max(0.0);
self.gold -= bill;
total += bill;
self.training_bill = 0.0;
if paid + unpaid > 0 {
self.sfx_ui(Sfx::Payday); self.sfx_ui(Sfx::Payday);
if unpaid > 0 { self.msg("msg.payday_walk", vec![n.to_string(), (total as i32).to_string()]);
self.msg("msg.payday_partial", vec![paid.to_string(), (total as i32).to_string(), unpaid.to_string()]); }
}
// a creature goes for its wage: the nearest treasury tile holding treasure, else the core
fn start_fetch_wage(&mut self, e: &mut Ent) {
let cur = e.tile();
let goal = |i: usize| {
let t = &self.map.t[i];
(t.kind == Kind::Treasury && t.owner == Owner::Player && t.gold > 0) || (self.counts[0] == 0 && self.map.adj_core(i))
};
let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), goal, true) } else { self.map.search(cur, self.map.walk_cost(), goal, true) };
match found {
Some(p) => {
let t = p.last().copied().unwrap_or(cur);
e.path = p;
e.job = Job::FetchWage(t);
}
// no way to a treasury: the wage is paid out anyway
None => self.take_wage(e, None),
}
}
// takes the wage out of the treasury tile (the rest from elsewhere); unpaid = angry
fn take_wage(&mut self, e: &mut Ent, at: Option<usize>) {
let mut need = e.pay_due;
if let Some(t) = at {
let tile = &mut self.map.t[t];
if tile.kind == Kind::Treasury {
let take = (tile.gold as f32).min(need).floor();
tile.gold -= take as i32;
self.gold -= take;
need -= take;
}
}
let rest = need.min(self.gold.max(0.0));
self.gold -= rest;
need -= rest;
e.pay_due = 0.0;
if need > 0.5 {
e.happy -= 0.45;
self.msg("msg.wage_unpaid", vec![format!("@{}", st(e.kind).name)]);
self.sfx(Sfx::Error, e.x, e.y);
} else { } else {
self.msg("msg.payday", vec![paid.to_string(), (total as i32).to_string()]); e.happy = (e.happy + 0.2).min(1.0);
self.fx(5, e.x, e.y, 0.0, 0.0, 0.8, 0xff30d0ff);
self.sfx(Sfx::Deposit, e.x, e.y);
}
}
// a new creature: report at the portal, go to the lair (aerie), set up the bed
fn settle(&mut self, e: &mut Ent, dt: f32) {
let me = e.id + 1;
match e.job {
Job::Register => {
e.working = true;
e.timer -= dt;
if e.timer <= 0.0 {
let cur = e.tile();
let bed_kind = if flies(e.kind) { Kind::Aerie } else { Kind::Lair };
let free = |i: usize| self.map.t[i].kind == bed_kind && self.map.t[i].bed == 0;
let found = if flies(e.kind) { self.map.search(cur, self.map.fly_cost(), free, true) } else { self.map.search(cur, self.map.walk_cost(), free, true) };
match found {
Some(p) => {
let b = p.last().copied().unwrap_or(cur);
self.map.t[b].bed = me;
e.bed = Some(b);
e.path = p;
e.job = Job::MakeBed(b);
e.timer = 4.0;
}
None => {
e.settled = true;
e.job = Job::None;
}
}
}
}
Job::MakeBed(b) => {
if self.step_path(e, dt) {
return;
}
if self.map.t[b].bed != me {
e.settled = true;
e.job = Job::None;
return;
}
e.working = true;
e.timer -= dt;
if e.timer <= 0.0 {
e.settled = true;
e.job = Job::None;
let (bx, by) = xy(b);
self.fx(8, bx as f32 + 0.5, by as f32 + 0.5, 0.0, 0.0, 0.6, 0xff40d0ff);
self.sfx(Sfx::Build, bx as f32 + 0.5, by as f32 + 0.5);
}
}
_ => {
e.job = Job::Register;
e.timer = 2.0;
} }
} }
} }
@@ -2519,6 +2631,28 @@ impl Game {
} }
return; return;
} }
// arrival: report at the portal and set up the bed before anything else
if !e.settled {
self.settle(e, dt);
return;
}
// payday: the wage is fetched from a treasury
if e.pay_due > 0.0 {
e.pay_t -= dt;
if e.pay_t <= 0.0 && !matches!(e.job, Job::FetchWage(_) | Job::Leave | Job::Rest(_)) {
self.unreserve(me);
self.start_fetch_wage(e);
return;
}
}
if let Job::FetchWage(t) = e.job {
if self.step_path(e, dt) {
return;
}
self.take_wage(e, Some(t));
e.job = Job::None;
return;
}
if let Job::Rest(b) = e.job { if let Job::Rest(b) = e.job {
if self.step_path(e, dt) { if self.step_path(e, dt) {
return; return;
@@ -4159,6 +4293,9 @@ pub fn job_name(e: &Ent) -> &'static str {
Job::TearBridge(_) => "job.tear_bridge", Job::TearBridge(_) => "job.tear_bridge",
Job::Rest(_) => "job.rest", Job::Rest(_) => "job.rest",
Job::Torment(_) => "job.torment", Job::Torment(_) => "job.torment",
Job::Register => "job.register",
Job::MakeBed(_) => "job.make_bed",
Job::FetchWage(_) => "job.fetch_wage",
Job::Help(_) => "job.help", Job::Help(_) => "job.help",
Job::AttackCore => { Job::AttackCore => {
if e.working { if e.working {
@@ -4382,7 +4519,8 @@ mod tests {
#[test] #[test]
fn training_is_billed_on_payday() { fn training_is_billed_on_payday() {
let mut g = Game::new(3, 64, 64); let mut g = Game::new(3, 64, 64);
g.spawn(EK::Kobold, false, 10.0, 10.0, 1); let (cx, cy) = g.map.core;
g.spawn(EK::Kobold, false, (cx + 4) as f32 + 0.5, (cy + 4) as f32 + 0.5, 1);
g.training_bill = 50.0; g.training_bill = 50.0;
let cost = g.next_payday_cost(); let cost = g.next_payday_cost();
assert_eq!(cost, 50.0 + st(EK::Kobold).wage as f32); assert_eq!(cost, 50.0 + st(EK::Kobold).wage as f32);
@@ -4392,8 +4530,45 @@ mod tests {
assert!(g.payday_warned); assert!(g.payday_warned);
g.payday = 0.01; g.payday = 0.01;
g.update(0.1); g.update(0.1);
// the training bill is paid at once, the wage when the kobold fetches it
assert_eq!(g.training_bill, 0.0); assert_eq!(g.training_bill, 0.0);
assert!((gold - g.gold - cost).abs() < 1.0); assert!((gold - g.gold - 50.0).abs() < 1.0);
let mut fetched = false;
for _ in 0..(70 * 20) {
g.update(0.05);
fetched |= g.ents.iter().any(|e| matches!(e.job, Job::FetchWage(_)));
}
assert!(fetched, "the kobold walks to the treasury");
assert!((gold - g.gold - cost).abs() < 1.0, "paid {}", gold - g.gold);
assert!(g.ents.iter().all(|e| e.pay_due == 0.0));
}
#[test]
fn new_creatures_report_and_set_up_their_bed() {
let mut g = Game::new(3, 64, 64);
let (cx, cy) = g.map.core;
g.gold = 3000.0;
g.build_rect(1, cx - 3, cy - 3, cx - 2, cy + 1);
g.build_rect(2, cx + 4, cy - 3, cx + 5, cy + 1);
// arrives like from a portal (the test map's portal is not connected yet)
let id = g.spawn(EK::Kobold, false, (cx + 4) as f32 + 0.5, (cy + 4) as f32 + 0.5, 1);
if let Some(e) = g.ents.iter_mut().find(|e| e.id == id) {
e.settled = false;
e.job = Job::Register;
e.timer = 2.0;
}
let mut made_bed = false;
for _ in 0..(90 * 20) {
g.update(0.05);
let e = g.ents.iter().find(|e| e.id == id).unwrap();
made_bed |= matches!(e.job, Job::MakeBed(_)) && e.path.is_empty();
if e.settled {
break;
}
}
let e = g.ents.iter().find(|e| e.id == id).unwrap();
assert!(made_bed && e.settled);
assert_eq!(g.map.t[e.bed.unwrap()].kind, Kind::Lair);
} }
// a fortress east of the start area, joined to it by a claimed corridor // a fortress east of the start area, joined to it by a claimed corridor