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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 10:43:33 +01:00
1 parent 75dc445d3b
commit 7e84cae910
7 files changed
+362 -31

No files matched your search

+14 -1
View File
@@ -180,7 +180,7 @@ tile.rock = Fels
tile.indestructible = unzerstörbar tile.indestructible = unzerstörbar
tile.earth = Erde tile.earth = Erde
tile.gold = Goldader tile.gold = Goldader
tile.gold_amount = {0} Gold tile.gold_amount = Wert: {0} Gold
tile.floor = Boden tile.floor = Boden
owner.player = erobert owner.player = erobert
owner.hero = Heldengebiet owner.hero = Heldengebiet
@@ -401,3 +401,16 @@ tile.bridge = Holzbrücke
tile.lavabridge = Steinbrücke tile.lavabridge = Steinbrücke
tile.shard = Splitter deines Seelenkerns tile.shard = Splitter deines Seelenkerns
tile.shard_info = Erobere diese Kammer, um den Splitter zurückzuholen. 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!
+14 -1
View File
@@ -181,7 +181,7 @@ tile.rock = Rock
tile.indestructible = indestructible tile.indestructible = indestructible
tile.earth = Earth tile.earth = Earth
tile.gold = Gold Vein tile.gold = Gold Vein
tile.gold_amount = {0} gold tile.gold_amount = Worth {0} gold
tile.floor = Floor tile.floor = Floor
owner.player = claimed owner.player = claimed
owner.hero = hero territory owner.hero = hero territory
@@ -402,3 +402,16 @@ tile.bridge = Wooden bridge
tile.lavabridge = Stone bridge tile.lavabridge = Stone bridge
tile.shard = Shard of your soul core tile.shard = Shard of your soul core
tile.shard_info = Claim this chamber to take the shard back. 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!
+178 -6
View File
@@ -6,6 +6,10 @@ use serde::{Deserialize, Serialize};
pub const CORE_HP: f32 = 2000.0; pub const CORE_HP: f32 = 2000.0;
pub const GATE_HP: f32 = 3000.0; pub const GATE_HP: f32 = 3000.0;
pub const PAYDAY: f32 = 150.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 FIRST_WAVE: f32 = 300.0;
pub const WAVE_GAP: f32 = 180.0; pub const WAVE_GAP: f32 = 180.0;
pub const MAX_SQUAD: usize = 18; pub const MAX_SQUAD: usize = 18;
@@ -169,6 +173,8 @@ pub struct Ent {
pub think: f32, pub think: f32,
pub repath: f32, pub repath: f32,
pub carry: i32, pub carry: i32,
#[serde(default)]
pub carry_res: u8, // treasure the imp carries (see map::RES_GOLD)
pub hunger: f32, pub hunger: f32,
pub energy: f32, pub energy: f32,
pub happy: f32, pub happy: f32,
@@ -220,6 +226,7 @@ impl Ent {
think: 0.0, think: 0.0,
repath: 0.0, repath: 0.0,
carry: 0, carry: 0,
carry_res: 0,
hunger: 0.0, hunger: 0.0,
energy: 1.0, energy: 1.0,
happy: 1.0, happy: 1.0,
@@ -311,6 +318,14 @@ pub struct Game {
pub entry_history: Vec<(i32, i32)>, // entry points of recent waves pub entry_history: Vec<(i32, i32)>, // entry points of recent waves
#[serde(default)] #[serde(default)]
pub planned_entries: Vec<(i32, i32)>, // entry points of the coming/current wave (minimap) 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)] #[serde(skip)]
pub hints: bool, // show hint messages (setting) pub hints: bool, // show hint messages (setting)
#[serde(skip, default = "hint_times")] #[serde(skip, default = "hint_times")]
@@ -407,6 +422,10 @@ impl Game {
planned_entries: Vec::new(), planned_entries: Vec::new(),
hints: true, hints: true,
hint_t: hint_times(), hint_t: hint_times(),
reserve: 0.0,
incoming: Vec::new(),
training_bill: 0.0,
payday_warned: false,
}; };
let (cx, cy) = g.map.core; let (cx, cy) = g.map.core;
// the soul core starts surrounded by a one tile wide ring of treasury // 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 { 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 { 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 cap = self.gold_cap();
let acc = amount.min((cap - self.gold).max(0.0)); let acc = amount.min((cap - self.gold).max(0.0));
self.gold += acc; self.gold += acc;
if acc > 0.0 {
self.incoming.push((res, acc));
}
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 { pub fn portals_owned(&self) -> usize {
(0..self.map.portals.len()).filter(|&k| self.map.portal_owned(k)).count() (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 { if self.state == 0 && !self.paused {
self.step(dt); self.step(dt);
} }
self.sync_treasury();
for f in self.fx.iter_mut() { for f in self.fx.iter_mut() {
f.t += dt; f.t += dt;
} }
@@ -651,8 +752,20 @@ impl Game {
} }
self.payday -= dt; 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 { if self.payday <= 0.0 {
self.payday = PAYDAY; self.payday = PAYDAY;
self.payday_warned = false;
self.do_payday(); self.do_payday();
} }
@@ -790,6 +903,11 @@ impl Game {
self.ents[i].happy -= 0.45; 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 { if paid + unpaid > 0 {
self.sfx_ui(Sfx::Payday); self.sfx_ui(Sfx::Payday);
if unpaid > 0 { if unpaid > 0 {
@@ -1532,6 +1650,13 @@ impl Game {
e.job = Job::None; e.job = Job::None;
return; 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); let (tx, ty) = xy(t);
e.face = sgn(tx as f32 + 0.5 - e.x); e.face = sgn(tx as f32 + 0.5 - e.x);
e.working = true; e.working = true;
@@ -1542,10 +1667,11 @@ impl Game {
let fxy = (e.y + ty as f32 + 0.5) * 0.5; let fxy = (e.y + ty as f32 + 0.5) * 0.5;
let tl = &mut self.map.t[t]; let tl = &mut self.map.t[t];
let done; let done;
if tl.kind == Kind::Gold { if let Some(res) = tl.vein() {
let take = tl.gold.min(25); let take = tl.gold.min(RES_STRIKE[res as usize]);
tl.gold -= take; tl.gold -= take;
e.carry += take; e.carry += take;
e.carry_res = res;
done = tl.gold <= 0; done = tl.gold <= 0;
self.fx(5, fxx, fxy, 0.0, 0.0, 0.5, 0xff30d0ff); self.fx(5, fxx, fxy, 0.0, 0.0, 0.5, 0xff30d0ff);
self.sfx(Sfx::GoldDig, fxx, fxy); self.sfx(Sfx::GoldDig, fxx, fxy);
@@ -1644,7 +1770,7 @@ impl Game {
Job::Deliver => { Job::Deliver => {
if self.is_deposit(cur) { if self.is_deposit(cur) {
let amount = e.carry as f32; let amount = e.carry as f32;
let got = self.add_gold(amount); let got = self.add_res(amount, e.carry_res);
if got < amount { if got < amount {
self.hint(3, "hint.treasury_full"); self.hint(3, "hint.treasury_full");
} }
@@ -1859,13 +1985,14 @@ impl Game {
} }
} }
Job::Train(t) => { 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); self.unreserve(me);
e.job = Job::None; e.job = Job::None;
return; return;
} }
e.working = true; e.working = true;
self.gold -= 1.5 * dt; self.training_bill += TRAIN_COST * dt;
e.xp += 7.0 * dt; e.xp += 7.0 * dt;
e.timer -= dt; e.timer -= dt;
if e.timer <= 0.0 { if e.timer <= 0.0 {
@@ -3359,6 +3486,51 @@ mod tests {
assert_eq!(g.map.hero_cost()(idx(rx + 1, cy)), None); 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] #[test]
fn some_imps_always_claim() { fn some_imps_always_claim() {
// lots of digging work: still at least 10% (min. one) of the imps claim the dug-out floor // lots of digging work: still at least 10% (min. one) of the imps claim the dug-out floor
+62 -1
View File
@@ -6,7 +6,7 @@ use macroquad::prelude::*;
pub const CELL: f32 = 16.0; pub const CELL: f32 = 16.0;
const ATLAS_COLS: u32 = 16; const ATLAS_COLS: u32 = 16;
const ATLAS_ROWS: u32 = 8; const ATLAS_ROWS: u32 = 9;
// atlas indices (4 variants each) // atlas indices (4 variants each)
pub const T_ROCK: u32 = 0; 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_SHARD: u32 = 112; // shard of the soul core
pub const D_BANNER: u32 = 113; // knight banner pub const D_BANNER: u32 = 113; // knight banner
pub const I_BRIDGE: u32 = 114; 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 struct Gfx {
pub atlas: Texture2D, pub atlas: Texture2D,
@@ -643,6 +649,58 @@ fn gen_atlas() -> Image {
None 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<f32> {
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<Color> {
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 cv.img
} }
@@ -1010,6 +1068,9 @@ impl Gfx {
Kind::Earth if reinforced => T_REINF + v, Kind::Earth if reinforced => T_REINF + v,
Kind::Earth => T_EARTH + v, Kind::Earth => T_EARTH + v,
Kind::Gold => T_GOLD + 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 => { Kind::Floor => {
if claimed { if claimed {
T_CLAIM + v T_CLAIM + v
+63 -8
View File
@@ -54,10 +54,13 @@ pub enum Kind {
Bridge, // wooden bridge over water (walkable) Bridge, // wooden bridge over water (walkable)
LavaBridge, // stone bridge over lava (walkable) LavaBridge, // stone bridge over lava (walkable)
Shard, // floor with a shard of the soul core (campaign) 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 // order matters for the packed savegame codec: only append new kinds
const KINDS: [Kind; 20] = [ const KINDS: [Kind; 23] = [
Kind::Rock, Kind::Rock,
Kind::Earth, Kind::Earth,
Kind::Gold, Kind::Gold,
@@ -78,8 +81,16 @@ const KINDS: [Kind; 20] = [
Kind::Bridge, Kind::Bridge,
Kind::LavaBridge, Kind::LavaBridge,
Kind::Shard, 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)] #[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
pub enum Owner { pub enum Owner {
None, None,
@@ -112,17 +123,29 @@ pub struct Tile {
pub var: u8, // texture variant pub var: u8, // texture variant
#[serde(rename = "rf", default)] #[serde(rename = "rf", default)]
pub reinforced: bool, // wall reinforced by imps 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 { impl Tile {
pub fn new(kind: Kind) -> 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 { 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 { 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<u8> {
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 { pub fn walkable(&self) -> bool {
matches!( matches!(
@@ -329,7 +352,7 @@ impl Map {
if d <= (r as f32 + 0.5).powi(2) - rng.f() * r as f32 { 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]; let tl = &mut t[y as usize * w + x as usize];
tl.kind = k; tl.kind = k;
if k == Kind::Gold { if tl.vein().is_some() {
tl.gold = rng.range(gold.0, gold.1); tl.gold = rng.range(gold.0, gold.1);
} }
} }
@@ -346,6 +369,21 @@ impl Map {
let r = rng.range(1, 2); let r = rng.range(1, 2);
blob(&mut t, &mut rng, cx, cy, r, Kind::Gold, (250, 550)); 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 // guaranteed gold near the start
for k in 0..3 { for k in 0..3 {
let cx = core.0 + rng.range(-3, 9); 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) // treasure caves: a cave surrounded by very rich gold veins (guarded by heroes, see Game::new)
for &(cx, cy) in &treasures { for &(cx, cy) in &treasures {
blob(&mut t, &mut rng, cx, cy, 4, Kind::Gold, (1000, 1600)); 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); room(&mut t, cx - 2, cy - 2, cx + 2, cy + 2, Owner::None);
} }
// starting area and core // starting area and core
@@ -404,7 +458,7 @@ impl Map {
} }
} }
for tl in t.iter_mut() { for tl in t.iter_mut() {
if tl.kind == Kind::Gold { if tl.vein().is_some() {
tl.hp = 6.0; tl.hp = 6.0;
} }
} }
@@ -477,7 +531,7 @@ impl Map {
match tl.kind { match tl.kind {
Kind::Earth if tl.reinforced => Some(70), Kind::Earth if tl.reinforced => Some(70),
Kind::Earth => Some(7), 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, Kind::Rock | Kind::Core | Kind::Tower | Kind::Water | Kind::Lava => None,
_ => Some(1), _ => Some(1),
} }
@@ -552,7 +606,7 @@ fn tile_record(t: &Tile) -> [u8; REC] {
let mut r = [0u8; REC]; let mut r = [0u8; REC];
r[0] = KINDS.iter().position(|&k| k == t.kind).unwrap_or(0) as u8; 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[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[3..7].copy_from_slice(&t.hp.to_le_bytes());
r[7..11].copy_from_slice(&t.gold.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()); 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<Vec<Tile>, D::Error> {
owner: *OWNERS.get(r[1] as usize).unwrap_or(&Owner::None), owner: *OWNERS.get(r[1] as usize).unwrap_or(&Owner::None),
marked: r[2] & 1 != 0, marked: r[2] & 1 != 0,
reinforced: r[2] & 2 != 0, reinforced: r[2] & 2 != 0,
res: (r[2] >> 2) & 3,
hp: f32::from_le_bytes(f(3)), hp: f32::from_le_bytes(f(3)),
gold: i32::from_le_bytes(f(7)), gold: i32::from_le_bytes(f(7)),
food: f32::from_le_bytes(f(11)), food: f32::from_le_bytes(f(11)),
+15 -11
View File
@@ -14,7 +14,7 @@ pub const WALL_H: f32 = 0.95;
const CHUNK: i32 = 16; const CHUNK: i32 = 16;
pub const MAX_LIGHTS: usize = 24; pub const MAX_LIGHTS: usize = 24;
const ATLAS_W: f32 = 256.0; 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 const DISP_AMP: f32 = 0.11; // wall surface irregularity
// ------------------------------------------------------------------ camera // ------------------------------------------------------------------ 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<Mesh>; 4] { fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall_sub: usize) -> [Option<Mesh>; 4] {
let mut mb = MB::new(); let mut mb = MB::new();
let ov = MB::new(); // textured overlays (currently unused) let ov = MB::new(); // textured overlays (currently unused)
let mut ln = MB::new(); 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::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::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::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 => { // each treasury tile shows what it holds (up to TREASURY_TILE)
let s = 0.35 + 0.55 * gold_fill; Kind::Treasury if t.gold > 10 => {
mb.cross(xf + 0.5, zf + 0.5, s, s * 0.8, D_COINS, dc); 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) 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; let mut h: u64 = 1469598103934665603;
for z in cz * CHUNK - 3..(cz + 1) * CHUNK + 3 { for z in cz * CHUNK - 3..(cz + 1) * CHUNK + 3 {
for x in cx * CHUNK - 3..(cx + 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) => { Some(t) => {
let extra = match t.kind { let extra = match t.kind {
Kind::Farm => t.food as u64, 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, _ => 0,
}; };
(t.kind as u64) | (t.owner as u64) << 5 | (t.marked as u64) << 7 | (t.reinforced as u64) << 8 | extra << 9 (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) // 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) { fn update_chunks(&mut self, g: &Game, gfx: &Gfx, m: &Mat4) {
self.ensure_chunks(); 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<Vec<f32>> = None; let mut amb: Option<Vec<f32>> = None;
for cz in 0..self.ncz { for cz in 0..self.ncz {
for cx in 0..self.ncx { for cx in 0..self.ncx {
if !Self::chunk_visible(m, cx, cz) { if !Self::chunk_visible(m, cx, cz) {
continue; 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]; let ch = &mut self.chunks[(cz * self.ncx + cx) as usize];
if ch.sig != sig { if ch.sig != sig {
let a = amb.get_or_insert_with(|| ambient_field(&g.map)); 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.mesh = m;
ch.overlay = o; ch.overlay = o;
ch.lines = l; 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 if tile.reinforced => [120, 70, 70, 255],
Kind::Earth => [86, 60, 40, 255], Kind::Earth => [86, 60, 40, 255],
Kind::Gold => [220, 180, 50, 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 { Kind::Floor => match tile.owner {
Owner::Player => [140, 60, 80, 255], Owner::Player => [140, 60, 80, 255],
Owner::Hero => [170, 175, 190, 255], Owner::Hero => [170, 175, 190, 255],
+16 -3
View File
@@ -420,13 +420,21 @@ impl Ui {
let wave_col = if g.wave_timer < 30.0 { RED } else { TXT }; 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 vol = if settings.muted { tr("hud.off") } else { format!("{}%", (settings.volume * 100.0).round() as i32) };
let mut segs: Vec<Vec<Part>> = vec![ let mut segs: Vec<Vec<Part>> = 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.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.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.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.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(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(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)], vec![txt(tr("hud.army"), TXT), Part::Gap, txt((g.army_value() as i32).to_string(), TXT)],
]; ];
if !g.hand.is_empty() { if !g.hand.is_empty() {
@@ -970,8 +978,9 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
out.push((tr("tile.reinforced_info"), DIM)); out.push((tr("tile.reinforced_info"), DIM));
} }
Kind::Earth => out.push((tr("tile.earth"), FRAME_HI)), Kind::Earth => out.push((tr("tile.earth"), FRAME_HI)),
Kind::Gold => { Kind::Gold | Kind::Silver | Kind::Ruby | Kind::Diamond => {
out.push((tr("tile.gold"), GOLD)); 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)); out.push((trf("tile.gold_amount", &[t.gold.to_string()]), GOLD));
} }
Kind::Floor => { Kind::Floor => {
@@ -1021,6 +1030,10 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
out.push((tr(ROOM_NAMES[r]), FRAME_HI)); out.push((tr(ROOM_NAMES[r]), FRAME_HI));
match k { match k {
Kind::Farm => out.push((trf("farm.food", &[(t.food as i32).to_string()]), GREEN)), 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::Lair => out.push((tr(if t.bed != 0 { "lair.taken" } else { "lair.free" }), DIM)),
Kind::Torture => { Kind::Torture => {
let used = g.ents.iter().filter(|e| e.alive && e.hero && e.captive != 0).count(); let used = g.ents.iter().filter(|e| e.alive && e.hero && e.captive != 0).count();