From 45060e85b78ffb161a09bdb9394a402aa4edbadc Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Sat, 7 Nov 2026 23:44:43 +0100 Subject: [PATCH] The other masters' hearts: Mossgrim a mossy knot of branches, Lady Nightwell a blue lily, Saffrax a golden sun in brass rings, each with its own beat Co-Authored-By: Claude Opus 5.5 --- src/render3d.rs | 133 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 130 insertions(+), 3 deletions(-) diff --git a/src/render3d.rs b/src/render3d.rs index b5be230..c373bf0 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -2802,6 +2802,126 @@ fn mushroom_stage(t: &Tile) -> u32 { } // the colour of a master side (the first master red, rivals green, blue and yellow) +// the hearts of the other masters (see draw_heart): hc the heart's centre, ph the phase of the +// beat (0..1), beat its strength; the floor veins pulse in the master's colour +#[allow(clippy::too_many_arguments)] +fn draw_rival_heart(base: Vec3, hc: Vec3, hp: f32, t: f32, ph: f32, beat: f32, right: Vec3, front: Vec3, side: u8) { + let up = Vec3::Y; + let mc = master_rgb(side); + let col = |v: Vec3, a: f32| Color::new(v.x.min(1.0), v.y.min(1.0), v.z.min(1.0), a); + let life = 0.45 + 0.55 * hp; // a hurt heart fades + match side { + // Mossgrim: a gnarled knot of branches grown into a heart, moss cushions on it that swell + // with every beat, green spores drifting up + 2 => { + let bark = Color::new(0.32, 0.22, 0.14, 1.0); + for k in 0..6 { + let a0 = k as f32 * 1.05; + let mut prev = hc + up * -0.5 + right * (a0.cos() * 0.08); + for q in 1..=7 { + let f = q as f32 / 7.0; + // two lobes: the branches part above and meet below + let side_ = if k % 2 == 0 { 1.0 } else { -1.0 }; + let x = side_ * (f * std::f32::consts::PI).sin() * 0.32 + (a0 + f * 4.0).sin() * 0.04; + let next = hc + up * (-0.5 + f * 0.85) + right * x + front * ((a0 + f * 3.0).cos() * 0.12); + line3(prev, next, 0.07 - f * 0.03, bark); + prev = next; + } + } + for k in 0..9 { + let a = k as f32 * 2.1; + let p = hc + right * (a.cos() * 0.26) + up * (a.sin() * 0.22 - 0.05) + front * 0.12; + let r = (0.07 + (k % 3) as f32 * 0.025) * (1.0 + beat * 0.35); + draw_sphere(p, r, None, col(vec3(0.16, 0.42, 0.14) * (life + beat * 0.5), 1.0)); + } + draw_sphere(hc, 0.7 * (1.0 + beat * 0.08), None, col(mc * 0.6, 0.1 + 0.1 * beat)); + for k in 0..12 { + let e = (t * 0.22 + k as f32 * 0.083).fract(); + let a = k as f32 * 2.4 + t * 0.4; + let p = hc + vec3(a.cos() * (0.3 + e * 0.6), -0.3 + e * 1.8, a.sin() * (0.3 + e * 0.6)); + draw_cube(p, Vec3::splat(0.035 * (1.0 - e) + 0.012), None, col(vec3(0.5, 1.0, 0.45), (1.0 - e) * 0.9)); + } + } + // Lady Nightwell: a blue lily whose petals open with every beat, silver moon motes circling + 3 => { + let open = 0.55 + 0.35 * beat + 0.1 * (t * 0.8).sin(); + for k in 0..6 { + let a = k as f32 * std::f32::consts::TAU / 6.0 + t * 0.15; + let dir = right * a.cos() + front * a.sin(); + let mut prev = hc + up * -0.25; + for q in 1..=6 { + let f = q as f32 / 6.0; + // a petal curves out and up, its tip bending outwards when open + let next = hc + up * (-0.25 + f * (0.75 - open * 0.25)) + dir * (f * (0.15 + open * 0.38) + f * f * open * 0.2); + let w = 0.16 * (f * std::f32::consts::PI).sin() + 0.02; + let shade = 0.55 + 0.45 * f; + line3(prev, next, w, col(vec3(0.35, 0.55, 1.0) * shade * life + vec3(0.25, 0.25, 0.3) * f, 1.0)); + prev = next; + } + } + // stamens, glowing at the beat + for k in 0..5 { + let a = k as f32 * 1.26; + let tip = hc + up * (0.2 + 0.05 * beat) + (right * a.cos() + front * a.sin()) * 0.08; + line3(hc - up * 0.15, tip, 0.02, Color::new(0.85, 0.85, 0.7, 1.0)); + draw_sphere(tip, 0.035, None, col(vec3(1.0, 0.95, 0.6), 1.0)); + } + draw_sphere(hc, 0.7 * (1.0 + beat * 0.1), None, col(mc * 0.7, 0.1 + 0.12 * beat)); + for k in 0..8 { + let a = t * 0.6 + k as f32 * 0.785; + let rr = 0.75 + 0.1 * (t * 1.3 + k as f32).sin(); + let p = hc + right * (a.cos() * rr) + front * (a.sin() * rr) + up * ((t * 0.9 + k as f32 * 1.7).sin() * 0.3); + draw_cube(p, Vec3::splat(0.04), None, Color::new(0.85, 0.9, 1.0, 0.85)); + } + } + // Saffrax: a golden sun inside turning brass rings, flaring with every beat, sulphur + // bubbles rising + _ => { + let flare = 1.0 + beat * 0.25; + draw_sphere(hc, 0.24 * flare, None, col(vec3(1.0, 0.82, 0.3) * (0.7 + 0.3 * life + beat * 0.3), 1.0)); + for k in 0..8 { + let a = k as f32 * 0.785 + t * 0.5; + let d = right * a.cos() + up * a.sin(); + line3(hc + d * 0.26 * flare, hc + d * (0.36 + 0.08 * beat) * flare, 0.035, col(vec3(1.0, 0.75, 0.2), 0.9)); + } + let brass = Color::new(0.72, 0.56, 0.24, 1.0); + let spin = t * 0.7 + ph * 1.5; + for (k, (r, tilt)) in [(0.5f32, 0.0f32), (0.58, 1.1), (0.66, 2.2)].iter().enumerate() { + let a = spin * (1.0 + k as f32 * 0.4) + tilt; + // a ring around an axis that turns with the time + let axis = (right * a.cos() + front * a.sin()).normalize_or_zero(); + let u = axis.cross(up).normalize_or_zero(); + let v = axis.cross(u).normalize_or_zero(); + for q in 0..16 { + let b0 = q as f32 / 16.0 * std::f32::consts::TAU; + let b1 = (q + 1) as f32 / 16.0 * std::f32::consts::TAU; + line3(hc + (u * b0.cos() + v * b0.sin()) * *r, hc + (u * b1.cos() + v * b1.sin()) * *r, 0.03, brass); + } + } + draw_sphere(hc, 0.8 * flare, None, col(mc * 0.7, 0.08 + 0.12 * beat)); + for k in 0..10 { + let e = (t * 0.3 + k as f32 * 0.1).fract(); + let a = k as f32 * 2.4; + let p = hc + vec3(a.cos() * (0.15 + e * 0.35), -0.35 + e * 2.0, a.sin() * (0.15 + e * 0.35)); + draw_sphere(p, 0.03 + 0.04 * e, None, Color::new(0.95, 0.9, 0.35, 0.7 * (1.0 - e))); + } + } + } + // veins across the floor in the master's colour, 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, col(mc * (0.4 + 0.6 * wave), (0.25 + 0.7 * wave) * (1.0 - f * 0.5) * (0.4 + 0.6 * hp))); + prev = next; + } + } +} + pub fn master_rgb(side: u8) -> Vec3 { match side { 2 => vec3(0.3, 0.95, 0.4), @@ -2829,7 +2949,10 @@ fn hue_to(tint: Option, c: Color) -> Color { // a soul core's crystal heart beating lub-dub (faster when hurt, cracked when badly hurt), // glowing veins across the floor, rising embers; `tint`: another master's colour for the red -fn draw_heart(base: Vec3, hp: f32, t: f32, cam_r: Vec3, tint: Option) { +// `side`: the master (0 the player); the other masters' hearts are their own: Mossgrim a mossy +// knot of branches, Lady Nightwell a blue lily, Saffrax a golden sun in brass rings +fn draw_heart(base: Vec3, hp: f32, t: f32, cam_r: Vec3, side: u8) { + let tint = (side != 0).then(|| master_rgb(side)); let tc = |r: f32, g: f32, b: f32, a: f32| hue_to(tint, Color::new(r, g, b, a)); let rate = 0.9 + (1.0 - hp) * 1.6; let ph = (t * rate).fract(); @@ -2840,6 +2963,10 @@ fn draw_heart(base: Vec3, hp: f32, t: f32, cam_r: Vec3, tint: Option) { 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 + if side != 0 { + draw_rival_heart(base, hc, hp, t, ph, beat, right, front, side); + return; + } let red = tc(0.38 + 0.38 * hp + 0.2 * beat, 0.02 + 0.04 * beat, 0.07, 1.0); let hot = tc(1.0, 0.35 + 0.25 * beat, 0.3, 0.7); // two lobes and the point below, then the facets as bright chips @@ -2895,12 +3022,12 @@ fn draw_heart(base: Vec3, hp: f32, t: f32, cam_r: Vec3, tint: Option) { fn draw_landmarks_glow(g: &Game, t: f32, cam_r: Vec3) { let (cx, cy) = g.map.core; let hp = (g.core_hp / CORE_HP).clamp(0.0, 1.0); - draw_heart(vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5), hp, t, cam_r, None); + draw_heart(vec3(cx as f32 + 1.5, 0.0, cy as f32 + 1.5), hp, t, cam_r, 0); // the other masters' hearts: the same heart in their colours for rv in g.rivals.iter().filter(|r| r.alive && g.map.shown(idx(r.core.0 + 1, r.core.1 + 1))) { let (rx, rz) = rv.centre(); let hp = (rv.hp / crate::rival::RIVAL_CORE_HP).clamp(0.0, 1.0); - draw_heart(vec3(rx, 0.0, rz), hp, t + rv.side as f32 * 0.37, cam_r, Some(master_rgb(rv.side))); + draw_heart(vec3(rx, 0.0, rz), hp, t + rv.side as f32 * 0.37, cam_r, rv.side); } // 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() {