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.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!
+14 -1
View File
@@ -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!
+178 -6
View File
@@ -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
+62 -1
View File
@@ -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<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
}
@@ -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
+63 -8
View File
@@ -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<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 {
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<Vec<Tile>, 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)),
+15 -11
View File
@@ -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<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 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<Vec<f32>> = 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],
+16 -3
View File
@@ -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<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.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<usize>, 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<usize>, 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();