More graphical finesse: own beds per creature, animated room furniture, treasure chests, elaborate core and portals, worn floors

- props.rs: furniture drawn near the camera every frame
  - beds built by each creature on arrival, matching its kind (straw, web, starry cushion,
    fur pile, chain bunk, ember cushion, rock hollow, camp bed); gone when the owner is
  - training: spinning dummies, weapon racks, wobbling targets; torture: turning wheel,
    iron maiden, brazier with glowing coals; temple: altar with candles and fire bowl,
    candle stands; library: book shelves, lecterns with floating books while studying
  - treasure chests in the treasury, closed when empty, filled with their treasure otherwise
- Soul core: stepped obsidian pedestal, four spikes with energy threads, circling runes,
  unsteady pulse when hurt
- Portals: circle of standing stones with runes, column of light and rising sparks
- Floors: cracked, chipped and shattered stones, grit and crack decals

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-03 12:30:30 +01:00
1 parent 036c13577c
commit 75232d95ee
5 files changed
+609 -42

No files matched your search

+110 -41
View File
@@ -587,16 +587,25 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall
}
// room decor
let dc = col(Vec3::splat(amb_at(amb, xf + 0.5, zf + 0.5)), 1.0);
match t.kind {
Kind::Lair => {
let (a, b) = cell_uv(D_BED);
mb.quad(
[vec3(xf + 0.05, 0.02, zf + 0.05), vec3(xf + 0.95, 0.02, zf + 0.05), vec3(xf + 0.95, 0.02, zf + 0.95), vec3(xf + 0.05, 0.02, zf + 0.95)],
[vec2(a.x, a.y), vec2(b.x, a.y), vec2(b.x, b.y), vec2(a.x, b.y)],
[dc; 4],
[Vec3::Y; 4],
);
// wear and tear: grit and cracks on plain floors
if matches!(t.kind, Kind::Floor) {
let hsh = (x as u32).wrapping_mul(2654435761) ^ (z as u32).wrapping_mul(2246822519);
let hsh = (hsh ^ (hsh >> 15)).wrapping_mul(2246822519);
let decal = match hsh % 11 {
0 | 1 => Some(D_RUBBLE),
2 => Some(D_CRACK),
_ => None,
};
if let Some(d) = decal {
let (a, b) = cell_uv(d);
// rotate by a quarter turn for variety
let uv = [vec2(a.x, a.y), vec2(b.x, a.y), vec2(b.x, b.y), vec2(a.x, b.y)];
let r = (hsh >> 8) as usize % 4;
let uv = [uv[r % 4], uv[(r + 1) % 4], uv[(r + 2) % 4], uv[(r + 3) % 4]];
mb.quad([vec3(xf, 0.012, zf), vec3(xf + 1.0, 0.012, zf), vec3(xf + 1.0, 0.012, zf + 1.0), vec3(xf, 0.012, zf + 1.0)], uv, [dc; 4], [Vec3::Y; 4]);
}
}
match t.kind {
Kind::Farm => {
let pos = [(0.3, 0.3), (0.7, 0.4), (0.45, 0.72)];
for (k, &(ox, oz)) in pos.iter().enumerate() {
@@ -605,12 +614,10 @@ fn build_chunk(map: &Map, amb: &[f32], cx: i32, cz: i32, atlas: &Texture2D, wall
}
}
}
Kind::Training if (x + z) % 2 == 0 => mb.cross(xf + 0.5, zf + 0.5, 0.7, 0.75, D_DUMMY, dc),
Kind::Library if z % 2 == 0 => mb.cross(xf + 0.5, zf + 0.5, 0.9, 0.85, D_SHELF, dc),
Kind::Torture if (x + z) % 2 == 1 => mb.cross(xf + 0.5, zf + 0.5, 0.8, 0.8, D_RACK, dc),
Kind::Temple if x % 3 == 1 && z % 3 == 1 => mb.cross(xf + 0.5, zf + 0.5, 0.8, 0.8, D_ALTAR, dc),
// training, torture, temple and library furniture and the beds: props.rs
// each treasury tile shows what it holds (up to TREASURY_TILE)
Kind::Treasury if t.gold > 10 => {
// (every third treasury tile holds a chest instead, see props.rs)
Kind::Treasury if t.gold > 10 && !crate::props::chest_tile(x, z) => {
let fill = (t.gold as f32 / TREASURY_TILE).clamp(0.0, 1.0);
let s = 0.35 + 0.55 * fill;
let sprite = [D_COINS, D_SILVER, D_RUBY, D_DIAMOND][(t.res & 3) as usize];
@@ -885,7 +892,7 @@ impl R3d {
}
let (cx, cy) = g.map.core;
let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0);
push(vec3(cx as f32 + 1.5, 1.2, cy as f32 + 1.5), 7.0, vec3(1.0, 0.25, 0.2), 1.1 + 0.2 * (t * 3.0).sin() * (1.5 - hp), 100.0);
push(vec3(cx as f32 + 1.5, 1.6, cy as f32 + 1.5), 7.0, vec3(1.0, 0.25, 0.2), 1.1 + 0.2 * (t * 3.0).sin() * (1.5 - hp), 100.0);
for (k, &(px, py)) in g.map.portals.iter().enumerate() {
let owned = g.map.portal_owned(k);
push(vec3(px as f32 + 1.5, 0.8, py as f32 + 1.5), 6.0, vec3(0.7, 0.3, 1.0), if owned { 1.0 } else { 0.35 }, 20.0);
@@ -1013,6 +1020,7 @@ impl R3d {
}
draw_landmarks_solid(g, t);
draw_towers_solid(g);
crate::props::draw_props_solid(g, cam, t);
// ---- unlit glow / overlays
gl_use_material(&self.glow);
@@ -1044,6 +1052,7 @@ impl R3d {
// translucent glows and effects last (they write depth, see glow_material)
draw_landmarks_glow(g, t);
draw_towers_glow(g, t);
crate::props::draw_props_glow(g, cam, t);
for f in &g.fx {
draw_fx(f, t);
}
@@ -1265,7 +1274,7 @@ thread_local! {
}
// Thick 3D line as a camera-facing quad (GL lines don't work with custom materials)
fn line3(a: Vec3, b: Vec3, w: f32, color: Color) {
pub fn line3(a: Vec3, b: Vec3, w: f32, color: Color) {
let eye = EYE.with(|e| e.get());
let side = (b - a).cross(eye).normalize_or_zero() * (w * 0.5);
let c = [(color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, (color.a * 255.0) as u8];
@@ -1273,7 +1282,7 @@ fn line3(a: Vec3, b: Vec3, w: f32, color: Color) {
draw_mesh(&Mesh { vertices: vec![v(a - side), v(a + side), v(b + side), v(b - side)], indices: vec![0, 1, 2, 0, 2, 3], texture: None });
}
fn disc(c: Vec3, r: f32, color: Color, seg: usize) {
pub fn disc(c: Vec3, r: f32, color: Color, seg: usize) {
let mut v = Vec::with_capacity(seg + 1);
let cc = [(color.r * 255.0) as u8, (color.g * 255.0) as u8, (color.b * 255.0) as u8, (color.a * 255.0) as u8];
v.push(Vertex { position: c, uv: Vec2::ZERO, color: cc, normal: Vec4::Y });
@@ -1298,11 +1307,31 @@ fn ring(c: Vec3, r: f32, color: Color) {
}
fn draw_landmarks_solid(g: &Game, t: f32) {
// core pedestal
// soul core: stepped obsidian pedestal with four spikes holding the orb
let (cx, cy) = g.map.core;
let c = vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5);
draw_cube(c + vec3(0.0, 0.15, 0.0), vec3(2.4, 0.3, 2.4), None, Color::from_rgba(45, 16, 20, 255));
draw_cube(c + vec3(0.0, 0.4, 0.0), vec3(1.4, 0.2, 1.4), None, Color::from_rgba(70, 26, 30, 255));
draw_cube(c + vec3(0.0, 0.08, 0.0), vec3(2.8, 0.16, 2.8), None, Color::from_rgba(34, 14, 18, 255));
draw_cube(c + vec3(0.0, 0.22, 0.0), vec3(2.1, 0.14, 2.1), None, Color::from_rgba(48, 18, 24, 255));
draw_cube(c + vec3(0.0, 0.35, 0.0), vec3(1.3, 0.14, 1.3), None, Color::from_rgba(66, 24, 30, 255));
for (k, (dx, dz)) in [(-1.15f32, -1.15f32), (1.15, -1.15), (1.15, 1.15), (-1.15, 1.15)].iter().enumerate() {
let b = c + vec3(*dx, 0.0, *dz);
draw_cube(b + vec3(0.0, 0.45, 0.0), vec3(0.26, 0.9, 0.26), None, Color::from_rgba(28, 20, 26, 255));
draw_cube(b + vec3(0.0, 1.05, 0.0), vec3(0.16, 0.3, 0.16), None, Color::from_rgba(40, 28, 36, 255));
draw_cube(b + vec3(0.0, 1.26, 0.0), vec3(0.08, 0.14, 0.08), None, Color::from_rgba(180, 140, 60, 255));
// claws reaching for the orb
line3(b + vec3(0.0, 1.0, 0.0), c + vec3(dx * 0.45, 1.55 - (k % 2) as f32 * 0.1, dz * 0.45), 0.07, Color::from_rgba(36, 24, 30, 255));
}
// portal circles: eight standing stones of different height
for &(px, py) in &g.map.portals {
let pc = vec3(px as f32 + 1.5, 0.0, py as f32 + 1.5);
for k in 0..8 {
let a = k as f32 * 0.785 + 0.39;
let h = 0.8 + ((k * 5 + px as usize) % 4) as f32 * 0.15;
let p = pc + vec3(a.cos() * 1.5, h * 0.5, a.sin() * 1.5);
draw_cube(p, vec3(0.28, h, 0.28), None, Color::from_rgba(52 + (k * 3) as u8, 44, 62, 255));
draw_cube(p + vec3(0.0, h * 0.5 + 0.04, 0.0), vec3(0.22, 0.08, 0.22), None, Color::from_rgba(70, 60, 82, 255));
}
}
// gate pillars
let (gx, gz) = g.gate_center();
if g.gate_hp > 0.0 {
@@ -1321,30 +1350,70 @@ fn draw_landmarks_solid(g: &Game, t: f32) {
}
fn draw_landmarks_glow(g: &Game, t: f32) {
// soul core
// soul core: pulsing orb (faster and more unsteady when hurt), energy threads to the
// spikes, runes circling around it
let (cx, cy) = g.map.core;
let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0);
let pulse = 1.0 + (t * (2.0 + (1.0 - hp) * 6.0)).sin() * 0.06;
let c = vec3(cx as f32 + 1.5, 1.15 + (t * 1.3).sin() * 0.06, cy as f32 + 1.5);
draw_sphere(c, 0.75 * pulse, None, Color::new(0.4 + 0.5 * hp, 0.05, 0.08, 1.0));
draw_sphere(c, 0.95 * pulse, None, Color::new(1.0, 0.2, 0.15, 0.25));
draw_sphere(c + vec3(-0.2, 0.25, 0.2), 0.18, None, Color::new(1.0, 0.85, 0.7, 0.8));
// portals
for (pk, &(px, py)) in g.map.portals.iter().enumerate() {
let owned = g.map.portal_owned(pk);
let pc = vec3(px as f32 + 1.5, 0.03, py as f32 + 1.5);
let arm = if owned { Color::new(0.8, 0.4, 1.0, 1.0) } else { Color::new(0.45, 0.42, 0.5, 1.0) };
disc(pc, 1.35, if owned { Color::new(0.2, 0.05, 0.3, 0.9) } else { Color::new(0.1, 0.1, 0.12, 0.9) }, 32);
let speed = if owned { 1.6 } else { 0.3 };
for a in 0..3 {
for k in 0..18 {
let f = k as f32 / 18.0;
let ang = t * speed + a as f32 * 2.094 + f * 4.0;
let r = f * 1.3;
let p = pc + vec3(ang.cos() * r, 0.1 + (1.0 - f) * 0.5, ang.sin() * r);
draw_cube(p, Vec3::splat(0.05 + 0.06 * (1.0 - f)), None, Color::new(arm.r, arm.g, arm.b, 0.4 + 0.6 * (1.0 - f)));
}
let beat = t * (2.0 + (1.0 - hp) * 6.0);
let pulse = 1.0 + beat.sin() * 0.06 + if hp < 0.4 { (t * 31.0).sin() * 0.03 } else { 0.0 };
let base = vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5);
let c = base + vec3(0.0, 1.6 + (t * 1.3).sin() * 0.06, 0.0);
draw_sphere(c, 0.62 * pulse, None, Color::new(0.4 + 0.5 * hp, 0.04, 0.07, 1.0));
draw_sphere(c, 0.32 * pulse, None, Color::new(1.0, 0.5 + 0.3 * hp, 0.4, 0.55));
draw_sphere(c, 0.85 * pulse, None, Color::new(1.0, 0.2, 0.15, 0.22));
draw_sphere(c, 1.15 * pulse, None, Color::new(1.0, 0.1, 0.1, 0.08));
draw_sphere(c + vec3(-0.18, 0.2, 0.18), 0.13, None, Color::new(1.0, 0.85, 0.7, 0.8));
for (k, (dx, dz)) in [(-1.15f32, -1.15f32), (1.15, -1.15), (1.15, 1.15), (-1.15, 1.15)].iter().enumerate() {
let tip = base + vec3(*dx, 1.33, *dz);
let flick = 0.5 + 0.5 * (t * 5.0 + k as f32 * 1.7).sin();
let mid = (tip + c) * 0.5 + vec3((t * 7.0 + k as f32).sin() * 0.08, 0.15, (t * 6.0 + k as f32).cos() * 0.08);
let col = Color::new(1.0, 0.25 + 0.3 * flick, 0.2, 0.35 + 0.45 * flick * hp.max(0.3));
line3(tip, mid, 0.04, col);
line3(mid, c, 0.04, col);
draw_sphere(tip + vec3(0.0, 0.05, 0.0), 0.07, None, Color::new(1.0, 0.6, 0.3, 0.6 + 0.4 * flick));
}
for k in 0..6 {
let a = t * 0.7 + k as f32 * 1.047;
let p = c + vec3(a.cos() * 1.05, (t * 1.1 + k as f32).sin() * 0.25, a.sin() * 1.05);
draw_cube(p, vec3(0.1, 0.16, 0.04), None, Color::new(1.0, 0.7, 0.3, 0.85));
}
// portals: a circle of standing stones with runes, the vortex, a column of light, sparks
for (pk, &(px, py)) in g.map.portals.iter().enumerate() {
let owned = g.map.portal_owned(pk);
let pc = vec3(px as f32 + 1.5, 0.03, py as f32 + 1.5);
let arm = if owned { Color::new(0.8, 0.4, 1.0, 1.0) } else { Color::new(0.45, 0.42, 0.5, 1.0) };
disc(pc, 1.35, if owned { Color::new(0.2, 0.05, 0.3, 0.9) } else { Color::new(0.1, 0.1, 0.12, 0.9) }, 32);
let speed = if owned { 1.6 } else { 0.3 };
for a in 0..3 {
for k in 0..18 {
let f = k as f32 / 18.0;
let ang = t * speed + a as f32 * 2.094 + f * 4.0;
let r = f * 1.3;
let p = pc + vec3(ang.cos() * r, 0.1 + (1.0 - f) * 0.5, ang.sin() * r);
draw_cube(p, Vec3::splat(0.05 + 0.06 * (1.0 - f)), None, Color::new(arm.r, arm.g, arm.b, 0.4 + 0.6 * (1.0 - f)));
}
}
// runes on the stones
for k in 0..8 {
let a = k as f32 * 0.785 + 0.39;
let glow = 0.5 + 0.5 * (t * 2.0 + k as f32 * 0.8).sin();
let h = 0.45 + (k % 3) as f32 * 0.12;
let p = pc + vec3(a.cos() * 1.4, h, a.sin() * 1.4);
draw_cube(p, vec3(0.08, 0.14, 0.08), None, Color::new(arm.r, arm.g, arm.b, if owned { 0.4 + 0.6 * glow } else { 0.25 }));
}
if owned {
// column of light and rising sparks
for k in 0..5 {
let y = 0.4 + k as f32 * 0.5;
draw_sphere(pc + vec3(0.0, y, 0.0), 0.35 - k as f32 * 0.05, None, Color::new(0.75, 0.45, 1.0, 0.1));
}
for k in 0..10 {
let ph = (t * 0.6 + k as f32 * 0.37).fract();
let a = k as f32 * 2.4 + t * 0.5;
let p = pc + vec3(a.cos() * (0.3 + 0.5 * ph), 0.2 + ph * 2.4, a.sin() * (0.3 + 0.5 * ph));
draw_cube(p, Vec3::splat(0.05), None, Color::new(0.9, 0.7, 1.0, 1.0 - ph));
}
}
}
// gate light
if g.gate_hp > 0.0 {