Imps set up torch stands in big rooms on their own ground

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 16:10:58 +01:00
1 parent 6c16bc7b18
commit c5378d0774
3 files changed
+122 -12

No files matched your search

+96 -3
View File
@@ -662,20 +662,43 @@ pub const STEP: f32 = 1.0 / 50.0; // 50 steps a second
// one frame never runs away into the future (a lagging tab catches up at most this much) // one frame never runs away into the future (a lagging tab catches up at most this much)
const MAX_STEPS_PER_FRAME: usize = 12; const MAX_STEPS_PER_FRAME: usize = 12;
// torch on a wall face: wall tile + direction (index into DIR4) towards the open side // torch on a wall face: wall tile + direction (index into DIR4) towards the open side;
// or a torch on a little stand in the middle of a floor tile (stand: wall = that tile)
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)] #[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Torch { pub struct Torch {
pub wall: usize, pub wall: usize,
pub dir: usize, pub dir: usize,
#[serde(default)]
pub stand: bool,
} }
// rooms where imps may set up a torch stand (no beds, hoards, mushrooms or crates in the way)
pub fn stand_ground(k: Kind) -> bool {
matches!(k, Kind::Floor | Kind::Training | Kind::Library | Kind::Torture | Kind::Temple | Kind::Prison | Kind::Graveyard | Kind::GuardPost | Kind::Arena | Kind::Altar)
}
// a torch stand goes where no torch shines closer than this (tiles)
pub const STAND_GAP: f32 = 4.5;
impl Torch { impl Torch {
// world position of the flame (x, z) on the ground plane // world position of the flame (x, z) on the ground plane
pub fn pos(&self) -> (f32, f32) { pub fn pos(&self) -> (f32, f32) {
let (x, y) = xy(self.wall); let (x, y) = xy(self.wall);
if self.stand {
return (x as f32 + 0.5, y as f32 + 0.5);
}
let (dx, dy) = DIR4[self.dir]; let (dx, dy) = DIR4[self.dir];
(x as f32 + 0.5 + dx as f32 * 0.58, y as f32 + 0.5 + dy as f32 * 0.58) (x as f32 + 0.5 + dx as f32 * 0.58, y as f32 + 0.5 + dy as f32 * 0.58)
} }
// height of the bottom of the flame
pub fn flame_h(&self) -> f32 {
if self.stand {
0.62
} else {
0.72
}
}
} }
pub const N_ROOMS: usize = 14; pub const N_ROOMS: usize = 14;
@@ -890,7 +913,7 @@ impl Game {
pub fn place_all_torches(&mut self) { pub fn place_all_torches(&mut self) {
// torches on walls that are gone (rivers, fortresses) are removed // torches on walls that are gone (rivers, fortresses) are removed
let map = &self.map; let map = &self.map;
self.torches.retain(|t| map.t[t.wall].solid()); self.torches.retain(|t| if t.stand { stand_ground(map.t[t.wall].kind) } else { map.t[t.wall].solid() });
for i in 0..n() { for i in 0..n() {
if self.map.t[i].owner == self.me { if self.map.t[i].owner == self.me {
self.place_torches(i); self.place_torches(i);
@@ -998,7 +1021,7 @@ impl Game {
continue; continue;
} }
// the direction from the wall to the floor is the opposite one // the direction from the wall to the floor is the opposite one
let t = Torch { wall: idx(wx, wy), dir: (d + 2) % 4 }; let t = Torch { wall: idx(wx, wy), dir: (d + 2) % 4, stand: false };
let (px, pz) = t.pos(); let (px, pz) = t.pos();
let near = self.torches.iter().any(|o| { let near = self.torches.iter().any(|o| {
let (ox, oz) = o.pos(); let (ox, oz) = o.pos();
@@ -1008,6 +1031,26 @@ impl Game {
self.torches.push(t); self.torches.push(t);
} }
} }
// a big room the wall torches do not light: a torch on a stand in the middle of it,
// on the master's own ground only (never neutral or foreign ground) and not next to a wall
let tile = self.map.t[i];
let open_around = (-1..=1).all(|dy| (-1..=1).all(|dx| self.map.get(x + dx, y + dy).map_or(false, |t| !t.solid())));
if tile.owner == self.me && stand_ground(tile.kind) && open_around {
let (px, pz) = (x as f32 + 0.5, y as f32 + 0.5);
let near = self.torches.iter().any(|o| {
let (ox, oz) = o.pos();
(ox - px).powi(2) + (oz - pz).powi(2) < STAND_GAP * STAND_GAP
});
if !near {
self.torches.push(Torch { wall: i, dir: 0, stand: true });
}
}
}
// torch stands go when their ground is lost or becomes a room they do not fit in
fn tidy_torch_stands(&mut self) {
let map = &self.map;
self.torches.retain(|t| !t.stand || (stand_ground(map.t[t.wall].kind) && !matches!(map.t[t.wall].owner, Owner::None | Owner::Hero)));
} }
pub(crate) fn hint(&mut self, slot: usize, key: &'static str) { pub(crate) fn hint(&mut self, slot: usize, key: &'static str) {
@@ -1563,6 +1606,7 @@ impl Game {
// every two seconds: corners between reinforced walls are closed // every two seconds: corners between reinforced walls are closed
if (self.time / 2.0).floor() != ((self.time - dt) / 2.0).floor() { if (self.time / 2.0).floor() != ((self.time - dt) / 2.0).floor() {
self.reinforce_corners(); self.reinforce_corners();
self.tidy_torch_stands();
} }
self.update_forge(dt); self.update_forge(dt);
self.update_research(); self.update_research();
@@ -5940,6 +5984,55 @@ mod tests {
assert!(best <= 2.0); assert!(best <= 2.0);
} }
#[test]
fn imps_set_up_torch_stands_in_big_rooms_on_their_own_ground() {
let mut g = Game::new(5, 96, 96);
let (cx, cy) = g.map.core;
// a big hall of own ground east of the core, and as big a neutral cave south of it
let hall = (cx + 8, cy - 7, cx + 22, cy + 7);
let cave = (cx - 7, cy + 12, cx + 7, cy + 26);
for (x0, y0, x1, y1, own) in [(hall.0, hall.1, hall.2, hall.3, true), (cave.0, cave.1, cave.2, cave.3, false)] {
for y in y0..=y1 {
for x in x0..=x1 {
let t = &mut g.map.t[idx(x, y)];
t.kind = Kind::Floor;
t.owner = if own { g.me } else { Owner::None };
}
}
}
g.torches.clear();
g.place_all_torches();
let inside = |t: &Torch, r: (i32, i32, i32, i32)| {
let (x, y) = xy(t.wall);
x >= r.0 && x <= r.2 && y >= r.1 && y <= r.3
};
let stands: Vec<Torch> = g.torches.iter().copied().filter(|t| t.stand).collect();
assert!(stands.iter().any(|t| inside(t, hall)), "the big hall gets torch stands");
assert!(!stands.iter().any(|t| inside(t, cave)), "never on neutral ground");
for t in &stands {
assert_eq!(g.map.t[t.wall].owner, g.me, "only on own ground");
let (x, y) = xy(t.wall);
assert!((-1..=1).all(|dy| (-1..=1).all(|dx| !g.map.t[idx(x + dx, y + dy)].solid())), "not next to a wall");
}
// every tile of the hall's middle is within reach of some torch
for y in hall.1 + 2..=hall.3 - 2 {
for x in hall.0 + 2..=hall.2 - 2 {
let lit = g.torches.iter().any(|t| {
let (px, pz) = t.pos();
(px - x as f32 - 0.5).powi(2) + (pz - y as f32 - 0.5).powi(2) <= (STAND_GAP + 1.0).powi(2)
});
assert!(lit, "({}, {}) lies in the dark", x, y);
}
}
// ground lost: the stand goes
let s0 = stands.iter().find(|t| inside(t, hall)).unwrap().wall;
g.map.t[s0].owner = Owner::Hero;
for _ in 0..60 {
g.update(0.05);
}
assert!(!g.torches.iter().any(|t| t.stand && t.wall == s0), "a stand on lost ground is removed");
}
#[test] #[test]
fn flyers_take_every_way_across_rivers() { fn flyers_take_every_way_across_rivers() {
let (mut g, rx) = river_game(false); let (mut g, rx) = river_game(false);
+4 -4
View File
@@ -398,19 +398,19 @@ impl Game {
// a thin wisp of smoke above every known torch, now and then a spark // a thin wisp of smoke above every known torch, now and then a spark
fn spawn_torch_smoke(&mut self, dt: f32) { fn spawn_torch_smoke(&mut self, dt: f32) {
let spots: Vec<(f32, f32)> = self.torches.iter().filter(|t| self.map.shown(t.wall)).map(|t| t.pos()).collect(); let spots: Vec<(f32, f32, f32)> = self.torches.iter().filter(|t| self.map.shown(t.wall)).map(|t| (t.pos().0, t.pos().1, t.flame_h())).collect();
for (x, y) in spots { for (x, y, fh) in spots {
if self.parts.len() >= PARTS_MAX - 100 { if self.parts.len() >= PARTS_MAX - 100 {
break; break;
} }
if self.vrng.f() < 1.1 * dt { if self.vrng.f() < 1.1 * dt {
let v = ((self.vrng.f() - 0.5) * 0.08, (self.vrng.f() - 0.5) * 0.08, 0.32 + self.vrng.f() * 0.12); let v = ((self.vrng.f() - 0.5) * 0.08, (self.vrng.f() - 0.5) * 0.08, 0.32 + self.vrng.f() * 0.12);
let life = 1.8 + self.vrng.f() * 0.6; let life = 1.8 + self.vrng.f() * 0.6;
self.part(SMOKE, (x, y, 1.0), v, life, abgr(70, 62, 58), 0.05); self.part(SMOKE, (x, y, fh + 0.28), v, life, abgr(70, 62, 58), 0.05);
} }
if self.vrng.f() < 0.25 * dt { if self.vrng.f() < 0.25 * dt {
let v = ((self.vrng.f() - 0.5) * 0.3, (self.vrng.f() - 0.5) * 0.3, 0.9); let v = ((self.vrng.f() - 0.5) * 0.3, (self.vrng.f() - 0.5) * 0.3, 0.9);
self.part(EMBER, (x, y, 0.95), v, 0.9, abgr(255, 170, 60), 0.025); self.part(EMBER, (x, y, fh + 0.23), v, 0.9, abgr(255, 170, 60), 0.025);
} }
} }
} }
+22 -5
View File
@@ -1106,7 +1106,7 @@ impl R3d {
for (k, tr) in g.torches.iter().enumerate() { for (k, tr) in g.torches.iter().enumerate() {
let (x, z) = tr.pos(); let (x, z) = tr.pos();
let fl = 0.85 + 0.1 * (t * 9.0 + k as f32 * 1.7).sin() + 0.05 * (t * 23.0 + k as f32).sin(); let fl = 0.85 + 0.1 * (t * 9.0 + k as f32 * 1.7).sin() + 0.05 * (t * 23.0 + k as f32).sin();
push(vec3(x, 0.85, z), 5.5, vec3(1.0, 0.62, 0.28), 1.5 * fl, 0.0); push(vec3(x, tr.flame_h() + 0.13, z), 5.5, vec3(1.0, 0.62, 0.28), 1.5 * fl, 0.0);
} }
let (cx, cy) = g.map.core; let (cx, cy) = g.map.core;
let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0); let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0);
@@ -1304,7 +1304,11 @@ impl R3d {
gl_use_default_material(); gl_use_default_material();
for tr in g.torches.iter().filter(|tr| g.map.shown(tr.wall)) { for tr in g.torches.iter().filter(|tr| g.map.shown(tr.wall)) {
let (x, z) = tr.pos(); let (x, z) = tr.pos();
draw_cube(vec3(x, 0.55, z), vec3(0.07, 0.3, 0.07), None, Color::from_rgba(70, 45, 28, 255)); if tr.stand {
draw_torch_stand(x, z);
} else {
draw_cube(vec3(x, 0.55, z), vec3(0.07, 0.3, 0.07), None, Color::from_rgba(70, 45, 28, 255));
}
} }
draw_landmarks_solid(g, t); draw_landmarks_solid(g, t);
draw_towers_solid(g); draw_towers_solid(g);
@@ -1328,7 +1332,7 @@ impl R3d {
} }
} }
for (k, tr) in g.torches.iter().enumerate().filter(|(_, tr)| g.map.shown(tr.wall)) { for (k, tr) in g.torches.iter().enumerate().filter(|(_, tr)| g.map.shown(tr.wall)) {
draw_torch_flame(tr.pos(), k as f32, t); draw_torch_flame(tr.pos(), tr.flame_h(), k as f32, t);
} }
draw_shadows(g); draw_shadows(g);
if let Some(f) = g.flag { if let Some(f) = g.flag {
@@ -2676,10 +2680,23 @@ fn draw_traps_glow(g: &Game, t: f32) {
} }
} }
// a torch on a little iron stand: three splayed legs, a ring and the fire bowl on top
fn draw_torch_stand(x: f32, z: f32) {
let iron = Color::from_rgba(52, 46, 44, 255);
let top = vec3(x, 0.5, z);
for k in 0..3 {
let a = k as f32 * std::f32::consts::TAU / 3.0 + 0.4;
let foot = vec3(x + a.cos() * 0.2, 0.0, z + a.sin() * 0.2);
line3(foot, top, 0.03, iron);
}
draw_cube(vec3(x, 0.26, z), vec3(0.16, 0.025, 0.16), None, iron);
draw_cube(vec3(x, 0.55, z), vec3(0.17, 0.09, 0.17), None, Color::from_rgba(70, 58, 50, 255));
draw_cube(vec3(x, 0.6, z), vec3(0.12, 0.02, 0.12), None, Color::from_rgba(255, 140, 40, 255));
}
// a torch flame: stacked glowing blocks, hot and white at the bottom, red at the tip, that // a torch flame: stacked glowing blocks, hot and white at the bottom, red at the tip, that
// sway and flicker; a soft halo around it (the smoke comes from particles.rs) // sway and flicker; a soft halo around it (the smoke comes from particles.rs)
fn draw_torch_flame((x, z): (f32, f32), k: f32, t: f32) { fn draw_torch_flame((x, z): (f32, f32), base: f32, k: f32, t: f32) {
let base = 0.72;
let flick = 0.8 + 0.2 * (t * 13.0 + k * 2.1).sin() + 0.1 * (t * 29.0 + k * 0.7).sin(); let flick = 0.8 + 0.2 * (t * 13.0 + k * 2.1).sin() + 0.1 * (t * 29.0 + k * 0.7).sin();
draw_sphere(vec3(x, base + 0.1, z), 0.24 * flick, None, Color::new(1.0, 0.55, 0.15, 0.16)); draw_sphere(vec3(x, base + 0.1, z), 0.24 * flick, None, Color::new(1.0, 0.55, 0.15, 0.16));
let n = 6; let n = 6;