// Room furniture and creature beds, drawn every frame near the camera so they can move:
// training dummies spin while in use, torture wheels turn, candles and braziers flicker,
// books float above the lecterns. Every creature builds its own bed in the lair (a web for
// the spider, a fur pile for the ogre, ...); it disappears with its owner.
// Solid parts are drawn with the default material (flat colours), glowing parts in the glow pass.
use crate::game::*;
use crate::map::*;
use crate::render3d::{disc, Cam};
use macroquad::prelude::*;
use std::collections::{HashMap, HashSet};
fn hash(x: i32, y: i32, s: u32) -> u32 {
let mut h = (x as u32).wrapping_mul(374761393) ^ (y as u32).wrapping_mul(668265263) ^ s.wrapping_mul(2246822519);
h = (h ^ (h >> 13)).wrapping_mul(1274126177);
h ^ (h >> 16)
}
fn rgb(r: u8, g: u8, b: u8, light: f32) -> Color {
Color::new(r as f32 / 255.0 * light, g as f32 / 255.0 * light, b as f32 / 255.0 * light, 1.0)
}
thread_local! {
// a bed being set up grows out of the floor: centre and size factor
static GROW: std::cell::Cell> = std::cell::Cell::new(None);
}
fn grown(p: Vec3) -> Vec3 {
match GROW.with(|g| g.get()) {
Some((c, k)) => c + (p - c) * k,
None => p,
}
}
fn cube(p: Vec3, s: Vec3, c: Color) {
let k = GROW.with(|g| g.get()).map_or(1.0, |g| g.1);
draw_cube(grown(p), s * k, None, c);
}
// the materials of the furniture: a texture of grey detail on every side of a box, tinted by its
// colour (gfx MATERIALS, in this order; the page keeps its own colours)
#[derive(Clone, Copy)]
pub enum Mat {
Wood,
Stone,
Iron,
Cloth,
Straw,
Fur,
Book,
Paper,
Crystal,
}
thread_local! {
// the material textures of the current graphics style (set every frame, see set_materials)
static MATS: std::cell::RefCell > = std::cell::RefCell::new(None);
}
pub fn set_materials(m: &[Texture2D; 9]) {
MATS.with(|t| *t.borrow_mut() = Some(m.clone()));
}
// the texture of a material in the current style
pub fn material(m: Mat) -> Option {
MATS.with(|t| t.borrow().as_ref().map(|a| a[m as usize].clone()))
}
// the material of a box of the buildings (render3d: towers, the soul core's throne, gates,
// portals, traps) by its colour and shape: dark and grey blocks are stone (thin ones iron),
// brown is wood, gold and brass are metal, strong colours are cloth; translucent and bright
// glowing parts stay plain
fn auto_mat(s: Vec3, c: Color) -> Option {
if c.a < 0.99 {
return None;
}
let (mx, mn) = (c.r.max(c.g).max(c.b), c.r.min(c.g).min(c.b));
let sat = if mx > 0.0 { (mx - mn) / mx } else { 0.0 };
let thin = s.x.min(s.y).min(s.z) < 0.07;
if mx > 0.85 && sat > 0.45 {
return None;
}
if mx < 0.22 || sat < 0.2 {
return Some(if thin { Mat::Iron } else { Mat::Stone });
}
if c.r > 0.6 && c.g > 0.45 && c.b < c.g * 0.6 {
return Some(Mat::Iron);
}
if c.r >= c.g && c.g >= c.b && mx < 0.8 {
return Some(Mat::Wood);
}
Some(Mat::Cloth)
}
// a box of a building, textured by its material (see auto_mat)
pub fn auto_cube(p: Vec3, s: Vec3, c: Color) {
match auto_mat(s, c) {
Some(m) => mc(m, p, s, c),
None => draw_cube(p, s, None, c),
}
}
// a box of a material; the detail is a little darker than white, the colour makes up for it
fn mc(m: Mat, p: Vec3, s: Vec3, c: Color) {
let k = GROW.with(|g| g.get()).map_or(1.0, |g| g.1);
let tex = MATS.with(|t| t.borrow().as_ref().map(|a| a[m as usize].clone()));
let c = if tex.is_some() { Color::new((c.r * 1.2).min(1.0), (c.g * 1.2).min(1.0), (c.b * 1.2).min(1.0), c.a) } else { c };
draw_cube(grown(p), s * k, tex.as_ref(), c);
}
fn line3(a: Vec3, b: Vec3, w: f32, c: Color) {
let k = GROW.with(|g| g.get()).map_or(1.0, |g| g.1);
crate::render3d::line3(grown(a), grown(b), w * k, c);
}
// a bed: complete, or growing while its creature sets it up (progress 0..1)
fn bed_built(kind: EK, c: Vec3, h: u32, l: f32, t: f32, progress: f32) {
if progress <= 0.0 {
return;
}
if progress < 1.0 {
// eased growth with a little bounce at the end
let k = (progress * (2.0 - progress)).min(1.0) * (1.0 + 0.08 * (progress * 9.0).sin() * (1.0 - progress));
GROW.with(|g| g.set(Some((c, k))));
bed(kind, c, h, l, t);
GROW.with(|g| g.set(None));
} else {
bed(kind, c, h, l, t);
}
}
// what is going on in the rooms this frame
struct Use {
busy: HashSet, // tiles where a creature trains, studies or prays
tortured: HashSet, // torture tiles with a prisoner on them
beds: HashMap, // bed owner (entity id + 1) -> kind, how far it is set up
}
fn usage(g: &Game) -> Use {
let mut u = Use { busy: HashSet::new(), tortured: HashSet::new(), beds: HashMap::new() };
for e in &g.ents {
if !e.alive {
continue;
}
match e.job {
Job::Train(t) | Job::Study(t) | Job::Pray(t) | Job::Forge(t) if e.working || e.path.is_empty() => {
u.busy.insert(t);
}
_ => {}
}
if e.captive == 3 {
u.tortured.insert(e.tile());
}
// the bed only stands once its owner has set it up (it is reserved earlier)
if (!e.hero || e.captive == 0) && e.settled {
u.beds.insert(e.id + 1, (e.kind, 1.0));
} else if let Job::MakeBed(_) = e.job {
// being set up: grows while the creature works on it (four seconds)
if e.path.is_empty() {
u.beds.insert(e.id + 1, (e.kind, (1.0 - e.timer / 4.0).clamp(0.0, 1.0)));
}
}
}
u
}
// tiles around the camera (the view is a rotated rectangle; a square around it is enough)
fn visible(cam: &Cam) -> (i32, i32, i32, i32) {
let r = (cam.zoom * 1.1) as i32 + 2;
let (cx, cz) = (cam.tx as i32, cam.tz as i32);
((cx - r).max(0), (cz - r).max(0), (cx + r).min(mw() as i32 - 1), (cz + r).min(mh() as i32 - 1))
}
fn light_of(t: &Tile) -> f32 {
if t.owner == Owner::Player {
0.85
} else {
0.55
}
}
pub fn draw_props_solid(g: &Game, cam: &Cam, t: f32) {
let u = usage(g);
let (x0, z0, x1, z1) = visible(cam);
for z in z0..=z1 {
for x in x0..=x1 {
let i = idx(x, z);
if !g.map.shown(i) {
continue;
}
let tile = &g.map.t[i];
let l = light_of(tile);
let c = vec3(x as f32 + 0.5, 0.0, z as f32 + 0.5);
let h = hash(x, z, 77);
match tile.kind {
Kind::Lair if tile.bed != 0 => {
if let Some(&(kind, progress)) = u.beds.get(&tile.bed) {
bed_built(kind, c, h, l, t, progress);
}
}
Kind::Aerie if tile.bed != 0 => {
if let Some(&(kind, progress)) = u.beds.get(&tile.bed) {
bed_built(kind, c, h, l, t, progress);
}
}
Kind::Aerie if h % 3 == 0 => perch(c, h, l),
Kind::Treasury if chest_tile(x, z) => chest(c, h, l, tile.gold as f32 / TREASURY_TILE, tile.res),
Kind::Training if (x + z) % 2 == 0 => training(c, h, l, t, u.busy.contains(&i) || u.busy.iter().any(|&b| adj8(b, i))),
Kind::Torture if (x + z) % 2 == 1 => torture(c, h, l, t, u.tortured.contains(&i) || u.tortured.iter().any(|&b| adj8(b, i))),
Kind::Temple if x % 3 == 1 && z % 3 == 1 => temple(c, h, l),
Kind::Temple if (x + z) % 2 == 0 && h % 3 == 0 => candle_stand(c + vec3(((h >> 4) % 5) as f32 * 0.1 - 0.2, 0.0, ((h >> 8) % 5) as f32 * 0.1 - 0.2), l),
Kind::Library if z % 2 == 0 => shelf(c, h, l),
Kind::Library if h % 3 == 0 => lectern(c, l),
Kind::ManaSpring => spring_basin(c, l),
Kind::RuneChest => rune_chest(c, l),
Kind::Forge if (x + z) % 2 == 0 => anvil(c, h, l),
Kind::Prison => prison_bars(g, x, z, c, l),
Kind::Graveyard if h % 4 == 0 => tombstone(c + vec3(0.25, 0.0, -0.25), h, l, 0.6),
Kind::GuardPost if x % 2 == 0 && z % 2 == 0 => guard_brazier(c, l),
Kind::Arena if h % 5 == 0 => arena_post(c, h, l),
Kind::Altar if x % 3 == 1 && z % 3 == 1 => altar(c, l),
Kind::Forge if h % 4 == 1 => furnace(c, l),
_ => {}
}
}
}
// graves: a mound and a stone, the fresher the darker the earth
for gr in &g.graves {
let (gx, gz) = xy(gr.tile);
if gx < x0 || gx > x1 || gz < z0 || gz > z1 {
continue;
}
let c = vec3(gx as f32 + 0.5, 0.0, gz as f32 + 0.5);
cube(c + vec3(0.0, 0.05, 0.08), vec3(0.34, 0.1, 0.6), rgb(70, 52, 40, 0.7));
tombstone(c + vec3(0.0, 0.0, -0.28), gr.tile as u32, 0.8, 1.0);
}
// creatures in chains: an iron post with chains to the creature
for e in g.ents.iter().filter(|e| e.alive && e.chained) {
if (e.disp_x as i32) < x0 || (e.disp_x as i32) > x1 || (e.disp_y as i32) < z0 || (e.disp_y as i32) > z1 {
continue;
}
let post = vec3(e.disp_x.floor() + 0.5, 0.0, e.disp_y.floor() + 0.15);
mc(Mat::Iron, post + vec3(0.0, 0.35, 0.0), vec3(0.08, 0.7, 0.08), rgb(70, 70, 78, 0.6));
for n in 0..6 {
let f = n as f32 / 6.0;
let side = if n % 2 == 0 { 0.08 } else { -0.08 };
let p = post.lerp(vec3(e.disp_x + side, 0.25, e.disp_y), f) + vec3(0.0, 0.3 * (1.0 - f) + 0.05 * (f * 3.14).sin(), 0.0);
mc(Mat::Iron, p, vec3(0.05, 0.05, 0.05), rgb(120, 120, 130, 0.6));
}
}
}
// the dwarves' iron-bound chest with the rune forge plans (the rune glows in the glow pass)
fn rune_chest(c: Vec3, l: f32) {
mc(Mat::Wood, c + vec3(0.0, 0.16, 0.0), vec3(0.5, 0.3, 0.34), rgb(92, 60, 34, l));
mc(Mat::Wood, c + vec3(0.0, 0.34, 0.0), vec3(0.52, 0.08, 0.36), rgb(110, 72, 40, l));
for dx in [-0.18f32, 0.18] {
mc(Mat::Wood, c + vec3(dx, 0.2, 0.0), vec3(0.05, 0.4, 0.38), rgb(176, 112, 56, l));
}
mc(Mat::Iron, c + vec3(0.0, 0.22, 0.18), vec3(0.08, 0.1, 0.03), rgb(200, 170, 80, l));
}
// prison: iron bars along the edges of the room (cell walls)
fn prison_bars(g: &Game, x: i32, z: i32, c: Vec3, l: f32) {
let iron = rgb(70, 70, 78, l);
for (dx, dz) in [(0i32, -1i32), (1, 0), (0, 1), (-1, 0)] {
let open = g.map.get(x + dx, z + dz).map_or(false, |t| t.kind != Kind::Prison && t.walkable());
if !open {
continue;
}
let edge = c + vec3(dx as f32 * 0.46, 0.0, dz as f32 * 0.46);
let side = vec3(dz as f32, 0.0, dx as f32);
for k in 0..5 {
let o = (k as f32 - 2.0) * 0.2;
mc(Mat::Iron, edge + side * o + vec3(0.0, 0.4, 0.0), vec3(0.04, 0.8, 0.04), iron);
}
mc(Mat::Iron, edge + vec3(0.0, 0.78, 0.0), side * 1.0 + vec3(0.05, 0.04, 0.05), iron);
}
}
fn tombstone(c: Vec3, h: u32, l: f32, size: f32) {
let tilt = ((h >> 3) % 3) as f32 * 0.04 - 0.04;
mc(Mat::Stone, c + vec3(tilt, 0.2 * size, 0.0), vec3(0.24, 0.4, 0.08) * size, rgb(110, 108, 112, l));
mc(Mat::Stone, c + vec3(tilt, 0.42 * size, 0.0), vec3(0.16, 0.06, 0.08) * size, rgb(120, 118, 122, l));
mc(Mat::Stone, c + vec3(tilt, 0.28 * size, 0.045), vec3(0.06, 0.16, 0.01) * size, rgb(60, 58, 62, l));
}
// guard post: a brazier on a tripod (its fire glows in the glow pass) and a small banner
fn guard_brazier(c: Vec3, l: f32) {
for k in 0..3 {
let a = k as f32 * 2.1;
mc(Mat::Iron, c + vec3(a.cos() * 0.12, 0.2, a.sin() * 0.12), vec3(0.04, 0.4, 0.04), rgb(60, 60, 66, l));
}
mc(Mat::Iron, c + vec3(0.0, 0.44, 0.0), vec3(0.3, 0.08, 0.3), rgb(70, 66, 70, l));
mc(Mat::Wood, c + vec3(0.35, 0.5, 0.0), vec3(0.03, 1.0, 0.03), rgb(90, 64, 40, l));
mc(Mat::Cloth, c + vec3(0.45, 0.82, 0.0), vec3(0.2, 0.28, 0.02), rgb(150, 24, 30, l));
}
// arena: a wooden post with a training weapon leaning against it
fn arena_post(c: Vec3, h: u32, l: f32) {
let o = vec3(((h >> 4) % 3) as f32 * 0.2 - 0.2, 0.0, ((h >> 6) % 3) as f32 * 0.2 - 0.2);
mc(Mat::Wood, c + o + vec3(0.0, 0.35, 0.0), vec3(0.1, 0.7, 0.1), rgb(110, 76, 44, l));
mc(Mat::Iron, c + o + vec3(0.12, 0.35, 0.0), vec3(0.03, 0.6, 0.03), rgb(150, 150, 160, l));
}
// sacrifice altar: a black block with a channel, candles at the corners (they glow)
fn altar(c: Vec3, l: f32) {
mc(Mat::Stone, c + vec3(0.0, 0.08, 0.0), vec3(1.6, 0.16, 1.2), rgb(36, 30, 38, l));
mc(Mat::Stone, c + vec3(0.0, 0.32, 0.0), vec3(1.0, 0.32, 0.6), rgb(28, 24, 30, l));
cube(c + vec3(0.0, 0.49, 0.0), vec3(0.8, 0.02, 0.1), rgb(90, 10, 20, l));
for (dx, dz) in [(-0.7f32, -0.5f32), (0.7, -0.5), (0.7, 0.5), (-0.7, 0.5)] {
cube(c + vec3(dx, 0.25, dz), vec3(0.06, 0.18, 0.06), rgb(220, 210, 190, l));
}
}
// forge: an anvil on a block, a hammer leaning against it
fn anvil(c: Vec3, h: u32, l: f32) {
let o = vec3(((h >> 3) % 3) as f32 * 0.08 - 0.08, 0.0, ((h >> 5) % 3) as f32 * 0.08 - 0.08);
mc(Mat::Wood, c + o + vec3(0.0, 0.12, 0.0), vec3(0.22, 0.24, 0.22), rgb(70, 50, 36, l));
mc(Mat::Iron, c + o + vec3(0.0, 0.28, 0.0), vec3(0.16, 0.08, 0.16), rgb(56, 56, 62, l));
mc(Mat::Iron, c + o + vec3(0.0, 0.35, 0.0), vec3(0.38, 0.06, 0.16), rgb(76, 76, 84, l));
mc(Mat::Iron, c + o + vec3(0.22, 0.35, 0.0), vec3(0.1, 0.04, 0.08), rgb(76, 76, 84, l));
}
// forge: a squat stone furnace (its fire glows in the glow pass)
fn furnace(c: Vec3, l: f32) {
mc(Mat::Stone, c + vec3(0.0, 0.25, 0.0), vec3(0.6, 0.5, 0.6), rgb(64, 52, 50, l));
mc(Mat::Stone, c + vec3(0.0, 0.55, 0.0), vec3(0.4, 0.1, 0.4), rgb(50, 42, 40, l));
mc(Mat::Stone, c + vec3(0.0, 0.75, 0.0), vec3(0.16, 0.32, 0.16), rgb(44, 38, 38, l));
}
// a stone basin around a mana spring
fn spring_basin(c: Vec3, l: f32) {
for (dx, dz, sx, sz) in [(0.0, -0.38, 0.8, 0.1), (0.0, 0.38, 0.8, 0.1), (-0.38, 0.0, 0.1, 0.8), (0.38, 0.0, 0.1, 0.8)] {
mc(Mat::Stone, c + vec3(dx, 0.08, dz), vec3(sx, 0.16, sz), rgb(90, 95, 110, l));
}
}
pub fn draw_props_glow(g: &Game, cam: &Cam, t: f32) {
let u = usage(g);
let (x0, z0, x1, z1) = visible(cam);
for z in z0..=z1 {
for x in x0..=x1 {
let i = idx(x, z);
if !g.map.shown(i) {
continue;
}
let tile = &g.map.t[i];
let c = vec3(x as f32 + 0.5, 0.0, z as f32 + 0.5);
let h = hash(x, z, 77);
let k = (x * 13 + z * 7) as f32;
match tile.kind {
Kind::Torture if (x + z) % 2 == 1 && h % 3 == 2 => {
// brazier coals
for n in 0..4 {
let a = n as f32 * 1.6 + k;
let f = 0.7 + 0.3 * (t * 7.0 + a).sin();
draw_sphere(c + vec3(a.cos() * 0.1, 0.55 + 0.03 * f, a.sin() * 0.1), 0.06 * f, None, Color::new(1.0, 0.35 + 0.2 * f, 0.05, 0.95));
}
draw_sphere(c + vec3(0.0, 0.62, 0.0), 0.16, None, Color::new(1.0, 0.4, 0.1, 0.25));
}
Kind::Temple if x % 3 == 1 && z % 3 == 1 => {
// candles on the altar and the eternal flame
for n in [-0.32f32, 0.32] {
flame(c + vec3(n, 0.68, -0.18), t, k + n);
}
let f = 1.0 + 0.15 * (t * 5.0 + k).sin();
draw_sphere(c + vec3(0.0, 0.62, 0.15), 0.1 * f, None, Color::new(1.0, 0.45, 0.1, 0.9));
draw_sphere(c + vec3(0.0, 0.66, 0.15), 0.2 * f, None, Color::new(1.0, 0.3, 0.05, 0.2));
}
Kind::Temple if (x + z) % 2 == 0 && h % 3 == 0 => {
let off = vec3(((h >> 4) % 5) as f32 * 0.1 - 0.2, 0.0, ((h >> 8) % 5) as f32 * 0.1 - 0.2);
flame(c + off + vec3(0.0, 0.52, 0.0), t, k);
}
Kind::Library if z % 2 != 0 && h % 3 == 0 => {
// books floating above the lectern while someone studies, otherwise resting
let busy = u.busy.contains(&i) || u.busy.iter().any(|&b| adj8(b, i));
let n = if busy { 3 } else { 1 };
for b in 0..n {
let a = t * 0.9 + b as f32 * 2.1 + k;
let y = 0.85 + (t * 1.7 + b as f32).sin() * 0.06 + b as f32 * 0.1;
let r = if busy { 0.32 } else { 0.0 };
let col = [Color::new(0.9, 0.3, 0.3, 1.0), Color::new(0.3, 0.5, 0.95, 1.0), Color::new(0.4, 0.85, 0.4, 1.0)][b % 3];
draw_cube(c + vec3(a.cos() * r, y, a.sin() * r), vec3(0.14, 0.04, 0.1), None, col);
if busy {
draw_sphere(c + vec3(a.cos() * r, y, a.sin() * r), 0.09, None, Color::new(0.7, 0.6, 1.0, 0.2));
}
}
}
Kind::Aerie if tile.bed != 0 => {
// the wyrm's nest glows
if let Some((EK::Wyrm, 1.0)) = u.beds.get(&tile.bed).copied() {
for n in 0..5 {
let a = n as f32 * 1.3 + k;
let f = 0.6 + 0.4 * (t * 3.0 + a).sin();
draw_sphere(c + vec3(a.cos() * 0.2, 0.06, a.sin() * 0.2), 0.06 * f, None, Color::new(1.0, 0.5, 0.1, 0.9));
}
}
}
Kind::GuardPost if x % 2 == 0 && z % 2 == 0 => {
let f = 0.8 + 0.2 * (t * 8.0 + k).sin();
draw_sphere(c + vec3(0.0, 0.55, 0.0), 0.11 * f, None, Color::new(1.0, 0.55, 0.1, 0.95));
draw_sphere(c + vec3(0.0, 0.6, 0.0), 0.25 * f, None, Color::new(1.0, 0.35, 0.05, 0.2));
}
Kind::Altar if x % 3 == 1 && z % 3 == 1 => {
for (dx, dz) in [(-0.7f32, -0.5f32), (0.7, -0.5), (0.7, 0.5), (-0.7, 0.5)] {
flame(c + vec3(dx, 0.38, dz), t, k + dx);
}
let p = 0.5 + 0.5 * (t * 1.5 + k).sin();
draw_cube(c + vec3(0.0, 0.5, 0.0), vec3(0.8, 0.02, 0.1), None, Color::new(1.0, 0.1, 0.15, 0.4 + 0.5 * p));
}
Kind::Forge if h % 4 == 1 && (x + z) % 2 != 0 => {
// furnace fire
let f = 0.7 + 0.3 * (t * 9.0 + k).sin();
draw_cube(c + vec3(0.0, 0.2, 0.31), vec3(0.28, 0.2 * f, 0.02), None, Color::new(1.0, 0.5, 0.1, 0.95));
draw_sphere(c + vec3(0.0, 0.25, 0.35), 0.22 * f, None, Color::new(1.0, 0.35, 0.05, 0.25));
}
Kind::Forge if (x + z) % 2 == 0 && (u.busy.contains(&i) || u.busy.iter().any(|&b| adj8(b, i))) => {
// sparks fly from the anvil while somebody hammers
for n in 0..5 {
let ph = (t * 2.2 + n as f32 * 0.21 + k * 0.1).fract();
let a = n as f32 * 1.3 + k;
let p = c + vec3(a.cos() * ph * 0.5, 0.4 + ph * 0.4 - ph * ph * 0.9, a.sin() * ph * 0.5);
draw_cube(p, Vec3::splat(0.03), None, Color::new(1.0, 0.75, 0.3, 1.0 - ph));
}
}
Kind::RuneChest => {
let f = 0.6 + 0.4 * (t * 2.5 + k).sin();
draw_cube(c + vec3(0.0, 0.39, 0.0), vec3(0.14, 0.02, 0.14), None, Color::new(0.35, 0.8, 1.0, f));
draw_sphere(c + vec3(0.0, 0.45, 0.0), 0.2, None, Color::new(0.3, 0.7, 1.0, 0.15 * f));
}
Kind::ManaSpring => {
// rising mana: a pulsing pool and motes that drift up
let f = 0.8 + 0.2 * (t * 2.0 + k).sin();
draw_cube(c + vec3(0.0, 0.1, 0.0), vec3(0.66, 0.04, 0.66), None, Color::new(0.3, 0.8, 1.0, 0.85 * f));
for n in 0..5 {
let a = n as f32 * 1.26 + k;
let y = ((t * 0.5 + n as f32 * 0.2) % 1.0) * 1.2;
draw_sphere(c + vec3(a.cos() * 0.2, 0.15 + y, a.sin() * 0.2), 0.05 * (1.2 - y), None, Color::new(0.5, 0.9, 1.0, 0.9));
}
draw_sphere(c + vec3(0.0, 0.4, 0.0), 0.3 * f, None, Color::new(0.3, 0.7, 1.0, 0.15));
}
Kind::Lair if tile.bed != 0 => {
// the flame priestess sleeps on embers
if let Some((EK::Priestess, 1.0)) = u.beds.get(&tile.bed).copied() {
for n in 0..3 {
let a = n as f32 * 2.1 + k;
let f = 0.6 + 0.4 * (t * 4.0 + a).sin();
draw_sphere(c + vec3(a.cos() * 0.22, 0.06, a.sin() * 0.22), 0.05 * f, None, Color::new(1.0, 0.45, 0.1, 0.9));
}
}
}
_ => {}
}
}
}
}
// treasury tiles with a chest (instead of a heap)
pub fn chest_tile(x: i32, z: i32) -> bool {
(x * 7 + z * 13).rem_euclid(3) == 0
}
// a chest: closed when empty, open and filled up to its content otherwise
fn chest(c: Vec3, h: u32, l: f32, fill: f32, res: u8) {
// quarter turns only: positions are rotated, box sizes swap width and depth
let q = h % 4;
let rot = q as f32 * std::f32::consts::FRAC_PI_2;
let (ca, sa) = (rot.cos(), rot.sin());
let at = |dx: f32, dz: f32, y: f32| c + vec3(dx * ca - dz * sa, y, dx * sa + dz * ca);
let cube = |p: Vec3, s: Vec3, col: Color| cube(p, if q % 2 == 1 { vec3(s.z, s.y, s.x) } else { s }, col);
let mcube = |m: Mat, p: Vec3, s: Vec3, col: Color| mc(m, p, if q % 2 == 1 { vec3(s.z, s.y, s.x) } else { s }, col);
let wood = rgb(66, 40, 22, l);
let band = rgb(150, 122, 60, l);
mcube(Mat::Wood, at(0.0, 0.0, 0.16), vec3(0.56, 0.32, 0.4), wood);
mcube(Mat::Iron, at(0.0, 0.0, 0.16), vec3(0.58, 0.05, 0.42), band);
mcube(Mat::Iron, at(0.0, 0.21, 0.18), vec3(0.08, 0.1, 0.02), band);
let fill = fill.clamp(0.0, 1.0);
if fill < 0.02 {
mcube(Mat::Wood, at(0.0, 0.0, 0.36), vec3(0.58, 0.08, 0.42), wood);
return;
}
// open lid leaning back
mcube(Mat::Wood, at(0.0, -0.24, 0.5), vec3(0.58, 0.32, 0.06), wood);
// content: the treasure kind decides the colour, the amount the height (heaped when full)
let col = match res {
1 => rgb(205, 210, 225, 1.0),
2 => rgb(220, 40, 70, 1.0),
3 => rgb(180, 235, 255, 1.0),
_ => rgb(240, 196, 60, 1.0),
};
let top = 0.06 + fill * 0.26;
cube(at(0.0, 0.0, top * 0.5 + 0.02), vec3(0.5, top, 0.34), col);
if fill > 0.7 {
cube(at(0.05, 0.02, top + 0.05), vec3(0.3, 0.08, 0.22), col);
for n in 0..3 {
let (dx, dz) = (((h >> (n * 3)) % 5) as f32 * 0.08 - 0.16, ((h >> (n * 2)) % 3) as f32 * 0.06 - 0.06);
cube(at(dx, dz, top + 0.1), vec3(0.06, 0.03, 0.06), rgb(255, 240, 180, 1.0));
}
}
}
fn adj8(a: usize, b: usize) -> bool {
let (ax, ay) = xy(a);
let (bx, by) = xy(b);
(ax - bx).abs() <= 1 && (ay - by).abs() <= 1
}
fn flame(p: Vec3, t: f32, k: f32) {
let f = 1.0 + 0.2 * (t * 11.0 + k).sin() + 0.1 * (t * 23.0 + k * 3.0).sin();
draw_sphere(p, 0.035 * f, None, Color::new(1.0, 0.85, 0.4, 1.0));
draw_sphere(p + vec3(0.0, 0.03, 0.0), 0.06 * f, None, Color::new(1.0, 0.5, 0.1, 0.35));
}
// ---------------------------------------------------------------- beds
fn bed(kind: EK, c: Vec3, h: u32, l: f32, t: f32) {
let rot = (h % 4) as f32 * 0.4 - 0.6;
let (ca, sa) = (rot.cos(), rot.sin());
let at = |dx: f32, dz: f32, y: f32| c + vec3(dx * ca - dz * sa, y, dx * sa + dz * ca);
match kind {
EK::Kobold => {
// straw pile
for n in 0..7 {
let (dx, dz) = (((h >> (n * 3)) % 7) as f32 * 0.08 - 0.24, ((h >> (n * 2 + 1)) % 7) as f32 * 0.08 - 0.24);
mc(Mat::Straw, at(dx, dz, 0.04), vec3(0.28, 0.06, 0.12), rgb(200, 170, 70, l * (0.85 + (n % 3) as f32 * 0.08)));
}
}
EK::Spider => {
// a web spanned flat over the floor and up to an imaginary corner
let wc = Color::new(0.85 * l + 0.1, 0.85 * l + 0.1, 0.9 * l + 0.1, 0.9);
let centre = c + vec3(0.0, 0.05, 0.0);
for n in 0..8 {
let a = n as f32 * std::f32::consts::FRAC_PI_4 + rot;
line3(centre, centre + vec3(a.cos() * 0.45, 0.0, a.sin() * 0.45), 0.015, wc);
}
for ring in 1..=3 {
let r = ring as f32 * 0.14;
for n in 0..8 {
let a0 = n as f32 * std::f32::consts::FRAC_PI_4 + rot;
let a1 = a0 + std::f32::consts::FRAC_PI_4;
line3(centre + vec3(a0.cos() * r, 0.0, a0.sin() * r), centre + vec3(a1.cos() * r, 0.0, a1.sin() * r), 0.012, wc);
}
}
// a few strands going up
for n in 0..3 {
let a = n as f32 * 2.1 + rot;
line3(centre + vec3(a.cos() * 0.4, 0.0, a.sin() * 0.4), centre + vec3(a.cos() * 0.15, 0.6, a.sin() * 0.15), 0.012, wc);
}
}
EK::Mage => {
// cushion with a starry blanket
mc(Mat::Cloth, at(0.0, 0.0, 0.06), vec3(0.62, 0.12, 0.5), rgb(70, 50, 120, l));
mc(Mat::Cloth, at(0.0, 0.08, 0.13), vec3(0.6, 0.03, 0.34), rgb(40, 30, 90, l));
for n in 0..4 {
let (dx, dz) = (((h >> (n * 4)) % 5) as f32 * 0.1 - 0.2, ((h >> (n * 4 + 2)) % 3) as f32 * 0.1 - 0.0);
cube(at(dx, dz, 0.15), vec3(0.04, 0.02, 0.04), rgb(240, 210, 90, 1.0));
}
mc(Mat::Cloth, at(0.0, -0.18, 0.15), vec3(0.3, 0.08, 0.14), rgb(200, 190, 220, l));
}
EK::Ogre => {
// heap of furs with a gnawed bone
for n in 0..5 {
let a = n as f32 * 1.3 + rot;
mc(Mat::Fur, c + vec3(a.cos() * 0.15, 0.05 + (n % 2) as f32 * 0.04, a.sin() * 0.15), vec3(0.38, 0.1, 0.3), rgb(110 + (n * 12) as u8, 76, 44, l));
}
cube(at(0.32, 0.2, 0.06), vec3(0.24, 0.04, 0.05), rgb(230, 220, 200, l));
}
EK::Jailer => {
// plank bunk with chains
mc(Mat::Wood, at(0.0, 0.0, 0.12), vec3(0.72, 0.06, 0.4), rgb(90, 66, 44, l));
for (dx, dz) in [(-0.3, -0.15), (0.3, -0.15), (-0.3, 0.15), (0.3, 0.15)] {
mc(Mat::Wood, at(dx, dz, 0.05), vec3(0.06, 0.1, 0.06), rgb(70, 50, 34, l));
}
for n in 0..6 {
mc(Mat::Iron, at(-0.3 + n as f32 * 0.12, 0.24, 0.03 + (n % 2) as f32 * 0.02), vec3(0.08, 0.03, 0.04), rgb(120, 120, 130, l));
}
}
EK::Priestess => {
// ember cushion (glowing coals in the glow pass)
mc(Mat::Cloth, at(0.0, 0.0, 0.05), vec3(0.6, 0.1, 0.48), rgb(150, 40, 30, l));
mc(Mat::Cloth, at(0.0, 0.0, 0.11), vec3(0.4, 0.03, 0.3), rgb(220, 110, 40, l));
}
EK::Bat => {
// roost: a beam between two posts to hang from
for side in [-0.32f32, 0.32] {
mc(Mat::Wood, at(side, 0.0, 0.45), vec3(0.06, 0.9, 0.06), rgb(90, 64, 40, l));
}
mc(Mat::Wood, at(0.0, 0.0, 0.88), vec3(0.72, 0.06, 0.06), rgb(110, 78, 48, l));
}
EK::Gargoyle => {
// stone plinth to sit on
mc(Mat::Stone, at(0.0, 0.0, 0.2), vec3(0.5, 0.4, 0.5), rgb(110, 110, 118, l));
mc(Mat::Stone, at(0.0, 0.0, 0.42), vec3(0.58, 0.06, 0.58), rgb(130, 130, 138, l));
}
EK::Owl => {
// twig nest on a post
mc(Mat::Wood, at(0.0, 0.0, 0.3), vec3(0.08, 0.6, 0.08), rgb(90, 64, 40, l));
for n in 0..6 {
let a = n as f32 * 1.05 + rot;
mc(Mat::Wood, c + vec3(a.cos() * 0.16, 0.62, a.sin() * 0.16), vec3(0.16, 0.06, 0.05), rgb(110 + (n * 6) as u8, 80, 44, l));
}
}
EK::Wyrm => {
// a big nest of dark rocks (the embers glow in the glow pass)
for n in 0..9 {
let a = n as f32 * 0.7 + rot;
mc(Mat::Stone, c + vec3(a.cos() * 0.42, 0.09, a.sin() * 0.42), vec3(0.2, 0.18 + (n % 3) as f32 * 0.04, 0.2), rgb(60, 44, 44, l));
}
}
EK::Chainbreaker => {
// a hollow of broken rocks and a burst chain
for n in 0..7 {
let a = n as f32 * 0.9 + rot;
mc(Mat::Stone, c + vec3(a.cos() * 0.36, 0.08, a.sin() * 0.36), vec3(0.18, 0.16 + (n % 3) as f32 * 0.05, 0.18), rgb(90, 86, 92, l * (0.8 + (n % 2) as f32 * 0.2)));
}
for n in 0..3 {
mc(Mat::Iron, at(-0.1 + n as f32 * 0.1, 0.05, 0.03), vec3(0.08, 0.03, 0.04), rgb(130, 130, 140, l));
}
}
_ => {
// defected heroes: a proper camp bed with a white sheet
mc(Mat::Wood, at(0.0, 0.0, 0.14), vec3(0.42, 0.05, 0.78), rgb(120, 86, 52, l));
mc(Mat::Cloth, at(0.0, 0.05, 0.18), vec3(0.38, 0.04, 0.5), rgb(220, 220, 228, l));
mc(Mat::Cloth, at(0.0, -0.28, 0.2), vec3(0.3, 0.06, 0.14), rgb(240, 240, 245, l));
for (dx, dz) in [(-0.18, -0.35), (0.18, -0.35), (-0.18, 0.35), (0.18, 0.35)] {
mc(Mat::Wood, at(dx, dz, 0.06), vec3(0.05, 0.12, 0.05), rgb(100, 70, 44, l));
}
}
}
let _ = t;
}
// an empty perch in the aerie: post with a crossbar
fn perch(c: Vec3, h: u32, l: f32) {
let dx = ((h >> 4) % 3) as f32 * 0.12 - 0.12;
mc(Mat::Wood, c + vec3(dx, 0.35, 0.0), vec3(0.06, 0.7, 0.06), rgb(96, 68, 42, l));
mc(Mat::Wood, c + vec3(dx, 0.68, 0.0), vec3(0.4, 0.05, 0.05), rgb(110, 78, 48, l));
}
// ---------------------------------------------------------------- training hall
fn training(c: Vec3, h: u32, l: f32, t: f32, busy: bool) {
match h % 3 {
0 => {
// spinning dummy: post, straw body, two arms
let spin = if busy { t * 6.0 } else { (h % 7) as f32 };
mc(Mat::Wood, c + vec3(0.0, 0.35, 0.0), vec3(0.08, 0.7, 0.08), rgb(100, 70, 44, l));
mc(Mat::Straw, c + vec3(0.0, 0.5, 0.0), vec3(0.2, 0.28, 0.2), rgb(200, 170, 100, l));
mc(Mat::Straw, c + vec3(0.0, 0.72, 0.0), vec3(0.14, 0.14, 0.14), rgb(210, 180, 120, l));
let (ax, az) = (spin.cos() * 0.3, spin.sin() * 0.3);
line3(c + vec3(-ax, 0.58, -az), c + vec3(ax, 0.58, az), 0.06, rgb(120, 84, 50, l));
mc(Mat::Wood, c + vec3(0.0, 0.03, 0.0), vec3(0.34, 0.06, 0.34), rgb(80, 56, 36, l));
}
1 => {
// weapon rack: frame with swords and an axe
mc(Mat::Wood, c + vec3(-0.3, 0.3, 0.0), vec3(0.06, 0.6, 0.06), rgb(100, 70, 44, l));
mc(Mat::Wood, c + vec3(0.3, 0.3, 0.0), vec3(0.06, 0.6, 0.06), rgb(100, 70, 44, l));
mc(Mat::Wood, c + vec3(0.0, 0.56, 0.0), vec3(0.66, 0.05, 0.08), rgb(110, 78, 48, l));
for n in 0..3 {
let x = -0.15 + n as f32 * 0.15;
mc(Mat::Iron, c + vec3(x, 0.3, 0.03), vec3(0.03, 0.46, 0.02), rgb(190, 195, 205, l));
mc(Mat::Wood, c + vec3(x, 0.12, 0.03), vec3(0.1, 0.02, 0.03), rgb(110, 80, 40, l));
}
}
_ => {
// target board on a stand, wobbling when hit
let wob = if busy { (t * 14.0).sin() * 0.05 } else { 0.0 };
mc(Mat::Wood, c + vec3(0.0, 0.25, 0.1), vec3(0.06, 0.5, 0.06), rgb(100, 70, 44, l));
let p = c + vec3(wob, 0.55, 0.0);
for (k, (s, col)) in [(0.5, rgb(230, 225, 210, l)), (0.36, rgb(200, 40, 40, l)), (0.22, rgb(230, 225, 210, l)), (0.1, rgb(200, 40, 40, l))].iter().enumerate() {
cube(p - vec3(0.0, 0.0, k as f32 * 0.01), vec3(*s, *s, 0.04), *col);
}
}
}
}
// ---------------------------------------------------------------- torture chamber
fn torture(c: Vec3, h: u32, l: f32, t: f32, busy: bool) {
match h % 3 {
0 => {
// the wheel turns while a prisoner is on it
let a0 = if busy { t * 1.5 } else { 0.3 };
let hub = c + vec3(0.0, 0.5, 0.0);
mc(Mat::Wood, c + vec3(-0.28, 0.25, 0.0), vec3(0.06, 0.5, 0.06), rgb(80, 56, 36, l));
mc(Mat::Wood, c + vec3(0.28, 0.25, 0.0), vec3(0.06, 0.5, 0.06), rgb(80, 56, 36, l));
let col = rgb(120, 84, 50, l);
for n in 0..8 {
let a = a0 + n as f32 * std::f32::consts::FRAC_PI_4;
let b = a + std::f32::consts::FRAC_PI_4;
let p = |a: f32| hub + vec3(0.0, a.sin() * 0.36, a.cos() * 0.36);
line3(hub, p(a), 0.03, col);
line3(p(a), p(b), 0.05, col);
}
mc(Mat::Iron, hub, vec3(0.62, 0.06, 0.06), rgb(90, 90, 100, l));
}
1 => {
// iron maiden, slightly open
mc(Mat::Iron, c + vec3(0.0, 0.42, 0.06), vec3(0.4, 0.84, 0.2), rgb(70, 70, 80, l));
mc(Mat::Iron, c + vec3(0.0, 0.78, 0.06), vec3(0.3, 0.12, 0.2), rgb(90, 90, 100, l));
mc(Mat::Iron, c + vec3(-0.12, 0.42, -0.14), vec3(0.18, 0.8, 0.04), rgb(80, 80, 92, l));
for n in 0..4 {
mc(Mat::Iron, c + vec3(0.08, 0.2 + n as f32 * 0.16, -0.06), vec3(0.03, 0.03, 0.12), rgb(170, 170, 180, l));
}
cube(c + vec3(0.0, 0.5, 0.17), vec3(0.08, 0.08, 0.02), rgb(160, 40, 40, l));
}
_ => {
// brazier on three legs with pokers (the coals glow in the glow pass)
for n in 0..3 {
let a = n as f32 * 2.094;
line3(c + vec3(a.cos() * 0.2, 0.0, a.sin() * 0.2), c + vec3(a.cos() * 0.08, 0.45, a.sin() * 0.08), 0.03, rgb(60, 60, 66, l));
}
mc(Mat::Iron, c + vec3(0.0, 0.5, 0.0), vec3(0.32, 0.08, 0.32), rgb(50, 46, 46, l));
line3(c + vec3(0.05, 0.52, 0.0), c + vec3(0.3, 0.8, 0.1), 0.02, rgb(120, 120, 130, l));
}
}
}
// ---------------------------------------------------------------- temple
fn temple(c: Vec3, h: u32, l: f32) {
// altar: stone block, red cloth, two candles (flames in the glow pass), a fire bowl behind
mc(Mat::Stone, c + vec3(0.0, 0.22, -0.1), vec3(0.9, 0.44, 0.42), rgb(70, 56, 72, l));
mc(Mat::Stone, c + vec3(0.0, 0.45, -0.1), vec3(0.94, 0.04, 0.46), rgb(90, 74, 92, l));
mc(Mat::Cloth, c + vec3(0.0, 0.3, 0.12), vec3(0.4, 0.3, 0.02), rgb(160, 40, 40, l));
cube(c + vec3(0.0, 0.3, 0.13), vec3(0.08, 0.08, 0.01), rgb(230, 190, 80, 1.0));
for n in [-0.32f32, 0.32] {
cube(c + vec3(n, 0.56, -0.18), vec3(0.05, 0.18, 0.05), rgb(230, 220, 200, l));
}
mc(Mat::Stone, c + vec3(0.0, 0.3, 0.15), vec3(0.1, 0.6, 0.1), rgb(60, 50, 62, l));
mc(Mat::Iron, c + vec3(0.0, 0.58, 0.15), vec3(0.24, 0.06, 0.24), rgb(90, 70, 40, l));
let _ = h;
}
fn candle_stand(c: Vec3, l: f32) {
mc(Mat::Iron, c + vec3(0.0, 0.2, 0.0), vec3(0.04, 0.4, 0.04), rgb(150, 120, 60, l));
mc(Mat::Iron, c + vec3(0.0, 0.02, 0.0), vec3(0.14, 0.04, 0.14), rgb(150, 120, 60, l));
cube(c + vec3(0.0, 0.45, 0.0), vec3(0.05, 0.1, 0.05), rgb(230, 220, 200, l));
}
// ---------------------------------------------------------------- library
fn shelf(c: Vec3, h: u32, l: f32) {
mc(Mat::Wood, c + vec3(0.0, 0.45, 0.0), vec3(0.86, 0.9, 0.3), rgb(70, 44, 28, l));
let colors = [(150, 40, 40), (40, 80, 140), (60, 120, 60), (160, 130, 50), (100, 60, 120), (180, 170, 150)];
for row in 0..3 {
let y = 0.18 + row as f32 * 0.28;
mc(Mat::Wood, c + vec3(0.0, y - 0.1, 0.0), vec3(0.8, 0.03, 0.32), rgb(90, 60, 38, l));
let mut x = -0.36;
let mut n = 0;
while x < 0.34 {
let w = 0.05 + ((h >> ((n + row * 5) % 24)) % 3) as f32 * 0.02;
let (r, gg, b) = colors[(hash(n, row, h) % 6) as usize];
let ht = 0.16 + ((h >> (n % 20)) % 3) as f32 * 0.025;
mc(Mat::Book, c + vec3(x + w * 0.5, y - 0.08 + ht * 0.5, -0.02), vec3(w * 0.9, ht, 0.24), rgb(r, gg, b, l));
x += w;
n += 1;
}
}
}
fn lectern(c: Vec3, l: f32) {
mc(Mat::Wood, c + vec3(0.0, 0.3, 0.0), vec3(0.1, 0.6, 0.1), rgb(90, 60, 38, l));
mc(Mat::Wood, c + vec3(0.0, 0.62, 0.0), vec3(0.42, 0.04, 0.3), rgb(110, 74, 46, l));
mc(Mat::Paper, c + vec3(0.0, 0.66, 0.0), vec3(0.3, 0.02, 0.2), rgb(230, 222, 200, l));
cube(c + vec3(0.0, 0.67, 0.0), vec3(0.01, 0.02, 0.2), rgb(120, 40, 40, l));
disc(c + vec3(0.0, 0.01, 0.0), 0.25, Color::new(0.0, 0.0, 0.0, 0.25), 12);
}