Room levels 2 and 3, researched in the library: a quarter better each, brighter, with gold trim

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 23:44:03 +01:00
1 parent f7b550a6b1
commit 9b49bc2c9d
23 files changed
+171 -10

No files matched your search

+35 -8
View File
@@ -486,6 +486,7 @@ pub struct MasterState {
pub incoming: Vec<(u8, f32, Option<usize>)>,
pub training_bill: f32,
pub counts: [usize; N_ROOMS],
pub room_level: Vec<u8>,
pub springs: usize,
pub known: Vec<bool>,
pub researchable: Vec<bool>,
@@ -522,6 +523,8 @@ pub struct Game {
pub hand: Vec<u32>,
#[serde(skip)]
pub counts: [usize; N_ROOMS], // room tiles per room kind (ROOM_KINDS order, recounted)
#[serde(default)]
pub room_level: Vec<u8>, // level of every room kind (ROOM_KINDS order; 1 when missing), researched
pub gate_hp: f32,
pub gate_hit_t: f32,
pub gate_stage: u32, // defender thresholds triggered (0..3)
@@ -783,6 +786,18 @@ pub fn room_index(k: Kind) -> Option<usize> {
ROOM_KINDS.iter().position(|&r| r == k)
}
impl Game {
// the researched level of a room kind of the master being processed (1, 2 or 3)
pub fn room_level_of(&self, k: Kind) -> u8 {
room_index(k).and_then(|r| self.room_level.get(r).copied()).unwrap_or(1).max(1)
}
// how much better a room works at its level: +25% per level above the first
pub fn room_bonus(&self, k: Kind) -> f32 {
1.0 + 0.25 * (self.room_level_of(k) - 1) as f32
}
}
pub(crate) fn adj4(a: usize, b: usize) -> bool {
let (ax, ay) = xy(a);
let (bx, by) = xy(b);
@@ -866,6 +881,7 @@ impl Game {
paused: false,
hand: Vec::new(),
counts: [0; N_ROOMS],
room_level: Vec::new(),
gate_hp: GATE_HP,
gate_hit_t: -100.0,
gate_stage: 0,
@@ -1126,7 +1142,7 @@ impl Game {
}
pub fn gold_cap(&self) -> f32 {
CORE_STORE + TREASURY_TILE * self.counts[0] as f32
CORE_STORE + TREASURY_TILE * self.counts[0] as f32 * self.room_bonus(Kind::Treasury)
}
pub fn add_gold(&mut self, amount: f32) -> f32 {
@@ -1433,6 +1449,7 @@ impl Game {
incoming: std::mem::take(&mut self.incoming),
training_bill: self.training_bill,
counts: self.counts,
room_level: self.room_level.clone(),
springs: self.springs,
known: std::mem::take(&mut self.known),
researchable: std::mem::take(&mut self.researchable),
@@ -1463,6 +1480,7 @@ impl Game {
self.incoming = m.incoming;
self.training_bill = m.training_bill;
self.counts = m.counts;
self.room_level = m.room_level;
self.springs = m.springs;
self.known = m.known;
self.researchable = m.researchable;
@@ -1606,9 +1624,17 @@ impl Game {
return;
}
self.time += dt;
// mushroom gardens grow faster at a higher level (of the master who owns them)
let farm = Kind::Farm;
let lv: Vec<(Owner, f32)> = self.master_sides().into_iter().map(|s| (Owner::of_side(s), {
let r = room_index(farm).unwrap_or(0);
let l = if s == self.me_side { self.room_level.get(r).copied() } else { self.masters.iter().find(|m| m.side == s).and_then(|m| m.room_level.get(r).copied()) };
1.0 + 0.25 * (l.unwrap_or(1).max(1) - 1) as f32
})).collect();
for t in self.map.t.iter_mut() {
if t.kind == Kind::Farm {
t.food = (t.food + dt / 18.0).min(3.0);
let f = lv.iter().find(|x| x.0 == t.owner).map_or(1.0, |x| x.1);
t.food = (t.food + dt * f / 18.0).min(3.0);
}
}
for side in self.master_sides() {
@@ -3798,8 +3824,9 @@ impl Game {
}
// resting heals: quickly in a bed room, slower next to the core; back to work at 75%
let bed_room = matches!(self.map.t[b].kind, Kind::Lair | Kind::Aerie);
e.hp = (e.hp + e.max_hp * dt / if bed_room { 12.0 } else { 24.0 }).min(e.max_hp);
e.energy = (e.energy + dt / 14.0).min(1.0);
let rest = if bed_room { self.room_bonus(self.map.t[b].kind) } else { 1.0 };
e.hp = (e.hp + e.max_hp * dt * rest / if bed_room { 12.0 } else { 24.0 }).min(e.max_hp);
e.energy = (e.energy + dt * rest / 14.0).min(1.0);
if e.hp >= e.max_hp * 0.75 {
e.job = Job::None;
}
@@ -3857,7 +3884,7 @@ impl Game {
if tl.food >= 1.0 {
tl.food -= 1.0;
e.hunger = 0.0;
e.hp = (e.hp + e.max_hp * 0.2).min(e.max_hp);
e.hp = (e.hp + e.max_hp * 0.2 * self.room_bonus(Kind::Temple)).min(e.max_hp);
}
self.unreserve(me);
e.job = Job::None;
@@ -3887,7 +3914,7 @@ impl Game {
}
e.working = true;
self.training_bill += TRAIN_COST * dt;
e.xp += 7.0 * dt;
e.xp += 7.0 * dt * self.room_bonus(Kind::Training);
e.timer -= dt;
if e.timer <= 0.0 {
self.unreserve(me);
@@ -3916,7 +3943,7 @@ impl Game {
}
e.working = true;
let rate = if e.kind == EK::Cogling { 2.5 } else { 1.0 } * (1.0 + 0.1 * (e.level - 1) as f32);
self.forge_work += rate * dt;
self.forge_work += rate * dt * self.room_bonus(Kind::Forge);
e.xp += 2.5 * dt;
e.timer -= dt;
if e.timer <= 0.0 {
@@ -3939,7 +3966,7 @@ impl Game {
EK::Owl => 1.3,
_ => 0.5,
} * (1.0 + 0.12 * (e.level - 1) as f32);
self.add_research(points * dt);
self.add_research(points * dt * self.room_bonus(Kind::Library));
e.xp += 3.0 * dt;
e.timer -= dt;
if e.timer <= 0.0 {
+3 -1
View File
@@ -352,6 +352,8 @@ pub struct Map {
pub visible: Vec<bool>, // fog of war: tiles in sight right now
#[serde(default, with = "crate::fog::bits")]
pub sky: Vec<bool>, // tiles above ground (daylight, grass); empty: all underground
#[serde(default)]
pub upgrades: Vec<(u8, u8, u8)>, // researched room levels for the look: (master side, room index, level)
}
fn default_size() -> usize {
@@ -604,7 +606,7 @@ impl Map {
tl.hp = 6.0;
}
}
Map { w, h, t, core, portals, legacy_portal: None, gate, treasures, explored: Vec::new(), visible: Vec::new(), sky: Vec::new() }
Map { w, h, t, core, portals, legacy_portal: None, gate, treasures, explored: Vec::new(), visible: Vec::new(), sky: Vec::new(), upgrades: Vec::new() }
}
// Carves a meandering river; the player's area, the core and portals stay dry.
+38
View File
@@ -440,6 +440,15 @@ impl MB {
}
}
// the researched level of the room on tile t (1: none)
fn room_level_at(map: &Map, t: &Tile) -> u8 {
if map.upgrades.is_empty() {
return 1;
}
let (Some(r), side) = (room_index(t.kind), t.owner.side()) else { return 1 };
map.upgrades.iter().find(|u| u.0 == side && u.1 as usize == r).map_or(1, |u| u.2)
}
// Ambient light field: own territory is lit, foreign areas are dim
fn ambient_field(map: &Map) -> Vec<f32> {
let mut f = vec![0.0f32; n()];
@@ -453,6 +462,11 @@ fn ambient_field(map: &Map) -> Vec<f32> {
if map.sky(i) {
f[i] = 1.6;
}
// rooms of a higher level shine brighter
let lv = room_level_at(map, t);
if lv > 1 && f[i] > 0.0 {
f[i] += 0.25 * (lv - 1) as f32;
}
if t.river() == Some(Kind::Lava) {
f[i] = f[i].max(0.8); // lava lights up its surroundings
}
@@ -853,6 +867,26 @@ fn build_chunk(map: &Map, amb: &[f32], glow: &[f32], cx: i32, cz: i32, atlas: &T
if !solid_at(map, x - 1, z) && !outdoors {
ln.strip(vec3(xf, 0.0, zf), vec3(xf, 0.0, zf + 1.0), Vec3::Y, OUTLINE_W, OUTLINE_C);
}
// a room of a higher level: gold trim along its border (twice at level 3)
let lv = room_level_at(map, &t);
if lv > 1 {
let gold = col(vec3(1.0, 0.78, 0.25), 1.0);
for (d, &(dx, dz)) in DIR4.iter().enumerate() {
if map.get(x + dx, z + dz).map_or(false, |o| o.kind == t.kind && o.owner == t.owner) {
continue;
}
for k in 0..(lv - 1) {
let m = 0.08 + 0.12 * k as f32;
let (p, q) = match d {
0 => (vec3(xf + m, 0.012, zf + m), vec3(xf + 1.0 - m, 0.012, zf + m)),
1 => (vec3(xf + 1.0 - m, 0.012, zf + m), vec3(xf + 1.0 - m, 0.012, zf + 1.0 - m)),
2 => (vec3(xf + 1.0 - m, 0.012, zf + 1.0 - m), vec3(xf + m, 0.012, zf + 1.0 - m)),
_ => (vec3(xf + m, 0.012, zf + 1.0 - m), vec3(xf + m, 0.012, zf + m)),
};
ln.strip(p, q, Vec3::Y, OUTLINE_W * 1.5, gold);
}
}
}
// room decor
let dc = col(lava_lit(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), lava_at(glow, xf + 0.5, zf + 0.5)), 1.0);
if t.plant() {
@@ -1083,6 +1117,10 @@ fn rgba(u: u32) -> Color {
fn chunk_sig(map: &Map, cx: i32, cz: i32) -> u64 {
let mut h: u64 = 1469598103934665603;
// researched room levels change the light and the trim of every room
for u in &map.upgrades {
h = (h ^ (u.0 as u64 | (u.1 as u64) << 8 | (u.2 as u64) << 16)).wrapping_mul(1099511628211);
}
for z in cz * CHUNK - 3..(cz + 1) * CHUNK + 3 {
for x in cx * CHUNK - 3..(cx + 1) * CHUNK + 3 {
let v = match map.get(x, z) {
+72 -1
View File
@@ -46,6 +46,26 @@ pub const RESEARCH: &[(usize, f32)] = &[
(51, 1000.0), // catapult
];
// room levels, researched once everything else is known: (index in ROOM_KINDS, level, points)
pub const UPGRADES: &[(usize, u8, f32)] = &[
(1, 2, 500.0), // lair
(2, 2, 500.0), // mushroom garden
(0, 2, 550.0), // treasury
(4, 2, 600.0), // library
(3, 2, 600.0), // training hall
(8, 2, 650.0), // forge
(6, 2, 650.0), // temple
(7, 2, 650.0), // aerie
(1, 3, 1000.0), // lair
(2, 3, 1000.0), // mushroom garden
(0, 3, 1100.0), // treasury
(4, 3, 1200.0), // library
(3, 3, 1200.0), // training hall
(8, 3, 1300.0), // forge
(6, 3, 1300.0), // temple
(7, 3, 1300.0), // aerie
];
// known from the start in free play
pub const BASE_TOOLS: &[usize] = &[0, 1, 2, 3, 4, 6, 7, 8, 9, 14, 17, 20, 45, 46, 49];
@@ -92,13 +112,45 @@ impl Game {
!self.known(tool) && self.researchable.get(tool).copied().unwrap_or(false) && RESEARCH.iter().any(|&(t, _)| t == tool)
}
// the room level being researched once no tool is left: index in UPGRADES
pub fn upgrade_target(&self) -> Option<usize> {
if self.research_target().is_some() || self.known.is_empty() {
return None;
}
UPGRADES.iter().position(|&(r, lv, _)| self.room_level.get(r).copied().unwrap_or(1).max(1) < lv)
}
pub(crate) fn add_research(&mut self, points: f32) {
if self.research_target().is_some() {
if self.research_target().is_some() || self.upgrade_target().is_some() {
self.research += points;
}
}
// a room kind reached level `lv` (the map keeps it for the look of the rooms)
pub fn set_room_level(&mut self, r: usize, lv: u8) {
if self.room_level.len() < crate::game::N_ROOMS {
self.room_level.resize(crate::game::N_ROOMS, 1);
}
self.room_level[r] = lv;
let side = self.me_side;
self.map.upgrades.retain(|u| !(u.0 == side && u.1 as usize == r));
if lv > 1 {
self.map.upgrades.push((side, r as u8, lv));
}
}
pub(crate) fn update_research(&mut self) {
if let Some(u) = self.upgrade_target() {
let (r, lv, cost) = UPGRADES[u];
if self.research >= cost {
self.research -= cost;
self.set_room_level(r, lv);
let roman = if lv == 2 { "II" } else { "III" };
self.msg("msg.room_upgraded", vec![format!("@{}", crate::game::ROOM_NAMES[r]), roman.to_string()]);
self.sfx_ui(Sfx::LevelUp);
}
return;
}
let Some(k) = self.research_target() else { return };
let (tool, cost) = RESEARCH[k];
if self.research >= cost {
@@ -154,4 +206,23 @@ mod tests {
assert!(g.can_research(10), "heal comes soon and can be researched");
assert!(!g.can_research(31), "magic doors are far away");
}
#[test]
fn once_everything_is_known_the_library_raises_room_levels() {
let mut g = Game::new(9, 64, 64);
assert!(g.upgrade_target().is_none(), "tools first");
for v in g.known.iter_mut() {
*v = true;
}
let u = g.upgrade_target().expect("a room level to research");
let (r, lv, cost) = UPGRADES[u];
assert_eq!(lv, 2);
let before = g.room_bonus(ROOM_KINDS[r]);
g.research = cost + 1.0;
g.update_research();
assert_eq!(g.room_level_of(ROOM_KINDS[r]), 2);
assert!(g.room_bonus(ROOM_KINDS[r]) > before);
assert!(g.map.upgrades.iter().any(|x| x.1 as usize == r && x.2 == 2), "the map shows it");
assert_ne!(g.upgrade_target(), Some(u), "the next one");
}
}
+5
View File
@@ -667,6 +667,11 @@ impl Ui {
let (tool, cost) = crate::research::RESEARCH[k];
let name: String = tool_info(TOOLS[tool]).name.chars().take(18).collect();
segs.push(vec![txt(tr("hud.research"), BLUE), Part::Gap, txt(name, TXT), Part::Gap, Part::Bar(g.research / cost, BLUE)]);
} else if let Some(u) = g.upgrade_target().filter(|_| g.counts[4] > 0) {
// everything known: the library works on room levels
let (r, lv, cost) = crate::research::UPGRADES[u];
let name: String = format!("{} {}", tr(ROOM_NAMES[r]), if lv == 2 { "II" } else { "III" }).chars().take(20).collect();
segs.push(vec![txt(tr("hud.research"), BLUE), Part::Gap, txt(name, TXT), Part::Gap, Part::Bar(g.research / cost, BLUE)]);
}
segs.push(vec![txt(tr("hud.payday"), if g.payday <= PAYDAY_WARN { RED } else { GOLD }), Part::Gap, txt(mmss(g.payday), TXT)]);
segs.push(vec![txt(tr("hud.army"), TXT), Part::Gap, txt((g.army_value() as i32).to_string(), TXT)]);