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:
1 parent
f13ab1a629
commit
52c4d1caf8
3 files changed
+212
-27
No files matched your search
+202
-27
@@ -171,6 +171,9 @@ pub enum Job {
|
||||
TearBridge(usize), // knight tears down a bridge of the player
|
||||
Rest(usize), // flying creature recovers in the aerie (flees when badly hurt)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -228,6 +231,16 @@ pub struct Ent {
|
||||
pub patrol: Vec<(i32, i32)>, // fortress patrol: route of tiles, walked in a loop
|
||||
#[serde(default)]
|
||||
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 {
|
||||
@@ -273,6 +286,9 @@ impl Ent {
|
||||
home: None,
|
||||
patrol: Vec::new(),
|
||||
patrol_k: 0,
|
||||
settled: true,
|
||||
pay_due: 0.0,
|
||||
pay_t: 0.0,
|
||||
}
|
||||
}
|
||||
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 pk = owned[(self.rng.next() as usize) % owned.len()];
|
||||
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.sfx(Sfx::PortalArrive, px as f32 + 1.5, py as f32 + 1.5);
|
||||
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) {
|
||||
let mut paid = 0;
|
||||
let mut unpaid = 0;
|
||||
let mut total = 0.0;
|
||||
let bill = self.training_bill.min(self.gold).max(0.0);
|
||||
self.gold -= bill;
|
||||
self.training_bill = 0.0;
|
||||
let mut n = 0;
|
||||
let mut total = bill;
|
||||
for i in 0..self.ents.len() {
|
||||
if !self.ents[i].alive || !self.ents[i].creature() {
|
||||
continue;
|
||||
}
|
||||
let e = &self.ents[i];
|
||||
let r = self.rng.f();
|
||||
let e = &mut self.ents[i];
|
||||
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];
|
||||
e.happy = (e.happy + 0.2).min(1.0);
|
||||
} else {
|
||||
unpaid += 1;
|
||||
self.ents[i].happy -= 0.45;
|
||||
e.pay_due += w;
|
||||
e.pay_t = r * 55.0;
|
||||
n += 1;
|
||||
total += w;
|
||||
}
|
||||
if n > 0 || bill > 0.0 {
|
||||
self.sfx_ui(Sfx::Payday);
|
||||
self.msg("msg.payday_walk", vec![n.to_string(), (total as i32).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;
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
if unpaid > 0 {
|
||||
self.msg("msg.payday_partial", vec![paid.to_string(), (total as i32).to_string(), unpaid.to_string()]);
|
||||
} else {
|
||||
self.msg("msg.payday", vec![paid.to_string(), (total as i32).to_string()]);
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
// 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 self.step_path(e, dt) {
|
||||
return;
|
||||
@@ -4159,6 +4293,9 @@ pub fn job_name(e: &Ent) -> &'static str {
|
||||
Job::TearBridge(_) => "job.tear_bridge",
|
||||
Job::Rest(_) => "job.rest",
|
||||
Job::Torment(_) => "job.torment",
|
||||
Job::Register => "job.register",
|
||||
Job::MakeBed(_) => "job.make_bed",
|
||||
Job::FetchWage(_) => "job.fetch_wage",
|
||||
Job::Help(_) => "job.help",
|
||||
Job::AttackCore => {
|
||||
if e.working {
|
||||
@@ -4382,7 +4519,8 @@ mod tests {
|
||||
#[test]
|
||||
fn training_is_billed_on_payday() {
|
||||
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;
|
||||
let cost = g.next_payday_cost();
|
||||
assert_eq!(cost, 50.0 + st(EK::Kobold).wage as f32);
|
||||
@@ -4392,8 +4530,45 @@ mod tests {
|
||||
assert!(g.payday_warned);
|
||||
g.payday = 0.01;
|
||||
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!((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
|
||||
|
||||
Reference in new issue
Block a user