Creature personality: favourite rooms, hatreds and brawls, abilities learned with the level
- every kind loves one room (working there lifts the mood) and cannot stand certain other kinds; close by they sour each other's mood and now and then brawl (they stop at 30% health) - learned abilities: imps dig faster, kobold dash, spider web, mage shadow step, ogre stomp, jailer shackle, priestess healing pulse, chainbreaker whirlwind, bat shriek, gargoyle stone form, owl volley, wyrm inferno, cogling bomb - bestiary lists likes, hatreds and the ability (highlighted from its level on) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
240e89d9cb
commit
2c8db33a69
7 files changed
+403
-4
No files matched your search
@@ -827,3 +827,36 @@ 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.
|
||||
tile.unexplored = Unerforschte Finsternis
|
||||
|
||||
# personality
|
||||
msg.brawl = Eine Prügelei! {0} und {1} können sich nicht ausstehen.
|
||||
job.brawl = Prügelt sich
|
||||
best.likes = Liebt: {0}
|
||||
best.hates = Kann nicht leiden: {0}
|
||||
best.ability = Ab St. {0}: {1} - {2}
|
||||
ability.dig.name = Alter Hase
|
||||
ability.dig.desc = gräbt ein Viertel schneller.
|
||||
ability.dash.name = Sprung
|
||||
ability.dash.desc = springt Feinde aus ein paar Feldern Entfernung mit einem schweren Hieb an.
|
||||
ability.web.name = Netz
|
||||
ability.web.desc = nach ihren Bissen bleiben Feinde oft kleben und werden langsam.
|
||||
ability.teleport.name = Schattenschritt
|
||||
ability.teleport.desc = im Kampf schwer verletzt, verschwindet er zurück in sein Bett.
|
||||
ability.stomp.name = Stampfer
|
||||
ability.stomp.desc = lässt den Boden beben: Feinde ringsum werden verletzt und betäubt.
|
||||
ability.shackle.name = Fessel
|
||||
ability.shackle.desc = seine Hiebe legen Feinde manchmal in Ketten.
|
||||
ability.pulse.name = Heilwelle
|
||||
ability.pulse.desc = heilt alle verletzten Kreaturen um sich herum auf einmal.
|
||||
ability.whirl.name = Wirbelwind
|
||||
ability.whirl.desc = wirbelt seine Äxte durch jeden Feind ringsum.
|
||||
ability.shriek.name = Kreischen
|
||||
ability.shriek.desc = ein durchdringender Schrei, der Feinde in der Nähe betäubt.
|
||||
ability.stoneform.name = Steingestalt
|
||||
ability.stoneform.desc = schwer verletzt, wird er zu Stein und nimmt eine Weile nur halben Schaden.
|
||||
ability.volley.name = Salve
|
||||
ability.volley.desc = jedem Geschoss folgt ein zweites auf einen anderen Feind.
|
||||
ability.inferno.name = Inferno
|
||||
ability.inferno.desc = sein Feueratem breitet sich viel weiter aus.
|
||||
ability.bomb.name = Bombe
|
||||
ability.bomb.desc = wirft eine selbstgebaute Bombe, die zwischen den Feinden explodiert.
|
||||
@@ -828,3 +828,36 @@ 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.
|
||||
tile.unexplored = Unexplored darkness
|
||||
|
||||
# personality
|
||||
msg.brawl = A brawl! The {0} and the {1} cannot stand each other.
|
||||
job.brawl = Brawling
|
||||
best.likes = Loves: {0}
|
||||
best.hates = Cannot stand: {0}
|
||||
best.ability = From Lv {0}: {1} - {2}
|
||||
ability.dig.name = Old hand
|
||||
ability.dig.desc = digs a quarter faster.
|
||||
ability.dash.name = Dash
|
||||
ability.dash.desc = leaps at enemies a few tiles away with a heavy blow.
|
||||
ability.web.name = Web
|
||||
ability.web.desc = its bites often leave enemies stuck and slow.
|
||||
ability.teleport.name = Shadow step
|
||||
ability.teleport.desc = badly hurt in a fight, it vanishes back to its bed.
|
||||
ability.stomp.name = Stomp
|
||||
ability.stomp.desc = shakes the ground: enemies around it are hurt and stunned.
|
||||
ability.shackle.name = Shackle
|
||||
ability.shackle.desc = its blows sometimes chain enemies in place.
|
||||
ability.pulse.name = Healing pulse
|
||||
ability.pulse.desc = heals all hurt creatures around her at once.
|
||||
ability.whirl.name = Whirlwind
|
||||
ability.whirl.desc = spins its axes through every enemy around it.
|
||||
ability.shriek.name = Shriek
|
||||
ability.shriek.desc = a piercing cry that stuns enemies nearby.
|
||||
ability.stoneform.name = Stone form
|
||||
ability.stoneform.desc = badly hurt, it turns to stone and takes half damage for a while.
|
||||
ability.volley.name = Volley
|
||||
ability.volley.desc = every bolt is followed by a second one at another enemy.
|
||||
ability.inferno.name = Inferno
|
||||
ability.inferno.desc = its fire breath spreads much wider.
|
||||
ability.bomb.name = Bomb
|
||||
ability.bomb.desc = throws a home-made bomb that explodes among the enemies.
|
||||
+43
-4
@@ -183,6 +183,7 @@ pub enum Job {
|
||||
Help(usize), // imp with nothing else to do helps another imp at this tile
|
||||
Forge(usize), // creature works in the forge (traps and doors)
|
||||
Install(u32), // imp carries a finished trap or door (id) to its spot and installs it
|
||||
Brawl(u32), // two creatures that cannot stand each other scuffle (never to the death)
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@@ -242,6 +243,10 @@ pub struct Ent {
|
||||
pub invis: f32, // spell: seconds of invisibility left (until it attacks)
|
||||
#[serde(default)]
|
||||
pub plague: f32, // spell: seconds of plague left (poison that spreads)
|
||||
#[serde(default)]
|
||||
pub ab_cd: f32, // cooldown of the learned ability
|
||||
#[serde(default)]
|
||||
pub mood_t: f32, // seconds until the next mood check (likes and hatreds)
|
||||
pub guard: bool, // stays at its home (gate defenders, treasure guards)
|
||||
#[serde(default)]
|
||||
pub home: Option<(f32, f32)>, // guard post
|
||||
@@ -311,6 +316,8 @@ impl Ent {
|
||||
shield: 0.0,
|
||||
invis: 0.0,
|
||||
plague: 0.0,
|
||||
ab_cd: 0.0,
|
||||
mood_t: 0.0,
|
||||
guard: false,
|
||||
home: None,
|
||||
patrol: Vec::new(),
|
||||
@@ -1732,7 +1739,7 @@ impl Game {
|
||||
};
|
||||
let target = match e.job {
|
||||
Job::Dig(t) | Job::Reinforce(t) | Job::Help(t) | Job::TearBridge(t) => tile(t),
|
||||
Job::Attack(id) | Job::Torment(id) | Job::Carry(id) => self.find(id).map(|j| (self.ents[j].x, self.ents[j].y)),
|
||||
Job::Attack(id) | Job::Torment(id) | Job::Carry(id) | Job::Brawl(id) => self.find(id).map(|j| (self.ents[j].x, self.ents[j].y)),
|
||||
Job::Repair(id) | Job::AttackTower(id) => self.towers.iter().find(|t| t.id == id).map(|t| t.pos()),
|
||||
_ => None,
|
||||
};
|
||||
@@ -1855,7 +1862,7 @@ impl Game {
|
||||
}
|
||||
}
|
||||
|
||||
fn los(&self, x0: f32, y0: f32, x1: f32, y1: f32) -> bool {
|
||||
pub(crate) fn los(&self, x0: f32, y0: f32, x1: f32, y1: f32) -> bool {
|
||||
let dx = x1 - x0;
|
||||
let dy = y1 - y0;
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
@@ -1918,6 +1925,9 @@ impl Game {
|
||||
e.path.clear();
|
||||
e.face = sgn(tx - e.x);
|
||||
e.working = true;
|
||||
if e.cd <= 0.0 && self.use_ability(e, j) {
|
||||
return true;
|
||||
}
|
||||
if e.cd <= 0.0 {
|
||||
e.cd = s.interval;
|
||||
self.hit(e, j);
|
||||
@@ -2007,7 +2017,7 @@ impl Game {
|
||||
.ents
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(o, x)| *o != j && x.alive && !x.held && x.side() != me && x.captive == 0 && (x.x - tx).powi(2) + (x.y - ty).powi(2) < 1.44)
|
||||
.filter(|(o, x)| *o != j && x.alive && !x.held && x.side() != me && x.captive == 0 && (x.x - tx).powi(2) + (x.y - ty).powi(2) < if crate::personality::knows(e, "inferno") { 6.25 } else { 1.44 })
|
||||
.map(|(o, _)| o)
|
||||
.collect();
|
||||
for o in splash {
|
||||
@@ -2023,6 +2033,27 @@ impl Game {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// learned abilities that come with a blow
|
||||
if crate::personality::knows(e, "web") && self.rng.f() < 0.3 {
|
||||
self.ents[j].slow = self.ents[j].slow.max(3.0);
|
||||
}
|
||||
if crate::personality::knows(e, "shackle") && self.rng.f() < 0.2 {
|
||||
self.ents[j].stun = self.ents[j].stun.max(2.0);
|
||||
}
|
||||
if crate::personality::knows(e, "volley") {
|
||||
let me = e.side();
|
||||
let second = self.ents.iter().enumerate().find(|(o, x)| *o != j && x.alive && !x.held && x.side() != me && x.captive == 0 && (x.x - tx).powi(2) + (x.y - ty).powi(2) < 6.0).map(|(o, _)| o);
|
||||
if let Some(o) = second {
|
||||
let (ox, oy) = (self.ents[o].x, self.ents[o].y);
|
||||
self.shot(e.x, e.y, 0.8, ox, oy, 0.45, 0.25, 0xffffa060);
|
||||
self.style_last_fx(crate::particles::ST_ARCANE);
|
||||
self.ents[o].hp -= dmg * 0.6;
|
||||
self.ents[o].flash = 0.15;
|
||||
if self.ents[o].hp <= 0.0 {
|
||||
self.kill(o);
|
||||
}
|
||||
}
|
||||
}
|
||||
let my_id = e.id;
|
||||
e.invis = 0.0; // attacking breaks invisibility
|
||||
let t = &mut self.ents[j];
|
||||
@@ -2605,7 +2636,8 @@ impl Game {
|
||||
let (tx, ty) = xy(t);
|
||||
e.face = sgn(tx as f32 + 0.5 - e.x);
|
||||
e.working = true;
|
||||
e.timer += dt * self.help_speed(t);
|
||||
let skilled = if crate::personality::knows(e, "dig") { 1.25 } else { 1.0 };
|
||||
e.timer += dt * self.help_speed(t) * skilled;
|
||||
if e.timer >= 0.45 {
|
||||
e.timer = 0.0;
|
||||
let fxx = (e.x + tx as f32 + 0.5) * 0.5;
|
||||
@@ -3008,6 +3040,12 @@ impl Game {
|
||||
self.hint(20, "hint.chainbreaker_restless");
|
||||
}
|
||||
}
|
||||
self.mood_check(e, dt);
|
||||
self.passive_abilities(e);
|
||||
if let Job::Brawl(id) = e.job {
|
||||
self.brawl(e, id, dt);
|
||||
return;
|
||||
}
|
||||
if let Job::Attack(id) = e.job {
|
||||
if !self.attack_job(e, id, dt) {
|
||||
e.job = Job::None;
|
||||
@@ -4751,6 +4789,7 @@ pub fn job_name(e: &Ent) -> &'static str {
|
||||
Job::Eat(_) => "job.eat",
|
||||
Job::Train(_) => "job.train",
|
||||
Job::Forge(_) => "job.forge",
|
||||
Job::Brawl(_) => "job.brawl",
|
||||
Job::Install(_) => "job.install",
|
||||
Job::Study(_) => "job.study",
|
||||
Job::Attack(_) => "job.fight",
|
||||
|
||||
@@ -12,6 +12,7 @@ mod gfx;
|
||||
mod i18n;
|
||||
mod map;
|
||||
mod menu;
|
||||
mod personality;
|
||||
mod platform;
|
||||
mod particles;
|
||||
mod props;
|
||||
|
||||
@@ -216,6 +216,19 @@ impl Menu {
|
||||
}
|
||||
if !HUMANS.contains(&kind) && !cave {
|
||||
facts.push((trf("best.lure", &[tr(&format!("best.{}.lure", id))]), BLUE_C));
|
||||
// personality: favourite room, hatreds, learned ability
|
||||
let p = crate::personality::persona(kind);
|
||||
if let Some(r) = crate::game::room_index(p.likes) {
|
||||
facts.push((trf("best.likes", &[tr(crate::game::ROOM_NAMES[r])]), GREEN_C));
|
||||
}
|
||||
if !p.hates.is_empty() {
|
||||
let names: Vec<String> = p.hates.iter().map(|&h| tr(st(h).name)).collect();
|
||||
facts.push((trf("best.hates", &[names.join(", ")]), RED_C));
|
||||
}
|
||||
if let Some((lv, a)) = p.ability {
|
||||
let line = trf("best.ability", &[lv.to_string(), tr(&format!("ability.{}.name", a)), tr(&format!("ability.{}.desc", a))]);
|
||||
facts.push((line, if level >= lv { GOLD } else { DIM }));
|
||||
}
|
||||
}
|
||||
text_key = format!("best.{}.text", id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
// Creature personality: every kind loves one room and cannot stand certain other kinds (bad
|
||||
// mood, now and then a brawl - never to the death), and learns an ability at a certain level.
|
||||
|
||||
use crate::game::*;
|
||||
use crate::map::*;
|
||||
use crate::particles::{abgr, RING};
|
||||
|
||||
pub struct Persona {
|
||||
pub likes: Kind,
|
||||
pub hates: &'static [EK],
|
||||
pub ability: Option<(u32, &'static str)>, // level, id (ability.<id>.name / .desc)
|
||||
}
|
||||
|
||||
pub fn persona(k: EK) -> Persona {
|
||||
let p = |likes, hates, ability| Persona { likes, hates, ability };
|
||||
match k {
|
||||
EK::Worker => p(Kind::Treasury, &[], Some((3, "dig"))),
|
||||
EK::Kobold => p(Kind::Training, &[EK::Mage], Some((5, "dash"))),
|
||||
EK::Spider => p(Kind::Farm, &[EK::Bat], Some((4, "web"))),
|
||||
EK::Mage => p(Kind::Library, &[EK::Kobold, EK::Ogre], Some((6, "teleport"))),
|
||||
EK::Ogre => p(Kind::Training, &[EK::Mage, EK::Cogling], Some((4, "stomp"))),
|
||||
EK::Jailer => p(Kind::Torture, &[EK::Priestess], Some((7, "shackle"))),
|
||||
EK::Priestess => p(Kind::Temple, &[EK::Jailer], Some((5, "pulse"))),
|
||||
EK::Chainbreaker => p(Kind::Training, &[], Some((6, "whirl"))),
|
||||
EK::Bat => p(Kind::Aerie, &[EK::Spider], Some((5, "shriek"))),
|
||||
EK::Gargoyle => p(Kind::Torture, &[EK::Owl], Some((5, "stoneform"))),
|
||||
EK::Owl => p(Kind::Library, &[EK::Gargoyle], Some((5, "volley"))),
|
||||
EK::Wyrm => p(Kind::Treasury, &[EK::Bat], Some((7, "inferno"))),
|
||||
EK::Cogling => p(Kind::Forge, &[EK::Ogre], Some((4, "bomb"))),
|
||||
_ => p(Kind::Lair, &[], None),
|
||||
}
|
||||
}
|
||||
|
||||
// has this unit learned its ability?
|
||||
pub fn knows(e: &Ent, id: &str) -> bool {
|
||||
!e.hero && persona(e.kind).ability.map_or(false, |(lv, a)| a == id && e.level >= lv)
|
||||
}
|
||||
|
||||
fn works_in(e: &Ent, g: &Game, kind: Kind) -> bool {
|
||||
match e.job {
|
||||
Job::Train(t) | Job::Study(t) | Job::Pray(t) | Job::Forge(t) | Job::Sleep(t) | Job::Eat(t) => g.map.t[t].kind == kind && e.path.is_empty(),
|
||||
Job::Torment(_) => kind == Kind::Torture && e.path.is_empty(),
|
||||
_ => g.map.t[e.tile()].kind == kind,
|
||||
}
|
||||
}
|
||||
|
||||
impl Game {
|
||||
// every few seconds: the favourite room cheers up, hated company sours the mood
|
||||
pub(crate) fn mood_check(&mut self, e: &mut Ent, dt: f32) {
|
||||
e.mood_t -= dt;
|
||||
if e.mood_t > 0.0 {
|
||||
return;
|
||||
}
|
||||
e.mood_t = 4.0;
|
||||
let p = persona(e.kind);
|
||||
if works_in(e, self, p.likes) {
|
||||
e.happy = (e.happy + 0.03).min(1.0);
|
||||
}
|
||||
if p.hates.is_empty() || matches!(e.job, Job::Brawl(_) | Job::Attack(_)) {
|
||||
return;
|
||||
}
|
||||
let foe = self.ents.iter().position(|o| o.alive && !o.held && o.creature() && p.hates.contains(&o.kind) && (o.x - e.x).powi(2) + (o.y - e.y).powi(2) < 9.0);
|
||||
let Some(j) = foe else { return };
|
||||
e.happy -= 0.015;
|
||||
let idle = |job: Job| matches!(job, Job::None | Job::Wander);
|
||||
if idle(e.job) && idle(self.ents[j].job) && self.rng.f() < 0.15 {
|
||||
// a brawl breaks out
|
||||
let other = self.ents[j].id;
|
||||
e.job = Job::Brawl(other);
|
||||
e.timer = 0.0;
|
||||
e.path.clear();
|
||||
self.ents[j].job = Job::Brawl(e.id);
|
||||
self.ents[j].timer = 0.0;
|
||||
self.ents[j].path.clear();
|
||||
let (a, b) = (st(e.kind).name, st(self.ents[j].kind).name);
|
||||
self.msg("msg.brawl", vec![format!("@{}", a), format!("@{}", b)]);
|
||||
}
|
||||
}
|
||||
|
||||
// two creatures scuffling: they stop at 30% health, after eight seconds or when slapped
|
||||
pub(crate) fn brawl(&mut self, e: &mut Ent, id: u32, dt: f32) {
|
||||
e.timer += dt;
|
||||
let Some(j) = self.find(id) else {
|
||||
e.job = Job::None;
|
||||
return;
|
||||
};
|
||||
let over = e.timer > 8.0 || e.hp < e.max_hp * 0.3 || self.ents[j].hp < self.ents[j].max_hp * 0.3 || !matches!(self.ents[j].job, Job::Brawl(_));
|
||||
if over {
|
||||
e.job = Job::None;
|
||||
if self.ents[j].job == Job::Brawl(e.id) {
|
||||
self.ents[j].job = Job::None;
|
||||
}
|
||||
e.happy -= 0.03;
|
||||
return;
|
||||
}
|
||||
let (tx, ty) = (self.ents[j].x, self.ents[j].y);
|
||||
let d = ((tx - e.x).powi(2) + (ty - e.y).powi(2)).sqrt();
|
||||
if d > 1.1 {
|
||||
if e.path.is_empty() || e.repath <= 0.0 {
|
||||
e.repath = 0.8;
|
||||
if let Some(p) = self.map.path_to(e.tile(), idx(tx as i32, ty as i32)) {
|
||||
e.path = p;
|
||||
}
|
||||
}
|
||||
self.step_path(e, dt);
|
||||
return;
|
||||
}
|
||||
e.path.clear();
|
||||
e.working = true;
|
||||
e.face = if tx > e.x { 1.0 } else { -1.0 };
|
||||
if e.cd <= 0.0 {
|
||||
e.cd = st(e.kind).interval;
|
||||
let o = &mut self.ents[j];
|
||||
o.hp = (o.hp - e.atk() * 0.4).max(o.max_hp * 0.25);
|
||||
o.flash = 0.15;
|
||||
self.fx(2, tx, ty, 0.0, 0.0, 0.25, 0xffffffff);
|
||||
self.sfx(Sfx::MeleeHit, tx, ty);
|
||||
}
|
||||
}
|
||||
|
||||
// active abilities in a fight; true = used instead of a normal blow
|
||||
pub(crate) fn use_ability(&mut self, e: &mut Ent, j: usize) -> bool {
|
||||
if e.ab_cd > 0.0 || e.hero || e.side != 0 {
|
||||
return false;
|
||||
}
|
||||
let (tx, ty) = (self.ents[j].x, self.ents[j].y);
|
||||
let me = e.side();
|
||||
let foes_near = |g: &Game, x: f32, y: f32, r: f32| -> Vec<usize> {
|
||||
(0..g.ents.len()).filter(|&o| {
|
||||
let u = &g.ents[o];
|
||||
u.alive && !u.held && u.side() != me && u.captive == 0 && !u.chained && (u.x - x).powi(2) + (u.y - y).powi(2) < r * r
|
||||
}).collect()
|
||||
};
|
||||
let hurt = |g: &mut Game, o: usize, dmg: f32| {
|
||||
let u = &mut g.ents[o];
|
||||
u.hp -= if u.shield > 0.0 { dmg * 0.5 } else { dmg };
|
||||
u.flash = 0.15;
|
||||
if u.hp <= 0.0 {
|
||||
g.kill(o);
|
||||
}
|
||||
};
|
||||
if knows(e, "stomp") || knows(e, "whirl") || knows(e, "shriek") {
|
||||
let (r, cd) = if knows(e, "shriek") { (2.0, 10.0) } else if knows(e, "whirl") { (1.6, 7.0) } else { (1.6, 8.0) };
|
||||
let hit = foes_near(self, e.x, e.y, r);
|
||||
if hit.len() < 2 && !knows(e, "shriek") {
|
||||
return false;
|
||||
}
|
||||
for o in hit {
|
||||
if knows(e, "shriek") {
|
||||
self.ents[o].stun = self.ents[o].stun.max(1.2);
|
||||
} else {
|
||||
if knows(e, "stomp") {
|
||||
self.ents[o].stun = self.ents[o].stun.max(1.0);
|
||||
}
|
||||
let dmg = e.atk() * if knows(e, "whirl") { 0.8 } else { 0.6 };
|
||||
hurt(self, o, dmg);
|
||||
}
|
||||
}
|
||||
self.part(RING, (e.x, e.y, 0.05), (0.0, 0.0, 0.0), 0.4, abgr(230, 200, 150), r);
|
||||
self.sfx(Sfx::Cannon, e.x, e.y);
|
||||
e.ab_cd = cd;
|
||||
e.cd = st(e.kind).interval;
|
||||
return true;
|
||||
}
|
||||
if knows(e, "bomb") {
|
||||
self.shot(e.x, e.y, 0.5, tx, ty, 0.2, 0.4, 0xff303030);
|
||||
self.style_last_fx(crate::particles::ST_CANNON);
|
||||
for o in foes_near(self, tx, ty, 1.3) {
|
||||
hurt(self, o, 40.0);
|
||||
}
|
||||
e.ab_cd = 10.0;
|
||||
e.cd = st(e.kind).interval;
|
||||
return true;
|
||||
}
|
||||
if knows(e, "dash") {
|
||||
let d = ((tx - e.x).powi(2) + (ty - e.y).powi(2)).sqrt();
|
||||
if d > 2.5 && d < 6.0 && self.los(e.x, e.y, tx, ty) {
|
||||
let (nx, ny) = (tx - (tx - e.x) / d * 0.8, ty - (ty - e.y) / d * 0.8);
|
||||
if self.map.walk(nx as i32, ny as i32) {
|
||||
self.fx(0, e.x, e.y, nx, ny, 0.15, 0xffffffff);
|
||||
e.x = nx;
|
||||
e.y = ny;
|
||||
e.path.clear();
|
||||
hurt(self, j, e.atk() * 1.2);
|
||||
e.ab_cd = 8.0;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
// abilities that work on their own (escape, protection, healing)
|
||||
pub(crate) fn passive_abilities(&mut self, e: &mut Ent) {
|
||||
if e.ab_cd > 0.0 || e.hero || e.side != 0 {
|
||||
return;
|
||||
}
|
||||
let fighting = matches!(e.job, Job::Attack(_));
|
||||
if knows(e, "teleport") && fighting && e.hp < e.max_hp * 0.3 {
|
||||
let (cx, cy) = self.map.core;
|
||||
let to = e.bed.map(|b| (xy(b).0 as f32 + 0.5, xy(b).1 as f32 + 0.5)).unwrap_or((cx as f32 - 0.5, cy as f32 + 1.5));
|
||||
self.fx(7, e.x, e.y, 0.0, 0.0, 0.6, 0xffff66cc);
|
||||
e.x = to.0;
|
||||
e.y = to.1;
|
||||
e.path.clear();
|
||||
e.job = Job::None;
|
||||
self.fx(7, e.x, e.y, 0.0, 0.0, 0.6, 0xffff66cc);
|
||||
self.sfx(Sfx::Summon, e.x, e.y);
|
||||
e.ab_cd = 30.0;
|
||||
} else if knows(e, "stoneform") && e.hp < e.max_hp * 0.4 {
|
||||
e.shield = 6.0;
|
||||
e.ab_cd = 25.0;
|
||||
self.fx(7, e.x, e.y, 0.0, 0.0, 0.6, 0xffa0a0a0);
|
||||
} else if knows(e, "pulse") {
|
||||
let hurt: Vec<usize> = (0..self.ents.len()).filter(|&o| {
|
||||
let u = &self.ents[o];
|
||||
u.alive && !u.held && u.creature() && u.hp < u.max_hp * 0.7 && (u.x - e.x).powi(2) + (u.y - e.y).powi(2) < 9.0
|
||||
}).collect();
|
||||
if hurt.len() >= 2 {
|
||||
for o in hurt {
|
||||
let u = &mut self.ents[o];
|
||||
u.hp = (u.hp + 30.0).min(u.max_hp);
|
||||
let (ux, uy) = (u.x, u.y);
|
||||
self.fx(4, ux, uy, 0.0, 0.0, 0.8, 0xff60ff80);
|
||||
}
|
||||
self.part(RING, (e.x, e.y, 0.05), (0.0, 0.0, 0.0), 0.5, abgr(120, 255, 140), 3.0);
|
||||
e.ab_cd = 12.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hated_neighbours_brawl_but_nobody_dies() {
|
||||
let mut g = Game::new(8, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
let a = g.spawn(EK::Mage, false, cx as f32 - 0.5, cy as f32 + 0.5, 2);
|
||||
let b = g.spawn(EK::Ogre, false, cx as f32 - 0.5, cy as f32 + 1.5, 2);
|
||||
for id in [a, b] {
|
||||
let j = g.find(id).unwrap();
|
||||
g.ents[j].settled = true;
|
||||
}
|
||||
let mut brawled = false;
|
||||
for _ in 0..(20 * 120) {
|
||||
g.update(0.05);
|
||||
brawled |= g.ents.iter().any(|e| matches!(e.job, Job::Brawl(_)));
|
||||
for id in [a, b] {
|
||||
let j = g.find(id).unwrap();
|
||||
g.ents[j].hunger = 0.0;
|
||||
g.ents[j].energy = 1.0;
|
||||
if !matches!(g.ents[j].job, Job::Brawl(_)) {
|
||||
g.ents[j].job = Job::None;
|
||||
g.ents[j].think = 99.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(brawled, "they quarrel");
|
||||
assert!(g.find(a).is_some() && g.find(b).is_some(), "nobody died");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ogres_learn_to_stomp() {
|
||||
let mut g = Game::new(8, 64, 64);
|
||||
let (cx, cy) = g.map.core;
|
||||
let o = g.spawn(EK::Ogre, false, cx as f32 - 0.5, cy as f32 + 1.5, 4);
|
||||
for k in 0..2 {
|
||||
g.spawn(EK::Knight, true, cx as f32 - 0.5 + k as f32 * 0.3, cy as f32 + 2.3, 1);
|
||||
}
|
||||
let j = g.find(o).unwrap();
|
||||
let mut e = g.ents[j].clone();
|
||||
let target = g.ents.iter().position(|x| x.hero).unwrap();
|
||||
assert!(g.use_ability(&mut e, target));
|
||||
assert!(g.ents.iter().filter(|x| x.hero).all(|x| x.stun > 0.0));
|
||||
}
|
||||
}
|
||||
@@ -238,6 +238,7 @@ impl Game {
|
||||
// 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 {
|
||||
|
||||
Reference in new issue
Block a user