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
+138
-17
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user