347 lines
14 KiB
Rust
347 lines
14 KiB
Rust
// The researched spells: seer's eye, cave-in, haste, stoneskin, gold rain, invisibility,
|
|
// plague and quake (lightning, heal and summon live in game.rs).
|
|
|
|
use crate::game::*;
|
|
use crate::map::*;
|
|
use crate::particles::{abgr, Part, DEBRIS, EMBER, MOTE, RING, SMOKE};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
|
|
pub enum Spell {
|
|
Eye,
|
|
CaveIn,
|
|
Haste,
|
|
Stoneskin,
|
|
GoldRain,
|
|
Invisible,
|
|
Plague,
|
|
Quake,
|
|
Freeze, // frost: cools lava into obsidian, chills enemies
|
|
}
|
|
|
|
pub const SPELLS: [Spell; 9] = [Spell::Eye, Spell::CaveIn, Spell::Haste, Spell::Stoneskin, Spell::GoldRain, Spell::Invisible, Spell::Plague, Spell::Quake, Spell::Freeze];
|
|
|
|
// the icon of a spell (the first eight share one row of the atlas)
|
|
pub fn spell_icon(sp: Spell) -> u32 {
|
|
match SPELLS.iter().position(|&t| t == sp).unwrap_or(0) {
|
|
k if k < 8 => crate::gfx::I_SPELLS + k as u32,
|
|
_ => crate::gfx::I_FREEZE,
|
|
}
|
|
}
|
|
|
|
pub const EYE_TIME: f32 = 20.0;
|
|
pub const EYE_RADIUS: f32 = 8.0;
|
|
|
|
impl Spell {
|
|
pub fn cost(self) -> f32 {
|
|
match self {
|
|
Spell::Eye => 40.0,
|
|
Spell::CaveIn => 70.0,
|
|
Spell::Haste => 30.0,
|
|
Spell::Stoneskin => 40.0,
|
|
Spell::GoldRain => 100.0,
|
|
Spell::Invisible => 50.0,
|
|
Spell::Plague => 80.0,
|
|
Spell::Quake => 90.0,
|
|
Spell::Freeze => 80.0,
|
|
}
|
|
}
|
|
// radius shown under the pointer
|
|
pub fn radius(self) -> f32 {
|
|
match self {
|
|
Spell::Eye => EYE_RADIUS,
|
|
Spell::CaveIn => 1.5,
|
|
Spell::Plague => 2.0,
|
|
Spell::Quake => 3.0,
|
|
Spell::Freeze => 2.5,
|
|
Spell::GoldRain => 0.0,
|
|
_ => 1.6,
|
|
}
|
|
}
|
|
pub fn id(self) -> &'static str {
|
|
match self {
|
|
Spell::Eye => "eye",
|
|
Spell::CaveIn => "cavein",
|
|
Spell::Haste => "haste",
|
|
Spell::Stoneskin => "stoneskin",
|
|
Spell::GoldRain => "goldrain",
|
|
Spell::Invisible => "invisible",
|
|
Spell::Plague => "plague",
|
|
Spell::Quake => "quake",
|
|
Spell::Freeze => "freeze",
|
|
}
|
|
}
|
|
}
|
|
|
|
fn foe(e: &Ent) -> bool {
|
|
e.alive && !e.held && e.captive == 0 && !e.chained && e.side() != 0
|
|
}
|
|
|
|
fn own(e: &Ent) -> bool {
|
|
e.alive && !e.held && !e.hero && e.side == 0 && !e.chained
|
|
}
|
|
|
|
impl Game {
|
|
fn sparkle(&mut self, x: f32, y: f32, n: usize, col: u32) {
|
|
for _ in 0..n {
|
|
let a = self.vrng.f() * 6.28;
|
|
let r = self.vrng.f() * 0.4;
|
|
let vh = 0.6 + self.vrng.f();
|
|
self.parts.push(Part { kind: MOTE, x: x + a.cos() * r, y: y + a.sin() * r, h: 0.1 + self.vrng.f() * 0.5, vx: 0.0, vy: 0.0, vh, t: 0.0, max: 1.0, col, size: 0.04 });
|
|
}
|
|
}
|
|
|
|
pub fn cast(&mut self, spell: Spell, wx: f32, wy: f32) {
|
|
let cost = spell.cost();
|
|
if self.mana < cost {
|
|
self.msg("msg.no_mana_cost", vec![(cost as i32).to_string()]);
|
|
self.sfx_ui(Sfx::Error);
|
|
return;
|
|
}
|
|
self.script_event(crate::mscript::ScriptEvent::Spell(crate::mscript::spell_name(spell)));
|
|
let r2 = spell.radius() * spell.radius();
|
|
let near = |e: &Ent| (e.x - wx).powi(2) + (e.y - wy).powi(2) < r2;
|
|
match spell {
|
|
Spell::Eye => {
|
|
self.reveals.push((wx, wy, EYE_TIME));
|
|
self.part(RING, (wx, wy, 0.1), (0.0, 0.0, 0.0), 0.8, abgr(200, 120, 255), EYE_RADIUS);
|
|
self.sparkle(wx, wy, 20, abgr(220, 160, 255));
|
|
}
|
|
Spell::CaveIn => {
|
|
let (cx, cy) = (wx as i32, wy as i32);
|
|
let mut fell = 0;
|
|
for y in cy - 1..=cy + 1 {
|
|
for x in cx - 1..=cx + 1 {
|
|
let Some(t) = self.map.get(x, y) else { continue };
|
|
if t.kind != Kind::Floor || x < 2 || y < 2 || x >= mw() as i32 - 2 || y >= mh() as i32 - 2 {
|
|
continue;
|
|
}
|
|
let i = idx(x, y);
|
|
let t = &mut self.map.t[i];
|
|
t.kind = Kind::Earth;
|
|
t.owner = Owner::None;
|
|
t.hp = 6.0;
|
|
t.gold = 0;
|
|
t.marked = false;
|
|
t.reinforced = false;
|
|
fell += 1;
|
|
for _ in 0..4 {
|
|
let v = (self.vrng.f() * 2.0 - 1.0, self.vrng.f() * 2.0 - 1.0, 1.0 + self.vrng.f());
|
|
self.parts.push(Part { kind: DEBRIS, x: x as f32 + 0.5, y: y as f32 + 0.5, h: 1.2, vx: v.0, vy: v.1, vh: v.2, t: 0.0, max: 1.6, col: abgr(110, 80, 56), size: 0.08 });
|
|
}
|
|
// whoever stands there is buried and pushed out
|
|
for j in 0..self.ents.len() {
|
|
if self.ents[j].alive && !self.ents[j].held && self.ents[j].tile() == i {
|
|
self.ents[j].hp -= 60.0;
|
|
self.ents[j].flash = 0.2;
|
|
for &(dx, dy) in &DIR8 {
|
|
if self.map.walk(x + dx * 2, y + dy * 2) {
|
|
self.ents[j].x = (x + dx * 2) as f32 + 0.5;
|
|
self.ents[j].y = (y + dy * 2) as f32 + 0.5;
|
|
self.ents[j].path.clear();
|
|
break;
|
|
}
|
|
}
|
|
if self.ents[j].hp <= 0.0 {
|
|
self.kill(j);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if fell == 0 {
|
|
self.hint(27, "hint.cavein_where");
|
|
self.sfx_ui(Sfx::Error);
|
|
return;
|
|
}
|
|
self.recount();
|
|
self.fx(3, wx, wy, 0.0, 0.0, 1.0, 0xff506070);
|
|
self.sfx(Sfx::DigDone, wx, wy);
|
|
}
|
|
Spell::Haste | Spell::Stoneskin => {
|
|
let mut n = 0;
|
|
for e in self.ents.iter_mut().filter(|e| own(e) && near(e)) {
|
|
if spell == Spell::Haste {
|
|
e.haste = 30.0;
|
|
} else {
|
|
e.shield = 30.0;
|
|
}
|
|
n += 1;
|
|
}
|
|
if n == 0 {
|
|
self.hint(28, "hint.spell_own");
|
|
self.sfx_ui(Sfx::Error);
|
|
return;
|
|
}
|
|
let col = if spell == Spell::Haste { abgr(255, 230, 90) } else { abgr(180, 180, 170) };
|
|
self.sparkle(wx, wy, 16, col);
|
|
}
|
|
Spell::Invisible => {
|
|
let target = self.ents.iter().enumerate().filter(|(_, e)| own(e) && e.kind != EK::Worker && near(e)).min_by(|a, b| {
|
|
let da = (a.1.x - wx).powi(2) + (a.1.y - wy).powi(2);
|
|
let db = (b.1.x - wx).powi(2) + (b.1.y - wy).powi(2);
|
|
da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
|
|
});
|
|
let Some((j, _)) = target else {
|
|
self.hint(28, "hint.spell_own");
|
|
self.sfx_ui(Sfx::Error);
|
|
return;
|
|
};
|
|
self.ents[j].invis = 25.0;
|
|
let (x, y) = (self.ents[j].x, self.ents[j].y);
|
|
self.sparkle(x, y, 14, abgr(200, 220, 255));
|
|
}
|
|
Spell::GoldRain => {
|
|
let got = self.add_gold(250.0);
|
|
if got <= 0.0 {
|
|
self.hint(29, "hint.treasury_full");
|
|
self.sfx_ui(Sfx::Error);
|
|
return;
|
|
}
|
|
for _ in 0..24 {
|
|
let (ox, oy) = (self.vrng.f() * 2.0 - 1.0, self.vrng.f() * 2.0 - 1.0);
|
|
let vh = -1.0 - self.vrng.f();
|
|
self.parts.push(Part { kind: crate::particles::COIN, x: wx + ox, y: wy + oy, h: 2.0 + self.vrng.f(), vx: 0.0, vy: 0.0, vh, t: 0.0, max: 2.0, col: abgr(255, 210, 60), size: 0.07 });
|
|
}
|
|
self.sfx(Sfx::Payday, wx, wy);
|
|
}
|
|
Spell::Plague => {
|
|
let mut n = 0;
|
|
for e in self.ents.iter_mut().filter(|e| foe(e) && near(e)) {
|
|
e.plague = 12.0;
|
|
n += 1;
|
|
}
|
|
if n == 0 {
|
|
self.hint(30, "hint.spell_foe");
|
|
self.sfx_ui(Sfx::Error);
|
|
return;
|
|
}
|
|
self.sparkle(wx, wy, 20, abgr(140, 220, 60));
|
|
}
|
|
Spell::Quake => {
|
|
let hit: Vec<usize> = (0..self.ents.len()).filter(|&j| foe(&self.ents[j]) && near(&self.ents[j])).collect();
|
|
for j in hit {
|
|
let e = &mut self.ents[j];
|
|
e.hp -= 40.0;
|
|
e.stun = e.stun.max(2.0);
|
|
e.flash = 0.15;
|
|
if e.hp <= 0.0 {
|
|
self.kill(j);
|
|
}
|
|
}
|
|
for k in 0..3 {
|
|
self.part(RING, (wx, wy, 0.05), (0.0, 0.0, 0.0), 0.5 + k as f32 * 0.2, abgr(200, 170, 130), 3.0);
|
|
}
|
|
for _ in 0..30 {
|
|
let a = self.vrng.f() * 6.28;
|
|
let r = self.vrng.f() * 3.0;
|
|
let vh = 1.5 + self.vrng.f() * 1.5;
|
|
self.parts.push(Part { kind: DEBRIS, x: wx + a.cos() * r, y: wy + a.sin() * r, h: 0.05, vx: 0.0, vy: 0.0, vh, t: 0.0, max: 1.2, col: abgr(110, 90, 70), size: 0.06 });
|
|
}
|
|
for _ in 0..8 {
|
|
let a = self.vrng.f() * 6.28;
|
|
self.parts.push(Part { kind: SMOKE, x: wx + a.cos() * 2.0, y: wy + a.sin() * 2.0, h: 0.2, vx: 0.0, vy: 0.0, vh: 0.3, t: 0.0, max: 2.0, col: abgr(120, 105, 90), size: 0.3 });
|
|
}
|
|
self.sfx(Sfx::Cannon, wx, wy);
|
|
}
|
|
Spell::Freeze => {
|
|
// lava turns to obsidian for a while; enemies are chilled and slowed
|
|
self.cool_lava_around(wx, wy, 2.5, crate::bridges::FREEZE_TIME);
|
|
let hit: Vec<usize> = (0..self.ents.len()).filter(|&j| foe(&self.ents[j]) && near(&self.ents[j])).collect();
|
|
for j in hit {
|
|
let e = &mut self.ents[j];
|
|
e.hp -= 20.0;
|
|
e.slow = e.slow.max(4.0);
|
|
e.flash = 0.15;
|
|
if e.hp <= 0.0 {
|
|
self.kill(j);
|
|
}
|
|
}
|
|
self.part(RING, (wx, wy, 0.05), (0.0, 0.0, 0.0), 0.6, abgr(170, 230, 255), 2.5);
|
|
self.sparkle(wx, wy, 24, abgr(200, 240, 255));
|
|
self.sfx(Sfx::Magic, wx, wy);
|
|
}
|
|
}
|
|
self.mana -= cost;
|
|
self.count(crate::stats::SPELLS, 1.0);
|
|
}
|
|
|
|
// running spell effects on a unit (called every frame)
|
|
pub(crate) fn tick_effects(&mut self, e: &mut Ent, dt: f32) {
|
|
e.haste = (e.haste - dt).max(0.0);
|
|
e.ab_cd = (e.ab_cd - dt).max(0.0);
|
|
e.shield = (e.shield - dt).max(0.0);
|
|
e.invis = (e.invis - dt).max(0.0);
|
|
if e.plague > 0.0 {
|
|
e.plague -= dt;
|
|
e.hp -= 4.0 * dt;
|
|
// it spreads to other enemies close by
|
|
if self.vrng.f() < dt {
|
|
for o in self.ents.iter_mut() {
|
|
if foe(o) && o.plague <= 0.0 && (o.x - e.x).powi(2) + (o.y - e.y).powi(2) < 1.5 {
|
|
o.plague = 10.0;
|
|
}
|
|
}
|
|
}
|
|
if self.vrng.f() < 4.0 * dt {
|
|
let vx = self.vrng.f() * 0.4 - 0.2;
|
|
self.parts.push(Part { kind: EMBER, x: e.x, y: e.y, h: 0.6, vx, vy: 0.0, vh: 0.2, t: 0.0, max: 0.8, col: abgr(150, 220, 70), size: 0.035 });
|
|
}
|
|
}
|
|
if e.haste > 0.0 && e.moving && self.vrng.f() < 6.0 * dt {
|
|
self.parts.push(Part { kind: MOTE, x: e.x, y: e.y, h: 0.15, vx: 0.0, vy: 0.0, vh: 0.2, t: 0.0, max: 0.5, col: abgr(255, 230, 90), size: 0.03 });
|
|
}
|
|
}
|
|
|
|
pub(crate) fn update_reveals(&mut self, dt: f32) {
|
|
for r in self.reveals.iter_mut() {
|
|
r.2 -= dt;
|
|
}
|
|
self.reveals.retain(|r| r.2 > 0.0);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn spells_do_what_they_say() {
|
|
let mut g = Game::new(5, 64, 64);
|
|
g.mana = 400.0;
|
|
let (cx, cy) = g.map.core;
|
|
for y in cy - 8..=cy - 4 {
|
|
for x in cx..=cx + 8 {
|
|
let t = &mut g.map.t[idx(x, y)];
|
|
t.kind = Kind::Floor;
|
|
t.owner = Owner::Player;
|
|
}
|
|
}
|
|
let (x, y) = (cx as f32 + 4.5, cy as f32 - 5.5);
|
|
let ogre = g.spawn(EK::Ogre, false, x, y, 2);
|
|
g.cast(Spell::Haste, x, y);
|
|
g.cast(Spell::Stoneskin, x, y);
|
|
let j = g.find(ogre).unwrap();
|
|
assert!(g.ents[j].haste > 0.0 && g.ents[j].shield > 0.0);
|
|
let knight = g.spawn(EK::Knight, true, x + 1.0, y, 1);
|
|
let hp0 = g.ents[g.find(knight).unwrap()].hp;
|
|
g.cast(Spell::Quake, x, y);
|
|
let k = g.find(knight).unwrap();
|
|
assert!(g.ents[k].hp < hp0 && g.ents[k].stun > 0.0);
|
|
assert!(g.ents[g.find(ogre).unwrap()].hp == g.ents[g.find(ogre).unwrap()].max_hp, "own units are spared");
|
|
g.mana = 400.0;
|
|
g.cast(Spell::Plague, x + 1.0, y);
|
|
assert!(g.ents[g.find(knight).unwrap()].plague > 0.0);
|
|
let gold = g.gold;
|
|
g.mana = 400.0;
|
|
g.cast(Spell::GoldRain, x, y);
|
|
assert!(g.gold > gold);
|
|
g.mana = 400.0;
|
|
g.cast(Spell::CaveIn, cx as f32 + 7.5, cy as f32 - 7.5);
|
|
assert_eq!(g.map.t[idx(cx + 7, cy - 7)].kind, Kind::Earth);
|
|
g.mana = 400.0;
|
|
g.cast(Spell::Eye, 40.0, 40.0);
|
|
assert_eq!(g.reveals.len(), 1);
|
|
}
|
|
}
|