Fixes for doors and arrival positions

This commit is contained in:
2026-10-26 15:22:35 +03:00
parent 88fd97fedd
commit e918ef8c4b
13 changed files with 359 additions and 145 deletions
+21 -3
View File
@@ -1949,6 +1949,14 @@ exact path an E press takes (latch the executing state, lock input, then tick
and the cooldown starts), and the executing block runs before the ImGui gate,
so headless tests drive actuators through it (demo-taxi `--test-furniture`).
`handleDoorPress(character, doorEntity)` is the same for doors — an E press
on a door entity: key-item unlock, then the `door_locked` events with a
same-press fall-through when the synchronous handlers unlock the door (F6
event contract), then toggle open / scene-switch (no swing animation here —
`DoorSystem` consumes `toggleRequested` later in the frame). Also runs
before the ImGui gate, so headless tests drive doors through it (demo-taxi
`--test-switch` F6 leg).
### DoorSystem & CellGrid doors
`CellGridSystem::buildDoorEntities()` spawns one runtime door entity per
@@ -2349,7 +2357,11 @@ section). Ephemeral doors (empty `doorId`) never touch the store.
`keyItemId` and the player inventory contains the item, the door unlocks
(keys are NOT consumed) and the press proceeds normally; otherwise the
events `door_locked_<doorId>` / `door_locked` (params: `door_id`,
`entity_id`) are emitted and nothing happens. Holding E on an unlocked
`entity_id`) are emitted. EventBus dispatch is synchronous, so when a
subscriber unlocks the door inside the handlers (the F6 event contract —
scene scripts answer with `door_unlock_<doorId>`), the SAME press
continues and opens the door; only handlers that leave the lock on
require a second press. Holding E on an unlocked
lockable door opens a small menu with "Close" and "Lock" (Lock requires
the key when `keyItemId` is set).
- **Lua** (`lua/LuaDoorApi.cpp`, example
@@ -2872,8 +2884,14 @@ ecs.switch_scene(path, { position = ..., rotation = { w=1, x=0, y=0, z=0 } })
(`EditorApp::renderSceneSwitchCover`, drawn from the ImGui render listener,
headless-safe) hides the scene for `SCENE_SWITCH_COVER_FRAMES` (45) frames
after each switch, covering the characters' initial T-pose while animations
warm up. It is armed at the end of `performSceneSwitch` and counted down in
`frameRenderingQueued` after `processPendingSceneSwitch()`.
warm up. It is armed TWICE: once at `switchScene()` queue time (game mode),
so the frame that queued the switch already presents with the cover up —
the switch itself then runs synchronously at the top of the next
`frameRenderingQueued` and its teardown+load hitch hides behind the cover
instead of freezing the last scenery frame on screen — and again at the
end of `performSceneSwitch`, which keeps the post-load coverage duration
unchanged. The counter is counted down in `frameRenderingQueued` after
`processPendingSceneSwitch()`.
The `switchScene` behavior tree node (name=scene path) queues the same switch
from AI trees (SmartObject/GOAP) and actuator/player-action trees; it fires
+11
View File
@@ -1394,6 +1394,17 @@ bool EditorApp::switchScene(const Ogre::String &scenePath,
m_pendingSceneSwitchPath = scenePath;
m_pendingSceneSwitchOptions = opts;
m_sceneSwitchPending = true;
/* Raise the loading cover NOW (game mode): the switch itself runs
* synchronously at the top of the next frame and stalls rendering
* for its whole teardown+load duration, so the last presented
* frame (empty scenery with the open door) would otherwise freeze
* on screen through the entire hitch. Arming at queue time makes
* the frame that queued the switch present with the cover already
* up; the re-arm at the end of performSceneSwitch keeps the full
* post-load coverage unchanged. */
if (m_gameMode == GameMode::Game)
m_sceneSwitchCoverFrames = SCENE_SWITCH_COVER_FRAMES;
return true;
}
+8 -3
View File
@@ -626,8 +626,10 @@ editor-validated-unique `doorId` field on standalone doors (F2). F6 consumes
`ecs.send_event("door_unlock_<doorId>")` (or
`ecs.door.unlock(doorId)`, a thin wrapper over
`ecs.global.set("door." .. doorId .. ".locked", false)`) which clears
the locked key; the door does **not** open by itself — the player
presses E again. Because the state is a plain global variable, scripts
the locked key. Because the EventBus dispatches synchronously, the
same press that bumped into the lock continues and opens the door
once a handler accepted; only handlers that keep the lock require a
second press. Because the state is a plain global variable, scripts
can also lock/unlock doors directly with `ecs.global.set()`.
4. If nothing unlocks it, the door stays shut; the prompt shows
"E Locked".
@@ -818,7 +820,10 @@ sub-millisecond while swings are real-time).
- `DoorSystem` detects swing completion (`currentAngle == openAngle` while
`isOpen && sceneSwitchPending`) and only then calls
`EditorApp::switchScene(sceneSwitchPath, {targetEntityName:
sceneSwitchTarget})`. The existing loading cover hides the switch.
sceneSwitchTarget})`. The loading cover is armed at switchScene() queue
time, so the screen goes black the frame the door completes opening —
before the synchronous tear-down/load hitch can freeze the last scenery
frame on screen.
- **Black occluder:** `buildDoorEntities()` (and the F2 standalone builder)
additionally creates a black occluder entity childed to the door entity,
for doors with a non-empty `sceneSwitchPath`. All of them get the same
@@ -3,6 +3,7 @@
#include "EditorApp.hpp"
#include "ProjectConfig.hpp"
#include "camera/EditorCamera.hpp"
#include "systems/ActuatorSystem.hpp"
#include "systems/CharacterRegistry.hpp"
#include "systems/DoorSystem.hpp"
#include "systems/PlayerControllerSystem.hpp"
@@ -427,23 +428,54 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
failReason = "F6 demo door not locked at start";
break;
}
/* Emulate the ActuatorSystem E-press on a locked door;
* the inline scene script answers door_locked with
* door_unlock_<doorId>. */
EventBus::getInstance().send("door_locked", "door_id",
doorId);
/* E-press the locked door through the exact path
* ActuatorSystem takes (handleDoorPress): the
* door_locked events go out and the inline scene
* script answers door_locked with
* door_unlock_<doorId> inside the synchronous
* dispatch, so the SAME press unlocks the door and
* requests the open swing instead of needing a
* second press. */
{
ActuatorSystem *actuators =
app->getActuatorSystem();
if (!actuators) {
failed = true;
failReason = "no ActuatorSystem";
break;
}
actuators->handleDoorPress(
app->getPlayerCharacterEntity(),
door);
if (DoorSystem::isDoorLockedById(doorId)) {
failed = true;
failReason = "F6 same press did not "
"unlock the door";
break;
}
if (!door.get<DoorComponent>()
.toggleRequested) {
failed = true;
failReason = "F6 same press did not "
"request the open swing";
break;
}
std::cout << "[test] F6 locked door: one E "
"press unlocked and opened it"
<< std::endl;
}
phase = 11;
break;
}
case 11: {
/* F6: wait for the scene script to unlock the door
* (the frame timeout covers a missing handler). */
if (DoorSystem::isDoorLockedById(doorId))
break;
/* F6: DoorSystem consumed the same-press open request
* and the swing is under way (waiting here on the
* consumed flag covers any frame-listener ordering). */
flecs::entity door = findDoor();
if (!door.is_alive())
break;
door.get_mut<DoorComponent>().toggleRequested = true;
if (door.get<DoorComponent>().toggleRequested)
break;
std::cout << "[test] F6 door unlocked via scene script, "
"opening"
<< std::endl;
@@ -761,8 +793,9 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
* lockable + locked by default, with a fixed gridUid so its global door
* ID is stable, and the grid entity carries an inline scene script that
* answers the "door_locked" event with "door_unlock_<doorId>" (the door
* event contract; the door never opens by itself, the player presses E
* again). The door's locked and open/closed state lives in the global
* event contract; the dispatch is synchronous, so the same press
* continues and opens the door). The door's locked and open/closed
* state lives in the global
* state store and survives the interior -> exterior -> interior scene
* switches.
*
@@ -2,6 +2,7 @@
#include "EditorApp.hpp"
#include "ProjectConfig.hpp"
#include "camera/EditorCamera.hpp"
#include "systems/ActuatorSystem.hpp"
#include "systems/CharacterRegistry.hpp"
#include "systems/DoorSystem.hpp"
#include "systems/EventBus.hpp"
@@ -145,6 +146,25 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
return found;
}
/* Resolves a named entity's live position (used for the arrival
* markers, whose placement is scene-specific). */
bool entityPosition(const char *name, Ogre::Vector3 &out)
{
bool found = false;
app->getWorld()
->query<EntityNameComponent, TransformComponent>()
.each([&](flecs::entity, EntityNameComponent &n,
TransformComponent &t) {
if (n.name != name)
return;
out = t.node ?
t.node->_getDerivedPosition() :
t.position;
found = true;
});
return found;
}
/* The F6 demo door entity (recreated on every scene load). */
flecs::entity findDoor()
{
@@ -227,7 +247,7 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
const TransformComponent &t = player.get<TransformComponent>();
Ogre::Vector3 pos =
t.node ? t.node->_getDerivedPosition() : t.position;
if (pos.distance(expected) > tol) {
if (pos.distance(expected) > tol && !failed) {
failed = true;
failReason = "player not at arrival point: (" +
Ogre::StringConverter::toString(pos.x) +
@@ -240,10 +260,13 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
return true;
}
/* The arrival markers sit at (0, 0, 24) facing -Z (toward the
* floor center); verify the camera took a position behind the
* character looking at the walking surface, and that the
* character's visual facing (local +Z) points at the center too. */
/* The arrival markers face the floor centre (yaw 180): the
* camera must take a position behind the character looking at
* the walking surface, and the character's visual facing (local
* +Z) points at the centre too. Their positions are resolved
* from each scene's arrival_* marker entity (scene A's sits at
* (0, 0, 24), scene B's a few units in front of its return
* doorway), so the layouts can move freely. */
bool checkCameraFacesCenter(const Ogre::Vector3 &charPos)
{
flecs::entity player = app->getPlayerCharacterEntity();
@@ -264,8 +287,9 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
Ogre::Vector3 cp = camNode->_getDerivedPosition();
Ogre::Vector3 vd = camNode->_getDerivedOrientation() *
Ogre::Vector3::NEGATIVE_UNIT_Z;
if (vd.z > -0.9f || cp.z < charPos.z + 1.0f ||
charFacing.z > -0.9f) {
if ((vd.z > -0.9f || cp.z < charPos.z + 1.0f ||
charFacing.z > -0.9f) &&
!failed) {
failed = true;
failReason =
"not facing the walking surface: camera pos (" +
@@ -320,23 +344,54 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
failReason = "F6 demo door not locked at start";
break;
}
/* Emulate the ActuatorSystem E-press on a locked door;
* the inline scene script answers door_locked with
* door_unlock_<doorId>. */
EventBus::getInstance().send("door_locked", "door_id",
doorId);
/* E-press the locked door through the exact path
* ActuatorSystem takes (handleDoorPress): the
* door_locked events go out and the inline scene
* script answers door_locked with
* door_unlock_<doorId> inside the synchronous
* dispatch, so the SAME press unlocks the door and
* requests the open swing instead of needing a
* second press. */
{
ActuatorSystem *actuators =
app->getActuatorSystem();
if (!actuators) {
failed = true;
failReason = "no ActuatorSystem";
break;
}
actuators->handleDoorPress(
app->getPlayerCharacterEntity(),
door);
if (DoorSystem::isDoorLockedById(doorId)) {
failed = true;
failReason = "F6 same press did not "
"unlock the door";
break;
}
if (!door.get<DoorComponent>()
.toggleRequested) {
failed = true;
failReason = "F6 same press did not "
"request the open swing";
break;
}
std::cout << "[test] F6 locked door: one E "
"press unlocked and opened it"
<< std::endl;
}
phase = 11;
break;
}
case 11: {
/* F6: wait for the scene script to unlock the door
* (the frame timeout covers a missing handler). */
if (DoorSystem::isDoorLockedById(doorId))
break;
/* F6: DoorSystem consumed the same-press open request
* and the swing is under way (waiting here on the
* consumed flag covers any frame-listener ordering). */
flecs::entity door = findDoor();
if (!door.is_alive())
break;
door.get_mut<DoorComponent>().toggleRequested = true;
if (door.get<DoorComponent>().toggleRequested)
break;
std::cout << "[test] F6 door unlocked via scene script, "
"opening"
<< std::endl;
@@ -433,14 +488,20 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
<< std::endl;
}
}
if (!checkPlayerNear(Ogre::Vector3(0.0f, 0.0f, 24.0f),
1.0f))
break;
if (!checkCameraFacesCenter(Ogre::Vector3(0.0f, 0.0f,
24.0f)))
break;
if (failed)
break;
{
Ogre::Vector3 arrival;
if (!entityPosition("arrival_b",
arrival)) {
failed = true;
failReason =
"arrival_b marker not found";
break;
}
checkPlayerNear(arrival, 1.0f);
checkCameraFacesCenter(arrival);
if (failed)
break;
}
std::cout << "[test] arrived in scene B at arrival_b, "
"camera faces the walking surface"
<< std::endl;
@@ -465,12 +526,20 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
case 2:
if (!entityExists("interrior"))
break;
if (!checkPlayerNear(Ogre::Vector3(0.0f, 0.0f, 24.0f),
1.0f))
break;
if (!checkCameraFacesCenter(Ogre::Vector3(0.0f, 0.0f,
24.0f)))
break;
{
Ogre::Vector3 arrival;
if (!entityPosition("arrival_a",
arrival)) {
failed = true;
failReason =
"arrival_a marker not found";
break;
}
checkPlayerNear(arrival, 1.0f);
checkCameraFacesCenter(arrival);
if (failed)
break;
}
/* F6: the door rebuilt with the scene must be snapped
* back to the persisted open + unlocked state. */
{
@@ -546,9 +615,10 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
* by default, with a fixed gridUid so its global door ID is stable, and
* the grid entity carries an inline scene script that answers the
* "door_locked" event with "door_unlock_<doorId>" (the door event
* contract; the door never opens by itself, the player presses E again).
* The door's locked and open/closed state lives in the global state
* store and survives the A -> B -> A scene switches.
* contract; the dispatch is synchronous, so the same press continues
* and opens the door). The door's locked and open/closed state lives
* in the global state store and survives the A -> B -> A scene
* switches.
*
* F1 demo content: both scene transitions go through scene-switch doors.
* Scene A's grid has the external exit doorway Z:0:0:15 configured as a
@@ -1495,7 +1495,7 @@
"uvMargin": 0.009999999776482582
},
"sceneScript": {
"inlineScript": "-- F6 demo: the internal doorway Z:0:0:8 of this grid is lockable\n-- and locked by default (see the grid's doorConfigs). The first time\n-- the player bumps into the locked door, this handler unlocks it through\n-- the door event contract; the door itself never opens on its own, the\n-- player presses E again after the unlock.\nlocal door_id = \"d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6:Z:0:0:8\"\n\necs.subscribe_event(\"door_locked\", function(event, params)\n if params and params.door_id == door_id then\n print(\"[demo] door_locked for \" .. door_id ..\n \" (locked=\" .. tostring(ecs.door.is_locked(door_id)) ..\n \"), unlocking\")\n ecs.send_event(\"door_unlock_\" .. door_id)\n print(\"[demo] door unlocked, locked=\" ..\n tostring(ecs.door.is_locked(door_id)))\n end\nend)\n",
"inlineScript": "-- F6 demo: the internal doorway Z:0:0:8 of this grid is lockable\n-- and locked by default (see the grid's doorConfigs). The first time\n-- the player bumps into the locked door, this handler unlocks it through\n-- the door event contract; the unlock dispatches synchronously, so the\n-- same E press continues and opens the door.\nlocal door_id = \"d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6:Z:0:0:8\"\n\necs.subscribe_event(\"door_locked\", function(event, params)\n if params and params.door_id == door_id then\n print(\"[demo] door_locked for \" .. door_id ..\n \" (locked=\" .. tostring(ecs.door.is_locked(door_id)) ..\n \"), unlocking\")\n ecs.send_event(\"door_unlock_\" .. door_id)\n print(\"[demo] door unlocked, locked=\" ..\n tostring(ecs.door.is_locked(door_id)))\n end\nend)\n",
"scriptPath": ""
},
"transform": {
@@ -3,6 +3,7 @@
#include "EditorApp.hpp"
#include "ProjectConfig.hpp"
#include "camera/EditorCamera.hpp"
#include "systems/ActuatorSystem.hpp"
#include "systems/CharacterRegistry.hpp"
#include "systems/DoorSystem.hpp"
#include "systems/PlayerControllerSystem.hpp"
@@ -516,23 +517,54 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
failReason = "F6 demo door not locked at start";
break;
}
/* Emulate the ActuatorSystem E-press on a locked door;
* the inline scene script answers door_locked with
* door_unlock_<doorId>. */
EventBus::getInstance().send("door_locked", "door_id",
doorId);
/* E-press the locked door through the exact path
* ActuatorSystem takes (handleDoorPress): the
* door_locked events go out and the inline scene
* script answers door_locked with
* door_unlock_<doorId> inside the synchronous
* dispatch, so the SAME press unlocks the door and
* requests the open swing instead of needing a
* second press. */
{
ActuatorSystem *actuators =
app->getActuatorSystem();
if (!actuators) {
failed = true;
failReason = "no ActuatorSystem";
break;
}
actuators->handleDoorPress(
app->getPlayerCharacterEntity(),
door);
if (DoorSystem::isDoorLockedById(doorId)) {
failed = true;
failReason = "F6 same press did not "
"unlock the door";
break;
}
if (!door.get<DoorComponent>()
.toggleRequested) {
failed = true;
failReason = "F6 same press did not "
"request the open swing";
break;
}
std::cout << "[test] F6 locked door: one E "
"press unlocked and opened it"
<< std::endl;
}
phase = 11;
break;
}
case 11: {
/* F6: wait for the scene script to unlock the door
* (the frame timeout covers a missing handler). */
if (DoorSystem::isDoorLockedById(doorId))
break;
/* F6: DoorSystem consumed the same-press open request
* and the swing is under way (waiting here on the
* consumed flag covers any frame-listener ordering). */
flecs::entity door = findDoor();
if (!door.is_alive())
break;
door.get_mut<DoorComponent>().toggleRequested = true;
if (door.get<DoorComponent>().toggleRequested)
break;
std::cout << "[test] F6 door unlocked via scene script, "
"opening"
<< std::endl;
@@ -2548,8 +2580,9 @@ struct SokobanResetTestListener : public Ogre::FrameListener {
* lockable + locked by default, with a fixed gridUid so its global door
* ID is stable, and the grid entity carries an inline scene script that
* answers the "door_locked" event with "door_unlock_<doorId>" (the door
* event contract; the door never opens by itself, the player presses E
* again). The door's locked and open/closed state lives in the global
* event contract; the dispatch is synchronous, so the same press
* continues and opens the door). The door's locked and open/closed
* state lives in the global
* state store and survives the interior -> exterior -> interior scene
* switches.
*
@@ -1495,7 +1495,7 @@
"uvMargin": 0.009999999776482582
},
"sceneScript": {
"inlineScript": "-- F6 demo: the internal doorway Z:0:0:8 of this grid is lockable\n-- and locked by default (see the grid's doorConfigs). The first time\n-- the player bumps into the locked door, this handler unlocks it through\n-- the door event contract; the door itself never opens on its own, the\n-- player presses E again after the unlock.\nlocal door_id = \"d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6:Z:0:0:8\"\n\necs.subscribe_event(\"door_locked\", function(event, params)\n if params and params.door_id == door_id then\n print(\"[demo] door_locked for \" .. door_id ..\n \" (locked=\" .. tostring(ecs.door.is_locked(door_id)) ..\n \"), unlocking\")\n ecs.send_event(\"door_unlock_\" .. door_id)\n print(\"[demo] door unlocked, locked=\" ..\n tostring(ecs.door.is_locked(door_id)))\n end\nend)\n",
"inlineScript": "-- F6 demo: the internal doorway Z:0:0:8 of this grid is lockable\n-- and locked by default (see the grid's doorConfigs). The first time\n-- the player bumps into the locked door, this handler unlocks it through\n-- the door event contract; the unlock dispatches synchronously, so the\n-- same E press continues and opens the door.\nlocal door_id = \"d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6:Z:0:0:8\"\n\necs.subscribe_event(\"door_locked\", function(event, params)\n if params and params.door_id == door_id then\n print(\"[demo] door_locked for \" .. door_id ..\n \" (locked=\" .. tostring(ecs.door.is_locked(door_id)) ..\n \"), unlocking\")\n ecs.send_event(\"door_unlock_\" .. door_id)\n print(\"[demo] door unlocked, locked=\" ..\n tostring(ecs.door.is_locked(door_id)))\n end\nend)\n",
"scriptPath": ""
},
"transform": {
@@ -2514,23 +2514,54 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
failReason = "F6 demo door not locked at start";
break;
}
/* Emulate the ActuatorSystem E-press on a locked door;
* the inline scene script answers door_locked with
* door_unlock_<doorId>. */
EventBus::getInstance().send("door_locked", "door_id",
doorId);
/* E-press the locked door through the exact path
* ActuatorSystem takes (handleDoorPress): the
* door_locked events go out and the inline scene
* script answers door_locked with
* door_unlock_<doorId> inside the synchronous
* dispatch, so the SAME press unlocks the door and
* requests the open swing instead of needing a
* second press. */
{
ActuatorSystem *actuators =
app->getActuatorSystem();
if (!actuators) {
failed = true;
failReason = "no ActuatorSystem";
break;
}
actuators->handleDoorPress(
app->getPlayerCharacterEntity(),
door);
if (DoorSystem::isDoorLockedById(doorId)) {
failed = true;
failReason = "F6 same press did not "
"unlock the door";
break;
}
if (!door.get<DoorComponent>()
.toggleRequested) {
failed = true;
failReason = "F6 same press did not "
"request the open swing";
break;
}
std::cout << "[test] F6 locked door: one E "
"press unlocked and opened it"
<< std::endl;
}
phase = 11;
break;
}
case 11: {
/* F6: wait for the scene script to unlock the door
* (the frame timeout covers a missing handler). */
if (DoorSystem::isDoorLockedById(doorId))
break;
/* F6: DoorSystem consumed the same-press open request
* and the swing is under way (waiting here on the
* consumed flag covers any frame-listener ordering). */
flecs::entity door = findDoor();
if (!door.is_alive())
break;
door.get_mut<DoorComponent>().toggleRequested = true;
if (door.get<DoorComponent>().toggleRequested)
break;
std::cout << "[test] F6 door unlocked via scene script, "
"opening"
<< std::endl;
@@ -6922,10 +6953,10 @@ struct DriveBenchmarkListener : public Ogre::FrameListener,
* lockable + locked by default, with a fixed gridUid so its global door
* ID is stable, and the grid entity carries an inline scene script that
* answers the "door_locked" event with "door_unlock_<doorId>" (the door
* event contract; the door never opens by itself, the player presses E
* again). The door's locked and open/closed state lives in the global
* state store and survives the interior -> exterior -> interior scene
* switches.
* event contract; the dispatch is synchronous, so the same press
* continues and opens the door). The door's locked and open/closed
* state lives in the global state store and survives the interior ->
* exterior -> interior scene switches.
*
* F1 demo content: both scene transitions go through scene-switch doors.
* The interior grid has the external exit doorway Z:0:0:15 configured as
@@ -1551,7 +1551,7 @@
"uvMargin": 0.009999999776482582
},
"sceneScript": {
"inlineScript": "-- F6 demo: the internal doorway Z:0:0:8 of this grid is lockable\n-- and locked by default (see the grid's doorConfigs). The first time\n-- the player bumps into the locked door, this handler unlocks it through\n-- the door event contract; the door itself never opens on its own, the\n-- player presses E again after the unlock.\nlocal door_id = \"d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6:Z:0:0:8\"\n\necs.subscribe_event(\"door_locked\", function(event, params)\n if params and params.door_id == door_id then\n print(\"[demo] door_locked for \" .. door_id ..\n \" (locked=\" .. tostring(ecs.door.is_locked(door_id)) ..\n \"), unlocking\")\n ecs.send_event(\"door_unlock_\" .. door_id)\n print(\"[demo] door unlocked, locked=\" ..\n tostring(ecs.door.is_locked(door_id)))\n end\nend)\n",
"inlineScript": "-- F6 demo: the internal doorway Z:0:0:8 of this grid is lockable\n-- and locked by default (see the grid's doorConfigs). The first time\n-- the player bumps into the locked door, this handler unlocks it through\n-- the door event contract; the unlock dispatches synchronously, so the\n-- same E press continues and opens the door.\nlocal door_id = \"d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6:Z:0:0:8\"\n\necs.subscribe_event(\"door_locked\", function(event, params)\n if params and params.door_id == door_id then\n print(\"[demo] door_locked for \" .. door_id ..\n \" (locked=\" .. tostring(ecs.door.is_locked(door_id)) ..\n \"), unlocking\")\n ecs.send_event(\"door_unlock_\" .. door_id)\n print(\"[demo] door unlocked, locked=\" ..\n tostring(ecs.door.is_locked(door_id)))\n end\nend)\n",
"scriptPath": ""
},
"transform": {
@@ -17,7 +17,8 @@
-- door_unlocked_<doorId> notification after a door was unlocked.
-- door_unlocked generic notification; params: door_id.
--
-- The door never opens by itself on unlock - the player presses E again.
-- A scripted unlock dispatches synchronously, so the same E press that
-- bumped into the lock continues and opens the door.
-- A door configured with keyItemId is unlocked by E when the player has
-- the item in the inventory (keys are NOT consumed); holding E on a
-- lockable door offers Close/Lock in the action menu.
@@ -127,6 +127,68 @@ void ActuatorSystem::executeAction(flecs::entity character,
"[ActuatorSystem] Executing action: " + actionName);
}
void ActuatorSystem::handleDoorPress(flecs::entity playerCharacter,
flecs::entity doorEntity)
{
if (!doorEntity.is_alive() || !doorEntity.has<DoorComponent>())
return;
auto &door = doorEntity.get_mut<DoorComponent>();
/* Locked doors (F6): the configured key item unlocks (keys are
* not consumed); otherwise the door_locked events are emitted. */
bool locked = DoorSystem::isDoorLocked(door);
if (locked && !door.keyItemId.empty() &&
playerCharacter.is_alive() &&
playerCharacter.has<InventoryComponent>() &&
playerCharacter.get<InventoryComponent>()
.hasItem(door.keyItemId)) {
DoorSystem::setDoorLocked(door.doorId, false);
locked = false;
}
if (locked) {
editScene::EventParams params;
params.setString("door_id", door.doorId);
params.setEntityId("entity_id", doorEntity.id());
EventBus &bus = EventBus::getInstance();
bus.send("door_locked_" + door.doorId, params);
bus.send("door_locked", params);
/* EventBus dispatches synchronously, so re-check the
* lock state: if a door_locked subscriber unlocked the
* door during the dispatch (the F6 event contract —
* scene scripts answer with door_unlock_<doorId>), the
* same press continues below instead of requiring a
* second press to open. */
locked = DoorSystem::isDoorLocked(door);
}
if (locked)
return;
if (!door.sceneSwitchPath.empty()) {
/* F1: the door swings open first; DoorSystem fires the
* switch at full opening. An already-open door switches
* immediately. */
if (door.isOpen && door.currentAngle == door.openAngle) {
SceneSwitchOptions opts;
opts.targetEntityName = door.sceneSwitchTarget;
m_editorApp->switchScene(door.sceneSwitchPath, opts);
} else {
door.toggleRequested = true;
door.sceneSwitchPending = true;
}
} else {
door.toggleRequested = true;
if (doorEntity.has<ActuatorComponent>()) {
auto &actuator = doorEntity.get<ActuatorComponent>();
if (!actuator.actionNames.empty() &&
!actuator.actionNames[0].empty()) {
executeAction(playerCharacter, doorEntity,
actuator.actionNames[0]);
}
}
}
}
void ActuatorSystem::executeItemAction(flecs::entity character,
flecs::entity itemEntity,
const Ogre::String &actionName)
@@ -564,69 +626,8 @@ void ActuatorSystem::update(float deltaTime)
auto &door =
targetEntity.get_mut<DoorComponent>();
if (input.ePressed) {
/* Locked doors (F6): the configured key
* item unlocks (keys are not consumed);
* otherwise only the door_locked events
* are emitted. */
bool locked = DoorSystem::isDoorLocked(door);
if (locked && !door.keyItemId.empty() &&
playerCharacter.is_alive() &&
playerCharacter
.has<InventoryComponent>() &&
playerCharacter
.get<InventoryComponent>()
.hasItem(door.keyItemId)) {
DoorSystem::setDoorLocked(
door.doorId, false);
locked = false;
}
if (locked) {
editScene::EventParams params;
params.setString("door_id",
door.doorId);
params.setEntityId(
"entity_id",
targetEntity.id());
EventBus &bus =
EventBus::getInstance();
bus.send("door_locked_" +
door.doorId,
params);
bus.send("door_locked", params);
} else if (!door.sceneSwitchPath.empty()) {
/* F1: the door swings open
* first; DoorSystem fires the
* switch at full opening. An
* already-open door switches
* immediately. */
if (door.isOpen &&
door.currentAngle ==
door.openAngle) {
SceneSwitchOptions opts;
opts.targetEntityName =
door.sceneSwitchTarget;
m_editorApp->switchScene(
door.sceneSwitchPath,
opts);
} else {
door.toggleRequested =
true;
door.sceneSwitchPending =
true;
}
} else {
door.toggleRequested = true;
if (!actuator.actionNames
.empty() &&
!actuator.actionNames[0]
.empty()) {
executeAction(
playerCharacter,
targetEntity,
actuator.actionNames
[0]);
}
}
handleDoorPress(playerCharacter,
targetEntity);
m_eHoldTime = 0.0f;
} else if (door.lockable && !door.doorId.empty() &&
!DoorSystem::isDoorLocked(door) &&
@@ -64,6 +64,17 @@ public:
flecs::entity actuatorEntity,
const Ogre::String &actionName);
/** The exact action of an E press on a door entity (the door
* branch of the E press handling): a held keyItem unlocks, else
* the door_locked events are emitted and — because the EventBus
* dispatches subscribers synchronously — the F6 scripted unlock
* (door_unlock_<doorId> inside a door_locked handler) lets the
* SAME press continue to toggle/scene-switch instead of
* requiring a second press. Runs before any ImGui use, so
* headless tests can drive doors through this. */
void handleDoorPress(flecs::entity playerCharacter,
flecs::entity doorEntity);
private:
struct ScreenActuator {
flecs::entity entity;