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
+454
-13
@@ -90,6 +90,8 @@ pub enum EK {
|
||||
Priest,
|
||||
Dwarf,
|
||||
Paladin,
|
||||
Jailer, // attracted by the torture chamber
|
||||
Priestess, // attracted by the temple
|
||||
}
|
||||
|
||||
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::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::Knight => s("unit.knight", 140.0, 11.0, 1.0, 1.9, 1.1, 0, 120),
|
||||
EK::Archer => s("unit.archer", 75.0, 9.0, 4.5, 2.1, 1.3, 0, 100),
|
||||
EK::Priest => s("unit.priest", 80.0, 5.0, 3.5, 1.9, 1.5, 0, 120),
|
||||
EK::Dwarf => s("unit.dwarf", 110.0, 9.0, 1.0, 2.0, 1.0, 0, 100),
|
||||
EK::Paladin => s("unit.paladin", 700.0, 26.0, 1.0, 1.7, 1.2, 0, 1000),
|
||||
// heroes: the wage applies when they defected to the player
|
||||
EK::Knight => s("unit.knight", 140.0, 11.0, 1.0, 1.9, 1.1, 70, 120),
|
||||
EK::Archer => s("unit.archer", 75.0, 9.0, 4.5, 2.1, 1.3, 60, 100),
|
||||
EK::Priest => s("unit.priest", 80.0, 5.0, 3.5, 1.9, 1.5, 60, 120),
|
||||
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
|
||||
Repair(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)]
|
||||
@@ -170,6 +178,14 @@ pub struct Ent {
|
||||
pub working: bool,
|
||||
#[serde(default)]
|
||||
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)
|
||||
#[serde(default)]
|
||||
pub home: Option<(f32, f32)>, // guard post
|
||||
@@ -206,6 +222,10 @@ impl Ent {
|
||||
moving: false,
|
||||
working: false,
|
||||
slow: 0.0,
|
||||
captive: 0,
|
||||
carrier: 0,
|
||||
torture_t: 0.0,
|
||||
skill_cd: 0.0,
|
||||
guard: false,
|
||||
home: None,
|
||||
}
|
||||
@@ -219,6 +239,10 @@ impl Ent {
|
||||
pub fn creature(&self) -> bool {
|
||||
!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)]
|
||||
@@ -484,7 +508,7 @@ impl Game {
|
||||
// gate defenders alive (not the treasure guards)
|
||||
pub fn gate_defenders_alive(&self) -> bool {
|
||||
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)]
|
||||
@@ -641,6 +665,12 @@ impl Game {
|
||||
if self.counts[3] >= 9 {
|
||||
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 mut r = self.rng.next() % total;
|
||||
let mut kind = EK::Kobold;
|
||||
@@ -822,6 +852,7 @@ impl Game {
|
||||
e.anim += dt;
|
||||
e.flash = (e.flash - dt).max(0.0);
|
||||
e.slow = (e.slow - dt).max(0.0);
|
||||
e.skill_cd -= dt;
|
||||
e.cd -= dt;
|
||||
e.think -= dt;
|
||||
e.repath -= dt;
|
||||
@@ -842,7 +873,9 @@ impl Game {
|
||||
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);
|
||||
} else if e.kind == EK::Worker {
|
||||
self.worker_ai(e, dt);
|
||||
@@ -966,7 +999,7 @@ impl Game {
|
||||
let mut best = None;
|
||||
let mut bd = radius * radius;
|
||||
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;
|
||||
}
|
||||
let d = (o.x - e.x).powi(2) + (o.y - e.y).powi(2);
|
||||
@@ -987,6 +1020,21 @@ impl Game {
|
||||
if d > 12.0 {
|
||||
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)) {
|
||||
e.path.clear();
|
||||
e.face = sgn(tx - e.x);
|
||||
@@ -1025,6 +1073,7 @@ impl Game {
|
||||
let col = match e.kind {
|
||||
EK::Mage => 0xffff44cc,
|
||||
EK::Priest => 0xff88ffff,
|
||||
EK::Priestess => 0xff3080ff,
|
||||
_ => 0xff70b0d0,
|
||||
};
|
||||
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);
|
||||
}
|
||||
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 t = &mut self.ents[j];
|
||||
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) {
|
||||
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];
|
||||
e.alive = false;
|
||||
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.fx(3, x, y, 0.0, 0.0, 0.7, 0xff505070);
|
||||
let s = st(kind);
|
||||
@@ -1204,6 +1313,9 @@ impl Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
Job::Carry(id) => {
|
||||
self.carry_job(e, id, dt);
|
||||
}
|
||||
Job::Repair(id) => {
|
||||
let Some(k) = self.towers.iter().position(|t| t.id == id) else {
|
||||
e.job = Job::None;
|
||||
@@ -1309,6 +1421,21 @@ impl Game {
|
||||
if e.carry >= CARRY_MAX && self.start_deliver(e) {
|
||||
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) {
|
||||
let end = p.last().copied().unwrap_or(cur);
|
||||
if let Some(t) = self.dig_neighbor(end, me) {
|
||||
@@ -1376,6 +1503,31 @@ impl Game {
|
||||
if e.hunger > 1.8 {
|
||||
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 {
|
||||
e.think = 0.4 + self.rng.f() * 0.3;
|
||||
if !matches!(e.job, Job::Attack(_) | Job::Leave) {
|
||||
@@ -1462,6 +1614,36 @@ impl Game {
|
||||
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 => {
|
||||
e.timer -= dt;
|
||||
if e.timer <= 0.0 {
|
||||
@@ -1661,6 +1843,26 @@ impl Game {
|
||||
self.creature_choose(e);
|
||||
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();
|
||||
if (e.kind == EK::Mage && r < 0.8) || r < 0.2 {
|
||||
if let Some(p) = self.find_room_tile(cur, Kind::Library, me, false) {
|
||||
@@ -1685,6 +1887,176 @@ impl Game {
|
||||
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
|
||||
|
||||
fn hero_ai(&mut self, e: &mut Ent, dt: f32) {
|
||||
@@ -1985,7 +2357,7 @@ impl Game {
|
||||
let mut best = None;
|
||||
let mut bd = r * r;
|
||||
for (j, e) in self.ents.iter().enumerate() {
|
||||
if !e.alive || e.held || !e.hero {
|
||||
if !e.alive || e.held || !e.active_hero() {
|
||||
continue;
|
||||
}
|
||||
let d = (e.x - px).powi(2) + (e.y - py).powi(2);
|
||||
@@ -2032,7 +2404,7 @@ impl Game {
|
||||
self.sfx(Sfx::Shoot, px, py);
|
||||
let mut hits = vec![(j, dmg)];
|
||||
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;
|
||||
}
|
||||
let rel = (e.x - px, e.y - py);
|
||||
@@ -2056,7 +2428,7 @@ impl Game {
|
||||
self.sfx(Sfx::Cannon, px, py);
|
||||
let mut hits = Vec::new();
|
||||
for (o, e) in self.ents.iter().enumerate() {
|
||||
if !e.alive || e.held || !e.hero {
|
||||
if !e.alive || e.held || !e.active_hero() {
|
||||
continue;
|
||||
}
|
||||
let d = ((e.x - ex).powi(2) + (e.y - ey).powi(2)).sqrt();
|
||||
@@ -2090,7 +2462,7 @@ impl Game {
|
||||
.ents
|
||||
.iter()
|
||||
.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| {
|
||||
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);
|
||||
@@ -2163,6 +2535,12 @@ impl Game {
|
||||
let id = self.ents[j].id;
|
||||
let me = id + 1;
|
||||
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];
|
||||
e.held = true;
|
||||
e.job = Job::None;
|
||||
@@ -2211,7 +2589,7 @@ impl Game {
|
||||
self.fx(1, wx, wy, 0.0, 0.0, 0.5, 0xffffeeaa);
|
||||
let mut kills = Vec::new();
|
||||
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.flash = 0.2;
|
||||
if e.hp <= 0.0 {
|
||||
@@ -2361,6 +2739,9 @@ pub fn job_name(e: &Ent) -> &'static str {
|
||||
Job::Leave => "job.leave",
|
||||
Job::Reinforce(_) => "job.reinforce",
|
||||
Job::Repair(_) => "job.repair",
|
||||
Job::Carry(_) => "job.carry",
|
||||
Job::Pray(_) => "job.pray",
|
||||
Job::HealCore => "job.heal_core",
|
||||
Job::AttackTower(_) => "job.attack_tower",
|
||||
Job::Rally => {
|
||||
if e.working {
|
||||
@@ -2515,4 +2896,64 @@ mod tests {
|
||||
println!("tower after attack: {:?}", t.map(|t| t.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..",
|
||||
"..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 => [
|
||||
"...kkkk..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 {
|
||||
SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ impl MusicDirector {
|
||||
use audio::Track;
|
||||
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 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 {
|
||||
Track::Title
|
||||
} else if g.state == 1 {
|
||||
|
||||
+28
-10
@@ -935,18 +935,28 @@ impl R3d {
|
||||
}
|
||||
};
|
||||
let mut mb = MB::new();
|
||||
let sw = 120.0;
|
||||
let sh = 24.0;
|
||||
let sw = SPRITE_TEX_W;
|
||||
let sh = SPRITE_TEX_H;
|
||||
for e in &g.ents {
|
||||
if !e.alive || e.held {
|
||||
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 h = w;
|
||||
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 rot = if e.working && !sleeping { (t * 14.0 + e.anim).sin() * 0.15 } else { 0.0 };
|
||||
let mut bob = if e.moving { (e.anim * 12.0).sin().abs() * 0.06 } 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 src = Gfx::sprite_rect(e.kind, white);
|
||||
let (mut u0, v0) = (src.x / sw, src.y / sh);
|
||||
@@ -954,8 +964,14 @@ impl R3d {
|
||||
if e.face < 0.0 {
|
||||
std::mem::swap(&mut u0, &mut u1);
|
||||
}
|
||||
let a = amb(e.x, e.y) * if sleeping { 0.6 } else { 1.0 };
|
||||
let c = if white { [255, 255, 255, 255] } else { col(Vec3::splat(a), 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 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
|
||||
// the unit no longer cut into the sprite
|
||||
let base = vec3(e.x, bob, e.y) + toward * 0.45;
|
||||
@@ -985,7 +1001,7 @@ impl R3d {
|
||||
if !e.alive || e.held {
|
||||
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 w = (px_per_tile * 0.6).clamp(12.0, 40.0);
|
||||
if e.hp < e.max_hp || e.hero {
|
||||
@@ -1206,9 +1222,11 @@ fn draw_shadows(g: &Game) {
|
||||
if !e.alive || e.held {
|
||||
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);
|
||||
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)
|
||||
} else if e.hero {
|
||||
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);
|
||||
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));
|
||||
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));
|
||||
if e.hero {
|
||||
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 {
|
||||
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::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