mod cavern;
mod dress;
mod geom;
mod layout;
pub mod manifest;
pub mod model;
use glam::Vec3;
use crate::gltf::{Asset, Collider, Material, Part, Physics};
use crate::mesh::Mesh;
use crate::palette::Palette;
use crate::recipe::DungeonParams;
use model::{Corridor, Door, DungeonModel, Room};
const TORCH_GLOW: Vec3 = Vec3::new(1.0, 0.55, 0.18);
pub fn generate(p: &DungeonParams, pal: &Palette) -> Result<Asset, String> {
let model = DungeonModel::new(p, pal)?;
Ok(merged_asset(&model))
}
fn dress_meshes(model: &DungeonModel, room: &Room) -> (Mesh, Mesh) {
let (mut opaque, mut emissive) = (Mesh::new(), Mesh::new());
for pp in dress::dress_room(
room,
model.p.theme,
model.p.density,
model.p.seed,
&model.pal,
) {
match pp.emissive {
Some(_) => emissive.merge(&pp.mesh),
None => opaque.merge(&pp.mesh),
}
}
(opaque, emissive)
}
fn carved_room(model: &DungeonModel, room: &Room, include_ceiling: bool) -> Mesh {
let walls = geom::room_mesh(
room,
model.p.theme,
&model.pal,
model.p.detail,
include_ceiling,
);
let doors: Vec<Door> = model
.doors
.iter()
.filter(|d| d.room == room.id)
.cloned()
.collect();
geom::carve_doorways(walls, &doors)
}
fn assemble(opaque: Mesh, emissive: Mesh) -> Asset {
let mut parts = vec![Part {
mesh: opaque,
material: Material {
roughness: 0.92,
..Default::default()
},
}];
if !emissive.positions.is_empty() {
parts.push(Part {
mesh: emissive,
material: Material {
roughness: 0.4,
emissive: TORCH_GLOW * 2.4,
..Default::default()
},
});
}
Asset::static_mesh(
"dungeon",
parts,
Some(Physics {
collider: Collider::TriMesh,
mass: 0.0,
friction: 0.9,
restitution: 0.0,
}),
)
}
fn build_merged(model: &DungeonModel, include_ceiling: bool) -> Asset {
let mut opaque = Mesh::new();
let mut emissive = Mesh::new();
let cavern = matches!(model.p.theme, crate::recipe::DungeonTheme::Cavern);
if cavern {
opaque.merge(&cavern::cavern_mesh(
&model.rooms,
&model.corridors,
&model.pal,
model.p.detail,
include_ceiling,
));
}
for room in &model.rooms {
if !cavern {
opaque.merge(&carved_room(model, room, include_ceiling));
}
let (o, e) = dress_meshes(model, room);
opaque.merge(&o);
emissive.merge(&e);
}
if !cavern {
for c in &model.corridors {
opaque.merge(&geom::corridor_mesh(
c,
model.p.theme,
&model.pal,
model.p.detail,
include_ceiling,
));
}
}
assemble(opaque, emissive)
}
pub fn merged_asset(model: &DungeonModel) -> Asset {
build_merged(model, true)
}
pub fn overview_asset(model: &DungeonModel) -> Asset {
build_merged(model, false)
}
pub(crate) fn room_asset(model: &DungeonModel, room: &Room) -> Asset {
let cavern = matches!(model.p.theme, crate::recipe::DungeonTheme::Cavern);
let owned: Vec<Corridor> = model
.corridors
.iter()
.filter(|c| c.a.min(c.b) == room.id)
.cloned()
.collect();
let mut opaque = if cavern {
cavern::cavern_mesh(
std::slice::from_ref(room),
&owned,
&model.pal,
model.p.detail,
true,
)
} else {
let mut m = carved_room(model, room, true);
for c in &owned {
m.merge(&geom::corridor_mesh(
c,
model.p.theme,
&model.pal,
model.p.detail,
true,
));
}
m
};
let (o, e) = dress_meshes(model, room);
opaque.merge(&o);
assemble(opaque, e)
}
pub(crate) fn place_boss(bp: &crate::recipe::BossParams, room: &Room, spawn_pos: Vec3) -> Asset {
let pal_name = crate::recipe::element_palette(bp.element);
let pal = crate::palette::by_name(pal_name);
let mut asset = super::boss::generate(bp, &pal);
let mut lo = Vec3::splat(f32::INFINITY);
let mut hi = Vec3::splat(f32::NEG_INFINITY);
for part in &asset.parts {
for p in &part.mesh.positions {
lo = lo.min(*p);
hi = hi.max(*p);
}
}
if !lo.is_finite() || !hi.is_finite() {
return asset;
}
let extent = hi - lo;
let room_w = (room.max.x - room.min.x).max(1.0);
let room_d = (room.max.z - room.min.z).max(1.0);
const MARGIN: f32 = 0.7;
let footprint = extent.x.max(extent.z).max(1e-4);
let scale = (room_w.min(room_d) * MARGIN / footprint).max(0.05);
let center_xz = Vec3::new((lo.x + hi.x) * 0.5, 0.0, (lo.z + hi.z) * 0.5);
let base_y = lo.y;
let target = Vec3::new(spawn_pos.x.round(), room.min.y.round(), spawn_pos.z.round());
let translate = Vec3::new(
target.x - center_xz.x * scale,
target.y - base_y * scale,
target.z - center_xz.z * scale,
);
for part in &mut asset.parts {
for p in &mut part.mesh.positions {
*p = *p * scale + translate;
}
}
if let Some(skel) = &mut asset.skeleton {
for j in &mut skel.joints {
j.translation = if j.parent.is_none() {
j.translation * scale + translate
} else {
j.translation * scale
};
}
}
if let Some(phys) = &mut asset.physics {
match &mut phys.collider {
Collider::Box { half_extents } => *half_extents *= scale,
Collider::Sphere { radius } => *radius *= scale,
Collider::Capsule { radius, height } => {
*radius *= scale;
*height *= scale;
}
Collider::TriMesh | Collider::Heightfield => {}
}
}
if let Some(bm) = &mut asset.boss {
bm.arena.recommended_radius *= scale;
for wp in &mut bm.weak_points {
for c in wp.offset.iter_mut() {
*c *= scale;
}
use crate::generators::boss::meta::ColliderJson;
match &mut wp.collider {
ColliderJson::Sphere { radius } => *radius *= scale,
ColliderJson::Box { half_extents } => {
for h in half_extents.iter_mut() {
*h *= scale;
}
}
ColliderJson::Capsule { radius, height } => {
*radius *= scale;
*height *= scale;
}
}
}
}
asset
}