Campaign of 30 missions: six against the rival masters Mossgrim and Lady Nightwell, islands, mana springs, sealed caves with chained creatures
- missions interleaved into one story (EN/DE), new epilogue, map sizes grow to 176x176 - islands in water or lava with treasure and/or a mana spring; rivers at least four tiles wide - sealed caves with treasure and a chained creature that joins once the cave is claimed - new goals: rival cores, mana springs, freed creatures - rival messages name the masters; new progress key for the new mission order Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
dd16c684e5
commit
ea0623fbe3
11 files changed
+413
-85
No files matched your search
+57
-12
@@ -83,6 +83,8 @@ pub struct SfxEvent {
|
||||
pub pos: Option<(f32, f32)>, // world position (for distance-based volume), None = UI
|
||||
}
|
||||
pub const MANA_MAX: f32 = 400.0;
|
||||
pub const SPRING_MANA: f32 = 0.6; // mana per second of each claimed mana spring
|
||||
pub const SPRING_MAX: f32 = 150.0; // more mana maximum per claimed spring
|
||||
pub const CARRY_MAX: i32 = 300;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
|
||||
@@ -241,6 +243,8 @@ pub struct Ent {
|
||||
pub side: u8, // 0 player, 2 and 3 rival masters (heroes: see side())
|
||||
#[serde(default)]
|
||||
pub target: Option<u8>, // attackers: side whose core (or gate) they march on
|
||||
#[serde(default)]
|
||||
pub chained: bool, // a lone creature chained in a sealed cave: joins when the cave is claimed
|
||||
}
|
||||
|
||||
fn yes() -> bool {
|
||||
@@ -295,6 +299,7 @@ impl Ent {
|
||||
pay_t: 0.0,
|
||||
side: 0,
|
||||
target: None,
|
||||
chained: false,
|
||||
}
|
||||
}
|
||||
pub fn tile(&self) -> usize {
|
||||
@@ -305,7 +310,7 @@ impl Ent {
|
||||
}
|
||||
// the player's creatures (not imps, heroes or the rivals' units)
|
||||
pub fn creature(&self) -> bool {
|
||||
!self.hero && self.side == 0 && self.kind != EK::Worker
|
||||
!self.hero && self.side == 0 && self.kind != EK::Worker && !self.chained
|
||||
}
|
||||
// 0 player, 1 humans, 2 and 3 rival masters
|
||||
pub fn side(&self) -> u8 {
|
||||
@@ -334,6 +339,9 @@ pub enum Goal {
|
||||
Loot(u32), // take treasure worth this much from knight vaults
|
||||
Towers, // raze all knight towers
|
||||
Survive(u32), // beat this many waves
|
||||
Rivals, // destroy the soul cores of all rival masters
|
||||
Springs(u32), // claim this many mana springs
|
||||
Freed(u32), // free this many chained creatures
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -424,7 +432,11 @@ pub struct Game {
|
||||
#[serde(default)]
|
||||
pub rivals: Vec<crate::rival::Rival>, // AI dungeon masters
|
||||
#[serde(default = "yes")]
|
||||
pub gate_on: bool, // free game: the humans' gate exists // tiles next to fighting heroes: knight ground there cannot be claimed
|
||||
pub gate_on: bool, // free game: the humans' gate exists
|
||||
#[serde(skip)]
|
||||
pub springs: usize, // claimed mana springs (recounted)
|
||||
#[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)
|
||||
#[serde(skip, default = "hint_times")]
|
||||
@@ -568,6 +580,8 @@ impl Game {
|
||||
mission: None,
|
||||
rivals: Vec::new(),
|
||||
gate_on: true,
|
||||
springs: 0,
|
||||
freed: 0,
|
||||
};
|
||||
let (cx, cy) = g.map.core;
|
||||
// the soul core starts surrounded by a one tile wide ring of treasury
|
||||
@@ -613,6 +627,9 @@ impl Game {
|
||||
Goal::Loot(n) => self.loot >= n as f32,
|
||||
Goal::Towers => !self.towers.iter().any(|t| t.hero),
|
||||
Goal::Survive(n) => self.wave >= n && !self.ents.iter().any(|e| e.alive && e.active_hero() && !e.guard && e.patrol.is_empty()),
|
||||
Goal::Rivals => self.rivals.iter().all(|r| !r.alive),
|
||||
Goal::Springs(n) => self.springs >= n as usize,
|
||||
Goal::Freed(n) => self.freed >= n,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,6 +711,7 @@ impl Game {
|
||||
|
||||
pub fn recount(&mut self) {
|
||||
let mut c = [0usize; 8];
|
||||
self.springs = self.map.t.iter().filter(|t| t.kind == Kind::ManaSpring && t.owner == Owner::Player).count();
|
||||
for t in &self.map.t {
|
||||
if t.owner == Owner::Player {
|
||||
if let Some(r) = room_index(t.kind) {
|
||||
@@ -704,6 +722,10 @@ impl Game {
|
||||
self.counts = c;
|
||||
}
|
||||
|
||||
pub fn mana_max(&self) -> f32 {
|
||||
MANA_MAX + SPRING_MAX * self.springs as f32
|
||||
}
|
||||
|
||||
pub fn gold_cap(&self) -> f32 {
|
||||
CORE_STORE + TREASURY_TILE * self.counts[0] as f32
|
||||
}
|
||||
@@ -907,7 +929,7 @@ impl Game {
|
||||
fn step(&mut self, dt: f32) {
|
||||
self.time += dt;
|
||||
self.recount();
|
||||
self.mana = (self.mana + 1.0 * dt).min(MANA_MAX);
|
||||
self.mana = (self.mana + (1.0 + SPRING_MANA * self.springs as f32) * dt).min(self.mana_max());
|
||||
self.core_hp = (self.core_hp + 0.5 * dt).min(CORE_HP);
|
||||
for t in self.map.t.iter_mut() {
|
||||
if t.kind == Kind::Farm {
|
||||
@@ -1532,7 +1554,23 @@ impl Game {
|
||||
self.msg("msg.level_up", vec![format!("@{}", s.name), e.level.to_string()]);
|
||||
}
|
||||
}
|
||||
if e.hero && e.captive != 0 {
|
||||
if e.chained {
|
||||
// waits in chains until the player's imps claim its cave
|
||||
e.moving = false;
|
||||
let (x, y) = xy(e.tile());
|
||||
let reached = std::iter::once((0, 0)).chain(DIR8.iter().copied()).any(|(dx, dy)| self.map.get(x + dx, y + dy).map_or(false, |t| t.owner == Owner::Player));
|
||||
if reached {
|
||||
e.chained = false;
|
||||
e.settled = false;
|
||||
e.job = Job::Register;
|
||||
e.timer = 1.0;
|
||||
e.happy = 1.0;
|
||||
self.freed += 1;
|
||||
self.fx(7, e.x, e.y, 0.0, 0.0, 1.2, 0xff44ddff);
|
||||
self.sfx(Sfx::Convert, e.x, e.y);
|
||||
self.msg("msg.creature_freed", vec![format!("@{}", st(e.kind).name)]);
|
||||
}
|
||||
} else if e.hero && e.captive != 0 {
|
||||
self.captive_update(e, dt);
|
||||
} else if e.rival() {
|
||||
self.rival_ai(e, dt);
|
||||
@@ -1667,7 +1705,7 @@ impl Game {
|
||||
let mut bd = radius * radius;
|
||||
for o in &self.ents {
|
||||
// every other side is an enemy
|
||||
if !o.alive || o.held || o.side() == e.side() || o.captive != 0 {
|
||||
if !o.alive || o.held || o.side() == e.side() || o.captive != 0 || o.chained {
|
||||
continue;
|
||||
}
|
||||
let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2);
|
||||
@@ -2118,6 +2156,11 @@ impl Game {
|
||||
self.sfx(Sfx::Payday, x, y);
|
||||
self.msg("msg.vault_taken", vec![t.gold.to_string()]);
|
||||
}
|
||||
if t.kind == Kind::ManaSpring {
|
||||
self.fx(7, x, y, 0.0, 0.0, 1.2, 0xffffe060);
|
||||
self.sfx(Sfx::Magic, x, y);
|
||||
self.msg("msg.spring_taken", vec![]);
|
||||
}
|
||||
if t.kind == Kind::Shard {
|
||||
self.shards_taken += 1;
|
||||
self.fx(7, x, y, 0.0, 0.0, 1.2, 0xff6060ff);
|
||||
@@ -2251,7 +2294,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),
|
||||
Owner::None => matches!(t.kind, Kind::Floor | Kind::Portal | Kind::Bridge | Kind::LavaBridge | Kind::Shard | Kind::Treasury | Kind::ManaSpring),
|
||||
// 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 => {
|
||||
@@ -2459,15 +2502,17 @@ impl Game {
|
||||
e.working = true;
|
||||
e.timer += dt * self.help_speed(t);
|
||||
// knight ground takes longer
|
||||
let knight = self.map.t[t].owner == Owner::Hero;
|
||||
if e.timer >= if knight { 3.0 } else { 1.0 } {
|
||||
let prev = self.map.t[t].owner;
|
||||
let knight = prev == Owner::Hero;
|
||||
if e.timer >= if prev != Owner::None { 3.0 } else { 1.0 } {
|
||||
self.unreserve(me);
|
||||
e.job = Job::None;
|
||||
let (tx, ty) = xy(t);
|
||||
if knight {
|
||||
self.take_knight_tile(t);
|
||||
self.alarm(tx as f32 + 0.5, ty as f32 + 0.5, e.id);
|
||||
}
|
||||
// vaults, shards and springs (of the knights, rivals or nobody)
|
||||
self.take_knight_tile(t);
|
||||
if let (Kind::Portal, Some(pk)) = (self.map.t[t].kind, self.map.portal_at(tx, ty)) {
|
||||
let (px, py) = self.map.portals[pk];
|
||||
for y in 0..3 {
|
||||
@@ -2827,7 +2872,7 @@ impl Game {
|
||||
}
|
||||
e.working = true;
|
||||
let rate = if e.kind == EK::Mage { 1.2 } else { 0.5 };
|
||||
self.mana = (self.mana + rate * dt).min(MANA_MAX);
|
||||
self.mana = (self.mana + rate * dt).min(self.mana_max());
|
||||
e.xp += 3.0 * dt;
|
||||
e.timer -= dt;
|
||||
if e.timer <= 0.0 {
|
||||
@@ -2843,7 +2888,7 @@ impl Game {
|
||||
}
|
||||
e.working = true;
|
||||
let rate = if e.kind == EK::Priestess { 1.6 } else { 0.9 };
|
||||
self.mana = (self.mana + rate * dt).min(MANA_MAX);
|
||||
self.mana = (self.mana + rate * dt).min(self.mana_max());
|
||||
e.happy = (e.happy + 0.01 * dt).min(1.0);
|
||||
e.xp += 1.0 * dt;
|
||||
e.timer -= dt;
|
||||
@@ -3781,7 +3826,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 && if t.hero { e.side() != 1 } else { e.side() != 0 };
|
||||
let enemy = e.captive == 0 && !e.chained && if t.hero { e.side() != 1 } else { e.side() != 0 };
|
||||
if !e.alive || e.held || !enemy {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user