Silver, ruby and diamond veins; treasury shows its treasures; announced payday
- New veins (appended kinds): silver (common), ruby (rare), diamond (very rare, far from the core); treasure caves contain gems; each treasure is worth a different amount per strike and per vein - Imps carry one kind of treasure at a time - Treasury tiles hold up to 500 each and show gold coins, silver bars, rubies or diamonds; spending empties random treasury tiles, the soul core keeps what does not fit - Training no longer drains gold continuously: it is billed on the next payday - Payday is announced 20 s before (message with the cost); the HUD shows a payday countdown and the cost of the next payday in red next to the gold counter - Tests for the treasury and the payday bill Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
75dc445d3b
commit
7e84cae910
7 files changed
+362
-31
No files matched your search
+178
-6
@@ -6,6 +6,10 @@ use serde::{Deserialize, Serialize};
|
||||
pub const CORE_HP: f32 = 2000.0;
|
||||
pub const GATE_HP: f32 = 3000.0;
|
||||
pub const PAYDAY: f32 = 150.0;
|
||||
pub const PAYDAY_WARN: f32 = 20.0; // the payday is announced this many seconds before
|
||||
pub const TREASURY_TILE: f32 = 500.0; // value one treasury tile holds
|
||||
pub const CORE_STORE: f32 = 2000.0; // value the soul core holds without any treasury
|
||||
pub const TRAIN_COST: f32 = 1.5; // gold per second of training (billed on payday)
|
||||
pub const FIRST_WAVE: f32 = 300.0;
|
||||
pub const WAVE_GAP: f32 = 180.0;
|
||||
pub const MAX_SQUAD: usize = 18;
|
||||
@@ -169,6 +173,8 @@ pub struct Ent {
|
||||
pub think: f32,
|
||||
pub repath: f32,
|
||||
pub carry: i32,
|
||||
#[serde(default)]
|
||||
pub carry_res: u8, // treasure the imp carries (see map::RES_GOLD)
|
||||
pub hunger: f32,
|
||||
pub energy: f32,
|
||||
pub happy: f32,
|
||||
@@ -220,6 +226,7 @@ impl Ent {
|
||||
think: 0.0,
|
||||
repath: 0.0,
|
||||
carry: 0,
|
||||
carry_res: 0,
|
||||
hunger: 0.0,
|
||||
energy: 1.0,
|
||||
happy: 1.0,
|
||||
@@ -311,6 +318,14 @@ pub struct Game {
|
||||
pub entry_history: Vec<(i32, i32)>, // entry points of recent waves
|
||||
#[serde(default)]
|
||||
pub planned_entries: Vec<(i32, i32)>, // entry points of the coming/current wave (minimap)
|
||||
#[serde(default)]
|
||||
pub reserve: f32, // value stored in the soul core (the rest lies in the treasury tiles)
|
||||
#[serde(skip)]
|
||||
pub incoming: Vec<(u8, f32)>, // treasures added since the last treasury update
|
||||
#[serde(default)]
|
||||
pub training_bill: f32, // training costs, paid on the next payday
|
||||
#[serde(default)]
|
||||
pub payday_warned: bool,
|
||||
#[serde(skip)]
|
||||
pub hints: bool, // show hint messages (setting)
|
||||
#[serde(skip, default = "hint_times")]
|
||||
@@ -407,6 +422,10 @@ impl Game {
|
||||
planned_entries: Vec::new(),
|
||||
hints: true,
|
||||
hint_t: hint_times(),
|
||||
reserve: 0.0,
|
||||
incoming: Vec::new(),
|
||||
training_bill: 0.0,
|
||||
payday_warned: false,
|
||||
};
|
||||
let (cx, cy) = g.map.core;
|
||||
// the soul core starts surrounded by a one tile wide ring of treasury
|
||||
@@ -533,16 +552,97 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn gold_cap(&self) -> f32 {
|
||||
2000.0 + 500.0 * self.counts[0] as f32
|
||||
CORE_STORE + TREASURY_TILE * self.counts[0] as f32
|
||||
}
|
||||
|
||||
pub fn add_gold(&mut self, amount: f32) -> f32 {
|
||||
self.add_res(amount, RES_GOLD)
|
||||
}
|
||||
|
||||
// adds treasure of one kind (as far as there is room); returns the value accepted
|
||||
pub fn add_res(&mut self, amount: f32, res: u8) -> f32 {
|
||||
let cap = self.gold_cap();
|
||||
let acc = amount.min((cap - self.gold).max(0.0));
|
||||
self.gold += acc;
|
||||
if acc > 0.0 {
|
||||
self.incoming.push((res, acc));
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
// what the next payday costs: wages of all creatures plus the training bill
|
||||
pub fn next_payday_cost(&self) -> f32 {
|
||||
let wages: f32 = self.ents.iter().filter(|e| e.alive && e.creature()).map(|e| st(e.kind).wage as f32 * (1.0 + 0.1 * (e.level - 1) as f32)).sum();
|
||||
wages + self.training_bill
|
||||
}
|
||||
|
||||
// The gold counter is the truth; the treasury tiles show where it lies. Added treasure fills
|
||||
// tiles of the same kind first, then empty ones, the rest goes into the core. Spending takes
|
||||
// from random treasury tiles first, then from the core.
|
||||
pub fn sync_treasury(&mut self) {
|
||||
let mut tiles = Vec::new();
|
||||
let mut stored = 0.0;
|
||||
for i in 0..self.map.t.len() {
|
||||
let t = &mut self.map.t[i];
|
||||
if t.kind == Kind::Treasury && t.owner == Owner::Player {
|
||||
tiles.push(i);
|
||||
stored += t.gold as f32;
|
||||
} else if t.gold != 0 && t.vein().is_none() && t.kind != Kind::Treasury {
|
||||
t.gold = 0; // a sold treasury tile loses its content
|
||||
t.res = 0;
|
||||
}
|
||||
}
|
||||
let diff = self.gold - (self.reserve + stored);
|
||||
let incoming = std::mem::take(&mut self.incoming);
|
||||
if diff < -0.5 {
|
||||
let mut need = -diff;
|
||||
while need > 0.5 {
|
||||
let full: Vec<usize> = tiles.iter().copied().filter(|&i| self.map.t[i].gold > 0).collect();
|
||||
if full.is_empty() {
|
||||
break;
|
||||
}
|
||||
let i = full[(self.rng.next() as usize) % full.len()];
|
||||
let t = &mut self.map.t[i];
|
||||
let take = (t.gold as f32).min(need.ceil());
|
||||
t.gold -= take as i32;
|
||||
need -= take;
|
||||
}
|
||||
self.reserve = (self.reserve - need.max(0.0)).max(0.0);
|
||||
} else if diff > 0.5 {
|
||||
let mut left = diff;
|
||||
let mut parts: Vec<(u8, f32)> = incoming;
|
||||
let typed: f32 = parts.iter().map(|p| p.1).sum();
|
||||
if typed < left {
|
||||
parts.push((RES_GOLD, left - typed));
|
||||
}
|
||||
for (res, amount) in parts {
|
||||
let mut a = amount.min(left);
|
||||
left -= a;
|
||||
// tiles holding this treasure first, then empty tiles
|
||||
for pass in 0..2 {
|
||||
for &i in &tiles {
|
||||
if a <= 0.5 {
|
||||
break;
|
||||
}
|
||||
let t = &mut self.map.t[i];
|
||||
let fits = if pass == 0 { t.gold > 0 && t.res == res } else { t.gold <= 0 };
|
||||
if !fits {
|
||||
continue;
|
||||
}
|
||||
let room = TREASURY_TILE - t.gold as f32;
|
||||
let put = a.min(room).floor();
|
||||
if put >= 1.0 {
|
||||
t.gold += put as i32;
|
||||
t.res = res;
|
||||
a -= put;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.reserve += a.max(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn portals_owned(&self) -> usize {
|
||||
(0..self.map.portals.len()).filter(|&k| self.map.portal_owned(k)).count()
|
||||
}
|
||||
@@ -624,6 +724,7 @@ impl Game {
|
||||
if self.state == 0 && !self.paused {
|
||||
self.step(dt);
|
||||
}
|
||||
self.sync_treasury();
|
||||
for f in self.fx.iter_mut() {
|
||||
f.t += dt;
|
||||
}
|
||||
@@ -651,8 +752,20 @@ impl Game {
|
||||
}
|
||||
|
||||
self.payday -= dt;
|
||||
if !self.payday_warned && self.payday <= PAYDAY_WARN {
|
||||
self.payday_warned = true;
|
||||
let cost = self.next_payday_cost();
|
||||
if cost >= 1.0 {
|
||||
self.msg("msg.payday_soon", vec![(self.payday.ceil() as i32).to_string(), (cost.ceil() as i32).to_string()]);
|
||||
self.sfx_ui(Sfx::FlagSet);
|
||||
if cost > self.gold {
|
||||
self.hint(22, "hint.payday_short");
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.payday <= 0.0 {
|
||||
self.payday = PAYDAY;
|
||||
self.payday_warned = false;
|
||||
self.do_payday();
|
||||
}
|
||||
|
||||
@@ -790,6 +903,11 @@ impl Game {
|
||||
self.ents[i].happy -= 0.45;
|
||||
}
|
||||
}
|
||||
// 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 {
|
||||
@@ -1532,6 +1650,13 @@ impl Game {
|
||||
e.job = Job::None;
|
||||
return;
|
||||
}
|
||||
// an imp carries one kind of treasure: other loads are delivered first
|
||||
if tile.vein().map_or(false, |r| e.carry > 0 && r != e.carry_res) {
|
||||
self.unreserve(me);
|
||||
e.job = Job::None;
|
||||
self.start_deliver(e);
|
||||
return;
|
||||
}
|
||||
let (tx, ty) = xy(t);
|
||||
e.face = sgn(tx as f32 + 0.5 - e.x);
|
||||
e.working = true;
|
||||
@@ -1542,10 +1667,11 @@ impl Game {
|
||||
let fxy = (e.y + ty as f32 + 0.5) * 0.5;
|
||||
let tl = &mut self.map.t[t];
|
||||
let done;
|
||||
if tl.kind == Kind::Gold {
|
||||
let take = tl.gold.min(25);
|
||||
if let Some(res) = tl.vein() {
|
||||
let take = tl.gold.min(RES_STRIKE[res as usize]);
|
||||
tl.gold -= take;
|
||||
e.carry += take;
|
||||
e.carry_res = res;
|
||||
done = tl.gold <= 0;
|
||||
self.fx(5, fxx, fxy, 0.0, 0.0, 0.5, 0xff30d0ff);
|
||||
self.sfx(Sfx::GoldDig, fxx, fxy);
|
||||
@@ -1644,7 +1770,7 @@ impl Game {
|
||||
Job::Deliver => {
|
||||
if self.is_deposit(cur) {
|
||||
let amount = e.carry as f32;
|
||||
let got = self.add_gold(amount);
|
||||
let got = self.add_res(amount, e.carry_res);
|
||||
if got < amount {
|
||||
self.hint(3, "hint.treasury_full");
|
||||
}
|
||||
@@ -1859,13 +1985,14 @@ impl Game {
|
||||
}
|
||||
}
|
||||
Job::Train(t) => {
|
||||
if cur != t || self.map.t[t].kind != Kind::Training || self.gold < 1.0 {
|
||||
// training is billed on payday; it stops when the bill would exceed the gold
|
||||
if cur != t || self.map.t[t].kind != Kind::Training || self.training_bill + 1.0 > self.gold {
|
||||
self.unreserve(me);
|
||||
e.job = Job::None;
|
||||
return;
|
||||
}
|
||||
e.working = true;
|
||||
self.gold -= 1.5 * dt;
|
||||
self.training_bill += TRAIN_COST * dt;
|
||||
e.xp += 7.0 * dt;
|
||||
e.timer -= dt;
|
||||
if e.timer <= 0.0 {
|
||||
@@ -3359,6 +3486,51 @@ mod tests {
|
||||
assert_eq!(g.map.hero_cost()(idx(rx + 1, cy)), None);
|
||||
}
|
||||
|
||||
fn treasury_total(g: &Game) -> f32 {
|
||||
g.reserve + g.map.t.iter().filter(|t| t.kind == Kind::Treasury && t.owner == Owner::Player).map(|t| t.gold as f32).sum::<f32>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn treasury_shows_its_treasures() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
g.update(0.01);
|
||||
// the starting gold lies in the treasury ring
|
||||
assert!((treasury_total(&g) - g.gold).abs() < 2.0);
|
||||
assert_eq!(g.reserve, 0.0);
|
||||
// rubies fill their own tiles
|
||||
g.add_res(1200.0, 2);
|
||||
g.update(0.01);
|
||||
let rubies: i32 = g.map.t.iter().filter(|t| t.kind == Kind::Treasury && t.res == 2).map(|t| t.gold).sum();
|
||||
assert_eq!(rubies, 1200);
|
||||
assert!(g.map.t.iter().all(|t| t.gold <= TREASURY_TILE as i32 || t.vein().is_some()));
|
||||
// spending empties tiles (randomly), the total always matches the gold counter
|
||||
g.gold -= 1700.0;
|
||||
g.update(0.01);
|
||||
assert!((treasury_total(&g) - g.gold).abs() < 2.0);
|
||||
// more than the treasury holds: the core keeps the rest
|
||||
g.gold = g.gold_cap();
|
||||
g.update(0.01);
|
||||
assert!((treasury_total(&g) - g.gold).abs() < 2.0);
|
||||
assert!(g.reserve > 0.0 && g.reserve <= CORE_STORE + 1.0);
|
||||
}
|
||||
|
||||
#[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);
|
||||
g.training_bill = 50.0;
|
||||
let cost = g.next_payday_cost();
|
||||
assert_eq!(cost, 50.0 + st(EK::Kobold).wage as f32);
|
||||
let gold = g.gold;
|
||||
g.payday = PAYDAY_WARN + 0.05;
|
||||
g.update(0.1);
|
||||
assert!(g.payday_warned);
|
||||
g.payday = 0.01;
|
||||
g.update(0.1);
|
||||
assert_eq!(g.training_bill, 0.0);
|
||||
assert!((gold - g.gold - cost).abs() < 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn some_imps_always_claim() {
|
||||
// lots of digging work: still at least 10% (min. one) of the imps claim the dug-out floor
|
||||
|
||||
Reference in new issue
Block a user