Torture chamber with prisoners and defectors, Jailer; temple with prayer, Flame Priestess
- defeated heroes are knocked out if the torture chamber has room; imps carry them in - torture drains HP over ~100 s; ~40% defect (paladins ~25%) and become own creatures - Jailer (6+ torture tiles): tanky melee that pulls enemies in with a chain - temple: creatures pray for mana (more than studying) and mood - Flame Priestess (6+ temple tiles): fireballs with splash, heals creatures and the Soul Core - tests: prisoner chain + defect rate, temple prayer + core healing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
903d6fb0f4
commit
a1ad5ad141
7 files changed
+545
-25
No files matched your search
@@ -309,3 +309,16 @@ hint.tower_where = Türme können nur auf eroberten, leeren Bodenfeldern gebaut
|
|||||||
hint.tower_mana = Deinen Magietürmen ist das Mana ausgegangen.
|
hint.tower_mana = Deinen Magietürmen ist das Mana ausgegangen.
|
||||||
job.repair = Repariert einen Turm
|
job.repair = Repariert einen Turm
|
||||||
job.attack_tower = Greift einen Turm an
|
job.attack_tower = Greift einen Turm an
|
||||||
|
|
||||||
|
unit.jailer = Kerkermeister
|
||||||
|
unit.priestess = Flammenpriesterin
|
||||||
|
msg.hero_captured = {0} bewusstlos geschlagen und gefangen genommen! Beute: {1} Gold.
|
||||||
|
msg.defected = {0} ist zu dir übergelaufen!
|
||||||
|
msg.prisoner_died = {0} ist unter der Folter gestorben.
|
||||||
|
job.carry = Trägt einen Gefangenen
|
||||||
|
job.pray = Betet
|
||||||
|
job.heal_core = Heilt den Seelenkern
|
||||||
|
info.unconscious = bewusstlos
|
||||||
|
info.carried = wird in die Folterkammer getragen
|
||||||
|
info.tortured = wird gefoltert
|
||||||
|
info.prison = Gefangene: {0}/{1}
|
||||||
@@ -310,3 +310,16 @@ hint.tower_where = Towers can only be built on claimed, empty floor tiles.
|
|||||||
hint.tower_mana = Your magic towers have run out of mana.
|
hint.tower_mana = Your magic towers have run out of mana.
|
||||||
job.repair = Repairing a tower
|
job.repair = Repairing a tower
|
||||||
job.attack_tower = Attacking a tower
|
job.attack_tower = Attacking a tower
|
||||||
|
|
||||||
|
unit.jailer = Jailer
|
||||||
|
unit.priestess = Flame Priestess
|
||||||
|
msg.hero_captured = {0} knocked out and taken prisoner! Loot: {1} gold.
|
||||||
|
msg.defected = {0} has defected to your side!
|
||||||
|
msg.prisoner_died = {0} died under torture.
|
||||||
|
job.carry = Carrying a prisoner
|
||||||
|
job.pray = Praying
|
||||||
|
job.heal_core = Healing the Soul Core
|
||||||
|
info.unconscious = unconscious
|
||||||
|
info.carried = being carried to the torture chamber
|
||||||
|
info.tortured = being tortured
|
||||||
|
info.prison = Prisoners: {0}/{1}
|
||||||
+454
-13
@@ -90,6 +90,8 @@ pub enum EK {
|
|||||||
Priest,
|
Priest,
|
||||||
Dwarf,
|
Dwarf,
|
||||||
Paladin,
|
Paladin,
|
||||||
|
Jailer, // attracted by the torture chamber
|
||||||
|
Priestess, // attracted by the temple
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct St {
|
pub struct St {
|
||||||
@@ -111,11 +113,14 @@ pub fn st(k: EK) -> St {
|
|||||||
EK::Spider => s("unit.spider", 95.0, 10.0, 1.0, 2.9, 1.0, 60, 0),
|
EK::Spider => s("unit.spider", 95.0, 10.0, 1.0, 2.9, 1.0, 60, 0),
|
||||||
EK::Mage => s("unit.mage", 60.0, 13.0, 4.5, 2.0, 1.4, 110, 0),
|
EK::Mage => s("unit.mage", 60.0, 13.0, 4.5, 2.0, 1.4, 110, 0),
|
||||||
EK::Ogre => s("unit.ogre", 230.0, 20.0, 1.0, 1.8, 1.4, 150, 0),
|
EK::Ogre => s("unit.ogre", 230.0, 20.0, 1.0, 1.8, 1.4, 150, 0),
|
||||||
EK::Knight => s("unit.knight", 140.0, 11.0, 1.0, 1.9, 1.1, 0, 120),
|
// heroes: the wage applies when they defected to the player
|
||||||
EK::Archer => s("unit.archer", 75.0, 9.0, 4.5, 2.1, 1.3, 0, 100),
|
EK::Knight => s("unit.knight", 140.0, 11.0, 1.0, 1.9, 1.1, 70, 120),
|
||||||
EK::Priest => s("unit.priest", 80.0, 5.0, 3.5, 1.9, 1.5, 0, 120),
|
EK::Archer => s("unit.archer", 75.0, 9.0, 4.5, 2.1, 1.3, 60, 100),
|
||||||
EK::Dwarf => s("unit.dwarf", 110.0, 9.0, 1.0, 2.0, 1.0, 0, 100),
|
EK::Priest => s("unit.priest", 80.0, 5.0, 3.5, 1.9, 1.5, 60, 120),
|
||||||
EK::Paladin => s("unit.paladin", 700.0, 26.0, 1.0, 1.7, 1.2, 0, 1000),
|
EK::Dwarf => s("unit.dwarf", 110.0, 9.0, 1.0, 2.0, 1.0, 60, 100),
|
||||||
|
EK::Paladin => s("unit.paladin", 700.0, 26.0, 1.0, 1.7, 1.2, 220, 1000),
|
||||||
|
EK::Jailer => s("unit.jailer", 260.0, 16.0, 1.0, 1.6, 1.3, 140, 0),
|
||||||
|
EK::Priestess => s("unit.priestess", 70.0, 14.0, 5.0, 2.0, 1.6, 120, 0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,6 +142,9 @@ pub enum Job {
|
|||||||
Reinforce(usize), // wall tile
|
Reinforce(usize), // wall tile
|
||||||
Repair(u32), // tower id
|
Repair(u32), // tower id
|
||||||
AttackTower(u32), // tower id
|
AttackTower(u32), // tower id
|
||||||
|
Carry(u32), // imp carries an unconscious hero (id) to the torture chamber
|
||||||
|
Pray(usize), // temple tile
|
||||||
|
HealCore, // flame priestess heals the soul core
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize)]
|
||||||
@@ -170,6 +178,14 @@ pub struct Ent {
|
|||||||
pub working: bool,
|
pub working: bool,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub slow: f32, // seconds of slowdown left (magic tower)
|
pub slow: f32, // seconds of slowdown left (magic tower)
|
||||||
|
#[serde(default)]
|
||||||
|
pub captive: u8, // heroes: 0 free, 1 unconscious, 2 carried, 3 in the torture chamber
|
||||||
|
#[serde(default)]
|
||||||
|
pub carrier: u32, // imp that has taken care of this captive
|
||||||
|
#[serde(default)]
|
||||||
|
pub torture_t: f32,
|
||||||
|
#[serde(default)]
|
||||||
|
pub skill_cd: f32, // special abilities (jailer pull, priestess heal)
|
||||||
pub guard: bool, // stays at its home (gate defenders, treasure guards)
|
pub guard: bool, // stays at its home (gate defenders, treasure guards)
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub home: Option<(f32, f32)>, // guard post
|
pub home: Option<(f32, f32)>, // guard post
|
||||||
@@ -206,6 +222,10 @@ impl Ent {
|
|||||||
moving: false,
|
moving: false,
|
||||||
working: false,
|
working: false,
|
||||||
slow: 0.0,
|
slow: 0.0,
|
||||||
|
captive: 0,
|
||||||
|
carrier: 0,
|
||||||
|
torture_t: 0.0,
|
||||||
|
skill_cd: 0.0,
|
||||||
guard: false,
|
guard: false,
|
||||||
home: None,
|
home: None,
|
||||||
}
|
}
|
||||||
@@ -219,6 +239,10 @@ impl Ent {
|
|||||||
pub fn creature(&self) -> bool {
|
pub fn creature(&self) -> bool {
|
||||||
!self.hero && self.kind != EK::Worker
|
!self.hero && self.kind != EK::Worker
|
||||||
}
|
}
|
||||||
|
// a hero that is still fighting (not a prisoner)
|
||||||
|
pub fn active_hero(&self) -> bool {
|
||||||
|
self.hero && self.captive == 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -484,7 +508,7 @@ impl Game {
|
|||||||
// gate defenders alive (not the treasure guards)
|
// gate defenders alive (not the treasure guards)
|
||||||
pub fn gate_defenders_alive(&self) -> bool {
|
pub fn gate_defenders_alive(&self) -> bool {
|
||||||
let (gx, gy) = self.gate_center();
|
let (gx, gy) = self.gate_center();
|
||||||
self.ents.iter().any(|e| e.alive && e.guard && e.home.map_or(false, |(x, y)| (x - gx).abs() < 1.0 && (y - gy).abs() < 1.0))
|
self.ents.iter().any(|e| e.alive && e.guard && e.captive == 0 && e.home.map_or(false, |(x, y)| (x - gx).abs() < 1.0 && (y - gy).abs() < 1.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -641,6 +665,12 @@ impl Game {
|
|||||||
if self.counts[3] >= 9 {
|
if self.counts[3] >= 9 {
|
||||||
opts.push((EK::Ogre, 2));
|
opts.push((EK::Ogre, 2));
|
||||||
}
|
}
|
||||||
|
if self.counts[5] >= 6 {
|
||||||
|
opts.push((EK::Jailer, 2));
|
||||||
|
}
|
||||||
|
if self.counts[6] >= 6 {
|
||||||
|
opts.push((EK::Priestess, 2));
|
||||||
|
}
|
||||||
let total: u32 = opts.iter().map(|o| o.1).sum();
|
let total: u32 = opts.iter().map(|o| o.1).sum();
|
||||||
let mut r = self.rng.next() % total;
|
let mut r = self.rng.next() % total;
|
||||||
let mut kind = EK::Kobold;
|
let mut kind = EK::Kobold;
|
||||||
@@ -822,6 +852,7 @@ impl Game {
|
|||||||
e.anim += dt;
|
e.anim += dt;
|
||||||
e.flash = (e.flash - dt).max(0.0);
|
e.flash = (e.flash - dt).max(0.0);
|
||||||
e.slow = (e.slow - dt).max(0.0);
|
e.slow = (e.slow - dt).max(0.0);
|
||||||
|
e.skill_cd -= dt;
|
||||||
e.cd -= dt;
|
e.cd -= dt;
|
||||||
e.think -= dt;
|
e.think -= dt;
|
||||||
e.repath -= dt;
|
e.repath -= dt;
|
||||||
@@ -842,7 +873,9 @@ impl Game {
|
|||||||
self.msg("msg.level_up", vec![format!("@{}", s.name), e.level.to_string()]);
|
self.msg("msg.level_up", vec![format!("@{}", s.name), e.level.to_string()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e.hero {
|
if e.hero && e.captive != 0 {
|
||||||
|
self.captive_update(e, dt);
|
||||||
|
} else if e.hero {
|
||||||
self.hero_ai(e, dt);
|
self.hero_ai(e, dt);
|
||||||
} else if e.kind == EK::Worker {
|
} else if e.kind == EK::Worker {
|
||||||
self.worker_ai(e, dt);
|
self.worker_ai(e, dt);
|
||||||
@@ -966,7 +999,7 @@ impl Game {
|
|||||||
let mut best = None;
|
let mut best = None;
|
||||||
let mut bd = radius * radius;
|
let mut bd = radius * radius;
|
||||||
for o in &self.ents {
|
for o in &self.ents {
|
||||||
if !o.alive || o.held || o.hero == e.hero {
|
if !o.alive || o.held || o.hero == e.hero || o.captive != 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2);
|
let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2);
|
||||||
@@ -987,6 +1020,21 @@ impl Game {
|
|||||||
if d > 12.0 {
|
if d > 12.0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if e.kind == EK::Jailer && e.skill_cd <= 0.0 && d > 1.4 && d < 4.5 && self.los(e.x, e.y, tx, ty) {
|
||||||
|
// jailer: pulls the enemy in with a chain
|
||||||
|
let (dx, dy) = norm(tx - e.x, ty - e.y);
|
||||||
|
let (nx, ny) = (e.x + dx * 0.9, e.y + dy * 0.9);
|
||||||
|
if self.map.walk(nx as i32, ny as i32) {
|
||||||
|
self.fx(0, tx, ty - 0.3, e.x, e.y - 0.3, 0.3, 0xff8090a0);
|
||||||
|
self.sfx(Sfx::Capture, e.x, e.y);
|
||||||
|
let t = &mut self.ents[j];
|
||||||
|
t.x = nx;
|
||||||
|
t.y = ny;
|
||||||
|
t.path.clear();
|
||||||
|
e.skill_cd = 6.0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
if d <= s.range + 0.45 && (s.range < 1.5 || self.los(e.x, e.y, tx, ty)) {
|
if d <= s.range + 0.45 && (s.range < 1.5 || self.los(e.x, e.y, tx, ty)) {
|
||||||
e.path.clear();
|
e.path.clear();
|
||||||
e.face = sgn(tx - e.x);
|
e.face = sgn(tx - e.x);
|
||||||
@@ -1025,6 +1073,7 @@ impl Game {
|
|||||||
let col = match e.kind {
|
let col = match e.kind {
|
||||||
EK::Mage => 0xffff44cc,
|
EK::Mage => 0xffff44cc,
|
||||||
EK::Priest => 0xff88ffff,
|
EK::Priest => 0xff88ffff,
|
||||||
|
EK::Priestess => 0xff3080ff,
|
||||||
_ => 0xff70b0d0,
|
_ => 0xff70b0d0,
|
||||||
};
|
};
|
||||||
self.fx(0, e.x, e.y - 0.3, tx, ty - 0.3, 0.25, col);
|
self.fx(0, e.x, e.y - 0.3, tx, ty - 0.3, 0.25, col);
|
||||||
@@ -1034,6 +1083,30 @@ impl Game {
|
|||||||
self.sfx(Sfx::MeleeHit, tx, ty);
|
self.sfx(Sfx::MeleeHit, tx, ty);
|
||||||
}
|
}
|
||||||
e.xp += dmg * 0.6;
|
e.xp += dmg * 0.6;
|
||||||
|
if e.kind == EK::Priestess {
|
||||||
|
// fireball: half damage to enemies around the target
|
||||||
|
self.fx(3, tx, ty, 0.0, 0.0, 0.5, 0xff3080ff);
|
||||||
|
let side = !e.hero;
|
||||||
|
let splash: Vec<usize> = self
|
||||||
|
.ents
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(o, x)| *o != j && x.alive && !x.held && x.hero == side && x.captive == 0 && (x.x - tx).powi(2) + (x.y - ty).powi(2) < 1.44)
|
||||||
|
.map(|(o, _)| o)
|
||||||
|
.collect();
|
||||||
|
for o in splash {
|
||||||
|
if self.ents[o].alive {
|
||||||
|
self.ents[o].hp -= dmg * 0.5;
|
||||||
|
self.ents[o].flash = 0.15;
|
||||||
|
if self.ents[o].hp <= 0.0 {
|
||||||
|
self.kill(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !self.ents[j].alive {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
let my_id = e.id;
|
let my_id = e.id;
|
||||||
let t = &mut self.ents[j];
|
let t = &mut self.ents[j];
|
||||||
t.hp -= dmg;
|
t.hp -= dmg;
|
||||||
@@ -1047,10 +1120,46 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// free places in the torture chamber (one prisoner per 2 tiles)
|
||||||
|
pub fn prison_space(&self) -> usize {
|
||||||
|
let cap = self.counts[5] / 2;
|
||||||
|
let used = self.ents.iter().filter(|e| e.alive && e.hero && e.captive != 0).count();
|
||||||
|
cap.saturating_sub(used)
|
||||||
|
}
|
||||||
|
|
||||||
fn kill(&mut self, j: usize) {
|
fn kill(&mut self, j: usize) {
|
||||||
|
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
|
||||||
|
let (kind, x, y) = (e.kind, e.x, e.y);
|
||||||
|
let id = e.id;
|
||||||
|
self.release_all(id);
|
||||||
|
let e = &mut self.ents[j];
|
||||||
|
e.captive = 1;
|
||||||
|
e.hp = 1.0;
|
||||||
|
e.carrier = 0;
|
||||||
|
e.torture_t = 0.0;
|
||||||
|
e.job = Job::None;
|
||||||
|
e.path.clear();
|
||||||
|
e.guard = false;
|
||||||
|
e.home = None;
|
||||||
|
let b = st(kind).bounty as f32;
|
||||||
|
let got = self.add_gold(b);
|
||||||
|
self.fx(5, x, y, 0.0, 0.0, 1.0, 0xff30d0ff);
|
||||||
|
self.sfx(Sfx::Capture, x, y);
|
||||||
|
self.msg("msg.hero_captured", vec![format!("@{}", st(kind).name), (got as i32).to_string()]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
let e = &mut self.ents[j];
|
let e = &mut self.ents[j];
|
||||||
e.alive = false;
|
e.alive = false;
|
||||||
let (id, kind, hero, x, y) = (e.id, e.kind, e.hero, e.x, e.y);
|
let (id, kind, hero, x, y) = (e.id, e.kind, e.hero, e.x, e.y);
|
||||||
|
// prisoners carried by this unit are dropped
|
||||||
|
for o in self.ents.iter_mut() {
|
||||||
|
if o.captive == 2 && o.carrier == id {
|
||||||
|
o.captive = 1;
|
||||||
|
o.carrier = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
self.release_all(id);
|
self.release_all(id);
|
||||||
self.fx(3, x, y, 0.0, 0.0, 0.7, 0xff505070);
|
self.fx(3, x, y, 0.0, 0.0, 0.7, 0xff505070);
|
||||||
let s = st(kind);
|
let s = st(kind);
|
||||||
@@ -1204,6 +1313,9 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Job::Carry(id) => {
|
||||||
|
self.carry_job(e, id, dt);
|
||||||
|
}
|
||||||
Job::Repair(id) => {
|
Job::Repair(id) => {
|
||||||
let Some(k) = self.towers.iter().position(|t| t.id == id) else {
|
let Some(k) = self.towers.iter().position(|t| t.id == id) else {
|
||||||
e.job = Job::None;
|
e.job = Job::None;
|
||||||
@@ -1309,6 +1421,21 @@ impl Game {
|
|||||||
if e.carry >= CARRY_MAX && self.start_deliver(e) {
|
if e.carry >= CARRY_MAX && self.start_deliver(e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// prisoners lying around are carried to the torture chamber first
|
||||||
|
let lying = self.ents.iter().any(|o| o.alive && o.captive == 1 && o.carrier == 0);
|
||||||
|
if lying && self.map.t.iter().any(|t| t.kind == Kind::Torture && t.bed == 0) {
|
||||||
|
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.captive_near(i).is_some(), true) {
|
||||||
|
let end = p.last().copied().unwrap_or(cur);
|
||||||
|
if let Some(id) = self.captive_near(end) {
|
||||||
|
if let Some(k) = self.find(id) {
|
||||||
|
self.ents[k].carrier = e.id;
|
||||||
|
}
|
||||||
|
e.path = p;
|
||||||
|
e.job = Job::Carry(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.dig_neighbor(i, me).is_some(), true) {
|
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.dig_neighbor(i, me).is_some(), true) {
|
||||||
let end = p.last().copied().unwrap_or(cur);
|
let end = p.last().copied().unwrap_or(cur);
|
||||||
if let Some(t) = self.dig_neighbor(end, me) {
|
if let Some(t) = self.dig_neighbor(end, me) {
|
||||||
@@ -1376,6 +1503,31 @@ impl Game {
|
|||||||
if e.hunger > 1.8 {
|
if e.hunger > 1.8 {
|
||||||
e.happy -= dt * 0.004;
|
e.happy -= dt * 0.004;
|
||||||
}
|
}
|
||||||
|
if e.kind == EK::Priestess && e.skill_cd <= 0.0 {
|
||||||
|
// flame priestess: heals the most hurt own creature nearby (or herself)
|
||||||
|
let mut best: Option<usize> = None;
|
||||||
|
let mut worst = if e.hp < e.max_hp * 0.8 { e.hp / e.max_hp } else { 0.8 };
|
||||||
|
for (j, o) in self.ents.iter().enumerate() {
|
||||||
|
if o.alive && !o.held && o.creature() && (o.x - e.x).powi(2) + (o.y - e.y).powi(2) < 4.5 * 4.5 && o.hp / o.max_hp < worst {
|
||||||
|
worst = o.hp / o.max_hp;
|
||||||
|
best = Some(j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(j) = best {
|
||||||
|
let o = &mut self.ents[j];
|
||||||
|
o.hp = (o.hp + 20.0).min(o.max_hp);
|
||||||
|
let (ox, oy) = (o.x, o.y);
|
||||||
|
self.fx(4, ox, oy, 0.0, 0.0, 0.8, 0xff60ff80);
|
||||||
|
self.sfx(Sfx::Heal, ox, oy);
|
||||||
|
e.skill_cd = 2.5;
|
||||||
|
} else if e.hp < e.max_hp * 0.8 {
|
||||||
|
e.hp = (e.hp + 20.0).min(e.max_hp);
|
||||||
|
self.fx(4, e.x, e.y, 0.0, 0.0, 0.8, 0xff60ff80);
|
||||||
|
e.skill_cd = 2.5;
|
||||||
|
} else {
|
||||||
|
e.skill_cd = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
if e.think <= 0.0 {
|
if e.think <= 0.0 {
|
||||||
e.think = 0.4 + self.rng.f() * 0.3;
|
e.think = 0.4 + self.rng.f() * 0.3;
|
||||||
if !matches!(e.job, Job::Attack(_) | Job::Leave) {
|
if !matches!(e.job, Job::Attack(_) | Job::Leave) {
|
||||||
@@ -1462,6 +1614,36 @@ impl Game {
|
|||||||
e.job = Job::None;
|
e.job = Job::None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Job::Pray(t) => {
|
||||||
|
if cur != t || self.map.t[t].kind != Kind::Temple {
|
||||||
|
self.unreserve(me);
|
||||||
|
e.job = Job::None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.working = true;
|
||||||
|
let rate = if e.kind == EK::Priestess { 1.6 } else { 0.9 };
|
||||||
|
self.mana = (self.mana + rate * dt).min(MANA_MAX);
|
||||||
|
e.happy = (e.happy + 0.01 * dt).min(1.0);
|
||||||
|
e.xp += 1.0 * dt;
|
||||||
|
e.timer -= dt;
|
||||||
|
if e.timer <= 0.0 {
|
||||||
|
self.unreserve(me);
|
||||||
|
e.job = Job::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Job::HealCore => {
|
||||||
|
if !self.map.adj_core(cur) || self.core_hp >= CORE_HP {
|
||||||
|
e.job = Job::None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.working = true;
|
||||||
|
let (cx, cy) = self.map.core;
|
||||||
|
e.face = sgn(cx as f32 + 1.5 - e.x);
|
||||||
|
self.core_hp = (self.core_hp + 8.0 * dt).min(CORE_HP);
|
||||||
|
if (e.anim * 1.5).fract() < dt * 1.5 {
|
||||||
|
self.fx(4, cx as f32 + 1.5, cy as f32 + 1.5, 0.0, 0.0, 0.8, 0xff60ff80);
|
||||||
|
}
|
||||||
|
}
|
||||||
Job::Wander => {
|
Job::Wander => {
|
||||||
e.timer -= dt;
|
e.timer -= dt;
|
||||||
if e.timer <= 0.0 {
|
if e.timer <= 0.0 {
|
||||||
@@ -1661,6 +1843,26 @@ impl Game {
|
|||||||
self.creature_choose(e);
|
self.creature_choose(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if e.kind == EK::Priestess && self.core_hp < CORE_HP * 0.9 && self.nearest_enemy(e, 8.0).is_none() {
|
||||||
|
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.map.adj_core(i), true) {
|
||||||
|
e.path = p;
|
||||||
|
e.job = Job::HealCore;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.counts[6] > 0 {
|
||||||
|
let r2 = self.rng.f();
|
||||||
|
if r2 < 0.3 || (e.kind == EK::Priestess && r2 < 0.7) {
|
||||||
|
if let Some(p) = self.find_room_tile(cur, Kind::Temple, me, false) {
|
||||||
|
let t = p.last().copied().unwrap_or(cur);
|
||||||
|
self.map.t[t].reserved = me;
|
||||||
|
e.path = p;
|
||||||
|
e.job = Job::Pray(t);
|
||||||
|
e.timer = 8.0 + self.rng.f() * 6.0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let r = self.rng.f();
|
let r = self.rng.f();
|
||||||
if (e.kind == EK::Mage && r < 0.8) || r < 0.2 {
|
if (e.kind == EK::Mage && r < 0.8) || r < 0.2 {
|
||||||
if let Some(p) = self.find_room_tile(cur, Kind::Library, me, false) {
|
if let Some(p) = self.find_room_tile(cur, Kind::Library, me, false) {
|
||||||
@@ -1685,6 +1887,176 @@ impl Game {
|
|||||||
self.wander(e, 4);
|
self.wander(e, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- prisoners
|
||||||
|
|
||||||
|
fn captive_update(&mut self, e: &mut Ent, dt: f32) {
|
||||||
|
e.moving = false;
|
||||||
|
match e.captive {
|
||||||
|
1 => {
|
||||||
|
// unconscious: bleeds out if nobody carries the hero away
|
||||||
|
e.torture_t += dt;
|
||||||
|
if e.torture_t > 60.0 {
|
||||||
|
e.alive = false;
|
||||||
|
self.release_all(e.id);
|
||||||
|
self.fx(3, e.x, e.y, 0.0, 0.0, 0.7, 0xff505070);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
// carried: follows the imp
|
||||||
|
match self.find(e.carrier) {
|
||||||
|
Some(k) => {
|
||||||
|
e.x = self.ents[k].x;
|
||||||
|
e.y = self.ents[k].y;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
e.captive = 1;
|
||||||
|
e.carrier = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let t = e.tile();
|
||||||
|
if self.map.t[t].kind != Kind::Torture {
|
||||||
|
// the chamber is gone: the prisoner breaks free
|
||||||
|
e.captive = 0;
|
||||||
|
e.hp = e.max_hp * 0.3;
|
||||||
|
self.release_all(e.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.working = true;
|
||||||
|
e.hp -= e.max_hp / 100.0 * dt;
|
||||||
|
e.torture_t += dt;
|
||||||
|
if e.torture_t >= 5.0 {
|
||||||
|
e.torture_t -= 5.0;
|
||||||
|
// ~40% defect over the whole torture (paladins ~25%)
|
||||||
|
let p = if e.kind == EK::Paladin { 0.0143 } else { 0.0252 };
|
||||||
|
if self.rng.f() < p {
|
||||||
|
self.defect(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.hp <= 0.0 {
|
||||||
|
e.alive = false;
|
||||||
|
self.release_all(e.id);
|
||||||
|
self.fx(3, e.x, e.y, 0.0, 0.0, 0.7, 0xff505070);
|
||||||
|
self.sfx(Sfx::HeroDeath, e.x, e.y);
|
||||||
|
self.msg("msg.prisoner_died", vec![format!("@{}", st(e.kind).name)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// a prisoner joins the player's side
|
||||||
|
fn defect(&mut self, e: &mut Ent) {
|
||||||
|
self.release_all(e.id);
|
||||||
|
e.hero = false;
|
||||||
|
e.captive = 0;
|
||||||
|
e.carrier = 0;
|
||||||
|
e.guard = false;
|
||||||
|
e.home = None;
|
||||||
|
e.hp = e.max_hp * 0.5;
|
||||||
|
e.happy = 0.6;
|
||||||
|
e.hunger = 0.5;
|
||||||
|
e.energy = 0.4;
|
||||||
|
e.job = Job::None;
|
||||||
|
e.path.clear();
|
||||||
|
e.bed = None;
|
||||||
|
self.fx(7, e.x, e.y, 0.0, 0.0, 1.0, 0xff4040e0);
|
||||||
|
self.sfx(Sfx::Convert, e.x, e.y);
|
||||||
|
self.msg("msg.defected", vec![format!("@{}", st(e.kind).name)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// unconscious hero lying next to tile i that no imp takes care of yet
|
||||||
|
fn captive_near(&self, i: usize) -> Option<u32> {
|
||||||
|
let (x, y) = xy(i);
|
||||||
|
self.ents.iter().find(|o| {
|
||||||
|
o.alive && o.hero && o.captive == 1 && o.carrier == 0 && (o.x as i32 - x).abs() <= 1 && (o.y as i32 - y).abs() <= 1
|
||||||
|
}).map(|o| o.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn free_torture_tile(&self, i: usize) -> bool {
|
||||||
|
let t = &self.map.t[i];
|
||||||
|
t.kind == Kind::Torture && t.owner == Owner::Player && t.bed == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn carry_job(&mut self, e: &mut Ent, id: u32, dt: f32) {
|
||||||
|
let Some(k) = self.find(id) else {
|
||||||
|
e.job = Job::None;
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let cur = e.tile();
|
||||||
|
match self.ents[k].captive {
|
||||||
|
1 => {
|
||||||
|
let (cx, cy) = (self.ents[k].x, self.ents[k].y);
|
||||||
|
if (cx - e.x).abs() < 1.2 && (cy - e.y).abs() < 1.2 {
|
||||||
|
// pick up and look for a free place in the torture chamber
|
||||||
|
match self.map.search(cur, self.map.walk_cost(), |i| self.free_torture_tile(i), true) {
|
||||||
|
Some(p) => {
|
||||||
|
let target = p.last().copied().unwrap_or(cur);
|
||||||
|
self.map.t[target].bed = id + 1;
|
||||||
|
let c = &mut self.ents[k];
|
||||||
|
c.captive = 2;
|
||||||
|
c.carrier = e.id;
|
||||||
|
e.path = p;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.ents[k].carrier = 0;
|
||||||
|
e.job = Job::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if e.path.is_empty() {
|
||||||
|
match self.map.path_to(cur, idx(cx as i32, cy as i32)) {
|
||||||
|
Some(p) if !p.is_empty() => e.path = p,
|
||||||
|
_ => {
|
||||||
|
self.ents[k].carrier = 0;
|
||||||
|
e.job = Job::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 if self.ents[k].carrier == e.id => {
|
||||||
|
if !self.step_path(e, dt) {
|
||||||
|
// arrived (or blocked): put the prisoner down on the reserved torture tile
|
||||||
|
let target = self.map.t.iter().position(|t| t.bed == id + 1);
|
||||||
|
match target {
|
||||||
|
Some(t) if t == cur || adj4(t, cur) || t == e.tile() => {
|
||||||
|
let (tx, ty) = xy(t);
|
||||||
|
let c = &mut self.ents[k];
|
||||||
|
c.captive = 3;
|
||||||
|
c.carrier = 0;
|
||||||
|
c.hp = c.max_hp; // the torture drains it over ~100 s
|
||||||
|
c.x = tx as f32 + 0.5;
|
||||||
|
c.y = ty as f32 + 0.5;
|
||||||
|
c.torture_t = 0.0;
|
||||||
|
self.sfx(Sfx::Capture, tx as f32 + 0.5, ty as f32 + 0.5);
|
||||||
|
e.job = Job::None;
|
||||||
|
}
|
||||||
|
Some(t) => {
|
||||||
|
if let Some(p) = self.map.path_to(cur, t) {
|
||||||
|
e.path = p;
|
||||||
|
} else {
|
||||||
|
self.ents[k].captive = 1;
|
||||||
|
self.ents[k].carrier = 0;
|
||||||
|
e.job = Job::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.ents[k].captive = 1;
|
||||||
|
self.ents[k].carrier = 0;
|
||||||
|
e.job = Job::None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
e.job = Job::None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.step_path(e, dt);
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- heroes
|
// ---------------------------------------------------------------- heroes
|
||||||
|
|
||||||
fn hero_ai(&mut self, e: &mut Ent, dt: f32) {
|
fn hero_ai(&mut self, e: &mut Ent, dt: f32) {
|
||||||
@@ -1985,7 +2357,7 @@ impl Game {
|
|||||||
let mut best = None;
|
let mut best = None;
|
||||||
let mut bd = r * r;
|
let mut bd = r * r;
|
||||||
for (j, e) in self.ents.iter().enumerate() {
|
for (j, e) in self.ents.iter().enumerate() {
|
||||||
if !e.alive || e.held || !e.hero {
|
if !e.alive || e.held || !e.active_hero() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let d = (e.x - px).powi(2) + (e.y - py).powi(2);
|
let d = (e.x - px).powi(2) + (e.y - py).powi(2);
|
||||||
@@ -2032,7 +2404,7 @@ impl Game {
|
|||||||
self.sfx(Sfx::Shoot, px, py);
|
self.sfx(Sfx::Shoot, px, py);
|
||||||
let mut hits = vec![(j, dmg)];
|
let mut hits = vec![(j, dmg)];
|
||||||
for (o, e) in self.ents.iter().enumerate() {
|
for (o, e) in self.ents.iter().enumerate() {
|
||||||
if o == j || !e.alive || e.held || !e.hero || hits.len() >= 3 {
|
if o == j || !e.alive || e.held || !e.active_hero() || hits.len() >= 3 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let rel = (e.x - px, e.y - py);
|
let rel = (e.x - px, e.y - py);
|
||||||
@@ -2056,7 +2428,7 @@ impl Game {
|
|||||||
self.sfx(Sfx::Cannon, px, py);
|
self.sfx(Sfx::Cannon, px, py);
|
||||||
let mut hits = Vec::new();
|
let mut hits = Vec::new();
|
||||||
for (o, e) in self.ents.iter().enumerate() {
|
for (o, e) in self.ents.iter().enumerate() {
|
||||||
if !e.alive || e.held || !e.hero {
|
if !e.alive || e.held || !e.active_hero() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let d = ((e.x - ex).powi(2) + (e.y - ey).powi(2)).sqrt();
|
let d = ((e.x - ex).powi(2) + (e.y - ey).powi(2)).sqrt();
|
||||||
@@ -2090,7 +2462,7 @@ impl Game {
|
|||||||
.ents
|
.ents
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|(o, e)| *o != j && e.alive && !e.held && e.hero && (e.x - ex).powi(2) + (e.y - ey).powi(2) < 9.0)
|
.filter(|(o, e)| *o != j && e.alive && !e.held && e.active_hero() && (e.x - ex).powi(2) + (e.y - ey).powi(2) < 9.0)
|
||||||
.min_by(|a, b| {
|
.min_by(|a, b| {
|
||||||
let da = (a.1.x - ex).powi(2) + (a.1.y - ey).powi(2);
|
let da = (a.1.x - ex).powi(2) + (a.1.y - ey).powi(2);
|
||||||
let db = (b.1.x - ex).powi(2) + (b.1.y - ey).powi(2);
|
let db = (b.1.x - ex).powi(2) + (b.1.y - ey).powi(2);
|
||||||
@@ -2163,6 +2535,12 @@ impl Game {
|
|||||||
let id = self.ents[j].id;
|
let id = self.ents[j].id;
|
||||||
let me = id + 1;
|
let me = id + 1;
|
||||||
self.unreserve(me);
|
self.unreserve(me);
|
||||||
|
for o in self.ents.iter_mut() {
|
||||||
|
if o.carrier == id && o.captive != 0 {
|
||||||
|
o.captive = 1;
|
||||||
|
o.carrier = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
let e = &mut self.ents[j];
|
let e = &mut self.ents[j];
|
||||||
e.held = true;
|
e.held = true;
|
||||||
e.job = Job::None;
|
e.job = Job::None;
|
||||||
@@ -2211,7 +2589,7 @@ impl Game {
|
|||||||
self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa);
|
self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa);
|
||||||
let mut kills = Vec::new();
|
let mut kills = Vec::new();
|
||||||
for (j, e) in self.ents.iter_mut().enumerate() {
|
for (j, e) in self.ents.iter_mut().enumerate() {
|
||||||
if e.alive && !e.held && e.hero && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 2.6 {
|
if e.alive && !e.held && e.active_hero() && (e.x - wx).powi(2) + (e.y - wy).powi(2) < 2.6 {
|
||||||
e.hp -= 55.0;
|
e.hp -= 55.0;
|
||||||
e.flash = 0.2;
|
e.flash = 0.2;
|
||||||
if e.hp <= 0.0 {
|
if e.hp <= 0.0 {
|
||||||
@@ -2361,6 +2739,9 @@ pub fn job_name(e: &Ent) -> &'static str {
|
|||||||
Job::Leave => "job.leave",
|
Job::Leave => "job.leave",
|
||||||
Job::Reinforce(_) => "job.reinforce",
|
Job::Reinforce(_) => "job.reinforce",
|
||||||
Job::Repair(_) => "job.repair",
|
Job::Repair(_) => "job.repair",
|
||||||
|
Job::Carry(_) => "job.carry",
|
||||||
|
Job::Pray(_) => "job.pray",
|
||||||
|
Job::HealCore => "job.heal_core",
|
||||||
Job::AttackTower(_) => "job.attack_tower",
|
Job::AttackTower(_) => "job.attack_tower",
|
||||||
Job::Rally => {
|
Job::Rally => {
|
||||||
if e.working {
|
if e.working {
|
||||||
@@ -2515,4 +2896,64 @@ mod tests {
|
|||||||
println!("tower after attack: {:?}", t.map(|t| t.hp));
|
println!("tower after attack: {:?}", t.map(|t| t.hp));
|
||||||
assert!(t.map_or(true, |t| t.hp < t.max_hp()));
|
assert!(t.map_or(true, |t| t.hp < t.max_hp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn prisoners_are_tortured_and_resolved() {
|
||||||
|
let mut converted = 0;
|
||||||
|
let mut died = 0;
|
||||||
|
for seed in 1..=40u32 {
|
||||||
|
let mut g = Game::new(seed, 64, 64);
|
||||||
|
g.gold = 5000.0;
|
||||||
|
let (cx, cy) = g.map.core;
|
||||||
|
g.build_rect(5, cx - 3, cy - 3, cx - 2, cy + 1); // torture chamber 2x5 = 5 places... 10 tiles
|
||||||
|
assert!(g.prison_space() >= 2, "space {}", g.prison_space());
|
||||||
|
let id = g.spawn(EK::Knight, true, (cx + 4) as f32 + 0.5, (cy + 4) as f32 + 0.5, 1);
|
||||||
|
let j = g.ents.iter().position(|e| e.id == id).unwrap();
|
||||||
|
g.kill(j);
|
||||||
|
assert_eq!(g.ents[j].captive, 1);
|
||||||
|
let mut reached = false;
|
||||||
|
for _ in 0..(200 * 20) {
|
||||||
|
g.update(0.05);
|
||||||
|
match g.ents.iter().find(|e| e.id == id) {
|
||||||
|
Some(e) if e.captive == 3 => reached = true,
|
||||||
|
Some(e) if !e.hero => {
|
||||||
|
converted += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
died += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(reached, "seed {}: prisoner never reached the torture chamber", seed);
|
||||||
|
}
|
||||||
|
println!("40 prisoners: {} defected, {} died", converted, died);
|
||||||
|
assert_eq!(converted + died, 40);
|
||||||
|
assert!((8..=26).contains(&converted), "defect rate off: {}/40", converted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn temple_prayer_and_priestess_heals_core() {
|
||||||
|
let mut g = Game::new(9, 64, 64);
|
||||||
|
g.gold = 5000.0;
|
||||||
|
let (cx, cy) = g.map.core;
|
||||||
|
g.build_rect(6, cx - 3, cy - 3, cx - 2, cy + 1); // temple
|
||||||
|
g.build_rect(1, cx + 4, cy - 3, cx + 5, cy - 1); // lair
|
||||||
|
g.build_rect(2, cx + 4, cy + 3, cx + 5, cy + 5); // farm
|
||||||
|
g.spawn(EK::Priestess, false, (cx + 4) as f32 + 0.5, (cy + 2) as f32 + 0.5, 1);
|
||||||
|
for k in 0..3 {
|
||||||
|
g.spawn(EK::Kobold, false, (cx - 1) as f32 + 0.5, (cy + k) as f32 + 0.5, 1);
|
||||||
|
}
|
||||||
|
g.core_hp = 1000.0;
|
||||||
|
let mut prayed = false;
|
||||||
|
for _ in 0..(120 * 20) {
|
||||||
|
g.update(0.05);
|
||||||
|
prayed |= g.ents.iter().any(|e| matches!(e.job, Job::Pray(_)) && e.working);
|
||||||
|
}
|
||||||
|
println!("core after 2 min: {:.0}, prayed: {}", g.core_hp, prayed);
|
||||||
|
assert!(prayed);
|
||||||
|
assert!(g.core_hp > 1300.0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+28
-1
@@ -650,6 +650,30 @@ fn sprite_rows(k: EK) -> [&'static str; 10] {
|
|||||||
"..kBkkBk..",
|
"..kBkkBk..",
|
||||||
"..kk..kk..",
|
"..kk..kk..",
|
||||||
],
|
],
|
||||||
|
EK::Jailer => [
|
||||||
|
"...kkkk...",
|
||||||
|
"..kCCCCk..",
|
||||||
|
"..kCkkCk..",
|
||||||
|
"..kCeeCk..",
|
||||||
|
".kCCCCCCk.",
|
||||||
|
"kCCRRRRCCk",
|
||||||
|
"kCCRRRRCCS",
|
||||||
|
".kCCCCCCkS",
|
||||||
|
"..kCk.kCkS",
|
||||||
|
"..kk...kk.",
|
||||||
|
],
|
||||||
|
EK::Priestess => [
|
||||||
|
"....oo..y.",
|
||||||
|
"...kkkk.o.",
|
||||||
|
"..kffffk..",
|
||||||
|
"..kfkfkk..",
|
||||||
|
"..kRRRRk..",
|
||||||
|
".kRRooRRk.",
|
||||||
|
".kRRooRRkf",
|
||||||
|
".kRRRRRRk.",
|
||||||
|
".kRRRRRRk.",
|
||||||
|
"..kkkkkk..",
|
||||||
|
],
|
||||||
EK::Paladin => [
|
EK::Paladin => [
|
||||||
"...kkkk..w",
|
"...kkkk..w",
|
||||||
"..kyyyyk.w",
|
"..kyyyyk.w",
|
||||||
@@ -693,7 +717,10 @@ fn pal(ch: char) -> Option<Color> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const SPRITE_KINDS: [EK; 10] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin];
|
pub const SPRITE_KINDS: [EK; 12] = [EK::Worker, EK::Kobold, EK::Spider, EK::Mage, EK::Ogre, EK::Knight, EK::Archer, EK::Priest, EK::Dwarf, EK::Paladin, EK::Jailer, EK::Priestess];
|
||||||
|
// width of the sprite texture in pixels
|
||||||
|
pub const SPRITE_TEX_W: f32 = (12 * 12) as f32;
|
||||||
|
pub const SPRITE_TEX_H: f32 = 24.0;
|
||||||
|
|
||||||
pub fn sprite_index(k: EK) -> u32 {
|
pub fn sprite_index(k: EK) -> u32 {
|
||||||
SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32
|
SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32
|
||||||
|
|||||||
+1
-1
@@ -83,7 +83,7 @@ impl MusicDirector {
|
|||||||
use audio::Track;
|
use audio::Track;
|
||||||
const BUILD: [Track; 3] = [Track::Build1, Track::Build2, Track::Build3];
|
const BUILD: [Track; 3] = [Track::Build1, Track::Build2, Track::Build3];
|
||||||
let gate_fight = g.time - g.gate_hit_t < 12.0 || g.gate_defenders_alive();
|
let gate_fight = g.time - g.gate_hit_t < 12.0 || g.gate_defenders_alive();
|
||||||
let invasion = g.ents.iter().any(|e| e.alive && e.hero && !e.guard);
|
let invasion = g.ents.iter().any(|e| e.alive && e.active_hero() && !e.guard);
|
||||||
let want = if title {
|
let want = if title {
|
||||||
Track::Title
|
Track::Title
|
||||||
} else if g.state == 1 {
|
} else if g.state == 1 {
|
||||||
|
|||||||
+28
-10
@@ -935,18 +935,28 @@ impl R3d {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut mb = MB::new();
|
let mut mb = MB::new();
|
||||||
let sw = 120.0;
|
let sw = SPRITE_TEX_W;
|
||||||
let sh = 24.0;
|
let sh = SPRITE_TEX_H;
|
||||||
for e in &g.ents {
|
for e in &g.ents {
|
||||||
if !e.alive || e.held {
|
if !e.alive || e.held {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let big = matches!(e.kind, EK::Ogre | EK::Paladin);
|
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer);
|
||||||
let w = if big { 0.8 } else { 0.62 };
|
let w = if big { 0.8 } else { 0.62 };
|
||||||
let h = w;
|
let h = w;
|
||||||
let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving;
|
let sleeping = matches!(e.job, Job::Sleep(_)) && !e.moving;
|
||||||
let bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
|
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } else { 0.0 };
|
||||||
let rot = if e.working && !sleeping { (t * 14.0 + e.anim).sin() * 0.15 } else { 0.0 };
|
let mut rot = if e.working && !sleeping { (t * 14.0 + e.anim).sin() * 0.15 } else { 0.0 };
|
||||||
|
// prisoners: lying unconscious, carried above an imp, or shaking in the torture chamber
|
||||||
|
match e.captive {
|
||||||
|
1 => rot = std::f32::consts::FRAC_PI_2,
|
||||||
|
2 => {
|
||||||
|
rot = std::f32::consts::FRAC_PI_2;
|
||||||
|
bob = 0.4;
|
||||||
|
}
|
||||||
|
3 => rot = (t * 25.0 + e.anim).sin() * 0.08,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
let white = e.flash > 0.07;
|
let white = e.flash > 0.07;
|
||||||
let src = Gfx::sprite_rect(e.kind, white);
|
let src = Gfx::sprite_rect(e.kind, white);
|
||||||
let (mut u0, v0) = (src.x / sw, src.y / sh);
|
let (mut u0, v0) = (src.x / sw, src.y / sh);
|
||||||
@@ -954,8 +964,14 @@ impl R3d {
|
|||||||
if e.face < 0.0 {
|
if e.face < 0.0 {
|
||||||
std::mem::swap(&mut u0, &mut u1);
|
std::mem::swap(&mut u0, &mut u1);
|
||||||
}
|
}
|
||||||
let a = amb(e.x, e.y) * if sleeping { 0.6 } else { 1.0 };
|
let a = amb(e.x, e.y) * if sleeping || e.captive == 1 { 0.6 } else { 1.0 };
|
||||||
let c = if white { [255, 255, 255, 255] } else { col(Vec3::splat(a), 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 {
|
||||||
|
col(Vec3::splat(a), 1.0)
|
||||||
|
};
|
||||||
// pulled towards the camera: unchanged on screen (orthographic), but walls behind
|
// pulled towards the camera: unchanged on screen (orthographic), but walls behind
|
||||||
// the unit no longer cut into the sprite
|
// the unit no longer cut into the sprite
|
||||||
let base = vec3(e.x, bob, e.y) + toward * 0.45;
|
let base = vec3(e.x, bob, e.y) + toward * 0.45;
|
||||||
@@ -985,7 +1001,7 @@ impl R3d {
|
|||||||
if !e.alive || e.held {
|
if !e.alive || e.held {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let big = matches!(e.kind, EK::Ogre | EK::Paladin);
|
let big = matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer);
|
||||||
let head = Cam::project(m, vec3(e.x, if big { 0.9 } else { 0.72 }, e.y));
|
let head = Cam::project(m, vec3(e.x, if big { 0.9 } else { 0.72 }, e.y));
|
||||||
let w = (px_per_tile * 0.6).clamp(12.0, 40.0);
|
let w = (px_per_tile * 0.6).clamp(12.0, 40.0);
|
||||||
if e.hp < e.max_hp || e.hero {
|
if e.hp < e.max_hp || e.hero {
|
||||||
@@ -1206,9 +1222,11 @@ fn draw_shadows(g: &Game) {
|
|||||||
if !e.alive || e.held {
|
if !e.alive || e.held {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let r = if matches!(e.kind, EK::Ogre | EK::Paladin) { 0.32 } else { 0.24 };
|
let r = if matches!(e.kind, EK::Ogre | EK::Paladin | EK::Jailer) { 0.32 } else { 0.24 };
|
||||||
disc(vec3(e.x, 0.012, e.y), r, Color::new(0.0, 0.0, 0.0, 0.4), 12);
|
disc(vec3(e.x, 0.012, e.y), r, Color::new(0.0, 0.0, 0.0, 0.4), 12);
|
||||||
let rc = if e.guard {
|
let rc = if e.captive != 0 {
|
||||||
|
Color::new(0.6, 0.6, 0.6, 0.6)
|
||||||
|
} else if e.guard {
|
||||||
Color::new(1.0, 0.85, 0.3, 0.9)
|
Color::new(1.0, 0.85, 0.3, 0.9)
|
||||||
} else if e.hero {
|
} else if e.hero {
|
||||||
Color::new(0.5, 0.8, 1.0, 0.8)
|
Color::new(0.5, 0.8, 1.0, 0.8)
|
||||||
|
|||||||
@@ -607,6 +607,10 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
|
|||||||
let s = st(e.kind);
|
let s = st(e.kind);
|
||||||
out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), if e.hero { BLUE } else { FRAME_HI }));
|
out.push((trf("info.level", &[tr(s.name), e.level.to_string()]), if e.hero { BLUE } else { FRAME_HI }));
|
||||||
out.push((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));
|
out.push((trf("info.hp", &[(e.hp.max(0.0) as i32).to_string(), (e.max_hp as i32).to_string()]), GREEN));
|
||||||
|
if e.hero && e.captive != 0 {
|
||||||
|
out.push((tr(["", "info.unconscious", "info.carried", "info.tortured"][e.captive.min(3) as usize]), RED));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
out.push((tr(job_name(e)), TXT));
|
out.push((tr(job_name(e)), TXT));
|
||||||
if e.hero {
|
if e.hero {
|
||||||
out.push((tr(if e.guard { "info.guard" } else { "info.intruder" }), BLUE));
|
out.push((tr(if e.guard { "info.guard" } else { "info.intruder" }), BLUE));
|
||||||
@@ -678,6 +682,10 @@ pub fn hover_info(g: &Game, ent: Option<usize>, tile: Option<(i32, i32)>) -> Vec
|
|||||||
match k {
|
match k {
|
||||||
Kind::Farm => out.push((trf("farm.food", &[(t.food as i32).to_string()]), GREEN)),
|
Kind::Farm => out.push((trf("farm.food", &[(t.food as i32).to_string()]), GREEN)),
|
||||||
Kind::Lair => out.push((tr(if t.bed != 0 { "lair.taken" } else { "lair.free" }), DIM)),
|
Kind::Lair => out.push((tr(if t.bed != 0 { "lair.taken" } else { "lair.free" }), DIM)),
|
||||||
|
Kind::Torture => {
|
||||||
|
let used = g.ents.iter().filter(|e| e.alive && e.hero && e.captive != 0).count();
|
||||||
|
out.push((trf("info.prison", &[used.to_string(), (g.counts[5] / 2).to_string()]), DIM));
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user