Fixes for doors and arrival positions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user