Phase 7 checkpoint

This commit is contained in:
2026-10-30 19:58:52 +03:00
parent 141bfd31c1
commit 214b05b40f
15 changed files with 1110 additions and 163 deletions
+64 -3
View File
@@ -3121,6 +3121,59 @@ hosts a staircase from its own level up to the next. The generator:
"Stairwell"; Lua exposes `stairRectName` on the `CellGrid`
component. A stairwell room on the top floor has no floor above -
the generator warns once and ignores those flags.
- Automatic stairwells (phase 7.3a): `TownGenSystem::
fillBuildingCells` calls `autoPlaceStairwell()` for plans with more
than one floor. An explicit plan stair cell wins; otherwise a
deterministic straight run is carved along the longer usable axis
(4 cells first, 3 as a fallback) on the first row/column that is
free of door cells and has a boarding cell on the ground floor and
a step-off cell on the top floor. Interior walls + internal
windows on the run cells are opened (the run may cross room
boundaries), as are the facing walls at the boarding and step-off
cells. When no run fits, one `TownGen: warning - no stairwell fits
building ...` line is logged and the building stays as before.
This covers the built-in parametric layouts, `town_layouts.txt`
layouts and inline sources without editing layout text; exterior-
only shells are unchanged (stairs are interior geometry). The DSL
help window documents `:type stairwell` + the automatic stairwell,
and `LayoutLibrary::examples()` ships "Two storeys, automatic
stairwell" and an explicit-stairwell-room recipe.
- Climbability envelope: the Jolt character only mounts a 0.174 m
riser when the approach is fast enough and the tread is not too
short. Measured: 1.4 m/s (NPC walk) and 1.5 m/s climb 0.30 m and
0.248 m treads; 1.2 m/s does not. Straight runs therefore need at
least 3 cells at 2 m cells (tread >= ~0.25) - 4-cell runs
(tread 0.30) are the verified default - and a one-cell 2 m dogleg
(tread ~0.09) is never climbable (the dogleg layout is for larger
cells or hand-authored grids). `FurniturePlacement::canPlace`
rejects stair cells and the cells directly above them ("blocks a
stairwell" / "above a stairwell opening").
- Exterior stairs (phase 7.4): `CellFlags::ExteriorStair` (bit 27) on
an upper-floor boundary cell with an exterior door builds a motel-
style outside stair: a 1.5 m walkway landing at the upper floor
flush with the door, a 0.9 m outer rail (wall buffer + collider),
and a straight solid flight descending along the facade to the grid
base level (0.18 cap, tread 0.30, 1.4 m deep against the wall),
using the stair mesh + collider. The facade side comes from the
cell's door/window/wall bits (exactly one door side required); the
flight direction is the along-facade side with room for the run
(+ preferred) and may extend past the building corner over the
ground. DSL: `(floor n (door side offset :gallery))` (upper floors
only); the DSL help documents it; the editor flag list shows
"Exterior Stair". Limits: no rail along the flight; the flight
bottoms out at the grid base (the terrain foundation must meet it);
the gallery door is a normal door (closed leaf blocks the capsule,
the navmesh treats doors as passable per F7).
- Grand / lobby stairs (phase 7.5): the layout follows a stairwell
block's aspect - compact blocks build the two-flight dogleg,
elongated blocks (aspect >= 1.5, run >= 3 cells) build ONE wide
straight flight spanning the block's short side (width =
shortCells * cellSize - 0.30); single-row runs keep the 1.8 m
comfort cap, so existing layouts are unchanged. Wide flights reuse
the same step contract. Curved/branching flights and railings
inside stairwell rooms are explicitly out of scope (the shaft walls
enclose the flights; an open grand stair ending mid-room would need
a railing system).
Coverage: `tests/cellgrid_door_test.cpp` test 4 (flag + rect round trip,
old-scene defaults), the terrain step
@@ -3130,14 +3183,22 @@ level 1 - hole planes absent inside the footprints / present outside,
stair mesh spanning exactly ground to top, both ring bands present, a
stairs mesh collider, the no-flag twin grid unchanged, plus a
rooms-first DSL stairwell layout previewed through `TownGenSystem` that
must build stair geometry; `road_geometry_overlap_test` DSL probes
(rooms-first + filled-shell flag placement);
must build stair geometry AND a two-storey layout with no stair room
that must get an automatic stairwell (cells + mesh), plus an
exterior-stair fixture (upper door + `ExteriorStair`: mesh outside the
facade, reaches the upper floor, Recast path ground->upper through the
gallery door), and a 3 x 2-cell grand-stair block (one continuous
3.7 m wide flight, the whole block opened above);
`road_geometry_overlap_test` DSL probes
(rooms-first + filled-shell flag placement and `door :gallery`);
`component_lua_test` test 33 (`stairRectName` round trip). Traversal
acceptance is the demo probe `demoTaxi --headless --test-stairs
demo_scene_vehicletest.json` (phase 7.2): a bare `CharacterComponent`
capsule walks up a three-cell straight flight on the flat vehicletest
floor through the normal `CharacterSystem` path (the headless player
spawn has no physics capsule), and a Recast rebuild finds a ground->top
spawn has no physics capsule), a 2 m-cell three-cell run climbs at NPC
walk speed, and an exterior-stair fixture is climbed from the ground
to the gallery; a Recast rebuild finds a ground->top
path only in the stair building. A 0.25 m riser variant was tried and
the capsule never mounted the first tread - keep generated risers at or
below 0.18 m.
@@ -164,6 +164,7 @@ const char* CellGridComponent::getFlagName(uint64_t flag)
case CellFlags::IntWindowZPos: return "Int Window Z+";
case CellFlags::IntWindowZNeg: return "Int Window Z-";
case CellFlags::StairWell: return "Stairwell";
case CellFlags::ExteriorStair: return "Exterior Stair";
default: return "Unknown";
}
}
@@ -209,5 +210,6 @@ std::vector<std::pair<uint64_t, const char*>> CellGridComponent::getAllFlags()
{ CellFlags::IntWindowZPos, "Int Window Z+" },
{ CellFlags::IntWindowZNeg, "Int Window Z-" },
{ CellFlags::StairWell, "Stairwell" },
{ CellFlags::ExteriorStair, "Exterior Stair" },
};
}
@@ -65,6 +65,13 @@ namespace CellFlags {
// (x, z) on consecutive levels.
constexpr uint64_t StairWell = 1ULL << 26;
// Phase 7.4: an exterior stair. On an upper-floor boundary cell
// carrying an exterior door on one side, the generator builds an
// outside walkway landing in front of the door and a straight
// flight descending along that facade to the ground level
// (motel-style external stairs). Does not cut any floors.
constexpr uint64_t ExteriorStair = 1ULL << 27;
// Masks
constexpr uint64_t AllExternalWalls = WallXNeg | WallXPos | WallZPos | WallZNeg;
constexpr uint64_t AllInternalWalls = IntWallXNeg | IntWallXPos | IntWallZPos | IntWallZNeg;
+132 -20
View File
@@ -5795,7 +5795,8 @@ generator — **engine-level, not taxi-specific** — so that both the
player (Jolt character) and NPCs (Recast navmesh) can travel between
every floor. Phase 8's interior generator consumes the result.
**Status:** 7.1, 7.2 and 7.3 DONE (2026-10-30). 7.4-7.5 planned.
**Status:** Phase 7 COMPLETE — 7.1-7.5 DONE, including 7.3a
(automatic stairwells for multi-storey layouts) (2026-10-30).
**How to read this phase.** 7.1 is the vertical slice: one hand-
authored grid, one stairwell, real geometry. 7.2 proves traversal.
@@ -5856,6 +5857,14 @@ verification, and every slice keeps the global invariant:
when a run is too short. The 0.18 m rise cap is EMPIRICAL: 0.25 m
risers block the Jolt character capsule dead at the first tread
(phase 7.2); Recast's 1.0 m climb was never the limiting factor.
- **Climbability envelope (phase 7.3a findings):** the Jolt character
mounts a 0.174 m riser only when the horizontal approach is fast
enough and the tread is not too short. Measured: 1.4 m/s (the NPC
walk speed) and 1.5 m/s climb 0.30 m and 0.248 m treads; 1.2 m/s
does not. Straight runs therefore need >= 3 cells at 2 m cells
(tread >= ~0.25) and the generator prefers 4-cell runs
(tread 0.30). A one-cell 2 m dogleg (tread ~0.09) is never
climbable; the dogleg is for larger cells / hand-authored grids.
- A run of flagged cells along one axis becomes a **straight flight**
rising toward +X / +Z (bottom at the run's low-coordinate end, top
tread flush with the destination rim). A lone cell becomes a
@@ -5958,29 +5967,131 @@ Verified:
built stair geometry.
- `component_lua_test` 67/67, `cellgrid_door_test` 15/15.
#### Phase 7.4 — Exterior stairs (motel-style)
#### Phase 7.3a — automatic stairwells for generated layouts — DONE
- Stairs outside the shell walls: exterior corridors/galleries on
upper floors reached by an external flight (as in many motels).
Reuses the 7.1 step geometry and materials; placement is driven by
authored cells/DSL, not by a town-wide rule.
The town generator now guarantees that every multi-storey building it
places has a stairwell:
Verify:
- Headless: character climbs from terrain level to the upper
gallery and enters through its exterior door; navmesh connects
terrain to the gallery.
- `TownGenSystem::fillBuildingCells` calls
`autoPlaceStairwell()`: an explicit plan stair cell (a
`:type stairwell` room) wins; otherwise a deterministic straight run
is carved along the longer usable axis - 4 cells first (tread
0.30 at the DSL's 2 m cells, the verified climbable shape), 3 cells
as a fallback - on the first wall/door-free row or column that has a
boarding cell on the ground floor and a step-off cell on the top
floor. Interior walls and internal windows on the run cells are
opened (a carved stairwell may cross room boundaries); the facing
walls at the boarding and step-off cells are opened too. When
nothing fits (very small lots) the building logs one
`TownGen: warning - no stairwell fits building ...` line and stays
as before.
- This covers the built-in parametric layouts (`village-house`,
`town-rowhouse`, `city-block`, `city-tower` - any plan drawn with
`rand-int min-floors max-floors` > 1), `town_layouts.txt` layouts
(`two-floor-house` etc.) and inline sources, without editing any
layout text. Exterior-only shells are unchanged (stairs are
interior geometry): generated town fronts stay bit-identical.
- Furniture placement now refuses stair cells and the cells directly
above them (`FurniturePlacement::canPlace`: "blocks a stairwell" /
"above a stairwell opening"), so room furniture cannot block a
stair or float over the opening.
- DSL help window: the `room` verb line documents
`:type stairwell` and the automatic stairwell; two new examples in
`LayoutLibrary::examples()`: "Two storeys, automatic stairwell"
and "Two-storey house with a stairwell room (rooms-first)" (the
explicit recipe: a 3-cell stair room with `connect` doorways on the
boarding and step-off sides).
#### Phase 7.5 — Grand / lobby stairs (research, then implement)
Verified: `cellGridStairs` additionally previews a two-storey layout
with NO stair room and asserts the automatic stairwell cells +
geometry; `road_geometry_overlap_test`, `component_lua_test`,
`cellgrid_door_test`, CTest 9/9, and the demo batteries
(`--test-stairs`: 4 m straight + 2 m 3-cell run climbs at NPC walk
speed; `--test-furniture`, `--test-population-switch`,
`--test-switch`) all PASS.
Open design question carried over from the original phase notes:
typing a room `stairwell` is the easiest DSL/component authoring
path, but lobbies of hotels, theatres and administrative buildings
want a **large stair inside an ordinary room** — wide flights,
possibly curved or branching, not a utilitarian shaft. Research
first: how a wide multi-cell flight is authored (room option vs.
dedicated cells), whether the 7.1 straight/dogleg layouts suffice
when scaled up, and what railing/balustrade geometry is needed.
Only then implement.
#### Phase 7.4 — Exterior stairs (motel-style) — DONE
As built (2026-10-30):
- `CellFlags::ExteriorStair` (bit 27, mirrored in `PlanFlags` with a
static_assert) on an UPPER-floor boundary cell carrying an exterior
door on one side. The generator builds, outside that facade:
- a walkway landing in front of the door (1.5 m deep, flush with the
upper floor surface) with a 0.9 m outer rail band (wall buffer +
collider, so nobody walks off and Recast sees the edge), and
- a straight flight descending along the facade from the landing to
the grid base level: 0.18 rise cap (0.175 at 4 m floors, 24
steps), tread 0.30 shrunk only when the facade is short (warn
below 0.12), solid boxes, depth 1.4 against the wall.
The flight runs in the direction (along the facade) that has room
for the run, +axis preferred, and extends past the building corner
over the ground when needed. Steps go into the stair mesh with its
collider; the rail goes into the internal wall buffer. Warnings
(once per cell) for: ambiguous facade doors, no facade opening at
all, no room for the flight, and the flag on the ground floor.
- Authoring: the DSL door verb accepts `:gallery` on upper floors -
`(floor 1 (door front -4 :gallery))` marks that door cell as an
exterior stair; the DSL help window documents it and the room verb's
stairwell note. The Cell Grid editor's per-cell flag list shows
"Exterior Stair" automatically. Serialization rides the raw cell
flags (no schema change); Lua unaffected (no new component field).
- Limits (documented, not blockers): no rail along the flight itself
(only the landing rail); the flight bottoms out at the grid base
level, so the terrain/foundation must meet the building base (the
town foundations already do); the gallery door is a normal door -
a closed leaf blocks the capsule until opened, while the navmesh
treats doors as passable (F7).
Verified:
- `TERRAIN_TEST_FILTER=cellGridStairs` builds an exterior fixture
(5 x 2 x 3 grid at 2 m cells, upper door + exterior flag): the stair
mesh reaches the upper floor and has geometry outside the facade
(z > 5), and a Recast path runs from the ground to the upper floor
through the gallery door.
- `demoTaxi --test-stairs` gained an exterior-walk phase: a capsule
climbs the outside flight from the ground to the gallery at NPC
walk speed (PASS at Y > 3.9). `road_geometry_overlap_test` asserts
the `:gallery` DSL flag placement. CTest 9/9 and the demo
batteries stay green.
#### Phase 7.5 — Grand / lobby stairs — DONE
Research conclusions (2026-10-30):
- **Authoring unit: the existing `:type stairwell` room**, no new
verb. The room's aspect decides the layout: compact blocks keep the
two-flight dogleg; **elongated blocks (aspect >= 1.5, run of >= 3
cells) now build ONE wide straight flight** spanning the block's
short side - the grand lobby stair. Wide flights are a generator
change on the cell flags, so hand-authored multi-row `StairWell`
blocks and DSL stairwell rooms get them alike.
- **Curved/branching flights: out of scope** (documented). A curved
stair would need a path-based mesh generator and is not justified by
the current building stock; branching is expressible as two
stairwell rooms.
- **Railings**: not generated inside stairwell rooms - the shaft walls
enclose the flights, and a wide flight spans the room up to the
0.15 m ring margin per side. An open grand stair ending mid-room
would need a railing system; deferred (the shipped layouts do not
need it).
- Wide flights reuse the 7.1 step contract: 0.18 rise cap, 0.30 tread
(shrunk on short runs with a warning), solid boxes, stair mesh +
collider, `stairRectName`/floor rect material.
As built: the block branch of `CellGridSystem::buildStairs` computes
the block aspect; aspect >= 1.5 with a run of >= 3 cells emits one
straight flight across the full short side (width = shortCells *
cellSize - 0.30); otherwise the dogleg is kept. Single-row runs keep
the 1.8 m comfort cap, so existing layouts are unchanged.
Verified: the `cellGridStairs` terrain step adds a 3 x 2-cell stairwell
block in a 6 x 2 x 6 grid and asserts the stair mesh's first tread
spans one continuous 3.7 m wide flight (z extent at the tread-top
level) and the level above is opened over the whole block;
`road_geometry_overlap_test` and the demo batteries stay green. The
DSL help window documents the wide-stairwell room and ships a "Grand
lobby stair (wide stairwell room)" example.
**Explicitly out of scope for Phase 7:** interior scene generation
and door wiring (Phase 8), spiral stairs, elevators, and town-wide
@@ -5989,6 +6100,7 @@ stairwells when Phase 8 makes upper floors enterable).
### Phase 8 — Enterable buildings (on-the-fly interiors)
- Interior depends on previous steps, most notably furniture and stairs.
- That is only place where we can start properly verifying verify some of previous things, so be ready to fix older problems here.
- Additionnally interior depends on beinc capable of creating furniture pieces with actionable items.
- using furniture prefabs, where with the help of editor we create scene, add one or more entities with meshes, add physics colliders, other objects. The instructions should be provided for the one who will do it. With constraints and a way to create multiple cell furniture.
- using Blender. The instructions should be provided for the one who will do it. With constraints and a way to create multiple cell furniture, and how to properly fill custom properties.
@@ -4605,16 +4605,30 @@ struct StairsTestListener : public Ogre::FrameListener {
Ogre::Vector3 stairOrigin = Ogre::Vector3(0, 0, 20);
Ogre::Vector3 plainOrigin = Ogre::Vector3(40, 0, 20);
Ogre::Vector3 dogOrigin = Ogre::Vector3(0, 0, 40);
Ogre::Vector3 extOrigin = Ogre::Vector3(0, 0, 60);
Ogre::SceneNode *stairNode = nullptr;
Ogre::SceneNode *plainNode = nullptr;
Ogre::SceneNode *dogNode = nullptr;
Ogre::SceneNode *extNode = nullptr;
Ogre::SceneNode *walkerNode = nullptr;
Ogre::SceneNode *dogWalkerNode = nullptr;
Ogre::SceneNode *extWalkerNode = nullptr;
flecs::entity stairGrid;
flecs::entity plainGrid;
flecs::entity dogGrid;
flecs::entity extGrid;
flecs::entity walker;
flecs::entity dogWalker;
flecs::entity extWalker;
flecs::entity navE;
bool navBuilt = false;
int buildFrame = -1;
int walkFrame = -1;
int dogBuildFrame = -1;
int dogWalkFrame = -1;
int extBuildFrame = -1;
int extWalkFrame = -1;
StairsTestListener(EditorApp *a)
: app(a)
@@ -4631,17 +4645,19 @@ struct StairsTestListener : public Ogre::FrameListener {
app->getRoot()->queueEndRendering();
}
flecs::entity buildGrid(const char *name, const Ogre::Vector3 &origin,
bool withStairs, Ogre::SceneNode *&nodeOut)
flecs::entity buildGridGeneric(const char *name,
const Ogre::Vector3 &origin, float cs,
int w, int h, int d, int stairMode,
Ogre::SceneNode *&nodeOut)
{
flecs::world *w = app->getWorld();
flecs::world *wf = app->getWorld();
Ogre::SceneNode *node = app->getSceneManager()
->getRootSceneNode()
->createChildSceneNode(name);
node->setPosition(origin);
nodeOut = node;
flecs::entity e = w->entity();
flecs::entity e = wf->entity();
TransformComponent t;
t.node = node;
t.position = origin;
@@ -4651,10 +4667,10 @@ struct StairsTestListener : public Ogre::FrameListener {
CellGridComponent grid;
grid.gridUid = name;
grid.width = GRID_W;
grid.height = GRID_H;
grid.depth = GRID_D;
grid.cellSize = CELL_SIZE;
grid.width = w;
grid.height = h;
grid.depth = d;
grid.cellSize = cs;
grid.cellHeight = CELL_HEIGHT;
for (int y = 0; y < grid.height; ++y)
for (int z = 0; z < grid.depth; ++z)
@@ -4663,26 +4679,41 @@ struct StairsTestListener : public Ogre::FrameListener {
c.flags = CellFlags::Floor |
CellFlags::Ceiling;
}
if (withStairs)
if (stairMode == 1) /* straight run x 2..4, z 1 */
for (int x = 2; x <= 4; ++x)
grid.getOrCreateCell(x, 0, 1)
.setFlag(CellFlags::StairWell);
else if (stairMode == 2) /* 2 m cells: 3-cell straight run x1..3 */
for (int x = 1; x <= 3; ++x)
grid.getOrCreateCell(x, 0, 1)
.setFlag(CellFlags::StairWell);
else if (stairMode == 3) /* exterior stair door cell */
grid.getOrCreateCell(0, 1, 2)
.setFlag(CellFlags::DoorZPos |
CellFlags::ExteriorStair);
e.set<CellGridComponent>(grid);
return e;
}
flecs::entity buildGrid(const char *name, const Ogre::Vector3 &origin,
bool withStairs, Ogre::SceneNode *&nodeOut)
{
return buildGridGeneric(name, origin, CELL_SIZE, GRID_W, GRID_H,
GRID_D, withStairs ? 1 : 0, nodeOut);
}
/* Bare capsule character walked by CharacterSystem (the same
* machinery the NPC tiers use; the headless player spawn has no
* physics capsule). */
flecs::entity buildWalker(const Ogre::Vector3 &pos)
flecs::entity buildWalker(const char *name, const Ogre::Vector3 &pos,
Ogre::SceneNode *&outNode)
{
flecs::world *w = app->getWorld();
Ogre::SceneNode *node = app->getSceneManager()
->getRootSceneNode()
->createChildSceneNode(
"StairsTestWalker");
->createChildSceneNode(name);
node->setPosition(pos);
walkerNode = node;
outNode = node;
flecs::entity e = w->entity();
TransformComponent t;
t.node = node;
@@ -4699,11 +4730,11 @@ struct StairsTestListener : public Ogre::FrameListener {
bool frameRenderingQueued(const Ogre::FrameEvent &) override
{
frame++;
if (failed || phase == 3) {
if (failed || phase == 7) {
app->getRoot()->queueEndRendering();
return true;
}
if (frame > 4000) {
if (frame > 8000) {
fail("timeout in phase " + std::to_string(phase));
return true;
}
@@ -4722,12 +4753,13 @@ struct StairsTestListener : public Ogre::FrameListener {
/* Let the cell-grid physics colliders materialize. */
if (frame - buildFrame < 40)
return true;
/* Spawn the walker on the ground floor inside the
* run's z-row and walk it toward +X. */
/* Spawn the walker just before the flight (short
* run-up; the narrow fixtures start close too). */
const Ogre::Vector3 start =
stairOrigin + Ogre::Vector3(0.0f, 0.35f,
stairOrigin + Ogre::Vector3(9.8f, 0.35f,
CELL_SIZE);
walker = buildWalker(start);
walker = buildWalker("StairsTestWalker", start,
walkerNode);
walkFrame = frame;
phase = 1;
std::cout << "[test] stairs: walking up from "
@@ -4763,6 +4795,101 @@ struct StairsTestListener : public Ogre::FrameListener {
return true;
}
case 2: {
/* 2 m-cell 3-cell straight run. */
if (dogBuildFrame < 0) {
dogGrid = buildGridGeneric(
"StairsTestNarrow", dogOrigin, 2.0f,
5, 2, 4, 2, dogNode);
dogBuildFrame = frame;
return true;
}
if (frame - dogBuildFrame < 40)
return true;
const Ogre::Vector3 start =
dogOrigin + Ogre::Vector3(0.0f, 0.35f, 2.0f);
dogWalker = buildWalker("StairsTestNarrowWalker",
start, dogWalkerNode);
dogWalkFrame = frame;
phase = 3;
std::cout << "[test] stairs: narrow-cell walk from "
<< start << std::endl;
return true;
}
case 3: {
/* Walk +X up the 3-cell flight. */
if (!dogWalker.is_alive() || !dogWalkerNode)
return true;
CharacterComponent &cc =
dogWalker.get_mut<CharacterComponent>();
const Ogre::Vector3 p = dogWalkerNode->getPosition();
const float lx = p.x - dogOrigin.x;
cc.linearVelocity = Ogre::Vector3(1.4f, 0.0f, 0.0f);
if (p.y > 3.9f) {
cc.linearVelocity = Ogre::Vector3::ZERO;
std::cout << "[test] stairs: narrow-cell run "
"reached upper floor at "
<< p << " ("
<< (frame - dogWalkFrame)
<< " frames, y "
<< p.y << ")" << std::endl;
phase = 4;
return true;
}
if (lx > 10.0f) {
cc.linearVelocity = Ogre::Vector3::ZERO;
fail("narrow-cell walker left the stairwell "
"without climbing (Y " +
Ogre::StringConverter::toString(p.y) +
", lx " +
Ogre::StringConverter::toString(lx) +
")");
}
return true;
}
case 4: {
/* Phase 7.4: exterior stair - walk up the outside
* flight from the ground to the gallery landing. */
if (extBuildFrame < 0) {
extGrid = buildGridGeneric(
"StairsTestExterior", extOrigin,
2.0f, 5, 2, 3, 3, extNode);
extBuildFrame = frame;
return true;
}
if (frame - extBuildFrame < 40)
return true;
const Ogre::Vector3 start =
extOrigin + Ogre::Vector3(8.2f, 0.35f, 5.75f);
extWalker = buildWalker("StairsTestExteriorWalker",
start, extWalkerNode);
extWalkFrame = frame;
phase = 5;
std::cout << "[test] stairs: exterior walk from "
<< start << std::endl;
return true;
}
case 5: {
/* Walk -X up the exterior flight. */
if (!extWalker.is_alive() || !extWalkerNode)
return true;
CharacterComponent &cc =
extWalker.get_mut<CharacterComponent>();
cc.linearVelocity = Ogre::Vector3(-1.5f, 0.0f, 0.0f);
const Ogre::Vector3 p = extWalkerNode->getPosition();
if (p.y > 3.9f) {
cc.linearVelocity = Ogre::Vector3::ZERO;
std::cout << "[test] stairs: exterior stair "
"reached the gallery at "
<< p << " ("
<< (frame - extWalkFrame)
<< " frames, y "
<< p.y << ")" << std::endl;
phase = 6;
return true;
}
return true;
}
case 6: {
/* Navmesh: ground->top path in the stair building,
* none in the stairless twin. */
if (buildFrame >= 0 && plainGrid.id() == 0) {
@@ -4841,7 +4968,7 @@ struct StairsTestListener : public Ogre::FrameListener {
std::cout << "[test] PASS: stairs are walkable and "
"navmesh-connected; stairless floors are "
"not" << std::endl;
phase = 3;
phase = 7;
app->getRoot()->queueEndRendering();
return true;
}
@@ -11998,7 +12125,7 @@ int main(int argc, char *argv[])
app.closeApp();
return 1;
}
if (stairsListener.phase != 3) {
if (stairsListener.phase != 7) {
std::cerr << "[test] FAIL: incomplete (stairs "
"phase "
<< stairsListener.phase << ")"
@@ -58,6 +58,8 @@ constexpr uint64_t IntWindowZPos = 1ULL << 24;
constexpr uint64_t IntWindowZNeg = 1ULL << 25;
/* Phase 7.1: stair cell (see CellFlags::StairWell). */
constexpr uint64_t StairWell = 1ULL << 26;
/* Phase 7.4: exterior stair cell (see CellFlags::ExteriorStair). */
constexpr uint64_t ExteriorStair = 1ULL << 27;
}
/** Roof entry; type mirrors RoofComponent::Type (Flat=0 .. Cylinder=4). */
@@ -373,6 +373,49 @@ const std::vector<LayoutLibrary::LayoutExample> &LayoutLibrary::examples()
" (windows all :spacing 4))\n"
"(roof flat 6 0 12 8 :level 1)\n"
"(roof gable 0 0 6 8 :height 2)\n" },
{ "Two storeys, automatic stairwell (phase 7.3)",
"(shell 12 8 :floors 2)\n"
"(floor 0 (door front center)\n"
" (windows all))\n"
"; Any layout with :floors 2 or more and no stairwell\n"
"; room gets a straight-run stairwell carved by the\n"
"; generator (boarding and step-off cells included).\n" },
{ "Two-storey house with a stairwell room (rooms-first)",
"(shell 12 8 :floors 2 :empty)\n"
"(floor 0 (room hall (rect 0 0 0.166 1))\n"
" (room stair (rect 0.166 0.25 0.666 0.5)\n"
" :type stairwell)\n"
" (room back (rect 0.166 0 1 0.25))\n"
" (room front (rect 0.166 0.5 1 1))\n"
" (room east (rect 0.666 0.25 1 0.5))\n"
" (connect hall stair)\n"
" (connect stair east)\n"
" (connect hall back)\n"
" (connect hall front)\n"
" (door front center)\n"
" (windows all))\n"
"(floor 1 (room upstairs (rect 0 0 1 1))\n"
" (windows all))\n"
"(roof gable :height 2)\n" },
{ "Grand lobby stair (wide stairwell room, phase 7.5)",
"(shell 12 12 :floors 2 :empty)\n"
"(floor 0 (room lobby (rect 0 0 0.333 1))\n"
" (room stair (rect 0.333 0.166 0.833 0.5)\n"
" :type stairwell)\n"
" (room east (rect 0.833 0.166 1 0.5))\n"
" (room back (rect 0.333 0 1 0.166))\n"
" (room south (rect 0.333 0.5 1 1))\n"
" (connect lobby stair)\n"
" (connect stair east)\n"
" (connect lobby back)\n"
" (connect lobby south)\n"
" (door front -4)\n"
" (windows all))\n"
"(floor 1 (room hall (rect 0 0 1 1))\n"
" (windows all))\n"
"(roof gable :height 2)\n"
"; An elongated stairwell room (here 3 x 2 cells) builds\n"
"; ONE wide straight flight instead of a dogleg.\n" },
{ "Windowless bathroom (per-room :windows none)",
"(shell 12 8 :empty)\n"
"(floor 0 (room living (rect 0 0 0.5 1))\n"
@@ -856,7 +899,7 @@ bool LayoutEval::evalList(const SExpr &e, Value &out)
else if (op == "cut")
ok = verbCut(pos, e);
else if (op == "door")
ok = verbDoor(pos, e);
ok = verbDoor(pos, kw, e);
else if (op == "windows")
ok = verbWindows(pos, kw, e);
else if (op == "idoor")
@@ -1177,9 +1220,11 @@ bool LayoutEval::verbCut(const std::vector<Value> &pos, const SExpr &at)
return true;
}
bool LayoutEval::verbDoor(const std::vector<Value> &pos, const SExpr &at)
bool LayoutEval::verbDoor(const std::vector<Value> &pos,
const std::map<std::string, Value> &kw,
const SExpr &at)
{
/* (door front|back|left|right center|<offset-m>) */
/* (door front|back|left|right center|<offset-m> [:gallery]) */
if (pos.size() < 1)
return fail(at, "door needs (door side [center|offset])");
std::string side;
@@ -1274,8 +1319,18 @@ bool LayoutEval::verbDoor(const std::vector<Value> &pos, const SExpr &at)
z = horizontal ? idx : (si == 0 ? m_plan->cellsZ - 1 : 0);
}
return placeOpening(x, z, wallBit[si] | winBit[si],
doorBit[si] | intDoorBit[si], at);
if (!placeOpening(x, z, wallBit[si] | winBit[si],
doorBit[si] | intDoorBit[si], at))
return false;
/* Phase 7.4: :gallery marks the door cell as an exterior stair
* (the generator adds an outside walkway landing and a flight
* descending along this facade). */
if (kw.find(":gallery") != kw.end()) {
if (m_floor < 1)
return fail(at, "door :gallery needs an upper floor");
m_plan->at(x, m_floor, z) |= PlanFlags::ExteriorStair;
}
return true;
}
void LayoutEval::windowsOnSide(int side, float spacing)
@@ -267,7 +267,9 @@ private:
bool verbRoomRaw(const SExpr &form);
bool verbCellSize(const std::vector<Value> &pos, const SExpr &at);
bool verbCut(const std::vector<Value> &pos, const SExpr &at);
bool verbDoor(const std::vector<Value> &pos, const SExpr &at);
bool verbDoor(const std::vector<Value> &pos,
const std::map<std::string, Value> &kw,
const SExpr &at);
bool verbWindows(const std::vector<Value> &pos,
const std::map<std::string, Value> &kw,
const SExpr &at);
+311 -109
View File
@@ -876,8 +876,18 @@ void CellGridSystem::buildStairs(const CellGridComponent &grid,
" stair cell(s) have no floor above (grid height " +
std::to_string(grid.height) + "); ignored");
}
if (stairCells.empty())
return;
if (stairCells.empty()) {
/* Interior stairwells may be absent while exterior stairs
* (phase 7.4) are requested; keep going for those. */
bool hasExterior = false;
for (const auto &cell : grid.cells)
if (cell.flags & CellFlags::ExteriorStair) {
hasExterior = true;
break;
}
if (!hasExterior)
return;
}
/* ---- Opening rims: one slab-edge ring per hole level. ---- */
for (int level = 1; level < grid.height; ++level) {
@@ -978,6 +988,138 @@ void CellGridSystem::buildStairs(const CellGridComponent &grid,
}
};
/* Dogleg over a rectangular block (one cell for a lone stair
* cell, 2x2+ cells where the footprint is wider than deep - the
* shape generated 2 m-cell layouts use, since a one-cell 2 m
* dogleg would be too steep to climb). Flight A rises along +Z
* on the -X band, the mid landing spans the +Z end, flight B
* rises along -Z on the +X band and tops out flush with the -Z
* rim, so columns stack boarding and step-off on the same side. */
auto emitDogleg = [&](int dx0, int dx1, int dz0, int dz1, int y) {
const float baseY = (float)y * ch;
const float botY = baseY + floorY;
const float minX = (float)dx0 * cs - half + STAIR_RING;
const float maxX = (float)dx1 * cs + half - STAIR_RING;
const float minZ = (float)dz0 * cs - half + STAIR_RING;
const float maxZ = (float)dz1 * cs + half - STAIR_RING;
const float spanX = maxX - minX;
const float spanZ = maxZ - minZ;
const float fw = (spanX - STAIR_FLIGHT_GAP) * 0.5f;
if (fw < 0.5f || spanZ < 1.2f) {
Ogre::LogManager::getSingleton().logMessage(
"CellGrid: warning - cell too small for a "
"dogleg stair at (" +
std::to_string(dx0) + "," + std::to_string(y) +
"," + std::to_string(dz0) + ")");
return;
}
int n;
float rise, tread;
planSteps(spanZ, n, rise, tread);
const int nA = (n + 1) / 2;
const int nB = n - nA;
/* Spend the span on flights first, keeping a landing of
* 0.6..1.2 m at the +Z end. */
const float landing =
std::min(1.2f, std::max(0.6f, spanZ * 0.3f));
float treadA = (spanZ - landing) / (float)nA;
if (treadA < 0.12f) {
Ogre::LogManager::getSingleton().logMessage(
"CellGrid: warning - dogleg tread too short at "
"(" + std::to_string(dx0) + "," +
std::to_string(y) + "," +
std::to_string(dz0) + ")");
treadA = 0.12f;
}
const float landingMinZ = minZ + (float)nA * treadA;
const float treadB =
nB > 0 ? (landingMinZ - minZ) / (float)nB : treadA;
const float midTop = botY + (float)nA * rise;
/* Flight A (-X band, rising +Z). */
for (int k = 0; k < nA; ++k) {
const float top = botY + (float)(k + 1) * rise;
const float sz = minZ + (float)k * treadA;
genStairBox(stairTb,
{ minX + fw * 0.5f,
(baseY + top) * 0.5f,
sz + treadA * 0.5f },
{ fw, top - baseY, treadA });
}
/* Mid landing across the full usable width. */
genStairBox(stairTb,
{ (minX + maxX) * 0.5f,
(baseY + midTop) * 0.5f,
(landingMinZ + maxZ) * 0.5f },
{ spanX, midTop - baseY, maxZ - landingMinZ });
/* Flight B (+X band, rising -Z, on the landing). */
for (int k = 0; k < nB; ++k) {
const float top = midTop + (float)(k + 1) * rise;
const float szHi = landingMinZ - (float)k * treadB;
genStairBox(stairTb,
{ maxX - fw * 0.5f,
(midTop + top) * 0.5f,
szHi - treadB * 0.5f },
{ fw, top - midTop, treadB });
}
};
/* Straight flight along +X over [bx0..bx1] x [bz0..bz1]: a
* single-row block keeps the comfortable 1.8 m width cap, a
* multi-row block (phase 7.5 grand stair) spans the whole block
* width as ONE wide flight. */
auto emitStraightX = [&](int bx0, int bx1, int bz0, int bz1, int y) {
const float baseY = (float)y * ch;
const float botY = baseY + floorY;
const float runLen = (float)(bx1 - bx0 + 1) * cs;
int n;
float rise, tread;
planSteps(runLen, n, rise, tread);
const float width =
(bz0 == bz1) ?
std::min(STAIR_MAX_WIDTH,
cs - 2.0f * STAIR_RING) :
(float)(bz1 - bz0 + 1) * cs -
2.0f * STAIR_RING;
const float xHigh = (float)bx1 * cs + half - STAIR_RING;
const float xStart = xHigh - (float)n * tread;
const float zc = ((float)bz0 + (float)bz1) * 0.5f * cs;
for (int i = 0; i < n; ++i) {
const float top = botY + (float)(i + 1) * rise;
const float sx = xStart + (float)i * tread;
genStairBox(stairTb,
{ sx + tread * 0.5f,
(baseY + top) * 0.5f, zc },
{ tread, top - baseY, width });
}
};
/* Straight flight along +Z over [bx0..bx1] x [bz0..bz1]. */
auto emitStraightZ = [&](int bx0, int bx1, int bz0, int bz1, int y) {
const float baseY = (float)y * ch;
const float botY = baseY + floorY;
const float runLen = (float)(bz1 - bz0 + 1) * cs;
int n;
float rise, tread;
planSteps(runLen, n, rise, tread);
const float width =
(bx0 == bx1) ?
std::min(STAIR_MAX_WIDTH,
cs - 2.0f * STAIR_RING) :
(float)(bx1 - bx0 + 1) * cs -
2.0f * STAIR_RING;
const float zHigh = (float)bz1 * cs + half - STAIR_RING;
const float zStart = zHigh - (float)n * tread;
const float xc = ((float)bx0 + (float)bx1) * 0.5f * cs;
for (int i = 0; i < n; ++i) {
const float top = botY + (float)(i + 1) * rise;
const float sz = zStart + (float)i * tread;
genStairBox(stairTb,
{ xc, (baseY + top) * 0.5f,
sz + tread * 0.5f },
{ width, top - baseY, tread });
}
};
std::set<int64_t> consumed;
for (const auto &cell : grid.cells) {
if (!(cell.flags & CellFlags::StairWell))
@@ -987,9 +1129,6 @@ void CellGridSystem::buildStairs(const CellGridComponent &grid,
if (consumed.count(cell.getKey()))
continue;
const float baseY = (float)cell.y * ch;
const float botY = baseY + floorY;
/* Straight run along X? */
int x0 = cell.x;
while (stairCells.count(cellKey(x0 - 1, cell.y, cell.z)))
@@ -1004,124 +1143,187 @@ void CellGridSystem::buildStairs(const CellGridComponent &grid,
while (stairCells.count(cellKey(cell.x, cell.y, z1 + 1)))
++z1;
if (x1 > x0) {
/* Straight flight along +X, bottom at x0. */
const float runLen = (float)(x1 - x0 + 1) * cs;
int n;
float rise, tread;
planSteps(runLen, n, rise, tread);
const float width = std::min(
STAIR_MAX_WIDTH, cs - 2.0f * STAIR_RING);
const float xHigh =
(float)x1 * cs + half - STAIR_RING;
const float xStart = xHigh - (float)n * tread;
for (int i = 0; i < n; ++i) {
const float top = botY + (float)(i + 1) * rise;
const float sx = xStart + (float)i * tread;
genStairBox(
stairTb,
{ sx + tread * 0.5f,
(baseY + top) * 0.5f,
(float)cell.z * cs },
{ tread, top - baseY, width });
if (x1 > x0 && z1 > z0) {
const int spanX = x1 - x0 + 1;
const int spanZ = z1 - z0 + 1;
const bool xLonger = spanX >= spanZ;
const int longSpan = xLonger ? spanX : spanZ;
const int shortSpan = xLonger ? spanZ : spanX;
/* Phase 7.5: an elongated block (aspect >= 1.5 and
* a run of at least 3 cells) becomes ONE wide
* straight flight - the grand lobby stair. Compact
* blocks keep the two-flight dogleg. */
if (longSpan >= 3 && longSpan * 2 >= shortSpan * 3) {
if (xLonger)
emitStraightX(x0, x1, z0, z1,
cell.y);
else
emitStraightZ(x0, x1, z0, z1,
cell.y);
} else {
emitDogleg(x0, x1, z0, z1, cell.y);
}
for (int z = z0; z <= z1; ++z)
for (int x = x0; x <= x1; ++x)
if (stairCells.count(
cellKey(x, cell.y, z)))
consumed.insert(
cellKey(x, cell.y,
z));
} else if (x1 > x0) {
emitStraightX(x0, x1, cell.z, cell.z, cell.y);
for (int x = x0; x <= x1; ++x)
consumed.insert(cellKey(x, cell.y, cell.z));
} else if (z1 > z0) {
/* Straight flight along +Z, bottom at z0. */
const float runLen = (float)(z1 - z0 + 1) * cs;
int n;
float rise, tread;
planSteps(runLen, n, rise, tread);
const float width = std::min(
STAIR_MAX_WIDTH, cs - 2.0f * STAIR_RING);
const float zHigh =
(float)z1 * cs + half - STAIR_RING;
const float zStart = zHigh - (float)n * tread;
for (int i = 0; i < n; ++i) {
const float top = botY + (float)(i + 1) * rise;
const float sz = zStart + (float)i * tread;
genStairBox(
stairTb,
{ (float)cell.x * cs,
(baseY + top) * 0.5f,
sz + tread * 0.5f },
{ width, top - baseY, tread });
}
emitStraightZ(cell.x, cell.x, z0, z1, cell.y);
for (int z = z0; z <= z1; ++z)
consumed.insert(cellKey(cell.x, cell.y, z));
} else {
/* Dogleg in a single cell: flight A rises along +Z
* on the -X band, the mid landing spans the +Z end,
* flight B rises along -Z on the +X band and tops
* out flush with the -Z rim. */
const float minX = (float)cell.x * cs - half +
STAIR_RING;
const float maxX = (float)cell.x * cs + half -
STAIR_RING;
const float minZ = (float)cell.z * cs - half +
STAIR_RING;
const float maxZ = (float)cell.z * cs + half -
STAIR_RING;
const float spanX = maxX - minX;
const float spanZ = maxZ - minZ;
const float fw =
(spanX - STAIR_FLIGHT_GAP) * 0.5f;
if (fw < 0.5f || spanZ < 1.2f) {
/* Lone stair cell: one-cell dogleg. */
emitDogleg(cell.x, cell.x, cell.z, cell.z, cell.y);
consumed.insert(cell.getKey());
}
}
/* ---- Phase 7.4: exterior stairs (motel-style). ---- */
const float extDepth = 1.5f;
const float extRailH = 0.9f;
const float extRailT = 0.08f;
for (const auto &cell : grid.cells) {
if (!(cell.flags & CellFlags::ExteriorStair))
continue;
if (cell.y < 1)
continue; /* no upper floor to reach */
/* Facade choice: the side with an exterior door; a single
* door side is required when several sides carry one. */
static const uint64_t sideDoor[4] = {
CellFlags::DoorZPos, CellFlags::DoorZNeg,
CellFlags::DoorXNeg, CellFlags::DoorXPos
};
static const uint64_t sideWin[4] = {
CellFlags::WindowZPos, CellFlags::WindowZNeg,
CellFlags::WindowXNeg, CellFlags::WindowXPos
};
static const uint64_t sideWall[4] = {
CellFlags::WallZPos, CellFlags::WallZNeg,
CellFlags::WallXNeg, CellFlags::WallXPos
};
int doorSide = -1;
int doorCount = 0;
int anySide = -1;
for (int s = 0; s < 4; ++s) {
if (cell.flags & sideDoor[s]) {
++doorCount;
doorSide = s;
}
if (anySide < 0 &&
(cell.flags & (sideDoor[s] | sideWin[s] |
sideWall[s])))
anySide = s;
}
const int side = doorSide >= 0 ? doorSide : anySide;
if (doorCount > 1 || side < 0) {
Ogre::LogManager::getSingleton().logMessage(
"CellGrid: warning - exterior stair at (" +
std::to_string(cell.x) + "," +
std::to_string(cell.y) + "," +
std::to_string(cell.z) +
") needs exactly one facade door");
continue;
}
const bool alongX = side < 2; /* Z faces run along X */
const float uCenter =
(float)(alongX ? cell.x : cell.z) * cs;
const float uMin = uCenter - half;
const float uMax = uCenter + half;
const float landY = (float)cell.y * ch + floorY;
const float groundY = 0.0f;
const int alongCount = alongX ? grid.width : grid.depth;
const int alongCell = alongX ? cell.x : cell.z;
int n = std::max(
2, (int)std::ceil((landY - groundY) /
STAIR_RISE_MAX));
const float stepRise = (landY - groundY) / (float)n;
float tread = STAIR_TREAD;
float run = (float)(n - 1) * tread;
const int availPos = alongCount - 1 - alongCell;
const int availNeg = alongCell;
int dir = 0;
if ((float)availPos * cs >= run + 0.2f)
dir = 1;
else if ((float)availNeg * cs >= run + 0.2f)
dir = -1;
if (dir == 0) {
const int avail =
std::max(availPos, availNeg);
dir = availPos >= availNeg ? 1 : -1;
const float fit = (float)avail * cs;
if (fit < 2.0f) {
Ogre::LogManager::getSingleton().logMessage(
"CellGrid: warning - cell too small "
"for a dogleg stair at (" +
"CellGrid: warning - exterior stair "
"at (" +
std::to_string(cell.x) + "," +
std::to_string(cell.y) + "," +
std::to_string(cell.z) +
") has no facade room for a flight");
continue;
}
tread = std::min(tread,
fit / (float)(n - 1));
run = (float)(n - 1) * tread;
if (tread < 0.12f) {
Ogre::LogManager::getSingleton().logMessage(
"CellGrid: warning - exterior stair "
"tread too short at (" +
std::to_string(cell.x) + "," +
std::to_string(cell.y) + "," +
std::to_string(cell.z) + ")");
consumed.insert(cell.getKey());
continue;
}
int n;
float rise, tread;
planSteps(spanZ, n, rise, tread);
const int nA = (n + 1) / 2;
const int nB = n - nA;
/* Keep at least 0.6 m of landing at the +Z end. */
float treadA = tread;
if ((float)nA * treadA > spanZ - 0.6f)
treadA = (spanZ - 0.6f) / (float)nA;
const float landingMinZ = minZ + (float)nA * treadA;
const float treadB =
nB > 0 ? (landingMinZ - minZ) / (float)nB
: treadA;
const float midTop = botY + (float)nA * rise;
/* Flight A (-X band, rising +Z). */
for (int k = 0; k < nA; ++k) {
const float top = botY + (float)(k + 1) * rise;
const float sz = minZ + (float)k * treadA;
genStairBox(
stairTb,
{ minX + fw * 0.5f,
(baseY + top) * 0.5f,
sz + treadA * 0.5f },
{ fw, top - baseY, treadA });
}
/* Facade plane and outward direction. */
const float face = (side == 0) ? (float)cell.z * cs + half :
(side == 1) ? (float)cell.z * cs - half :
(side == 2) ? (float)cell.x * cs - half :
(float)cell.x * cs + half;
const float outSign = (side == 0 || side == 3) ? 1.0f : -1.0f;
/* Box placement in facade space: u along the face, v
* outward from the wall plane. */
auto placeFacadeBox = [&](float uMid, float uSize,
float vMid, float vSize,
float yMin, float yMax,
Procedural::TriangleBuffer &tb) {
const float yc = (yMin + yMax) * 0.5f;
Ogre::Vector3 center, size;
if (alongX) {
center = Ogre::Vector3(
uMid, yc, face + outSign * vMid);
size = Ogre::Vector3(uSize, yMax - yMin,
vSize);
} else {
center = Ogre::Vector3(
face + outSign * vMid, yc, uMid);
size = Ogre::Vector3(vSize, yMax - yMin,
uSize);
}
/* Mid landing across the full usable width. */
genStairBox(stairTb,
{ (minX + maxX) * 0.5f,
(baseY + midTop) * 0.5f,
(landingMinZ + maxZ) * 0.5f },
{ spanX, midTop - baseY,
maxZ - landingMinZ });
/* Flight B (+X band, rising -Z, on the landing). */
for (int k = 0; k < nB; ++k) {
const float top = midTop + (float)(k + 1) * rise;
const float szHi =
landingMinZ - (float)k * treadB;
genStairBox(
stairTb,
{ maxX - fw * 0.5f,
(midTop + top) * 0.5f,
szHi - treadB * 0.5f },
{ fw, top - midTop, treadB });
}
consumed.insert(cell.getKey());
genStairBox(tb, center, size);
};
/* Walkway landing in front of the door. */
placeFacadeBox(uCenter, cs, extDepth * 0.5f, extDepth,
landY - 0.3f, landY, stairTb);
/* Outer rail on the landing. */
placeFacadeBox(uCenter, cs, extDepth - extRailT * 0.5f,
extRailT, landY, landY + extRailH, wallTb);
/* Straight flight descending along the facade. */
const float uEdge = dir > 0 ? uMax : uMin;
for (int j = 0; j < n - 1; ++j) {
const float top =
groundY + (float)(n - 1 - j) * stepRise;
const float uStart = dir > 0 ?
uEdge + (float)j * tread :
uEdge - (float)(j + 1) * tread;
placeFacadeBox(uStart + tread * 0.5f, tread, 0.7f,
1.4f, groundY, top, stairTb);
}
}
}
@@ -112,6 +112,19 @@ bool FurniturePlacement::canPlace(const CellGridComponent &grid,
*error = "requires a floor";
return false;
}
/* Phase 7.3a: stairs and their upper-floor openings are not
* furniture space. */
if (cell->flags & CellFlags::StairWell) {
if (error)
*error = "blocks a stairwell";
return false;
}
const Cell *below = grid.findCell(cx, request.y - 1, cz);
if (below && (below->flags & CellFlags::StairWell)) {
if (error)
*error = "above a stairwell opening";
return false;
}
}
if (def->requiresWall ||
@@ -7525,6 +7525,42 @@ bool TerrainTestRunner::testCellGridStairs(EditorApp &app, TerrainSystem *ts)
}
}
tgs.clearLayoutPreview();
/* Phase 7.3a: a multi-storey layout WITHOUT a stair room
* gets an automatic straight-run stairwell. */
if (!tgs.previewLayout(
"(shell 12 8 :floors 2)"
"(floor 0 (door front center))",
nullptr, 0, 42, Ogre::Vector3(0, 0, 0), "full",
&dslError)) {
fail("auto-stairwell preview failed: " + dslError);
} else {
flecs::entity autoBuilding;
w->each([&](flecs::entity e, CellGridComponent &) {
if (!autoBuilding.is_alive())
autoBuilding = e;
});
int stairCells = 0;
if (autoBuilding.is_alive())
for (const Cell &c :
autoBuilding.get<CellGridComponent>()
.cells)
if (c.flags & CellFlags::StairWell)
++stairCells;
if (stairCells < 4)
fail("multi-storey layout got no automatic "
"stairwell");
cellSys->update();
if (autoBuilding.is_alive() &&
autoBuilding.has<TransformComponent>() &&
stairTestAttachedMesh(
autoBuilding.get<TransformComponent>().node,
"_stairs")
.isNull())
fail("automatic stairwell built no stair "
"geometry");
}
tgs.clearLayoutPreview();
}
/* One 8 x 3 x 4 grid (32 x 4 m footprint, 4 m floors), every cell
@@ -7701,15 +7737,164 @@ bool TerrainTestRunner::testCellGridStairs(EditorApp &app, TerrainSystem *ts)
if (!stairTestAttachedMesh(plainNode, "_intWall").isNull())
fail("grid without stair flags built opening rings");
/* Phase 7.4: exterior stair - an upper-floor door cell with
* CellFlags::ExteriorStair gets a walkway landing outside the
* facade and a straight flight down to the ground; a navmesh path
* connects the ground to the upper floor through the door. */
flecs::entity extE;
Ogre::SceneNode *extNode = nullptr;
{
extE = w->entity();
extNode = sm->getRootSceneNode()->createChildSceneNode(
"StairsTestExterior");
extE.set<TransformComponent>(
{ extNode, Ogre::Vector3::ZERO,
Ogre::Quaternion::IDENTITY,
Ogre::Vector3::UNIT_SCALE });
CellGridComponent grid;
grid.gridUid = "stairs-test-exterior";
grid.width = 5;
grid.height = 2;
grid.depth = 3;
grid.cellSize = 2.0f;
grid.cellHeight = 4.0f;
for (int y = 0; y < grid.height; ++y)
for (int z = 0; z < grid.depth; ++z)
for (int x = 0; x < grid.width; ++x) {
Cell &c = grid.getOrCreateCell(x, y, z);
c.flags = CellFlags::Floor |
CellFlags::Ceiling;
}
/* Upper-floor door on the +Z facade, at the -X end so the
* flight has the rest of the facade to descend along. */
grid.getOrCreateCell(0, 1, 2)
.setFlag(CellFlags::DoorZPos);
grid.getOrCreateCell(0, 1, 2)
.setFlag(CellFlags::ExteriorStair);
extE.set<CellGridComponent>(grid);
}
cellSys->update();
Ogre::MeshPtr extStairs =
stairTestAttachedMesh(extNode, "_stairs");
float extMaxY = -1.0e9f;
int extOutside = 0;
stairTestScanMesh(extStairs, [&](const Ogre::Vector3 &p) {
extMaxY = std::max(extMaxY, p.y);
if (p.z > 5.05f)
++extOutside;
});
if (extMaxY < 4.1f)
fail("exterior stair does not reach the upper floor");
if (extOutside == 0)
fail("exterior stair has no geometry outside the facade");
NavMeshSystem *navSys = app.getNavMeshSystem();
if (!navSys) {
fail("no NavMeshSystem");
} else {
flecs::entity navE = w->entity();
NavMeshComponent nm;
nm.cellSize = 0.3f;
nm.cellHeight = 0.2f;
nm.agentHeight = 2.0f;
nm.agentRadius = 0.4f;
nm.agentMaxClimb = 1.0f;
nm.agentMaxSlope = 20.0f;
nm.tileSize = 48;
navE.set<NavMeshComponent>(nm);
navSys->rebuild(navE);
std::vector<Ogre::Vector3> path;
const Ogre::Vector3 ground(8.2f, 0.5f, 5.75f);
const Ogre::Vector3 upper(0.0f, 4.7f, 3.5f);
bool found =
navSys->findPath(navE, ground, upper, path);
if (!found || path.empty() ||
(path.back() - upper).length() > 1.5f)
fail("no navmesh path from the ground to the "
"upper floor through the gallery door");
float pathMaxY = -1.0e9f;
for (const auto &p : path)
pathMaxY = std::max(pathMaxY, p.y);
if (pathMaxY < 3.0f)
fail("navmesh path reaches the upper target "
"without climbing the exterior stair");
navE.destruct();
}
/* Phase 7.5: grand/lobby stair - an elongated stairwell block
* (3 x 2 cells, aspect 1.5) becomes ONE wide straight flight
* instead of a dogleg, and the level above is opened over the
* whole block. */
flecs::entity grandE;
Ogre::SceneNode *grandNode = nullptr;
{
grandE = w->entity();
grandNode = sm->getRootSceneNode()->createChildSceneNode(
"StairsTestGrand");
grandE.set<TransformComponent>(
{ grandNode, Ogre::Vector3::ZERO,
Ogre::Quaternion::IDENTITY,
Ogre::Vector3::UNIT_SCALE });
CellGridComponent grid;
grid.gridUid = "stairs-test-grand";
grid.width = 6;
grid.height = 2;
grid.depth = 6;
grid.cellSize = 2.0f;
grid.cellHeight = 4.0f;
for (int y = 0; y < grid.height; ++y)
for (int z = 0; z < grid.depth; ++z)
for (int x = 0; x < grid.width; ++x) {
Cell &c = grid.getOrCreateCell(x, y, z);
c.flags = CellFlags::Floor |
CellFlags::Ceiling;
}
for (int x = 1; x <= 3; ++x)
for (int z = 1; z <= 2; ++z)
grid.getOrCreateCell(x, 0, z)
.setFlag(CellFlags::StairWell);
grandE.set<CellGridComponent>(grid);
}
cellSys->update();
{
Ogre::MeshPtr grand =
stairTestAttachedMesh(grandNode, "_stairs");
const float firstTop = 0.2f + 4.0f / 23.0f;
float zMin = 1.0e9f, zMax = -1.0e9f;
stairTestScanMesh(grand, [&](const Ogre::Vector3 &p) {
if (std::fabs(p.y - firstTop) < 0.01f) {
zMin = std::min(zMin, p.z);
zMax = std::max(zMax, p.z);
}
});
if (grand.isNull() || zMax - zMin < 3.4f)
fail("grand stair is not one wide flight");
Ogre::MeshPtr grandFloor =
stairTestAttachedMesh(grandNode, "_floor");
int inHole = 0, outsideHole = 0;
countPlanes(grandFloor, 4.2f, Rect{ 1.0f, 7.0f, 1.0f, 5.0f },
inHole, outsideHole);
if (inHole != 0 || outsideHole == 0)
fail("grand stair block opening missing");
}
/* Teardown: destruct the entities and let the CellGridSystem sweep
* own the Ogre resources (it tracks the meshes per grid). */
gridE.destruct();
plainE.destruct();
if (extE.is_alive())
extE.destruct();
if (grandE.is_alive())
grandE.destruct();
cellSys->update();
if (gridNode->numAttachedObjects() == 0)
sm->destroySceneNode(gridNode);
if (plainNode->numAttachedObjects() == 0)
sm->destroySceneNode(plainNode);
if (extNode && extNode->numAttachedObjects() == 0)
sm->destroySceneNode(extNode);
if (grandNode && grandNode->numAttachedObjects() == 0)
sm->destroySceneNode(grandNode);
if (!ok)
return false;
@@ -55,6 +55,9 @@ static_assert(LayoutLib::PlanFlags::IntWindowZPos ==
* untranslated like every other cell flag. */
static_assert(LayoutLib::PlanFlags::StairWell == CellFlags::StairWell,
"");
static_assert(LayoutLib::PlanFlags::ExteriorStair ==
CellFlags::ExteriorStair,
"");
/* Foundation reaches this far below the lowest terrain corner. */
static const float FOUNDATION_MARGIN = 0.5f;
/* The foundation block(s) overhang the shell walls by this much per
@@ -1154,6 +1157,11 @@ void TownGenSystem::fillBuildingCells(const TownGen::TownBuilding &b,
c.flags = flags;
}
/* Phase 7.3a: every multi-storey plan gets a stairwell so the
* upper floors are reachable; an explicit stairwell room wins. */
if (plan.floors > 1)
autoPlaceStairwell(grid, b.address);
/* Lock every external doorway: the leaf is there (and blocks
* the doorway physically and on the navmesh) but cannot be
* opened before the interior exists. */
@@ -1171,6 +1179,135 @@ void TownGenSystem::fillBuildingCells(const TownGen::TownBuilding &b,
grid.markDirty();
}
bool TownGenSystem::autoPlaceStairwell(CellGridComponent &grid,
const std::string &address)
{
const int floors = grid.height;
if (floors < 2)
return false;
for (const Cell &c : grid.cells) {
if (c.flags & CellFlags::StairWell)
return true; /* explicit stairwell room wins */
}
const int W = grid.width;
const int D = grid.depth;
/* Internal walls + internal windows are opened on the run cells:
* a carved stairwell may cross room boundaries. */
const uint64_t intOpenMask =
CellFlags::AllInternalWalls | (1ULL << 22) | (1ULL << 23) |
(1ULL << 24) | (1ULL << 25);
auto cellAt = [&](int x, int y, int z) { return grid.findCell(x, y, z); };
auto hasFloor = [&](int x, int y, int z) {
const Cell *c = cellAt(x, y, z);
return c && (c->flags & CellFlags::Floor);
};
struct Run {
bool alongX = true;
int start = -1;
int cross = 0;
int len = 0;
};
Run best;
/* Deterministic scan: 4-cell runs first (tread 0.30 at 2 m cells,
* the verified climbable shape), 3-cell fallback; rows/columns in
* order. A run needs the boarding cell at the low end (ground
* floor) and the step-off cell at the high end (top floor). */
auto tryAxis = [&](bool alongX) {
const int axisLen = alongX ? W : D;
const int crossLen = alongX ? D : W;
for (int wants = 4; wants >= 3 && best.start < 0; --wants) {
if (wants + 2 > axisLen)
continue;
for (int cross = 0; cross < crossLen && best.start < 0;
++cross) {
for (int start = 1; start + wants < axisLen;
++start) {
bool ok = true;
for (int y = 0; y + 1 < floors && ok;
++y)
for (int i = 0; i < wants && ok;
++i) {
const int x = alongX ?
start + i :
cross;
const int z = alongX ?
cross :
start + i;
const Cell *c =
cellAt(x, y, z);
if (!c ||
(c->flags &
CellFlags::AllDoors))
ok = false;
}
const int bx = alongX ? start - 1 : cross;
const int bz = alongX ? cross : start - 1;
const int ex =
alongX ? start + wants : cross;
const int ez =
alongX ? cross :
start + wants;
if (!ok || !hasFloor(bx, 0, bz) ||
!hasFloor(ex, floors - 1, ez))
continue;
best.alongX = alongX;
best.start = start;
best.cross = cross;
best.len = wants;
break;
}
}
}
};
/* Prefer the longer usable axis; try the other as a fallback. */
if (W >= D) {
tryAxis(true);
if (best.start < 0)
tryAxis(false);
} else {
tryAxis(false);
if (best.start < 0)
tryAxis(true);
}
if (best.start < 0) {
Ogre::LogManager::getSingleton().logMessage(
"TownGen: warning - no stairwell fits building " +
address + " (" + std::to_string(W) + "x" +
std::to_string(D) + " cells, " +
std::to_string(floors) +
" floors); upper floors are unreachable");
return false;
}
for (int y = 0; y + 1 < floors; ++y)
for (int i = 0; i < best.len; ++i) {
const int x = best.alongX ? best.start + i : best.cross;
const int z = best.alongX ? best.cross : best.start + i;
Cell *c = cellAt(x, y, z);
c->flags |= CellFlags::StairWell;
c->flags &= ~intOpenMask;
}
/* Open the facing walls at the boarding and step-off cells. */
const int bx = best.alongX ? best.start - 1 : best.cross;
const int bz = best.alongX ? best.cross : best.start - 1;
const int ex = best.alongX ? best.start + best.len : best.cross;
const int ez = best.alongX ? best.cross : best.start + best.len;
if (Cell *bc = cellAt(bx, 0, bz))
bc->flags &= ~(best.alongX ? CellFlags::IntWallXPos :
CellFlags::IntWallZPos);
if (Cell *ec = cellAt(ex, floors - 1, ez))
ec->flags &= ~(best.alongX ? CellFlags::IntWallXNeg :
CellFlags::IntWallZNeg);
return true;
}
void TownGenSystem::createPlanRooms(flecs::entity building,
const TownGen::TownBuilding &b,
const CellGridComponent &grid)
@@ -220,6 +220,15 @@ private:
static void fillBuildingCells(const TownGen::TownBuilding &b,
CellGridComponent &grid,
const std::string &genMode);
/** Phase 7.3a: carve an automatic stairwell into a multi-storey
* plan that declares none (a `:type stairwell` room wins). Picks
* a deterministic straight run along the longer usable axis (4
* cells, 3 as a fallback) with a boarding cell on the ground
* floor and a step-off cell on the top floor; interior walls on
* the run and the two facing entry/exit walls are opened. Logs a
* warning and returns false when nothing fits. */
static bool autoPlaceStairwell(CellGridComponent &grid,
const std::string &address);
/** Phase 6.5.F: materialize the plan's room metadata (roomIds +
* roomNames/roomTypes/roomFurnitureTags) as RoomComponent children
* for full/interiorOnly grids, so DSL-authored rooms are furnished
@@ -4519,6 +4519,32 @@ static bool runTownGenProbe()
}
}
}
/* Phase 7.4: a door with :gallery on an upper floor marks its
* cell as an exterior stair. */
{
BuildingPlan pExt;
LayoutEval evExt(lb, &rngDsl, nullptr);
if (!evExt.evaluate(
"(shell 12 8 :floors 2)"
"(floor 1 (door front -4 :gallery))",
pExt, &derr)) {
printf(" *** dsl door :gallery: eval failed "
"'%s' ***\n", derr.c_str());
ok = false;
} else {
const uint64_t doorCell = pExt.at(1, 1, 3);
const uint64_t other = pExt.at(2, 1, 3);
if (!(doorCell & PlanFlags::ExteriorStair) ||
!(doorCell & PlanFlags::DoorZPos) ||
(other & PlanFlags::ExteriorStair)) {
printf(" *** dsl door :gallery: flags "
"%llx/%llx ***\n",
(unsigned long long)doorCell,
(unsigned long long)other);
ok = false;
}
}
}
/* Phase 6.5.F: furniture-selection metadata is part of the
* room plan and stays parallel to room names/types. */
+11 -4
View File
@@ -752,16 +752,23 @@ private:
" (floor n body...) per-floor context\n"
" (door front|back|left|right center|<offset-m>)"
" center on a non-rect footprint moves to the "
"nearest floor cell (warning)\n"
"nearest floor cell (warning); upper floors accept "
":gallery (outside walkway + flight down the facade)\n"
" (windows all|front|back|left|right [:spacing m])"
" 'all' covers the whole footprint boundary of a "
"rooms-first shell (incl. courtyard walls)\n"
" (room name (rect x0 z0 x1 z1) [:type type] "
"[:windows none|all] [:furniture (tag ...)] "
"[:compose (rule ...)]) "
"fractions of the shell; :windows none suppresses "
"exterior windows on the room's boundary cells "
"(bathroom/storage); declare rooms before the "
"fractions of the shell; :type stairwell marks the "
"room as a staircase (its ceiling and the floor above "
"are opened; connect it to the rooms on both ends of "
"the run) - multi-storey layouts without one get an "
"automatic straight-run stairwell; an elongated "
"stairwell room (aspect >= 1.5, run >= 3 cells) builds "
"one wide grand flight; :windows none "
"suppresses exterior windows on the room's boundary "
"cells (bathroom/storage); declare rooms before the "
"windows verb\n"
" (connect roomA roomB) door in the "
"shared wall (alias: idoor)\n"