diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md
index 77ab565..597995b 100644
--- a/src/features/editScene/AGENTS.md
+++ b/src/features/editScene/AGENTS.md
@@ -333,6 +333,12 @@ cd demos/demo-taxi
# and drives when observed, symbolic advance when not, parked at the
# destination parking afterwards, evening leg drives home:
./demoTaxi --headless --test-traffic-schedule
+# ...or headless parking acceptance probe (phase 6.6.8): sweeps a
+# scripted day asserting exclusive (area, slot, chain) occupancy and
+# bit-exact re-probes, then forces a high-detail despawn/respawn,
+# checks the live drop-off against the claimed park pose and carjacks
+# the car to verify fleet/claim release:
+./demoTaxi --headless --test-parking
# ...or probe natural (non-forced) high-detail spawns from a given player
# position; prints how many population NPCs are spawned plus per-NPC
# entity/master-mesh/age/sex diagnostics at frame N:
@@ -770,8 +776,8 @@ selected NPC's full 24h schedule + home/work/interest bindings, plus a "Path"
pane (the active taxi-goal path first, else the motion path, as a waypoint
list) and a "Locations" pane (the area's `TownLocations::LocationSet`). NPCs
with an active taxi goal are marked `[TAXI state]`, traffic drivers get a
-`[CAR]` marker with a per-car section (parked-at, phase, active leg,
-high-detail state). While the
+`[CAR]` marker with a per-car section (claimed slot + chain occupancy,
+phase, active leg, high-detail state, park watchdog/fallback). While the
overlay is open the mouse is ungrabbed so its widgets are reachable; **F7**
or **ESC** closes it and grabs the mouse again — ESC never opens the pause
menu while the overlay is up (overlay closes first, next ESC pauses). The
@@ -1195,16 +1201,24 @@ layer must see the fresh flag in the SAME frame, otherwise the frame a
drive leg ends leaves the registry position at the last symbolic car
position for one frame; `postPhysicsUpdate` right after NpcTaxiSystem's)
owns the population's car fleet. When an area's population and
-`TownLocations::LocationSet` exist, each Parking location rolls a
-deterministic hash (fnv1a of the location name, the TownLocations
-per-lot discipline) against `carOwnershipRatio` for one car, capped at
-`maxCarsPerArea`; areas without parking rects (only the layout DSL
+`TownLocations::LocationSet` exist, each ADDRESS with at least one
+Parking (or curb-fallback) spot rolls ONE deterministic hash (fnv1a of
+the lowest-index slot's name — curb-only towns keep their legacy
+per-location rolls bit-identically) against `carOwnershipRatio` for one
+car, capped at `maxCarsPerArea`, parked at the address's lowest-index
+free slot; areas without parking rects (only the layout DSL
`parking` verb creates Parking POIs) fall back to the Curb locations as
on-street parking. The driver is the first adult (ageYears >= 18)
whose `npcHome` string column matches the parking spot's address.
-Parked cars sit nose along the nearest road edge
-(`RoadPath::nearestEdgeInfo`, deterministic direction bit) with the
-handbrake on and no driver.
+Parked cars sit with the handbrake on and no driver; the pose comes
+from `parkedPlacement()`: a bay-slot POI (phase 6.6.1) parks at its
+own position with its recorded nose-to-street heading, while a curb
+POI is projected onto the curb line (`RoadPath::curbChainPose`,
+phase 6.6.4 — `nearestEdgeInfo` halfW + `curbLateralOffset` past the
+carriageway edge on the POI's side so the body clears the outer lane,
+nose along the edge with the fnv1a(name) direction bit, spot k shifted
+k * (`curbChainCarLength` + `curbChainGap`) backwards along the nose —
+the chain slots are picked by the 6.6.3 occupancy registry).
Schedule-adherent driving (6.A, replaced the 5.4 endless cruising
loop): `evalSymbolic` re-derives each car's state from
@@ -1228,6 +1242,36 @@ advances along the route at `cruiseSpeed`). **Purity rule:
it back** (no live walker/car positions, no alight-position persist);
live entities are visual approximations only.
+**Slot occupancy (6.6.3)** is pure too: `rebuildClaims()` runs every
+frame before the per-car evaluation and replays the whole day's claim
+events from day start — per-car chains with occupancy-blind default
+endpoints give pure event times, then a global (time, driver-id,
+leg)-ordered replay claims the destination slot at the WALK departure
+(the live tier needs it to drive to) and releases a visitor origin at
+the DRIVE departure (the car physically leaves then). Home spots stay
+permanently claimed. The same clock always reproduces the same
+assignments bit-exactly, so clock scrubs, pauses and scene reloads need
+no reconciliation code; the per-leg claimed endpoints are recorded in
+`TrafficCar::legClaims` and the route geometry uses them while the
+timing always comes from the default route. `TrafficInfo` exposes the
+held claim (`area`, `parkedAtSlot`, `parkedAtChain`) plus the live park
+watchdog/fallback state (`parkBlockedSeconds`, `fallbackActive`) for F7
+and headless probes.
+
+**Pad reachability (6.6.6, helper-geometry pivot):** `slotReachable()`
+always returns true — raised pads are one solid frustum whose top
+face touches the road at `Pad::topY` and whose sides are walkable
+30-degree slopes, non-fitting pads park parallel at the curb line,
+so there is no natural-terrain seam left to probe (the old
+`padMaxGrade` terrain-slope probe died with the runtime pad
+compliance). `TownGenSystem::createParkingPad()` builds the actual
+pads as helper geometry (see RoadSystem automatic compliance below).
+Every drive leg also inserts a park-APPROACH spine waypoint
+(`parkApproachSpineDist` 5 m, on the approach side of the park pose)
+so the final segment runs ALONG the slot's nose axis — a diagonal
+approach stops at the AI's 2.5 m arriveRadius with LATERAL error,
+which the alight gate (lateral 1.5 m) rejects.
+
Two tiers, symbolic ground truth (same design as NpcDetailSystem): the
`TrafficCar` record survives spawn/despawn and scene switches; the
high-detail tier (camera within 150 m, hysteresis 220 m — the taxi demo
@@ -1249,9 +1293,22 @@ setState no-ops before AnimationTreeSystem has per-entity state; the
re-assert is an early-out once latched). Observed physics is
best-effort: the AI can park AFTER the symbolic clock already chained
into the next leg (late board, junction yield), so the alight check
-lives outside the Driving phase and fires whenever the AI reports
-arrived; when the symbolic layer is driving another leg by then the
-car despawns to resync. On despawn nothing is persisted back — the
+lives outside the Driving phase and fires on the drop-off gate
+regardless of phase; when the symbolic layer is driving another leg by
+then the car despawns to resync. Drop-off discipline (6.6.5): the
+leg route's FINAL waypoint is the park pose (not its lane projection),
+so the approach steers into the slot, and the driver alights only when
+the live car sits inside the park pose's box — `parkArriveM` (1.5 m)
+laterally off the slot's nose axis, `parkLongitudinalM` (3.0 m) along
+it — and slower than `parkAlightSpeedMps` (0.6 m/s), never mid-lane on
+bare `AiDriverComponent::arrived` (the AI's 2.5 m arriveRadius only
+ever undershoots longitudinally). An arrived-but-short car gets one
+creep retarget onto the pose; an approach that stops improving inside
+`parkEngageM` (9 m) escalates after `parkFallbackSeconds` (5 s) to
+`retargetToFallback()` — the nearest free curb-chain spot, warned
+once. The claim does NOT move on fallback (the symbolic layer stays
+untouched; the deviation self-heals on the next leg or despawn). On
+despawn nothing is persisted back — the
next spawn reads the clock-pure symbolic state.
`NpcDetailSystem::isSpawnEligible` consults
`NpcTrafficSystem::isDriving`/`isSeated` so a suspended or seated
@@ -1262,12 +1319,16 @@ carjacking) leaves the fleet: the record is erased, the driver alights
in place and returns to its schedule. Symbolic cars are invisible and
cannot collide — junction priority only exists in the high-detail
tier, so a transient overlap at the tier boundary is accepted.
-Headless probe: `--test-traffic-schedule` (exterior scene): scrubs the
+Headless probes: `--test-traffic-schedule` (exterior scene): scrubs the
clock to a driver's work leg — car parked at home before departure,
driver boards and the car drives when observed, symbolic tier advances
when not, car parked at the destination parking after arrival with the
driver back on schedule, evening return leg drives home (replaces the
-5.4 `--test-traffic-town` cruising probe).
+5.4 `--test-traffic-town` cruising probe), and the final parked-at name
+is validated against the area's `LocationSet`. `--test-parking` is the
+6.6 acceptance gate: exclusive occupancy and exact snapshots over a
+scripted day, reachable slot names, despawn/respawn with an unchanged
+claim, live park-box/mid-lane validation and carjack claim release.
### EditorSkyboxSystem / EditorSunSystem
@@ -1561,9 +1622,20 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
Evaluation failures
warn and fall back to the district default (`evaluate()` resets the
output plan on entry, so the fallback retry starts clean). Parking rects become
- extra POIs (`"
parking N"` — buildings are no longer 1:1
- with POIs, `TownGenSystem` gathers via `TownPoi::building`); an
- optional `town_layouts.txt` in the project directory extends the
+ one POI per enumerated bay slot (phase 6.6.1,
+ `roadlib/TownParking::enumerateSlots` — 2.6 x 5.2 m bays, 2 rows +
+ center aisle when deep enough, cap 12/rect, too-small rects degrade
+ to one centered slot), named `" parking [i]-s"` and
+ carrying the nose-to-street heading (`TownPoi::hasHeading`/
+ `headingYaw` = the building yaw) — buildings are no longer 1:1 with
+ POIs, `TownGenSystem` gathers via `TownPoi::building`; slots blocking
+ the front-door->road walk corridor or the doorstep clearance are
+ dropped deterministically with a `TownResult::warnings` entry (phase
+ 6.6.2, `TownParking::pruneSlots`). Each rect also emits a
+ `TownResult::Pad` (building/address/center/yaw/w/d plus `edge`, the
+ frontage-edge pin used by terrain compliance); frontage length
+ ranking is XZ-only so road-height compliance cannot flip near ties.
+ An optional `town_layouts.txt` in the project directory extends the
library. **Town areas** (phase 4.5 item 4): `RoadGraph::townAreas`
(`RoadTownArea`: name, square/circle XZ scope, district, seed,
densityScale) scope generation per settlement —
@@ -1717,7 +1789,10 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
collider and LOD set (`RoadPageGeometry::bufferEntities`/`bodyIds`;
`_c` mesh-name suffix, see `ProceduralRoadGeometry.md` §16). This
keeps every mesh in the 16-bit index range and frustum-culls tighter
- than one page-sized mesh. Note: an upstream Ogre `MeshLodGenerator`
+ than one page-sized mesh. `ensureRoadMaterial()` marks the ensured
+ material resource ready with its pointer so chunk conversion can bind
+ it immediately; `ProceduralMaterialSystem` still restyles it on the
+ next update. Note: an upstream Ogre `MeshLodGenerator`
bug (LOD index buffer typed by index count instead of vertex count,
overrunning 16-bit LOD buffers of 32-bit-index meshes) is fixed in the
vendored Ogre fork — chunking still applies regardless.
@@ -1820,10 +1895,81 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
terrain to fit. The standalone "Comply Roads to Terrain" button
keeps the full `roadThickness + 0.05` lift with no cut budget for a
raised roadbed look.
+ **Parking pads are helper geometry (6.6.6, pivot):** town layouts'
+ parking rects become runtime pad entities, NOT terrain compliance —
+ runtime terrain edits were unreliable (fixup writes racing page
+ colliders, junction-wedge exclusion zones swallowing whole pads) and
+ slowed startup. `TownGen.cpp` decides purely whether each
+ `TownResult::Pad` gets a frustum at all; a rect that cannot host a
+ clean driveway instead parks PARALLEL at the roadside (user-directed:
+ a perpendicular bay without a driveway never has enough room). The
+ fit rules: `topY` = the frontage road's top − 0.2 (NEVER above it,
+ so the pad never pokes through the road); `apronReach` = the
+ street-ward extension of the top face to the carriageway edge
+ (half-width + 0.25 curb backoff, no overlap into the road body), so
+ the top face bridges the road's entry-apron band. Raised requires
+ road top − lot ground > 0.35, no terrain poking through the parking
+ surface and `drop` <= 4 m. House contact is allowed only when
+ `topY <= floorY + 0.15` (a door-threshold step) — the frustum then
+ meets the foundation and can never cut a wall, the stairs or the
+ door; a HIGHER platform must clear the house footprint and the
+ doorstep clearance entirely (one street-ward shift of the rect,
+ bays included, may buy the room; the top face's street edge stays
+ pinned to the carriageway edge). `Pad::drop` is settled in the
+ pure layer (bottom face 0.4 m below the LOWEST ground over the
+ footprint, so no slope foot floats). Failing pads get no geometry
+ and ONE parallel roadside spot per address on the curb line
+ (`RoadPath::curbChainPose`, nose along the street) — a tight
+ multi-spot chain was rejected because the AI stops in the lane
+ behind the car in front and never reaches the curb; extra capacity
+ comes from the ordinary curb chain.
+ `TownGenSystem::createParkingPad()` (called from `createBuilding`
+ with the building's pads) builds ONE SOLID FRUSTUM per raised pad
+ (user-directed design): a single 12-triangle mesh (TriangleBuffer
+ filled directly — top, bottom and four side faces) whose top face
+ is the parking surface sized `pad.w x (pad.d + apronReach)` at
+ `topY`, and whose bottom face is dropped by `Pad::drop` and
+ expanded by drop * tan(30deg) per side, so all four sides form
+ walkable 30-degree slopes whose bottom edges sit below the
+ terrain — no seams, no gaps, no loose parts, driveways never block
+ building exits. The collider is the mesh itself
+ (`PhysicsColliderComponent::ShapeType::Mesh`); because the mesh is
+ procedural, `EditorPhysicsSystem::updateRigidBody()` DEFERS body
+ creation while a Mesh collider references a still-dirty
+ TriangleBufferComponent (retried next sync) — without that the
+ body used to be built before ProceduralMeshSystem converted the
+ buffer and silently fell back to a 0.5 m default box (a ghost
+ pad). The terrain is never written. Bay-slot parked
+ poses inherit `topY` through the parking POI positions (TownGen sets
+ the POI's y to the pad top; `TownLocations`/`parkedPlacement` copy
+ it), so cars spawn on the parking surface. The pad
+ geometry regenerates with its building (per-page streaming and
+ per-building regeneration both pass the pads through); the
+ "parking-pad" entity is the stable per-address anchor for future
+ parking infrastructure (garages). Raised pads whose
+ driveway reach is deep enough (apronReach >= 2.6 m) also emit ONE
+ overflow parking POI (" parking-ovf", nose along the
+ street) at the driveway mouth — a car parks there only when every
+ designated slot is taken (the pick tiers: free bays -> free
+ overflow -> curb chain), never stacked (one exclusively-claimed
+ pose) and never in front of the door (it sits at the road end of
+ the driveway). Narrow lots with neither driveway room nor a
+ side-wall strip are a PLAN PROBLEM / TODO for the later, more
+ general placement system (6.6.2's corridor/pruning rules keep such
+ lots car-free rather than blocking exits).
+ Commercial layouts size their rects for at least 4 bays
+ (town_layouts.txt: the corner-shop's 13x5.2 rect = 5 bays);
+ residential keep vehicles+1 headroom (the curb chain supplements
+ the single roadside spot).
Covered by the `autoCompliance` and `autoComplianceAprons` terrain
tests (converge, no terrain-over-road violations, bounded hover,
idempotent — a second `complyAll()` reports 0 changes, with and
- without entry aprons).
+ without entry aprons) and by `TERRAIN_TEST_FILTER=townParkingPads`
+ (sculpted fixture: the rect beside the house, the road 1 m above
+ the local terrain — at least one raised frustum MUST fit; raised
+ pads = exactly one frustum entity with a mesh collider at `topY`,
+ flat pads build no geometry, terrain bit-identical under the
+ pads).
- **Region streaming (M5)**: when the terrain has `streamingEnabled`, the
road graph persists per terrain page in
`heightmaps//roads/_.json` (`RoadRegionStore`,
diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt
index b3a763e..67a9f17 100644
--- a/src/features/editScene/CMakeLists.txt
+++ b/src/features/editScene/CMakeLists.txt
@@ -892,6 +892,8 @@ add_library(RoadGeometryLib STATIC
roadlib/RoadPath.hpp
roadlib/TownGen.cpp
roadlib/TownGen.hpp
+ roadlib/TownParking.cpp
+ roadlib/TownParking.hpp
roadlib/TownPopulation.cpp
roadlib/TownPopulation.hpp
roadlib/TownLocations.cpp
diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp
index 305dea8..d96b56f 100644
--- a/src/features/editScene/EditorApp.cpp
+++ b/src/features/editScene/EditorApp.cpp
@@ -496,6 +496,10 @@ void EditorApp::destroyEditorSystems()
m_zoneSystem.reset();
m_aiDriverSystem.reset();
m_vehicleSystem.reset();
+ /* Hair ragdolls hold Jolt constraints; the destructor removes them
+ * from the JPH::PhysicsSystem, so the hair system must go before
+ * the physics system. */
+ m_hairPhysicsSystem.reset();
m_physicsSystem.reset();
/* Flush the RTShader generator cache before clearing materials. This
diff --git a/src/features/editScene/TerrainDoc.md b/src/features/editScene/TerrainDoc.md
index 2178ee2..10d24b0 100644
--- a/src/features/editScene/TerrainDoc.md
+++ b/src/features/editScene/TerrainDoc.md
@@ -74,6 +74,9 @@ Sculpting never touches the base layer: `setHeightAt`/brushes write into the spa
per-page **fixup chunk** store (`heightmaps//terrain_fixup/x{cX}_z{cZ}.bin`),
which is lazily disk-loaded, LRU-capped, and saved on eviction when dirty. Blend maps
and aux maps are likewise per-page files under `heightmaps//`.
+When a page is (re)built, `fillPageHeightData()` evaluates the base layer at its legacy
+integer sample positions but overlays the fixup layer at the exact double-precision
+vertex coordinates, so road/pad planes are not shifted by position truncation.
Height queries (`getHeightAt`, `raycastTerrain`) hit loaded pages when possible and fall
back to the analytic base+fixup evaluation for far/unloaded terrain, so tools (teleport
diff --git a/src/features/editScene/demos/demo-taxi/PLAN.md b/src/features/editScene/demos/demo-taxi/PLAN.md
index 2391d6d..8136cf8 100644
--- a/src/features/editScene/demos/demo-taxi/PLAN.md
+++ b/src/features/editScene/demos/demo-taxi/PLAN.md
@@ -3814,67 +3814,569 @@ and schedule/job behavior. Vehicle damage/economy simulation is likewise
gameplay scope; Phase 6.5 supplies and proves the repair-bay furniture, POI,
path and action wiring it will use.
-### Phase 6.6 — Parking spots & vehicle drop-off (problem statement)
+### Phase 6.6 — Parking spots & vehicle drop-off
-Observed in the demo (2026-10-24), all four stem from the same gap:
-there is no first-class parking concept, only `Parking`/`Curb` POI
-points that a single hash places a car on:
+**Status: DONE (2026-10-26; hardened 2026-10-28) — 6.6.1 through 6.6.8
+complete: named bay slots, pruning, curb chains, pure occupancy claims,
+drop-off discipline, helper-geometry parking pads (frustum fit rules +
+parallel roadside spots + park-approach spine), determinism and the
+acceptance probe battery.**
-1. **Cars block building exits.** Parking/curb spots are placed
- without regard to door positions or the walking paths between a
- building's entrance and the road, so parked cars end up blocking
- the way out (visually blocking exit from the doorway).
-2. **Drivers abandon the car mid-road.** When a drive leg finishes,
- the driver alights wherever the AI managed to stop — frequently in
- the traffic lane — instead of reaching a parking spot or at least
- pulling to the road side/shoulder. Vehicles must never be dropped
- in a lane.
-3. **Stacked cars.** Spot assignment has no occupancy tracking: a
- visiting car's `parkingFor()` destination pick can land on another
- car's home spot (and curb POIs can coincide), so two vehicles
- spawn/park exactly on top of each other.
-4. **Cars can't reach the road.** Spots beside the road sit on
- un-flattened, sometimes sloped ground; a spawned vehicle cannot
- climb from its spot onto the road surface. Parking needs terrain
- compliance (flattened pad) and a drivable ramp/connection to the
- road.
+#### The idea (plain words)
-Design considerations to solve for (not yet formulated into an
-implementation — the DSL side especially needs thought):
+Parking becomes a first-class SLOT model, end to end:
-- **Parking lots in the layout DSL.** A lot should be definable per
- building lot (most important later for commercial buildings, which
- NEED proper lots; residential can start minimal). Placement rule of
- thumb: between the road and the building, but leaving enough clear
- space for foot travel between the parked cars and the building
- entrance — the building<->road walking path must stay passable and
- door clearance zones must stay car-free.
-- **Discrete slots, not points.** A parking lot/rect should enumerate
- individual slots (position + orientation, capacity > 1) so multiple
- cars are handled gracefully: occupancy is tracked per slot, no two
- cars ever receive the same slot (home spots, destination picks and
- spawn placement all draw from the same occupancy registry).
-- **Drop-off discipline.** The symbolic layer already chains cars
- through `destParkPos`; the high-detail AI must actually ARRIVE there
- (or at the nearest free fallback slot, or at least the road
- shoulder) before the driver alights — never mid-lane. Blocked or
- unreachable slots need a defined fallback.
-- **Access ramps & compliance.** Parking pads get terrain flattening
- and a shallow, drivable connection ramp to the road edge so NPC
- vehicles can reliably drive in/out; spawn-time validation that the
- car can physically reach the road from its slot.
-- **Curb fallback stays** for layouts without parking rects, but curb
- placement must be at the road side, oriented along the edge, out of
- the lane, and door-clear.
+1. The DSL parking rect enumerates discrete named bay slots (a pose +
+ capacity > 1) instead of collapsing to one center point — task
+ 6.6.1, with door/corridor placement rules in 6.6.2.
+2. Every spot pick (home, visitor, spawn) draws from a per-area
+ occupancy registry, so no two cars ever share a slot — task 6.6.3;
+ curb-only areas get a deterministic roadside slot chain in 6.6.4.
+3. A drive leg ends when the car ACTUALLY reaches its slot (or a
+ roadside fallback) before the driver steps out, never mid-lane —
+ task 6.6.5.
+4. Pads become ONE SOLID FRUSTUM each where a driveway fits (a
+ parking platform whose top face touches the road and whose
+ 30-degree slopes walk down to the terrain); every other rect
+ parks PARALLEL at the roadside, and the terrain is never
+ touched — task 6.6.6.
-### Phase 6.7 - DSL, schedule and POIs.
+Determinism rules (6.6.7) and the probe battery (6.6.8) are
+cross-cutting; defects observed in the demo (2026-10-24) are tagged
+P1-P4 and cited per task.
+
+#### Why: parking today (verified symbols)
+
+- DSL `(parking x z w d)` / `(parking (rect-front-left w d))` records
+ a lot-frame rect `PlanParking{x,z,w,d}` (LayoutEval.cpp ~1803;
+ struct at BuildingPlan.hpp:76).
+- TownGen turns each rect into ONE POI — `TownPoi::Parking` at the
+ rect center, `" parking [i]"` (TownGen.cpp ~841-871).
+- `TownLocations::buildLocations()` maps every POI to one point
+ `NpcLocation` (Parking; everything else Curb, TownLocations.cpp
+ ~166) — that bare point is all the traffic stack ever sees.
+- `NpcTrafficSystem`: `parkedPlacement()` (:162) parks AT the POI
+ point and only orients the nose along the nearest road edge
+ (`nearestEdgeInfo` + a fnv1a name direction bit); `parkingFor()`
+ (:188) picks the destination as the nearest Parking-or-Curb in the
+ whole town (tie = lowest location index) with NO occupancy; the leg
+ route shifts EVERY waypoint — the destination included — into the
+ right lane (`RoadPath::offsetToRightLane`, RoadPath.cpp:323); the
+ live tier alights on bare `AiDriverComponent::arrived` (:835) and
+ drops the driver at `doorWorld()` (:688) wherever the car stopped.
+ Only the symbolic layer ever "reaches" the park pose
+ (`lr->destParkPos`, :464, clock-pure).
+
+#### Defects observed in the demo (2026-10-24)
+
+- **P1 — cars block building exits:** spots are placed with no
+ `TownBuilding::doorstep` / walk-corridor constraint anywhere.
+- **P2 — drivers abandon the car mid-lane:** the AI stops at the
+ lane-shifted final route waypoint; no park-pose arrival gate exists.
+- **P3 — stacked cars:** nearest-by-distance with no occupancy hands
+ one spot to several cars; a curb POI is a bare point, so two cars
+ coincide exactly.
+- **P4 — cars can't reach the road:** the POI point sits on
+ un-flattened terrain with no pad, no apron and no reachability
+ check.
+
+#### Tasks
+
+**Task 6.6.1 — Discrete bay slots (pure town model; enables all) —
+DONE (2026-10-26).**
+
+- What: a small pure helper (`roadlib/TownParking.{hpp,cpp}`) expands
+ each `PlanParking` rect into N bay slots from the rect + the lot's
+ frontage axes: a double row + center aisle when the depth allows,
+ else one row; bay size = traffic-sedan chassis footprint + a door
+ margin; per-rect cap (~12, config). Names `" parking
+ -s"` stay address-stable. Slots carry a heading (nose to the
+ road) via a new optional pose on `TownPoi`/`NpcLocation`;
+ `parkedPlacement()` prefers it over the road-edge projection (which
+ would park a bay car diagonally across its bay).
+ Implemented as specced: `TownParking::enumerateSlots` (bay 2.6 x
+ 5.2 m from the sedan collider half extents 1.0107/2.4834 -> 2.02 x
+ 4.97 m + margins; 2 rows + 6 m aisle when d >= 2*5.2+6, else 1
+ road-side row; cap 12; a too-small rect degrades to one centered
+ slot); `TownPoi`/`NpcLocation` gained `hasHeading` + `headingYaw`;
+ `parkedPlacement()` prefers the heading.
+- Done when: the road_geometry_overlap_test town probe asserts the
+ bay math — count, inside-rect, spacing >= bay width, seed-identical
+ sets, road-facing headings.
+ (`runTownParkingProbe`, registered in the probe battery: pure
+ single/double-row/cap/degraded cases plus the TownGen integration —
+ 48 slot POIs over 12 buildings with names `" parking-s"`
+ (single rect: no rect index), `headingYaw == TownBuilding::yaw` and
+ row-geometry/determinism assertions.)
+
+**Task 6.6.2 — Door & corridor rules (pure; fixes P1) — DONE
+(2026-10-26).**
+
+- What: slot expansion/pruning keeps a ~1.5 m walk corridor from the
+ front door (`doorCellX` / `doorstep`) to the frontage road and a
+ doorstep clearance zone car-free; violators drop deterministically
+ with a `TownResult::warnings` entry. The placement rule of thumb
+ goes into the DSL help (`LayoutLibrary::examples()`).
+ Implemented: `TownParking::WalkCorridor{doorX, doorZ, halfWidth
+ 0.75, doorClear 1.5}` + `TownParking::pruneSlots()` (AABB overlap
+ of each bay against the road-edge->doorstep strip plus the
+ doorstep clearance square, pure and deterministic); TownGen prunes
+ in the rect frame (doorstep projected onto the frontage axes) and
+ warns `"lot strip () parking rect : dropped K
+ bay slot(s) blocking the door walk corridor"`. The DSL help text
+ (TerrainEditor.hpp — where the parking verb is documented; the
+ examples list itself was left untouched) now states the corridor
+ rule.
+- Done when: the probe finds every doorstep->frontage corridor clear
+ of bays, with stable per-seed drops and warnings.
+ (`runTownParkingProbe`: three pure prune cases plus the wide-rect
+ `lot-w` integration — 9..11 survivors per building, no survivor
+ inside the doorstep square, the drop warning, and
+ regenerate-identical determinism.)
+
+**Task 6.6.3 — Occupancy registry & assignment (fixes P3) — DONE
+(2026-10-26).**
+
+- What: a per-area occupancy map slot-name -> car key. Home cars:
+ the fnv1a ownership roll (`carOwnershipRatio`, `maxCarsPerArea`)
+ moves from per-LOCATION to per-ADDRESS — with slot POIs a
+ per-location roll would multiply car counts — and takes the
+ address's lowest-index free slot. Visitors: `parkingFor()` filters
+ FREE Parking slots (nearest, tie = lowest location index as today),
+ then free Curbs — never an occupied spot; curb-chain k picks
+ (6.6.4) draw the same way. Release on despawn / carjack / scene
+ switch; rebuilt deterministically on load.
+- **Implementation deviation — the map is PURE, not incremental
+ runtime state.** The first (incremental latch + reconcile)
+ version broke `--test-traffic-schedule`'s bit-exact re-probe:
+ claim order followed wall evaluation order, so a clock scrub
+ re-resolved shared destinations in driver-id order instead of
+ departure-time order. `NpcTrafficSystem::rebuildClaims()` instead
+ replays the whole day's claim events from day start every frame —
+ per-car chains with occupancy-blind default endpoints give pure
+ event times (phase A), then a global (time, driver-id)-ordered
+ replay (phase B) claims the destination slot at the WALK departure
+ (the live tier needs it to drive to) and releases the visitor
+ origin at the DRIVE departure (the car physically leaves then).
+ Same clock -> same assignments, bit-exact under scrubs, so
+ "release/rebuild on load" needs no code at all. Home spots stay
+ permanently claimed (a visitor never takes someone's home slot;
+ the owner always re-parks exactly at home). Seed rolls ride on
+ the lowest-index slot's NAME per address, so curb-only towns keep
+ their legacy fleet bit-identically.
+- Done when: `--test-parking` runs a scripted day with N cars sharing
+ no slot, and the same seed reproduces the same assignments.
+ (Interim: `--test-traffic-schedule` sweep + re-probe bit-exactness
+ with 18 cars covers the purity; the dedicated parking battery
+ lands with 6.6.8.)
+
+**Task 6.6.4 — Curb fallback chain (pure geometry; fixes P3;
+selection in 6.6.3) — DONE (2026-10-26).**
+
+- What: a curb location becomes a chain of parallel street spots: the
+ k-th spot shifted k * (carLen + gap) along the edge from the POI
+ projection (starting side from the existing fnv1a(name) direction
+ bit), centered on the curb line (`nearestEdgeInfo` halfW + a small
+ outward margin so the body clears the outer lane), nose along the
+ edge. Pure `RoadPath` geometry; k = 0 keeps today's pose, so
+ curb-only towns change only where cars would have stacked.
+ Implemented as `RoadPath::curbChainPose(graph, poiPos, k, carLen,
+ gap, curbMargin, flipDir, pos, rot)` with `parkedPlacement()`
+ gaining a `chainIndex` parameter and NpcTrafficSystem the config
+ `curbChainCarLength` 5.0 / `curbChainGap` 0.8 /
+ `curbLateralOffset` 1.2; the chain grows BEHIND the nose and the
+ lateral side is the POI's side of the road (centerline POIs fall
+ back to right-of-direction). **Deviation from the "k = 0 keeps
+ today's pose" line:** ALL curb spots — k = 0 included — move to the
+ curb line. Keeping k = 0 at the raw POI point would have left P1
+ unfixed in curb-only towns (the curb POI sits ~3 m in front of the
+ door — on the walkway, not at the curb — and every current town is
+ curb-only until layouts gain parking rects); the old "park at the
+ bare point" behavior has no remaining consumer.
+- Done when: the probe asserts chain spacing >= carLen, poses at or
+ outside the curb line (never inside the outer lane) and no
+ coincident pairs.
+ (`runTownParkingProbe` curb-chain section: k=0 curb-line pose with
+ the sedan body clear of the outer lane, k=1/2 spacing behind the
+ nose, flipped-direction chain growth, far-side/centerline lateral
+ picks, empty-graph false; `--test-traffic-schedule`, `--test-taxi`,
+ `--test-vehicle-drive` and `--test-determinism` all re-run green
+ after the global curb-pose change.)
+
+**Task 6.6.5 — Drop-off discipline (traffic + AI; fixes P2) — DONE
+(2026-10-26).**
+
+- What: keep the lane offsets along the route body but REPLACE the
+ final waypoint with the park pose (`lr.destParkPos` / `destParkRot`)
+ so the approach steers into the slot. Gate alighting (:835) on the
+ live pose being within `parkArriveM` (~1.5 m, config) of the park
+ pose — or of the fallback pose — instead of bare `arrived`.
+ Fallback after ~5 s blocked: drive to the nearest curb-chain pose
+ (6.6.4), warn once — never mid-lane. The symbolic layer stays
+ untouched (it already parks at `lr->destParkPos`, :464).
+ Implemented as specced, with the alight gate as a BOX instead of a
+ radius: `parkArriveM` (1.5 m) laterally off the slot's nose axis and
+ `parkLongitudinalM` (3.0 m) along it, plus `parkAlightSpeedMps`
+ (0.6) — a radius gate would demand 2.5x the AI's arriveRadius
+ precision longitudinally, while only the lateral part keeps the body
+ out of the carriageway (a parallel-parked car a meter back along
+ the curb is parked fine). The gate evaluates against the
+ `liveDestPos`/`liveDestRot` captured at AI attach (survives the
+ symbolic clock chaining into the next leg). An arrived-but-short
+ car gets one creep retarget onto the pose; the blocked watchdog
+ (inside `parkEngageM` 9 m without improving the approach minimum)
+ escalates after `parkFallbackSeconds` (5 s) to
+ `retargetToFallback()` = nearest curb with a free chain spot, warn
+ once; **the claim does NOT move on fallback** — the symbolic layer
+ stays clock-pure and the deviation self-heals on the next leg or
+ despawn. `TrafficInfo::parkedAtSlot` exposes the held claim.
+ Teardown fix piggy-backed here (found by this task's reruns):
+ `EditorApp::destroyEditorSystems()` now resets
+ `m_hairPhysicsSystem` before `m_physicsSystem` — hair ragdolls
+ remove their Jolt constraints in the system destructor, which needs
+ a live JPH::PhysicsSystem (previously a hair-wearing NPC alive at
+ shutdown crashed teardown in Jolt's ConstraintManager).
+- Done when: `--test-parking` shows every drop outside the
+ carriageway (lateral >= resolved halfW) and the final pose within
+ tolerance unless the fallback fired; `--test-traffic-schedule`
+ passes unchanged.
+ (`--test-traffic-schedule`, `--test-taxi`, `--test-vehicle-drive`,
+ `--test-determinism` and `--test-population-switch` all re-run PASS
+ with exit 0; the dedicated `--test-parking` acceptance gate also
+ passes.)
+
+**Task 6.6.6 — Pad helper geometry & drivable connection (fixes P4) —
+DONE.**
+
+- What: each parking rect becomes RUNTIME HELPER GEOMETRY instead of a
+ terrain edit. The terrain is never modified at runtime: the
+ original comply-based design (fixups written inside every
+ `complyAll()` pass) was unreliable in game mode — pad fixups
+ raced page-collider rebuilds, the junction-wedge exclusion could
+ swallow whole pads (a 2*halfWidth disc reached 21 m while a pad sat
+ 13.8 m from a T-junction node, leaving it 1.1 m below the raised
+ roadbed and beaching the car on the bench edge), and runtime
+ compliance slowed startup. Editorial "Auto Comply Roads & Terrain"
+ keeps working for roads; pads are pure geometry.
+- Design (user-directed): ONE SOLID FRUSTUM per pad — start from a
+ box whose top face is the parking surface (the pad rect extended
+ street-ward so it touches the road) sitting 0.2 below the road's
+ surface, then drop and expand the bottom face so all four sides
+ form 30-degree slopes whose bottom edges sit below the terrain.
+ A single 12-triangle mesh, exact mesh collider, no seams, no gaps,
+ no loose parts; the slopes are walkable, so driveways never block
+ building exits. (An earlier multi-part variant — slab + pitched
+ mouth + segmented skirts — was rejected as overcomplication and
+ replaced by this.)
+- Grading is pure and lives in TownGen. A pad gets a frustum ONLY
+ where it fits cleanly; every other parking rect parks PARALLEL at
+ the roadside (user-directed: a perpendicular bay without a
+ driveway never has enough room). The fit rules:
+ `TownResult::Pad::topY` = the frontage road's top − 0.2, NEVER
+ above it (a higher platform would poke through the road where the
+ road level drops along the frontage); `Pad::apronReach` = the
+ street-ward extension of the top face to the carriageway edge
+ (half-width + 0.25 curb backoff, NO overlap into the road body),
+ so the top face bridges the road's entry-apron band without
+ intersecting the road. Raised requires road top − lot ground >
+ 0.35, no terrain poking through the parking surface, and a drop
+ (top face to bottom face) of at most 4 m. House contact is
+ allowed only when the platform is at/below the house floor plus a
+ 0.15 m door-threshold step (`topY <= floorY + 0.15`) — then the
+ frustum meets the foundation and can never cut a wall, the stairs
+ or the door; a HIGHER platform must clear the house footprint and
+ the doorstep clearance entirely (one street-ward shift of the
+ rect, bays included, may buy the room; the top face's street edge
+ stays pinned to the carriageway edge). Failing pads get no
+ geometry and ONE parallel roadside spot per address on the curb
+ line (`RoadPath::curbChainPose`, nose along the street) — a tight
+ multi-spot chain was tried and rejected: it demands real
+ parallel-parking skill the AI does not have (it stops in the lane
+ behind the car in front and never reaches the curb). Extra
+ capacity comes from the ordinary curb chain. The bay-slot POI
+ positions of raised pads carry `topY`, so `TownLocations`,
+ `parkedPlacement()` and the F7 poses all inherit the parking
+ surface height.
+- Implementation: `TownGenSystem::createParkingPad()` (called from
+ `createBuilding` with the building's pads, so per-page streaming
+ and per-building regeneration both build them) parents a
+ "parking-pad" entity to the building: a static "parking"-textured
+ 12-triangle frustum (TriangleBuffer filled directly — top,
+ bottom and four side faces; UVs pinned to the texture center)
+ sized `pad.w x (pad.d + apronReach)` on top, with the bottom face
+ dropped by the pure-layer-settled `Pad::drop` (0.4 m below the
+ LOWEST ground over the footprint, so no slope foot floats) and
+ expanded by drop * tan(30deg) per side. The node sits at the top
+ face's center (`topY`); child transforms are building-local (the
+ world->local conversion bit me once: slabs built from world coords
+ landed ~46 m north of their pads). The collider is the mesh
+ itself (`ShapeType::Mesh`).
+ Physics ordering fix piggy-backed here (found by this task's
+ reruns): `EditorPhysicsSystem::updateRigidBody()` now DEFERS body
+ creation while a `ShapeType::Mesh` collider references a
+ `TriangleBufferComponent` on the same entity that is still dirty
+ (retried next sync) — previously the body was built before
+ `ProceduralMeshSystem` converted the buffer, the mesh load threw,
+ and `buildCompoundShape()` silently fell back to a 0.5 m default
+ box, leaving pads as ghost colliders cars drove straight through.
+ `slotReachable()` now always returns true (the only slopes are the
+ walkable 30-degree frustum sides); AiDriverSystem gained a
+ stuck-escape (full throttle + no movement for 2 s -> one
+ zero-input frame to release the brake-before-reverse latch, then
+ 1.2 s reverse with a one-waypoint route rewind) so a wedged car
+ backs out instead of beaching forever, a grazing-hit filter in the
+ whisker loop (hits whose normal is near-perpendicular to the ray
+ — an adjacent bay's flank — no longer clamp to a stop) and a
+ final-approach creep floor so the last waypoint is always driven,
+ not crept to a stall. Leg routes leaving a bay slot insert a
+ straight-out spine waypoint (`bayExitSpineDist` 8 m) so
+ pure-pursuit exits the bay dead straight, and every drive leg
+ inserts a park-APPROACH spine (`parkApproachSpineDist` 5 m, on
+ the approach side of the pose) so the final segment runs ALONG
+ the slot's nose axis — without it a diagonal final segment stops
+ at the AI's 2.5 m arriveRadius with the error landing LATERALLY
+ (outside the alight gate), which is exactly why the parallel
+ roadside spots initially never got reached. Raised pads whose
+ driveway reach is deep enough (apronReach >= 2.6 m) emit one
+ overflow POI ("`` parking-ovf", nose along the street)
+ picked after the free bays and before the curb chain — the
+ between-road-and-house fallback, never stacked and never in front
+ of the door.
+- Narrow spots (user-directed rules): a parking rect that cannot
+ host a clean frustum (road at lot level, terrain poking through,
+ too big a drop, or a high platform that would bury the exit) gets
+ NO geometry and parks parallel at the roadside. Parking spots
+ near a house's side walls are placed when the lot's space permits;
+ layouts with neither the room for a driveway nor a side-wall strip
+ are marked as a PLAN PROBLEM / TODO for the later, more general
+ placement system (the corridor/pruning rules of 6.6.2 already keep
+ such lots car-free rather than blocking exits).
+- Verified: `TERRAIN_TEST_FILTER=townParkingPads` (sculpted fixture:
+ the rect beside the house, the road 1 m above the local terrain —
+ at least one raised frustum MUST fit; raised pads = exactly one
+ frustum entity with a mesh collider whose node sits at `topY`,
+ flat pads build no geometry, the terrain bit-identical under the
+ pads), the full terrain suite PASS, `road_geometry_overlap_test`
+ (raised-town bay contract + wide-rect corridor pruning + the new
+ at-grade PARALLEL-spot contract: one curb-line spot per building,
+ nose along the street, deterministic), and the whole demo battery
+ including `--test-parking` and `--test-traffic-schedule` PASS.
+
+**Task 6.6.7 — Determinism rules (cross-cutting) — DONE.**
+
+- What: every new decision stays a clock- and RNG-free pure function:
+ slots = f(rect, frontage axes, bay dims); assignment = f(slot
+ names, live occupancy, fixed order); curb chain = f(name hash, edge
+ geometry). Slot names derive per address, so editing one lot never
+ shifts another lot's slots (household id band principle,
+ `popSeed ^ fnv1a(address)`). Occupancy is the ONLY runtime state.
+- Done when: `--test-determinism` stays bit-exact across a regen with
+ one lot's parking layout edited.
+- Verified: pure town/pad regeneration is identical, the parking
+ probe's revisited (phase, position, area, slot, chain) snapshots are
+ bit-exact (including backward clock scrubs), and the existing
+ `--test-determinism` / `--test-traffic-schedule` probes stay green.
+
+**Task 6.6.8 — Tests & probes — DONE.**
+
+- Pure: road_geometry_overlap_test gains slot-enumeration, corridor
+ and curb-chain probes (6.6.1 / 6.6.2 / 6.6.4), plus pad metadata,
+ determinism and pinned-frontage projection checks (6.6.6).
+- Runtime: new `demoTaxi --headless --test-parking` — occupancy
+ exclusivity over a scripted day (incl. despawn and carjack), no
+ mid-lane drops, spot->road reachability, bit-exact determinism
+ snapshot. `--test-traffic-schedule` parked-at assertions switch to
+ slot names; the F7 overlay's per-car section shows slot +
+ occupancy + fallback state.
+- Verified: `road_geometry_overlap_test` exits 0; `--test-parking`
+ PASS (18 cars, exclusive claims over 64 day probes, three exact
+ re-probes, forced high-detail despawn/respawn with an unchanged
+ claim, park-box + mid-lane checks, carjack releases the fleet
+ record); `--test-traffic-schedule` validates the final slot name
+ against the area `LocationSet`. `TrafficInfo` exposes area, chain,
+ watchdog and fallback fields, and F7 shows them.
+
+#### Delivery order
+
+1. 6.6.1 -> 6.6.2 -> 6.6.4 — pure town model; nothing behavioral.
+2. 6.6.3 + 6.6.5 together — one coherent traffic-behavior change.
+3. 6.6.6 — helper pad geometry, after the roads comply (editor-side;
+ the runtime never touches terrain).
+4. Tests land per task; `--test-parking` is the acceptance gate.
+ 6.6.4 and the curbside half of 6.6.5 change curb poses globally —
+ re-run `--test-traffic-schedule`, `--test-taxi` and
+ `--test-vehicle-drive` after each.
+
+### Phase 6.7 — Animation state machine travel
+
+**Status: NOT STARTED (design decomposed 2026-10-28).** Scope: the
+animation tree (registry, runtime, editor) and every controller that
+switches states. Robustness and tests are explicit requirements, not
+afterthoughts.
+
+#### The idea (plain words)
+
+Today a state change is a hard switch: `setState()` cross-fades
+immediately to whatever state the controller names, mid-animation if
+needed. **Travel** is a *guided* change: the tree walks from the
+current state to the requested one along declared **connect links**,
+playing each intermediate state to the END of its animation before
+moving on. `idle -> walking -> running` stops being a pop and becomes
+a smooth accelerate/decelerate chain, which is what kills the visible
+feet sliding.
+
+#### Vocabulary
+
+- **State** — a named child of a `stateMachine` node (existing).
+- **Connect link** — a NEW directed `A -> B` declaration on a
+ `stateMachine` node meaning "B may follow A". Sibling of the
+ existing `endTransitions` map (which stays: it is the automatic
+ "when A ends, go to B" rule).
+- **Travel** — a NEW request `travelTo(e, sm, target)`: follow the
+ shortest connect path current -> target, one state at a time.
+- **Start state / end state** — NEW optional markers on the children
+ of a nested `stateMachine` node (a state machine used as a state of
+ an upper machine).
+
+#### Semantics (the contract, in order)
+
+1. `travelTo(e, sm, target)` computes the shortest connect path from
+ the current state to `target` (BFS; ties broken by state NAME, so
+ results are deterministic).
+2. **No path = old behavior.** If no connect path exists (no links,
+ target unreachable, current state unknown), travel degrades to the
+ plain `setState()` switch. Never an error, never a hang.
+3. Each hop completes the CURRENT state's animation before the next
+ state starts: a one-shot clip advances at its end; a LOOPING clip
+ advances at the loop boundary (end of the current iteration), never
+ mid-cycle. The hop itself is the usual cross-fade.
+4. A new `travelTo` while a travel is running REPLACES the pending
+ path: the in-flight hop finishes, then the machine travels from
+ there to the new target (recomputed). No queue of stale commands.
+5. Arrival = the last state is reached and fading in. If that state
+ loops, it simply keeps looping; travel never force-stops it.
+6. Nested machines: when the upper machine ACTIVATES a nested
+ `stateMachine` node, the nested machine enters its **start state**
+ (only if it was not already active). When the upper machine wants
+ to leave that node, the nested machine first TRAVELS to its **end
+ state**; only when that travel completes does the node deactivate
+ (fade out). With no end state marked, deactivation is immediate
+ (today's behavior).
+7. Robustness: deleted states/links, a missing target, a disabled
+ tree or a despawned entity cancel the travel and fall back to the
+ plain switch (one log line per entity, not per frame). Pending
+ travel is runtime-only: scene load / spawn / teleport re-issues
+ from the controller's logical state, nothing is serialized.
+
+#### Why: what exists today (verified symbols)
+
+- Node model: `AnimationTreeNode` (components/AnimationTree.hpp) with
+ types `output` / `stateMachine` / `state` / `speed` / `animation`,
+ `endTransitions` on `stateMachine`.
+- Runtime: `AnimationTreeSystem::setState(e, sm, state, reset)`
+ cross-fades immediately; `checkEndTransitions()` fires the automatic
+ transition at 99% of the clip (AnimationTreeSystem.cpp:739-818).
+- Definitions live in `AnimationTreeRegistry` (animation_tree.json),
+ edited in Tools -> Animation Tree Registry
+ (ui/AnimationTreeRegistryEditor.cpp).
+- Hard-switch call sites to migrate:
+ `PlayerControllerSystem.cpp:615` (idle/walking/running/swimming via
+ `pc.locomotionStateMachine`),
+ `VehicleControllerSystem.cpp:257,524`,
+ `NpcTrafficSystem.cpp:1110,1202,1978` (`locomotion: driving/idle`),
+ `NpcTaxiSystem.cpp:520,634`,
+ `SmartObjectSystem.cpp:151,160`,
+ `BehaviorTreeSystem.cpp:449`,
+ `PathFollowingSystem.cpp:146`.
+
+#### Tasks
+
+**Task 6.7.1 — Connect links & markers (data model + editor).**
+
+- Add `connectLinks` (directed pairs) and the optional `startState` /
+ `endState` markers to `AnimationTreeNode`; serialize them in
+ animation_tree.json with backward-compatible loading (old files keep
+ working, empty links = switch-only).
+- Registry editor: create/remove links between states of one machine,
+ mark start/end on nested machines; validate on save (no dangling
+ state names, warn on unreachable states, no duplicate links).
+- Done when: a hand-built tree round-trips links+markers through
+ save/load, invalid entries are rejected with a clear message, and
+ the demo's existing trees load unchanged.
+
+**Task 6.7.2 — Travel runtime (`travelTo`).**
+
+- Implement path search + the per-entity/per-machine pending-travel
+ state and the hop advancement rules 1-5 above, next to `setState`
+ (which stays for hard resets). Reuse the existing cross-fade and
+ end-of-clip detection (share code with `checkEndTransitions`).
+- Done when: a headless probe walks A->B->C and asserts the order, the
+ end-of-clip timing (one-shot AND looping), the no-path fallback and
+ command replacement mid-travel; no per-frame allocations in the
+ steady state.
+
+**Task 6.7.3 — Nested start/end states.**
+
+- Implement activation/deactivation gating for nested machines
+ (rule 6), including "already active" re-entry and aborting a pending
+ exit when the node is reactivated.
+- Done when: the probe shows enter->start, exit->travel-to-end->fade-
+ out, reactivation during the exit aborting the exit, and immediate
+ deactivation when no end state is marked.
+
+**Task 6.7.4 — Controller migration.**
+
+- Switch the call sites listed above to `travelTo` for LOCOMOTION
+ (idle/walk/run/swim), vehicle enter/exit and smart-object actions;
+ keep plain `setState` for spawn/teleport/scene-load resets.
+- Give the stock locomotion tree sensible connect links
+ (idle<->walking<->running, plus the swim chain) so migration is
+ visible on day one.
+- Done when: the full demo battery passes unchanged and a visual
+ walk/run/stop cycle shows no state popping (screenshot/video check).
+
+**Task 6.7.5 — Test harness & regression coverage.**
+
+- Pure: path search (shortest, ties, unreachable, cycles), JSON round-
+ trip, editor validation.
+- Runtime: travel timing, nested start/end, controller-level
+ enter/exit-vehicle travel; register the probes in the existing test
+ executables (no new framework).
+- Done when: the new probes plus the terrain suite,
+ road_geometry_overlap_test and all demoTaxi tests exit 0, and the
+ docs (this file + editScene/AGENTS.md) describe travel semantics.
+
+#### Delivery order
+
+1. 6.7.1 data model (runtime can already read links).
+2. 6.7.2 travel core + probes.
+3. 6.7.3 nested start/end + probes.
+4. 6.7.4 controller migration, one system at a time, re-running the
+ demo battery after each.
+5. 6.7.5 final coverage + docs.
+
+#### Explicit non-goals (for later phases)
+
+- Foot IK / ground alignment, root-motion warping, animation blending
+ beyond the existing cross-fade, and any AI/schedule behavior change:
+ travel is an ANIMATION-layer feature only.
+
+### Phase 6.8 - DSL, schedule and POIs.
- it should be possible to declare macro blocks in DSL similar to Lisp macros to reuse parametrized DSL fragments in many places.
-- this is relevant to exterior scene
+ - Documentation and help window should be updated with new syntax.
+- exterior scene:
- per-NPC schedule uses symbolic names, like home, job, leisure, etc.
+ - per-NPC schedule uses blocks of 1 in-game hour to set schedule, not random time pereiods. The scheduler should try to do realistic schedules and prefer shorter distances between locations, but not overuse same locations making some of them crowded and some empty.
+ - per NPC daily schedule should start and end at home if no special scripted circumstances occur. In later case the script creates the schedule for the NPC until gives up back to the system.
+ - these symbolic names are resolved to actual POIs at schedule build and update time, symbolic name should not be forgotten, but association of symbolic names with POIs is kept until re-schedule occurs.
+ - the schedule is built for in-game week
+ - once out of schedule, re-scheduling occurs.
- POIs should count their usage in the schedules of NPCs
- if POI is not home, when schedule is built, prefer POIs with less usage and less distance to NPC "home". That serves a purpose of making the town more interesting and spreading NPCs around town.
- POIs are declared with tags with DSL or node set up for interiors and node-based for exterior. Should easily handle large number of POIs in editor UX and performance.
+ - For job POIs, the specification of the job in tags is kept, so when re-scheduling, they keep their "profeession" but job location can be different.
+ - Currently NPCs do not prefer any job type or leisure type, but that will be added at one point. However they prefer to keep the same job type, using the same tags in search as last one.
- symbolic names from schedule are searched as POI tags
- home is declared as house where NPC sleeps in and starts game in, should be set from the start for that NPC.
- all other symbolic names are searched among POIs as tags and best ones are selected.
@@ -3883,28 +4385,8 @@ implementation — the DSL side especially needs thought):
- for interior scene
- POIS declared in DSL can fully declare behavior tree for action and action itself. that we can declare behavior tree, action, actuator/POI in DSL the same way as with nodes/components directly, completely with GOAP/HTN data. Lua events and calls should be easily supported too. All these blocks will be heavily reused from lots of places, so it should be easy to reference them from other parts of DSL.
- for both interior and exterior - POIs which are have job tag, can also have job specification tags, like "shop, salesman", or "office, secretary" and these should be preferred when looking for other job. Also special kinds of job like "police", "ambulance",... should make NPCs listed there into special lists used by special game systems implemented later.
-
-### Phase 6.8 - Initial game story fragment
-
-The purpose of this phase is to check we have enough narration tools to implement fluent short story fragment.
-This phase should be moved to a position where we think we have enough narration tools to implement this.
-- First we need to point-out missing modelling/animation pieces.
-- Second we need to check and finish our dialogue system based on ImGui as usual and Ink interpreter based on Lua (src/features/editScene/lua-scripts/narrator)
-- Third we need to create and polish a framework to create stories and narrative missions like that without major development work with minimal efforts to keep ourselves productive. Probably another DSL on top of Lua or Ink-based narrative solution, which should be very clear to use and implement, without writing tons of scripts, C++ code and .ink files everywhere.
-- Note that not whole narration is text, often it won't start with dialogue option or actuator, it might be time event or occurance at special place or near NPC matching some parameters..
-pd
-- The story: player character wakes up in startup exterior scene. He gets his own thought message that he needs to visit student dormitory to put out ad about rent for bed vacancy for a girl as he has heavy comfort issues he can't solve himself buf is fully capable of paying the rent. He arrives to the place and sees how one girl is rudely evicted from the dormitory by three other girls. The player character protects that girl or confronts her and brings her home:
- - alternatives:
- - she agrees to go with player character in the car as passenger and enters his initial house as permanent resident.
- - the player character forces her into the car trunk and brings her home, which she enters as permanent resident.
- - the alternative defines the outcome of initial relationship
- - after the above events girl serves game sandbox functions:
- - player character can call her from stationary phone locations (will be added) to ask her to deliver him a car
- - getting back home and talking to the girl will restore character health (will be added as system)
- - talking to the girl will level up player character if enough XP is generated (develop as level up and stats will be added)
- - the player character can ask girl to prepare food for him.
- - special taxi missions depending on game events.
- - options unlock in relationship progress of the girl to the player character.
+- for quicker search for POIs interior POIs should be indexed per building/per lot to be found quicker.
+- something for research: it would be nice if i.e. shop employees would enter back door, but shop cutomers will enter front door; the POI for employee would be "shop, salesman" and for customer "shop, shopping". Some simple idea is needed...
### Phase 7 — CellGrid stair generation - before interiors
@@ -3960,6 +4442,13 @@ requirements moved from Phase 5.9). Builds on the 4.7
population/schedule systems, the 5.9 layout metadata (tags/score) and
the Phase 6.5/8 furniture/interior work — layouts already carry
`:type`-tagged rooms for this.
+- this needs to be cleaned up and reformulated, need more ideas, and now it is a mess.
+- the residents specoified in DSL are initial ones for new game. They should be stored and loaded in game saves. People should be able to die, give birth, transition from baby to a child, from child to teenager, from teenager to adult, from adult to elder.
+- for cultural/censorship reasons it should be possible to prevent babies, children and teenagers from spawning, with them still simulated but assumed-only. However whole mechanics should remain working.
+- the whole relationship thing should be implemented in Lua completely and behaviors should query relationship system with dedicated queries, when game AI needs to decide which action to perform.
+- each NPC has associated GOAP action/HTN task which affects NPC states, groups, etc. These should be created in Lua script for each NPC.
+- stats for each NPCs also should be created by Lua script.
+- all these numbers and parameters should be persistently stored in save games.
- **Population control at three levels**: town, area and layout must
each be able to define and control the population in numbers
@@ -4046,6 +4535,29 @@ the Phase 6.5/8 furniture/interior work — layouts already carry
with distance, a timeout resets the chain to fare 1, completion
events fire, HUD state updates.
+### Phase 10.1 - Initial game story fragment
+
+The purpose of this phase is to check we have enough narration tools to implement fluent short story fragment.
+This phase should be moved to a position where we think we have enough narration tools to implement this.
+- First we need to point-out missing modelling/animation pieces.
+- Second we need to check and finish our dialogue system based on ImGui as usual and Ink interpreter based on Lua (src/features/editScene/lua-scripts/narrator)
+- Third we need to create and polish a framework to create stories and narrative missions like that without major development work with minimal efforts to keep ourselves productive. Probably another DSL on top of Lua or Ink-based narrative solution, which should be very clear to use and implement, without writing tons of scripts, C++ code and .ink files everywhere.
+- Note that not whole narration is text, often it won't start with dialogue option or actuator, it might be time event or occurance at special place or near NPC matching some parameters..
+pd
+- The story: player character wakes up in startup exterior scene. He gets his own thought message that he needs to visit student dormitory to put out ad about rent for bed vacancy for a girl as he has heavy comfort issues he can't solve himself buf is fully capable of paying the rent. He arrives to the place and sees how one girl is rudely evicted from the dormitory by three other girls. The player character protects that girl or confronts her and brings her home:
+ - alternatives:
+ - she agrees to go with player character in the car as passenger and enters his initial house as permanent resident.
+ - the player character forces her into the car trunk and brings her home, which she enters as permanent resident.
+ - the alternative defines the outcome of initial relationship
+ - after the above events girl serves game sandbox functions:
+ - player character can call her from stationary phone locations (will be added) to ask her to deliver him a car
+ - getting back home and talking to the girl will restore character health (will be added as system)
+ - talking to the girl will level up player character if enough XP is generated (develop as level up and stats will be added)
+ - the player character can ask girl to prepare food for him.
+ - special taxi missions depending on game events.
+ - options unlock in relationship progress of the girl to the player character.
+
+
### Phase 10.5 — NPC looks: all ages, retargeted animations, more clothes
Today every townsperson renders with adult catalog parts
diff --git a/src/features/editScene/demos/demo-taxi/demo_main.cpp b/src/features/editScene/demos/demo-taxi/demo_main.cpp
index 12c6499..a4761de 100644
--- a/src/features/editScene/demos/demo-taxi/demo_main.cpp
+++ b/src/features/editScene/demos/demo-taxi/demo_main.cpp
@@ -2,6 +2,7 @@
#include
#include
#include "EditorApp.hpp"
+#include "GameMode.hpp"
#include "ProjectConfig.hpp"
#include "camera/EditorCamera.hpp"
#include "systems/CharacterRegistry.hpp"
@@ -12,7 +13,9 @@
#include "systems/VehicleSystem.hpp"
#include "systems/CharacterSystem.hpp"
#include "systems/PhysicsSystem.hpp"
+#include "physics/physics.h"
#include "systems/TerrainSystem.hpp"
+#include "systems/TownGenSystem.hpp"
#include "systems/RoadSystem.hpp"
#include "systems/RenderOriginSystem.hpp"
#include "systems/EventBus.hpp"
@@ -47,6 +50,9 @@
#include