Interior/exterior implemented in the demo
This commit is contained in:
@@ -32,6 +32,13 @@ cmake --build build-vscode --target editSceneEditor -j4
|
||||
cd build-vscode/src/features/editScene
|
||||
./editSceneEditor
|
||||
|
||||
# Open a project directory (chdir into it before OGRE init; all
|
||||
# CWD-relative paths — resources.cfg, config JSONs, scenes, prefabs,
|
||||
# heightmaps/ — resolve against it; see "Project Directory" below)
|
||||
./editSceneEditor --project /path/to/project
|
||||
./editSceneEditor --project /path/to/project --game # force game mode
|
||||
./editSceneEditor --project /path/to/project --editor # force editor mode
|
||||
|
||||
# Run game mode (loads the configured base scene through the startup menu)
|
||||
./editSceneEditor --game
|
||||
|
||||
@@ -101,17 +108,31 @@ cd demos/demo-scene-switching
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use portal,
|
||||
# Escape = pause menu (frees the cursor).
|
||||
|
||||
# Demo: same scene-switching setup, but scene A additionally holds an
|
||||
# "interrior" entity with a CellGridComponent (room with floor, ceiling,
|
||||
# interior walls and an exit door) carrying its own ProceduralMaterial +
|
||||
# ProceduralTexture; the grid's texture rectangle names reference the
|
||||
# texture's named rects ("floor" / "ceiling") without any
|
||||
# Lot/District/Town parent.
|
||||
# Demo: same scene-switching setup, but both transitions go through
|
||||
# F1 scene-switch doors instead of portal actuators. Scene A holds an
|
||||
# "interrior" entity with an interiorOnly CellGridComponent (room with
|
||||
# floor, ceiling, interior walls, boundary windows with opaque glass and
|
||||
# doors) carrying its own ProceduralMaterial + ProceduralTexture; its
|
||||
# external doorway Z:0:0:15 is configured (doorConfigs) as a scene-switch
|
||||
# door to demo_scene_b.json (target arrival_b). Scene B holds an
|
||||
# exteriorOnly CellGridComponent (entity "x1"/"w1" - just the shell with
|
||||
# opaque window glass) whose grid-wide doorSceneSwitchPath /
|
||||
# doorSceneSwitchTarget makes its external doorway Z:0:0:0 the way back
|
||||
# to demo_scene_a.json (target arrival_a). E on such a door swings the
|
||||
# leaf open; the switch fires only when the leaf is fully open, with a
|
||||
# black occluder hiding the ungenerated half of the building. Scene A's
|
||||
# internal doorway Z:0:0:8 additionally demos F6 (persistent + lockable,
|
||||
# locked by default, unlocked by the grid's inline scene script through
|
||||
# the door event contract).
|
||||
cd demos/demo-scene-switching-extra
|
||||
./demoSceneSwitchingExtra
|
||||
# ...or headless smoke run (one frame, then exit):
|
||||
./demoSceneSwitchingExtra --headless --exit-after-first-frame
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use portal,
|
||||
# ...or headless end-to-end check of the A -> B -> A round trip through
|
||||
# both scene-switch doors plus the F6 locked-door contract
|
||||
# (exits non-zero on failure):
|
||||
./demoSceneSwitchingExtra --headless --test-switch
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door,
|
||||
# Escape = pause menu (frees the cursor).
|
||||
|
||||
|
||||
@@ -150,6 +171,55 @@ to `EditorApp`.
|
||||
| `Playing` | Player controller and gameplay systems active |
|
||||
| `Paused` | Pause menu / character sheet open, gameplay systems frozen |
|
||||
|
||||
## Project Directory (F8)
|
||||
|
||||
A "project" is a directory the editor/game treats as its working directory.
|
||||
`main.cpp` handles `--project <dir>` (or `--project=<dir>`) by `chdir()`ing
|
||||
into the project root **before** OGRE initialization, so every CWD-relative
|
||||
path (`resources.cfg`, config JSONs, scenes, prefabs, `heightmaps/`,
|
||||
`lua-scripts`) resolves against it. `EditorApp::getProjectConfig()` /
|
||||
`getProjectRoot()` expose the state; **File -> Open Project...** in the
|
||||
editor switches project at runtime (`EditorApp::openProject()`: chdir +
|
||||
reload project.json + clear scene). The dialog is an ImGui directory
|
||||
browser (ImGui has no native file dialog): subdirectory list with `..`
|
||||
navigation, dot-directories hidden, dirs containing `project.json`
|
||||
tagged `[project]`, plus an editable path field (Enter navigates,
|
||||
relative paths resolve against the browsed dir) and "Open This
|
||||
Directory".
|
||||
|
||||
A project root may contain `project.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"appName": "My Game",
|
||||
"startScene": "scenes/level1.json",
|
||||
"gameMode": true
|
||||
}
|
||||
```
|
||||
|
||||
- `appName` (fallback: directory name) drives the window title and the
|
||||
per-project save directory `<user-data>/<appName>/saves/`
|
||||
(`SaveLoadSystem::setAppName()`; default remains `World2`).
|
||||
- `gameMode: true` makes the editor binary enter game mode by default for
|
||||
the project (`--editor` forces editor mode, `--game` forces game mode).
|
||||
- `gameMode` + `startScene`: game mode skips `startup_menu.json` and calls
|
||||
`startNewGame(startScene)` right after `initApp()`, like a release
|
||||
binary. Caveat: demos that create meshes programmatically in their
|
||||
`demo_main.cpp` (e.g. `DemoFloorPlaneA`) only have them in their own
|
||||
binary — the generic editor binary logs missing-mesh warnings for those.
|
||||
|
||||
**Release binaries.** Demo executables (e.g. `demoSceneSwitchingExtra`)
|
||||
embed their project at build time: CMake reads the demo's `project.json`
|
||||
(`string(JSON ...)`) and generates `project.h` via `configure_file`
|
||||
(`EDITSCENE_PROJECT_APP_NAME`, `EDITSCENE_PROJECT_START_SCENE`,
|
||||
`EDITSCENE_PROJECT_GAME_MODE`; the target gets
|
||||
`-DEDITSCENE_HAS_EMBEDDED_PROJECT` and the generated include dir).
|
||||
`CMAKE_CONFIGURE_DEPENDS` on `project.json` re-generates the header when
|
||||
the file changes. The demo's `main()` passes the embedded app name to the
|
||||
`EditorApp` constructor and sets a `ProjectConfig` rooted at the CWD, so
|
||||
the binary runs its project with no flags and no editor UI. Ship the
|
||||
binary + its staged project directory as the distributable.
|
||||
|
||||
## Frame Update Order
|
||||
|
||||
`EditorApp::frameRenderingQueued()` updates systems in this order when not paused:
|
||||
@@ -454,16 +524,238 @@ when `CellGridComponent::doorActionName` is set, also runs that action.
|
||||
`doorOpenAngle` at `doorOpenSpeed`, disables the door's rigid body while the
|
||||
door is not fully closed, and re-enables it once closed again.
|
||||
|
||||
Scene switching doors: when `CellGridComponent::doorSceneSwitchPath` is set,
|
||||
activation instead queues `EditorApp::switchScene()` directly (no action or
|
||||
behavior tree involved) with `doorSceneSwitchTarget` as the teleport target
|
||||
entity name in the new scene (same mechanism as the "switchScene" BT node's
|
||||
"@name" param). The leaf does not swing and the prompt always reads "E Open".
|
||||
Scene switching doors (F1): when `CellGridComponent::doorSceneSwitchPath`
|
||||
is set (or the per-door override's `sceneSwitchPath`), activation swings
|
||||
the leaf like a normal door (`toggleRequested` +
|
||||
`DoorComponent::sceneSwitchPending`); `DoorSystem` queues
|
||||
`EditorApp::switchScene(sceneSwitchPath, {targetEntityName:
|
||||
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.
|
||||
|
||||
New `CellGridComponent` fields (serialized in the scene JSON and exposed to
|
||||
Lua): `doorsEnabled`, `doorRectName`, `doorMeshName`, `doorUseMeshMaterial`,
|
||||
`doorOpenAngle`, `doorOpenSpeed`, `doorActionName`, `doorSceneSwitchPath`,
|
||||
`doorSceneSwitchTarget`.
|
||||
`doorOpenAngle`, `doorOpenSpeed`, `doorSwingReversed` (F3: negates the
|
||||
applied swing angle via `DoorSystem::swingOrientation()`, also used when
|
||||
snapping a restored-open door; stored angles stay positive),
|
||||
`doorActionName`, `doorSceneSwitchPath`, `doorSceneSwitchTarget`.
|
||||
|
||||
#### Generation mode (F4/F5)
|
||||
|
||||
`CellGridComponent::generationMode` (string: `"full"` default,
|
||||
`"interiorOnly"`, `"exteriorOnly"`; helpers `interiorOnlyMode()` /
|
||||
`exteriorOnlyMode()`; serialized + Lua-bound; editor combo in the Cell Grid
|
||||
editor) selects which parts `buildCellGrid()` generates:
|
||||
|
||||
- `"interiorOnly"` skips the exterior shell: external wall planes
|
||||
(`buildWalls`), external window panels (`buildWindows`), external window
|
||||
frame placement, external corners (`buildCorners`) and roofs
|
||||
(`buildRoofs`). Floors, ceilings, internal walls/frames, furniture, the
|
||||
exit doorway wall panels (`extDoorsTb`), external door frames and all
|
||||
door entities stay — interior-only scenes are paired with exterior-only
|
||||
scenes and transition through scene-switch doors. Boundary windows
|
||||
(cells carrying both the internal and the matching external window
|
||||
flag) get a glass pane at the internal wall plane, hiding the void
|
||||
outside (see below).
|
||||
- `"exteriorOnly"` keeps only the shell (external walls, external
|
||||
door/window panels, roofs, corners, external frames, exit door
|
||||
entities) and adds a glass pane per external window opening.
|
||||
- Window glass (both modes): `CellGridSystem::buildGlass()` generates a
|
||||
thin box (2 cm) per window opening into `meshData.glassMesh`, which
|
||||
gets a collider in the static shell (no climbing through windows).
|
||||
exteriorOnly panes sit at the external wall plane; interiorOnly panes
|
||||
only cover boundary windows and sit at the internal wall plane; pure
|
||||
interior room-to-room windows get no glass. The pane uses
|
||||
`glassMaterialName` when set, otherwise a built-in per-grid
|
||||
`CellGridGlass_<id>` material created/updated by
|
||||
`getGlassMaterialName()` — **opaque on purpose** (no alpha blend,
|
||||
depth write on, diffuse alpha forced to 1) because it must hide the
|
||||
half of the building the mode does not generate; the `glassColor`
|
||||
alpha is ignored by the built-in material, `glassReflectivity` drives
|
||||
specular/shininess. Fields `glassColor` / `glassMaterialName` /
|
||||
`glassReflectivity` are serialized and Lua-bound; the editor shows them
|
||||
when the mode is exteriorOnly or interiorOnly.
|
||||
- Skipped parts have empty buffers, so they get no meshes and no physics
|
||||
colliders (colliders follow the mesh lists).
|
||||
|
||||
#### Door identity & per-door configuration (F0)
|
||||
|
||||
A doorway's identity is its **canonical edge key** (`"X:x:y:z"` /
|
||||
`"Z:x:y:z"`, `CellGridSystem::doorEdgeKey()` — both cells sharing a door
|
||||
edge produce the same key). A door's **global ID** is
|
||||
`<gridUid>:<edgeKey>` where `gridUid` is a serialized
|
||||
`CellGridComponent::gridUid` (a random UUID generated lazily by
|
||||
`ensureGridUid()`), so door IDs survive grid entity renames, grid rebuilds
|
||||
and scene loads. `DoorComponent` carries the runtime copies `edgeKey` and
|
||||
`doorId` (empty `doorId` = ephemeral door: no persistence).
|
||||
|
||||
Per-doorway configuration lives **inside the grid component** as
|
||||
`CellGridComponent::doorConfigs` (`std::map<std::string,
|
||||
CellGridDoorConfig>`, keyed by edge key, serialized + Lua-bound as the
|
||||
`doorConfigs` table). Entries override the grid-wide `door*` defaults for
|
||||
one doorway (`hasOverride` gates the behaviour fields: `openAngle`,
|
||||
`openSpeed`, `swingReversed`, `actionName`, `sceneSwitchPath`,
|
||||
`sceneSwitchTarget`) and add per-door flags: `label` (editor UX),
|
||||
`disabled` (spawn no door entity for this doorway), `persistent`,
|
||||
`lockable`, `lockedByDefault`, `keyItemId` (consumed by F6). Doorways
|
||||
without an entry behave exactly as before (grid defaults, ephemeral).
|
||||
Entries whose doorway no longer exists are **orphaned**: they are kept
|
||||
(never auto-deleted, serialized like live entries) until pruned or
|
||||
reassigned in the editor.
|
||||
|
||||
Editor UX: door entities no longer appear in the scene tree
|
||||
(`EditorUISystem::renderEntityNode()` skips children with `DoorComponent`,
|
||||
like `GeneratedPhysicsTag`). Configuration happens in the Cell Grid
|
||||
editor's **Doors panel** (`ui/CellGridEditor.cpp::renderDoorEditor()`):
|
||||
grid-wide defaults, the list of all unique doorways
|
||||
(`CellGridSystem::collectDoorways()`) with badges, "Pick Door in Viewport"
|
||||
(click raycasts against the grid's door leaves via
|
||||
`ui/DoorPickState.hpp` + `EditorUISystem::onMousePressed`), a highlight of
|
||||
the selected door (leaf material swapped to `GizmoYellow` while selected),
|
||||
per-door override widgets with the full copyable global door ID, and an
|
||||
orphaned-config section with **Prune** and **Reassign...** (moves the entry
|
||||
to another doorway of the grid; warns when the target already has a config
|
||||
and when Lua-visible identity changes).
|
||||
|
||||
Runtime lookup: `CellGridSystem::findDoorEntity(gridEntity, edgeKey)`
|
||||
(static) finds the live door entity of a doorway by matching
|
||||
`DoorComponent::edgeKey` on the grid's children.
|
||||
|
||||
Tests: `tests/cellgrid_door_test.cpp` (target `cellgrid_door_test`, CTest
|
||||
`cellgridDoorTest`; headless — edge-key canonicalization, doorway dedup,
|
||||
uid stability, serialization round trip incl. orphans, old-scene defaults)
|
||||
and `tests/component_lua_test.cpp` test 32b (Lua get/set of `gridUid` +
|
||||
`doorConfigs`).
|
||||
|
||||
#### Persistent door state (F6)
|
||||
|
||||
Doors with a global ID (F0: `persistent`, `lockable` or scene-switch
|
||||
doors) keep their state in the `GlobalStateStore` (F9) under
|
||||
`door.<doorId>.locked` and `door.<doorId>.isOpen`, so it survives CellGrid
|
||||
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.
|
||||
- **Write path**: `DoorSystem::update()` stores `isOpen` when a swing
|
||||
completes.
|
||||
- **Locked state**: `DoorSystem::isDoorLocked()` / `isDoorLockedById()` /
|
||||
`setDoorLocked()`. `setDoorLocked(id, false)` emits the notifications
|
||||
`door_unlocked_<doorId>` and `door_unlocked` (params: `door_id`).
|
||||
- **Unlock events** (EventBus has no wildcards): `DoorSystem` subscribes
|
||||
per lockable door to `door_unlock_<doorId>` (refreshed each update,
|
||||
removed with the door) plus the generic `door_unlock` (param `door_id`).
|
||||
- **Interaction**: E on a locked door shows "E Locked"; if the door has a
|
||||
`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
|
||||
lockable door opens a small menu with "Close" and "Lock" (Lock requires
|
||||
the key when `keyItemId` is set).
|
||||
- **Lua** (`lua/LuaDoorApi.cpp`, example
|
||||
`lua-examples/door_lock_example.lua`): `ecs.door.is_locked(doorId)`,
|
||||
`ecs.door.is_open(doorId)`, `ecs.door.lock(doorId)`,
|
||||
`ecs.door.unlock(doorId)`; the events are reachable via
|
||||
`ecs.subscribe_event` / `ecs.send_event`.
|
||||
- **Modes**: editor mode resets the store to defaults on startup/scene
|
||||
load (a door always starts in its scene-defined state); game mode starts
|
||||
a new game from defaults and restores from the save's `globalState` on
|
||||
load (`global_state.json` is the cross-session cache, like
|
||||
`item_state.json`).
|
||||
|
||||
Tests: `tests/cellgrid_door_test.cpp` tests 6–9 (defaults/lock helpers,
|
||||
unlock events incl. cleanup on door removal, swing-completion persistence,
|
||||
Lua `door_locked` → `door_unlock_<id>` round trip with `ecs.door.*`).
|
||||
The `demo-scene-switching-extra` demo scene A has a persistent locked
|
||||
door (internal doorway `Z:0:0:8`) with an inline scene script unlocking
|
||||
it on the first bump; `--headless --test-switch` verifies the whole flow
|
||||
including the state restore after the A→B→A round trip.
|
||||
|
||||
#### Standalone doors (F2)
|
||||
|
||||
Door entity construction is factored into `DoorBuilder`
|
||||
(`systems/DoorBuilder.*`: `DoorBuildParams` + `DoorBuilder::build()`), used
|
||||
by both `CellGridSystem` and `StandaloneDoorSystem`, so both produce the
|
||||
identical subtree (hinge node, leaf, collider child, actuator, F1 occluder,
|
||||
F6 defaults/snap). Leaf mesh creation stays with the caller.
|
||||
|
||||
`StandaloneDoorComponent` (`components/StandaloneDoor.hpp`, serialized
|
||||
section `standaloneDoor`) builds a door without a CellGrid: the entity's
|
||||
`TransformComponent` is the doorway placement (center of the opening at
|
||||
floor level, local +X along the wall, +Z out of the room). It carries the
|
||||
full per-door config (`meshName`/`useMeshMaterial`/`rectName`, explicit
|
||||
`leafWidth`/`leafHeight`/`leafThickness`, `openAngle`/`openSpeed`/
|
||||
`swingReversed`, `actionName`, `sceneSwitchPath`/`sceneSwitchTarget`,
|
||||
`persistent`/`lockable`/`lockedByDefault`/`keyItemId`, `doorId`) plus a
|
||||
runtime `dirty` flag (not serialized; deserialization leaves it set so the
|
||||
door is built on load). `StandaloneDoorSystem`
|
||||
(`systems/StandaloneDoorSystem.*`, created next to `DoorSystem` in
|
||||
`EditorApp`) rebuilds the subtree when `dirty` is set and polls for dead
|
||||
owners (no OnRemove observer — entity IDs must not shift, see
|
||||
CellGridSystem). The procedural leaf is a box with the hinge edge at the
|
||||
origin spanning +X/+Y, skinned by the entity's own
|
||||
`ProceduralMaterialComponent` material, with `rectName` selecting the UV
|
||||
rect of the entity's `ProceduralTextureComponent` atlas. `doorId` is
|
||||
auto-generated (UUID) on first build when the door is persistent/lockable/
|
||||
a scene-switch door and stays serialized from then on; persistence and
|
||||
locking reuse the F6 `door.<doorId>.*` store keys. Editor: "Add Component
|
||||
-> Game -> Standalone Door" (`ui/StandaloneDoorEditor.*` sets `dirty` on
|
||||
every change and warns on duplicate `doorId`). Lua: `StandaloneDoor`
|
||||
component binding (setter sets `dirty`).
|
||||
|
||||
Tests: `tests/component_lua_test.cpp` test 35 (Lua round trip) and
|
||||
`tests/cellgrid_door_test.cpp` test 12 (serializer round trip). Runtime
|
||||
build needs a SceneManager, so it is covered by editor/demo runs rather
|
||||
than headless tests.
|
||||
|
||||
#### Navigation vs doors (F7)
|
||||
|
||||
Doors interact with the navmesh in three ways, all keyed off the
|
||||
`DoorComponent`:
|
||||
|
||||
- **Doors are not obstacles**: `NavMeshSystem::collectStaticEntities()`
|
||||
skips `DoorComponent` entities (static-rigid-body and forced-source
|
||||
paths, and the flecs-hierarchy walk for CellGrid/District/Lot
|
||||
geometry), so closed doors no longer block doorways in the mesh.
|
||||
- **Doorway area cost**: doorway floors are painted with
|
||||
`TileCacheNavMesh::EDITSCENE_AREA_DOOR` (id 1) via `rcMarkBoxArea`
|
||||
after walkable-area erosion in `rasterizeTileLayers()`; the query
|
||||
filter assigns it `NavMeshComponent::doorAreaCost` (default 5.0,
|
||||
serialized, Lua-bound, NavMesh editor widget), so paths prefer
|
||||
doorless detours but may cross doorways. Volumes come from
|
||||
`NavMeshSystem::getDoorVolume()` — the closed-pose hinge transform
|
||||
(parent derived transform * `DoorComponent::closedOrientation`) plus
|
||||
the leaf box collider's extents, padded 0.3 — refreshed every frame
|
||||
(`collectDoorVolumes()`) and applied to tiles at (re)build time.
|
||||
- **Locked doors block**: `NavMeshSystem::syncDoorObstacles()` adds a
|
||||
DetourTileCache box obstacle (`addBoxObstacle`/`removeObstacle`, refs
|
||||
in `NavMeshState::doorObstacles`) for every locked door
|
||||
(`DoorSystem::isDoorLocked()`) and removes it on unlock or when the
|
||||
door entity dies; `TileCacheNavMesh::update()` pumps the tile-cache
|
||||
request queue each `NavMeshSystem::update()`.
|
||||
|
||||
`PathFollowingSystem::handleDoorAhead()` auto-opens doors for NPCs: when
|
||||
the segment to the current waypoint passes within 1.2 m of a closed,
|
||||
unlocked, non-scene-switch door's centre and the character is within
|
||||
2.5 m, it sets `toggleRequested` and holds position until the leaf
|
||||
swings past 40°. NPCs never close doors behind them. Debug aid:
|
||||
`NavMeshSystem::getPolyAreaAt()` reports the area id at a position.
|
||||
|
||||
Test: `testNavMeshDoors` in the `--run-terrain-tests` suite (builds a
|
||||
floor + wall + doorway with a real door entity headlessly; path through
|
||||
the doorway, door-area marking, lock blocks, unlock restores).
|
||||
|
||||
### SceneScriptComponent & SceneScriptSystem
|
||||
|
||||
@@ -529,11 +821,45 @@ Game-mode saves are JSON files in the OS user-data directory (see
|
||||
- `characterRegistry` – full character registry state
|
||||
- `runtimeEntities` – runtime-spawned entities (dropped items, etc.)
|
||||
- `characterRuntimeData` – per-character component overrides
|
||||
- `globalState` – typed global variables (`GlobalStateStore`, F9)
|
||||
- `luaData` – data from Lua save callbacks
|
||||
|
||||
On load, if the saved character is owned by a spawner, the controller target is
|
||||
restored to the spawner name so the character respawns correctly.
|
||||
|
||||
### GlobalStateStore (F9 global persistent storage)
|
||||
|
||||
`GlobalStateStore` (`systems/GlobalStateStore.hpp/.cpp`) is the generic
|
||||
persistent variable storage for gameplay systems: a scene-independent
|
||||
singleton of typed variables (string name + `bool` / `int64` / `double` /
|
||||
`std::string`), shared between C++ and Lua (`ecs.global.*`, registered by
|
||||
`registerLuaGlobalStateApi()`, see `lua-examples/global_state_example.lua`).
|
||||
|
||||
- **Defaults**: systems declare the variables they use with
|
||||
`declareDefault()`; reading an unset variable returns the declared default
|
||||
(or the caller's fallback). Defaults are NOT persisted - only explicitly
|
||||
`set()` values land in the save file's `globalState` section and in the
|
||||
`global_state.json` auto-save cache.
|
||||
- **Key naming**: dot-namespaced; each system owns a prefix. Registered
|
||||
prefixes:
|
||||
- `door.<doorId>.locked`, `door.<doorId>.isOpen` - persistent door state
|
||||
(F6; `doorId` is the F0 global door ID `<gridUid>:<edgeKey>`).
|
||||
- **Mode semantics**: game mode loads `global_state.json` at startup,
|
||||
`startNewGame()` resets to defaults, `loadGame()` restores the save's
|
||||
`globalState` (old saves without it load as defaults). Editor mode calls
|
||||
`clearToDefaults()` at startup and on scene (re)load
|
||||
(`EditorUISystem::loadScene`, `EditorApp::openProject`) with auto-save
|
||||
disabled - the editor never loads or writes the cache file, so a previous
|
||||
game session cannot leak into the edited scene.
|
||||
- **Scene switches do not touch the store** (singleton, like the other
|
||||
registries) - state survives `switchScene()`.
|
||||
- `renamePrefix(old, new)` moves all explicit values under a key prefix
|
||||
(used by F0 door reassignment to keep a moved door's persisted state).
|
||||
|
||||
Tests: `tests/global_state_test.cpp` (target `global_state_test`, CTest
|
||||
`globalStateTest`; headless - C++/Lua round trips, defaults, serialization,
|
||||
`clearToDefaults`, `renamePrefix`).
|
||||
|
||||
### Scene Switching
|
||||
|
||||
`EditorApp::switchScene(path, opts)` (queued; executed at the top of the next
|
||||
@@ -570,7 +896,8 @@ ecs.switch_scene(path, { position = ..., rotation = { w=1, x=0, y=0, z=0 } })
|
||||
re-clamps for ~120 frames until the character reports a floor, covering the
|
||||
window where streaming terrain colliders are not built yet.
|
||||
- Persistent non-scene storages (CharacterRegistry, AnimationTreeRegistry,
|
||||
ItemRegistry, item/container state registries, Lua state) are untouched.
|
||||
ItemRegistry, item/container state registries, GlobalStateStore, Lua state)
|
||||
are untouched.
|
||||
Additionally, despawning a character (spawner or registry driven) now syncs
|
||||
its live position/rotation back to its `CharacterRegistry` record so a later
|
||||
respawn is position-faithful; inventory/BT state remains entity-local and is
|
||||
|
||||
@@ -16,6 +16,7 @@ set(EDITSCENE_SOURCES
|
||||
main.cpp
|
||||
EditorApp.cpp
|
||||
GameMode.cpp
|
||||
ProjectConfig.cpp
|
||||
systems/EditorUISystem.cpp
|
||||
systems/SceneSerializer.cpp
|
||||
systems/PhysicsSystem.cpp
|
||||
@@ -37,6 +38,10 @@ set(EDITSCENE_SOURCES
|
||||
systems/ProceduralMeshSystem.cpp
|
||||
systems/CellGridSystem.cpp
|
||||
systems/DoorSystem.cpp
|
||||
systems/DoorBuilder.cpp
|
||||
systems/StandaloneDoorSystem.cpp
|
||||
components/StandaloneDoorModule.cpp
|
||||
ui/StandaloneDoorEditor.cpp
|
||||
systems/NormalDebugSystem.cpp
|
||||
systems/RoomLayoutSystem.cpp
|
||||
systems/FurnitureLibrary.cpp
|
||||
@@ -46,6 +51,7 @@ set(EDITSCENE_SOURCES
|
||||
systems/AnimationTreeRegistry.cpp
|
||||
systems/ContainerStateRegistry.cpp
|
||||
systems/ItemStateRegistry.cpp
|
||||
systems/GlobalStateStore.cpp
|
||||
systems/SaveLoadSystem.cpp
|
||||
systems/SaveLoadDialog.cpp
|
||||
systems/PlayerControllerSystem.cpp
|
||||
@@ -200,6 +206,8 @@ set(EDITSCENE_SOURCES
|
||||
lua/LuaCharacterClassApi.cpp
|
||||
lua/LuaCharacterApi.cpp
|
||||
lua/LuaSaveLoadApi.cpp
|
||||
lua/LuaGlobalStateApi.cpp
|
||||
lua/LuaDoorApi.cpp
|
||||
lua/LuaTerrainApi.cpp
|
||||
lua/LuaSceneSwitchApi.cpp
|
||||
systems/TerrainTests.cpp
|
||||
@@ -252,10 +260,13 @@ set(EDITSCENE_HEADERS
|
||||
systems/AnimationTreeRegistry.hpp
|
||||
systems/ContainerStateRegistry.hpp
|
||||
systems/ItemStateRegistry.hpp
|
||||
systems/GlobalStateStore.hpp
|
||||
systems/PlayerControllerSystem.hpp
|
||||
systems/EditorUISystem.hpp
|
||||
systems/CellGridSystem.hpp
|
||||
systems/DoorSystem.hpp
|
||||
systems/DoorBuilder.hpp
|
||||
systems/StandaloneDoorSystem.hpp
|
||||
systems/NormalDebugSystem.hpp
|
||||
systems/RoomLayoutSystem.hpp
|
||||
systems/FurnitureLibrary.hpp
|
||||
@@ -287,7 +298,9 @@ set(EDITSCENE_HEADERS
|
||||
systems/GoapPlannerSystem.hpp
|
||||
components/Actuator.hpp
|
||||
components/Door.hpp
|
||||
components/StandaloneDoor.hpp
|
||||
ui/ActuatorEditor.hpp
|
||||
ui/StandaloneDoorEditor.hpp
|
||||
systems/EventBus.hpp
|
||||
components/EventHandler.hpp
|
||||
systems/EventHandlerSystem.hpp
|
||||
@@ -392,6 +405,8 @@ set(EDITSCENE_HEADERS
|
||||
lua/LuaCharacterClassApi.hpp
|
||||
lua/LuaCharacterApi.hpp
|
||||
lua/LuaSaveLoadApi.hpp
|
||||
lua/LuaGlobalStateApi.hpp
|
||||
lua/LuaDoorApi.hpp
|
||||
lua/LuaTerrainApi.hpp
|
||||
lua/LuaSceneSwitchApi.hpp
|
||||
)
|
||||
@@ -712,6 +727,87 @@ target_include_directories(scene_switch_test PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lpeg-1.1.0
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test: CellGrid door identity + per-door config (F0, headless)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Links the full editScene sources (minus main.cpp) like scene_switch_test,
|
||||
# but runs without OGRE initialization: doorway identity is pure grid math
|
||||
# and the SceneSerializer round trip needs no SceneManager for the CellGrid
|
||||
# component.
|
||||
set(CELLGRID_DOOR_TEST_SOURCES ${EDITSCENE_SOURCES})
|
||||
list(REMOVE_ITEM CELLGRID_DOOR_TEST_SOURCES main.cpp)
|
||||
|
||||
add_executable(cellgrid_door_test
|
||||
tests/cellgrid_door_test.cpp
|
||||
${CELLGRID_DOOR_TEST_SOURCES}
|
||||
)
|
||||
|
||||
add_dependencies(cellgrid_door_test morph)
|
||||
|
||||
target_compile_definitions(cellgrid_door_test PRIVATE JPH_DEBUG_RENDERER)
|
||||
|
||||
target_link_libraries(cellgrid_door_test
|
||||
OgreMain
|
||||
OgreBites
|
||||
OgreOverlay
|
||||
OgreMeshLodGenerator
|
||||
OgrePaging
|
||||
OgreTerrain
|
||||
flecs::flecs_static
|
||||
nlohmann_json::nlohmann_json
|
||||
Jolt::Jolt
|
||||
OgreProcedural::OgreProcedural
|
||||
RecastNavigation::Recast
|
||||
RecastNavigation::Detour
|
||||
RecastNavigation::DetourTileCache
|
||||
RecastNavigation::DetourCrowd
|
||||
RecastNavigation::DebugUtils
|
||||
PackageArchive
|
||||
RoadGeometryLib
|
||||
lua
|
||||
SDL2::SDL2
|
||||
)
|
||||
|
||||
target_include_directories(cellgrid_door_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/Recast/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/Detour/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DetourTileCache/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DetourCrowd/Include
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/recastnavigation/DebugUtils/Include
|
||||
${CMAKE_SOURCE_DIR}/src/FastNoiseLite
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lpeg-1.1.0
|
||||
)
|
||||
|
||||
add_test(NAME cellgridDoorTest
|
||||
COMMAND cellgrid_door_test
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test: Global persistent variable storage (F9, headless)
|
||||
# ---------------------------------------------------------------------------
|
||||
# Links only the store and its Lua API: no OGRE, no flecs.
|
||||
add_executable(global_state_test
|
||||
tests/global_state_test.cpp
|
||||
systems/GlobalStateStore.cpp
|
||||
lua/LuaGlobalStateApi.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(global_state_test
|
||||
lua
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
target_include_directories(global_state_test PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/src/lua/lua-5.4.8/src
|
||||
)
|
||||
|
||||
add_test(NAME globalStateTest
|
||||
COMMAND global_state_test
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Road Geometry Library — standalone wedge/segment generation (M5)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -46,11 +46,14 @@
|
||||
#include "systems/AnimationTreeRegistry.hpp"
|
||||
#include "systems/ContainerStateRegistry.hpp"
|
||||
#include "systems/ItemStateRegistry.hpp"
|
||||
#include "systems/GlobalStateStore.hpp"
|
||||
#include "systems/CharacterClassSystem.hpp"
|
||||
#include "systems/PregnancySystem.hpp"
|
||||
#include "components/CharacterClassDatabase.hpp"
|
||||
#include "lua/LuaCharacterApi.hpp"
|
||||
#include "lua/LuaSaveLoadApi.hpp"
|
||||
#include "lua/LuaDoorApi.hpp"
|
||||
#include "lua/LuaGlobalStateApi.hpp"
|
||||
#include "lua/LuaSceneSwitchApi.hpp"
|
||||
#include "systems/PlayerControllerSystem.hpp"
|
||||
#include "systems/SceneSerializer.hpp"
|
||||
@@ -105,6 +108,7 @@
|
||||
#include "components/PathFollowing.hpp"
|
||||
#include "systems/ActuatorSystem.hpp"
|
||||
#include "systems/DoorSystem.hpp"
|
||||
#include "systems/StandaloneDoorSystem.hpp"
|
||||
#include "systems/EventHandlerSystem.hpp"
|
||||
#include "systems/EventBus.hpp"
|
||||
#include "systems/SceneScriptSystem.hpp"
|
||||
@@ -243,8 +247,8 @@ void ImGuiRenderListener::postViewportUpdate(
|
||||
// EditorApp Implementation
|
||||
//=============================================================================
|
||||
|
||||
EditorApp::EditorApp()
|
||||
: OgreBites::ApplicationContext("EditSceneEditor")
|
||||
EditorApp::EditorApp(const Ogre::String &appName)
|
||||
: OgreBites::ApplicationContext(appName)
|
||||
, m_sceneMgr(nullptr)
|
||||
, m_overlaySystem(nullptr)
|
||||
, m_imguiOverlay(nullptr)
|
||||
@@ -393,6 +397,7 @@ void EditorApp::destroyEditorSystems()
|
||||
m_eventHandlerSystem.reset();
|
||||
m_actuatorSystem.reset();
|
||||
m_doorSystem.reset();
|
||||
m_standaloneDoorSystem.reset();
|
||||
m_goapPlannerSystem.reset();
|
||||
m_pathFollowingSystem.reset();
|
||||
m_goapRunnerSystem.reset();
|
||||
@@ -709,6 +714,11 @@ void EditorApp::setup()
|
||||
|
||||
// Setup Door system (swing animation for cell grid doors)
|
||||
m_doorSystem = std::make_unique<DoorSystem>(m_world);
|
||||
m_doorSystem->setEditorApp(this);
|
||||
|
||||
// Standalone doors (F2, same DoorBuilder as CellGrid doors)
|
||||
m_standaloneDoorSystem =
|
||||
std::make_unique<StandaloneDoorSystem>(m_world, m_sceneMgr);
|
||||
|
||||
// Wire CellGridSystem into NavMeshSystem so it can collect
|
||||
// batched frame/furniture geometry from StaticGeometry.
|
||||
@@ -743,6 +753,18 @@ void EditorApp::setup()
|
||||
ContainerStateRegistry::getInstance().loadFromFile(
|
||||
"container_state.json");
|
||||
|
||||
/* F9: the global state store is the cross-session cache in
|
||||
* game mode; editor mode always starts from system defaults
|
||||
* and must not leak a previous game session's state (nor
|
||||
* overwrite the cache file), so auto-save is off there. */
|
||||
if (m_gameMode == GameMode::Game) {
|
||||
GlobalStateStore::getInstance().loadFromFile(
|
||||
"global_state.json");
|
||||
} else {
|
||||
GlobalStateStore::getInstance().setAutoSaveEnabled(false);
|
||||
GlobalStateStore::getInstance().clearToDefaults();
|
||||
}
|
||||
|
||||
m_characterClassSystem =
|
||||
std::make_unique<CharacterClassSystem>(m_world, this);
|
||||
m_pregnancySystem = std::make_unique<PregnancySystem>(m_world);
|
||||
@@ -757,20 +779,29 @@ void EditorApp::setup()
|
||||
bool startupMenuLoaded = false;
|
||||
|
||||
if (m_gameMode == GameMode::Game) {
|
||||
/* A game project (project.json with gameMode +
|
||||
* startScene) skips the startup menu and goes straight
|
||||
* to its start scene, like a release binary. */
|
||||
bool directStart = m_projectConfig.gameMode &&
|
||||
!m_projectConfig.startScene.empty();
|
||||
|
||||
// Load startup menu scene configured in editor.
|
||||
// This must happen before show() so the
|
||||
// StartupMenuComponent entity exists for font preparation.
|
||||
SceneSerializer serializer(m_world, m_sceneMgr);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Game mode: Loading startup_menu.json...");
|
||||
if (serializer.loadFromFile("startup_menu.json",
|
||||
if (!directStart) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Game mode: Loading startup_menu.json...");
|
||||
}
|
||||
if (!directStart &&
|
||||
serializer.loadFromFile("startup_menu.json",
|
||||
m_uiSystem.get())) {
|
||||
PrefabSystem prefabSys(m_world, m_sceneMgr);
|
||||
prefabSys.resolveInstances();
|
||||
startupMenuLoaded = true;
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Game mode: startup_menu.json loaded");
|
||||
} else {
|
||||
} else if (!directStart) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"Game mode: Failed to load startup_menu.json: " +
|
||||
serializer.getLastError());
|
||||
@@ -839,6 +870,8 @@ void EditorApp::setup()
|
||||
editScene::registerLuaItemApi(L);
|
||||
editScene::registerLuaTerrainApi(L);
|
||||
editScene::registerLuaSceneSwitchApi(L);
|
||||
editScene::registerLuaGlobalStateApi(L);
|
||||
editScene::registerLuaDoorApi(L);
|
||||
editScene::setSceneSwitchEditorApp(this);
|
||||
|
||||
// Scene scripts execute in this shared Lua state.
|
||||
@@ -940,6 +973,49 @@ void EditorApp::setHeadless(bool headless)
|
||||
m_headless = headless;
|
||||
}
|
||||
|
||||
void EditorApp::setProjectConfig(const ProjectConfig &config)
|
||||
{
|
||||
m_projectConfig = config;
|
||||
if (!config.appName.empty())
|
||||
SaveLoadSystem::setAppName(config.appName);
|
||||
}
|
||||
|
||||
bool EditorApp::openProject(const std::string &dir)
|
||||
{
|
||||
std::error_code ec;
|
||||
std::filesystem::path abs =
|
||||
std::filesystem::absolute(std::filesystem::path(dir), ec);
|
||||
if (ec || !std::filesystem::is_directory(abs)) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"openProject: not a directory: " + dir);
|
||||
return false;
|
||||
}
|
||||
std::filesystem::current_path(abs, ec);
|
||||
if (ec) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"openProject: cannot enter directory: " + dir);
|
||||
return false;
|
||||
}
|
||||
|
||||
setProjectConfig(loadProjectConfig(abs.string()));
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"openProject: project root is now " + abs.string());
|
||||
|
||||
/* Best-effort window title update to the project app name. */
|
||||
if (!m_headless && !mWindows.empty() && mWindows[0].native) {
|
||||
SDL_SetWindowTitle((SDL_Window *)mWindows[0].native,
|
||||
m_projectConfig.appName.c_str());
|
||||
}
|
||||
|
||||
/* Drop the current scene: relative scene/prefab paths now resolve
|
||||
* against the new project root. */
|
||||
clearScene();
|
||||
/* F9: opening a project in the editor resets the global state to
|
||||
* system defaults (editor mode never carries game state). */
|
||||
GlobalStateStore::getInstance().clearToDefaults();
|
||||
return true;
|
||||
}
|
||||
|
||||
void EditorApp::setGamePlayState(GamePlayState state)
|
||||
{
|
||||
m_gamePlayState = state;
|
||||
@@ -990,6 +1066,9 @@ void EditorApp::clearScene()
|
||||
void EditorApp::startNewGame(const Ogre::String &scenePath)
|
||||
{
|
||||
clearScene();
|
||||
/* F9: a new game starts from system defaults, not from the
|
||||
* cross-session cache. */
|
||||
GlobalStateStore::getInstance().clearToDefaults();
|
||||
SceneSerializer serializer(m_world, m_sceneMgr);
|
||||
if (serializer.loadFromFile(scenePath, m_uiSystem.get())) {
|
||||
PrefabSystem prefabSys(m_world, m_sceneMgr);
|
||||
@@ -1487,6 +1566,7 @@ void EditorApp::saveGame(const std::string &slotPath,
|
||||
saveData["containerState"] =
|
||||
ContainerStateRegistry::getInstance().serialize();
|
||||
saveData["itemState"] = ItemStateRegistry::getInstance().serialize();
|
||||
saveData["globalState"] = GlobalStateStore::getInstance().serialize();
|
||||
|
||||
/* Runtime entities — skip characters and player controller */
|
||||
saveData["runtimeEntities"] = nlohmann::json::array();
|
||||
@@ -1578,6 +1658,13 @@ void EditorApp::loadGame(const std::string &slotPath)
|
||||
if (saveData.contains("itemState"))
|
||||
ItemStateRegistry::getInstance().deserialize(
|
||||
saveData["itemState"]);
|
||||
/* Old saves have no globalState section: fall back to system
|
||||
* defaults (clearToDefaults) instead of keeping stale values. */
|
||||
if (saveData.contains("globalState"))
|
||||
GlobalStateStore::getInstance().deserialize(
|
||||
saveData["globalState"]);
|
||||
else
|
||||
GlobalStateStore::getInstance().clearToDefaults();
|
||||
|
||||
/* Destroy ALL spawned characters so the registry spawn is the
|
||||
* only source of characters. */
|
||||
@@ -2160,6 +2247,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
|
||||
m_doorSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Standalone doors (rebuild dirty door entities) --- */
|
||||
if (m_standaloneDoorSystem) {
|
||||
m_standaloneDoorSystem->update(evt.timeSinceLastFrame);
|
||||
}
|
||||
|
||||
/* --- Event Handler system (event-driven BTs) --- */
|
||||
if (m_eventHandlerSystem) {
|
||||
m_eventHandlerSystem->update(evt.timeSinceLastFrame);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <flecs.h>
|
||||
#include <memory>
|
||||
#include "lua/LuaState.hpp"
|
||||
#include "ProjectConfig.hpp"
|
||||
|
||||
// Forward declarations
|
||||
class EditorUISystem;
|
||||
@@ -49,6 +50,7 @@ class PathFollowingSystem;
|
||||
class GoapPlannerSystem;
|
||||
class ActuatorSystem;
|
||||
class DoorSystem;
|
||||
class StandaloneDoorSystem;
|
||||
class EventHandlerSystem;
|
||||
class ItemSystem;
|
||||
class CharacterClassSystem;
|
||||
@@ -153,7 +155,7 @@ public:
|
||||
enum class GameMode { Editor, Game };
|
||||
enum class GamePlayState { Menu, Playing, Paused };
|
||||
|
||||
EditorApp();
|
||||
EditorApp(const Ogre::String &appName = "EditSceneEditor");
|
||||
virtual ~EditorApp();
|
||||
|
||||
// OgreBites::ApplicationContext overrides
|
||||
@@ -250,6 +252,24 @@ public:
|
||||
void saveGame(const std::string &slotPath, const std::string &slotName);
|
||||
void loadGame(const std::string &slotPath);
|
||||
|
||||
// Project directory (F8)
|
||||
void setProjectConfig(const ProjectConfig &config);
|
||||
const ProjectConfig &getProjectConfig() const
|
||||
{
|
||||
return m_projectConfig;
|
||||
}
|
||||
const std::string &getProjectRoot() const
|
||||
{
|
||||
return m_projectConfig.rootDir;
|
||||
}
|
||||
/**
|
||||
* Open a project directory at runtime (editor File -> Open
|
||||
* Project...): chdir()s into it, loads its project.json, switches
|
||||
* the save directory to the project's appName and clears the
|
||||
* current scene. Returns false when the directory does not exist.
|
||||
*/
|
||||
bool openProject(const std::string &dir);
|
||||
|
||||
// Input access
|
||||
GameInputState &getGameInputState()
|
||||
{
|
||||
@@ -330,6 +350,18 @@ public:
|
||||
{
|
||||
return m_pregnancySystem.get();
|
||||
}
|
||||
NavMeshSystem *getNavMeshSystem() const
|
||||
{
|
||||
return m_navMeshSystem.get();
|
||||
}
|
||||
CellGridSystem *getCellGridSystem() const
|
||||
{
|
||||
return m_cellGridSystem.get();
|
||||
}
|
||||
DoorSystem *getDoorSystem() const
|
||||
{
|
||||
return m_doorSystem.get();
|
||||
}
|
||||
Ogre::ImGuiOverlay *getImGuiOverlay() const
|
||||
{
|
||||
return m_imguiOverlay;
|
||||
@@ -380,6 +412,7 @@ private:
|
||||
std::unique_ptr<GoapPlannerSystem> m_goapPlannerSystem;
|
||||
std::unique_ptr<ActuatorSystem> m_actuatorSystem;
|
||||
std::unique_ptr<DoorSystem> m_doorSystem;
|
||||
std::unique_ptr<StandaloneDoorSystem> m_standaloneDoorSystem;
|
||||
std::unique_ptr<EventHandlerSystem> m_eventHandlerSystem;
|
||||
std::unique_ptr<ItemSystem> m_itemSystem;
|
||||
std::unique_ptr<CharacterClassSystem> m_characterClassSystem;
|
||||
@@ -399,6 +432,10 @@ private:
|
||||
bool m_debugBuoyancy = false;
|
||||
bool m_headless = false;
|
||||
|
||||
/* Project directory (F8): empty rootDir = plain session in the
|
||||
* binary directory. */
|
||||
ProjectConfig m_projectConfig;
|
||||
|
||||
void destroyEditorSystems();
|
||||
float m_playTime = 0.0f;
|
||||
std::string m_currentBaseScene;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
#include "ProjectConfig.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
std::string sanitizeAppName(const std::string &name)
|
||||
{
|
||||
std::string out = name;
|
||||
for (auto &c : out) {
|
||||
bool ok = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '.' || c == '_' ||
|
||||
c == '-' || c == ' ';
|
||||
if (!ok)
|
||||
c = '_';
|
||||
}
|
||||
if (out.empty())
|
||||
out = "project";
|
||||
return out;
|
||||
}
|
||||
|
||||
ProjectConfig loadProjectConfig(const std::string &dir)
|
||||
{
|
||||
ProjectConfig cfg;
|
||||
cfg.rootDir = dir;
|
||||
cfg.appName = std::filesystem::path(dir).filename().string();
|
||||
|
||||
std::filesystem::path jsonPath =
|
||||
std::filesystem::path(dir) / "project.json";
|
||||
std::ifstream file(jsonPath);
|
||||
if (!file.is_open())
|
||||
return cfg;
|
||||
|
||||
try {
|
||||
nlohmann::json j;
|
||||
file >> j;
|
||||
cfg.appName = j.value("appName", cfg.appName);
|
||||
cfg.startScene = j.value("startScene", "");
|
||||
cfg.gameMode = j.value("gameMode", false);
|
||||
cfg.loaded = true;
|
||||
} catch (const std::exception &e) {
|
||||
fprintf(stderr, "WARNING: could not parse %s: %s\n",
|
||||
jsonPath.string().c_str(), e.what());
|
||||
}
|
||||
return cfg;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#ifndef EDITSCENE_PROJECT_CONFIG_HPP
|
||||
#define EDITSCENE_PROJECT_CONFIG_HPP
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Project directory configuration (F8).
|
||||
*
|
||||
* A "project" is a directory that acts as the editor/game working
|
||||
* directory: scenes, prefabs, resources.cfg, runtime config JSONs and
|
||||
* heightmaps/ are all resolved relative to it (the process chdir()s into
|
||||
* the project root at startup when --project is given, or the release
|
||||
* binary simply runs from it).
|
||||
*
|
||||
* The project root may contain a project.json:
|
||||
*
|
||||
* {
|
||||
* "appName": "My Game",
|
||||
* "startScene": "scenes/level1.json",
|
||||
* "gameMode": true
|
||||
* }
|
||||
*
|
||||
* appName - per-project identity: drives the window title and the
|
||||
* per-project save directory (<user-data>/<appName>/saves/).
|
||||
* Falls back to the directory name when missing.
|
||||
* startScene - scene loaded directly by game mode (skips the startup
|
||||
* menu) when gameMode is true.
|
||||
* gameMode - the project is a playable game; the editor binary enters
|
||||
* game mode by default for such projects (--editor forces
|
||||
* editor mode).
|
||||
*/
|
||||
struct ProjectConfig {
|
||||
/* Absolute path of the project root; empty when no project is open
|
||||
* (plain editor session in the binary directory). */
|
||||
std::string rootDir;
|
||||
|
||||
/* Per-project application name (window title, save directory). */
|
||||
std::string appName;
|
||||
|
||||
/* Game-mode start scene (relative to the project root). */
|
||||
std::string startScene;
|
||||
|
||||
/* Project defaults to game mode. */
|
||||
bool gameMode = false;
|
||||
|
||||
/* True when a project.json was found and parsed. */
|
||||
bool loaded = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Load <dir>/project.json. Tolerates a missing or malformed file:
|
||||
* appName falls back to the directory name, loaded is set false and a
|
||||
* warning is logged on parse errors. rootDir is always set to dir.
|
||||
*/
|
||||
ProjectConfig loadProjectConfig(const std::string &dir);
|
||||
|
||||
/** Make an appName safe for use as a filesystem path component. */
|
||||
std::string sanitizeAppName(const std::string &name);
|
||||
|
||||
#endif // EDITSCENE_PROJECT_CONFIG_HPP
|
||||
@@ -1,5 +1,28 @@
|
||||
#include "CellGrid.hpp"
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <cstdio>
|
||||
|
||||
const std::string& CellGridComponent::ensureGridUid()
|
||||
{
|
||||
if (!gridUid.empty())
|
||||
return gridUid;
|
||||
|
||||
// Random UUID-like hex string (8-4-4-4-12), no external dependency.
|
||||
std::random_device rd;
|
||||
std::mt19937_64 gen(((uint64_t)rd() << 32) ^ (uint64_t)rd());
|
||||
uint64_t a = gen(), b = gen();
|
||||
char buf[40];
|
||||
snprintf(buf, sizeof(buf), "%08x-%04x-%04x-%04x-%04x%08x",
|
||||
(unsigned)(a & 0xffffffffu),
|
||||
(unsigned)((a >> 32) & 0xffffu),
|
||||
(unsigned)(((a >> 48) & 0x0fffu) | 0x4000u), // version 4
|
||||
(unsigned)((b & 0x3fffu) | 0x8000u), // variant 1
|
||||
(unsigned)((b >> 16) & 0xffffu),
|
||||
(unsigned)((b >> 32) & 0xffffffffu));
|
||||
gridUid = buf;
|
||||
return gridUid;
|
||||
}
|
||||
|
||||
Cell* CellGridComponent::findCell(int x, int y, int z)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
#include <chrono>
|
||||
#include <flecs.h>
|
||||
@@ -105,6 +106,38 @@ struct FurnitureCell {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Per-doorway configuration override (F0).
|
||||
*
|
||||
* Stored in CellGridComponent::doorConfigs keyed by the canonical doorway
|
||||
* edge key ("X:x:y:z" / "Z:x:y:z" - the same key buildDoorEntities() uses
|
||||
* to deduplicate doorways, see CellGridSystem::doorEdgeKey()). Entries
|
||||
* override the grid-wide door* defaults for one specific doorway and
|
||||
* survive scene loads and grid rebuilds. Entries whose doorway no longer
|
||||
* exists are "orphaned": they are kept (never auto-deleted) until pruned
|
||||
* or reassigned in the Cell Grid editor.
|
||||
*/
|
||||
struct CellGridDoorConfig {
|
||||
// Identity / UX
|
||||
std::string label; // user-visible name ("Kitchen door")
|
||||
|
||||
// Behaviour overrides (grid defaults apply when hasOverride is false)
|
||||
bool hasOverride = false;
|
||||
float openAngle = 100.0f; // copied from grid on first override
|
||||
float openSpeed = 180.0f;
|
||||
bool swingReversed = false; // F3
|
||||
std::string actionName;
|
||||
std::string sceneSwitchPath; // F1 (empty = normal swinging door)
|
||||
std::string sceneSwitchTarget;
|
||||
bool disabled = false; // spawn no door entity for this doorway
|
||||
|
||||
// Persistence / locking (F6)
|
||||
bool persistent = false; // track state in the global storage
|
||||
bool lockable = false;
|
||||
bool lockedByDefault = false;
|
||||
std::string keyItemId; // item that unlocks this door
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Cell grid for procedural building generation
|
||||
*
|
||||
@@ -114,6 +147,39 @@ struct FurnitureCell {
|
||||
* Used by: House/Lot generation, dungeon generation
|
||||
*/
|
||||
struct CellGridComponent {
|
||||
// Stable identity of this grid (F0): a UUID generated when the
|
||||
// component is created or first serialized. Door global IDs are
|
||||
// "<gridUid>:<edgeKey>", so they survive grid entity renames.
|
||||
std::string gridUid;
|
||||
|
||||
// Per-doorway configuration overrides keyed by canonical edge key
|
||||
// (see CellGridDoorConfig). Serialized inside this component.
|
||||
std::map<std::string, CellGridDoorConfig> doorConfigs;
|
||||
|
||||
// Generation mode (F4/F5, serialized as a string):
|
||||
// "full" - default, the whole building
|
||||
// "interiorOnly" - skip the exterior shell (external walls, external
|
||||
// window panels + frames, roofs, external corners);
|
||||
// floors, ceilings, internal walls/frames, furniture,
|
||||
// exit door wall panels + frames and all door entities
|
||||
// are still generated (paired exterior scene handles
|
||||
// the shell)
|
||||
// "exteriorOnly" - F5: only the exterior shell
|
||||
std::string generationMode = "full";
|
||||
|
||||
bool interiorOnlyMode() const { return generationMode == "interiorOnly"; }
|
||||
bool exteriorOnlyMode() const { return generationMode == "exteriorOnly"; }
|
||||
|
||||
// Window glass (F5, exteriorOnly/interiorOnly): window openings get an
|
||||
// opaque glossy pane (exteriorOnly: every external window; interiorOnly:
|
||||
// boundary windows) that hides the missing half of the building and is
|
||||
// part of the static collider set, so the player cannot climb through
|
||||
// windows. The built-in material is used when glassMaterialName is
|
||||
// empty; the glassColor alpha is ignored by it (opaque).
|
||||
Ogre::ColourValue glassColor = Ogre::ColourValue(0.4f, 0.6f, 0.8f, 0.35f);
|
||||
std::string glassMaterialName; // empty = built-in CellGridGlass
|
||||
float glassReflectivity = 0.8f; // built-in material gloss [0..1]
|
||||
|
||||
// Grid dimensions (in cells)
|
||||
int width = 10; // X dimension
|
||||
int height = 1; // Y dimension (floors)
|
||||
@@ -154,6 +220,7 @@ struct CellGridComponent {
|
||||
bool doorUseMeshMaterial = false; // Custom mesh keeps its own material
|
||||
float doorOpenAngle = 100.0f; // Swing angle when open (degrees)
|
||||
float doorOpenSpeed = 180.0f; // Swing speed (degrees per second)
|
||||
bool doorSwingReversed = false; // F3: swing to the other side
|
||||
std::string doorActionName; // Optional action run on activation
|
||||
// Scene switching doors (e.g. interior <-> exterior): when
|
||||
// doorSceneSwitchPath is set, activating a door queues an
|
||||
@@ -192,6 +259,10 @@ struct CellGridComponent {
|
||||
|
||||
// Mark for rebuild
|
||||
void markDirty() { dirty = true; version++; }
|
||||
|
||||
// Return gridUid, generating a random UUID first when empty.
|
||||
// Call before building door IDs or serializing.
|
||||
const std::string& ensureGridUid();
|
||||
|
||||
// Convert local cell position to world position
|
||||
Ogre::Vector3 cellToWorld(int x, int y, int z) const;
|
||||
|
||||
@@ -27,6 +27,9 @@ struct DoorComponent {
|
||||
// Swing configuration (copied from CellGridComponent at build time)
|
||||
float openAngle = 100.0f; // Target angle when open (degrees)
|
||||
float openSpeed = 180.0f; // Swing speed (degrees per second)
|
||||
bool swingReversed = false; // F3: swing to the other side (negates
|
||||
// the applied angle; currentAngle and
|
||||
// openAngle stay positive)
|
||||
|
||||
// Runtime: current swing angle (0 = closed)
|
||||
float currentAngle = 0.0f;
|
||||
@@ -39,11 +42,39 @@ struct DoorComponent {
|
||||
// node sits on the side edge of the doorway)
|
||||
Ogre::Vector3 centerOffset = Ogre::Vector3::ZERO;
|
||||
|
||||
// Scene switching (copied from CellGridComponent at build time):
|
||||
// when sceneSwitchPath is set, activation switches scenes instead
|
||||
// of swinging the leaf
|
||||
// Door identity (F0): canonical doorway edge key within the owning
|
||||
// grid ("X:x:y:z" / "Z:x:y:z") and the global persistent ID
|
||||
// "<gridUid>:<edgeKey>" (empty = ephemeral door, never persisted).
|
||||
std::string edgeKey;
|
||||
std::string doorId;
|
||||
|
||||
// Persistence / locking (F6, copied from the per-door
|
||||
// CellGridDoorConfig at build time). The actual locked and
|
||||
// open/closed state lives in the GlobalStateStore under
|
||||
// "door.<doorId>.locked" / "door.<doorId>.isOpen"; these flags only
|
||||
// say the door participates. See DoorSystem::isDoorLocked() /
|
||||
// setDoorLocked().
|
||||
bool persistent = false; // open/closed state is persisted
|
||||
bool lockable = false; // can be locked/unlocked
|
||||
std::string keyItemId; // inventory item that unlocks/locks it
|
||||
|
||||
// Scene switching (copied from CellGridComponent or the per-door
|
||||
// CellGridDoorConfig override at build time):
|
||||
// when sceneSwitchPath is set, activation swings the leaf open and
|
||||
// the scene switch fires once the door is fully open (F1)
|
||||
std::string sceneSwitchPath; // Target scene path (empty = disabled)
|
||||
std::string sceneSwitchTarget; // Teleport target entity name
|
||||
|
||||
// F1: a scene-switch door first swings open; DoorSystem fires the
|
||||
// switch (and the "door_scene_switch" event) only when the leaf
|
||||
// reaches openAngle. Runtime only, set by ActuatorSystem.
|
||||
bool sceneSwitchPending = false;
|
||||
|
||||
// F1: unlit 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.
|
||||
Ogre::Entity *occluder = nullptr;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DOOR_HPP
|
||||
|
||||
@@ -27,6 +27,10 @@ struct NavMeshComponent {
|
||||
float regionMergeSize = 20.0f;
|
||||
int tileSize = 48; // cells per tile
|
||||
|
||||
// F7: traversal cost multiplier for doorway polys (> 1 makes
|
||||
// paths prefer doorless routes but still allows doorways)
|
||||
float doorAreaCost = 5.0f;
|
||||
|
||||
// Runtime flags
|
||||
bool enabled = true;
|
||||
bool debugDraw = false;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
#ifndef EDITSCENE_STANDALONE_DOOR_HPP
|
||||
#define EDITSCENE_STANDALONE_DOOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Standalone door component (F2, serialized).
|
||||
*
|
||||
* A door without a CellGrid: the entity's TransformComponent marks the
|
||||
* doorway (center of the opening at floor level; local +X runs along the
|
||||
* wall, +Z faces out of the room) and StandaloneDoorSystem builds the
|
||||
* runtime door subtree under it with DoorBuilder - hinge node, leaf,
|
||||
* collider child, actuator and (for scene-switch doors) the F1 black
|
||||
* occluder, identical to CellGrid doors.
|
||||
*
|
||||
* All behaviour flags mirror the CellGrid per-door configuration
|
||||
* (CellGridDoorConfig); persistence and locking work through the same
|
||||
* GlobalStateStore keys "door.<doorId>.locked" / ".isOpen".
|
||||
*
|
||||
* Identity: `doorId` is the global persistent ID, user-editable and
|
||||
* expected to be unique scene-wide (the editor warns on duplicates).
|
||||
* When empty while the door needs one (persistent/lockable/scene-switch),
|
||||
* a random UUID is generated on first build and stays in the serialized
|
||||
* component from then on. Empty + no persistence flags = ephemeral door.
|
||||
*/
|
||||
struct StandaloneDoorComponent {
|
||||
// Leaf geometry
|
||||
std::string meshName; // empty = procedural box leaf
|
||||
bool useMeshMaterial = false; // keep the custom mesh's material
|
||||
std::string rectName; // UV rect name in the entity's own
|
||||
// ProceduralTexture (optional)
|
||||
float leafWidth = 1.0f; // procedural leaf dimensions
|
||||
float leafHeight = 2.0f; // (explicit, not cell-derived)
|
||||
float leafThickness = 0.08f;
|
||||
|
||||
// Behaviour
|
||||
float openAngle = 100.0f; // degrees
|
||||
float openSpeed = 180.0f; // degrees per second
|
||||
bool swingReversed = false; // F3
|
||||
std::string actionName; // optional actuator action
|
||||
std::string sceneSwitchPath; // F1 (empty = normal swinging door)
|
||||
std::string sceneSwitchTarget;
|
||||
|
||||
// Persistence / locking (F6)
|
||||
bool persistent = false;
|
||||
bool lockable = false;
|
||||
bool lockedByDefault = false;
|
||||
std::string keyItemId;
|
||||
std::string doorId; // global ID; see the class comment
|
||||
|
||||
// Runtime: rebuild the door subtree on the next
|
||||
// StandaloneDoorSystem::update (set by the editor on changes;
|
||||
// not serialized)
|
||||
bool dirty = true;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_STANDALONE_DOOR_HPP
|
||||
@@ -0,0 +1,21 @@
|
||||
#include "StandaloneDoor.hpp"
|
||||
#include "../ui/ComponentRegistration.hpp"
|
||||
#include "../ui/StandaloneDoorEditor.hpp"
|
||||
|
||||
REGISTER_COMPONENT_GROUP("Standalone Door", "Game", StandaloneDoorComponent,
|
||||
StandaloneDoorEditor)
|
||||
{
|
||||
registry.registerComponent<StandaloneDoorComponent>(
|
||||
"Standalone Door", "Game",
|
||||
std::make_unique<StandaloneDoorEditor>(),
|
||||
// Adder
|
||||
[](flecs::entity e) {
|
||||
if (!e.has<StandaloneDoorComponent>())
|
||||
e.set<StandaloneDoorComponent>({});
|
||||
},
|
||||
// Remover
|
||||
[](flecs::entity e) {
|
||||
if (e.has<StandaloneDoorComponent>())
|
||||
e.remove<StandaloneDoorComponent>();
|
||||
});
|
||||
}
|
||||
@@ -32,11 +32,34 @@ set(DEMO_SOURCES ${EDITSCENE_SOURCES})
|
||||
list(REMOVE_ITEM DEMO_SOURCES main.cpp)
|
||||
list(TRANSFORM DEMO_SOURCES PREPEND "${EDITSCENE_SOURCE_DIR}/")
|
||||
|
||||
# --- Embedded project configuration (F8 release binary) ---------------
|
||||
# Read project.json at configure time and embed its parameters into the
|
||||
# binary via a generated project.h, so the release binary is attached to
|
||||
# its project directory without needing --project. Editing project.json
|
||||
# re-triggers the CMake configure step (CMAKE_CONFIGURE_DEPENDS).
|
||||
set(PROJECT_JSON_PATH "${CMAKE_CURRENT_SOURCE_DIR}/project.json")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
|
||||
"${PROJECT_JSON_PATH}")
|
||||
file(READ "${PROJECT_JSON_PATH}" PROJECT_JSON_TEXT)
|
||||
string(JSON PROJECT_APP_NAME GET "${PROJECT_JSON_TEXT}" appName)
|
||||
string(JSON PROJECT_START_SCENE GET "${PROJECT_JSON_TEXT}" startScene)
|
||||
string(JSON PROJECT_GAME_MODE GET "${PROJECT_JSON_TEXT}" gameMode)
|
||||
if(PROJECT_GAME_MODE)
|
||||
set(PROJECT_GAME_MODE_VALUE 1)
|
||||
else()
|
||||
set(PROJECT_GAME_MODE_VALUE 0)
|
||||
endif()
|
||||
configure_file("${CMAKE_CURRENT_SOURCE_DIR}/project.h.in"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/generated/project.h" @ONLY)
|
||||
|
||||
add_executable(demoSceneSwitchingExtra
|
||||
demo_main.cpp
|
||||
${DEMO_SOURCES}
|
||||
)
|
||||
|
||||
target_compile_definitions(demoSceneSwitchingExtra
|
||||
PRIVATE EDITSCENE_HAS_EMBEDDED_PROJECT)
|
||||
|
||||
add_dependencies(demoSceneSwitchingExtra morph)
|
||||
|
||||
# Define JPH_DEBUG_RENDERER for physics debug drawing (same as editor)
|
||||
@@ -65,6 +88,7 @@ target_link_libraries(demoSceneSwitchingExtra
|
||||
)
|
||||
|
||||
target_include_directories(demoSceneSwitchingExtra PRIVATE
|
||||
${CMAKE_CURRENT_BINARY_DIR}/generated
|
||||
${EDITSCENE_SOURCE_DIR}
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/Recast/Include
|
||||
${EDITSCENE_SOURCE_DIR}/recastnavigation/Detour/Include
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
#include <iostream>
|
||||
#include "EditorApp.hpp"
|
||||
#include "ProjectConfig.hpp"
|
||||
#include "camera/EditorCamera.hpp"
|
||||
#include "systems/CharacterRegistry.hpp"
|
||||
#include "systems/BehaviorTreeSystem.hpp"
|
||||
#include "components/ActionDatabase.hpp"
|
||||
#include "systems/DoorSystem.hpp"
|
||||
#include "systems/EventBus.hpp"
|
||||
#include "systems/GlobalStateStore.hpp"
|
||||
#include "components/Door.hpp"
|
||||
#include "components/EntityName.hpp"
|
||||
#include "components/Transform.hpp"
|
||||
#include <Ogre.h>
|
||||
#include <OgreRoot.h>
|
||||
#include <ProceduralBoxGenerator.h>
|
||||
#include <ProceduralMeshGenerator.h>
|
||||
#include <filesystem>
|
||||
|
||||
#ifdef EDITSCENE_HAS_EMBEDDED_PROJECT
|
||||
#include "project.h"
|
||||
#endif
|
||||
|
||||
struct ExitAfterFirstFrameListener : public Ogre::FrameListener {
|
||||
Ogre::Root *root;
|
||||
@@ -28,6 +34,8 @@ struct ExitAfterFirstFrameListener : public Ogre::FrameListener {
|
||||
/* Same application as the editor/game, but with a larger initial window. */
|
||||
class DemoApp : public EditorApp {
|
||||
public:
|
||||
using EditorApp::EditorApp;
|
||||
|
||||
OgreBites::NativeWindowPair
|
||||
createWindow(const Ogre::String &name, uint32_t w, uint32_t h,
|
||||
Ogre::NameValuePairList miscParams) override
|
||||
@@ -73,75 +81,56 @@ static void createFloorMesh(const Ogre::String &meshName,
|
||||
mesh->getSubMesh(0)->setMaterialName(materialName);
|
||||
}
|
||||
|
||||
/*
|
||||
* Visible pillar meshes marking the actuator positions ("portal_a" in
|
||||
* scene A, "portal_b" in scene B). The ActuatorSystem already draws a
|
||||
* screen-space indicator, but a physical marker makes the spot visible
|
||||
* from across the floor. The box is 0.6 x 1.6 x 0.6 centered on the
|
||||
* entity origin, so the entities sit at y = 0.8.
|
||||
*/
|
||||
static void createMarkerMesh(const Ogre::String &meshName,
|
||||
const Ogre::String &materialName,
|
||||
float dr, float dg, float db)
|
||||
{
|
||||
const Ogre::String group =
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME;
|
||||
|
||||
if (!Ogre::MeshManager::getSingleton()
|
||||
.getByName(meshName, group)
|
||||
.isNull())
|
||||
return;
|
||||
|
||||
Ogre::MaterialPtr mat = Ogre::MaterialManager::getSingleton().create(
|
||||
materialName, group);
|
||||
Ogre::Pass *pass = mat->getTechnique(0)->getPass(0);
|
||||
pass->setDiffuse(dr, dg, db, 1.0f);
|
||||
pass->setAmbient(dr * 0.4f, dg * 0.4f, db * 0.4f);
|
||||
pass->setSpecular(0.1f, 0.1f, 0.1f, 1.0f);
|
||||
|
||||
Procedural::BoxGenerator boxGen;
|
||||
boxGen.setSizeX(0.6f).setSizeY(1.6f).setSizeZ(0.6f);
|
||||
Ogre::MeshPtr mesh = boxGen.realizeMesh(meshName, group);
|
||||
if (mesh && mesh->getNumSubMeshes() > 0)
|
||||
mesh->getSubMesh(0)->setMaterialName(materialName);
|
||||
}
|
||||
|
||||
static void createDemoResources()
|
||||
{
|
||||
/* Scene A: green floor, orange portal marker. */
|
||||
/* Scene A: green floor. */
|
||||
createFloorMesh("DemoFloorPlaneA", "DemoFloorMaterialA",
|
||||
0.35f, 0.5f, 0.35f);
|
||||
createMarkerMesh("DemoActuatorMarkerA", "DemoActuatorMarkerMaterialA",
|
||||
0.9f, 0.5f, 0.1f);
|
||||
|
||||
/* Scene B: blue floor, magenta portal marker. */
|
||||
/* Scene B: blue floor. */
|
||||
createFloorMesh("DemoFloorPlaneB", "DemoFloorMaterialB",
|
||||
0.3f, 0.4f, 0.6f);
|
||||
createMarkerMesh("DemoActuatorMarkerB", "DemoActuatorMarkerMaterialB",
|
||||
0.8f, 0.2f, 0.6f);
|
||||
}
|
||||
|
||||
/*
|
||||
* End-to-end check for --test-switch: drives the same path an E-press on
|
||||
* the actuator would take (ActionDatabase action -> behavior tree ->
|
||||
* "switchScene" node -> EditorApp::switchScene() queue ->
|
||||
* performSceneSwitch() on the next frame with the "@arrival_*" teleport),
|
||||
* for a full A -> B -> A round trip. The prompt/targeting glue is
|
||||
* screen-space and therefore not covered headless. The ActuatorSystem is
|
||||
* not involved because its interaction path needs an ImGui context; the
|
||||
* tree is evaluated through a local BehaviorTreeSystem exactly like
|
||||
* ActuatorSystem::isActionComplete() does.
|
||||
* a scene-switch door takes (DoorComponent toggleRequested +
|
||||
* sceneSwitchPending -> DoorSystem swing -> EditorApp::switchScene()
|
||||
* queue once the leaf is fully open -> performSceneSwitch() on the next
|
||||
* frame with the "@arrival_*" teleport), for a full A -> B -> A round
|
||||
* trip: scene A exits through the "interrior" grid's door Z:0:0:15 and
|
||||
* scene B returns through its own grid's door Z:0:0:0. The
|
||||
* prompt/targeting glue is screen-space and therefore not covered
|
||||
* headless.
|
||||
*/
|
||||
struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
EditorApp *app;
|
||||
BehaviorTreeSystem *bt;
|
||||
int frame = 0;
|
||||
int phase = 0;
|
||||
int phase = 10; /* 10-12: F6 locked door, then 0-3 scene switching */
|
||||
bool failed = false;
|
||||
Ogre::String failReason;
|
||||
|
||||
SceneSwitchTestListener(EditorApp *a, BehaviorTreeSystem *b)
|
||||
: app(a), bt(b)
|
||||
/* F6 demo door: internal doorway Z:0:0:8 of the "interrior" grid,
|
||||
* lockable + locked by default (doorConfigs in demo_scene_a.json);
|
||||
* the inline scene script unlocks it on the first door_locked bump. */
|
||||
const Ogre::String doorId =
|
||||
"d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6:Z:0:0:8";
|
||||
|
||||
/* F1 demo doors: scene A exits through the external doorway
|
||||
* Z:0:0:15 of the "interrior" grid (scene-switch door to
|
||||
* demo_scene_b.json), scene B returns through the external doorway
|
||||
* Z:0:0:0 of its own exteriorOnly grid (scene-switch door back to
|
||||
* demo_scene_a.json). For both legs the switch must fire only
|
||||
* when the leaf is fully open. */
|
||||
const Ogre::String exitDoorId =
|
||||
"d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6:Z:0:0:15";
|
||||
const Ogre::String returnDoorId =
|
||||
"77370b1d-e8ab-44a6-865b-e55c15b0fc78:Z:0:0:0";
|
||||
int exitOpenWait = 0;
|
||||
int returnOpenWait = 0;
|
||||
|
||||
SceneSwitchTestListener(EditorApp *a)
|
||||
: app(a)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -156,44 +145,79 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
return found;
|
||||
}
|
||||
|
||||
/* Returns false while the player is not available yet (retry).
|
||||
* Hard failures set failed/failReason. */
|
||||
bool runAction(const char *actionName, const char *expectPath)
|
||||
/* The F6 demo door entity (recreated on every scene load). */
|
||||
flecs::entity findDoor()
|
||||
{
|
||||
flecs::entity player = app->getPlayerCharacterEntity();
|
||||
if (!player.is_alive())
|
||||
return findDoorById(doorId);
|
||||
}
|
||||
|
||||
flecs::entity findDoorById(const Ogre::String &id)
|
||||
{
|
||||
flecs::entity result = flecs::entity::null();
|
||||
app->getWorld()->query<DoorComponent>().each(
|
||||
[&](flecs::entity e, DoorComponent &d) {
|
||||
if (d.doorId == id)
|
||||
result = e;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Emulate the E-press on a scene-switch door. */
|
||||
bool requestDoorSwitch(const Ogre::String &id)
|
||||
{
|
||||
flecs::entity door = findDoorById(id);
|
||||
if (!door.is_alive())
|
||||
return false;
|
||||
|
||||
ActionDatabase *db = ActionDatabase::getSingletonPtr();
|
||||
const GoapAction *action =
|
||||
db ? db->findAction(actionName) : nullptr;
|
||||
if (!action) {
|
||||
DoorComponent &d = door.get_mut<DoorComponent>();
|
||||
if (!d.occluder) {
|
||||
failed = true;
|
||||
failReason = Ogre::String("action not found: ") +
|
||||
actionName;
|
||||
return true;
|
||||
}
|
||||
|
||||
BehaviorTreeSystem::Status status =
|
||||
bt->evaluatePlayerAction(player.id(),
|
||||
action->behaviorTree, 0.016f,
|
||||
true);
|
||||
if (status != BehaviorTreeSystem::Status::success) {
|
||||
failed = true;
|
||||
failReason = Ogre::String("action did not succeed: ") +
|
||||
actionName;
|
||||
return true;
|
||||
}
|
||||
if (!app->hasPendingSceneSwitch() ||
|
||||
app->getPendingSceneSwitchPath() != expectPath) {
|
||||
failed = true;
|
||||
failReason = Ogre::String("no pending switch to ") +
|
||||
expectPath;
|
||||
failReason = "F1 scene-switch door has no occluder";
|
||||
return true;
|
||||
}
|
||||
d.toggleRequested = true;
|
||||
d.sceneSwitchPending = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Waits for the queued scene switch of a scene-switch door;
|
||||
* returns true once the switch to expectPath is pending (the door
|
||||
* must be fully open by then). */
|
||||
bool waitDoorSwitch(const Ogre::String &id, const Ogre::String &label,
|
||||
const char *expectPath, int &openWait)
|
||||
{
|
||||
flecs::entity door = findDoorById(id);
|
||||
if (!door.is_alive())
|
||||
return false;
|
||||
const DoorComponent &d = door.get<DoorComponent>();
|
||||
bool fullyOpen = d.isOpen && d.currentAngle == d.openAngle;
|
||||
if (app->hasPendingSceneSwitch()) {
|
||||
if (!fullyOpen) {
|
||||
failed = true;
|
||||
failReason =
|
||||
"F1 scene switch fired before the door was fully open";
|
||||
return true;
|
||||
}
|
||||
if (app->getPendingSceneSwitchPath() != expectPath) {
|
||||
failed = true;
|
||||
failReason = "F1 wrong pending switch path";
|
||||
return true;
|
||||
}
|
||||
std::cout << "[test] F1 " << label
|
||||
<< " fully open, switch queued" << std::endl;
|
||||
return true;
|
||||
}
|
||||
if (fullyOpen) {
|
||||
/* Frame-listener ordering grace. */
|
||||
if (++openWait > 5) {
|
||||
failed = true;
|
||||
failReason = "F1 scene switch never fired";
|
||||
}
|
||||
} else {
|
||||
openWait = 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool checkPlayerNear(const Ogre::Vector3 &expected, float tol)
|
||||
{
|
||||
flecs::entity player = app->getPlayerCharacterEntity();
|
||||
@@ -218,9 +242,8 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
|
||||
/* 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 with the portal
|
||||
* pillar (z = 28) behind the camera, and that the character's
|
||||
* visual facing (local +Z) points at the center too. */
|
||||
* character looking at the walking surface, and that the
|
||||
* character's visual facing (local +Z) points at the center too. */
|
||||
bool checkCameraFacesCenter(const Ogre::Vector3 &charPos)
|
||||
{
|
||||
flecs::entity player = app->getPlayerCharacterEntity();
|
||||
@@ -269,7 +292,10 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
app->getRoot()->queueEndRendering();
|
||||
return true;
|
||||
}
|
||||
if (frame > 1200) {
|
||||
/* The swings are real-time (doorOpenSpeed deg/s) while a
|
||||
* headless frame is sub-millisecond, so the door legs need a
|
||||
* few thousand frames (~10 s wall clock at 1000+ fps). */
|
||||
if (frame > 10000) {
|
||||
failed = true;
|
||||
failReason = "timeout waiting for scene switch";
|
||||
app->getRoot()->queueEndRendering();
|
||||
@@ -277,18 +303,92 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
}
|
||||
|
||||
switch (phase) {
|
||||
case 0:
|
||||
case 10: {
|
||||
/* F6: the demo door starts locked (lockedByDefault). */
|
||||
if (frame < 5)
|
||||
break; /* let the character spawn */
|
||||
if (!runAction("goto_scene_b", "demo_scene_b.json"))
|
||||
break;
|
||||
if (!failed)
|
||||
std::cout << "[test] queued switch A -> B"
|
||||
<< std::endl;
|
||||
phase = 1;
|
||||
flecs::entity door = findDoor();
|
||||
if (!door.is_alive()) {
|
||||
if (frame > 60) {
|
||||
failed = true;
|
||||
failReason = "F6 demo door not found";
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!DoorSystem::isDoorLockedById(doorId)) {
|
||||
failed = true;
|
||||
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);
|
||||
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;
|
||||
flecs::entity door = findDoor();
|
||||
if (!door.is_alive())
|
||||
break;
|
||||
door.get_mut<DoorComponent>().toggleRequested = true;
|
||||
std::cout << "[test] F6 door unlocked via scene script, "
|
||||
"opening"
|
||||
<< std::endl;
|
||||
phase = 12;
|
||||
break;
|
||||
}
|
||||
case 12: {
|
||||
/* F6: the swing completes and the open state lands in
|
||||
* the global store. */
|
||||
flecs::entity door = findDoor();
|
||||
if (!door.is_alive())
|
||||
break;
|
||||
const DoorComponent &d = door.get<DoorComponent>();
|
||||
if (!d.isOpen || d.currentAngle != d.openAngle)
|
||||
break;
|
||||
if (!GlobalStateStore::getInstance().getBool(
|
||||
"door." + doorId + ".isOpen")) {
|
||||
failed = true;
|
||||
failReason = "F6 door open state not persisted";
|
||||
break;
|
||||
}
|
||||
std::cout << "[test] F6 door open and persisted"
|
||||
<< std::endl;
|
||||
phase = 0;
|
||||
break;
|
||||
}
|
||||
case 0: {
|
||||
/* F1: A -> B through the exit scene-switch door. */
|
||||
if (frame < 5)
|
||||
break;
|
||||
if (!findDoorById(exitDoorId).is_alive()) {
|
||||
if (frame > 60) {
|
||||
failed = true;
|
||||
failReason = "F1 exit door not found";
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (!requestDoorSwitch(exitDoorId))
|
||||
break;
|
||||
phase = 13;
|
||||
break;
|
||||
}
|
||||
case 13: {
|
||||
/* F1: the scene switch must fire only once the leaf
|
||||
* is fully open. */
|
||||
if (waitDoorSwitch(exitDoorId, "exit door",
|
||||
"demo_scene_b.json", exitOpenWait))
|
||||
phase = 1;
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
if (!entityExists("portal_b"))
|
||||
if (!entityExists("x1"))
|
||||
break;
|
||||
if (!checkPlayerNear(Ogre::Vector3(0.0f, 0.0f, 24.0f),
|
||||
1.0f))
|
||||
@@ -301,15 +401,26 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
std::cout << "[test] arrived in scene B at arrival_b, "
|
||||
"camera faces the walking surface"
|
||||
<< std::endl;
|
||||
if (!runAction("goto_scene_a", "demo_scene_a.json"))
|
||||
/* F1: B -> A through scene B's own scene-switch
|
||||
* door (external doorway Z:0:0:0 of its
|
||||
* exteriorOnly grid). */
|
||||
if (!findDoorById(returnDoorId).is_alive()) {
|
||||
failed = true;
|
||||
failReason = "F1 return door not found";
|
||||
break;
|
||||
if (!failed)
|
||||
std::cout << "[test] queued switch B -> A"
|
||||
<< std::endl;
|
||||
phase = 2;
|
||||
}
|
||||
if (!requestDoorSwitch(returnDoorId))
|
||||
break;
|
||||
phase = 14;
|
||||
break;
|
||||
case 14:
|
||||
/* F1: same fully-open contract on the way back. */
|
||||
if (waitDoorSwitch(returnDoorId, "return door",
|
||||
"demo_scene_a.json", returnOpenWait))
|
||||
phase = 2;
|
||||
break;
|
||||
case 2:
|
||||
if (!entityExists("portal_a"))
|
||||
if (!entityExists("interrior"))
|
||||
break;
|
||||
if (!checkPlayerNear(Ogre::Vector3(0.0f, 0.0f, 24.0f),
|
||||
1.0f))
|
||||
@@ -317,6 +428,25 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
if (!checkCameraFacesCenter(Ogre::Vector3(0.0f, 0.0f,
|
||||
24.0f)))
|
||||
break;
|
||||
/* F6: the door rebuilt with the scene must be snapped
|
||||
* back to the persisted open + unlocked state. */
|
||||
{
|
||||
flecs::entity door = findDoor();
|
||||
if (!door.is_alive())
|
||||
break;
|
||||
const DoorComponent &d =
|
||||
door.get<DoorComponent>();
|
||||
if (!d.isOpen || d.currentAngle != d.openAngle ||
|
||||
DoorSystem::isDoorLockedById(doorId)) {
|
||||
failed = true;
|
||||
failReason =
|
||||
"F6 door state not restored after scene switch";
|
||||
break;
|
||||
}
|
||||
std::cout << "[test] F6 door state restored "
|
||||
"after scene switch"
|
||||
<< std::endl;
|
||||
}
|
||||
if (!failed) {
|
||||
std::cout << "[test] arrived back in scene A "
|
||||
"at arrival_a, camera faces the "
|
||||
@@ -336,24 +466,36 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
* hand-authored scenes (demo_scene_a.json and demo_scene_b.json), each
|
||||
* containing a flat colored floor plane with a static physics collider,
|
||||
* a character spawner ("s1", character registry ID 2, same character
|
||||
* setup as town8.json), a PlayerControllerComponent targeting it and an
|
||||
* actuator ("portal_a" / "portal_b", marked by a colored pillar).
|
||||
* setup as town8.json) and a PlayerControllerComponent targeting it.
|
||||
* Scene A additionally holds an "interrior" entity with a
|
||||
* CellGridComponent (a room with floor, ceiling, interior walls and an
|
||||
* exit door) carrying its own ProceduralMaterial + ProceduralTexture;
|
||||
* the grid's texture rectangle names reference the texture's named rects
|
||||
* ("floor" / "ceiling") without any Lot/District/Town parent.
|
||||
* CellGridComponent in interiorOnly generation mode (a room with floor,
|
||||
* ceiling, interior walls, windows with opaque glass and an exit door)
|
||||
* carrying its own ProceduralMaterial + ProceduralTexture; the grid's
|
||||
* texture rectangle names reference the texture's named rects
|
||||
* ("floor" / "ceiling") without any Lot/District/Town parent. Scene B
|
||||
* holds an exteriorOnly CellGridComponent (entity "x1"/"w1") - just the
|
||||
* building shell with opaque window glass, seen from the outside.
|
||||
*
|
||||
* Each actuator names one action ("goto_scene_b" / "goto_scene_a") whose
|
||||
* behavior tree is defined in the scene's top-level "actionDatabase"
|
||||
* block: a sequence ending in a "switchScene" node that queues an
|
||||
* EditorApp::switchScene() to the other scene with a
|
||||
* "@arrival_a"/"@arrival_b" teleport target, so the player reappears
|
||||
* right next to the return portal. Walking into the pillar's radius
|
||||
* shows the "E goto_scene_*" prompt; pressing E runs the tree and the
|
||||
* scene switch executes at the start of the next frame. Both scenes
|
||||
* carry their own player controller, so each switch is a clean takeover
|
||||
* (see EditorApp::performSceneSwitch); travel back and forth is endless.
|
||||
* F6 demo content: scene A grid's internal doorway Z:0:0:8 is configured
|
||||
* (doorConfigs in demo_scene_a.json) as persistent + 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 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
|
||||
* scene-switch door to demo_scene_b.json (target arrival_b); scene B's
|
||||
* exteriorOnly grid sets doorSceneSwitchPath/demo_scene_a.json +
|
||||
* doorSceneSwitchTarget/arrival_a grid-wide, so its external doorway
|
||||
* Z:0:0:0 is the way back. E on such a door swings the leaf open first;
|
||||
* the scene switch fires only when the leaf reaches the open angle, and
|
||||
* a black occluder box behind the doorway hides the missing half of the
|
||||
* building while it swings. Both scenes carry their own player
|
||||
* controller, so each switch is a clean takeover (see
|
||||
* EditorApp::performSceneSwitch); travel back and forth is endless.
|
||||
*
|
||||
* The mouse is grabbed while Playing (built-in game-mode behaviour);
|
||||
* Escape toggles the pause menu, which frees the cursor.
|
||||
@@ -365,20 +507,47 @@ struct SceneSwitchTestListener : public Ogre::FrameListener {
|
||||
* without a rebuild).
|
||||
*
|
||||
* Extra flags:
|
||||
* --test-switch headless-friendly end-to-end check: executes both
|
||||
* portal actions and verifies the A -> B -> A round
|
||||
* trip and the arrival teleports; exits 0 on PASS.
|
||||
* --test-switch headless-friendly end-to-end check: verifies the F6
|
||||
* locked door (locked at start, unlocked by the scene
|
||||
* script through the door event contract, open state
|
||||
* persisted and restored across the scene switches) and
|
||||
* both F1 scene-switch doors (A -> B and B -> A each
|
||||
* fire only when the leaf is fully open, black occluder
|
||||
* present), verifying the round-trip arrival teleports;
|
||||
* exits 0 on PASS.
|
||||
*/
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
try {
|
||||
#ifdef EDITSCENE_HAS_EMBEDDED_PROJECT
|
||||
/* Release binary: project parameters are embedded at build
|
||||
* time from project.json (generated project.h); the project
|
||||
* root is the working directory the binary runs from. */
|
||||
DemoApp app(EDITSCENE_PROJECT_APP_NAME);
|
||||
{
|
||||
ProjectConfig cfg;
|
||||
cfg.rootDir =
|
||||
std::filesystem::current_path().string();
|
||||
cfg.appName = EDITSCENE_PROJECT_APP_NAME;
|
||||
cfg.startScene = EDITSCENE_PROJECT_START_SCENE;
|
||||
cfg.gameMode = EDITSCENE_PROJECT_GAME_MODE;
|
||||
cfg.loaded = true;
|
||||
app.setProjectConfig(cfg);
|
||||
}
|
||||
#else
|
||||
DemoApp app;
|
||||
#endif
|
||||
app.setGameMode(EditorApp::GameMode::Game);
|
||||
|
||||
bool headless = false;
|
||||
bool exitAfterFirstFrame = false;
|
||||
bool testSwitch = false;
|
||||
bool sceneArgGiven = false;
|
||||
#ifdef EDITSCENE_HAS_EMBEDDED_PROJECT
|
||||
Ogre::String sceneFile = EDITSCENE_PROJECT_START_SCENE;
|
||||
#else
|
||||
Ogre::String sceneFile = "demo_scene_a.json";
|
||||
#endif
|
||||
for (int i = 1; i < argc; i++) {
|
||||
Ogre::String arg = argv[i];
|
||||
if (arg == "--headless") {
|
||||
@@ -389,6 +558,7 @@ int main(int argc, char *argv[])
|
||||
testSwitch = true;
|
||||
} else if (arg.length() > 0 && arg[0] != '-') {
|
||||
sceneFile = arg;
|
||||
sceneArgGiven = true;
|
||||
}
|
||||
}
|
||||
app.setHeadless(headless);
|
||||
@@ -411,25 +581,22 @@ int main(int argc, char *argv[])
|
||||
/* Meshes + materials referenced by the scene entities. */
|
||||
createDemoResources();
|
||||
|
||||
/* With an embedded project, EditorApp::setup() skipped the
|
||||
* startup menu; the start scene defaults to the embedded
|
||||
* one and can still be overridden positionally. */
|
||||
(void)sceneArgGiven;
|
||||
std::cout << "[demo] starting new game with scene: "
|
||||
<< sceneFile << std::endl;
|
||||
app.startNewGame(sceneFile);
|
||||
std::cout << "[demo] controls: mouse = look, W/A/S/D = move, "
|
||||
"Shift = run, E = use portal, Escape = pause menu"
|
||||
"Shift = run, E = use door, Escape = pause menu"
|
||||
<< std::endl;
|
||||
|
||||
ExitAfterFirstFrameListener exitListener(app.getRoot());
|
||||
if (exitAfterFirstFrame && !testSwitch)
|
||||
app.getRoot()->addFrameListener(&exitListener);
|
||||
|
||||
/* The test evaluates the action trees exactly like
|
||||
* ActuatorSystem::isActionComplete() does; only the
|
||||
* switchScene/debugPrint nodes are used, so no animation
|
||||
* or character system is needed. */
|
||||
BehaviorTreeSystem testBt(*app.getWorld(), app.getSceneManager(),
|
||||
nullptr, nullptr);
|
||||
testBt.setEditorApp(&app);
|
||||
SceneSwitchTestListener testListener(&app, &testBt);
|
||||
SceneSwitchTestListener testListener(&app);
|
||||
if (testSwitch)
|
||||
app.getRoot()->addFrameListener(&testListener);
|
||||
|
||||
|
||||
@@ -86,32 +86,6 @@
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"behaviorTree": {
|
||||
"children": [
|
||||
{
|
||||
"name": "[demo] portal A: switching to scene B",
|
||||
"type": "debugPrint"
|
||||
},
|
||||
{
|
||||
"name": "demo_scene_b.json",
|
||||
"params": "@arrival_b",
|
||||
"type": "switchScene"
|
||||
}
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "goto_scene_b",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"bitNames": [
|
||||
@@ -130,7 +104,7 @@
|
||||
"entities": [
|
||||
{
|
||||
"children": [],
|
||||
"id": 488,
|
||||
"id": 4294967793,
|
||||
"name": {
|
||||
"name": "arrival_a"
|
||||
},
|
||||
@@ -155,7 +129,7 @@
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 489,
|
||||
"id": 4294967794,
|
||||
"light": {
|
||||
"castShadows": false,
|
||||
"constantAttenuation": 1.0,
|
||||
@@ -231,7 +205,7 @@
|
||||
},
|
||||
"shapeType": "box"
|
||||
},
|
||||
"id": 490,
|
||||
"id": 4294967792,
|
||||
"name": {
|
||||
"name": "demo_floor"
|
||||
},
|
||||
@@ -266,42 +240,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"actuator": {
|
||||
"actionNames": [
|
||||
"goto_scene_b"
|
||||
],
|
||||
"height": 1.7999999523162842,
|
||||
"radius": 1.5
|
||||
},
|
||||
"children": [],
|
||||
"id": 491,
|
||||
"name": {
|
||||
"name": "portal_a"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "DemoActuatorMarkerA",
|
||||
"visible": true
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.800000011920929,
|
||||
"z": 28.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"characterSpawner": {
|
||||
"despawnDistance": 200.0,
|
||||
@@ -309,7 +247,7 @@
|
||||
"spawnDistance": 100.0
|
||||
},
|
||||
"children": [],
|
||||
"id": 492,
|
||||
"id": 4294967790,
|
||||
"name": {
|
||||
"name": "s1"
|
||||
},
|
||||
@@ -334,7 +272,7 @@
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 493,
|
||||
"id": 4294967789,
|
||||
"name": {
|
||||
"name": "player"
|
||||
},
|
||||
@@ -569,7 +507,7 @@
|
||||
"z": 7
|
||||
},
|
||||
{
|
||||
"flags": 147463,
|
||||
"flags": 4326403,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 8
|
||||
@@ -581,13 +519,13 @@
|
||||
"z": 8
|
||||
},
|
||||
{
|
||||
"flags": 163851,
|
||||
"flags": 8521731,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 8
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 9
|
||||
@@ -599,13 +537,13 @@
|
||||
"z": 9
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 9
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 10
|
||||
@@ -617,13 +555,13 @@
|
||||
"z": 10
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 10
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 11
|
||||
@@ -635,13 +573,13 @@
|
||||
"z": 11
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 11
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 12
|
||||
@@ -653,13 +591,13 @@
|
||||
"z": 12
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 12
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 13
|
||||
@@ -671,13 +609,13 @@
|
||||
"z": 13
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 13
|
||||
},
|
||||
{
|
||||
"flags": 81943,
|
||||
"flags": 20976643,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 14
|
||||
@@ -689,7 +627,7 @@
|
||||
"z": 14
|
||||
},
|
||||
{
|
||||
"flags": 98331,
|
||||
"flags": 25171971,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 14
|
||||
@@ -697,10 +635,45 @@
|
||||
],
|
||||
"depth": 10,
|
||||
"doorActionName": "",
|
||||
"doorConfigs": {
|
||||
"Z:0:0:15": {
|
||||
"actionName": "",
|
||||
"disabled": false,
|
||||
"hasOverride": true,
|
||||
"keyItemId": "",
|
||||
"label": "Exit door",
|
||||
"lockable": false,
|
||||
"lockedByDefault": false,
|
||||
"openAngle": 100.0,
|
||||
"openSpeed": 180.0,
|
||||
"persistent": false,
|
||||
"sceneSwitchPath": "demo_scene_b.json",
|
||||
"sceneSwitchTarget": "arrival_b",
|
||||
"swingReversed": false
|
||||
},
|
||||
"Z:0:0:8": {
|
||||
"actionName": "",
|
||||
"disabled": false,
|
||||
"hasOverride": false,
|
||||
"keyItemId": "",
|
||||
"label": "Demo locked door",
|
||||
"lockable": true,
|
||||
"lockedByDefault": true,
|
||||
"openAngle": 100.0,
|
||||
"openSpeed": 180.0,
|
||||
"persistent": true,
|
||||
"sceneSwitchPath": "",
|
||||
"sceneSwitchTarget": "",
|
||||
"swingReversed": false
|
||||
}
|
||||
},
|
||||
"doorMeshName": "",
|
||||
"doorOpenAngle": 100.0,
|
||||
"doorOpenSpeed": 180.0,
|
||||
"doorRectName": "",
|
||||
"doorSceneSwitchPath": "",
|
||||
"doorSceneSwitchTarget": "",
|
||||
"doorSwingReversed": false,
|
||||
"doorUseMeshMaterial": false,
|
||||
"doorsEnabled": true,
|
||||
"extDoorFrameRectName": "",
|
||||
@@ -709,7 +682,17 @@
|
||||
"floorRectName": "floor",
|
||||
"friction": 0.5,
|
||||
"furnitureCells": [],
|
||||
"generationMode": "interiorOnly",
|
||||
"generationScript": "",
|
||||
"glassColor": [
|
||||
0.4000000059604645,
|
||||
0.6000000238418579,
|
||||
0.800000011920929,
|
||||
0.3499999940395355
|
||||
],
|
||||
"glassMaterialName": "",
|
||||
"glassReflectivity": 0.800000011920929,
|
||||
"gridUid": "d6f6a1e2-4b5c-4d6e-8f70-a1b2c3d4e5f6",
|
||||
"height": 1,
|
||||
"intDoorFrameRectName": "",
|
||||
"intWallRectName": "",
|
||||
@@ -733,7 +716,7 @@
|
||||
"minY": 0,
|
||||
"minZ": -8
|
||||
},
|
||||
"id": 495,
|
||||
"id": 4294967786,
|
||||
"name": {
|
||||
"name": "r1"
|
||||
},
|
||||
@@ -758,7 +741,7 @@
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 496,
|
||||
"id": 8589935095,
|
||||
"name": {
|
||||
"name": "r2"
|
||||
},
|
||||
@@ -810,7 +793,7 @@
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 509,
|
||||
"id": 4294967816,
|
||||
"name": {
|
||||
"name": "r3"
|
||||
},
|
||||
@@ -821,7 +804,7 @@
|
||||
"createCeiling": true,
|
||||
"createFloor": true,
|
||||
"createInteriorWalls": true,
|
||||
"createWindows": false,
|
||||
"createWindows": true,
|
||||
"exits": [
|
||||
false,
|
||||
true,
|
||||
@@ -861,7 +844,7 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 494,
|
||||
"id": 4294967785,
|
||||
"name": {
|
||||
"name": "interrior"
|
||||
},
|
||||
@@ -1511,6 +1494,10 @@
|
||||
"textureSize": 512,
|
||||
"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",
|
||||
"scriptPath": ""
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -86,32 +86,6 @@
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"behaviorTree": {
|
||||
"children": [
|
||||
{
|
||||
"name": "[demo] portal B: switching to scene A",
|
||||
"type": "debugPrint"
|
||||
},
|
||||
{
|
||||
"name": "demo_scene_a.json",
|
||||
"params": "@arrival_a",
|
||||
"type": "switchScene"
|
||||
}
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "goto_scene_a",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"bitNames": [
|
||||
@@ -138,7 +112,7 @@
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 24.0
|
||||
"z": 18.265350341796875
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.0,
|
||||
@@ -162,7 +136,7 @@
|
||||
"cellSize": 2.0,
|
||||
"cells": [
|
||||
{
|
||||
"flags": 147495,
|
||||
"flags": 37757955,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
@@ -174,13 +148,13 @@
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 163883,
|
||||
"flags": 41953283,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
@@ -192,13 +166,13 @@
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 16391,
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
@@ -210,25 +184,25 @@
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 32779,
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 81943,
|
||||
"flags": 20976643,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
},
|
||||
{
|
||||
"flags": 65555,
|
||||
"flags": 16781315,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
},
|
||||
{
|
||||
"flags": 98331,
|
||||
"flags": 25171971,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
@@ -236,10 +210,14 @@
|
||||
],
|
||||
"depth": 10,
|
||||
"doorActionName": "",
|
||||
"doorConfigs": {},
|
||||
"doorMeshName": "",
|
||||
"doorOpenAngle": 100.0,
|
||||
"doorOpenSpeed": 180.0,
|
||||
"doorRectName": "",
|
||||
"doorSceneSwitchPath": "demo_scene_a.json",
|
||||
"doorSceneSwitchTarget": "arrival_a",
|
||||
"doorSwingReversed": true,
|
||||
"doorUseMeshMaterial": false,
|
||||
"doorsEnabled": true,
|
||||
"extDoorFrameRectName": "",
|
||||
@@ -248,7 +226,17 @@
|
||||
"floorRectName": "",
|
||||
"friction": 0.5,
|
||||
"furnitureCells": [],
|
||||
"generationMode": "exteriorOnly",
|
||||
"generationScript": "",
|
||||
"glassColor": [
|
||||
0.4000000059604645,
|
||||
0.6000000238418579,
|
||||
0.800000011920929,
|
||||
0.3499999940395355
|
||||
],
|
||||
"glassMaterialName": "",
|
||||
"glassReflectivity": 0.800000011920929,
|
||||
"gridUid": "77370b1d-e8ab-44a6-865b-e55c15b0fc78",
|
||||
"height": 1,
|
||||
"intDoorFrameRectName": "",
|
||||
"intWallRectName": "",
|
||||
@@ -306,7 +294,7 @@
|
||||
"createCeiling": true,
|
||||
"createFloor": true,
|
||||
"createInteriorWalls": true,
|
||||
"createWindows": false,
|
||||
"createWindows": true,
|
||||
"exits": [
|
||||
true,
|
||||
false,
|
||||
@@ -376,9 +364,9 @@
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": -0.9691458940505981,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 32.09623718261719
|
||||
"z": 22.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
@@ -506,42 +494,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"actuator": {
|
||||
"actionNames": [
|
||||
"goto_scene_a"
|
||||
],
|
||||
"height": 1.7999999523162842,
|
||||
"radius": 1.5
|
||||
},
|
||||
"children": [],
|
||||
"id": 495,
|
||||
"name": {
|
||||
"name": "portal_b"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "DemoActuatorMarkerB",
|
||||
"visible": true
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.800000011920929,
|
||||
"z": 28.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"characterSpawner": {
|
||||
"despawnDistance": 200.0,
|
||||
@@ -549,7 +501,7 @@
|
||||
"spawnDistance": 100.0
|
||||
},
|
||||
"children": [],
|
||||
"id": 496,
|
||||
"id": 495,
|
||||
"name": {
|
||||
"name": "s1"
|
||||
},
|
||||
@@ -574,7 +526,7 @@
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 497,
|
||||
"id": 496,
|
||||
"name": {
|
||||
"name": "player"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,667 @@
|
||||
{
|
||||
"actionDatabase": {
|
||||
"actions": [
|
||||
{
|
||||
"behaviorTree": {
|
||||
"children": [
|
||||
{
|
||||
"name": "luaHello",
|
||||
"params": "message=Welcome to the game!",
|
||||
"type": "luaTask"
|
||||
},
|
||||
{
|
||||
"name": "main/action",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "action/sitting-ground",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "dly",
|
||||
"params": "9.0",
|
||||
"type": "delay"
|
||||
},
|
||||
{
|
||||
"name": "main/locomotion",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "locomotion/idle",
|
||||
"type": "setAnimationState"
|
||||
}
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "lua_hello_action",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"behaviorTree": {
|
||||
"children": [
|
||||
{
|
||||
"name": "main/action",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "action/sitting-ground",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "dly",
|
||||
"params": "6.0",
|
||||
"type": "delay"
|
||||
},
|
||||
{
|
||||
"name": "main/locomotion",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "locomotion/idle",
|
||||
"type": "setAnimationState"
|
||||
},
|
||||
{
|
||||
"name": "luaHello",
|
||||
"params": "message=\"hello, world!\"",
|
||||
"type": "luaTask"
|
||||
}
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "testAction",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"behaviorTree": {
|
||||
"children": [
|
||||
{
|
||||
"name": "[demo] portal A: switching to scene B",
|
||||
"type": "debugPrint"
|
||||
},
|
||||
{
|
||||
"name": "demo_scene_b.json",
|
||||
"params": "@arrival_b",
|
||||
"type": "switchScene"
|
||||
}
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "goto_scene_b",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"behaviorTree": {
|
||||
"children": [
|
||||
{
|
||||
"name": "[demo] portal B: switching to scene A",
|
||||
"type": "debugPrint"
|
||||
},
|
||||
{
|
||||
"name": "demo_scene_a.json",
|
||||
"params": "@arrival_a",
|
||||
"type": "switchScene"
|
||||
}
|
||||
],
|
||||
"type": "sequence"
|
||||
},
|
||||
"cost": 1,
|
||||
"effects": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
},
|
||||
"name": "goto_scene_a",
|
||||
"preconditions": {
|
||||
"bits": 0,
|
||||
"mask": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"bitNames": [
|
||||
{
|
||||
"index": 1,
|
||||
"name": "hungry"
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"name": "thirsty"
|
||||
}
|
||||
],
|
||||
"goals": []
|
||||
},
|
||||
"bookmarks": [],
|
||||
"entities": [
|
||||
{
|
||||
"children": [],
|
||||
"id": 488,
|
||||
"name": {
|
||||
"name": "arrival_b"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 24.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.0,
|
||||
"x": 0.0,
|
||||
"y": 1.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"cellGrid": {
|
||||
"ceilingRectName": "",
|
||||
"cellHeight": 4.0,
|
||||
"cellSize": 2.0,
|
||||
"cells": [
|
||||
{
|
||||
"flags": 37757955,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 2097667,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 41953283,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
},
|
||||
{
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 1
|
||||
},
|
||||
{
|
||||
"flags": 4195331,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 3,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 8390659,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 2
|
||||
},
|
||||
{
|
||||
"flags": 20976643,
|
||||
"x": -1,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
},
|
||||
{
|
||||
"flags": 16781315,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
},
|
||||
{
|
||||
"flags": 25171971,
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"z": 3
|
||||
}
|
||||
],
|
||||
"depth": 10,
|
||||
"doorActionName": "",
|
||||
"doorConfigs": {},
|
||||
"doorMeshName": "",
|
||||
"doorOpenAngle": 100.0,
|
||||
"doorOpenSpeed": 180.0,
|
||||
"doorRectName": "",
|
||||
"doorSceneSwitchPath": "demo_scene_a.json",
|
||||
"doorSceneSwitchTarget": "arrival_a",
|
||||
"doorSwingReversed": false,
|
||||
"doorUseMeshMaterial": false,
|
||||
"doorsEnabled": true,
|
||||
"extDoorFrameRectName": "",
|
||||
"extWallRectName": "",
|
||||
"extWindowFrameRectName": "",
|
||||
"floorRectName": "",
|
||||
"friction": 0.5,
|
||||
"furnitureCells": [],
|
||||
"generationMode": "exteriorOnly",
|
||||
"generationScript": "",
|
||||
"glassColor": [
|
||||
0.4000000059604645,
|
||||
0.6000000238418579,
|
||||
0.800000011920929,
|
||||
0.3499999940395355
|
||||
],
|
||||
"glassMaterialName": "",
|
||||
"glassReflectivity": 0.800000011920929,
|
||||
"gridUid": "77370b1d-e8ab-44a6-865b-e55c15b0fc78",
|
||||
"height": 1,
|
||||
"intDoorFrameRectName": "",
|
||||
"intWallRectName": "",
|
||||
"intWindowFrameRectName": "",
|
||||
"roofSideRectName": "",
|
||||
"roofTopRectName": "",
|
||||
"width": 10
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"children": [],
|
||||
"clearArea": {
|
||||
"clearCells": true,
|
||||
"clearFurniture": true,
|
||||
"clearRoofs": false,
|
||||
"clearRooms": false,
|
||||
"maxX": 10,
|
||||
"maxY": 1,
|
||||
"maxZ": 10,
|
||||
"minX": -10,
|
||||
"minY": 0,
|
||||
"minZ": -10
|
||||
},
|
||||
"id": 491,
|
||||
"name": {
|
||||
"name": "c1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 492,
|
||||
"name": {
|
||||
"name": "r1"
|
||||
},
|
||||
"room": {
|
||||
"connectedRoomIds": [],
|
||||
"createCeiling": true,
|
||||
"createFloor": true,
|
||||
"createInteriorWalls": true,
|
||||
"createWindows": true,
|
||||
"exits": [
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"fillRoomWithFurniture": false,
|
||||
"furnitureSeed": 42,
|
||||
"furnitureYOffset": 0.05000000074505806,
|
||||
"maxX": 2,
|
||||
"maxY": 1,
|
||||
"maxZ": 4,
|
||||
"minX": -1,
|
||||
"minY": 0,
|
||||
"minZ": 0,
|
||||
"persistentId": "room_1788689581735260047_499",
|
||||
"roomType": "",
|
||||
"tags": []
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 490,
|
||||
"name": {
|
||||
"name": "w1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"id": 489,
|
||||
"name": {
|
||||
"name": "x1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": -0.9691458940505981,
|
||||
"y": 0.0,
|
||||
"z": 32.09623718261719
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 493,
|
||||
"light": {
|
||||
"castShadows": false,
|
||||
"constantAttenuation": 1.0,
|
||||
"diffuseColor": {
|
||||
"a": 1.0,
|
||||
"b": 1.0,
|
||||
"g": 1.0,
|
||||
"r": 1.0
|
||||
},
|
||||
"direction": {
|
||||
"x": 0.30000001192092896,
|
||||
"y": -1.0,
|
||||
"z": 0.20000000298023224
|
||||
},
|
||||
"intensity": 1.0,
|
||||
"lightType": "directional",
|
||||
"linearAttenuation": 0.0,
|
||||
"quadraticAttenuation": 0.0,
|
||||
"range": 100.0,
|
||||
"specularColor": {
|
||||
"a": 1.0,
|
||||
"b": 0.5,
|
||||
"g": 0.5,
|
||||
"r": 0.5
|
||||
},
|
||||
"spotlightFalloff": 1.0,
|
||||
"spotlightInnerAngle": 30.0,
|
||||
"spotlightOuterAngle": 45.0
|
||||
},
|
||||
"name": {
|
||||
"name": "demo_light"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 10.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"collider": {
|
||||
"halfHeight": 1.0,
|
||||
"meshName": "",
|
||||
"offset": {
|
||||
"x": 0.0,
|
||||
"y": -0.10000000149011612,
|
||||
"z": 0.0
|
||||
},
|
||||
"parameters": {
|
||||
"x": 30.0,
|
||||
"y": 0.10000000149011612,
|
||||
"z": 30.0
|
||||
},
|
||||
"radius": 0.5,
|
||||
"rotationOffset": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"shapeType": "box"
|
||||
},
|
||||
"id": 494,
|
||||
"name": {
|
||||
"name": "demo_floor"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "DemoFloorPlaneB",
|
||||
"visible": true
|
||||
},
|
||||
"rigidBody": {
|
||||
"bodyType": "static",
|
||||
"enabled": true,
|
||||
"friction": 0.800000011920929,
|
||||
"isSensor": false,
|
||||
"mass": 1.0,
|
||||
"restitution": 0.0
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"actuator": {
|
||||
"actionNames": [
|
||||
"goto_scene_a"
|
||||
],
|
||||
"height": 1.7999999523162842,
|
||||
"radius": 1.5
|
||||
},
|
||||
"children": [],
|
||||
"id": 495,
|
||||
"name": {
|
||||
"name": "portal_b"
|
||||
},
|
||||
"renderable": {
|
||||
"meshName": "DemoActuatorMarkerB",
|
||||
"visible": true
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.800000011920929,
|
||||
"z": 28.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"characterSpawner": {
|
||||
"despawnDistance": 200.0,
|
||||
"registryId": 2,
|
||||
"spawnDistance": 100.0
|
||||
},
|
||||
"children": [],
|
||||
"id": 496,
|
||||
"name": {
|
||||
"name": "s1"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": -3.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"children": [],
|
||||
"id": 497,
|
||||
"name": {
|
||||
"name": "player"
|
||||
},
|
||||
"playerController": {
|
||||
"actuatorColor": [
|
||||
0.0,
|
||||
0.4000000059604645,
|
||||
1.0
|
||||
],
|
||||
"actuatorCooldown": 1.5,
|
||||
"actuatorDistance": 25.0,
|
||||
"actuatorLabelFontSize": 12.0,
|
||||
"cameraMode": 0,
|
||||
"distantCircleRadius": 8.0,
|
||||
"fpsBoneName": "Head",
|
||||
"idleState": "idle",
|
||||
"locomotionStateMachine": "locomotion",
|
||||
"mouseSensitivity": 0.20000000298023224,
|
||||
"nearCircleRadius": 14.0,
|
||||
"runState": "running",
|
||||
"swimFastState": "swimming-fast",
|
||||
"swimIdleState": "swim-idle",
|
||||
"swimState": "swimming",
|
||||
"targetCharacterName": "s1",
|
||||
"tpsDistance": 3.0,
|
||||
"tpsHeight": 2.0,
|
||||
"walkState": "walking"
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"rotation": {
|
||||
"w": 1.0,
|
||||
"x": 0.0,
|
||||
"y": 0.0,
|
||||
"z": 0.0
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
"y": 1.0,
|
||||
"z": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": "1.0"
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
[Window][Debug##Default]
|
||||
Pos=60,60
|
||||
Size=400,400
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Entity Hierarchy]
|
||||
Pos=0,0
|
||||
Size=300,1043
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Property Editor]
|
||||
Pos=1570,0
|
||||
Size=350,1043
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Load Scene]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][Save Scene]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
LastUsed=20260908
|
||||
|
||||
[Window][StartupMenu]
|
||||
Pos=0,0
|
||||
Size=1920,1043
|
||||
|
||||
[Window][Prefab Browser]
|
||||
Pos=300,300
|
||||
Size=250,400
|
||||
|
||||
[Window][Create Prefab]
|
||||
Pos=655,364
|
||||
Size=305,77
|
||||
|
||||
[Window][3D Cursor]
|
||||
Pos=300,100
|
||||
Size=280,350
|
||||
|
||||
[Window][Mesh Browser]
|
||||
Pos=533,240
|
||||
Size=416,406
|
||||
|
||||
[Window][Action Database (Singleton)]
|
||||
Pos=326,33
|
||||
Size=404,694
|
||||
|
||||
[Window][Dialogue Settings]
|
||||
Pos=300,100
|
||||
Size=400,500
|
||||
|
||||
[Window][DialogueBox]
|
||||
Pos=0,681
|
||||
Size=1682,227
|
||||
|
||||
[Window][Character Class Database]
|
||||
Pos=303,109
|
||||
Size=600,600
|
||||
|
||||
[Window][Character Registry]
|
||||
Pos=476,258
|
||||
Size=903,429
|
||||
|
||||
[Window][Delete Prefab]
|
||||
Pos=797,467
|
||||
Size=325,109
|
||||
|
||||
[Window][PauseMenu]
|
||||
Pos=0,0
|
||||
Size=2490,1536
|
||||
LastUsed=20260905
|
||||
|
||||
[Window][Item Registry]
|
||||
Pos=60,60
|
||||
Size=600,500
|
||||
|
||||
[Window][Inventory Dialog Config]
|
||||
Pos=300,100
|
||||
Size=350,200
|
||||
|
||||
[Window][Character Sheet]
|
||||
Pos=0,0
|
||||
Size=1920,1043
|
||||
|
||||
[Window][Load Game]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
|
||||
[Window][Save Game]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
|
||||
[Window][Animation Tree Registry]
|
||||
Pos=60,60
|
||||
Size=900,600
|
||||
|
||||
[Window][Confirm Blend Map Resolution Change]
|
||||
Pos=815,477
|
||||
Size=290,105
|
||||
|
||||
[Window][Confirm Heightmap Resolution Change]
|
||||
Pos=815,477
|
||||
Size=290,105
|
||||
|
||||
[Window][Road Graph Invalid]
|
||||
Pos=892,217
|
||||
Size=136,127
|
||||
LastUsed=20260731
|
||||
|
||||
[Window][Wedge Geometry Debug]
|
||||
Pos=10,10
|
||||
Size=420,520
|
||||
LastUsed=20260814
|
||||
|
||||
[Window][Road Graph Valid]
|
||||
Pos=882,486
|
||||
Size=156,71
|
||||
LastUsed=20260821
|
||||
|
||||
[Window][Switch Scene]
|
||||
Pos=710,321
|
||||
Size=500,400
|
||||
LastUsed=20260906
|
||||
|
||||
[Window][Open Project]
|
||||
Pos=300,100
|
||||
Size=639,377
|
||||
LastUsed=20260908
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"fontPath": "Jupiteroid-Bold.ttf",
|
||||
"fontSize": 16.0
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
/* Generated by CMake from project.json - do not edit.
|
||||
* Embeds the project parameters into the release binary so it runs its
|
||||
* project with no --project flag (see GameFeatures202609.md, F8). */
|
||||
#define EDITSCENE_PROJECT_APP_NAME "@PROJECT_APP_NAME@"
|
||||
#define EDITSCENE_PROJECT_START_SCENE "@PROJECT_START_SCENE@"
|
||||
#define EDITSCENE_PROJECT_GAME_MODE @PROJECT_GAME_MODE_VALUE@
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"appName": "demo-scene-switching-extra",
|
||||
"startScene": "demo_scene_a.json",
|
||||
"gameMode": true
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
# staging) at least once.
|
||||
|
||||
file(COPY "${EDITSCENE_SRC}/resources.cfg" DESTINATION "${DEMO_DIR}")
|
||||
file(COPY "${SRC_DIR}/project.json" DESTINATION "${DEMO_DIR}")
|
||||
|
||||
foreach(f demo_scene_a.json demo_scene_b.json)
|
||||
set(link "${DEMO_DIR}/${f}")
|
||||
|
||||
@@ -34,13 +34,16 @@ menu or startup menu.
|
||||
|
||||
### Save File Location
|
||||
|
||||
Saves are stored in an OS-dependent user data directory:
|
||||
Saves are stored in an OS-dependent user data directory. The `<appName>`
|
||||
path component defaults to `World2` and is replaced by the per-project
|
||||
`appName` when a project directory is opened (`--project`, embedded
|
||||
`project.h` in a release binary, or File -> Open Project...):
|
||||
|
||||
| Platform | Path |
|
||||
|----------|------|
|
||||
| Linux | `~/.local/share/World2/saves/` (or `$XDG_DATA_HOME/World2/saves/`) |
|
||||
| Windows | `%APPDATA%/World2/saves/` |
|
||||
| macOS | `~/Library/Application Support/World2/saves/` |
|
||||
| Linux | `~/.local/share/<appName>/saves/` (or `$XDG_DATA_HOME/<appName>/saves/`) |
|
||||
| Windows | `%APPDATA%/<appName>/saves/` |
|
||||
| macOS | `~/Library/Application Support/<appName>/saves/` |
|
||||
|
||||
Each save is a single `.json` file named `save_NNN.json`.
|
||||
|
||||
@@ -61,6 +64,7 @@ Each save is a single `.json` file named `save_NNN.json`.
|
||||
"characterRegistry": { ... },
|
||||
"containerState": { ... },
|
||||
"itemState": { ... },
|
||||
"globalState": { ... },
|
||||
"runtimeEntities": [ ... ],
|
||||
"characterRuntimeData": { ... },
|
||||
"luaData": { ... }
|
||||
@@ -76,6 +80,7 @@ Each save is a single `.json` file named `save_NNN.json`.
|
||||
| `characterRegistry` | object | Serialized `CharacterRegistry` (stats, skills, needs, levels, XP) |
|
||||
| `containerState` | object | Serialized `ContainerStateRegistry` (chest/loot contents) |
|
||||
| `itemState` | object | Serialized `ItemStateRegistry` (world item pickup state) |
|
||||
| `globalState` | object | Serialized `GlobalStateStore` (typed global variables, F9). Missing in old saves: the store falls back to system defaults |
|
||||
| `runtimeEntities` | array | Runtime-spawned entities (dropped items, etc.) |
|
||||
| `characterRuntimeData` | object | Runtime component overrides per character (inventory, GOAP state, animation state) |
|
||||
| `luaData` | object | Data collected from Lua save callbacks |
|
||||
@@ -258,6 +263,7 @@ EditorApp::saveGame(slotPath, slotName)
|
||||
├── Serialize CharacterRegistry
|
||||
├── Serialize ContainerStateRegistry
|
||||
├── Serialize ItemStateRegistry
|
||||
├── Serialize GlobalStateStore (globalState section)
|
||||
├── Serialize runtime entities (dropped items, etc.)
|
||||
├── Serialize character runtime component overrides
|
||||
├── Collect Lua save callback data
|
||||
@@ -290,6 +296,7 @@ EditorApp::loadGame(slotPath)
|
||||
├── Restore CharacterRegistry
|
||||
├── Restore ContainerStateRegistry
|
||||
├── Restore ItemStateRegistry
|
||||
├── Restore GlobalStateStore (defaults when the save has no globalState)
|
||||
├── Destroy all existing character entities
|
||||
├── Spawn persistent characters from registry
|
||||
├── Restore character runtime component overrides
|
||||
@@ -318,6 +325,50 @@ Persists container slot overrides keyed by `containerId`. Auto-saves to
|
||||
Persists world item state (picked up / disabled) keyed by `instanceId`.
|
||||
Auto-saves to `item_state.json`.
|
||||
|
||||
### GlobalStateStore (F9)
|
||||
|
||||
Generic typed key-value storage for gameplay systems: string name +
|
||||
`bool` / `int64` / `double` / `string` value, accessible from C++
|
||||
(`GlobalStateStore::getInstance()`) and Lua (`ecs.global.*`). Persisted in
|
||||
the save file's `globalState` section; auto-saves to `global_state.json`
|
||||
in game mode (the cross-session cache).
|
||||
|
||||
Key semantics:
|
||||
|
||||
- Systems declare defaults with `declareDefault()`; reading an unset
|
||||
variable returns the declared default (or the caller's fallback).
|
||||
Defaults are **not** serialized - only explicitly `set()` values are.
|
||||
- Keys are dot-namespaced; each system owns a prefix registered in
|
||||
`AGENTS.md`. Door state (F6): `door.<doorId>.locked`,
|
||||
`door.<doorId>.isOpen`.
|
||||
- **Game mode:** startup loads `global_state.json`; `startNewGame()` resets
|
||||
to defaults; `loadGame()` restores the save's `globalState` (old saves
|
||||
without it load as defaults).
|
||||
- **Editor mode:** the store is `clearToDefaults()` at startup and on scene
|
||||
(re)load (`EditorUISystem::loadScene`, `EditorApp::openProject`), the
|
||||
cache file is never loaded and auto-save is disabled, so a previous game
|
||||
session never leaks into the edited scene.
|
||||
|
||||
Lua API:
|
||||
|
||||
```lua
|
||||
ecs.global.set(name, value) -- boolean/integer/float/string; nil removes
|
||||
ecs.global.get_bool(name [, default]) -- typed reads
|
||||
ecs.global.get_int(name [, default])
|
||||
ecs.global.get_float(name [, default])
|
||||
ecs.global.get_string(name [, default])
|
||||
ecs.global.has(name)
|
||||
ecs.global.remove(name)
|
||||
```
|
||||
|
||||
See `lua-examples/global_state_example.lua`.
|
||||
|
||||
Door persistence (F6) builds on this: `door.<doorId>.locked` /
|
||||
`door.<doorId>.isOpen`, managed by `DoorSystem` (see AGENTS.md "Persistent
|
||||
door state (F6)"), with the Lua wrappers `ecs.door.is_locked/is_open/
|
||||
lock/unlock` (`lua/LuaDoorApi.cpp`, example
|
||||
`lua-examples/door_lock_example.lua`).
|
||||
|
||||
---
|
||||
|
||||
## Runtime Entities
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
-- F6: persistent door state - locked doors, events, key items.
|
||||
--
|
||||
-- Persistent door state lives in the global storage under
|
||||
-- "door.<doorId>.locked" (bool) and "door.<doorId>.isOpen" (bool)
|
||||
-- where doorId is the F0 global door ID "<gridUid>:<edgeKey>" (see the
|
||||
-- Doors panel of the CellGrid editor - the door ID is shown there and can
|
||||
-- be copied). Doors without persistent/lockable/scene-switch config are
|
||||
-- ephemeral (empty doorId) and never touch the store.
|
||||
--
|
||||
-- Event contract (EventBus, usable from C++ and Lua):
|
||||
-- door_locked_<doorId> sent when a player (or NPC) bumps into a
|
||||
-- door_locked locked door; params: door_id, entity_id.
|
||||
-- The generic variant is for handlers managing
|
||||
-- many doors.
|
||||
-- door_unlock_<doorId> send to unlock one door.
|
||||
-- door_unlock generic unlock; params: door_id.
|
||||
-- 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 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.
|
||||
|
||||
local door_id = "11111111-2222-3333-4444-555555555555:Z:0:0:8"
|
||||
|
||||
-- Reading state (also works for doors in scenes that are not loaded):
|
||||
if ecs.door.is_locked(door_id) then
|
||||
print("door is locked")
|
||||
end
|
||||
print("door open: " .. tostring(ecs.door.is_open(door_id)))
|
||||
|
||||
-- Direct control (thin wrappers over ecs.global.set on the door keys):
|
||||
-- ecs.door.lock(door_id)
|
||||
-- ecs.door.unlock(door_id)
|
||||
-- Equivalent low-level form:
|
||||
-- ecs.global.set("door." .. door_id .. ".locked", true)
|
||||
|
||||
-- Example: a quest-gated door. The first bump tells the player what is
|
||||
-- needed; once the quest flag is set in the global storage, the next bump
|
||||
-- unlocks the door.
|
||||
ecs.subscribe_event("door_locked_" .. door_id, function(event, params)
|
||||
if ecs.global.get_bool("quest.found_cellar_key", false) then
|
||||
print("the cellar key fits - the door clicks open")
|
||||
ecs.send_event("door_unlock_" .. door_id)
|
||||
else
|
||||
print("the door is locked - find the cellar key")
|
||||
-- (show a dialogue, play a sound, ...)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Somewhere in the quest script:
|
||||
-- ecs.global.set("quest.found_cellar_key", true)
|
||||
|
||||
-- Unlock notification (e.g. to award XP or advance a quest):
|
||||
ecs.subscribe_event("door_unlocked", function(event, params)
|
||||
print("unlocked: " .. tostring(params.door_id))
|
||||
end)
|
||||
@@ -0,0 +1,81 @@
|
||||
-- =============================================================================
|
||||
-- Global Persistent Storage Lua API Examples (F9)
|
||||
-- =============================================================================
|
||||
-- The ecs.global API gives scripts access to the GlobalStateStore: a
|
||||
-- scene-independent singleton of typed variables (boolean, integer, float,
|
||||
-- string) shared with C++ and persisted in the save file's "globalState"
|
||||
-- section.
|
||||
--
|
||||
-- Semantics:
|
||||
-- - Values set here are readable from C++ (GlobalStateStore::getInstance())
|
||||
-- and vice versa.
|
||||
-- - The store survives scene switches.
|
||||
-- - Game mode: variables persist across sessions via the save file and the
|
||||
-- global_state.json cache; a new game starts from system defaults.
|
||||
-- - Editor mode: the store is reset to defaults on startup and on scene
|
||||
-- (re)load, so edited scenes always start from a clean state.
|
||||
--
|
||||
-- Naming convention: dot-namespaced keys; each gameplay system owns a
|
||||
-- prefix (e.g. doors use "door.<doorId>.locked" / "door.<doorId>.isOpen",
|
||||
-- see AGENTS.md).
|
||||
-- =============================================================================
|
||||
|
||||
-- =============================================================================
|
||||
-- Setting Variables
|
||||
-- =============================================================================
|
||||
-- The type is inferred from the Lua value: boolean, integer number, float
|
||||
-- number or string. Setting nil removes the variable.
|
||||
|
||||
ecs.global.set("quest.main.started", true) -- boolean
|
||||
ecs.global.set("quest.main.stage", 2) -- integer
|
||||
ecs.global.set("player.reputation", 0.75) -- float
|
||||
ecs.global.set("world.last_scene", "town.json") -- string
|
||||
|
||||
-- =============================================================================
|
||||
-- Typed Reads
|
||||
-- =============================================================================
|
||||
-- Explicit getters avoid Lua's single-number-type ambiguity: get_int only
|
||||
-- reads integer variables, get_float only float variables. The optional
|
||||
-- second argument is the fallback returned when the variable is unset (or
|
||||
-- has a different type).
|
||||
|
||||
local started = ecs.global.get_bool("quest.main.started") -- false default
|
||||
local stage = ecs.global.get_int("quest.main.stage", 1) -- fallback 1
|
||||
local rep = ecs.global.get_float("player.reputation", 0.0)
|
||||
local scene = ecs.global.get_string("world.last_scene", "none")
|
||||
|
||||
-- An int is NOT readable as float and vice versa (fallback kicks in):
|
||||
assert(ecs.global.get_float("quest.main.stage", -1.0) == -1.0)
|
||||
|
||||
-- =============================================================================
|
||||
-- Existence and Removal
|
||||
-- =============================================================================
|
||||
|
||||
if ecs.global.has("quest.main.started") then
|
||||
print("Main quest state exists")
|
||||
end
|
||||
|
||||
ecs.global.remove("player.reputation")
|
||||
ecs.global.set("world.last_scene", nil) -- same as remove()
|
||||
|
||||
-- =============================================================================
|
||||
-- Door State Example (F6 consumer)
|
||||
-- =============================================================================
|
||||
-- Persistent doors store their state under the "door." prefix, keyed by
|
||||
-- the door's global ID (shown in the Cell Grid editor's Doors panel):
|
||||
--
|
||||
-- door.<doorId>.locked (boolean)
|
||||
-- door.<doorId>.isOpen (boolean)
|
||||
--
|
||||
-- Because the state is a plain global variable, scripts can lock/unlock
|
||||
-- doors directly:
|
||||
|
||||
local doorId = "3f2a1b4c-....:X:3:0:1" -- copy from the editor Doors panel
|
||||
|
||||
-- Unlock a door:
|
||||
ecs.global.set("door." .. doorId .. ".locked", false)
|
||||
|
||||
-- Check whether a door is open:
|
||||
if ecs.global.get_bool("door." .. doorId .. ".isOpen") then
|
||||
print("The door is open")
|
||||
end
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "components/PathFollowing.hpp"
|
||||
#include "components/EventHandler.hpp"
|
||||
#include "components/SceneScript.hpp"
|
||||
#include "components/StandaloneDoor.hpp"
|
||||
#include "components/Item.hpp"
|
||||
#include "components/Inventory.hpp"
|
||||
#include "components/GeneratedPhysicsTag.hpp"
|
||||
@@ -843,6 +844,8 @@ static void registerAllComponents()
|
||||
lua_setfield(L, -2, "enabled");
|
||||
lua_pushboolean(L, c.debugDraw ? 1 : 0);
|
||||
lua_setfield(L, -2, "debugDraw");
|
||||
lua_pushnumber(L, c.doorAreaCost);
|
||||
lua_setfield(L, -2, "doorAreaCost");
|
||||
, if (lua_getfield(L, idx, "cellSize"), lua_isnumber(L, -1))
|
||||
c.cellSize = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
@@ -866,6 +869,9 @@ static void registerAllComponents()
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "debugDraw"), lua_isboolean(L, -1))
|
||||
c.debugDraw = lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorAreaCost"), lua_isnumber(L, -1))
|
||||
c.doorAreaCost = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1););
|
||||
|
||||
// --- NavMeshAgent ---
|
||||
@@ -1422,12 +1428,57 @@ static void registerAllComponents()
|
||||
lua_setfield(L, -2, "doorOpenAngle");
|
||||
lua_pushnumber(L, c.doorOpenSpeed);
|
||||
lua_setfield(L, -2, "doorOpenSpeed");
|
||||
lua_pushboolean(L, c.doorSwingReversed ? 1 : 0);
|
||||
lua_setfield(L, -2, "doorSwingReversed");
|
||||
lua_pushstring(L, c.doorActionName.c_str());
|
||||
lua_setfield(L, -2, "doorActionName");
|
||||
lua_pushstring(L, c.doorSceneSwitchPath.c_str());
|
||||
lua_setfield(L, -2, "doorSceneSwitchPath");
|
||||
lua_pushstring(L, c.doorSceneSwitchTarget.c_str());
|
||||
lua_setfield(L, -2, "doorSceneSwitchTarget");
|
||||
lua_pushstring(L, c.gridUid.c_str());
|
||||
lua_setfield(L, -2, "gridUid");
|
||||
lua_pushstring(L, c.generationMode.c_str());
|
||||
lua_setfield(L, -2, "generationMode");
|
||||
pushColourValue(L, c.glassColor);
|
||||
lua_setfield(L, -2, "glassColor");
|
||||
lua_pushstring(L, c.glassMaterialName.c_str());
|
||||
lua_setfield(L, -2, "glassMaterialName");
|
||||
lua_pushnumber(L, c.glassReflectivity);
|
||||
lua_setfield(L, -2, "glassReflectivity");
|
||||
lua_newtable(L);
|
||||
for (const auto &pair : c.doorConfigs) {
|
||||
const CellGridDoorConfig &dc = pair.second;
|
||||
lua_newtable(L);
|
||||
lua_pushstring(L, dc.label.c_str());
|
||||
lua_setfield(L, -2, "label");
|
||||
lua_pushboolean(L, dc.hasOverride ? 1 : 0);
|
||||
lua_setfield(L, -2, "hasOverride");
|
||||
lua_pushnumber(L, dc.openAngle);
|
||||
lua_setfield(L, -2, "openAngle");
|
||||
lua_pushnumber(L, dc.openSpeed);
|
||||
lua_setfield(L, -2, "openSpeed");
|
||||
lua_pushboolean(L, dc.swingReversed ? 1 : 0);
|
||||
lua_setfield(L, -2, "swingReversed");
|
||||
lua_pushstring(L, dc.actionName.c_str());
|
||||
lua_setfield(L, -2, "actionName");
|
||||
lua_pushstring(L, dc.sceneSwitchPath.c_str());
|
||||
lua_setfield(L, -2, "sceneSwitchPath");
|
||||
lua_pushstring(L, dc.sceneSwitchTarget.c_str());
|
||||
lua_setfield(L, -2, "sceneSwitchTarget");
|
||||
lua_pushboolean(L, dc.disabled ? 1 : 0);
|
||||
lua_setfield(L, -2, "disabled");
|
||||
lua_pushboolean(L, dc.persistent ? 1 : 0);
|
||||
lua_setfield(L, -2, "persistent");
|
||||
lua_pushboolean(L, dc.lockable ? 1 : 0);
|
||||
lua_setfield(L, -2, "lockable");
|
||||
lua_pushboolean(L, dc.lockedByDefault ? 1 : 0);
|
||||
lua_setfield(L, -2, "lockedByDefault");
|
||||
lua_pushstring(L, dc.keyItemId.c_str());
|
||||
lua_setfield(L, -2, "keyItemId");
|
||||
lua_setfield(L, -2, pair.first.c_str());
|
||||
}
|
||||
lua_setfield(L, -2, "doorConfigs");
|
||||
, if (lua_getfield(L, idx, "width"), lua_isnumber(L, -1))
|
||||
c.width = (int)lua_tointeger(L, -1);
|
||||
lua_pop(L, 1);
|
||||
@@ -1462,6 +1513,10 @@ static void registerAllComponents()
|
||||
if (lua_getfield(L, idx, "doorOpenSpeed"), lua_isnumber(L, -1))
|
||||
c.doorOpenSpeed = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorSwingReversed"),
|
||||
lua_isboolean(L, -1))
|
||||
c.doorSwingReversed = lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorActionName"), lua_isstring(L, -1))
|
||||
c.doorActionName = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
@@ -1472,8 +1527,233 @@ static void registerAllComponents()
|
||||
if (lua_getfield(L, idx, "doorSceneSwitchTarget"),
|
||||
lua_isstring(L, -1))
|
||||
c.doorSceneSwitchTarget = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "gridUid"), lua_isstring(L, -1))
|
||||
c.gridUid = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "generationMode"), lua_isstring(L, -1))
|
||||
c.generationMode = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "glassColor"), lua_istable(L, -1))
|
||||
c.glassColor = readColourValue(L, lua_gettop(L));
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "glassMaterialName"),
|
||||
lua_isstring(L, -1))
|
||||
c.glassMaterialName = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "glassReflectivity"),
|
||||
lua_isnumber(L, -1))
|
||||
c.glassReflectivity = (float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorConfigs"), lua_istable(L, -1)) {
|
||||
c.doorConfigs.clear();
|
||||
lua_pushnil(L);
|
||||
while (lua_next(L, -2) != 0) {
|
||||
if (lua_isstring(L, -2) && lua_istable(L, -1)) {
|
||||
CellGridDoorConfig dc;
|
||||
if (lua_getfield(L, -1, "label"),
|
||||
lua_isstring(L, -1))
|
||||
dc.label = lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "hasOverride"),
|
||||
lua_isboolean(L, -1))
|
||||
dc.hasOverride =
|
||||
lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "openAngle"),
|
||||
lua_isnumber(L, -1))
|
||||
dc.openAngle =
|
||||
(float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "openSpeed"),
|
||||
lua_isnumber(L, -1))
|
||||
dc.openSpeed =
|
||||
(float)lua_tonumber(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "swingReversed"),
|
||||
lua_isboolean(L, -1))
|
||||
dc.swingReversed =
|
||||
lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "actionName"),
|
||||
lua_isstring(L, -1))
|
||||
dc.actionName =
|
||||
lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1,
|
||||
"sceneSwitchPath"),
|
||||
lua_isstring(L, -1))
|
||||
dc.sceneSwitchPath =
|
||||
lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1,
|
||||
"sceneSwitchTarget"),
|
||||
lua_isstring(L, -1))
|
||||
dc.sceneSwitchTarget =
|
||||
lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "disabled"),
|
||||
lua_isboolean(L, -1))
|
||||
dc.disabled =
|
||||
lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "persistent"),
|
||||
lua_isboolean(L, -1))
|
||||
dc.persistent =
|
||||
lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "lockable"),
|
||||
lua_isboolean(L, -1))
|
||||
dc.lockable =
|
||||
lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1,
|
||||
"lockedByDefault"),
|
||||
lua_isboolean(L, -1))
|
||||
dc.lockedByDefault =
|
||||
lua_toboolean(L, -1) != 0;
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, -1, "keyItemId"),
|
||||
lua_isstring(L, -1))
|
||||
dc.keyItemId =
|
||||
lua_tostring(L, -1);
|
||||
lua_pop(L, 1);
|
||||
c.doorConfigs[lua_tostring(L, -2)] = dc;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
}
|
||||
lua_pop(L, 1););
|
||||
|
||||
// --- StandaloneDoor (F2) ---
|
||||
REGISTER_COMPONENT(
|
||||
StandaloneDoorComponent, "StandaloneDoor",
|
||||
lua_pushstring(L, c.meshName.c_str());
|
||||
lua_setfield(L, -2, "meshName");
|
||||
lua_pushboolean(L, c.useMeshMaterial ? 1 : 0);
|
||||
lua_setfield(L, -2, "useMeshMaterial");
|
||||
lua_pushstring(L, c.rectName.c_str());
|
||||
lua_setfield(L, -2, "rectName");
|
||||
lua_pushnumber(L, c.leafWidth);
|
||||
lua_setfield(L, -2, "leafWidth");
|
||||
lua_pushnumber(L, c.leafHeight);
|
||||
lua_setfield(L, -2, "leafHeight");
|
||||
lua_pushnumber(L, c.leafThickness);
|
||||
lua_setfield(L, -2, "leafThickness");
|
||||
lua_pushnumber(L, c.openAngle);
|
||||
lua_setfield(L, -2, "openAngle");
|
||||
lua_pushnumber(L, c.openSpeed);
|
||||
lua_setfield(L, -2, "openSpeed");
|
||||
lua_pushboolean(L, c.swingReversed ? 1 : 0);
|
||||
lua_setfield(L, -2, "swingReversed");
|
||||
lua_pushstring(L, c.actionName.c_str());
|
||||
lua_setfield(L, -2, "actionName");
|
||||
lua_pushstring(L, c.sceneSwitchPath.c_str());
|
||||
lua_setfield(L, -2, "sceneSwitchPath");
|
||||
lua_pushstring(L, c.sceneSwitchTarget.c_str());
|
||||
lua_setfield(L, -2, "sceneSwitchTarget");
|
||||
lua_pushboolean(L, c.persistent ? 1 : 0);
|
||||
lua_setfield(L, -2, "persistent");
|
||||
lua_pushboolean(L, c.lockable ? 1 : 0);
|
||||
lua_setfield(L, -2, "lockable");
|
||||
lua_pushboolean(L, c.lockedByDefault ? 1 : 0);
|
||||
lua_setfield(L, -2, "lockedByDefault");
|
||||
lua_pushstring(L, c.keyItemId.c_str());
|
||||
lua_setfield(L, -2, "keyItemId");
|
||||
lua_pushstring(L, c.doorId.c_str());
|
||||
lua_setfield(L, -2, "doorId");
|
||||
, bool changed = false;
|
||||
if (lua_getfield(L, idx, "meshName"), lua_isstring(L, -1)) {
|
||||
c.meshName = lua_tostring(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "useMeshMaterial"), lua_isboolean(L, -1)) {
|
||||
c.useMeshMaterial = lua_toboolean(L, -1) != 0;
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "rectName"), lua_isstring(L, -1)) {
|
||||
c.rectName = lua_tostring(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "leafWidth"), lua_isnumber(L, -1)) {
|
||||
c.leafWidth = (float)lua_tonumber(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "leafHeight"), lua_isnumber(L, -1)) {
|
||||
c.leafHeight = (float)lua_tonumber(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "leafThickness"), lua_isnumber(L, -1)) {
|
||||
c.leafThickness = (float)lua_tonumber(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "openAngle"), lua_isnumber(L, -1)) {
|
||||
c.openAngle = (float)lua_tonumber(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "openSpeed"), lua_isnumber(L, -1)) {
|
||||
c.openSpeed = (float)lua_tonumber(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "swingReversed"), lua_isboolean(L, -1)) {
|
||||
c.swingReversed = lua_toboolean(L, -1) != 0;
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "actionName"), lua_isstring(L, -1)) {
|
||||
c.actionName = lua_tostring(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "sceneSwitchPath"),
|
||||
lua_isstring(L, -1)) {
|
||||
c.sceneSwitchPath = lua_tostring(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "sceneSwitchTarget"),
|
||||
lua_isstring(L, -1)) {
|
||||
c.sceneSwitchTarget = lua_tostring(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "persistent"), lua_isboolean(L, -1)) {
|
||||
c.persistent = lua_toboolean(L, -1) != 0;
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "lockable"), lua_isboolean(L, -1)) {
|
||||
c.lockable = lua_toboolean(L, -1) != 0;
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "lockedByDefault"),
|
||||
lua_isboolean(L, -1)) {
|
||||
c.lockedByDefault = lua_toboolean(L, -1) != 0;
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "keyItemId"), lua_isstring(L, -1)) {
|
||||
c.keyItemId = lua_tostring(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (lua_getfield(L, idx, "doorId"), lua_isstring(L, -1)) {
|
||||
c.doorId = lua_tostring(L, -1);
|
||||
changed = true;
|
||||
}
|
||||
lua_pop(L, 1);
|
||||
if (changed)
|
||||
c.dirty = true;);
|
||||
|
||||
// --- Room ---
|
||||
REGISTER_COMPONENT(
|
||||
RoomComponent, "Room", lua_pushinteger(L, c.minX);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#include "LuaDoorApi.hpp"
|
||||
#include "../systems/DoorSystem.hpp"
|
||||
#include "../systems/GlobalStateStore.hpp"
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
static int luaDoorIsLocked(lua_State *L)
|
||||
{
|
||||
const char *doorId = luaL_checkstring(L, 1);
|
||||
lua_pushboolean(L, DoorSystem::isDoorLockedById(doorId) ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaDoorIsOpen(lua_State *L)
|
||||
{
|
||||
const char *doorId = luaL_checkstring(L, 1);
|
||||
lua_pushboolean(L, GlobalStateStore::getInstance().getBool(
|
||||
std::string("door.") + doorId + ".isOpen") ?
|
||||
1 :
|
||||
0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaDoorLock(lua_State *L)
|
||||
{
|
||||
const char *doorId = luaL_checkstring(L, 1);
|
||||
DoorSystem::setDoorLocked(doorId, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int luaDoorUnlock(lua_State *L)
|
||||
{
|
||||
const char *doorId = luaL_checkstring(L, 1);
|
||||
DoorSystem::setDoorLocked(doorId, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void registerLuaDoorApi(lua_State *L)
|
||||
{
|
||||
lua_getglobal(L, "ecs");
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
lua_newtable(L);
|
||||
lua_pushcfunction(L, luaDoorIsLocked);
|
||||
lua_setfield(L, -2, "is_locked");
|
||||
lua_pushcfunction(L, luaDoorIsOpen);
|
||||
lua_setfield(L, -2, "is_open");
|
||||
lua_pushcfunction(L, luaDoorLock);
|
||||
lua_setfield(L, -2, "lock");
|
||||
lua_pushcfunction(L, luaDoorUnlock);
|
||||
lua_setfield(L, -2, "unlock");
|
||||
lua_setfield(L, -2, "door");
|
||||
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef EDITSCENE_LUA_DOOR_API_HPP
|
||||
#define EDITSCENE_LUA_DOOR_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register the door persistence Lua API (F6).
|
||||
*
|
||||
* Adds the "ecs.door" table:
|
||||
* ecs.door.is_locked(doorId) -- persistent locked state
|
||||
* ecs.door.is_open(doorId) -- persisted open/closed state
|
||||
* ecs.door.lock(doorId) -- set the locked state
|
||||
* ecs.door.unlock(doorId) -- clear it (fires door_unlocked_* events)
|
||||
*
|
||||
* All functions operate on the GlobalStateStore door keys
|
||||
* ("door.<doorId>.locked" / ".isOpen"), so they work for doors in any
|
||||
* scene, loaded or not. Combine with ecs.subscribe_event("door_locked_*")
|
||||
* / ecs.send_event("door_unlock_<doorId>") for scripted lock behaviour.
|
||||
*
|
||||
* @param L The Lua state (the "ecs" table must already exist).
|
||||
*/
|
||||
void registerLuaDoorApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_DOOR_API_HPP
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "LuaGlobalStateApi.hpp"
|
||||
#include "../systems/GlobalStateStore.hpp"
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
static int luaGlobalSet(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
GlobalStateStore &store = GlobalStateStore::getInstance();
|
||||
switch (lua_type(L, 2)) {
|
||||
case LUA_TBOOLEAN:
|
||||
store.set(name, lua_toboolean(L, 2) != 0);
|
||||
break;
|
||||
case LUA_TNUMBER:
|
||||
if (lua_isinteger(L, 2))
|
||||
store.set(name, (int64_t)lua_tointeger(L, 2));
|
||||
else
|
||||
store.set(name, (double)lua_tonumber(L, 2));
|
||||
break;
|
||||
case LUA_TSTRING:
|
||||
store.set(name, std::string(lua_tostring(L, 2)));
|
||||
break;
|
||||
case LUA_TNIL:
|
||||
store.remove(name);
|
||||
break;
|
||||
default:
|
||||
return luaL_error(
|
||||
L, "ecs.global.set: value must be boolean, number, "
|
||||
"string or nil");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int luaGlobalGetBool(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
bool fallback = lua_toboolean(L, 2) != 0;
|
||||
lua_pushboolean(L,
|
||||
GlobalStateStore::getInstance().getBool(name, fallback) ?
|
||||
1 :
|
||||
0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGlobalGetInt(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
int64_t fallback = (int64_t)luaL_optinteger(L, 2, 0);
|
||||
lua_pushinteger(L, (lua_Integer)GlobalStateStore::getInstance().getInt(
|
||||
name, fallback));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGlobalGetFloat(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
double fallback = luaL_optnumber(L, 2, 0.0);
|
||||
lua_pushnumber(L, GlobalStateStore::getInstance().getFloat(name,
|
||||
fallback));
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGlobalGetString(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
const char *fallback = luaL_optstring(L, 2, "");
|
||||
lua_pushstring(L, GlobalStateStore::getInstance()
|
||||
.getString(name, fallback)
|
||||
.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGlobalHas(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
lua_pushboolean(L, GlobalStateStore::getInstance().has(name) ? 1 : 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int luaGlobalRemove(lua_State *L)
|
||||
{
|
||||
const char *name = luaL_checkstring(L, 1);
|
||||
GlobalStateStore::getInstance().remove(name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void registerLuaGlobalStateApi(lua_State *L)
|
||||
{
|
||||
lua_getglobal(L, "ecs");
|
||||
if (!lua_istable(L, -1)) {
|
||||
lua_pop(L, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
lua_newtable(L);
|
||||
lua_pushcfunction(L, luaGlobalSet);
|
||||
lua_setfield(L, -2, "set");
|
||||
lua_pushcfunction(L, luaGlobalGetBool);
|
||||
lua_setfield(L, -2, "get_bool");
|
||||
lua_pushcfunction(L, luaGlobalGetInt);
|
||||
lua_setfield(L, -2, "get_int");
|
||||
lua_pushcfunction(L, luaGlobalGetFloat);
|
||||
lua_setfield(L, -2, "get_float");
|
||||
lua_pushcfunction(L, luaGlobalGetString);
|
||||
lua_setfield(L, -2, "get_string");
|
||||
lua_pushcfunction(L, luaGlobalHas);
|
||||
lua_setfield(L, -2, "has");
|
||||
lua_pushcfunction(L, luaGlobalRemove);
|
||||
lua_setfield(L, -2, "remove");
|
||||
lua_setfield(L, -2, "global");
|
||||
|
||||
lua_pop(L, 1);
|
||||
}
|
||||
|
||||
} // namespace editScene
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef EDITSCENE_LUA_GLOBALSTATE_API_HPP
|
||||
#define EDITSCENE_LUA_GLOBALSTATE_API_HPP
|
||||
#pragma once
|
||||
|
||||
#include <lua.hpp>
|
||||
|
||||
namespace editScene
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Register the global persistent storage Lua API (F9).
|
||||
*
|
||||
* Adds the "ecs.global" table:
|
||||
* ecs.global.set(name, value) -- boolean/integer/float/string;
|
||||
* nil removes the variable
|
||||
* ecs.global.get_bool(name [, default]) -- typed reads
|
||||
* ecs.global.get_int(name [, default])
|
||||
* ecs.global.get_float(name [, default])
|
||||
* ecs.global.get_string(name [, default])
|
||||
* ecs.global.has(name)
|
||||
* ecs.global.remove(name)
|
||||
*
|
||||
* The backing store is the GlobalStateStore singleton, so variables are
|
||||
* shared with C++ and persisted in the save file's "globalState" section.
|
||||
*
|
||||
* @param L The Lua state (the "ecs" table must already exist).
|
||||
*/
|
||||
void registerLuaGlobalStateApi(lua_State *L);
|
||||
|
||||
} // namespace editScene
|
||||
|
||||
#endif // EDITSCENE_LUA_GLOBALSTATE_API_HPP
|
||||
@@ -1,6 +1,9 @@
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include "EditorApp.hpp"
|
||||
#include "ProjectConfig.hpp"
|
||||
#include "systems/SceneSerializer.hpp"
|
||||
#include "systems/CharacterRegistry.hpp"
|
||||
#include "systems/TerrainTests.hpp"
|
||||
#include "OgreRoot.h"
|
||||
|
||||
@@ -53,19 +56,28 @@ struct TerrainTestFrameListener : public Ogre::FrameListener {
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
try {
|
||||
EditorApp app;
|
||||
|
||||
// Parse command line arguments
|
||||
// Parse command line arguments (project dir first: it
|
||||
// changes the working directory and the app name before
|
||||
// EditorApp is constructed)
|
||||
bool gameMode = false;
|
||||
bool editorMode = false;
|
||||
bool debugBuoyancy = false;
|
||||
bool exitAfterFirstFrame = false;
|
||||
bool headless = false;
|
||||
int terrainTestIterations = 0;
|
||||
Ogre::String sceneFile;
|
||||
Ogre::String projectDir;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
Ogre::String arg = argv[i];
|
||||
if (arg == "--game") {
|
||||
gameMode = true;
|
||||
} else if (arg == "--editor") {
|
||||
editorMode = true;
|
||||
} else if (arg == "--project") {
|
||||
if (i + 1 < argc)
|
||||
projectDir = argv[++i];
|
||||
} else if (arg.find("--project=") == 0) {
|
||||
projectDir = arg.substr(10);
|
||||
} else if (arg == "--debug-buoyancy") {
|
||||
debugBuoyancy = true;
|
||||
} else if (arg == "--exit-after-first-frame") {
|
||||
@@ -83,7 +95,38 @@ int main(int argc, char *argv[])
|
||||
}
|
||||
}
|
||||
|
||||
if (gameMode) {
|
||||
/* Project directory (F8): enter the project root before any
|
||||
* OGRE initialization so every CWD-relative path
|
||||
* (resources.cfg, config JSONs, scenes, prefabs,
|
||||
* heightmaps/) resolves against it. */
|
||||
ProjectConfig projectConfig;
|
||||
if (!projectDir.empty()) {
|
||||
std::error_code ec;
|
||||
std::filesystem::path abs = std::filesystem::absolute(
|
||||
std::filesystem::path(projectDir), ec);
|
||||
if (ec || !std::filesystem::is_directory(abs)) {
|
||||
std::cerr << "--project: not a directory: "
|
||||
<< projectDir << std::endl;
|
||||
return 1;
|
||||
}
|
||||
std::filesystem::current_path(abs, ec);
|
||||
if (ec) {
|
||||
std::cerr << "--project: cannot enter directory: "
|
||||
<< projectDir << std::endl;
|
||||
return 1;
|
||||
}
|
||||
projectConfig = loadProjectConfig(abs.string());
|
||||
std::cout << "Project: " << projectConfig.appName
|
||||
<< " (" << abs.string() << ")" << std::endl;
|
||||
}
|
||||
|
||||
EditorApp app(projectConfig.appName.empty() ?
|
||||
Ogre::String("EditSceneEditor") :
|
||||
projectConfig.appName);
|
||||
app.setProjectConfig(projectConfig);
|
||||
|
||||
if (gameMode ||
|
||||
(projectConfig.gameMode && !editorMode)) {
|
||||
app.setGameMode(EditorApp::GameMode::Game);
|
||||
}
|
||||
|
||||
@@ -98,6 +141,29 @@ int main(int argc, char *argv[])
|
||||
|
||||
app.initApp();
|
||||
|
||||
if (headless && !CharacterRegistry::getSingletonPtr()) {
|
||||
/* Headless mode never creates EditorUISystem, which
|
||||
* owns the CharacterRegistry singleton; a scene with a
|
||||
* character spawner asserts without it (same workaround
|
||||
* as the demo mains). */
|
||||
static CharacterRegistry s_characterRegistry;
|
||||
s_characterRegistry.setWorld(app.getWorld());
|
||||
s_characterRegistry.setSceneManager(app.getSceneManager());
|
||||
s_characterRegistry.initialize();
|
||||
}
|
||||
|
||||
/* A game project with a start scene goes straight into it
|
||||
* (no startup menu), like a release binary. Done after
|
||||
* initApp() so main() can create programmatic resources
|
||||
* first if it needs to. */
|
||||
bool gameSceneStarted = false;
|
||||
if (app.getGameMode() == EditorApp::GameMode::Game &&
|
||||
projectConfig.gameMode && !projectConfig.startScene.empty() &&
|
||||
sceneFile.empty()) {
|
||||
app.startNewGame(projectConfig.startScene);
|
||||
gameSceneStarted = true;
|
||||
}
|
||||
|
||||
// Use frame-driven terrain test if requested.
|
||||
// Terrain init requires an active render loop (GPU).
|
||||
TerrainTestFrameListener terrainTestListener(
|
||||
@@ -125,6 +191,12 @@ int main(int argc, char *argv[])
|
||||
if (exitAfterFirstFrame)
|
||||
app.getRoot()->addFrameListener(&exitListener);
|
||||
app.getRoot()->startRendering();
|
||||
|
||||
/* Destroy scene entities while the systems are still alive
|
||||
* (same teardown-ordering reason as the demo mains: spawner
|
||||
* OnRemove observers dereference their systems). */
|
||||
if (gameSceneStarted)
|
||||
app.clearScene();
|
||||
app.closeApp();
|
||||
|
||||
/* Propagate terrain test result as process exit code so CI can
|
||||
|
||||
@@ -116,6 +116,12 @@ struct MeshProcess : public dtTileCacheMeshProcess {
|
||||
if (polyAreas[i] == DT_TILECACHE_WALKABLE_AREA) {
|
||||
polyAreas[i] = SAMPLE_POLYAREA_GROUND;
|
||||
polyFlags[i] = SAMPLE_POLYFLAGS_WALK;
|
||||
} else if (polyAreas[i] ==
|
||||
TileCacheNavMesh::EDITSCENE_AREA_DOOR) {
|
||||
/* F7: keep the door area id (the query
|
||||
* filter assigns it a higher traversal
|
||||
* cost) but leave the poly walkable. */
|
||||
polyFlags[i] = SAMPLE_POLYFLAGS_WALK;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -276,6 +282,10 @@ TileCacheNavMesh::TileCacheNavMesh(Ogre::SceneManager *sceneMgr,
|
||||
m_filter->setIncludeFlags(0xFFFF);
|
||||
m_filter->setExcludeFlags(0);
|
||||
m_filter->setAreaCost(SAMPLE_POLYAREA_GROUND, 1.0f);
|
||||
m_filter->setAreaCost(EDITSCENE_AREA_DOOR,
|
||||
m_params.doorAreaCost > 1.0f ?
|
||||
m_params.doorAreaCost :
|
||||
1.0f);
|
||||
|
||||
m_extents[0] = 32.0f;
|
||||
m_extents[1] = 32.0f;
|
||||
@@ -641,6 +651,21 @@ int TileCacheNavMesh::rasterizeTileLayers(int tx, int ty, TileCacheData *tiles,
|
||||
if (!rcErodeWalkableArea(m_ctx, tcfg.walkableRadius, *rc.chf))
|
||||
return 0;
|
||||
|
||||
/* F7: paint doorway volumes with the door area id (after
|
||||
* erosion so the doorway stays passable even though walls pinch
|
||||
* it; the query filter's area cost, not erosion, deters
|
||||
* pathing through doors). */
|
||||
for (const Ogre::AxisAlignedBox &vol : m_doorVolumes) {
|
||||
if (vol.isNull() || !vol.isFinite())
|
||||
continue;
|
||||
float vbmin[3] = { vol.getMinimum().x, vol.getMinimum().y,
|
||||
vol.getMinimum().z };
|
||||
float vbmax[3] = { vol.getMaximum().x, vol.getMaximum().y,
|
||||
vol.getMaximum().z };
|
||||
rcMarkBoxArea(m_ctx, vbmin, vbmax, EDITSCENE_AREA_DOOR,
|
||||
*rc.chf);
|
||||
}
|
||||
|
||||
if (!rcBuildDistanceField(m_ctx, *rc.chf))
|
||||
return 0;
|
||||
|
||||
@@ -779,6 +804,48 @@ void TileCacheNavMesh::getTileCoords(const float *pos, int &tx, int &ty)
|
||||
ty = m_th - 1;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Door volumes and dynamic obstacles (F7)
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
void TileCacheNavMesh::setDoorVolumes(
|
||||
const std::vector<Ogre::AxisAlignedBox> &volumes)
|
||||
{
|
||||
m_doorVolumes = volumes;
|
||||
}
|
||||
|
||||
uint32_t TileCacheNavMesh::addObstacle(const Ogre::AxisAlignedBox &box)
|
||||
{
|
||||
if (!m_tileCache || box.isNull() || !box.isFinite())
|
||||
return 0;
|
||||
float bmin[3] = { box.getMinimum().x, box.getMinimum().y,
|
||||
box.getMinimum().z };
|
||||
float bmax[3] = { box.getMaximum().x, box.getMaximum().y,
|
||||
box.getMaximum().z };
|
||||
dtObstacleRef ref = 0;
|
||||
dtStatus status = m_tileCache->addBoxObstacle(bmin, bmax, &ref);
|
||||
if (dtStatusFailed(status) || !ref)
|
||||
return 0;
|
||||
return static_cast<uint32_t>(ref);
|
||||
}
|
||||
|
||||
void TileCacheNavMesh::removeObstacle(uint32_t ref)
|
||||
{
|
||||
if (!m_tileCache || !ref)
|
||||
return;
|
||||
m_tileCache->removeObstacle(static_cast<dtObstacleRef>(ref));
|
||||
}
|
||||
|
||||
void TileCacheNavMesh::update()
|
||||
{
|
||||
if (!m_tileCache || !m_navMesh)
|
||||
return;
|
||||
/* Processes queued obstacle add/remove requests (and the tile
|
||||
* rebuilds they trigger). Must be pumped regularly while
|
||||
* obstacles are in use. */
|
||||
m_tileCache->update(0, m_navMesh);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Queries
|
||||
// ------------------------------------------------------------------
|
||||
@@ -876,6 +943,29 @@ Ogre::Vector3 TileCacheNavMesh::getRandomPoint()
|
||||
return Ogre::Vector3(pt[0], pt[1], pt[2]);
|
||||
}
|
||||
|
||||
unsigned char TileCacheNavMesh::getAreaAt(const Ogre::Vector3 &pos)
|
||||
{
|
||||
if (!m_navQuery || !m_navMesh)
|
||||
return 0xFF;
|
||||
|
||||
float p[3] = { pos.x, pos.y, pos.z };
|
||||
dtPolyRef ref = 0;
|
||||
float nearest[3];
|
||||
dtStatus status = m_navQuery->findNearestPoly(p, m_extents, m_filter,
|
||||
&ref, nearest);
|
||||
if (dtStatusFailed(status) || !ref)
|
||||
return 0xFF;
|
||||
|
||||
const dtMeshTile *tile = nullptr;
|
||||
const dtPoly *poly = nullptr;
|
||||
if (dtStatusFailed(
|
||||
m_navMesh->getTileAndPolyByRef(ref, &tile, &poly)) ||
|
||||
!poly)
|
||||
return 0xFF;
|
||||
|
||||
return poly->getArea();
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Debug draw
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
@@ -26,6 +26,12 @@ class PartitionedMesh;
|
||||
*/
|
||||
class TileCacheNavMesh {
|
||||
public:
|
||||
/* Area id painted on doorway volumes during tile rasterization
|
||||
* (F7). Kept below DT_TILECACHE_WALKABLE_AREA (63); the mesh
|
||||
* process keeps walk flags on these polys so paths remain possible
|
||||
* but the query filter's area cost makes detours preferable. */
|
||||
static constexpr unsigned char EDITSCENE_AREA_DOOR = 1;
|
||||
|
||||
struct BuildParams {
|
||||
float cellSize = 0.3f;
|
||||
float cellHeight = 0.2f;
|
||||
@@ -41,6 +47,10 @@ public:
|
||||
float detailSampleDist = 6.0f;
|
||||
float detailSampleMaxError = 1.0f;
|
||||
int tileSize = 48; // voxels per tile
|
||||
// F7: traversal cost multiplier for doorway polys
|
||||
// (EDITSCENE_AREA_DOOR); > 1 makes paths prefer doorless
|
||||
// routes but still allows doorways.
|
||||
float doorAreaCost = 5.0f;
|
||||
};
|
||||
|
||||
TileCacheNavMesh(Ogre::SceneManager *sceneMgr,
|
||||
@@ -57,6 +67,22 @@ public:
|
||||
// --- Partial rebuild ---
|
||||
void rebuildTilesInArea(const Ogre::AxisAlignedBox &area);
|
||||
|
||||
// --- Door volumes (F7) ---
|
||||
// Doorway volumes (closed-pose bounds of every door) are painted
|
||||
// with EDITSCENE_AREA_DOOR during tile rasterization. Call before
|
||||
// build()/rebuildTilesInArea() to (re)apply; tiles are only
|
||||
// re-marked when (re)built, so call sites that move doors must
|
||||
// dirty the affected tiles.
|
||||
void setDoorVolumes(const std::vector<Ogre::AxisAlignedBox> &volumes);
|
||||
|
||||
// --- Dynamic obstacles (F7: locked doors) ---
|
||||
// DetourTileCache queues obstacle requests; update() must be
|
||||
// called regularly (per frame) to process them. Returns 0 on
|
||||
// failure.
|
||||
uint32_t addObstacle(const Ogre::AxisAlignedBox &box);
|
||||
void removeObstacle(uint32_t ref);
|
||||
void update();
|
||||
|
||||
// --- Queries ---
|
||||
bool findPath(const Ogre::Vector3 &start,
|
||||
const Ogre::Vector3 &end,
|
||||
@@ -65,6 +91,10 @@ public:
|
||||
Ogre::Vector3 &out);
|
||||
Ogre::Vector3 getRandomPoint();
|
||||
|
||||
/* Area id of the nearest poly to pos (EDITSCENE_AREA_DOOR,
|
||||
* ground 0, ...); 0xFF when nothing found. Debug/testing aid. */
|
||||
unsigned char getAreaAt(const Ogre::Vector3 &pos);
|
||||
|
||||
// --- Debug ---
|
||||
void drawNavMesh();
|
||||
void clearDebugDraw();
|
||||
@@ -80,6 +110,10 @@ private:
|
||||
float m_bmax[3];
|
||||
std::unique_ptr<PartitionedMesh> m_partitionedMesh;
|
||||
|
||||
// F7: doorway volumes painted with EDITSCENE_AREA_DOOR during
|
||||
// tile rasterization (world/render space, same as input geometry)
|
||||
std::vector<Ogre::AxisAlignedBox> m_doorVolumes;
|
||||
|
||||
// Tile cache
|
||||
dtTileCacheAlloc *m_talloc;
|
||||
dtTileCacheCompressor *m_tcomp;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "ActuatorSystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "BehaviorTreeSystem.hpp"
|
||||
#include "DoorSystem.hpp"
|
||||
#include "EventBus.hpp"
|
||||
#include "ItemSystem.hpp"
|
||||
#include "ItemRegistry.hpp"
|
||||
#include "ItemStateRegistry.hpp"
|
||||
@@ -195,14 +197,27 @@ void ActuatorSystem::drawActionMenu(flecs::entity actuatorEntity)
|
||||
ImGui::Text("Select an action:");
|
||||
ImGui::Separator();
|
||||
|
||||
for (const auto &name : actuator.actionNames) {
|
||||
if (name.empty())
|
||||
continue;
|
||||
if (ImGui::Button(
|
||||
name.c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x,
|
||||
0))) {
|
||||
m_pendingActionName = name;
|
||||
/* Door menus use the pseudo-actions captured when the menu
|
||||
* was opened (F6); other actuators list their actions. */
|
||||
if (actuatorEntity.has<DoorComponent>()) {
|
||||
for (const auto &name : m_doorMenuActions) {
|
||||
if (ImGui::Button(
|
||||
name.c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x,
|
||||
0))) {
|
||||
m_pendingActionName = name;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const auto &name : actuator.actionNames) {
|
||||
if (name.empty())
|
||||
continue;
|
||||
if (ImGui::Button(
|
||||
name.c_str(),
|
||||
ImVec2(ImGui::GetContentRegionAvail().x,
|
||||
0))) {
|
||||
m_pendingActionName = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,12 +453,16 @@ void ActuatorSystem::update(float deltaTime)
|
||||
// Doors toggle open/close on E
|
||||
const auto &door =
|
||||
targetEntity.get<DoorComponent>();
|
||||
// Scene switching doors never toggle
|
||||
m_labelText =
|
||||
(door.isOpen &&
|
||||
door.sceneSwitchPath.empty()) ?
|
||||
"E Close" :
|
||||
"E Open";
|
||||
if (DoorSystem::isDoorLocked(door)) {
|
||||
m_labelText = "E Locked";
|
||||
} else {
|
||||
// Scene switching doors never toggle
|
||||
m_labelText =
|
||||
(door.isOpen &&
|
||||
door.sceneSwitchPath.empty()) ?
|
||||
"E Close" :
|
||||
"E Open";
|
||||
}
|
||||
} else {
|
||||
auto &actuator =
|
||||
targetEntity.get<ActuatorComponent>();
|
||||
@@ -476,8 +495,23 @@ void ActuatorSystem::update(float deltaTime)
|
||||
// Menu rendering happens in render()
|
||||
|
||||
if (!m_pendingActionName.empty()) {
|
||||
executeAction(playerCharacter, menuActuator,
|
||||
m_pendingActionName);
|
||||
if (menuActuator.is_alive() &&
|
||||
menuActuator.has<DoorComponent>()) {
|
||||
/* Door menu pseudo-actions (F6) */
|
||||
auto &door =
|
||||
menuActuator.get_mut<DoorComponent>();
|
||||
if (m_pendingActionName == "Lock") {
|
||||
if (door.isOpen)
|
||||
door.toggleRequested = true;
|
||||
DoorSystem::setDoorLocked(door.doorId,
|
||||
true);
|
||||
} else if (m_pendingActionName == "Close") {
|
||||
door.toggleRequested = true;
|
||||
}
|
||||
} else {
|
||||
executeAction(playerCharacter, menuActuator,
|
||||
m_pendingActionName);
|
||||
}
|
||||
m_pendingActionName.clear();
|
||||
m_menuOpen = false;
|
||||
m_menuActuatorId = 0;
|
||||
@@ -511,16 +545,59 @@ void ActuatorSystem::update(float deltaTime)
|
||||
// Doors toggle open/close on E and optionally
|
||||
// run the configured action; scene switching
|
||||
// doors queue a scene switch instead
|
||||
auto &door =
|
||||
targetEntity.get_mut<DoorComponent>();
|
||||
if (input.ePressed) {
|
||||
auto &door =
|
||||
targetEntity.get_mut<DoorComponent>();
|
||||
if (!door.sceneSwitchPath.empty()) {
|
||||
SceneSwitchOptions opts;
|
||||
opts.targetEntityName =
|
||||
door.sceneSwitchTarget;
|
||||
m_editorApp->switchScene(
|
||||
door.sceneSwitchPath,
|
||||
opts);
|
||||
/* 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
|
||||
@@ -535,6 +612,36 @@ void ActuatorSystem::update(float deltaTime)
|
||||
}
|
||||
}
|
||||
m_eHoldTime = 0.0f;
|
||||
} else if (door.lockable && !door.doorId.empty() &&
|
||||
!DoorSystem::isDoorLocked(door) &&
|
||||
m_eHoldTime > 0.3f && !m_menuOpen) {
|
||||
/* Hold E on a lockable door: door
|
||||
* menu with Close / Lock (F6). */
|
||||
m_doorMenuActions.clear();
|
||||
if (door.isOpen &&
|
||||
door.sceneSwitchPath.empty())
|
||||
m_doorMenuActions.push_back(
|
||||
"Close");
|
||||
bool hasKey =
|
||||
door.keyItemId.empty() ||
|
||||
(playerCharacter.is_alive() &&
|
||||
playerCharacter.has<
|
||||
InventoryComponent>() &&
|
||||
playerCharacter
|
||||
.get<InventoryComponent>()
|
||||
.hasItem(door.keyItemId));
|
||||
if (hasKey)
|
||||
m_doorMenuActions.push_back(
|
||||
"Lock");
|
||||
if (!m_doorMenuActions.empty()) {
|
||||
m_menuOpen = true;
|
||||
m_menuActuatorId =
|
||||
targetEntity.id();
|
||||
m_eWasHeld = true;
|
||||
if (m_editorApp)
|
||||
m_editorApp->setWindowGrab(
|
||||
false);
|
||||
}
|
||||
}
|
||||
} else if (actuator.actionNames.size() == 1 &&
|
||||
!actuator.actionNames[0].empty()) {
|
||||
|
||||
@@ -91,6 +91,10 @@ private:
|
||||
bool m_eWasHeld = false;
|
||||
Ogre::String m_pendingActionName;
|
||||
|
||||
// Door menu (F6): pseudo-actions ("Close"/"Lock") shown when E is
|
||||
// held on a lockable door; filled when the menu opens
|
||||
std::vector<std::string> m_doorMenuActions;
|
||||
|
||||
// Currently executing action state
|
||||
flecs::entity_t m_executingActuatorId = 0;
|
||||
flecs::entity_t m_executingCharacterId = 0;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "CellGridSystem.hpp"
|
||||
#include "DoorBuilder.hpp"
|
||||
#include "DoorSystem.hpp"
|
||||
#include "FurnitureLibrary.hpp"
|
||||
#include "PhysicsSystem.hpp"
|
||||
#include "../components/CellGrid.hpp"
|
||||
@@ -390,13 +392,39 @@ void CellGridSystem::buildCellGrid(flecs::entity entity,
|
||||
Procedural::TriangleBuffer floorTb, ceilingTb, extWallTb, intWallTb,
|
||||
extWindowsTb, intWindowsTb, extDoorsTb, intDoorsTb, roofTopTb, roofSideTb;
|
||||
|
||||
buildFloorsAndCeilings(grid, floorTb, ceilingTb);
|
||||
/* F4: interiorOnly skips the exterior shell (external walls, external
|
||||
* window panels + frames, roofs, external corners); exit doorway wall
|
||||
* panels (extDoorsTb), exit door frames and all door entities stay, so
|
||||
* a paired exterior scene can take over through scene-switch doors.
|
||||
* F5: exteriorOnly generates only that shell (plus opaque glass panes
|
||||
* in the external window openings): no floors/ceilings, internal
|
||||
* walls/frames, internal door panels/entities or furniture.
|
||||
* Both limited modes get opaque glass panes: exteriorOnly in every
|
||||
* external window, interiorOnly in boundary windows (see
|
||||
* buildGlass), hiding the missing half of the building. */
|
||||
const bool shell = !grid.interiorOnlyMode();
|
||||
const bool exteriorOnly = grid.exteriorOnlyMode();
|
||||
|
||||
Procedural::TriangleBuffer glassTb;
|
||||
|
||||
if (!exteriorOnly) {
|
||||
buildFloorsAndCeilings(grid, floorTb, ceilingTb);
|
||||
}
|
||||
buildWalls(grid, extWallTb, intWallTb, intWindowsTb);
|
||||
buildCorners(grid, extWallTb);
|
||||
buildInternalCorners(grid, intWallTb);
|
||||
if (shell) {
|
||||
buildCorners(grid, extWallTb);
|
||||
}
|
||||
if (!exteriorOnly) {
|
||||
buildInternalCorners(grid, intWallTb);
|
||||
}
|
||||
buildDoors(grid, extDoorsTb, intDoorsTb);
|
||||
buildWindows(grid, extWindowsTb, intWindowsTb);
|
||||
buildRoofs(entity, grid, roofTopTb, roofSideTb);
|
||||
if (shell) {
|
||||
buildRoofs(entity, grid, roofTopTb, roofSideTb);
|
||||
}
|
||||
if (exteriorOnly || grid.interiorOnlyMode()) {
|
||||
buildGlass(grid, glassTb);
|
||||
}
|
||||
|
||||
// Apply UV mapping for each part (use rect if specified, otherwise default)
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
@@ -504,6 +532,17 @@ void CellGridSystem::buildCellGrid(flecs::entity entity,
|
||||
meshData.entities.push_back(entity3d);
|
||||
}
|
||||
|
||||
// F5: opaque glass panes in the window openings (exteriorOnly:
|
||||
// external windows; interiorOnly: boundary windows)
|
||||
if (glassTb.getVertices().size() >= 3) {
|
||||
meshData.glassMesh = baseName + "_glass";
|
||||
auto mesh = convertToMesh(meshData.glassMesh, glassTb,
|
||||
getGlassMaterialName(entity, grid));
|
||||
auto entity3d = m_sceneMgr->createEntity(meshData.glassMesh);
|
||||
transform.node->attachObject(entity3d);
|
||||
meshData.entities.push_back(entity3d);
|
||||
}
|
||||
|
||||
// External doors
|
||||
if (extDoorsTb.getVertices().size() >= 3) {
|
||||
meshData.doorMeshes.push_back(baseName + "_extDoors");
|
||||
@@ -562,7 +601,8 @@ void CellGridSystem::buildCellGrid(flecs::entity entity,
|
||||
" extDoors=" + std::to_string(extDoorsTb.getVertices().size()) +
|
||||
" intDoors=" + std::to_string(intDoorsTb.getVertices().size()) +
|
||||
" roofTop=" + std::to_string(roofTopTb.getVertices().size()) +
|
||||
" roofSide=" + std::to_string(roofSideTb.getVertices().size()));
|
||||
" roofSide=" + std::to_string(roofSideTb.getVertices().size()) +
|
||||
" glass=" + std::to_string(glassTb.getVertices().size()));
|
||||
|
||||
// Build window and door frames (creates StaticGeometry region)
|
||||
try {
|
||||
@@ -577,15 +617,18 @@ void CellGridSystem::buildCellGrid(flecs::entity entity,
|
||||
}
|
||||
|
||||
// Build furniture (non-isolated items go into the same StaticGeometry)
|
||||
try {
|
||||
buildFurniture(entity, grid);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CellGrid: Error building furniture: " +
|
||||
std::string(e.what()));
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CellGrid: Unknown error building furniture");
|
||||
// (F5: exteriorOnly has no interior, hence no furniture)
|
||||
if (!exteriorOnly) {
|
||||
try {
|
||||
buildFurniture(entity, grid);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CellGrid: Error building furniture: " +
|
||||
std::string(e.what()));
|
||||
} catch (...) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CellGrid: Unknown error building furniture");
|
||||
}
|
||||
}
|
||||
|
||||
// Build door leaf entities (one per unique doorway)
|
||||
@@ -775,11 +818,20 @@ void CellGridSystem::buildWalls(const CellGridComponent &grid,
|
||||
&intWallTb },
|
||||
};
|
||||
|
||||
const bool shell = !grid.interiorOnlyMode();
|
||||
const bool interior = !grid.exteriorOnlyMode();
|
||||
|
||||
for (const auto &cell : grid.cells) {
|
||||
Ogre::Vector3 origin = grid.cellToWorld(cell.x, cell.y, cell.z);
|
||||
uint64_t flags = cell.flags;
|
||||
|
||||
for (const auto &bit : bits_solid) {
|
||||
/* F4: interiorOnly generates no external wall planes.
|
||||
* F5: exteriorOnly generates no internal wall planes. */
|
||||
if (!shell && bit.tb == &extWallTb)
|
||||
continue;
|
||||
if (!interior && bit.tb != &extWallTb)
|
||||
continue;
|
||||
if ((flags & bit.bit) == bit.bit) {
|
||||
genPlane(bit.sizeX, bit.sizeY, bit.normal,
|
||||
bit.offset, *bit.tb, origin);
|
||||
@@ -1254,10 +1306,12 @@ void CellGridSystem::buildDoors(const CellGridComponent &grid,
|
||||
}
|
||||
|
||||
// Internal doors - append directly to intDoorsTb
|
||||
if (cell.hasFlag(CellFlags::IntDoorXNeg) ||
|
||||
cell.hasFlag(CellFlags::IntDoorXPos) ||
|
||||
cell.hasFlag(CellFlags::IntDoorZNeg) ||
|
||||
cell.hasFlag(CellFlags::IntDoorZPos)) {
|
||||
// (F5: skipped entirely in exteriorOnly mode)
|
||||
if (!grid.exteriorOnlyMode() &&
|
||||
(cell.hasFlag(CellFlags::IntDoorXNeg) ||
|
||||
cell.hasFlag(CellFlags::IntDoorXPos) ||
|
||||
cell.hasFlag(CellFlags::IntDoorZNeg) ||
|
||||
cell.hasFlag(CellFlags::IntDoorZPos))) {
|
||||
float sideWidth =
|
||||
(2.0f * hScale - 0.2f - doorWidth) / 2.0f;
|
||||
float moffset =
|
||||
@@ -1398,10 +1452,12 @@ void CellGridSystem::buildWindows(const CellGridComponent &grid,
|
||||
Ogre::Vector3 origin = grid.cellToWorld(cell.x, cell.y, cell.z);
|
||||
|
||||
// External windows - append directly to extWindowsTb
|
||||
if (cell.hasFlag(CellFlags::WindowXNeg) ||
|
||||
cell.hasFlag(CellFlags::WindowXPos) ||
|
||||
cell.hasFlag(CellFlags::WindowZNeg) ||
|
||||
cell.hasFlag(CellFlags::WindowZPos)) {
|
||||
// (F4: skipped entirely in interiorOnly mode)
|
||||
if (!grid.interiorOnlyMode() &&
|
||||
(cell.hasFlag(CellFlags::WindowXNeg) ||
|
||||
cell.hasFlag(CellFlags::WindowXPos) ||
|
||||
cell.hasFlag(CellFlags::WindowZNeg) ||
|
||||
cell.hasFlag(CellFlags::WindowZPos))) {
|
||||
// External cell width = 2.0 * hScale (full cell size)
|
||||
float externalCellWidth = 2.0f * hScale;
|
||||
float sideWidth =
|
||||
@@ -1700,10 +1756,12 @@ void CellGridSystem::buildWindows(const CellGridComponent &grid,
|
||||
}
|
||||
|
||||
// Internal windows - append directly to intWindowsTb
|
||||
if (cell.hasFlag(CellFlags::IntWindowXNeg) ||
|
||||
cell.hasFlag(CellFlags::IntWindowXPos) ||
|
||||
cell.hasFlag(CellFlags::IntWindowZNeg) ||
|
||||
cell.hasFlag(CellFlags::IntWindowZPos)) {
|
||||
// (F5: skipped entirely in exteriorOnly mode)
|
||||
if (!grid.exteriorOnlyMode() &&
|
||||
(cell.hasFlag(CellFlags::IntWindowXNeg) ||
|
||||
cell.hasFlag(CellFlags::IntWindowXPos) ||
|
||||
cell.hasFlag(CellFlags::IntWindowZNeg) ||
|
||||
cell.hasFlag(CellFlags::IntWindowZPos))) {
|
||||
float intWallOffset = 0.1f;
|
||||
// Internal cell width = 2.0 * hScale - 2 * intWallOffset (1.8f for cellSize=4.0)
|
||||
float internalCellWidth =
|
||||
@@ -2042,6 +2100,124 @@ static void deformRoofSideVertices(Procedural::TriangleBuffer &tb,
|
||||
}
|
||||
}
|
||||
|
||||
std::string CellGridSystem::getGlassMaterialName(
|
||||
flecs::entity entity, const CellGridComponent &grid)
|
||||
{
|
||||
if (!grid.glassMaterialName.empty())
|
||||
return grid.glassMaterialName;
|
||||
|
||||
/* Built-in fake-glass material, one per grid entity so per-grid
|
||||
* glassColor/glassReflectivity apply; updated in place on rebuild. */
|
||||
std::string name = "CellGridGlass_" + std::to_string(entity.id());
|
||||
auto &matMgr = Ogre::MaterialManager::getSingleton();
|
||||
Ogre::MaterialPtr mat = matMgr.getByName(
|
||||
name, Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
if (!mat) {
|
||||
mat = matMgr.create(
|
||||
name,
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
|
||||
}
|
||||
Ogre::Pass *pass = mat->getTechnique(0)->getPass(0);
|
||||
float gloss = Ogre::Math::Clamp(grid.glassReflectivity, 0.0f, 1.0f);
|
||||
/* Opaque on purpose: the glass exists to hide the missing half
|
||||
* of a generation-limited grid (exteriorOnly: the empty interior;
|
||||
* interiorOnly: the void outside), so it must not be
|
||||
* see-through. A custom glassMaterialName can still override. */
|
||||
pass->setDepthCheckEnabled(true);
|
||||
pass->setDepthWriteEnabled(true);
|
||||
pass->setDiffuse(grid.glassColor.r, grid.glassColor.g,
|
||||
grid.glassColor.b, 1.0f);
|
||||
pass->setAmbient(grid.glassColor.r * 0.5f, grid.glassColor.g * 0.5f,
|
||||
grid.glassColor.b * 0.5f);
|
||||
pass->setSpecular(gloss, gloss, gloss, 1.0f);
|
||||
pass->setShininess(16.0f + gloss * 112.0f);
|
||||
mat->compile();
|
||||
return name;
|
||||
}
|
||||
|
||||
void CellGridSystem::buildGlass(const CellGridComponent &grid,
|
||||
Procedural::TriangleBuffer &glassTb)
|
||||
{
|
||||
/* One thin box pane per window opening, matching the dimensions
|
||||
* used by buildWindows(). The pane becomes part of the static
|
||||
* collider set (meshData.glassMesh), so the player cannot climb
|
||||
* through windows.
|
||||
*
|
||||
* exteriorOnly: pane in every external window opening (the pane
|
||||
* hides the empty interior). interiorOnly: pane in every
|
||||
* BOUNDARY window - cells carrying both the internal and the
|
||||
* matching external window flag - placed at the internal wall
|
||||
* plane (there is no external wall in this mode; the pane hides
|
||||
* the void outside). Pure interior windows between rooms get
|
||||
* no glass. */
|
||||
const bool interiorOnly = grid.interiorOnlyMode();
|
||||
const float hScale = grid.cellSize / 2.0f;
|
||||
const float halfCell = 1.0f * hScale;
|
||||
const float cornerWidth = 0.2f;
|
||||
const float intPlane = halfCell - 0.1f;
|
||||
const float windowWidth = 1.6f * hScale;
|
||||
const float windowHeight = 2.0f * (grid.cellHeight / 4.0f);
|
||||
const float windowBottomOffset = 0.8f * (grid.cellHeight / 4.0f);
|
||||
const float windowCenterY = windowBottomOffset + windowHeight / 2.0f;
|
||||
const float thickness = 0.02f;
|
||||
|
||||
for (const auto &cell : grid.cells) {
|
||||
Ogre::Vector3 origin = grid.cellToWorld(cell.x, cell.y, cell.z);
|
||||
|
||||
auto addPane = [&](const Ogre::Vector3 ¢er, bool alongX) {
|
||||
Procedural::BoxGenerator()
|
||||
.setSizeX(alongX ? thickness : windowWidth)
|
||||
.setSizeY(windowHeight)
|
||||
.setSizeZ(alongX ? windowWidth : thickness)
|
||||
.setNumSegX(1)
|
||||
.setNumSegY(1)
|
||||
.setNumSegZ(1)
|
||||
.setPosition(origin + center)
|
||||
.setEnableNormals(true)
|
||||
.addToTriangleBuffer(glassTb);
|
||||
};
|
||||
|
||||
if (interiorOnly) {
|
||||
if (cell.hasFlag(CellFlags::IntWindowXNeg) &&
|
||||
cell.hasFlag(CellFlags::WindowXNeg))
|
||||
addPane(Ogre::Vector3(-intPlane, windowCenterY, 0),
|
||||
true);
|
||||
if (cell.hasFlag(CellFlags::IntWindowXPos) &&
|
||||
cell.hasFlag(CellFlags::WindowXPos))
|
||||
addPane(Ogre::Vector3(intPlane, windowCenterY, 0),
|
||||
true);
|
||||
if (cell.hasFlag(CellFlags::IntWindowZNeg) &&
|
||||
cell.hasFlag(CellFlags::WindowZNeg))
|
||||
addPane(Ogre::Vector3(0, windowCenterY,
|
||||
-intPlane),
|
||||
false);
|
||||
if (cell.hasFlag(CellFlags::IntWindowZPos) &&
|
||||
cell.hasFlag(CellFlags::WindowZPos))
|
||||
addPane(Ogre::Vector3(0, windowCenterY,
|
||||
intPlane),
|
||||
false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cell.hasFlag(CellFlags::WindowXNeg))
|
||||
addPane(Ogre::Vector3(-halfCell - cornerWidth,
|
||||
windowCenterY, 0),
|
||||
true);
|
||||
if (cell.hasFlag(CellFlags::WindowXPos))
|
||||
addPane(Ogre::Vector3(halfCell + cornerWidth,
|
||||
windowCenterY, 0),
|
||||
true);
|
||||
if (cell.hasFlag(CellFlags::WindowZNeg))
|
||||
addPane(Ogre::Vector3(0, windowCenterY,
|
||||
-halfCell - cornerWidth),
|
||||
false);
|
||||
if (cell.hasFlag(CellFlags::WindowZPos))
|
||||
addPane(Ogre::Vector3(0, windowCenterY,
|
||||
halfCell + cornerWidth),
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
void CellGridSystem::buildRoofs(flecs::entity cellGridEntity,
|
||||
const CellGridComponent &grid,
|
||||
Procedural::TriangleBuffer &roofTopTb,
|
||||
@@ -2678,6 +2854,18 @@ void CellGridSystem::destroyCellGridMeshes(flecs::entity entity)
|
||||
removeMesh(it->second.intWindowsMesh);
|
||||
removeMesh(it->second.roofMesh);
|
||||
removeMesh(it->second.roofSideMesh);
|
||||
removeMesh(it->second.glassMesh);
|
||||
it->second.glassMesh.clear();
|
||||
|
||||
// Built-in F5 glass material (per grid entity)
|
||||
auto &matMgr = Ogre::MaterialManager::getSingleton();
|
||||
std::string glassMat =
|
||||
"CellGridGlass_" + std::to_string(entity.id());
|
||||
try {
|
||||
if (matMgr.resourceExists(glassMat))
|
||||
matMgr.remove(glassMat);
|
||||
} catch (...) {
|
||||
}
|
||||
for (const auto &name : it->second.doorMeshes) {
|
||||
removeMesh(name);
|
||||
}
|
||||
@@ -2843,6 +3031,7 @@ void CellGridSystem::buildPhysicsColliders(flecs::entity entity)
|
||||
addMeshCollider(meshData.intWindowsMesh);
|
||||
addMeshCollider(meshData.roofMesh);
|
||||
addMeshCollider(meshData.roofSideMesh);
|
||||
addMeshCollider(meshData.glassMesh);
|
||||
for (const auto &doorMesh : meshData.doorMeshes)
|
||||
addMeshCollider(doorMesh);
|
||||
for (const auto &windowMesh : meshData.windowMeshes)
|
||||
@@ -4108,7 +4297,8 @@ void CellGridSystem::placeWindowFrames(
|
||||
frameEntities.push_back(ent);
|
||||
};
|
||||
|
||||
if (extMesh) {
|
||||
/* F4: no external window frames in interiorOnly mode. */
|
||||
if (extMesh && !grid.interiorOnlyMode()) {
|
||||
frameOffset = 0.1f;
|
||||
if (cell.hasFlag(CellFlags::WindowXNeg)) {
|
||||
Ogre::Vector3 pos =
|
||||
@@ -4148,7 +4338,8 @@ void CellGridSystem::placeWindowFrames(
|
||||
}
|
||||
}
|
||||
|
||||
if (intMesh) {
|
||||
/* F5: no internal window frames in exteriorOnly mode. */
|
||||
if (intMesh && !grid.exteriorOnlyMode()) {
|
||||
frameOffset = 0.1f;
|
||||
if (cell.hasFlag(CellFlags::IntWindowXNeg)) {
|
||||
Ogre::Vector3 pos =
|
||||
@@ -4283,7 +4474,8 @@ void CellGridSystem::placeDoorFrames(flecs::entity entity,
|
||||
}
|
||||
}
|
||||
|
||||
if (intMesh) {
|
||||
/* F5: no internal door frames in exteriorOnly mode. */
|
||||
if (intMesh && !grid.exteriorOnlyMode()) {
|
||||
if (cell.hasFlag(CellFlags::IntDoorXNeg)) {
|
||||
Ogre::Vector3 pos =
|
||||
origin +
|
||||
@@ -4434,7 +4626,8 @@ void CellGridSystem::placeWindowFramesInStaticGeometry(
|
||||
}
|
||||
};
|
||||
|
||||
if (extMesh) {
|
||||
/* F4: no external window frames in interiorOnly mode. */
|
||||
if (extMesh && !grid.interiorOnlyMode()) {
|
||||
frameOffset = 0.1f;
|
||||
if (cell.hasFlag(CellFlags::WindowXNeg)) {
|
||||
Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell - frameOffset, windowY, 0);
|
||||
@@ -4458,7 +4651,8 @@ void CellGridSystem::placeWindowFramesInStaticGeometry(
|
||||
}
|
||||
}
|
||||
|
||||
if (intMesh) {
|
||||
/* F5: no internal window frames in exteriorOnly mode. */
|
||||
if (intMesh && !grid.exteriorOnlyMode()) {
|
||||
frameOffset = 0.1f;
|
||||
if (cell.hasFlag(CellFlags::IntWindowXNeg)) {
|
||||
Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell + frameOffset, windowY, 0);
|
||||
@@ -4556,7 +4750,8 @@ void CellGridSystem::placeDoorFramesInStaticGeometry(
|
||||
}
|
||||
}
|
||||
|
||||
if (intMesh) {
|
||||
/* F5: no internal door frames in exteriorOnly mode. */
|
||||
if (intMesh && !grid.exteriorOnlyMode()) {
|
||||
if (cell.hasFlag(CellFlags::IntDoorXNeg)) {
|
||||
Ogre::Vector3 pos = origin + Ogre::Vector3(-halfCell + frameOffset, intDoorFrameY, 0);
|
||||
Ogre::Quaternion rot(Ogre::Degree(-90), Ogre::Vector3::UNIT_Y);
|
||||
@@ -4629,8 +4824,81 @@ void CellGridSystem::createDoorLeafMesh(const CellGridComponent &grid,
|
||||
generateLodForMesh(leafMesh);
|
||||
}
|
||||
|
||||
std::string CellGridSystem::doorEdgeKey(int cellX, int cellY, int cellZ,
|
||||
int side)
|
||||
{
|
||||
// Canonical key of the shared cell edge: side 2/3 are X-facing
|
||||
// edges (X+ uses the neighbour's coordinate), side 0/1 are
|
||||
// Z-facing. Both cells sharing a door edge produce the same key.
|
||||
if (side == 3)
|
||||
cellX += 1;
|
||||
else if (side == 1)
|
||||
cellZ += 1;
|
||||
char axis = (side == 2 || side == 3) ? 'X' : 'Z';
|
||||
return std::string(1, axis) + ":" + std::to_string(cellX) + ":" +
|
||||
std::to_string(cellY) + ":" + std::to_string(cellZ);
|
||||
}
|
||||
|
||||
std::vector<CellGridSystem::DoorwayInfo>
|
||||
CellGridSystem::collectDoorways(const CellGridComponent &grid)
|
||||
{
|
||||
std::vector<DoorwayInfo> result;
|
||||
std::set<std::string> seen;
|
||||
|
||||
auto tryAdd = [&](const Cell &cell, int side, bool internal) {
|
||||
std::string key = doorEdgeKey(cell.x, cell.y, cell.z, side);
|
||||
if (!seen.insert(key).second)
|
||||
return;
|
||||
DoorwayInfo info;
|
||||
info.edgeKey = key;
|
||||
info.cellX = cell.x;
|
||||
info.cellY = cell.y;
|
||||
info.cellZ = cell.z;
|
||||
info.side = side;
|
||||
info.internal = internal;
|
||||
result.push_back(info);
|
||||
};
|
||||
|
||||
for (const auto &cell : grid.cells) {
|
||||
if (cell.hasFlag(CellFlags::DoorXNeg))
|
||||
tryAdd(cell, 2, false);
|
||||
if (cell.hasFlag(CellFlags::DoorXPos))
|
||||
tryAdd(cell, 3, false);
|
||||
if (cell.hasFlag(CellFlags::DoorZNeg))
|
||||
tryAdd(cell, 0, false);
|
||||
if (cell.hasFlag(CellFlags::DoorZPos))
|
||||
tryAdd(cell, 1, false);
|
||||
if (cell.hasFlag(CellFlags::IntDoorXNeg))
|
||||
tryAdd(cell, 2, true);
|
||||
if (cell.hasFlag(CellFlags::IntDoorXPos))
|
||||
tryAdd(cell, 3, true);
|
||||
if (cell.hasFlag(CellFlags::IntDoorZNeg))
|
||||
tryAdd(cell, 0, true);
|
||||
if (cell.hasFlag(CellFlags::IntDoorZPos))
|
||||
tryAdd(cell, 1, true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
flecs::entity CellGridSystem::findDoorEntity(flecs::entity gridEntity,
|
||||
const std::string &edgeKey)
|
||||
{
|
||||
flecs::entity result = flecs::entity::null();
|
||||
if (!gridEntity.is_valid() || !gridEntity.is_alive())
|
||||
return result;
|
||||
gridEntity.children([&](flecs::entity child) {
|
||||
if (result.is_valid())
|
||||
return;
|
||||
if (!child.has<DoorComponent>())
|
||||
return;
|
||||
if (child.get<DoorComponent>().edgeKey == edgeKey)
|
||||
result = child;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
void CellGridSystem::buildDoorEntities(flecs::entity entity,
|
||||
const CellGridComponent &grid,
|
||||
CellGridComponent &grid,
|
||||
const std::string &materialName,
|
||||
flecs::entity materialEntity)
|
||||
{
|
||||
@@ -4706,27 +4974,25 @@ void CellGridSystem::buildDoorEntities(flecs::entity entity,
|
||||
|
||||
// side: 0 = Z-, 1 = Z+, 2 = X-, 3 = X+
|
||||
auto tryPlaceDoor = [&](const Cell &cell, int side, bool internal) {
|
||||
// Canonical key of the shared cell edge
|
||||
std::string key;
|
||||
if (side == 2)
|
||||
key = "X:" + std::to_string(cell.x) + ":" +
|
||||
std::to_string(cell.y) + ":" +
|
||||
std::to_string(cell.z);
|
||||
else if (side == 3)
|
||||
key = "X:" + std::to_string(cell.x + 1) + ":" +
|
||||
std::to_string(cell.y) + ":" +
|
||||
std::to_string(cell.z);
|
||||
else if (side == 0)
|
||||
key = "Z:" + std::to_string(cell.x) + ":" +
|
||||
std::to_string(cell.y) + ":" +
|
||||
std::to_string(cell.z);
|
||||
else
|
||||
key = "Z:" + std::to_string(cell.x) + ":" +
|
||||
std::to_string(cell.y) + ":" +
|
||||
std::to_string(cell.z + 1);
|
||||
/* F5: exteriorOnly has no interior, hence no internal door
|
||||
* entities - only exit doors are built. */
|
||||
if (internal && grid.exteriorOnlyMode())
|
||||
return;
|
||||
|
||||
// Canonical key of the shared cell edge (F0: also the
|
||||
// door's identity key within this grid)
|
||||
std::string key = doorEdgeKey(cell.x, cell.y, cell.z, side);
|
||||
if (!placedDoors.insert(key).second)
|
||||
return; // doorway already has a door
|
||||
|
||||
// Per-doorway configuration override (F0)
|
||||
const CellGridDoorConfig *doorCfg = nullptr;
|
||||
auto cfgIt = grid.doorConfigs.find(key);
|
||||
if (cfgIt != grid.doorConfigs.end())
|
||||
doorCfg = &cfgIt->second;
|
||||
if (doorCfg && doorCfg->disabled)
|
||||
return; // no door entity for this doorway
|
||||
|
||||
// Same placement as the door frame for this side
|
||||
// (see placeDoorFramesInStaticGeometry)
|
||||
Ogre::Vector3 origin =
|
||||
@@ -4769,126 +5035,53 @@ void CellGridSystem::buildDoorEntities(flecs::entity entity,
|
||||
break;
|
||||
}
|
||||
|
||||
Ogre::Entity *leafEnt = nullptr;
|
||||
try {
|
||||
leafEnt = m_sceneMgr->createEntity(leafMeshName);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"CellGrid: Error creating door entity: " +
|
||||
std::string(e.what()));
|
||||
// Resolved behaviour + geometry for the shared door builder
|
||||
// (F2): grid defaults with the per-doorway F0 override applied.
|
||||
bool hasOverride = doorCfg && doorCfg->hasOverride;
|
||||
|
||||
DoorBuildParams params;
|
||||
params.openAngle = hasOverride ? doorCfg->openAngle :
|
||||
grid.doorOpenAngle;
|
||||
params.openSpeed = hasOverride ? doorCfg->openSpeed :
|
||||
grid.doorOpenSpeed;
|
||||
params.swingReversed = hasOverride ? doorCfg->swingReversed :
|
||||
grid.doorSwingReversed;
|
||||
params.actionName = hasOverride ? doorCfg->actionName :
|
||||
grid.doorActionName;
|
||||
params.sceneSwitchPath = hasOverride ? doorCfg->sceneSwitchPath :
|
||||
grid.doorSceneSwitchPath;
|
||||
params.sceneSwitchTarget = hasOverride ?
|
||||
doorCfg->sceneSwitchTarget :
|
||||
grid.doorSceneSwitchTarget;
|
||||
params.persistent = doorCfg && doorCfg->persistent;
|
||||
params.lockable = doorCfg && doorCfg->lockable;
|
||||
params.lockedByDefault = doorCfg && doorCfg->lockedByDefault;
|
||||
if (doorCfg)
|
||||
params.keyItemId = doorCfg->keyItemId;
|
||||
/* F0 identity: a global persistent ID is assigned to doors
|
||||
* that need one (persistence, locking or scene switching);
|
||||
* ephemeral doors keep an empty ID. */
|
||||
if (params.persistent || params.lockable ||
|
||||
!params.sceneSwitchPath.empty())
|
||||
params.doorId = grid.ensureGridUid() + ":" + key;
|
||||
params.edgeKey = key;
|
||||
params.friction = grid.friction;
|
||||
params.leafMeshName = leafMeshName;
|
||||
params.customMesh = usingCustomMesh;
|
||||
params.useMeshMaterial = useMeshMaterial;
|
||||
params.materialName = materialName;
|
||||
params.customBounds = customBounds;
|
||||
params.leafWidth = leafWidth;
|
||||
params.leafHeight = leafHeight;
|
||||
params.leafThickness = leafThickness;
|
||||
|
||||
flecs::entity doorEntity =
|
||||
DoorBuilder::build(m_world, m_sceneMgr, entity,
|
||||
transform.node, pos, rot, params);
|
||||
if (!doorEntity.is_valid())
|
||||
return;
|
||||
}
|
||||
if (!leafEnt)
|
||||
return;
|
||||
if (!useMeshMaterial && !materialName.empty())
|
||||
leafEnt->setMaterialName(materialName);
|
||||
|
||||
// The hinge node is the rotation pivot (DoorSystem rotates it
|
||||
// around local Y), so it sits on the side edge of the doorway
|
||||
// with the leaf extending +X from it in door-local space, not
|
||||
// in the doorway center. Its height is also used for the
|
||||
// actuator prompt position.
|
||||
Ogre::Vector3 hingePos;
|
||||
Ogre::Vector3 colliderCenter;
|
||||
Ogre::Vector3 colliderHalfExtents;
|
||||
if (usingCustomMesh) {
|
||||
// Custom mesh: hinge at the mesh origin (mesh spans
|
||||
// +X/+Y from it); shift the hinge so the leaf is
|
||||
// centered in the doorway when closed
|
||||
Ogre::Vector3 center = Ogre::Vector3::ZERO;
|
||||
if (!customBounds.isNull() &&
|
||||
customBounds.isFinite()) {
|
||||
center = customBounds.getCenter();
|
||||
colliderHalfExtents =
|
||||
customBounds.getHalfSize();
|
||||
} else {
|
||||
colliderHalfExtents =
|
||||
Ogre::Vector3(0.5f, 1.5f, 0.05f);
|
||||
}
|
||||
hingePos = pos - rot * Ogre::Vector3(center.x, 0, 0);
|
||||
hingePos.y = pos.y;
|
||||
colliderCenter = center;
|
||||
} else {
|
||||
// Procedural leaf: mesh spans [0, leafWidth] x
|
||||
// [0, leafHeight] from the hinge edge; the child node
|
||||
// centers it vertically on the hinge
|
||||
hingePos = pos -
|
||||
rot * Ogre::Vector3(leafWidth / 2.0f, 0, 0);
|
||||
hingePos.y = pos.y + leafHeight / 2.0f;
|
||||
colliderHalfExtents =
|
||||
Ogre::Vector3(leafWidth / 2.0f,
|
||||
leafHeight / 2.0f,
|
||||
leafThickness / 2.0f);
|
||||
colliderCenter =
|
||||
Ogre::Vector3(leafWidth / 2.0f, 0, 0);
|
||||
}
|
||||
|
||||
Ogre::SceneNode *hingeNode =
|
||||
transform.node->createChildSceneNode();
|
||||
hingeNode->setPosition(hingePos);
|
||||
hingeNode->setOrientation(rot);
|
||||
|
||||
if (usingCustomMesh) {
|
||||
hingeNode->attachObject(leafEnt);
|
||||
} else {
|
||||
Ogre::SceneNode *leafNode =
|
||||
hingeNode->createChildSceneNode();
|
||||
leafNode->setPosition(
|
||||
Ogre::Vector3(0, -leafHeight / 2.0f, 0));
|
||||
leafNode->attachObject(leafEnt);
|
||||
}
|
||||
|
||||
// Door entity (runtime - no EditorMarkerComponent, so it is
|
||||
// neither serialized nor editable; it dies with the grid
|
||||
// through the ChildOf cascade)
|
||||
flecs::entity doorEntity = m_world.entity();
|
||||
doorEntity.child_of(entity);
|
||||
doorEntity.set<TransformComponent>(
|
||||
{hingeNode, hingePos, rot, Ogre::Vector3::UNIT_SCALE});
|
||||
|
||||
RenderableComponent renderable;
|
||||
renderable.entity = leafEnt;
|
||||
renderable.meshName = leafMeshName;
|
||||
doorEntity.set<RenderableComponent>(renderable);
|
||||
|
||||
DoorComponent door;
|
||||
door.openAngle = grid.doorOpenAngle;
|
||||
door.openSpeed = grid.doorOpenSpeed;
|
||||
door.closedOrientation = rot;
|
||||
door.centerOffset = colliderCenter;
|
||||
door.sceneSwitchPath = grid.doorSceneSwitchPath;
|
||||
door.sceneSwitchTarget = grid.doorSceneSwitchTarget;
|
||||
doorEntity.set<DoorComponent>(door);
|
||||
|
||||
RigidBodyComponent rb;
|
||||
rb.bodyType = RigidBodyComponent::BodyType::Static;
|
||||
rb.friction = grid.friction;
|
||||
doorEntity.set<RigidBodyComponent>(rb);
|
||||
|
||||
ActuatorComponent actuator;
|
||||
actuator.radius = 1.5f;
|
||||
actuator.height = 1.8f;
|
||||
if (!grid.doorActionName.empty())
|
||||
actuator.actionNames.push_back(grid.doorActionName);
|
||||
doorEntity.set<ActuatorComponent>(actuator);
|
||||
|
||||
// Box collider for the closed door (child of the door
|
||||
// entity with zero local offset, shape offset to the leaf
|
||||
// center); DoorSystem disables the body while the door is
|
||||
// not fully closed
|
||||
flecs::entity colliderEntity = m_world.entity();
|
||||
colliderEntity.child_of(doorEntity);
|
||||
colliderEntity.set<TransformComponent>(
|
||||
{nullptr, Ogre::Vector3::ZERO,
|
||||
Ogre::Quaternion::IDENTITY,
|
||||
Ogre::Vector3::UNIT_SCALE});
|
||||
PhysicsColliderComponent collider;
|
||||
collider.shapeType = PhysicsColliderComponent::ShapeType::Box;
|
||||
collider.parameters = colliderHalfExtents;
|
||||
collider.offset = colliderCenter;
|
||||
colliderEntity.set<PhysicsColliderComponent>(collider);
|
||||
|
||||
meshData.doorEntities.push_back(doorEntity);
|
||||
meshData.doorEntities.push_back({key, doorEntity});
|
||||
};
|
||||
|
||||
for (const auto &cell : grid.cells) {
|
||||
@@ -4925,7 +5118,8 @@ void CellGridSystem::destroyDoorEntities(flecs::entity entity)
|
||||
if (it == m_entityMeshes.end())
|
||||
return;
|
||||
|
||||
for (auto doorEntity : it->second.doorEntities) {
|
||||
for (auto &pair : it->second.doorEntities) {
|
||||
flecs::entity doorEntity = pair.second;
|
||||
if (!doorEntity.is_valid() || !doorEntity.is_alive())
|
||||
continue;
|
||||
|
||||
|
||||
@@ -79,6 +79,36 @@ public:
|
||||
return m_lotBaseMeshes;
|
||||
}
|
||||
|
||||
// --- Doorway identity (F0) ---
|
||||
|
||||
/* One unique doorway of a grid (a cell edge carrying any of the 8
|
||||
* door flags, deduplicated across the two cells sharing the edge). */
|
||||
struct DoorwayInfo {
|
||||
std::string edgeKey; // canonical "X:x:y:z" / "Z:x:y:z"
|
||||
int cellX = 0, cellY = 0, cellZ = 0; // cell the flag was on
|
||||
int side = 0; // 0=Z-, 1=Z+, 2=X-, 3=X+
|
||||
bool internal = false;
|
||||
};
|
||||
|
||||
/* Canonical edge key of the doorway on the given side of cell
|
||||
* (x, y, z) - the same key buildDoorEntities() uses to deduplicate
|
||||
* doorways, and the key of CellGridComponent::doorConfigs. */
|
||||
static std::string doorEdgeKey(int cellX, int cellY, int cellZ,
|
||||
int side);
|
||||
|
||||
/* All unique doorways of the grid (empty when cells have no door
|
||||
* flags). Used by buildDoorEntities() and the Cell Grid editor
|
||||
* Doors panel. */
|
||||
static std::vector<DoorwayInfo>
|
||||
collectDoorways(const struct CellGridComponent &grid);
|
||||
|
||||
/* Find the runtime door entity for a doorway of the given grid
|
||||
* entity (flecs::entity::null() when none - e.g. doorsEnabled off
|
||||
* or the doorway config is disabled). Matches DoorComponent::edgeKey
|
||||
* on the grid's children. */
|
||||
static flecs::entity findDoorEntity(flecs::entity gridEntity,
|
||||
const std::string &edgeKey);
|
||||
|
||||
private:
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
@@ -117,6 +147,14 @@ private:
|
||||
void buildInternalCorners(const struct CellGridComponent &grid,
|
||||
Procedural::TriangleBuffer &intWallTb);
|
||||
|
||||
// F5: glass panes for external window openings (exteriorOnly mode);
|
||||
// returns the material name used (built-in per-grid or
|
||||
// glassMaterialName)
|
||||
void buildGlass(const struct CellGridComponent &grid,
|
||||
Procedural::TriangleBuffer &glassTb);
|
||||
std::string getGlassMaterialName(
|
||||
flecs::entity entity, const struct CellGridComponent &grid);
|
||||
|
||||
// Build roofs
|
||||
void buildRoofs(flecs::entity lotEntity,
|
||||
const struct CellGridComponent &grid,
|
||||
@@ -173,7 +211,7 @@ private:
|
||||
|
||||
// Door leaf entities (one per unique doorway, swing open/closed)
|
||||
void buildDoorEntities(flecs::entity entity,
|
||||
const struct CellGridComponent &grid,
|
||||
struct CellGridComponent &grid,
|
||||
const std::string &materialName,
|
||||
flecs::entity materialEntity);
|
||||
void createDoorLeafMesh(const struct CellGridComponent &grid,
|
||||
@@ -234,6 +272,7 @@ private:
|
||||
std::vector<std::string> windowMeshes;
|
||||
std::string roofMesh;
|
||||
std::string roofSideMesh;
|
||||
std::string glassMesh;
|
||||
std::vector<Ogre::Entity *> entities;
|
||||
|
||||
// Track texture dependency for automatic rebuild when texture changes
|
||||
@@ -261,8 +300,9 @@ private:
|
||||
std::vector<flecs::entity> isolatedFurnitureEntities;
|
||||
flecs::entity physicsParentEntity = flecs::entity::null();
|
||||
|
||||
// Door leaf entities (runtime, one per unique doorway)
|
||||
std::vector<flecs::entity> doorEntities;
|
||||
// Door leaf entities (runtime, one per unique doorway), each
|
||||
// with its canonical edge key (F0)
|
||||
std::vector<std::pair<std::string, flecs::entity> > doorEntities;
|
||||
std::string doorLeafMesh; // procedural leaf mesh (custom mesh not owned)
|
||||
};
|
||||
std::unordered_map<uint64_t, MeshData> m_entityMeshes;
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
#include "DoorBuilder.hpp"
|
||||
#include "DoorSystem.hpp"
|
||||
#include "GlobalStateStore.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/RigidBody.hpp"
|
||||
#include "../components/PhysicsCollider.hpp"
|
||||
#include "../components/Actuator.hpp"
|
||||
#include "../components/Renderable.hpp"
|
||||
#include <OgreSceneManager.h>
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreEntity.h>
|
||||
#include <OgreMeshManager.h>
|
||||
#include <OgreMaterialManager.h>
|
||||
#include <OgreTechnique.h>
|
||||
#include <OgrePass.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <ProceduralBoxGenerator.h>
|
||||
|
||||
flecs::entity DoorBuilder::build(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
flecs::entity parentEntity,
|
||||
Ogre::SceneNode *parentNode,
|
||||
const Ogre::Vector3 &doorwayPos,
|
||||
const Ogre::Quaternion &doorwayRot,
|
||||
const DoorBuildParams ¶ms)
|
||||
{
|
||||
if (params.leafMeshName.empty() || !parentNode)
|
||||
return flecs::entity::null();
|
||||
|
||||
Ogre::Entity *leafEnt = nullptr;
|
||||
try {
|
||||
leafEnt = sceneMgr->createEntity(params.leafMeshName);
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"DoorBuilder: Error creating door entity: " +
|
||||
std::string(e.what()));
|
||||
return flecs::entity::null();
|
||||
}
|
||||
if (!leafEnt)
|
||||
return flecs::entity::null();
|
||||
if (!params.useMeshMaterial && !params.materialName.empty())
|
||||
leafEnt->setMaterialName(params.materialName);
|
||||
|
||||
// The hinge node is the rotation pivot (DoorSystem rotates it
|
||||
// around local Y), so it sits on the side edge of the doorway
|
||||
// with the leaf extending +X from it in door-local space, not
|
||||
// in the doorway center. Its height is also used for the
|
||||
// actuator prompt position.
|
||||
Ogre::Vector3 hingePos;
|
||||
Ogre::Vector3 colliderCenter;
|
||||
Ogre::Vector3 colliderHalfExtents;
|
||||
if (params.customMesh) {
|
||||
// Custom mesh: hinge at the mesh origin (mesh spans
|
||||
// +X/+Y from it); shift the hinge so the leaf is
|
||||
// centered in the doorway when closed
|
||||
Ogre::Vector3 center = Ogre::Vector3::ZERO;
|
||||
if (!params.customBounds.isNull() &&
|
||||
params.customBounds.isFinite()) {
|
||||
center = params.customBounds.getCenter();
|
||||
colliderHalfExtents = params.customBounds.getHalfSize();
|
||||
} else {
|
||||
colliderHalfExtents = Ogre::Vector3(0.5f, 1.5f, 0.05f);
|
||||
}
|
||||
hingePos = doorwayPos - doorwayRot * Ogre::Vector3(center.x, 0, 0);
|
||||
hingePos.y = doorwayPos.y;
|
||||
colliderCenter = center;
|
||||
} else {
|
||||
// Procedural leaf: mesh spans [0, leafWidth] x
|
||||
// [0, leafHeight] from the hinge edge; the child node
|
||||
// centers it vertically on the hinge
|
||||
hingePos = doorwayPos -
|
||||
doorwayRot * Ogre::Vector3(params.leafWidth / 2.0f, 0, 0);
|
||||
hingePos.y = doorwayPos.y + params.leafHeight / 2.0f;
|
||||
colliderHalfExtents =
|
||||
Ogre::Vector3(params.leafWidth / 2.0f,
|
||||
params.leafHeight / 2.0f,
|
||||
params.leafThickness / 2.0f);
|
||||
colliderCenter = Ogre::Vector3(params.leafWidth / 2.0f, 0, 0);
|
||||
}
|
||||
|
||||
Ogre::SceneNode *hingeNode = parentNode->createChildSceneNode();
|
||||
hingeNode->setPosition(hingePos);
|
||||
hingeNode->setOrientation(doorwayRot);
|
||||
|
||||
if (params.customMesh) {
|
||||
hingeNode->attachObject(leafEnt);
|
||||
} else {
|
||||
Ogre::SceneNode *leafNode = hingeNode->createChildSceneNode();
|
||||
leafNode->setPosition(
|
||||
Ogre::Vector3(0, -params.leafHeight / 2.0f, 0));
|
||||
leafNode->attachObject(leafEnt);
|
||||
}
|
||||
|
||||
// Door entity (runtime - no EditorMarkerComponent, so it is
|
||||
// neither serialized nor editable; it dies with the parent
|
||||
// through the ChildOf cascade)
|
||||
flecs::entity doorEntity = world.entity();
|
||||
doorEntity.child_of(parentEntity);
|
||||
doorEntity.set<TransformComponent>(
|
||||
{hingeNode, hingePos, doorwayRot, Ogre::Vector3::UNIT_SCALE});
|
||||
|
||||
RenderableComponent renderable;
|
||||
renderable.entity = leafEnt;
|
||||
renderable.meshName = params.leafMeshName;
|
||||
doorEntity.set<RenderableComponent>(renderable);
|
||||
|
||||
DoorComponent door;
|
||||
door.edgeKey = params.edgeKey;
|
||||
door.openAngle = params.openAngle;
|
||||
door.openSpeed = params.openSpeed;
|
||||
door.swingReversed = params.swingReversed;
|
||||
door.closedOrientation = doorwayRot;
|
||||
door.centerOffset = colliderCenter;
|
||||
door.sceneSwitchPath = params.sceneSwitchPath;
|
||||
door.sceneSwitchTarget = params.sceneSwitchTarget;
|
||||
door.persistent = params.persistent;
|
||||
door.lockable = params.lockable;
|
||||
door.keyItemId = params.keyItemId;
|
||||
door.doorId = params.doorId;
|
||||
|
||||
/* 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). */
|
||||
bool wasOpen = false;
|
||||
if (!door.doorId.empty()) {
|
||||
wasOpen = DoorSystem::declareDoorDefaults(
|
||||
door.doorId, params.lockedByDefault);
|
||||
}
|
||||
if (wasOpen) {
|
||||
door.isOpen = true;
|
||||
door.currentAngle = door.openAngle;
|
||||
hingeNode->setOrientation(
|
||||
DoorSystem::swingOrientation(door, door.openAngle));
|
||||
}
|
||||
doorEntity.set<DoorComponent>(door);
|
||||
|
||||
RigidBodyComponent rb;
|
||||
rb.bodyType = RigidBodyComponent::BodyType::Static;
|
||||
rb.friction = params.friction;
|
||||
/* A door restored open keeps its collider disabled. */
|
||||
rb.enabled = !wasOpen;
|
||||
doorEntity.set<RigidBodyComponent>(rb);
|
||||
|
||||
ActuatorComponent actuator;
|
||||
actuator.radius = 1.5f;
|
||||
actuator.height = 1.8f;
|
||||
if (!params.actionName.empty())
|
||||
actuator.actionNames.push_back(params.actionName);
|
||||
doorEntity.set<ActuatorComponent>(actuator);
|
||||
|
||||
// Box collider for the closed door (child of the door
|
||||
// entity with zero local offset, shape offset to the leaf
|
||||
// center); DoorSystem disables the body while the door is
|
||||
// not fully closed
|
||||
flecs::entity colliderEntity = world.entity();
|
||||
colliderEntity.child_of(doorEntity);
|
||||
colliderEntity.set<TransformComponent>(
|
||||
{nullptr, Ogre::Vector3::ZERO, Ogre::Quaternion::IDENTITY,
|
||||
Ogre::Vector3::UNIT_SCALE});
|
||||
PhysicsColliderComponent collider;
|
||||
collider.shapeType = PhysicsColliderComponent::ShapeType::Box;
|
||||
collider.parameters = colliderHalfExtents;
|
||||
collider.offset = colliderCenter;
|
||||
colliderEntity.set<PhysicsColliderComponent>(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
|
||||
* 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);
|
||||
}
|
||||
|
||||
Ogre::Entity *occEnt = nullptr;
|
||||
try {
|
||||
occEnt = sceneMgr->createEntity("CellGridDoorOccluderBox");
|
||||
} catch (const std::exception &e) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"DoorBuilder: Error creating door occluder: " +
|
||||
std::string(e.what()));
|
||||
}
|
||||
if (occEnt) {
|
||||
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);
|
||||
occNode->setOrientation(doorwayRot);
|
||||
occNode->setScale(occScale);
|
||||
occNode->attachObject(occEnt);
|
||||
|
||||
flecs::entity occEntity = world.entity();
|
||||
occEntity.child_of(doorEntity);
|
||||
occEntity.set<TransformComponent>(
|
||||
{occNode, occPos, doorwayRot, occScale});
|
||||
RenderableComponent occRenderable;
|
||||
occRenderable.entity = occEnt;
|
||||
occRenderable.meshName = "CellGridDoorOccluderBox";
|
||||
occEntity.set<RenderableComponent>(occRenderable);
|
||||
|
||||
doorEntity.get_mut<DoorComponent>().occluder = occEnt;
|
||||
}
|
||||
}
|
||||
|
||||
return doorEntity;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
#ifndef EDITSCENE_DOOR_BUILDER_HPP
|
||||
#define EDITSCENE_DOOR_BUILDER_HPP
|
||||
#pragma once
|
||||
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Shared door entity builder (F2).
|
||||
*
|
||||
* Factors the door-entity construction that used to live inside
|
||||
* CellGridSystem::buildDoorEntities() so both CellGrid doors and the
|
||||
* StandaloneDoorComponent produce the identical entity structure:
|
||||
*
|
||||
* parentEntity
|
||||
* └── door entity (TransformComponent hinge node, RenderableComponent
|
||||
* 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)
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Leaf mesh creation stays with the caller: pass a ready mesh name
|
||||
* (CellGrid generates its UV-mapped procedural leaf; standalone doors
|
||||
* generate a plain box leaf or use a custom mesh). Custom meshes must be
|
||||
* modeled with the hinge edge at the origin, spanning +X/+Y.
|
||||
*/
|
||||
struct DoorBuildParams {
|
||||
/* Behaviour (already resolved by the caller: grid defaults +
|
||||
* per-door override, or the StandaloneDoorComponent values). */
|
||||
float openAngle = 100.0f; // degrees
|
||||
float openSpeed = 180.0f; // degrees per second
|
||||
bool swingReversed = false; // F3
|
||||
std::string actionName; // optional actuator action
|
||||
std::string sceneSwitchPath; // F1 (empty = normal swinging door)
|
||||
std::string sceneSwitchTarget;
|
||||
bool persistent = false; // F6
|
||||
bool lockable = false;
|
||||
bool lockedByDefault = false;
|
||||
std::string keyItemId;
|
||||
std::string doorId; // "" = ephemeral (never persisted)
|
||||
std::string edgeKey; // CellGrid only (findDoorEntity)
|
||||
float friction = 0.5f;
|
||||
|
||||
/* Leaf. */
|
||||
std::string leafMeshName; // required
|
||||
bool customMesh = false; // hinge-at-origin mesh vs
|
||||
// procedural box leaf
|
||||
bool useMeshMaterial = false; // keep the mesh's own material
|
||||
Ogre::String materialName; // applied otherwise (when non-empty)
|
||||
Ogre::AxisAlignedBox customBounds; // customMesh bounds (for the
|
||||
// collider + hinge shift)
|
||||
float leafWidth = 1.0f; // procedural leaf dimensions
|
||||
float leafHeight = 2.0f;
|
||||
float leafThickness = 0.08f;
|
||||
};
|
||||
|
||||
class DoorBuilder {
|
||||
public:
|
||||
/**
|
||||
* Build a door entity subtree.
|
||||
*
|
||||
* @param parentEntity ECS parent (grid entity / standalone door
|
||||
* entity); the door dies with it through the
|
||||
* ChildOf cascade.
|
||||
* @param parentNode scene node the hinge node is attached to.
|
||||
* @param doorwayPos doorway center at floor level, in
|
||||
* parentNode-local space.
|
||||
* @param doorwayRot doorway orientation in parentNode-local
|
||||
* space (leaf spans +X from the hinge, wall
|
||||
* runs along local X).
|
||||
* @return the door entity, or a null entity on failure.
|
||||
*/
|
||||
static flecs::entity build(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr,
|
||||
flecs::entity parentEntity,
|
||||
Ogre::SceneNode *parentNode,
|
||||
const Ogre::Vector3 &doorwayPos,
|
||||
const Ogre::Quaternion &doorwayRot,
|
||||
const DoorBuildParams ¶ms);
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DOOR_BUILDER_HPP
|
||||
@@ -1,20 +1,96 @@
|
||||
#include "DoorSystem.hpp"
|
||||
#include "GlobalStateStore.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/RigidBody.hpp"
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreEntity.h>
|
||||
#include <algorithm>
|
||||
#include <unordered_set>
|
||||
|
||||
DoorSystem::DoorSystem(flecs::world &world)
|
||||
: m_world(world)
|
||||
, m_doorQuery(world.query<DoorComponent, TransformComponent>())
|
||||
{
|
||||
/* Generic unlock event: "door_unlock" with param "door_id". */
|
||||
m_genericUnlockSub = EventBus::getInstance().subscribe(
|
||||
"door_unlock",
|
||||
[](const Ogre::String &, const editScene::EventParams ¶ms) {
|
||||
std::string doorId = params.getString("door_id", "");
|
||||
if (!doorId.empty())
|
||||
setDoorLocked(doorId, false);
|
||||
});
|
||||
}
|
||||
|
||||
DoorSystem::~DoorSystem() = default;
|
||||
DoorSystem::~DoorSystem()
|
||||
{
|
||||
EventBus &bus = EventBus::getInstance();
|
||||
for (auto &it : m_unlockSubs)
|
||||
bus.unsubscribe(it.second);
|
||||
m_unlockSubs.clear();
|
||||
if (m_genericUnlockSub) {
|
||||
bus.unsubscribe(m_genericUnlockSub);
|
||||
m_genericUnlockSub = 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool DoorSystem::isDoorLocked(const DoorComponent &door)
|
||||
{
|
||||
if (door.doorId.empty())
|
||||
return false;
|
||||
return isDoorLockedById(door.doorId);
|
||||
}
|
||||
|
||||
bool DoorSystem::isDoorLockedById(const std::string &doorId)
|
||||
{
|
||||
return GlobalStateStore::getInstance().getBool(
|
||||
"door." + doorId + ".locked");
|
||||
}
|
||||
|
||||
void DoorSystem::setDoorLocked(const std::string &doorId, bool locked)
|
||||
{
|
||||
if (doorId.empty())
|
||||
return;
|
||||
|
||||
GlobalStateStore::getInstance().set("door." + doorId + ".locked",
|
||||
locked);
|
||||
|
||||
if (!locked) {
|
||||
/* Unlock notifications: per-door and generic. */
|
||||
editScene::EventParams params;
|
||||
params.setString("door_id", doorId);
|
||||
EventBus &bus = EventBus::getInstance();
|
||||
bus.send("door_unlocked_" + doorId, params);
|
||||
bus.send("door_unlocked", params);
|
||||
}
|
||||
}
|
||||
|
||||
bool DoorSystem::declareDoorDefaults(const std::string &doorId,
|
||||
bool lockedByDefault)
|
||||
{
|
||||
if (doorId.empty())
|
||||
return false;
|
||||
|
||||
GlobalStateStore &store = GlobalStateStore::getInstance();
|
||||
store.declareDefault("door." + doorId + ".locked", lockedByDefault);
|
||||
store.declareDefault("door." + doorId + ".isOpen", false);
|
||||
return store.getBool("door." + doorId + ".isOpen");
|
||||
}
|
||||
|
||||
Ogre::Quaternion DoorSystem::swingOrientation(const DoorComponent &door,
|
||||
float angle)
|
||||
{
|
||||
return door.closedOrientation *
|
||||
Ogre::Quaternion(Ogre::Degree(door.swingReversed ? -angle :
|
||||
angle),
|
||||
Ogre::Vector3::UNIT_Y);
|
||||
}
|
||||
|
||||
void DoorSystem::update(float deltaTime)
|
||||
{
|
||||
std::unordered_set<std::string> liveLockableDoors;
|
||||
|
||||
m_doorQuery.each([&](flecs::entity entity, DoorComponent &door,
|
||||
TransformComponent &transform) {
|
||||
// Consume toggle requests (from ActuatorSystem or scripts)
|
||||
@@ -26,8 +102,15 @@ void DoorSystem::update(float deltaTime)
|
||||
auto &rb = entity.get_mut<RigidBodyComponent>();
|
||||
rb.enabled = false;
|
||||
}
|
||||
// A close cancels a pending scene switch (F1)
|
||||
if (!door.isOpen)
|
||||
door.sceneSwitchPending = false;
|
||||
}
|
||||
|
||||
// Track lockable doors for the per-door unlock subscriptions
|
||||
if (door.lockable && !door.doorId.empty())
|
||||
liveLockableDoors.insert(door.doorId);
|
||||
|
||||
float targetAngle = door.isOpen ? door.openAngle : 0.0f;
|
||||
if (door.currentAngle == targetAngle)
|
||||
return;
|
||||
@@ -44,10 +127,7 @@ void DoorSystem::update(float deltaTime)
|
||||
|
||||
if (transform.node) {
|
||||
transform.node->setOrientation(
|
||||
door.closedOrientation *
|
||||
Ogre::Quaternion(
|
||||
Ogre::Degree(door.currentAngle),
|
||||
Ogre::Vector3::UNIT_Y));
|
||||
swingOrientation(door, door.currentAngle));
|
||||
}
|
||||
|
||||
// Fully closed again: restore the collider in the closed pose
|
||||
@@ -56,5 +136,62 @@ void DoorSystem::update(float deltaTime)
|
||||
auto &rb = entity.get_mut<RigidBodyComponent>();
|
||||
rb.enabled = true;
|
||||
}
|
||||
|
||||
// Swing completed: persist the open state (F6)
|
||||
if (door.currentAngle == targetAngle &&
|
||||
!door.doorId.empty()) {
|
||||
GlobalStateStore::getInstance().set(
|
||||
"door." + door.doorId + ".isOpen", door.isOpen);
|
||||
}
|
||||
|
||||
// F1: a scene-switch door fires the switch only when fully
|
||||
// open; the loading cover hides the actual transition
|
||||
if (door.sceneSwitchPending && door.isOpen &&
|
||||
door.currentAngle == door.openAngle) {
|
||||
door.sceneSwitchPending = false;
|
||||
if (!door.sceneSwitchPath.empty()) {
|
||||
editScene::EventParams params;
|
||||
params.setString("path", door.sceneSwitchPath);
|
||||
params.setString("target",
|
||||
door.sceneSwitchTarget);
|
||||
params.setString("door_id", door.doorId);
|
||||
EventBus::getInstance().send("door_scene_switch",
|
||||
params);
|
||||
if (m_editorApp) {
|
||||
SceneSwitchOptions opts;
|
||||
opts.targetEntityName =
|
||||
door.sceneSwitchTarget;
|
||||
m_editorApp->switchScene(
|
||||
door.sceneSwitchPath, opts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// F1: the black occluder is visible unless the door is
|
||||
// fully closed
|
||||
if (door.occluder)
|
||||
door.occluder->setVisible(door.currentAngle != 0.0f);
|
||||
});
|
||||
|
||||
// Refresh the per-door "door_unlock_<doorId>" subscriptions
|
||||
// (EventBus has no wildcard matching).
|
||||
EventBus &bus = EventBus::getInstance();
|
||||
for (auto it = m_unlockSubs.begin(); it != m_unlockSubs.end();) {
|
||||
if (liveLockableDoors.count(it->first)) {
|
||||
++it;
|
||||
} else {
|
||||
bus.unsubscribe(it->second);
|
||||
it = m_unlockSubs.erase(it);
|
||||
}
|
||||
}
|
||||
for (const std::string &doorId : liveLockableDoors) {
|
||||
if (m_unlockSubs.count(doorId))
|
||||
continue;
|
||||
m_unlockSubs[doorId] = bus.subscribe(
|
||||
"door_unlock_" + doorId,
|
||||
[doorId](const Ogre::String &,
|
||||
const editScene::EventParams &) {
|
||||
setDoorLocked(doorId, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
#define EDITSCENE_DOOR_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include "EventBus.hpp"
|
||||
#include <flecs.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* Door system - animates CellGrid door entities.
|
||||
@@ -13,6 +16,16 @@
|
||||
* local Y at the configured speed, and disables the door's
|
||||
* RigidBodyComponent while the door is not fully closed (the collider
|
||||
* only exists in the closed pose).
|
||||
*
|
||||
* Persistent door state (F6) lives in the GlobalStateStore under
|
||||
* "door.<doorId>.locked" / "door.<doorId>.isOpen" (doorId is the F0 global
|
||||
* door ID "<gridUid>:<edgeKey>"; 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
|
||||
* static helpers below and through the EventBus: sending
|
||||
* "door_unlock_<doorId>" or the generic "door_unlock" event (param
|
||||
* door_id) unlocks a door; "door_unlocked_<doorId>" / "door_unlocked" are
|
||||
* emitted as notifications.
|
||||
*/
|
||||
class DoorSystem {
|
||||
public:
|
||||
@@ -21,10 +34,40 @@ public:
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
/* F1: needed to queue the scene switch when a scene-switch door
|
||||
* finishes swinging open. */
|
||||
void setEditorApp(class EditorApp *app) { m_editorApp = app; }
|
||||
|
||||
/* Persistent door state helpers (F6). All are no-ops / false for
|
||||
* ephemeral doors (empty doorId). */
|
||||
static bool isDoorLocked(const struct DoorComponent &door);
|
||||
static bool isDoorLockedById(const std::string &doorId);
|
||||
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. */
|
||||
static bool declareDoorDefaults(const std::string &doorId,
|
||||
bool lockedByDefault);
|
||||
|
||||
/* Hinge orientation for a swing angle (F3: swingReversed negates
|
||||
* the applied angle; the stored angles stay positive). Used by
|
||||
* update() and by the door builders when snapping a restored-open
|
||||
* door to its persisted pose. */
|
||||
static Ogre::Quaternion
|
||||
swingOrientation(const struct DoorComponent &door, float angle);
|
||||
|
||||
private:
|
||||
flecs::world &m_world;
|
||||
class EditorApp *m_editorApp = nullptr;
|
||||
flecs::query<struct DoorComponent, struct TransformComponent>
|
||||
m_doorQuery;
|
||||
|
||||
/* Per-door "door_unlock_<doorId>" subscriptions (EventBus has no
|
||||
* wildcard matching); refreshed each update, cleaned up when the
|
||||
* door entity disappears. */
|
||||
std::unordered_map<std::string, EventBus::ListenerId> m_unlockSubs;
|
||||
EventBus::ListenerId m_genericUnlockSub = 0;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DOOR_SYSTEM_HPP
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "../components/GeneratedPhysicsTag.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../ui/DoorPickState.hpp"
|
||||
#include "GlobalStateStore.hpp"
|
||||
#include "EditorUISystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
#include "DialogueSystem.hpp"
|
||||
@@ -140,6 +143,44 @@ bool EditorUISystem::onMousePressed(const Ogre::Ray &mouseRay)
|
||||
if (ImGui::GetIO().WantCaptureMouse)
|
||||
return false;
|
||||
|
||||
// Door pick mode (F0): the Cell Grid editor's Doors panel armed a
|
||||
// pick; this click selects a doorway of that grid and is consumed.
|
||||
{
|
||||
DoorPickState &pick = DoorPickState::instance();
|
||||
if (pick.active) {
|
||||
pick.active = false;
|
||||
pick.done = true;
|
||||
pick.resultValid = false;
|
||||
pick.resultEdgeKey.clear();
|
||||
flecs::entity gridEntity =
|
||||
m_world.entity(pick.gridEntityId);
|
||||
if (gridEntity.is_alive()) {
|
||||
float bestT = FLT_MAX;
|
||||
gridEntity.children([&](flecs::entity child) {
|
||||
if (!child.has<DoorComponent>() ||
|
||||
!child.has<RenderableComponent>())
|
||||
return;
|
||||
Ogre::Entity *oe =
|
||||
child.get<RenderableComponent>()
|
||||
.entity;
|
||||
if (!oe)
|
||||
return;
|
||||
auto hit = Ogre::Math::intersects(
|
||||
mouseRay,
|
||||
oe->getWorldBoundingBox());
|
||||
if (hit.first && hit.second < bestT) {
|
||||
bestT = hit.second;
|
||||
pick.resultValid = true;
|
||||
pick.resultEdgeKey =
|
||||
child.get<DoorComponent>()
|
||||
.edgeKey;
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Prefab spawn mode (M6): record the press; the click-vs-drag
|
||||
// decision is made on release (5 px threshold). The gizmo already
|
||||
// had priority above so spawners stay movable while placing.
|
||||
@@ -407,6 +448,11 @@ void EditorUISystem::update(float deltaTime)
|
||||
renderPrefabBrowser();
|
||||
renderCursorPanel();
|
||||
|
||||
// Render Open Project dialog
|
||||
if (m_showOpenProjectDialog) {
|
||||
renderOpenProjectDialog();
|
||||
}
|
||||
|
||||
// Render Navigation panel (teleport + world bookmarks)
|
||||
if (m_showNavigation) {
|
||||
m_navigationPanel.render(&m_showNavigation);
|
||||
@@ -503,6 +549,9 @@ void EditorUISystem::renderHierarchyWindow()
|
||||
if (ImGui::MenuItem("Switch Scene...")) {
|
||||
showFileDialog(FileDialogMode::Switch);
|
||||
}
|
||||
if (ImGui::MenuItem("Open Project...")) {
|
||||
m_showOpenProjectDialog = true;
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem("Save Action DB",
|
||||
nullptr)) {
|
||||
@@ -787,10 +836,12 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
|
||||
}
|
||||
|
||||
// Collect children using flecs::ChildOf relationship
|
||||
// (skip auto-generated physics entities)
|
||||
// (skip auto-generated physics and door entities; doors are
|
||||
// configured per-doorway in the CellGrid editor instead)
|
||||
std::vector<flecs::entity> children;
|
||||
entity.children([&](flecs::entity child) {
|
||||
if (!child.has<GeneratedPhysicsTag>()) {
|
||||
if (!child.has<GeneratedPhysicsTag>() &&
|
||||
!child.has<DoorComponent>()) {
|
||||
children.push_back(child);
|
||||
}
|
||||
});
|
||||
@@ -1367,6 +1418,11 @@ void EditorUISystem::loadScene(const std::string &filepath)
|
||||
if (!m_serializer)
|
||||
return;
|
||||
|
||||
/* F9: editor mode always starts a (re)loaded scene from system
|
||||
* defaults - the global state store never carries game-session
|
||||
* state into the editor. */
|
||||
GlobalStateStore::getInstance().clearToDefaults();
|
||||
|
||||
if (m_serializer->loadFromFile(filepath, this)) {
|
||||
m_navigationPanel.setBookmarks(m_serializer->getBookmarks());
|
||||
SceneScriptSystem::loadPendingScripts(m_world);
|
||||
@@ -1835,6 +1891,152 @@ void EditorUISystem::renderPrefabBrowser()
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void EditorUISystem::renderOpenProjectDialog()
|
||||
{
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(LEFT_PANEL_WIDTH, 100),
|
||||
ImGuiCond_FirstUseEver);
|
||||
ImGui::SetNextWindowSize(ImVec2(520, 430), ImGuiCond_FirstUseEver);
|
||||
|
||||
ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse;
|
||||
if (!ImGui::Begin("Open Project", &m_showOpenProjectDialog,
|
||||
flags)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
/* Lazy init: start browsing at the current project root's
|
||||
* parent (or the working directory). */
|
||||
if (m_openProjectBrowseDir.empty()) {
|
||||
std::error_code ec;
|
||||
fs::path start = fs::current_path(ec);
|
||||
if (m_editorApp && !m_editorApp->getProjectRoot().empty()) {
|
||||
fs::path parent =
|
||||
fs::path(m_editorApp->getProjectRoot())
|
||||
.parent_path();
|
||||
if (!parent.empty() && fs::is_directory(parent, ec))
|
||||
start = parent;
|
||||
}
|
||||
m_openProjectBrowseDir = start.string();
|
||||
m_openProjectDirsDirty = true;
|
||||
std::snprintf(m_openProjectPath, sizeof(m_openProjectPath),
|
||||
"%s", m_openProjectBrowseDir.c_str());
|
||||
}
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Open a project directory: the working directory switches to "
|
||||
"it (scenes, prefabs, config JSONs resolve relative to it), "
|
||||
"the save directory follows the project's appName, and the "
|
||||
"current scene is closed. Directories containing a "
|
||||
"project.json are marked [project].");
|
||||
if (m_editorApp &&
|
||||
!m_editorApp->getProjectRoot().empty()) {
|
||||
ImGui::Text("Current project: %s",
|
||||
m_editorApp->getProjectConfig().appName.c_str());
|
||||
ImGui::TextWrapped("%s",
|
||||
m_editorApp->getProjectRoot().c_str());
|
||||
}
|
||||
|
||||
/* Editable path: Enter navigates the browser (relative paths
|
||||
* resolve against the browsed directory). */
|
||||
if (ImGui::InputText("Directory", m_openProjectPath,
|
||||
sizeof(m_openProjectPath),
|
||||
ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
std::error_code ec;
|
||||
fs::path p(m_openProjectPath);
|
||||
if (p.is_relative())
|
||||
p = fs::path(m_openProjectBrowseDir) / p;
|
||||
p = fs::weakly_canonical(p, ec);
|
||||
if (!ec && fs::is_directory(p, ec)) {
|
||||
m_openProjectBrowseDir = p.string();
|
||||
m_openProjectDirsDirty = true;
|
||||
m_openProjectError.clear();
|
||||
std::snprintf(m_openProjectPath,
|
||||
sizeof(m_openProjectPath), "%s",
|
||||
m_openProjectBrowseDir.c_str());
|
||||
} else {
|
||||
m_openProjectError = "Not a directory: " +
|
||||
std::string(m_openProjectPath);
|
||||
}
|
||||
}
|
||||
|
||||
/* Subdirectory listing, rebuilt on navigation. Dot-directories
|
||||
* are hidden. */
|
||||
if (m_openProjectDirsDirty) {
|
||||
m_openProjectDirsDirty = false;
|
||||
m_openProjectDirs.clear();
|
||||
std::error_code ec;
|
||||
for (const auto &entry :
|
||||
fs::directory_iterator(m_openProjectBrowseDir, ec)) {
|
||||
if (!entry.is_directory(ec))
|
||||
continue;
|
||||
std::string name = entry.path().filename().string();
|
||||
if (name.empty() || name[0] == '.')
|
||||
continue;
|
||||
bool isProject =
|
||||
fs::exists(entry.path() / "project.json", ec);
|
||||
m_openProjectDirs.emplace_back(name, isProject);
|
||||
}
|
||||
std::sort(m_openProjectDirs.begin(), m_openProjectDirs.end(),
|
||||
[](const auto &a, const auto &b) {
|
||||
return a.first < b.first;
|
||||
});
|
||||
}
|
||||
|
||||
auto enterDir = [&](const fs::path &p) {
|
||||
m_openProjectBrowseDir = p.string();
|
||||
m_openProjectDirsDirty = true;
|
||||
m_openProjectError.clear();
|
||||
std::snprintf(m_openProjectPath, sizeof(m_openProjectPath),
|
||||
"%s", m_openProjectBrowseDir.c_str());
|
||||
};
|
||||
|
||||
if (ImGui::BeginChild("##projdirs", ImVec2(0, 180),
|
||||
ImGuiChildFlags_Borders)) {
|
||||
if (ImGui::Selectable("..")) {
|
||||
fs::path cur(m_openProjectBrowseDir);
|
||||
fs::path parent = cur.parent_path();
|
||||
if (!parent.empty() && parent != cur)
|
||||
enterDir(parent);
|
||||
}
|
||||
for (const auto &dir : m_openProjectDirs) {
|
||||
std::string label =
|
||||
dir.first + (dir.second ? " [project]" : "");
|
||||
if (ImGui::Selectable(label.c_str()))
|
||||
enterDir(fs::path(m_openProjectBrowseDir) /
|
||||
dir.first);
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
if (!m_openProjectError.empty()) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Text,
|
||||
ImVec4(1.0f, 0.4f, 0.4f, 1.0f));
|
||||
ImGui::TextWrapped("%s", m_openProjectError.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
if (ImGui::Button("Open This Directory", ImVec2(180, 0))) {
|
||||
if (m_editorApp &&
|
||||
m_editorApp->openProject(m_openProjectBrowseDir)) {
|
||||
m_showOpenProjectDialog = false;
|
||||
m_openProjectBrowseDir.clear();
|
||||
m_openProjectError.clear();
|
||||
} else {
|
||||
m_openProjectError =
|
||||
"Failed to open: " + m_openProjectBrowseDir;
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0))) {
|
||||
m_showOpenProjectDialog = false;
|
||||
m_openProjectBrowseDir.clear();
|
||||
m_openProjectError.clear();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void EditorUISystem::renderCursorPanel()
|
||||
{
|
||||
if (!m_showCursorPanel)
|
||||
|
||||
@@ -229,6 +229,7 @@ public:
|
||||
void showCreatePrefabDialog(flecs::entity entity);
|
||||
void renderPrefabBrowser();
|
||||
void renderCursorPanel();
|
||||
void renderOpenProjectDialog();
|
||||
void renderDialogueSettingsWindow();
|
||||
void renderInventoryConfigWindow();
|
||||
|
||||
@@ -331,6 +332,20 @@ private:
|
||||
|
||||
// 3D Cursor state
|
||||
bool m_showCursorPanel = false;
|
||||
|
||||
// Open Project dialog state (F8)
|
||||
bool m_showOpenProjectDialog = false;
|
||||
char m_openProjectPath[512] = { 0 };
|
||||
/* Open Project directory browser (ImGui has no native file
|
||||
* dialog): m_openProjectBrowseDir is the directory being browsed
|
||||
* (m_openProjectPath mirrors it for manual entry);
|
||||
* m_openProjectDirs caches its subdirectories - name plus a
|
||||
* "contains project.json" flag - rebuilt when the dirty flag is
|
||||
* set. */
|
||||
std::string m_openProjectBrowseDir;
|
||||
std::vector<std::pair<std::string, bool>> m_openProjectDirs;
|
||||
bool m_openProjectDirsDirty = true;
|
||||
std::string m_openProjectError;
|
||||
enum class CursorInteractionMode {
|
||||
None,
|
||||
Place, // Click to raycast-place on surface
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
#include "GlobalStateStore.hpp"
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
GlobalStateStore &GlobalStateStore::getInstance()
|
||||
{
|
||||
static GlobalStateStore instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
GlobalStateStore::GlobalStateStore()
|
||||
{
|
||||
m_autoSavePath = "global_state.json";
|
||||
}
|
||||
|
||||
GlobalStateStore::Value GlobalStateStore::makeBool(bool v)
|
||||
{
|
||||
Value val;
|
||||
val.type = Value::BOOL;
|
||||
val.boolVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
GlobalStateStore::Value GlobalStateStore::makeInt(int64_t v)
|
||||
{
|
||||
Value val;
|
||||
val.type = Value::INT;
|
||||
val.intVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
GlobalStateStore::Value GlobalStateStore::makeFloat(double v)
|
||||
{
|
||||
Value val;
|
||||
val.type = Value::FLOAT;
|
||||
val.floatVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
GlobalStateStore::Value GlobalStateStore::makeString(const std::string &v)
|
||||
{
|
||||
Value val;
|
||||
val.type = Value::STRING;
|
||||
val.strVal = v;
|
||||
return val;
|
||||
}
|
||||
|
||||
void GlobalStateStore::declareDefault(const std::string &name, bool v)
|
||||
{
|
||||
if (!name.empty())
|
||||
m_defaults[name] = makeBool(v);
|
||||
}
|
||||
|
||||
void GlobalStateStore::declareDefault(const std::string &name, int64_t v)
|
||||
{
|
||||
if (!name.empty())
|
||||
m_defaults[name] = makeInt(v);
|
||||
}
|
||||
|
||||
void GlobalStateStore::declareDefault(const std::string &name, double v)
|
||||
{
|
||||
if (!name.empty())
|
||||
m_defaults[name] = makeFloat(v);
|
||||
}
|
||||
|
||||
void GlobalStateStore::declareDefault(const std::string &name,
|
||||
const std::string &v)
|
||||
{
|
||||
if (!name.empty())
|
||||
m_defaults[name] = makeString(v);
|
||||
}
|
||||
|
||||
void GlobalStateStore::set(const std::string &name, bool v)
|
||||
{
|
||||
if (name.empty())
|
||||
return;
|
||||
m_values[name] = makeBool(v);
|
||||
autoSave();
|
||||
}
|
||||
|
||||
void GlobalStateStore::set(const std::string &name, int64_t v)
|
||||
{
|
||||
if (name.empty())
|
||||
return;
|
||||
m_values[name] = makeInt(v);
|
||||
autoSave();
|
||||
}
|
||||
|
||||
void GlobalStateStore::set(const std::string &name, double v)
|
||||
{
|
||||
if (name.empty())
|
||||
return;
|
||||
m_values[name] = makeFloat(v);
|
||||
autoSave();
|
||||
}
|
||||
|
||||
void GlobalStateStore::set(const std::string &name, const std::string &v)
|
||||
{
|
||||
if (name.empty())
|
||||
return;
|
||||
m_values[name] = makeString(v);
|
||||
autoSave();
|
||||
}
|
||||
|
||||
bool GlobalStateStore::getBool(const std::string &name, bool fallback) const
|
||||
{
|
||||
auto it = m_values.find(name);
|
||||
if (it != m_values.end() && it->second.type == Value::BOOL)
|
||||
return it->second.boolVal;
|
||||
auto dit = m_defaults.find(name);
|
||||
if (dit != m_defaults.end() && dit->second.type == Value::BOOL)
|
||||
return dit->second.boolVal;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
int64_t GlobalStateStore::getInt(const std::string &name,
|
||||
int64_t fallback) const
|
||||
{
|
||||
auto it = m_values.find(name);
|
||||
if (it != m_values.end() && it->second.type == Value::INT)
|
||||
return it->second.intVal;
|
||||
auto dit = m_defaults.find(name);
|
||||
if (dit != m_defaults.end() && dit->second.type == Value::INT)
|
||||
return dit->second.intVal;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
double GlobalStateStore::getFloat(const std::string &name,
|
||||
double fallback) const
|
||||
{
|
||||
auto it = m_values.find(name);
|
||||
if (it != m_values.end() && it->second.type == Value::FLOAT)
|
||||
return it->second.floatVal;
|
||||
auto dit = m_defaults.find(name);
|
||||
if (dit != m_defaults.end() && dit->second.type == Value::FLOAT)
|
||||
return dit->second.floatVal;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
std::string GlobalStateStore::getString(const std::string &name,
|
||||
const std::string &fallback) const
|
||||
{
|
||||
auto it = m_values.find(name);
|
||||
if (it != m_values.end() && it->second.type == Value::STRING)
|
||||
return it->second.strVal;
|
||||
auto dit = m_defaults.find(name);
|
||||
if (dit != m_defaults.end() && dit->second.type == Value::STRING)
|
||||
return dit->second.strVal;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
bool GlobalStateStore::has(const std::string &name) const
|
||||
{
|
||||
return m_values.count(name) || m_defaults.count(name);
|
||||
}
|
||||
|
||||
void GlobalStateStore::remove(const std::string &name)
|
||||
{
|
||||
if (m_values.erase(name))
|
||||
autoSave();
|
||||
}
|
||||
|
||||
void GlobalStateStore::renamePrefix(const std::string &oldPrefix,
|
||||
const std::string &newPrefix)
|
||||
{
|
||||
if (oldPrefix.empty() || oldPrefix == newPrefix)
|
||||
return;
|
||||
std::vector<std::pair<std::string, Value>> moved;
|
||||
for (auto it = m_values.begin(); it != m_values.end();) {
|
||||
if (it->first.compare(0, oldPrefix.size(), oldPrefix) == 0) {
|
||||
moved.push_back(
|
||||
{ newPrefix + it->first.substr(oldPrefix.size()),
|
||||
it->second });
|
||||
it = m_values.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
for (auto &pair : moved)
|
||||
m_values[pair.first] = pair.second;
|
||||
if (!moved.empty())
|
||||
autoSave();
|
||||
}
|
||||
|
||||
void GlobalStateStore::clearToDefaults()
|
||||
{
|
||||
m_values.clear();
|
||||
autoSave();
|
||||
}
|
||||
|
||||
nlohmann::json GlobalStateStore::serialize() const
|
||||
{
|
||||
nlohmann::json j;
|
||||
j["version"] = "1.0";
|
||||
nlohmann::json values = nlohmann::json::object();
|
||||
for (const auto &pair : m_values) {
|
||||
const Value &v = pair.second;
|
||||
switch (v.type) {
|
||||
case Value::BOOL:
|
||||
values[pair.first] = v.boolVal;
|
||||
break;
|
||||
case Value::INT:
|
||||
values[pair.first] = v.intVal;
|
||||
break;
|
||||
case Value::FLOAT:
|
||||
values[pair.first] = v.floatVal;
|
||||
break;
|
||||
case Value::STRING:
|
||||
values[pair.first] = v.strVal;
|
||||
break;
|
||||
}
|
||||
}
|
||||
j["values"] = values;
|
||||
return j;
|
||||
}
|
||||
|
||||
void GlobalStateStore::deserialize(const nlohmann::json &j)
|
||||
{
|
||||
m_values.clear();
|
||||
if (!j.contains("values") || !j["values"].is_object())
|
||||
return;
|
||||
for (auto &[name, val] : j["values"].items()) {
|
||||
if (val.is_boolean())
|
||||
m_values[name] = makeBool(val.get<bool>());
|
||||
else if (val.is_number_integer())
|
||||
m_values[name] = makeInt(val.get<int64_t>());
|
||||
else if (val.is_number_float())
|
||||
m_values[name] = makeFloat(val.get<double>());
|
||||
else if (val.is_string())
|
||||
m_values[name] = makeString(val.get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
bool GlobalStateStore::saveToFile(const std::string &filepath)
|
||||
{
|
||||
try {
|
||||
std::ofstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
m_lastError = "Cannot open " + filepath;
|
||||
return false;
|
||||
}
|
||||
file << serialize().dump(4);
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
m_lastError = std::string("Save error: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool GlobalStateStore::loadFromFile(const std::string &filepath)
|
||||
{
|
||||
try {
|
||||
std::ifstream file(filepath);
|
||||
if (!file.is_open()) {
|
||||
m_lastError = "Cannot open " + filepath;
|
||||
return false;
|
||||
}
|
||||
nlohmann::json j;
|
||||
file >> j;
|
||||
deserialize(j);
|
||||
return true;
|
||||
} catch (const std::exception &e) {
|
||||
m_lastError = std::string("Load error: ") + e.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalStateStore::autoSave()
|
||||
{
|
||||
if (m_autoSaveEnabled && !m_autoSavePath.empty())
|
||||
saveToFile(m_autoSavePath);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#ifndef EDITSCENE_GLOBALSTATESTORE_HPP
|
||||
#define EDITSCENE_GLOBALSTATESTORE_HPP
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* Global persistent variable storage (F9).
|
||||
*
|
||||
* A scene-independent singleton store of typed variables (string name +
|
||||
* bool / int64 / double / string value) that can be created, set, read and
|
||||
* removed from both C++ and Lua (see lua/LuaGlobalStateApi.cpp). It is
|
||||
* the generic persistent storage for gameplay systems; each system owns a
|
||||
* dot-namespaced key prefix (registered in AGENTS.md). First consumer:
|
||||
* persistent door state under "door.<doorId>.locked" / ".isOpen" (F6).
|
||||
*
|
||||
* Semantics:
|
||||
* - Systems declare the variables they use with declareDefault(); reading
|
||||
* an unset variable returns its declared default (or the caller's
|
||||
* fallback). Defaults are NOT persisted - only explicitly set() values
|
||||
* are serialized, keeping saves small and letting defaults evolve in
|
||||
* code.
|
||||
* - The store survives EditorApp::switchScene() (singleton, scene
|
||||
* independent).
|
||||
* - Save-game integration: EditorApp::saveGame()/loadGame() carry the
|
||||
* store in the save file's "globalState" section.
|
||||
* - autoSave() mirrors the store to global_state.json (CWD = project dir
|
||||
* after F8), the cross-session cache in game mode. Editor mode does
|
||||
* NOT load that file: EditorApp calls clearToDefaults() at startup and
|
||||
* on scene (re)load so editing always starts from system defaults.
|
||||
*/
|
||||
class GlobalStateStore {
|
||||
public:
|
||||
static GlobalStateStore &getInstance();
|
||||
|
||||
/* Register a default for a variable. Reading an unset variable
|
||||
* returns its declared default. Re-declaring updates the default;
|
||||
* explicitly set values are unaffected. Not persisted. */
|
||||
void declareDefault(const std::string &name, bool v);
|
||||
void declareDefault(const std::string &name, int64_t v);
|
||||
void declareDefault(const std::string &name, double v);
|
||||
void declareDefault(const std::string &name, const std::string &v);
|
||||
|
||||
/* Explicitly set a variable (persisted). Triggers autoSave(). */
|
||||
void set(const std::string &name, bool v);
|
||||
void set(const std::string &name, int64_t v);
|
||||
void set(const std::string &name, double v);
|
||||
void set(const std::string &name, const std::string &v);
|
||||
|
||||
/* Typed reads: explicit value first, then the declared default,
|
||||
* then the caller's fallback. A type mismatch falls through to the
|
||||
* next source (no implicit conversion). */
|
||||
bool getBool(const std::string &name, bool fallback = false) const;
|
||||
int64_t getInt(const std::string &name, int64_t fallback = 0) const;
|
||||
double getFloat(const std::string &name, double fallback = 0.0) const;
|
||||
std::string getString(const std::string &name,
|
||||
const std::string &fallback = "") const;
|
||||
|
||||
/* True when the variable has an explicit value or a declared
|
||||
* default. */
|
||||
bool has(const std::string &name) const;
|
||||
|
||||
/* Remove the explicit value (the declared default, if any, still
|
||||
* applies). Triggers autoSave(). */
|
||||
void remove(const std::string &name);
|
||||
|
||||
/* Move all explicitly set values whose name starts with oldPrefix
|
||||
* to newPrefix (F0 door reassignment state migration). Defaults
|
||||
* are not renamed (they are re-declared by the systems). */
|
||||
void renamePrefix(const std::string &oldPrefix,
|
||||
const std::string &newPrefix);
|
||||
|
||||
/* Drop all explicit values, keeping the declared defaults.
|
||||
* Triggers autoSave(). */
|
||||
void clearToDefaults();
|
||||
|
||||
nlohmann::json serialize() const;
|
||||
void deserialize(const nlohmann::json &j);
|
||||
|
||||
bool saveToFile(const std::string &filepath);
|
||||
bool loadFromFile(const std::string &filepath);
|
||||
const std::string &getLastError() const { return m_lastError; }
|
||||
|
||||
void autoSave();
|
||||
void setAutoSaveEnabled(bool enabled) { m_autoSaveEnabled = enabled; }
|
||||
|
||||
private:
|
||||
GlobalStateStore();
|
||||
~GlobalStateStore() = default;
|
||||
|
||||
GlobalStateStore(const GlobalStateStore &) = delete;
|
||||
GlobalStateStore &operator=(const GlobalStateStore &) = delete;
|
||||
|
||||
struct Value {
|
||||
enum Type { BOOL, INT, FLOAT, STRING } type = BOOL;
|
||||
bool boolVal = false;
|
||||
int64_t intVal = 0;
|
||||
double floatVal = 0.0;
|
||||
std::string strVal;
|
||||
};
|
||||
|
||||
static Value makeBool(bool v);
|
||||
static Value makeInt(int64_t v);
|
||||
static Value makeFloat(double v);
|
||||
static Value makeString(const std::string &v);
|
||||
|
||||
std::unordered_map<std::string, Value> m_values;
|
||||
std::unordered_map<std::string, Value> m_defaults;
|
||||
mutable std::string m_lastError;
|
||||
std::string m_autoSavePath;
|
||||
bool m_autoSaveEnabled = true;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_GLOBALSTATESTORE_HPP
|
||||
@@ -7,8 +7,10 @@
|
||||
#include "../components/StaticGeometryMember.hpp"
|
||||
#include "../components/PhysicsCollider.hpp"
|
||||
#include "../components/CellGrid.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../recast/TileCacheNavMesh.hpp"
|
||||
#include "CellGridSystem.hpp"
|
||||
#include "DoorSystem.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
@@ -75,9 +77,14 @@ static void collectEntitiesFromNode(Ogre::SceneNode *node,
|
||||
// and all its descendants. This is needed because flecs parent/child
|
||||
// hierarchy does NOT map to Ogre SceneNode hierarchy; each entity has
|
||||
// its own independent scene node.
|
||||
// F7: door entities (and their subtrees: leaf, collider, occluder) are
|
||||
// skipped - doors are not navmesh obstacles; the doorway floor is
|
||||
// painted with the door area id instead (see getDoorVolume).
|
||||
static void collectEntitiesFromFlecsEntity(flecs::entity e,
|
||||
std::vector<Ogre::Entity *> &out)
|
||||
{
|
||||
if (e.has<DoorComponent>())
|
||||
return;
|
||||
if (e.has<TransformComponent>()) {
|
||||
auto &trans = e.get<TransformComponent>();
|
||||
if (trans.node)
|
||||
@@ -119,14 +126,18 @@ void NavMeshSystem::update(float deltaTime)
|
||||
|
||||
auto &state = m_states[e.id()];
|
||||
|
||||
/* F7: refresh doorway volumes before any (re)build so
|
||||
* fresh tiles get the door area id. */
|
||||
if (state.navmesh && state.navmesh->isBuilt())
|
||||
state.navmesh->setDoorVolumes(collectDoorVolumes());
|
||||
|
||||
// Handle full rebuild request
|
||||
if (comp.dirty) {
|
||||
buildOrUpdate(e.id(), comp, state);
|
||||
comp.dirty = false;
|
||||
comp.needsPartialRebuild = false;
|
||||
state.lastTransformVersions.clear();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
|
||||
// Check for geometry changes that need partial rebuild
|
||||
bool changed = false;
|
||||
@@ -191,6 +202,15 @@ void NavMeshSystem::update(float deltaTime)
|
||||
if (state.debugDraw)
|
||||
state.navmesh->drawNavMesh();
|
||||
}
|
||||
}
|
||||
|
||||
/* F7: pump queued tile-cache requests (locked-door
|
||||
* obstacles) and keep the obstacle set in sync with the
|
||||
* live doors' locked state. */
|
||||
if (state.navmesh && state.navmesh->isBuilt()) {
|
||||
state.navmesh->update();
|
||||
syncDoorObstacles(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,6 +219,7 @@ void NavMeshSystem::buildOrUpdate(flecs::entity_t e, NavMeshComponent &comp,
|
||||
{
|
||||
state.navmesh.reset();
|
||||
state.lastTransformVersions.clear();
|
||||
state.doorObstacles.clear();
|
||||
|
||||
TileCacheNavMesh::BuildParams params;
|
||||
params.cellSize = comp.cellSize;
|
||||
@@ -212,8 +233,10 @@ void NavMeshSystem::buildOrUpdate(flecs::entity_t e, NavMeshComponent &comp,
|
||||
params.regionMinSize = comp.regionMinSize;
|
||||
params.regionMergeSize = comp.regionMergeSize;
|
||||
params.tileSize = comp.tileSize;
|
||||
params.doorAreaCost = comp.doorAreaCost;
|
||||
|
||||
state.navmesh = std::make_unique<TileCacheNavMesh>(m_sceneMgr, params);
|
||||
state.navmesh->setDoorVolumes(collectDoorVolumes());
|
||||
|
||||
std::vector<Ogre::Entity *> entities;
|
||||
std::vector<Ogre::Entity *> tempEntities;
|
||||
@@ -237,6 +260,11 @@ void NavMeshSystem::buildOrUpdate(flecs::entity_t e, NavMeshComponent &comp,
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"[NavMeshSystem] Navmesh built successfully");
|
||||
|
||||
/* F7: doors that are already locked get their obstacle right
|
||||
* away (syncDoorObstacles keeps this in sync from now on). */
|
||||
syncDoorObstacles(state);
|
||||
state.navmesh->update();
|
||||
|
||||
if (state.debugDraw)
|
||||
state.navmesh->drawNavMesh();
|
||||
|
||||
@@ -298,6 +326,11 @@ void NavMeshSystem::collectStaticEntities(std::vector<Ogre::Entity *> &out,
|
||||
return;
|
||||
if (!e.has<RenderableComponent>())
|
||||
return;
|
||||
// F7: doors are not obstacles; the doorway gets
|
||||
// an area cost instead (locked doors become
|
||||
// dynamic obstacles, see syncDoorObstacles)
|
||||
if (e.has<DoorComponent>())
|
||||
return;
|
||||
auto &rend = e.get<RenderableComponent>();
|
||||
if (rend.entity) {
|
||||
addExisting(rend.entity);
|
||||
@@ -326,6 +359,9 @@ void NavMeshSystem::collectStaticEntities(std::vector<Ogre::Entity *> &out,
|
||||
NavMeshGeometrySource &nmg) {
|
||||
if (!nmg.include)
|
||||
return;
|
||||
// F7: doors are not obstacles (see path 1)
|
||||
if (e.has<DoorComponent>())
|
||||
return;
|
||||
Ogre::String meshName;
|
||||
if (e.has<RenderableComponent>()) {
|
||||
auto &rend = e.get<RenderableComponent>();
|
||||
@@ -515,6 +551,113 @@ Ogre::AxisAlignedBox NavMeshSystem::getEntityBounds(flecs::entity e)
|
||||
return worldBB;
|
||||
}
|
||||
|
||||
Ogre::AxisAlignedBox NavMeshSystem::getDoorVolume(flecs::entity doorEntity)
|
||||
{
|
||||
if (!doorEntity.has<DoorComponent>() ||
|
||||
!doorEntity.has<TransformComponent>())
|
||||
return Ogre::AxisAlignedBox::EXTENT_NULL;
|
||||
auto &door = doorEntity.get<DoorComponent>();
|
||||
auto &trans = doorEntity.get<TransformComponent>();
|
||||
if (!trans.node)
|
||||
return Ogre::AxisAlignedBox::EXTENT_NULL;
|
||||
|
||||
/* Closed-pose world transform of the hinge: the live node
|
||||
* transform includes the current swing angle, so rebuild it
|
||||
* from the parent's derived transform and the stored closed
|
||||
* orientation. */
|
||||
Ogre::Quaternion parentRot = Ogre::Quaternion::IDENTITY;
|
||||
Ogre::Vector3 parentScale = Ogre::Vector3::UNIT_SCALE;
|
||||
Ogre::Node *parent = trans.node->getParent();
|
||||
if (parent) {
|
||||
parentRot = parent->_getDerivedOrientation();
|
||||
parentScale = parent->_getDerivedScale();
|
||||
}
|
||||
Ogre::Vector3 hingePos = trans.node->_getDerivedPosition();
|
||||
Ogre::Quaternion closedRot = parentRot * door.closedOrientation;
|
||||
Ogre::Vector3 scale = parentScale * trans.node->getScale();
|
||||
|
||||
/* The door's box-collider child carries the closed-pose leaf
|
||||
* shape exactly (offset = DoorComponent::centerOffset). */
|
||||
Ogre::Vector3 halfExtents(0.5f, 1.0f, 0.1f);
|
||||
bool found = false;
|
||||
doorEntity.children([&](flecs::entity child) {
|
||||
if (found || !child.has<PhysicsColliderComponent>())
|
||||
return;
|
||||
halfExtents = child.get<PhysicsColliderComponent>().parameters;
|
||||
found = true;
|
||||
});
|
||||
halfExtents.x *= Ogre::Math::Abs(scale.x);
|
||||
halfExtents.y *= Ogre::Math::Abs(scale.y);
|
||||
halfExtents.z *= Ogre::Math::Abs(scale.z);
|
||||
|
||||
Ogre::Vector3 center =
|
||||
hingePos + closedRot * (scale * door.centerOffset);
|
||||
|
||||
/* Pad XZ so the marked area survives agent-radius erosion at
|
||||
* the doorway edges, and Y so the floor spans under the leaf
|
||||
* are caught. */
|
||||
Ogre::Vector3 pad(0.3f, 0.3f, 0.3f);
|
||||
return Ogre::AxisAlignedBox(center - halfExtents - pad,
|
||||
center + halfExtents + pad);
|
||||
}
|
||||
|
||||
std::vector<Ogre::AxisAlignedBox> NavMeshSystem::collectDoorVolumes()
|
||||
{
|
||||
std::vector<Ogre::AxisAlignedBox> out;
|
||||
m_world.query<DoorComponent, TransformComponent>().each(
|
||||
[&](flecs::entity de, DoorComponent &, TransformComponent &) {
|
||||
Ogre::AxisAlignedBox vol = getDoorVolume(de);
|
||||
if (!vol.isNull())
|
||||
out.push_back(vol);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
void NavMeshSystem::syncDoorObstacles(NavMeshState &state)
|
||||
{
|
||||
if (!state.navmesh || !state.navmesh->isBuilt())
|
||||
return;
|
||||
|
||||
/* Drop obstacles whose door entity is gone (grid rebuilds
|
||||
* destroy and respawn door entities). */
|
||||
for (auto it = state.doorObstacles.begin();
|
||||
it != state.doorObstacles.end();) {
|
||||
if (!m_world.entity(it->first).is_alive()) {
|
||||
state.navmesh->removeObstacle(it->second);
|
||||
it = state.doorObstacles.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
m_world.query<DoorComponent, TransformComponent>().each(
|
||||
[&](flecs::entity de, DoorComponent &door,
|
||||
TransformComponent &) {
|
||||
auto it = state.doorObstacles.find(de.id());
|
||||
bool locked = DoorSystem::isDoorLocked(door);
|
||||
if (locked && it == state.doorObstacles.end()) {
|
||||
Ogre::AxisAlignedBox vol = getDoorVolume(de);
|
||||
if (vol.isNull())
|
||||
return;
|
||||
uint32_t ref = state.navmesh->addObstacle(vol);
|
||||
if (ref)
|
||||
state.doorObstacles[de.id()] = ref;
|
||||
} else if (!locked && it != state.doorObstacles.end()) {
|
||||
state.navmesh->removeObstacle(it->second);
|
||||
state.doorObstacles.erase(it);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
unsigned char NavMeshSystem::getPolyAreaAt(flecs::entity navmeshEntity,
|
||||
const Ogre::Vector3 &pos)
|
||||
{
|
||||
auto it = m_states.find(navmeshEntity.id());
|
||||
if (it == m_states.end() || !it->second.navmesh)
|
||||
return 0xFF;
|
||||
return it->second.navmesh->getAreaAt(pos);
|
||||
}
|
||||
|
||||
bool NavMeshSystem::findPath(flecs::entity navmeshEntity, Ogre::Vector3 start,
|
||||
Ogre::Vector3 end,
|
||||
std::vector<Ogre::Vector3> &path)
|
||||
|
||||
@@ -49,6 +49,12 @@ public:
|
||||
// --- CellGrid integration ---
|
||||
void setCellGridSystem(CellGridSystem *system);
|
||||
|
||||
/* Area id of the nearest navmesh poly to pos (F7: door areas
|
||||
* report TileCacheNavMesh::EDITSCENE_AREA_DOOR); 0xFF when no
|
||||
* navmesh/poly. Debug/testing aid. */
|
||||
unsigned char getPolyAreaAt(flecs::entity navmeshEntity,
|
||||
const Ogre::Vector3 &pos);
|
||||
|
||||
private:
|
||||
struct NavMeshState {
|
||||
std::unique_ptr<TileCacheNavMesh> navmesh;
|
||||
@@ -56,6 +62,9 @@ private:
|
||||
bool firstBuild = true;
|
||||
// Track transform versions for rebuild
|
||||
std::unordered_map<flecs::entity_t, unsigned int> lastTransformVersions;
|
||||
// F7: tile-cache obstacle refs for locked doors, keyed by
|
||||
// door entity id
|
||||
std::unordered_map<flecs::entity_t, uint32_t> doorObstacles;
|
||||
};
|
||||
|
||||
flecs::world &m_world;
|
||||
@@ -76,6 +85,13 @@ private:
|
||||
const Ogre::Quaternion &rot,
|
||||
const Ogre::Vector3 &scale);
|
||||
Ogre::AxisAlignedBox getEntityBounds(flecs::entity e);
|
||||
|
||||
// F7: door volumes (closed-pose doorway bounds, render space)
|
||||
// paint the door area id during tile rasterization; locked doors
|
||||
// additionally get a tile-cache obstacle that blocks pathing.
|
||||
Ogre::AxisAlignedBox getDoorVolume(flecs::entity door);
|
||||
std::vector<Ogre::AxisAlignedBox> collectDoorVolumes();
|
||||
void syncDoorObstacles(NavMeshState &state);
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_NAVMESH_SYSTEM_HPP
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
#include "PathFollowingSystem.hpp"
|
||||
#include "AnimationTreeSystem.hpp"
|
||||
#include "NavMeshSystem.hpp"
|
||||
#include "DoorSystem.hpp"
|
||||
#include "CharacterRegistry.hpp"
|
||||
#include "../components/PathFollowing.hpp"
|
||||
#include "../components/Character.hpp"
|
||||
#include "../components/CharacterIdentity.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
PathFollowingSystem::PathFollowingSystem(flecs::world &world,
|
||||
@@ -75,6 +78,59 @@ void PathFollowingSystem::rotateTowards(flecs::entity e,
|
||||
trans.rotation = Ogre::Quaternion(yaw, Ogre::Vector3::UNIT_Y);
|
||||
}
|
||||
|
||||
bool PathFollowingSystem::handleDoorAhead(const Ogre::Vector3 &charPos,
|
||||
const Ogre::Vector3 &waypointPos)
|
||||
{
|
||||
bool wait = false;
|
||||
m_world.query<DoorComponent, TransformComponent>().each(
|
||||
[&](flecs::entity, DoorComponent &door,
|
||||
TransformComponent &dtrans) {
|
||||
/* Scene-switch doors belong to the player. */
|
||||
if (!door.sceneSwitchPath.empty())
|
||||
return;
|
||||
/* Locked doors are navmesh obstacles; paths
|
||||
* already avoid them. */
|
||||
if (DoorSystem::isDoorLocked(door))
|
||||
return;
|
||||
if (!dtrans.node)
|
||||
return;
|
||||
|
||||
Ogre::Vector3 center =
|
||||
dtrans.node->_getDerivedPosition() +
|
||||
dtrans.node->_getDerivedOrientation() *
|
||||
door.centerOffset;
|
||||
|
||||
/* Distance from the XZ segment charPos->waypoint
|
||||
* to the door centre. */
|
||||
Ogre::Vector2 a(charPos.x, charPos.z);
|
||||
Ogre::Vector2 b(waypointPos.x, waypointPos.z);
|
||||
Ogre::Vector2 p(center.x, center.z);
|
||||
Ogre::Vector2 ab = b - a;
|
||||
float t = 0.0f;
|
||||
float abLen2 = ab.squaredLength();
|
||||
if (abLen2 > 1e-6f) {
|
||||
t = (p - a).dotProduct(ab) / abLen2;
|
||||
t = std::max(0.0f, std::min(1.0f, t));
|
||||
}
|
||||
float segDist = (a + ab * t - p).length();
|
||||
float charDist = (p - a).length();
|
||||
|
||||
if (segDist > 1.2f || charDist > 2.5f)
|
||||
return;
|
||||
|
||||
if (door.currentAngle < 40.0f) {
|
||||
/* Closed or still swinging: request the
|
||||
* toggle once and hold position until
|
||||
* the leaf clears the doorway. */
|
||||
if (door.currentAngle < 5.0f &&
|
||||
!door.toggleRequested && !door.isOpen)
|
||||
door.toggleRequested = true;
|
||||
wait = true;
|
||||
}
|
||||
});
|
||||
return wait;
|
||||
}
|
||||
|
||||
void PathFollowingSystem::applyLocomotionState(flecs::entity e)
|
||||
{
|
||||
if (!e.has<PathFollowingComponent>() || !m_animTreeSystem)
|
||||
@@ -188,6 +244,16 @@ void PathFollowingSystem::update(float deltaTime)
|
||||
toTarget = waypointPos - charPos;
|
||||
}
|
||||
|
||||
/* F7: hold while a door ahead on the path is
|
||||
* closed or still swinging open. */
|
||||
if (handleDoorAhead(charPos, waypointPos)) {
|
||||
if (!cc.useRootMotion)
|
||||
cc.linearVelocity =
|
||||
Ogre::Vector3::ZERO;
|
||||
applyLocomotionState(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Move toward waypoint
|
||||
toTarget.y = 0;
|
||||
if (toTarget.squaredLength() > 0.0001f) {
|
||||
|
||||
@@ -35,6 +35,13 @@ private:
|
||||
Ogre::Vector3 getEntityPosition(flecs::entity e);
|
||||
void rotateTowards(flecs::entity e, const Ogre::Vector3 &direction,
|
||||
float deltaTime);
|
||||
/* F7: request a toggle on a closed, unlocked door whose doorway
|
||||
* the segment charPos->waypointPos is about to cross; returns
|
||||
* true while the character should hold position (door closed or
|
||||
* still swinging). Locked doors need no handling - they are
|
||||
* navmesh obstacles (NavMeshSystem::syncDoorObstacles). */
|
||||
bool handleDoorAhead(const Ogre::Vector3 &charPos,
|
||||
const Ogre::Vector3 &waypointPos);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "SaveLoadSystem.hpp"
|
||||
#include "../ProjectConfig.hpp"
|
||||
#include <OgreLogManager.h>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
@@ -6,6 +7,22 @@
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
/* ===================================================================== */
|
||||
/* Application name (per-project save directory, F8) */
|
||||
/* ===================================================================== */
|
||||
|
||||
static std::string s_appName = "World2";
|
||||
|
||||
void SaveLoadSystem::setAppName(const std::string &appName)
|
||||
{
|
||||
s_appName = sanitizeAppName(appName);
|
||||
}
|
||||
|
||||
const std::string &SaveLoadSystem::getAppName()
|
||||
{
|
||||
return s_appName;
|
||||
}
|
||||
|
||||
/* ===================================================================== */
|
||||
/* OS-dependent save directory */
|
||||
/* ===================================================================== */
|
||||
@@ -17,24 +34,25 @@ std::string SaveLoadSystem::getSaveDirectory()
|
||||
#ifdef _WIN32
|
||||
const char *appdata = getenv("APPDATA");
|
||||
if (appdata)
|
||||
dir = std::string(appdata) + "/World2/saves/";
|
||||
dir = std::string(appdata) + "/" + s_appName + "/saves/";
|
||||
else
|
||||
dir = "./saves/";
|
||||
#elif defined(__APPLE__)
|
||||
const char *home = getenv("HOME");
|
||||
if (home)
|
||||
dir = std::string(home) +
|
||||
"/Library/Application Support/World2/saves/";
|
||||
dir = std::string(home) + "/Library/Application Support/" +
|
||||
s_appName + "/saves/";
|
||||
else
|
||||
dir = "./saves/";
|
||||
#else /* Linux */
|
||||
const char *xdgDataHome = getenv("XDG_DATA_HOME");
|
||||
if (xdgDataHome)
|
||||
dir = std::string(xdgDataHome) + "/World2/saves/";
|
||||
dir = std::string(xdgDataHome) + "/" + s_appName + "/saves/";
|
||||
else {
|
||||
const char *home = getenv("HOME");
|
||||
if (home)
|
||||
dir = std::string(home) + "/.local/share/World2/saves/";
|
||||
dir = std::string(home) + "/.local/share/" + s_appName +
|
||||
"/saves/";
|
||||
else
|
||||
dir = "./saves/";
|
||||
}
|
||||
|
||||
@@ -10,9 +10,13 @@
|
||||
* Save/load system for game-mode saves.
|
||||
*
|
||||
* Manages save slots in an OS-dependent user data directory:
|
||||
* Linux: ~/.local/share/World2/saves/
|
||||
* Windows: %APPDATA%/World2/saves/
|
||||
* macOS: ~/Library/Application Support/World2/saves/
|
||||
* Linux: ~/.local/share/<appName>/saves/
|
||||
* Windows: %APPDATA%/<appName>/saves/
|
||||
* macOS: ~/Library/Application Support/<appName>/saves/
|
||||
*
|
||||
* <appName> defaults to "World2" (backwards compatible) and is replaced
|
||||
* by the per-project appName when a project directory is opened (F8, see
|
||||
* ProjectConfig) via setAppName().
|
||||
*
|
||||
* Each slot is a single JSON file containing:
|
||||
* - Save metadata (base scene, timestamp, play time, slot name)
|
||||
@@ -33,6 +37,18 @@ public:
|
||||
std::string baseScene;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the application name used for the save directory
|
||||
* (<user-data>/<appName>/saves/). Called when a project directory
|
||||
* with its own appName is opened; defaults to "World2".
|
||||
*/
|
||||
static void setAppName(const std::string &appName);
|
||||
|
||||
/**
|
||||
* Get the application name used for the save directory.
|
||||
*/
|
||||
static const std::string &getAppName();
|
||||
|
||||
/**
|
||||
* Get the OS-dependent save directory.
|
||||
* Creates the directory if it doesn't exist.
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
#include "../components/Skybox.hpp"
|
||||
#include "../components/EventHandler.hpp"
|
||||
#include "../components/SceneScript.hpp"
|
||||
#include "../components/StandaloneDoor.hpp"
|
||||
#include "../components/ActionDatabase.hpp"
|
||||
#include "../components/ActionDebug.hpp"
|
||||
#include "../components/SmartObject.hpp"
|
||||
@@ -410,6 +411,9 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
|
||||
if (entity.has<SceneScriptComponent>()) {
|
||||
json["sceneScript"] = serializeSceneScript(entity);
|
||||
}
|
||||
if (entity.has<StandaloneDoorComponent>()) {
|
||||
json["standaloneDoor"] = serializeStandaloneDoor(entity);
|
||||
}
|
||||
if (entity.has<GoapPlannerComponent>()) {
|
||||
json["goapPlanner"] = serializeGoapPlanner(entity);
|
||||
}
|
||||
@@ -652,6 +656,9 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
|
||||
if (json.contains("sceneScript")) {
|
||||
deserializeSceneScript(entity, json["sceneScript"]);
|
||||
}
|
||||
if (json.contains("standaloneDoor")) {
|
||||
deserializeStandaloneDoor(entity, json["standaloneDoor"]);
|
||||
}
|
||||
if (json.contains("goapPlanner")) {
|
||||
deserializeGoapPlanner(entity, json["goapPlanner"]);
|
||||
}
|
||||
@@ -967,6 +974,9 @@ void SceneSerializer::deserializeEntityComponents(
|
||||
if (json.contains("sceneScript")) {
|
||||
deserializeSceneScript(entity, json["sceneScript"]);
|
||||
}
|
||||
if (json.contains("standaloneDoor")) {
|
||||
deserializeStandaloneDoor(entity, json["standaloneDoor"]);
|
||||
}
|
||||
if (json.contains("goapPlanner")) {
|
||||
deserializeGoapPlanner(entity, json["goapPlanner"]);
|
||||
}
|
||||
@@ -2580,6 +2590,7 @@ nlohmann::json SceneSerializer::serializeCellGrid(flecs::entity entity)
|
||||
json["cellSize"] = grid.cellSize;
|
||||
json["cellHeight"] = grid.cellHeight;
|
||||
json["generationScript"] = grid.generationScript;
|
||||
json["generationMode"] = grid.generationMode;
|
||||
|
||||
// Serialize texture rectangles
|
||||
json["floorRectName"] = grid.floorRectName;
|
||||
@@ -2601,10 +2612,43 @@ nlohmann::json SceneSerializer::serializeCellGrid(flecs::entity entity)
|
||||
json["doorUseMeshMaterial"] = grid.doorUseMeshMaterial;
|
||||
json["doorOpenAngle"] = grid.doorOpenAngle;
|
||||
json["doorOpenSpeed"] = grid.doorOpenSpeed;
|
||||
json["doorSwingReversed"] = grid.doorSwingReversed;
|
||||
json["doorActionName"] = grid.doorActionName;
|
||||
json["doorSceneSwitchPath"] = grid.doorSceneSwitchPath;
|
||||
json["doorSceneSwitchTarget"] = grid.doorSceneSwitchTarget;
|
||||
|
||||
// F5: window glass (exteriorOnly)
|
||||
json["glassColor"] = { grid.glassColor.r, grid.glassColor.g,
|
||||
grid.glassColor.b, grid.glassColor.a };
|
||||
json["glassMaterialName"] = grid.glassMaterialName;
|
||||
json["glassReflectivity"] = grid.glassReflectivity;
|
||||
|
||||
// F0: grid identity + per-doorway configuration overrides.
|
||||
// The uid is generated lazily on first save when missing (e.g.
|
||||
// components created from Lua).
|
||||
json["gridUid"] =
|
||||
const_cast<CellGridComponent &>(grid).ensureGridUid();
|
||||
nlohmann::json doorConfigsJson = nlohmann::json::object();
|
||||
for (const auto &pair : grid.doorConfigs) {
|
||||
const CellGridDoorConfig &cfg = pair.second;
|
||||
nlohmann::json cj;
|
||||
cj["label"] = cfg.label;
|
||||
cj["hasOverride"] = cfg.hasOverride;
|
||||
cj["openAngle"] = cfg.openAngle;
|
||||
cj["openSpeed"] = cfg.openSpeed;
|
||||
cj["swingReversed"] = cfg.swingReversed;
|
||||
cj["actionName"] = cfg.actionName;
|
||||
cj["sceneSwitchPath"] = cfg.sceneSwitchPath;
|
||||
cj["sceneSwitchTarget"] = cfg.sceneSwitchTarget;
|
||||
cj["disabled"] = cfg.disabled;
|
||||
cj["persistent"] = cfg.persistent;
|
||||
cj["lockable"] = cfg.lockable;
|
||||
cj["lockedByDefault"] = cfg.lockedByDefault;
|
||||
cj["keyItemId"] = cfg.keyItemId;
|
||||
doorConfigsJson[pair.first] = cj;
|
||||
}
|
||||
json["doorConfigs"] = doorConfigsJson;
|
||||
|
||||
// Serialize cells
|
||||
nlohmann::json cellsJson = nlohmann::json::array();
|
||||
for (const auto &cell : grid.cells) {
|
||||
@@ -2793,6 +2837,7 @@ void SceneSerializer::deserializeCellGrid(flecs::entity entity,
|
||||
grid.cellSize = json.value("cellSize", 4.0f);
|
||||
grid.cellHeight = json.value("cellHeight", 4.0f);
|
||||
grid.generationScript = json.value("generationScript", "");
|
||||
grid.generationMode = json.value("generationMode", "full");
|
||||
|
||||
// Deserialize texture rectangles
|
||||
grid.floorRectName = json.value("floorRectName", "");
|
||||
@@ -2814,10 +2859,46 @@ void SceneSerializer::deserializeCellGrid(flecs::entity entity,
|
||||
grid.doorUseMeshMaterial = json.value("doorUseMeshMaterial", false);
|
||||
grid.doorOpenAngle = json.value("doorOpenAngle", 100.0f);
|
||||
grid.doorOpenSpeed = json.value("doorOpenSpeed", 180.0f);
|
||||
grid.doorSwingReversed = json.value("doorSwingReversed", false);
|
||||
grid.doorActionName = json.value("doorActionName", "");
|
||||
grid.doorSceneSwitchPath = json.value("doorSceneSwitchPath", "");
|
||||
grid.doorSceneSwitchTarget = json.value("doorSceneSwitchTarget", "");
|
||||
|
||||
// F5: window glass (exteriorOnly)
|
||||
if (json.contains("glassColor") && json["glassColor"].is_array() &&
|
||||
json["glassColor"].size() >= 4) {
|
||||
grid.glassColor = Ogre::ColourValue(json["glassColor"][0],
|
||||
json["glassColor"][1],
|
||||
json["glassColor"][2],
|
||||
json["glassColor"][3]);
|
||||
}
|
||||
grid.glassMaterialName = json.value("glassMaterialName", "");
|
||||
grid.glassReflectivity = json.value("glassReflectivity", 0.8f);
|
||||
|
||||
// F0: grid identity + per-doorway configuration overrides. Old
|
||||
// scenes have no gridUid - a fresh one is generated below.
|
||||
grid.gridUid = json.value("gridUid", "");
|
||||
if (json.contains("doorConfigs") && json["doorConfigs"].is_object()) {
|
||||
for (auto &[key, cj] : json["doorConfigs"].items()) {
|
||||
CellGridDoorConfig cfg;
|
||||
cfg.label = cj.value("label", "");
|
||||
cfg.hasOverride = cj.value("hasOverride", false);
|
||||
cfg.openAngle = cj.value("openAngle", 0.0f);
|
||||
cfg.openSpeed = cj.value("openSpeed", 0.0f);
|
||||
cfg.swingReversed = cj.value("swingReversed", false);
|
||||
cfg.actionName = cj.value("actionName", "");
|
||||
cfg.sceneSwitchPath = cj.value("sceneSwitchPath", "");
|
||||
cfg.sceneSwitchTarget = cj.value("sceneSwitchTarget", "");
|
||||
cfg.disabled = cj.value("disabled", false);
|
||||
cfg.persistent = cj.value("persistent", false);
|
||||
cfg.lockable = cj.value("lockable", false);
|
||||
cfg.lockedByDefault = cj.value("lockedByDefault", false);
|
||||
cfg.keyItemId = cj.value("keyItemId", "");
|
||||
grid.doorConfigs[key] = cfg;
|
||||
}
|
||||
}
|
||||
grid.ensureGridUid();
|
||||
|
||||
// Deserialize cells
|
||||
if (json.contains("cells") && json["cells"].is_array()) {
|
||||
for (const auto &cellJson : json["cells"]) {
|
||||
@@ -4086,6 +4167,7 @@ nlohmann::json SceneSerializer::serializeNavMesh(flecs::entity entity)
|
||||
json["regionMinSize"] = nm.regionMinSize;
|
||||
json["regionMergeSize"] = nm.regionMergeSize;
|
||||
json["tileSize"] = nm.tileSize;
|
||||
json["doorAreaCost"] = nm.doorAreaCost;
|
||||
json["enabled"] = nm.enabled;
|
||||
json["debugDraw"] = nm.debugDraw;
|
||||
return json;
|
||||
@@ -4106,6 +4188,7 @@ void SceneSerializer::deserializeNavMesh(flecs::entity entity,
|
||||
nm.regionMinSize = json.value("regionMinSize", 50.0f);
|
||||
nm.regionMergeSize = json.value("regionMergeSize", 20.0f);
|
||||
nm.tileSize = json.value("tileSize", 48);
|
||||
nm.doorAreaCost = json.value("doorAreaCost", 5.0f);
|
||||
nm.enabled = json.value("enabled", true);
|
||||
nm.debugDraw = json.value("debugDraw", false);
|
||||
nm.dirty = true;
|
||||
@@ -4194,6 +4277,56 @@ void SceneSerializer::deserializeSceneScript(flecs::entity entity,
|
||||
entity.set<SceneScriptComponent>(script);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeStandaloneDoor(flecs::entity entity)
|
||||
{
|
||||
const StandaloneDoorComponent &door =
|
||||
entity.get<StandaloneDoorComponent>();
|
||||
nlohmann::json json;
|
||||
json["meshName"] = door.meshName;
|
||||
json["useMeshMaterial"] = door.useMeshMaterial;
|
||||
json["rectName"] = door.rectName;
|
||||
json["leafWidth"] = door.leafWidth;
|
||||
json["leafHeight"] = door.leafHeight;
|
||||
json["leafThickness"] = door.leafThickness;
|
||||
json["openAngle"] = door.openAngle;
|
||||
json["openSpeed"] = door.openSpeed;
|
||||
json["swingReversed"] = door.swingReversed;
|
||||
json["actionName"] = door.actionName;
|
||||
json["sceneSwitchPath"] = door.sceneSwitchPath;
|
||||
json["sceneSwitchTarget"] = door.sceneSwitchTarget;
|
||||
json["persistent"] = door.persistent;
|
||||
json["lockable"] = door.lockable;
|
||||
json["lockedByDefault"] = door.lockedByDefault;
|
||||
json["keyItemId"] = door.keyItemId;
|
||||
json["doorId"] = door.doorId;
|
||||
return json;
|
||||
}
|
||||
|
||||
void SceneSerializer::deserializeStandaloneDoor(flecs::entity entity,
|
||||
const nlohmann::json &json)
|
||||
{
|
||||
StandaloneDoorComponent door;
|
||||
door.meshName = json.value("meshName", "");
|
||||
door.useMeshMaterial = json.value("useMeshMaterial", false);
|
||||
door.rectName = json.value("rectName", "");
|
||||
door.leafWidth = json.value("leafWidth", 1.0f);
|
||||
door.leafHeight = json.value("leafHeight", 2.0f);
|
||||
door.leafThickness = json.value("leafThickness", 0.08f);
|
||||
door.openAngle = json.value("openAngle", 100.0f);
|
||||
door.openSpeed = json.value("openSpeed", 180.0f);
|
||||
door.swingReversed = json.value("swingReversed", false);
|
||||
door.actionName = json.value("actionName", "");
|
||||
door.sceneSwitchPath = json.value("sceneSwitchPath", "");
|
||||
door.sceneSwitchTarget = json.value("sceneSwitchTarget", "");
|
||||
door.persistent = json.value("persistent", false);
|
||||
door.lockable = json.value("lockable", false);
|
||||
door.lockedByDefault = json.value("lockedByDefault", false);
|
||||
door.keyItemId = json.value("keyItemId", "");
|
||||
door.doorId = json.value("doorId", "");
|
||||
// dirty stays true so StandaloneDoorSystem builds the door
|
||||
entity.set<StandaloneDoorComponent>(door);
|
||||
}
|
||||
|
||||
nlohmann::json SceneSerializer::serializeItem(flecs::entity entity)
|
||||
{
|
||||
const ItemComponent &item = entity.get<ItemComponent>();
|
||||
|
||||
@@ -278,6 +278,7 @@ private:
|
||||
nlohmann::json serializeActuator(flecs::entity entity);
|
||||
nlohmann::json serializeEventHandler(flecs::entity entity);
|
||||
nlohmann::json serializeSceneScript(flecs::entity entity);
|
||||
nlohmann::json serializeStandaloneDoor(flecs::entity entity);
|
||||
nlohmann::json serializeGoapPlanner(flecs::entity entity);
|
||||
nlohmann::json serializeGoapRunner(flecs::entity entity);
|
||||
nlohmann::json serializeBehaviorTree(flecs::entity entity);
|
||||
@@ -294,6 +295,8 @@ private:
|
||||
const nlohmann::json &json);
|
||||
void deserializeSceneScript(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeStandaloneDoor(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeGoapPlanner(flecs::entity entity,
|
||||
const nlohmann::json &json);
|
||||
void deserializeGoapRunner(flecs::entity entity,
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
#include "StandaloneDoorSystem.hpp"
|
||||
#include "DoorBuilder.hpp"
|
||||
#include "../components/StandaloneDoor.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../components/ProceduralMaterial.hpp"
|
||||
#include "../components/ProceduralTexture.hpp"
|
||||
#include <OgreSceneManager.h>
|
||||
#include <OgreSceneNode.h>
|
||||
#include <OgreMeshManager.h>
|
||||
#include <OgreLogManager.h>
|
||||
#include <ProceduralTriangleBuffer.h>
|
||||
#include <ProceduralBoxGenerator.h>
|
||||
#include <random>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
/* Random UUID-like hex string (8-4-4-4-12), same scheme as
|
||||
* CellGridComponent::ensureGridUid(). */
|
||||
std::string generateDoorUid()
|
||||
{
|
||||
std::random_device rd;
|
||||
std::mt19937_64 gen(((uint64_t)rd() << 32) ^ (uint64_t)rd());
|
||||
uint64_t a = gen(), b = gen();
|
||||
char buf[40];
|
||||
snprintf(buf, sizeof(buf), "%08x-%04x-%04x-%04x-%04x%08x",
|
||||
(unsigned)(a & 0xffffffffu),
|
||||
(unsigned)((a >> 32) & 0xffffu),
|
||||
(unsigned)(((a >> 48) & 0x0fffu) | 0x4000u),
|
||||
(unsigned)((b & 0x3fffu) | 0x8000u),
|
||||
(unsigned)((b >> 16) & 0xffffu),
|
||||
(unsigned)((b >> 32) & 0xffffffffu));
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
StandaloneDoorSystem::StandaloneDoorSystem(flecs::world &world,
|
||||
Ogre::SceneManager *sceneMgr)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
{
|
||||
}
|
||||
|
||||
void StandaloneDoorSystem::destroyBuilt(BuiltDoor &built)
|
||||
{
|
||||
flecs::entity doorEntity = m_world.entity(built.doorEntity);
|
||||
if (doorEntity.is_alive())
|
||||
doorEntity.destruct();
|
||||
if (built.ownsLeafMesh && !built.leafMeshName.empty()) {
|
||||
auto &meshMgr = Ogre::MeshManager::getSingleton();
|
||||
if (meshMgr.resourceExists(built.leafMeshName))
|
||||
meshMgr.remove(built.leafMeshName);
|
||||
}
|
||||
built = BuiltDoor();
|
||||
}
|
||||
|
||||
void StandaloneDoorSystem::cleanupDead()
|
||||
{
|
||||
for (auto it = m_built.begin(); it != m_built.end();) {
|
||||
if (!m_world.entity(it->first).is_alive()) {
|
||||
destroyBuilt(it->second);
|
||||
it = m_built.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StandaloneDoorSystem::rebuild(flecs::entity entity,
|
||||
StandaloneDoorComponent &door)
|
||||
{
|
||||
BuiltDoor &built = m_built[entity.id()];
|
||||
destroyBuilt(built);
|
||||
|
||||
if (!entity.has<TransformComponent>())
|
||||
return;
|
||||
auto &transform = entity.get<TransformComponent>();
|
||||
if (!transform.node)
|
||||
return;
|
||||
|
||||
/* Doors that need a persistent ID but have none get a generated
|
||||
* UUID, written back so it stays stable from then on. */
|
||||
if (door.doorId.empty() &&
|
||||
(door.persistent || door.lockable ||
|
||||
!door.sceneSwitchPath.empty())) {
|
||||
door.doorId = generateDoorUid();
|
||||
}
|
||||
|
||||
// Resolve the leaf mesh: custom mesh or procedural box leaf.
|
||||
DoorBuildParams params;
|
||||
params.openAngle = door.openAngle;
|
||||
params.openSpeed = door.openSpeed;
|
||||
params.swingReversed = door.swingReversed;
|
||||
params.actionName = door.actionName;
|
||||
params.sceneSwitchPath = door.sceneSwitchPath;
|
||||
params.sceneSwitchTarget = door.sceneSwitchTarget;
|
||||
params.persistent = door.persistent;
|
||||
params.lockable = door.lockable;
|
||||
params.lockedByDefault = door.lockedByDefault;
|
||||
params.keyItemId = door.keyItemId;
|
||||
params.doorId = door.doorId;
|
||||
params.leafWidth = door.leafWidth;
|
||||
params.leafHeight = door.leafHeight;
|
||||
params.leafThickness = door.leafThickness;
|
||||
|
||||
// The entity's own procedural material (if any) skins the
|
||||
// procedural leaf; rectName picks the UV rect of its texture atlas.
|
||||
if (entity.has<ProceduralMaterialComponent>()) {
|
||||
params.materialName =
|
||||
entity.get<ProceduralMaterialComponent>().materialName;
|
||||
}
|
||||
|
||||
if (!door.meshName.empty()) {
|
||||
auto &meshMgr = Ogre::MeshManager::getSingleton();
|
||||
Ogre::MeshPtr customMesh = meshMgr.getByName(door.meshName);
|
||||
if (!customMesh) {
|
||||
Ogre::String group = Ogre::ResourceGroupManager::
|
||||
getSingleton()
|
||||
.findGroupContainingResource(
|
||||
door.meshName);
|
||||
if (!group.empty())
|
||||
customMesh = meshMgr.load(door.meshName, group);
|
||||
}
|
||||
if (customMesh) {
|
||||
params.leafMeshName = door.meshName;
|
||||
params.customMesh = true;
|
||||
params.useMeshMaterial = door.useMeshMaterial;
|
||||
params.customBounds = customMesh->getBounds();
|
||||
} else {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"StandaloneDoor: Could not load door mesh '" +
|
||||
door.meshName +
|
||||
"', falling back to procedural door");
|
||||
}
|
||||
}
|
||||
|
||||
if (params.leafMeshName.empty()) {
|
||||
/* Procedural box leaf, hinge edge at the origin, spanning
|
||||
* +X/+Y (same convention as CellGrid doors). */
|
||||
std::string leafMeshName =
|
||||
"StandaloneDoor_" + std::to_string(entity.id()) +
|
||||
"_leaf";
|
||||
auto &meshMgr = Ogre::MeshManager::getSingleton();
|
||||
if (meshMgr.resourceExists(leafMeshName))
|
||||
meshMgr.remove(leafMeshName);
|
||||
|
||||
Procedural::TriangleBuffer leafTb;
|
||||
Procedural::BoxGenerator()
|
||||
.setSizeX(door.leafWidth)
|
||||
.setSizeY(door.leafHeight)
|
||||
.setSizeZ(door.leafThickness)
|
||||
.setNumSegX(1)
|
||||
.setNumSegY(2)
|
||||
.setNumSegZ(1)
|
||||
.setPosition(Ogre::Vector3(door.leafWidth / 2.0f,
|
||||
door.leafHeight / 2.0f, 0))
|
||||
.setEnableNormals(true)
|
||||
.addToTriangleBuffer(leafTb);
|
||||
|
||||
// UV rect mapping into the entity's own procedural
|
||||
// texture atlas (optional)
|
||||
if (!door.rectName.empty() &&
|
||||
entity.has<ProceduralTextureComponent>()) {
|
||||
const TextureRectInfo *rect =
|
||||
entity.get<ProceduralTextureComponent>()
|
||||
.getNamedRect(door.rectName);
|
||||
if (rect) {
|
||||
for (auto &v : leafTb.getVertices()) {
|
||||
v.mUV.x = rect->u1 +
|
||||
v.mUV.x * (rect->u2 - rect->u1);
|
||||
v.mUV.y = rect->v1 +
|
||||
v.mUV.y * (rect->v2 - rect->v1);
|
||||
}
|
||||
} else {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"StandaloneDoor: texture rect '" +
|
||||
door.rectName + "' not found");
|
||||
}
|
||||
}
|
||||
|
||||
Ogre::MeshPtr leafMesh = leafTb.transformToMesh(leafMeshName);
|
||||
if (!params.materialName.empty() &&
|
||||
leafMesh->getNumSubMeshes() > 0) {
|
||||
leafMesh->getSubMesh(0)->setMaterialName(
|
||||
params.materialName);
|
||||
}
|
||||
params.leafMeshName = leafMeshName;
|
||||
built.leafMeshName = leafMeshName;
|
||||
built.ownsLeafMesh = true;
|
||||
}
|
||||
|
||||
/* The entity's transform IS the doorway placement (center of the
|
||||
* opening at floor level, local +X along the wall). */
|
||||
flecs::entity doorEntity =
|
||||
DoorBuilder::build(m_world, m_sceneMgr, entity, transform.node,
|
||||
Ogre::Vector3::ZERO,
|
||||
Ogre::Quaternion::IDENTITY, params);
|
||||
if (!doorEntity.is_valid())
|
||||
return;
|
||||
built.doorEntity = doorEntity.id();
|
||||
}
|
||||
|
||||
void StandaloneDoorSystem::update(float deltaTime)
|
||||
{
|
||||
(void)deltaTime;
|
||||
cleanupDead();
|
||||
|
||||
m_world.query<StandaloneDoorComponent>().each(
|
||||
[&](flecs::entity entity, StandaloneDoorComponent &door) {
|
||||
if (!door.dirty)
|
||||
return;
|
||||
door.dirty = false;
|
||||
rebuild(entity, door);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#ifndef EDITSCENE_STANDALONE_DOOR_SYSTEM_HPP
|
||||
#define EDITSCENE_STANDALONE_DOOR_SYSTEM_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <Ogre.h>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
/**
|
||||
* Standalone door system (F2).
|
||||
*
|
||||
* Builds the runtime door subtree for entities with a serialized
|
||||
* StandaloneDoorComponent, using DoorBuilder (the same entity structure
|
||||
* as CellGrid doors: hinge node + leaf, collider child, actuator, F1
|
||||
* occluder for scene-switch doors). Rebuilds when the component's dirty
|
||||
* flag is set (editor changes); cleans up meshes/entities when the owning
|
||||
* entity disappears (polled, like CellGridSystem, because OnRemove
|
||||
* observer registration shifts entity IDs and breaks SceneSerializer's
|
||||
* hardcoded ID resolution).
|
||||
*
|
||||
* The leaf is a custom mesh (meshName, hinge edge at the origin spanning
|
||||
* +X/+Y) or a procedural box of leafWidth x leafHeight x leafThickness;
|
||||
* the procedural leaf uses the entity's own ProceduralMaterialComponent
|
||||
* material when present, with rectName selecting the UV rect of the
|
||||
* entity's ProceduralTextureComponent atlas.
|
||||
*/
|
||||
class StandaloneDoorSystem {
|
||||
public:
|
||||
StandaloneDoorSystem(flecs::world &world, Ogre::SceneManager *sceneMgr);
|
||||
~StandaloneDoorSystem() = default;
|
||||
|
||||
void update(float deltaTime);
|
||||
|
||||
private:
|
||||
struct BuiltDoor {
|
||||
flecs::entity_t doorEntity = 0;
|
||||
std::string leafMeshName; // "" when a shared/custom mesh
|
||||
bool ownsLeafMesh = false;
|
||||
};
|
||||
|
||||
void rebuild(flecs::entity entity,
|
||||
struct StandaloneDoorComponent &door);
|
||||
void destroyBuilt(BuiltDoor &built);
|
||||
void cleanupDead();
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
std::unordered_map<flecs::entity_t, BuiltDoor> m_built;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_STANDALONE_DOOR_SYSTEM_HPP
|
||||
@@ -15,16 +15,25 @@
|
||||
#include "../components/TriangleBuffer.hpp"
|
||||
#include "../components/Renderable.hpp"
|
||||
#include "../components/NavMesh.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../components/RigidBody.hpp"
|
||||
#include "../components/Lod.hpp"
|
||||
#include "../components/PhysicsCollider.hpp"
|
||||
#include "../systems/SceneSerializer.hpp"
|
||||
#include "../systems/SpawnerRegionStore.hpp"
|
||||
#include "../systems/RoadRegionStore.hpp"
|
||||
#include "../systems/WorldMapData.hpp"
|
||||
#include "../systems/NavMeshSystem.hpp"
|
||||
#include "../systems/DoorSystem.hpp"
|
||||
#include "../systems/GlobalStateStore.hpp"
|
||||
#include "../recast/TileCacheNavMesh.hpp"
|
||||
#include "../ui/NavigationPanel.hpp"
|
||||
#include "../physics/physics.h"
|
||||
#include "../roadlib/RoadGeometryLib.hpp"
|
||||
|
||||
#include <ProceduralBoxGenerator.h>
|
||||
#include <OgreMeshManager.h>
|
||||
|
||||
#include <Jolt/Physics/PhysicsSystem.h>
|
||||
#include <Jolt/Physics/Body/BodyInterface.h>
|
||||
|
||||
@@ -5701,6 +5710,256 @@ bool TerrainTestRunner::testPrefabJsonCache(EditorApp &app,
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* testNavMeshDoors (F7: navigation vs doors) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/* Create (once) a centred box mesh plus an entity and node at pos. */
|
||||
static Ogre::Entity *navTestBox(EditorApp &app, const Ogre::String &name,
|
||||
const Ogre::Vector3 &size,
|
||||
const Ogre::Vector3 &pos,
|
||||
Ogre::SceneNode **outNode)
|
||||
{
|
||||
const Ogre::String group =
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME;
|
||||
if (Ogre::MeshManager::getSingleton()
|
||||
.getByName(name, group)
|
||||
.isNull()) {
|
||||
Procedural::BoxGenerator boxGen;
|
||||
boxGen.setSizeX(size.x).setSizeY(size.y).setSizeZ(size.z);
|
||||
boxGen.realizeMesh(name, group);
|
||||
}
|
||||
Ogre::SceneManager *sm = app.getSceneManager();
|
||||
Ogre::Entity *ent = sm->createEntity(name + "Ent", name);
|
||||
Ogre::SceneNode *node =
|
||||
sm->getRootSceneNode()->createChildSceneNode(name + "Node");
|
||||
node->setPosition(pos);
|
||||
node->attachObject(ent);
|
||||
if (outNode)
|
||||
*outNode = node;
|
||||
return ent;
|
||||
}
|
||||
|
||||
static flecs::entity navTestStaticEntity(flecs::world *w, Ogre::Entity *ent,
|
||||
Ogre::SceneNode *node)
|
||||
{
|
||||
flecs::entity e = w->entity();
|
||||
e.set<TransformComponent>(
|
||||
{ node, node->getPosition(), Ogre::Quaternion::IDENTITY,
|
||||
Ogre::Vector3::UNIT_SCALE });
|
||||
RigidBodyComponent rb;
|
||||
rb.bodyType = RigidBodyComponent::BodyType::Static;
|
||||
e.set<RigidBodyComponent>(rb);
|
||||
RenderableComponent rend;
|
||||
rend.entity = ent;
|
||||
rend.meshName = ent->getMesh()->getName();
|
||||
e.set<RenderableComponent>(rend);
|
||||
return e;
|
||||
}
|
||||
|
||||
static void navTestDestroyEntity(EditorApp &app, flecs::entity e)
|
||||
{
|
||||
Ogre::SceneManager *sm = app.getSceneManager();
|
||||
if (e.has<RenderableComponent>()) {
|
||||
Ogre::Entity *ent = e.get<RenderableComponent>().entity;
|
||||
if (ent) {
|
||||
Ogre::SceneNode *node = ent->getParentSceneNode();
|
||||
if (node)
|
||||
node->detachAllObjects();
|
||||
sm->destroyEntity(ent);
|
||||
}
|
||||
}
|
||||
if (e.has<TransformComponent>()) {
|
||||
Ogre::SceneNode *node = e.get<TransformComponent>().node;
|
||||
if (node) {
|
||||
node->detachAllObjects();
|
||||
sm->destroySceneNode(node);
|
||||
}
|
||||
}
|
||||
if (e.is_alive())
|
||||
e.destruct();
|
||||
}
|
||||
|
||||
bool TerrainTestRunner::testNavMeshDoors(EditorApp &app, TerrainSystem *ts)
|
||||
{
|
||||
(void)ts;
|
||||
NavMeshSystem *navSys = app.getNavMeshSystem();
|
||||
if (!navSys) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainTests: FAIL - no NavMeshSystem in app");
|
||||
return false;
|
||||
}
|
||||
|
||||
flecs::world *w = app.getWorld();
|
||||
Ogre::SceneManager *sm = app.getSceneManager();
|
||||
|
||||
/* Geometry: a flat 20x20 floor split by a wall at z=0 with a
|
||||
* doorway gap at x in [-1, 1] - the only passage between the
|
||||
* two halves. */
|
||||
Ogre::SceneNode *floorNode, *wallLNode, *wallRNode;
|
||||
Ogre::Entity *floorEnt =
|
||||
navTestBox(app, "NavTestFloor", Ogre::Vector3(20, 0.2f, 20),
|
||||
Ogre::Vector3(0, -0.1f, 0), &floorNode);
|
||||
Ogre::Entity *wallLEnt =
|
||||
navTestBox(app, "NavTestWallL", Ogre::Vector3(9, 3, 0.4f),
|
||||
Ogre::Vector3(-5.5f, 1.5f, 0), &wallLNode);
|
||||
Ogre::Entity *wallREnt =
|
||||
navTestBox(app, "NavTestWallR", Ogre::Vector3(9, 3, 0.4f),
|
||||
Ogre::Vector3(5.5f, 1.5f, 0), &wallRNode);
|
||||
|
||||
flecs::entity floorE = navTestStaticEntity(w, floorEnt, floorNode);
|
||||
flecs::entity wallLE = navTestStaticEntity(w, wallLEnt, wallLNode);
|
||||
flecs::entity wallRE = navTestStaticEntity(w, wallREnt, wallRNode);
|
||||
|
||||
/* Door: hinge node on the left doorway edge, leaf extends +X;
|
||||
* the box-collider child describes the closed-pose leaf shape
|
||||
* (same layout as DoorBuilder produces). The door carries a
|
||||
* static RigidBodyComponent + RenderableComponent, so it would
|
||||
* be collected as navmesh geometry without the F7 exclusion. */
|
||||
const Ogre::String group =
|
||||
Ogre::ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME;
|
||||
if (Ogre::MeshManager::getSingleton()
|
||||
.getByName("NavTestDoorLeaf", group)
|
||||
.isNull()) {
|
||||
Procedural::BoxGenerator boxGen;
|
||||
boxGen.setSizeX(2.0f).setSizeY(2.0f).setSizeZ(0.1f);
|
||||
boxGen.realizeMesh("NavTestDoorLeaf", group);
|
||||
}
|
||||
Ogre::Entity *leafEnt =
|
||||
sm->createEntity("NavTestDoorLeafEnt", "NavTestDoorLeaf");
|
||||
Ogre::SceneNode *hingeNode =
|
||||
sm->getRootSceneNode()->createChildSceneNode("NavTestDoorHinge");
|
||||
hingeNode->setPosition(Ogre::Vector3(-1.0f, 1.0f, 0.0f));
|
||||
Ogre::SceneNode *leafNode =
|
||||
hingeNode->createChildSceneNode("NavTestDoorLeafSwing");
|
||||
leafNode->setPosition(Ogre::Vector3(1.0f, 0.0f, 0.0f));
|
||||
leafNode->attachObject(leafEnt);
|
||||
|
||||
flecs::entity doorE = w->entity();
|
||||
doorE.set<TransformComponent>(
|
||||
{ hingeNode, hingeNode->getPosition(),
|
||||
Ogre::Quaternion::IDENTITY, Ogre::Vector3::UNIT_SCALE });
|
||||
RenderableComponent drend;
|
||||
drend.entity = leafEnt;
|
||||
drend.meshName = "NavTestDoorLeaf";
|
||||
doorE.set<RenderableComponent>(drend);
|
||||
RigidBodyComponent drb;
|
||||
drb.bodyType = RigidBodyComponent::BodyType::Static;
|
||||
doorE.set<RigidBodyComponent>(drb);
|
||||
DoorComponent door;
|
||||
door.doorId = "navtestdoor";
|
||||
door.persistent = true;
|
||||
door.lockable = true;
|
||||
door.centerOffset = Ogre::Vector3(1.0f, 0.0f, 0.0f);
|
||||
doorE.set<DoorComponent>(door);
|
||||
|
||||
flecs::entity colliderE = w->entity();
|
||||
colliderE.child_of(doorE);
|
||||
colliderE.set<TransformComponent>(
|
||||
{ nullptr, Ogre::Vector3::ZERO, Ogre::Quaternion::IDENTITY,
|
||||
Ogre::Vector3::UNIT_SCALE });
|
||||
PhysicsColliderComponent col;
|
||||
col.shapeType = PhysicsColliderComponent::ShapeType::Box;
|
||||
col.parameters = Ogre::Vector3(1.0f, 1.0f, 0.1f);
|
||||
col.offset = Ogre::Vector3(1.0f, 0.0f, 0.0f);
|
||||
colliderE.set<PhysicsColliderComponent>(col);
|
||||
|
||||
/* Navmesh manager entity. */
|
||||
flecs::entity navE = w->entity();
|
||||
{
|
||||
NavMeshComponent nm;
|
||||
nm.cellSize = 0.3f;
|
||||
nm.cellHeight = 0.2f;
|
||||
nm.agentHeight = 2.0f;
|
||||
nm.agentRadius = 0.4f;
|
||||
nm.agentMaxClimb = 0.5f;
|
||||
nm.agentMaxSlope = 30.0f;
|
||||
nm.tileSize = 48;
|
||||
nm.doorAreaCost = 5.0f;
|
||||
navE.set<NavMeshComponent>(nm);
|
||||
}
|
||||
navSys->rebuild(navE);
|
||||
|
||||
bool ok = true;
|
||||
const Ogre::Vector3 start(-5.0f, 0.5f, -5.0f);
|
||||
const Ogre::Vector3 target(5.0f, 0.5f, 5.0f);
|
||||
std::vector<Ogre::Vector3> path;
|
||||
|
||||
/* 1. A path through the closed door's doorway exists (doors
|
||||
* are not obstacles; the doorway floor is painted with the
|
||||
* door area instead). */
|
||||
bool found = navSys->findPath(navE, start, target, path);
|
||||
if (!found || path.empty() ||
|
||||
(path.back() - target).length() > 1.5f) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainTests: FAIL - no path through doorway (closed door)");
|
||||
ok = false;
|
||||
}
|
||||
|
||||
/* 2. The doorway polys carry the door area id. */
|
||||
if (ok && navSys->getPolyAreaAt(navE, Ogre::Vector3(0, 0.5f, 0)) !=
|
||||
TileCacheNavMesh::EDITSCENE_AREA_DOOR) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainTests: FAIL - doorway polys not marked as door area, got " +
|
||||
Ogre::StringConverter::toString(
|
||||
(int)navSys->getPolyAreaAt(
|
||||
navE, Ogre::Vector3(0, 0.5f, 0))));
|
||||
ok = false;
|
||||
}
|
||||
|
||||
/* 3. Locking the door adds a tile-cache obstacle; with the
|
||||
* only passage blocked the target becomes unreachable. */
|
||||
DoorSystem::setDoorLocked("navtestdoor", true);
|
||||
for (int i = 0; i < 5; ++i)
|
||||
navSys->update(0.016f);
|
||||
if (ok) {
|
||||
found = navSys->findPath(navE, start, target, path);
|
||||
if (found && !path.empty() &&
|
||||
(path.back() - target).length() <= 1.5f) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainTests: FAIL - path still reaches through locked door");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* 4. Unlocking removes the obstacle and restores the path. */
|
||||
DoorSystem::setDoorLocked("navtestdoor", false);
|
||||
for (int i = 0; i < 5; ++i)
|
||||
navSys->update(0.016f);
|
||||
if (ok) {
|
||||
found = navSys->findPath(navE, start, target, path);
|
||||
if (!found || path.empty() ||
|
||||
(path.back() - target).length() > 1.5f) {
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainTests: FAIL - path not restored after unlock");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Cleanup: the store keys, then the entities and the manually
|
||||
* created Ogre objects. */
|
||||
GlobalStateStore::getInstance().remove("door.navtestdoor.locked");
|
||||
GlobalStateStore::getInstance().remove("door.navtestdoor.isOpen");
|
||||
|
||||
/* navTestDestroyEntity(doorE) destroys the hinge node, which
|
||||
* cascades to the leaf child node. */
|
||||
if (colliderE.is_alive())
|
||||
colliderE.destruct();
|
||||
navTestDestroyEntity(app, doorE);
|
||||
navTestDestroyEntity(app, floorE);
|
||||
navTestDestroyEntity(app, wallLE);
|
||||
navTestDestroyEntity(app, wallRE);
|
||||
if (navE.is_alive())
|
||||
navE.destruct();
|
||||
|
||||
if (!ok)
|
||||
return false;
|
||||
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TerrainTests: navmesh door test passed");
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Logging */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -5805,6 +6064,7 @@ int TerrainTestRunner::run(EditorApp &app, int iterations)
|
||||
{ "roadRegionStore", testRoadRegionStore },
|
||||
{ "roadRegionStreaming", testRoadRegionStreaming },
|
||||
{ "roadRegionMigration", testRoadRegionMigration },
|
||||
{ "navMeshDoors", testNavMeshDoors },
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ static bool testTerrainPrefabSpawners(EditorApp &app, TerrainSystem *ts);
|
||||
static bool testRoadRegionStore(EditorApp &app, TerrainSystem *ts);
|
||||
static bool testRoadRegionStreaming(EditorApp &app, TerrainSystem *ts);
|
||||
static bool testRoadRegionMigration(EditorApp &app, TerrainSystem *ts);
|
||||
static bool testNavMeshDoors(EditorApp &app, TerrainSystem *ts);
|
||||
static bool testMemoryStability(EditorApp &app);
|
||||
|
||||
static void logResult(const TerrainTestResult &r);
|
||||
|
||||
@@ -0,0 +1,802 @@
|
||||
/**
|
||||
* @file cellgrid_door_test.cpp
|
||||
* @brief Headless tests for F0 door identity and per-door configuration.
|
||||
*
|
||||
* Runs without OGRE initialization (no window, no GL): doorway identity
|
||||
* (edge keys, doorway collection) is pure grid math, and the
|
||||
* serialization round trip only needs a flecs world (SceneSerializer
|
||||
* with a null SceneManager touches no OGRE objects for name + CellGrid
|
||||
* components).
|
||||
*
|
||||
* Covered:
|
||||
* - CellGridSystem::doorEdgeKey canonicalization (both cells sharing a
|
||||
* door edge produce the same key)
|
||||
* - CellGridSystem::collectDoorways deduplication and internal flag
|
||||
* - CellGridComponent::ensureGridUid generation and stability
|
||||
* - doorConfigs + gridUid serialization round trip (incl. orphaned
|
||||
* entries surviving the round trip)
|
||||
* - old scenes without gridUid/doorConfigs loading with defaults
|
||||
* - Lua get/set of the new CellGrid fields (gridUid, doorConfigs) is
|
||||
* covered by component_lua_test (test 32)
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include "../systems/CellGridSystem.hpp"
|
||||
#include "../systems/SceneSerializer.hpp"
|
||||
#include "../systems/DoorSystem.hpp"
|
||||
#include "../systems/GlobalStateStore.hpp"
|
||||
#include "../systems/EventBus.hpp"
|
||||
#include "../components/CellGrid.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../components/StandaloneDoor.hpp"
|
||||
#include "../components/Transform.hpp"
|
||||
#include "../lua/LuaEventApi.hpp"
|
||||
#include "../lua/LuaGlobalStateApi.hpp"
|
||||
#include "../lua/LuaDoorApi.hpp"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCount = 0;
|
||||
static int passCount = 0;
|
||||
|
||||
#define TEST(name) \
|
||||
do { \
|
||||
testCount++; \
|
||||
printf(" TEST %d: %s ... ", testCount, name); \
|
||||
} while (0)
|
||||
|
||||
#define PASS() \
|
||||
do { \
|
||||
passCount++; \
|
||||
printf("PASS\n"); \
|
||||
} while (0)
|
||||
|
||||
#define FAIL(msg) \
|
||||
do { \
|
||||
printf("FAIL: %s\n", std::string(msg).c_str()); \
|
||||
return 1; \
|
||||
} while (0)
|
||||
|
||||
static int testEdgeKeyCanonicalization()
|
||||
{
|
||||
TEST("doorEdgeKey canonicalization");
|
||||
|
||||
// The X+ edge of cell (3,0,5) is the X- edge of cell (4,0,5).
|
||||
if (CellGridSystem::doorEdgeKey(3, 0, 5, 3) !=
|
||||
CellGridSystem::doorEdgeKey(4, 0, 5, 2))
|
||||
FAIL("X+/X- edge keys differ");
|
||||
if (CellGridSystem::doorEdgeKey(3, 0, 5, 3) != "X:4:0:5")
|
||||
FAIL("X edge key format wrong");
|
||||
|
||||
// The Z+ edge of cell (3,0,5) is the Z- edge of cell (3,0,6).
|
||||
if (CellGridSystem::doorEdgeKey(3, 0, 5, 1) !=
|
||||
CellGridSystem::doorEdgeKey(3, 0, 6, 0))
|
||||
FAIL("Z+/Z- edge keys differ");
|
||||
if (CellGridSystem::doorEdgeKey(3, 0, 5, 1) != "Z:3:0:6")
|
||||
FAIL("Z edge key format wrong");
|
||||
|
||||
// Z- and X- keep the cell coordinate.
|
||||
if (CellGridSystem::doorEdgeKey(3, 0, 5, 0) != "Z:3:0:5")
|
||||
FAIL("Z- edge key wrong");
|
||||
if (CellGridSystem::doorEdgeKey(3, 0, 5, 2) != "X:3:0:5")
|
||||
FAIL("X- edge key wrong");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testCollectDoorways()
|
||||
{
|
||||
TEST("collectDoorways deduplication");
|
||||
|
||||
CellGridComponent grid;
|
||||
grid.width = 4;
|
||||
grid.height = 1;
|
||||
grid.depth = 4;
|
||||
|
||||
// Two cells sharing a door edge, both flag it.
|
||||
Cell a;
|
||||
a.x = 0;
|
||||
a.y = 0;
|
||||
a.z = 0;
|
||||
a.setFlag(CellFlags::DoorXPos);
|
||||
grid.cells.push_back(a);
|
||||
Cell b;
|
||||
b.x = 1;
|
||||
b.y = 0;
|
||||
b.z = 0;
|
||||
b.setFlag(CellFlags::DoorXNeg);
|
||||
grid.cells.push_back(b);
|
||||
// An internal door on the Z- edge of (0,0,1).
|
||||
Cell c;
|
||||
c.x = 0;
|
||||
c.y = 0;
|
||||
c.z = 1;
|
||||
c.setFlag(CellFlags::IntDoorZNeg);
|
||||
grid.cells.push_back(c);
|
||||
|
||||
auto doorways = CellGridSystem::collectDoorways(grid);
|
||||
if (doorways.size() != 2)
|
||||
FAIL("expected 2 unique doorways, got " +
|
||||
std::to_string(doorways.size()));
|
||||
|
||||
bool foundShared = false;
|
||||
bool foundInternal = false;
|
||||
for (const auto &dw : doorways) {
|
||||
if (dw.edgeKey == "X:1:0:0" && !dw.internal)
|
||||
foundShared = true;
|
||||
if (dw.edgeKey == "Z:0:0:1" && dw.internal)
|
||||
foundInternal = true;
|
||||
}
|
||||
if (!foundShared)
|
||||
FAIL("shared-edge doorway X:1:0:0 missing or mislabeled");
|
||||
if (!foundInternal)
|
||||
FAIL("internal doorway Z:0:0:1 missing or mislabeled");
|
||||
|
||||
// No door flags -> no doorways.
|
||||
CellGridComponent empty;
|
||||
if (!CellGridSystem::collectDoorways(empty).empty())
|
||||
FAIL("empty grid produced doorways");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testGridUid()
|
||||
{
|
||||
TEST("ensureGridUid generation and stability");
|
||||
|
||||
CellGridComponent grid;
|
||||
if (!grid.gridUid.empty())
|
||||
FAIL("gridUid not empty by default");
|
||||
const std::string &uid = grid.ensureGridUid();
|
||||
if (uid.size() != 36)
|
||||
FAIL("gridUid is not a 36-char UUID: " + uid);
|
||||
if (grid.ensureGridUid() != uid)
|
||||
FAIL("ensureGridUid not stable");
|
||||
|
||||
CellGridComponent other;
|
||||
if (other.ensureGridUid() == uid)
|
||||
FAIL("two grids got the same uid");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testSerializationRoundTrip()
|
||||
{
|
||||
TEST("doorConfigs + gridUid serialization round trip");
|
||||
|
||||
flecs::world world;
|
||||
SceneSerializer serializer(world, nullptr);
|
||||
|
||||
flecs::entity e = world.entity();
|
||||
CellGridComponent grid;
|
||||
grid.gridUid = "11111111-2222-3333-4444-555555555555";
|
||||
|
||||
Cell cell;
|
||||
cell.x = 0;
|
||||
cell.y = 0;
|
||||
cell.z = 0;
|
||||
cell.setFlag(CellFlags::DoorXPos);
|
||||
grid.cells.push_back(cell);
|
||||
|
||||
CellGridDoorConfig cfg;
|
||||
cfg.label = "Kitchen door";
|
||||
cfg.hasOverride = true;
|
||||
cfg.openAngle = 42.0f;
|
||||
cfg.openSpeed = 99.0f;
|
||||
cfg.swingReversed = true;
|
||||
cfg.actionName = "knock";
|
||||
cfg.sceneSwitchPath = "interior.json";
|
||||
cfg.sceneSwitchTarget = "arrival";
|
||||
cfg.persistent = true;
|
||||
cfg.lockable = true;
|
||||
cfg.lockedByDefault = true;
|
||||
cfg.keyItemId = "key_brass";
|
||||
grid.doorConfigs["X:1:0:0"] = cfg;
|
||||
|
||||
// Orphaned entry: no doorway with this edge key exists. It must
|
||||
// survive the round trip (never auto-deleted).
|
||||
CellGridDoorConfig orphan;
|
||||
orphan.label = "Old cellar door";
|
||||
orphan.lockable = true;
|
||||
grid.doorConfigs["Z:9:9:9"] = orphan;
|
||||
|
||||
e.set<CellGridComponent>(grid);
|
||||
|
||||
nlohmann::json json = serializer.serializeEntity(e);
|
||||
if (!json.contains("cellGrid"))
|
||||
FAIL("no cellGrid section in serialized entity");
|
||||
const nlohmann::json &cj = json["cellGrid"];
|
||||
if (cj.value("gridUid", "") !=
|
||||
"11111111-2222-3333-4444-555555555555")
|
||||
FAIL("gridUid not serialized");
|
||||
if (!cj.contains("doorConfigs") || !cj["doorConfigs"].is_object() ||
|
||||
cj["doorConfigs"].size() != 2)
|
||||
FAIL("doorConfigs not serialized");
|
||||
|
||||
// Deserialize into a fresh entity.
|
||||
flecs::entity e2 = world.entity();
|
||||
serializer.deserializeEntityComponents(e2, json, flecs::entity::null(),
|
||||
nullptr, false, false, false);
|
||||
if (!e2.has<CellGridComponent>())
|
||||
FAIL("CellGridComponent missing after deserialize");
|
||||
const CellGridComponent &g2 = e2.get<CellGridComponent>();
|
||||
if (g2.gridUid != "11111111-2222-3333-4444-555555555555")
|
||||
FAIL("gridUid mismatch after round trip");
|
||||
if (g2.doorConfigs.size() != 2)
|
||||
FAIL("doorConfigs size mismatch after round trip");
|
||||
|
||||
auto it = g2.doorConfigs.find("X:1:0:0");
|
||||
if (it == g2.doorConfigs.end())
|
||||
FAIL("live door config missing after round trip");
|
||||
const CellGridDoorConfig &c2 = it->second;
|
||||
if (c2.label != "Kitchen door" || !c2.hasOverride ||
|
||||
c2.openAngle != 42.0f || c2.openSpeed != 99.0f ||
|
||||
!c2.swingReversed || c2.actionName != "knock" ||
|
||||
c2.sceneSwitchPath != "interior.json" ||
|
||||
c2.sceneSwitchTarget != "arrival" || !c2.persistent ||
|
||||
!c2.lockable || !c2.lockedByDefault ||
|
||||
c2.keyItemId != "key_brass" || c2.disabled)
|
||||
FAIL("door config fields mismatch after round trip");
|
||||
|
||||
auto oit = g2.doorConfigs.find("Z:9:9:9");
|
||||
if (oit == g2.doorConfigs.end())
|
||||
FAIL("orphaned door config lost in round trip");
|
||||
if (oit->second.label != "Old cellar door" || !oit->second.lockable)
|
||||
FAIL("orphaned door config fields mismatch");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testOldSceneDefaults()
|
||||
{
|
||||
TEST("old scene without gridUid/doorConfigs loads with defaults");
|
||||
|
||||
flecs::world world;
|
||||
SceneSerializer serializer(world, nullptr);
|
||||
|
||||
// Simulate an old scene: a cellGrid section without the F0 fields.
|
||||
nlohmann::json json;
|
||||
json["cellGrid"] = { { "width", 3 },
|
||||
{ "height", 1 },
|
||||
{ "depth", 3 },
|
||||
{ "doorOpenAngle", 90.0f } };
|
||||
|
||||
flecs::entity e = world.entity();
|
||||
serializer.deserializeEntityComponents(e, json, flecs::entity::null(),
|
||||
nullptr, false, false, false);
|
||||
if (!e.has<CellGridComponent>())
|
||||
FAIL("CellGridComponent missing after deserialize");
|
||||
const CellGridComponent &g = e.get<CellGridComponent>();
|
||||
if (!g.doorConfigs.empty())
|
||||
FAIL("doorConfigs not empty for old scene");
|
||||
if (g.gridUid.empty())
|
||||
FAIL("gridUid not generated for old scene");
|
||||
if (g.gridUid.size() != 36)
|
||||
FAIL("generated gridUid is not a 36-char UUID");
|
||||
if (g.doorOpenAngle != 90.0f)
|
||||
FAIL("existing field lost");
|
||||
|
||||
// The generated uid must be kept on re-serialization (stability).
|
||||
nlohmann::json out = serializer.serializeEntity(e);
|
||||
if (out["cellGrid"].value("gridUid", "") != g.gridUid)
|
||||
FAIL("generated gridUid not stable across save");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// F6: persistent door state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testDoorDefaultsAndLocked()
|
||||
{
|
||||
TEST("declareDoorDefaults + locked helpers + unlock notifications");
|
||||
|
||||
GlobalStateStore &store = GlobalStateStore::getInstance();
|
||||
const std::string id = "f6t1:X:1:0:0";
|
||||
store.remove("door." + id + ".locked");
|
||||
store.remove("door." + id + ".isOpen");
|
||||
|
||||
// Ephemeral doors are never locked.
|
||||
DoorComponent ephemeral;
|
||||
if (DoorSystem::isDoorLocked(ephemeral))
|
||||
FAIL("ephemeral door reports locked");
|
||||
|
||||
// Declare defaults: locked by default, closed by default.
|
||||
if (DoorSystem::declareDoorDefaults(id, true))
|
||||
FAIL("declareDoorDefaults did not return closed default");
|
||||
if (!DoorSystem::isDoorLockedById(id))
|
||||
FAIL("locked default not applied");
|
||||
|
||||
DoorComponent door;
|
||||
door.doorId = id;
|
||||
if (!DoorSystem::isDoorLocked(door))
|
||||
FAIL("component-level isDoorLocked wrong");
|
||||
|
||||
// Unlock notifications: per-door and generic, with door_id param.
|
||||
int perDoorFired = 0;
|
||||
int genericFired = 0;
|
||||
std::string seenId;
|
||||
EventBus &bus = EventBus::getInstance();
|
||||
EventBus::ListenerId s1 = bus.subscribe(
|
||||
"door_unlocked_" + id,
|
||||
[&](const Ogre::String &, const editScene::EventParams &p) {
|
||||
perDoorFired++;
|
||||
seenId = p.getString("door_id", "");
|
||||
});
|
||||
EventBus::ListenerId s2 = bus.subscribe(
|
||||
"door_unlocked",
|
||||
[&](const Ogre::String &, const editScene::EventParams &p) {
|
||||
genericFired++;
|
||||
seenId = p.getString("door_id", "");
|
||||
});
|
||||
|
||||
DoorSystem::setDoorLocked(id, false);
|
||||
if (DoorSystem::isDoorLockedById(id))
|
||||
FAIL("setDoorLocked(false) did not unlock");
|
||||
if (perDoorFired != 1 || genericFired != 1)
|
||||
FAIL("unlock notifications not fired");
|
||||
if (seenId != id)
|
||||
FAIL("door_id param wrong in notification");
|
||||
|
||||
// Locking fires no notifications.
|
||||
DoorSystem::setDoorLocked(id, true);
|
||||
if (!DoorSystem::isDoorLockedById(id))
|
||||
FAIL("setDoorLocked(true) did not lock");
|
||||
if (perDoorFired != 1 || genericFired != 1)
|
||||
FAIL("notifications fired on lock");
|
||||
|
||||
bus.unsubscribe(s1);
|
||||
bus.unsubscribe(s2);
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testDoorUnlockEvents()
|
||||
{
|
||||
TEST("door_unlock_<id> and generic door_unlock events");
|
||||
|
||||
flecs::world world;
|
||||
const std::string id = "f6t2:Z:0:0:1";
|
||||
|
||||
flecs::entity e = world.entity();
|
||||
DoorComponent door;
|
||||
door.doorId = id;
|
||||
door.lockable = true;
|
||||
e.set<DoorComponent>(door);
|
||||
e.set<TransformComponent>({nullptr, Ogre::Vector3::ZERO,
|
||||
Ogre::Quaternion::IDENTITY,
|
||||
Ogre::Vector3::UNIT_SCALE});
|
||||
|
||||
{
|
||||
DoorSystem ds(world);
|
||||
DoorSystem::setDoorLocked(id, true);
|
||||
ds.update(0.016f); // registers the per-door subscription
|
||||
|
||||
EventBus::getInstance().send("door_unlock_" + id);
|
||||
if (DoorSystem::isDoorLockedById(id))
|
||||
FAIL("per-door unlock event ignored");
|
||||
|
||||
DoorSystem::setDoorLocked(id, true);
|
||||
editScene::EventParams params;
|
||||
params.setString("door_id", id);
|
||||
EventBus::getInstance().send("door_unlock", params);
|
||||
if (DoorSystem::isDoorLockedById(id))
|
||||
FAIL("generic door_unlock event ignored");
|
||||
|
||||
// A generic unlock for a different door leaves this one alone.
|
||||
DoorSystem::setDoorLocked(id, true);
|
||||
editScene::EventParams other;
|
||||
other.setString("door_id", "f6t2:other");
|
||||
EventBus::getInstance().send("door_unlock", other);
|
||||
if (!DoorSystem::isDoorLockedById(id))
|
||||
FAIL("generic unlock leaked across doors");
|
||||
|
||||
// The subscription disappears with the door entity.
|
||||
e.destruct();
|
||||
ds.update(0.016f);
|
||||
DoorSystem::setDoorLocked(id, true);
|
||||
EventBus::getInstance().send("door_unlock_" + id);
|
||||
if (!DoorSystem::isDoorLockedById(id))
|
||||
FAIL("stale subscription survived door removal");
|
||||
}
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testDoorOpenStatePersisted()
|
||||
{
|
||||
TEST("open/closed state persisted when the swing completes");
|
||||
|
||||
flecs::world world;
|
||||
GlobalStateStore &store = GlobalStateStore::getInstance();
|
||||
const std::string id = "f6t3:X:2:0:2";
|
||||
const std::string openKey = "door." + id + ".isOpen";
|
||||
store.remove(openKey);
|
||||
|
||||
flecs::entity e = world.entity();
|
||||
DoorComponent door;
|
||||
door.doorId = id;
|
||||
door.persistent = true;
|
||||
door.openAngle = 90.0f;
|
||||
door.openSpeed = 180.0f;
|
||||
e.set<DoorComponent>(door);
|
||||
e.set<TransformComponent>({nullptr, Ogre::Vector3::ZERO,
|
||||
Ogre::Quaternion::IDENTITY,
|
||||
Ogre::Vector3::UNIT_SCALE});
|
||||
|
||||
DoorSystem ds(world);
|
||||
if (DoorSystem::declareDoorDefaults(id, false))
|
||||
FAIL("fresh door not closed by default");
|
||||
|
||||
// Open it: request a toggle, then swing to completion.
|
||||
e.get_mut<DoorComponent>().toggleRequested = true;
|
||||
for (int i = 0; i < 120; i++)
|
||||
ds.update(1.0f / 60.0f);
|
||||
if (!e.get<DoorComponent>().isOpen)
|
||||
FAIL("door did not open");
|
||||
if (!store.getBool(openKey))
|
||||
FAIL("open state not persisted");
|
||||
|
||||
// Close it again.
|
||||
e.get_mut<DoorComponent>().toggleRequested = true;
|
||||
for (int i = 0; i < 120; i++)
|
||||
ds.update(1.0f / 60.0f);
|
||||
if (e.get<DoorComponent>().isOpen)
|
||||
FAIL("door did not close");
|
||||
if (store.getBool(openKey))
|
||||
FAIL("closed state not persisted");
|
||||
|
||||
// Rebuild path: declareDoorDefaults returns the persisted state.
|
||||
store.set(openKey, true);
|
||||
if (!DoorSystem::declareDoorDefaults(id, false))
|
||||
FAIL("persisted open state not returned on rebuild");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testDoorLuaRoundTrip()
|
||||
{
|
||||
TEST("Lua: door_locked handler unlocks via events; ecs.door.* API");
|
||||
|
||||
const std::string id = "f6t4:X:3:0:3";
|
||||
|
||||
lua_State *L = luaL_newstate();
|
||||
if (!L)
|
||||
FAIL("luaL_newstate failed");
|
||||
luaL_openlibs(L);
|
||||
editScene::registerLuaEventApi(L);
|
||||
editScene::registerLuaGlobalStateApi(L);
|
||||
editScene::registerLuaDoorApi(L);
|
||||
|
||||
flecs::world world;
|
||||
flecs::entity e = world.entity();
|
||||
DoorComponent door;
|
||||
door.doorId = id;
|
||||
door.lockable = true;
|
||||
e.set<DoorComponent>(door);
|
||||
e.set<TransformComponent>({nullptr, Ogre::Vector3::ZERO,
|
||||
Ogre::Quaternion::IDENTITY,
|
||||
Ogre::Vector3::UNIT_SCALE});
|
||||
|
||||
DoorSystem ds(world);
|
||||
DoorSystem::declareDoorDefaults(id, true);
|
||||
ds.update(0.016f); // registers the per-door subscription
|
||||
|
||||
std::string script =
|
||||
"seen = nil\n"
|
||||
"ecs.subscribe_event('door_locked', function(ev, params)\n"
|
||||
" seen = params.door_id\n"
|
||||
" assert(ecs.door.is_locked(params.door_id))\n"
|
||||
" ecs.send_event('door_unlock_' .. params.door_id)\n"
|
||||
" assert(not ecs.door.is_locked(params.door_id))\n"
|
||||
"end)\n"
|
||||
"ecs.send_event('door_locked', { door_id = '" + id + "' })\n"
|
||||
"assert(seen == '" + id + "')\n"
|
||||
"ecs.door.lock('" + id + "')\n"
|
||||
"assert(ecs.door.is_locked('" + id + "'))\n"
|
||||
"ecs.door.unlock('" + id + "')\n"
|
||||
"assert(not ecs.door.is_locked('" + id + "'))\n"
|
||||
"assert(ecs.door.is_open('" + id + "') == false)\n";
|
||||
if (luaL_dostring(L, script.c_str()) != LUA_OK) {
|
||||
std::string err = lua_tostring(L, -1);
|
||||
lua_close(L);
|
||||
FAIL("lua script failed: " + err);
|
||||
}
|
||||
|
||||
if (DoorSystem::isDoorLockedById(id))
|
||||
FAIL("door still locked after Lua round trip");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testSceneSwitchDoorTiming()
|
||||
{
|
||||
TEST("F1: scene switch fires only when fully open (+ event)");
|
||||
|
||||
flecs::world world;
|
||||
|
||||
flecs::entity e = world.entity();
|
||||
DoorComponent door;
|
||||
door.sceneSwitchPath = "scene_b.json";
|
||||
door.sceneSwitchTarget = "arrival_b";
|
||||
door.openAngle = 90.0f;
|
||||
door.openSpeed = 180.0f;
|
||||
e.set<DoorComponent>(door);
|
||||
e.set<TransformComponent>({nullptr, Ogre::Vector3::ZERO,
|
||||
Ogre::Quaternion::IDENTITY,
|
||||
Ogre::Vector3::UNIT_SCALE});
|
||||
|
||||
int fired = 0;
|
||||
std::string seenPath, seenTarget;
|
||||
EventBus::ListenerId sub = EventBus::getInstance().subscribe(
|
||||
"door_scene_switch",
|
||||
[&](const Ogre::String &, const editScene::EventParams &p) {
|
||||
fired++;
|
||||
seenPath = p.getString("path", "");
|
||||
seenTarget = p.getString("target", "");
|
||||
});
|
||||
|
||||
DoorSystem ds(world); /* no EditorApp: only the event fires */
|
||||
|
||||
// Open with a pending switch.
|
||||
{
|
||||
DoorComponent &d = e.get_mut<DoorComponent>();
|
||||
d.toggleRequested = true;
|
||||
d.sceneSwitchPending = true;
|
||||
}
|
||||
|
||||
// Mid-swing: nothing may fire.
|
||||
for (int i = 0; i < 15; i++)
|
||||
ds.update(1.0f / 60.0f);
|
||||
if (fired != 0)
|
||||
FAIL("scene switch fired before the door was fully open");
|
||||
if (e.get<DoorComponent>().currentAngle <= 0.0f ||
|
||||
e.get<DoorComponent>().currentAngle >= 90.0f)
|
||||
FAIL("door not mid-swing in timing test");
|
||||
|
||||
// Swing to completion: fires exactly once, with the params.
|
||||
for (int i = 0; i < 60; i++)
|
||||
ds.update(1.0f / 60.0f);
|
||||
if (fired != 1)
|
||||
FAIL("scene switch did not fire at full opening");
|
||||
if (seenPath != "scene_b.json" || seenTarget != "arrival_b")
|
||||
FAIL("door_scene_switch params wrong");
|
||||
if (e.get<DoorComponent>().sceneSwitchPending)
|
||||
FAIL("sceneSwitchPending not consumed");
|
||||
|
||||
// Close cancels a pending switch: open again with pending, then
|
||||
// close mid-swing; no event may fire when the swing completes.
|
||||
{
|
||||
DoorComponent &d = e.get_mut<DoorComponent>();
|
||||
d.toggleRequested = true; /* close */
|
||||
}
|
||||
for (int i = 0; i < 30; i++)
|
||||
ds.update(1.0f / 60.0f);
|
||||
{
|
||||
DoorComponent &d = e.get_mut<DoorComponent>();
|
||||
d.toggleRequested = true; /* reopen */
|
||||
d.sceneSwitchPending = true;
|
||||
}
|
||||
ds.update(1.0f / 60.0f);
|
||||
{
|
||||
/* Mid-swing close cancels the pending switch. */
|
||||
DoorComponent &d = e.get_mut<DoorComponent>();
|
||||
d.toggleRequested = true;
|
||||
}
|
||||
for (int i = 0; i < 120; i++)
|
||||
ds.update(1.0f / 60.0f);
|
||||
if (fired != 1)
|
||||
FAIL("cancelled scene switch fired anyway");
|
||||
if (e.get<DoorComponent>().sceneSwitchPending)
|
||||
FAIL("sceneSwitchPending not cleared on close");
|
||||
|
||||
EventBus::getInstance().unsubscribe(sub);
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testSwingReversed()
|
||||
{
|
||||
TEST("F3: grid doorSwingReversed round trip + swingOrientation sign");
|
||||
|
||||
// Serializer round trip of the grid-wide field.
|
||||
flecs::world world;
|
||||
SceneSerializer serializer(world, nullptr);
|
||||
|
||||
flecs::entity e = world.entity();
|
||||
CellGridComponent grid;
|
||||
grid.doorSwingReversed = true;
|
||||
e.set<CellGridComponent>(grid);
|
||||
|
||||
nlohmann::json json = serializer.serializeEntity(e);
|
||||
if (!json["cellGrid"].value("doorSwingReversed", false))
|
||||
FAIL("doorSwingReversed not serialized");
|
||||
|
||||
flecs::entity e2 = world.entity();
|
||||
serializer.deserializeEntityComponents(e2, json, flecs::entity::null(),
|
||||
nullptr, false, false, false);
|
||||
if (!e2.get<CellGridComponent>().doorSwingReversed)
|
||||
FAIL("doorSwingReversed lost in round trip");
|
||||
|
||||
// Old scenes default to false.
|
||||
nlohmann::json old;
|
||||
old["cellGrid"] = { { "width", 1 }, { "height", 1 }, { "depth", 1 } };
|
||||
flecs::entity e3 = world.entity();
|
||||
serializer.deserializeEntityComponents(e3, old, flecs::entity::null(),
|
||||
nullptr, false, false, false);
|
||||
if (e3.get<CellGridComponent>().doorSwingReversed)
|
||||
FAIL("old scene did not default doorSwingReversed to false");
|
||||
|
||||
// swingOrientation: +90 deg about local Y maps +Z to +X; reversed
|
||||
// maps it to -X. (Quaternion math needs no OGRE init.)
|
||||
DoorComponent door;
|
||||
door.closedOrientation = Ogre::Quaternion::IDENTITY;
|
||||
Ogre::Vector3 n = DoorSystem::swingOrientation(door, 90.0f) *
|
||||
Ogre::Vector3::UNIT_Z;
|
||||
if (n.x < 0.99f)
|
||||
FAIL("normal swing does not turn +Z towards +X");
|
||||
door.swingReversed = true;
|
||||
Ogre::Vector3 r = DoorSystem::swingOrientation(door, 90.0f) *
|
||||
Ogre::Vector3::UNIT_Z;
|
||||
if (r.x > -0.99f)
|
||||
FAIL("reversed swing does not turn +Z towards -X");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testStandaloneDoorSerialization()
|
||||
{
|
||||
TEST("F2: StandaloneDoor serialization round trip");
|
||||
|
||||
flecs::world world;
|
||||
SceneSerializer serializer(world, nullptr);
|
||||
|
||||
flecs::entity e = world.entity();
|
||||
StandaloneDoorComponent door;
|
||||
door.meshName = "door.mesh";
|
||||
door.useMeshMaterial = true;
|
||||
door.rectName = "door";
|
||||
door.leafWidth = 1.2f;
|
||||
door.leafHeight = 2.1f;
|
||||
door.leafThickness = 0.06f;
|
||||
door.openAngle = 95.0f;
|
||||
door.openSpeed = 150.0f;
|
||||
door.swingReversed = true;
|
||||
door.actionName = "knock";
|
||||
door.sceneSwitchPath = "outside.json";
|
||||
door.sceneSwitchTarget = "arrival";
|
||||
door.persistent = true;
|
||||
door.lockable = true;
|
||||
door.lockedByDefault = true;
|
||||
door.keyItemId = "key_brass";
|
||||
door.doorId = "door-test-1";
|
||||
e.set<StandaloneDoorComponent>(door);
|
||||
|
||||
nlohmann::json json = serializer.serializeEntity(e);
|
||||
if (!json.contains("standaloneDoor"))
|
||||
FAIL("no standaloneDoor section in serialized entity");
|
||||
|
||||
flecs::entity e2 = world.entity();
|
||||
serializer.deserializeEntityComponents(e2, json, flecs::entity::null(),
|
||||
nullptr, false, false, false);
|
||||
if (!e2.has<StandaloneDoorComponent>())
|
||||
FAIL("StandaloneDoorComponent missing after deserialize");
|
||||
const StandaloneDoorComponent &d2 = e2.get<StandaloneDoorComponent>();
|
||||
if (d2.meshName != "door.mesh" || !d2.useMeshMaterial ||
|
||||
d2.rectName != "door" || d2.leafWidth != 1.2f ||
|
||||
d2.leafHeight != 2.1f || d2.leafThickness != 0.06f ||
|
||||
d2.openAngle != 95.0f || d2.openSpeed != 150.0f ||
|
||||
!d2.swingReversed || d2.actionName != "knock" ||
|
||||
d2.sceneSwitchPath != "outside.json" ||
|
||||
d2.sceneSwitchTarget != "arrival" || !d2.persistent ||
|
||||
!d2.lockable || !d2.lockedByDefault ||
|
||||
d2.keyItemId != "key_brass" || d2.doorId != "door-test-1")
|
||||
FAIL("standaloneDoor fields mismatch after round trip");
|
||||
if (!d2.dirty)
|
||||
FAIL("dirty must stay set so the system builds the door");
|
||||
|
||||
// Old scenes without the section simply do not gain the component.
|
||||
flecs::entity e3 = world.entity();
|
||||
nlohmann::json old;
|
||||
old["cellGrid"] = { { "width", 1 }, { "height", 1 }, { "depth", 1 } };
|
||||
serializer.deserializeEntityComponents(e3, old, flecs::entity::null(),
|
||||
nullptr, false, false, false);
|
||||
if (e3.has<StandaloneDoorComponent>())
|
||||
FAIL("old scene gained a StandaloneDoorComponent");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testGenerationModeSerialization()
|
||||
{
|
||||
TEST("F4/F5: generationMode + glass serialization round trip");
|
||||
|
||||
flecs::world world;
|
||||
SceneSerializer serializer(world, nullptr);
|
||||
|
||||
flecs::entity e = world.entity();
|
||||
CellGridComponent grid;
|
||||
grid.generationMode = "interiorOnly";
|
||||
grid.glassColor = Ogre::ColourValue(0.1f, 0.2f, 0.3f, 0.4f);
|
||||
grid.glassMaterialName = "MyGlass";
|
||||
grid.glassReflectivity = 0.25f;
|
||||
e.set<CellGridComponent>(grid);
|
||||
|
||||
nlohmann::json json = serializer.serializeEntity(e);
|
||||
if (json["cellGrid"].value("generationMode", "") != "interiorOnly")
|
||||
FAIL("generationMode not serialized");
|
||||
if (json["cellGrid"].value("glassMaterialName", "") != "MyGlass")
|
||||
FAIL("glassMaterialName not serialized");
|
||||
|
||||
flecs::entity e2 = world.entity();
|
||||
serializer.deserializeEntityComponents(e2, json, flecs::entity::null(),
|
||||
nullptr, false, false, false);
|
||||
const CellGridComponent &g2 = e2.get<CellGridComponent>();
|
||||
if (!g2.interiorOnlyMode())
|
||||
FAIL("generationMode lost in round trip");
|
||||
if (g2.glassColor != Ogre::ColourValue(0.1f, 0.2f, 0.3f, 0.4f))
|
||||
FAIL("glassColor lost in round trip");
|
||||
if (g2.glassMaterialName != "MyGlass" ||
|
||||
g2.glassReflectivity != 0.25f)
|
||||
FAIL("glass material fields lost in round trip");
|
||||
|
||||
// Old scenes default to "full" and built-in glass defaults.
|
||||
nlohmann::json old;
|
||||
old["cellGrid"] = { { "width", 1 }, { "height", 1 }, { "depth", 1 } };
|
||||
flecs::entity e3 = world.entity();
|
||||
serializer.deserializeEntityComponents(e3, old, flecs::entity::null(),
|
||||
nullptr, false, false, false);
|
||||
const CellGridComponent &g3 = e3.get<CellGridComponent>();
|
||||
if (g3.generationMode != "full" || g3.interiorOnlyMode() ||
|
||||
g3.exteriorOnlyMode())
|
||||
FAIL("old scene did not default generationMode to full");
|
||||
if (!g3.glassMaterialName.empty() || g3.glassReflectivity != 0.8f)
|
||||
FAIL("old scene did not default glass fields");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("CellGrid door identity / per-door config tests (F0, F6)\n");
|
||||
printf("====================================================\n");
|
||||
|
||||
/* The store is a singleton shared by the process; keep it off the
|
||||
* disk during tests. */
|
||||
GlobalStateStore::getInstance().setAutoSaveEnabled(false);
|
||||
|
||||
int failures = 0;
|
||||
failures += testEdgeKeyCanonicalization();
|
||||
failures += testCollectDoorways();
|
||||
failures += testGridUid();
|
||||
failures += testSerializationRoundTrip();
|
||||
failures += testOldSceneDefaults();
|
||||
failures += testDoorDefaultsAndLocked();
|
||||
failures += testDoorUnlockEvents();
|
||||
failures += testDoorOpenStatePersisted();
|
||||
failures += testDoorLuaRoundTrip();
|
||||
failures += testSceneSwitchDoorTiming();
|
||||
failures += testSwingReversed();
|
||||
failures += testStandaloneDoorSerialization();
|
||||
failures += testGenerationModeSerialization();
|
||||
|
||||
printf("====================================================\n");
|
||||
printf("Results: %d/%d passed\n", passCount, testCount);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -862,7 +862,8 @@ static int testNavMeshComponent(lua_State *L)
|
||||
" agentRadius = 0.5,"
|
||||
" agentMaxClimb = 0.5,"
|
||||
" agentMaxSlope = 45.0,"
|
||||
" enabled = true"
|
||||
" enabled = true,"
|
||||
" doorAreaCost = 7.5"
|
||||
"});"
|
||||
"local n = ecs.get_component(id, 'NavMesh');"
|
||||
"assert(n ~= nil, 'NavMesh should exist');"
|
||||
@@ -872,7 +873,8 @@ static int testNavMeshComponent(lua_State *L)
|
||||
"assert(n.agentRadius == 0.5, 'wrong agentRadius');"
|
||||
"assert(n.agentMaxClimb == 0.5, 'wrong agentMaxClimb');"
|
||||
"assert(n.agentMaxSlope == 45.0, 'wrong agentMaxSlope');"
|
||||
"assert(n.enabled == true, 'wrong enabled')");
|
||||
"assert(n.enabled == true, 'wrong enabled');"
|
||||
"assert(n.doorAreaCost == 7.5, 'wrong doorAreaCost')");
|
||||
if (!ok)
|
||||
FAIL("NavMesh component assertion failed");
|
||||
|
||||
@@ -1059,6 +1061,7 @@ static int testCellGridComponent(lua_State *L)
|
||||
" depth = 10,"
|
||||
" cellSize = 1.0,"
|
||||
" cellHeight = 0.5,"
|
||||
" generationMode = 'interiorOnly',"
|
||||
" doorsEnabled = false,"
|
||||
" doorRectName = 'door',"
|
||||
" doorMeshName = 'door.mesh',"
|
||||
@@ -1067,7 +1070,11 @@ static int testCellGridComponent(lua_State *L)
|
||||
" doorOpenSpeed = 120.0,"
|
||||
" doorActionName = 'knock',"
|
||||
" doorSceneSwitchPath = 'outside.json',"
|
||||
" doorSceneSwitchTarget = 'arrival'"
|
||||
" doorSceneSwitchTarget = 'arrival',"
|
||||
" glassColor = { r = 0.1, g = 0.2, b = 0.3,"
|
||||
" a = 0.4 },"
|
||||
" glassMaterialName = 'MyGlass',"
|
||||
" glassReflectivity = 0.25"
|
||||
"});"
|
||||
"local c = ecs.get_component(id, 'CellGrid');"
|
||||
"assert(c ~= nil, 'CellGrid should exist');"
|
||||
@@ -1076,6 +1083,8 @@ static int testCellGridComponent(lua_State *L)
|
||||
"assert(c.depth == 10, 'wrong depth');"
|
||||
"assert(c.cellSize == 1.0, 'wrong cellSize');"
|
||||
"assert(c.cellHeight == 0.5, 'wrong cellHeight');"
|
||||
"assert(c.generationMode == 'interiorOnly', "
|
||||
"'wrong generationMode');"
|
||||
"assert(c.doorsEnabled == false, 'wrong doorsEnabled');"
|
||||
"assert(c.doorRectName == 'door', 'wrong doorRectName');"
|
||||
"assert(c.doorMeshName == 'door.mesh', "
|
||||
@@ -1091,7 +1100,13 @@ static int testCellGridComponent(lua_State *L)
|
||||
"assert(c.doorSceneSwitchPath == 'outside.json', "
|
||||
"'wrong doorSceneSwitchPath');"
|
||||
"assert(c.doorSceneSwitchTarget == 'arrival', "
|
||||
"'wrong doorSceneSwitchTarget')");
|
||||
"'wrong doorSceneSwitchTarget');"
|
||||
"assert(c.glassColor.r == 0.1 and "
|
||||
"c.glassColor.a == 0.4, 'wrong glassColor');"
|
||||
"assert(c.glassMaterialName == 'MyGlass', "
|
||||
"'wrong glassMaterialName');"
|
||||
"assert(c.glassReflectivity == 0.25, "
|
||||
"'wrong glassReflectivity')");
|
||||
if (!ok)
|
||||
FAIL("CellGrid component assertion failed");
|
||||
|
||||
@@ -1099,6 +1114,155 @@ static int testCellGridComponent(lua_State *L)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 32b: CellGrid door identity fields (F0): gridUid + doorConfigs
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCellGridDoorConfig(lua_State *L)
|
||||
{
|
||||
TEST("CellGrid gridUid + doorConfigs (F0)");
|
||||
|
||||
bool ok = runLua(
|
||||
L, "local id = ecs.create_entity();"
|
||||
"ecs.set_component(id, 'CellGrid', {"
|
||||
" width = 4,"
|
||||
" gridUid = 'uid-test-1234',"
|
||||
" doorSwingReversed = true,"
|
||||
" doorConfigs = {"
|
||||
" ['X:1:0:0'] = {"
|
||||
" label = 'Kitchen door',"
|
||||
" hasOverride = true,"
|
||||
" openAngle = 42.0,"
|
||||
" openSpeed = 99.0,"
|
||||
" swingReversed = true,"
|
||||
" actionName = 'knock',"
|
||||
" sceneSwitchPath = 'interior.json',"
|
||||
" sceneSwitchTarget = 'arrival',"
|
||||
" disabled = false,"
|
||||
" persistent = true,"
|
||||
" lockable = true,"
|
||||
" lockedByDefault = true,"
|
||||
" keyItemId = 'key_brass'"
|
||||
" },"
|
||||
" ['Z:9:9:9'] = { label = 'Old cellar door',"
|
||||
" lockable = true }"
|
||||
" }"
|
||||
"});"
|
||||
"local c = ecs.get_component(id, 'CellGrid');"
|
||||
"assert(c ~= nil, 'CellGrid should exist');"
|
||||
"assert(c.gridUid == 'uid-test-1234', 'wrong gridUid');"
|
||||
"assert(c.doorSwingReversed == true, "
|
||||
"'wrong doorSwingReversed');"
|
||||
"assert(type(c.doorConfigs) == 'table', "
|
||||
"'doorConfigs missing');"
|
||||
"local d = c.doorConfigs['X:1:0:0'];"
|
||||
"assert(d ~= nil, 'door config X:1:0:0 missing');"
|
||||
"assert(d.label == 'Kitchen door', 'wrong label');"
|
||||
"assert(d.hasOverride == true, 'wrong hasOverride');"
|
||||
"assert(d.openAngle == 42.0, 'wrong openAngle');"
|
||||
"assert(d.openSpeed == 99.0, 'wrong openSpeed');"
|
||||
"assert(d.swingReversed == true, 'wrong swingReversed');"
|
||||
"assert(d.actionName == 'knock', 'wrong actionName');"
|
||||
"assert(d.sceneSwitchPath == 'interior.json', "
|
||||
"'wrong sceneSwitchPath');"
|
||||
"assert(d.sceneSwitchTarget == 'arrival', "
|
||||
"'wrong sceneSwitchTarget');"
|
||||
"assert(d.disabled == false, 'wrong disabled');"
|
||||
"assert(d.persistent == true, 'wrong persistent');"
|
||||
"assert(d.lockable == true, 'wrong lockable');"
|
||||
"assert(d.lockedByDefault == true, "
|
||||
"'wrong lockedByDefault');"
|
||||
"assert(d.keyItemId == 'key_brass', 'wrong keyItemId');"
|
||||
"local o = c.doorConfigs['Z:9:9:9'];"
|
||||
"assert(o ~= nil, 'orphan config missing');"
|
||||
"assert(o.label == 'Old cellar door', "
|
||||
"'wrong orphan label');"
|
||||
"assert(o.lockable == true, 'wrong orphan lockable');"
|
||||
"assert(not o.hasOverride, "
|
||||
"'orphan hasOverride should be falsy');"
|
||||
"local n = 0;"
|
||||
"for k, v in pairs(c.doorConfigs) do n = n + 1 end;"
|
||||
"assert(n == 2, 'expected 2 door configs');"
|
||||
"ecs.set_component(id, 'CellGrid',"
|
||||
" { doorConfigs = {} });"
|
||||
"local c2 = ecs.get_component(id, 'CellGrid');"
|
||||
"assert(next(c2.doorConfigs) == nil, "
|
||||
"'doorConfigs not cleared')");
|
||||
if (!ok)
|
||||
FAIL("CellGrid door config assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 32c: StandaloneDoor component (F2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testStandaloneDoorComponent(lua_State *L)
|
||||
{
|
||||
TEST("StandaloneDoor component (F2)");
|
||||
|
||||
bool ok = runLua(
|
||||
L, "local id = ecs.create_entity();"
|
||||
"ecs.set_component(id, 'StandaloneDoor', {"
|
||||
" meshName = 'door.mesh',"
|
||||
" useMeshMaterial = true,"
|
||||
" rectName = 'door',"
|
||||
" leafWidth = 1.2,"
|
||||
" leafHeight = 2.1,"
|
||||
" leafThickness = 0.06,"
|
||||
" openAngle = 95.0,"
|
||||
" openSpeed = 150.0,"
|
||||
" swingReversed = true,"
|
||||
" actionName = 'knock',"
|
||||
" sceneSwitchPath = 'outside.json',"
|
||||
" sceneSwitchTarget = 'arrival',"
|
||||
" persistent = true,"
|
||||
" lockable = true,"
|
||||
" lockedByDefault = true,"
|
||||
" keyItemId = 'key_brass',"
|
||||
" doorId = 'door-test-1'"
|
||||
"});"
|
||||
"local c = ecs.get_component(id, 'StandaloneDoor');"
|
||||
"assert(c ~= nil, 'StandaloneDoor should exist');"
|
||||
"assert(c.meshName == 'door.mesh', 'wrong meshName');"
|
||||
"assert(c.useMeshMaterial == true, "
|
||||
"'wrong useMeshMaterial');"
|
||||
"assert(c.rectName == 'door', 'wrong rectName');"
|
||||
"assert(math.abs(c.leafWidth - 1.2) < 0.001, "
|
||||
"'wrong leafWidth');"
|
||||
"assert(math.abs(c.leafHeight - 2.1) < 0.001, "
|
||||
"'wrong leafHeight');"
|
||||
"assert(math.abs(c.leafThickness - 0.06) < 0.001, "
|
||||
"'wrong leafThickness');"
|
||||
"assert(c.openAngle == 95.0, 'wrong openAngle');"
|
||||
"assert(c.openSpeed == 150.0, 'wrong openSpeed');"
|
||||
"assert(c.swingReversed == true, 'wrong swingReversed');"
|
||||
"assert(c.actionName == 'knock', 'wrong actionName');"
|
||||
"assert(c.sceneSwitchPath == 'outside.json', "
|
||||
"'wrong sceneSwitchPath');"
|
||||
"assert(c.sceneSwitchTarget == 'arrival', "
|
||||
"'wrong sceneSwitchTarget');"
|
||||
"assert(c.persistent == true, 'wrong persistent');"
|
||||
"assert(c.lockable == true, 'wrong lockable');"
|
||||
"assert(c.lockedByDefault == true, "
|
||||
"'wrong lockedByDefault');"
|
||||
"assert(c.keyItemId == 'key_brass', 'wrong keyItemId');"
|
||||
"assert(c.doorId == 'door-test-1', 'wrong doorId');"
|
||||
"ecs.set_component(id, 'StandaloneDoor',"
|
||||
" { leafWidth = 0.9, lockable = false });"
|
||||
"local c2 = ecs.get_component(id, 'StandaloneDoor');"
|
||||
"assert(math.abs(c2.leafWidth - 0.9) < 0.001, "
|
||||
"'leafWidth not updated');"
|
||||
"assert(c2.lockable == false, 'lockable not updated')");
|
||||
if (!ok)
|
||||
FAIL("StandaloneDoor component assertion failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 33: Room component
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1972,6 +2136,8 @@ int main()
|
||||
failures += testGoapBlackboardComponent(L);
|
||||
failures += testPrefabInstanceComponent(L);
|
||||
failures += testCellGridComponent(L);
|
||||
failures += testCellGridDoorConfig(L);
|
||||
failures += testStandaloneDoorComponent(L);
|
||||
failures += testRoomComponent(L);
|
||||
failures += testLotComponent(L);
|
||||
failures += testDistrictComponent(L);
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/**
|
||||
* @file global_state_test.cpp
|
||||
* @brief Headless tests for the F9 global persistent variable storage.
|
||||
*
|
||||
* Links only GlobalStateStore + the Lua global-state API (no OGRE, no
|
||||
* flecs): the store is a plain singleton and the Lua API needs just a
|
||||
* Lua state with an "ecs" table.
|
||||
*
|
||||
* Covered:
|
||||
* - set/get/has/remove for all four types from C++
|
||||
* - int vs float distinction
|
||||
* - declared defaults and caller fallbacks; defaults not persisted
|
||||
* - serialize/deserialize round trip
|
||||
* - clearToDefaults semantics
|
||||
* - renamePrefix (F0 door reassignment state migration)
|
||||
* - Lua API round trip (ecs.global.*) and C++ <-> Lua sharing
|
||||
*
|
||||
* Build with CMake:
|
||||
* cmake --build <build-dir> --target global_state_test
|
||||
* Run:
|
||||
* <build-dir>/src/features/editScene/global_state_test
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include "../systems/GlobalStateStore.hpp"
|
||||
#include "../lua/LuaGlobalStateApi.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include <lua.h>
|
||||
#include <lauxlib.h>
|
||||
#include <lualib.h>
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static int testCount = 0;
|
||||
static int passCount = 0;
|
||||
|
||||
#define TEST(name) \
|
||||
do { \
|
||||
testCount++; \
|
||||
printf(" TEST %d: %s ... ", testCount, name); \
|
||||
} while (0)
|
||||
|
||||
#define PASS() \
|
||||
do { \
|
||||
passCount++; \
|
||||
printf("PASS\n"); \
|
||||
} while (0)
|
||||
|
||||
#define FAIL(msg) \
|
||||
do { \
|
||||
printf("FAIL: %s\n", std::string(msg).c_str()); \
|
||||
return 1; \
|
||||
} while (0)
|
||||
|
||||
/* Run a Lua snippet, FAIL on error. */
|
||||
#define RUN_LUA(code) \
|
||||
do { \
|
||||
if (!runLua(L, code)) \
|
||||
FAIL("Lua error in: " #code); \
|
||||
} while (0)
|
||||
|
||||
static bool runLua(lua_State *L, const char *code)
|
||||
{
|
||||
if (luaL_dostring(L, code) != LUA_OK) {
|
||||
fprintf(stderr, "Lua error: %s\n", lua_tostring(L, -1));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static int testCppTypes()
|
||||
{
|
||||
TEST("C++ set/get/has/remove for all types");
|
||||
|
||||
GlobalStateStore &s = GlobalStateStore::getInstance();
|
||||
s.setAutoSaveEnabled(false);
|
||||
|
||||
s.set("test.bool", true);
|
||||
s.set("test.int", (int64_t)-42);
|
||||
s.set("test.float", 1.5);
|
||||
s.set("test.str", std::string("hello"));
|
||||
|
||||
if (!s.getBool("test.bool"))
|
||||
FAIL("bool readback failed");
|
||||
if (s.getInt("test.int") != -42)
|
||||
FAIL("int readback failed");
|
||||
if (s.getFloat("test.float") != 1.5)
|
||||
FAIL("float readback failed");
|
||||
if (s.getString("test.str") != "hello")
|
||||
FAIL("string readback failed");
|
||||
|
||||
if (!s.has("test.bool") || s.has("test.missing"))
|
||||
FAIL("has() wrong");
|
||||
|
||||
// Type mismatch falls back, no implicit conversion.
|
||||
if (s.getFloat("test.int", 7.0) != 7.0)
|
||||
FAIL("int read as float should fall back");
|
||||
if (s.getBool("test.str", true) != true)
|
||||
FAIL("string read as bool should fall back");
|
||||
|
||||
// Unknown keys return the caller fallback.
|
||||
if (s.getBool("nope", true) != true ||
|
||||
s.getInt("nope", 5) != 5 ||
|
||||
s.getFloat("nope", 2.5) != 2.5 ||
|
||||
s.getString("nope", "fb") != "fb")
|
||||
FAIL("fallbacks wrong");
|
||||
|
||||
s.remove("test.bool");
|
||||
if (s.has("test.bool"))
|
||||
FAIL("remove() failed");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testDefaults()
|
||||
{
|
||||
TEST("declared defaults and persistence");
|
||||
|
||||
GlobalStateStore &s = GlobalStateStore::getInstance();
|
||||
|
||||
s.declareDefault("door.abc.locked", true);
|
||||
s.declareDefault("door.abc.isOpen", false);
|
||||
s.declareDefault("quest.stage", (int64_t)1);
|
||||
s.declareDefault("player.name", std::string("Nameless"));
|
||||
|
||||
if (!s.getBool("door.abc.locked"))
|
||||
FAIL("declared bool default not returned");
|
||||
if (s.getBool("door.abc.isOpen"))
|
||||
FAIL("declared false default not returned");
|
||||
if (s.getInt("quest.stage") != 1)
|
||||
FAIL("declared int default not returned");
|
||||
if (s.getString("player.name") != "Nameless")
|
||||
FAIL("declared string default not returned");
|
||||
if (!s.has("door.abc.locked"))
|
||||
FAIL("has() should be true for declared defaults");
|
||||
|
||||
// Defaults are not persisted: only set() values serialize.
|
||||
nlohmann::json j = s.serialize();
|
||||
if (j["values"].contains("door.abc.locked"))
|
||||
FAIL("default leaked into serialization");
|
||||
|
||||
// An explicit value shadows the default; remove() re-exposes it.
|
||||
s.set("door.abc.locked", false);
|
||||
if (s.getBool("door.abc.locked"))
|
||||
FAIL("explicit value did not shadow default");
|
||||
s.remove("door.abc.locked");
|
||||
if (!s.getBool("door.abc.locked"))
|
||||
FAIL("remove() did not re-expose default");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testSerializeRoundTrip()
|
||||
{
|
||||
TEST("serialize/deserialize round trip");
|
||||
|
||||
GlobalStateStore &s = GlobalStateStore::getInstance();
|
||||
s.clearToDefaults();
|
||||
s.set("rt.bool", true);
|
||||
s.set("rt.int", (int64_t)9223372036854775807LL);
|
||||
s.set("rt.negint", (int64_t)-7);
|
||||
s.set("rt.float", 2.25);
|
||||
s.set("rt.str", std::string("door: abc"));
|
||||
|
||||
nlohmann::json j = s.serialize();
|
||||
|
||||
s.clearToDefaults();
|
||||
if (s.has("rt.bool"))
|
||||
FAIL("clearToDefaults left a value");
|
||||
|
||||
s.deserialize(j);
|
||||
if (!s.getBool("rt.bool") ||
|
||||
s.getInt("rt.int") != 9223372036854775807LL ||
|
||||
s.getInt("rt.negint") != -7 || s.getFloat("rt.float") != 2.25 ||
|
||||
s.getString("rt.str") != "door: abc")
|
||||
FAIL("round trip mismatch");
|
||||
|
||||
// Old saves without the section load cleanly (values cleared).
|
||||
s.deserialize(nlohmann::json::object());
|
||||
if (s.has("rt.bool"))
|
||||
FAIL("deserialize of empty json kept values");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testClearAndRename()
|
||||
{
|
||||
TEST("clearToDefaults and renamePrefix");
|
||||
|
||||
GlobalStateStore &s = GlobalStateStore::getInstance();
|
||||
s.declareDefault("keep.default", (int64_t)9);
|
||||
s.set("door.uid1:X:1:0:0.locked", true);
|
||||
s.set("door.uid1:X:1:0:0.isOpen", false);
|
||||
s.set("door.uid2:Z:0:0:1.locked", true);
|
||||
s.set("unrelated", (int64_t)3);
|
||||
|
||||
// F0 door reassignment: door.uid1:X:1:0:0 -> door.uid1:X:2:0:0
|
||||
s.renamePrefix("door.uid1:X:1:0:0.", "door.uid1:X:2:0:0.");
|
||||
if (s.has("door.uid1:X:1:0:0.locked"))
|
||||
FAIL("old prefix key survived renamePrefix");
|
||||
if (!s.getBool("door.uid1:X:2:0:0.locked"))
|
||||
FAIL("renamed locked key missing");
|
||||
if (s.getBool("door.uid1:X:2:0:0.isOpen"))
|
||||
FAIL("renamed isOpen key wrong");
|
||||
if (!s.getBool("door.uid2:Z:0:0:1.locked") ||
|
||||
s.getInt("unrelated") != 3)
|
||||
FAIL("unrelated keys clobbered by renamePrefix");
|
||||
|
||||
s.clearToDefaults();
|
||||
if (s.has("door.uid1:X:2:0:0.locked") || s.has("unrelated"))
|
||||
FAIL("clearToDefaults kept explicit values");
|
||||
// Declared defaults survive clearToDefaults.
|
||||
if (s.getInt("keep.default") != 9)
|
||||
FAIL("clearToDefaults dropped declared defaults");
|
||||
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int testLuaApi()
|
||||
{
|
||||
TEST("Lua ecs.global.* API");
|
||||
|
||||
lua_State *L = luaL_newstate();
|
||||
luaL_openlibs(L);
|
||||
lua_newtable(L);
|
||||
lua_setglobal(L, "ecs");
|
||||
editScene::registerLuaGlobalStateApi(L);
|
||||
|
||||
RUN_LUA("assert(type(ecs.global) == 'table', 'ecs.global missing');"
|
||||
"ecs.global.set('lua.bool', true);"
|
||||
"ecs.global.set('lua.int', 42);"
|
||||
"ecs.global.set('lua.float', 0.5);"
|
||||
"ecs.global.set('lua.str', 'from lua');"
|
||||
"assert(ecs.global.get_bool('lua.bool') == true);"
|
||||
"assert(ecs.global.get_int('lua.int') == 42);"
|
||||
"assert(ecs.global.get_float('lua.float') == 0.5);"
|
||||
"assert(ecs.global.get_string('lua.str') == 'from lua');"
|
||||
"assert(ecs.global.has('lua.bool'));"
|
||||
"assert(not ecs.global.has('lua.nope'));"
|
||||
// int vs float distinction
|
||||
"assert(ecs.global.get_int('lua.float', -1) == -1,"
|
||||
" 'float readable as int');"
|
||||
"assert(ecs.global.get_float('lua.int', -1.0) == -1.0,"
|
||||
" 'int readable as float');"
|
||||
// defaults
|
||||
"assert(ecs.global.get_int('lua.missing', 77) == 77);"
|
||||
"assert(ecs.global.get_string('lua.missing', 'd') == 'd');"
|
||||
// remove
|
||||
"ecs.global.remove('lua.bool');"
|
||||
"assert(not ecs.global.has('lua.bool'));"
|
||||
// set nil == remove
|
||||
"ecs.global.set('lua.int', nil);"
|
||||
"assert(not ecs.global.has('lua.int'));");
|
||||
|
||||
// C++ sees what Lua set.
|
||||
GlobalStateStore &s = GlobalStateStore::getInstance();
|
||||
if (s.getFloat("lua.float") != 0.5)
|
||||
FAIL("C++ cannot read Lua-set float");
|
||||
if (s.getString("lua.str") != "from lua")
|
||||
FAIL("C++ cannot read Lua-set string");
|
||||
|
||||
// Lua sees what C++ sets (and declared defaults).
|
||||
s.set("cpp.flag", true);
|
||||
s.declareDefault("door.xyz.locked", false);
|
||||
RUN_LUA("assert(ecs.global.get_bool('cpp.flag') == true,"
|
||||
" 'Lua cannot read C++-set bool');"
|
||||
"assert(ecs.global.get_bool('door.xyz.locked') == false,"
|
||||
" 'Lua cannot read declared default');"
|
||||
"assert(ecs.global.has('door.xyz.locked'),"
|
||||
" 'declared default not visible in Lua');");
|
||||
|
||||
lua_close(L);
|
||||
PASS();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("GlobalStateStore tests (F9)\n");
|
||||
printf("====================================================\n");
|
||||
|
||||
int failures = 0;
|
||||
failures += testCppTypes();
|
||||
failures += testDefaults();
|
||||
failures += testSerializeRoundTrip();
|
||||
failures += testClearAndRename();
|
||||
failures += testLuaApi();
|
||||
|
||||
printf("====================================================\n");
|
||||
printf("Results: %d/%d passed\n", passCount, testCount);
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
@@ -1,14 +1,25 @@
|
||||
#include "CellGridEditor.hpp"
|
||||
#include "DoorPickState.hpp"
|
||||
#include "../components/CellGrid.hpp"
|
||||
#include "../components/ProceduralMaterial.hpp"
|
||||
#include "../components/ProceduralTexture.hpp"
|
||||
#include "../components/Renderable.hpp"
|
||||
#include "../systems/CellGridSystem.hpp"
|
||||
#include "../systems/FurnitureLibrary.hpp"
|
||||
#include "../systems/GlobalStateStore.hpp"
|
||||
#include <OgreStringConverter.h>
|
||||
#include <cstring>
|
||||
|
||||
|
||||
bool CellGridEditor::renderComponent(flecs::entity entity, CellGridComponent& grid)
|
||||
{
|
||||
if (m_currentEntity != entity) {
|
||||
// Switched to another grid: drop door selection/highlight/pick
|
||||
m_selectedDoorKey.clear();
|
||||
m_reassignSource.clear();
|
||||
clearDoorHighlight();
|
||||
DoorPickState::instance().cancel();
|
||||
}
|
||||
m_currentEntity = entity;
|
||||
bool modified = false;
|
||||
|
||||
@@ -35,6 +46,60 @@ bool CellGridEditor::renderComponent(flecs::entity entity, CellGridComponent& gr
|
||||
modified = true;
|
||||
}
|
||||
|
||||
// Generation mode (F4/F5)
|
||||
{
|
||||
const char* modes[] = { "Full", "Interior Only", "Exterior Only" };
|
||||
const char* modeValues[] = { "full", "interiorOnly",
|
||||
"exteriorOnly" };
|
||||
int modeIndex = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (grid.generationMode == modeValues[i]) {
|
||||
modeIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ImGui::Combo("Generation Mode", &modeIndex, modes, 3)) {
|
||||
grid.generationMode = modeValues[modeIndex];
|
||||
modified = true;
|
||||
}
|
||||
if (grid.interiorOnlyMode()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(no exterior shell)");
|
||||
} else if (grid.exteriorOnlyMode()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(shell only)");
|
||||
}
|
||||
}
|
||||
|
||||
// Window glass settings (F5, exteriorOnly/interiorOnly; the
|
||||
// built-in material is opaque, the color alpha is ignored by it)
|
||||
if (grid.exteriorOnlyMode() || grid.interiorOnlyMode()) {
|
||||
float glassCol[4] = { grid.glassColor.r, grid.glassColor.g,
|
||||
grid.glassColor.b, grid.glassColor.a };
|
||||
if (ImGui::ColorEdit4("Glass Color", glassCol)) {
|
||||
grid.glassColor = Ogre::ColourValue(glassCol[0], glassCol[1],
|
||||
glassCol[2], glassCol[3]);
|
||||
modified = true;
|
||||
}
|
||||
char glassMatBuf[256];
|
||||
snprintf(glassMatBuf, sizeof(glassMatBuf), "%s",
|
||||
grid.glassMaterialName.c_str());
|
||||
if (ImGui::InputText("Glass Material", glassMatBuf,
|
||||
sizeof(glassMatBuf))) {
|
||||
grid.glassMaterialName = glassMatBuf;
|
||||
modified = true;
|
||||
}
|
||||
if (grid.glassMaterialName.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(built-in)");
|
||||
}
|
||||
if (ImGui::SliderFloat("Glass Reflectivity",
|
||||
&grid.glassReflectivity, 0.0f, 1.0f,
|
||||
"%.2f")) {
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Statistics
|
||||
@@ -58,58 +123,9 @@ bool CellGridEditor::renderComponent(flecs::entity entity, CellGridComponent& gr
|
||||
|
||||
// Door editor
|
||||
if (ImGui::CollapsingHeader("Doors")) {
|
||||
if (ImGui::Checkbox("Enabled", &grid.doorsEnabled)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::DragFloat("Open Angle", &grid.doorOpenAngle,
|
||||
1.0f, 0.0f, 180.0f, "%.0f deg")) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::DragFloat("Open Speed", &grid.doorOpenSpeed,
|
||||
1.0f, 1.0f, 720.0f, "%.0f deg/s")) {
|
||||
grid.markDirty();
|
||||
}
|
||||
char meshBuffer[256];
|
||||
strncpy(meshBuffer, grid.doorMeshName.c_str(),
|
||||
sizeof(meshBuffer) - 1);
|
||||
meshBuffer[sizeof(meshBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Custom Mesh (optional)", meshBuffer,
|
||||
sizeof(meshBuffer))) {
|
||||
grid.doorMeshName = meshBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::Checkbox("Use Mesh Material",
|
||||
&grid.doorUseMeshMaterial)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
char actionBuffer[256];
|
||||
strncpy(actionBuffer, grid.doorActionName.c_str(),
|
||||
sizeof(actionBuffer) - 1);
|
||||
actionBuffer[sizeof(actionBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Action on Toggle (optional)",
|
||||
actionBuffer, sizeof(actionBuffer))) {
|
||||
grid.doorActionName = actionBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
char sceneBuffer[512];
|
||||
strncpy(sceneBuffer, grid.doorSceneSwitchPath.c_str(),
|
||||
sizeof(sceneBuffer) - 1);
|
||||
sceneBuffer[sizeof(sceneBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Scene Switch Path (optional)",
|
||||
sceneBuffer, sizeof(sceneBuffer))) {
|
||||
grid.doorSceneSwitchPath = sceneBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
char targetBuffer[256];
|
||||
strncpy(targetBuffer, grid.doorSceneSwitchTarget.c_str(),
|
||||
sizeof(targetBuffer) - 1);
|
||||
targetBuffer[sizeof(targetBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Teleport Target (optional)",
|
||||
targetBuffer, sizeof(targetBuffer))) {
|
||||
grid.doorSceneSwitchTarget = targetBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
renderDoorEditor(entity, grid);
|
||||
}
|
||||
updateDoorHighlight(entity, grid);
|
||||
|
||||
// Script editor
|
||||
if (ImGui::CollapsingHeader("Generation Script")) {
|
||||
@@ -484,3 +500,470 @@ void CellGridEditor::renderTextureRectEditor(flecs::entity entity, CellGridCompo
|
||||
|
||||
ImGui::Unindent();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// F0: per-doorway configuration panel
|
||||
// ============================================================================
|
||||
|
||||
void CellGridEditor::renderDoorEditor(flecs::entity entity, CellGridComponent& grid)
|
||||
{
|
||||
// --- Grid-wide door defaults (apply to every doorway without an
|
||||
// override entry) ---
|
||||
if (ImGui::Checkbox("Enabled", &grid.doorsEnabled)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::DragFloat("Open Angle", &grid.doorOpenAngle,
|
||||
1.0f, 0.0f, 180.0f, "%.0f deg")) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::DragFloat("Open Speed", &grid.doorOpenSpeed,
|
||||
1.0f, 1.0f, 720.0f, "%.0f deg/s")) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::Checkbox("Reversed swing", &grid.doorSwingReversed)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
char meshBuffer[256];
|
||||
strncpy(meshBuffer, grid.doorMeshName.c_str(), sizeof(meshBuffer) - 1);
|
||||
meshBuffer[sizeof(meshBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Custom Mesh (optional)", meshBuffer,
|
||||
sizeof(meshBuffer))) {
|
||||
grid.doorMeshName = meshBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::Checkbox("Use Mesh Material", &grid.doorUseMeshMaterial)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
char actionBuffer[256];
|
||||
strncpy(actionBuffer, grid.doorActionName.c_str(),
|
||||
sizeof(actionBuffer) - 1);
|
||||
actionBuffer[sizeof(actionBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Action on Toggle (optional)",
|
||||
actionBuffer, sizeof(actionBuffer))) {
|
||||
grid.doorActionName = actionBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
char sceneBuffer[512];
|
||||
strncpy(sceneBuffer, grid.doorSceneSwitchPath.c_str(),
|
||||
sizeof(sceneBuffer) - 1);
|
||||
sceneBuffer[sizeof(sceneBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Scene Switch Path (optional)",
|
||||
sceneBuffer, sizeof(sceneBuffer))) {
|
||||
grid.doorSceneSwitchPath = sceneBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
char targetBuffer[256];
|
||||
strncpy(targetBuffer, grid.doorSceneSwitchTarget.c_str(),
|
||||
sizeof(targetBuffer) - 1);
|
||||
targetBuffer[sizeof(targetBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Teleport Target (optional)",
|
||||
targetBuffer, sizeof(targetBuffer))) {
|
||||
grid.doorSceneSwitchTarget = targetBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Grid identity: make sure the uid exists so door IDs shown below
|
||||
// are the final stable ones.
|
||||
const std::string& uid = grid.ensureGridUid();
|
||||
ImGui::Text("Grid UID: %s", uid.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Copy##griduid")) {
|
||||
ImGui::SetClipboardText(uid.c_str());
|
||||
}
|
||||
|
||||
// --- Doorway list ---
|
||||
std::vector<CellGridSystem::DoorwayInfo> doorways =
|
||||
CellGridSystem::collectDoorways(grid);
|
||||
|
||||
// Pick in viewport: the next click raycasts against this grid's door
|
||||
// leaves (handled by EditorUISystem::onMousePressed).
|
||||
DoorPickState& pick = DoorPickState::instance();
|
||||
if (pick.done) {
|
||||
if (pick.resultValid && pick.gridEntityId == entity.id()) {
|
||||
m_selectedDoorKey = pick.resultEdgeKey;
|
||||
}
|
||||
pick.done = false;
|
||||
}
|
||||
if (pick.active && pick.gridEntityId == entity.id()) {
|
||||
if (ImGui::Button("Cancel Pick")) {
|
||||
pick.cancel();
|
||||
}
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Click a door leaf in the viewport...");
|
||||
} else {
|
||||
bool canPick = !doorways.empty();
|
||||
if (!canPick) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button("Pick Door in Viewport")) {
|
||||
pick.arm(entity);
|
||||
}
|
||||
if (!canPick) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
auto effectiveSwitchPath = [&](const std::string& key) -> std::string {
|
||||
auto it = grid.doorConfigs.find(key);
|
||||
if (it != grid.doorConfigs.end() && it->second.hasOverride) {
|
||||
return it->second.sceneSwitchPath;
|
||||
}
|
||||
return grid.doorSceneSwitchPath;
|
||||
};
|
||||
|
||||
if (doorways.empty()) {
|
||||
ImGui::TextDisabled("No doorways - set door flags on cells first");
|
||||
} else if (ImGui::BeginListBox("Doorways", ImVec2(-FLT_MIN, 120))) {
|
||||
for (const auto& dw : doorways) {
|
||||
std::string row = dw.edgeKey;
|
||||
auto cfgIt = grid.doorConfigs.find(dw.edgeKey);
|
||||
if (cfgIt != grid.doorConfigs.end()) {
|
||||
const CellGridDoorConfig& c = cfgIt->second;
|
||||
if (!c.label.empty()) {
|
||||
row += " \"" + c.label + "\"";
|
||||
}
|
||||
if (c.disabled) row += " [disabled]";
|
||||
if (c.persistent) row += " [persistent]";
|
||||
if (c.lockable) row += " [lockable]";
|
||||
}
|
||||
if (!effectiveSwitchPath(dw.edgeKey).empty()) {
|
||||
row += " [switch]";
|
||||
}
|
||||
if (dw.internal) {
|
||||
row += " (int)";
|
||||
}
|
||||
ImGui::PushID(dw.edgeKey.c_str());
|
||||
if (ImGui::Selectable(row.c_str(),
|
||||
m_selectedDoorKey == dw.edgeKey)) {
|
||||
m_selectedDoorKey = dw.edgeKey;
|
||||
m_reassignSource.clear();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndListBox();
|
||||
}
|
||||
|
||||
// --- Selected doorway editor ---
|
||||
bool selectedIsDoorway = false;
|
||||
for (const auto& dw : doorways) {
|
||||
if (dw.edgeKey == m_selectedDoorKey) {
|
||||
selectedIsDoorway = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!selectedIsDoorway && !m_selectedDoorKey.empty() &&
|
||||
grid.doorConfigs.find(m_selectedDoorKey) == grid.doorConfigs.end()) {
|
||||
// Selection refers to nothing at all (e.g. doorway removed and no
|
||||
// config kept): drop it.
|
||||
m_selectedDoorKey.clear();
|
||||
}
|
||||
|
||||
if (selectedIsDoorway) {
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Doorway: %s", m_selectedDoorKey.c_str());
|
||||
std::string doorId = uid + ":" + m_selectedDoorKey;
|
||||
ImGui::Text("Door ID: %s", doorId.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Copy##doorid")) {
|
||||
ImGui::SetClipboardText(doorId.c_str());
|
||||
}
|
||||
|
||||
auto it = grid.doorConfigs.find(m_selectedDoorKey);
|
||||
bool hasEntry = it != grid.doorConfigs.end();
|
||||
bool configure = hasEntry;
|
||||
if (ImGui::Checkbox("Configure this door", &configure)) {
|
||||
if (configure) {
|
||||
// Create the entry as a snapshot of the grid defaults;
|
||||
// hasOverride stays false until the user opts in.
|
||||
CellGridDoorConfig cfg;
|
||||
cfg.openAngle = grid.doorOpenAngle;
|
||||
cfg.openSpeed = grid.doorOpenSpeed;
|
||||
cfg.swingReversed = grid.doorSwingReversed;
|
||||
cfg.actionName = grid.doorActionName;
|
||||
cfg.sceneSwitchPath = grid.doorSceneSwitchPath;
|
||||
cfg.sceneSwitchTarget = grid.doorSceneSwitchTarget;
|
||||
grid.doorConfigs[m_selectedDoorKey] = cfg;
|
||||
} else {
|
||||
grid.doorConfigs.erase(m_selectedDoorKey);
|
||||
}
|
||||
grid.markDirty();
|
||||
hasEntry = configure;
|
||||
it = grid.doorConfigs.find(m_selectedDoorKey);
|
||||
}
|
||||
|
||||
if (hasEntry) {
|
||||
CellGridDoorConfig& cfg = it->second;
|
||||
|
||||
char labelBuffer[128];
|
||||
strncpy(labelBuffer, cfg.label.c_str(),
|
||||
sizeof(labelBuffer) - 1);
|
||||
labelBuffer[sizeof(labelBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Label", labelBuffer,
|
||||
sizeof(labelBuffer))) {
|
||||
cfg.label = labelBuffer;
|
||||
}
|
||||
|
||||
if (ImGui::Checkbox("Override grid defaults",
|
||||
&cfg.hasOverride)) {
|
||||
if (cfg.hasOverride) {
|
||||
// Snapshot current grid defaults as the starting
|
||||
// point for the override values.
|
||||
cfg.openAngle = grid.doorOpenAngle;
|
||||
cfg.openSpeed = grid.doorOpenSpeed;
|
||||
cfg.swingReversed = grid.doorSwingReversed;
|
||||
cfg.actionName = grid.doorActionName;
|
||||
cfg.sceneSwitchPath = grid.doorSceneSwitchPath;
|
||||
cfg.sceneSwitchTarget = grid.doorSceneSwitchTarget;
|
||||
}
|
||||
grid.markDirty();
|
||||
}
|
||||
if (cfg.hasOverride) {
|
||||
if (ImGui::DragFloat("Open Angle##door", &cfg.openAngle,
|
||||
1.0f, 0.0f, 180.0f, "%.0f deg")) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::DragFloat("Open Speed##door", &cfg.openSpeed,
|
||||
1.0f, 1.0f, 720.0f, "%.0f deg/s")) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::Checkbox("Reversed swing",
|
||||
&cfg.swingReversed)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
char actBuffer[256];
|
||||
strncpy(actBuffer, cfg.actionName.c_str(),
|
||||
sizeof(actBuffer) - 1);
|
||||
actBuffer[sizeof(actBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Action on Toggle##door", actBuffer,
|
||||
sizeof(actBuffer))) {
|
||||
cfg.actionName = actBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
char pathBuffer[512];
|
||||
strncpy(pathBuffer, cfg.sceneSwitchPath.c_str(),
|
||||
sizeof(pathBuffer) - 1);
|
||||
pathBuffer[sizeof(pathBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Scene Switch Path##door",
|
||||
pathBuffer, sizeof(pathBuffer))) {
|
||||
cfg.sceneSwitchPath = pathBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
char tgtBuffer[256];
|
||||
strncpy(tgtBuffer, cfg.sceneSwitchTarget.c_str(),
|
||||
sizeof(tgtBuffer) - 1);
|
||||
tgtBuffer[sizeof(tgtBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Teleport Target##door", tgtBuffer,
|
||||
sizeof(tgtBuffer))) {
|
||||
cfg.sceneSwitchTarget = tgtBuffer;
|
||||
grid.markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::Checkbox("Disabled (no door entity)",
|
||||
&cfg.disabled)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::Checkbox("Persistent state", &cfg.persistent)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (ImGui::Checkbox("Lockable", &cfg.lockable)) {
|
||||
grid.markDirty();
|
||||
}
|
||||
if (cfg.lockable) {
|
||||
if (ImGui::Checkbox("Locked by default",
|
||||
&cfg.lockedByDefault)) {
|
||||
// only a default - no rebuild needed
|
||||
}
|
||||
char keyBuffer[256];
|
||||
strncpy(keyBuffer, cfg.keyItemId.c_str(),
|
||||
sizeof(keyBuffer) - 1);
|
||||
keyBuffer[sizeof(keyBuffer) - 1] = '\0';
|
||||
if (ImGui::InputText("Key Item ID", keyBuffer,
|
||||
sizeof(keyBuffer))) {
|
||||
cfg.keyItemId = keyBuffer;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGui::Button("Reset to grid defaults")) {
|
||||
grid.doorConfigs.erase(m_selectedDoorKey);
|
||||
grid.markDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Orphaned configs (doorway removed/moved, config kept) ---
|
||||
std::vector<std::string> orphans;
|
||||
for (const auto& pair : grid.doorConfigs) {
|
||||
bool live = false;
|
||||
for (const auto& dw : doorways) {
|
||||
if (dw.edgeKey == pair.first) {
|
||||
live = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!live) {
|
||||
orphans.push_back(pair.first);
|
||||
}
|
||||
}
|
||||
|
||||
if (!orphans.empty()) {
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.6f, 0.0f, 1.0f),
|
||||
"Orphaned door configs (doorway no longer exists):");
|
||||
for (const auto& key : orphans) {
|
||||
ImGui::PushID(key.c_str());
|
||||
const CellGridDoorConfig& cfg = grid.doorConfigs[key];
|
||||
std::string row = key;
|
||||
if (!cfg.label.empty()) {
|
||||
row += " \"" + cfg.label + "\"";
|
||||
}
|
||||
ImGui::TextUnformatted(row.c_str());
|
||||
|
||||
if (m_reassignSource != key) {
|
||||
if (ImGui::SmallButton("Prune")) {
|
||||
grid.doorConfigs.erase(key);
|
||||
grid.markDirty();
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
bool canReassign = !doorways.empty();
|
||||
if (!canReassign) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::SmallButton("Reassign...")) {
|
||||
m_reassignSource = key;
|
||||
m_reassignTarget = -1;
|
||||
}
|
||||
if (!canReassign) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
} else {
|
||||
// Target doorway picker
|
||||
std::string preview = m_reassignTarget >= 0
|
||||
? doorways[m_reassignTarget].edgeKey
|
||||
: "Select target doorway...";
|
||||
if (ImGui::BeginCombo("Target", preview.c_str())) {
|
||||
for (int i = 0; i < (int)doorways.size(); ++i) {
|
||||
std::string tlabel = doorways[i].edgeKey;
|
||||
bool occupied = grid.doorConfigs.find(
|
||||
doorways[i].edgeKey) != grid.doorConfigs.end();
|
||||
if (occupied) {
|
||||
tlabel += " (has config - will overwrite)";
|
||||
}
|
||||
ImGui::PushID(i);
|
||||
if (ImGui::Selectable(tlabel.c_str(),
|
||||
m_reassignTarget == i)) {
|
||||
m_reassignTarget = i;
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (m_reassignTarget >= 0) {
|
||||
const std::string& newKey =
|
||||
doorways[m_reassignTarget].edgeKey;
|
||||
bool overwrite = grid.doorConfigs.find(newKey) !=
|
||||
grid.doorConfigs.end();
|
||||
if (overwrite) {
|
||||
ImGui::TextColored(
|
||||
ImVec4(1.0f, 0.2f, 0.2f, 1.0f),
|
||||
"Target already has a config - it will be "
|
||||
"overwritten!");
|
||||
}
|
||||
if (cfg.persistent || cfg.lockable ||
|
||||
!cfg.sceneSwitchPath.empty()) {
|
||||
ImGui::TextColored(
|
||||
ImVec4(1.0f, 0.6f, 0.0f, 1.0f),
|
||||
"The door's global ID changes; Lua scripts "
|
||||
"referencing the old ID must be updated.");
|
||||
}
|
||||
if (ImGui::SmallButton(overwrite
|
||||
? "Overwrite and Move" : "Move Config")) {
|
||||
CellGridDoorConfig moved =
|
||||
grid.doorConfigs[m_reassignSource];
|
||||
grid.doorConfigs.erase(m_reassignSource);
|
||||
grid.doorConfigs[newKey] = moved;
|
||||
grid.markDirty();
|
||||
// F6: persisted state follows the config - move
|
||||
// the door's global-storage keys to the new ID.
|
||||
GlobalStateStore::getInstance().renamePrefix(
|
||||
"door." + uid + ":" + m_reassignSource + ".",
|
||||
"door." + uid + ":" + newKey + ".");
|
||||
m_reassignSource.clear();
|
||||
m_selectedDoorKey = newKey;
|
||||
ImGui::PopID();
|
||||
break;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
}
|
||||
if (ImGui::SmallButton("Cancel##reassign")) {
|
||||
m_reassignSource.clear();
|
||||
}
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CellGridEditor::updateDoorHighlight(flecs::entity gridEntity,
|
||||
CellGridComponent& grid)
|
||||
{
|
||||
(void)grid;
|
||||
// Drop the highlight if the highlighted door vanished (grid rebuild
|
||||
// destroys and recreates the runtime door entities).
|
||||
if (m_highlightedDoor.is_valid()) {
|
||||
if (!m_highlightedDoor.is_alive() ||
|
||||
!m_highlightedDoor.has<RenderableComponent>() ||
|
||||
!m_highlightedDoor.get<RenderableComponent>().entity) {
|
||||
m_highlightedDoor = flecs::entity::null();
|
||||
m_highlightSavedMaterials.clear();
|
||||
}
|
||||
}
|
||||
|
||||
flecs::entity target = flecs::entity::null();
|
||||
if (!m_selectedDoorKey.empty()) {
|
||||
target = CellGridSystem::findDoorEntity(gridEntity,
|
||||
m_selectedDoorKey);
|
||||
}
|
||||
|
||||
if (target == m_highlightedDoor) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearDoorHighlight();
|
||||
if (target.is_valid() && target.is_alive() &&
|
||||
target.has<RenderableComponent>()) {
|
||||
Ogre::Entity* oe = target.get<RenderableComponent>().entity;
|
||||
if (oe) {
|
||||
for (unsigned i = 0; i < oe->getNumSubEntities(); ++i) {
|
||||
Ogre::SubEntity* sub = oe->getSubEntity(i);
|
||||
m_highlightSavedMaterials.push_back(
|
||||
sub->getMaterialName());
|
||||
sub->setMaterialName("GizmoYellow");
|
||||
}
|
||||
m_highlightedDoor = target;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CellGridEditor::clearDoorHighlight()
|
||||
{
|
||||
if (m_highlightedDoor.is_valid() && m_highlightedDoor.is_alive() &&
|
||||
m_highlightedDoor.has<RenderableComponent>()) {
|
||||
Ogre::Entity* oe = m_highlightedDoor.get<RenderableComponent>().entity;
|
||||
if (oe) {
|
||||
unsigned n = std::min<unsigned>(
|
||||
oe->getNumSubEntities(),
|
||||
(unsigned)m_highlightSavedMaterials.size());
|
||||
for (unsigned i = 0; i < n; ++i) {
|
||||
oe->getSubEntity(i)->setMaterialName(
|
||||
m_highlightSavedMaterials[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
m_highlightedDoor = flecs::entity::null();
|
||||
m_highlightSavedMaterials.clear();
|
||||
}
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct CellGridComponent;
|
||||
struct CellGridDoorConfig;
|
||||
|
||||
class CellGridEditor : public ComponentEditor<CellGridComponent> {
|
||||
public:
|
||||
bool renderComponent(flecs::entity entity, CellGridComponent& grid) override;
|
||||
const char* getName() const override { return "Cell Grid"; }
|
||||
|
||||
|
||||
private:
|
||||
void renderCellEditor(CellGridComponent& grid);
|
||||
void renderFurnitureEditor(CellGridComponent& grid);
|
||||
void renderScriptEditor(CellGridComponent& grid);
|
||||
void renderTextureRectEditor(flecs::entity entity, CellGridComponent& grid);
|
||||
|
||||
// F0: per-doorway configuration panel (door list, overrides,
|
||||
// pick-in-viewport, highlight, orphan handling)
|
||||
void renderDoorEditor(flecs::entity entity, CellGridComponent& grid);
|
||||
|
||||
// Leaf material highlight for the doorway selected in the door panel
|
||||
void updateDoorHighlight(flecs::entity gridEntity,
|
||||
CellGridComponent& grid);
|
||||
void clearDoorHighlight();
|
||||
|
||||
// State for UI
|
||||
int selectedCellX = 0, selectedCellY = 0, selectedCellZ = 0;
|
||||
int newCellX = 0, newCellY = 0, newCellZ = 0;
|
||||
char scriptBuffer[16384] = {0};
|
||||
bool showGrid = false;
|
||||
flecs::entity m_currentEntity = flecs::entity::null();
|
||||
|
||||
// Door panel state
|
||||
std::string m_selectedDoorKey; // edge key of the selected doorway
|
||||
flecs::entity m_highlightedDoor = flecs::entity::null();
|
||||
std::vector<std::string> m_highlightSavedMaterials;
|
||||
int m_reassignTarget = -1; // doorway combo index for reassign
|
||||
std::string m_reassignSource; // orphan edge key being reassigned
|
||||
};
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef EDITSCENE_DOORPICKSTATE_HPP
|
||||
#define EDITSCENE_DOORPICKSTATE_HPP
|
||||
#pragma once
|
||||
|
||||
#include <flecs.h>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* Shared state between the Cell Grid editor's Doors panel and
|
||||
* EditorUISystem for "pick door in viewport" (F0).
|
||||
*
|
||||
* The panel arms a pick for the grid entity it edits; the next viewport
|
||||
* click (EditorUISystem::onMousePressed) raycasts against the grid's
|
||||
* door leaf entities, records the picked doorway edge key here and
|
||||
* consumes the click. The panel polls done/resultValid on its next
|
||||
* frame.
|
||||
*/
|
||||
struct DoorPickState {
|
||||
bool active = false; // a pick is armed
|
||||
uint64_t gridEntityId = 0; // flecs id of the owning grid entity
|
||||
bool done = false; // a pick attempt finished
|
||||
bool resultValid = false; // a door leaf was hit
|
||||
std::string resultEdgeKey; // picked doorway edge key
|
||||
|
||||
static DoorPickState &instance()
|
||||
{
|
||||
static DoorPickState s;
|
||||
return s;
|
||||
}
|
||||
|
||||
void arm(flecs::entity gridEntity)
|
||||
{
|
||||
active = true;
|
||||
done = false;
|
||||
resultValid = false;
|
||||
resultEdgeKey.clear();
|
||||
gridEntityId = gridEntity.id();
|
||||
}
|
||||
|
||||
void cancel()
|
||||
{
|
||||
active = false;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_DOORPICKSTATE_HPP
|
||||
@@ -69,6 +69,12 @@ void NavMeshEditor::renderParams(NavMeshComponent &comp)
|
||||
comp.tileSize = 16;
|
||||
if (comp.tileSize > 128)
|
||||
comp.tileSize = 128;
|
||||
// F7: doorway polys get this traversal cost; > 1 makes
|
||||
// paths prefer doorless routes without forbidding doors
|
||||
ImGui::InputFloat("Door Area Cost", &comp.doorAreaCost,
|
||||
0.5f, 0.0f, "%.1f");
|
||||
if (comp.doorAreaCost < 1.0f)
|
||||
comp.doorAreaCost = 1.0f;
|
||||
ImGui::TreePop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
#include "StandaloneDoorEditor.hpp"
|
||||
#include <imgui.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
bool editString(const char *label, std::string &value)
|
||||
{
|
||||
char buf[256];
|
||||
snprintf(buf, sizeof(buf), "%s", value.c_str());
|
||||
if (ImGui::InputText(label, buf, sizeof(buf))) {
|
||||
value = buf;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool StandaloneDoorEditor::renderComponent(flecs::entity entity,
|
||||
StandaloneDoorComponent &door)
|
||||
{
|
||||
bool modified = false;
|
||||
ImGui::PushID("StandaloneDoor");
|
||||
|
||||
ImGui::Text("Standalone Door (F2)");
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(entity transform = doorway placement)");
|
||||
ImGui::Separator();
|
||||
|
||||
if (editString("Door ID", door.doorId))
|
||||
modified = true;
|
||||
if (!door.doorId.empty()) {
|
||||
/* Warn when another standalone door already uses this ID:
|
||||
* persistence keys would collide in the save file. */
|
||||
int duplicates = 0;
|
||||
entity.world()
|
||||
.query<const StandaloneDoorComponent>()
|
||||
.each([&](flecs::entity other,
|
||||
const StandaloneDoorComponent &d) {
|
||||
if (other != entity && d.doorId == door.doorId)
|
||||
duplicates++;
|
||||
});
|
||||
if (duplicates > 0) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
|
||||
"Duplicate door ID (%d other door%s)",
|
||||
duplicates, duplicates > 1 ? "s" : "");
|
||||
}
|
||||
} else {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("(auto-generated when needed)");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Leaf");
|
||||
if (editString("Mesh Name", door.meshName))
|
||||
modified = true;
|
||||
if (ImGui::Checkbox("Use Mesh Material", &door.useMeshMaterial))
|
||||
modified = true;
|
||||
if (editString("Texture Rect", door.rectName))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Leaf Width", &door.leafWidth, 0.01f, 0.05f,
|
||||
10.0f, "%.2f"))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Leaf Height", &door.leafHeight, 0.01f, 0.05f,
|
||||
10.0f, "%.2f"))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Leaf Thickness", &door.leafThickness, 0.005f,
|
||||
0.01f, 1.0f, "%.3f"))
|
||||
modified = true;
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Behaviour");
|
||||
if (ImGui::DragFloat("Open Angle", &door.openAngle, 1.0f, -180.0f,
|
||||
180.0f, "%.0f deg"))
|
||||
modified = true;
|
||||
if (ImGui::DragFloat("Open Speed", &door.openSpeed, 1.0f, 1.0f,
|
||||
720.0f, "%.0f deg/s"))
|
||||
modified = true;
|
||||
if (ImGui::Checkbox("Swing Reversed", &door.swingReversed))
|
||||
modified = true;
|
||||
if (editString("Action Name", door.actionName))
|
||||
modified = true;
|
||||
if (editString("Scene Switch Path", door.sceneSwitchPath))
|
||||
modified = true;
|
||||
if (!door.sceneSwitchPath.empty()) {
|
||||
if (editString("Scene Switch Target",
|
||||
door.sceneSwitchTarget))
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::Text("Persistence / Locking");
|
||||
if (ImGui::Checkbox("Persistent", &door.persistent))
|
||||
modified = true;
|
||||
if (ImGui::Checkbox("Lockable", &door.lockable))
|
||||
modified = true;
|
||||
if (door.lockable) {
|
||||
if (ImGui::Checkbox("Locked By Default",
|
||||
&door.lockedByDefault))
|
||||
modified = true;
|
||||
if (editString("Key Item ID", door.keyItemId))
|
||||
modified = true;
|
||||
}
|
||||
|
||||
if (modified)
|
||||
door.dirty = true;
|
||||
|
||||
ImGui::PopID();
|
||||
return modified;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef EDITSCENE_STANDALONE_DOOR_EDITOR_HPP
|
||||
#define EDITSCENE_STANDALONE_DOOR_EDITOR_HPP
|
||||
#pragma once
|
||||
|
||||
#include "ComponentEditor.hpp"
|
||||
#include "../components/StandaloneDoor.hpp"
|
||||
|
||||
class StandaloneDoorEditor : public ComponentEditor<StandaloneDoorComponent> {
|
||||
public:
|
||||
const char *getName() const override
|
||||
{
|
||||
return "Standalone Door";
|
||||
}
|
||||
|
||||
protected:
|
||||
bool renderComponent(flecs::entity entity,
|
||||
StandaloneDoorComponent &door) override;
|
||||
};
|
||||
|
||||
#endif // EDITSCENE_STANDALONE_DOOR_EDITOR_HPP
|
||||
Reference in New Issue
Block a user