feat(core): polygonal room shapes (rect/octagon/ellipse/plus)
RoomCarver now rolls a per-room shape within its chosen rectangle, weighted by a new RoomConfig.shapes (ShapeWeights). The rect is still picked first and unchanged, so room centers — and the corridors wired between them — are identical to before; only the carved interior differs. - Shapes built from per-row contiguous spans → guaranteed 4-connected and convex, so flood-fill never strands a cell. - Hard invariant: every shape contains rect.center() (the corridor target), with a fallback to a full rect for tiny rooms or any non-covering shape — keeps MstConnect/CorridorCarver correct. - ShapeWeights::default() is rect-only (preserves old behavior + existing tests); the dungeon recipe opts into ShapeWeights::varied(). - Configs lose Eq (now carry f64 weights); serde unaffected. Core: 116 tests green (new forced-shape invariants test: in-bounds, center carved, non-rectangular, 4-connected); connectivity + determinism + door tests all pass with varied shapes active. Surfaced as 4 shape-weight sliders in the web playground. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f849d82d31
commit
a1d2a63937
4 changed files with 273 additions and 13 deletions
|
|
@ -27,7 +27,7 @@ pub mod bsp;
|
||||||
pub use bsp::{BspConfig, BspPartition};
|
pub use bsp::{BspConfig, BspPartition};
|
||||||
|
|
||||||
pub mod room;
|
pub mod room;
|
||||||
pub use room::{RoomCarver, RoomConfig};
|
pub use room::{RoomCarver, RoomConfig, ShapeWeights};
|
||||||
|
|
||||||
pub mod connect;
|
pub mod connect;
|
||||||
pub use connect::{ConnectConfig, MstConnect};
|
pub use connect::{ConnectConfig, MstConnect};
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::geometry::Rect;
|
use crate::geometry::{Point, Rect};
|
||||||
use crate::map::Tile;
|
use crate::map::Tile;
|
||||||
use crate::pass::{GenContext, Pass};
|
use crate::pass::{GenContext, Pass};
|
||||||
use crate::region::RegionKind;
|
use crate::region::RegionKind;
|
||||||
|
|
@ -49,7 +49,7 @@ use crate::rng::Rng;
|
||||||
/// `margin` of cells is reserved on every side. The margin keeps room floors off
|
/// `margin` of cells is reserved on every side. The margin keeps room floors off
|
||||||
/// the leaf boundary so the surrounding wall — and the corridors that pierce it
|
/// the leaf boundary so the surrounding wall — and the corridors that pierce it
|
||||||
/// — have room to exist.
|
/// — have room to exist.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct RoomConfig {
|
pub struct RoomConfig {
|
||||||
/// The smallest allowed room extent (in cells) on either axis. A leaf that
|
/// The smallest allowed room extent (in cells) on either axis. A leaf that
|
||||||
/// cannot hold a `min_size`-square room (after the margin) is skipped.
|
/// cannot hold a `min_size`-square room (after the margin) is skipped.
|
||||||
|
|
@ -59,26 +59,92 @@ pub struct RoomConfig {
|
||||||
pub max_size: i32,
|
pub max_size: i32,
|
||||||
/// Cells reserved on every side between the room and its leaf edge.
|
/// Cells reserved on every side between the room and its leaf edge.
|
||||||
pub margin: i32,
|
pub margin: i32,
|
||||||
|
/// Relative weights for the shape each room is carved in (see
|
||||||
|
/// [`ShapeWeights`]). The primitive default is rectangle-only.
|
||||||
|
pub shapes: ShapeWeights,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RoomConfig {
|
impl Default for RoomConfig {
|
||||||
/// A sensible default for the v1 dungeon: rooms between 4 and 10 cells on a
|
/// A sensible default for the v1 dungeon: rooms between 4 and 10 cells on a
|
||||||
/// side, set in one cell from each leaf edge.
|
/// side, set in one cell from each leaf edge, carved as plain rectangles.
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
RoomConfig {
|
RoomConfig {
|
||||||
min_size: 4,
|
min_size: 4,
|
||||||
max_size: 10,
|
max_size: 10,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
|
shapes: ShapeWeights::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Relative weights for the shape a room is carved in within its chosen
|
||||||
|
/// rectangle. Each room independently rolls a [`RoomShape`] with probability
|
||||||
|
/// proportional to these weights; a `0.0` weight disables that shape, and an
|
||||||
|
/// all-zero set falls back to a plain rectangle.
|
||||||
|
///
|
||||||
|
/// The [`Default`] is **rectangle-only** — the original behavior — so a bare
|
||||||
|
/// [`RoomCarver`] still carves rectangles. The dungeon recipe opts into a richer
|
||||||
|
/// mix via [`ShapeWeights::varied`] (see [`DungeonConfig`](crate::recipes::dungeon::DungeonConfig)).
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct ShapeWeights {
|
||||||
|
/// Weight of a full axis-aligned rectangle (the whole picked rect).
|
||||||
|
pub rect: f64,
|
||||||
|
/// Weight of an octagon — a rectangle with chamfered (cut) corners.
|
||||||
|
pub octagon: f64,
|
||||||
|
/// Weight of an inscribed ellipse — a rounded room.
|
||||||
|
pub ellipse: f64,
|
||||||
|
/// Weight of a plus/cross — central bars spanning the rect's width and height.
|
||||||
|
pub plus: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ShapeWeights {
|
||||||
|
/// Rectangle-only — the original room shape. Recipes opt into variety.
|
||||||
|
fn default() -> Self {
|
||||||
|
ShapeWeights {
|
||||||
|
rect: 1.0,
|
||||||
|
octagon: 0.0,
|
||||||
|
ellipse: 0.0,
|
||||||
|
plus: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ShapeWeights {
|
||||||
|
/// A balanced mix used by the dungeon recipe: rectangles stay common, with a
|
||||||
|
/// healthy share of octagons and ellipses and the occasional plus.
|
||||||
|
pub fn varied() -> Self {
|
||||||
|
ShapeWeights {
|
||||||
|
rect: 2.0,
|
||||||
|
octagon: 1.5,
|
||||||
|
ellipse: 1.5,
|
||||||
|
plus: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The shape a room is carved in within its chosen rectangle.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum RoomShape {
|
||||||
|
/// The full rectangle (every cell).
|
||||||
|
Rect,
|
||||||
|
/// A rectangle with chamfered corners.
|
||||||
|
Octagon,
|
||||||
|
/// An inscribed ellipse.
|
||||||
|
Ellipse,
|
||||||
|
/// A central cross spanning the rect.
|
||||||
|
Plus,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Below this extent on either axis a room is always a plain rectangle — a
|
||||||
|
/// shape too small to read as anything else (and to keep tiny rooms solid).
|
||||||
|
const SHAPE_MIN: i32 = 5;
|
||||||
|
|
||||||
/// A room-carving shaper pass (spec §4.7).
|
/// A room-carving shaper pass (spec §4.7).
|
||||||
///
|
///
|
||||||
/// Turns each placeholder [`Room`](crate::region::RegionKind::Room) region into
|
/// Turns each placeholder [`Room`](crate::region::RegionKind::Room) region into
|
||||||
/// an actual carved room. Construct one with [`RoomCarver::new`]; it implements
|
/// an actual carved room. Construct one with [`RoomCarver::new`]; it implements
|
||||||
/// [`Pass`] with the stable name `"room_carver"`.
|
/// [`Pass`] with the stable name `"room_carver"`.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct RoomCarver {
|
pub struct RoomCarver {
|
||||||
/// The size/margin limits rooms are carved under.
|
/// The size/margin limits rooms are carved under.
|
||||||
cfg: RoomConfig,
|
cfg: RoomConfig,
|
||||||
|
|
@ -122,6 +188,129 @@ impl RoomCarver {
|
||||||
|
|
||||||
Some(Rect::new(x, y, w, h))
|
Some(Rect::new(x, y, w, h))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Rolls a [`RoomShape`] weighted by the config, drawing exactly one value
|
||||||
|
/// from `rng`. Weights are scaled to integer "milli-weights" so the pick is
|
||||||
|
/// a single deterministic [`Rng::range`] draw; an all-zero (or negative) set
|
||||||
|
/// degenerates to [`RoomShape::Rect`].
|
||||||
|
fn pick_shape(&self, rng: &mut Rng) -> RoomShape {
|
||||||
|
let w = self.cfg.shapes;
|
||||||
|
let table = [
|
||||||
|
(RoomShape::Rect, w.rect),
|
||||||
|
(RoomShape::Octagon, w.octagon),
|
||||||
|
(RoomShape::Ellipse, w.ellipse),
|
||||||
|
(RoomShape::Plus, w.plus),
|
||||||
|
];
|
||||||
|
let scaled: [(RoomShape, i32); 4] = table.map(|(s, wt)| (s, (wt.max(0.0) * 1000.0) as i32));
|
||||||
|
let total: i32 = scaled.iter().map(|(_, n)| n).sum();
|
||||||
|
if total <= 0 {
|
||||||
|
return RoomShape::Rect;
|
||||||
|
}
|
||||||
|
let mut pick = rng.range(0, total);
|
||||||
|
for (s, n) in scaled {
|
||||||
|
if pick < n {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
pick -= n;
|
||||||
|
}
|
||||||
|
RoomShape::Rect
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The interior floor cells for `shape` within `rect`, in row-major order.
|
||||||
|
///
|
||||||
|
/// Every shape is built from per-row **contiguous** column spans, which makes
|
||||||
|
/// the cell set 4-connected and convex enough that a flood-fill reaches every
|
||||||
|
/// cell from any other (so a shaped room never strands part of itself). As a
|
||||||
|
/// hard invariant the result always contains `rect.center()` — the cell
|
||||||
|
/// `MstConnect`/`CorridorCarver` wire corridors to — and a rect too small to
|
||||||
|
/// read as a shape (or, defensively, any shape that failed to cover the center)
|
||||||
|
/// falls back to the full rectangle.
|
||||||
|
fn shape_cells(rect: Rect, shape: RoomShape) -> Vec<Point> {
|
||||||
|
if shape == RoomShape::Rect || rect.w < SHAPE_MIN || rect.h < SHAPE_MIN {
|
||||||
|
return rect.iter().collect();
|
||||||
|
}
|
||||||
|
let cells = match shape {
|
||||||
|
RoomShape::Rect => unreachable!("handled above"),
|
||||||
|
RoomShape::Octagon => octagon_cells(rect),
|
||||||
|
RoomShape::Ellipse => ellipse_cells(rect),
|
||||||
|
RoomShape::Plus => plus_cells(rect),
|
||||||
|
};
|
||||||
|
if cells.contains(&rect.center()) {
|
||||||
|
cells
|
||||||
|
} else {
|
||||||
|
rect.iter().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collect row-major cells where row `r` (0-based within `rect`) spans the
|
||||||
|
/// inclusive *local* column range returned by `span(r)` — clamped to the rect.
|
||||||
|
/// A row whose `right < left` contributes nothing.
|
||||||
|
fn span_cells(rect: Rect, mut span: impl FnMut(i32) -> (i32, i32)) -> Vec<Point> {
|
||||||
|
let mut cells = Vec::new();
|
||||||
|
for r in 0..rect.h {
|
||||||
|
let (l, right) = span(r);
|
||||||
|
let lo = l.max(0);
|
||||||
|
let hi = right.min(rect.w - 1);
|
||||||
|
for c in lo..=hi {
|
||||||
|
cells.push(Point::new(rect.x + c, rect.y + r));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cells
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A rectangle with chamfered corners: each corner has a triangular bite of
|
||||||
|
/// side `min(w, h) / 3` removed.
|
||||||
|
fn octagon_cells(rect: Rect) -> Vec<Point> {
|
||||||
|
let (w, h) = (rect.w, rect.h);
|
||||||
|
let cut = (w.min(h) / 3).max(1);
|
||||||
|
span_cells(rect, |r| {
|
||||||
|
let top = (cut - r).max(0);
|
||||||
|
let bottom = (cut - (h - 1 - r)).max(0);
|
||||||
|
let inset = top.max(bottom);
|
||||||
|
(inset, w - 1 - inset)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An ellipse inscribed in the rect, filled by per-row half-widths so each row
|
||||||
|
/// is one contiguous span (the center row spans the full width, so the bounding
|
||||||
|
/// box stays exactly `rect`).
|
||||||
|
fn ellipse_cells(rect: Rect) -> Vec<Point> {
|
||||||
|
let a = rect.w as f64 / 2.0;
|
||||||
|
let b = rect.h as f64 / 2.0;
|
||||||
|
let ecx = (rect.w - 1) as f64 / 2.0;
|
||||||
|
let ecy = (rect.h - 1) as f64 / 2.0;
|
||||||
|
span_cells(rect, |r| {
|
||||||
|
let dy = (r as f64 - ecy) / b;
|
||||||
|
let s = 1.0 - dy * dy;
|
||||||
|
if s < 0.0 {
|
||||||
|
return (1, 0); // empty row
|
||||||
|
}
|
||||||
|
let hw = a * s.sqrt();
|
||||||
|
((ecx - hw).ceil() as i32, (ecx + hw).floor() as i32)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A plus/cross: a vertical bar (thickness `w/3`) and a horizontal bar
|
||||||
|
/// (thickness `h/3`), both centered on `rect.center()` so the center is always
|
||||||
|
/// carved and the bars reach all four edges.
|
||||||
|
fn plus_cells(rect: Rect) -> Vec<Point> {
|
||||||
|
let (w, h) = (rect.w, rect.h);
|
||||||
|
let tw = (w / 3).max(1);
|
||||||
|
let th = (h / 3).max(1);
|
||||||
|
let cx = w / 2; // local center column, matching Rect::center
|
||||||
|
let cy = h / 2; // local center row
|
||||||
|
let vx0 = (cx - tw / 2).max(0);
|
||||||
|
let vx1 = (vx0 + tw - 1).min(w - 1);
|
||||||
|
let hy0 = (cy - th / 2).max(0);
|
||||||
|
let hy1 = (hy0 + th - 1).min(h - 1);
|
||||||
|
span_cells(rect, |r| {
|
||||||
|
if r >= hy0 && r <= hy1 {
|
||||||
|
(0, w - 1) // a full-width row through the horizontal bar
|
||||||
|
} else {
|
||||||
|
(vx0, vx1) // the vertical bar only
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pass for RoomCarver {
|
impl Pass for RoomCarver {
|
||||||
|
|
@ -144,19 +333,27 @@ impl Pass for RoomCarver {
|
||||||
// cells. Never removed — that would break RegionId-as-index.
|
// cells. Never removed — that would break RegionId-as-index.
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
// Roll the room's shape and compute its interior cells. The rect is
|
||||||
|
// picked first (above) exactly as before, so room positions/sizes —
|
||||||
|
// and therefore the corridors wired between room centers — are
|
||||||
|
// unchanged by shaping; only the carved interior differs.
|
||||||
|
let shape = self.pick_shape(rng);
|
||||||
|
let cells = shape_cells(room, shape);
|
||||||
|
|
||||||
// Carve Floor over the room rect. Writes route through the bounds-safe
|
// Carve Floor over the shaped cells. Writes route through the
|
||||||
// accessor, so any cell outside the grid is silently skipped.
|
// bounds-safe accessor, so any cell outside the grid is silently
|
||||||
for p in room.iter() {
|
// skipped.
|
||||||
|
for &p in &cells {
|
||||||
ctx.tiles.set(p, Tile::Floor);
|
ctx.tiles.set(p, Tile::Floor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finalize the region: bounds become the actual room rect, and cells
|
// Finalize the region: bounds is the picked rect (the AABB enclosing
|
||||||
// become exactly the room interior floor cells (row-major order).
|
// the shape, whose center is guaranteed carved), and cells are
|
||||||
// DoorPlacer relies on `cells` being the room interior only.
|
// exactly the carved interior floor cells. DoorPlacer relies on
|
||||||
|
// `cells` being the room interior only.
|
||||||
let region = &mut ctx.regions[idx];
|
let region = &mut ctx.regions[idx];
|
||||||
region.bounds = room;
|
region.bounds = room;
|
||||||
region.cells = room.iter().collect();
|
region.cells = cells;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -247,6 +444,7 @@ mod tests {
|
||||||
min_size: 3,
|
min_size: 3,
|
||||||
max_size: 6,
|
max_size: 6,
|
||||||
margin: 2,
|
margin: 2,
|
||||||
|
shapes: ShapeWeights::default(),
|
||||||
};
|
};
|
||||||
let leaves = [Rect::new(2, 2, 18, 18)];
|
let leaves = [Rect::new(2, 2, 18, 18)];
|
||||||
let mut ctx = ctx_with_leaves(24, 24, &leaves);
|
let mut ctx = ctx_with_leaves(24, 24, &leaves);
|
||||||
|
|
@ -283,6 +481,7 @@ mod tests {
|
||||||
min_size: 6,
|
min_size: 6,
|
||||||
max_size: 10,
|
max_size: 10,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
|
shapes: ShapeWeights::default(),
|
||||||
};
|
};
|
||||||
// 4x4 leaf: after a 1-cell margin the interior is 2x2 < min_size 6.
|
// 4x4 leaf: after a 1-cell margin the interior is 2x2 < min_size 6.
|
||||||
let leaves = [Rect::new(1, 1, 4, 4)];
|
let leaves = [Rect::new(1, 1, 4, 4)];
|
||||||
|
|
@ -311,6 +510,7 @@ mod tests {
|
||||||
min_size: 4,
|
min_size: 4,
|
||||||
max_size: 8,
|
max_size: 8,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
|
shapes: ShapeWeights::default(),
|
||||||
};
|
};
|
||||||
let leaves = [
|
let leaves = [
|
||||||
Rect::new(0, 0, 16, 16), // big: carved
|
Rect::new(0, 0, 16, 16), // big: carved
|
||||||
|
|
@ -380,6 +580,7 @@ mod tests {
|
||||||
min_size: 4,
|
min_size: 4,
|
||||||
max_size: 8,
|
max_size: 8,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
|
shapes: ShapeWeights::default(),
|
||||||
};
|
};
|
||||||
// Grid is 8x8, but the leaf claims x:4..16 — half of any room may fall
|
// Grid is 8x8, but the leaf claims x:4..16 — half of any room may fall
|
||||||
// outside the grid. The carve must clip silently.
|
// outside the grid. The carve must clip silently.
|
||||||
|
|
@ -426,4 +627,53 @@ mod tests {
|
||||||
// The room region was carved.
|
// The room region was carved.
|
||||||
assert!(!ctx.regions[0].cells.is_empty());
|
assert!(!ctx.regions[0].cells.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Each non-rectangular shape, when forced, carves a room that: stays inside
|
||||||
|
/// its bounds (all Floor), contains its `bounds.center()` as a carved cell
|
||||||
|
/// (the corridor-target invariant), is strictly smaller than its bounding
|
||||||
|
/// area (genuinely not a rectangle), and is 4-connected (no stranded cells).
|
||||||
|
#[test]
|
||||||
|
fn forced_shapes_fit_bounds_contain_center_and_are_connected() {
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
let only = |octagon, ellipse, plus| ShapeWeights { rect: 0.0, octagon, ellipse, plus };
|
||||||
|
let cases = [
|
||||||
|
("octagon", only(1.0, 0.0, 0.0)),
|
||||||
|
("ellipse", only(0.0, 1.0, 0.0)),
|
||||||
|
("plus", only(0.0, 0.0, 1.0)),
|
||||||
|
];
|
||||||
|
for (name, weights) in cases {
|
||||||
|
let cfg = RoomConfig { min_size: 9, max_size: 13, margin: 1, shapes: weights };
|
||||||
|
let leaves = [Rect::new(0, 0, 20, 20)];
|
||||||
|
let mut ctx = ctx_with_leaves(20, 20, &leaves);
|
||||||
|
run(&mut ctx, cfg, 0x9001);
|
||||||
|
let r = &ctx.regions[0];
|
||||||
|
|
||||||
|
assert!(!r.cells.is_empty(), "{name}: carved nothing");
|
||||||
|
for &p in &r.cells {
|
||||||
|
assert!(r.bounds.contains(p), "{name}: cell {p:?} outside bounds {:?}", r.bounds);
|
||||||
|
assert_eq!(ctx.tiles.get(p), Some(&Tile::Floor), "{name}: cell {p:?} not Floor");
|
||||||
|
}
|
||||||
|
|
||||||
|
let center = r.bounds.center();
|
||||||
|
assert!(r.cells.contains(¢er), "{name}: center {center:?} must be carved");
|
||||||
|
assert_eq!(ctx.tiles.get(center), Some(&Tile::Floor));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
(r.cells.len() as i32) < r.bounds.w * r.bounds.h,
|
||||||
|
"{name}: a shaped room must carve fewer cells than its bounding area"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flood-fill from the first cell must reach every cell (4-connected).
|
||||||
|
let set: BTreeSet<(i32, i32)> = r.cells.iter().map(|p| (p.x, p.y)).collect();
|
||||||
|
let mut seen = BTreeSet::new();
|
||||||
|
let mut stack = vec![(r.cells[0].x, r.cells[0].y)];
|
||||||
|
while let Some((x, y)) = stack.pop() {
|
||||||
|
if !set.contains(&(x, y)) || !seen.insert((x, y)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stack.extend([(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]);
|
||||||
|
}
|
||||||
|
assert_eq!(seen.len(), r.cells.len(), "{name}: shape must be 4-connected");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::pass::Pipeline;
|
use crate::pass::Pipeline;
|
||||||
use crate::passes::{
|
use crate::passes::{
|
||||||
BspConfig, BspPartition, ConnectConfig, CorridorCarver, DoorConfig, DoorPlacer, MstConnect,
|
BspConfig, BspPartition, ConnectConfig, CorridorCarver, DoorConfig, DoorPlacer, MstConnect,
|
||||||
RoomCarver, RoomConfig,
|
RoomCarver, RoomConfig, ShapeWeights,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Configuration for the [`dungeon`] recipe.
|
/// Configuration for the [`dungeon`] recipe.
|
||||||
|
|
@ -90,6 +90,7 @@ impl Default for DungeonConfig {
|
||||||
min_size: 5,
|
min_size: 5,
|
||||||
max_size: 11,
|
max_size: 11,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
|
shapes: ShapeWeights::varied(),
|
||||||
},
|
},
|
||||||
connect: ConnectConfig {
|
connect: ConnectConfig {
|
||||||
extra_edge_ratio: 0.30,
|
extra_edge_ratio: 0.30,
|
||||||
|
|
@ -410,6 +411,7 @@ mod tests {
|
||||||
min_size: 5,
|
min_size: 5,
|
||||||
max_size: 10,
|
max_size: 10,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
|
shapes: ShapeWeights::default(),
|
||||||
},
|
},
|
||||||
..DungeonConfig::default()
|
..DungeonConfig::default()
|
||||||
};
|
};
|
||||||
|
|
@ -432,6 +434,7 @@ mod tests {
|
||||||
min_size: 4,
|
min_size: 4,
|
||||||
max_size: 10,
|
max_size: 10,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
|
shapes: ShapeWeights::default(),
|
||||||
},
|
},
|
||||||
..DungeonConfig::default()
|
..DungeonConfig::default()
|
||||||
};
|
};
|
||||||
|
|
@ -450,6 +453,7 @@ mod tests {
|
||||||
min_size: 8,
|
min_size: 8,
|
||||||
max_size: 4,
|
max_size: 4,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
|
shapes: ShapeWeights::default(),
|
||||||
},
|
},
|
||||||
bsp: BspConfig {
|
bsp: BspConfig {
|
||||||
min_leaf: 20,
|
min_leaf: 20,
|
||||||
|
|
@ -464,6 +468,7 @@ mod tests {
|
||||||
min_size: 0,
|
min_size: 0,
|
||||||
max_size: 4,
|
max_size: 4,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
|
shapes: ShapeWeights::default(),
|
||||||
},
|
},
|
||||||
..DungeonConfig::default()
|
..DungeonConfig::default()
|
||||||
};
|
};
|
||||||
|
|
@ -474,6 +479,7 @@ mod tests {
|
||||||
min_size: 2,
|
min_size: 2,
|
||||||
max_size: 4,
|
max_size: 4,
|
||||||
margin: -1,
|
margin: -1,
|
||||||
|
shapes: ShapeWeights::default(),
|
||||||
},
|
},
|
||||||
..DungeonConfig::default()
|
..DungeonConfig::default()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,10 @@ const SLIDERS = [
|
||||||
['rooms.margin', 'margin', 0, 3, 1],
|
['rooms.margin', 'margin', 0, 3, 1],
|
||||||
['connect.extra_edge_ratio', 'loop edges', 0, 0.8, 0.02],
|
['connect.extra_edge_ratio', 'loop edges', 0, 0.8, 0.02],
|
||||||
['doors.door_chance', 'door chance', 0, 1, 0.05],
|
['doors.door_chance', 'door chance', 0, 1, 0.05],
|
||||||
|
['rooms.shapes.rect', '▢ rect', 0, 3, 0.1],
|
||||||
|
['rooms.shapes.octagon', '⬡ octagon', 0, 3, 0.1],
|
||||||
|
['rooms.shapes.ellipse', '◯ ellipse', 0, 3, 0.1],
|
||||||
|
['rooms.shapes.plus', '✚ plus', 0, 3, 0.1],
|
||||||
];
|
];
|
||||||
|
|
||||||
function getPath(obj, path) {
|
function getPath(obj, path) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue