Dwarf caves in all modes: neutral dwarves guard a hoard and sometimes a rune chest; the rune forge tower; imps repair towers before claiming

- 1-4 cave slots by map size, 50% each (at least one); knight treasure caves only where a mission places them
- dwarves fight intruders near the cave, never chase far, heal at home, grow with distance and the player's army, never count for victory
- rune chest: one free rune forge tower (chain bolt to three more enemies), upgradeable for gold, sold once it comes back (next mission / free play at once), unbuilt ones carry over
- new tool (key N) shown in the tower menu while a rune tower can be built or upgraded

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 15:14:36 +01:00
1 parent d92b45a0b8
commit 9a9f04ae1f
17 files changed
+658 -32

No files matched your search

+4 -1
View File
@@ -55,7 +55,10 @@ An original dungeon management game (genre: dungeon building & defence) that run
- Chosen in the **New Game dialog**: width and height separately, **64–256 tiles** in steps of 32 (remembered in the settings)
- Content scales with the area: rock, gold veins and caves; the Hero Gate lies on the far side, the **grace period grows with the distance** to the gate
- **One portal per 64×64 tiles**; every claimed portal brings creatures (more portals = faster arrivals, still limited by the lair)
- Bigger maps get **guarded treasure caves** (one less than the number of portals): very rich gold veins, defended by a hero squad that stays at its post
- **Dwarf caves** in every mode: 1 to 4 slots depending on the map size, each filled with a 50% chance (at least one cave). A hoard (1500-4000 in gold and gems, more the further from the core) inside a cave ringed by rich veins, guarded by 4-6 neutral dwarves (bronze rings). Dwarves fight whoever comes within about 7 tiles of the cave, never chase further, heal at home and never count as opponents for victory. Their level grows with the distance to the core, and every 30 s they gain levels until they match about half the player's army strength.
- Knight treasure caves only exist where a campaign mission places them; everywhere else dwarf caves took their place.
- 30% of the dwarf caves hold a **rune chest**: claiming it gives one free build of the **rune forge tower** (1400 HP, range 10, the rune bolt jumps on to three more enemies). It can be upgraded for gold (800 / 1200). Selling it returns the base tower once: in the campaign as a chest for the next mission, in free play at once. Unbuilt rune towers carry over to the next mission.
- Imps repair damaged towers for free; repairs come before claiming more ground.
- Savegames store the tiles packed (run-length encoded, base64), so even 256×256 maps stay small (~300 KB); version 1 savegames still load
## Game content
+1 -1
View File
@@ -10,7 +10,7 @@ Dig out your underground realm, claim the portal to attract creatures, keep them
- 3D isometric view with a freely rotatable and tiltable camera
- Procedural pixel art, irregular cave walls, flickering torch light
- CP437 text-mode style interface with the DINOBYTE bitmap font
- Map sizes from 64×64 to 256×256 tiles (one portal per 64×64 tiles, guarded treasure caves on bigger maps)
- Map sizes from 64×64 to 256×256 tiles (one portal per 64×64 tiles, dwarf caves with treasure and rune forge plans)
- English and German (language files in `lang/`, easy to extend)
See [DESIGN.md](DESIGN.md) for all design decisions and [CREDITS.md](CREDITS.md) for third-party assets.
+14
View File
@@ -624,3 +624,17 @@ hud.rival1 = Grün
hud.rival2 = Blau
msg.creature_freed = Ein {0} sprengt seine Ketten und schließt sich dir an!
msg.spring_taken = Eine Manaquelle gehört dir: mehr Mana und ein höheres Maximum.
tool.rune.label = Rune
tool.rune.name = Runenschmiede-Turm
tool.rune.desc = Pläne aus einer Zwergenkiste: kostenlos gebaut, ein Turm je Kiste. Riesige Reichweite, Runenblitze springen auf drei weitere Feinde über. Verkaufst du ihn, kommt der Basisturm einmal zurück, in der nächsten Mission.
tower.rune = Runenschmiede-Turm
cost.rune = kostenlos: noch {0} (Ausbau {1} / {2} Gold)
hint.rune_none = Keine Runenschmiede-Pläne mehr. Nimm eine Zwergenkiste ein, um neue zu bekommen.
msg.rune_chest = Eine Zwergenkiste! Die Pläne eines Runenschmiede-Turms gehören dir - bau ihn kostenlos im Turmmenü.
msg.rune_next = Die Runenschmiede-Pläne sind verstaut: Der Turm kommt in der nächsten Mission zurück.
msg.rune_back = Die Runenschmiede-Pläne sind zurück - du darfst den Turm noch einmal bauen.
msg.dwarf_slain = Ein Zwerg fällt. +{0} Gold.
info.dwarf = Bewacht den Schatz einer Zwergenhöhle
info.dwarf_neutral = Neutral: kämpft gegen jeden, der zu nahe kommt, verlässt die Höhle nie.
tile.rune_chest = Zwergische Runenkiste
tile.rune_chest_info = Nimm sie ein: die Pläne eines Runenschmiede-Turms (einmal kostenlos bauen).
+14
View File
@@ -625,3 +625,17 @@ hud.rival1 = Green
hud.rival2 = Blue
msg.creature_freed = A {0} breaks its chains and joins you!
msg.spring_taken = A mana spring is yours: more mana and a higher maximum.
tool.rune.label = Rune
tool.rune.name = Rune Forge Tower
tool.rune.desc = Plans from a dwarven chest: built for free, one tower per chest. Huge range, rune bolts jump on to three more enemies. Sell it and the base tower comes back once, in the next mission.
tower.rune = Rune Forge Tower
cost.rune = free: {0} left (upgrades {1} / {2} gold)
hint.rune_none = No rune forge plans left. Claim a dwarven chest for a new one.
msg.rune_chest = A dwarven chest! The plans of a rune forge tower are yours - build it for free from the tower menu.
msg.rune_next = The rune forge plans are packed away: the tower comes back in the next mission.
msg.rune_back = The rune forge plans are back - you may build the tower once more.
msg.dwarf_slain = A dwarf falls. +{0} gold.
info.dwarf = Guards the hoard of a dwarf cave
info.dwarf_neutral = Neutral: fights whoever comes close, never leaves the cave.
tile.rune_chest = Dwarven rune chest
tile.rune_chest_info = Claim it: the plans of a rune forge tower (one free build).
+9 -2
View File
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
// From which mission (0-based) on each tool is available (same order as ui::TOOLS):
// dig, hand, treasury, lair, farm, training, library, sell, flag, lightning, heal, imp,
// torture, temple, arrow tower, cannon tower, magic tower, bridge, aerie, steel
pub const TOOL_UNLOCK: [usize; 20] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17];
pub const TOOL_UNLOCK: [usize; 21] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17, 0];
pub struct FortDef {
pub at: (f32, f32), // centre as a fraction of the map size
@@ -79,6 +79,10 @@ fn cave(at: (f32, f32), chained: Option<(EK, u32)>, treasure: i32) -> CaveDef {
pub struct Carry {
pub creatures: Vec<(EK, u32, f32)>, // kind, level, experience
pub gold: f32,
#[serde(default)]
pub rune_free: u32, // unbuilt rune forge towers from chests
#[serde(default)]
pub rune_final: u32, // unbuilt or sold rune forge towers that were already given back once
}
pub const CARRY_CREATURES: usize = 5;
@@ -499,6 +503,7 @@ pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
let y = ((ry * h as f32) as i32).clamp(10, h as i32 - 10);
g.add_rival(side, x, y, diff);
}
g.place_dwarf_caves(d.seed);
let heart = d.gate || d.forts.iter().any(|f| f.heart);
if !heart {
g.gate_hp = 0.0;
@@ -524,6 +529,8 @@ pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
if let Some(c) = carry {
joiners.extend(c.creatures.iter().copied());
g.gold += c.gold;
g.rune_free += c.rune_free;
g.rune_final += c.rune_final;
}
for (k, &(kind, level, xp)) in joiners.iter().enumerate() {
let a = k as f32 * 0.9;
@@ -543,7 +550,7 @@ pub fn carry_from(g: &Game) -> Carry {
let mut c: Vec<&Ent> = g.ents.iter().filter(|e| e.alive && e.creature()).collect();
let power = |e: &Ent| e.max_hp * e.atk() / st(e.kind).interval;
c.sort_by(|a, b| power(b).partial_cmp(&power(a)).unwrap_or(std::cmp::Ordering::Equal));
Carry { creatures: c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(), gold: (g.gold * CARRY_GOLD).floor() }
Carry { creatures: c.iter().take(CARRY_CREATURES).map(|e| (e.kind, e.level, e.xp)).collect(), gold: (g.gold * CARRY_GOLD).floor(), rune_free: g.rune_free, rune_final: g.rune_final + g.rune_next }
}
#[cfg(test)]
+355
View File
@@ -0,0 +1,355 @@
// Dwarf caves: a neutral people that guards a treasure in a cave surrounded by rich veins.
// The dwarves defend themselves but never stray far from their cave, and they never count
// as opponents for victory. Some caves hold a chest with the plans of a rune forge tower:
// the player may build it once for free.
use crate::game::*;
use crate::map::*;
use serde::{Deserialize, Serialize};
pub const DWARF_SIDE: u8 = 4;
const LEASH: f32 = 7.0; // dwarves do not chase enemies further from the cave centre
const CHEST_CHANCE: f32 = 0.3;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DwarfCave {
pub at: (i32, i32), // centre
pub base: u32, // base level of its dwarves (grows with the distance to the player's core)
}
impl Game {
// 1 to 4 cave slots depending on the map size, each filled with a 50% chance (at least one)
pub fn place_dwarf_caves(&mut self, seed: u32) {
let mut rng = Rng(seed.wrapping_mul(0x9e37_79b9) ^ 0x5bd1_e995 | 1);
for _ in 0..8 {
rng.next();
}
let area = (mw() * mh()) as f32 / 4096.0;
let slots = (area.sqrt().ceil() as usize).clamp(1, 4);
let mut n = (0..slots).filter(|_| rng.f() < 0.5).count();
if n == 0 {
n = 1;
}
for _ in 0..n {
if let Some(at) = self.dwarf_spot(&mut rng) {
self.carve_dwarf_cave(at, &mut rng);
}
}
}
// a spot far from everything else whose surroundings are untouched neutral rock and earth
fn dwarf_spot(&self, rng: &mut Rng) -> Option<(i32, i32)> {
let (w, h) = (mw() as i32, mh() as i32);
let (cx, cy) = (self.map.core.0 + 1, self.map.core.1 + 1);
let mut others: Vec<(i32, i32)> = vec![(cx, cy), (self.map.gate.0 + 1, self.map.gate.1 + 1)];
others.extend(self.map.portals.iter().map(|&(x, y)| (x + 1, y + 1)));
others.extend(self.map.treasures.iter().copied());
others.extend(self.rivals.iter().map(|r| (r.core.0 + 1, r.core.1 + 1)));
others.extend(self.dwarf_caves.iter().map(|c| c.at));
let mut best = None;
let mut bd = -1;
for _ in 0..200 {
let p = (rng.range(9, w - 10), rng.range(9, h - 10));
if (p.0 - cx).pow(2) + (p.1 - cy).pow(2) < 20 * 20 {
continue;
}
let clear = (p.1 - 6..=p.1 + 6).all(|y| {
(p.0 - 6..=p.0 + 6).all(|x| {
let t = &self.map.t[idx(x, y)];
t.owner == Owner::None && !t.liquid() && !matches!(t.kind, Kind::Core | Kind::Portal | Kind::Gate | Kind::Tower | Kind::Shard | Kind::Bridge | Kind::LavaBridge | Kind::Treasury | Kind::ManaSpring | Kind::RuneChest)
})
});
if !clear {
continue;
}
let d = others.iter().map(|q| (p.0 - q.0).pow(2) + (p.1 - q.1).pow(2)).min().unwrap_or(i32::MAX);
if d < 14 * 14 {
continue;
}
// not hidden among the knights: keep off fortress ground and units
if self.ents.iter().any(|e| e.alive && (e.x - p.0 as f32).abs() < 8.0 && (e.y - p.1 as f32).abs() < 8.0) {
continue;
}
if d > bd {
bd = d;
best = Some(p);
}
}
best
}
fn carve_dwarf_cave(&mut self, (cx, cy): (i32, i32), rng: &mut Rng) {
let (px, py) = (self.map.core.0 + 1, self.map.core.1 + 1);
let dist = (((cx - px).pow(2) + (cy - py).pow(2)) as f32).sqrt();
// rich veins around the cave, with gems
for y in cy - 5..=cy + 5 {
for x in cx - 5..=cx + 5 {
let d = (((x - cx).pow(2) + (y - cy).pow(2)) as f32).sqrt();
let t = &mut self.map.t[idx(x, y)];
t.marked = false;
t.reinforced = false;
t.hp = 6.0;
if (x - cx).abs() <= 2 && (y - cy).abs() <= 2 {
t.kind = Kind::Floor;
t.gold = 0;
} else if d <= 5.2 && rng.f() < 0.7 {
let r = rng.f();
let (k, g) = if r < 0.08 { (Kind::Diamond, rng.range(1200, 1800)) } else if r < 0.28 { (Kind::Ruby, rng.range(800, 1200)) } else if r < 0.45 { (Kind::Silver, rng.range(500, 800)) } else { (Kind::Gold, rng.range(800, 1400)) };
t.kind = k;
t.gold = g;
} else {
t.kind = Kind::Earth;
t.gold = 0;
}
}
}
// the hoard: treasury piles in the corners and along the walls, more the further away
let hoard = (1500.0 + dist * 20.0).min(4000.0) as i32;
let spots = [(-2, -2), (2, -2), (-2, 2), (2, 2), (0, -2), (0, 2), (-2, 0), (2, 0)];
let piles = ((hoard + TREASURY_TILE as i32 - 1) / TREASURY_TILE as i32).clamp(1, spots.len() as i32) as usize;
for (k, &(dx, dy)) in spots.iter().take(piles).enumerate() {
let t = &mut self.map.t[idx(cx + dx, cy + dy)];
t.kind = Kind::Treasury;
t.gold = (hoard - k as i32 * TREASURY_TILE as i32).clamp(0, TREASURY_TILE as i32);
let r = rng.f();
t.res = if r < 0.12 { 3 } else if r < 0.35 { 2 } else if r < 0.55 { 1 } else { 0 };
}
if rng.f() < CHEST_CHANCE {
self.map.t[idx(cx, cy)].kind = Kind::RuneChest;
}
let base = (2 + (dist / 30.0) as u32).min(7);
let count = 4 + (dist > 60.0) as usize + (dist > 100.0) as usize;
for k in 0..count {
let a = k as f32 / count as f32 * std::f32::consts::TAU;
let id = self.spawn(EK::Dwarf, false, cx as f32 + 0.5 + a.cos() * 1.2, cy as f32 + 0.5 + a.sin() * 1.2, base);
if let Some(e) = self.ents.iter_mut().find(|e| e.id == id) {
e.side = DWARF_SIDE;
e.home = Some((cx as f32 + 0.5, cy as f32 + 0.5));
e.happy = 1.0;
}
}
self.dwarf_caves.push(DwarfCave { at: (cx, cy), base });
}
// Every 30 s the dwarves of a cave grow stronger until they match about half the player's
// army (or their base level): one of the weakest idle dwarves gains a level.
pub fn update_dwarves(&mut self, dt: f32) {
self.dwarf_t -= dt;
if self.dwarf_t > 0.0 || self.dwarf_caves.is_empty() {
return;
}
self.dwarf_t = 30.0;
let army = self.army_value() * 0.5;
for c in 0..self.dwarf_caves.len() {
let home = (self.dwarf_caves[c].at.0 as f32 + 0.5, self.dwarf_caves[c].at.1 as f32 + 0.5);
let mine: Vec<usize> = (0..self.ents.len()).filter(|&j| {
let e = &self.ents[j];
e.alive && e.dwarf() && e.home == Some(home)
}).collect();
if mine.is_empty() {
continue;
}
let list: Vec<(EK, u32)> = mine.iter().map(|&j| (self.ents[j].kind, self.ents[j].level)).collect();
if Game::squad_value(&list) >= army {
continue;
}
if let Some(&j) = mine.iter().filter(|&&j| self.ents[j].level < 10 && !matches!(self.ents[j].job, Job::Attack(_))).min_by_key(|&&j| self.ents[j].level) {
let e = &mut self.ents[j];
e.level += 1;
let old = e.max_hp;
e.max_hp = st(e.kind).hp * (1.0 + 0.15 * (e.level - 1) as f32);
e.hp += e.max_hp - old;
}
}
}
// a dwarf: fights what comes close to the cave, otherwise potters about at home and heals
pub fn dwarf_ai(&mut self, e: &mut Ent, dt: f32) {
let (hx, hy) = e.home.unwrap_or((e.x, e.y));
let from_home = |x: f32, y: f32| ((x - hx).powi(2) + (y - hy).powi(2)).sqrt();
if e.think <= 0.0 {
e.think = 0.4;
if !matches!(e.job, Job::Attack(_)) {
if let Some(id) = self.nearest_enemy(e, 6.5) {
if let Some(j) = self.find(id) {
if from_home(self.ents[j].x, self.ents[j].y) < LEASH {
e.job = Job::Attack(id);
e.path.clear();
e.repath = 0.0;
}
}
}
}
}
if let Job::Attack(id) = e.job {
let near = self.find(id).map_or(false, |j| from_home(self.ents[j].x, self.ents[j].y) < LEASH + 1.0);
if near && from_home(e.x, e.y) < LEASH + 1.0 && self.attack_job(e, id, dt) {
return;
}
e.job = Job::None;
e.path.clear();
e.repath = 0.0;
}
e.hp = (e.hp + e.max_hp * 0.01 * dt).min(e.max_hp);
if e.path.is_empty() && e.repath <= 0.0 {
e.repath = 3.0 + self.rng.f() * 4.0;
let (tx, ty) = if from_home(e.x, e.y) > 3.0 { (hx, hy) } else { (hx + self.rng.f() * 4.0 - 2.0, hy + self.rng.f() * 4.0 - 2.0) };
if self.map.walk(tx as i32, ty as i32) {
if let Some(p) = self.map.path_to(e.tile(), idx(tx as i32, ty as i32)) {
e.path = p;
}
}
}
self.step_path(e, dt);
}
// rune forge towers that can be built now
pub fn rune_available(&self) -> u32 {
self.rune_free + self.rune_final
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dwarves(g: &Game) -> usize {
g.ents.iter().filter(|e| e.alive && e.dwarf()).count()
}
#[test]
fn caves_by_map_size() {
for seed in 1..=10u32 {
for &(w, h, max) in &[(64usize, 64usize, 1usize), (128, 128, 2), (256, 256, 4)] {
let g = Game::new_free(seed, w, h, 1);
let n = g.dwarf_caves.len();
assert!(n >= 1 && n <= max, "seed {} {}x{}: {} caves", seed, w, h, n);
assert!(g.map.treasures.is_empty(), "knight caves replaced");
assert!(dwarves(&g) >= 4 * n);
for c in &g.dwarf_caves {
let t = &g.map.t[idx(c.at.0, c.at.1)];
assert!(matches!(t.kind, Kind::Floor | Kind::RuneChest), "cave centre {:?}", t.kind);
}
}
}
}
#[test]
fn dwarves_are_no_opponents_and_stay_home() {
let mut g = (1..100u32).map(|s| Game::new_free(s, 64, 64, 0)).find(|g| g.rivals.len() == 1 && !g.gate_on).unwrap();
g.eliminate_rival(0);
for _ in 0..40 {
g.update(0.05);
}
assert_eq!(g.state, 1, "dwarves alive, but the game is won");
assert!(dwarves(&g) > 0);
}
#[test]
fn dwarves_fight_intruders_but_do_not_chase() {
let mut g = Game::new_free(3, 64, 64, 0);
let c = g.dwarf_caves[0].clone();
// an ogre of the player walks into the cave
let id = g.spawn(EK::Ogre, false, c.at.0 as f32 + 0.5, c.at.1 as f32 + 0.5, 3);
let mut hit = false;
for _ in 0..(20 * 20) {
g.update(0.05);
if let Some(j) = g.find(id) {
hit |= g.ents[j].hp < g.ents[j].max_hp;
}
}
assert!(hit, "the dwarves attack");
let home = (c.at.0 as f32 + 0.5, c.at.1 as f32 + 0.5);
for e in g.ents.iter().filter(|e| e.alive && e.dwarf()) {
let d = ((e.x - home.0).powi(2) + (e.y - home.1).powi(2)).sqrt();
assert!(d < LEASH + 2.5, "dwarf strayed {:.1} tiles", d);
}
}
#[test]
fn dwarves_grow_with_the_army() {
let mut g = Game::new_free(5, 64, 64, 0);
let lvl = |g: &Game| g.ents.iter().filter(|e| e.alive && e.dwarf()).map(|e| e.level).sum::<u32>();
let before = lvl(&g);
let (cx, cy) = g.map.core;
for k in 0..8 {
g.spawn(EK::Ogre, false, cx as f32 + 1.0 + k as f32 * 0.2, cy as f32 - 1.0, 8);
}
g.dwarf_t = 0.0;
g.update_dwarves(0.1);
assert!(lvl(&g) > before);
}
#[test]
fn rune_chest_gives_a_free_tower_once() {
let mut g = Game::new_free(5, 64, 64, 0);
let (cx, cy) = g.map.core;
let (tx, ty) = (cx + 4, cy + 1);
g.map.t[idx(tx, ty)].kind = Kind::RuneChest;
g.map.t[idx(tx, ty)].owner = Owner::None;
g.open_rune_chest(idx(tx, ty));
assert_eq!(g.rune_available(), 1);
g.map.t[idx(tx, ty)].owner = Owner::Player;
let gold = g.gold;
g.build_tower(TowerKind::Rune, tx, ty);
assert_eq!(g.towers.len(), 1);
assert_eq!(g.gold, gold, "free");
assert_eq!(g.rune_available(), 0);
g.build_tower(TowerKind::Rune, tx + 1, ty);
assert_eq!(g.towers.len(), 1, "only one per chest");
// free play: selling gives the base tower back at once - but only once
g.sell_rect(tx, ty, tx, ty);
assert_eq!(g.rune_available(), 1);
g.build_tower(TowerKind::Rune, tx, ty);
assert!(g.towers[0].rune_final);
g.sell_rect(tx, ty, tx, ty);
assert_eq!(g.rune_available(), 0, "no second refund");
// in a mission the tower comes back in the next one
let mut m = crate::campaign::start_mission(0, None);
let (cx, cy) = m.map.core;
m.rune_free = 1;
m.build_tower(TowerKind::Rune, cx + 4, cy + 1);
assert_eq!(m.towers.len(), 1, "built in the mission");
m.sell_rect(cx + 4, cy + 1, cx + 4, cy + 1);
assert_eq!((m.rune_available(), m.rune_next), (0, 1));
let c = crate::campaign::carry_from(&m);
let next = crate::campaign::start_mission(1, Some(&c));
assert_eq!(next.rune_final, 1);
}
#[test]
fn imps_repair_towers_before_claiming() {
let mut g = Game::new(4, 64, 64);
let (cx, cy) = g.map.core;
// plenty of unclaimed floor next to the dungeon
for y in cy - 3..=cy + 5 {
for x in cx + 6..=cx + 12 {
g.map.t[idx(x, y)].kind = Kind::Floor;
g.map.t[idx(x, y)].owner = Owner::None;
}
}
let (tx, ty) = (cx + 4, cy - 2);
g.map.t[idx(tx, ty)].kind = Kind::Floor;
g.map.t[idx(tx, ty)].owner = Owner::Player;
g.build_tower(TowerKind::Arrow, tx, ty);
assert_eq!(g.towers.len(), 1);
g.towers[0].hp = 50.0;
let gold = g.gold;
for _ in 0..(20 * 20) {
g.update(0.05);
}
assert!(g.towers[0].hp >= g.towers[0].max_hp() - 1.0, "repaired: {}", g.towers[0].hp);
assert!(g.gold >= gold - 1.0, "repairs are free");
}
#[test]
fn missions_have_dwarf_caves() {
let mut total = 0;
for i in 0..crate::campaign::missions().len() {
let g = crate::campaign::start_mission(i, None);
total += g.dwarf_caves.len();
}
println!("{} dwarf caves in the campaign", total);
assert!(total >= 20);
}
}
+1 -1
View File
@@ -134,7 +134,7 @@ impl Game {
let id = self.next_id;
self.next_id += 1;
let s = tower_stats(kind);
let mut tw = Tower { id, kind, tile: i, level: 1, hp: s.hp, cd: 1.0, aim: 0.0, flash: 0.0, hero: true };
let mut tw = Tower { id, kind, tile: i, level: 1, hp: s.hp, cd: 1.0, aim: 0.0, flash: 0.0, hero: true, rune_final: false };
while tw.level < f.tower_level.min(3) {
let old = tw.max_hp();
tw.level += 1;
+138 -17
View File
@@ -120,6 +120,8 @@ pub struct St {
pub bounty: i32,
}
pub const RUNE_TOOL: usize = 20; // index of the rune forge tower in ui::TOOLS
pub fn st(k: EK) -> St {
let s = |name, hp, atk, range, speed, interval, wage, bounty| St { name, hp, atk, range, speed, interval, wage, bounty };
match k {
@@ -322,7 +324,11 @@ impl Ent {
}
// a unit of a rival master
pub fn rival(&self) -> bool {
!self.hero && self.side >= 2
!self.hero && (self.side == 2 || self.side == 3)
}
// a dwarf of a dwarf cave (neutral, never an opponent for victory)
pub fn dwarf(&self) -> bool {
!self.hero && self.side == crate::dwarves::DWARF_SIDE
}
// a hero that is still fighting (not a prisoner)
pub fn active_hero(&self) -> bool {
@@ -436,6 +442,16 @@ pub struct Game {
#[serde(skip)]
pub springs: usize, // claimed mana springs (recounted)
#[serde(default)]
pub dwarf_caves: Vec<crate::dwarves::DwarfCave>,
#[serde(default)]
pub dwarf_t: f32,
#[serde(default)]
pub rune_free: u32, // rune forge towers that can be built for free (from chests)
#[serde(default)]
pub rune_final: u32, // the same, but given back for a sold tower: no second refund
#[serde(default)]
pub rune_next: u32, // sold rune forge towers: back as a chest in the next mission
#[serde(default)]
pub freed: u32, // chained creatures freed // tiles next to fighting heroes: knight ground there cannot be claimed
#[serde(skip)]
pub hints: bool, // show hint messages (setting)
@@ -511,6 +527,7 @@ impl Game {
let dist = ((gx - cx).abs() + (gy - cy).abs()) as f32;
g.wave_timer = FIRST_WAVE + ((dist - 45.0) * 1.5).clamp(0.0, 300.0);
g.guard_treasures();
g.place_dwarf_caves(seed);
g.msg("msg.welcome1", vec![]);
g.msg("msg.welcome2", vec![]);
g.msg("msg.welcome3", vec![]);
@@ -582,6 +599,11 @@ impl Game {
gate_on: true,
springs: 0,
freed: 0,
dwarf_caves: Vec::new(),
dwarf_t: 30.0,
rune_free: 0,
rune_final: 0,
rune_next: 0,
};
let (cx, cy) = g.map.core;
// the soul core starts surrounded by a one tile wide ring of treasury
@@ -615,6 +637,11 @@ impl Game {
// ---------------------------------------------------------------- campaign missions
// the rune forge tower only shows up in the build menu once there is one to build or upgrade
pub fn tool_visible(&self, tool: usize) -> bool {
tool != RUNE_TOOL || self.rune_available() > 0 || self.towers.iter().any(|t| !t.hero && t.kind == TowerKind::Rune)
}
pub fn tool_allowed(&self, tool: usize) -> bool {
self.mission.as_ref().map_or(true, |m| m.tools.get(tool).copied().unwrap_or(true))
}
@@ -989,6 +1016,7 @@ impl Game {
if !self.rivals.is_empty() {
self.update_rivals(dt);
}
self.update_dwarves(dt);
self.check_end();
}
@@ -1357,7 +1385,7 @@ impl Game {
}
// strength of a squad (same formula as the army value)
fn squad_value(list: &[(EK, u32)]) -> f32 {
pub(crate) fn squad_value(list: &[(EK, u32)]) -> f32 {
let (mut h, mut d) = (0.0, 0.0);
for &(k, l) in list {
let s = st(k);
@@ -1574,6 +1602,8 @@ impl Game {
self.captive_update(e, dt);
} else if e.rival() {
self.rival_ai(e, dt);
} else if e.dwarf() {
self.dwarf_ai(e, dt);
} else if e.hero {
self.hero_ai(e, dt);
} else if e.kind == EK::Worker {
@@ -1900,6 +1930,10 @@ impl Game {
self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff);
self.sfx(Sfx::HeroDeath, x, y);
self.msg("msg.hero_slain", vec![format!("@{}", s.name), (got as i32).to_string()]);
} else if side == crate::dwarves::DWARF_SIDE {
let got = self.add_gold(s.bounty as f32 * 0.5);
self.sfx(Sfx::HeroDeath, x, y);
self.msg("msg.dwarf_slain", vec![(got as i32).to_string()]);
} else if side >= 2 {
// a creature of a rival master: half its wage as booty
let got = self.add_gold(s.wage as f32 * 0.5);
@@ -2161,6 +2195,9 @@ impl Game {
self.sfx(Sfx::Magic, x, y);
self.msg("msg.spring_taken", vec![]);
}
if t.kind == Kind::RuneChest {
self.open_rune_chest(i);
}
if t.kind == Kind::Shard {
self.shards_taken += 1;
self.fx(7, x, y, 0.0, 0.0, 1.2, 0xff6060ff);
@@ -2169,6 +2206,16 @@ impl Game {
}
}
// a dwarven chest: the plans of a rune forge tower (one free build)
pub fn open_rune_chest(&mut self, i: usize) {
let (x, y) = (xy(i).0 as f32 + 0.5, xy(i).1 as f32 + 0.5);
self.map.t[i].kind = Kind::Floor;
self.rune_free += 1;
self.fx(7, x, y, 0.0, 0.0, 1.4, 0xff40c0ff);
self.sfx(Sfx::LevelUp, x, y);
self.msg("msg.rune_chest", vec![]);
}
// a fortress unit spotted an enemy: guards and patrols nearby join the fight
fn alarm(&mut self, x: f32, y: f32, target: u32) {
for o in self.ents.iter_mut() {
@@ -2294,7 +2341,7 @@ impl Game {
fn claimable(&self, i: usize, me: u32) -> bool {
let t = &self.map.t[i];
let ok = match t.owner {
Owner::None => matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Treasury | Kind::ManaSpring),
Owner::None => matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Treasury | Kind::ManaSpring | Kind::RuneChest),
// knight ground (also vaults and shard chambers), but not while heroes fight there
// knight and rival ground (also vaults and shard chambers), but not while enemies fight there
Owner::Hero | Owner::Rival1 | Owner::Rival2 => {
@@ -2604,13 +2651,7 @@ impl Game {
if e.carry > 0 && self.start_deliver(e) {
return;
}
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.claimable(i, me), true) {
let end = p.last().copied().unwrap_or(cur);
self.map.t[end].reserved = me;
e.path = p;
e.job = Job::Claim(end);
return;
}
// damaged towers are repaired (free of charge) before more ground is claimed
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.damaged_tower_near(i).is_some(), true) {
let end = p.last().copied().unwrap_or(cur);
if let Some(id) = self.damaged_tower_near(end) {
@@ -2619,6 +2660,13 @@ impl Game {
return;
}
}
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.claimable(i, me), true) {
let end = p.last().copied().unwrap_or(cur);
self.map.t[end].reserved = me;
e.path = p;
e.job = Job::Claim(end);
return;
}
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.reinforce_neighbor(i, me).is_some(), true) {
let end = p.last().copied().unwrap_or(cur);
if let Some(w) = self.reinforce_neighbor(end, me) {
@@ -3662,6 +3710,16 @@ impl Game {
let i = idx(x, y);
if let Some(k) = self.towers.iter().position(|t| t.tile == i && !t.hero) {
refund += self.towers[k].invested() as f32 * 0.5;
if self.towers[k].kind == TowerKind::Rune && !self.towers[k].rune_final {
// the base tower comes back once: in the next mission (free play: at once)
if self.mission.is_some() {
self.rune_next += 1;
self.msg("msg.rune_next", vec![]);
} else {
self.rune_final += 1;
self.msg("msg.rune_back", vec![]);
}
}
self.towers.remove(k);
self.map.t[i].kind = Kind::Floor;
continue;
@@ -3758,11 +3816,25 @@ impl Game {
self.sfx_ui(Sfx::Error);
return;
}
// rune forge towers: one build per opened chest (fresh plans first)
let mut rune_final = false;
if kind == TowerKind::Rune {
if self.rune_free > 0 {
self.rune_free -= 1;
} else if self.rune_final > 0 {
self.rune_final -= 1;
rune_final = true;
} else {
self.hint(24, "hint.rune_none");
self.sfx_ui(Sfx::Error);
return;
}
}
self.gold -= cost;
self.map.t[i].kind = Kind::Tower;
let id = self.next_id;
self.next_id += 1;
let tower = Tower { id, kind, tile: i, level: 1, hp: s.hp, cd: 1.0, aim: 0.0, flash: 0.0, hero: false };
let tower = Tower { id, kind, tile: i, level: 1, hp: s.hp, cd: 1.0, aim: 0.0, flash: 0.0, hero: false, rune_final };
let (px, py) = tower.pos();
self.towers.push(tower);
// units standing on the tile step aside
@@ -3826,8 +3898,7 @@ impl Game {
for (j, e) in self.ents.iter().enumerate() {
// knight towers shoot the player's units, the player's towers shoot heroes
// knight towers shoot everybody but the humans, the player's towers everybody else
let enemy = e.captive == 0 && !e.chained && if t.hero { e.side() != 1 } else { e.side() != 0 };
if !e.alive || e.held || !enemy {
if !e.alive || e.held || !self.tower_enemy(t, e) {
continue;
}
let d = (e.x - px).powi(2) + (e.y - py).powi(2);
@@ -3910,18 +3981,20 @@ impl Game {
TowerKind::Arrow => (px, py, t.top() + 0.25),
TowerKind::Cannon => (px + aim.cos() * 0.55, py + aim.sin() * 0.55, t.top() + 0.2),
TowerKind::Magic => (px, py, t.top() + 0.45),
TowerKind::Rune => (px, py, t.top() + 0.55),
};
let col = match t.kind {
TowerKind::Arrow => 0xffd0e0f0,
TowerKind::Cannon => 0xff303030,
TowerKind::Magic => 0xffff60d0,
TowerKind::Rune => 0xff40c0ff,
};
self.shot(mx, my, mh, ex, ey, eh, 0.3, col);
self.fx_at(2, ex, ey, eh, 0.3, 0xffffffff);
self.sfx(match t.kind {
TowerKind::Arrow => Sfx::Shoot,
TowerKind::Cannon => Sfx::Cannon,
TowerKind::Magic => Sfx::Magic,
TowerKind::Magic | TowerKind::Rune => Sfx::Magic,
}, px, py);
let dmg = t.dmg();
match target {
@@ -3983,6 +4056,7 @@ impl Game {
TowerKind::Arrow => (px, py, t.top() + 0.25),
TowerKind::Cannon => (px + aim.cos() * 0.55, py + aim.sin() * 0.55, t.top() + 0.2),
TowerKind::Magic => (px, py, t.top() + 0.45),
TowerKind::Rune => (px, py, t.top() + 0.55),
};
let dmg = t.dmg();
match t.kind {
@@ -4065,10 +4139,46 @@ impl Game {
}
}
}
TowerKind::Rune => {
// a rune bolt that jumps on to up to three more enemies nearby
self.shot(mx, my, mh, ex, ey, 0.45, 0.25, 0xff40c0ff);
self.fx(2, ex, ey, 0.0, 0.0, 0.35, 0xffffc040);
self.sfx(Sfx::Magic, px, py);
let mut hit = vec![j];
let (mut lx, mut ly) = (ex, ey);
self.tower_damage(j, dmg);
for n in 0..3 {
let next = self
.ents
.iter()
.enumerate()
.filter(|(o, e)| !hit.contains(o) && e.alive && !e.held && self.tower_enemy(&t, e) && (e.x - lx).powi(2) + (e.y - ly).powi(2) < 3.5 * 3.5)
.min_by(|a, b| {
let da = (a.1.x - lx).powi(2) + (a.1.y - ly).powi(2);
let db = (b.1.x - lx).powi(2) + (b.1.y - ly).powi(2);
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(o, _)| o);
let Some(o) = next else { break };
let (ox, oy) = (self.ents[o].x, self.ents[o].y);
self.fx(0, lx, ly, ox, oy, 0.25, 0xff40c0ff);
hit.push(o);
if self.ents[o].alive {
self.tower_damage(o, dmg * (0.75 - 0.1 * n as f32));
}
lx = ox;
ly = oy;
}
}
}
}
}
// a unit the tower shoots at
fn tower_enemy(&self, t: &Tower, e: &Ent) -> bool {
e.captive == 0 && !e.chained && if t.hero { e.side() != 1 } else { e.side() != 0 }
}
// a hero attacks a tower; false = tower gone or unreachable
fn attack_tower(&mut self, e: &mut Ent, id: u32, dt: f32) -> bool {
let Some(k) = self.towers.iter().position(|t| t.id == id) else { return false };
@@ -4127,8 +4237,8 @@ impl Game {
}
let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held) else { return false };
let e = &self.ents[j];
if e.rival() {
return false; // the rivals' creatures cannot be picked up
if e.rival() || e.dwarf() {
return false; // the rivals' creatures and the dwarves cannot be picked up
}
if e.hero {
if e.captive != 1 {
@@ -4209,6 +4319,9 @@ impl Game {
pub fn slap(&mut self, target: Option<usize>) -> u8 {
let Some(j) = target.filter(|&j| j < self.ents.len() && self.ents[j].alive && !self.ents[j].held) else { return 0 };
let (x, y) = (self.ents[j].x, self.ents[j].y);
if self.ents[j].dwarf() {
return 0;
}
if !self.ents[j].hero && !self.ents[j].rival() {
let e = &mut self.ents[j];
e.boost = 15.0;
@@ -4323,6 +4436,7 @@ pub enum TowerKind {
Arrow,
Cannon,
Magic,
Rune, // dwarven rune forge tower: from a chest of a dwarf cave, built for free
}
pub struct TowerStats {
@@ -4340,6 +4454,7 @@ pub fn tower_stats(k: TowerKind) -> TowerStats {
TowerKind::Arrow => TowerStats { cost: [250, 200, 300], hp: 300.0, dmg: 10.0, range: 6.0, min_range: 0.0, interval: 0.8, mana: 0.0 },
TowerKind::Cannon => TowerStats { cost: [450, 350, 500], hp: 450.0, dmg: 32.0, range: 8.5, min_range: 2.0, interval: 2.6, mana: 0.0 },
TowerKind::Magic => TowerStats { cost: [350, 300, 450], hp: 300.0, dmg: 24.0, range: 7.0, min_range: 0.0, interval: 1.5, mana: 8.0 },
TowerKind::Rune => TowerStats { cost: [0, 800, 1200], hp: 1400.0, dmg: 55.0, range: 10.0, min_range: 0.0, interval: 1.3, mana: 0.0 },
}
}
@@ -4348,6 +4463,7 @@ pub fn tower_name(k: TowerKind) -> &'static str {
TowerKind::Arrow => "tower.arrow",
TowerKind::Cannon => "tower.cannon",
TowerKind::Magic => "tower.magic",
TowerKind::Rune => "tower.rune",
}
}
@@ -4363,6 +4479,8 @@ pub struct Tower {
pub flash: f32,
#[serde(default)]
pub hero: bool, // tower of a knight fortress: shoots the player's units
#[serde(default)]
pub rune_final: bool, // rune forge tower built from a refund: selling it gives nothing back
}
impl Tower {
@@ -4386,6 +4504,7 @@ impl Tower {
TowerKind::Arrow => 1.0 + 0.3 * (self.level - 1) as f32,
TowerKind::Cannon => 0.6 + 0.15 * (self.level - 1) as f32,
TowerKind::Magic => 1.2 + 0.3 * (self.level - 1) as f32,
TowerKind::Rune => 1.4 + 0.25 * (self.level - 1) as f32,
}
}
// gold spent on this tower so far
@@ -4615,7 +4734,9 @@ mod tests {
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);
// (tiles partly filled with rubies take no gold, so the core may hold a little more)
let gem_room: f32 = g.map.t.iter().filter(|t| t.kind == Kind::Treasury && t.owner == Owner::Player && t.res != 0).map(|t| TREASURY_TILE - t.gold as f32).sum();
assert!(g.reserve > 0.0 && g.reserve <= CORE_STORE + gem_room + 1.0, "reserve {} gold {} cap {}", g.reserve, g.gold, g.gold_cap());
}
#[test]
+19
View File
@@ -69,6 +69,7 @@ pub const T_STEEL: u32 = 130; // 4 variants: riveted iron plates
pub const I_STEEL: u32 = 134;
pub const T_RIVAL1: u32 = 135; // 4 variants: floor of the green master
pub const T_RIVAL2: u32 = 139; // 4 variants: floor of the blue master
pub const I_RUNE: u32 = 143; // rune forge tower
pub struct Gfx {
pub atlas: Texture2D,
@@ -570,6 +571,23 @@ fn gen_atlas() -> Image {
None
}
});
cv.cell(I_RUNE, |x, y| {
// a stone obelisk with a glowing rune, bronze bands
let body = (5..=10).contains(&x) && y >= 3 && y <= 14 && !(y == 3 && (x == 5 || x == 10));
if !body {
return None;
}
let rune = (x == 7 || x == 8) && (6..=10).contains(&y) || (y == 8 && (6..=9).contains(&x));
Some(if rune {
c(90, 210, 255)
} else if y == 4 || y == 12 {
c(190, 120, 60)
} else if (x + y) % 5 == 0 {
c(80, 80, 92)
} else {
c(110, 110, 124)
})
});
cv.cell(I_SUMMON, |x, y| {
let d = (((x - 8) * (x - 8) + (y - 8) * (y - 8)) as f32).sqrt();
let a = ((y - 8) as f32).atan2((x - 8) as f32);
@@ -1383,6 +1401,7 @@ impl Gfx {
Kind::Aerie => T_AERIE + v,
Kind::Steel => T_STEEL + v,
Kind::ManaSpring => T_PORTALFLOOR + v,
Kind::RuneChest => T_FLOOR + v,
}
}
+1
View File
@@ -3,6 +3,7 @@
mod audio;
mod campaign;
mod cloud;
mod dwarves;
mod fortress;
mod game;
mod gfx;
+6 -3
View File
@@ -60,13 +60,14 @@ pub enum Kind {
Aerie, // room for flying creatures (their beds)
Steel, // steel wall: hardly anyone gets through
ManaSpring, // crystal spring: once claimed, more mana and a higher mana maximum
RuneChest, // dwarven chest with the plans of a rune forge tower (claim it: one free build)
}
// options for the map generator (campaign missions)
#[derive(Clone, Debug)]
pub struct GenOpts {
pub portals: Option<usize>, // None: one per 64 x 64 tiles
pub treasures: Option<usize>, // guarded treasure caves; None: portals - 1
pub treasures: Option<usize>, // knight treasure caves; None: none (free play has dwarf caves instead)
pub gate: bool, // hero gate at the far side
pub core_y: Option<i32>, // row of the core (None: about the middle)
}
@@ -88,7 +89,7 @@ pub struct River {
}
// order matters for the packed savegame codec: only append new kinds
const KINDS: [Kind; 26] = [
const KINDS: [Kind; 27] = [
Kind::Rock,
Kind::Earth,
Kind::Gold,
@@ -115,6 +116,7 @@ const KINDS: [Kind; 26] = [
Kind::Aerie,
Kind::Steel,
Kind::ManaSpring,
Kind::RuneChest,
];
// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond
@@ -210,6 +212,7 @@ impl Tile {
| Kind::Shard
| Kind::Aerie
| Kind::ManaSpring
| Kind::RuneChest
)
}
// river tiles (open, but nobody can walk there)
@@ -349,7 +352,7 @@ impl Map {
let area = (w * h) as f32 / 4096.0;
let scale = |n: f32| (n * area).round() as i32;
let n_portals = opts.portals.unwrap_or((area.round() as usize).max(1));
let n_treasures = opts.treasures.unwrap_or(n_portals.saturating_sub(1));
let n_treasures = opts.treasures.unwrap_or(0);
let mut t = vec![Tile::new(Kind::Earth); w * h];
for (i, tile) in t.iter_mut().enumerate() {
+16
View File
@@ -98,6 +98,7 @@ pub fn draw_props_solid(g: &Game, cam: &Cam, t: f32) {
Kind::Library if z % 2 == 0 => shelf(c, h, l),
Kind::Library if h % 3 == 0 => lectern(c, l),
Kind::ManaSpring => spring_basin(c, l),
Kind::RuneChest => rune_chest(c, l),
_ => {}
}
}
@@ -118,6 +119,16 @@ pub fn draw_props_solid(g: &Game, cam: &Cam, t: f32) {
}
}
// the dwarves' iron-bound chest with the rune forge plans (the rune glows in the glow pass)
fn rune_chest(c: Vec3, l: f32) {
cube(c + vec3(0.0, 0.16, 0.0), vec3(0.5, 0.3, 0.34), rgb(92, 60, 34, l));
cube(c + vec3(0.0, 0.34, 0.0), vec3(0.52, 0.08, 0.36), rgb(110, 72, 40, l));
for dx in [-0.18f32, 0.18] {
cube(c + vec3(dx, 0.2, 0.0), vec3(0.05, 0.4, 0.38), rgb(176, 112, 56, l));
}
cube(c + vec3(0.0, 0.22, 0.18), vec3(0.08, 0.1, 0.03), rgb(200, 170, 80, l));
}
// a stone basin around a mana spring
fn spring_basin(c: Vec3, l: f32) {
for (dx, dz, sx, sz) in [(0.0, -0.38, 0.8, 0.1), (0.0, 0.38, 0.8, 0.1), (-0.38, 0.0, 0.1, 0.8), (0.38, 0.0, 0.1, 0.8)] {
@@ -183,6 +194,11 @@ pub fn draw_props_glow(g: &Game, cam: &Cam, t: f32) {
}
}
}
Kind::RuneChest => {
let f = 0.6 + 0.4 * (t * 2.5 + k).sin();
draw_cube(c + vec3(0.0, 0.39, 0.0), vec3(0.14, 0.02, 0.14), None, Color::new(0.35, 0.8, 1.0, f));
draw_sphere(c + vec3(0.0, 0.45, 0.0), 0.2, None, Color::new(0.3, 0.7, 1.0, 0.15 * f));
}
Kind::ManaSpring => {
// rising mana: a pulsing pool and motes that drift up
let f = 0.8 + 0.2 * (t * 2.0 + k).sin();
+54
View File
@@ -1347,6 +1347,34 @@ fn draw_towers_solid(g: &Game) {
bx(vec3(dx, h + 0.12, dz), vec3(0.06, 0.18, 0.06), col(120, 104, 140));
}
}
TowerKind::Rune => {
// dwarven rune forge: broad granite foot, bronze bands, a tapering keep with
// carved buttresses, an anvil crown and a forge hearth on top (runes glow)
bx(vec3(0.0, 0.07, 0.0), vec3(0.96, 0.14, 0.96), col(70, 66, 70));
bx(vec3(0.0, 0.19, 0.0), vec3(0.84, 0.1, 0.84), col(84, 80, 86));
let steps = 5;
for k in 0..steps {
let y0 = 0.24 + (h - 0.24) * k as f32 / steps as f32;
let hh = (h - 0.24) / steps as f32;
let w = 0.72 - k as f32 * 0.05;
bx(vec3(0.0, y0 + hh * 0.5, 0.0), vec3(w, hh + 0.005, w), col(96 - k as u8 * 2, 92 - k as u8 * 2, 98));
bx(vec3(0.0, y0 + 0.01, 0.0), vec3(w + 0.03, 0.035, w + 0.03), shade_c(176, 112, 56, f));
}
for (dx, dz) in [(0.38f32, 0.38f32), (-0.38, 0.38), (0.38, -0.38), (-0.38, -0.38)] {
bx(vec3(dx, h * 0.3, dz), vec3(0.14, h * 0.6, 0.14), col(78, 74, 80));
bx(vec3(dx, h * 0.6 + 0.05, dz), vec3(0.17, 0.08, 0.17), shade_c(176, 112, 56, f));
}
// gate with a bronze door
bx(vec3(0.0, 0.42, 0.37), vec3(0.26, 0.4, 0.04), shade_c(120, 76, 40, f));
bx(vec3(0.0, 0.64, 0.38), vec3(0.32, 0.06, 0.05), col(110, 106, 112));
// anvil crown and hearth
bx(vec3(0.0, h + 0.05, 0.0), vec3(0.62, 0.1, 0.62), col(60, 58, 64));
bx(vec3(0.0, h + 0.16, 0.0), vec3(0.3, 0.12, 0.42), col(48, 48, 54));
bx(vec3(0.0, h + 0.25, 0.0), vec3(0.42, 0.08, 0.56), col(56, 56, 62));
for (dx, dz) in [(-0.27f32, -0.27f32), (0.27, -0.27), (0.27, 0.27), (-0.27, 0.27)] {
bx(vec3(dx, h + 0.16, dz), vec3(0.08, 0.22, 0.08), shade_c(176, 112, 56, f));
}
}
}
// level pips on the plinth
for k in 0..tw.level {
@@ -1357,6 +1385,29 @@ fn draw_towers_solid(g: &Game) {
fn draw_towers_glow(g: &Game, t: f32) {
for (k, tw) in g.towers.iter().enumerate() {
if tw.kind == TowerKind::Rune {
// forge glow on the crown, runes on the keep and three rune stones circling it
let (x, z) = tw.pos();
let h = tower_height(tw);
let pulse = 1.0 + (t * 3.0 + k as f32).sin() * 0.12 + tw.flash * 2.5;
draw_sphere(vec3(x, h + 0.34, z), 0.1 * pulse, None, Color::new(1.0, 0.75, 0.3, 0.95));
draw_sphere(vec3(x, h + 0.36, z), 0.24 * pulse, None, Color::new(1.0, 0.55, 0.15, 0.22));
for r in 0..4 {
let y = 0.4 + (h - 0.5) * r as f32 / 4.0;
let glow = 0.35 + 0.65 * ((t * 2.0 - r as f32 * 0.9 + k as f32).sin() * 0.5 + 0.5);
let w = 0.72 - r as f32 * 0.05;
for (dx, dz) in [(w * 0.5 + 0.01, 0.0f32), (-w * 0.5 - 0.01, 0.0), (0.0, w * 0.5 + 0.01), (0.0, -w * 0.5 - 0.01)] {
draw_cube(vec3(x + dx, y + 0.1, z + dz), vec3(if dx == 0.0 { 0.12 } else { 0.02 }, 0.1, if dz == 0.0 { 0.12 } else { 0.02 }), None, Color::new(0.35, 0.8, 1.0, glow));
}
}
for n in 0..(2 + tw.level) {
let a = t * 1.2 + n as f32 * std::f32::consts::TAU / (2 + tw.level) as f32;
let p = vec3(x + a.cos() * 0.62, h * 0.7 + (t * 2.0 + n as f32).sin() * 0.08, z + a.sin() * 0.62);
draw_cube(p, vec3(0.1, 0.12, 0.1), None, Color::new(0.3, 0.75, 1.0, 0.9));
draw_sphere(p, 0.12, None, Color::new(0.3, 0.7, 1.0, 0.2));
}
continue;
}
if tw.kind != TowerKind::Magic {
continue;
}
@@ -1595,6 +1646,8 @@ fn draw_shadows(g: &Game) {
disc(vec3(e.x, 0.012, e.y), r, Color::new(0.0, 0.0, 0.0, 0.4), 12);
let rc = if e.captive != 0 {
Color::new(0.6, 0.6, 0.6, 0.6)
} else if e.dwarf() {
Color::new(0.85, 0.55, 0.25, 0.85)
} else if e.rival() {
if e.side == 2 {
Color::new(0.35, 0.95, 0.4, 0.85)
@@ -1806,6 +1859,7 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::Aerie => [170, 140, 90, 255],
Kind::Steel => [150, 160, 180, 255],
Kind::ManaSpring => [90, 230, 255, 255],
Kind::RuneChest => [80, 200, 255, 255],
}
}
+2 -1
View File
@@ -85,6 +85,7 @@ impl Game {
g.msg("msg.welcome1", vec![]);
g.msg("msg.welcome2", vec![]);
g.msg(if rivals == 0 { "msg.welcome_humans" } else if humans { "msg.welcome_both" } else { "msg.welcome_rivals" }, vec![rivals.to_string()]);
g.place_dwarf_caves(seed);
g
}
@@ -267,7 +268,7 @@ impl Game {
}
}
fn eliminate_rival(&mut self, k: usize) {
pub(crate) fn eliminate_rival(&mut self, k: usize) {
let side = self.rivals[k].side;
let owner = Owner::of_side(side);
self.rivals[k].alive = false;
+2 -1
View File
@@ -98,7 +98,8 @@ mod tests {
let mut g = Game::new(7, 256, 256);
let gen = t0.elapsed();
assert_eq!(g.map.portals.len(), 16);
assert_eq!(g.map.treasures.len(), 15);
assert_eq!(g.map.treasures.len(), 0); // knight caves are replaced by dwarf caves
assert!((1..=4).contains(&g.dwarf_caves.len()));
let (cx, cy) = g.map.core;
g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true);
let t1 = std::time::Instant::now();
+2 -2
View File
@@ -25,7 +25,7 @@ pub enum Action {
}
pub fn all_actions() -> Vec<Action> {
let mut v: Vec<Action> = (0..20).map(Action::Tool).collect();
let mut v: Vec<Action> = (0..21).map(Action::Tool).collect();
v.extend_from_slice(&[
Action::CamForward,
Action::CamBack,
@@ -70,7 +70,7 @@ impl Action {
fn default_key(self) -> KeyCode {
use KeyCode::*;
match self {
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T, U, Y, O][i.min(19)],
Action::Tool(i) => [Key1, Key2, Key3, Key4, Key5, Key6, Key7, Key8, Key9, Key0, X, V, K, J, B, C, T, U, Y, O, N][i.min(20)],
Action::CamForward => W,
Action::CamBack => S,
Action::CamLeft => A,
+20 -3
View File
@@ -160,7 +160,7 @@ pub enum Tool {
}
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
pub const TOOLS: [Tool; 20] = [
pub const TOOLS: [Tool; 21] = [
Tool::Dig,
Tool::Hand,
Tool::Room(0),
@@ -181,6 +181,7 @@ pub const TOOLS: [Tool; 20] = [
Tool::Bridge,
Tool::Room(7),
Tool::Steel,
Tool::Tower(TowerKind::Rune),
];
@@ -206,6 +207,7 @@ pub fn tool_info(t: Tool) -> ToolInfo {
Tool::Tower(TowerKind::Arrow) => ("arrow", I_ARROW, None),
Tool::Tower(TowerKind::Cannon) => ("cannon", I_CANNON, None),
Tool::Tower(TowerKind::Magic) => ("magic", I_MAGIC, None),
Tool::Tower(TowerKind::Rune) => ("rune", I_RUNE, None),
Tool::Sell => ("sell", I_SELL, None),
Tool::Flag => ("flag", I_FLAG, None),
Tool::Lightning => ("lightning", I_BOLT, None),
@@ -229,6 +231,10 @@ pub fn tool_cost(t: Tool, g: &Game) -> String {
Tool::Summon => trf("cost.mana", &[(g.summon_cost() as i32).to_string()]),
Tool::Bridge => trf("cost.bridge", &[BRIDGE_COST[0].to_string(), BRIDGE_COST[1].to_string()]),
Tool::Steel => trf("cost.steel", &[STEEL_COST[0].to_string(), STEEL_COST[1].to_string()]),
Tool::Tower(TowerKind::Rune) => {
let s = tower_stats(TowerKind::Rune);
trf("cost.rune", &[g.rune_available().to_string(), s.cost[1].to_string(), s.cost[2].to_string()])
}
Tool::Tower(k) => {
let s = tower_stats(k);
let mut c = trf("cost.tower", &[s.cost[0].to_string(), s.cost[1].to_string(), s.cost[2].to_string()]);
@@ -246,7 +252,7 @@ pub const CATS: [(&str, &[usize]); 6] = [
("cat.dig", &[0]),
("cat.hand", &[1]),
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13, 18, 17]),
("cat.towers", &[14, 15, 16]),
("cat.towers", &[14, 15, 16, 20]),
("cat.spells", &[9, 10, 11]),
("cat.orders", &[8, 7, 19]),
];
@@ -654,6 +660,8 @@ impl Ui {
if e.alive && !e.held {
let c = if e.hero {
Color::new(0.6, 0.85, 1.0, 1.0)
} else if e.dwarf() {
Color::new(0.85, 0.55, 0.25, 1.0)
} else if e.rival() {
if e.side == 2 {
Color::new(0.3, 0.95, 0.4, 1.0)
@@ -762,7 +770,7 @@ impl Ui {
// the open row of tools (wraps into several rows if needed)
if let Some((c, x0, bw, bcols)) = popup {
let tools = CATS[c].1;
let tools: Vec<usize> = CATS[c].1.iter().copied().filter(|&i| g.tool_visible(i)).collect();
let per_row = (((cols - 2) as f32 / bcols).floor() as usize).max(1).min(tools.len());
let nrows = (tools.len() + per_row - 1) / per_row;
let pw = per_row as f32 * bw + 2.0 * CW();
@@ -1052,6 +1060,11 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
out.push((tr(["", "info.unconscious", "info.carried", "info.tortured"][e.captive.min(3) as usize]), RED));
return out;
}
if e.dwarf() {
out.push((tr("info.dwarf"), Color::new(0.9, 0.6, 0.3, 1.0)));
out.push((tr("info.dwarf_neutral"), DIM));
return out;
}
out.push((tr(job_name(e)), TXT));
if e.hero {
out.push((tr(if e.guard { "info.guard" } else { "info.intruder" }), BLUE));
@@ -1136,6 +1149,10 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
out.push((tr("tile.shard"), RED));
out.push((tr("tile.shard_info"), DIM));
}
Kind::RuneChest => {
out.push((tr("tile.rune_chest"), BLUE));
out.push((tr("tile.rune_chest_info"), DIM));
}
Kind::Gate => {
out.push((tr("tile.gate"), BLUE));
out.push((trf("info.hp", &[(g.gate_hp as i32).to_string(), (GATE_HP as i32).to_string()]), BLUE));