From 1159bb5a475c2f6dfcbef5655bbe96f1db229791 Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Fri, 11 Sep 2026 16:38:12 +0300 Subject: [PATCH] Visual barriers for doors are now black --- src/features/editScene/AGENTS.md | 37 ++-- src/features/editScene/GameFeatures202609.md | 40 +++- src/features/editScene/components/Door.hpp | 8 +- .../demo_main.cpp | 37 ++++ .../demo_scene_exterior.json | 112 +++++------ .../demo-scene-switching-extra/demo_main.cpp | 62 ++++++ .../editScene/systems/DoorBuilder.cpp | 188 ++++++++++++++---- .../editScene/systems/DoorBuilder.hpp | 11 +- src/features/editScene/systems/DoorSystem.cpp | 12 +- src/features/editScene/systems/DoorSystem.hpp | 16 +- .../editScene/tests/cellgrid_door_test.cpp | 7 + 11 files changed, 401 insertions(+), 129 deletions(-) diff --git a/src/features/editScene/AGENTS.md b/src/features/editScene/AGENTS.md index ee97798..02ce1e9 100644 --- a/src/features/editScene/AGENTS.md +++ b/src/features/editScene/AGENTS.md @@ -633,14 +633,25 @@ sceneSwitchTarget})` and emits the `door_scene_switch` event (params: `path`, `target`, `door_id`) only when the leaf reaches `openAngle` — the existing loading cover hides the transition. Pressing E on an already-open scene-switch door switches immediately; a close cancels a -pending switch. The prompt stays "E Open" (never "E Close"). The door -builder also adds an unlit black occluder box (`CellGridDoorOccluderBox` -mesh + `CellGridDoorOccluderBlack` material) covering the doorway a few -cm behind the closed leaf plane, childed to the grid node (it does NOT -swing with the hinge) and tracked via `DoorComponent::occluder`; -`DoorSystem` hides it while the door is fully closed so the player never -sees the missing room interior through the opened doorway before the -switch fires. +pending switch. The prompt stays "E Open" (never "E Close"). Unlike normal +persistent doors, scene-switch doors never restore the persisted open +state: `DoorSystem::declareDoorDefaults(..., sceneSwitchDoor = true)` +resets their stored `isOpen`, so a door the player left through is always +closed again when the scene reloads (before the loading cover lifts). The +door builder also adds a black occluder covering the doorway, childed to +the grid node (it does NOT swing with the hinge) and tracked via +`DoorComponent::occluder`; `DoorSystem` hides it while the door is fully +closed so the player never sees the missing room interior through the +opened doorway before the switch fires. Doors swinging TOWARDS the player +get a flat box (`CellGridDoorOccluderBox` mesh) a few cm behind the closed +leaf plane; doors swinging AWAY (`doorSwingReversed`) get an open-front +tunnel mesh (`CellGridDoorOccluderTunnel_*`, shared per size) whose back +panel sits behind the fully-open leaf sweep, with side/top/bottom walls +flared 1 cm into the frame so no sliver of the void shows around the +frame. The shared `CellGridDoorOccluderBlack` material keeps lighting +ENABLED with all-black colours (diffuse/ambient/specular/emissive) — +with lighting disabled RTSS ignores material colours and renders the pass +white. New `CellGridComponent` fields (serialized in the scene JSON and exposed to Lua): `doorsEnabled`, `doorRectName`, `doorMeshName`, `doorUseMeshMaterial`, @@ -745,10 +756,12 @@ rebuilds, scene switches and save/load (the save file's `globalState` section). Ephemeral doors (empty `doorId`) never touch the store. - **Defaults & restore**: `DoorBuilder::build()` calls - `DoorSystem::declareDoorDefaults(doorId, lockedByDefault)` (declares the - store defaults, returns the persisted open state) and snaps a door that - was left open straight to `openAngle` with its collider disabled — no - swing animation on load. + `DoorSystem::declareDoorDefaults(doorId, lockedByDefault, + sceneSwitchDoor)` (declares the store defaults, returns the persisted + open state) and snaps a door that was left open straight to `openAngle` + with its collider disabled — no swing animation on load. Scene-switch + doors (`sceneSwitchDoor = true`) always reopen closed instead and their + stored `isOpen` is reset, so a door left through is never ajar on return. - **Write path**: `DoorSystem::update()` stores `isOpen` when a swing completes. - **Locked state**: `DoorSystem::isDoorLocked()` / `isDoorLockedById()` / diff --git a/src/features/editScene/GameFeatures202609.md b/src/features/editScene/GameFeatures202609.md index f5842b1..e601c2e 100644 --- a/src/features/editScene/GameFeatures202609.md +++ b/src/features/editScene/GameFeatures202609.md @@ -604,7 +604,10 @@ editor-validated-unique `doorId` field on standalone doors (F2). F6 consumes isOpen)` when a swing **completes** (reaches 0 or `openAngle`). - Door creation (`buildDoorEntities`, standalone builder) reads the store and snaps `currentAngle`/`isOpen`/collider to the persisted state - instantly (no swing animation on load). + instantly (no swing animation on load) — except scene-switch doors, + which always reopen closed (`declareDoorDefaults(..., + sceneSwitchDoor = true)` resets their stored `isOpen`), so a door left + through is never ajar when the player returns. - Survives scene switches: the store is a singleton and scene switch does not touch it; a door re-created in a revisited scene restores its state. @@ -768,11 +771,22 @@ switch via the toggle path). `DoorSystem` fires the switch — and the new the leaf reaches `openAngle` (`setEditorApp()` wires the `EditorApp` call; without it only the event fires, which is what the headless test uses). The door builder creates the black occluder (shared unit-box mesh -`CellGridDoorOccluderBox` + unlit `CellGridDoorOccluderBlack` material), +`CellGridDoorOccluderBox` + `CellGridDoorOccluderBlack` material), childed to the grid node so it does not swing, sized from the closed-leaf collider extents and pushed 2 cm behind the leaf plane; `DoorSystem` -hides it while fully closed. Tests: `cellgrid_door_test` test 10 (event -timing — nothing mid-swing, exactly once at full opening, close cancels). +hides it while fully closed. Doors swinging AWAY from the player +(`doorSwingReversed`) instead get an open-front tunnel mesh +(`CellGridDoorOccluderTunnel_*`, shared per doorway size): the back panel +sits behind the fully-open leaf sweep so the swinging leaf stays visible, +and side/top/bottom walls flared 1 cm into the frame keep the darkness +hole-free. The occluder material keeps lighting enabled with all-black +colours — an unlit pass renders white under RTSS (FFPColour defaults the +output to white when no lighting SRS runs). Scene-switch doors never +restore the persisted open state: `declareDoorDefaults()` resets their +stored `isOpen`, so they are always closed again when the scene reloads +(before the loading cover lifts). Tests: `cellgrid_door_test` test 10 +(event timing — nothing mid-swing, exactly once at full opening, close +cancels) plus the `declareDoorDefaults` scene-switch reset in test 8. Demo: `demo-scene-switching-extra` scene A exit doorway `Z:0:0:15` is a scene-switch door to scene B; `--headless --test-switch` does the A→B leg through it and asserts the switch queues only at full opening (the @@ -794,13 +808,17 @@ sub-millisecond while swings are real-time). `EditorApp::switchScene(sceneSwitchPath, {targetEntityName: sceneSwitchTarget})`. The existing loading cover hides the switch. - **Black occluder:** `buildDoorEntities()` (and the F2 standalone builder) - additionally creates an unlit black box ("cube") entity childed to the - door entity, sized to the doorway opening, placed a few cm behind the - closed leaf plane, for doors with a non-empty `sceneSwitchPath`. Its - visibility follows the door state: hidden while fully closed, visible - while open/swinging (`RenderQueue`/visible flag toggled by `DoorSystem` - from `currentAngle`), so the player never sees the missing room interior - behind the opened doorway before the switch fires. + additionally creates a black occluder entity childed to the door entity, + for doors with a non-empty `sceneSwitchPath`. Doors swinging towards the + player get a flat box sized to the doorway opening, placed a few cm + behind the closed leaf plane; doors swinging away (`swingReversed`) get + an open-front tunnel whose back panel sits behind the fully-open leaf + sweep, with side/top/bottom walls closing the gaps around the frame. + Its visibility follows the door state: hidden while fully closed, visible + while open/swinging (visible flag toggled by `DoorSystem` from + `currentAngle`), so the player never sees the missing room interior + behind the opened doorway before the switch fires. The material is lit + with all-black colours (unlit passes render white under RTSS). - The prompt stays "E Open" (never "E Close") for scene-switch doors, as today. diff --git a/src/features/editScene/components/Door.hpp b/src/features/editScene/components/Door.hpp index a8549ae..9ce9edd 100644 --- a/src/features/editScene/components/Door.hpp +++ b/src/features/editScene/components/Door.hpp @@ -70,10 +70,12 @@ struct DoorComponent { // reaches openAngle. Runtime only, set by ActuatorSystem. bool sceneSwitchPending = false; - // F1: unlit black box covering the doorway of a scene-switch door, + // F1: black box covering the doorway of a scene-switch door, // created by the door builder a few cm behind the closed leaf plane - // (child of the grid node, so it does NOT swing with the hinge). - // Hidden while the door is fully closed. Runtime only. + // (an open-front tunnel behind the fully-open leaf sweep for doors + // swinging away from the player); child of the grid node, so it + // does NOT swing with the hinge. Hidden while the door is fully + // closed. Runtime only. Ogre::Entity *occluder = nullptr; }; diff --git a/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_main.cpp b/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_main.cpp index 3d5fd95..e5f9b94 100644 --- a/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_main.cpp +++ b/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_main.cpp @@ -482,6 +482,21 @@ struct SceneSwitchTestListener : public Ogre::FrameListener { } break; } + /* The exit door swings TOWARDS the player, so it + * keeps the flat occluder box. */ + { + const DoorComponent &d = + findDoorById(exitDoorId) + .get(); + if (!d.occluder || + d.occluder->getMesh()->getName() != + "CellGridDoorOccluderBox") { + failed = true; + failReason = + "F1 swing-towards door lost the flat occluder"; + break; + } + } if (!requestDoorSwitch(exitDoorId)) break; phase = 13; @@ -608,6 +623,28 @@ struct SceneSwitchTestListener : public Ogre::FrameListener { "after scene switch" << std::endl; } + /* F1: the scene-switch exit door rebuilt with the + * scene must be closed again - scene-switch doors + * never restore the persisted open state. */ + { + flecs::entity exitDoor = + findDoorById(exitDoorId); + if (!exitDoor.is_alive()) + break; + const DoorComponent &d = + exitDoor.get(); + if (d.isOpen || d.currentAngle != 0.0f || + GlobalStateStore::getInstance().getBool( + "door." + exitDoorId + ".isOpen")) { + failed = true; + failReason = + "F1 exit door not closed after scene switch"; + break; + } + std::cout << "[test] F1 exit door closed after " + "scene switch" + << std::endl; + } if (!failed) { std::cout << "[test] arrived back in the interior " "scene at arrival_a, camera faces the " diff --git a/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_scene_exterior.json b/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_scene_exterior.json index e2c1a67..a12a292 100644 --- a/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_scene_exterior.json +++ b/src/features/editScene/demos/demo-interior-exterior-dynamics/demo_scene_exterior.json @@ -110,9 +110,9 @@ }, "transform": { "position": { - "x": 4000.6, - "y": 9.9905, - "z": 3995.9 + "x": 4000.60009765625, + "y": 9.990500450134277, + "z": 3995.89990234375 }, "rotation": { "w": 0.0, @@ -217,7 +217,7 @@ "doorRectName": "", "doorSceneSwitchPath": "demo_scene_interior.json", "doorSceneSwitchTarget": "arrival_a", - "doorSwingReversed": true, + "doorSwingReversed": false, "doorUseMeshMaterial": false, "doorsEnabled": true, "extDoorFrameRectName": "", @@ -365,7 +365,7 @@ "transform": { "position": { "x": 4000.0, - "y": 10.0148, + "y": 10.014800071716309, "z": 4000.0 }, "rotation": { @@ -442,14 +442,14 @@ "spawnDistance": 100.0 }, "children": [], - "id": 495, + "id": 494, "name": { "name": "s1" }, "transform": { "position": { "x": 4000.0, - "y": 9.9039, + "y": 9.903900146484375, "z": 3975.0 }, "rotation": { @@ -467,7 +467,7 @@ }, { "children": [], - "id": 496, + "id": 495, "name": { "name": "player" }, @@ -517,7 +517,7 @@ }, { "children": [], - "id": 497, + "id": 496, "name": { "name": "terrain" }, @@ -525,7 +525,7 @@ "auxMaps": [], "baseNoise": { "amplitude": 15.0, - "frequency": 0.0003, + "frequency": 0.0003000000142492354, "lacunarity": 2.0, "octaves": 3, "persistence": 0.5, @@ -536,7 +536,7 @@ "detailNoise": { "amplitude": 10.0, "enabled": false, - "frequency": 0.006, + "frequency": 0.006000000052154064, "lacunarity": 2.0, "octaves": 4, "persistence": 0.5, @@ -576,12 +576,12 @@ "roadLodDistance": 200.0, "roadMaterialName": "RoadMaterial", "roadMeshTemplate": "road_segment.mesh", - "roadThickness": 0.3, + "roadThickness": 0.30000001192092896, "roadVisibilityDistance": 1000.0, "sidewalkEnabled": false, - "sidewalkHeight": 0.15, + "sidewalkHeight": 0.15000000596046448, "sidewalkMeshTemplate": "", - "sidewalkThickness": 0.3, + "sidewalkThickness": 0.30000001192092896, "sidewalkWidth": 1.5 }, "streamingEnabled": true, @@ -611,89 +611,89 @@ }, { "children": [], - "id": 498, + "id": 497, "name": { "name": "sky" }, "skybox": { "cloudiness": 0.0, "dayBottomColor": [ - 0.6, - 0.8, + 0.6000000238418579, + 0.800000011920929, 1.0 ], "dayTopColor": [ - 0.2, + 0.20000000298023224, 0.5, 1.0 ], "enabled": true, - "moonSize": 0.03, + "moonSize": 0.029999999329447746, "nightBottomColor": [ - 0.05, - 0.05, - 0.15 + 0.05000000074505806, + 0.05000000074505806, + 0.15000000596046448 ], "nightTopColor": [ 0.0, 0.0, - 0.05 + 0.05000000074505806 ], "size": 443.0, "starsEnabled": false, - "sunSize": 0.05, + "sunSize": 0.05000000074505806, "sunriseColor": [ 1.0, 0.5, - 0.2 + 0.20000000298023224 ], "sunsetColor": [ 1.0, - 0.3, - 0.1 + 0.30000001192092896, + 0.10000000149011612 ] }, "sun": { "ambientDay": [ - 0.3, - 0.3, - 0.3 + 0.30000001192092896, + 0.30000001192092896, + 0.30000001192092896 ], "ambientNight": [ - 0.05, - 0.05, - 0.15 + 0.05000000074505806, + 0.05000000074505806, + 0.15000000596046448 ], "ambientSunrise": [ - 0.3, - 0.2, - 0.15 + 0.30000001192092896, + 0.20000000298023224, + 0.15000000596046448 ], "ambientSunset": [ 0.25, - 0.15, - 0.1 + 0.15000000596046448, + 0.10000000149011612 ], "castShadows": true, "enabled": true, - "intensity": 1.79, + "intensity": 1.7899999618530273, "moonColor": [ - 0.3, - 0.3, + 0.30000001192092896, + 0.30000001192092896, 0.5 ], - "moonSphereSize": 3.4, + "moonSphereSize": 3.4000000953674316, "orbitTilt": 15.0, "showMoonSphere": true, "showSunSphere": true, "sunColor": [ 1.0, - 0.95, - 0.8 + 0.949999988079071, + 0.800000011920929 ], "sunSphereSize": 5.0, - "timeOfDay": 6.226653575897217, - "timeSpeed": 0.13 + "timeOfDay": 8.35942554473877, + "timeSpeed": 0.12999999523162842 }, "transform": { "position": { @@ -716,7 +716,7 @@ }, { "children": [], - "id": 499, + "id": 498, "name": { "name": "water" }, @@ -739,12 +739,12 @@ } }, "waterPhysics": { - "defaultAngularDrag": 0.05, + "defaultAngularDrag": 0.05000000074505806, "defaultBuoyancy": 1.0, "defaultLinearDrag": 0.25, - "defaultSubmergedThreshold": 0.1, + "defaultSubmergedThreshold": 0.10000000149011612, "enabled": true, - "gravity": 9.81, + "gravity": 9.8100004196167, "waterDensity": 1000.0, "waterSurfaceY": 6.0 }, @@ -752,18 +752,18 @@ "autoUpdateFromWaterPhysics": true, "enabled": true, "planeSize": 12000.0, - "reflectivity": 0.38, + "reflectivity": 0.3799999952316284, "renderTextureSize": 512, - "tiling": 0.012, + "tiling": 0.012000000104308128, "waterColor": [ 0.0, - 0.3, + 0.30000001192092896, 0.5, - 0.8 + 0.800000011920929 ], "waterSurfaceY": 6.0, - "waveScale": 0.031, - "waveSpeed": 0.98 + "waveScale": 0.03099999949336052, + "waveSpeed": 0.9800000190734863 } } ], diff --git a/src/features/editScene/demos/demo-scene-switching-extra/demo_main.cpp b/src/features/editScene/demos/demo-scene-switching-extra/demo_main.cpp index e571c2f..d2f75dc 100644 --- a/src/features/editScene/demos/demo-scene-switching-extra/demo_main.cpp +++ b/src/features/editScene/demos/demo-scene-switching-extra/demo_main.cpp @@ -374,6 +374,21 @@ struct SceneSwitchTestListener : public Ogre::FrameListener { } break; } + /* The exit door swings TOWARDS the player, so it + * keeps the flat occluder box. */ + { + const DoorComponent &d = + findDoorById(exitDoorId) + .get(); + if (!d.occluder || + d.occluder->getMesh()->getName() != + "CellGridDoorOccluderBox") { + failed = true; + failReason = + "F1 swing-towards door lost the flat occluder"; + break; + } + } if (!requestDoorSwitch(exitDoorId)) break; phase = 13; @@ -390,6 +405,31 @@ struct SceneSwitchTestListener : public Ogre::FrameListener { case 1: if (!entityExists("x1")) break; + /* F1: the return door swings AWAY from the player + * (doorSwingReversed in demo_scene_b.json), so it + * must have the deep tunnel occluder that keeps + * the swinging leaf visible. */ + { + flecs::entity returnDoor = + findDoorById(returnDoorId); + if (returnDoor.is_alive()) { + const DoorComponent &d = + returnDoor.get(); + if (!d.occluder || + d.occluder->getMesh() + ->getName() + .find("CellGridDoorOccluderTunnel") != + 0) { + failed = true; + failReason = + "F1 swing-away door has no tunnel occluder"; + break; + } + std::cout << "[test] F1 swing-away door " + "has tunnel occluder" + << std::endl; + } + } if (!checkPlayerNear(Ogre::Vector3(0.0f, 0.0f, 24.0f), 1.0f)) break; @@ -447,6 +487,28 @@ struct SceneSwitchTestListener : public Ogre::FrameListener { "after scene switch" << std::endl; } + /* F1: the scene-switch exit door rebuilt with the + * scene must be closed again - scene-switch doors + * never restore the persisted open state. */ + { + flecs::entity exitDoor = + findDoorById(exitDoorId); + if (!exitDoor.is_alive()) + break; + const DoorComponent &d = + exitDoor.get(); + if (d.isOpen || d.currentAngle != 0.0f || + GlobalStateStore::getInstance().getBool( + "door." + exitDoorId + ".isOpen")) { + failed = true; + failReason = + "F1 exit door not closed after scene switch"; + break; + } + std::cout << "[test] F1 exit door closed after " + "scene switch" + << std::endl; + } if (!failed) { std::cout << "[test] arrived back in scene A " "at arrival_a, camera faces the " diff --git a/src/features/editScene/systems/DoorBuilder.cpp b/src/features/editScene/systems/DoorBuilder.cpp index c4835e2..e51cde4 100644 --- a/src/features/editScene/systems/DoorBuilder.cpp +++ b/src/features/editScene/systems/DoorBuilder.cpp @@ -14,8 +14,122 @@ #include #include #include +#include #include #include +#include +#include + +/* F1: shared occluder resources: the flat unit box and the black + * material. NOTE: the material must keep lighting ENABLED with + * all-black colours - with lighting disabled RTSS ignores the material + * colours (FFPColour defaults the pass output to white), which made the + * occluder render white. */ +static void ensureOccluderResources(const Ogre::String &group) +{ + if (Ogre::MeshManager::getSingleton() + .getByName("CellGridDoorOccluderBox", group) + .isNull()) { + Procedural::BoxGenerator boxGen; + boxGen.setSizeX(1.0f).setSizeY(1.0f).setSizeZ(1.0f); + boxGen.realizeMesh("CellGridDoorOccluderBox", group); + } + if (Ogre::MaterialManager::getSingleton() + .getByName("CellGridDoorOccluderBlack", group) + .isNull()) { + Ogre::MaterialPtr mat = + Ogre::MaterialManager::getSingleton().create( + "CellGridDoorOccluderBlack", group); + Ogre::Pass *pass = mat->getTechnique(0)->getPass(0); + pass->setDiffuse(Ogre::ColourValue::Black); + pass->setAmbient(Ogre::ColourValue::Black); + pass->setSpecular(Ogre::ColourValue::Black); + pass->setEmissive(Ogre::ColourValue::Black); + pass->setCullingMode(Ogre::CULL_NONE); + } +} + +/* F1: open-front black tunnel for scene-switch doors that swing AWAY + * from the player (swingReversed): the back panel sits behind the + * fully-open leaf sweep so the swinging leaf stays visible, and + * side/top/bottom walls close the gaps around the frame so no sliver of + * the ungenerated void shows. The coordinates are baked into the mesh + * in hinge-local space (+X along the doorway from the hinge, +Z away + * from the player), so the occluder node gets no position offset and no + * scale. Meshes are shared between doors of equal size. */ +static Ogre::String +occluderTunnelMesh(const Ogre::Vector3 ¢er, + const Ogre::Vector3 &halfExtents, + const Ogre::String &group) +{ + const float hx = halfExtents.x; + const float hy = halfExtents.y; + const float hz = halfExtents.z; + /* The leaf spans [0, x1] from the hinge; its tip sweeps at most + * x1 behind the closed plane (openAngle >= 90). Keep the back + * panel one leaf thickness plus a small margin behind that. */ + const float x0 = center.x - hx, x1 = center.x + hx; + const float y0 = center.y - hy, y1 = center.y + hy; + const float z0 = center.z + hz + 0.02f; + const float z1 = z0 + x1 + hz * 2.0f + 0.05f; + /* Flare the walls 1 cm INTO the frame/jamb: no gap at the + * opening edges and no z-fighting with the jamb faces. */ + const float f = 0.01f; + + char name[160]; + snprintf(name, sizeof(name), + "CellGridDoorOccluderTunnel_%d_%d_%d_%d_%d_%d", + (int)lroundf(center.x * 1000.0f), + (int)lroundf(center.y * 1000.0f), + (int)lroundf(center.z * 1000.0f), + (int)lroundf(hx * 1000.0f), (int)lroundf(hy * 1000.0f), + (int)lroundf(hz * 1000.0f)); + if (!Ogre::MeshManager::getSingleton().getByName(name, group).isNull()) + return name; + + const float x0f = x0 - f, x1f = x1 + f; + const float y0f = y0 - f, y1f = y1 + f; + + Ogre::ManualObject mo(name); + mo.begin("CellGridDoorOccluderBlack", + Ogre::RenderOperation::OT_TRIANGLE_LIST); + auto quad = [&mo](const Ogre::Vector3 &a, const Ogre::Vector3 &b, + const Ogre::Vector3 &c, const Ogre::Vector3 &d, + const Ogre::Vector3 &normal) { + Ogre::uint32 base = mo.getCurrentVertexCount(); + mo.position(a); + mo.normal(normal); + mo.position(b); + mo.normal(normal); + mo.position(c); + mo.normal(normal); + mo.position(d); + mo.normal(normal); + mo.triangle(base, base + 1, base + 2); + mo.triangle(base, base + 2, base + 3); + }; + /* Back panel (facing the player, -Z). */ + quad(Ogre::Vector3(x0f, y0f, z1), Ogre::Vector3(x1f, y0f, z1), + Ogre::Vector3(x1f, y1f, z1), Ogre::Vector3(x0f, y1f, z1), + Ogre::Vector3::NEGATIVE_UNIT_Z); + /* Left / right walls (facing into the tunnel). */ + quad(Ogre::Vector3(x0f, y0f, z0), Ogre::Vector3(x0f, y1f, z0), + Ogre::Vector3(x0f, y1f, z1), Ogre::Vector3(x0f, y0f, z1), + Ogre::Vector3::UNIT_X); + quad(Ogre::Vector3(x1f, y0f, z0), Ogre::Vector3(x1f, y0f, z1), + Ogre::Vector3(x1f, y1f, z1), Ogre::Vector3(x1f, y1f, z0), + Ogre::Vector3::NEGATIVE_UNIT_X); + /* Bottom / top walls. */ + quad(Ogre::Vector3(x0f, y0f, z0), Ogre::Vector3(x1f, y0f, z0), + Ogre::Vector3(x1f, y0f, z1), Ogre::Vector3(x0f, y0f, z1), + Ogre::Vector3::UNIT_Y); + quad(Ogre::Vector3(x0f, y1f, z0), Ogre::Vector3(x0f, y1f, z1), + Ogre::Vector3(x1f, y1f, z1), Ogre::Vector3(x1f, y1f, z0), + Ogre::Vector3::NEGATIVE_UNIT_Y); + mo.end(); + mo.convertToMesh(name, group); + return name; +} flecs::entity DoorBuilder::build(flecs::world &world, Ogre::SceneManager *sceneMgr, @@ -121,11 +235,15 @@ flecs::entity DoorBuilder::build(flecs::world &world, /* F6: declare the store defaults and snap the door to the * persisted open state (rebuilds and reloads restore the - * state the door had when it was saved/left). */ + * state the door had when it was saved/left). Scene-switch + * doors are the exception: they always reopen closed (the + * switch fires from the fully-open pose, so a persisted + * "open" would restore the door ajar on return). */ bool wasOpen = false; if (!door.doorId.empty()) { wasOpen = DoorSystem::declareDoorDefaults( - door.doorId, params.lockedByDefault); + door.doorId, params.lockedByDefault, + !door.sceneSwitchPath.empty()); } if (wasOpen) { door.isOpen = true; @@ -164,37 +282,45 @@ flecs::entity DoorBuilder::build(flecs::world &world, collider.offset = colliderCenter; colliderEntity.set(collider); - /* F1: scene-switch doors get an unlit black occluder box - * covering the doorway, a few cm behind the closed leaf - * plane, so the player never sees the missing room - * interior through the opened doorway before the switch - * fires. The node hangs off the PARENT node (not the - * hinge), so it does not swing with the leaf; DoorSystem + /* F1: scene-switch doors get a black occluder covering the + * doorway, so the player never sees the missing room interior + * through the opened doorway before the switch fires. Doors + * swinging TOWARDS the player keep the flat box a few cm behind + * the closed leaf plane; doors swinging AWAY (swingReversed) get + * an open-front tunnel whose back panel sits behind the + * fully-open leaf sweep, with side/top/bottom walls closing the + * gaps around the frame. The node hangs off the PARENT node + * (not the hinge), so it does not swing with the leaf; DoorSystem * toggles its visibility from the swing angle. */ if (!door.sceneSwitchPath.empty()) { const Ogre::String group = Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME; - if (Ogre::MeshManager::getSingleton() - .getByName("CellGridDoorOccluderBox", group) - .isNull()) { - Procedural::BoxGenerator boxGen; - boxGen.setSizeX(1.0f).setSizeY(1.0f).setSizeZ(1.0f); - boxGen.realizeMesh("CellGridDoorOccluderBox", group); - } - if (Ogre::MaterialManager::getSingleton() - .getByName("CellGridDoorOccluderBlack", group) - .isNull()) { - Ogre::MaterialPtr mat = - Ogre::MaterialManager::getSingleton().create( - "CellGridDoorOccluderBlack", group); - Ogre::Pass *pass = mat->getTechnique(0)->getPass(0); - pass->setLightingEnabled(false); - pass->setDiffuse(Ogre::ColourValue::Black); + ensureOccluderResources(group); + + Ogre::String meshName = "CellGridDoorOccluderBox"; + Ogre::Vector3 occScale(colliderHalfExtents.x * 2.0f, + colliderHalfExtents.y * 2.0f, + 0.05f); + Ogre::Vector3 occPos = + hingePos + + doorwayRot * + (colliderCenter + + Ogre::Vector3(0, 0, + colliderHalfExtents.z + + 0.02f)); + if (params.swingReversed) { + /* Swing-away door: deep tunnel occluder with the + * geometry baked into the mesh (hinge-local). */ + meshName = occluderTunnelMesh(colliderCenter, + colliderHalfExtents, + group); + occScale = Ogre::Vector3::UNIT_SCALE; + occPos = hingePos; } Ogre::Entity *occEnt = nullptr; try { - occEnt = sceneMgr->createEntity("CellGridDoorOccluderBox"); + occEnt = sceneMgr->createEntity(meshName); } catch (const std::exception &e) { Ogre::LogManager::getSingleton().logMessage( "DoorBuilder: Error creating door occluder: " + @@ -204,16 +330,6 @@ flecs::entity DoorBuilder::build(flecs::world &world, occEnt->setMaterialName("CellGridDoorOccluderBlack"); occEnt->setVisible(wasOpen); - Ogre::Vector3 occScale(colliderHalfExtents.x * 2.0f, - colliderHalfExtents.y * 2.0f, - 0.05f); - Ogre::Vector3 occPos = - hingePos + - doorwayRot * - (colliderCenter + - Ogre::Vector3(0, 0, - colliderHalfExtents.z + - 0.02f)); Ogre::SceneNode *occNode = parentNode->createChildSceneNode(); occNode->setPosition(occPos); @@ -227,7 +343,7 @@ flecs::entity DoorBuilder::build(flecs::world &world, {occNode, occPos, doorwayRot, occScale}); RenderableComponent occRenderable; occRenderable.entity = occEnt; - occRenderable.meshName = "CellGridDoorOccluderBox"; + occRenderable.meshName = meshName; occEntity.set(occRenderable); doorEntity.get_mut().occluder = occEnt; diff --git a/src/features/editScene/systems/DoorBuilder.hpp b/src/features/editScene/systems/DoorBuilder.hpp index 7b603e6..a16feb4 100644 --- a/src/features/editScene/systems/DoorBuilder.hpp +++ b/src/features/editScene/systems/DoorBuilder.hpp @@ -18,13 +18,16 @@ * leaf, DoorComponent, RigidBodyComponent static, ActuatorComponent) * ├── collider child (PhysicsColliderComponent box of the closed * │ leaf; disabled by DoorSystem while not fully closed) - * └── occluder child (F1, scene-switch doors only: unlit black box - * behind the leaf plane, node on the PARENT node so it does not - * swing; DoorSystem toggles its visibility) + * └── occluder child (F1, scene-switch doors only: black box + * behind the leaf plane - an open-front tunnel for doors + * swinging away from the player -, node on the PARENT node so + * it does not swing; DoorSystem toggles its visibility) * * The builder also declares the F6 store defaults for persistent doors * (DoorSystem::declareDoorDefaults) and snaps a door that was left open - * straight to its persisted pose with the collider disabled. + * straight to its persisted pose with the collider disabled; scene-switch + * doors always reopen closed instead (declareDoorDefaults resets their + * stored isOpen). * * Leaf mesh creation stays with the caller: pass a ready mesh name * (CellGrid generates its UV-mapped procedural leaf; standalone doors diff --git a/src/features/editScene/systems/DoorSystem.cpp b/src/features/editScene/systems/DoorSystem.cpp index dc10563..c65b348 100644 --- a/src/features/editScene/systems/DoorSystem.cpp +++ b/src/features/editScene/systems/DoorSystem.cpp @@ -67,7 +67,7 @@ void DoorSystem::setDoorLocked(const std::string &doorId, bool locked) } bool DoorSystem::declareDoorDefaults(const std::string &doorId, - bool lockedByDefault) + bool lockedByDefault, bool sceneSwitchDoor) { if (doorId.empty()) return false; @@ -75,7 +75,15 @@ bool DoorSystem::declareDoorDefaults(const std::string &doorId, GlobalStateStore &store = GlobalStateStore::getInstance(); store.declareDefault("door." + doorId + ".locked", lockedByDefault); store.declareDefault("door." + doorId + ".isOpen", false); - return store.getBool("door." + doorId + ".isOpen"); + bool isOpen = store.getBool("door." + doorId + ".isOpen"); + if (isOpen && sceneSwitchDoor) { + /* Scene-switch doors always reopen closed (the switch + * fires from the fully-open pose); reset the stored state + * so Lua/queries never see a stale "open". */ + store.set("door." + doorId + ".isOpen", false); + isOpen = false; + } + return isOpen; } Ogre::Quaternion DoorSystem::swingOrientation(const DoorComponent &door, diff --git a/src/features/editScene/systems/DoorSystem.hpp b/src/features/editScene/systems/DoorSystem.hpp index fa69794..bf4a9b6 100644 --- a/src/features/editScene/systems/DoorSystem.hpp +++ b/src/features/editScene/systems/DoorSystem.hpp @@ -20,8 +20,9 @@ * Persistent door state (F6) lives in the GlobalStateStore under * "door..locked" / "door..isOpen" (doorId is the F0 global * door ID ":"; empty = ephemeral door). The system - * writes isOpen when a swing completes; CellGridSystem snaps a rebuilt - * door to the persisted state. Locked state is managed through the + * writes isOpen when a swing completes; the door builders snap a rebuilt + * door to the persisted state (except scene-switch doors, which always + * reopen closed). Locked state is managed through the * static helpers below and through the EventBus: sending * "door_unlock_" or the generic "door_unlock" event (param * door_id) unlocks a door; "door_unlocked_" / "door_unlocked" are @@ -45,10 +46,15 @@ public: static void setDoorLocked(const std::string &doorId, bool locked); /* Declare the store defaults for a persistent door (called by the - * door builders when a door is (re)created) and return the - * persisted open state. */ + * door builders when a door is (re)created) and return the open + * state the door should start in. Scene-switch doors + * (sceneSwitchDoor = true) always reopen closed and their stored + * isOpen is reset: the switch fires from the fully-open pose, so a + * persisted "open" would otherwise restore the door ajar on + * return. */ static bool declareDoorDefaults(const std::string &doorId, - bool lockedByDefault); + bool lockedByDefault, + bool sceneSwitchDoor = false); /* Hinge orientation for a swing angle (F3: swingReversed negates * the applied angle; the stored angles stay positive). Used by diff --git a/src/features/editScene/tests/cellgrid_door_test.cpp b/src/features/editScene/tests/cellgrid_door_test.cpp index 9d80185..4f3ac32 100644 --- a/src/features/editScene/tests/cellgrid_door_test.cpp +++ b/src/features/editScene/tests/cellgrid_door_test.cpp @@ -462,6 +462,13 @@ static int testDoorOpenStatePersisted() if (!DoorSystem::declareDoorDefaults(id, false)) FAIL("persisted open state not returned on rebuild"); + // Scene-switch doors always reopen closed: the persisted open + // state is ignored and the store key is reset. + if (DoorSystem::declareDoorDefaults(id, false, true)) + FAIL("scene-switch door restored open"); + if (store.getBool(openKey)) + FAIL("scene-switch door did not reset the stored open state"); + PASS(); return 0; }