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:
1 parent
6c16bc7b18
commit
c5378d0774
3 files changed
+121
-11
No files matched your search
+96
-3
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-4
@@ -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,8 +1304,12 @@ 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();
|
||||||
|
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_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);
|
||||||
crate::props::draw_props_solid(g, cam, t);
|
crate::props::draw_props_solid(g, cam, t);
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in new issue
Block a user