From 7e84cae910d00aa5c2851e42ac9bad0ff7fcacbd Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 3 Nov 2026 10:43:33 +0100 Subject: [PATCH] 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 --- lang/de.lang | 15 +++- lang/en.lang | 15 +++- src/game.rs | 184 ++++++++++++++++++++++++++++++++++++++++++++++-- src/gfx.rs | 63 ++++++++++++++++- src/map.rs | 71 ++++++++++++++++--- src/render3d.rs | 26 ++++--- src/ui.rs | 19 ++++- 7 files changed, 362 insertions(+), 31 deletions(-) diff --git a/lang/de.lang b/lang/de.lang index 9f6bc17..a656711 100644 --- a/lang/de.lang +++ b/lang/de.lang @@ -180,7 +180,7 @@ tile.rock = Fels tile.indestructible = unzerstörbar tile.earth = Erde tile.gold = Goldader -tile.gold_amount = {0} Gold +tile.gold_amount = Wert: {0} Gold tile.floor = Boden owner.player = erobert owner.hero = Heldengebiet @@ -401,3 +401,16 @@ tile.bridge = Holzbrücke tile.lavabridge = Steinbrücke tile.shard = Splitter deines Seelenkerns tile.shard_info = Erobere diese Kammer, um den Splitter zurückzuholen. + +# treasures and payday +tile.silver = Silberader +tile.ruby = Rubinader +tile.diamond = Diamantader +res.gold = Gold +res.silver = Silber +res.ruby = Rubinen +res.diamond = Diamanten +treasury.content = Enthält {0} in {1} +hud.payday = Zahltag +msg.payday_soon = Zahltag in {0} Sekunden: Deine Kreaturen wollen {1} Gold. +hint.payday_short = Für den nächsten Zahltag reicht das Gold nicht. Unbezahlte Kreaturen werden wütend! diff --git a/lang/en.lang b/lang/en.lang index 9321448..bab6857 100644 --- a/lang/en.lang +++ b/lang/en.lang @@ -181,7 +181,7 @@ tile.rock = Rock tile.indestructible = indestructible tile.earth = Earth tile.gold = Gold Vein -tile.gold_amount = {0} gold +tile.gold_amount = Worth {0} gold tile.floor = Floor owner.player = claimed owner.hero = hero territory @@ -402,3 +402,16 @@ tile.bridge = Wooden bridge tile.lavabridge = Stone bridge tile.shard = Shard of your soul core tile.shard_info = Claim this chamber to take the shard back. + +# treasures and payday +tile.silver = Silver vein +tile.ruby = Ruby vein +tile.diamond = Diamond vein +res.gold = gold +res.silver = silver +res.ruby = rubies +res.diamond = diamonds +treasury.content = Holds {0} worth of {1} +hud.payday = Payday +msg.payday_soon = Payday in {0} seconds: your creatures want {1} gold. +hint.payday_short = There is not enough gold for the coming payday. Unpaid creatures get angry! diff --git a/src/game.rs b/src/game.rs index e312ff7..4a67c19 100644 --- a/src/game.rs +++ b/src/game.rs @@ -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 = 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::() + } + + #[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 diff --git a/src/gfx.rs b/src/gfx.rs index 8b53785..19368c5 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -6,7 +6,7 @@ use macroquad::prelude::*; pub const CELL: f32 = 16.0; const ATLAS_COLS: u32 = 16; -const ATLAS_ROWS: u32 = 8; +const ATLAS_ROWS: u32 = 9; // atlas indices (4 variants each) pub const T_ROCK: u32 = 0; @@ -56,6 +56,12 @@ pub const T_FORTWALL: u32 = 108; // 4 variants: knight fortress masonry pub const D_SHARD: u32 = 112; // shard of the soul core pub const D_BANNER: u32 = 113; // knight banner pub const I_BRIDGE: u32 = 114; +pub const T_SILVER: u32 = 115; // 2 variants each: veins in the earth +pub const T_RUBY: u32 = 117; +pub const T_DIAMOND: u32 = 119; +pub const D_SILVER: u32 = 121; // treasury content +pub const D_RUBY: u32 = 122; +pub const D_DIAMOND: u32 = 123; pub struct Gfx { pub atlas: Texture2D, @@ -643,6 +649,58 @@ fn gen_atlas() -> Image { None } }); + // ---- more treasures: veins (earth with embedded pieces) and treasury piles + let vein = |x: i32, y: i32, v: u32, seed: u32, hi: Color, mid: Color, lo: Color, size: f32| -> Color { + let mut best = 99.0f32; + for k in 0..5 { + let nx = 2 + (hash(k, 1, v + seed) % 12) as i32; + let ny = 2 + (hash(k, 2, v + seed) % 12) as i32; + best = best.min((((x - nx).abs() + (y - ny).abs()) as f32) - if k % 2 == 0 { 0.5 } else { 0.0 }); + } + if best < size * 0.4 { + hi + } else if best < size { + mid + } else if best < size + 0.8 { + lo + } else { + earth_px(x, y, v + seed) + } + }; + for v in 0..2u32 { + cv.cell(T_SILVER + v, |x, y| Some(vein(x, y, v, 170, c(245, 245, 255), c(180, 186, 200), c(100, 104, 118), 2.0))); + cv.cell(T_RUBY + v, |x, y| Some(vein(x, y, v, 180, c(255, 150, 170), c(210, 30, 60), c(110, 16, 36), 1.6))); + cv.cell(T_DIAMOND + v, |x, y| Some(vein(x, y, v, 190, c(255, 255, 255), c(150, 230, 255), c(70, 140, 190), 1.4))); + } + // treasury heaps, same dome shape as the gold pile: silver bars, rubies, diamonds + let heap = |x: i32, y: i32| -> Option { + let dx = (x as f32 - 7.5) / 7.0; + let dy = (y as f32 - 11.0) / 5.0; + let d = dx * dx + dy * dy; + if d < 1.0 && y <= 14 { + Some(d) + } else { + None + } + }; + cv.cell(D_SILVER, |x, y| { + let d = heap(x, y)?; + // bars: 4 px wide bricks in rows of 2 + let row = y / 2; + let off = if row % 2 == 1 { 2 } else { 0 }; + Some(if y % 2 == 0 { c(240, 242, 250) } else if (x + off) % 4 == 0 { c(110, 114, 128) } else if d > 0.6 { c(150, 156, 172) } else { c(196, 200, 216) }) + }); + let gems = |x: i32, y: i32, seed: u32, hi: Color, mid: Color, lo: Color| -> Option { + let d = heap(x, y)?; + // facetted stones: diamond shaped cells of 4 px + let (u, v) = (x + y, x - y + 16); + let (cu, cv) = (u / 4, v / 4); + let e = (u % 4 == 0) || (v % 4 == 0); + let h = hash(cu, cv, seed) % 3; + Some(if e { lo } else if h == 0 || (u % 4 == 1 && v % 4 == 1) { hi } else if d > 0.65 { lo } else { mid }) + }; + cv.cell(D_RUBY, |x, y| gems(x, y, 200, c(255, 150, 170), c(215, 30, 60), c(120, 12, 36))); + cv.cell(D_DIAMOND, |x, y| gems(x, y, 210, c(255, 255, 255), c(170, 232, 255), c(80, 150, 200))); cv.img } @@ -1010,6 +1068,9 @@ impl Gfx { Kind::Earth if reinforced => T_REINF + v, Kind::Earth => T_EARTH + v, Kind::Gold => T_GOLD + v, + Kind::Silver => T_SILVER + (v & 1), + Kind::Ruby => T_RUBY + (v & 1), + Kind::Diamond => T_DIAMOND + (v & 1), Kind::Floor => { if claimed { T_CLAIM + v diff --git a/src/map.rs b/src/map.rs index fa063dc..e7560c2 100644 --- a/src/map.rs +++ b/src/map.rs @@ -54,10 +54,13 @@ pub enum Kind { Bridge, // wooden bridge over water (walkable) LavaBridge, // stone bridge over lava (walkable) Shard, // floor with a shard of the soul core (campaign) + Silver, // silver vein: common, little value + Ruby, // ruby vein: rare, valuable + Diamond, // diamond vein: very rare, very valuable } // order matters for the packed savegame codec: only append new kinds -const KINDS: [Kind; 20] = [ +const KINDS: [Kind; 23] = [ Kind::Rock, Kind::Earth, Kind::Gold, @@ -78,8 +81,16 @@ const KINDS: [Kind; 20] = [ Kind::Bridge, Kind::LavaBridge, Kind::Shard, + Kind::Silver, + Kind::Ruby, + Kind::Diamond, ]; +// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond +pub const RES_GOLD: u8 = 0; +// value taken per pick strike for each treasure +pub const RES_STRIKE: [i32; 4] = [25, 20, 40, 60]; + #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)] pub enum Owner { None, @@ -112,17 +123,29 @@ pub struct Tile { pub var: u8, // texture variant #[serde(rename = "rf", default)] pub reinforced: bool, // wall reinforced by imps + #[serde(rename = "rs", default)] + pub res: u8, // treasury: kind of treasure stored here (see RES_GOLD) } impl Tile { pub fn new(kind: Kind) -> Tile { - Tile { kind, owner: Owner::None, hp: 0.0, gold: 0, marked: false, reserved: 0, food: 0.0, bed: 0, var: 0, reinforced: false } + Tile { kind, owner: Owner::None, hp: 0.0, gold: 0, marked: false, reserved: 0, food: 0.0, bed: 0, var: 0, reinforced: false, res: 0 } } pub fn solid(&self) -> bool { - matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold) + matches!(self.kind, Kind::Rock | Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond) } pub fn diggable(&self) -> bool { - matches!(self.kind, Kind::Earth | Kind::Gold) + matches!(self.kind, Kind::Earth | Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond) + } + // the treasure of a vein (None for everything else) + pub fn vein(&self) -> Option { + match self.kind { + Kind::Gold => Some(0), + Kind::Silver => Some(1), + Kind::Ruby => Some(2), + Kind::Diamond => Some(3), + _ => None, + } } pub fn walkable(&self) -> bool { matches!( @@ -329,7 +352,7 @@ impl Map { if d <= (r as f32 + 0.5).powi(2) - rng.f() * r as f32 { let tl = &mut t[y as usize * w + x as usize]; tl.kind = k; - if k == Kind::Gold { + if tl.vein().is_some() { tl.gold = rng.range(gold.0, gold.1); } } @@ -346,6 +369,21 @@ impl Map { let r = rng.range(1, 2); blob(&mut t, &mut rng, cx, cy, r, Kind::Gold, (250, 550)); } + // more treasures: silver is common, rubies are rare, diamonds very rare and far away + for _ in 0..scale(7.0) { + let (cx, cy) = (rng.range(4, wi - 5), rng.range(4, hi - 5)); + let r = rng.range(1, 2); + blob(&mut t, &mut rng, cx, cy, r, Kind::Silver, (200, 400)); + } + for _ in 0..scale(3.0) { + let (cx, cy) = (rng.range(wi / 4, wi - 5), rng.range(4, hi - 5)); + blob(&mut t, &mut rng, cx, cy, 1, Kind::Ruby, (500, 800)); + } + for _ in 0..scale(1.2).max(1) { + let (cx, cy) = (rng.range(wi / 3, wi - 5), rng.range(4, hi - 5)); + let r = rng.range(0, 1); + blob(&mut t, &mut rng, cx, cy, r, Kind::Diamond, (1000, 1500)); + } // guaranteed gold near the start for k in 0..3 { let cx = core.0 + rng.range(-3, 9); @@ -374,6 +412,22 @@ impl Map { // treasure caves: a cave surrounded by very rich gold veins (guarded by heroes, see Game::new) for &(cx, cy) in &treasures { blob(&mut t, &mut rng, cx, cy, 4, Kind::Gold, (1000, 1600)); + // some of the riches are gems + for y in cy - 4..=cy + 4 { + for x in cx - 4..=cx + 4 { + if inner(x, y) && t[y as usize * w + x as usize].kind == Kind::Gold { + let r = rng.f(); + let tl = &mut t[y as usize * w + x as usize]; + if r < 0.08 { + tl.kind = Kind::Diamond; + tl.gold = rng.range(1200, 1800); + } else if r < 0.3 { + tl.kind = Kind::Ruby; + tl.gold = rng.range(800, 1200); + } + } + } + } room(&mut t, cx - 2, cy - 2, cx + 2, cy + 2, Owner::None); } // starting area and core @@ -404,7 +458,7 @@ impl Map { } } for tl in t.iter_mut() { - if tl.kind == Kind::Gold { + if tl.vein().is_some() { tl.hp = 6.0; } } @@ -477,7 +531,7 @@ impl Map { match tl.kind { Kind::Earth if tl.reinforced => Some(70), Kind::Earth => Some(7), - Kind::Gold => Some(12), + Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond => Some(12), Kind::Rock | Kind::Core | Kind::Tower | Kind::Water | Kind::Lava => None, _ => Some(1), } @@ -552,7 +606,7 @@ fn tile_record(t: &Tile) -> [u8; REC] { let mut r = [0u8; REC]; r[0] = KINDS.iter().position(|&k| k == t.kind).unwrap_or(0) as u8; r[1] = OWNERS.iter().position(|&o| o == t.owner).unwrap_or(0) as u8; - r[2] = t.marked as u8 | (t.reinforced as u8) << 1; + r[2] = t.marked as u8 | (t.reinforced as u8) << 1 | (t.res & 3) << 2; r[3..7].copy_from_slice(&t.hp.to_le_bytes()); r[7..11].copy_from_slice(&t.gold.to_le_bytes()); r[11..15].copy_from_slice(&t.food.to_le_bytes()); @@ -616,6 +670,7 @@ fn tiles_de<'de, D: Deserializer<'de>>(d: D) -> Result, D::Error> { owner: *OWNERS.get(r[1] as usize).unwrap_or(&Owner::None), marked: r[2] & 1 != 0, reinforced: r[2] & 2 != 0, + res: (r[2] >> 2) & 3, hp: f32::from_le_bytes(f(3)), gold: i32::from_le_bytes(f(7)), food: f32::from_le_bytes(f(11)), diff --git a/src/render3d.rs b/src/render3d.rs index 1ecfe12..1d11bb1 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -14,7 +14,7 @@ pub const WALL_H: f32 = 0.95; const CHUNK: i32 = 16; pub const MAX_LIGHTS: usize = 24; const ATLAS_W: f32 = 256.0; -const ATLAS_H: f32 = 128.0; +const ATLAS_H: f32 = 144.0; const DISP_AMP: f32 = 0.11; // wall surface irregularity // ------------------------------------------------------------------ camera @@ -480,7 +480,7 @@ fn block_shell(mk: &mut MB, map: &Map, x: i32, z: i32, n: usize, fill: [u8; 4], } } -fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: &Texture2D, wall_sub: usize) -> [Option; 4] { +fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall_sub: usize) -> [Option; 4] { let mut mb = MB::new(); let ov = MB::new(); // textured overlays (currently unused) let mut ln = MB::new(); @@ -609,9 +609,12 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, gold_fill: f32, atlas: Kind::Library if z % 2 == 0 => mb.cross(xf + 0.5, zf + 0.5, 0.9, 0.85, D_SHELF, dc), Kind::Torture if (x + z) % 2 == 1 => mb.cross(xf + 0.5, zf + 0.5, 0.8, 0.8, D_RACK, dc), Kind::Temple if x % 3 == 1 && z % 3 == 1 => mb.cross(xf + 0.5, zf + 0.5, 0.8, 0.8, D_ALTAR, dc), - Kind::Treasury if gold_fill > 0.02 => { - let s = 0.35 + 0.55 * gold_fill; - mb.cross(xf + 0.5, zf + 0.5, s, s * 0.8, D_COINS, dc); + // each treasury tile shows what it holds (up to TREASURY_TILE) + Kind::Treasury if t.gold > 10 => { + let fill = (t.gold as f32 / TREASURY_TILE).clamp(0.0, 1.0); + let s = 0.35 + 0.55 * fill; + let sprite = [D_COINS, D_SILVER, D_RUBY, D_DIAMOND][(t.res & 3) as usize]; + mb.cross(xf + 0.5, zf + 0.5, s, s * 0.8, sprite, dc); } _ => {} } @@ -763,7 +766,7 @@ fn rgba(u: u32) -> Color { Color::from_rgba(u as u8, (u >> 8) as u8, (u >> 16) as u8, (u >> 24) as u8) } -fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 { +fn chunk_sig(map: &Map, cx: i32, cz: i32) -> u64 { let mut h: u64 = 1469598103934665603; for z in cz * CHUNK - 3..(cz + 1) * CHUNK + 3 { for x in cx * CHUNK - 3..(cx + 1) * CHUNK + 3 { @@ -771,7 +774,7 @@ fn chunk_sig(map: &Map, cx: i32, cz: i32, fill_bucket: u8) -> u64 { Some(t) => { let extra = match t.kind { Kind::Farm => t.food as u64, - Kind::Treasury => fill_bucket as u64, + Kind::Treasury => (t.gold / 50) as u64 | (t.res as u64) << 5, _ => 0, }; (t.kind as u64) | (t.owner as u64) << 5 | (t.marked as u64) << 7 | (t.reinforced as u64) << 8 | extra << 9 @@ -817,19 +820,17 @@ impl R3d { // Rebuilds visible chunks whose content changed (off-screen chunks are updated when they appear) fn update_chunks(&mut self, g: &Game, gfx: &Gfx, m: &Mat4) { self.ensure_chunks(); - let fill = (g.gold / g.gold_cap()).clamp(0.0, 1.0); - let bucket = (fill * 6.0) as u8; let mut amb: Option> = None; for cz in 0..self.ncz { for cx in 0..self.ncx { if !Self::chunk_visible(m, cx, cz) { continue; } - let sig = chunk_sig(&g.map, cx, cz, bucket); + let sig = chunk_sig(&g.map, cx, cz); let ch = &mut self.chunks[(cz * self.ncx + cx) as usize]; if ch.sig != sig { let a = amb.get_or_insert_with(|| ambient_field(&g.map)); - let [m, o, l, k] = build_chunk(&g.map, a, cx, cz, bucket as f32 / 6.0, &gfx.atlas, self.wall_sub); + let [m, o, l, k] = build_chunk(&g.map, a, cx, cz, &gfx.atlas, self.wall_sub); ch.mesh = m; ch.overlay = o; ch.lines = l; @@ -1525,6 +1526,9 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] { Kind::Earth if tile.reinforced => [120, 70, 70, 255], Kind::Earth => [86, 60, 40, 255], Kind::Gold => [220, 180, 50, 255], + Kind::Silver => [190, 195, 210, 255], + Kind::Ruby => [220, 40, 70, 255], + Kind::Diamond => [170, 235, 255, 255], Kind::Floor => match tile.owner { Owner::Player => [140, 60, 80, 255], Owner::Hero => [170, 175, 190, 255], diff --git a/src/ui.rs b/src/ui.rs index d671d0e..257e514 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -420,13 +420,21 @@ impl Ui { let wave_col = if g.wave_timer < 30.0 { RED } else { TXT }; let vol = if settings.muted { tr("hud.off") } else { format!("{}%", (settings.volume * 100.0).round() as i32) }; let mut segs: Vec> = vec![ - vec![txt(tr("hud.gold"), GOLD), Part::Gap, txt(format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT)], + vec![ + txt(tr("hud.gold"), GOLD), + Part::Gap, + txt(format!("{}/{}", g.gold as i32, g.gold_cap() as i32), TXT), + Part::Gap, + // what the next payday costs, in red + txt(format!("-{}", g.next_payday_cost().ceil() as i32), RED), + ], vec![txt(tr("hud.mana"), MANA), Part::Gap, Part::Bar(g.mana / MANA_MAX, MANA), Part::Gap, txt(format!("{:<3}", g.mana as i32), TXT)], vec![txt(tr("hud.core"), RED), Part::Gap, Part::Bar(g.core_hp / CORE_HP, RED)], vec![txt(tr("hud.gate"), BLUE), Part::Gap, Part::Bar(g.gate_hp / GATE_HP, BLUE)], vec![txt(tr("hud.creatures"), TXT), Part::Gap, txt(format!("{}/{}", g.creature_count(), cap), TXT)], vec![txt(tr("hud.workers"), TXT), Part::Gap, txt(g.worker_count().to_string(), TXT)], vec![txt(trf("hud.wave", &[(g.wave + 1).to_string()]), wave_col), Part::Gap, txt(mmss(g.wave_timer), TXT)], + vec![txt(tr("hud.payday"), if g.payday <= PAYDAY_WARN { RED } else { GOLD }), Part::Gap, txt(mmss(g.payday), TXT)], vec![txt(tr("hud.army"), TXT), Part::Gap, txt((g.army_value() as i32).to_string(), TXT)], ]; if !g.hand.is_empty() { @@ -970,8 +978,9 @@ pub fn hover_info(g: &Game, ent: Option, tile: Option<(i32, i32)>) -> Vec out.push((tr("tile.reinforced_info"), DIM)); } Kind::Earth => out.push((tr("tile.earth"), FRAME_HI)), - Kind::Gold => { - out.push((tr("tile.gold"), GOLD)); + Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond => { + let key = ["tile.gold", "tile.silver", "tile.ruby", "tile.diamond"][t.vein().unwrap_or(0) as usize]; + out.push((tr(key), GOLD)); out.push((trf("tile.gold_amount", &[t.gold.to_string()]), GOLD)); } Kind::Floor => { @@ -1021,6 +1030,10 @@ pub fn hover_info(g: &Game, ent: Option, tile: Option<(i32, i32)>) -> Vec out.push((tr(ROOM_NAMES[r]), FRAME_HI)); match k { Kind::Farm => out.push((trf("farm.food", &[(t.food as i32).to_string()]), GREEN)), + Kind::Treasury if t.gold > 0 => { + let what = ["res.gold", "res.silver", "res.ruby", "res.diamond"][(t.res & 3) as usize]; + out.push((trf("treasury.content", &[t.gold.to_string(), tr(what)]), GOLD)); + } Kind::Lair => out.push((tr(if t.bed != 0 { "lair.taken" } else { "lair.free" }), DIM)), Kind::Torture => { let used = g.ents.iter().filter(|e| e.alive && e.hero && e.captive != 0).count();