use core::f32::consts::{FRAC_PI_2, PI, TAU};
use glam::{Mat4, Quat, Vec3};
use crate::generators::monster::rig::{
Gait, GaitDesc, MonsterRig, PrimTint, RigBuilder, add_joint, push_cone, push_flat,
};
use crate::mesh::{Mesh, cuboid, lathe};
use crate::recipe::{BossArchetype, BossParams};
use super::meta::{ColliderJson, PartMeta, WeakPointMeta};
pub struct BossRig {
pub rig: MonsterRig,
pub weak_points: Vec<WeakPointMeta>,
pub parts: Vec<PartMeta>,
pub extra_mesh: Option<Mesh>,
}
pub fn build_boss_rig(p: &BossParams) -> BossRig {
match p.archetype {
BossArchetype::Hydra => plan_hydra(p),
BossArchetype::Colossus => plan_colossus(p),
BossArchetype::Lich => plan_lich(p),
BossArchetype::SwarmQueen => plan_swarm_queen(p),
BossArchetype::DragonLord => plan_dragon_lord(p),
}
}
fn plan_hydra(p: &BossParams) -> BossRig {
let s = p.size.clamp(0.4, 8.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let mut parts = Vec::new();
let mut weak_points = Vec::new();
let hips = r.joint(None, "hips", v(0.0, 0.5 * s, -0.8 * s));
let shoulders = r.joint(Some(hips), "shoulders", v(0.0, 0.6 * s, 0.45 * s));
let core = r.joint(Some(shoulders), "core", v(0.0, 1.0 * s, 0.6 * s));
let tail1 = r.joint(Some(hips), "tail1", v(0.0, 0.36 * s, -1.5 * s));
let tail2 = r.joint(Some(tail1), "tail2", v(0.0, 0.22 * s, -2.2 * s));
let tail3 = r.joint(Some(tail2), "tail3", v(0.0, 0.1 * s, -2.9 * s));
r.ellip(hips, shoulders, 0.5 * s, 0.12 * s, 0, 0.18 * s);
r.ellip(shoulders, core, 0.32 * s, 0.06 * s, 0, 0.12 * s);
r.cone(hips, tail1, 0.34 * s, 0.18 * s, 0, 0.08 * s);
r.cone(tail1, tail2, 0.18 * s, 0.08 * s, 0, 0.06 * s);
r.cone(tail2, tail3, 0.08 * s, 0.02 * s, 0, 0.05 * s);
for (dx, dz, name) in [
(0.46, 0.2, "leg_fl"),
(-0.46, 0.2, "leg_fr"),
(0.46, -0.95, "leg_bl"),
(-0.46, -0.95, "leg_br"),
] {
let up = r.joint(
Some(hips),
&format!("{name}_up"),
v(dx * s, 0.42 * s, dz * s),
);
let ft = r.joint(Some(up), &format!("{name}_ft"), v(dx * s, 0.0, dz * s));
r.cone(up, ft, 0.17 * s, 0.1 * s, 2, 0.05 * s);
}
let nheads = 5usize;
let mut head_joints = Vec::new();
for i in 0..nheads {
let f = (i as f32 / (nheads - 1) as f32 - 0.5) * 2.0; let rise = 1.0 - 0.22 * f.abs(); let b = r.joint(
Some(core),
&format!("neck{i}_0"),
v(f * 0.45 * s, 1.25 * s, 0.5 * s),
);
let m1 = r.joint(
Some(b),
&format!("neck{i}_1"),
v(f * 0.62 * s, (2.05 * rise) * s, 0.32 * s),
);
let m2 = r.joint(
Some(m1),
&format!("neck{i}_2"),
v(f * 0.82 * s, (2.85 * rise) * s, 0.66 * s),
);
let head = r.joint(
Some(m2),
&format!("neck{i}_head"),
v(f * 0.95 * s, (3.25 * rise) * s, 1.02 * s),
);
r.cone(b, m1, 0.15 * s, 0.12 * s, 2, 0.05 * s);
r.cone(m1, m2, 0.12 * s, 0.1 * s, 2, 0.045 * s);
r.cone(m2, head, 0.1 * s, 0.11 * s, 2, 0.045 * s);
head_joints.push(head);
parts.push(PartMeta {
name: format!("head.{}", i + 1),
joint: format!("neck{i}_head"),
destructible: true,
});
}
if p.weak_points {
weak_points.push(WeakPointMeta {
name: "weak_point.core".into(),
joint: "core".into(),
collider: ColliderJson::Sphere { radius: 0.45 * s },
offset: [0.0, 0.0, 0.0],
destructible: true,
phase: 2,
});
}
let gait = GaitDesc {
legs: Vec::new(),
spine: vec![hips, shoulders, core],
wings: Vec::new(),
tail: vec![tail1, tail2, tail3],
head: None, style: Gait::Slither,
};
let mut rig = r.finish(gait);
for &head in &head_joints {
add_head_features(&mut rig, head, s);
}
add_dorsal_spikes(&mut rig, s, &[hips, shoulders, core], &head_joints);
rig.bounds = crate::generators::monster::rig::compute_bounds(&rig);
BossRig {
rig,
weak_points,
parts,
extra_mesh: None,
}
}
fn add_head_features(rig: &mut MonsterRig, head: usize, s: f32) {
let v = Vec3::new;
let hp = rig.joint_world(head);
let snout = add_joint(rig, head, "snout", hp + v(0.0, -0.06 * s, 0.36 * s));
push_flat(
rig,
head,
snout,
v(0.16 * s, 0.15 * s, 0.32 * s),
3,
0.05 * s,
PrimTint::Body,
);
let jaw = add_joint(rig, head, "jaw", hp + v(0.0, -0.19 * s, 0.3 * s));
push_cone(
rig,
head,
jaw,
0.12 * s,
0.05 * s,
3,
0.035 * s,
PrimTint::Body,
);
for side in [-1.0f32, 1.0] {
let eb = add_joint(
rig,
head,
"eye",
hp + v(side * 0.12 * s, 0.05 * s, 0.18 * s),
);
let eo = add_joint(
rig,
eb,
"eye_out",
hp + v(side * 0.14 * s, 0.05 * s, 0.21 * s),
);
push_cone(
rig,
eb,
eo,
0.075 * s,
0.05 * s,
4,
0.006 * s,
PrimTint::Eye,
);
}
for side in [-1.0f32, 1.0] {
let hb = add_joint(
rig,
head,
"horn",
hp + v(side * 0.08 * s, 0.08 * s, -0.02 * s),
);
let hm = add_joint(
rig,
hb,
"horn_mid",
hp + v(side * 0.1 * s, 0.14 * s, -0.16 * s),
);
let ht = add_joint(
rig,
hm,
"horn_tip",
hp + v(side * 0.1 * s, 0.16 * s, -0.32 * s),
);
push_cone(
rig,
hb,
hm,
0.038 * s,
0.022 * s,
5,
0.012 * s,
PrimTint::Horn,
);
push_cone(
rig,
hm,
ht,
0.022 * s,
0.006 * s,
5,
0.01 * s,
PrimTint::Horn,
);
}
}
fn add_dorsal_spikes(rig: &mut MonsterRig, s: f32, back: &[usize], heads: &[usize]) {
let v = Vec3::new;
for (i, &j) in back.iter().enumerate() {
let jp = rig.joint_world(j);
let h = (0.22 - 0.03 * i as f32) * s;
let b0 = add_joint(rig, j, "spike", jp + v(0.0, 0.24 * s, 0.0));
let b1 = add_joint(rig, b0, "spike_tip", jp + v(0.0, 0.24 * s + h, -0.08 * s));
push_cone(
rig,
b0,
b1,
0.05 * s,
0.006 * s,
5,
0.012 * s,
PrimTint::Horn,
);
}
for &head in heads {
let hp = rig.joint_world(head);
let b0 = add_joint(rig, head, "neck_spike", hp + v(0.0, 0.16 * s, -0.06 * s));
let b1 = add_joint(rig, b0, "neck_spike_tip", hp + v(0.0, 0.28 * s, -0.14 * s));
push_cone(
rig,
b0,
b1,
0.035 * s,
0.005 * s,
5,
0.01 * s,
PrimTint::Horn,
);
}
}
fn plan_colossus(p: &BossParams) -> BossRig {
let s = p.size.clamp(0.4, 8.0);
let bulk = 1.0 + 0.5 * p.menace.clamp(0.0, 1.0);
let armor = p.armor.clamp(0.0, 1.0);
let plates = p.plates.clamp(0.0, 1.0);
let horns = p.horns.clamp(0.0, 1.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let mut parts = Vec::new();
let mut weak_points = Vec::new();
let hips = r.joint(None, "hips", v(0.0, 1.02 * s, 0.0));
let spine = r.joint(Some(hips), "spine", v(0.0, 1.28 * s, 0.03 * s));
let chest = r.joint(Some(spine), "chest", v(0.0, 1.56 * s, 0.09 * s));
let neck = r.joint(Some(chest), "neck", v(0.0, 1.72 * s, 0.16 * s));
let head = r.joint(Some(neck), "head", v(0.0, 1.9 * s, 0.24 * s));
let bk = bulk * (1.0 + 0.1 * armor);
r.ellip(hips, chest, 0.25 * s * bk, 0.05 * s, 0, 0.08 * s); r.cone(hips, chest, 0.23 * s * bk, 0.26 * s * bk, 0, 0.08 * s);
r.flat(
chest,
chest,
v(0.4 * s * bk, 0.2 * s, 0.26 * s),
0,
0.06 * s,
); r.cone(chest, neck, 0.16 * s, 0.15 * s, 1, 0.04 * s); r.cone(neck, head, 0.15 * s, 0.16 * s, 1, 0.04 * s);
r.flat(head, head, v(0.24 * s, 0.24 * s, 0.28 * s), 1, 0.035 * s);
parts.push(PartMeta {
name: "head".into(),
joint: "head".into(),
destructible: true,
});
let mut legs = Vec::new();
for side in [-1.0f32, 1.0] {
let th = r.joint(Some(hips), "thigh", v(side * 0.26 * s, 0.86 * s, 0.0));
let sn = r.joint(Some(th), "shin", v(side * 0.27 * s, 0.44 * s, 0.02 * s));
let ft = r.joint(Some(sn), "foot", v(side * 0.28 * s, 0.05 * s, 0.18 * s));
r.cone(th, sn, 0.22 * s * bulk, 0.19 * s, 2, 0.02 * s);
r.cone(sn, ft, 0.19 * s, 0.14 * s, 2, 0.025 * s);
r.flat(ft, ft, v(0.17 * s, 0.09 * s, 0.24 * s), 2, 0.02 * s); legs.push(vec![th, sn, ft]);
}
for (side, name) in [(-1.0f32, "arm.l"), (1.0f32, "arm.r")] {
let sh = r.joint(
Some(chest),
&format!("{name}_upper"),
v(side * 0.52 * s, 1.5 * s, 0.06 * s),
);
let el = r.joint(
Some(sh),
&format!("{name}_fore"),
v(side * 0.62 * s, 1.08 * s, 0.18 * s),
);
let hn = r.joint(
Some(el),
&format!("{name}_hand"),
v(side * 0.66 * s, 0.72 * s, 0.24 * s),
);
r.cone(sh, el, 0.2 * s * bulk, 0.18 * s * bulk, 2, 0.025 * s);
r.cone(el, hn, 0.18 * s, 0.16 * s, 2, 0.03 * s);
r.flat(hn, hn, v(0.2 * s, 0.19 * s, 0.2 * s), 2, 0.025 * s); parts.push(PartMeta {
name: name.into(),
joint: format!("{name}_fore"),
destructible: true,
});
}
let chest_r2 = 0.26 * s * bk;
let core = r.joint(
Some(chest),
"core",
v(0.0, 1.52 * s, 0.09 * s + 0.78 * chest_r2),
);
r.cone(chest, core, 0.02 * s, 0.02 * s, 1, 0.02 * s);
if p.weak_points {
weak_points.push(WeakPointMeta {
name: "weak_point.core".into(),
joint: "core".into(),
collider: ColliderJson::Sphere {
radius: (0.22 + 0.06 * plates) * s,
},
offset: [0.0, 0.0, 0.0],
destructible: true,
phase: 2,
});
}
let gait = GaitDesc {
legs,
spine: vec![hips, spine, chest, neck, head],
wings: Vec::new(),
tail: Vec::new(),
head: Some(head),
style: Gait::Walk,
};
let mut rig = r.finish(gait);
let core_r = (0.24 + 0.05 * plates) * s;
push_flat(
&mut rig,
core,
core,
v(core_r, core_r, core_r),
3,
0.05 * s,
PrimTint::Eye,
);
add_armor_plates(&mut rig, chest, core, s, 0.6 + 0.4 * armor.max(plates));
add_horn_crown(&mut rig, head, s, horns);
rig.bounds = crate::generators::monster::rig::compute_bounds(&rig);
BossRig {
rig,
weak_points,
parts,
extra_mesh: None,
}
}
fn add_armor_plates(rig: &mut MonsterRig, chest: usize, core: usize, s: f32, intensity: f32) {
let v = Vec3::new;
let cp = rig.joint_world(chest);
let g = 0.16 + 0.08 * intensity; for side in [-1.0f32, 1.0] {
let pb = add_joint(
rig,
chest,
"pauldron",
cp + v(side * 0.5 * s, 0.1 * s, 0.02 * s),
);
push_cone(
rig,
chest,
pb,
0.02 * s,
0.02 * s,
1,
0.02 * s,
PrimTint::Body,
);
push_flat(
rig,
pb,
pb,
v(g * 1.5 * s, g * 0.6 * s, g * 1.25 * s),
4,
0.025 * s,
PrimTint::Body,
);
let po = add_joint(
rig,
pb,
"pauldron_out",
cp + v(side * 0.72 * s, -0.04 * s, 0.0),
);
push_cone(
rig,
chest,
po,
0.02 * s,
0.02 * s,
1,
0.02 * s,
PrimTint::Body,
);
push_flat(
rig,
po,
po,
v(g * 0.75 * s, g * 0.5 * s, g * 1.0 * s),
4,
0.025 * s,
PrimTint::Body,
);
}
let core_p = rig.joint_world(core);
let bp = add_joint(
rig,
chest,
"chestplate",
core_p + v(0.0, 0.24 * s, -0.02 * s),
);
push_cone(
rig,
chest,
bp,
0.02 * s,
0.02 * s,
1,
0.02 * s,
PrimTint::Body,
);
push_flat(
rig,
bp,
bp,
v((0.34 + 0.1 * intensity) * s, 0.1 * s, 0.08 * s),
4,
0.03 * s,
PrimTint::Body,
);
}
fn add_horn_crown(rig: &mut MonsterRig, head: usize, s: f32, horns: f32) {
if horns <= 0.0 {
return;
}
let v = Vec3::new;
let hp = rig.joint_world(head);
let len = 0.06 + 0.1 * horns;
for side in [-1.0f32, 1.0] {
let hb = add_joint(
rig,
head,
"horn",
hp + v(side * 0.14 * s, 0.12 * s, 0.06 * s),
);
let ht = add_joint(
rig,
hb,
"horn_tip",
hp + v(side * 0.18 * s, (0.12 + 0.5 * len) * s, (0.06 - len) * s),
);
push_cone(
rig,
hb,
ht,
0.06 * s,
0.015 * s,
5,
0.01 * s,
PrimTint::Body,
);
}
}
fn plan_lich(p: &BossParams) -> BossRig {
let s = p.size.clamp(0.4, 8.0);
let crown_k = p.crown.clamp(0.0, 1.0);
let regalia = p.regalia.clamp(0.0, 1.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let mut parts = Vec::new();
let mut weak_points = Vec::new();
let hips = r.joint(None, "hips", v(0.0, 0.98 * s, 0.0));
let waist = r.joint(Some(hips), "waist", v(0.0, 1.18 * s, 0.0));
let chest = r.joint(Some(waist), "chest", v(0.0, 1.48 * s, 0.02 * s));
let neck = r.joint(Some(chest), "neck", v(0.0, 1.66 * s, 0.05 * s));
let head = r.joint(Some(neck), "head", v(0.0, 1.82 * s, 0.07 * s));
let skirt_mid = r.joint(Some(hips), "skirt_mid", v(0.0, 0.5 * s, 0.02 * s));
let hem = r.joint(Some(skirt_mid), "hem", v(0.0, 0.02 * s, 0.04 * s));
r.cone(waist, chest, 0.1 * s, 0.115 * s, 0, 0.045 * s);
r.ellip(waist, chest, 0.09 * s, 0.02 * s, 0, 0.045 * s);
r.cone(hips, skirt_mid, 0.11 * s, 0.14 * s, 0, 0.05 * s);
r.cone(skirt_mid, hem, 0.14 * s, 0.16 * s, 0, 0.05 * s);
r.flat(hem, hem, v(0.31 * s, 0.11 * s, 0.29 * s), 0, 0.06 * s);
r.flat(chest, chest, v(0.2 * s, 0.075 * s, 0.11 * s), 0, 0.045 * s);
r.cone(chest, neck, 0.05 * s, 0.048 * s, 1, 0.03 * s); r.cone(neck, head, 0.048 * s, 0.06 * s, 1, 0.03 * s);
parts.push(PartMeta {
name: "head".into(),
joint: "head".into(),
destructible: true,
});
let mut legs = Vec::new();
for side in [-1.0f32, 1.0] {
let th = r.joint(Some(hips), "thigh", v(side * 0.08 * s, 0.62 * s, 0.0));
let sn = r.joint(Some(th), "shin", v(side * 0.08 * s, 0.32 * s, 0.02 * s));
let ft = r.joint(Some(sn), "foot", v(side * 0.09 * s, 0.03 * s, 0.1 * s));
r.cone(th, sn, 0.055 * s, 0.04 * s, 2, 0.018 * s);
r.cone(sn, ft, 0.04 * s, 0.03 * s, 2, 0.02 * s);
legs.push(vec![th, sn, ft]);
}
{
let sh = r.joint(Some(chest), "upperarm_l", v(-0.19 * s, 1.46 * s, 0.03 * s));
let el = r.joint(Some(sh), "forearm_l", v(-0.35 * s, 1.62 * s, 0.16 * s));
let hn = r.joint(Some(el), "hand_l", v(-0.42 * s, 1.86 * s, 0.3 * s));
r.cone(sh, el, 0.05 * s, 0.042 * s, 2, 0.02 * s);
r.cone(el, hn, 0.042 * s, 0.03 * s, 2, 0.018 * s);
r.ellip(el, el, 0.085 * s, 0.03 * s, 2, 0.025 * s); r.ellip(hn, hn, 0.05 * s, 0.0, 2, 0.014 * s); parts.push(PartMeta {
name: "arm.l".into(),
joint: "forearm_l".into(),
destructible: true,
});
}
{
let sh = r.joint(Some(chest), "upperarm_r", v(0.19 * s, 1.44 * s, 0.03 * s));
let el = r.joint(Some(sh), "forearm_r", v(0.27 * s, 1.08 * s, 0.1 * s));
let hn = r.joint(Some(el), "hand_r", v(0.29 * s, 0.76 * s, 0.16 * s));
r.cone(sh, el, 0.05 * s, 0.042 * s, 2, 0.02 * s);
r.cone(el, hn, 0.042 * s, 0.03 * s, 2, 0.018 * s);
r.ellip(el, el, 0.09 * s, 0.04 * s, 2, 0.025 * s); r.ellip(hn, hn, 0.05 * s, 0.0, 2, 0.014 * s); parts.push(PartMeta {
name: "arm.r".into(),
joint: "forearm_r".into(),
destructible: true,
});
}
let gait = GaitDesc {
legs,
spine: vec![hips, waist, chest, neck, head],
wings: Vec::new(),
tail: Vec::new(),
head: Some(head),
style: Gait::Walk,
};
let mut rig = r.finish(gait);
add_lich_skull(&mut rig, head, s);
add_robe_folds(&mut rig, hips, s);
let chest_wp = rig.joint_world(chest);
let core = add_joint(
&mut rig,
chest,
"core",
chest_wp + v(0.0, 0.02 * s, 0.11 * s),
);
let core_r = 0.075 * s;
push_flat(
&mut rig,
core,
core,
v(core_r, core_r, core_r),
3,
0.03 * s,
PrimTint::Eye,
);
add_lich_eyes(&mut rig, head, s);
add_crown(&mut rig, head, s, crown_k, regalia);
let core_wp = rig.joint_world(core);
for i in 0..3usize {
let phase = i as f32 / 3.0 * TAU;
let radius = (0.5 + 0.04 * i as f32) * s;
let y = 0.12 * s * (i as f32 - 1.0);
let pivot = add_joint(&mut rig, core, "implement_pivot", core_wp);
let tip = add_joint(
&mut rig,
pivot,
&format!("implement.{}", i + 1),
core_wp + v(radius * phase.cos(), y, radius * phase.sin()),
);
if i % 2 == 0 {
let orb_r = 0.075 * s;
push_flat(
&mut rig,
tip,
tip,
v(orb_r, orb_r, orb_r),
6,
0.02 * s,
PrimTint::Eye,
);
} else {
push_flat(
&mut rig,
tip,
tip,
v(0.02 * s, 0.16 * s, 0.05 * s),
6,
0.015 * s,
PrimTint::Horn,
);
}
parts.push(PartMeta {
name: format!("implement.{}", i + 1),
joint: format!("implement.{}", i + 1),
destructible: true,
});
}
if p.weak_points {
weak_points.push(WeakPointMeta {
name: "weak_point.phylactery".into(),
joint: "core".into(),
collider: ColliderJson::Sphere {
radius: core_r * 1.4,
},
offset: [0.0, 0.0, 0.0],
destructible: true,
phase: 2,
});
}
rig.bounds = crate::generators::monster::rig::compute_bounds(&rig);
let throne_pos = v(0.0, 0.62 * s, -0.24 * s);
let throne = add_joint(&mut rig, hips, "throne", throne_pos);
let throne_back = add_joint(
&mut rig,
throne,
"throne_back",
throne_pos + v(0.0, 0.9 * s, -0.32 * s),
);
push_flat(
&mut rig,
throne_back,
throne_back,
v(0.42 * s, 1.0 * s, 0.16 * s),
7,
0.02 * s,
PrimTint::Body,
);
parts.push(PartMeta {
name: "throne".into(),
joint: "throne".into(),
destructible: true,
});
BossRig {
rig,
weak_points,
parts,
extra_mesh: Some(build_throne_mesh(s, throne_pos)),
}
}
fn add_lich_eyes(rig: &mut MonsterRig, head: usize, s: f32) {
let v = Vec3::new;
let hp = rig.joint_world(head);
for side in [-1.0f32, 1.0] {
let eb = add_joint(
rig,
head,
"eye",
hp + v(side * 0.045 * s, 0.01 * s, 0.07 * s),
);
push_flat(
rig,
eb,
eb,
v(0.018 * s, 0.018 * s, 0.018 * s),
4,
0.006 * s,
PrimTint::Eye,
);
}
}
fn add_crown(rig: &mut MonsterRig, head: usize, s: f32, crown: f32, regalia: f32) {
let v = Vec3::new;
let hp = rig.joint_world(head);
let n = 5usize;
let h = (0.1 + 0.16 * crown) * s;
for i in 0..n {
let f = (i as f32 / (n - 1) as f32 - 0.5) * 2.0; let base = hp + v(f * 0.085 * s, 0.09 * s, 0.03 * s - 0.02 * f.abs() * s);
let b0 = add_joint(rig, head, "crown_spike", base);
let b1 = add_joint(
rig,
b0,
"crown_spike_tip",
base + v(f * 0.02 * s, h, -0.02 * s),
);
push_cone(
rig,
b0,
b1,
0.026 * s,
0.007 * s,
5,
0.014 * s,
PrimTint::Horn,
);
}
if regalia > 0.0 {
let band_r = (0.028 + 0.014 * regalia) * s;
push_flat(
rig,
head,
head,
v(0.11 * s, band_r, 0.11 * s),
5,
0.012 * s,
PrimTint::Horn,
);
}
}
fn add_lich_skull(rig: &mut MonsterRig, head: usize, s: f32) {
let v = Vec3::new;
let hp = rig.joint_world(head);
push_flat(
rig,
head,
head,
v(0.095 * s, 0.11 * s, 0.1 * s),
1,
0.025 * s,
PrimTint::Body,
);
let brow = add_joint(rig, head, "brow", hp + v(0.0, 0.035 * s, 0.07 * s));
push_flat(
rig,
brow,
brow,
v(0.1 * s, 0.028 * s, 0.045 * s),
1,
0.012 * s,
PrimTint::Body,
);
for side in [-1.0f32, 1.0] {
let ch = add_joint(
rig,
head,
"cheek",
hp + v(side * 0.062 * s, -0.02 * s, 0.06 * s),
);
push_flat(
rig,
ch,
ch,
v(0.028 * s, 0.03 * s, 0.03 * s),
1,
0.01 * s,
PrimTint::Body,
);
}
let chin = add_joint(rig, head, "chin", hp + v(0.0, -0.14 * s, 0.05 * s));
push_cone(
rig,
head,
chin,
0.075 * s,
0.028 * s,
1,
0.02 * s,
PrimTint::Body,
);
}
fn add_robe_folds(rig: &mut MonsterRig, hips: usize, s: f32) {
let v = Vec3::new;
let hipw = rig.joint_world(hips);
let n = 7usize;
for i in 0..n {
let frac = i as f32 / (n - 1) as f32;
let theta = (frac - 0.5) * PI * 1.35; let (top_y, r_top) = (0.5 * s, 0.15 * s);
let (bot_y, r_bot) = (0.06 * s, 0.31 * s);
let top = hipw + v(theta.sin() * r_top, top_y - hipw.y, theta.cos() * r_top);
let bot = hipw + v(theta.sin() * r_bot, bot_y - hipw.y, theta.cos() * r_bot);
let jt = add_joint(rig, hips, "robe_fold", top);
let jb = add_joint(rig, jt, "robe_fold_tip", bot);
push_cone(
rig,
jt,
jb,
0.02 * s,
0.03 * s,
0,
0.015 * s,
PrimTint::Body,
);
}
}
fn build_throne_mesh(s: f32, pos: Vec3) -> Mesh {
let v = Vec3::new;
let stone = crate::palette::srgb(19, 24, 18); let trim = crate::palette::srgb(46, 52, 40); let w = 0.4 * s; let mut m = Mesh::new();
m.merge(&cuboid(v(0.0, 0.0, 0.0), v(w, 0.08 * s, 0.34 * s), stone));
m.merge(&cuboid(
v(0.0, -0.34 * s, -0.02 * s),
v(w * 0.9, 0.26 * s, 0.3 * s),
stone,
));
m.merge(&cuboid(
v(0.0, -0.6 * s, -0.02 * s),
v(w * 1.05, 0.06 * s, 0.36 * s),
stone,
));
let mut back = cuboid(v(0.0, 0.85 * s, -0.3 * s), v(w, 0.75 * s, 0.07 * s), stone);
let mut bore = lathe(
&[
(0.0, -0.4 * s),
(0.22 * s, -0.4 * s),
(0.22 * s, 0.42 * s),
(0.0, 0.5 * s),
],
12,
|_, _| stone,
);
bore.transform(Mat4::from_rotation_translation(
Quat::from_rotation_x(FRAC_PI_2),
v(0.0, 0.85 * s, -0.3 * s),
));
back = crate::csg::subtract(&back, &bore);
m.merge(&back);
for side in [-1.0f32, 1.0] {
m.merge(&cuboid(
v(side * (w - 0.03 * s), 0.85 * s, -0.28 * s),
v(0.05 * s, 0.75 * s, 0.08 * s),
trim,
));
}
for side in [-1.0f32, 1.0] {
m.merge(&cuboid(
v(side * w, 0.28 * s, 0.02 * s),
v(0.06 * s, 0.06 * s, 0.3 * s),
trim,
));
m.merge(&cuboid(
v(side * w, 0.08 * s, 0.3 * s),
v(0.06 * s, 0.2 * s, 0.06 * s),
stone,
));
m.merge(&cuboid(
v(side * (w - 0.04 * s), 1.7 * s, -0.3 * s),
v(0.05 * s, 0.12 * s, 0.05 * s),
trim,
));
}
m.merge(&cuboid(
v(0.0, 1.78 * s, -0.3 * s),
v(0.06 * s, 0.18 * s, 0.06 * s),
trim,
));
m.translate(pos);
m
}
fn plan_swarm_queen(p: &BossParams) -> BossRig {
let s = p.size.clamp(0.4, 8.0);
let spikes = p.spikes.clamp(0.0, 1.0);
let eyes_n = p.eyes.clamp(2, 16) as usize;
let menace = p.menace.clamp(0.0, 1.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let mut parts = Vec::new();
let mut weak_points = Vec::new();
let thorax = r.joint(None, "thorax", v(0.0, 0.95 * s, 0.15 * s));
let head = r.joint(Some(thorax), "head", v(0.0, 1.05 * s, 0.85 * s));
let abdomen = r.joint(Some(thorax), "abdomen", v(0.0, 0.75 * s, -1.15 * s));
let abtip = r.joint(Some(abdomen), "abtip", v(0.0, 0.5 * s, -2.15 * s));
r.ellip(thorax, thorax, 0.42 * s, 0.05 * s, 0, 0.14 * s); r.ellip(head, head, 0.28 * s, 0.0, 1, 0.1 * s); r.cone(thorax, head, 0.24 * s, 0.22 * s, 0, 0.09 * s);
r.cone(thorax, abdomen, 0.3 * s, 0.58 * s, 0, 0.16 * s);
r.ellip(abdomen, abdomen, 0.66 * s, 0.0, 0, 0.2 * s);
r.ellip(abdomen, abtip, 0.58 * s, 0.16 * s, 0, 0.18 * s);
parts.push(PartMeta {
name: "abdomen".into(),
joint: "abdomen".into(),
destructible: true,
});
parts.push(PartMeta {
name: "head".into(),
joint: "head".into(),
destructible: true,
});
let mut legs = Vec::new();
for side in [-1.0f32, 1.0] {
for kk in 0..3 {
let zc = 0.6 * s - kk as f32 * 0.65 * s;
let foot_z = zc + (1.0 - kk as f32) * 0.45 * s;
let root = r.joint(Some(thorax), "coxa", v(side * 0.42 * s, 0.9 * s, zc));
let knee = r.joint(
Some(root),
"knee",
v(side * 1.05 * s, 1.15 * s, zc + 0.12 * s),
);
let foot = r.joint(Some(knee), "foot", v(side * 1.6 * s, 0.0, foot_z));
r.cone(root, knee, 0.17 * s, 0.13 * s, 2, 0.05 * s);
r.cone(knee, foot, 0.13 * s, 0.06 * s, 2, 0.05 * s);
legs.push(vec![root, knee, foot]);
}
}
let gait = GaitDesc {
legs,
spine: vec![head, thorax, abdomen, abtip],
wings: Vec::new(),
tail: Vec::new(),
head: Some(head),
style: Gait::Crawl,
};
let mut rig = r.finish(gait);
add_swarm_head_features(&mut rig, head, s, eyes_n);
add_carapace_spikes(&mut rig, thorax, abdomen, s, spikes);
let nsacs = (5.0 + 3.0 * menace).round().clamp(5.0, 8.0) as usize;
add_brood_sacs(
&mut rig,
abdomen,
abtip,
s,
nsacs,
&mut parts,
&mut weak_points,
p.weak_points,
);
rig.bounds = crate::generators::monster::rig::compute_bounds(&rig);
BossRig {
rig,
weak_points,
parts,
extra_mesh: None,
}
}
fn add_swarm_head_features(rig: &mut MonsterRig, head: usize, s: f32, eyes_n: usize) {
let v = Vec3::new;
let hp = rig.joint_world(head);
for side in [-1.0f32, 1.0] {
let mb = add_joint(
rig,
head,
"mandible",
hp + v(side * 0.14 * s, -0.06 * s, 0.22 * s),
);
let mt = add_joint(
rig,
mb,
"mandible_tip",
hp + v(side * 0.26 * s, -0.16 * s, 0.44 * s),
);
push_cone(
rig,
mb,
mt,
0.05 * s,
0.012 * s,
3,
0.02 * s,
PrimTint::Horn,
);
}
let n = eyes_n.max(2);
for i in 0..n {
let f = (i as f32 / (n - 1).max(1) as f32 - 0.5) * 2.0; let row = (i % 2) as f32; let eb = add_joint(
rig,
head,
"eye",
hp + v(
f * 0.2 * s,
0.05 * s - row * 0.09 * s,
0.24 * s + row * 0.02 * s,
),
);
push_flat(
rig,
eb,
eb,
v(0.03 * s, 0.03 * s, 0.03 * s),
4,
0.008 * s,
PrimTint::Eye,
);
}
}
fn add_carapace_spikes(rig: &mut MonsterRig, thorax: usize, abdomen: usize, s: f32, spikes: f32) {
let v = Vec3::new;
let n = (3.0 + 4.0 * spikes).round().max(2.0) as usize;
let tp = rig.joint_world(thorax);
let ap = rig.joint_world(abdomen);
for i in 0..n {
let f = i as f32 / (n - 1).max(1) as f32;
let base_pos = tp.lerp(ap, f) + v(0.0, (0.4 - 0.12 * f) * s, 0.0);
let h = (0.14 + 0.14 * spikes) * s * (1.0 - 0.3 * f);
let b0 = add_joint(rig, thorax, "carapace_spike", base_pos);
let b1 = add_joint(
rig,
b0,
"carapace_spike_tip",
base_pos + v(0.0, h, -0.05 * s),
);
push_cone(
rig,
b0,
b1,
0.05 * s,
0.008 * s,
3,
0.015 * s,
PrimTint::Horn,
);
}
}
#[allow(clippy::too_many_arguments)]
fn add_brood_sacs(
rig: &mut MonsterRig,
abdomen: usize,
abtip: usize,
s: f32,
n: usize,
parts: &mut Vec<PartMeta>,
weak_points: &mut Vec<WeakPointMeta>,
emit_weak_points: bool,
) {
let v = Vec3::new;
let ap = rig.joint_world(abdomen);
let tp = rig.joint_world(abtip);
let axis = (tp - ap).normalize_or_zero();
let side = axis.cross(Vec3::Y).normalize_or_zero();
let up = side.cross(axis).normalize_or_zero();
let golden = 2.399_963_2; let field = crate::generators::monster::body::organic_field(rig);
for i in 0..n {
let t = 0.12 + 0.7 * (i as f32 / (n - 1).max(1) as f32);
let anchor = ap.lerp(tp, t);
let theta = i as f32 * golden;
let dir = (side * theta.cos() + up * theta.sin()).normalize_or_zero();
let mut d_surf = 0.1 * s;
let step = 0.03 * s;
while d_surf < 2.5 * s && field(anchor + dir * d_surf) < 0.0 {
d_surf += step;
}
let r1 = (0.22 - 0.06 * t) * s;
let base_pos = anchor + dir * (d_surf - 0.35 * r1);
let tip_pos = anchor + dir * (d_surf + 1.25 * r1);
let name = format!("brood_sac.{}", i + 1);
let base = add_joint(rig, abdomen, "sac_base", base_pos);
let tip = add_joint(rig, base, &name, tip_pos);
push_flat(
rig,
base,
tip,
v(r1, r1, r1 * 1.15),
5,
0.012 * s,
PrimTint::Eye,
);
parts.push(PartMeta {
name: name.clone(),
joint: name.clone(),
destructible: true,
});
if emit_weak_points {
weak_points.push(WeakPointMeta {
name: format!("weak_point.brood_sac.{}", i + 1),
joint: name,
collider: ColliderJson::Sphere { radius: r1 * 1.3 },
offset: [0.0, 0.0, 0.0],
destructible: true,
phase: 2,
});
}
}
}
fn plan_dragon_lord(p: &BossParams) -> BossRig {
let s = p.size.clamp(0.4, 8.0);
let menace = p.menace.clamp(0.0, 1.0);
let horns_k = p.horns.clamp(0.0, 1.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let mut parts = Vec::new();
let mut weak_points = Vec::new();
let hips = r.joint(None, "hips", v(0.0, 0.56 * s, -0.75 * s));
let waist = r.joint(Some(hips), "waist", v(0.0, 0.62 * s, 0.0));
let chest = r.joint(Some(waist), "chest", v(0.0, 0.74 * s, 0.7 * s));
let shoulders = r.joint(Some(chest), "shoulders", v(0.0, 0.9 * s, 1.15 * s));
let neck0 = r.joint(Some(shoulders), "neck0", v(0.0, 1.5 * s, 1.35 * s));
let neck1 = r.joint(Some(neck0), "neck1", v(0.0, 2.15 * s, 1.5 * s));
let neck2 = r.joint(Some(neck1), "neck2", v(0.0, 2.75 * s, 1.65 * s));
let head = r.joint(Some(neck2), "head", v(0.0, 3.2 * s, 1.95 * s));
r.ellip(hips, waist, 0.4 * s, 0.06 * s, 0, 0.16 * s);
r.cone(hips, waist, 0.38 * s, 0.32 * s, 0, 0.14 * s);
r.cone(waist, chest, 0.32 * s, 0.26 * s, 0, 0.13 * s);
r.cone(chest, shoulders, 0.26 * s, 0.22 * s, 0, 0.11 * s);
r.cone(shoulders, neck0, 0.22 * s, 0.18 * s, 1, 0.09 * s);
r.cone(neck0, neck1, 0.17 * s, 0.145 * s, 1, 0.07 * s);
r.cone(neck1, neck2, 0.145 * s, 0.13 * s, 1, 0.06 * s);
r.cone(neck2, head, 0.13 * s, 0.15 * s, 1, 0.055 * s);
parts.push(PartMeta {
name: "head".into(),
joint: "head".into(),
destructible: true,
});
let tail1 = r.joint(Some(hips), "tail1", v(0.0, 0.42 * s, -1.6 * s));
let tail2 = r.joint(Some(tail1), "tail2", v(0.0, 0.26 * s, -2.5 * s));
let tail3 = r.joint(Some(tail2), "tail3", v(0.0, 0.12 * s, -3.3 * s));
let tail4 = r.joint(Some(tail3), "tail4", v(0.0, 0.04 * s, -4.0 * s));
r.cone(hips, tail1, 0.34 * s, 0.2 * s, 0, 0.09 * s);
r.cone(tail1, tail2, 0.2 * s, 0.1 * s, 0, 0.07 * s);
r.cone(tail2, tail3, 0.1 * s, 0.045 * s, 0, 0.055 * s);
r.cone(tail3, tail4, 0.045 * s, 0.012 * s, 0, 0.04 * s);
let mut legs = Vec::new();
for (dx, dz, name) in [
(0.34, 0.6, "leg_fl"),
(-0.34, 0.6, "leg_fr"),
(0.38, -0.7, "leg_bl"),
(-0.38, -0.7, "leg_br"),
] {
let up = r.joint(
Some(hips),
&format!("{name}_up"),
v(dx * s, 0.42 * s, dz * s),
);
let ft = r.joint(Some(up), &format!("{name}_ft"), v(dx * s, 0.0, dz * s));
r.cone(up, ft, 0.15 * s, 0.09 * s, 2, 0.045 * s);
legs.push(vec![up, ft]);
}
let mut wings = Vec::new();
for (side, name) in [(-1.0f32, "wing.l"), (1.0f32, "wing.r")] {
let root = r.joint(
Some(shoulders),
&format!("{name}_root"),
v(side * 0.35 * s, 1.3 * s, 0.7 * s),
);
let wrist = r.joint(
Some(root),
&format!("{name}_wrist"),
v(side * 1.7 * s, 2.35 * s, 0.55 * s),
);
let tip = r.joint(Some(wrist), name, v(side * 3.0 * s, 1.9 * s, 0.4 * s));
r.flat(root, tip, v(1.5 * s, 0.9 * s, 0.05 * s), 4, 0.02 * s);
r.cone(root, wrist, 0.12 * s, 0.07 * s, 4, 0.03 * s);
r.cone(wrist, tip, 0.07 * s, 0.025 * s, 4, 0.025 * s);
for (fdx, fdy) in [(2.35f32, 1.15f32), (1.75f32, 0.95f32), (1.05f32, 1.0f32)] {
let ftip = r.joint(
Some(wrist),
&format!("{name}_finger"),
v(side * fdx * s, fdy * s, 0.45 * s),
);
r.cone(wrist, ftip, 0.04 * s, 0.012 * s, 4, 0.02 * s);
}
wings.push(root);
parts.push(PartMeta {
name: name.to_string(),
joint: name.to_string(),
destructible: true,
});
}
let gait = GaitDesc {
legs,
spine: vec![hips, waist, chest, shoulders, neck0, neck1, neck2, head],
wings,
tail: vec![tail1, tail2, tail3, tail4],
head: Some(head),
style: Gait::Fly,
};
let mut rig = r.finish(gait);
add_dragon_head_features(&mut rig, head, s, horns_k);
add_dragon_dorsal_spikes(
&mut rig,
s,
&[
neck1, neck0, shoulders, chest, waist, hips, tail1, tail2, tail3,
],
);
let chest_wp = rig.joint_world(chest);
let heart = add_joint(
&mut rig,
chest,
"heart",
chest_wp + v(0.0, -0.02 * s, 0.5 * s),
);
let heart_r = (0.26 + 0.06 * menace) * s;
push_flat(
&mut rig,
heart,
heart,
v(heart_r, heart_r, heart_r),
3,
0.045 * s,
PrimTint::Eye,
);
if p.weak_points {
weak_points.push(WeakPointMeta {
name: "weak_point.heart".into(),
joint: "heart".into(),
collider: ColliderJson::Sphere {
radius: heart_r * 1.3,
},
offset: [0.0, 0.0, 0.0],
destructible: true,
phase: 2,
});
}
rig.bounds = crate::generators::monster::rig::compute_bounds(&rig);
BossRig {
rig,
weak_points,
parts,
extra_mesh: None,
}
}
fn add_dragon_head_features(rig: &mut MonsterRig, head: usize, s: f32, horns: f32) {
let v = Vec3::new;
let hp = rig.joint_world(head);
push_flat(
rig,
head,
head,
v(0.2 * s, 0.19 * s, 0.22 * s),
1,
0.045 * s,
PrimTint::Body,
);
let snout = add_joint(rig, head, "snout", hp + v(0.0, -0.08 * s, 0.46 * s));
push_cone(
rig,
head,
snout,
0.17 * s,
0.07 * s,
1,
0.04 * s,
PrimTint::Body,
);
let brow = add_joint(rig, head, "brow", hp + v(0.0, 0.09 * s, 0.16 * s));
push_flat(
rig,
brow,
brow,
v(0.19 * s, 0.06 * s, 0.11 * s),
1,
0.02 * s,
PrimTint::Body,
);
let jaw = add_joint(rig, head, "jaw", hp + v(0.0, -0.24 * s, 0.34 * s));
push_cone(
rig,
head,
jaw,
0.15 * s,
0.06 * s,
1,
0.04 * s,
PrimTint::Body,
);
for side in [-1.0f32, 1.0] {
let eb = add_joint(
rig,
head,
"eye",
hp + v(side * 0.14 * s, 0.06 * s, 0.22 * s),
);
let eo = add_joint(
rig,
eb,
"eye_out",
hp + v(side * 0.17 * s, 0.06 * s, 0.27 * s),
);
push_cone(
rig,
eb,
eo,
0.055 * s,
0.035 * s,
6,
0.008 * s,
PrimTint::Eye,
);
}
let hlen = (0.45 + 0.4 * horns) * s;
for side in [-1.0f32, 1.0] {
let hb = add_joint(
rig,
head,
"horn",
hp + v(side * 0.13 * s, 0.14 * s, -0.02 * s),
);
let hm = add_joint(
rig,
hb,
"horn_mid",
hp + v(side * 0.19 * s, 0.22 * s + 0.35 * hlen, -0.5 * hlen),
);
let ht = add_joint(
rig,
hm,
"horn_tip",
hp + v(side * 0.22 * s, 0.24 * s + 0.5 * hlen, -1.05 * hlen),
);
push_cone(
rig,
hb,
hm,
0.09 * s,
0.055 * s,
5,
0.016 * s,
PrimTint::Horn,
);
push_cone(
rig,
hm,
ht,
0.055 * s,
0.012 * s,
5,
0.014 * s,
PrimTint::Horn,
);
}
for side in [-1.0f32, 1.0] {
let fb = add_joint(
rig,
head,
"fang",
hp + v(side * 0.08 * s, -0.28 * s, 0.32 * s),
);
let ft = add_joint(
rig,
fb,
"fang_tip",
hp + v(side * 0.07 * s, -0.4 * s, 0.34 * s),
);
push_cone(
rig,
fb,
ft,
0.022 * s,
0.004 * s,
6,
0.01 * s,
PrimTint::Horn,
);
}
}
fn add_dragon_dorsal_spikes(rig: &mut MonsterRig, s: f32, chain: &[usize]) {
let v = Vec3::new;
let n = chain.len();
for (i, &j) in chain.iter().enumerate() {
let jp = rig.joint_world(j);
let t = i as f32 / (n - 1).max(1) as f32;
let h = (0.28 - 0.14 * (t - 0.35).abs()) * s;
let b0 = add_joint(rig, j, "spike", jp + v(0.0, 0.2 * s, 0.0));
let b1 = add_joint(rig, b0, "spike_tip", jp + v(0.0, 0.2 * s + h, -0.1 * s));
push_cone(
rig,
b0,
b1,
0.055 * s,
0.006 * s,
5,
0.012 * s,
PrimTint::Horn,
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::Mesh;
use glam::{Mat4, Quat};
fn boss(json: &str) -> BossParams {
serde_json::from_str(json).unwrap()
}
#[test]
fn hydra_rig_is_wellformed() {
let br = build_boss_rig(&boss(r#"{"kind":"boss","archetype":"hydra"}"#));
let n = br.rig.skeleton.joints.len();
assert!(br.rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(br.rig.prims.iter().any(|d| d.fold_rank == 0), "has a core");
assert!(
br.parts
.iter()
.filter(|p| p.name.starts_with("head."))
.count()
>= 3
);
assert!(br.weak_points.iter().any(|w| w.name == "weak_point.core"));
}
#[test]
fn hydra_hostile_input_cannot_panic() {
let br = build_boss_rig(&boss(
r#"{"kind":"boss","archetype":"hydra","size":1e30,"phases":999,"eyes":999999,"horns":1e30}"#,
));
let n = br.rig.skeleton.joints.len();
assert!(br.rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(n < 2000, "joint count bounded: {n}");
}
fn max_edge_stretch(bind: &Mesh, posed: &Mesh) -> f32 {
let mut m = 0.0f32;
for tri in bind.indices.chunks(3) {
for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
let bl = bind.positions[a as usize].distance(bind.positions[b as usize]);
let pl = posed.positions[a as usize].distance(posed.positions[b as usize]);
if bl > 1e-5 {
m = m.max(pl / bl);
}
}
}
m
}
#[test]
fn colossus_rig_is_wellformed() {
let br = build_boss_rig(&boss(r#"{"kind":"boss","archetype":"colossus"}"#));
let n = br.rig.skeleton.joints.len();
assert!(br.rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(br.rig.prims.iter().any(|d| d.fold_rank == 0), "has a core");
assert!(
br.parts.iter().any(|p| p.name == "arm.l" && p.destructible),
"has destructible arm.l"
);
assert!(
br.parts.iter().any(|p| p.name == "arm.r" && p.destructible),
"has destructible arm.r"
);
assert!(br.parts.iter().any(|p| p.name == "head"), "has head part");
assert!(
br.weak_points
.iter()
.any(|w| w.name == "weak_point.core" && w.joint == "core" && w.destructible)
);
assert!(
br.rig.skeleton.joints.iter().any(|j| j.name == "core"),
"has a core joint"
);
}
#[test]
fn colossus_hostile_input_cannot_panic() {
let br = build_boss_rig(&boss(
r#"{"kind":"boss","archetype":"colossus","size":1e30,"phases":999,"eyes":999999,"horns":1e30,"plates":1e30,"armor":1e30}"#,
));
let n = br.rig.skeleton.joints.len();
assert!(br.rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(n < 2000, "joint count bounded: {n}");
}
#[test]
fn colossus_skinning_has_no_webs() {
let p = boss(r#"{"kind":"boss","archetype":"colossus","element":"volcanic"}"#);
let br = build_boss_rig(&p);
let pal = crate::palette::by_name("volcanic");
let mut mesh = super::super::body::build_body(&br.rig, p.size, p.detail, p.seed, 0.0, &pal);
mesh.validate()
.expect("bind mesh valid (no degenerate/zero-area tris)");
crate::generators::monster::skin_body(&mut mesh, &br.rig);
assert!(mesh.is_skinned(), "boss mesh must be skinned");
let skel = &br.rig.skeleton;
let mut channels = Vec::new();
for (i, j) in skel.joints.iter().enumerate() {
if j.name.contains("upper") || j.name.contains("fore") || j.name == "thigh" {
channels.push(crate::gltf::Channel {
joint: i,
times: vec![0.0, 1.0],
data: crate::gltf::ChannelData::Rotation(vec![
Quat::IDENTITY,
Quat::from_rotation_x(0.6),
]),
});
}
}
let clip = crate::gltf::AnimationClip {
name: "probe".into(),
channels,
};
let globals = crate::anim::pose_at(skel, &clip, 1.0);
let ibms: Vec<Mat4> = (0..skel.joints.len())
.map(|i| skel.global(i).inverse())
.collect();
let posed = crate::anim::skin_mesh(&mesh, &globals, &ibms);
let moved = mesh
.positions
.iter()
.zip(&posed.positions)
.any(|(a, b)| a.distance(*b) > 0.01);
assert!(moved, "limb bend pose should deform the mesh");
let stretch = max_edge_stretch(&mesh, &posed);
assert!(
stretch < 8.0,
"edge stretch {stretch} indicates a skinning web between arms/core/torso"
);
}
#[test]
fn hydra_skinning_has_no_webs() {
let p = boss(r#"{"kind":"boss","archetype":"hydra","element":"infernal"}"#);
let br = build_boss_rig(&p);
let pal = crate::palette::by_name("infernal");
let mut mesh = super::super::body::build_body(&br.rig, p.size, p.detail, p.seed, 0.0, &pal);
mesh.validate()
.expect("bind mesh valid (no degenerate/zero-area tris)");
crate::generators::monster::skin_body(&mut mesh, &br.rig);
assert!(mesh.is_skinned(), "boss mesh must be skinned");
let skel = &br.rig.skeleton;
let mut channels = Vec::new();
for (i, j) in skel.joints.iter().enumerate() {
if j.name.ends_with("_0") || j.name.ends_with("_1") {
channels.push(crate::gltf::Channel {
joint: i,
times: vec![0.0, 1.0],
data: crate::gltf::ChannelData::Rotation(vec![
Quat::IDENTITY,
Quat::from_rotation_x(0.6),
]),
});
}
}
let clip = crate::gltf::AnimationClip {
name: "probe".into(),
channels,
};
let globals = crate::anim::pose_at(skel, &clip, 1.0);
let ibms: Vec<Mat4> = (0..skel.joints.len())
.map(|i| skel.global(i).inverse())
.collect();
let posed = crate::anim::skin_mesh(&mesh, &globals, &ibms);
let moved = mesh
.positions
.iter()
.zip(&posed.positions)
.any(|(a, b)| a.distance(*b) > 0.01);
assert!(moved, "neck bend pose should deform the mesh");
let stretch = max_edge_stretch(&mesh, &posed);
assert!(
stretch < 8.0,
"edge stretch {stretch} indicates a skinning web between necks/torso"
);
}
#[test]
fn lich_rig_is_wellformed() {
let br = build_boss_rig(&boss(r#"{"kind":"boss","archetype":"lich"}"#));
let n = br.rig.skeleton.joints.len();
assert!(br.rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(br.rig.prims.iter().any(|d| d.fold_rank == 0), "has a core");
assert!(
br.rig.skeleton.joints.iter().any(|j| j.name == "core"),
"has a core joint"
);
assert!(br.parts.iter().any(|p| p.name == "head"), "has head part");
assert!(
br.parts.iter().any(|p| p.name == "throne"),
"has a throne part"
);
assert!(
br.rig.skeleton.joints.iter().any(|j| j.name == "throne"),
"has a throne joint"
);
assert!(
br.parts
.iter()
.filter(|p| p.name.starts_with("implement."))
.count()
>= 2,
"has floating implement parts"
);
assert!(
br.weak_points
.iter()
.any(|w| w.name == "weak_point.phylactery" && w.joint == "core" && w.destructible)
);
assert!(
br.extra_mesh.is_some(),
"throne is a merged CSG mesh, not an SDF primitive"
);
let throne = br.extra_mesh.as_ref().unwrap();
assert!(
throne.triangle_count() > 12,
"throne carve adds geometry: {}",
throne.triangle_count()
);
throne.validate().expect("throne mesh valid");
}
#[test]
fn lich_hostile_input_cannot_panic() {
let br = build_boss_rig(&boss(
r#"{"kind":"boss","archetype":"lich","size":1e30,"phases":999,"eyes":999999,"crown":1e30,"regalia":-1e30}"#,
));
let n = br.rig.skeleton.joints.len();
assert!(br.rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(n < 2000, "joint count bounded: {n}");
}
#[test]
fn lich_skinning_has_no_webs() {
let p = boss(r#"{"kind":"boss","archetype":"lich","element":"necrotic"}"#);
let br = build_boss_rig(&p);
let pal = crate::palette::by_name("necrotic");
let mut mesh = super::super::body::build_body(&br.rig, p.size, p.detail, p.seed, 0.0, &pal);
if let Some(extra) = &br.extra_mesh {
mesh.merge(extra);
}
mesh.validate()
.expect("bind mesh valid (no degenerate/zero-area tris)");
crate::generators::monster::skin_body(&mut mesh, &br.rig);
assert!(mesh.is_skinned(), "boss mesh must be skinned");
let skel = &br.rig.skeleton;
let mut channels = Vec::new();
for (i, j) in skel.joints.iter().enumerate() {
if j.name.contains("upperarm") || j.name.contains("forearm") || j.name == "thigh" {
channels.push(crate::gltf::Channel {
joint: i,
times: vec![0.0, 1.0],
data: crate::gltf::ChannelData::Rotation(vec![
Quat::IDENTITY,
Quat::from_rotation_x(0.6),
]),
});
}
if j.name == "implement_pivot" {
channels.push(crate::gltf::Channel {
joint: i,
times: vec![0.0, 1.0],
data: crate::gltf::ChannelData::Rotation(vec![
Quat::IDENTITY,
Quat::from_rotation_y(1.2),
]),
});
}
}
let clip = crate::gltf::AnimationClip {
name: "probe".into(),
channels,
};
let globals = crate::anim::pose_at(skel, &clip, 1.0);
let ibms: Vec<Mat4> = (0..skel.joints.len())
.map(|i| skel.global(i).inverse())
.collect();
let posed = crate::anim::skin_mesh(&mesh, &globals, &ibms);
let moved = mesh
.positions
.iter()
.zip(&posed.positions)
.any(|(a, b)| a.distance(*b) > 0.01);
assert!(moved, "limb/implement pose should deform the mesh");
let stretch = max_edge_stretch(&mesh, &posed);
assert!(
stretch < 8.0,
"edge stretch {stretch} indicates a skinning web between limbs/implements/torso"
);
let sz = p.size.clamp(0.4, 8.0);
let mut throne_moved = 0usize;
let mut throne_checked = 0usize;
for (a, b) in mesh.positions.iter().zip(&posed.positions) {
if a.z < -0.42 * sz {
throne_checked += 1;
if a.distance(*b) > 0.01 {
throne_moved += 1;
}
}
}
assert!(
throne_checked > 0,
"probe should sample throne backrest vertices"
);
assert_eq!(
throne_moved, 0,
"throne must stay rigid/static under a pose"
);
}
#[test]
fn swarm_queen_rig_is_wellformed() {
let br = build_boss_rig(&boss(r#"{"kind":"boss","archetype":"swarm_queen"}"#));
let n = br.rig.skeleton.joints.len();
assert!(br.rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(br.rig.prims.iter().any(|d| d.fold_rank == 0), "has a core");
assert!(
br.rig.skeleton.joints.iter().any(|j| j.name == "abdomen"),
"has an abdomen joint"
);
assert!(
br.parts
.iter()
.any(|p| p.name == "abdomen" && p.destructible),
"has destructible abdomen part"
);
assert!(br.parts.iter().any(|p| p.name == "head"), "has head part");
assert!(
br.parts
.iter()
.filter(|p| p.name.starts_with("brood_sac."))
.count()
>= 3,
"has at least 3 brood_sac parts"
);
assert!(
br.weak_points
.iter()
.filter(|w| w.name.starts_with("weak_point.brood_sac.") && w.destructible)
.count()
>= 3,
"has at least 3 brood_sac weak points"
);
}
#[test]
fn swarm_queen_hostile_input_cannot_panic() {
let br = build_boss_rig(&boss(
r#"{"kind":"boss","archetype":"swarm_queen","size":1e30,"phases":999,"eyes":999999,"spikes":1e30,"menace":1e30}"#,
));
let n = br.rig.skeleton.joints.len();
assert!(br.rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(n < 2000, "joint count bounded: {n}");
}
#[test]
fn swarm_queen_skinning_has_no_webs() {
let p = boss(r#"{"kind":"boss","archetype":"swarm_queen","element":"fungal"}"#);
let br = build_boss_rig(&p);
let pal = crate::palette::by_name("fungal");
let mut mesh = super::super::body::build_body(&br.rig, p.size, p.detail, p.seed, 0.0, &pal);
mesh.validate()
.expect("bind mesh valid (no degenerate/zero-area tris)");
crate::generators::monster::skin_body(&mut mesh, &br.rig);
assert!(mesh.is_skinned(), "boss mesh must be skinned");
let skel = &br.rig.skeleton;
let mut channels = Vec::new();
for (i, j) in skel.joints.iter().enumerate() {
if j.name == "coxa" || j.name == "knee" {
channels.push(crate::gltf::Channel {
joint: i,
times: vec![0.0, 1.0],
data: crate::gltf::ChannelData::Rotation(vec![
Quat::IDENTITY,
Quat::from_rotation_x(0.6),
]),
});
}
}
let clip = crate::gltf::AnimationClip {
name: "probe".into(),
channels,
};
let globals = crate::anim::pose_at(skel, &clip, 1.0);
let ibms: Vec<Mat4> = (0..skel.joints.len())
.map(|i| skel.global(i).inverse())
.collect();
let posed = crate::anim::skin_mesh(&mesh, &globals, &ibms);
let moved = mesh
.positions
.iter()
.zip(&posed.positions)
.any(|(a, b)| a.distance(*b) > 0.01);
assert!(moved, "leg bend pose should deform the mesh");
let stretch = max_edge_stretch(&mesh, &posed);
assert!(
stretch < 8.0,
"edge stretch {stretch} indicates a skinning web between legs/thorax/brood sacs"
);
}
#[test]
fn dragon_lord_rig_is_wellformed() {
let br = build_boss_rig(&boss(r#"{"kind":"boss","archetype":"dragon_lord"}"#));
let n = br.rig.skeleton.joints.len();
assert!(br.rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(br.rig.prims.iter().any(|d| d.fold_rank == 0), "has a core");
assert!(
br.rig.skeleton.joints.iter().any(|j| j.name == "head"),
"has a head joint (breath origin)"
);
assert!(br.parts.iter().any(|p| p.name == "head"), "has head part");
assert!(
br.parts
.iter()
.any(|p| p.name == "wing.l" && p.destructible),
"has destructible wing.l"
);
assert!(
br.parts
.iter()
.any(|p| p.name == "wing.r" && p.destructible),
"has destructible wing.r"
);
assert!(
br.rig.skeleton.joints.iter().any(|j| j.name == "heart"),
"has a heart joint"
);
assert!(
br.weak_points
.iter()
.any(|w| w.name == "weak_point.heart" && w.joint == "heart" && w.destructible)
);
}
#[test]
fn dragon_lord_hostile_input_cannot_panic() {
let br = build_boss_rig(&boss(
r#"{"kind":"boss","archetype":"dragon_lord","size":1e30,"phases":999,"eyes":999999,"horns":1e30,"wings":1e30,"menace":1e30}"#,
));
let n = br.rig.skeleton.joints.len();
assert!(br.rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(n < 2000, "joint count bounded: {n}");
}
#[test]
fn dragon_lord_skinning_has_no_webs() {
let p = boss(r#"{"kind":"boss","archetype":"dragon_lord","element":"arctic"}"#);
let br = build_boss_rig(&p);
let pal = crate::palette::by_name("arctic");
let mut mesh = super::super::body::build_body(&br.rig, p.size, p.detail, p.seed, 0.0, &pal);
mesh.validate()
.expect("bind mesh valid (no degenerate/zero-area tris)");
crate::generators::monster::skin_body(&mut mesh, &br.rig);
assert!(mesh.is_skinned(), "boss mesh must be skinned");
let skel = &br.rig.skeleton;
let mut channels = Vec::new();
for (i, j) in skel.joints.iter().enumerate() {
if j.name.starts_with("neck") {
channels.push(crate::gltf::Channel {
joint: i,
times: vec![0.0, 1.0],
data: crate::gltf::ChannelData::Rotation(vec![
Quat::IDENTITY,
Quat::from_rotation_x(0.35),
]),
});
}
if j.name.ends_with("_root") {
channels.push(crate::gltf::Channel {
joint: i,
times: vec![0.0, 1.0],
data: crate::gltf::ChannelData::Rotation(vec![
Quat::IDENTITY,
Quat::from_rotation_z(0.35),
]),
});
}
}
let clip = crate::gltf::AnimationClip {
name: "probe".into(),
channels,
};
let globals = crate::anim::pose_at(skel, &clip, 1.0);
let ibms: Vec<Mat4> = (0..skel.joints.len())
.map(|i| skel.global(i).inverse())
.collect();
let posed = crate::anim::skin_mesh(&mesh, &globals, &ibms);
let moved = mesh
.positions
.iter()
.zip(&posed.positions)
.any(|(a, b)| a.distance(*b) > 0.01);
assert!(moved, "neck/wing pose should deform the mesh");
let sz = p.size.clamp(0.4, 8.0);
let mut rear_checked = 0usize;
let mut rear_moved = 0usize;
for (a, b) in mesh.positions.iter().zip(&posed.positions) {
if a.z < -sz {
rear_checked += 1;
if a.distance(*b) > 0.05 * sz {
rear_moved += 1;
}
}
}
assert!(
rear_checked > 0,
"probe should sample rear torso/tail verts"
);
assert_eq!(
rear_moved, 0,
"wing/neck pose should not drag rear torso/tail vertices (web)"
);
let stretch = max_edge_stretch(&mesh, &posed);
assert!(
stretch < 8.0,
"edge stretch {stretch} indicates a skinning web between wings/neck/torso"
);
}
}