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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-04 06:55:37 +01:00
1 parent 1696297ac3
commit 4f20223c93
13 files changed
+1267 -24

No files matched your search

+172 -1
View File
@@ -14,7 +14,7 @@ pub const WALL_H: f32 = 0.95;
const CHUNK: i32 = 16;
pub const MAX_LIGHTS: usize = 24;
const ATLAS_W: f32 = 256.0;
const ATLAS_H: f32 = 144.0;
const ATLAS_H: f32 = (crate::gfx::ATLAS_ROWS * 16) as f32;
const DISP_AMP: f32 = 0.11; // wall surface irregularity
// ------------------------------------------------------------------ camera
@@ -1027,6 +1027,7 @@ impl R3d {
draw_towers_solid(g);
crate::props::draw_props_solid(g, cam, t);
draw_parts_solid(g);
draw_traps_solid(g, t);
// ---- unlit glow / overlays
gl_use_material(&self.glow);
@@ -1063,6 +1064,7 @@ impl R3d {
for f in &g.fx {
draw_fx(f, t);
}
draw_traps_glow(g, t);
draw_parts_glow(g);
gl_use_default_material();
crate::ui::begin_ui();
@@ -2119,6 +2121,173 @@ fn draw_projectile(f: &Fx, p: f32, t: f32) {
}
}
// a crate with a finished trap or door (in the forge or carried by an imp)
fn trap_crate(at: Vec3) {
draw_cube(at + vec3(0.0, 0.14, 0.0), vec3(0.34, 0.28, 0.34), None, Color::from_rgba(120, 82, 46, 255));
for dy in [0.04f32, 0.24] {
draw_cube(at + vec3(0.0, dy, 0.0), vec3(0.36, 0.035, 0.36), None, Color::from_rgba(70, 70, 76, 255));
}
draw_cube(at + vec3(0.0, 0.29, 0.0), vec3(0.1, 0.02, 0.1), None, Color::from_rgba(200, 60, 40, 255));
}
// traps and doors: the installed pieces, crates in the forge and on the way
fn draw_traps_solid(g: &Game, t: f32) {
use crate::traps::*;
for tr in &g.traps {
let (tx, tz) = xy(tr.tile);
let c = vec3(tx as f32 + 0.5, 0.0, tz as f32 + 0.5);
match tr.stage {
CRATED => {
let (x, z) = xy(tr.crate_tile);
let k = (tr.id % 3) as f32 * 0.2 - 0.2;
trap_crate(vec3(x as f32 + 0.5 + k, 0.0, z as f32 + 0.5 - k));
}
CARRIED => {
if let Some(e) = g.ents.iter().find(|e| e.id + 1 == tr.carrier) {
trap_crate(vec3(e.x, 0.42, e.y));
}
}
ARMED => {
let iron = Color::from_rgba(70, 70, 78, 255);
let fresh = (1.0 - tr.fired / 0.6).clamp(0.0, 1.0);
match tr.kind {
TrapKind::Spikes => {
draw_cube(c + vec3(0.0, 0.02, 0.0), vec3(0.8, 0.04, 0.8), None, iron);
let up = 0.06 + 0.3 * fresh;
for q in 0..9 {
let (dx, dz) = ((q % 3) as f32 * 0.24 - 0.24, (q / 3) as f32 * 0.24 - 0.24);
line3(c + vec3(dx, 0.03, dz), c + vec3(dx, 0.03 + up, dz), 0.05, Color::from_rgba(200, 200, 212, 255));
}
}
TrapKind::Poison => {
draw_cube(c + vec3(0.0, 0.03, 0.0), vec3(0.6, 0.06, 0.6), None, iron);
for q in 0..3 {
draw_cube(c + vec3(q as f32 * 0.16 - 0.16, 0.065, 0.0), vec3(0.06, 0.02, 0.5), None, Color::from_rgba(30, 40, 26, 255));
}
}
TrapKind::Fire => {
// a demon head on a pedestal, its mouth towards the last target
draw_cube(c + vec3(0.0, 0.15, 0.0), vec3(0.36, 0.3, 0.36), None, Color::from_rgba(50, 40, 44, 255));
let (ax, az) = (tr.aim.cos(), tr.aim.sin());
draw_cube(c + vec3(0.0, 0.4, 0.0), vec3(0.3, 0.22, 0.3), None, Color::from_rgba(70, 56, 52, 255));
draw_cube(c + vec3(ax * 0.16, 0.38, az * 0.16), vec3(0.14, 0.1, 0.14), None, Color::from_rgba(30, 20, 20, 255));
for sgn in [-1.0f32, 1.0] {
line3(c + vec3(-az * 0.12 * sgn, 0.5, ax * 0.12 * sgn), c + vec3(-az * 0.2 * sgn - ax * 0.08, 0.68, ax * 0.2 * sgn - az * 0.08), 0.04, Color::from_rgba(214, 204, 178, 255));
}
}
TrapKind::Roller => {
// the boulder waits in its niche (until it rolls)
if tr.fired > 18.0 || tr.cd <= 0.0 {
draw_sphere(c + vec3(0.0, 0.3, 0.0), 0.3, None, Color::from_rgba(120, 112, 104, 255));
}
draw_cube(c + vec3(0.0, 0.03, 0.0), vec3(0.8, 0.06, 0.8), None, Color::from_rgba(60, 56, 52, 255));
}
TrapKind::Alarm => {
draw_cube(c + vec3(0.0, 0.35, 0.0), vec3(0.06, 0.7, 0.06), None, Color::from_rgba(90, 64, 40, 255));
draw_cube(c + vec3(0.0, 0.7, 0.0), vec3(0.34, 0.05, 0.06), None, Color::from_rgba(90, 64, 40, 255));
let swing = (t * 18.0).sin() * 0.06 * fresh.max(((tr.fired < 3.0) as u8) as f32);
draw_cube(c + vec3(swing, 0.58, 0.0), vec3(0.18, 0.18, 0.18), None, Color::from_rgba(220, 180, 70, 255));
}
TrapKind::Mine => {
draw_cube(c + vec3(0.0, 0.015, 0.0), vec3(0.4, 0.03, 0.4), None, Color::from_rgba(50, 54, 66, 255));
}
TrapKind::Frost => {
draw_cube(c + vec3(0.0, 0.06, 0.0), vec3(0.4, 0.12, 0.4), None, Color::from_rgba(80, 90, 110, 255));
}
_ => {
// doors: posts, lintel and the leaf across the corridor
let across_x = g.map.get(tx - 1, tz).map_or(true, |t| t.solid()) || g.map.get(tx + 1, tz).map_or(true, |t| t.solid());
let (len, thin) = if across_x { (vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0)) } else { (vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0)) };
let size = |l: f32, h: f32, w: f32| len * l + Vec3::Y * h + thin * w;
let (leaf, trim) = match tr.kind {
TrapKind::WoodDoor => (Color::from_rgba(120, 80, 44, 255), Color::from_rgba(70, 68, 74, 255)),
TrapKind::IronDoor => (Color::from_rgba(96, 98, 108, 255), Color::from_rgba(150, 150, 160, 255)),
_ => (Color::from_rgba(70, 36, 100, 255), Color::from_rgba(30, 20, 40, 255)),
};
for sgn in [-1.0f32, 1.0] {
draw_cube(c + len * (0.44 * sgn) + Vec3::Y * 0.6, size(0.12, 1.2, 0.3), None, Color::from_rgba(40, 32, 36, 255));
}
draw_cube(c + Vec3::Y * 1.22, size(1.0, 0.14, 0.32), None, Color::from_rgba(40, 32, 36, 255));
let shake = if tr.fired < 0.2 { (t * 60.0).sin() * 0.02 } else { 0.0 };
draw_cube(c + Vec3::Y * 0.56 + thin * shake, size(0.76, 1.1, 0.1), None, leaf);
for y in [0.25f32, 0.85] {
draw_cube(c + Vec3::Y * y, size(0.78, 0.06, 0.12), None, trim);
}
if tr.locked {
draw_cube(c + Vec3::Y * 0.6, size(0.86, 0.08, 0.16), None, Color::from_rgba(60, 60, 66, 255));
draw_cube(c + Vec3::Y * 0.6 + thin * 0.09, vec3(0.1, 0.12, 0.1), None, Color::from_rgba(230, 190, 70, 255));
}
// damage: darker the more it is beaten
let _ = tr.hp / tr.kind.hp();
}
}
}
_ => {}
}
}
for r in &g.rollers {
let spin = r.t * 9.0;
let at = vec3(r.x, 0.3, r.y);
draw_sphere(at, 0.3, None, Color::from_rgba(120, 112, 104, 255));
draw_cube(at + vec3(spin.cos() * 0.22 * r.dy.abs(), spin.sin() * 0.22, spin.cos() * 0.22 * r.dx.abs()), Vec3::splat(0.1), None, Color::from_rgba(80, 74, 70, 255));
}
}
fn draw_traps_glow(g: &Game, t: f32) {
use crate::traps::*;
for tr in &g.traps {
let (tx, tz) = xy(tr.tile);
let c = vec3(tx as f32 + 0.5, 0.0, tz as f32 + 0.5);
if tr.stage == ORDERED || tr.stage == CRATED || tr.stage == CARRIED {
// the planned spot: a pulsing outline with a small progress bar
let a = 0.4 + 0.3 * (t * 3.0 + tr.id as f32).sin();
let col = Color::new(0.4, 0.85, 1.0, a);
let y = 0.05;
for (p0, p1) in [((-0.42f32, -0.42f32), (0.42f32, -0.42f32)), ((0.42, -0.42), (0.42, 0.42)), ((0.42, 0.42), (-0.42, 0.42)), ((-0.42, 0.42), (-0.42, -0.42))] {
line3(c + vec3(p0.0, y, p0.1), c + vec3(p1.0, y, p1.1), 0.04, col);
}
let f = if tr.stage == ORDERED { (tr.progress / tr.kind.work()).clamp(0.0, 1.0) } else { 1.0 };
line3(c + vec3(-0.35, y, 0.0), c + vec3(-0.35 + 0.7 * f, y, 0.0), 0.08, Color::new(1.0, 0.7, 0.3, 0.8));
continue;
}
if tr.stage != ARMED {
continue;
}
match tr.kind {
TrapKind::Poison => disc(c + vec3(0.0, 0.07, 0.0), 0.22, Color::new(0.4, 1.0, 0.3, 0.3 + 0.15 * (t * 2.0).sin()), 12),
TrapKind::Fire => {
let (ax, az) = (tr.aim.cos(), tr.aim.sin());
let f = 0.5 + 0.5 * (t * 6.0).sin();
draw_sphere(c + vec3(ax * 0.24, 0.38, az * 0.24), 0.05 + 0.03 * f, None, Color::new(1.0, 0.5, 0.1, 0.9));
}
TrapKind::Mine => {
let glow = 0.4 + 0.4 * (t * 2.5 + tr.id as f32).sin();
ring(c + vec3(0.0, 0.04, 0.0), 0.16, Color::new(0.35, 0.8, 1.0, glow));
line3(c + vec3(-0.1, 0.04, 0.0), c + vec3(0.1, 0.04, 0.0), 0.03, Color::new(0.35, 0.8, 1.0, glow));
line3(c + vec3(0.0, 0.04, -0.1), c + vec3(0.0, 0.04, 0.1), 0.03, Color::new(0.35, 0.8, 1.0, glow));
}
TrapKind::Frost => {
let p = 1.0 + 0.1 * (t * 2.0).sin();
draw_cube(c + vec3(0.0, 0.3, 0.0), vec3(0.12, 0.36 * p, 0.12), None, Color::new(0.75, 0.92, 1.0, 0.9));
draw_cube(c + vec3(0.1, 0.22, 0.05), vec3(0.07, 0.2, 0.07), None, Color::new(0.7, 0.9, 1.0, 0.85));
draw_cube(c + vec3(-0.08, 0.2, -0.06), vec3(0.07, 0.16, 0.07), None, Color::new(0.7, 0.9, 1.0, 0.85));
disc(c + vec3(0.0, 0.02, 0.0), 2.5, Color::new(0.6, 0.85, 1.0, 0.06), 24);
}
TrapKind::MagicDoor => {
let across_x = g.map.get(tx - 1, tz).map_or(true, |t| t.solid()) || g.map.get(tx + 1, tz).map_or(true, |t| t.solid());
let thin = if across_x { vec3(0.0, 0.0, 1.0) } else { vec3(1.0, 0.0, 0.0) };
let glow = 0.5 + 0.5 * (t * 2.0).sin();
for side in [-1.0f32, 1.0] {
let p = c + Vec3::Y * 0.6 + thin * (0.06 * side);
draw_cube(p, vec3(0.06, 0.4, 0.06), None, Color::new(0.9, 0.5, 1.0, 0.5 + 0.4 * glow));
draw_cube(p + Vec3::Y * 0.06, vec3(0.22, 0.06, 0.06), None, Color::new(0.9, 0.5, 1.0, 0.5 + 0.4 * glow));
}
}
_ => {}
}
}
}
// particles that glow (embers, sparks, smoke, flashes, shock waves)
fn draw_parts_glow(g: &Game) {
use crate::particles::*;
@@ -2361,6 +2530,8 @@ pub fn minimap_color(tile: &Tile) -> [u8; 4] {
Kind::Steel => [150, 160, 180, 255],
Kind::ManaSpring => [90, 230, 255, 255],
Kind::RuneChest => [80, 200, 255, 255],
Kind::Forge => [150, 70, 40, 255],
Kind::Door => [140, 90, 50, 255],
}
}