Eight new spells: seer's eye, cave-in, haste, stoneskin, gold rain, invisibility, plague, quake
- researched in the library (and unlocked in the campaign), in the spells menu with own icons and a radius under the pointer - haste speeds up everything a creature does, stoneskin halves damage, invisible creatures are ignored by enemies and towers until they attack (drawn see-through), plague spreads among enemies, quake hurts and stuns enemies only, cave-in turns floor back into earth - particles for every spell Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a54316c86e
commit
7f4d5ca7b4
11 files changed
+532
-12
No files matched your search
@@ -797,3 +797,32 @@ research.next = danach: {0}
|
||||
research.done = Hier gibt es nichts mehr zu erforschen.
|
||||
msg.researched = Erforscht: {0}! Jetzt im Baumenü.
|
||||
msg.tool_research = Noch nicht erforscht - Kreaturen in der Bibliothek arbeiten daran.
|
||||
|
||||
# spells
|
||||
hint.cavein_where = Ein Einsturz braucht freien Boden (keine Räume) unter dem Zeiger.
|
||||
hint.spell_own = Wirke ihn auf deine eigenen Kreaturen.
|
||||
hint.spell_foe = Wirke ihn auf Feinde.
|
||||
tool.eye.label = Auge
|
||||
tool.eye.name = Seherauge
|
||||
tool.eye.desc = Öffnet ein magisches Auge: Ein großer Bereich um die Stelle wird 20 Sekunden lang sichtbar.
|
||||
tool.cavein.label = Einsturz
|
||||
tool.cavein.name = Einsturz
|
||||
tool.cavein.desc = Die Decke stürzt ein: Freier Boden wird wieder zu Erde (3x3), wer dort steht, wird verletzt und hinausgestoßen. Versperrt Gänge.
|
||||
tool.haste.label = Eile
|
||||
tool.haste.name = Eile
|
||||
tool.haste.desc = Deine Kreaturen rund um die Stelle bewegen sich und handeln 30 Sekunden lang viel schneller.
|
||||
tool.stoneskin.label = Steinhaut
|
||||
tool.stoneskin.name = Steinhaut
|
||||
tool.stoneskin.desc = Deine Kreaturen rund um die Stelle nehmen 30 Sekunden lang nur halben Schaden.
|
||||
tool.goldrain.label = Goldregen
|
||||
tool.goldrain.name = Goldregen
|
||||
tool.goldrain.desc = Verwandelt Mana in 250 Gold für deine Schatzkammer.
|
||||
tool.invisible.label = Unsichtbar
|
||||
tool.invisible.name = Unsichtbarkeit
|
||||
tool.invisible.desc = Eine Kreatur wird 25 Sekunden lang unsichtbar: Feinde und Türme beachten sie nicht, bis sie angreift.
|
||||
tool.plague.label = Seuche
|
||||
tool.plague.name = Seuche
|
||||
tool.plague.desc = Eine Krankheit, die an den Feinden rund um die Stelle zehrt und von einem zum nächsten springt.
|
||||
tool.quake.label = Beben
|
||||
tool.quake.name = Beben
|
||||
tool.quake.desc = Der Boden bebt: Feinde rund um die Stelle werden verletzt und zwei Sekunden betäubt. Deine Kreaturen bleiben standfest.
|
||||
@@ -798,3 +798,32 @@ research.next = then: {0}
|
||||
research.done = Nothing left to research here.
|
||||
msg.researched = Researched: {0}! It is in the build menu now.
|
||||
msg.tool_research = Not researched yet - creatures studying in the library work on it.
|
||||
|
||||
# spells
|
||||
hint.cavein_where = A cave-in needs open floor (not rooms) under the pointer.
|
||||
hint.spell_own = Cast it on your own creatures.
|
||||
hint.spell_foe = Cast it on enemies.
|
||||
tool.eye.label = Eye
|
||||
tool.eye.name = Seer's Eye
|
||||
tool.eye.desc = Opens a magic eye: a wide area around the spot is revealed for 20 seconds.
|
||||
tool.cavein.label = Cave-in
|
||||
tool.cavein.name = Cave-in
|
||||
tool.cavein.desc = The ceiling comes down: open floor turns back into earth (3x3), whoever stands there is hurt and pushed out. Blocks corridors.
|
||||
tool.haste.label = Haste
|
||||
tool.haste.name = Haste
|
||||
tool.haste.desc = Your creatures around the spot move and act much faster for 30 seconds.
|
||||
tool.stoneskin.label = Stoneskin
|
||||
tool.stoneskin.name = Stoneskin
|
||||
tool.stoneskin.desc = Your creatures around the spot take only half damage for 30 seconds.
|
||||
tool.goldrain.label = Gold rain
|
||||
tool.goldrain.name = Gold Rain
|
||||
tool.goldrain.desc = Turns mana into 250 gold for your treasury.
|
||||
tool.invisible.label = Invisible
|
||||
tool.invisible.name = Invisibility
|
||||
tool.invisible.desc = One creature becomes invisible for 25 seconds: enemies and towers ignore it until it attacks.
|
||||
tool.plague.label = Plague
|
||||
tool.plague.name = Plague
|
||||
tool.plague.desc = A sickness that eats at the enemies around the spot and jumps from one to the next.
|
||||
tool.quake.label = Quake
|
||||
tool.quake.name = Quake
|
||||
tool.quake.desc = The ground shakes: enemies around the spot are hurt and stunned for two seconds. Your creatures keep their footing.
|
||||
+1
-1
@@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
|
||||
// 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,
|
||||
// torture, temple, arrow tower, cannon tower, magic tower, bridge, aerie, steel
|
||||
pub const TOOL_UNLOCK: [usize; 32] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17, 0, 7, 7, 10, 12, 12, 7, 16, 16, 7, 10, 16];
|
||||
pub const TOOL_UNLOCK: [usize; 40] = [0, 0, 0, 0, 0, 2, 5, 0, 0, 3, 5, 0, 8, 12, 3, 7, 12, 1, 8, 17, 0, 7, 7, 10, 12, 12, 7, 16, 16, 7, 10, 16, 9, 13, 6, 10, 14, 17, 19, 22];
|
||||
|
||||
pub struct FortDef {
|
||||
pub at: (f32, f32), // centre as a fraction of the map size
|
||||
|
||||
+30
-6
@@ -234,6 +234,14 @@ pub struct Ent {
|
||||
pub boost: f32, // seconds of "slapped" speed-up left
|
||||
#[serde(default)]
|
||||
pub stun: f32, // seconds of stun left (punched)
|
||||
#[serde(default)]
|
||||
pub haste: f32, // spell: seconds of haste left
|
||||
#[serde(default)]
|
||||
pub shield: f32, // spell: seconds of stoneskin left (half damage)
|
||||
#[serde(default)]
|
||||
pub invis: f32, // spell: seconds of invisibility left (until it attacks)
|
||||
#[serde(default)]
|
||||
pub plague: f32, // spell: seconds of plague left (poison that spreads)
|
||||
pub guard: bool, // stays at its home (gate defenders, treasure guards)
|
||||
#[serde(default)]
|
||||
pub home: Option<(f32, f32)>, // guard post
|
||||
@@ -299,6 +307,10 @@ impl Ent {
|
||||
waypoint: None,
|
||||
boost: 0.0,
|
||||
stun: 0.0,
|
||||
haste: 0.0,
|
||||
shield: 0.0,
|
||||
invis: 0.0,
|
||||
plague: 0.0,
|
||||
guard: false,
|
||||
home: None,
|
||||
patrol: Vec::new(),
|
||||
@@ -468,6 +480,8 @@ pub struct Game {
|
||||
pub researchable: Vec<bool>, // tools that can be researched in the library
|
||||
#[serde(default)]
|
||||
pub research: f32, // research points towards the current item
|
||||
#[serde(default)]
|
||||
pub reveals: Vec<(f32, f32, f32)>, // seer's eye: x, y, seconds left
|
||||
#[serde(skip)]
|
||||
pub forge_work: f32, // work done in the forge this frame
|
||||
#[serde(skip)]
|
||||
@@ -638,6 +652,7 @@ impl Game {
|
||||
rollers: Vec::new(),
|
||||
clouds: Vec::new(),
|
||||
forge_work: 0.0,
|
||||
reveals: Vec::new(),
|
||||
known: Vec::new(),
|
||||
researchable: Vec::new(),
|
||||
research: 0.0,
|
||||
@@ -1022,6 +1037,12 @@ impl Game {
|
||||
self.turn(&mut e, x0, y0);
|
||||
self.ents[i] = e;
|
||||
}
|
||||
// units that wasted away (plague)
|
||||
for j in 0..self.ents.len() {
|
||||
if self.ents[j].alive && !self.ents[j].held && self.ents[j].hp <= 0.0 {
|
||||
self.kill(j);
|
||||
}
|
||||
}
|
||||
self.ents.retain(|e| e.alive);
|
||||
}
|
||||
|
||||
@@ -1083,6 +1104,7 @@ impl Game {
|
||||
self.update_towers(dt);
|
||||
self.update_forge(dt);
|
||||
self.update_research();
|
||||
self.update_reveals(dt);
|
||||
if !self.rivals.is_empty() {
|
||||
self.update_rivals(dt);
|
||||
}
|
||||
@@ -1633,8 +1655,9 @@ impl Game {
|
||||
e.moving = false;
|
||||
return;
|
||||
}
|
||||
// slapped units work and move faster
|
||||
let dt = if e.boost > 0.0 { dt * 1.5 } else { dt };
|
||||
self.tick_effects(e, dt);
|
||||
// slapped units work and move faster, hasted ones even more
|
||||
let dt = if e.boost > 0.0 { dt * 1.5 } else { dt } * if e.haste > 0.0 { 1.6 } else { 1.0 };
|
||||
e.cd -= dt;
|
||||
e.think -= dt;
|
||||
e.repath -= dt;
|
||||
@@ -1850,7 +1873,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 || o.chained {
|
||||
if !o.alive || o.held || o.side() == e.side() || o.captive != 0 || o.chained || o.invis > 0.0 {
|
||||
continue;
|
||||
}
|
||||
let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2);
|
||||
@@ -1996,8 +2019,9 @@ impl Game {
|
||||
}
|
||||
}
|
||||
let my_id = e.id;
|
||||
e.invis = 0.0; // attacking breaks invisibility
|
||||
let t = &mut self.ents[j];
|
||||
t.hp -= dmg;
|
||||
t.hp -= if t.shield > 0.0 { dmg * 0.5 } else { dmg };
|
||||
t.flash = 0.15;
|
||||
if t.creature() && !matches!(t.job, Job::Attack(_)) {
|
||||
t.job = Job::Attack(my_id);
|
||||
@@ -4063,7 +4087,7 @@ impl Game {
|
||||
// damage dealt to a unit by a tower (no attacker unit involved)
|
||||
fn tower_damage(&mut self, j: usize, dmg: f32) {
|
||||
let e = &mut self.ents[j];
|
||||
e.hp -= dmg;
|
||||
e.hp -= if e.shield > 0.0 { dmg * 0.5 } else { dmg };
|
||||
e.flash = 0.15;
|
||||
if e.hp <= 0.0 && e.alive {
|
||||
self.kill(j);
|
||||
@@ -4079,7 +4103,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
|
||||
if !e.alive || e.held || !self.tower_enemy(t, e) {
|
||||
if !e.alive || e.held || !self.tower_enemy(t, e) || e.invis > 0.0 {
|
||||
continue;
|
||||
}
|
||||
let d = (e.x - px).powi(2) + (e.y - py).powi(2);
|
||||
|
||||
+94
@@ -72,6 +72,7 @@ pub const T_RIVAL2: u32 = 139; // 4 variants: floor of the blue master
|
||||
pub const I_RUNE: u32 = 143; // rune forge tower
|
||||
pub const T_FORGE: u32 = 144; // 4 variants: sooty flagstones with glowing cracks
|
||||
pub const I_TRAPS: u32 = 148; // 10 icons in TRAP_KINDS order
|
||||
pub const I_SPELLS: u32 = 158; // 8 icons in SPELLS order
|
||||
|
||||
pub struct Gfx {
|
||||
pub atlas: Texture2D,
|
||||
@@ -863,6 +864,99 @@ fn gen_atlas() -> Image {
|
||||
for (k, f) in icons.iter().enumerate() {
|
||||
cv.cell(I_TRAPS + k as u32, |x, y| f(x, y));
|
||||
}
|
||||
// spell icons
|
||||
let spells: [fn(i32, i32) -> Option<Color>; 8] = [
|
||||
// seer's eye
|
||||
|x, y| {
|
||||
let (dx, dy) = ((x - 8) as f32, (y - 8) as f32 * 1.8);
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
if d < 2.5 {
|
||||
Some(c(40, 10, 60))
|
||||
} else if d < 4.5 {
|
||||
Some(c(200, 120, 255))
|
||||
} else if d < 7.0 {
|
||||
Some(c(235, 225, 245))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// cave-in
|
||||
|x, y| {
|
||||
if y >= 9 && (x + y * 3) % 5 != 0 {
|
||||
Some(c(110, 80, 56))
|
||||
} else if y < 9 && ((x * 7 + y * 5) % 9 == 0) {
|
||||
Some(c(140, 110, 80))
|
||||
} else if y < 3 {
|
||||
Some(c(70, 60, 56))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// haste
|
||||
|x, y| {
|
||||
let bolt = (y < 8 && (x - (12 - y / 2)).abs() <= 1) || (y >= 8 && (x - (10 - (y - 8) / 2) + 4).abs() <= 1) || (y == 8 && (5..=11).contains(&x));
|
||||
if bolt {
|
||||
Some(c(255, 230, 90))
|
||||
} else if (y == 4 || y == 11) && x < 4 {
|
||||
Some(c(255, 200, 80))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// stoneskin
|
||||
|x, y| {
|
||||
if (3..=12).contains(&x) && (2..=13).contains(&y) && !(y > 10 && (x - 8).abs() + (y - 10) > 5) {
|
||||
Some(if (x + y) % 4 == 0 { c(110, 110, 104) } else { c(170, 168, 160) })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// gold rain
|
||||
|x, y| {
|
||||
if y % 4 == (x / 3) % 4 && x % 3 != 2 {
|
||||
Some(c(255, 210, 60))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// invisibility
|
||||
|x, y| {
|
||||
let d = (x - 8).pow(2) + (y - 7).pow(2);
|
||||
if (d <= 20 && (x + y) % 2 == 0) || (y >= 11 && (x - 8).abs() <= 4 && (x + y) % 2 == 0) {
|
||||
Some(c(200, 220, 255))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// plague
|
||||
|x, y| {
|
||||
let d = |cx: i32, cy: i32, r: i32| (x - cx).pow(2) + (y - cy).pow(2) <= r * r;
|
||||
if d(8, 8, 5) && !d(8, 8, 2) {
|
||||
Some(c(140, 210, 60))
|
||||
} else if d(8, 8, 1) || d(3, 4, 1) || d(13, 12, 1) {
|
||||
Some(c(90, 150, 40))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
// quake
|
||||
|x, y| {
|
||||
if y >= 7 && y <= 9 && (x + (y - 8)) % 4 != 0 {
|
||||
Some(c(140, 110, 80))
|
||||
} else if (x - 8 + (y - 8).abs() * 0).abs() <= 0 && y > 9 {
|
||||
Some(c(60, 40, 30))
|
||||
} else if y > 9 && (x + y) % 3 == 0 {
|
||||
Some(c(100, 76, 56))
|
||||
} else if y < 6 && (x * 5 + y) % 7 == 0 {
|
||||
Some(c(170, 140, 100))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
];
|
||||
for (k, f) in spells.iter().enumerate() {
|
||||
cv.cell(I_SPELLS + k as u32, |x, y| f(x, y));
|
||||
}
|
||||
// aerie: straw and twigs on stone, a few feathers
|
||||
for v in 0..4u32 {
|
||||
cv.cell(T_AERIE + v, |x, y| {
|
||||
|
||||
@@ -23,6 +23,7 @@ mod sfx;
|
||||
mod tracks;
|
||||
mod traps;
|
||||
mod sound;
|
||||
mod spells;
|
||||
mod touch;
|
||||
mod ui;
|
||||
mod xm;
|
||||
@@ -118,6 +119,7 @@ fn world_action(app: &mut App, ui: &mut Ui, r3d: &R3d, inp: &mut Input, mx: f32,
|
||||
(Tool::Summon, _) => app.game.summon(wx, wy),
|
||||
(Tool::Tower(k), Some((x, z))) => app.game.build_tower(k, x, z),
|
||||
(Tool::Trap(k), Some((x, z))) => app.game.place_trap(k, x, z),
|
||||
(Tool::Spell(sp), _) => app.game.cast(sp, wx, wy),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1049,6 +1051,7 @@ fn draw_frame(app: &mut App, gfx: &Gfx, r3d: &mut R3d, ui: &mut Ui, menu: &mut M
|
||||
Tool::Steel => (Color::new(0.75, 0.8, 0.9, 1.0), None),
|
||||
Tool::Tower(_) => (Color::new(1.0, 0.6, 0.25, 1.0), None),
|
||||
Tool::Trap(_) => (Color::new(1.0, 0.4, 0.3, 1.0), None),
|
||||
Tool::Spell(sp) => (Color::new(0.8, 0.55, 1.0, 1.0), Some((ground.x, ground.y, sp.radius().max(0.6)))),
|
||||
};
|
||||
let ov = Overlay {
|
||||
hover: if over_ui { None } else { hover_tile.filter(|&(x, z)| inb(x, z)) },
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ pub fn fx_pos(f: &Fx, p: f32) -> (f32, f32, f32) {
|
||||
}
|
||||
|
||||
impl Game {
|
||||
fn part(&mut self, kind: u8, (x, y, h): (f32, f32, f32), (vx, vy, vh): (f32, f32, f32), max: f32, col: u32, size: f32) {
|
||||
pub(crate) fn part(&mut self, kind: u8, (x, y, h): (f32, f32, f32), (vx, vy, vh): (f32, f32, f32), max: f32, col: u32, size: f32) {
|
||||
if self.parts.len() < PARTS_MAX {
|
||||
self.parts.push(Part { kind, x, y, h, vx, vy, vh, t: 0.0, max, col, size });
|
||||
}
|
||||
|
||||
+9
-1
@@ -200,6 +200,8 @@ uniform float LightCount;
|
||||
void main() {
|
||||
vec4 tex = texture2D(Texture, uv);
|
||||
if (tex.a < 0.5) discard;
|
||||
// see-through units (invisibility): every other pixel left out
|
||||
if (color.a < 0.99 && mod(floor(gl_FragCoord.x) + floor(gl_FragCoord.y), 2.0) < 1.0) discard;
|
||||
vec3 n = normalize(wnorm);
|
||||
vec3 light = color.rgb;
|
||||
for (int i = 0; i < 24; i++) {
|
||||
@@ -1200,12 +1202,18 @@ impl R3d {
|
||||
std::mem::swap(&mut u0, &mut u1);
|
||||
}
|
||||
let a = amb(e.x, e.y) * if sleeping || e.captive == 1 { 0.6 } else { 1.0 };
|
||||
// spell looks: stone grey, plague green, see-through when invisible
|
||||
let alpha = if e.invis > 0.0 { 0.35 } else { 1.0 };
|
||||
let c = if white {
|
||||
[255, 255, 255, 255]
|
||||
} else if e.captive == 3 {
|
||||
col(vec3(a * 1.2, a * 0.55, a * 0.55), 1.0)
|
||||
} else if e.shield > 0.0 {
|
||||
col(vec3(a * 0.85, a * 0.85, a * 0.8), alpha)
|
||||
} else if e.plague > 0.0 {
|
||||
col(vec3(a * 0.75, a * 1.15, a * 0.6), alpha)
|
||||
} else {
|
||||
col(Vec3::splat(a), 1.0)
|
||||
col(Vec3::splat(a), alpha)
|
||||
};
|
||||
// pulled towards the camera: unchanged on screen (orthographic), but walls behind
|
||||
// the unit no longer cut into the sprite
|
||||
|
||||
@@ -9,23 +9,31 @@ use crate::game::*;
|
||||
pub const RESEARCH: &[(usize, f32)] = &[
|
||||
(5, 200.0), // training hall
|
||||
(10, 250.0), // heal
|
||||
(34, 250.0), // haste
|
||||
(21, 300.0), // forge
|
||||
(22, 250.0), // spike pit
|
||||
(29, 250.0), // wooden door
|
||||
(32, 300.0), // seer's eye
|
||||
(15, 400.0), // cannon tower
|
||||
(12, 450.0), // torture chamber
|
||||
(26, 300.0), // alarm bell
|
||||
(35, 400.0), // stoneskin
|
||||
(11, 500.0), // summon
|
||||
(13, 550.0), // temple
|
||||
(23, 500.0), // poison vent
|
||||
(30, 500.0), // iron door
|
||||
(33, 550.0), // cave-in
|
||||
(18, 550.0), // aerie
|
||||
(16, 650.0), // magic tower
|
||||
(36, 600.0), // gold rain
|
||||
(24, 650.0), // fire jet
|
||||
(25, 650.0), // stone roller
|
||||
(37, 700.0), // invisibility
|
||||
(19, 700.0), // steel walls
|
||||
(28, 800.0), // frost glow
|
||||
(38, 850.0), // plague
|
||||
(27, 900.0), // rune mine
|
||||
(39, 950.0), // quake
|
||||
(31, 1100.0), // magic door
|
||||
];
|
||||
|
||||
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
// 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,
|
||||
}
|
||||
|
||||
pub const SPELLS: [Spell; 8] = [Spell::Eye, Spell::CaveIn, Spell::Haste, Spell::Stoneskin, Spell::GoldRain, Spell::Invisible, Spell::Plague, Spell::Quake];
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
// 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::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",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
self.mana -= cost;
|
||||
}
|
||||
|
||||
// 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.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);
|
||||
}
|
||||
}
|
||||
@@ -158,10 +158,11 @@ pub enum Tool {
|
||||
Bridge,
|
||||
Steel,
|
||||
Trap(crate::traps::TrapKind),
|
||||
Spell(crate::spells::Spell),
|
||||
}
|
||||
|
||||
// New tools are appended so that existing key bindings ("tool<i>") keep their meaning
|
||||
pub const TOOLS: [Tool; 32] = [
|
||||
pub const TOOLS: [Tool; 40] = [
|
||||
Tool::Dig,
|
||||
Tool::Hand,
|
||||
Tool::Room(0),
|
||||
@@ -194,6 +195,14 @@ pub const TOOLS: [Tool; 32] = [
|
||||
Tool::Trap(crate::traps::TrapKind::WoodDoor),
|
||||
Tool::Trap(crate::traps::TrapKind::IronDoor),
|
||||
Tool::Trap(crate::traps::TrapKind::MagicDoor),
|
||||
Tool::Spell(crate::spells::Spell::Eye),
|
||||
Tool::Spell(crate::spells::Spell::CaveIn),
|
||||
Tool::Spell(crate::spells::Spell::Haste),
|
||||
Tool::Spell(crate::spells::Spell::Stoneskin),
|
||||
Tool::Spell(crate::spells::Spell::GoldRain),
|
||||
Tool::Spell(crate::spells::Spell::Invisible),
|
||||
Tool::Spell(crate::spells::Spell::Plague),
|
||||
Tool::Spell(crate::spells::Spell::Quake),
|
||||
];
|
||||
|
||||
|
||||
@@ -218,6 +227,7 @@ pub fn tool_info(t: Tool) -> ToolInfo {
|
||||
Tool::Room(7) => ("aerie", T_AERIE, Some(7)),
|
||||
Tool::Room(_) => ("forge", T_FORGE, Some(8)),
|
||||
Tool::Trap(k) => (k.id(), I_TRAPS + crate::traps::TRAP_KINDS.iter().position(|&t| t == k).unwrap_or(0) as u32, None),
|
||||
Tool::Spell(sp) => (sp.id(), I_SPELLS + crate::spells::SPELLS.iter().position(|&t| t == sp).unwrap_or(0) as u32, None),
|
||||
Tool::Tower(TowerKind::Arrow) => ("arrow", I_ARROW, None),
|
||||
Tool::Tower(TowerKind::Cannon) => ("cannon", I_CANNON, None),
|
||||
Tool::Tower(TowerKind::Magic) => ("magic", I_MAGIC, None),
|
||||
@@ -240,6 +250,7 @@ pub fn tool_info(t: Tool) -> ToolInfo {
|
||||
// costs that do not depend on the game (bestiary)
|
||||
pub fn tool_cost_static(t: Tool) -> String {
|
||||
match t {
|
||||
Tool::Spell(sp) => trf("cost.mana", &[(sp.cost() as i32).to_string()]),
|
||||
Tool::Trap(k) => trf("cost.work", &[(k.work() as i32).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()]),
|
||||
@@ -261,7 +272,7 @@ pub fn tool_cost_static(t: Tool) -> String {
|
||||
|
||||
pub fn tool_cost(t: Tool, g: &Game) -> String {
|
||||
match t {
|
||||
Tool::Trap(_) => tool_cost_static(t),
|
||||
Tool::Trap(_) | Tool::Spell(_) => tool_cost_static(t),
|
||||
Tool::Room(r) => trf("cost.gold_tile", &[ROOM_COST[r].to_string()]),
|
||||
Tool::Lightning => trf("cost.mana", &["50".into()]),
|
||||
Tool::Heal => trf("cost.mana", &["60".into()]),
|
||||
@@ -291,7 +302,7 @@ pub const CATS: [(&str, &[usize]); 7] = [
|
||||
("cat.rooms", &[2, 3, 4, 5, 6, 12, 13, 18, 21, 17]),
|
||||
("cat.towers", &[14, 15, 16, 20]),
|
||||
("cat.traps", &[22, 23, 24, 25, 26, 27, 28, 29, 30, 31]),
|
||||
("cat.spells", &[9, 10, 11]),
|
||||
("cat.spells", &[9, 10, 11, 32, 33, 34, 35, 36, 37, 38, 39]),
|
||||
("cat.orders", &[8, 7, 19]),
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user