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
|
||||
|
||||
+62
-1
@@ -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
@@ -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
@@ -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],
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user