Creature equipment: the forge makes iron, steel and rune gear for the strongest creatures

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-06 00:26:59 +01:00
1 parent 82dd8c73e1
commit 4c440fb97f
25 files changed
+215 -4

No files matched your search

+104
View File
@@ -0,0 +1,104 @@
// Creature equipment: when no trap or door is ordered, the forge makes armour and weapons in
// three tiers - iron, steel (once steel walls are known) and rune (once the magic door, the
// last discovery of the library, is known). Every finished piece goes to the strongest creature without anything that good yet:
// more health and harder blows. The tier shows as a little coloured mark at the health bar.
use crate::game::*;
// tier 1..3: forge work, gold, health and attack factor
pub const GEAR_WORK: [f32; 3] = [40.0, 70.0, 110.0];
pub const GEAR_GOLD: [f32; 3] = [60.0, 140.0, 280.0];
pub const GEAR_FACTOR: [f32; 4] = [1.0, 1.1, 1.22, 1.38];
pub fn gear_factor(gear: u8) -> f32 {
GEAR_FACTOR[(gear as usize).min(3)]
}
impl Game {
// the best tier the forge can make now (1..3)
pub fn gear_tier(&self) -> u8 {
if self.known(31) {
3
} else if self.known(19) {
2
} else {
1
}
}
// is a creature waiting for a piece of the best tier (and none in stock)?
pub fn gear_wanted(&self) -> bool {
let tier = self.gear_tier();
self.gear_stock.get(tier as usize - 1).copied().unwrap_or(0) == 0 && self.ents.iter().any(|e| e.alive && e.creature() && e.gear < tier)
}
// forge work without an order: the next piece of equipment
pub(crate) fn forge_gear(&mut self, work: f32) {
if !self.gear_wanted() {
return;
}
let tier = self.gear_tier();
let k = tier as usize - 1;
self.gear_work += work;
if self.gear_work >= GEAR_WORK[k] && self.gold >= GEAR_GOLD[k] {
self.gear_work -= GEAR_WORK[k];
self.gold -= GEAR_GOLD[k];
if self.gear_stock.len() < 3 {
self.gear_stock.resize(3, 0);
}
self.gear_stock[k] += 1;
}
self.equip_from_stock();
}
// finished pieces go to the strongest creatures that have nothing that good
pub(crate) fn equip_from_stock(&mut self) {
for k in (0..self.gear_stock.len().min(3)).rev() {
while self.gear_stock[k] > 0 {
let tier = k as u8 + 1;
let best = (0..self.ents.len())
.filter(|&j| {
let e = &self.ents[j];
e.alive && e.creature() && e.gear < tier
})
.max_by(|&a, &b| {
let (ea, eb) = (&self.ents[a], &self.ents[b]);
(ea.level, ea.max_hp as i32).cmp(&(eb.level, eb.max_hp as i32))
});
let Some(j) = best else { break };
self.gear_stock[k] -= 1;
let e = &mut self.ents[j];
let f = gear_factor(tier) / gear_factor(e.gear);
e.max_hp *= f;
e.hp *= f;
e.gear = tier;
let (x, y, kind) = (e.x, e.y, e.kind);
self.fx(7, x, y, 0.0, 0.0, 0.6, 0xff40c0ff);
self.msg("msg.gear_given", vec![format!("@gear.{}", tier), format!("@{}", st(kind).name)]);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_forge_equips_the_strongest_creature() {
let mut g = Game::new(4, 64, 64);
let (cx, cy) = g.map.core;
let weak = g.spawn(EK::Kobold, false, cx as f32 + 4.5, cy as f32 + 1.5, 1);
let strong = g.spawn(EK::Ogre, false, cx as f32 + 5.5, cy as f32 + 1.5, 5);
g.gold = 1000.0;
assert!(g.gear_wanted());
let hp0 = g.ents.iter().find(|e| e.id == strong).unwrap().max_hp;
g.forge_gear(GEAR_WORK[0] + 1.0);
let s = g.ents.iter().find(|e| e.id == strong).unwrap();
let w = g.ents.iter().find(|e| e.id == weak).unwrap();
assert_eq!(s.gear, 1, "the ogre got the first piece");
assert_eq!(w.gear, 0);
assert!(s.max_hp > hp0 && s.atk() > st(EK::Ogre).atk * (1.0 + 0.12 * 4.0));
assert!(g.gold < 1000.0);
}
}
+17 -2
View File
@@ -305,6 +305,8 @@ pub struct Ent {
pub chained: bool, // a lone creature chained in a sealed cave: joins when the cave is claimed
#[serde(default)]
pub boss: u8, // a champion of the Order leading a wave (see bosses.rs), 0: none
#[serde(default)]
pub gear: u8, // equipment from the forge: 0 none, 1 iron, 2 steel, 3 rune (armory.rs)
#[serde(skip, default = "zero_f32")]
pub disp_x: f32, // smoothed screen position (not part of the saved or synced world state)
#[serde(skip, default = "zero_f32")]
@@ -381,6 +383,7 @@ impl Ent {
target: None,
chained: false,
boss: 0,
gear: 0,
disp_x: 0.0,
disp_y: 0.0,
}
@@ -389,7 +392,7 @@ impl Ent {
idx((self.x as i32).clamp(0, mw() as i32 - 1), (self.y as i32).clamp(0, mh() as i32 - 1))
}
pub fn atk(&self) -> f32 {
st(self.kind).atk * (1.0 + 0.12 * (self.level - 1) as f32)
st(self.kind).atk * (1.0 + 0.12 * (self.level - 1) as f32) * crate::armory::gear_factor(self.gear)
}
// the player's creatures (not imps, heroes or the rivals' units)
pub fn creature(&self) -> bool {
@@ -494,6 +497,8 @@ pub struct MasterState {
pub training_bill: f32,
pub counts: [usize; N_ROOMS],
pub room_level: Vec<u8>,
pub gear_stock: Vec<u16>,
pub gear_work: f32,
pub springs: usize,
pub known: Vec<bool>,
pub researchable: Vec<bool>,
@@ -531,6 +536,10 @@ pub struct Game {
#[serde(skip)]
pub counts: [usize; N_ROOMS], // room tiles per room kind (ROOM_KINDS order, recounted)
#[serde(default)]
pub gear_stock: Vec<u16>, // finished equipment waiting for a creature, per tier (armory.rs)
#[serde(default)]
pub gear_work: f32, // forge work on the next piece
#[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,
@@ -893,6 +902,8 @@ impl Game {
hand: Vec::new(),
counts: [0; N_ROOMS],
room_level: Vec::new(),
gear_stock: Vec::new(),
gear_work: 0.0,
gate_hp: GATE_HP,
gate_hit_t: -100.0,
gate_stage: 0,
@@ -1463,6 +1474,8 @@ impl Game {
training_bill: self.training_bill,
counts: self.counts,
room_level: self.room_level.clone(),
gear_stock: self.gear_stock.clone(),
gear_work: self.gear_work,
springs: self.springs,
known: std::mem::take(&mut self.known),
researchable: std::mem::take(&mut self.researchable),
@@ -1494,6 +1507,8 @@ impl Game {
self.training_bill = m.training_bill;
self.counts = m.counts;
self.room_level = m.room_level;
self.gear_stock = m.gear_stock;
self.gear_work = m.gear_work;
self.springs = m.springs;
self.known = m.known;
self.researchable = m.researchable;
@@ -2374,7 +2389,7 @@ impl Game {
e.xp -= 100.0 * e.level as f32;
e.level += 1;
let old = e.max_hp;
e.max_hp = s.hp * (1.0 + 0.15 * (e.level - 1) as f32);
e.max_hp = s.hp * (1.0 + 0.15 * (e.level - 1) as f32) * crate::armory::gear_factor(e.gear);
e.hp += e.max_hp - old;
self.fx(7, e.x, e.y, 0.0, 0.0, 0.8, 0xff44ddff);
if !e.hero {
+1
View File
@@ -1,6 +1,7 @@
// DungeonCraft – dungeon building & defence in the browser (Rust + macroquad, WASM).
mod achievements;
mod armory;
mod audio;
mod campaign;
mod campaign_doc;
+8
View File
@@ -1741,6 +1741,14 @@ impl R3d {
if e.guard {
gfx.glyph('☼', head.x - (GW * small * 0.5).floor(), head.y - 2.0 * GH * small, small, Color::from_rgba(255, 220, 90, 255));
}
// equipment from the forge: a little mark right of the health bar
if e.gear > 0 && px_per_tile >= 20.0 * F() as f32 {
let k = 2.0 * F() as f32;
let x = head.x + (cols as f32 * cw * 0.5).ceil() + 2.0 * k;
let c = [Color::from_rgba(170, 170, 180, 255), Color::from_rgba(110, 160, 255, 255), Color::from_rgba(90, 240, 255, 255)][(e.gear as usize - 1).min(2)];
draw_rectangle(x, head.y - GH * small, 3.0 * k, 4.0 * k, Color::from_rgba(20, 16, 20, 255));
draw_rectangle(x + k * 0.5, head.y - GH * small + k * 0.5, 2.0 * k, 3.0 * k, c);
}
// rank badge left of the health bar
if e.level > 1 && px_per_tile >= 20.0 * F() as f32 {
let k = 2.0 * F() as f32;
+5 -1
View File
@@ -206,6 +206,10 @@ impl Game {
// the forge: the work of the creatures there goes into the oldest order
pub(crate) fn update_forge(&mut self, dt: f32) {
let work = std::mem::take(&mut self.forge_work);
if !self.traps.iter().any(|t| t.stage == ORDERED) {
// no order: the forge makes equipment
self.forge_gear(work);
}
if let Some(k) = self.traps.iter().position(|t| t.stage == ORDERED) {
self.traps[k].progress += work;
if self.traps[k].progress >= self.traps[k].kind.work() {
@@ -244,7 +248,7 @@ impl Game {
// is there forge work to do (an order waiting)?
pub fn forge_orders(&self) -> bool {
self.traps.iter().any(|t| t.stage == ORDERED)
self.traps.iter().any(|t| t.stage == ORDERED) || self.gear_wanted()
}
// an imp looking for work: a finished crate to carry; returns the path to the crate
+3
View File
@@ -1518,6 +1518,9 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), if e.hero { BLUE } else { FRAME_HI }));
out.push((trf("info.rank", &[tr(&format!("rank.{}", e.level.clamp(1, 10)))]), GOLD));
out.push((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));
if e.gear > 0 {
out.push((tr(&format!("gear.{}", e.gear.min(3))), BLUE));
}
if e.hero && e.captive != 0 {
out.push((tr(["", "info.unconscious", "info.carried", "info.tortured"][e.captive.min(3) as usize]), RED));
return out;