From 33b9879b4c9afc065fbe2f6b50f77b31bc0e80dc Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Tue, 3 Nov 2026 20:18:50 +0100 Subject: [PATCH] Dark demonic dungeon towers, a beating crystal heart and demon gate portals - own towers in black obsidian and dark iron with red glowing joints, bone spikes, skulls, chains; arrow spire with a ballista, hellmouth cannon with a demon-head barrel, dark spire with horns and a crystal; every level adds parts - soul core: a crystal heart beating lub-dub (faster when hurt, cracked at low health) held by chains on an obsidian throne, veins with a pulse across the floor, rising embers - portals: an arch of stone and bone with a skull and horns, a vertical vortex with runes and falling sparks; grey and dead until claimed Co-Authored-By: Claude Opus 5.5 --- src/render3d.rs | 438 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 367 insertions(+), 71 deletions(-) diff --git a/src/render3d.rs b/src/render3d.rs index 47dd282..d905798 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -1055,7 +1055,7 @@ impl R3d { draw_overlay(&g.map, ov, self.wall_sub); draw_entry_warnings(g, t, self.wall_sub); // translucent glows and effects last (they write depth, see glow_material) - draw_landmarks_glow(g, t); + draw_landmarks_glow(g, t, cam.basis().1); draw_towers_glow(g, t); draw_gear_glow(g, cam, t); crate::props::draw_props_glow(g, cam, t); @@ -1357,8 +1357,154 @@ fn draw_gear_glow(g: &Game, cam: &Cam, t: f32) { } } +// the dungeon's own towers: black obsidian and dark iron, bone spikes, skulls and chains (the +// red glowing joints are drawn in the glow pass); every level adds visible parts +fn draw_dark_tower(tw: &Tower) { + let (x, z) = tw.pos(); + let c = vec3(x, 0.0, z); + let h = tower_height(tw); + let f = 0.8 + tw.flash * 1.5; + let l = tw.level; + let obs = |k: u8| shade_c(30 + k, 24 + k, 34 + k, f); + let iron = shade_c(58, 56, 64, f); + let bone = shade_c(214, 204, 178, f); + let blood = shade_c(128, 18, 26, f); + let bx = |p: Vec3, s: Vec3, cl: Color| draw_cube(c + p, s, None, cl); + let skull = |p: Vec3, dir: Vec3| { + bx(p, vec3(0.15, 0.14, 0.15), bone); + let side = vec3(dir.z, 0.0, -dir.x); + for sgn in [-1.0f32, 1.0] { + bx(p + dir * 0.076 + side * (0.035 * sgn) + vec3(0.0, 0.015, 0.0), vec3(0.04, 0.04, 0.04), shade_c(20, 8, 10, 1.0)); + } + bx(p + vec3(0.0, -0.08, 0.0) + dir * 0.02, vec3(0.1, 0.04, 0.1), bone); + }; + let faces = [vec3(0.0, 0.0, 1.0), vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, -1.0), vec3(-1.0, 0.0, 0.0)]; + match tw.kind { + TowerKind::Arrow => { + // spike spire: stepped obsidian foot, a tapering keep, a crown of bone spikes + bx(vec3(0.0, 0.07, 0.0), vec3(0.92, 0.14, 0.92), obs(4)); + bx(vec3(0.0, 0.19, 0.0), vec3(0.78, 0.1, 0.78), obs(10)); + let segs = 4; + for k in 0..segs { + let y0 = 0.24 + (h - 0.24) * k as f32 / segs as f32; + let hh = (h - 0.24) / segs as f32; + let w = 0.64 - k as f32 * 0.04; + bx(vec3(0.0, y0 + hh * 0.5, 0.0), vec3(w, hh - 0.03, w), obs((k * 4) as u8 + 8)); + } + // buttress spikes on the corners + for (dx, dz) in [(0.36f32, 0.36f32), (-0.36, 0.36), (0.36, -0.36), (-0.36, -0.36)] { + bx(vec3(dx, 0.35, dz), vec3(0.12, 0.5, 0.12), obs(6)); + line3(c + vec3(dx, 0.58, dz), c + vec3(dx * 1.45, 0.95, dz * 1.45), 0.06, bone); + } + bx(vec3(0.0, h + 0.03, 0.0), vec3(0.86, 0.07, 0.86), iron); + // crown of bone spikes (more with every level) + let n = 4 + 2 * l as usize; + for k in 0..n { + let a = k as f32 / n as f32 * std::f32::consts::TAU; + let p = c + vec3(a.cos() * 0.4, h + 0.06, a.sin() * 0.4); + line3(p, p + vec3(a.cos() * 0.08, 0.26 + 0.06 * (k % 2) as f32, a.sin() * 0.08), 0.05, bone); + } + // a small ballista turned towards the last target + let (ax, az) = (tw.aim.cos(), tw.aim.sin()); + line3(c + vec3(-ax * 0.25, h + 0.16, -az * 0.25), c + vec3(ax * 0.32, h + 0.16, az * 0.32), 0.06, iron); + line3(c + vec3(-az * 0.22, h + 0.2, ax * 0.22), c + vec3(az * 0.22, h + 0.2, -ax * 0.22), 0.04, shade_c(90, 60, 40, f)); + skull(vec3(0.0, h * 0.72, 0.33), faces[0]); + if l >= 2 { + // banner of the dungeon + bx(vec3(0.34, h + 0.5, -0.34), vec3(0.03, 0.9, 0.03), iron); + bx(vec3(0.46, h + 0.72, -0.34), vec3(0.22, 0.34, 0.02), blood); + skull(vec3(0.33, h * 0.72, 0.0), faces[1]); + } + if l >= 3 { + skull(vec3(0.0, h * 0.72, -0.33), faces[2]); + skull(vec3(-0.33, h * 0.72, 0.0), faces[3]); + for sgn in [-1.0f32, 1.0] { + line3(c + vec3(sgn * 0.3, h + 0.08, 0.0), c + vec3(sgn * 0.55, h + 0.55, 0.0), 0.08, bone); + line3(c + vec3(sgn * 0.55, h + 0.55, 0.0), c + vec3(sgn * 0.45, h + 0.75, 0.0), 0.05, bone); + } + } + } + TowerKind::Cannon => { + // hellmouth: a riveted iron drum on an obsidian foot, a demon-head barrel on top + bx(vec3(0.0, 0.07, 0.0), vec3(0.96, 0.14, 0.96), obs(4)); + for k in 0..3 { + let y = 0.14 + (h - 0.14) * (k as f32 + 0.5) / 3.0; + let r = 0.86 - k as f32 * 0.04; + bx(vec3(0.0, y, 0.0), vec3(r, (h - 0.14) / 3.0 - 0.02, r * 0.8), obs(14)); + bx(vec3(0.0, y, 0.0), vec3(r * 0.8, (h - 0.14) / 3.0 - 0.02, r), obs(10)); + // rivets + for q in 0..8 { + let a = q as f32 * 0.785; + bx(vec3(a.cos() * r * 0.46, y, a.sin() * r * 0.46), Vec3::splat(0.045), iron); + } + } + bx(vec3(0.0, h + 0.04, 0.0), vec3(0.66, 0.08, 0.66), iron); + let (ax, az) = (tw.aim.cos(), tw.aim.sin()); + let along = |d: f32, y: f32| c + vec3(ax * d, h + y, az * d); + // the demon head: a heavy block with horns, its open jaw is the muzzle + draw_cube(along(0.0, 0.24), Vec3::splat(0.34), None, iron); + for k in 1..4 { + draw_cube(along(k as f32 * 0.14, 0.24), Vec3::splat(0.3 - k as f32 * 0.02), None, shade_c(44, 42, 50, f)); + } + draw_cube(along(0.56, 0.24), Vec3::splat(0.26), None, obs(20)); + for sgn in [-1.0f32, 1.0] { + let side = vec3(-az * sgn, 0.0, ax * sgn); + line3(along(0.05, 0.4) + side * 0.14, along(-0.1, 0.72) + side * 0.26, 0.07, bone); + } + if l >= 2 { + // chains around the drum + for k in 0..8 { + let a0 = k as f32 * 0.785; + let a1 = a0 + 0.785; + line3(c + vec3(a0.cos() * 0.47, h * 0.55, a0.sin() * 0.47), c + vec3(a1.cos() * 0.47, h * 0.5 - 0.04, a1.sin() * 0.47), 0.035, iron); + } + skull(vec3(0.0, h * 0.5, 0.45), faces[0]); + } + if l >= 3 { + for k in 0..8 { + let a = k as f32 * 0.785 + 0.39; + let p = c + vec3(a.cos() * 0.44, h * 0.85, a.sin() * 0.44); + line3(p, p + vec3(a.cos() * 0.25, 0.08, a.sin() * 0.25), 0.05, bone); + } + skull(vec3(0.0, h * 0.5, -0.45), faces[2]); + } + } + _ => { + // dark spire: an obsidian needle between curved horns, claws hold the crystal + bx(vec3(0.0, 0.06, 0.0), vec3(0.86, 0.12, 0.86), obs(4)); + bx(vec3(0.0, 0.17, 0.0), vec3(0.6, 0.1, 0.6), obs(12)); + bx(vec3(0.0, h * 0.5, 0.0), vec3(0.34, h, 0.34), obs(8)); + bx(vec3(0.0, h * 0.3, 0.0), vec3(0.44, h * 0.3, 0.44), obs(14)); + let horns = 2 + l as usize; + for k in 0..horns { + let a = k as f32 / horns as f32 * std::f32::consts::TAU + 0.4; + let mut prev = c + vec3(a.cos() * 0.4, 0.2, a.sin() * 0.4); + for q in 1..=5 { + let fq = q as f32 / 5.0; + let r = 0.4 + (fq * 3.14).sin() * 0.18 - fq * 0.22; + let next = c + vec3(a.cos() * r, 0.2 + fq * (h - 0.1), a.sin() * r); + line3(prev, next, 0.09 - fq * 0.05, if q == 5 { bone } else { obs(18) }); + prev = next; + } + } + for k in 0..4 { + let a = k as f32 * 1.57 + 0.78; + line3(c + vec3(a.cos() * 0.14, h, a.sin() * 0.14), c + vec3(a.cos() * 0.24, h + 0.42, a.sin() * 0.24), 0.05, bone); + } + skull(vec3(0.0, h * 0.35, 0.24), faces[0]); + if l >= 3 { + skull(vec3(0.0, h * 0.35, -0.24), faces[2]); + } + } + } +} + fn draw_towers_solid(g: &Game) { for tw in &g.towers { + if !tw.hero && tw.kind != TowerKind::Rune { + draw_dark_tower(tw); + continue; + } let (x, z) = tw.pos(); let c = vec3(x, 0.0, z); let h = tower_height(tw); @@ -1477,6 +1623,55 @@ fn draw_towers_solid(g: &Game) { fn draw_towers_glow(g: &Game, t: f32) { for (k, tw) in g.towers.iter().enumerate() { + if !tw.hero && tw.kind != TowerKind::Rune { + // red glowing joints, arrow slits and the hellmouth's fire + let (x, z) = tw.pos(); + let c = vec3(x, 0.0, z); + let h = tower_height(tw); + let glow = 0.55 + 0.35 * (t * 2.2 + k as f32).sin() + tw.flash * 2.0; + let ember = Color::new(1.0, 0.22, 0.08, glow.min(1.0)); + match tw.kind { + TowerKind::Arrow => { + for q in 1..4 { + let y = 0.24 + (h - 0.24) * q as f32 / 4.0; + let w = 0.64 - q as f32 * 0.04 + 0.01; + draw_cube(c + vec3(0.0, y - 0.015, 0.0), vec3(w, 0.025, w), None, ember); + } + for d in [vec3(0.0, 0.0, 0.31), vec3(0.31, 0.0, 0.0), vec3(0.0, 0.0, -0.31), vec3(-0.31, 0.0, 0.0)] { + draw_cube(c + d + vec3(0.0, h * 0.45, 0.0), vec3(if d.x == 0.0 { 0.05 } else { 0.02 }, 0.18, if d.z == 0.0 { 0.05 } else { 0.02 }), None, Color::new(1.0, 0.35, 0.1, 0.9)); + } + } + TowerKind::Cannon => { + let (ax, az) = (tw.aim.cos(), tw.aim.sin()); + let m = c + vec3(ax * 0.7, h + 0.24, az * 0.7); + draw_sphere(m, 0.1 * (1.0 + tw.flash * 3.0), None, Color::new(1.0, 0.5, 0.1, 0.95)); + draw_sphere(m, 0.2 * (1.0 + tw.flash * 3.0), None, Color::new(1.0, 0.3, 0.05, 0.3)); + for q in 1..3 { + let y = 0.14 + (h - 0.14) * q as f32 / 3.0; + draw_cube(c + vec3(0.0, y, 0.0), vec3(0.84, 0.02, 0.84), None, ember); + } + } + _ => { + // the crystal in the claws and shards around it + let cc = vec3(x, h + 0.42 + (t * 2.0 + k as f32).sin() * 0.05, z); + let pulse = 1.0 + (t * 4.0 + k as f32).sin() * 0.1 + tw.flash * 2.0; + draw_sphere(cc, 0.15 * pulse, None, Color::new(0.85, 0.3, 1.0, 0.95)); + draw_sphere(cc, 0.32 * pulse, None, Color::new(0.7, 0.2, 1.0, 0.22)); + for q in 0..(1 + tw.level) { + let a = t * 1.5 + q as f32 * 2.1; + draw_cube(cc + vec3(a.cos() * 0.42, (t * 2.0 + q as f32).sin() * 0.12, a.sin() * 0.42), vec3(0.05, 0.12, 0.05), None, Color::new(0.85, 0.4, 1.0, 0.9)); + } + for q in 0..3 { + let y = h * (0.45 + q as f32 * 0.17); + let gl = 0.4 + 0.6 * ((t * 2.5 - q as f32 * 0.8 + k as f32).sin() * 0.5 + 0.5); + for (dx, dz) in [(0.175f32, 0.0f32), (-0.175, 0.0), (0.0, 0.175), (0.0, -0.175)] { + draw_cube(c + vec3(dx, y, dz), vec3(if dx == 0.0 { 0.08 } else { 0.02 }, 0.07, if dz == 0.0 { 0.08 } else { 0.02 }), None, Color::new(1.0, 0.25, 0.4, gl)); + } + } + } + } + continue; + } if tw.kind == TowerKind::Rune { // forge glow on the crown, runes on the keep and three rune stones circling it let (x, z) = tw.pos(); @@ -1551,6 +1746,21 @@ pub fn disc(c: Vec3, r: f32, color: Color, seg: usize) { draw_mesh(&Mesh { vertices: v, indices: i, texture: None }); } +// an upright ellipse in the x-y plane (portal vortex) +fn vdisc(c: Vec3, rx: f32, ry: f32, color: Color, seg: usize) { + 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]; + let mut v = vec![Vertex { position: c, uv: Vec2::ZERO, color: cc, normal: Vec4::Z }]; + for k in 0..seg { + let a = k as f32 / seg as f32 * std::f32::consts::TAU; + v.push(Vertex { position: c + vec3(a.cos() * rx, a.sin() * ry, 0.0), uv: Vec2::ZERO, color: cc, normal: Vec4::Z }); + } + let mut i = Vec::with_capacity(seg * 3); + for k in 0..seg as u16 { + i.extend_from_slice(&[0, 1 + k, 1 + (k + 1) % seg as u16]); + } + draw_mesh(&Mesh { vertices: v, indices: i, texture: None }); +} + fn ring(c: Vec3, r: f32, color: Color) { let seg = 32; for k in 0..seg { @@ -1561,19 +1771,37 @@ fn ring(c: Vec3, r: f32, color: Color) { } fn draw_landmarks_solid(g: &Game, t: f32) { - // soul core: stepped obsidian pedestal with four spikes holding the orb + // soul core: an obsidian throne whose four curved spikes hold the crystal heart in chains 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.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 obs = |k: u8| Color::from_rgba(28 + k, 14 + k / 2, 20 + k / 2, 255); + draw_cube(c + vec3(0.0, 0.08, 0.0), vec3(2.9, 0.16, 2.9), None, obs(6)); + draw_cube(c + vec3(0.0, 0.22, 0.0), vec3(2.3, 0.14, 2.3), None, obs(14)); + draw_cube(c + vec3(0.0, 0.36, 0.0), vec3(1.5, 0.14, 1.5), None, obs(24)); + draw_cube(c + vec3(0.0, 0.48, 0.0), vec3(0.8, 0.1, 0.8), None, obs(32)); + let heart = c + vec3(0.0, 1.55, 0.0); + let iron = Color::from_rgba(70, 64, 72, 255); + for (k, (dx, dz)) in [(-1.2f32, -1.2f32), (1.2, -1.2), (1.2, 1.2), (-1.2, 1.2)].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)); + draw_cube(b + vec3(0.0, 0.3, 0.0), vec3(0.36, 0.6, 0.36), None, obs(4)); + // a curved spike leaning towards the heart + let mut prev = b + vec3(0.0, 0.55, 0.0); + for q in 1..=5 { + let f = q as f32 / 5.0; + let next = b + vec3(dx * 0.22 * f * f, 0.55 + f * 0.9, dz * 0.22 * f * f); + line3(prev, next, 0.2 - f * 0.14, if q == 5 { Color::from_rgba(200, 180, 150, 255) } else { obs(10) }); + prev = next; + } + // a sagging chain from the spike to the heart (links alternate) + let top = b + vec3(dx * 0.12, 1.3, dz * 0.12); + for q in 0..9 { + let f0 = q as f32 / 9.0; + let f1 = (q + 1) as f32 / 9.0; + let sag = |f: f32| -0.18 * (f * std::f32::consts::PI).sin() + (t * 1.3 + k as f32).sin() * 0.02; + let p0 = top.lerp(heart, f0 * 0.82) + vec3(0.0, sag(f0), 0.0); + let p1 = top.lerp(heart, f1 * 0.82) + vec3(0.0, sag(f1), 0.0); + line3(p0, p1, if q % 2 == 0 { 0.06 } else { 0.035 }, iron); + } } // rival masters' cores: obsidian pedestal (the orb glows in the glow pass) for rv in g.rivals.iter().filter(|r| r.alive) { @@ -1585,15 +1813,44 @@ fn draw_landmarks_solid(g: &Game, t: f32) { draw_cube(c + vec3(dx, 0.5, dz), vec3(0.22, 1.0, 0.22), None, Color::from_rgba(30, 28, 38, 255)); } } - // portal circles: eight standing stones of different height - for &(px, py) in &g.map.portals { + // portals: a demon gate - an arch of stone blocks with bone ribs and horns on a stepped + // platform (the vortex glows in the glow pass); grey and dead until claimed + 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.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)); + let stone = |k: u8| if owned { Color::from_rgba(46 + k, 30 + k / 2, 58 + k, 255) } else { Color::from_rgba(56 + k, 54 + k, 60 + k, 255) }; + let bone = if owned { Color::from_rgba(212, 200, 176, 255) } else { Color::from_rgba(150, 146, 140, 255) }; + draw_cube(pc + vec3(0.0, 0.05, 0.0), vec3(2.9, 0.1, 2.9), None, stone(0)); + draw_cube(pc + vec3(0.0, 0.14, 0.0), vec3(2.4, 0.08, 1.4), None, stone(8)); + let r = 1.25; + for side in [-1.0f32, 1.0] { + // pillars of stacked, slightly offset blocks + for k in 0..5 { + let off = if k % 2 == 0 { 0.03 } else { -0.03 }; + draw_cube(pc + vec3(side * (r + off), 0.32 + k as f32 * 0.36, 0.0), vec3(0.42, 0.34, 0.5), None, stone((k * 5) as u8 + 6)); + } + // a bone rib along the inner edge + line3(pc + vec3(side * (r - 0.27), 0.2, 0.26), pc + vec3(side * (r - 0.27), 1.95, 0.26), 0.06, bone); + line3(pc + vec3(side * (r - 0.27), 0.2, -0.26), pc + vec3(side * (r - 0.27), 1.95, -0.26), 0.06, bone); + } + // the arch: blocks on a half circle + for k in 0..=8 { + let a = k as f32 / 8.0 * std::f32::consts::PI; + let p = pc + vec3(a.cos() * r, 2.0 + a.sin() * r * 0.9, 0.0); + draw_cube(p, vec3(0.4, 0.34, 0.52), None, stone(16 + (k % 3) as u8 * 4)); + } + // keystone skull and horns + let top = pc + vec3(0.0, 2.0 + r * 0.9 + 0.12, 0.0); + draw_cube(top, vec3(0.3, 0.28, 0.3), None, bone); + for sgn in [-1.0f32, 1.0] { + draw_cube(top + vec3(sgn * 0.07, 0.03, 0.155), vec3(0.07, 0.07, 0.02), None, Color::from_rgba(20, 8, 16, 255)); + let mut prev = top + vec3(sgn * 0.15, 0.08, 0.0); + for q in 1..=4 { + let f = q as f32 / 4.0; + let next = top + vec3(sgn * (0.15 + f * 0.55), 0.08 + (f * 2.2).sin() * 0.45, 0.0); + line3(prev, next, 0.1 - f * 0.07, bone); + prev = next; + } } } // gate pillars @@ -1636,34 +1893,68 @@ fn draw_landmarks_solid(g: &Game, t: f32) { let _ = t; } -fn draw_landmarks_glow(g: &Game, t: f32) { - // soul core: pulsing orb (faster and more unsteady when hurt), energy threads to the - // spikes, runes circling around it +fn draw_landmarks_glow(g: &Game, t: f32, cam_r: Vec3) { + // soul core: a faceted crystal heart beating lub-dub (faster when hurt, cracked when badly + // hurt), glowing veins across the floor, rising embers let (cx, cy) = g.map.core; let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0); - 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 rate = 0.9 + (1.0 - hp) * 1.6; + let ph = (t * rate).fract(); + let beat = (-((ph - 0.02) / 0.06).powi(2)).exp() + 0.65 * (-((ph - 0.2) / 0.06).powi(2)).exp(); + let pulse = 1.0 + beat * 0.09; 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)); + let hc = base + vec3(0.0, 1.55 + (t * 1.1).sin() * 0.03, 0.0); + let right = vec3(cam_r.x, 0.0, cam_r.z).normalize_or_zero(); + let up = Vec3::Y; + let eye = EYE.with(|e| e.get()); + let front = vec3(eye.x, 0.0, eye.z).normalize_or_zero(); // towards the viewer + let red = Color::new(0.38 + 0.38 * hp + 0.2 * beat, 0.02 + 0.04 * beat, 0.07, 1.0); + let hot = Color::new(1.0, 0.35 + 0.25 * beat, 0.3, 0.7); + // two lobes and the point below, then the facets as bright chips + let s0 = 0.36 * pulse; + for sgn in [-1.0f32, 1.0] { + draw_sphere(hc + right * (sgn * 0.21 * pulse) + up * (0.12 * pulse), s0 * 0.85, None, red); } - 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)); + for (k, (y, r)) in [(-0.05f32, 0.36f32), (-0.22, 0.28), (-0.36, 0.18), (-0.46, 0.09)].iter().enumerate() { + draw_sphere(hc + up * (y * pulse), r * pulse, None, if k == 0 { red } else { Color::new(red.r * 0.9, red.g, red.b, 1.0) }); } + draw_sphere(hc + right * (-0.12 * pulse) + up * (0.2 * pulse), 0.1 * pulse, None, Color::new(1.0, 0.75, 0.7, 0.75)); + for k in 0..7 { + let a = k as f32 * 0.9 + 0.4; + let p = hc + right * (a.cos() * 0.22 * pulse) + up * (a.sin() * 0.2 * pulse - 0.05) + front * 0.3; + draw_cube(p, Vec3::splat(0.05), None, Color::new(1.0, 0.5 + 0.3 * ((t * 3.0 + k as f32).sin() * 0.5 + 0.5), 0.45, 0.8)); + } + draw_sphere(hc, 0.75 * pulse, None, Color::new(1.0, 0.15, 0.15, 0.16 + 0.12 * beat)); + draw_sphere(hc, 1.1 * pulse, None, Color::new(1.0, 0.1, 0.1, 0.06 + 0.05 * beat)); + // cracks when hurt + let cracks = ((1.0 - hp) * 7.0) as usize; + for k in 0..cracks.min(6) { + let a = k as f32 * 2.3; + let p0 = hc + right * (a.cos() * 0.08) + up * (a.sin() * 0.1); + let p1 = p0 + right * (a.cos() * 0.25) + up * ((a * 1.7).sin() * 0.22); + line3(p0 + front * 0.37, p1 + front * 0.33, 0.025, Color::new(0.08, 0.0, 0.02, 1.0)); + } + // veins across the floor, a pulse running outwards with every beat + for k in 0..8 { + let a = k as f32 * 0.785 + 0.2; + let mut prev = base + vec3(a.cos() * 1.3, 0.02, a.sin() * 1.3); + for q in 1..=6 { + let f = q as f32 / 6.0; + let wig = ((k * 7 + q * 3) as f32).sin() * 0.18; + let next = base + vec3(a.cos() * (1.3 + f * 2.2) - a.sin() * wig, 0.02, a.sin() * (1.3 + f * 2.2) + a.cos() * wig); + let wave = (-((f - ph * 1.6) / 0.18).powi(2)).exp(); + line3(prev, next, 0.05, Color::new(1.0, 0.15 + 0.3 * wave, 0.1, (0.25 + 0.7 * wave) * (1.0 - f * 0.5) * (0.4 + 0.6 * hp))); + prev = next; + } + } + // embers rising from the heart + for k in 0..10 { + let e = (t * 0.35 + k as f32 * 0.137).fract(); + let a = k as f32 * 2.4; + let p = hc + vec3(a.cos() * (0.2 + e * 0.5), -0.2 + e * 2.0, a.sin() * (0.2 + e * 0.5)); + draw_cube(p, Vec3::splat(0.04 * (1.0 - e) + 0.01), None, Color::new(1.0, 0.45 + 0.4 * (1.0 - e), 0.15, 1.0 - e)); + } + let _ = hot; // rival masters' cores in their colours for rv in g.rivals.iter().filter(|r| r.alive) { let (rx, rz) = rv.centre(); @@ -1678,42 +1969,47 @@ fn draw_landmarks_glow(g: &Game, t: f32) { draw_cube(c + vec3(a.cos() * 0.9, (t + k as f32).sin() * 0.2, a.sin() * 0.9), vec3(0.08, 0.14, 0.04), None, Color::new(col.x, col.y, col.z, 0.8)); } } - // portals: a circle of standing stones with runes, the vortex, a column of light, sparks + // portals: runes on the arch and a vertical vortex inside it, sparks pouring out and falling 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 pc = vec3(px as f32 + 1.5, 0.0, py as f32 + 1.5); + let arm = if owned { Color::new(0.8, 0.35, 1.0, 1.0) } else { Color::new(0.45, 0.42, 0.5, 1.0) }; + let mid = pc + vec3(0.0, 1.3, 0.0); + let (rx, ry) = (0.95, 1.55); + // the dark heart of the vortex: a vertical disc in the arch + vdisc(mid, rx, ry, if owned { Color::new(0.16, 0.02, 0.26, 0.92) } else { Color::new(0.08, 0.08, 0.1, 0.85) }, 28); + let speed = if owned { 2.0 } else { 0.25 }; + for armk in 0..3 { + for k in 0..22 { + let f = k as f32 / 22.0; + let ang = t * speed + armk as f32 * 2.094 + f * 5.0; + let p = mid + vec3(ang.cos() * rx * f, ang.sin() * ry * f * 0.9, 0.0); + draw_cube(p, Vec3::splat(0.05 + 0.05 * f), None, Color::new(arm.r, arm.g, arm.b, if owned { 0.3 + 0.7 * f } else { 0.25 })); } } - // 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 })); + // runes on the pillars and the arch + for k in 0..10 { + let glow = 0.5 + 0.5 * (t * 2.0 - k as f32 * 0.6).sin(); + let p = if k < 6 { + let side = if k % 2 == 0 { -1.0 } else { 1.0 }; + pc + vec3(side * 1.25, 0.4 + (k / 2) as f32 * 0.6, 0.27) + } else { + let a = (k - 6) as f32 / 3.0 * std::f32::consts::PI * 0.7 + 0.47; + pc + vec3(a.cos() * 1.25, 2.0 + a.sin() * 1.12, 0.27) + }; + draw_cube(p, vec3(0.12, 0.14, 0.02), None, Color::new(arm.r, arm.g, arm.b, if owned { 0.35 + 0.65 * glow } else { 0.2 })); } 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)); + draw_sphere(mid, 0.35 + 0.05 * (t * 3.0).sin(), None, Color::new(0.95, 0.75, 1.0, 0.5)); + // sparks pour out of the vortex and fall to the floor + for k in 0..14 { + let e = (t * 0.5 + k as f32 * 0.071).fract(); + let a = k as f32 * 2.4; + let dir = if k % 2 == 0 { 1.0 } else { -1.0 }; + let p = mid + vec3(a.cos() * 0.5 * (1.0 - e), a.sin() * 0.6 + e * 0.4 - e * e * 1.9, dir * e * 1.1); + draw_cube(p, Vec3::splat(0.045 * (1.0 - e) + 0.015), None, Color::new(0.95, 0.65, 1.0, 1.0 - e)); } + disc(pc + vec3(0.0, 0.2, 0.0), 1.1, Color::new(0.6, 0.25, 0.9, 0.12 + 0.05 * (t * 2.0).sin()), 24); } } // gate light