Champions of the Order lead every fifth wave: four bosses with abilities, a health bar and loot
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9debc08a0a
commit
9535e68df0
23 files changed
+303
-1
No files matched your search
+168
@@ -0,0 +1,168 @@
|
||||
// Bosses: every fifth wave is led by one of the Order's champions - a paladin with a name, more
|
||||
// than twice the health, an ability of his own and loot for whoever brings him down (gold and a
|
||||
// free rune forge tower). His health bar stands at the top of the screen while he lives.
|
||||
|
||||
use crate::game::*;
|
||||
use crate::particles::*;
|
||||
|
||||
// the champions (Ent::boss = index + 1): ability cooldowns in seconds
|
||||
pub const BOSSES: [(&str, f32); 4] = [("aldric", 12.0), ("seraphine", 10.0), ("halvard", 18.0), ("rothgar", 9.0)];
|
||||
pub const BOSS_HP: f32 = 2.5; // health factor
|
||||
pub const BOSS_GOLD: f32 = 600.0;
|
||||
|
||||
pub fn boss_name(b: u8) -> String {
|
||||
format!("@boss.{}", BOSSES[(b.max(1) as usize - 1) % BOSSES.len()].0)
|
||||
}
|
||||
|
||||
impl Game {
|
||||
// the unit at index j leads the wave as champion number `b` (1..=4)
|
||||
pub(crate) fn make_boss(&mut self, j: usize, b: u8) {
|
||||
let e = &mut self.ents[j];
|
||||
e.boss = b;
|
||||
e.max_hp *= BOSS_HP;
|
||||
e.hp = e.max_hp;
|
||||
e.ab_cd = 4.0;
|
||||
let name = boss_name(b);
|
||||
self.msg("msg.boss_comes", vec![name]);
|
||||
}
|
||||
|
||||
// the champion's ability, used by itself whenever it is ready
|
||||
pub(crate) fn boss_ability(&mut self, e: &mut Ent, dt: f32) {
|
||||
e.ab_cd -= dt;
|
||||
if e.ab_cd > 0.0 || e.captive != 0 {
|
||||
return;
|
||||
}
|
||||
let (bx, by) = (e.x, e.y);
|
||||
let idx_b = (e.boss.max(1) as usize - 1) % BOSSES.len();
|
||||
let foe = |o: &Ent| o.alive && !o.held && !o.hero && o.captive == 0 && !o.chained;
|
||||
let near = |o: &Ent, r: f32| (o.x - bx).powi(2) + (o.y - by).powi(2) < r * r;
|
||||
let used = match idx_b {
|
||||
// Sir Aldric: shield wall - heroes around are healed and shielded
|
||||
0 => {
|
||||
for o in self.ents.iter_mut().filter(|o| o.alive && o.hero && (o.x - bx).powi(2) + (o.y - by).powi(2) < 16.0) {
|
||||
o.hp = (o.hp + 60.0).min(o.max_hp);
|
||||
o.shield = o.shield.max(6.0);
|
||||
}
|
||||
self.part(RING, (bx, by, 0.1), (0.0, 0.0, 0.0), 0.7, abgr(255, 230, 140), 4.0);
|
||||
true
|
||||
}
|
||||
// Dame Seraphine: radiant burst - the dungeon's units around are burnt by light
|
||||
1 => {
|
||||
let hit: Vec<usize> = (0..self.ents.len()).filter(|&k| foe(&self.ents[k]) && near(&self.ents[k], 3.0)).collect();
|
||||
if hit.is_empty() {
|
||||
false
|
||||
} else {
|
||||
for k in hit {
|
||||
self.damage_unit(k, 45.0);
|
||||
}
|
||||
self.part(FLASH, (bx, by, 0.5), (0.0, 0.0, 0.0), 0.5, abgr(255, 250, 200), 3.0);
|
||||
true
|
||||
}
|
||||
}
|
||||
// Brother Halvard: call to arms - two knights join him
|
||||
2 => {
|
||||
let level = e.level;
|
||||
for k in 0..2 {
|
||||
let a = k as f32 * 3.1;
|
||||
let id = self.spawn(EK::Knight, true, bx + a.cos(), by + a.sin(), level.max(2) - 1);
|
||||
if let Some(o) = self.ents.iter_mut().find(|o| o.id == id) {
|
||||
o.target = e.target;
|
||||
}
|
||||
}
|
||||
self.fx(4, bx, by, 0.0, 0.0, 0.8, 0xffffe0a0);
|
||||
true
|
||||
}
|
||||
// Captain Rothgar: charge - he rushes at an enemy, hits hard and stuns
|
||||
_ => {
|
||||
let target = (0..self.ents.len()).filter(|&k| foe(&self.ents[k]) && near(&self.ents[k], 6.0)).min_by(|&a, &b| {
|
||||
let da = (self.ents[a].x - bx).powi(2) + (self.ents[a].y - by).powi(2);
|
||||
let db = (self.ents[b].x - bx).powi(2) + (self.ents[b].y - by).powi(2);
|
||||
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
|
||||
});
|
||||
match target {
|
||||
Some(k) => {
|
||||
let (tx, ty) = (self.ents[k].x, self.ents[k].y);
|
||||
let d = ((tx - bx).powi(2) + (ty - by).powi(2)).sqrt().max(0.01);
|
||||
if self.map.walk((tx - (tx - bx) / d * 0.8) as i32, (ty - (ty - by) / d * 0.8) as i32) {
|
||||
e.x = tx - (tx - bx) / d * 0.8;
|
||||
e.y = ty - (ty - by) / d * 0.8;
|
||||
e.path.clear();
|
||||
}
|
||||
self.ents[k].stun = self.ents[k].stun.max(1.5);
|
||||
self.damage_unit(k, 50.0);
|
||||
for _ in 0..10 {
|
||||
let v = (self.vrng.f() * 2.0 - 1.0, self.vrng.f() * 2.0 - 1.0, 0.5);
|
||||
self.parts.push(Part { kind: DEBRIS, x: bx, y: by, h: 0.1, vx: v.0, vy: v.1, vh: v.2, t: 0.0, max: 0.8, col: abgr(150, 130, 110), size: 0.05 });
|
||||
}
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
};
|
||||
e.ab_cd = if used { BOSSES[idx_b].1 } else { 1.0 };
|
||||
}
|
||||
|
||||
// a champion fell (or was captured): his loot
|
||||
pub(crate) fn boss_loot(&mut self, j: usize) {
|
||||
let b = self.ents[j].boss;
|
||||
if b == 0 {
|
||||
return;
|
||||
}
|
||||
self.ents[j].boss = 0;
|
||||
let got = self.add_gold(BOSS_GOLD);
|
||||
self.rune_free += 1;
|
||||
let (x, y) = (self.ents[j].x, self.ents[j].y);
|
||||
self.fx(7, x, y, 0.0, 0.0, 1.2, 0xff40e0ff);
|
||||
self.sfx_ui(Sfx::Victory);
|
||||
self.msg("msg.boss_slain", vec![boss_name(b), (got as i32).to_string()]);
|
||||
}
|
||||
|
||||
// the living champion with the most health (for the bar at the top)
|
||||
pub fn boss_alive(&self) -> Option<&Ent> {
|
||||
self.ents.iter().filter(|e| e.alive && e.boss > 0 && e.captive == 0).max_by(|a, b| a.hp.partial_cmp(&b.hp).unwrap_or(std::cmp::Ordering::Equal))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn every_fifth_wave_brings_a_champion_with_loot() {
|
||||
let mut g = Game::new(8, 80, 64);
|
||||
g.wave = 4; // the next wave is the fifth
|
||||
let before = g.ents.len();
|
||||
g.spawn_wave();
|
||||
let boss = g.ents[before..].iter().position(|e| e.boss > 0).map(|k| k + before).expect("a champion");
|
||||
assert!(g.ents[boss].max_hp > st(g.ents[boss].kind).hp * 2.0);
|
||||
assert!(g.boss_alive().is_some());
|
||||
let (gold, runes) = (g.gold, g.rune_free);
|
||||
g.boss_loot(boss);
|
||||
assert!(g.gold > gold && g.rune_free == runes + 1);
|
||||
assert!(g.boss_alive().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn champions_use_their_abilities() {
|
||||
for b in 1..=4u8 {
|
||||
let mut g = Game::new(8, 80, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
let (x, y) = (cx as f32 + 1.5, cy as f32 + 4.5);
|
||||
let id = g.spawn(EK::Paladin, true, x, y, 5);
|
||||
let j = g.ents.iter().position(|e| e.id == id).unwrap();
|
||||
g.make_boss(j, b);
|
||||
// an ogre of the dungeon close by
|
||||
g.spawn(EK::Ogre, false, x + 1.5, y, 3);
|
||||
let n0 = g.ents.len();
|
||||
let mut e = g.ents[j].clone();
|
||||
e.ab_cd = 0.0;
|
||||
e.hp = e.max_hp * 0.5;
|
||||
g.boss_ability(&mut e, 0.1);
|
||||
assert!(e.ab_cd > 1.5, "champion {} used his ability", b);
|
||||
if b == 3 {
|
||||
assert_eq!(g.ents.len(), n0 + 2, "two knights joined");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
@@ -296,6 +296,8 @@ pub struct Ent {
|
||||
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
|
||||
#[serde(default)]
|
||||
pub boss: u8, // a champion of the Order leading a wave (see bosses.rs), 0: none
|
||||
#[serde(skip, default = "zero_f32")]
|
||||
pub disp_x: f32, // smoothed screen position (not part of the saved or synced world state)
|
||||
#[serde(skip, default = "zero_f32")]
|
||||
@@ -366,6 +368,7 @@ impl Ent {
|
||||
side: 0,
|
||||
target: None,
|
||||
chained: false,
|
||||
boss: 0,
|
||||
disp_x: 0.0,
|
||||
disp_y: 0.0,
|
||||
}
|
||||
@@ -2152,6 +2155,12 @@ impl Game {
|
||||
self.last_wave_value = Self::squad_value(&list);
|
||||
let before = self.ents.len();
|
||||
self.spawn_squad(&list, false);
|
||||
// the paladin of every fifth wave is one of the Order's champions
|
||||
if boss.is_some() {
|
||||
if let Some(j) = (before..self.ents.len()).find(|&j| self.ents[j].kind == Paladin) {
|
||||
self.make_boss(j, 1 + ((w / 5 - 1) % 4) as u8);
|
||||
}
|
||||
}
|
||||
if goal != self.me_side {
|
||||
for e in self.ents[before..].iter_mut() {
|
||||
e.target = Some(goal);
|
||||
@@ -2731,6 +2740,7 @@ impl Game {
|
||||
}
|
||||
|
||||
pub(crate) fn kill(&mut self, j: usize) {
|
||||
self.boss_loot(j);
|
||||
let e = &self.ents[j];
|
||||
if e.hero && e.captive == 0 && self.prison_space() > 0 {
|
||||
// knocked out instead of killed: imps carry the hero to the torture chamber
|
||||
@@ -4450,6 +4460,9 @@ impl Game {
|
||||
// ---------------------------------------------------------------- heroes
|
||||
|
||||
fn hero_ai(&mut self, e: &mut Ent, dt: f32) {
|
||||
if e.boss > 0 {
|
||||
self.boss_ability(e, dt);
|
||||
}
|
||||
if e.kind == EK::Fairy {
|
||||
// fairy dust: the heroes around are healed a little all the time
|
||||
for o in self.ents.iter_mut() {
|
||||
|
||||
@@ -30,6 +30,7 @@ mod save;
|
||||
#[cfg(test)]
|
||||
mod scenes;
|
||||
mod blocks;
|
||||
mod bosses;
|
||||
mod bridges;
|
||||
mod mscript;
|
||||
mod settings;
|
||||
|
||||
+1
-1
@@ -377,7 +377,7 @@ impl Game {
|
||||
true
|
||||
}
|
||||
|
||||
fn damage_unit(&mut self, j: usize, dmg: f32) {
|
||||
pub(crate) fn damage_unit(&mut self, j: usize, dmg: f32) {
|
||||
let o = &mut self.ents[j];
|
||||
o.hp -= dmg;
|
||||
o.flash = 0.15;
|
||||
|
||||
@@ -719,6 +719,18 @@ impl Ui {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- a champion of the Order on the field: his name and health under the top bar
|
||||
if let Some(b) = g.boss_alive() {
|
||||
let name = crate::i18n::tr(crate::bosses::boss_name(b.boss).trim_start_matches('@'));
|
||||
let bw = (name.chars().count() as i32 + 26).min(cols - 4);
|
||||
let bx = ((cols - bw) / 2) as f32 * CW();
|
||||
let by = self.hud_h;
|
||||
gfx.window(bx, by, bw, 3, S(), false, RED, BG);
|
||||
gfx.text(&name, bx + CW() * 2.0, by + CH(), S(), GOLD);
|
||||
let bar_cols = (bw - name.chars().count() as i32 - 6).max(6);
|
||||
ascii_bar(gfx, bx + CW() * (name.chars().count() as f32 + 4.0), by + CH(), bar_cols as usize, b.hp / b.max_hp, RED, S());
|
||||
}
|
||||
|
||||
// ---------------- toolbar: buttons of 6 x 3 characters, icons and labels on the grid
|
||||
let tb_rows = 5;
|
||||
let tb_y = sh - tb_rows as f32 * CH();
|
||||
|
||||
Reference in new issue
Block a user