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

+139 -3
View File
@@ -6,7 +6,7 @@ use macroquad::prelude::*;
pub const CELL: f32 = 16.0;
const ATLAS_COLS: u32 = 16;
const ATLAS_ROWS: u32 = 9;
pub const ATLAS_ROWS: u32 = 14;
// atlas indices (4 variants each)
pub const T_ROCK: u32 = 0;
@@ -70,6 +70,8 @@ pub const I_STEEL: u32 = 134;
pub const T_RIVAL1: u32 = 135; // 4 variants: floor of the green master
pub const T_RIVAL2: u32 = 139; // 4 variants: floor of the blue master
pub const I_RUNE: u32 = 143; // rune forge tower
pub const T_FORGE: u32 = 144; // 4 variants: sooty flagstones with glowing cracks
pub const I_TRAPS: u32 = 148; // 10 icons in TRAP_KINDS order
pub struct Gfx {
pub atlas: Texture2D,
@@ -749,6 +751,118 @@ fn gen_atlas() -> Image {
let h = hash(cu, cv, seed) % 3;
Some(if e { lo } else if h == 0 || (u % 4 == 1 && v % 4 == 1) { hi } else if d > 0.65 { lo } else { mid })
};
// forge: sooty dark flagstones, iron filings and glowing cracks
for v in 0..4u32 {
cv.cell(T_FORGE + v, |x, y| {
let base = stones(x, y, v + 300, (52, 46, 46), (22, 18, 18), 8);
if (x * 7 + y * 3 + v as i32 * 11) % 23 == 0 || ((x + y * 2 + v as i32) % 19 == 0 && y % 8 != 0) {
Some(c(255, 120, 40))
} else if hash(x, y, v + 301) % 13 == 0 {
Some(c(120, 120, 130))
} else {
Some(base)
}
});
}
// trap and door icons
let icons: [fn(i32, i32) -> Option<Color>; 10] = [
// spikes
|x, y| {
if y >= 12 {
Some(c(90, 90, 100))
} else if y >= 3 && (x % 4 - 2).abs() <= (y - 3) / 4 && (2..14).contains(&x) {
Some(if x % 4 == 2 { c(230, 230, 240) } else { c(160, 160, 175) })
} else {
None
}
},
// poison cloud
|x, y| {
let d = |cx: i32, cy: i32, r: i32| (x - cx).pow(2) + (y - cy).pow(2) <= r * r;
if d(5, 9, 4) || d(10, 8, 4) || d(8, 5, 3) {
Some(if (x + y) % 5 == 0 { c(200, 255, 140) } else { c(110, 200, 70) })
} else {
None
}
},
// fire jet
|x, y| {
let w = (y - 1).max(0) / 3;
if (x - 8).abs() <= w && y <= 14 && y >= 1 {
Some(if (x - 8).abs() <= w / 2 && y > 6 { c(255, 230, 120) } else { c(255, 120, 30) })
} else {
None
}
},
// stone roller
|x, y| {
let d = (x - 8).pow(2) + (y - 8).pow(2);
if d <= 36 {
Some(if (x + y) % 6 == 0 || d > 28 { c(80, 76, 72) } else { c(140, 132, 124) })
} else if y == 14 && x < 15 {
Some(c(90, 70, 50))
} else {
None
}
},
// alarm bell
|x, y| {
let w = 2 + y / 3;
if (2..=11).contains(&y) && (x - 8).abs() <= w.min(6) {
Some(if x < 8 { c(255, 220, 90) } else { c(200, 160, 50) })
} else if (12..=13).contains(&y) && (x - 8).abs() <= 1 {
Some(c(140, 100, 40))
} else {
None
}
},
// rune mine
|x, y| {
let d = (((x - 8).pow(2) + (y - 8).pow(2)) as f32).sqrt();
if (5.0..6.5).contains(&d) || ((x - 8).abs() <= 1 && (3..=12).contains(&y)) || (y == 8 && (5..=11).contains(&x)) {
Some(c(90, 200, 255))
} else {
None
}
},
// frost glow
|x, y| {
let (dx, dy) = (x - 8, y - 8);
if (dx == 0 || dy == 0 || dx == dy || dx == -dy) && dx.abs() <= 6 && dy.abs() <= 6 {
Some(if dx.abs() + dy.abs() <= 2 { c(255, 255, 255) } else { c(150, 220, 255) })
} else {
None
}
},
// wooden door
|x, y| {
if (3..=12).contains(&x) && y >= 2 {
Some(if x % 3 == 0 { c(80, 50, 28) } else if y == 5 || y == 11 { c(70, 70, 76) } else { c(130, 86, 48) })
} else {
None
}
},
// iron door
|x, y| {
if (3..=12).contains(&x) && y >= 2 {
Some(if (x == 4 || x == 11) && y % 3 == 0 { c(200, 200, 210) } else if y == 7 { c(60, 60, 66) } else { c(110, 112, 122) })
} else {
None
}
},
// magic door
|x, y| {
if (3..=12).contains(&x) && y >= 2 {
let rune = (x == 8 && (5..=11).contains(&y)) || (y == 8 && (6..=10).contains(&x));
Some(if rune { c(240, 160, 255) } else if x == 3 || x == 12 || y == 2 { c(70, 40, 100) } else { c(110, 60, 160) })
} else {
None
}
},
];
for (k, f) in icons.iter().enumerate() {
cv.cell(I_TRAPS + k as u32, |x, y| f(x, y));
}
// aerie: straw and twigs on stone, a few feathers
for v in 0..4u32 {
cv.cell(T_AERIE + v, |x, y| {
@@ -1005,6 +1119,19 @@ fn sprite_rows(k: EK) -> [&'static str; 10] {
".kubbbbuk.",
"..ky..yk..",
],
// cogling: a sooty little tinkerer with brass goggles and a hammer
EK::Cogling => [
"........SS",
"...kkkk.SS",
"..kOOOOk.b",
".kyekkeykb",
"..kOooOk.b",
"...kCCk..b",
"..kOCCOkbb",
"..kOCCOk..",
"...kO.Ok..",
"...kk.kk..",
],
EK::Wyrm => [
"R.......kw",
"RR....krrk",
@@ -1074,7 +1201,7 @@ fn pal(ch: char) -> Option<Color> {
})
}
pub const SPRITE_KINDS: [EK; 17] = [
pub const SPRITE_KINDS: [EK; 18] = [
EK::Worker,
EK::Kobold,
EK::Spider,
@@ -1092,9 +1219,10 @@ pub const SPRITE_KINDS: [EK; 17] = [
EK::Gargoyle,
EK::Owl,
EK::Wyrm,
EK::Cogling,
];
// width of the sprite texture in pixels
pub const SPRITE_TEX_W: f32 = (17 * 12) as f32;
pub const SPRITE_TEX_W: f32 = (SPRITE_KINDS.len() * 12) as f32;
pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * SPR_CELL) as f32;
pub fn sprite_index(k: EK) -> u32 {
@@ -1504,6 +1632,14 @@ impl Gfx {
Kind::Steel => T_STEEL + v,
Kind::ManaSpring => T_PORTALFLOOR + v,
Kind::RuneChest => T_FLOOR + v,
Kind::Forge => T_FORGE + v,
Kind::Door => {
if claimed {
T_CLAIM + v
} else {
T_FLOOR + v
}
}
}
}