diff --git a/assets/blender/buildings/parts/FURNITURE_MODELING.md b/assets/blender/buildings/parts/FURNITURE_MODELING.md index b367a63..eabe7df 100644 --- a/assets/blender/buildings/parts/FURNITURE_MODELING.md +++ b/assets/blender/buildings/parts/FURNITURE_MODELING.md @@ -148,8 +148,9 @@ current definition sets are loadable. At runtime `FurnitureLibrary` discovers `furniture-*.json`, validates schema/identity/bounds and exposes stable ids and aliases. -To add the phase 6.5 layer fixtures to the existing source (idempotent), -then export and render the preview gallery: +To add the phase 6.5 fixtures (layer fixes + the 6.5.I store/garage +set) to the existing source (idempotent), then export and render the +preview gallery: ```bash ../../blender-bin/bin/blender -b assets/blender/buildings/parts/furniture.blend \ @@ -165,6 +166,30 @@ and an atlas UV rectangle. `render_furniture_gallery.py` and `generate_furniture_gallery_index.py` are the generators. The gallery target is opt-in so routine builds do not render every asset. +### The fixture script (`add_furniture_fixtures.py`) + +`add_furniture_fixtures.py` extends `furniture.blend` in place and is +safe to re-run (every root is created only when missing, duplicate +markers are never added again): + +- Layer fixtures: `carpet1` (`thin-floor`), `switch1` and `picture1` + (`thin-wall`), `ceiling-light1` (`ceiling`) and `shop-shelf1` + (`normal`, with a simplified `collision-shelf-proxy`). +- Store/garage fixtures (6.5.I): `counter1` (shop/counter/essential, + `work` attachment marker for the demo's working-counter action), + `workbench1` (garage/working) and `bay1` (vehicle-sized repair bay, + `bay` marker plus visual stripes). +- `ensure_attachment(root)` gives every fixture root one root-local + `position-attach` marker (the guard matches the whole + `position-attach` prefix — Blender renames colliding objects with + numeric suffixes, so an exact-name check would re-create a second + marker whenever a renamed marker survives; the demo's + `demo_scene_furniture.json` POI test depends on one marker per root). + +The phase 6.5.I demo scene and its headless check live in +`src/features/editScene/demos/demo-taxi/` (`demo_scene_furniture.json`, +`./demoTaxi --headless --test-furniture`). + ## Choosing the authoring layer | Need | Preferred home | diff --git a/assets/blender/buildings/parts/add_furniture_fixtures.py b/assets/blender/buildings/parts/add_furniture_fixtures.py index f77ada2..29c8332 100644 --- a/assets/blender/buildings/parts/add_furniture_fixtures.py +++ b/assets/blender/buildings/parts/add_furniture_fixtures.py @@ -62,8 +62,15 @@ def box(root, name, location, dimensions, mat, collision=False): def ensure_attachment(root): - """A root-local marker used by the runtime layer-transform checks.""" - if any(child.name == "position-attach" for child in root.children): + """A root-local marker used by the runtime layer-transform checks. + + The guard matches the whole 'position-attach' prefix: Blender + renames colliding objects with numeric suffixes, so an exact-name + check would re-create a second marker whenever a deduped or + renamed marker survives in the blend. + """ + if any(child.name.startswith("position-attach") + for child in root.children): return marker = bpy.data.objects.new("position-attach", None) marker.empty_display_size = 0.05 @@ -128,5 +135,50 @@ if created: (1.5, 0.4, 1.5), None, collision=True) root["collision_mesh"] = collision.name +root, created = make_root("furniture-counter1", "shop, counter, essential", + "normal", True, False) +if created: + box(root, "CounterTop", (0, 0, 0.95), (1.8, 0.6, 0.05), wood) + for z in (-0.3, 0.3): + box(root, "CounterLeg", (z, 0, 0.47), (0.06, 0.65, 0.95), wood) + collision = box(root, "collision-counter", (0, 0, 0.5), + (1.8, 0.6, 1.0), None, collision=True) + root["collision_mesh"] = collision.name + ensure_attachment(root) + work_mark = bpy.data.objects.new("position-work", None) + work_mark.empty_display_size = 0.05 + bpy.context.scene.collection.objects.link(work_mark) + work_mark.parent = root + work_mark.location = (0, -0.4, 1.0) + work_mark["name"] = "work" + work_mark["type"] = "attachment" + +root, created = make_root("furniture-workbench1", + "garage, working, optional", + "normal", True, False) +if created: + box(root, "BenchTop", (0, 0, 0.9), (4.2, 0.8, 0.06), wood) + for x in (-1.9, 0.0, 1.9): + box(root, "BenchLeg", (x, 0, 0.44), (0.08, 0.7, 0.9), wood) + box(root, "BenchVise", (1.4, 0, 0.78), (0.25, 0.3, 0.25), trim) + collision = box(root, "collision-workbench", (0, 0, 0.5), + (4.2, 0.8, 1.0), None, collision=True) + root["collision_mesh"] = collision.name + +root, created = make_root("furniture-bay1", + "garage, bay, optional", + "thin-floor", True, False) +if created: + box(root, "BayPad", (0, 0, 0.01), (5.2, 2.2, 0.02), accent) + box(root, "BayStripe", (0, 0, 0.011), (4.7, 1.6, 0.004), picture) + ensure_attachment(root) + bay_mark = bpy.data.objects.new("position-bay", None) + bay_mark.empty_display_size = 0.05 + bpy.context.scene.collection.objects.link(bay_mark) + bay_mark.parent = root + bay_mark.location = (0, 0, 0.02) + bay_mark["name"] = "bay" + bay_mark["type"] = "attachment" + bpy.ops.wm.save_as_mainfile(filepath=bpy.data.filepath) print("Phase 6.5 furniture fixtures present.") diff --git a/assets/blender/buildings/parts/furniture.blend b/assets/blender/buildings/parts/furniture.blend index 89aca23..ae8024a 100644 --- a/assets/blender/buildings/parts/furniture.blend +++ b/assets/blender/buildings/parts/furniture.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:566aee1a70955861de7848bdb20e63fe9c1290235a9f1b911e2c48a2bed161b3 -size 2254581 +oid sha256:7d58cd17f72af742d9df89948d17a14df3eb95fbcb997a3d76f042334746136f +size 2245024 diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md index f9f1423..c92603d 100644 --- a/src/features/editScene/AGENTS.md +++ b/src/features/editScene/AGENTS.md @@ -293,6 +293,15 @@ cd demos/demo-taxi # the interior scene and come back identical (ids + attributes) after # the exterior reload's clear + deterministic regeneration: ./demoTaxi --headless --test-population-switch +# ...or headless phase 6.5.I furniture check: loads the furnished scene +# (demo_scene_furniture.json, staged as a symlink) and asserts the +# apartment/store compositions committed (5 + 5 placements), the +# authored garage furnitureCells survive, the store-counter/garage-bay +# attachment POIs were reconciled into actuators by the player's scene +# script, the sedan parks inside the bay fixture footprint and both +# work_counter/repair_vehicle hooks latch through +# ActuatorSystem::executeAction (the headless E-press entry point): +./demoTaxi --headless --test-furniture # ...or headless check of the NPC motion core (phase 4.7.5): sweeps the # game clock over a full day at a slowed walk speed, asserting NPCs enter # IN_TRANSIT, that positions move across the day and that re-visiting an @@ -1934,6 +1943,12 @@ block still run. While the player is driving a vehicle (`PlayerControllerComponent::driving`), prompt collection is skipped entirely — E then means "exit the vehicle" (handled by `VehicleControllerSystem`). +`executeAction(character, actuatorEntity, actionName)` is public: it is the +exact path an E press takes (latch the executing state, lock input, then tick +`isActionComplete` from `update()` until the action's behavior tree finishes +and the cooldown starts), and the executing block runs before the ImGui gate, +so headless tests drive actuators through it (demo-taxi `--test-furniture`). + ### DoorSystem & CellGrid doors `CellGridSystem::buildDoorEntities()` spawns one runtime door entity per @@ -2061,8 +2076,13 @@ one shared `furniture-atlas.png`/`M_Furniture_Atlas` material, and plus schema-valid JSON with mesh bounds and action/position marker metadata; `validate_furniture_export.py` then checks UV bands and shared mesh variants in the build. `add_furniture_fixtures.py` adds the layer -fixtures (carpet, switch, picture, ceiling light, shop shelf) to the -source blend; `furniture_gallery` renders per-asset pivot/bounds/ +fixtures (carpet, switch, picture, ceiling light, shop shelf) plus the +phase 6.5.I store/garage fixtures (counter with a `work` attachment +marker, workbench, vehicle-sized `bay` fixture) to the source blend; +every fixture root keeps exactly one `position-attach` marker (the +script's `ensure_attachment` matches the whole prefix, so Blender's +numeric-collision renames cannot create duplicates). `furniture_gallery` +renders per-asset pivot/bounds/ collider/occupancy/POI previews and an HTML index with atlas UV regions into `resources/buildings/parts/gallery/index.html`. Authoring conventions live in @@ -2161,6 +2181,60 @@ are the editor-authored prefab example. Coverage: `TERRAIN_TEST_FILTER=furniturePoiQuery` (mesh + prefab markers, stable ids/transforms across rebuilds). +Lua hooks (6.5.H): `lua/LuaFurnitureApi.cpp` exposes `ecs.furniture.*` +(`lua-examples/furniture_layout_example.lua`, +`furniture_action_binding_example.lua`). `definitions()` lists the +library; `can_place(gridUid, request, roomId)` / `commit(gridUid, +requests, roomId)` / `remove(gridUid, placementId)` drive the shared +`FurniturePlacement` kernel from script — a request's `placementId` +field is the stable update handle (re-committing the same id updates +the placement instead of failing on its own overlap; commit is +all-or-nothing). `query_pois({gridUid|roomId|furnitureTag|actionName| +type})` / `find_poi(id)` read the stable POI records. The ordered room +events `on_room_before`/`on_room_after` (refs for +`off_room_before`/`off_room_after`) wrap `RoomLayoutSystem`'s default +evaluator for every reprocessed room: a before handler receives a +mutable context table (`gridUid`/`roomId`/`roomType`/`tags`/`seed`/ +`suppress_default`/`placements`) whose in-place edits are read back as +one atomic transaction — any rejection rolls the transaction back and +falls back to the default evaluator (a bad script cannot strand a room +empty); `suppress_default = true` skips the default evaluator for that +room. Script-owned placements survive room regeneration; common +reusable arrangements stay DSL/composition work — Lua is for unique +shops/garages and behavior (see the decision matrix below). + +Worked example (6.5.I): `demos/demo-taxi/demo_scene_furniture.json` — +one scene with an apartment CellGrid (tag-walk placements + a +`(group ...)` composition + a `zneg`-anchored `(row ...)`), a store +(shelf `row` + counter `group` on the `zpos` edge) and a garage +(authored `workbench1`/`bay1` furnitureCells). An inline scene script +on the `player` entity reconciles on a throttled `frame` tick +(POI entities are runtime-only and are regenerated on every grid +rebuild): it converts the exported plain attachment markers (store +counter `work`, garage `bay`) into `ActuatorComponent`s bound to +`work_counter`/`repair_vehicle` luaTask actions, which latch +`hooks.work_counter`/`hooks.repair_vehicle` ints in the global store. +`./demoTaxi --headless --test-furniture` verifies the committed +placement counts, the authored garage cells, the POI→actuator wiring, +the sedan parking inside the bay fixture footprint and both hooks +latching through `ActuatorSystem::executeAction` (the E-press entry +point; the executing block in `ActuatorSystem::update` runs before the +ImGui gate, so it is headless-safe). + +Authoring decision matrix (Blender → placement kernel → +prefab/DSL/Lua): + +| Concern | Home | +| --- | --- | +| Mesh, atlas, pivot, bounds, collision proxy | Blender root properties (`FURNITURE_MODELING.md`) | +| One-off local facts ("sit here", "stand at the counter") | Blender `action-*`/`position-*` markers → runtime POIs | +| Cell occupancy, overlap, rotation, groups/rows, identity | Shared `FurniturePlacement` kernel + `FurnitureLibrary` | +| Reusable arrangements (table + chairs, shelf rows) | Layout DSL `:compose` groups/rows or RoomComponent compositions | +| Room-wide tag selection | DSL `:furniture` terms / room tag lists (AND/OR/NOT terms) | +| Rich multi-entity behavior | Editor prefab (Furniture prefab mode) + companion definition | +| Unique interiors, scripted behavior, POI→action binding | `ecs.furniture.*` + scene scripts (Lua) | +| Invariants (determinism, rebuild stability, POI ids, nav/colliders) | C++ (`CellGridSystem`, `RoomLayoutSystem`, terrain tests) | + #### Generation mode (F4/F5) `CellGridComponent::generationMode` (string: `"full"` default, diff --git a/src/features/editScene/demos/demo-taxi/PLAN.md b/src/features/editScene/demos/demo-taxi/PLAN.md index 4efebcb..10f474e 100644 --- a/src/features/editScene/demos/demo-taxi/PLAN.md +++ b/src/features/editScene/demos/demo-taxi/PLAN.md @@ -3510,6 +3510,58 @@ Atlas follow-up: variants sharing a Blender mesh datablock now remap UVs once per datablock (fixes round-table/pole colors), and `validate_furniture_export.py` runs in the build to verify shared-material GLBs, band-local UVs and identical UV bounds across variants. +6.5.H is complete: `lua/LuaFurnitureApi.cpp` exposes `ecs.furniture` from +script — `definitions()` lists the library, `can_place`/`commit`/`remove` +drive the shared `FurniturePlacement` kernel (script-owned placements +survive room regeneration; a request's `placementId` is the stable update +handle and commit is all-or-nothing), `query_pois`/`find_poi` read the +stable POI records, and the ordered room events +`on_room_before`/`on_room_after` (refs for `off_room_before`/ +`off_room_after`) wrap `RoomLayoutSystem`'s default evaluator for every +reprocessed room. A before handler mutates a context table in place; +its edits return as one atomic transaction, a rejection (or a handler +error, logged with the room id) rolls the transaction back and falls +back to the default evaluator — a bad script cannot strand a room empty +— and `suppress_default = true` removes the default evaluation for +fully-scripted rooms; after handlers observe the final plan. +Examples: `lua-examples/furniture_layout_example.lua` (definitions, +placement transactions, events, POI queries) and +`lua-examples/furniture_action_binding_example.lua` (the compact garage +stable-id POI-to-actuator binding). Headless coverage is the +"Lua furniture room events, fallback and transactions" case of +`cellgrid_door_test` (event order, suppression, valid/invalid edits, +transaction rollback/fallback, determinism and serialization). +6.5.I is complete: the Blender fixture set gained the store/garage +assets (counter1 with an exported `work` attachment marker, workbench1, +vehicle-sized bay1 with its `bay` marker) and exactly one +`position-attach` marker per fixture root (`ensure_attachment` matches +the whole `position-attach` prefix, so Blender's numeric rename +suffixes can never duplicate markers). The worked scene +`demos/demo-taxi/demo_scene_furniture.json` furnishes an apartment +(tag walk + a centered table multi-cell group + a wall row), a store +(shelf row + counter group with aisle clearance, `work` marker) and a +garage (authored `workbench1`/`bay1` furnitureCells); an inline scene +script on the player entity reconciles on a throttled `frame` tick, +converting the attachment POIs into actuators bound to +`work_counter`/`repair_vehicle` luaTask actions that latch +`hooks.work_counter`/`hooks.repair_vehicle` ints in the global store. +`demoTaxi --headless --test-furniture` proves the committed placement +counts (5 apartment + 5 store), the authored garage cells, the +POI-to-actuator wiring, the sedan parking inside the bay footprint and +both hooks latching through `ActuatorSystem::executeAction` — now +public, the exact path an E press takes (the executing block in +`ActuatorSystem::update` runs before the ImGui gate, so it works +headless). The full battery passes: `furniture_test` 13/13, +`cellgrid_door_test` 15/15 (incl. the Lua room events), +`component_lua_test` 66/66, `road_geometry_overlap_test`, the terrain +tests (`navMeshFurniture`, `furniturePoiQuery`, `furnitureLayers`, +`townPlanRooms`, `townCompositions`), `--test-switch` and +`--test-population-switch`. Docs/examples stay in sync: AGENTS.md +gained the `--test-furniture` battery entry, the `ecs.furniture` API +notes, the ActuatorSystem `executeAction` note and the Blender -> +kernel -> prefab/DSL/Lua authoring decision matrix; +`FURNITURE_MODELING.md` documents the fixture script and points at the +demo. **6.5.A — Canonical definition schema and loader hardening:** @@ -3815,6 +3867,42 @@ implementation — the DSL side especially needs thought): placement must be at the road side, oriented along the edge, out of the lane, and door-clear. +### Phase 6.7 - 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 + - schedule uses symbolic names, like home, job, leisure, etc. + - 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. + - 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. + - all the symbolic names are resolved per-NPC. They change very rarely, but still can change. + - schedule stores symbolic names/tags for locations but caches NPC-specific name resolutions. +- 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. +- 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 7 — CellGrid stair generation - before interiors Multi-storey CellGrid buildings (`height` > 1) generate stacked floors diff --git a/src/features/editScene/demos/demo-taxi/demo_main.cpp b/src/features/editScene/demos/demo-taxi/demo_main.cpp index 92e3265..614bf6c 100644 --- a/src/features/editScene/demos/demo-taxi/demo_main.cpp +++ b/src/features/editScene/demos/demo-taxi/demo_main.cpp @@ -17,6 +17,10 @@ #include "systems/RenderOriginSystem.hpp" #include "systems/EventBus.hpp" #include "systems/GlobalStateStore.hpp" +#include "systems/CellGridSystem.hpp" +#include "systems/ActuatorSystem.hpp" +#include "components/CellGrid.hpp" +#include "components/Actuator.hpp" #include "systems/NpcPopulationSystem.hpp" #include "systems/NpcMotionSystem.hpp" #include "systems/NpcDetailSystem.hpp" @@ -5055,6 +5059,403 @@ struct TrafficScheduleTestListener : public Ogre::FrameListener { } }; +/* + * --test-furniture (phase 6.5.I): the furnished demo scene + * (demo_scene_furniture.json) end-to-end. Three full-mode CellGrids: + * an apartment furnished purely by a room composition rule, a shop + * furnished by a row + a counter group, and a garage holding authored + * furnitureCells. Verifies: + * + * 1. the composition walk committed (5 apartment + 5 store + * placements) and the authored garage furnitureCells survived + * deserialization untouched (placed + stable placementIds), + * 2. furniture POIs exist: the store counter's "work" attachment + * marker and the garage bay's "bay" marker, queried through + * CellGridSystem::queryFurniturePois by grid + marker name, + * 3. the player's scene script registered the luaTask behavior tree + * nodes AND its throttled "frame" reconcile converted those POIs + * into actuators (ActuatorComponent with the matching action + * names) after the load-time POI (re)builds, + * 4. the sedan teleported onto the garage bay settles INSIDE the bay + * fixture's world footprint (thin floor adds no collider, so the + * car rests on the demo floor - parking must line up), and + * 5. executing the POI actuators' actions through + * ActuatorSystem::executeAction (the exact path an E press takes) + * latches hooks.work_counter / hooks.repair_vehicle in the + * global store through the registered luaTask nodes. + */ +struct FurnitureDemoTestListener : public Ogre::FrameListener { + EditorApp *app; + int frame = 0; + int phase = 0; + bool failed = false; + Ogre::String failReason; + + /* Captured at phase 1. */ + flecs::entity aptGrid; + flecs::entity storeGrid; + flecs::entity garageGrid; + flecs::entity workPoi; + flecs::entity bayPoi; + Ogre::Vector3 bayWorld = Ogre::Vector3::ZERO; + int aptPlacements = 0; + int storePlacements = 0; + int64_t workBaseline = 0; + int64_t repairBaseline = 0; + int settleFrames = 0; + int actionFrames = 0; + bool teleported = false; + bool workFired = false; + bool workLatched = false; + bool repairFired = false; + + FurnitureDemoTestListener(EditorApp *a) : app(a) + { + } + + bool fail(const Ogre::String &reason) + { + failed = true; + failReason = reason; + app->getRoot()->queueEndRendering(); + return true; + } + + flecs::entity findGrid(const char *name) + { + flecs::entity result = flecs::entity::null(); + app->getWorld() + ->query() + .each([&](flecs::entity e, EntityNameComponent &n, + CellGridComponent &) { + if (n.name == name) + result = e; + }); + return result; + } + + /* The furniture scene's parked sedan (VehicleComponent + + * EntityNameComponent), like findVehicle() in the drive tests. */ + flecs::entity findVehicle() + { + flecs::entity result = flecs::entity::null(); + app->getWorld() + ->query() + .each([&](flecs::entity e, EntityNameComponent &n, + VehicleComponent &) { + if (n.name == "sedan1") + result = e; + }); + return result; + } + + /** The world-space point the garage bay fixture centres on: + * the "bay" POI marker sits at the placement root (def local + * origin), transformed through the render origin like physics. */ + bool captureBayWorld() + { + if (!bayPoi.is_alive() || + !bayPoi.has()) + return false; + Ogre::Vector3 renderPos = + bayPoi.get().node + ->_getDerivedPosition(); + RenderOriginSystem *ro = RenderOriginSystem::getInstance(); + if (ro) { + JPH::DVec3 w = ro->renderToWorld(renderPos); + bayWorld = Ogre::Vector3((float)w.GetX(), + (float)w.GetY(), + (float)w.GetZ()); + } else { + bayWorld = renderPos; + } + return true; + } + + bool frameRenderingQueued(const Ogre::FrameEvent &) override + { + frame++; + if (failed || phase == 4) { + app->getRoot()->queueEndRendering(); + return true; + } + if (frame > 20000) + return fail("timeout in furniture test"); + + switch (phase) { + case 0: + /* Let the scene resolve, the three grids build + * (including the texture-ready rebuild that + * recreates the POI entities) and the scene + * script's throttled reconcile attach the + * actuators. */ + if (frame < 150) + break; + phase = 1; + break; + case 1: { + aptGrid = findGrid("furn-apt"); + storeGrid = findGrid("furn-store"); + garageGrid = findGrid("furn-garage"); + if (!aptGrid.is_alive() || !storeGrid.is_alive() || + !garageGrid.is_alive()) + return fail("furniture demo grids not found"); + const CellGridComponent &apt = + aptGrid.get(); + const CellGridComponent &store = + storeGrid.get(); + const CellGridComponent &garage = + garageGrid.get(); + if (apt.gridUid != "furn-apt" || + store.gridUid != "furn-store" || + garage.gridUid != "furn-garage") + return fail("grid uids not deserialized"); + + /* Composition commits: apartment 1 root + 4 + * children, store 4 shelves + 1 counter root. */ + aptPlacements = (int)apt.furnitureCells.size(); + storePlacements = (int)store.furnitureCells.size(); + if (aptPlacements != 5) + return fail( + "apartment compositions did not " + "commit 5 placements (got " + + std::to_string(aptPlacements) + ")"); + if (storePlacements != 5) { + std::string dump; + for (const auto &fc : store.furnitureCells) + dump += "(" + std::to_string(fc.x) + + "," + std::to_string(fc.z) + + ")=" + fc.furnitureType + " "; + return fail( + "store compositions did not commit " + "5 placements (got " + + std::to_string(storePlacements) + + " cells: " + dump + ")"); + } + + /* Authored garage cells survive untouched. */ + const auto &gcells = garage.furnitureCells; + if (gcells.size() != 2 || + gcells[0].furnitureType != "workbench1" || + gcells[0].placementId != + "furn-garage-workbench" || + gcells[0].owner != "authored" || + gcells[0].rotation != 1 || + gcells[1].furnitureType != "bay1" || + gcells[1].placementId != "furn-garage-bay" || + gcells[1].owner != "authored") + return fail( + "garage authored furnitureCells " + "wrong"); + + /* The furniture POIs were generated from the + * Blender attachment markers. */ + CellGridSystem *cgs = app->getCellGridSystem(); + if (!cgs) + return fail("no CellGridSystem"); + CellGridSystem::FurniturePoiQuery q; + q.gridEntity = storeGrid; + q.type = "attachment"; + workPoi = flecs::entity::null(); + for (const auto &poi : cgs->queryFurniturePois(q)) { + if (poi.name == "work") + workPoi = poi.entity; + } + if (!workPoi.is_alive()) + return fail("store counter 'work' POI missing"); + q.gridEntity = garageGrid; + bayPoi = flecs::entity::null(); + for (const auto &poi : cgs->queryFurniturePois(q)) { + if (poi.name == "bay") + bayPoi = poi.entity; + } + if (!bayPoi.is_alive()) + return fail("garage bay 'bay' POI missing"); + + /* The scene script's Lua reconcile must have + * converted the plain attachment markers into + * actuators with the demo actions. */ + auto checkActuator = + [&](flecs::entity poi, const char *action, + const char *what) -> bool { + if (!poi.is_alive() || + !poi.has()) { + fail(Ogre::String(what) + + " POI carries no " + "ActuatorComponent"); + return false; + } + const auto &a = poi.get(); + bool ok = false; + for (const auto &n : a.actionNames) { + if (n == action) + ok = true; + } + if (!ok) { + fail(Ogre::String(what) + + " POI actuator lacks action '" + + action + "'"); + return false; + } + return true; + }; + if (!checkActuator(workPoi, "work_counter", + "store counter")) + return true; + if (!checkActuator(bayPoi, "repair_vehicle", + "garage bay")) + return true; + + std::cout + << "[test] furniture scene verified: " + << aptPlacements + << " apt + " << storePlacements + << " store placements, garage cells intact, " + "POIs + script actuators wired" + << std::endl; + phase = 2; + break; + } + case 2: { + /* Park the sedan in the garage bay fixture. */ + flecs::entity sedan = findVehicle(); + if (!sedan.is_alive()) + return fail("sedan1 vehicle not found"); + if (!teleported) { + if (!captureBayWorld()) + return fail("bay POI has no transform"); + JoltPhysicsWrapper *physics = + app->getEditorPhysicsSystem() + ->getPhysicsWrapper(); + JPH::BodyID body = + sedan.get() + .bodyID; + physics->setPositionAndRotation( + body, + bayWorld + Ogre::Vector3(0.0f, + 0.6f, 0.0f), + Ogre::Quaternion::IDENTITY); + physics->setVelocities( + body, Ogre::Vector3::ZERO, + Ogre::Vector3::ZERO); + teleported = true; + break; + } + if (++settleFrames < 150) + break; + JPH::BodyID body = + sedan.get().bodyID; + JoltPhysicsWrapper *physics = + app->getEditorPhysicsSystem() + ->getPhysicsWrapper(); + Ogre::Vector3 p = physics->getPosition(body); + Ogre::Vector3 v = physics->getLinearVelocity(body); + if (std::fabs(p.x - bayWorld.x) > 2.2f || + std::fabs(p.z - bayWorld.z) > 1.0f) + return fail( + "sedan did not settle inside the " + "bay footprint (" + + std::to_string(p.x - bayWorld.x) + + ", " + + std::to_string(p.z - bayWorld.z) + + ")"); + if (v.length() > 0.3f) + return fail( + "sedan still rolling in the bay"); + std::cout << "[test] sedan settled in the bay at (" + << p.x << ", " << p.z << ")" << std::endl; + phase = 3; + break; + } + case 3: { + /* Execute the actions exactly like an E press + * would: executeAction latches the actuator + * running state and the game's own update() + * ticks the tree until it finishes (the + * executing block runs before the ImGui gate, so + * it works headless); the luaTask nodes latch + * the hooks in the global store. One action at + * a time: the executing state holds a single + * action. */ + ActuatorSystem *actuators = app->getActuatorSystem(); + if (!actuators) + return fail("no ActuatorSystem"); + if (!workFired) { + if (!workPoi.is_alive()) { + return fail("work POI died " + "before its action"); + } + workBaseline = + GlobalStateStore::getInstance().getInt( + "hooks.work_counter"); + workFired = true; + actionFrames = 0; + actuators->executeAction( + app->getPlayerCharacterEntity(), + workPoi, "work_counter"); + break; + } + if (!workLatched) { + int64_t workNow = + GlobalStateStore::getInstance().getInt( + "hooks.work_counter"); + if (workNow >= workBaseline + 1) { + workLatched = true; + std::cout << "[test] work_counter " + << "latched " << workNow + << std::endl; + break; + } + if (++actionFrames > 240) + return fail( + "work_counter never " + "latched (stays " + + std::to_string(workNow) + ")"); + break; + } + if (!repairFired) { + if (!bayPoi.is_alive()) + return fail("bay POI died " + "before its action"); + repairBaseline = + GlobalStateStore::getInstance().getInt( + "hooks.repair_vehicle"); + repairFired = true; + actionFrames = 0; + actuators->executeAction( + app->getPlayerCharacterEntity(), + bayPoi, "repair_vehicle"); + break; + } + /* Repair latched -> everything verified. */ + int64_t repairNow = + GlobalStateStore::getInstance() + .getInt("hooks.repair_vehicle"); + if (repairNow >= repairBaseline + 1) { + std::cout << "[test] PASS: furniture demo (" + << aptPlacements << " apt + " + << storePlacements + << " store placements, garage " + "cells intact, POI actuators " + "+ luaTask hooks latched)" + << std::endl; + phase = 4; + app->getRoot()->queueEndRendering(); + break; + } + if (++actionFrames > 240) + return fail( + "repair_vehicle never latched " + "(stays " + + std::to_string(repairNow) + ")"); + } + default: + break; + } + return true; + } +}; + /* * --benchmark (phase 5.5): fixed-camera performance probe on the * generated town. Waits for the town population + traffic fleet, then @@ -6598,6 +6999,17 @@ struct DriveBenchmarkListener : public Ogre::FrameListener, * VehicleControllerSystem::exitVehicle() and checks the * character is back on foot next to the sedan; exits 0 * on PASS. + * --test-furniture + * headless-friendly phase 6.5.I check on the furnished + * demo scene (demo_scene_furniture.json): the + * apartment/shop composition commits, the authored + * garage furnitureCells, the furniture POIs + * (counter "work"/garage "bay"), the scene script's + * throttled reconcile converted them into actuators, + * the sedan parking inside the bay fixture footprint + * and both POI actions latching their hooks luaTask + * counters through ActuatorSystem::executeAction; + * exits 0 on PASS. */ int main(int argc, char *argv[]) { @@ -6640,6 +7052,7 @@ int main(int argc, char *argv[]) bool testTrafficFollow = false; bool testTrafficYield = false; bool testTrafficSchedule = false; + bool testFurniture = false; bool benchmark = false; bool benchmarkDrive = false; float driveSpeed = 13.0f; @@ -6704,6 +7117,8 @@ int main(int argc, char *argv[]) testTrafficYield = true; } else if (arg == "--test-traffic-schedule") { testTrafficSchedule = true; + } else if (arg == "--test-furniture") { + testFurniture = true; } else if (arg == "--benchmark") { benchmark = true; } else if (arg == "--benchmark-drive") { @@ -6824,6 +7239,9 @@ int main(int argc, char *argv[]) /* The schedule traffic test needs the generated town. */ if (testTrafficSchedule && !sceneArgGiven) sceneFile = "demo_scene_exterior.json"; + /* The furniture test loads the furnished demo scene. */ + if (testFurniture && !sceneArgGiven) + sceneFile = "demo_scene_furniture.json"; /* The benchmark measures the generated town. */ if (benchmark && !sceneArgGiven) sceneFile = "demo_scene_exterior.json"; @@ -6966,6 +7384,10 @@ int main(int argc, char *argv[]) if (testTrafficSchedule) app.getRoot()->addFrameListener(&trafficScheduleListener); + FurnitureDemoTestListener furnitureTestListener(&app); + if (testFurniture) + app.getRoot()->addFrameListener(&furnitureTestListener); + BenchmarkListener benchmarkListener(&app); if (benchmark) { app.getRoot()->addFrameListener(&benchmarkListener); @@ -7344,6 +7766,26 @@ int main(int argc, char *argv[]) } } + if (testFurniture) { + if (furnitureTestListener.failed) { + std::cerr << "[test] FAIL: " + << furnitureTestListener.failReason + << std::endl; + app.clearScene(); + app.closeApp(); + return 1; + } + if (furnitureTestListener.phase != 4) { + std::cerr << "[test] FAIL: incomplete (furniture " + "phase " + << furnitureTestListener.phase << ")" + << std::endl; + app.clearScene(); + app.closeApp(); + return 1; + } + } + if (benchmark) { if (benchmarkListener.failed) { std::cerr << "[benchmark] FAIL: " diff --git a/src/features/editScene/demos/demo-taxi/demo_scene_furniture.json b/src/features/editScene/demos/demo-taxi/demo_scene_furniture.json new file mode 100644 index 0000000..eb68216 --- /dev/null +++ b/src/features/editScene/demos/demo-taxi/demo_scene_furniture.json @@ -0,0 +1,3142 @@ +{ + "actionDatabase": { + "actions": [ + { + "behaviorTree": { + "children": [ + { + "name": "vehicleEnter", + "params": "seat=driver", + "type": "luaTask" + } + ], + "type": "sequence" + }, + "cost": 1, + "effects": { + "bits": 0, + "mask": 0 + }, + "name": "vehicle_seat_driver", + "preconditions": { + "bits": 0, + "mask": 0 + } + }, + { + "behaviorTree": { + "children": [ + { + "name": "vehicleEnter", + "params": "seat=passenger", + "type": "luaTask" + } + ], + "type": "sequence" + }, + "cost": 1, + "effects": { + "bits": 0, + "mask": 0 + }, + "name": "vehicle_seat_passenger", + "preconditions": { + "bits": 0, + "mask": 0 + } + }, + { + "behaviorTree": { + "children": [ + { + "name": "vehicleEnter", + "params": "seat=rl", + "type": "luaTask" + } + ], + "type": "sequence" + }, + "cost": 1, + "effects": { + "bits": 0, + "mask": 0 + }, + "name": "vehicle_seat_rl", + "preconditions": { + "bits": 0, + "mask": 0 + } + }, + { + "behaviorTree": { + "children": [ + { + "name": "vehicleEnter", + "params": "seat=rr", + "type": "luaTask" + } + ], + "type": "sequence" + }, + "cost": 1, + "effects": { + "bits": 0, + "mask": 0 + }, + "name": "vehicle_seat_rr", + "preconditions": { + "bits": 0, + "mask": 0 + } + }, + { + "behaviorTree": { + "type": "sequence", + "children": [ + { + "name": "furnWorkCounter", + "params": "", + "type": "luaTask" + } + ] + }, + "cost": 1, + "effects": { + "bits": 0, + "mask": 0 + }, + "name": "work_counter", + "preconditions": { + "bits": 0, + "mask": 0 + } + }, + { + "behaviorTree": { + "type": "sequence", + "children": [ + { + "name": "furnRepairVehicle", + "params": "", + "type": "luaTask" + } + ] + }, + "cost": 1, + "effects": { + "bits": 0, + "mask": 0 + }, + "name": "repair_vehicle", + "preconditions": { + "bits": 0, + "mask": 0 + } + } + ], + "bitNames": [ + { + "index": 1, + "name": "hungry" + }, + { + "index": 2, + "name": "thirsty" + } + ], + "goals": [] + }, + "bookmarks": [], + "entities": [ + { + "children": [], + "id": 700, + "light": { + "castShadows": false, + "constantAttenuation": 1.0, + "diffuseColor": { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + "direction": { + "x": 0.30000001192092896, + "y": -1.0, + "z": 0.20000000298023224 + }, + "intensity": 1.0, + "lightType": "directional", + "linearAttenuation": 0.0, + "quadraticAttenuation": 0.0, + "range": 100.0, + "specularColor": { + "a": 1.0, + "b": 0.5, + "g": 0.5, + "r": 0.5 + }, + "spotlightFalloff": 1.0, + "spotlightInnerAngle": 30.0, + "spotlightOuterAngle": 45.0 + }, + "name": { + "name": "demo_light" + }, + "transform": { + "position": { + "x": 0.0, + "y": 30.0, + "z": -6.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + }, + { + "children": [], + "collider": { + "halfHeight": 1.0, + "meshName": "", + "offset": { + "x": 0.0, + "y": -0.10000000149011612, + "z": 0.0 + }, + "parameters": { + "x": 200.0, + "y": 0.10000000149011612, + "z": 200.0 + }, + "radius": 0.5, + "rotationOffset": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "shapeType": "box" + }, + "id": 701, + "name": { + "name": "demo_floor" + }, + "renderable": { + "meshName": "DemoFloorPlaneVehicleTest", + "visible": true + }, + "rigidBody": { + "bodyType": "static", + "enabled": true, + "friction": 0.800000011920929, + "isSensor": false, + "mass": 1.0, + "restitution": 0.0 + }, + "transform": { + "position": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + }, + { + "characterSpawner": { + "despawnDistance": 200.0, + "registryId": 2, + "spawnDistance": 100.0 + }, + "children": [], + "id": 702, + "name": { + "name": "s1" + }, + "transform": { + "position": { + "x": -20.0, + "y": 0.05, + "z": 8.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + }, + { + "children": [], + "id": 703, + "name": { + "name": "player" + }, + "playerController": { + "actuatorColor": [ + 0.0, + 0.4000000059604645, + 1.0 + ], + "actuatorCooldown": 1.5, + "actuatorDistance": 25.0, + "actuatorLabelFontSize": 12.0, + "cameraMode": 0, + "distantCircleRadius": 8.0, + "fpsBoneName": "Head", + "idleState": "idle", + "locomotionStateMachine": "locomotion", + "mouseSensitivity": 0.20000000298023224, + "nearCircleRadius": 14.0, + "runState": "running", + "swimFastState": "swimming-fast", + "swimIdleState": "swim-idle", + "swimState": "swimming", + "targetCharacterName": "s1", + "tpsDistance": 3.0, + "tpsHeight": 2.0, + "walkState": "walking" + }, + "transform": { + "position": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + }, + "sceneScript": { + "inlineScript": "-- Phase 6.5.I: furniture interaction demo script.\n-- The store counter and the garage bay fixtures export plain\n-- \"attachment\" position markers with no action of their own, so this\n-- scene script turns their POIs (\"work\" / \"bay\") into interactive\n-- actuators and registers the two luaTask nodes the actions run.\n-- The counter -> hooks.work_counter and the bay ->\n-- hooks.repair_vehicle global latches demonstrate the full\n-- POI -> actuator -> behavior tree -> Lua chain headlessly\n-- (demoTaxi --headless --test-furniture asserts both latches).\n--\n-- POI entities are runtime-only and are destroyed and recreated when\n-- their grid rebuilds (e.g. the texture-ready rebuild right after\n-- scene load), so the actuators are (re-)attached from a throttled\n-- \"frame\" reconcile instead of binding once at script load.\n\nlocal POI_ACTIONS = {\n\t[\"furn-store\"] = { work = \"work_counter\" },\n\t[\"furn-garage\"] = { bay = \"repair_vehicle\" },\n}\n\necs.behavior_tree.register_node(\"furnWorkCounter\", function(entity_id, params)\n\tlocal n = ecs.global.get_int(\"hooks.work_counter\", 0)\n\tecs.global.set(\"hooks.work_counter\", n + 1)\n\tprint(\"[furniture-demo] work_counter -> \" .. (n + 1))\n\treturn \"success\"\nend)\n\necs.behavior_tree.register_node(\"furnRepairVehicle\", function(entity_id, params)\n\tlocal n = ecs.global.get_int(\"hooks.repair_vehicle\", 0)\n\tecs.global.set(\"hooks.repair_vehicle\", n + 1)\n\tprint(\"[furniture-demo] repair_vehicle -> \" .. (n + 1))\n\treturn \"success\"\nend)\n\nlocal throttle = 0.0\necs.subscribe_event(\"frame\", function(event, params)\n\tthrottle = throttle + (params and params.dt or 0.016)\n\tif throttle < 0.25 then\n\t\treturn\n\tend\n\tthrottle = 0\n\tfor gridUid, names in pairs(POI_ACTIONS) do\n\t\tlocal pois = ecs.furniture.query_pois({\n\t\t\tgridUid = gridUid, type = \"attachment\" })\n\t\tfor _, poi in ipairs(pois) do\n\t\t\tlocal action = names[poi.name]\n\t\t\tif action then\n\t\t\t\tecs.set_component(poi.entity_id, \"Actuator\", {\n\t\t\t\t\tradius = 3.0,\n\t\t\t\t\theight = 3.0,\n\t\t\t\t\tactionNames = { action },\n\t\t\t\t})\n\t\t\tend\n\t\tend\n\tend\nend)\n\nprint(\"[furniture-demo] script loaded\")\n\n", + "scriptPath": "" + } + }, + { + "children": [ + { + "children": [], + "name": { + "name": "sedan1_door_rl" + }, + "renderable": { + "meshName": "sedan-v2-door-rl.mesh", + "visible": true + }, + "transform": { + "position": { + "x": 0.9192, + "y": -0.2869, + "z": -0.132 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + }, + "actuator": { + "actionNames": [ + "vehicle_seat_rl" + ], + "height": 2.0, + "radius": 1.2 + } + }, + { + "children": [], + "name": { + "name": "sedan1_door_rr" + }, + "renderable": { + "meshName": "sedan-v2-door-rr.mesh", + "visible": true + }, + "transform": { + "position": { + "x": -0.9192, + "y": -0.2869, + "z": -0.132 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + }, + "actuator": { + "actionNames": [ + "vehicle_seat_rr" + ], + "height": 2.0, + "radius": 1.2 + } + }, + { + "children": [], + "name": { + "name": "sedan1_door_fl" + }, + "renderable": { + "meshName": "sedan-v2-door-fl.mesh", + "visible": true + }, + "transform": { + "position": { + "x": 0.9192, + "y": -0.2868, + "z": 0.801 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + }, + "actuator": { + "actionNames": [ + "vehicle_seat_driver" + ], + "height": 2.0, + "radius": 1.2 + } + }, + { + "children": [], + "name": { + "name": "sedan1_door_fr" + }, + "renderable": { + "meshName": "sedan-v2-door-fr.mesh", + "visible": true + }, + "transform": { + "position": { + "x": -0.9192, + "y": -0.2868, + "z": 0.801 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + }, + "actuator": { + "actionNames": [ + "vehicle_seat_passenger" + ], + "height": 2.0, + "radius": 1.2 + } + }, + { + "children": [], + "name": { + "name": "sedan1_hood" + }, + "renderable": { + "meshName": "sedan-v2-hood.mesh", + "visible": true + }, + "transform": { + "position": { + "x": -0.0, + "y": 0.266, + "z": 0.9992 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + }, + { + "children": [], + "name": { + "name": "sedan1_trunk" + }, + "renderable": { + "meshName": "sedan-v2-trunk.mesh", + "visible": true + }, + "transform": { + "position": { + "x": 0.0, + "y": 0.2744, + "z": -1.446 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + } + ], + "collider": { + "halfHeight": 1.0, + "meshName": "", + "offset": { + "x": 0.0, + "y": 0.1535, + "z": -0.0629 + }, + "parameters": { + "x": 1.0107, + "y": 0.6126, + "z": 2.4834 + }, + "radius": 0.5, + "rotationOffset": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "shapeType": "box" + }, + "name": { + "name": "sedan1" + }, + "renderable": { + "meshName": "sedan-v2.mesh", + "visible": true + }, + "rigidBody": { + "bodyType": "dynamic", + "enabled": true, + "friction": 0.8, + "isSensor": false, + "mass": 1200.0, + "restitution": 0.0 + }, + "transform": { + "position": { + "x": 6.0, + "y": 1.5, + "z": -24.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + }, + "vehicle": { + "cameraDistance": 6.0, + "cameraHeight": 1.6, + "engineBrake": 0.0, + "maxPitchRollAngleDeg": 60.0, + "maxTorque": 300.0, + "rearSteer": false, + "seatOffset": { + "x": 0.48, + "y": -0.28, + "z": 0.2012 + }, + "steeringRatio": 4.0, + "steeringWheelMeshName": "sedan-v2-steering-wheel.mesh", + "steeringWheelOffset": { + "x": 0.4915, + "y": 0.0807, + "z": 0.601 + }, + "steeringWheelTiltDeg": 25.0, + "wheelMeshName": "sedan-v2-wheel.mesh", + "wheels": [ + { + "driven": false, + "maxHandBrakeTorque": 0.0, + "maxSteerAngleDeg": 35.0, + "position": { + "x": 0.7805, + "y": -0.2052, + "z": 1.4806 + }, + "radius": 0.3918, + "suspensionDamping": 1.0, + "suspensionFrequency": 1.3, + "suspensionMaxLength": 0.35, + "suspensionMinLength": 0.1, + "width": 0.3145 + }, + { + "driven": false, + "maxHandBrakeTorque": 0.0, + "maxSteerAngleDeg": 35.0, + "position": { + "x": -0.7805, + "y": -0.2052, + "z": 1.4806 + }, + "radius": 0.3918, + "suspensionDamping": 1.0, + "suspensionFrequency": 1.3, + "suspensionMaxLength": 0.35, + "suspensionMinLength": 0.1, + "width": 0.3145 + }, + { + "driven": true, + "maxHandBrakeTorque": 300.0, + "maxSteerAngleDeg": 0.0, + "position": { + "x": 0.7805, + "y": -0.2052, + "z": -1.2806 + }, + "radius": 0.3918, + "suspensionDamping": 1.0, + "suspensionFrequency": 1.3, + "suspensionMaxLength": 0.35, + "suspensionMinLength": 0.1, + "width": 0.3145 + }, + { + "driven": true, + "maxHandBrakeTorque": 300.0, + "maxSteerAngleDeg": 0.0, + "position": { + "x": -0.7805, + "y": -0.2052, + "z": -1.2806 + }, + "radius": 0.3918, + "suspensionDamping": 1.0, + "suspensionFrequency": 1.3, + "suspensionMaxLength": 0.35, + "suspensionMinLength": 0.1, + "width": 0.3145 + } + ], + "seats": [ + { + "name": "driver", + "offset": { + "x": 0.48, + "y": -0.28, + "z": 0.2012 + }, + "driver": true + }, + { + "name": "passenger", + "offset": { + "x": -0.48, + "y": -0.28, + "z": 0.2012 + }, + "driver": false + }, + { + "name": "rl", + "offset": { + "x": 0.48, + "y": -0.28, + "z": -0.6032 + }, + "driver": false + }, + { + "name": "rr", + "offset": { + "x": -0.48, + "y": -0.28, + "z": -0.6032 + }, + "driver": false + } + ], + "seatCharacterScale": 1.0, + "tireFrictionLongitudinal": 1.3, + "tireFrictionLateral": 3.0, + "antiRollBarStiffness": 2000.0, + "centerOfMassOffset": { + "x": 0.0, + "y": -0.4617, + "z": 0.0 + }, + "downforceCoefficient": 35.0 + }, + "id": 704 + }, + { + "cellGrid": { + "ceilingRectName": "ceiling", + "cellHeight": 4.0, + "cellSize": 2.0, + "cells": [], + "depth": 5, + "doorActionName": "", + "doorConfigs": {}, + "doorMeshName": "", + "doorOpenAngle": 100.0, + "doorOpenSpeed": 180.0, + "doorRectName": "", + "doorSceneSwitchPath": "", + "doorSceneSwitchTarget": "", + "doorSwingReversed": false, + "doorUseMeshMaterial": false, + "doorsEnabled": true, + "extDoorFrameRectName": "", + "extWallRectName": "", + "extWindowFrameRectName": "", + "floorRectName": "floor", + "friction": 0.5, + "furnitureCells": [], + "generationMode": "full", + "generationScript": "", + "glassColor": [ + 0.4000000059604645, + 0.6000000238418579, + 0.800000011920929, + 0.3499999940395355 + ], + "glassMaterialName": "", + "glassReflectivity": 0.800000011920929, + "gridUid": "furn-apt", + "height": 1, + "intDoorFrameRectName": "", + "intWallRectName": "", + "intWindowFrameRectName": "", + "roofSideRectName": "", + "roofTopRectName": "", + "width": 8 + }, + "children": [ + { + "children": [], + "id": 711, + "name": { + "name": "apt-main-room" + }, + "room": { + "connectedRoomIds": [], + "createCeiling": true, + "createFloor": true, + "createInteriorWalls": true, + "createWindows": false, + "exits": [ + false, + false, + false, + false + ], + "fillRoomWithFurniture": false, + "furnitureCompositions": [ + { + "anchor": "center", + "children": [ + { + "offsetX": -2, + "offsetZ": -2, + "rotationOffset": 0, + "tags": [ + "lounge", + "filler" + ] + }, + { + "offsetX": 2, + "offsetZ": -2, + "rotationOffset": 0, + "tags": [ + "lounge", + "filler" + ] + }, + { + "offsetX": -2, + "offsetZ": 2, + "rotationOffset": 0, + "tags": [ + "lounge", + "filler" + ] + }, + { + "offsetX": 2, + "offsetZ": 2, + "rotationOffset": 0, + "tags": [ + "lounge", + "filler" + ] + } + ], + "count": 0, + "kind": "group", + "rootRotation": -1, + "skip": 0, + "step": 0, + "tags": [ + "livingroom", + "essential" + ] + } + ], + "furnitureSeed": 42, + "furnitureYOffset": 0.05, + "maxX": 8, + "maxY": 1, + "maxZ": 5, + "minX": 0, + "minY": 0, + "minZ": 0, + "persistentId": "apt-main", + "roomType": "livingroom", + "tags": [ + "apt-main" + ] + }, + "transform": { + "position": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + } + ], + "id": 710, + "name": { + "name": "furn-apt" + }, + "proceduralMaterial": { + "ambient": { + "b": 0.20000000298023224, + "g": 0.20000000298023224, + "r": 0.20000000298023224 + }, + "diffuse": { + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + "diffuseTextureId": "id_furniture_apt_1", + "materialId": "id_furniture_apt_mat", + "materialName": "ProceduralMat_id_furniture_apt_1", + "roughness": 0.5, + "shininess": 32.0, + "specular": { + "b": 0.0, + "g": 0.0, + "r": 0.0 + } + }, + "proceduralTexture": { + "colors": [ + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + } + ], + "namedRects": [ + { + "name": "ceiling", + "u1": 0.10000000149011612, + "u2": 0.20000000298023224, + "v1": 0.0, + "v2": 0.10000000149011612 + }, + { + "name": "floor", + "u1": 0.0, + "u2": 0.10000000149011612, + "v1": 0.0, + "v2": 0.10000000149011612 + } + ], + "textureId": "id_furniture_apt_1", + "textureName": "ProceduralTex_id_furniture_apt_1", + "textureSize": 512, + "uvMargin": 0.009999999776482582 + }, + "transform": { + "position": { + "x": -22.0, + "y": -0.2, + "z": 4.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + }, + { + "cellGrid": { + "ceilingRectName": "ceiling", + "cellHeight": 4.0, + "cellSize": 2.0, + "cells": [], + "depth": 10, + "doorActionName": "", + "doorConfigs": {}, + "doorMeshName": "", + "doorOpenAngle": 100.0, + "doorOpenSpeed": 180.0, + "doorRectName": "", + "doorSceneSwitchPath": "", + "doorSceneSwitchTarget": "", + "doorSwingReversed": false, + "doorUseMeshMaterial": false, + "doorsEnabled": true, + "extDoorFrameRectName": "", + "extWallRectName": "", + "extWindowFrameRectName": "", + "floorRectName": "floor", + "friction": 0.5, + "furnitureCells": [], + "generationMode": "full", + "generationScript": "", + "glassColor": [ + 0.4000000059604645, + 0.6000000238418579, + 0.800000011920929, + 0.3499999940395355 + ], + "glassMaterialName": "", + "glassReflectivity": 0.800000011920929, + "gridUid": "furn-store", + "height": 1, + "intDoorFrameRectName": "", + "intWallRectName": "", + "intWindowFrameRectName": "", + "roofSideRectName": "", + "roofTopRectName": "", + "width": 8 + }, + "children": [ + { + "children": [], + "id": 721, + "name": { + "name": "store-floor-room" + }, + "room": { + "connectedRoomIds": [], + "createCeiling": true, + "createFloor": true, + "createInteriorWalls": true, + "createWindows": false, + "exits": [ + false, + false, + false, + false + ], + "fillRoomWithFurniture": false, + "furnitureCompositions": [ + { + "anchor": "zpos", + "children": [], + "count": 4, + "kind": "row", + "rootRotation": -1, + "skip": 0, + "step": 1, + "tags": [ + "shop", + "display" + ] + }, + { + "anchor": "center", + "children": [], + "count": 0, + "kind": "group", + "rootRotation": -1, + "skip": 0, + "step": 0, + "tags": [ + "shop", + "counter" + ] + } + ], + "furnitureSeed": 42, + "furnitureYOffset": 0.05, + "maxX": 8, + "maxY": 1, + "maxZ": 10, + "minX": 0, + "minY": 0, + "minZ": 0, + "persistentId": "store-floor", + "roomType": "shop", + "tags": [ + "store-floor" + ] + }, + "transform": { + "position": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + } + ], + "id": 720, + "name": { + "name": "furn-store" + }, + "proceduralMaterial": { + "ambient": { + "b": 0.20000000298023224, + "g": 0.20000000298023224, + "r": 0.20000000298023224 + }, + "diffuse": { + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + "diffuseTextureId": "id_furniture_store_1", + "materialId": "id_furniture_store_mat", + "materialName": "ProceduralMat_id_furniture_store_1", + "roughness": 0.5, + "shininess": 32.0, + "specular": { + "b": 0.0, + "g": 0.0, + "r": 0.0 + } + }, + "proceduralTexture": { + "colors": [ + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + } + ], + "namedRects": [ + { + "name": "ceiling", + "u1": 0.10000000149011612, + "u2": 0.20000000298023224, + "v1": 0.0, + "v2": 0.10000000149011612 + }, + { + "name": "floor", + "u1": 0.0, + "u2": 0.10000000149011612, + "v1": 0.0, + "v2": 0.10000000149011612 + } + ], + "textureId": "id_furniture_store_1", + "textureName": "ProceduralTex_id_furniture_store_1", + "textureSize": 512, + "uvMargin": 0.009999999776482582 + }, + "transform": { + "position": { + "x": 26.0, + "y": -0.2, + "z": 4.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + }, + { + "cellGrid": { + "ceilingRectName": "ceiling", + "cellHeight": 4.0, + "cellSize": 2.0, + "cells": [], + "depth": 6, + "doorActionName": "", + "doorConfigs": {}, + "doorMeshName": "", + "doorOpenAngle": 100.0, + "doorOpenSpeed": 180.0, + "doorRectName": "", + "doorSceneSwitchPath": "", + "doorSceneSwitchTarget": "", + "doorSwingReversed": false, + "doorUseMeshMaterial": false, + "doorsEnabled": true, + "extDoorFrameRectName": "", + "extWallRectName": "", + "extWindowFrameRectName": "", + "floorRectName": "floor", + "friction": 0.5, + "furnitureCells": [ + { + "groupId": "", + "owner": "authored", + "placementId": "furn-garage-workbench", + "rotation": 1, + "tags": [], + "x": 1, + "y": 0, + "z": 2, + "furnitureType": "workbench1" + }, + { + "groupId": "", + "owner": "authored", + "placementId": "furn-garage-bay", + "rotation": 0, + "tags": [], + "x": 3, + "y": 0, + "z": 3, + "furnitureType": "bay1" + } + ], + "generationMode": "full", + "generationScript": "", + "glassColor": [ + 0.4000000059604645, + 0.6000000238418579, + 0.800000011920929, + 0.3499999940395355 + ], + "glassMaterialName": "", + "glassReflectivity": 0.800000011920929, + "gridUid": "furn-garage", + "height": 1, + "intDoorFrameRectName": "", + "intWallRectName": "", + "intWindowFrameRectName": "", + "roofSideRectName": "", + "roofTopRectName": "", + "width": 6 + }, + "children": [ + { + "children": [], + "id": 731, + "name": { + "name": "garage-bay-room" + }, + "room": { + "connectedRoomIds": [], + "createCeiling": true, + "createFloor": true, + "createInteriorWalls": true, + "createWindows": false, + "exits": [ + false, + false, + false, + false + ], + "fillRoomWithFurniture": false, + "furnitureCompositions": [], + "furnitureSeed": 42, + "furnitureYOffset": 0.05, + "maxX": 6, + "maxY": 1, + "maxZ": 6, + "minX": 0, + "minY": 0, + "minZ": 0, + "persistentId": "garage-bay", + "roomType": "garage", + "tags": [] + }, + "transform": { + "position": { + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + } + ], + "id": 730, + "name": { + "name": "furn-garage" + }, + "proceduralMaterial": { + "ambient": { + "b": 0.20000000298023224, + "g": 0.20000000298023224, + "r": 0.20000000298023224 + }, + "diffuse": { + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + "diffuseTextureId": "id_furniture_garage_1", + "materialId": "id_furniture_garage_mat", + "materialName": "ProceduralMat_id_furniture_garage_1", + "roughness": 0.5, + "shininess": 32.0, + "specular": { + "b": 0.0, + "g": 0.0, + "r": 0.0 + } + }, + "proceduralTexture": { + "colors": [ + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + }, + { + "a": 1.0, + "b": 0.0, + "g": 0.0, + "r": 0.0 + }, + { + "a": 1.0, + "b": 1.0, + "g": 1.0, + "r": 1.0 + } + ], + "namedRects": [ + { + "name": "ceiling", + "u1": 0.10000000149011612, + "u2": 0.20000000298023224, + "v1": 0.0, + "v2": 0.10000000149011612 + }, + { + "name": "floor", + "u1": 0.0, + "u2": 0.10000000149011612, + "v1": 0.0, + "v2": 0.10000000149011612 + } + ], + "textureId": "id_furniture_garage_1", + "textureName": "ProceduralTex_id_furniture_garage_1", + "textureSize": 512, + "uvMargin": 0.009999999776482582 + }, + "transform": { + "position": { + "x": 0.0, + "y": -0.2, + "z": -18.0 + }, + "rotation": { + "w": 1.0, + "x": 0.0, + "y": 0.0, + "z": 0.0 + }, + "scale": { + "x": 1.0, + "y": 1.0, + "z": 1.0 + } + } + } + ], + "version": "1.0" +} diff --git a/src/features/editScene/demos/demo-taxi/stage_runtime.cmake b/src/features/editScene/demos/demo-taxi/stage_runtime.cmake index 5651d95..5ef1a4a 100644 --- a/src/features/editScene/demos/demo-taxi/stage_runtime.cmake +++ b/src/features/editScene/demos/demo-taxi/stage_runtime.cmake @@ -18,7 +18,7 @@ file(COPY "${EDITSCENE_SRC}/resources.cfg" DESTINATION "${DEMO_DIR}") file(COPY "${SRC_DIR}/project.json" DESTINATION "${DEMO_DIR}") foreach(f demo_scene_interior.json demo_scene_exterior.json - demo_scene_vehicletest.json) + demo_scene_vehicletest.json demo_scene_furniture.json) set(link "${DEMO_DIR}/${f}") if(EXISTS "${link}" OR IS_SYMLINK "${link}") file(REMOVE "${link}") diff --git a/src/features/editScene/lua-examples/furniture_action_binding_example.lua b/src/features/editScene/lua-examples/furniture_action_binding_example.lua new file mode 100644 index 0000000..8d50c35 --- /dev/null +++ b/src/features/editScene/lua-examples/furniture_action_binding_example.lua @@ -0,0 +1,102 @@ +-- Phase 6.5.G/H: stable-id action binding for furniture POIs. +-- +-- Blender-exported furniture markers become runtime POI entities with +-- STABLE ids (furniture..) but plain +-- "attachment" position markers carry no behavior of their own - just a +-- transform. This example binds such a POI to an Actuator + a +-- behavior-tree action by its stable id, using the worked garage case +-- from demos/demo-taxi/demo_scene_furniture.json (see that script for +-- the multi-grid throttled variant used by the demo's --test-furniture). +-- +-- Contract: +-- - Stable placement ids come from either side of the pipeline: an +-- authored furnitureCells placementId in the scene JSON, a +-- deterministic generated placementId, or one you pass to +-- ecs.furniture.commit. The POI id is placementId + marker name, +-- so it survives grid rebuilds and scene loads. +-- - POI ENTITIES are runtime-only: their flecs ids are recreated on +-- every grid rebuild (the texture-ready rebuild right after scene +-- load does exactly that). Resolve the id fresh in a throttled +-- "frame" reconcile instead of caching entity ids at script load. +-- - ecs.set_component on "Actuator" PATCHES fields (it does not +-- replace the component), so re-attaching does not clobber +-- runtime state like isExecuting. +-- +-- The demo scene's garage bay fixture ("furn-garage" grid, authored +-- placement "furn-garage-bay", bay1 def's "bay" attachment marker) is +-- addressed here by the resulting POI id +-- furniture.furn-garage-bay.bay. + +-- ----------------------------------------------------------------------------- +-- 1. The behavior the action runs (registered once per Lua state) +-- ----------------------------------------------------------------------------- +-- luaTask nodes receive (entity_id, params) and return a status string; +-- args come from the node's parameter string ("key=value ..."). This +-- one latches a counter in the global store so a headless test can +-- assert the whole POI -> actuator -> tree -> Lua chain. + +ecs.behavior_tree.register_node("repairVehicle", function(entity_id, params) + local n = ecs.global.get_int("hooks.repair_vehicle", 0) + ecs.global.set("hooks.repair_vehicle", n + 1) + print("[garage] repair_vehicle performed by entity " .. entity_id) + return "success" +end) + +-- ----------------------------------------------------------------------------- +-- 2. Bind the stable POI id to the action +-- ----------------------------------------------------------------------------- +-- A POI's entity carries SmartObject/Actuator-compatible fields; here +-- we make it an E-interactable actuator with a 3 m radius. Run +-- (re-)binding from a throttled "frame" tick: the first frames after a +-- scene load run before the grid has rebuilt its POIs, and a rebuild +-- destroys + recreates them (fresh entity ids, same stable id). + +local POI_ID = "furniture.furn-garage-bay.bay" -- stable id +local ACTION = "repair_vehicle" + +local throttle = 0.0 +ecs.subscribe_event("frame", function(event, params) + throttle = throttle + (params and params.dt or 0.016) + if throttle < 0.25 then + return + end + throttle = 0 + + local poi = ecs.furniture.find_poi(POI_ID) + if not poi then + return -- grid not built yet (or the placement was removed) + end + ecs.set_component(poi.entity_id, "Actuator", { + radius = 3.0, + height = 3.0, + actionNames = { ACTION }, + }) +end) + +-- ----------------------------------------------------------------------------- +-- 3. Reading POI metadata by stable id (GOAP/schedule users) +-- ----------------------------------------------------------------------------- +-- Every POI reports its placement, marker name/type, owning room, the +-- furniture definition type and a grid-local transform - enough for AI +-- queries without touching entity ids: +-- poi.id / poi.placementId / poi.name / poi.type / poi.actionName +-- poi.roomId / poi.furnitureType / poi.position {x,y,z} +-- poi.rotation {w,x,y,z} +-- +-- Filtered queries over all furniture: +-- ecs.furniture.query_pois({ roomId = "...", type = "attachment" }) +-- ecs.furniture.query_pois({ furnitureTag = "garage" }) +-- ecs.furniture.query_pois({ actionName = "repair_vehicle" }) + +-- ----------------------------------------------------------------------------- +-- 4. Executing the action (headless test path) +-- ----------------------------------------------------------------------------- +-- A player standing in range presses E and ActuatorSystem runs +-- "repair_vehicle" through the behavior tree (input locked while it +-- runs). Tests skip the UI path: ActuatorSystem::executeAction is the +-- same entry point an E press takes and it works headless (C++ side): +-- +-- app.getActuatorSystem()->executeAction(player, poiEntity, action) +-- +-- The demo's demo_main.cpp FurnitureDemoTestListener does exactly that +-- and waits for hooks.repair_vehicle to latch. diff --git a/src/features/editScene/lua-examples/furniture_layout_example.lua b/src/features/editScene/lua-examples/furniture_layout_example.lua new file mode 100644 index 0000000..8d303ab --- /dev/null +++ b/src/features/editScene/lua-examples/furniture_layout_example.lua @@ -0,0 +1,172 @@ +-- Phase 6.5.H: furniture layout from Lua - the ecs.furniture API. +-- +-- Declarative furniture belongs to the room layout (the layout DSL +-- :compose groups/rows, the editor's room property panel or the tag walk +-- in RoomLayoutSystem). Lua is for unique interiors and behavior: this +-- example lists definitions, validates and commits script-owned +-- placements inside one transaction, hooks the ordered room-generation +-- events and queries furniture POIs. +-- +-- Grid identity: placements and POIs address a grid by its serialized +-- CellGridComponent.gridUid (random UUID, shown in the Cell Grid +-- editor) - never by flecs entity id; entity ids are not stable across +-- scene loads. Rooms are addressed by RoomComponent.persistentId +-- ("townroom-
--" for generated rooms, or a may be +-- empty for unnamed node rooms - pass "" then). +-- +-- See also furniture_action_binding_example.lua for binding stable POI +-- ids to actions, and the worked demo: demos/demo-taxi +-- demo_scene_furniture.json (store/garage/apartment + Lua actuators). + +-- ----------------------------------------------------------------------------- +-- Listing definitions +-- ----------------------------------------------------------------------------- +-- Every schema-v1 definition loaded from resources/buildings/parts and +-- prefab companions: id, tags, layer ("normal"/"thin-floor"/"thin-wall"/ +-- "ceiling") and selection weight. Read-only; editing goes through the +-- Blender export or the Create Prefab furniture mode. + +for _, def in ipairs(ecs.furniture.definitions()) do + if def.id == "shop-shelf1" then + print("shop-shelf1 tags: " .. table.concat(def.tags, ",")) + end +end + +-- ----------------------------------------------------------------------------- +-- The placement request shape (shared with the kernel and the DSL) +-- ----------------------------------------------------------------------------- +-- definition required, resolved by FurnitureLibrary::findByName +-- (definition id, legacy display name or alias). +-- x/y/z required integer grid cells (y = floor row, usually the +-- room's minY). +-- rotation optional 0..3 quarter turns (0 = north). +-- owner optional, defaults to "script" for Lua placements. +-- Script-owned furniture is NOT cleared by room +-- regeneration - delete it with ecs.furniture.remove. +-- groupId optional; placements sharing one groupId commit as an +-- atomic group (root + children, rollback on any failure). +-- placementId optional stable update handle: placing again with the +-- same id UPDATES that placement instead of failing on the +-- overlap (stable-id updates, see furniture_test test 13). +-- tags optional record of the selecting tag terms. + +local shelf = { + definition = "shop-shelf1", + x = 2, y = 0, z = 9, + rotation = 0, + placementId = "script-shelf-1", +} + +-- ----------------------------------------------------------------------------- +-- Validate and commit (atomic, through FurniturePlacement) +-- ----------------------------------------------------------------------------- +-- can_place/commit take (gridUid, request-or-array, roomId). Both +-- return ok, error and both validate against the SAME rules the C++ +-- evaluators use: room containment, grid cells, floor/wall +-- requirements, door/window blockage, definition overlap and (for +-- commit) the whole batch against committed placements AND itself. + +local ok, err = ecs.furniture.can_place("11111111-2222-3333-4444-555555555555", + shelf, "store-floor") +if not ok then + print("shelf rejected: " .. (err or "?")) +end + +-- Commit is all-or-nothing: a two-item arrangement that fails on the +-- second item leaves nothing behind. +local ok, err = ecs.furniture.commit( + "11111111-2222-3333-4444-555555555555", + { + { definition = "shop-shelf1", x = 1, y = 0, z = 9, rotation = 0, + placementId = "script-shelf-a" }, + { definition = "counter1", x = 3, y = 0, z = 4, rotation = 0, + placementId = "script-counter" }, + }, "store-floor") +if not ok then + print("store layout rejected: " .. (err or "?")) +end + +-- Removal by placement id; returns the number of cells removed (0 when +-- the id is unknown). +ecs.furniture.remove("11111111-2222-3333-4444-555555555555", + "script-shelf-a") + +-- ----------------------------------------------------------------------------- +-- Ordered room-generation events (on_room_before / on_room_after) +-- ----------------------------------------------------------------------------- +-- Handlers run in registration order around RoomLayoutSystem's default +-- evaluator for EVERY reprocessed room (grid rebuild, save/load, room +-- edit). The context table is a snapshot of the room; BEFORE handlers +-- mutate it in place and their edits are read back as one transaction: +-- +-- context fields: gridUid, roomId, roomType, tags, seed, +-- suppress_default (bool), placements (array of +-- request tables - read pushEvent/requestFromTable +-- in lua/LuaFurnitureApi.cpp for the exact shape). +-- +-- - Mutating placements adds script-owned furniture committed +-- atomically right after the handlers; any rejection rolls the +-- whole transaction back and falls back to the default evaluator +-- (a bad script cannot strand a room empty). +-- - context.suppress_default = true skips the default evaluator for +-- that room (fully scripted rooms). +-- - A handler error is logged with the room id and treated the same +-- as a rejection. +-- on_room_after handlers are read-only observers of the final plan. +-- Both return registry refs for off_room_before/off_room_after. + +local before_ref = ecs.furniture.on_room_before(function(context) + -- Example: furnish every tag-matching workshop room from script. + for _, tag in ipairs(context.tags) do + if tag == "garage" then + context.suppress_default = true + context.placements = { + { definition = "workbench1", x = 1, y = 0, z = 1, + rotation = 1, placementId = "script-bench" }, + { definition = "bay1", x = 4, y = 0, z = 1, + rotation = 1, placementId = "script-bay" }, + } + end + end +end) + +ecs.furniture.on_room_after(function(context) + print("room " .. context.roomId .. " furnished with " .. + #context.placements .. " script placements") +end) + +-- ...and undo: +-- ecs.furniture.off_room_before(before_ref) + +-- ----------------------------------------------------------------------------- +-- Querying furniture POIs +-- ----------------------------------------------------------------------------- +-- POI ids are stable across grid rebuilds and scene loads: +-- furniture.. +-- with a deterministic "#N" suffix when one placement exports the same +-- marker name twice. Query filters (any combination): gridUid, +-- roomId, furnitureTag, actionName, type ("action" for action-* / +-- behavior markers, "attachment" for plain position-* markers). + +local pois = ecs.furniture.query_pois({ + gridUid = "11111111-2222-3333-4444-555555555555", + roomId = "store-floor", + -- furnitureTag = "shop", actionName = "work", type = "attachment", +}) + +for _, poi in ipairs(pois) do + -- poi: id, entity_id, placementId, name, type, actionName, roomId, + -- furnitureType, position {x,y,z} (grid-local render space), + -- rotation {w,x,y,z}. + print(string.format("POI %s at (%.2f, %.2f, %.2f)", + poi.id, poi.position.x, poi.position.y, + poi.position.z)) +end + +-- Direct lookup of one stable id (nil + reason when unknown - POI +-- entities are runtime-only and regenerated, so always re-resolve +-- after a grid rebuild): +local poi = ecs.furniture.find_poi("furniture.script-counter.work") +if poi then + print("counter POI action: " .. tostring(poi.actionName)) +end diff --git a/src/features/editScene/systems/ActuatorSystem.hpp b/src/features/editScene/systems/ActuatorSystem.hpp index 02f0998..8892eba 100644 --- a/src/features/editScene/systems/ActuatorSystem.hpp +++ b/src/features/editScene/systems/ActuatorSystem.hpp @@ -54,6 +54,16 @@ public: return m_menuOpen; } + /** Begin executing `actionName` on `actuatorEntity` for + * `character` (the path an E press takes). The execution ticks + * in update()/isActionComplete() until the action's behavior tree + * finishes, then starts the cooldown; that block runs before the + * ImGui gate, so headless tests can drive actuators through + * this. */ + void executeAction(flecs::entity character, + flecs::entity actuatorEntity, + const Ogre::String &actionName); + private: struct ScreenActuator { flecs::entity entity; @@ -65,9 +75,6 @@ private: Ogre::Vector2 projectToScreen(const Ogre::Vector3 &worldPoint); bool isInRange(const Ogre::Vector3 &charPos, const Ogre::Vector3 &objPos, float radius, float height); - void executeAction(flecs::entity character, - flecs::entity actuatorEntity, - const Ogre::String &actionName); void executeItemAction(flecs::entity character, flecs::entity itemEntity, const Ogre::String &actionName);