Traps and doors made in a new forge room, installed by imps; new creature: the cogling
- forge room: creatures work there (coglings fastest), the work goes into the oldest planned trap or door; finished pieces wait as crates, imps carry and install them - traps: spike pit, poison vent (cloud), fire jet, stone roller, alarm bell (calls creatures), rune mine (one big blast), frost glow (slows around it) - doors: wood, iron, magic (hurls attackers back); own creatures pass, enemies smash them on their way; clicking a door with a door tool locks it for everybody - new 'Traps' category in the toolbar, forge in the rooms; texts EN/DE, bestiary entries; atlas grown for new textures and icons - tests: forge to installed trap, spikes hurt, doors lock and get smashed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1696297ac3
commit
4f20223c93
13 files changed
+1267
-24
No files matched your search
+89
-7
@@ -107,6 +107,7 @@ pub enum EK {
|
||||
Gargoyle, // aerie + torture chamber: stone, tough
|
||||
Owl, // aerie + library: arcane bolts from afar
|
||||
Wyrm, // big aerie + treasury: rare, very strong, fire breath
|
||||
Cogling, // forge: a sooty little tinkerer, builds traps and doors fastest
|
||||
}
|
||||
|
||||
pub struct St {
|
||||
@@ -143,6 +144,7 @@ pub fn st(k: EK) -> St {
|
||||
EK::Gargoyle => s("unit.gargoyle", 260.0, 15.0, 1.0, 2.2, 1.2, 120, 0),
|
||||
EK::Owl => s("unit.owl", 70.0, 12.0, 4.5, 2.8, 1.3, 100, 0),
|
||||
EK::Wyrm => s("unit.wyrm", 650.0, 30.0, 3.0, 2.5, 1.4, 380, 0),
|
||||
EK::Cogling => s("unit.cogling", 90.0, 9.0, 1.0, 2.4, 0.9, 70, 0),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +181,8 @@ pub enum Job {
|
||||
MakeBed(usize), // new creature: sets up its bed
|
||||
FetchWage(usize), // payday: walks to the treasury (or the core) for the wage
|
||||
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
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
@@ -405,7 +409,7 @@ pub struct Game {
|
||||
pub paused: bool,
|
||||
pub hand: Vec<u32>,
|
||||
#[serde(skip)]
|
||||
pub counts: [usize; 8], // treasury, lair, farm, training hall, library, torture chamber, temple, aerie (recounted)
|
||||
pub counts: [usize; N_ROOMS], // room tiles per room kind (ROOM_KINDS order, recounted)
|
||||
pub gate_hp: f32,
|
||||
pub gate_hit_t: f32,
|
||||
pub gate_stage: u32, // defender thresholds triggered (0..3)
|
||||
@@ -452,6 +456,14 @@ pub struct Game {
|
||||
pub stuck: Vec<crate::particles::Part>, // arrows stuck in walls and floors, scorch marks
|
||||
#[serde(skip, default = "crate::particles::vrng")]
|
||||
pub vrng: Rng, // random numbers for the particles only
|
||||
#[serde(default)]
|
||||
pub traps: Vec<crate::traps::Trap>, // traps and doors (ordered, in the forge, carried, armed)
|
||||
#[serde(default)]
|
||||
pub rollers: Vec<crate::traps::Roller>,
|
||||
#[serde(default)]
|
||||
pub clouds: Vec<crate::traps::Cloud>,
|
||||
#[serde(skip)]
|
||||
pub forge_work: f32, // work done in the forge this frame
|
||||
#[serde(skip)]
|
||||
pub demo: Option<crate::demo::Demo>, // the endless bridge battle behind the title menu
|
||||
#[serde(default)]
|
||||
@@ -492,9 +504,11 @@ impl Torch {
|
||||
}
|
||||
}
|
||||
|
||||
pub const ROOM_KINDS: [Kind; 8] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Torture, Kind::Temple, Kind::Aerie];
|
||||
pub const ROOM_COST: [i32; 8] = [60, 80, 100, 140, 160, 200, 180, 120];
|
||||
pub const ROOM_NAMES: [&str; 8] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library", "room.torture", "room.temple", "room.aerie"];
|
||||
pub const N_ROOMS: usize = 9;
|
||||
pub const ROOM_FORGE: usize = 8;
|
||||
pub const ROOM_KINDS: [Kind; N_ROOMS] = [Kind::Treasury, Kind::Lair, Kind::Farm, Kind::Training, Kind::Library, Kind::Torture, Kind::Temple, Kind::Aerie, Kind::Forge];
|
||||
pub const ROOM_COST: [i32; N_ROOMS] = [60, 80, 100, 140, 160, 200, 180, 120, 150];
|
||||
pub const ROOM_NAMES: [&str; N_ROOMS] = ["room.treasury", "room.lair", "room.farm", "room.training", "room.library", "room.torture", "room.temple", "room.aerie", "room.forge"];
|
||||
|
||||
// bridge cost per tile: over water, over lava
|
||||
pub const BRIDGE_COST: [i32; 2] = [30, 75];
|
||||
@@ -585,7 +599,7 @@ impl Game {
|
||||
state: 0,
|
||||
paused: false,
|
||||
hand: Vec::new(),
|
||||
counts: [0; 8],
|
||||
counts: [0; N_ROOMS],
|
||||
gate_hp: GATE_HP,
|
||||
gate_hit_t: -100.0,
|
||||
gate_stage: 0,
|
||||
@@ -613,6 +627,10 @@ impl Game {
|
||||
springs: 0,
|
||||
freed: 0,
|
||||
demo: None,
|
||||
traps: Vec::new(),
|
||||
rollers: Vec::new(),
|
||||
clouds: Vec::new(),
|
||||
forge_work: 0.0,
|
||||
parts: Vec::new(),
|
||||
stuck: Vec::new(),
|
||||
vrng: crate::particles::vrng(),
|
||||
@@ -754,7 +772,7 @@ impl Game {
|
||||
}
|
||||
|
||||
pub fn recount(&mut self) {
|
||||
let mut c = [0usize; 8];
|
||||
let mut c = [0usize; N_ROOMS];
|
||||
self.springs = self.map.t.iter().filter(|t| t.kind == Kind::ManaSpring && t.owner == Owner::Player).count();
|
||||
for t in &self.map.t {
|
||||
if t.owner == Owner::Player {
|
||||
@@ -1050,6 +1068,7 @@ impl Game {
|
||||
self.update_contested();
|
||||
self.update_units(dt);
|
||||
self.update_towers(dt);
|
||||
self.update_forge(dt);
|
||||
if !self.rivals.is_empty() {
|
||||
self.update_rivals(dt);
|
||||
}
|
||||
@@ -1260,6 +1279,9 @@ impl Game {
|
||||
if self.counts[6] >= 6 {
|
||||
opts.push((EK::Priestess, 2));
|
||||
}
|
||||
if self.counts[ROOM_FORGE] >= 6 {
|
||||
opts.push((EK::Cogling, 3));
|
||||
}
|
||||
// the Chainbreaker only visits big dungeons with experienced creatures and a rich treasury
|
||||
let room_tiles: usize = self.counts.iter().sum();
|
||||
let veteran = self.ents.iter().any(|e| e.alive && e.creature() && e.level >= 5);
|
||||
@@ -1687,6 +1709,18 @@ impl Game {
|
||||
}
|
||||
let next = e.path[0];
|
||||
let tile = self.map.t[next];
|
||||
if tile.kind == Kind::Door {
|
||||
if e.side() != 0 {
|
||||
// enemies smash doors on their way
|
||||
if self.bash_door(e, next, dt) {
|
||||
return true;
|
||||
}
|
||||
} else if tile.res & 1 == 1 && e.tile() != next {
|
||||
// locked: our own creatures have to find another way
|
||||
e.path.clear();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if !tile.walkable() && !(flies(e.kind) && tile.liquid()) {
|
||||
if (e.hero || e.rival()) && tile.diggable() {
|
||||
// heroes dig their way through
|
||||
@@ -2563,6 +2597,9 @@ impl Game {
|
||||
Job::Carry(id) => {
|
||||
self.carry_job(e, id, dt);
|
||||
}
|
||||
Job::Install(id) => {
|
||||
self.install_job(e, id, dt);
|
||||
}
|
||||
Job::Repair(id) => {
|
||||
let Some(k) = self.towers.iter().position(|t| t.id == id) else {
|
||||
e.job = Job::None;
|
||||
@@ -2731,6 +2768,16 @@ impl Game {
|
||||
if e.carry > 0 && self.start_deliver(e) {
|
||||
return;
|
||||
}
|
||||
// finished traps and doors are carried to their spots
|
||||
if let Some((id, p)) = self.crate_to_carry(cur) {
|
||||
if let Some(k) = self.traps.iter().position(|t| t.id == id) {
|
||||
self.traps[k].carrier = me;
|
||||
}
|
||||
e.path = p;
|
||||
e.job = Job::Install(id);
|
||||
e.timer = 0.0;
|
||||
return;
|
||||
}
|
||||
// damaged towers are repaired (free of charge) before more ground is claimed
|
||||
if let Some(p) = self.map.search(cur, self.map.walk_cost(), |i| self.damaged_tower_near(i).is_some(), true) {
|
||||
let end = p.last().copied().unwrap_or(cur);
|
||||
@@ -2992,6 +3039,22 @@ impl Game {
|
||||
e.job = Job::None;
|
||||
}
|
||||
}
|
||||
Job::Forge(t) => {
|
||||
if cur != t || self.map.t[t].kind != Kind::Forge || !self.forge_orders() {
|
||||
self.unreserve(me);
|
||||
e.job = Job::None;
|
||||
return;
|
||||
}
|
||||
e.working = true;
|
||||
let rate = if e.kind == EK::Cogling { 2.5 } else { 1.0 } * (1.0 + 0.1 * (e.level - 1) as f32);
|
||||
self.forge_work += rate * dt;
|
||||
e.xp += 2.5 * dt;
|
||||
e.timer -= dt;
|
||||
if e.timer <= 0.0 {
|
||||
self.unreserve(me);
|
||||
e.job = Job::None;
|
||||
}
|
||||
}
|
||||
Job::Study(t) => {
|
||||
if cur != t || self.map.t[t].kind != Kind::Library {
|
||||
self.unreserve(me);
|
||||
@@ -3281,6 +3344,20 @@ impl Game {
|
||||
}
|
||||
}
|
||||
}
|
||||
// the forge: orders waiting (coglings love it, others lend a hand now and then)
|
||||
if self.forge_orders() && self.counts[ROOM_FORGE] > 0 {
|
||||
let r = self.rng.f();
|
||||
if (e.kind == EK::Cogling && r < 0.9) || r < 0.25 {
|
||||
if let Some(p) = self.find_room_tile(cur, Kind::Forge, me, false) {
|
||||
let t = p.last().copied().unwrap_or(cur);
|
||||
self.map.t[t].reserved = me;
|
||||
e.path = p;
|
||||
e.job = Job::Forge(t);
|
||||
e.timer = 10.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) {
|
||||
@@ -3788,6 +3865,9 @@ impl Game {
|
||||
continue;
|
||||
}
|
||||
let i = idx(x, y);
|
||||
if self.remove_trap_at(i) {
|
||||
continue;
|
||||
}
|
||||
if let Some(k) = self.towers.iter().position(|t| t.tile == i && !t.hero) {
|
||||
refund += self.towers[k].invested() as f32 * 0.5;
|
||||
if self.towers[k].kind == TowerKind::Rune && !self.towers[k].rune_final {
|
||||
@@ -4620,6 +4700,8 @@ pub fn job_name(e: &Ent) -> &'static str {
|
||||
}
|
||||
Job::Eat(_) => "job.eat",
|
||||
Job::Train(_) => "job.train",
|
||||
Job::Forge(_) => "job.forge",
|
||||
Job::Install(_) => "job.install",
|
||||
Job::Study(_) => "job.study",
|
||||
Job::Attack(_) => "job.fight",
|
||||
Job::TearBridge(_) => "job.tear_bridge",
|
||||
@@ -5191,7 +5273,7 @@ mod tests {
|
||||
#[test]
|
||||
fn aerie_attracts_flyers() {
|
||||
let mut g = Game::new(3, 64, 64);
|
||||
g.counts = [16, 0, 9, 0, 6, 4, 0, 16];
|
||||
g.counts = [16, 0, 9, 0, 6, 4, 0, 16, 0];
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for _ in 0..200 {
|
||||
g.try_portal_for_test();
|
||||
|
||||
Reference in new issue
Block a user