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:
1 parent
d92b45a0b8
commit
9a9f04ae1f
17 files changed
+658
-32
No files matched your search
@@ -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)
|
- 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
|
- 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)
|
- **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
|
- 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
|
## Game content
|
||||||
|
|||||||
@@ -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
|
- 3D isometric view with a freely rotatable and tiltable camera
|
||||||
- Procedural pixel art, irregular cave walls, flickering torch light
|
- Procedural pixel art, irregular cave walls, flickering torch light
|
||||||
- CP437 text-mode style interface with the DINOBYTE bitmap font
|
- 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)
|
- 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.
|
See [DESIGN.md](DESIGN.md) for all design decisions and [CREDITS.md](CREDITS.md) for third-party assets.
|
||||||
|
|||||||
@@ -624,3 +624,17 @@ hud.rival1 = Grün
|
|||||||
hud.rival2 = Blau
|
hud.rival2 = Blau
|
||||||
msg.creature_freed = Ein {0} sprengt seine Ketten und schließt sich dir an!
|
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.
|
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).
|
||||||
@@ -625,3 +625,17 @@ hud.rival1 = Green
|
|||||||
hud.rival2 = Blue
|
hud.rival2 = Blue
|
||||||
msg.creature_freed = A {0} breaks its chains and joins you!
|
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.
|
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
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
// From which mission (0-based) on each tool is available (same order as ui::TOOLS):
|
// 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,
|
// dig, hand, treasury, lair, farm, training, library, sell, flag, lightning, heal, imp,
|
||||||
// torture, temple, arrow tower, cannon tower, magic tower, bridge, aerie, steel
|
// 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 struct FortDef {
|
||||||
pub at: (f32, f32), // centre as a fraction of the map size
|
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 struct Carry {
|
||||||
pub creatures: Vec<(EK, u32, f32)>, // kind, level, experience
|
pub creatures: Vec<(EK, u32, f32)>, // kind, level, experience
|
||||||
pub gold: f32,
|
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;
|
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);
|
let y = ((ry * h as f32) as i32).clamp(10, h as i32 - 10);
|
||||||
g.add_rival(side, x, y, diff);
|
g.add_rival(side, x, y, diff);
|
||||||
}
|
}
|
||||||
|
g.place_dwarf_caves(d.seed);
|
||||||
let heart = d.gate || d.forts.iter().any(|f| f.heart);
|
let heart = d.gate || d.forts.iter().any(|f| f.heart);
|
||||||
if !heart {
|
if !heart {
|
||||||
g.gate_hp = 0.0;
|
g.gate_hp = 0.0;
|
||||||
@@ -524,6 +529,8 @@ pub fn start_mission(i: usize, carry: Option<&Carry>) -> Game {
|
|||||||
if let Some(c) = carry {
|
if let Some(c) = carry {
|
||||||
joiners.extend(c.creatures.iter().copied());
|
joiners.extend(c.creatures.iter().copied());
|
||||||
g.gold += c.gold;
|
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() {
|
for (k, &(kind, level, xp)) in joiners.iter().enumerate() {
|
||||||
let a = k as f32 * 0.9;
|
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 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;
|
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));
|
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)]
|
#[cfg(test)]
|
||||||
|
|||||||
+355
@@ -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
@@ -134,7 +134,7 @@ impl Game {
|
|||||||
let id = self.next_id;
|
let id = self.next_id;
|
||||||
self.next_id += 1;
|
self.next_id += 1;
|
||||||
let s = tower_stats(kind);
|
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) {
|
while tw.level < f.tower_level.min(3) {
|
||||||
let old = tw.max_hp();
|
let old = tw.max_hp();
|
||||||
tw.level += 1;
|
tw.level += 1;
|
||||||
|
|||||||
+138
-17
@@ -120,6 +120,8 @@ pub struct St {
|
|||||||
pub bounty: i32,
|
pub bounty: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const RUNE_TOOL: usize = 20; // index of the rune forge tower in ui::TOOLS
|
||||||
|
|
||||||
pub fn st(k: EK) -> St {
|
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 };
|
let s = |name, hp, atk, range, speed, interval, wage, bounty| St { name, hp, atk, range, speed, interval, wage, bounty };
|
||||||
match k {
|
match k {
|
||||||
@@ -322,7 +324,11 @@ impl Ent {
|
|||||||
}
|
}
|
||||||
// a unit of a rival master
|
// a unit of a rival master
|
||||||
pub fn rival(&self) -> bool {
|
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)
|
// a hero that is still fighting (not a prisoner)
|
||||||
pub fn active_hero(&self) -> bool {
|
pub fn active_hero(&self) -> bool {
|
||||||
@@ -436,6 +442,16 @@ pub struct Game {
|
|||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub springs: usize, // claimed mana springs (recounted)
|
pub springs: usize, // claimed mana springs (recounted)
|
||||||
#[serde(default)]
|
#[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
|
pub freed: u32, // chained creatures freed // tiles next to fighting heroes: knight ground there cannot be claimed
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub hints: bool, // show hint messages (setting)
|
pub hints: bool, // show hint messages (setting)
|
||||||
@@ -511,6 +527,7 @@ impl Game {
|
|||||||
let dist = ((gx - cx).abs() + (gy - cy).abs()) as f32;
|
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.wave_timer = FIRST_WAVE + ((dist - 45.0) * 1.5).clamp(0.0, 300.0);
|
||||||
g.guard_treasures();
|
g.guard_treasures();
|
||||||
|
g.place_dwarf_caves(seed);
|
||||||
g.msg("msg.welcome1", vec![]);
|
g.msg("msg.welcome1", vec![]);
|
||||||
g.msg("msg.welcome2", vec![]);
|
g.msg("msg.welcome2", vec![]);
|
||||||
g.msg("msg.welcome3", vec![]);
|
g.msg("msg.welcome3", vec![]);
|
||||||
@@ -582,6 +599,11 @@ impl Game {
|
|||||||
gate_on: true,
|
gate_on: true,
|
||||||
springs: 0,
|
springs: 0,
|
||||||
freed: 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;
|
let (cx, cy) = g.map.core;
|
||||||
// the soul core starts surrounded by a one tile wide ring of treasury
|
// the soul core starts surrounded by a one tile wide ring of treasury
|
||||||
@@ -615,6 +637,11 @@ impl Game {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- campaign missions
|
// ---------------------------------------------------------------- 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 {
|
pub fn tool_allowed(&self, tool: usize) -> bool {
|
||||||
self.mission.as_ref().map_or(true, |m| m.tools.get(tool).copied().unwrap_or(true))
|
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() {
|
if !self.rivals.is_empty() {
|
||||||
self.update_rivals(dt);
|
self.update_rivals(dt);
|
||||||
}
|
}
|
||||||
|
self.update_dwarves(dt);
|
||||||
self.check_end();
|
self.check_end();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1357,7 +1385,7 @@ impl Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// strength of a squad (same formula as the army value)
|
// 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);
|
let (mut h, mut d) = (0.0, 0.0);
|
||||||
for &(k, l) in list {
|
for &(k, l) in list {
|
||||||
let s = st(k);
|
let s = st(k);
|
||||||
@@ -1574,6 +1602,8 @@ impl Game {
|
|||||||
self.captive_update(e, dt);
|
self.captive_update(e, dt);
|
||||||
} else if e.rival() {
|
} else if e.rival() {
|
||||||
self.rival_ai(e, dt);
|
self.rival_ai(e, dt);
|
||||||
|
} else if e.dwarf() {
|
||||||
|
self.dwarf_ai(e, dt);
|
||||||
} else if e.hero {
|
} else if e.hero {
|
||||||
self.hero_ai(e, dt);
|
self.hero_ai(e, dt);
|
||||||
} else if e.kind == EK::Worker {
|
} 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.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff);
|
||||||
self.sfx(Sfx::HeroDeath, x, y);
|
self.sfx(Sfx::HeroDeath, x, y);
|
||||||
self.msg("msg.hero_slain", vec![format!("@{}", s.name), (got as i32).to_string()]);
|
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 {
|
} else if side >= 2 {
|
||||||
// a creature of a rival master: half its wage as booty
|
// a creature of a rival master: half its wage as booty
|
||||||
let got = self.add_gold(s.wage as f32 * 0.5);
|
let got = self.add_gold(s.wage as f32 * 0.5);
|
||||||
@@ -2161,6 +2195,9 @@ impl Game {
|
|||||||
self.sfx(Sfx::Magic, x, y);
|
self.sfx(Sfx::Magic, x, y);
|
||||||
self.msg("msg.spring_taken", vec![]);
|
self.msg("msg.spring_taken", vec![]);
|
||||||
}
|
}
|
||||||
|
if t.kind == Kind::RuneChest {
|
||||||
|
self.open_rune_chest(i);
|
||||||
|
}
|
||||||
if t.kind == Kind::Shard {
|
if t.kind == Kind::Shard {
|
||||||
self.shards_taken += 1;
|
self.shards_taken += 1;
|
||||||
self.fx(7, x, y, 0.0, 0.0, 1.2, 0xff6060ff);
|
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
|
// a fortress unit spotted an enemy: guards and patrols nearby join the fight
|
||||||
fn alarm(&mut self, x: f32, y: f32, target: u32) {
|
fn alarm(&mut self, x: f32, y: f32, target: u32) {
|
||||||
for o in self.ents.iter_mut() {
|
for o in self.ents.iter_mut() {
|
||||||
@@ -2294,7 +2341,7 @@ impl Game {
|
|||||||
fn claimable(&self, i: usize, me: u32) -> bool {
|
fn claimable(&self, i: usize, me: u32) -> bool {
|
||||||
let t = &self.map.t[i];
|
let t = &self.map.t[i];
|
||||||
let ok = match t.owner {
|
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 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
|
// knight and rival ground (also vaults and shard chambers), but not while enemies fight there
|
||||||
Owner::Hero | Owner::Rival1 | Owner::Rival2 => {
|
Owner::Hero | Owner::Rival1 | Owner::Rival2 => {
|
||||||
@@ -2604,13 +2651,7 @@ impl Game {
|
|||||||
if e.carry > 0 && self.start_deliver(e) {
|
if e.carry > 0 && self.start_deliver(e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.claimable(i, me), true) {
|
// damaged towers are repaired (free of charge) before more ground is claimed
|
||||||
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.damaged_tower_near(i).is_some(), true) {
|
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);
|
let end = p.last().copied().unwrap_or(cur);
|
||||||
if let Some(id) = self.damaged_tower_near(end) {
|
if let Some(id) = self.damaged_tower_near(end) {
|
||||||
@@ -2619,6 +2660,13 @@ impl Game {
|
|||||||
return;
|
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) {
|
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);
|
let end = p.last().copied().unwrap_or(cur);
|
||||||
if let Some(w) = self.reinforce_neighbor(end, me) {
|
if let Some(w) = self.reinforce_neighbor(end, me) {
|
||||||
@@ -3662,6 +3710,16 @@ impl Game {
|
|||||||
let i = idx(x, y);
|
let i = idx(x, y);
|
||||||
if let Some(k) = self.towers.iter().position(|t| t.tile == i && !t.hero) {
|
if let Some(k) = self.towers.iter().position(|t| t.tile == i && !t.hero) {
|
||||||
refund += self.towers[k].invested() as f32 * 0.5;
|
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.towers.remove(k);
|
||||||
self.map.t[i].kind = Kind::Floor;
|
self.map.t[i].kind = Kind::Floor;
|
||||||
continue;
|
continue;
|
||||||
@@ -3758,11 +3816,25 @@ impl Game {
|
|||||||
self.sfx_ui(Sfx::Error);
|
self.sfx_ui(Sfx::Error);
|
||||||
return;
|
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.gold -= cost;
|
||||||
self.map.t[i].kind = Kind::Tower;
|
self.map.t[i].kind = Kind::Tower;
|
||||||
let id = self.next_id;
|
let id = self.next_id;
|
||||||
self.next_id += 1;
|
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();
|
let (px, py) = tower.pos();
|
||||||
self.towers.push(tower);
|
self.towers.push(tower);
|
||||||
// units standing on the tile step aside
|
// units standing on the tile step aside
|
||||||
@@ -3826,8 +3898,7 @@ impl Game {
|
|||||||
for (j, e) in self.ents.iter().enumerate() {
|
for (j, e) in self.ents.iter().enumerate() {
|
||||||
// knight towers shoot the player's units, the player's towers shoot heroes
|
// 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
|
// 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 || !self.tower_enemy(t, e) {
|
||||||
if !e.alive || e.held || !enemy {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let d = (e.x - px).powi(2) + (e.y - py).powi(2);
|
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::Arrow => (px, py, t.top() + 0.25),
|
||||||
TowerKind::Cannon => (px + aim.cos() * 0.55, py + aim.sin() * 0.55, t.top() + 0.2),
|
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::Magic => (px, py, t.top() + 0.45),
|
||||||
|
TowerKind::Rune => (px, py, t.top() + 0.55),
|
||||||
};
|
};
|
||||||
let col = match t.kind {
|
let col = match t.kind {
|
||||||
TowerKind::Arrow => 0xffd0e0f0,
|
TowerKind::Arrow => 0xffd0e0f0,
|
||||||
TowerKind::Cannon => 0xff303030,
|
TowerKind::Cannon => 0xff303030,
|
||||||
TowerKind::Magic => 0xffff60d0,
|
TowerKind::Magic => 0xffff60d0,
|
||||||
|
TowerKind::Rune => 0xff40c0ff,
|
||||||
};
|
};
|
||||||
self.shot(mx, my, mh, ex, ey, eh, 0.3, col);
|
self.shot(mx, my, mh, ex, ey, eh, 0.3, col);
|
||||||
self.fx_at(2, ex, ey, eh, 0.3, 0xffffffff);
|
self.fx_at(2, ex, ey, eh, 0.3, 0xffffffff);
|
||||||
self.sfx(match t.kind {
|
self.sfx(match t.kind {
|
||||||
TowerKind::Arrow => Sfx::Shoot,
|
TowerKind::Arrow => Sfx::Shoot,
|
||||||
TowerKind::Cannon => Sfx::Cannon,
|
TowerKind::Cannon => Sfx::Cannon,
|
||||||
TowerKind::Magic => Sfx::Magic,
|
TowerKind::Magic | TowerKind::Rune => Sfx::Magic,
|
||||||
}, px, py);
|
}, px, py);
|
||||||
let dmg = t.dmg();
|
let dmg = t.dmg();
|
||||||
match target {
|
match target {
|
||||||
@@ -3983,6 +4056,7 @@ impl Game {
|
|||||||
TowerKind::Arrow => (px, py, t.top() + 0.25),
|
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::Cannon => (px + aim.cos() * 0.55, py + aim.sin() * 0.55, t.top() + 0.2),
|
||||||
TowerKind::Magic => (px, py, t.top() + 0.45),
|
TowerKind::Magic => (px, py, t.top() + 0.45),
|
||||||
|
TowerKind::Rune => (px, py, t.top() + 0.55),
|
||||||
};
|
};
|
||||||
let dmg = t.dmg();
|
let dmg = t.dmg();
|
||||||
match t.kind {
|
match t.kind {
|
||||||
@@ -4065,9 +4139,45 @@ 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
|
// a hero attacks a tower; false = tower gone or unreachable
|
||||||
fn attack_tower(&mut self, e: &mut Ent, id: u32, dt: f32) -> bool {
|
fn attack_tower(&mut self, e: &mut Ent, id: u32, dt: f32) -> bool {
|
||||||
@@ -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 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];
|
let e = &self.ents[j];
|
||||||
if e.rival() {
|
if e.rival() || e.dwarf() {
|
||||||
return false; // the rivals' creatures cannot be picked up
|
return false; // the rivals' creatures and the dwarves cannot be picked up
|
||||||
}
|
}
|
||||||
if e.hero {
|
if e.hero {
|
||||||
if e.captive != 1 {
|
if e.captive != 1 {
|
||||||
@@ -4209,6 +4319,9 @@ impl Game {
|
|||||||
pub fn slap(&mut self, target: Option<usize>) -> u8 {
|
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 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);
|
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() {
|
if !self.ents[j].hero && !self.ents[j].rival() {
|
||||||
let e = &mut self.ents[j];
|
let e = &mut self.ents[j];
|
||||||
e.boost = 15.0;
|
e.boost = 15.0;
|
||||||
@@ -4323,6 +4436,7 @@ pub enum TowerKind {
|
|||||||
Arrow,
|
Arrow,
|
||||||
Cannon,
|
Cannon,
|
||||||
Magic,
|
Magic,
|
||||||
|
Rune, // dwarven rune forge tower: from a chest of a dwarf cave, built for free
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TowerStats {
|
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::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::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::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::Arrow => "tower.arrow",
|
||||||
TowerKind::Cannon => "tower.cannon",
|
TowerKind::Cannon => "tower.cannon",
|
||||||
TowerKind::Magic => "tower.magic",
|
TowerKind::Magic => "tower.magic",
|
||||||
|
TowerKind::Rune => "tower.rune",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4363,6 +4479,8 @@ pub struct Tower {
|
|||||||
pub flash: f32,
|
pub flash: f32,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub hero: bool, // tower of a knight fortress: shoots the player's units
|
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 {
|
impl Tower {
|
||||||
@@ -4386,6 +4504,7 @@ impl Tower {
|
|||||||
TowerKind::Arrow => 1.0 + 0.3 * (self.level - 1) as f32,
|
TowerKind::Arrow => 1.0 + 0.3 * (self.level - 1) as f32,
|
||||||
TowerKind::Cannon => 0.6 + 0.15 * (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::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
|
// gold spent on this tower so far
|
||||||
@@ -4615,7 +4734,9 @@ mod tests {
|
|||||||
g.gold = g.gold_cap();
|
g.gold = g.gold_cap();
|
||||||
g.update(0.01);
|
g.update(0.01);
|
||||||
assert!((treasury_total(&g) - g.gold).abs() < 2.0);
|
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]
|
#[test]
|
||||||
|
|||||||
+19
@@ -69,6 +69,7 @@ pub const T_STEEL: u32 = 130; // 4 variants: riveted iron plates
|
|||||||
pub const I_STEEL: u32 = 134;
|
pub const I_STEEL: u32 = 134;
|
||||||
pub const T_RIVAL1: u32 = 135; // 4 variants: floor of the green master
|
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 T_RIVAL2: u32 = 139; // 4 variants: floor of the blue master
|
||||||
|
pub const I_RUNE: u32 = 143; // rune forge tower
|
||||||
|
|
||||||
pub struct Gfx {
|
pub struct Gfx {
|
||||||
pub atlas: Texture2D,
|
pub atlas: Texture2D,
|
||||||
@@ -570,6 +571,23 @@ fn gen_atlas() -> Image {
|
|||||||
None
|
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| {
|
cv.cell(I_SUMMON, |x, y| {
|
||||||
let d = (((x - 8) * (x - 8) + (y - 8) * (y - 8)) as f32).sqrt();
|
let d = (((x - 8) * (x - 8) + (y - 8) * (y - 8)) as f32).sqrt();
|
||||||
let a = ((y - 8) as f32).atan2((x - 8) as f32);
|
let a = ((y - 8) as f32).atan2((x - 8) as f32);
|
||||||
@@ -1383,6 +1401,7 @@ impl Gfx {
|
|||||||
Kind::Aerie => T_AERIE + v,
|
Kind::Aerie => T_AERIE + v,
|
||||||
Kind::Steel => T_STEEL + v,
|
Kind::Steel => T_STEEL + v,
|
||||||
Kind::ManaSpring => T_PORTALFLOOR + v,
|
Kind::ManaSpring => T_PORTALFLOOR + v,
|
||||||
|
Kind::RuneChest => T_FLOOR + v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
mod audio;
|
mod audio;
|
||||||
mod campaign;
|
mod campaign;
|
||||||
mod cloud;
|
mod cloud;
|
||||||
|
mod dwarves;
|
||||||
mod fortress;
|
mod fortress;
|
||||||
mod game;
|
mod game;
|
||||||
mod gfx;
|
mod gfx;
|
||||||
|
|||||||
+6
-3
@@ -60,13 +60,14 @@ pub enum Kind {
|
|||||||
Aerie, // room for flying creatures (their beds)
|
Aerie, // room for flying creatures (their beds)
|
||||||
Steel, // steel wall: hardly anyone gets through
|
Steel, // steel wall: hardly anyone gets through
|
||||||
ManaSpring, // crystal spring: once claimed, more mana and a higher mana maximum
|
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)
|
// options for the map generator (campaign missions)
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct GenOpts {
|
pub struct GenOpts {
|
||||||
pub portals: Option<usize>, // None: one per 64 x 64 tiles
|
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 gate: bool, // hero gate at the far side
|
||||||
pub core_y: Option<i32>, // row of the core (None: about the middle)
|
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
|
// order matters for the packed savegame codec: only append new kinds
|
||||||
const KINDS: [Kind; 26] = [
|
const KINDS: [Kind; 27] = [
|
||||||
Kind::Rock,
|
Kind::Rock,
|
||||||
Kind::Earth,
|
Kind::Earth,
|
||||||
Kind::Gold,
|
Kind::Gold,
|
||||||
@@ -115,6 +116,7 @@ const KINDS: [Kind; 26] = [
|
|||||||
Kind::Aerie,
|
Kind::Aerie,
|
||||||
Kind::Steel,
|
Kind::Steel,
|
||||||
Kind::ManaSpring,
|
Kind::ManaSpring,
|
||||||
|
Kind::RuneChest,
|
||||||
];
|
];
|
||||||
|
|
||||||
// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond
|
// treasures (veins, treasury content, imp loads): 0 gold, 1 silver, 2 ruby, 3 diamond
|
||||||
@@ -210,6 +212,7 @@ impl Tile {
|
|||||||
| Kind::Shard
|
| Kind::Shard
|
||||||
| Kind::Aerie
|
| Kind::Aerie
|
||||||
| Kind::ManaSpring
|
| Kind::ManaSpring
|
||||||
|
| Kind::RuneChest
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// river tiles (open, but nobody can walk there)
|
// river tiles (open, but nobody can walk there)
|
||||||
@@ -349,7 +352,7 @@ impl Map {
|
|||||||
let area = (w * h) as f32 / 4096.0;
|
let area = (w * h) as f32 / 4096.0;
|
||||||
let scale = |n: f32| (n * area).round() as i32;
|
let scale = |n: f32| (n * area).round() as i32;
|
||||||
let n_portals = opts.portals.unwrap_or((area.round() as usize).max(1));
|
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];
|
let mut t = vec![Tile::new(Kind::Earth); w * h];
|
||||||
for (i, tile) in t.iter_mut().enumerate() {
|
for (i, tile) in t.iter_mut().enumerate() {
|
||||||
|
|||||||
@@ -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 z % 2 == 0 => shelf(c, h, l),
|
||||||
Kind::Library if h % 3 == 0 => lectern(c, l),
|
Kind::Library if h % 3 == 0 => lectern(c, l),
|
||||||
Kind::ManaSpring => spring_basin(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
|
// a stone basin around a mana spring
|
||||||
fn spring_basin(c: Vec3, l: f32) {
|
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)] {
|
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 => {
|
Kind::ManaSpring => {
|
||||||
// rising mana: a pulsing pool and motes that drift up
|
// rising mana: a pulsing pool and motes that drift up
|
||||||
let f = 0.8 + 0.2 * (t * 2.0 + k).sin();
|
let f = 0.8 + 0.2 * (t * 2.0 + k).sin();
|
||||||
|
|||||||
@@ -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));
|
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
|
// level pips on the plinth
|
||||||
for k in 0..tw.level {
|
for k in 0..tw.level {
|
||||||
@@ -1357,6 +1385,29 @@ fn draw_towers_solid(g: &Game) {
|
|||||||
|
|
||||||
fn draw_towers_glow(g: &Game, t: f32) {
|
fn draw_towers_glow(g: &Game, t: f32) {
|
||||||
for (k, tw) in g.towers.iter().enumerate() {
|
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 {
|
if tw.kind != TowerKind::Magic {
|
||||||
continue;
|
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);
|
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 {
|
let rc = if e.captive != 0 {
|
||||||
Color::new(0.6, 0.6, 0.6, 0.6)
|
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() {
|
} else if e.rival() {
|
||||||
if e.side == 2 {
|
if e.side == 2 {
|
||||||
Color::new(0.35, 0.95, 0.4, 0.85)
|
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::Aerie => [170, 140, 90, 255],
|
||||||
Kind::Steel => [150, 160, 180, 255],
|
Kind::Steel => [150, 160, 180, 255],
|
||||||
Kind::ManaSpring => [90, 230, 255, 255],
|
Kind::ManaSpring => [90, 230, 255, 255],
|
||||||
|
Kind::RuneChest => [80, 200, 255, 255],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -85,6 +85,7 @@ impl Game {
|
|||||||
g.msg("msg.welcome1", vec![]);
|
g.msg("msg.welcome1", vec![]);
|
||||||
g.msg("msg.welcome2", 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.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
|
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 side = self.rivals[k].side;
|
||||||
let owner = Owner::of_side(side);
|
let owner = Owner::of_side(side);
|
||||||
self.rivals[k].alive = false;
|
self.rivals[k].alive = false;
|
||||||
|
|||||||
+2
-1
@@ -98,7 +98,8 @@ mod tests {
|
|||||||
let mut g = Game::new(7, 256, 256);
|
let mut g = Game::new(7, 256, 256);
|
||||||
let gen = t0.elapsed();
|
let gen = t0.elapsed();
|
||||||
assert_eq!(g.map.portals.len(), 16);
|
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;
|
let (cx, cy) = g.map.core;
|
||||||
g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true);
|
g.mark_rect(cx - 5, cy - 5, cx + 8, cy + 8, true);
|
||||||
let t1 = std::time::Instant::now();
|
let t1 = std::time::Instant::now();
|
||||||
|
|||||||
+2
-2
@@ -25,7 +25,7 @@ pub enum Action {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_actions() -> Vec<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(&[
|
v.extend_from_slice(&[
|
||||||
Action::CamForward,
|
Action::CamForward,
|
||||||
Action::CamBack,
|
Action::CamBack,
|
||||||
@@ -70,7 +70,7 @@ impl Action {
|
|||||||
fn default_key(self) -> KeyCode {
|
fn default_key(self) -> KeyCode {
|
||||||
use KeyCode::*;
|
use KeyCode::*;
|
||||||
match self {
|
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::CamForward => W,
|
||||||
Action::CamBack => S,
|
Action::CamBack => S,
|
||||||
Action::CamLeft => A,
|
Action::CamLeft => A,
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ pub enum Tool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
|
// 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::Dig,
|
||||||
Tool::Hand,
|
Tool::Hand,
|
||||||
Tool::Room(0),
|
Tool::Room(0),
|
||||||
@@ -181,6 +181,7 @@ pub const TOOLS: [Tool; 20] = [
|
|||||||
Tool::Bridge,
|
Tool::Bridge,
|
||||||
Tool::Room(7),
|
Tool::Room(7),
|
||||||
Tool::Steel,
|
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::Arrow) => ("arrow", I_ARROW, None),
|
||||||
Tool::Tower(TowerKind::Cannon) => ("cannon", I_CANNON, None),
|
Tool::Tower(TowerKind::Cannon) => ("cannon", I_CANNON, None),
|
||||||
Tool::Tower(TowerKind::Magic) => ("magic", I_MAGIC, None),
|
Tool::Tower(TowerKind::Magic) => ("magic", I_MAGIC, None),
|
||||||
|
Tool::Tower(TowerKind::Rune) => ("rune", I_RUNE, None),
|
||||||
Tool::Sell => ("sell", I_SELL, None),
|
Tool::Sell => ("sell", I_SELL, None),
|
||||||
Tool::Flag => ("flag", I_FLAG, None),
|
Tool::Flag => ("flag", I_FLAG, None),
|
||||||
Tool::Lightning => ("lightning", I_BOLT, 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::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::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::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) => {
|
Tool::Tower(k) => {
|
||||||
let s = tower_stats(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()]);
|
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.dig", &[0]),
|
||||||
("cat.hand", &[1]),
|
("cat.hand", &[1]),
|
||||||
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13, 18, 17]),
|
("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.spells", &[9, 10, 11]),
|
||||||
("cat.orders", &[8, 7, 19]),
|
("cat.orders", &[8, 7, 19]),
|
||||||
];
|
];
|
||||||
@@ -654,6 +660,8 @@ impl Ui {
|
|||||||
if e.alive && !e.held {
|
if e.alive && !e.held {
|
||||||
let c = if e.hero {
|
let c = if e.hero {
|
||||||
Color::new(0.6, 0.85, 1.0, 1.0)
|
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() {
|
} else if e.rival() {
|
||||||
if e.side == 2 {
|
if e.side == 2 {
|
||||||
Color::new(0.3, 0.95, 0.4, 1.0)
|
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)
|
// the open row of tools (wraps into several rows if needed)
|
||||||
if let Some((c, x0, bw, bcols)) = popup {
|
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 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 nrows = (tools.len() + per_row - 1) / per_row;
|
||||||
let pw = per_row as f32 * bw + 2.0 * CW();
|
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));
|
out.push((tr(["", "info.unconscious", "info.carried", "info.tortured"][e.captive.min(3) as usize]), RED));
|
||||||
return out;
|
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));
|
out.push((tr(job_name(e)), TXT));
|
||||||
if e.hero {
|
if e.hero {
|
||||||
out.push((tr(if e.guard { "info.guard" } else { "info.intruder" }), BLUE));
|
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"), RED));
|
||||||
out.push((tr("tile.shard_info"), DIM));
|
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 => {
|
Kind::Gate => {
|
||||||
out.push((tr("tile.gate"), BLUE));
|
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));
|
out.push((trf("info.hp", &[(g.gate_hp as i32).to_string(), (GATE_HP as i32).to_string()]), BLUE));
|
||||||
|
|||||||
Reference in new issue
Block a user