diff --git a/lang/de.lang b/lang/de.lang index d79694b..c225594 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -571,3 +571,8 @@ account.users_changed = Benutzer aktualisiert. 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.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! diff --git a/lang/en.lang b/lang/en.lang index 4183fc1..4d14710 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -572,3 +572,8 @@ account.users_changed = Users updated. 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.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! diff --git a/src/game.rs b/src/game.rs index 437713b..ff839a1 100644 --- a/src/game.rs +++ b/src/game.rs @@ -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 = (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) { + 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