Major refactoring - internal server state machine implemented
This commit is contained in:
1 parent
71c654e5b6
commit
66e93d46f4
48 files changed
+3948
-3696
No files matched your search
@@ -0,0 +1,392 @@
|
||||
# Game State Architecture
|
||||
|
||||
## Purpose
|
||||
|
||||
The game now separates authoritative gameplay state from rendering and runtime resources. The current implementation is an in-process server, but public consumers use a snapshot-and-command API designed to support a future network client.
|
||||
|
||||
This refactor does **not** add multiplayer networking. It establishes the state ownership and API boundaries needed before networking, replication, multiple local player representations, or server-side synchronization are implemented.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The architecture has four layers:
|
||||
|
||||
1. `GameState` is the aggregate root for persistent, authoritative state.
|
||||
2. `InternalGameServer` owns `GameState`, advances the local simulation, handles commands, and produces immutable snapshots.
|
||||
3. Runtime objects such as `Location`, `Character`, and `InteractiveObject` hold rendering resources and non-owning bindings to server-owned state. `Location` also translates local input, scripts, and collision results into semantic server commands.
|
||||
4. Client-facing systems such as `MenuManager` read immutable snapshots and submit typed commands through `IGameServer`.
|
||||
|
||||
```text
|
||||
Input / UI
|
||||
| typed GameCommand
|
||||
v
|
||||
IGameServer
|
||||
|
|
||||
v
|
||||
InternalGameServer ---- owns ----> GameState
|
||||
| |
|
||||
| update | stable state objects
|
||||
v v
|
||||
Location / Character / InteractiveObject runtime
|
||||
|
|
||||
v
|
||||
Renderer
|
||||
|
||||
IGameServer ---- immutable GameSnapshot ----> UI and other clients
|
||||
```
|
||||
|
||||
## Authoritative State Model
|
||||
|
||||
### `GameState`
|
||||
|
||||
`GameState` is the top-level state object. It contains:
|
||||
|
||||
- Campaign ID and current location ID.
|
||||
- Day, night, dawn, and darklands flags.
|
||||
- A map of campaign-level players keyed by stable player ID.
|
||||
- A map of typed `LocationGameState` records keyed by location ID.
|
||||
- Inventory, money, player-health HUD values, quests, tutorial progress, chat data, taxi state, and script-global values.
|
||||
|
||||
Live `Location` pointers and rendering objects are no longer stored in `GameState`.
|
||||
|
||||
### Campaign-level players
|
||||
|
||||
Player `CharacterState` objects are stored in `GameState::players`. The current single-player game uses the ID `"player"`.
|
||||
|
||||
The player state is no longer owned by a `Character` or duplicated as a separately authoritative state for each loaded location. This permits additional player IDs to be introduced later without changing the aggregate ownership model.
|
||||
|
||||
`LocationGameState` does not retain a player pointer or back-reference to `GameState`.
|
||||
Its update boundary receives `GameState::players` by reference, allowing character
|
||||
lookup and simulation to treat every campaign player by stable ID.
|
||||
|
||||
### `LocationGameState`
|
||||
|
||||
Each location record owns:
|
||||
|
||||
- One `LocationState`.
|
||||
- NPC `CharacterState` objects keyed by configured ID.
|
||||
- `InteractiveObjectState` objects keyed by configured ID.
|
||||
- Trigger-zone enabled/inside flags.
|
||||
- Teleport-zone active flags.
|
||||
- Serialized dialogue runtime state.
|
||||
- Serialized Lua runtime globals.
|
||||
|
||||
State objects use `shared_ptr` internally to keep their addresses stable while local runtime objects are bound to them. These pointers never appear in public snapshots.
|
||||
|
||||
### Stable identity
|
||||
|
||||
- Locations use campaign configuration IDs.
|
||||
- Players use keys from `GameState::players`.
|
||||
- NPCs use required location-local configuration IDs.
|
||||
- Interactive objects use required location-local configuration IDs.
|
||||
- Trigger and teleport zones use their configured IDs.
|
||||
|
||||
Missing, empty, duplicate, and reserved IDs are content errors. Runtime references,
|
||||
Lua APIs, snapshots, and persistence all use these IDs rather than configuration
|
||||
array positions.
|
||||
|
||||
## Server Interface
|
||||
|
||||
`IGameServer` is the public state boundary. It exposes:
|
||||
|
||||
- `snapshot()` for an immutable, revisioned `GameSnapshot`.
|
||||
- `submit()` for typed `GameCommand` values.
|
||||
- `update()` for advancing or polling the implementation.
|
||||
- `save()` and `load()` for server-side persistence.
|
||||
|
||||
No public method returns a mutable state pointer or reference.
|
||||
|
||||
### Snapshots
|
||||
|
||||
`GameSnapshot` and its nested snapshot types are value-only read models. They contain no renderer objects, runtime `Location` pointers, mutable references, or active callbacks.
|
||||
|
||||
Snapshots include:
|
||||
|
||||
- Campaign and world-mode state.
|
||||
- All players.
|
||||
- Locations, NPCs, interactive objects, triggers, and teleports.
|
||||
- Inventory item IDs.
|
||||
- Quest views.
|
||||
- Tutorial, chat, taxi, money, and health data.
|
||||
- Script-global values.
|
||||
|
||||
Snapshots have a monotonically increasing revision. `InternalGameServer` caches the current immutable snapshot and rebuilds it only after the authoritative state changes.
|
||||
|
||||
### Commands
|
||||
|
||||
`GameCommand` is a C++17 `std::variant` of semantic command types. Current commands cover:
|
||||
|
||||
- World mode and current location.
|
||||
- Player health and money.
|
||||
- Tutorial progression and flags.
|
||||
- Chat history and unread state.
|
||||
- Taxi state.
|
||||
- Inventory additions and removals.
|
||||
- Script globals.
|
||||
- Character transforms, attack targets, navigation destinations, stopping,
|
||||
facing, replanning, health, damage, death reset, attack combos, and animation
|
||||
metadata.
|
||||
- Interactive-object activation.
|
||||
- Trigger-zone state.
|
||||
- Quest-definition reloads.
|
||||
|
||||
A future network implementation can serialize equivalent requests instead of mutating local state.
|
||||
|
||||
## Internal Server
|
||||
|
||||
`InternalGameServer` is the current `IGameServer` implementation. It:
|
||||
|
||||
- Exclusively owns the aggregate `GameState`.
|
||||
- Privately stores loaded runtime `Location` objects.
|
||||
- Executes typed commands against authoritative state.
|
||||
- Advances the active location simulation through `update(deltaMs)`.
|
||||
- Synchronizes non-bound auxiliary state such as dialogue, Lua globals, triggers, and teleports.
|
||||
- Builds and caches immutable snapshots.
|
||||
- Delegates save/load serialization to the aggregate state model.
|
||||
|
||||
`Game` remains a privileged host during this migration. It can access private
|
||||
internal-server operations for campaign loading, location construction, callback
|
||||
wiring, and persistence restoration. These host operations are not exposed by
|
||||
`IGameServer`.
|
||||
|
||||
### Animation and combat
|
||||
|
||||
Logical animation frame clocks live in `CharacterState`. Animation frame counts are
|
||||
copied from the loaded clip metadata during runtime construction, after which state
|
||||
transitions and frame-19 combat impacts are deterministic authoritative simulation.
|
||||
|
||||
`CharacterState::update()` returns value-only events containing arrival Lua function
|
||||
names, attack impacts, navigation requests, and death-animation completion. `LocationGameState` applies
|
||||
queued damage after every character has advanced. Runtime `Character` objects only
|
||||
pose loaded models and produce audio, particles, and presentation callbacks.
|
||||
|
||||
### Character state operations
|
||||
|
||||
Gameplay operations mutate the authoritative `CharacterState`, not the runtime
|
||||
`Character`:
|
||||
|
||||
- `setTarget()` normalizes the destination, avoids redundant requests, stores the
|
||||
optional Lua arrival function name, and asks the authoritative `LocationState` to
|
||||
call `planPathExceptMyself()`.
|
||||
- `forceReplan()` recalculates the current requested destination using the same
|
||||
location navigation state.
|
||||
- `stopInPlace()` clears navigation and any pending arrival function.
|
||||
- `stopAndRotateToTarget()`, `resetPlayerAfterDeath()`, `setHp()`, and
|
||||
`tryAttackCombo()` update their respective state directly.
|
||||
|
||||
`InternalGameServer::submit()` resolves the character and location, then invokes
|
||||
these operations for the corresponding semantic command. The runtime `Location`
|
||||
provides convenience methods for submitting those commands from input, scripts,
|
||||
collision handling, and presentation-event processing.
|
||||
|
||||
## Runtime and Visual Object Bindings
|
||||
|
||||
`Location`, `Character`, and `InteractiveObject` no longer own their authoritative state objects.
|
||||
|
||||
- `Location` keeps a private non-owning reference to its `LocationState` and a stable location ID.
|
||||
- `Character` keeps a private non-owning **const** reference named `characterState`, plus stable entity and location IDs.
|
||||
- `InteractiveObject` keeps a private non-owning reference to its `InteractiveObjectState`; its configured object name is its stable ID.
|
||||
|
||||
The bound state objects are owned by `GameState` or `LocationGameState`. The internal
|
||||
server advances them directly before runtime presentation. Rendering reads the bound
|
||||
state; runtime input, scripts, and collision handling submit semantic commands that
|
||||
invoke `CharacterState` operations against the authoritative `LocationState`.
|
||||
|
||||
Navigation target selection, replanning, stopping, facing, death reset, health
|
||||
assignment, and attack-combo clock changes are implemented by `CharacterState`.
|
||||
`Character` no longer exposes gameplay mutation helpers or a generic state mutation
|
||||
callback. Its only state-facing setup responsibility is reporting frame ranges from
|
||||
loaded animation assets through `SetCharacterAnimationMetadataCommand`; otherwise it
|
||||
focuses on model posing, rendering, audio, particles, and runtime callbacks.
|
||||
|
||||
Death-animation and health-change callbacks remain runtime-only on `Character`.
|
||||
Arrival behavior is represented solely by the serialized Lua function name in
|
||||
`CharacterState`; `Location` resolves and invokes that name through `ScriptEngine`
|
||||
when it consumes the arrival event.
|
||||
|
||||
`GameObjectLoader` now:
|
||||
|
||||
1. Loads static creation data from location configuration.
|
||||
2. Creates or reuses server-owned state objects.
|
||||
3. Applies saved runtime fields over the configuration defaults.
|
||||
4. Constructs visual objects bound to those state objects.
|
||||
|
||||
Configuration remains the source of visual assets, animation paths, meshes, textures, and fixed tuning data.
|
||||
|
||||
## State Update Flow
|
||||
|
||||
The normal frame flow is:
|
||||
|
||||
1. SDL input is processed.
|
||||
2. UI code submits semantic commands through `IGameServer`.
|
||||
3. The internal server freezes location character observations and calls
|
||||
`CharacterState::update()` for the player and each NPC.
|
||||
4. Attack impacts are queued, then damage is applied after all characters advance.
|
||||
5. The runtime location consumes value-only events. Navigation requests are sent
|
||||
back as semantic destination commands, whose server handler calls
|
||||
`CharacterState::setTarget()` with the authoritative `LocationState`; arrival
|
||||
function names are resolved through `ScriptEngine`.
|
||||
6. Runtime characters apply the authoritative animation frame to loaded models and
|
||||
play audio, particles, and presentation callbacks.
|
||||
7. Auxiliary dialogue, Lua, trigger, and teleport state is captured into `LocationGameState`.
|
||||
8. The server increments its revision and invalidates the cached snapshot.
|
||||
9. Rendering reads the bound local state; UI and external-style consumers read an immutable snapshot.
|
||||
|
||||
Commands that update bound character or object state do not reload the whole location. This avoids replaying dialogue-node effects or Lua callbacks.
|
||||
|
||||
## Campaign and Location Lifecycle
|
||||
|
||||
Only one location is normally loaded as a runtime object.
|
||||
|
||||
### Campaign loading
|
||||
|
||||
When a campaign starts:
|
||||
|
||||
- Existing campaign state and runtime locations are cleared.
|
||||
- Quest definitions are loaded.
|
||||
- A campaign-level player state is created under the `"player"` ID.
|
||||
- The visual player is created and bound to that state.
|
||||
|
||||
### Location loading
|
||||
|
||||
When a location is loaded:
|
||||
|
||||
- A `LocationGameState` record is created or reused.
|
||||
- Static configuration is loaded.
|
||||
- NPC and interactive-object visuals are created against state owned by the location record.
|
||||
- The shared campaign player is attached.
|
||||
- Dialogue, Lua, navigation, trigger, teleport, and presentation callbacks are initialized.
|
||||
- The runtime `Location` is registered privately with `InternalGameServer`.
|
||||
|
||||
### Location unloading
|
||||
|
||||
When leaving a location:
|
||||
|
||||
- Auxiliary runtime state is captured.
|
||||
- The visual/runtime `Location` is destroyed.
|
||||
- Its typed `LocationGameState` remains in `GameState`.
|
||||
|
||||
The player state remains campaign-level and survives location changes.
|
||||
|
||||
## Save and Load Schema
|
||||
|
||||
Persistence uses one strict, versioned schema. `schemaVersion` is required and
|
||||
unsupported versions are rejected rather than migrated.
|
||||
|
||||
- Campaign players are stored once in an ID-keyed `players` object.
|
||||
- Locations are keyed by location ID.
|
||||
- Each location stores NPC and interactive-object state in ID-keyed objects.
|
||||
- Pending interaction targets and character references use stable IDs.
|
||||
- Immutable content such as meshes, initial taxi configuration, and object
|
||||
definitions is reconstructed from campaign configuration.
|
||||
- Configuration is loaded first and mutable saved state is overlaid in place, so
|
||||
runtime render bindings keep stable addresses.
|
||||
|
||||
Old user save slots are intentionally unsupported. Bundled start-state snapshots
|
||||
must be regenerated with `Game::saveInitialState()` whenever the schema changes.
|
||||
|
||||
## UI, Scripts, Dialogue, and Quests
|
||||
|
||||
### UI
|
||||
|
||||
`MenuManager` no longer holds `GameState&` or a mutable `Inventory*`.
|
||||
|
||||
It now:
|
||||
|
||||
- Reads inventory, quests, tutorial state, chat, money, health, location, and world mode from `GameSnapshot`.
|
||||
- Submits typed commands for mutations.
|
||||
- Continues to own presentation-only UI trees, screen state, textures, and callbacks.
|
||||
|
||||
### Scripts
|
||||
|
||||
Lua remains an in-process single-player runtime. Character bindings submit semantic
|
||||
commands through `Location`; the server invokes `CharacterState` operations on the
|
||||
centrally owned state. NPC, interactive-object, trigger-zone, and teleport-zone
|
||||
bindings address entities by configured ID. Object, inventory, quest, and
|
||||
global-state bindings remain trusted local integrations.
|
||||
|
||||
Each location script registers initialization, enter, and exit callbacks.
|
||||
Initialization runs only when the location has no retained state; loading the Lua
|
||||
file again for rendering does not repeat state-resetting setup. Enter and exit
|
||||
callbacks continue to run for every location transition.
|
||||
|
||||
The scalar Lua runtime state for each location is stored under `LocationGameState::scriptGlobals`. Multiplayer location scripts are expected to be empty, so remote script execution is intentionally outside the current scope.
|
||||
|
||||
### Dialogue and cutscenes
|
||||
|
||||
Dialogue runtime state is captured in each `LocationGameState`. Dialogue and cutscene rendering remain local presentation systems.
|
||||
|
||||
Dialogues and cutscenes are outside the planned multiplayer gameplay mode, but their existing single-player save and callback behavior is retained.
|
||||
|
||||
### Quests, inventory, and tutorial
|
||||
|
||||
These systems remain part of `GameState` for single-player compatibility. UI access now crosses the server interface. They can remain empty or disabled in multiplayer mode as planned.
|
||||
|
||||
## Multiplayer Readiness
|
||||
|
||||
The refactor provides:
|
||||
|
||||
- One authoritative aggregate state owner.
|
||||
- Multiple player slots keyed by stable ID.
|
||||
- Stable location, character, object, and zone identities.
|
||||
- Immutable state snapshots suitable for replication.
|
||||
- Semantic commands suitable for request serialization.
|
||||
- A replaceable public `IGameServer` contract.
|
||||
- A single active-location model matching the initial multiplayer scope.
|
||||
|
||||
Still required for real multiplayer:
|
||||
|
||||
- Network transport and command serialization.
|
||||
- Authentication and player/session assignment.
|
||||
- Server tick scheduling independent of rendering.
|
||||
- Snapshot delta generation or other replication strategy.
|
||||
- Latency handling, interpolation, reconciliation, and prediction.
|
||||
- Command validation, authorization, and anti-cheat rules.
|
||||
- Multiplayer spawning/despawning and location membership.
|
||||
- Presentation events delivered over the network.
|
||||
|
||||
The current local implementation intentionally preserves synchronous Lua and animation behavior. A remote implementation must define which of those behaviors remain server-authoritative and which become client presentation.
|
||||
|
||||
## Platform Build Integration
|
||||
|
||||
The new state and server files are registered in:
|
||||
|
||||
- `proj-windows/CMakeLists.txt`
|
||||
- `proj-linux/CMakeLists.txt`
|
||||
- `proj-web/CMakeLists.txt`
|
||||
- `proj-android/app/jni/src/CMakeLists.txt`
|
||||
|
||||
The refactor uses portable C++17 and introduces no Windows-only gameplay code.
|
||||
|
||||
The Windows Debug target was verified with:
|
||||
|
||||
```powershell
|
||||
cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -- /m
|
||||
```
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- Runtime C++ callbacks are not part of `CharacterState` or public snapshots;
|
||||
serializable Lua arrival function names are state data.
|
||||
- `InteractiveObjectState` animation callbacks are also removed from public snapshots.
|
||||
- Character target and attacker references use stable IDs (`"player"` or a unique
|
||||
location-local NPC ID).
|
||||
- Some top-level values remain mirrors for compatibility, notably player health used by the HUD and scripts.
|
||||
- Dialogue and Lua are synchronized as auxiliary location state rather than fully networked subsystems.
|
||||
- `Game` currently uses privileged internal-server APIs for local content construction. A remote client implementation will need a client-side runtime-location factory driven by configuration and snapshots.
|
||||
|
||||
## Main Files
|
||||
|
||||
- `src/GameState.h` / `src/GameState.cpp` — aggregate state and top-level persistence.
|
||||
- `src/LocationGameState.h` / `src/LocationGameState.cpp` — typed location state, simulation, and ID-keyed persistence.
|
||||
- `src/game_server/IGameServer.h` — public snapshot and command contract.
|
||||
- `src/game_server/InternalGameServer.h` / `.cpp` — in-process authoritative implementation.
|
||||
- `src/Game.h` / `src/Game.cpp` — application host, campaign lifecycle, and runtime wiring.
|
||||
- `src/Location.h` / `src/Location.cpp` — loaded location runtime, command routing,
|
||||
interactions, collisions, scripts, and presentation-event consumption.
|
||||
- `src/Character.h` / `src/Character.cpp` — passive character presentation bound to
|
||||
a const authoritative `CharacterState`.
|
||||
- `src/CharacterState.h` / `src/CharacterState.cpp` — deterministic character
|
||||
simulation, navigation operations, combat state, and logical animation clock.
|
||||
- `src/items/InteractiveObject.h` / `.cpp` — visual object runtime bound to central object state.
|
||||
- `src/items/GameObjectLoader.h` / `.cpp` — configuration/state overlay and runtime construction.
|
||||
- `src/MenuManager.h` / `src/MenuManager.cpp` — snapshot-driven UI and command submission.
|
||||
@@ -96,6 +96,8 @@ add_library(main SHARED
|
||||
../../../../src/Location.cpp
|
||||
../../../../src/LocationState.h
|
||||
../../../../src/LocationState.cpp
|
||||
../../../../src/LocationGameState.h
|
||||
../../../../src/LocationGameState.cpp
|
||||
../../../../src/LocationEditor.h
|
||||
../../../../src/LocationEditor.cpp
|
||||
../../../../src/NpcCar.h
|
||||
@@ -104,6 +106,9 @@ add_library(main SHARED
|
||||
../../../../src/GameConstants.cpp
|
||||
../../../../src/GameState.h
|
||||
../../../../src/GameState.cpp
|
||||
../../../../src/game_server/IGameServer.h
|
||||
../../../../src/game_server/InternalGameServer.h
|
||||
../../../../src/game_server/InternalGameServer.cpp
|
||||
../../../../src/ScriptEngine.h
|
||||
../../../../src/ScriptEngine.cpp
|
||||
../../../../src/navigation/PathFinder.h
|
||||
|
||||
@@ -121,6 +121,8 @@ add_executable(ShadowOverBishkekDemo
|
||||
../src/Location.cpp
|
||||
../src/LocationState.h
|
||||
../src/LocationState.cpp
|
||||
../src/LocationGameState.h
|
||||
../src/LocationGameState.cpp
|
||||
../src/LocationEditor.h
|
||||
../src/LocationEditor.cpp
|
||||
../src/NpcCar.h
|
||||
@@ -129,6 +131,9 @@ add_executable(ShadowOverBishkekDemo
|
||||
../src/GameConstants.cpp
|
||||
../src/GameState.h
|
||||
../src/GameState.cpp
|
||||
../src/game_server/IGameServer.h
|
||||
../src/game_server/InternalGameServer.h
|
||||
../src/game_server/InternalGameServer.cpp
|
||||
../src/ScriptEngine.h
|
||||
../src/ScriptEngine.cpp
|
||||
../src/navigation/PathFinder.h
|
||||
|
||||
@@ -115,6 +115,8 @@ set(SOURCES
|
||||
../src/Location.cpp
|
||||
../src/LocationState.h
|
||||
../src/LocationState.cpp
|
||||
../src/LocationGameState.h
|
||||
../src/LocationGameState.cpp
|
||||
../src/LocationEditor.h
|
||||
../src/LocationEditor.cpp
|
||||
../src/NpcCar.h
|
||||
@@ -123,6 +125,9 @@ set(SOURCES
|
||||
../src/GameConstants.cpp
|
||||
../src/GameState.h
|
||||
../src/GameState.cpp
|
||||
../src/game_server/IGameServer.h
|
||||
../src/game_server/InternalGameServer.h
|
||||
../src/game_server/InternalGameServer.cpp
|
||||
../src/ScriptEngine.h
|
||||
../src/ScriptEngine.cpp
|
||||
../src/navigation/PathFinder.h
|
||||
|
||||
@@ -63,6 +63,8 @@ add_executable(ShadowOverBishkekDemo WIN32
|
||||
../src/Location.cpp
|
||||
../src/LocationState.h
|
||||
../src/LocationState.cpp
|
||||
../src/LocationGameState.h
|
||||
../src/LocationGameState.cpp
|
||||
../src/LocationEditor.h
|
||||
../src/LocationEditor.cpp
|
||||
../src/NpcCar.h
|
||||
@@ -71,6 +73,9 @@ add_executable(ShadowOverBishkekDemo WIN32
|
||||
../src/GameConstants.cpp
|
||||
../src/GameState.h
|
||||
../src/GameState.cpp
|
||||
../src/game_server/IGameServer.h
|
||||
../src/game_server/InternalGameServer.h
|
||||
../src/game_server/InternalGameServer.cpp
|
||||
../src/ScriptEngine.h
|
||||
../src/ScriptEngine.cpp
|
||||
../src/navigation/PathFinder.h
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"objects": [
|
||||
{
|
||||
"id": "WashingMachine002",
|
||||
"name": "WashingMachine002",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -27,6 +28,7 @@
|
||||
"activateFunction": "on_washing_machine1_click"
|
||||
},
|
||||
{
|
||||
"id": "Dryer002",
|
||||
"name": "Dryer002",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -53,6 +55,7 @@
|
||||
"activateFunction": "on_dryer1_click"
|
||||
},
|
||||
{
|
||||
"id": "Toilet001",
|
||||
"name": "Toilet001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -79,6 +82,7 @@
|
||||
"activateFunction": "on_toilet_click"
|
||||
},
|
||||
{
|
||||
"id": "Sink002",
|
||||
"name": "Sink002",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -105,6 +109,7 @@
|
||||
"activateFunction": "on_sink1_click"
|
||||
},
|
||||
{
|
||||
"id": "Sink003",
|
||||
"name": "Sink003",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -131,6 +136,7 @@
|
||||
"activateFunction": "on_sink1_click"
|
||||
},
|
||||
{
|
||||
"id": "Sink004",
|
||||
"name": "Sink004",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -157,6 +163,7 @@
|
||||
"activateFunction": "on_sink1_click"
|
||||
},
|
||||
{
|
||||
"id": "Sink005",
|
||||
"name": "Sink005",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -183,6 +190,7 @@
|
||||
"activateFunction": "on_sink1_click"
|
||||
},
|
||||
{
|
||||
"id": "Shower001",
|
||||
"name": "Shower001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -209,6 +217,7 @@
|
||||
"activateFunction": "on_shower_click"
|
||||
},
|
||||
{
|
||||
"id": "Freezer001",
|
||||
"name": "Freezer001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -235,6 +244,7 @@
|
||||
"activateFunction": "on_freezer_click"
|
||||
},
|
||||
{
|
||||
"id": "Boombox001",
|
||||
"name": "Boombox001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -261,6 +271,7 @@
|
||||
"activateFunction": "on_boombox_click"
|
||||
},
|
||||
{
|
||||
"id": "WashingMachine001",
|
||||
"name": "WashingMachine001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -287,6 +298,7 @@
|
||||
"activateFunction": "on_washing_machine1_click"
|
||||
},
|
||||
{
|
||||
"id": "Shelf001",
|
||||
"name": "Shelf001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -313,6 +325,7 @@
|
||||
"activateFunction": "on_shelf_click"
|
||||
},
|
||||
{
|
||||
"id": "Stove001",
|
||||
"name": "Stove001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -339,6 +352,7 @@
|
||||
"activateFunction": "on_stove1_click"
|
||||
},
|
||||
{
|
||||
"id": "Dryer001",
|
||||
"name": "Dryer001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -365,6 +379,7 @@
|
||||
"activateFunction": "on_dryer1_click"
|
||||
},
|
||||
{
|
||||
"id": "Sink001",
|
||||
"name": "Sink001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -391,6 +406,7 @@
|
||||
"activateFunction": "on_sink1_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Room_1_1_0_Leaf_001",
|
||||
"name": "Door_Room_1_1_0_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Room_1_1_0_Leaf_001.txt.bin",
|
||||
@@ -415,6 +431,7 @@
|
||||
"activateFunction": "on_locked_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Room_1_-1_0_Leaf_001",
|
||||
"name": "Door_Room_1_-1_0_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Room_1_-1_0_Leaf_001.txt.bin",
|
||||
@@ -439,6 +456,7 @@
|
||||
"activateFunction": "on_locked_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Room_-1_1_0_Leaf_001",
|
||||
"name": "Door_Room_-1_1_0_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Room_-1_1_0_Leaf_001.txt.bin",
|
||||
@@ -463,6 +481,7 @@
|
||||
"activateFunction": "on_locked_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Room_-1_-1_0_Leaf_001",
|
||||
"name": "Door_Room_-1_-1_0_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Room_-1_-1_0_Leaf_001.txt.bin",
|
||||
@@ -487,6 +506,7 @@
|
||||
"activateFunction": "on_locked_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Room_1_1_1_Leaf_001",
|
||||
"name": "Door_Room_1_1_1_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Room_1_1_1_Leaf_001.txt.bin",
|
||||
@@ -511,6 +531,7 @@
|
||||
"activateFunction": "on_locked_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Room_1_-1_1_Leaf_001",
|
||||
"name": "Door_Room_1_-1_1_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Room_1_-1_1_Leaf_001.txt.bin",
|
||||
@@ -535,6 +556,7 @@
|
||||
"activateFunction": "on_locked_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Utility_1_1_2_Leaf_001",
|
||||
"name": "Door_Utility_1_1_2_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Utility_1_1_2_Leaf_001.txt.bin",
|
||||
@@ -559,6 +581,7 @@
|
||||
"activateFunction": "on_locked_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Utility_1_-1_2_Leaf_001",
|
||||
"name": "Door_Utility_1_-1_2_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Utility_1_-1_2_Leaf_001.txt.bin",
|
||||
@@ -583,6 +606,7 @@
|
||||
"activateFunction": "on_locked_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Utility_-1_1_2_Leaf_001",
|
||||
"name": "Door_Utility_-1_1_2_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Utility_-1_1_2_Leaf_001.txt.bin",
|
||||
@@ -599,6 +623,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Door_Utility_-1_-1_2_Leaf_001",
|
||||
"name": "Door_Utility_-1_-1_2_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Utility_-1_-1_2_Leaf_001.txt.bin",
|
||||
@@ -615,6 +640,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Phone001",
|
||||
"name": "Phone001",
|
||||
"texturePath": "resources/w/dorm2/phone001_tex001.png",
|
||||
"meshPath": "resources/w/dorm2/phone001_001.txt.bin",
|
||||
@@ -640,6 +666,7 @@
|
||||
"activateFunction": "on_phone_pickup"
|
||||
},
|
||||
{
|
||||
"id": "Journal001",
|
||||
"name": "Journal001",
|
||||
"texturePath": "resources/w/dorm2/Book_Cover_tex002.png",
|
||||
"meshPath": "resources/w/dorm2/journal002_002.txt.bin",
|
||||
@@ -665,6 +692,7 @@
|
||||
"activateFunction": "on_journal_pickup"
|
||||
},
|
||||
{
|
||||
"id": "Bed001",
|
||||
"name": "Bed001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -691,6 +719,7 @@
|
||||
"activateFunction": "on_bed_sleep"
|
||||
},
|
||||
{
|
||||
"id": "Music001",
|
||||
"name": "Music001",
|
||||
"texturePath": "resources/w/exterior/texAtlas002.png",
|
||||
"meshPath": "resources/w/dorm2/D_Music_004.txt.bin",
|
||||
@@ -706,6 +735,7 @@
|
||||
"activateFunction": ""
|
||||
},
|
||||
{
|
||||
"id": "Door_Bath_-1_1_1_Leaf_002",
|
||||
"name": "Door_Bath_-1_1_1_Leaf_002",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Bath_-1_1_1_Leaf_002.txt.bin",
|
||||
@@ -733,6 +763,7 @@
|
||||
"activateFunction": "on_player_bathroom_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Bath_-1_-1_1_Leaf_002",
|
||||
"name": "Door_Bath_-1_-1_1_Leaf_002",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Bath_-1_-1_1_Leaf_002.txt.bin",
|
||||
@@ -757,6 +788,7 @@
|
||||
"activateFunction": "on_alik_bathroom_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Room_-1_1_1_Leaf_001",
|
||||
"name": "Door_Room_-1_1_1_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Room_-1_1_1_Leaf_001.txt.bin",
|
||||
@@ -784,6 +816,7 @@
|
||||
"activateFunction": "on_player_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Door_Room_-1_-1_1_Leaf_001",
|
||||
"name": "Door_Room_-1_-1_1_Leaf_001",
|
||||
"texturePath": "resources/w/dorm2/interior_door_tex003.png",
|
||||
"meshPath": "resources/w/dorm2/Door_Room_-1_-1_1_Leaf_001.txt.bin",
|
||||
@@ -811,6 +844,7 @@
|
||||
"activateFunction": "on_alik_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_Bath_W_N_2_001",
|
||||
"name": "Room_Cover_Bath_W_N_2_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/dorm2/Room_Cover_Bath_W_N_2_002.txt.bin",
|
||||
@@ -825,6 +859,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_LivingRoom_W_N_2_001",
|
||||
"name": "Room_Cover_LivingRoom_W_N_2_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/dorm2/Room_Cover_LivingRoom_W_N_2_002.txt.bin",
|
||||
@@ -839,6 +874,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_LivingRoom_W_S_2_001",
|
||||
"name": "Room_Cover_LivingRoom_W_S_2_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/dorm2/Room_Cover_LivingRoom_W_S_2_002.txt.bin",
|
||||
@@ -853,6 +889,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_Main_Hall_And_Corridors_002",
|
||||
"name": "Room_Cover_Main_Hall_And_Corridors_002",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/dorm2/Room_Cover_Main_Hall_And_Corridors_002.txt.bin",
|
||||
@@ -867,6 +904,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_Others_001",
|
||||
"name": "Room_Cover_Others_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/dorm2/Room_Cover_All_002.txt.bin",
|
||||
@@ -881,6 +919,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_Utility_W_N_3_001",
|
||||
"name": "Room_Cover_Utility_W_N_3_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/dorm2/Room_Cover_Utility_W_N_3_002.txt.bin",
|
||||
@@ -895,6 +934,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_Utility_W_S_3_001",
|
||||
"name": "Room_Cover_Utility_W_S_3_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/dorm2/Room_Cover_Utility_W_S_3_002.txt.bin",
|
||||
@@ -909,6 +949,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Roof",
|
||||
"name": "Roof",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/dorm2/Shadow_Roof001_002.txt.bin",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"objects": [
|
||||
{
|
||||
"id": "Container001",
|
||||
"name": "Container001",
|
||||
"texturePath": "resources/w/exterior/derbis001_tex001.png",
|
||||
"meshPath": "resources/w/exterior/Derbis_001.txt.bin",
|
||||
@@ -25,6 +26,7 @@
|
||||
"activateFunction": "on_container_clicked"
|
||||
},
|
||||
{
|
||||
"id": "car001",
|
||||
"name": "car001",
|
||||
"texturePath": "resources/e/car4096blue_v002.png",
|
||||
"meshPath": "resources/e/car004.txt.bin",
|
||||
@@ -49,6 +51,7 @@
|
||||
"scale": 0.7
|
||||
},
|
||||
{
|
||||
"id": "car001wheels",
|
||||
"name": "car001wheels",
|
||||
"texturePath": "resources/w/exterior/Wheel_base003.png",
|
||||
"meshPath": "resources/w/exterior/wheels002.txt.bin",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"objects": [
|
||||
{
|
||||
"id": "Bookshelf002",
|
||||
"name": "Bookshelf002",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -27,6 +28,7 @@
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"id": "Bookshelf003",
|
||||
"name": "Bookshelf003",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -53,6 +55,7 @@
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"id": "Bookshelf004",
|
||||
"name": "Bookshelf004",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -79,6 +82,7 @@
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"id": "Bookshelf005",
|
||||
"name": "Bookshelf005",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -105,6 +109,7 @@
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"id": "Cake001",
|
||||
"name": "Cake001",
|
||||
"texturePath": "resources/w/exterior/texAtlas002.png",
|
||||
"meshPath": "resources/w/interior/D_Cake_005.txt.bin",
|
||||
@@ -131,6 +136,7 @@
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"id": "Computer001",
|
||||
"name": "Computer001",
|
||||
"texturePath": "resources/w/interior/computer_texture002.png",
|
||||
"textureDarkandsPath": "resources/w/interior/computer_texture002_dark.png",
|
||||
@@ -155,6 +161,7 @@
|
||||
"activateFunction": "on_computer_clicked"
|
||||
},
|
||||
{
|
||||
"id": "Knife001",
|
||||
"name": "Knife001",
|
||||
"texturePath": "resources/w/gg/Props.png",
|
||||
"meshPath": "resources/w/interior/Knife_lay001_004.txt.bin",
|
||||
@@ -177,6 +184,7 @@
|
||||
"castShadowNight" : false
|
||||
},
|
||||
{
|
||||
"id": "Book001",
|
||||
"name": "Book001",
|
||||
"texturePath": "resources/w/interior/Book_Cover_tex003.png",
|
||||
"meshPath": "resources/w/interior/book002_002.txt.bin",
|
||||
@@ -202,6 +210,7 @@
|
||||
"castShadowNight" : false
|
||||
},
|
||||
{
|
||||
"id": "Bookshelf001",
|
||||
"name": "Bookshelf001",
|
||||
"texturePath": "resources/transparent.png",
|
||||
"meshPath": "resources/w/Cube_001.txt.bin",
|
||||
@@ -228,6 +237,7 @@
|
||||
"castShadow": false
|
||||
},
|
||||
{
|
||||
"id": "ReportCard001",
|
||||
"name": "ReportCard001",
|
||||
"texturePath": "resources/w/exterior/texAtlas002.png",
|
||||
"meshPath": "resources/w/interior/D_report_cardGG_005.txt.bin",
|
||||
@@ -252,6 +262,7 @@
|
||||
"activateFunction": "on_report_card_pickup"
|
||||
},
|
||||
{
|
||||
"id": "Note001",
|
||||
"name": "Note001",
|
||||
"texturePath": "resources/w/interior/note001_tex003.png",
|
||||
"meshPath": "resources/w/interior/note001_002.txt.bin",
|
||||
@@ -276,6 +287,7 @@
|
||||
"activateFunction": "on_note_pickup"
|
||||
},
|
||||
{
|
||||
"id": "car001",
|
||||
"name": "car001",
|
||||
"texturePath": "resources/e/car4096blue_v002.png",
|
||||
"meshPath": "resources/e/car004.txt.bin",
|
||||
@@ -299,6 +311,7 @@
|
||||
"scale": 0.7
|
||||
},
|
||||
{
|
||||
"id": "car001wheels",
|
||||
"name": "car001wheels",
|
||||
"texturePath": "resources/w/exterior/Wheel_base003.png",
|
||||
"meshPath": "resources/w/exterior/wheels002.txt.bin",
|
||||
@@ -322,6 +335,7 @@
|
||||
"scale": 0.7
|
||||
},
|
||||
{
|
||||
"id": "Room_N_2_Leaf001",
|
||||
"name": "Room_N_2_Leaf001",
|
||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||
"meshPath": "resources/w/interior/outputRoom_N_2_Leaf001.txt.bin",
|
||||
@@ -349,6 +363,7 @@
|
||||
"activateFunction": "on_library_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Room_N_1_Leaf001",
|
||||
"name": "Room_N_1_Leaf001",
|
||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||
"meshPath": "resources/w/interior/outputRoom_N_1_Leaf001.txt.bin",
|
||||
@@ -376,6 +391,7 @@
|
||||
"activateFunction": "on_n2_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Room_S_1_Leaf001",
|
||||
"name": "Room_S_1_Leaf001",
|
||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||
"meshPath": "resources/w/interior/outputRoom_S_1_Leaf001.txt.bin",
|
||||
@@ -403,6 +419,7 @@
|
||||
"activateFunction": "on_s2_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Room_S_2_Leaf001",
|
||||
"name": "Room_S_2_Leaf001",
|
||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||
"meshPath": "resources/w/interior/outputRoom_S_2_Leaf001.txt.bin",
|
||||
@@ -430,6 +447,7 @@
|
||||
"activateFunction": "on_teachers_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Hall_Leaf001",
|
||||
"name": "Hall_Leaf001",
|
||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||
"meshPath": "resources/w/interior/outputHall_Leaf001.txt.bin",
|
||||
@@ -457,6 +475,7 @@
|
||||
"activateFunction": "on_hall_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Room_S_0_Leaf001",
|
||||
"name": "Room_S_0_Leaf001",
|
||||
"texturePath": "resources/w/interior/uni_interior_door_tex001.png",
|
||||
"meshPath": "resources/w/interior/outputRoom_S_0_Leaf001.txt.bin",
|
||||
@@ -484,6 +503,7 @@
|
||||
"activateFunction": "on_s1_door_click"
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_Corridor_001",
|
||||
"name": "Room_Cover_Corridor_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/interior/Room_Cover_Corridor_003.txt.bin",
|
||||
@@ -501,6 +521,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_Main_Hall_001",
|
||||
"name": "Room_Cover_Main_Hall_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/interior/Room_Cover_Main_Hall_003.txt.bin",
|
||||
@@ -518,6 +539,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_South_1_001",
|
||||
"name": "Room_Cover_South_1_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/interior/Room_Cover_South_1_003.txt.bin",
|
||||
@@ -535,6 +557,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_North_2_001",
|
||||
"name": "Room_Cover_North_2_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/interior/Room_Cover_North_2_003.txt.bin",
|
||||
@@ -552,6 +575,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_South_2_001",
|
||||
"name": "Room_Cover_South_2_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/interior/Room_Cover_South_2_003.txt.bin",
|
||||
@@ -569,6 +593,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_North_3_001",
|
||||
"name": "Room_Cover_North_3_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/interior/Room_Cover_North_3_003.txt.bin",
|
||||
@@ -586,6 +611,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "Room_Cover_South_3_001",
|
||||
"name": "Room_Cover_South_3_001",
|
||||
"texturePath": "resources/black.png",
|
||||
"meshPath": "resources/w/interior/Room_Cover_South_3_003.txt.bin",
|
||||
|
||||
@@ -145,7 +145,7 @@
|
||||
"canAttack": true
|
||||
},
|
||||
{
|
||||
"id": "npc_02_woman",
|
||||
"id": "npc_altynai",
|
||||
"name": "Алтынай",
|
||||
"animationIdlePath": "resources/w/girl/girl_idle001_small.anim",
|
||||
"animationWalkPath": "resources/w/girl/girl_walk010_small.anim",
|
||||
|
||||
+317
-1200
File diff suppressed because it is too large.
Load diff
@@ -15,7 +15,7 @@
|
||||
true,
|
||||
true
|
||||
],
|
||||
"currentLocationName": "uni_exterior",
|
||||
"currentLocationId": "",
|
||||
"globalFloats": {
|
||||
"player_hp": 200.0
|
||||
},
|
||||
@@ -28,98 +28,179 @@
|
||||
"isNight": false,
|
||||
"locations": {
|
||||
"uni_exterior": {
|
||||
"dialogueState": {
|
||||
"dialogue": {
|
||||
"active": false
|
||||
},
|
||||
"interactiveObjectStates": [],
|
||||
"locationState": {
|
||||
"interactiveObjects": {
|
||||
"intBin_Cans": {
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 0.6000000238418579,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
},
|
||||
"intBin_Plastic": {
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 1.399999976158142,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
},
|
||||
"intBin_Waste_A": {
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 2.200000047683716,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
},
|
||||
"intBin_Waste_B": {
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 3.0,
|
||||
"positionY": 0.45736101269721985,
|
||||
"positionZ": 3.8499999046325684,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
},
|
||||
"intRoof": {
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 0.0,
|
||||
"positionY": 3.1999998092651367,
|
||||
"positionZ": 0.0,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
},
|
||||
"intVending.00": {
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": -1.0,
|
||||
"positionY": 0.9150000214576721,
|
||||
"positionZ": -3.9000000953674316,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
},
|
||||
"intVending.01": {
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 0.0,
|
||||
"positionY": 0.9150000214576721,
|
||||
"positionZ": -3.9000000953674316,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
},
|
||||
"intVending.02": {
|
||||
"alpha": 1.0,
|
||||
"isActive": true,
|
||||
"isAnimating": false,
|
||||
"positionX": 1.0,
|
||||
"positionY": 0.9150000214576721,
|
||||
"positionZ": -3.9000000953674316,
|
||||
"rotationY": 0.0,
|
||||
"scale": 1.0
|
||||
}
|
||||
},
|
||||
"npcs": {},
|
||||
"scriptGlobals": {},
|
||||
"state": {
|
||||
"activeNavigationIndex": 0,
|
||||
"cameraAzimuth": -2.3499999046325684,
|
||||
"cameraInclination": 1.103600025177002,
|
||||
"isDarklands": false,
|
||||
"isDawn": false,
|
||||
"isNight": false,
|
||||
"targetInteractNpcIndex": -1,
|
||||
"targetInteractiveObjectIndex": -1,
|
||||
"targetTeleportZoneIndex": -1,
|
||||
"targetInteractNpcId": "",
|
||||
"targetInteractiveObjectId": "",
|
||||
"targetTeleportZoneId": "",
|
||||
"taxi": {
|
||||
"taxiArrivedCallbackCalled": false,
|
||||
"taxiCar": {
|
||||
"arrivedCallbackCalled": false,
|
||||
"car": {
|
||||
"currentWaypoint": 0,
|
||||
"mode": 1,
|
||||
"positionX": -100.0,
|
||||
"positionY": -100.0,
|
||||
"positionZ": -100.0,
|
||||
"position": [
|
||||
-100.0,
|
||||
-100.0,
|
||||
-100.0
|
||||
],
|
||||
"rotation": 0.0,
|
||||
"steeringAngle": 0.0,
|
||||
"velocity": 0.0,
|
||||
"waypointReachRadius": 3.0,
|
||||
"waypoints": []
|
||||
},
|
||||
"taxiCarInitialState": {
|
||||
"currentWaypoint": 0,
|
||||
"mode": 1,
|
||||
"positionX": -100.0,
|
||||
"positionY": -100.0,
|
||||
"positionZ": -100.0,
|
||||
"rotation": 0.0,
|
||||
"steeringAngle": 0.0,
|
||||
"velocity": 0.0,
|
||||
"waypointReachRadius": 3.0,
|
||||
"waypoints": []
|
||||
},
|
||||
"taxiCarTriggerPositionX": 0.0,
|
||||
"taxiCarTriggerPositionY": 0.0,
|
||||
"taxiCarTriggerPositionZ": 0.0,
|
||||
"taxiDefaultWaypoints": []
|
||||
"waypoints": [],
|
||||
"wheelAngle": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"npcStates": [],
|
||||
"playerState": {
|
||||
"attackTargetIndex": -1,
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetIndex": -1,
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [],
|
||||
"position": [
|
||||
-6, 0, 12.8
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6, 0, 12.8
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-6, 0, 12.8
|
||||
]
|
||||
},
|
||||
"scriptGlobals": null,
|
||||
"teleportZoneState": {},
|
||||
"triggerZoneState": {}
|
||||
"teleportZones": {},
|
||||
"triggerZones": {}
|
||||
}
|
||||
},
|
||||
"money": 5500,
|
||||
"playerHp": 200.0,
|
||||
"playerMaxHp": 200.0,
|
||||
"players": {
|
||||
"player": {
|
||||
"animationFrame": 0.0,
|
||||
"attackTargetId": "",
|
||||
"attack_cooldown": 0.0,
|
||||
"battle_state": 0,
|
||||
"currentState": 0,
|
||||
"currentWaypointIndex": 0,
|
||||
"enabled": true,
|
||||
"faceTargetId": "",
|
||||
"facingAngle": 0.0,
|
||||
"homeDriftCheckTimer": 5.0,
|
||||
"homePosition": [
|
||||
0.0,
|
||||
0.0,
|
||||
0.0
|
||||
],
|
||||
"hp": 200.0,
|
||||
"pathWaypoints": [
|
||||
[
|
||||
-6.025001525878906,
|
||||
0.0,
|
||||
12.775001525878906
|
||||
]
|
||||
],
|
||||
"position": [
|
||||
-6.0,
|
||||
0.0,
|
||||
12.800000190734863
|
||||
],
|
||||
"requestedWalkTarget": [
|
||||
-6.0,
|
||||
0.0,
|
||||
12.800000190734863
|
||||
],
|
||||
"showWeapon": true,
|
||||
"targetFacingAngle": 0.0,
|
||||
"visibleRange": 6.0,
|
||||
"walkTarget": [
|
||||
-6.025001525878906,
|
||||
0.0,
|
||||
12.775001525878906
|
||||
]
|
||||
}
|
||||
},
|
||||
"questJournal": {
|
||||
"quests": []
|
||||
},
|
||||
"savedAt": "2026-07-24 23:40",
|
||||
"savedAt": "2026-10-28 23:24",
|
||||
"schemaVersion": 1,
|
||||
"taxiIsCalled": false,
|
||||
"tutorialJournalScreenOpened": false,
|
||||
"tutorialNeedOpenTaxiScreen": false,
|
||||
"tutorialPhoneChatScreenOpened": false,
|
||||
"tutorialStep": 9
|
||||
"tutorialStep": 0,
|
||||
"tutorialTaxiHintDone": false
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
-- uni_exterior level with static objects and navigation only.
|
||||
-- ============================================
|
||||
|
||||
function callback_init_location()
|
||||
end
|
||||
|
||||
function callback_enter_location()
|
||||
end
|
||||
|
||||
@@ -10,6 +13,7 @@ function callback_exit_location()
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_init_location,
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
+26
-27
@@ -2,18 +2,8 @@
|
||||
-- NPC PATROL WAYPOINTS
|
||||
-- ============================================
|
||||
|
||||
bathroom_door_opened = false
|
||||
player_door_opened = false
|
||||
alik_door_opened = false
|
||||
|
||||
|
||||
game_api.start_dialogue("dialog_start001")
|
||||
|
||||
phone_picked_up = false
|
||||
journal_picked_up = false
|
||||
|
||||
function on_npc_interact(npc_index)
|
||||
if npc_index == 0 then
|
||||
function on_npc_interact(npc_id)
|
||||
if npc_id == "npc_alik" then
|
||||
--[[
|
||||
local player_alik_aware = game_api.getIntValue("alik_aware")
|
||||
local player_container_aware = game_api.getIntValue("player_container_aware")
|
||||
@@ -25,7 +15,7 @@ function on_npc_interact(npc_index)
|
||||
else
|
||||
game_api.start_dialogue("dialog_alik001")
|
||||
end]]
|
||||
game_api.player_stop_and_rotate_to_npc(0)
|
||||
game_api.player_stop_and_rotate_to_npc("npc_alik")
|
||||
game_api.start_dialogue("dialog_alik002")
|
||||
end
|
||||
end
|
||||
@@ -119,8 +109,8 @@ end
|
||||
|
||||
function callback_deactivate_cover_alik_room()
|
||||
game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_S_2_001")
|
||||
game_api.set_npc_enabled(0, true)
|
||||
game_api.npc_rotate_to(0, 90)
|
||||
game_api.set_npc_enabled("npc_alik", true)
|
||||
game_api.npc_rotate_to("npc_alik", 90)
|
||||
end
|
||||
|
||||
function callback_deactivate_cover_bathroom()
|
||||
@@ -161,7 +151,7 @@ function closeAlikDoor()
|
||||
game_api.activate_interactive_object("Room_Cover_LivingRoom_W_S_2_001")
|
||||
game_api.set_object_alpha("Room_Cover_LivingRoom_W_S_2_001", 1)
|
||||
game_api.set_object_rotation("Door_Room_-1_-1_1_Leaf_001", 0)
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled("npc_alik", false)
|
||||
if (bathroom_door_opened) then
|
||||
game_api.switch_navigation(3) -- closed only alik (a)
|
||||
else
|
||||
@@ -330,11 +320,27 @@ function on_toilet_click()
|
||||
game_api.player_stop()
|
||||
end
|
||||
|
||||
function callback_init_location()
|
||||
bathroom_door_opened = false
|
||||
player_door_opened = false
|
||||
alik_door_opened = false
|
||||
phone_picked_up = false
|
||||
journal_picked_up = false
|
||||
|
||||
game_api.start_dialogue("dialog_start001")
|
||||
game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_N_2_001")
|
||||
game_api.rotate_object("Door_Utility_-1_1_2_Leaf_001", 90, 0.01, nil)
|
||||
game_api.rotate_object("Door_Utility_-1_-1_2_Leaf_001", -90, 0.01, nil)
|
||||
game_api.set_npc_enabled("npc_alik", false)
|
||||
game_api.switch_navigation(0)
|
||||
print("----callback_init_location----")
|
||||
end
|
||||
|
||||
function callback_enter_location()
|
||||
print("Enter location dorm")
|
||||
local hp = game_api.getFloatValue("player_hp")
|
||||
game_api.set_player_hp(hp)
|
||||
game_api.set_teleport_active(0, false)
|
||||
game_api.set_teleport_active("tp_dorm_to_loc2", false)
|
||||
if (game_api.is_night()) then
|
||||
closeAlikDoor()
|
||||
end
|
||||
@@ -347,25 +353,18 @@ function callback_exit_location()
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_init_location,
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
|
||||
function callback_call_taxi()
|
||||
game_api.set_teleport_active(0, true)
|
||||
game_api.set_trigger_zone_enabled(0, false)
|
||||
game_api.set_teleport_active("tp_dorm_to_loc2", true)
|
||||
game_api.set_trigger_zone_enabled("taxi_zone001", false)
|
||||
end
|
||||
|
||||
game_api.deactivate_interactive_object("Room_Cover_LivingRoom_W_N_2_001")
|
||||
|
||||
game_api.rotate_object("Door_Utility_-1_1_2_Leaf_001", 90, 0.01, nil)
|
||||
game_api.rotate_object("Door_Utility_-1_-1_2_Leaf_001", -90, 0.01, nil)
|
||||
|
||||
game_api.set_call_taxi_callback(
|
||||
callback_call_taxi
|
||||
)
|
||||
|
||||
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.switch_navigation(0)
|
||||
print("Lua script loaded successfully--!")
|
||||
@@ -1,16 +1,5 @@
|
||||
print("Lua script loaded successfully!")
|
||||
|
||||
function callback_enter_location()
|
||||
end
|
||||
|
||||
function callback_exit_location()
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
|
||||
function darklands_exit001_enter_callback()
|
||||
if game_api.is_darklands() then
|
||||
print("Lua script start_darklands_transition")
|
||||
@@ -77,11 +66,14 @@ game_api.set_enter_night_callback(
|
||||
)
|
||||
|
||||
|
||||
function callback_init_location()
|
||||
end
|
||||
|
||||
function callback_enter_location()
|
||||
print("Enter location uni exterior")
|
||||
local hp = game_api.getFloatValue("player_hp")
|
||||
game_api.set_player_hp(hp)
|
||||
game_api.set_teleport_active(0, false)
|
||||
game_api.set_teleport_active("tp_loc2_to_dorm", false)
|
||||
if (game_api.is_night()) then
|
||||
game_api.deactivate_interactive_object("car001")
|
||||
game_api.deactivate_interactive_object("car001wheels")
|
||||
@@ -100,6 +92,7 @@ function callback_exit_location()
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_init_location,
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
@@ -108,10 +101,6 @@ game_api.set_trigger_zone_callbacks("darklands_exit001",
|
||||
darklands_exit001_enter_callback,
|
||||
nil
|
||||
)
|
||||
game_api.set_trigger_zone_callbacks("darklands_exit002",
|
||||
darklands_exit001_enter_callback,
|
||||
nil
|
||||
)
|
||||
game_api.set_trigger_zone_callbacks("darklands_exit003",
|
||||
darklands_exit001_enter_callback,
|
||||
nil
|
||||
@@ -129,12 +118,12 @@ game_api.set_trigger_zone_callbacks("darklands_exit005",
|
||||
|
||||
function taxi_zone001_enter_callback()
|
||||
game_api.start_dialogue("dialog_taxi001")
|
||||
game_api.set_trigger_zone_enabled(0, false)
|
||||
game_api.set_trigger_zone_enabled("taxi_zone001", false)
|
||||
end
|
||||
|
||||
function callback_call_taxi()
|
||||
game_api.set_teleport_active(0, true)
|
||||
game_api.set_trigger_zone_enabled(0, false)
|
||||
game_api.set_teleport_active("tp_loc2_to_dorm", true)
|
||||
game_api.set_trigger_zone_enabled("taxi_zone001", false)
|
||||
end
|
||||
|
||||
game_api.set_call_taxi_callback(
|
||||
|
||||
+129
-136
@@ -1,14 +1,6 @@
|
||||
CONST_MSG_EMPTY = "(пусто)"
|
||||
CONST_MSG_WHERE = "Бекзат, ты где? Я жду..."
|
||||
|
||||
hall_door_opened = false
|
||||
teacher_door_opened = false
|
||||
|
||||
player_hold_book = false
|
||||
teacher_arrived_to_library = false
|
||||
teacher_told_about_book = false
|
||||
|
||||
|
||||
--[[
|
||||
0 - day0
|
||||
1 - darklands
|
||||
@@ -17,14 +9,6 @@ teacher_told_about_book = false
|
||||
4 - day
|
||||
|
||||
]]
|
||||
was_state_when_player_left = 0
|
||||
was_day_when_player_left = 0
|
||||
|
||||
morning_can_open_door_index = 0
|
||||
morning_did_open_door_index = 0
|
||||
|
||||
|
||||
ghost_gone = false
|
||||
|
||||
function dialog_aiperi_give_key()
|
||||
local player_hold_key = game_api.getIntValue("player_hold_key")
|
||||
@@ -65,7 +49,7 @@ function knife_dialog_zone001_enter_callback()
|
||||
if (player_hold_knife == 0) then
|
||||
local lection_is_over = game_api.getIntValue("lection_is_over")
|
||||
if lection_is_over == 1 then
|
||||
game_api.npc_stop_and_rotate_to_player(1)
|
||||
game_api.npc_stop_and_rotate_to_player("npc_02_woman")
|
||||
if (player_hold_key == 1) then
|
||||
game_api.start_dialogue("knife_dialog002")
|
||||
else
|
||||
@@ -79,7 +63,7 @@ function knife_dialog_zone001_enter_callback()
|
||||
if (aiperi_talked_after_knife == 0) then
|
||||
game_api.start_dialogue("knife_dialog_second001")
|
||||
end
|
||||
game_api.set_trigger_zone_enabled(1, false)
|
||||
game_api.set_trigger_zone_enabled("knife_dialog_zone001", false)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -110,11 +94,11 @@ game_api.set_trigger_zone_callbacks("knife_dialog_zone001",
|
||||
function on_knife_pickup()
|
||||
game_api.pickup_item("knife")
|
||||
game_api.deactivate_interactive_object("Knife001")
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.setIntValue("player_hold_knife", 1)
|
||||
|
||||
game_api.set_trigger_zone_enabled(2, true)
|
||||
game_api.npc_walk_to(0, -4.57412, 0, 6.78495, on_teacher_arrived2)
|
||||
game_api.set_trigger_zone_enabled("teacher_dialog_zone001", true)
|
||||
game_api.npc_walk_to("npc_teacher01", -4.57412, 0, 6.78495, on_teacher_arrived2)
|
||||
game_api.quest_set_objective_completed("aiperi_knife", "aiperi_knife_take")
|
||||
game_api.player_stop()
|
||||
end
|
||||
@@ -133,14 +117,14 @@ function on_book_pickup()
|
||||
player_hold_book = true
|
||||
game_api.quest_set_objective_completed("study_project", "study_project_book")
|
||||
game_api.start_dialogue("book_dialog003")
|
||||
--game_api.set_trigger_zone_enabled(3, true)
|
||||
--game_api.set_trigger_zone_enabled("book_dialog_zone001", true)
|
||||
end
|
||||
else
|
||||
game_api.remove_item("book")
|
||||
game_api.activate_interactive_object("Book001")
|
||||
player_hold_book = false
|
||||
game_api.start_dialogue("book_dialog004")
|
||||
--game_api.set_trigger_zone_enabled(3, false)
|
||||
--game_api.set_trigger_zone_enabled("book_dialog_zone001", false)
|
||||
end
|
||||
else
|
||||
print("on_book_pickup step2")
|
||||
@@ -218,14 +202,14 @@ function on_cake_clicked()
|
||||
game_api.player_stop()
|
||||
end
|
||||
|
||||
function on_npc_interact(npc_index)
|
||||
function on_npc_interact(npc_id)
|
||||
local player_ghost_aware = game_api.getIntValue("ghost_aware")
|
||||
local player_hold_key = game_api.getIntValue("player_hold_key")
|
||||
local day = game_api.getIntValue("day")
|
||||
|
||||
print("[Lua] NPC interaction! Index: " .. tostring(npc_index))
|
||||
if npc_index == 1 then
|
||||
game_api.player_stop_and_rotate_to_npc(1)
|
||||
print("[Lua] NPC interaction! ID: " .. tostring(npc_id))
|
||||
if npc_id == "npc_02_woman" then
|
||||
game_api.player_stop_and_rotate_to_npc("npc_02_woman")
|
||||
if (game_api.is_dawn()) or (day > 0) then
|
||||
game_api.start_dialogue("dialog_aiperi_morning001")
|
||||
else
|
||||
@@ -236,8 +220,8 @@ function on_npc_interact(npc_index)
|
||||
end
|
||||
end
|
||||
end
|
||||
if npc_index == 0 then
|
||||
game_api.player_stop_and_rotate_to_npc(0)
|
||||
if npc_id == "npc_teacher01" then
|
||||
game_api.player_stop_and_rotate_to_npc("npc_teacher01")
|
||||
if (day == 0) then
|
||||
if (teacher_arrived_to_library) then
|
||||
if (teacher_told_about_book) then
|
||||
@@ -264,8 +248,8 @@ function on_npc_interact(npc_index)
|
||||
end
|
||||
end
|
||||
end
|
||||
if npc_index == 2 then
|
||||
game_api.player_stop_and_rotate_to_npc(2)
|
||||
if npc_id == "npc_04_ghost" then
|
||||
game_api.player_stop_and_rotate_to_npc("npc_04_ghost")
|
||||
local report_card_signed = game_api.getIntValue("report_card_signed")
|
||||
if (report_card_signed == 1) then
|
||||
game_api.start_dialogue("dialog_with_ghost003")
|
||||
@@ -273,18 +257,18 @@ function on_npc_interact(npc_index)
|
||||
game_api.start_dialogue("dialog_with_ghost001")
|
||||
end
|
||||
end
|
||||
if npc_index == 6 then
|
||||
game_api.player_stop_and_rotate_to_npc(6)
|
||||
if npc_id == "npc_altynai" then
|
||||
game_api.player_stop_and_rotate_to_npc("npc_altynai")
|
||||
game_api.start_dialogue("dialog_female_student002")
|
||||
end
|
||||
end
|
||||
|
||||
function on_quest_over()
|
||||
ghost_gone = true
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.quest_set_objective_completed("ghost_release", "ghost_release_show")
|
||||
|
||||
game_api.start_cutscene("game_complete_cutscene001")
|
||||
@@ -297,7 +281,7 @@ end
|
||||
|
||||
function teacher_zone001_enter_callback()
|
||||
game_api.start_dialogue("teacher_dialog002")
|
||||
game_api.set_trigger_zone_enabled(2, false)
|
||||
game_api.set_trigger_zone_enabled("teacher_dialog_zone001", false)
|
||||
teacher_told_about_book = true
|
||||
game_api.player_stop()
|
||||
end
|
||||
@@ -474,9 +458,9 @@ function on_hall_door_click()
|
||||
game_api.switch_navigation(1)
|
||||
game_api.rotate_object("Hall_Leaf001", 90, 0.5, nil)
|
||||
game_api.fade_object("Room_Cover_Main_Hall_001", 0, 0.5, callback_deactivate_cover_main_hall)
|
||||
game_api.set_npc_enabled(0, true)
|
||||
game_api.set_npc_enabled(6, true)
|
||||
game_api.npc_rotate_to(6, 180)
|
||||
game_api.set_npc_enabled("npc_teacher01", true)
|
||||
game_api.set_npc_enabled("npc_altynai", true)
|
||||
game_api.npc_rotate_to("npc_altynai", 180)
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
game_api.player_stop()
|
||||
end
|
||||
@@ -558,22 +542,22 @@ function on_n2_door_click()
|
||||
end
|
||||
|
||||
function on_teacher_arrived()
|
||||
game_api.npc_rotate_to(0, 180)
|
||||
game_api.npc_rotate_to("npc_teacher01", 180)
|
||||
teacher_arrived_to_library = true
|
||||
end
|
||||
|
||||
function on_student_arrived()
|
||||
game_api.npc_rotate_to(6, 270)
|
||||
game_api.npc_rotate_to("npc_altynai", 270)
|
||||
end
|
||||
|
||||
function on_teacher_arrived2()
|
||||
game_api.npc_rotate_to(0, 0)
|
||||
game_api.npc_rotate_to("npc_teacher01", 0)
|
||||
end
|
||||
|
||||
function book_dialog_zone001_enter_callback()
|
||||
if (game_api.is_darklands()) then
|
||||
game_api.start_dialogue("ghost_dialog002")
|
||||
game_api.set_trigger_zone_enabled(3, false)
|
||||
game_api.set_trigger_zone_enabled("book_dialog_zone001", false)
|
||||
game_api.advance_darklands_hud()
|
||||
else
|
||||
if (player_hold_book) and (game_api.is_night() == false) then
|
||||
@@ -621,9 +605,9 @@ function on_sleep_cutscene()
|
||||
print("Cutscene 2 done!")
|
||||
|
||||
game_api.set_night()
|
||||
--game_api.set_trigger_zone_enabled(1, false)
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
--game_api.set_trigger_zone_enabled("knife_dialog_zone001", false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
game_api.switch_navigation(16)
|
||||
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
||||
game_api.set_object_rotation("Hall_Leaf001", 0)
|
||||
@@ -649,14 +633,14 @@ function on_teacher_arrived_intermediate()
|
||||
game_api.rotate_object("Room_N_2_Leaf001", -90, 0.5, nil)
|
||||
game_api.fade_object("Room_Cover_North_3_001", 0, 0.5, callback_deactivate_cover_n3)
|
||||
game_api.switch_navigation(2)
|
||||
game_api.npc_walk_to(0, 4.94194, 0, 6.64111, on_teacher_arrived)
|
||||
game_api.npc_walk_to("npc_teacher01", 4.94194, 0, 6.64111, on_teacher_arrived)
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
end
|
||||
|
||||
function on_lection_cutscene_fadein_callback()
|
||||
print("Lection cutscene in progress!")
|
||||
-- -0.0234149 0 10.1139
|
||||
game_api.npc_set_position(0, 0.0, 0.0, 10.1139)
|
||||
game_api.npc_set_position("npc_teacher01", 0.0, 0.0, 10.1139)
|
||||
|
||||
--4.91002 0 10.375
|
||||
--game_api.player_set_position(-2.41002, 0, 10.375)
|
||||
@@ -670,10 +654,10 @@ function callback_on_lection_cutscene_over()
|
||||
print("Cutscene done!")
|
||||
game_api.setIntValue("lection_is_over", 1)
|
||||
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_rotate_to(1, 180)
|
||||
game_api.npc_walk_to(0, 0.758123, 0, 6.50063, on_teacher_arrived_intermediate)
|
||||
game_api.npc_walk_to(6, 5.28998, 0, -5.525, on_student_arrived)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_rotate_to("npc_02_woman", 180)
|
||||
game_api.npc_walk_to("npc_teacher01", 0.758123, 0, 6.50063, on_teacher_arrived_intermediate)
|
||||
game_api.npc_walk_to("npc_altynai", 5.28998, 0, -5.525, on_student_arrived)
|
||||
|
||||
local aiperi_knife_aware = game_api.getIntValue("aiperi_knife_aware")
|
||||
|
||||
@@ -715,38 +699,36 @@ function on_bookshelf_teacher_clicked()
|
||||
on_report_card_pickup()
|
||||
end
|
||||
|
||||
first_time_darklands = true
|
||||
|
||||
function callback_enter_darklands()
|
||||
if (first_time_darklands) then
|
||||
game_api.start_dialogue("ghost_dialog001")
|
||||
first_time_darklands = false
|
||||
end
|
||||
|
||||
game_api.set_trigger_zone_enabled(1, false)
|
||||
game_api.set_trigger_zone_enabled("knife_dialog_zone001", false)
|
||||
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
|
||||
if (ghost_gone) then
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
else
|
||||
game_api.set_npc_enabled(2, true)
|
||||
game_api.set_npc_enabled(3, true)
|
||||
game_api.set_npc_enabled(4, true)
|
||||
game_api.set_npc_enabled(5, true)
|
||||
game_api.set_npc_enabled("npc_04_ghost", true)
|
||||
game_api.set_npc_enabled("ghost_01x", true)
|
||||
game_api.set_npc_enabled("ghost_02x", true)
|
||||
game_api.set_npc_enabled("ghost_03x", true)
|
||||
|
||||
game_api.npc_set_hp(3, 35)
|
||||
game_api.npc_set_hp(4, 35)
|
||||
game_api.npc_set_hp(5, 35)
|
||||
game_api.npc_set_hp("ghost_01x", 35)
|
||||
game_api.npc_set_hp("ghost_02x", 35)
|
||||
game_api.npc_set_hp("ghost_03x", 35)
|
||||
|
||||
game_api.npc_set_position(3, 0, 0, -6.4)
|
||||
game_api.npc_set_position(4, -5.0, 0, 12)
|
||||
game_api.npc_set_position(5, 5.0, 0, 12)
|
||||
game_api.npc_set_position("ghost_01x", 0, 0, -6.4)
|
||||
game_api.npc_set_position("ghost_02x", -5.0, 0, 12)
|
||||
game_api.npc_set_position("ghost_03x", 5.0, 0, 12)
|
||||
end
|
||||
|
||||
game_api.switch_navigation(5)
|
||||
@@ -755,13 +737,13 @@ end
|
||||
|
||||
function callback_exit_darklands()
|
||||
game_api.start_cutscene("darklands_exit001")
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
|
||||
morning_did_open_door_index = 0
|
||||
setDay1MorningSetup()
|
||||
@@ -873,13 +855,13 @@ function setDay0setup()
|
||||
|
||||
game_api.deactivate_interactive_object("Note001")
|
||||
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
|
||||
game_api.activate_interactive_object("car001")
|
||||
game_api.activate_interactive_object("car001wheels")
|
||||
@@ -903,7 +885,6 @@ function setDay1setup()
|
||||
game_api.activate_interactive_object("Room_Cover_South_2_001")
|
||||
game_api.deactivate_interactive_object("Room_Cover_South_3_001")
|
||||
|
||||
game_api.set_object_rotation("Room_N_0_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_N_1_Leaf001", -90)
|
||||
game_api.set_object_rotation("Room_N_2_Leaf001", -90)
|
||||
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
||||
@@ -911,15 +892,15 @@ function setDay1setup()
|
||||
game_api.set_object_rotation("Room_S_2_Leaf001", 90)
|
||||
game_api.set_object_rotation("Hall_Leaf001", 90)
|
||||
|
||||
game_api.set_npc_enabled(0, true)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled(6, true)
|
||||
game_api.set_npc_enabled("npc_teacher01", true)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.set_npc_enabled("npc_altynai", true)
|
||||
|
||||
game_api.npc_set_position(1, 0.98998, 0, -7.525)
|
||||
game_api.npc_set_position("npc_02_woman", 0.98998, 0, -7.525)
|
||||
|
||||
game_api.deactivate_interactive_object("Cake001")
|
||||
|
||||
@@ -939,7 +920,6 @@ function setDay1MorningSetup()
|
||||
game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
|
||||
game_api.activate_interactive_object("Room_Cover_Main_Hall_001")
|
||||
|
||||
game_api.set_object_rotation("Room_N_0_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_N_1_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
||||
@@ -964,15 +944,15 @@ function setDay1MorningSetup()
|
||||
|
||||
-- TODO: Make check for teacher_door_opened false or true
|
||||
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
|
||||
game_api.npc_set_position(1, 0.98998, 0, -7.525)
|
||||
game_api.npc_set_position("npc_02_woman", 0.98998, 0, -7.525)
|
||||
|
||||
if morning_did_open_door_index == 1 then
|
||||
game_api.deactivate_interactive_object("Room_Cover_South_1_001")
|
||||
@@ -1043,7 +1023,6 @@ function setDay1NightSetup()
|
||||
game_api.deactivate_interactive_object("Room_Cover_Corridor_001")
|
||||
game_api.activate_interactive_object("Room_Cover_Main_Hall_001")
|
||||
|
||||
game_api.set_object_rotation("Room_N_0_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_N_1_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_N_2_Leaf001", 0)
|
||||
game_api.set_object_rotation("Room_S_0_Leaf001", 0)
|
||||
@@ -1059,13 +1038,13 @@ function setDay1NightSetup()
|
||||
game_api.set_object_alpha("Room_Cover_Corridor_001", 1)
|
||||
game_api.set_object_alpha("Room_Cover_Main_Hall_001", 1)
|
||||
|
||||
game_api.set_npc_enabled(0, false)
|
||||
game_api.set_npc_enabled(1, false)
|
||||
game_api.set_npc_enabled(2, false)
|
||||
game_api.set_npc_enabled(3, false)
|
||||
game_api.set_npc_enabled(4, false)
|
||||
game_api.set_npc_enabled(5, false)
|
||||
game_api.set_npc_enabled(6, false)
|
||||
game_api.set_npc_enabled("npc_teacher01", false)
|
||||
game_api.set_npc_enabled("npc_02_woman", false)
|
||||
game_api.set_npc_enabled("npc_04_ghost", false)
|
||||
game_api.set_npc_enabled("ghost_01x", false)
|
||||
game_api.set_npc_enabled("ghost_02x", false)
|
||||
game_api.set_npc_enabled("ghost_03x", false)
|
||||
game_api.set_npc_enabled("npc_altynai", false)
|
||||
|
||||
game_api.deactivate_interactive_object("car001")
|
||||
game_api.deactivate_interactive_object("car001wheels")
|
||||
@@ -1222,8 +1201,8 @@ game_api.set_chat_callbacks(
|
||||
)
|
||||
|
||||
function callback_aiperi_dialog_morning()
|
||||
game_api.player_stop_and_rotate_to_npc(1)
|
||||
game_api.npc_stop_and_rotate_to_player(1)
|
||||
game_api.player_stop_and_rotate_to_npc("npc_02_woman")
|
||||
game_api.npc_stop_and_rotate_to_player("npc_02_woman")
|
||||
game_api.start_dialogue("dialog_aiperi_morning001")
|
||||
end
|
||||
|
||||
@@ -1246,9 +1225,9 @@ function on_aiperi_opens_door()
|
||||
end
|
||||
|
||||
--Aiperi incoming
|
||||
game_api.npc_set_position(1, 0, 0, 6.5)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_walk_to(1, 0.0, 0, 9.1, callback_aiperi_dialog_morning)
|
||||
game_api.npc_set_position("npc_02_woman", 0, 0, 6.5)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_walk_to("npc_02_woman", 0.0, 0, 9.1, callback_aiperi_dialog_morning)
|
||||
game_api.player_walk_to(0.0, 0.0, 10.0, nil)
|
||||
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
@@ -1268,9 +1247,9 @@ function on_aiperi_opens_door()
|
||||
end
|
||||
|
||||
--Aiperi incoming
|
||||
game_api.npc_set_position(1, 0.0, 0, 6.43818)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_walk_to(1, 2.34349, 0, 6.31229, callback_aiperi_dialog_morning)
|
||||
game_api.npc_set_position("npc_02_woman", 0.0, 0, 6.43818)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_walk_to("npc_02_woman", 2.34349, 0, 6.31229, callback_aiperi_dialog_morning)
|
||||
game_api.player_walk_to(3.52174, 0, 6.28993, nil)
|
||||
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
@@ -1290,9 +1269,9 @@ function on_aiperi_opens_door()
|
||||
end
|
||||
|
||||
--Aiperi incoming
|
||||
game_api.npc_set_position(1, 0, 0, -1.7911)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_walk_to(1, 2.11898, 0, -1.7074, callback_aiperi_dialog_morning)
|
||||
game_api.npc_set_position("npc_02_woman", 0, 0, -1.7911)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_walk_to("npc_02_woman", 2.11898, 0, -1.7074, callback_aiperi_dialog_morning)
|
||||
game_api.player_walk_to(3.36055, 0, -2.0345, nil)
|
||||
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
@@ -1312,9 +1291,9 @@ function on_aiperi_opens_door()
|
||||
game_api.switch_navigation(9)
|
||||
end
|
||||
--Aiperi incoming
|
||||
game_api.npc_set_position(1, 0, 0, -1.7911)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_walk_to(1, -2.454, 0, -1.61871, callback_aiperi_dialog_morning)
|
||||
game_api.npc_set_position("npc_02_woman", 0, 0, -1.7911)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_walk_to("npc_02_woman", -2.454, 0, -1.61871, callback_aiperi_dialog_morning)
|
||||
game_api.player_walk_to(-3.63363, 0, -1.50727, nil)
|
||||
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
@@ -1334,9 +1313,9 @@ function on_aiperi_opens_door()
|
||||
end
|
||||
|
||||
--Aiperi incoming
|
||||
game_api.npc_set_position(1, 0, 0, -9.77948)
|
||||
game_api.set_npc_enabled(1, true)
|
||||
game_api.npc_walk_to(1, -2.53977, 0, -9.77852, callback_aiperi_dialog_morning)
|
||||
game_api.npc_set_position("npc_02_woman", 0, 0, -9.77948)
|
||||
game_api.set_npc_enabled("npc_02_woman", true)
|
||||
game_api.npc_walk_to("npc_02_woman", -2.53977, 0, -9.77852, callback_aiperi_dialog_morning)
|
||||
game_api.player_walk_to(-3.64494, 0, -9.66711, nil)
|
||||
|
||||
game_api.play_sound("audio/702171__foxfire__door-opening.ogg")
|
||||
@@ -1345,7 +1324,24 @@ function on_aiperi_opens_door()
|
||||
end
|
||||
|
||||
function on_aiperi_morning_dialog_finished()
|
||||
game_api.npc_walk_to(1, 0.98998, 0, -7.525, nil)
|
||||
game_api.npc_walk_to("npc_02_woman", 0.98998, 0, -7.525, nil)
|
||||
end
|
||||
|
||||
function callback_init_location()
|
||||
hall_door_opened = false
|
||||
teacher_door_opened = false
|
||||
player_hold_book = false
|
||||
teacher_arrived_to_library = false
|
||||
teacher_told_about_book = false
|
||||
was_state_when_player_left = 0
|
||||
was_day_when_player_left = 0
|
||||
morning_can_open_door_index = 0
|
||||
morning_did_open_door_index = 0
|
||||
ghost_gone = false
|
||||
first_time_darklands = true
|
||||
|
||||
setDay0setup()
|
||||
game_api.switch_navigation(0)
|
||||
end
|
||||
|
||||
function callback_enter_location()
|
||||
@@ -1437,6 +1433,7 @@ function callback_exit_location()
|
||||
end
|
||||
|
||||
game_api.set_location_callbacks(
|
||||
callback_init_location,
|
||||
callback_enter_location,
|
||||
callback_exit_location
|
||||
)
|
||||
@@ -1467,9 +1464,5 @@ end
|
||||
|
||||
game_api.set_cutscene_callback("game_complete_cutscene001", callback_game_complete_cutscene)
|
||||
|
||||
|
||||
|
||||
setDay0setup()
|
||||
game_api.switch_navigation(0)
|
||||
--debugAllOpen()
|
||||
print("Lua script loaded successfully!---!")
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"objects": [
|
||||
{
|
||||
"id": "intBin_Cans",
|
||||
"name": "intBin_Cans",
|
||||
"texturePath": "resources/w/shelter/models/intBin_Cans_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intBin_Cans.txt.bin",
|
||||
@@ -14,6 +15,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intBin_Plastic",
|
||||
"name": "intBin_Plastic",
|
||||
"texturePath": "resources/w/shelter/models/intBin_Plastic_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intBin_Plastic.txt.bin",
|
||||
@@ -27,6 +29,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intBin_Waste_A",
|
||||
"name": "intBin_Waste_A",
|
||||
"texturePath": "resources/w/shelter/models/intBin_Waste_A_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intBin_Waste_A.txt.bin",
|
||||
@@ -40,6 +43,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intBin_Waste_B",
|
||||
"name": "intBin_Waste_B",
|
||||
"texturePath": "resources/w/shelter/models/intBin_Waste_B_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intBin_Waste_B.txt.bin",
|
||||
@@ -53,6 +57,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intRoof",
|
||||
"name": "intRoof",
|
||||
"texturePath": "resources/w/shelter/models/intRoof_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intRoof.txt",
|
||||
@@ -66,6 +71,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intVending.00",
|
||||
"name": "intVending.00",
|
||||
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intVending.txt",
|
||||
@@ -79,6 +85,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intVending.01",
|
||||
"name": "intVending.01",
|
||||
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intVending.txt",
|
||||
@@ -92,6 +99,7 @@
|
||||
"interactionRadius": 0.0
|
||||
},
|
||||
{
|
||||
"id": "intVending.02",
|
||||
"name": "intVending.02",
|
||||
"texturePath": "resources/w/shelter/models/vending_atlas.png",
|
||||
"meshPath": "resources/w/shelter/models/intVending.txt",
|
||||
|
||||
+116
-450
@@ -9,10 +9,11 @@
|
||||
#include "render/TextureManager.h"
|
||||
#include "utils/Utils.h"
|
||||
#include "TextModel.h"
|
||||
#include "game_server/IGameServer.h"
|
||||
#include "navigation/PathFinder.h"
|
||||
#include "Location.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
const float ATTACK_COOLDOWN_TIME = 1.6f;
|
||||
extern float x;
|
||||
extern float y;
|
||||
|
||||
@@ -26,6 +27,8 @@ void Character::loadAnimation(AnimationState animState, const std::string& filen
|
||||
} else {
|
||||
data.totalFrames = 1;
|
||||
}
|
||||
gameServer_.submit(SetCharacterAnimationMetadataCommand{
|
||||
locationId_, entityId_, animState, data.model.startingFrame, data.totalFrames});
|
||||
}
|
||||
|
||||
void Character::loadBinaryAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile) {
|
||||
@@ -36,11 +39,14 @@ void Character::loadBinaryAnimation(AnimationState animState, const std::string&
|
||||
} else {
|
||||
data.totalFrames = 1;
|
||||
}
|
||||
gameServer_.submit(SetCharacterAnimationMetadataCommand{
|
||||
locationId_, entityId_, animState, data.model.startingFrame, data.totalFrames});
|
||||
}
|
||||
|
||||
std::unique_ptr<Character> Character::createFromState(CharacterState st, Renderer& renderer, const char* zipFile) {
|
||||
auto c = std::make_unique<Character>();
|
||||
c->state = std::move(st);
|
||||
std::unique_ptr<Character> Character::createFromState(const CharacterState& st, IGameServer& server,
|
||||
const std::string& locationId, const std::string& entityId,
|
||||
Renderer& renderer, const char* zipFile) {
|
||||
auto c = std::make_unique<Character>(st, server, locationId, entityId);
|
||||
|
||||
const std::string zip = zipFile ? zipFile : "";
|
||||
|
||||
@@ -56,7 +62,7 @@ std::unique_ptr<Character> Character::createFromState(CharacterState st, Rendere
|
||||
}
|
||||
};
|
||||
|
||||
for (const auto& [animState, path] : c->state.creationInfo.animationPaths)
|
||||
for (const auto& [animState, path] : c->characterState.creationInfo.animationPaths)
|
||||
loadAnim(animState, path);
|
||||
|
||||
auto loadTex = [&](const std::string& path) -> std::shared_ptr<Texture> {
|
||||
@@ -70,27 +76,27 @@ std::unique_ptr<Character> Character::createFromState(CharacterState st, Rendere
|
||||
}
|
||||
};
|
||||
|
||||
if (!c->state.creationInfo.singleTexturePath.empty()) {
|
||||
auto tex = loadTex(c->state.creationInfo.singleTexturePath);
|
||||
if (!c->characterState.creationInfo.singleTexturePath.empty()) {
|
||||
auto tex = loadTex(c->characterState.creationInfo.singleTexturePath);
|
||||
if (tex) c->setTexture(tex);
|
||||
}
|
||||
for (const auto& [meshName, texPath] : c->state.creationInfo.meshTexturePaths) {
|
||||
for (const auto& [meshName, texPath] : c->characterState.creationInfo.meshTexturePaths) {
|
||||
auto tex = loadTex(texPath);
|
||||
if (tex) c->setTexture(meshName, tex);
|
||||
}
|
||||
|
||||
if (!c->state.creationInfo.weaponMeshPath.empty()) {
|
||||
const std::string& wp = c->state.creationInfo.weaponMeshPath;
|
||||
if (!c->characterState.creationInfo.weaponMeshPath.empty()) {
|
||||
const std::string& wp = c->characterState.creationInfo.weaponMeshPath;
|
||||
try {
|
||||
if (wp.size() >= 4 && wp.substr(wp.size() - 4) == ".bin")
|
||||
c->weaponMesh.data = LoadModelFromBinFile(wp, zip);
|
||||
else
|
||||
c->weaponMesh.data = LoadFromTextFile02(wp, zip);
|
||||
|
||||
if (c->state.creationInfo.weaponMeshMove != Eigen::Vector3f::Zero())
|
||||
c->weaponMesh.data.Move(c->state.creationInfo.weaponMeshMove);
|
||||
if (c->state.creationInfo.weaponMeshScale != 1.0f)
|
||||
c->weaponMesh.data.Scale(c->state.creationInfo.weaponMeshScale);
|
||||
if (c->characterState.creationInfo.weaponMeshMove != Eigen::Vector3f::Zero())
|
||||
c->weaponMesh.data.Move(c->characterState.creationInfo.weaponMeshMove);
|
||||
if (c->characterState.creationInfo.weaponMeshScale != 1.0f)
|
||||
c->weaponMesh.data.Scale(c->characterState.creationInfo.weaponMeshScale);
|
||||
|
||||
c->weaponMesh.RefreshVBO();
|
||||
} catch (const std::exception& e) {
|
||||
@@ -98,100 +104,14 @@ std::unique_ptr<Character> Character::createFromState(CharacterState st, Rendere
|
||||
}
|
||||
}
|
||||
|
||||
c->weaponTexture = loadTex(c->state.creationInfo.weaponTexturePath);
|
||||
c->weaponAttachBoneName = c->state.creationInfo.weaponAttachBoneName;
|
||||
c->weaponInitialPosition = c->state.creationInfo.weaponInitialPosition;
|
||||
c->weaponInitialRotation = c->state.creationInfo.weaponInitialRotation.toRotationMatrix();
|
||||
c->weaponTexture = loadTex(c->characterState.creationInfo.weaponTexturePath);
|
||||
c->weaponAttachBoneName = c->characterState.creationInfo.weaponAttachBoneName;
|
||||
c->weaponInitialPosition = c->characterState.creationInfo.weaponInitialPosition;
|
||||
c->weaponInitialRotation = c->characterState.creationInfo.weaponInitialRotation.toRotationMatrix();
|
||||
|
||||
c->setTarget(c->state.position);
|
||||
return c;
|
||||
}
|
||||
|
||||
void Character::setTarget(const Eigen::Vector3f& target, std::function<void()> onArrived, std::string callbackName) {
|
||||
Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
|
||||
|
||||
const bool sameRequestedTarget =
|
||||
(normalizedTarget - state.requestedWalkTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
const bool alreadyMovingToTarget =
|
||||
!state.pathWaypoints.empty() || (state.walkTarget - normalizedTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
const bool stoppedAfterFailedPath =
|
||||
state.pathWaypoints.empty() && (state.walkTarget - state.position).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
|
||||
if (!onArrived && sameRequestedTarget && (alreadyMovingToTarget || stoppedAfterFailedPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.requestedWalkTarget = normalizedTarget;
|
||||
state.onArrivedCallback = std::move(onArrived);
|
||||
state.onArrivedCallbackName = std::move(callbackName);
|
||||
|
||||
if (pathPlanner) {
|
||||
state.pathWaypoints = pathPlanner(state.position, normalizedTarget);
|
||||
state.currentWaypointIndex = 0;
|
||||
|
||||
if (!state.pathWaypoints.empty()) {
|
||||
for (Eigen::Vector3f& waypoint : state.pathWaypoints) {
|
||||
waypoint.y() = 0.f;
|
||||
}
|
||||
state.walkTarget = state.pathWaypoints.back();
|
||||
return;
|
||||
}
|
||||
|
||||
state.walkTarget = Eigen::Vector3f(state.position.x(), 0.f, state.position.z());
|
||||
state.onArrivedCallback = nullptr;
|
||||
state.onArrivedCallbackName.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
state.pathWaypoints.clear();
|
||||
state.currentWaypointIndex = 0;
|
||||
state.walkTarget = normalizedTarget;
|
||||
}
|
||||
|
||||
void Character::stopAndRotateToTarget(int npcId)
|
||||
{
|
||||
state.onArrivedCallback = nullptr;
|
||||
state.onArrivedCallbackName = "";
|
||||
state.walkTarget = state.position;
|
||||
state.requestedWalkTarget = state.position;
|
||||
state.faceTargetIndex = npcId;
|
||||
state.pathWaypoints.clear();
|
||||
state.currentWaypointIndex = 0;
|
||||
}
|
||||
|
||||
void Character::setPathPlanner(PathPlanner planner) {
|
||||
pathPlanner = std::move(planner);
|
||||
}
|
||||
|
||||
void Character::resetPlayerAfterDeath() {
|
||||
if (!state.isPlayer) return;
|
||||
state.currentState = AnimationState::STAND;
|
||||
state.resetAnim = true;
|
||||
state.showWeapon = false;
|
||||
state.battle_state = 0;
|
||||
state.attack = 0;
|
||||
}
|
||||
|
||||
void Character::forceReplan() {
|
||||
if (!pathPlanner) return;
|
||||
|
||||
const Eigen::Vector3f normalizedTarget(state.requestedWalkTarget.x(), 0.f, state.requestedWalkTarget.z());
|
||||
state.pathWaypoints = pathPlanner(state.position, normalizedTarget);
|
||||
state.currentWaypointIndex = 0;
|
||||
|
||||
if (!state.pathWaypoints.empty()) {
|
||||
for (Eigen::Vector3f& waypoint : state.pathWaypoints) {
|
||||
waypoint.y() = 0.f;
|
||||
}
|
||||
state.walkTarget = state.pathWaypoints.back();
|
||||
return;
|
||||
}
|
||||
|
||||
state.walkTarget = Eigen::Vector3f(state.position.x(), 0.f, state.position.z());
|
||||
state.onArrivedCallback = nullptr;
|
||||
state.onArrivedCallbackName.clear();
|
||||
}
|
||||
|
||||
void Character::setTexture(std::shared_ptr<FRG::Texture> texture) {
|
||||
for (auto& animEntry : animations) {
|
||||
for (const auto& name : animEntry.second.model.meshNamesOrdered) {
|
||||
@@ -205,275 +125,38 @@ void Character::setTexture(const std::string& meshName, std::shared_ptr<Texture>
|
||||
}
|
||||
|
||||
AnimationState Character::resolveActiveState() const {
|
||||
if (animations.count(state.currentState)) return state.currentState;
|
||||
if (animations.count(characterState.currentState)) return characterState.currentState;
|
||||
return AnimationState::STAND;
|
||||
}
|
||||
|
||||
float Character::getSpeedMultiplier(float transitionProgress) const {
|
||||
if (state.isPlayer) {
|
||||
return 1.0f + (1.5f - 1.0f) * transitionProgress;
|
||||
} else {
|
||||
return 1.0f - (1.0f - 0.1f) * transitionProgress;
|
||||
}
|
||||
}
|
||||
|
||||
void Character::update(int64_t deltaMs, const CharacterResolver& resolver) {
|
||||
if (!state.enabled) return;
|
||||
|
||||
if (state.slowMoTimeRemaining > 0.0f) {
|
||||
state.slowMoTimeRemaining -= static_cast<float>(deltaMs) / 1000.0f;
|
||||
|
||||
float transitionProgress;
|
||||
if (state.slowMoTimeRemaining > state.slowMoActiveTime - state.slowMoTransitionTime) {
|
||||
float timeInTransition = state.slowMoActiveTime - state.slowMoTimeRemaining;
|
||||
transitionProgress = (std::min)(1.0f, timeInTransition / state.slowMoTransitionTime);
|
||||
} else if (state.slowMoTimeRemaining < state.slowMoTransitionTime) {
|
||||
transitionProgress = (std::max)(0.0f, state.slowMoTimeRemaining / state.slowMoTransitionTime);
|
||||
} else {
|
||||
transitionProgress = 1.0f;
|
||||
}
|
||||
|
||||
float speedMultiplier = getSpeedMultiplier(transitionProgress);
|
||||
deltaMs = static_cast<int64_t>(static_cast<float>(deltaMs) * speedMultiplier);
|
||||
}
|
||||
|
||||
Character* attackTarget = resolver ? resolver(state.attackTargetIndex) : nullptr;
|
||||
Character* faceTarget = resolver ? resolver(state.faceTargetIndex) : nullptr;
|
||||
|
||||
Eigen::Vector3f activeTarget;
|
||||
Eigen::Vector3f lookTarget;
|
||||
|
||||
if (attackTarget == nullptr) {
|
||||
state.battle_state = 0;
|
||||
state.attack = 0;
|
||||
}
|
||||
|
||||
if (attackTarget != nullptr) {
|
||||
float distToGhost = (attackTarget->state.position - state.position).norm();
|
||||
if (distToGhost >= state.visibleRange) {
|
||||
if (state.isPlayer) {
|
||||
setTarget(attackTarget->state.position);
|
||||
}
|
||||
state.battle_state = 0;
|
||||
} else if (distToGhost < state.visibleRange && distToGhost >= kAttackRange) {
|
||||
setTarget(attackTarget->state.position);
|
||||
state.battle_state = 0;
|
||||
} else {
|
||||
state.battle_state = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.pathWaypoints.empty() && state.currentWaypointIndex < state.pathWaypoints.size()) {
|
||||
activeTarget = state.pathWaypoints[state.currentWaypointIndex];
|
||||
} else {
|
||||
activeTarget = state.walkTarget;
|
||||
}
|
||||
|
||||
if (state.battle_state == 0) {
|
||||
lookTarget = activeTarget;
|
||||
} else {
|
||||
lookTarget = attackTarget->state.position;
|
||||
}
|
||||
|
||||
if (state.currentState == AnimationState::WALK || state.currentState == AnimationState::STAND) {
|
||||
Eigen::Vector3f toTarget = activeTarget - state.position;
|
||||
toTarget.y() = 0.f;
|
||||
float dist = toTarget.norm();
|
||||
|
||||
if (dist > CharacterState::kWalkThreshold) {
|
||||
Eigen::Vector3f dir = toTarget / dist;
|
||||
float moveAmount = state.walkSpeed * static_cast<float>(deltaMs) / 1000.f;
|
||||
if (moveAmount >= dist) {
|
||||
state.position = activeTarget;
|
||||
state.position.y() = 0.f;
|
||||
} else {
|
||||
state.position += dir * moveAmount;
|
||||
}
|
||||
state.targetFacingAngle = atan2(dir.x(), -dir.z());
|
||||
state.currentState = AnimationState::WALK;
|
||||
} else {
|
||||
state.currentState = AnimationState::STAND;
|
||||
|
||||
const bool hasNextWaypoint = !state.pathWaypoints.empty() &&
|
||||
state.currentWaypointIndex + 1 < state.pathWaypoints.size();
|
||||
if (hasNextWaypoint) {
|
||||
++state.currentWaypointIndex;
|
||||
} else {
|
||||
state.pathWaypoints.clear();
|
||||
state.currentWaypointIndex = 0;
|
||||
}
|
||||
|
||||
if (!hasNextWaypoint && state.onArrivedCallback) {
|
||||
auto cb = std::move(state.onArrivedCallback);
|
||||
state.onArrivedCallback = nullptr;
|
||||
state.onArrivedCallbackName.clear();
|
||||
cb();
|
||||
}
|
||||
|
||||
if (faceTarget) {
|
||||
Eigen::Vector3f toFace = faceTarget->state.position - state.position;
|
||||
toFace.y() = 0.f;
|
||||
float d = toFace.norm();
|
||||
if (d > 1e-3f) {
|
||||
state.targetFacingAngle = atan2(toFace.x() / d, -toFace.z() / d);
|
||||
} else {
|
||||
state.targetFacingAngle = state.facingAngle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.hp <= 0) {
|
||||
if (state.currentState != AnimationState::ACTION_TO_DEATH &&
|
||||
state.currentState != AnimationState::DEATH_IDLE) {
|
||||
state.currentState = AnimationState::ACTION_TO_DEATH;
|
||||
state.resetAnim = true;
|
||||
state.attackTargetIndex = CharacterState::kNoTarget;
|
||||
}
|
||||
} else {
|
||||
if (state.battle_state == 1) {
|
||||
state.targetFacingAngle = atan2(lookTarget.x() - state.position.x(),
|
||||
-(lookTarget.z() - state.position.z()));
|
||||
|
||||
if (state.currentState == AnimationState::STAND || state.currentState == AnimationState::WALK) {
|
||||
state.currentState = AnimationState::STAND_TO_ACTION;
|
||||
state.resetAnim = true;
|
||||
}
|
||||
|
||||
if (state.canAttack && state.attack == 0 && state.attack_cooldown < 0.f &&
|
||||
state.currentState == AnimationState::ACTION_IDLE) {
|
||||
state.attack = 1;
|
||||
state.attack_cooldown = ATTACK_COOLDOWN_TIME;
|
||||
}
|
||||
if (state.canAttack && state.attack_cooldown >= 0.f) {
|
||||
state.attack_cooldown -= deltaMs / 1000.f;
|
||||
}
|
||||
|
||||
if (state.attack == 1 && state.currentState == AnimationState::ACTION_IDLE) {
|
||||
if (attackTarget != nullptr && attackTarget->state.hp > 0) {
|
||||
state.currentState = AnimationState::ACTION_ATTACK;
|
||||
state.resetAnim = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (state.currentState == AnimationState::STAND_TO_ACTION ||
|
||||
state.currentState == AnimationState::ACTION_IDLE ||
|
||||
state.currentState == AnimationState::ACTION_ATTACK) {
|
||||
state.currentState = AnimationState::ACTION_TO_STAND;
|
||||
state.resetAnim = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rotate toward target facing angle at constant angular speed
|
||||
float angleDiff = state.targetFacingAngle - state.facingAngle;
|
||||
while (angleDiff > static_cast<float>(M_PI)) angleDiff -= 2.f * static_cast<float>(M_PI);
|
||||
while (angleDiff < -static_cast<float>(M_PI)) angleDiff += 2.f * static_cast<float>(M_PI);
|
||||
float rotStep = state.rotationSpeed * static_cast<float>(deltaMs) / 1000.f;
|
||||
|
||||
if (std::fabs(angleDiff) <= rotStep) {
|
||||
state.facingAngle = state.targetFacingAngle;
|
||||
// One-shot: once aligned, clear the face target so the NPC stops tracking.
|
||||
state.faceTargetIndex = CharacterState::kNoTarget;
|
||||
} else {
|
||||
state.facingAngle += (angleDiff > 0.f ? rotStep : -rotStep);
|
||||
}
|
||||
|
||||
if (!state.isPlayer && state.hp > 0.f && state.battle_state == 0 && state.maxHomeDrift > 0.f) {
|
||||
state.homeDriftCheckTimer -= static_cast<float>(deltaMs) / 1000.f;
|
||||
if (state.homeDriftCheckTimer <= 0.f) {
|
||||
state.homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL;
|
||||
Eigen::Vector3f toHome = state.homePosition - state.position;
|
||||
toHome.y() = 0.f;
|
||||
if (!isMoving() && toHome.norm() > state.maxHomeDrift) {
|
||||
setTarget(state.homePosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
|
||||
if (hitSparkEmitter.isConfigured()) {
|
||||
hitSparkEmitter.update(static_cast<float>(deltaMs));
|
||||
}
|
||||
|
||||
auto it = animations.find(state.currentState);
|
||||
if (events) {
|
||||
if (events->deathAnimationComplete && onDeathAnimComplete)
|
||||
onDeathAnimComplete();
|
||||
if (events->attackImpact && audioPlayer) {
|
||||
audioPlayer->playSoundAsync(characterState.isPlayer
|
||||
? "resources/audio/547041__cogfirestudios__hit-swing-sword-small-3.ogg"
|
||||
: "resources/audio/13925__adcbicycle__21.ogg");
|
||||
}
|
||||
}
|
||||
|
||||
if (!characterState.enabled) return;
|
||||
auto it = animations.find(characterState.currentState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
auto& anim = it->second;
|
||||
|
||||
if (state.resetAnim) {
|
||||
state.resetAnim = false;
|
||||
anim.currentFrame = 0;
|
||||
}
|
||||
int prevFrame = anim.currentFrame;
|
||||
anim.currentFrame += static_cast<float>(deltaMs) / 24.f;
|
||||
|
||||
if (static_cast<int>(anim.currentFrame) >= 20 && state.currentState == AnimationState::STAND_TO_ACTION) {
|
||||
state.showWeapon = true;
|
||||
} else if (static_cast<int>(anim.currentFrame) <= 32 && state.currentState == AnimationState::ACTION_TO_STAND) {
|
||||
state.showWeapon = true;
|
||||
} else if (state.currentState == AnimationState::ACTION_ATTACK_2 ||
|
||||
state.currentState == AnimationState::ACTION_ATTACK ||
|
||||
state.currentState == AnimationState::ACTION_IDLE) {
|
||||
state.showWeapon = true;
|
||||
} else {
|
||||
state.showWeapon = false;
|
||||
}
|
||||
|
||||
auto attackDirectionToTarget = [&]() {
|
||||
Eigen::Vector3f dir = attackTarget->state.position - state.position;
|
||||
dir.y() = 0.f;
|
||||
float n = dir.norm();
|
||||
if (n > 1e-3f) dir /= n;
|
||||
else dir = Eigen::Vector3f::Zero();
|
||||
return dir;
|
||||
};
|
||||
|
||||
if (prevFrame < 19 && static_cast<int>(anim.currentFrame) >= 19 &&
|
||||
(state.currentState == AnimationState::ACTION_ATTACK ||
|
||||
state.currentState == AnimationState::ACTION_ATTACK_2)) {
|
||||
if (attackTarget != nullptr) {
|
||||
attackTarget->applyDamage(10.f, attackDirectionToTarget(), index);
|
||||
|
||||
if (audioPlayer) {
|
||||
if (state.isPlayer)
|
||||
{
|
||||
audioPlayer->playSoundAsync("resources/audio/547041__cogfirestudios__hit-swing-sword-small-3.ogg");
|
||||
}
|
||||
else
|
||||
{
|
||||
audioPlayer->playSoundAsync("resources/audio/13925__adcbicycle__21.ogg");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int frms = anim.model.animations[0].keyFrames[anim.model.animations[0].keyFrames.size() - 1].frame;
|
||||
if (static_cast<int>(anim.currentFrame) >= frms) {
|
||||
anim.currentFrame = anim.model.startingFrame;
|
||||
|
||||
if (state.currentState == AnimationState::STAND_TO_ACTION) {
|
||||
state.currentState = AnimationState::ACTION_IDLE;
|
||||
}
|
||||
if (state.currentState == AnimationState::ACTION_TO_STAND) {
|
||||
state.currentState = AnimationState::STAND;
|
||||
}
|
||||
if (state.currentState == AnimationState::ACTION_ATTACK) {
|
||||
state.currentState = AnimationState::ACTION_IDLE;
|
||||
state.attack = 0;
|
||||
}
|
||||
if (state.currentState == AnimationState::ACTION_TO_DEATH) {
|
||||
state.currentState = AnimationState::DEATH_IDLE;
|
||||
if (state.onDeathAnimComplete) {
|
||||
state.onDeathAnimComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
anim.currentFrame = (std::min)(
|
||||
characterState.animationFrame, static_cast<float>((std::max)(0, anim.totalFrames - 1)));
|
||||
|
||||
if (static_cast<int>(anim.currentFrame) != anim.lastFrame) {
|
||||
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
|
||||
anim.model.startBones, anim.model.animations[0].keyFrames,
|
||||
static_cast<int>(anim.currentFrame));
|
||||
if (!state.useGpuSkinning) {
|
||||
if (!characterState.useGpuSkinning) {
|
||||
anim.model.Interpolate(static_cast<int>(anim.currentFrame));
|
||||
}
|
||||
anim.lastFrame = static_cast<int>(anim.currentFrame);
|
||||
@@ -488,9 +171,9 @@ void Character::draw(Renderer& renderer) {
|
||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
if (!state.enabled) return;
|
||||
if (!characterState.enabled) return;
|
||||
|
||||
if (state.useGpuSkinning) {
|
||||
if (characterState.useGpuSkinning) {
|
||||
drawGpuSkinning(renderer);
|
||||
return;
|
||||
}
|
||||
@@ -509,10 +192,10 @@ void Character::draw(Renderer& renderer) {
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
auto& anim = it->second;
|
||||
for (const auto& name : anim.model.meshNamesOrdered) {
|
||||
@@ -563,10 +246,10 @@ void Character::drawGpuSkinning(Renderer& renderer) {
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
||||
static_cast<int>(anim.gpuSkinningShaderData.skinningMatrices.size()), false,
|
||||
@@ -610,24 +293,8 @@ bool Character::prepareGpuSkinning() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Character::tryAttackCombo()
|
||||
{
|
||||
logger() << "Try Attack Combo" << std::endl;
|
||||
auto it = animations.find(state.currentState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
auto& anim = it->second;
|
||||
|
||||
if (static_cast<int>(anim.currentFrame) >= 20 &&
|
||||
(state.currentState == AnimationState::ACTION_ATTACK ||
|
||||
state.currentState == AnimationState::ACTION_ATTACK_2)) {
|
||||
logger() << "Try Attack Combo Success" << std::endl;
|
||||
anim.currentFrame = anim.model.startingFrame;
|
||||
}
|
||||
}
|
||||
|
||||
void Character::drawAttachedWeapon(Renderer& renderer) {
|
||||
if (!state.showWeapon) return;
|
||||
if (!characterState.showWeapon) return;
|
||||
if (weaponMesh.data.PositionData.size() == 0 || !weaponTexture) return;
|
||||
|
||||
auto it = animations.find(resolveActiveState());
|
||||
@@ -643,10 +310,10 @@ void Character::drawAttachedWeapon(Renderer& renderer) {
|
||||
Eigen::Matrix4f currentBone = anim.gpuSkinningShaderData.skinningMatrices[boneIdx] * bindBone;
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix(Eigen::Vector3f(currentBone.block<3, 1>(0, 3)));
|
||||
renderer.RotateMatrix(Eigen::Matrix3f(currentBone.block<3, 3>(0, 0)));
|
||||
renderer.TranslateMatrix(weaponInitialPosition);
|
||||
@@ -660,8 +327,8 @@ void Character::drawAttachedWeapon(Renderer& renderer) {
|
||||
// ==================== Shadow depth pass ====================
|
||||
|
||||
void Character::drawShadowDepth(Renderer& renderer) {
|
||||
if (!state.enabled) return;
|
||||
if (state.useGpuSkinning) {
|
||||
if (!characterState.enabled) return;
|
||||
if (characterState.useGpuSkinning) {
|
||||
drawShadowDepthGpuSkinning(renderer);
|
||||
} else {
|
||||
drawShadowDepthCpu(renderer);
|
||||
@@ -674,10 +341,10 @@ void Character::drawShadowDepthCpu(Renderer& renderer) {
|
||||
if (it == animations.end()) return;
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
auto& anim = it->second;
|
||||
for (const auto& name : anim.model.meshNamesOrdered) {
|
||||
@@ -710,10 +377,10 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
|
||||
renderer.shaderManager.PushShader(shadowSkinningShader);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
||||
@@ -740,7 +407,7 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
|
||||
// ==================== Main pass with shadows ====================
|
||||
|
||||
void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
|
||||
if (!state.enabled) return;
|
||||
if (!characterState.enabled) return;
|
||||
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||
}
|
||||
@@ -748,7 +415,7 @@ void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightF
|
||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
if (state.useGpuSkinning) {
|
||||
if (characterState.useGpuSkinning) {
|
||||
drawGpuSkinningWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera);
|
||||
} else {
|
||||
drawCpuWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera);
|
||||
@@ -779,10 +446,10 @@ void Character::drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lig
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
auto& anim = it->second;
|
||||
for (const auto& name : anim.model.meshNamesOrdered) {
|
||||
@@ -841,10 +508,10 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
||||
@@ -887,7 +554,7 @@ void Character::drawNight(Renderer& renderer,
|
||||
const Eigen::Matrix4f& cameraViewInverse,
|
||||
const float* ambientColor, const float* fogColor)
|
||||
{
|
||||
if (!state.enabled) return;
|
||||
if (!characterState.enabled) return;
|
||||
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||
}
|
||||
@@ -895,7 +562,7 @@ void Character::drawNight(Renderer& renderer,
|
||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
if (state.useGpuSkinning) {
|
||||
if (characterState.useGpuSkinning) {
|
||||
drawNightGpuSkinning(renderer, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
|
||||
pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor);
|
||||
} else {
|
||||
@@ -939,10 +606,10 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
||||
static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
|
||||
@@ -1010,10 +677,10 @@ void Character::drawNightCpu(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
auto& anim = it->second;
|
||||
for (const auto& name : anim.model.meshNamesOrdered) {
|
||||
@@ -1045,14 +712,14 @@ void Character::drawNightWithShadow(Renderer& renderer,
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
|
||||
const float* ambientColor, const float* fogColor)
|
||||
{
|
||||
if (!state.enabled) return;
|
||||
if (!characterState.enabled) return;
|
||||
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||
}
|
||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
if (state.useGpuSkinning) {
|
||||
if (characterState.useGpuSkinning) {
|
||||
drawNightGpuSkinningWithShadow(renderer, plPosEye, plDirEye, plColors, plCount,
|
||||
plWorldXZ, plLimitXZ, cameraViewInverse,
|
||||
lightFromCamera, shadowMapTex, ambientColor, fogColor);
|
||||
@@ -1107,10 +774,10 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
||||
static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
|
||||
@@ -1191,10 +858,10 @@ void Character::drawNightCpuWithShadow(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
auto& anim = it->second;
|
||||
for (const auto& name : anim.model.meshNamesOrdered) {
|
||||
@@ -1232,16 +899,15 @@ void Character::setupHitSparks(std::shared_ptr<Texture> sparkTexture) {
|
||||
hitSparkEmitter.markConfigured();
|
||||
}
|
||||
|
||||
void Character::applyDamage(float damageAmount, const Eigen::Vector3f& attackDirection, int attackerIndex) {
|
||||
logger() << "Character::applyDamage" << std::endl;
|
||||
state.hp -= damageAmount;
|
||||
if (state.hp < 0) state.hp = 0;
|
||||
|
||||
if (state.onHpChanged) state.onHpChanged(state.hp, state.initialHp, attackerIndex);
|
||||
void Character::presentDamage(float hp, float maxHp,
|
||||
const Eigen::Vector3f& attackDirection,
|
||||
const std::string& attackerId) {
|
||||
if (onHpChanged)
|
||||
onHpChanged(hp, maxHp, attackerId);
|
||||
|
||||
if (!hitSparkEmitter.isConfigured()) return;
|
||||
|
||||
Eigen::Vector3f emitPos = state.position + Eigen::Vector3f(0.f, 1.f, 0.f);
|
||||
Eigen::Vector3f emitPos = characterState.position + Eigen::Vector3f(0.f, 1.f, 0.f);
|
||||
hitSparkEmitter.resetEmissionPoints({ emitPos });
|
||||
hitSparkEmitter.setEmissionDirection(attackDirection);
|
||||
|
||||
@@ -1262,12 +928,12 @@ void Character::drawName(TextRenderer& textRenderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix)
|
||||
{
|
||||
if (state.isPlayer) return;
|
||||
if (!state.enabled) return;
|
||||
if (state.hp <= 0.f) return;
|
||||
if (state.npcName.empty()) return;
|
||||
if (characterState.isPlayer) return;
|
||||
if (!characterState.enabled) return;
|
||||
if (characterState.hp <= 0.f) return;
|
||||
if (characterState.npcName.empty()) return;
|
||||
|
||||
Eigen::Vector4f worldPos(state.position.x(), state.position.y() + 1.83f, state.position.z(), 1.f);
|
||||
Eigen::Vector4f worldPos(characterState.position.x(), characterState.position.y() + 1.83f, characterState.position.z(), 1.f);
|
||||
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
|
||||
if (clip.w() <= 0.f) return;
|
||||
|
||||
@@ -1278,11 +944,11 @@ void Character::drawName(TextRenderer& textRenderer,
|
||||
float screenX = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
|
||||
float screenY = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
|
||||
|
||||
std::array<float, 4> color = state.canAttack
|
||||
std::array<float, 4> color = characterState.canAttack
|
||||
? std::array<float, 4>{ 1.f, 0.f, 0.f, 1.f }
|
||||
: std::array<float, 4>{ 0.f, 1.f, 0.f, 1.f };
|
||||
|
||||
const std::string& translatedName = Character::nameTranslations.translateRef(state.npcName, g_currentLanguage);
|
||||
const std::string& translatedName = Character::nameTranslations.translateRef(characterState.npcName, g_currentLanguage);
|
||||
|
||||
textRenderer.drawText(translatedName, screenX, screenY, 1.0f, true, color);
|
||||
}
|
||||
@@ -1291,12 +957,12 @@ void Character::drawHealthBar(Renderer& renderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix)
|
||||
{
|
||||
if (state.isPlayer || !state.canAttack) return;
|
||||
if (!state.enabled) return;
|
||||
if (state.hp <= 0.f) return;
|
||||
if (state.initialHp <= 0.f) return;
|
||||
if (characterState.isPlayer || !characterState.canAttack) return;
|
||||
if (!characterState.enabled) return;
|
||||
if (characterState.hp <= 0.f) return;
|
||||
if (characterState.initialHp <= 0.f) return;
|
||||
|
||||
Eigen::Vector4f worldPos(state.position.x(), state.position.y() + 1.75f, state.position.z(), 1.f);
|
||||
Eigen::Vector4f worldPos(characterState.position.x(), characterState.position.y() + 1.75f, characterState.position.z(), 1.f);
|
||||
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
|
||||
if (clip.w() <= 0.f) return;
|
||||
float ndcX = clip.x() / clip.w();
|
||||
@@ -1314,7 +980,7 @@ void Character::drawHealthBar(Renderer& renderer,
|
||||
const float bottom = sy - barH * 0.5f;
|
||||
const float top = sy + barH * 0.5f;
|
||||
|
||||
float frac = state.hp / state.initialHp;
|
||||
float frac = characterState.hp / characterState.initialHp;
|
||||
if (frac < 0.f) frac = 0.f;
|
||||
if (frac > 1.f) frac = 1.f;
|
||||
const float split = left + barW * frac;
|
||||
|
||||
+33
-46
@@ -11,30 +11,25 @@
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include "dialogue/TranslationDatabase.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class TextRenderer;
|
||||
class IGameServer;
|
||||
|
||||
class Character {
|
||||
public:
|
||||
Character(const CharacterState& boundState, IGameServer& server,
|
||||
std::string locationId, std::string entityId)
|
||||
: characterState(boundState), gameServer_(server),
|
||||
locationId_(std::move(locationId)), entityId_(std::move(entityId)){}
|
||||
const std::string& entityId() const { return entityId_; }
|
||||
void bindToLocation(std::string locationId) { locationId_ = std::move(locationId); }
|
||||
static constexpr float kAttackRange = 1.25f;
|
||||
|
||||
// Resolves a character index to a live Character pointer.
|
||||
// kNoTarget (-1) → nullptr, kPlayerIndex (-2) → Location::player, 0..n → npcs[n].
|
||||
using CharacterResolver = std::function<Character*(int index)>;
|
||||
|
||||
using PathPlanner = std::function<std::vector<Eigen::Vector3f>(
|
||||
const Eigen::Vector3f& start,
|
||||
const Eigen::Vector3f& end)>;
|
||||
|
||||
// All serializable game state (position, hp, flags, pathfinding, etc.)
|
||||
CharacterState state;
|
||||
|
||||
int index = CharacterState::kNoTarget;
|
||||
|
||||
// ---- Asset loading ----
|
||||
void loadAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile = "");
|
||||
void loadBinaryAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile = "");
|
||||
@@ -46,31 +41,19 @@ public:
|
||||
|
||||
// Creates a fully-loaded Character from a CharacterState (including its creationInfo).
|
||||
// Loads all animations, textures, and weapon mesh/texture from the paths stored in creationInfo.
|
||||
static std::unique_ptr<Character> createFromState(CharacterState st, Renderer& renderer, const char* zipFile);
|
||||
static std::unique_ptr<Character> createFromState(
|
||||
const CharacterState& initialState, IGameServer& server,
|
||||
const std::string& locationId, const std::string& entityId,
|
||||
Renderer& renderer, const char* zipFile);
|
||||
|
||||
// ---- Per-frame update ----
|
||||
// resolver maps character indices to live pointers so attackTargetIndex / faceTargetIndex
|
||||
// can be dereferenced inside update(). Pass nullptr only if no targets are set.
|
||||
void update(int64_t deltaMs, const CharacterResolver& resolver = nullptr);
|
||||
|
||||
void setPathPlanner(PathPlanner planner);
|
||||
|
||||
// ---- Navigation (path data lives in state; pathPlanner lives here) ----
|
||||
// callbackName, if given, is the global Lua function name backing onArrived —
|
||||
// stored in state.onArrivedCallbackName so it survives save/load (see ScriptEngine::rehydrateNamedCallback).
|
||||
void setTarget(const Eigen::Vector3f& target, std::function<void()> onArrived = nullptr, std::string callbackName = "");
|
||||
void stopAndRotateToTarget(int npcId);
|
||||
|
||||
void forceReplan();
|
||||
void stopInPlace() { state.stopInPlace(); }
|
||||
|
||||
// ---- Reset ----
|
||||
void resetPlayerAfterDeath();
|
||||
|
||||
void tryAttackCombo();
|
||||
// Applies authoritative animation state to runtime resources and consumes
|
||||
// presentation-only events emitted by CharacterState::update.
|
||||
void update(int64_t deltaMs, const CharacterUpdateEvents* events = nullptr);
|
||||
|
||||
// ---- Damage ----
|
||||
void applyDamage(float damageAmount, const Eigen::Vector3f& attackDirection = Eigen::Vector3f::Zero(), int attackerIndex = CharacterState::kNoTarget);
|
||||
void presentDamage(float hp, float maxHp, const Eigen::Vector3f& attackDirection,
|
||||
const std::string& attackerId);
|
||||
|
||||
// ---- Spark setup ----
|
||||
void setupHitSparks(std::shared_ptr<Texture> sparkTexture);
|
||||
@@ -102,13 +85,10 @@ public:
|
||||
const float* ambientColor, const float* fogColor);
|
||||
|
||||
// ---- Query helpers (delegate to state) ----
|
||||
bool isMoving() const { return state.isMoving(); }
|
||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return state.getRequestedWalkTarget(); }
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const { return state.getCurrentNavigationTarget(); }
|
||||
float getHp() const { return state.getHp(); }
|
||||
void setHp(float newHp) { state.setHp(newHp); }
|
||||
|
||||
float getSpeedMultiplier(float transitionProgress) const;
|
||||
bool isMoving() const { return characterState.isMoving(); }
|
||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return characterState.getRequestedWalkTarget(); }
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const { return characterState.getCurrentNavigationTarget(); }
|
||||
float getHp() const { return characterState.getHp(); }
|
||||
|
||||
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
|
||||
VertexRenderStruct weaponMesh;
|
||||
@@ -123,16 +103,23 @@ public:
|
||||
static Dialogue::TranslationDatabase nameTranslations;
|
||||
|
||||
private:
|
||||
friend class Game;
|
||||
friend class Location;
|
||||
friend class ScriptEngine;
|
||||
friend class LocationEditor;
|
||||
friend class PathVisualization;
|
||||
friend class InternalGameServer;
|
||||
const CharacterState& characterState; // private non-owning binding to server-owned storage
|
||||
IGameServer& gameServer_;
|
||||
std::string locationId_;
|
||||
std::string entityId_;
|
||||
std::function<void()> onDeathAnimComplete;
|
||||
std::function<void(float, float, const std::string&)> onHpChanged;
|
||||
std::map<AnimationState, BoneAnimationDataNew> animations;
|
||||
VertexRenderStruct modelMutable;
|
||||
VertexRenderStruct healthBarMesh;
|
||||
std::unordered_map<std::string, std::shared_ptr<Texture>> meshTextures;
|
||||
|
||||
PathPlanner pathPlanner;
|
||||
|
||||
static constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
|
||||
static constexpr float HOME_DRIFT_CHECK_INTERVAL = 5.0f;
|
||||
|
||||
AnimationState resolveActiveState() const;
|
||||
bool prepareGpuSkinning();
|
||||
void drawAttachedWeapon(Renderer& renderer);
|
||||
|
||||
+308
-6
@@ -1,7 +1,17 @@
|
||||
#include "CharacterState.h"
|
||||
#include "LocationState.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
namespace {
|
||||
constexpr float ATTACK_COOLDOWN_TIME = 1.6f;
|
||||
constexpr float HOME_DRIFT_CHECK_INTERVAL = 5.0f;
|
||||
constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
|
||||
constexpr float PI = 3.14159265358979323846f;
|
||||
}
|
||||
|
||||
bool CharacterState::isMoving() const {
|
||||
Eigen::Vector3f toTarget = walkTarget - position;
|
||||
toTarget.y() = 0.f;
|
||||
@@ -20,14 +30,304 @@ void CharacterState::stopInPlace() {
|
||||
requestedWalkTarget = walkTarget;
|
||||
pathWaypoints.clear();
|
||||
currentWaypointIndex = 0;
|
||||
onArrivedCallback = nullptr;
|
||||
onArrivedCallbackName.clear();
|
||||
}
|
||||
|
||||
void CharacterState::setTarget(
|
||||
const Eigen::Vector3f& target,
|
||||
std::string callbackName,
|
||||
const LocationState& location,
|
||||
const std::string& entityId)
|
||||
{
|
||||
const Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());
|
||||
const bool sameRequestedTarget =
|
||||
(normalizedTarget - requestedWalkTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
const bool alreadyMovingToTarget =
|
||||
!pathWaypoints.empty() ||
|
||||
(walkTarget - normalizedTarget).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
const bool stoppedAfterFailedPath =
|
||||
pathWaypoints.empty() &&
|
||||
(walkTarget - position).norm() <= TARGET_REPLAN_THRESHOLD;
|
||||
if (callbackName.empty() && sameRequestedTarget &&
|
||||
(alreadyMovingToTarget || stoppedAfterFailedPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestedWalkTarget = normalizedTarget;
|
||||
onArrivedCallbackName = std::move(callbackName);
|
||||
pathWaypoints = location.planPathExceptMyself(position, normalizedTarget, entityId);
|
||||
currentWaypointIndex = 0;
|
||||
if (!pathWaypoints.empty()) {
|
||||
for (Eigen::Vector3f& waypoint : pathWaypoints) waypoint.y() = 0.f;
|
||||
walkTarget = pathWaypoints.back();
|
||||
return;
|
||||
}
|
||||
|
||||
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||
onArrivedCallbackName.clear();
|
||||
}
|
||||
|
||||
void CharacterState::forceReplan(
|
||||
const LocationState& location, const std::string& entityId)
|
||||
{
|
||||
const Eigen::Vector3f normalizedTarget(
|
||||
requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
|
||||
pathWaypoints = location.planPathExceptMyself(position, normalizedTarget, entityId);
|
||||
currentWaypointIndex = 0;
|
||||
if (!pathWaypoints.empty()) {
|
||||
for (Eigen::Vector3f& waypoint : pathWaypoints) waypoint.y() = 0.f;
|
||||
walkTarget = pathWaypoints.back();
|
||||
return;
|
||||
}
|
||||
|
||||
walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
|
||||
onArrivedCallbackName.clear();
|
||||
}
|
||||
|
||||
void CharacterState::stopAndRotateToTarget(std::string characterId)
|
||||
{
|
||||
stopInPlace();
|
||||
faceTargetId = std::move(characterId);
|
||||
}
|
||||
|
||||
void CharacterState::resetPlayerAfterDeath()
|
||||
{
|
||||
if (!isPlayer) return;
|
||||
currentState = AnimationState::STAND;
|
||||
resetAnim = true;
|
||||
showWeapon = false;
|
||||
battle_state = 0;
|
||||
attack = 0;
|
||||
}
|
||||
|
||||
void CharacterState::tryAttackCombo()
|
||||
{
|
||||
if (static_cast<int>(animationFrame) < 20 ||
|
||||
(currentState != AnimationState::ACTION_ATTACK &&
|
||||
currentState != AnimationState::ACTION_ATTACK_2)) {
|
||||
return;
|
||||
}
|
||||
const auto index = static_cast<std::size_t>(currentState);
|
||||
animationFrame = index < animationStartFrames.size()
|
||||
? static_cast<float>(animationStartFrames[index]) : 0.f;
|
||||
}
|
||||
|
||||
void CharacterState::setHp(float newHp) {
|
||||
hp = newHp;
|
||||
if (hp < 0.f) hp = 0.f;
|
||||
if (onHpChanged) onHpChanged(hp, initialHp, CharacterState::kNoTarget);
|
||||
}
|
||||
|
||||
void CharacterState::setAnimationMetadata(
|
||||
AnimationState state, int startFrame, int frameCount)
|
||||
{
|
||||
const auto index = static_cast<std::size_t>(state);
|
||||
if (index < animationFrameCounts.size()) {
|
||||
animationStartFrames[index] = (std::max)(0, startFrame);
|
||||
animationFrameCounts[index] = (std::max)(1, frameCount);
|
||||
}
|
||||
}
|
||||
|
||||
CharacterUpdateEvents CharacterState::update(
|
||||
std::int64_t deltaMs, const CharacterUpdateContext& context)
|
||||
{
|
||||
CharacterUpdateEvents events;
|
||||
if (!enabled) return events;
|
||||
|
||||
if (slowMoTimeRemaining > 0.0f) {
|
||||
slowMoTimeRemaining = (std::max)(
|
||||
0.0f, slowMoTimeRemaining - static_cast<float>(deltaMs) / 1000.0f);
|
||||
|
||||
float transitionProgress = 1.0f;
|
||||
if (slowMoTimeRemaining > slowMoActiveTime - slowMoTransitionTime) {
|
||||
const float timeInTransition = slowMoActiveTime - slowMoTimeRemaining;
|
||||
transitionProgress = (std::min)(1.0f, timeInTransition / slowMoTransitionTime);
|
||||
} else if (slowMoTimeRemaining < slowMoTransitionTime) {
|
||||
transitionProgress = (std::max)(0.0f, slowMoTimeRemaining / slowMoTransitionTime);
|
||||
}
|
||||
const float speedMultiplier = isPlayer
|
||||
? 1.0f + 0.5f * transitionProgress
|
||||
: 1.0f - 0.9f * transitionProgress;
|
||||
deltaMs = static_cast<std::int64_t>(
|
||||
static_cast<float>(deltaMs) * speedMultiplier);
|
||||
}
|
||||
|
||||
const CharacterObservation* attackTarget = context.attackTarget;
|
||||
const CharacterObservation* faceTarget = context.faceTarget;
|
||||
if (!attackTarget) {
|
||||
battle_state = 0;
|
||||
attack = 0;
|
||||
attackTargetId.clear();
|
||||
} else {
|
||||
const float distance = (attackTarget->position - position).norm();
|
||||
if (distance >= visibleRange) {
|
||||
if (isPlayer &&
|
||||
(attackTarget->position - requestedWalkTarget).norm() > TARGET_REPLAN_THRESHOLD) {
|
||||
events.navigationRequested = true;
|
||||
events.navigationTarget = attackTarget->position;
|
||||
}
|
||||
battle_state = 0;
|
||||
} else if (distance >= 1.25f) {
|
||||
if ((attackTarget->position - requestedWalkTarget).norm() > TARGET_REPLAN_THRESHOLD) {
|
||||
events.navigationRequested = true;
|
||||
events.navigationTarget = attackTarget->position;
|
||||
}
|
||||
battle_state = 0;
|
||||
} else {
|
||||
battle_state = 1;
|
||||
}
|
||||
}
|
||||
|
||||
const Eigen::Vector3f activeTarget = getCurrentNavigationTarget();
|
||||
const Eigen::Vector3f lookTarget =
|
||||
battle_state == 1 && attackTarget ? attackTarget->position : activeTarget;
|
||||
|
||||
if (currentState == AnimationState::WALK || currentState == AnimationState::STAND) {
|
||||
Eigen::Vector3f toTarget = activeTarget - position;
|
||||
toTarget.y() = 0.f;
|
||||
const float distance = toTarget.norm();
|
||||
if (distance > kWalkThreshold) {
|
||||
const Eigen::Vector3f direction = toTarget / distance;
|
||||
const float moveAmount = walkSpeed * static_cast<float>(deltaMs) / 1000.f;
|
||||
position = moveAmount >= distance ? activeTarget : position + direction * moveAmount;
|
||||
position.y() = 0.f;
|
||||
targetFacingAngle = std::atan2(direction.x(), -direction.z());
|
||||
currentState = AnimationState::WALK;
|
||||
} else {
|
||||
currentState = AnimationState::STAND;
|
||||
const bool hasNextWaypoint =
|
||||
!pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size();
|
||||
if (hasNextWaypoint) {
|
||||
++currentWaypointIndex;
|
||||
} else {
|
||||
pathWaypoints.clear();
|
||||
currentWaypointIndex = 0;
|
||||
if (!onArrivedCallbackName.empty()) {
|
||||
events.arrivedCallbackName = onArrivedCallbackName;
|
||||
onArrivedCallbackName.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (faceTarget) {
|
||||
Eigen::Vector3f toFace = faceTarget->position - position;
|
||||
toFace.y() = 0.f;
|
||||
const float distanceToFace = toFace.norm();
|
||||
targetFacingAngle = distanceToFace > 1e-3f
|
||||
? std::atan2(toFace.x() / distanceToFace, -toFace.z() / distanceToFace)
|
||||
: facingAngle;
|
||||
} else if (!faceTargetId.empty()) {
|
||||
faceTargetId.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hp <= 0.f) {
|
||||
if (currentState != AnimationState::ACTION_TO_DEATH &&
|
||||
currentState != AnimationState::DEATH_IDLE) {
|
||||
currentState = AnimationState::ACTION_TO_DEATH;
|
||||
resetAnim = true;
|
||||
attackTargetId.clear();
|
||||
}
|
||||
} else if (battle_state == 1 && attackTarget) {
|
||||
targetFacingAngle = std::atan2(
|
||||
lookTarget.x() - position.x(), -(lookTarget.z() - position.z()));
|
||||
if (currentState == AnimationState::STAND || currentState == AnimationState::WALK) {
|
||||
currentState = AnimationState::STAND_TO_ACTION;
|
||||
resetAnim = true;
|
||||
}
|
||||
if (canAttack && attack == 0 && attack_cooldown < 0.f &&
|
||||
currentState == AnimationState::ACTION_IDLE) {
|
||||
attack = 1;
|
||||
attack_cooldown = ATTACK_COOLDOWN_TIME;
|
||||
}
|
||||
if (canAttack && attack_cooldown >= 0.f)
|
||||
attack_cooldown -= static_cast<float>(deltaMs) / 1000.f;
|
||||
if (attack == 1 && currentState == AnimationState::ACTION_IDLE &&
|
||||
attackTarget->hp > 0.f) {
|
||||
currentState = AnimationState::ACTION_ATTACK;
|
||||
resetAnim = true;
|
||||
}
|
||||
} else if (currentState == AnimationState::STAND_TO_ACTION ||
|
||||
currentState == AnimationState::ACTION_IDLE ||
|
||||
currentState == AnimationState::ACTION_ATTACK) {
|
||||
currentState = AnimationState::ACTION_TO_STAND;
|
||||
resetAnim = true;
|
||||
}
|
||||
|
||||
float angleDiff = targetFacingAngle - facingAngle;
|
||||
while (angleDiff > PI) angleDiff -= 2.f * PI;
|
||||
while (angleDiff < -PI) angleDiff += 2.f * PI;
|
||||
const float rotationStep = rotationSpeed * static_cast<float>(deltaMs) / 1000.f;
|
||||
if (std::fabs(angleDiff) <= rotationStep) {
|
||||
facingAngle = targetFacingAngle;
|
||||
faceTargetId.clear();
|
||||
} else {
|
||||
facingAngle += angleDiff > 0.f ? rotationStep : -rotationStep;
|
||||
}
|
||||
|
||||
if (!isPlayer && hp > 0.f && battle_state == 0 && maxHomeDrift > 0.f) {
|
||||
homeDriftCheckTimer -= static_cast<float>(deltaMs) / 1000.f;
|
||||
if (homeDriftCheckTimer <= 0.f) {
|
||||
homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL;
|
||||
Eigen::Vector3f toHome = homePosition - position;
|
||||
toHome.y() = 0.f;
|
||||
if (!isMoving() && toHome.norm() > maxHomeDrift) {
|
||||
events.navigationRequested = true;
|
||||
events.navigationTarget = homePosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resetAnim) {
|
||||
resetAnim = false;
|
||||
const auto index = static_cast<std::size_t>(currentState);
|
||||
animationFrame = index < animationStartFrames.size()
|
||||
? static_cast<float>(animationStartFrames[index]) : 0.f;
|
||||
}
|
||||
const int previousFrame = static_cast<int>(animationFrame);
|
||||
animationFrame += static_cast<float>(deltaMs) / 24.f;
|
||||
|
||||
if (previousFrame < 19 && static_cast<int>(animationFrame) >= 19 &&
|
||||
(currentState == AnimationState::ACTION_ATTACK ||
|
||||
currentState == AnimationState::ACTION_ATTACK_2) &&
|
||||
attackTarget) {
|
||||
events.attackImpact = true;
|
||||
events.attackTargetId = attackTargetId;
|
||||
events.attackDirection = attackTarget->position - position;
|
||||
events.attackDirection.y() = 0.f;
|
||||
const float length = events.attackDirection.norm();
|
||||
if (length > 1e-3f) events.attackDirection /= length;
|
||||
else events.attackDirection = Eigen::Vector3f::Zero();
|
||||
}
|
||||
|
||||
showWeapon =
|
||||
(static_cast<int>(animationFrame) >= 20 && currentState == AnimationState::STAND_TO_ACTION) ||
|
||||
(static_cast<int>(animationFrame) <= 32 && currentState == AnimationState::ACTION_TO_STAND) ||
|
||||
currentState == AnimationState::ACTION_ATTACK_2 ||
|
||||
currentState == AnimationState::ACTION_ATTACK ||
|
||||
currentState == AnimationState::ACTION_IDLE;
|
||||
|
||||
const auto animationIndex = static_cast<std::size_t>(currentState);
|
||||
const int frameCount = animationIndex < animationFrameCounts.size()
|
||||
? animationFrameCounts[animationIndex] : 1;
|
||||
if (frameCount > 1 && static_cast<int>(animationFrame) >= frameCount - 1) {
|
||||
if (currentState == AnimationState::STAND_TO_ACTION)
|
||||
currentState = AnimationState::ACTION_IDLE;
|
||||
else if (currentState == AnimationState::ACTION_TO_STAND)
|
||||
currentState = AnimationState::STAND;
|
||||
else if (currentState == AnimationState::ACTION_ATTACK ||
|
||||
currentState == AnimationState::ACTION_ATTACK_2) {
|
||||
currentState = AnimationState::ACTION_IDLE;
|
||||
attack = 0;
|
||||
} else if (currentState == AnimationState::ACTION_TO_DEATH) {
|
||||
currentState = AnimationState::DEATH_IDLE;
|
||||
events.deathAnimationComplete = true;
|
||||
}
|
||||
const auto nextIndex = static_cast<std::size_t>(currentState);
|
||||
animationFrame = nextIndex < animationStartFrames.size()
|
||||
? static_cast<float>(animationStartFrames[nextIndex]) : 0.f;
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
void CharacterState::save(nlohmann::json& out) const
|
||||
@@ -41,8 +341,8 @@ void CharacterState::save(nlohmann::json& out) const
|
||||
out["currentState"] = static_cast<int>(currentState);
|
||||
out["attack_cooldown"] = attack_cooldown;
|
||||
out["showWeapon"] = showWeapon;
|
||||
out["attackTargetIndex"] = attackTargetIndex;
|
||||
out["faceTargetIndex"] = faceTargetIndex;
|
||||
out["attackTargetId"] = attackTargetId;
|
||||
out["faceTargetId"] = faceTargetId;
|
||||
out["homePosition"] = { homePosition.x(), homePosition.y(), homePosition.z() };
|
||||
out["walkTarget"] = { walkTarget.x(), walkTarget.y(), walkTarget.z() };
|
||||
out["requestedWalkTarget"] = { requestedWalkTarget.x(), requestedWalkTarget.y(), requestedWalkTarget.z() };
|
||||
@@ -53,6 +353,7 @@ void CharacterState::save(nlohmann::json& out) const
|
||||
out["pathWaypoints"] = std::move(wpArr);
|
||||
out["currentWaypointIndex"] = currentWaypointIndex;
|
||||
out["homeDriftCheckTimer"] = homeDriftCheckTimer;
|
||||
out["animationFrame"] = animationFrame;
|
||||
out["visibleRange"] = visibleRange;
|
||||
if (!onArrivedCallbackName.empty())
|
||||
out["onArrivedCallbackName"] = onArrivedCallbackName;
|
||||
@@ -76,8 +377,8 @@ void CharacterState::load(const nlohmann::json& in)
|
||||
currentState = static_cast<AnimationState>(in.value("currentState", static_cast<int>(currentState)));
|
||||
attack_cooldown = in.value("attack_cooldown", attack_cooldown);
|
||||
showWeapon = in.value("showWeapon", showWeapon);
|
||||
attackTargetIndex = in.value("attackTargetIndex", attackTargetIndex);
|
||||
faceTargetIndex = in.value("faceTargetIndex", faceTargetIndex);
|
||||
attackTargetId = in.value("attackTargetId", attackTargetId);
|
||||
faceTargetId = in.value("faceTargetId", faceTargetId);
|
||||
if (in.contains("homePosition")) homePosition = readVec3(in["homePosition"], homePosition);
|
||||
if (in.contains("walkTarget")) walkTarget = readVec3(in["walkTarget"], walkTarget);
|
||||
if (in.contains("requestedWalkTarget")) requestedWalkTarget = readVec3(in["requestedWalkTarget"], requestedWalkTarget);
|
||||
@@ -89,6 +390,7 @@ void CharacterState::load(const nlohmann::json& in)
|
||||
}
|
||||
currentWaypointIndex = in.value("currentWaypointIndex", currentWaypointIndex);
|
||||
homeDriftCheckTimer = in.value("homeDriftCheckTimer", homeDriftCheckTimer);
|
||||
animationFrame = in.value("animationFrame", animationFrame);
|
||||
onArrivedCallbackName = in.value("onArrivedCallbackName", std::string());
|
||||
}
|
||||
|
||||
|
||||
+59
-20
@@ -2,7 +2,9 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/Geometry>
|
||||
#include "external/nlohmann/json.hpp"
|
||||
@@ -12,6 +14,10 @@ namespace FRG {
|
||||
|
||||
constexpr float VISIBLE_RANGE = 6.f;
|
||||
|
||||
struct CharacterUpdateContext;
|
||||
struct CharacterUpdateEvents;
|
||||
struct LocationState;
|
||||
|
||||
enum class AnimationState {
|
||||
STAND = 0,
|
||||
WALK = 1,
|
||||
@@ -24,6 +30,8 @@ enum class AnimationState {
|
||||
DEATH_IDLE = 8,
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Paths and fixed parameters needed to recreate a Character's visual assets
|
||||
// from scratch (e.g. after loading a saved game).
|
||||
struct CharacterCreationInfo {
|
||||
@@ -45,19 +53,14 @@ struct CharacterCreationInfo {
|
||||
float weaponMeshScale = 1.0f;
|
||||
};
|
||||
|
||||
// All serializable game state for a character.
|
||||
// This is a member of Character; rendering-only data lives in Character itself.
|
||||
// All serializable authoritative state for a character. GameState or
|
||||
// LocationGameState owns it; rendering-only data lives in Character.
|
||||
class CharacterState : public ISaveable {
|
||||
public:
|
||||
// Special index values for attackTargetIndex / faceTargetIndex
|
||||
static constexpr int kNoTarget = -1; // no target (was nullptr)
|
||||
static constexpr int kPlayerIndex = -2; // refers to Location::player
|
||||
|
||||
// Distance below which the character is considered to have reached its target.
|
||||
static constexpr float kWalkThreshold = 0.05f;
|
||||
|
||||
// --- Identity ---
|
||||
std::string npcId;
|
||||
std::string npcName;
|
||||
bool isPlayer = false;
|
||||
bool enabled = true;
|
||||
@@ -84,9 +87,11 @@ public:
|
||||
// Weapon visibility toggle (changes during animation)
|
||||
bool showWeapon = true;
|
||||
|
||||
// --- Index-based character references (replace raw pointers) ---
|
||||
int attackTargetIndex = kNoTarget;
|
||||
int faceTargetIndex = kNoTarget;
|
||||
// --- Stable character references ---
|
||||
// Empty means no target. "player" is the campaign player; NPC IDs are
|
||||
// location-local and validated while loading location content.
|
||||
std::string attackTargetId;
|
||||
std::string faceTargetId;
|
||||
|
||||
// --- Home position for drift recovery ---
|
||||
Eigen::Vector3f homePosition = Eigen::Vector3f::Zero();
|
||||
@@ -97,6 +102,13 @@ public:
|
||||
float slowMoActiveTime = 6.0f;
|
||||
float slowMoTransitionTime = 0.5f;
|
||||
|
||||
// --- Authoritative logical animation clock ---
|
||||
// Frame counts are immutable metadata copied from loaded clips during
|
||||
// character construction. Simulation advances animationFrame itself.
|
||||
float animationFrame = 0.0f;
|
||||
std::array<int, 9> animationStartFrames = {{0, 0, 0, 0, 0, 0, 0, 0, 0}};
|
||||
std::array<int, 9> animationFrameCounts = {{1, 1, 1, 1, 1, 1, 1, 1, 1}};
|
||||
|
||||
// --- Movement / visual parameters ---
|
||||
float walkSpeed = 3.0f;
|
||||
float rotationSpeed = 4.0f;
|
||||
@@ -115,14 +127,8 @@ public:
|
||||
// Internal drift-check timer (serializable so it survives save/load).
|
||||
float homeDriftCheckTimer = 5.0f;
|
||||
|
||||
// --- Non-serializable callbacks (re-wired after each load) ---
|
||||
std::function<void()> onArrivedCallback;
|
||||
std::function<void()> onDeathAnimComplete;
|
||||
std::function<void(float, float, int)> onHpChanged;
|
||||
|
||||
// Global Lua function name backing onArrivedCallback, if it was sourced from a
|
||||
// script (see ScriptEngine::rehydrateNamedCallback). Serialized so the callback
|
||||
// can be re-looked-up by name after a save/load; empty if none or non-Lua.
|
||||
// Global Lua function called when the character reaches the final waypoint.
|
||||
// The name is serialized, so no runtime callback needs to be rehydrated.
|
||||
std::string onArrivedCallbackName;
|
||||
|
||||
// --- Asset / creation info (used to reload visuals from a saved state) ---
|
||||
@@ -133,12 +139,45 @@ public:
|
||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; }
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||
void stopInPlace();
|
||||
void setTarget(const Eigen::Vector3f& target,
|
||||
std::string callbackName,
|
||||
const LocationState& location,
|
||||
const std::string& entityId);
|
||||
void forceReplan(const LocationState& location,
|
||||
const std::string& entityId);
|
||||
void stopAndRotateToTarget(std::string characterId);
|
||||
void resetPlayerAfterDeath();
|
||||
void tryAttackCombo();
|
||||
float getHp() const { return hp; }
|
||||
void setHp(float newHp);
|
||||
|
||||
CharacterUpdateEvents update(std::int64_t deltaMs, const CharacterUpdateContext& context);
|
||||
void setAnimationMetadata(AnimationState state, int startFrame, int frameCount);
|
||||
// --- Serialisation (runtime mutable fields only; creation info is reloaded from config) ---
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
};
|
||||
|
||||
using PlayerStateMap =
|
||||
std::unordered_map<std::string, std::shared_ptr<CharacterState>>;
|
||||
|
||||
struct CharacterObservation {
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float hp = 0.f;
|
||||
};
|
||||
|
||||
struct CharacterUpdateContext {
|
||||
const CharacterObservation* attackTarget = nullptr;
|
||||
const CharacterObservation* faceTarget = nullptr;
|
||||
};
|
||||
|
||||
struct CharacterUpdateEvents {
|
||||
bool attackImpact = false;
|
||||
bool deathAnimationComplete = false;
|
||||
bool navigationRequested = false;
|
||||
Eigen::Vector3f navigationTarget = Eigen::Vector3f::Zero();
|
||||
std::string arrivedCallbackName;
|
||||
std::string attackTargetId;
|
||||
Eigen::Vector3f attackDirection = Eigen::Vector3f::Zero();
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+149
-84
@@ -77,15 +77,18 @@ namespace FRG
|
||||
|
||||
Location* Game::currentLocation() const {
|
||||
if (gameState.currentLocationName.empty()) return nullptr;
|
||||
auto it = gameState.locations.find(gameState.currentLocationName);
|
||||
return (it != gameState.locations.end()) ? it->second.get() : nullptr;
|
||||
const auto& locations = internalGameServer.runtimeLocations();
|
||||
auto it = locations.find(gameState.currentLocationName);
|
||||
return (it != locations.end()) ? it->second.get() : nullptr;
|
||||
}
|
||||
|
||||
Game::Game()
|
||||
: newTickCount(0)
|
||||
: audioPlayer(std::make_unique<AudioPlayerAsync>())
|
||||
, internalGameServer()
|
||||
, gameState(internalGameServer.hostState())
|
||||
, menuManager(renderer, internalGameServer, *audioPlayer)
|
||||
, newTickCount(0)
|
||||
, lastTickCount(0)
|
||||
, audioPlayer(std::make_unique<AudioPlayerAsync>())
|
||||
, menuManager(renderer, gameState, *audioPlayer)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -391,15 +394,23 @@ namespace FRG
|
||||
menuManager.skipCutsceneFunc = [this]() {
|
||||
if (currentLocation()) currentLocation()->dialogueSystem.skipCutscene();
|
||||
};
|
||||
|
||||
menuManager.callTaxiFunc = [this]() {
|
||||
if (currentLocation()) currentLocation()->onTaxiCalled();
|
||||
};
|
||||
});
|
||||
|
||||
loadSteps.push([this]() {
|
||||
try {
|
||||
menuManager.setup(gameState.inventory, CONST_ZIP_FILE);
|
||||
menuManager.setup(CONST_ZIP_FILE);
|
||||
gameState.questJournal.onQuestUnlocked = [this](const std::string& id) {
|
||||
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
|
||||
if (quest) menuManager.showToast(menuManager.toastTextureQuestNew, quest->definition.title);
|
||||
};
|
||||
gameState.questJournal.onQuestCompleted = [this](const std::string& id) {
|
||||
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
|
||||
if (quest) menuManager.showToast(menuManager.toastTextureQuestCompleted, quest->definition.title);
|
||||
};
|
||||
gameState.questJournal.onQuestFailed = [this](const std::string& id) {
|
||||
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
|
||||
if (quest) menuManager.showToast(menuManager.toastTextureQuestFailed, quest->definition.title);
|
||||
};
|
||||
logger() << "UI loaded successfully" << std::endl;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
@@ -515,7 +526,8 @@ namespace FRG
|
||||
}
|
||||
|
||||
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
|
||||
return player && loadedCampaignId == campaignId && gameState.locations.count(locationId) > 0;
|
||||
return player && loadedCampaignId == campaignId
|
||||
&& internalGameServer.runtimeLocations().count(locationId) > 0;
|
||||
}
|
||||
|
||||
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
|
||||
@@ -556,20 +568,24 @@ namespace FRG
|
||||
void Game::loadCampaign(const std::string& campaignId) {
|
||||
loadedCampaignId = campaignId;
|
||||
gameState.campaignId = campaignId;
|
||||
gameState.storedLocationStates.clear();
|
||||
gameState.locations.clear();
|
||||
gameState.players.clear();
|
||||
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
|
||||
// The player stays loaded for the whole campaign; locations come and go around him.
|
||||
player = Location::createPlayer(renderer, audioPlayer.get());
|
||||
auto& playerState = gameState.players["player"];
|
||||
if (!playerState) playerState = std::make_shared<CharacterState>();
|
||||
player = Location::createPlayer(renderer, audioPlayer.get(), *playerState, internalGameServer);
|
||||
if (player)
|
||||
player->state.onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
}
|
||||
|
||||
void Game::unloadCampaign() {
|
||||
unloadLocation(false);
|
||||
player.reset();
|
||||
gameState.players.clear();
|
||||
loadedCampaignId.clear();
|
||||
gameState.storedLocationStates.clear();
|
||||
gameState.locations.clear();
|
||||
renderer.textureManager.UnloadUnused(); // the player's textures
|
||||
}
|
||||
|
||||
@@ -580,23 +596,24 @@ namespace FRG
|
||||
<< locationId << "'" << std::endl;
|
||||
return;
|
||||
}
|
||||
const bool initializeLocation =
|
||||
!gameState.locations[locationId].hasPersistentState;
|
||||
std::shared_ptr<Location> loc = createLocation(*config);
|
||||
wireLocation(loc.get());
|
||||
if (shadowMap) loc->shadowMap = shadowMap;
|
||||
if (initializeLocation)
|
||||
loc->scriptEngine.callLocationInitCallback();
|
||||
// Back to how the player left it (or to the start state's snapshot of it).
|
||||
auto stored = gameState.storedLocationStates.find(locationId);
|
||||
if (stored != gameState.storedLocationStates.end()) loc->load(stored->second);
|
||||
gameState.locations[locationId] = loc;
|
||||
internalGameServer.bindRuntimeLocation(locationId, loc);
|
||||
}
|
||||
|
||||
void Game::unloadLocation(bool storeState) {
|
||||
for (auto& [name, loc] : gameState.locations) {
|
||||
if (!storeState) continue;
|
||||
nlohmann::json locJson;
|
||||
loc->save(locJson);
|
||||
gameState.storedLocationStates[name] = std::move(locJson);
|
||||
if (storeState) internalGameServer.synchronizeRuntimeAuxiliaryState();
|
||||
else {
|
||||
for (const auto& [name, loc] : internalGameServer.runtimeLocations())
|
||||
gameState.locations.erase(name);
|
||||
}
|
||||
gameState.locations.clear();
|
||||
internalGameServer.runtimeLocations().clear();
|
||||
// Textures only the location's models and characters used are freed now.
|
||||
renderer.textureManager.UnloadUnused();
|
||||
}
|
||||
@@ -619,11 +636,12 @@ namespace FRG
|
||||
Location* loc = currentLocation();
|
||||
if (!loc) return;
|
||||
if (loc->player) {
|
||||
loc->player->state.position = destPos;
|
||||
loc->player->setTarget(destPos);
|
||||
loc->player->stopInPlace();
|
||||
loc->player->state.facingAngle = destRotY;
|
||||
loc->player->state.targetFacingAngle = destRotY;
|
||||
loc->updateCharacterState(*loc->player, [&](CharacterState& workingState) {
|
||||
workingState.position = destPos;
|
||||
workingState.facingAngle = destRotY;
|
||||
workingState.targetFacingAngle = destRotY;
|
||||
});
|
||||
loc->stopCharacter(*loc->player);
|
||||
}
|
||||
loc->state.cameraAzimuth = destRotY;
|
||||
loc->state.isDarklands = gameState.isDarklands;
|
||||
@@ -632,14 +650,16 @@ namespace FRG
|
||||
loc->scriptEngine.callLocationEnterCallback();
|
||||
|
||||
//Reset taxi
|
||||
loc->state.taxiCar = loc->state.taxiCarInitialState;
|
||||
loc->state.taxiArrivedCallbackCalled = false;
|
||||
auto locationState = gameState.locations.find(destName);
|
||||
if (locationState != gameState.locations.end())
|
||||
locationState->second.resetTaxi();
|
||||
gameState.taxiIsCalled = false;
|
||||
|
||||
menuManager.onLocationChanged(destName);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
void Game::performFullReset() {
|
||||
gameState.currentLocationName.clear();
|
||||
gameState.isDarklands = false;
|
||||
@@ -666,9 +686,9 @@ namespace FRG
|
||||
|
||||
// Rebuild the loaded location from its config; the others have no stored state
|
||||
// any more, so they are built fresh when the player first goes there.
|
||||
gameState.storedLocationStates.clear();
|
||||
gameState.locations.clear();
|
||||
std::vector<std::string> loadedIds;
|
||||
for (auto& [name, loc] : gameState.locations) loadedIds.push_back(name);
|
||||
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loadedIds.push_back(name);
|
||||
unloadLocation(false);
|
||||
for (const std::string& id : loadedIds) loadLocation(id);
|
||||
|
||||
@@ -698,15 +718,8 @@ namespace FRG
|
||||
if (!content.empty()) {
|
||||
try {
|
||||
nlohmann::json root = nlohmann::json::parse(content);
|
||||
const std::string campaignId = gameState.campaignId;
|
||||
// GameState::load() merges into existing maps rather than replacing them,
|
||||
// so any Lua globals set during play that aren't in the pristine snapshot
|
||||
// must be cleared explicitly before loading it.
|
||||
gameState.globalInts.clear();
|
||||
gameState.globalFloats.clear();
|
||||
gameState.load(root); // applies to the loaded location in place and stores
|
||||
internalGameServer.load(root); // applies to the loaded location in place and stores
|
||||
// the other locations' states for when they load.
|
||||
gameState.campaignId = campaignId; // the snapshot may predate campaigns
|
||||
return;
|
||||
} catch (const std::exception& e) {
|
||||
logger() << "[reset] Failed to parse " << path << ": " << e.what()
|
||||
@@ -729,7 +742,7 @@ namespace FRG
|
||||
|
||||
void Game::saveInitialState() {
|
||||
nlohmann::json root;
|
||||
gameState.save(root);
|
||||
internalGameServer.save(root);
|
||||
// Written next to the executable; copy it over the campaign's start-state file.
|
||||
const std::string startStatePath = activeCampaign().startStatePath;
|
||||
const std::string path = startStatePath.substr(startStatePath.find_last_of('/') + 1);
|
||||
@@ -747,20 +760,28 @@ namespace FRG
|
||||
|
||||
std::shared_ptr<Location> Game::createLocation(const LocationConfig& config)
|
||||
{
|
||||
LocationGameState& authoritative = gameState.locations[config.id];
|
||||
if (!authoritative.location) authoritative.location = std::make_shared<LocationState>();
|
||||
|
||||
if (authoritative.location->navigationMaps.empty())
|
||||
authoritative.location->initNavigation(config.navigationJsonPaths);
|
||||
|
||||
LocationSetup params;
|
||||
params.gameObjectsJsonPath = config.gameObjectsJsonPath;
|
||||
params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath;
|
||||
params.npcsJsonPath = config.npcsJsonPath;
|
||||
params.dialoguesJsonPath = config.dialoguesJsonPath;
|
||||
params.navigationJsonPaths = config.navigationJsonPaths;
|
||||
//params.navigationJsonPaths = config.navigationJsonPaths;
|
||||
params.scriptPath = config.scriptPath;
|
||||
params.teleportsJsonPath = config.teleportsJsonPath;
|
||||
params.triggerZonesJsonPath = config.triggerZonesJsonPath;
|
||||
params.lightsJsonPath = config.lightsJsonPath;
|
||||
params.playerPosition = config.playerPosition;
|
||||
params.player = player;
|
||||
params.gameState = &authoritative;
|
||||
|
||||
auto loc = std::make_shared<Location>(renderer, gameState.inventory);
|
||||
auto loc = std::make_shared<Location>(
|
||||
renderer, gameState.inventory, *authoritative.location, internalGameServer, config.id);
|
||||
loc->setup(params, &gameState.questJournal, audioPlayer.get());
|
||||
loc->scriptEngine.setGlobalStore(&gameState.globalInts);
|
||||
loc->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
|
||||
@@ -779,8 +800,8 @@ namespace FRG
|
||||
menuManager.setChatUnread(idx, unread, msg);
|
||||
};
|
||||
for (auto& npc : loc->npcs) {
|
||||
if (npc && npc->state.canAttack) {
|
||||
npc->state.onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
||||
if (npc && npc->characterState.canAttack) {
|
||||
npc->onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
|
||||
}
|
||||
}
|
||||
loc->requestDarklandsPlayBattleMusic = [this]() {
|
||||
@@ -792,24 +813,23 @@ namespace FRG
|
||||
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
||||
};
|
||||
|
||||
if (config.lockInteractiveObjectsDuringTutorial) {
|
||||
// Objects can be picked up once the tutorial reaches "pick up an item".
|
||||
loc->areInteractiveObjectsLocked = [this]() {
|
||||
return gameState.tutorialStep < TutorialStep::PickUpItem;
|
||||
};
|
||||
}
|
||||
authoritative.lockInteractiveObjectsDuringTutorial =
|
||||
config.lockInteractiveObjectsDuringTutorial;
|
||||
|
||||
if (config.taxi) {
|
||||
const LocationConfig::Taxi& taxi = *config.taxi;
|
||||
loc->state.taxiCarTriggerPosition = taxi.triggerPosition;
|
||||
loc->state.taxiCarWaypoints = taxi.waypoints;
|
||||
loc->state.taxiCarInitialState.position = taxi.initialPosition;
|
||||
loc->state.taxiCarInitialState.rotation = taxi.initialRotation;
|
||||
loc->state.taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||
loc->state.taxiCarInitialState.velocity = 0.f;
|
||||
loc->state.taxiCarInitialState.steeringAngle = 0.f;
|
||||
loc->state.taxiCarInitialState.waypointReachRadius = taxi.waypointReachRadius;
|
||||
loc->state.taxiCar = loc->state.taxiCarInitialState;
|
||||
LocationState& locationState = *authoritative.location;
|
||||
locationState.taxiCarTriggerPosition = taxi.triggerPosition;
|
||||
locationState.taxiCarWaypoints = taxi.waypoints;
|
||||
locationState.taxiCarInitialState.position = taxi.initialPosition;
|
||||
locationState.taxiCarInitialState.rotation = taxi.initialRotation;
|
||||
locationState.taxiCarInitialState.mode = NpcCar::Mode::NONE;
|
||||
locationState.taxiCarInitialState.velocity = 0.f;
|
||||
locationState.taxiCarInitialState.steeringAngle = 0.f;
|
||||
locationState.taxiCarInitialState.waypointReachRadius = taxi.waypointReachRadius;
|
||||
locationState.taxiCar.waypointReachRadius = taxi.waypointReachRadius;
|
||||
if (!authoritative.hasPersistentState)
|
||||
authoritative.resetTaxi();
|
||||
}
|
||||
|
||||
return loc;
|
||||
@@ -866,24 +886,30 @@ namespace FRG
|
||||
if (loc->player) {
|
||||
// Raw pointer: the player belongs to the location (and outlives it), so a
|
||||
// shared_ptr here would keep unloaded locations alive. Rewired on every load.
|
||||
loc->player->state.onHpChanged = [this, loc](float hp, float maxHp, int attackerIndex) {
|
||||
loc->player->onHpChanged = [this, loc](float hp, float maxHp, const std::string& attackerId) {
|
||||
menuManager.updateHealthBar(hp, maxHp);
|
||||
|
||||
if (attackerIndex != CharacterState::kNoTarget) {
|
||||
if (!attackerId.empty()) {
|
||||
//Someone attacked, cancel dialogue and agro the player
|
||||
|
||||
if (loc->dialogueSystem.isDialogueActive()) {
|
||||
loc->dialogueSystem.stopDialogue();
|
||||
}
|
||||
|
||||
if ((loc->player->state.attackTargetIndex == CharacterState::kNoTarget ||
|
||||
((loc->player->state.attackTargetIndex >= 0) &&
|
||||
(loc->npcs[loc->player->state.attackTargetIndex]->state.hp <= 0)))
|
||||
bool currentTargetDead = false;
|
||||
for (const auto& npc : loc->npcs) {
|
||||
if (npc && npc->entityId() == loc->player->characterState.attackTargetId) {
|
||||
currentTargetDead = npc->characterState.hp <= 0.f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((loc->player->characterState.attackTargetId.empty() || currentTargetDead)
|
||||
&&
|
||||
loc->player->isMoving() == false
|
||||
&&
|
||||
loc->player->getHp() > 0) {
|
||||
loc->player->state.attackTargetIndex = attackerIndex;
|
||||
internalGameServer.submit(SetCharacterTargetCommand{
|
||||
loc->locationId(), "player", attackerId});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -905,7 +931,7 @@ namespace FRG
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
logger() << "Shadow map initialized" << std::endl;
|
||||
|
||||
for (auto& [name, loc] : gameState.locations) loc->shadowMap = shadowMap;
|
||||
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loc->shadowMap = shadowMap;
|
||||
|
||||
}
|
||||
void Game::destroyShadowMapAndProparateToLocations()
|
||||
@@ -915,7 +941,7 @@ namespace FRG
|
||||
shadowMap.reset();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
for (auto& [name, loc] : gameState.locations) loc->shadowMap.reset();
|
||||
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loc->shadowMap.reset();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
}
|
||||
@@ -1177,7 +1203,9 @@ namespace FRG
|
||||
|
||||
if (loadingCompleted && currentLocation())
|
||||
{
|
||||
currentLocation()->update(delta);
|
||||
// The authoritative implementation advances simulation; a future
|
||||
// network client can use the same call to poll/apply server updates.
|
||||
internalGameServer.update(delta);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1228,6 +1256,10 @@ namespace FRG
|
||||
}
|
||||
|
||||
void Game::update() {
|
||||
// Host systems (locations, scripts and loading) still execute in-process.
|
||||
// Publish any changes they made since the previous frame as a new immutable
|
||||
// revision before presentation clients query the server.
|
||||
internalGameServer.markHostMutation();
|
||||
SDL_Event event;
|
||||
const bool mobileIsPortrait = loadingCompleted && (Environment::width < Environment::height);
|
||||
while (SDL_PollEvent(&event)) {
|
||||
@@ -1332,8 +1364,9 @@ namespace FRG
|
||||
}
|
||||
else if (event.button.button == SDL_BUTTON_RIGHT
|
||||
&& event.type == SDL_MOUSEBUTTONUP
|
||||
&& editorMode == EditorMode::Navigation
|
||||
&& currentLocation()) {
|
||||
&& currentLocation()
|
||||
&& currentLocation()->editorMode == EditorMode::Navigation
|
||||
) {
|
||||
currentLocation()->editor.handleRightClick();
|
||||
}
|
||||
}
|
||||
@@ -1378,15 +1411,32 @@ namespace FRG
|
||||
}
|
||||
if (event.key.keysym.sym == SDLK_1)
|
||||
{
|
||||
startNightTransition();
|
||||
currentLocation()->switchNavigation(0);
|
||||
//startNightTransition();
|
||||
}
|
||||
if (event.key.keysym.sym == SDLK_2)
|
||||
{
|
||||
startDarklandsTransition();
|
||||
currentLocation()->switchNavigation(1);
|
||||
//startDarklandsTransition();
|
||||
}
|
||||
if (event.key.keysym.sym == SDLK_3)
|
||||
{
|
||||
logger() << "Player position: " << currentLocation()->player->state.position.transpose() << std::endl;
|
||||
currentLocation()->switchNavigation(2);
|
||||
//logger() << "Player position: " << currentLocation()->player->characterState.position.transpose() << std::endl;
|
||||
}
|
||||
if (event.key.keysym.sym == SDLK_n)
|
||||
{
|
||||
|
||||
if (currentLocation()->editorMode == EditorMode::None)
|
||||
{
|
||||
currentLocation()->editorMode = EditorMode::Navigation;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLocation()->editorMode = EditorMode::None;
|
||||
}
|
||||
|
||||
//logger() << "Player position: " << currentLocation()->player->characterState.position.transpose() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1438,12 +1488,18 @@ namespace FRG
|
||||
if (!currentLocation()) return;
|
||||
|
||||
if (currentLocation()->player) {
|
||||
currentLocation()->player->state.slowMoTimeRemaining = currentLocation()->player->state.slowMoActiveTime;
|
||||
currentLocation()->updateCharacterState(
|
||||
*currentLocation()->player, [](CharacterState& workingState) {
|
||||
workingState.slowMoTimeRemaining = workingState.slowMoActiveTime;
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& npc : currentLocation()->npcs) {
|
||||
if (npc) {
|
||||
npc->state.slowMoTimeRemaining = npc->state.slowMoActiveTime;
|
||||
currentLocation()->updateCharacterState(
|
||||
*npc, [](CharacterState& workingState) {
|
||||
workingState.slowMoTimeRemaining = workingState.slowMoActiveTime;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1816,10 +1872,11 @@ namespace FRG
|
||||
if (content.empty()) return {};
|
||||
try {
|
||||
nlohmann::json root = nlohmann::json::parse(content);
|
||||
if (root.value("schemaVersion", -1) != GameState::kSchemaVersion) return {};
|
||||
SaveSlotInfo info;
|
||||
info.empty = false;
|
||||
info.campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId));
|
||||
info.locationName = root.value("currentLocationName", "");
|
||||
info.campaignId = root.at("campaignId").get<std::string>();
|
||||
info.locationName = root.at("currentLocationId").get<std::string>();
|
||||
info.savedAt = root.value("savedAt", "");
|
||||
return info;
|
||||
} catch (...) {
|
||||
@@ -1830,7 +1887,7 @@ namespace FRG
|
||||
void Game::saveGame(int slot)
|
||||
{
|
||||
nlohmann::json root;
|
||||
gameState.save(root);
|
||||
internalGameServer.save(root);
|
||||
std::string path = "save_slot" + std::to_string(slot) + ".json";
|
||||
|
||||
/*std::ofstream file(path);
|
||||
@@ -1858,11 +1915,19 @@ namespace FRG
|
||||
return;
|
||||
}
|
||||
|
||||
// The save's campaign and location may not be loaded yet; the state is applied once they are.
|
||||
const std::string campaignId = root.value("campaignId", std::string(CampaignRegistry::kDefaultCampaignId));
|
||||
const std::string locationId = root.value("currentLocationName", std::string());
|
||||
if (root.value("schemaVersion", -1) != GameState::kSchemaVersion) {
|
||||
std::cerr << "[save] Unsupported save schema in " << path << std::endl;
|
||||
return;
|
||||
}
|
||||
const std::string campaignId = root.value("campaignId", std::string());
|
||||
const std::string locationId = root.value("currentLocationId", std::string());
|
||||
const CampaignConfig* campaign = CampaignRegistry::instance().find(campaignId);
|
||||
if (!campaign || !campaign->findLocation(locationId)) {
|
||||
std::cerr << "[save] Unknown campaign/location in " << path << std::endl;
|
||||
return;
|
||||
}
|
||||
enterLocationThen(campaignId, locationId, [this, root]() {
|
||||
gameState.load(root);
|
||||
internalGameServer.load(root);
|
||||
playMusicForLoadedGame();
|
||||
menuManager.enterGameplay(true);
|
||||
});
|
||||
|
||||
+15
-8
@@ -22,6 +22,7 @@
|
||||
#include "Location.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
#include "GameState.h"
|
||||
#include "game_server/InternalGameServer.h"
|
||||
namespace FRG {
|
||||
|
||||
struct LocationConfig;
|
||||
@@ -73,9 +74,7 @@ namespace FRG {
|
||||
// content reload, don't have a meaningful denominator, so the bar is hidden.
|
||||
bool showLoadingProgressBar = true;
|
||||
|
||||
GameState gameState;
|
||||
|
||||
EditorMode editorMode = EditorMode::None;
|
||||
//EditorMode editorMode = EditorMode::None;
|
||||
|
||||
// Returns false if a transition is already in progress.
|
||||
bool startDarklandsTransition();
|
||||
@@ -83,6 +82,16 @@ namespace FRG {
|
||||
|
||||
std::unique_ptr<AudioPlayerAsync> audioPlayer;
|
||||
|
||||
private:
|
||||
InternalGameServer internalGameServer;
|
||||
// Host-only alias while location/script orchestration is moved behind typed
|
||||
// commands. The referenced object is owned by internalGameServer.
|
||||
GameState& gameState;
|
||||
|
||||
public:
|
||||
IGameServer& server() { return internalGameServer; }
|
||||
const IGameServer& server() const { return internalGameServer; }
|
||||
|
||||
MenuManager menuManager;
|
||||
|
||||
void activateSlowMoEffect();
|
||||
@@ -128,10 +137,8 @@ namespace FRG {
|
||||
void loadGame(int slot);
|
||||
void playMusicForLoadedGame();
|
||||
SaveSlotInfo readSlotInfo(int slot) const;
|
||||
// Loading model: one campaign at a time, and within it only the location the player
|
||||
// is in (gameState.locations). Leaving a location stores its state in
|
||||
// gameState.storedLocationStates and releases it; the player is created once per
|
||||
// campaign and shared by its locations.
|
||||
// Loading model: one campaign at a time. InternalGameServer privately owns the
|
||||
// one live Location while GameState retains pure value state for every location.
|
||||
std::string loadedCampaignId;
|
||||
std::shared_ptr<Character> player;
|
||||
|
||||
@@ -146,7 +153,7 @@ namespace FRG {
|
||||
void unloadCampaign();
|
||||
// Builds the campaign's location and restores its stored state, if any.
|
||||
void loadLocation(const std::string& locationId);
|
||||
// Releases the loaded location, keeping its state in storedLocationStates if asked.
|
||||
// Releases the loaded runtime location, retaining its value state when requested.
|
||||
void unloadLocation(bool storeState);
|
||||
// Teleport zones: moves the player to another location of the campaign.
|
||||
void teleportPlayer(const std::string& destName, const Eigen::Vector3f& destPos, float destRotY);
|
||||
|
||||
+70
-44
@@ -2,23 +2,10 @@
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// Saves made before the tutorial rework kept the dorm and the university tutorials
|
||||
// as two state machines (tutorialStep 0..6 + uniIntTutorialState 0..4); map them
|
||||
// onto the single TutorialStep sequence.
|
||||
static TutorialStep migrateLegacyTutorialStep(int dormStep, int uniIntState)
|
||||
{
|
||||
if (dormStep <= 4) return static_cast<TutorialStep>(dormStep); // Step0..Step4 kept their values
|
||||
switch (uniIntState) {
|
||||
case 2: return TutorialStep::DarklandsHealth; // DarklandsActive
|
||||
case 3: return TutorialStep::DarklandsAttack; // DarklandsStep13
|
||||
case 4: return TutorialStep::Completed; // DarklandsFull
|
||||
default: return TutorialStep::EnterDarklands; // Step10 / Step11
|
||||
}
|
||||
}
|
||||
|
||||
bool GameState::completeTutorialStep(TutorialStep step)
|
||||
{
|
||||
if (tutorialStep != step || step == TutorialStep::Completed) return false;
|
||||
@@ -26,8 +13,29 @@ bool GameState::completeTutorialStep(TutorialStep step)
|
||||
return true;
|
||||
}
|
||||
|
||||
CharacterState* GameState::findCharacter(
|
||||
const std::string& locationId, const std::string& characterId)
|
||||
{
|
||||
auto player = players.find(characterId);
|
||||
if (player != players.end() && player->second) return player->second.get();
|
||||
auto location = locations.find(locationId);
|
||||
return location == locations.end()
|
||||
? nullptr : location->second.findCharacter(characterId, players);
|
||||
}
|
||||
|
||||
const CharacterState* GameState::findCharacter(
|
||||
const std::string& locationId, const std::string& characterId) const
|
||||
{
|
||||
auto player = players.find(characterId);
|
||||
if (player != players.end() && player->second) return player->second.get();
|
||||
auto location = locations.find(locationId);
|
||||
return location == locations.end()
|
||||
? nullptr : location->second.findCharacter(characterId, players);
|
||||
}
|
||||
|
||||
void GameState::save(nlohmann::json& out) const
|
||||
{
|
||||
out["schemaVersion"] = kSchemaVersion;
|
||||
// Timestamp
|
||||
std::time_t now = std::time(nullptr);
|
||||
std::tm* tm = std::localtime(&now);
|
||||
@@ -37,7 +45,7 @@ void GameState::save(nlohmann::json& out) const
|
||||
|
||||
// --- World ---
|
||||
out["campaignId"] = campaignId;
|
||||
out["currentLocationName"] = currentLocationName;
|
||||
out["currentLocationId"] = currentLocationName;
|
||||
out["isDarklands"] = isDarklands;
|
||||
out["isNight"] = isNight;
|
||||
out["isDawn"] = isDawn;
|
||||
@@ -87,16 +95,21 @@ void GameState::save(nlohmann::json& out) const
|
||||
questJournal.save(questJson);
|
||||
out["questJournal"] = std::move(questJson);
|
||||
|
||||
// --- Players ---
|
||||
out["players"] = nlohmann::json::object();
|
||||
for (const auto& [id, player] : players) {
|
||||
if (!player) continue;
|
||||
nlohmann::json playerJson;
|
||||
player->save(playerJson);
|
||||
out["players"][id] = std::move(playerJson);
|
||||
}
|
||||
|
||||
// --- Locations ---
|
||||
// Unloaded locations keep their last stored state; loaded ones are saved live.
|
||||
nlohmann::json locsJson = nlohmann::json::object();
|
||||
for (const auto& [name, stored] : storedLocationStates) locsJson[name] = stored;
|
||||
for (const auto& [name, loc] : locations) {
|
||||
if (loc) {
|
||||
nlohmann::json locJson;
|
||||
loc->save(locJson);
|
||||
locsJson[name] = std::move(locJson);
|
||||
}
|
||||
for (const auto& [name, location] : locations) {
|
||||
nlohmann::json locJson;
|
||||
location.save(locJson);
|
||||
locsJson[name] = std::move(locJson);
|
||||
}
|
||||
out["locations"] = std::move(locsJson);
|
||||
|
||||
@@ -105,12 +118,16 @@ void GameState::save(nlohmann::json& out) const
|
||||
|
||||
void GameState::load(const nlohmann::json& in)
|
||||
{
|
||||
const int schemaVersion = in.value("schemaVersion", -1);
|
||||
if (schemaVersion != kSchemaVersion)
|
||||
throw std::runtime_error(
|
||||
"Unsupported game-state schema version: " +
|
||||
std::to_string(schemaVersion));
|
||||
//logger() << "GameState::load step 1" << std::endl;
|
||||
|
||||
// --- World ---
|
||||
// Literal default: saves made before campaigns existed belong to the main game.
|
||||
campaignId = in.value("campaignId", std::string("main"));
|
||||
currentLocationName = in.value("currentLocationName", currentLocationName);
|
||||
campaignId = in.at("campaignId").get<std::string>();
|
||||
currentLocationName = in.at("currentLocationId").get<std::string>();
|
||||
isDarklands = in.value("isDarklands", isDarklands);
|
||||
isNight = in.value("isNight", isNight);
|
||||
isDawn = in.value("isDawn", isDawn);
|
||||
@@ -142,11 +159,8 @@ void GameState::load(const nlohmann::json& in)
|
||||
//logger() << "GameState::load step 5" << std::endl;
|
||||
|
||||
// --- Tutorial ---
|
||||
if (in.contains("uniIntTutorialState")) {
|
||||
tutorialStep = migrateLegacyTutorialStep(in.value("tutorialStep", 0), in["uniIntTutorialState"].get<int>());
|
||||
} else {
|
||||
tutorialStep = static_cast<TutorialStep>(in.value("tutorialStep", static_cast<int>(tutorialStep)));
|
||||
}
|
||||
tutorialStep = static_cast<TutorialStep>(
|
||||
in.at("tutorialStep").get<int>());
|
||||
tutorialPhoneChatScreenOpened = in.value("tutorialPhoneChatScreenOpened", tutorialPhoneChatScreenOpened);
|
||||
tutorialJournalScreenOpened = in.value("tutorialJournalScreenOpened", tutorialJournalScreenOpened);
|
||||
tutorialNeedOpenTaxiScreen = in.value("tutorialNeedOpenTaxiScreen", tutorialNeedOpenTaxiScreen);
|
||||
@@ -193,20 +207,32 @@ void GameState::load(const nlohmann::json& in)
|
||||
|
||||
logger() << "GameState::load step 10" << std::endl;
|
||||
|
||||
// --- Locations ---
|
||||
// Every location's state is stored (applied when the location loads); the ones
|
||||
// already loaded get it right away.
|
||||
storedLocationStates.clear();
|
||||
if (in.contains("locations") && in["locations"].is_object()) {
|
||||
for (const auto& [name, locJson] : in["locations"].items()) {
|
||||
//logger() << "GameState::load step 10.1: " << name << std::endl;
|
||||
storedLocationStates[name] = locJson;
|
||||
// --- Players ---
|
||||
const auto& playerValues = in.at("players");
|
||||
if (!playerValues.is_object() || !playerValues.contains("player"))
|
||||
throw std::runtime_error("Game state requires players.player");
|
||||
for (const auto& [id, value] : playerValues.items()) {
|
||||
auto& player = players[id];
|
||||
if (!player) player = std::make_shared<CharacterState>();
|
||||
player->load(value);
|
||||
}
|
||||
for (auto it = players.begin(); it != players.end();) {
|
||||
if (!playerValues.contains(it->first)) it = players.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
|
||||
auto it = locations.find(name);
|
||||
if (it != locations.end() && it->second) {
|
||||
it->second->load(locJson);
|
||||
}
|
||||
}
|
||||
// --- Locations ---
|
||||
const auto& locationValues = in.at("locations");
|
||||
if (!locationValues.is_object() ||
|
||||
!locationValues.contains(currentLocationName))
|
||||
throw std::runtime_error(
|
||||
"Game state does not contain current location '" +
|
||||
currentLocationName + "'");
|
||||
for (const auto& [id, value] : locationValues.items())
|
||||
locations[id].load(value);
|
||||
for (auto it = locations.begin(); it != locations.end();) {
|
||||
if (!locationValues.contains(it->first)) it = locations.erase(it);
|
||||
else ++it;
|
||||
}
|
||||
//logger() << "GameState::load step 11" << std::endl;
|
||||
|
||||
|
||||
+13
-8
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include "items/Item.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "Location.h"
|
||||
#include "LocationGameState.h"
|
||||
#include "ISaveable.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <unordered_map>
|
||||
@@ -32,17 +32,17 @@ struct StoredChatMessage {
|
||||
bool incoming;
|
||||
};
|
||||
|
||||
using LocationMap = std::unordered_map<std::string, std::shared_ptr<Location>>;
|
||||
|
||||
struct GameState : public ISaveable {
|
||||
static constexpr int kSchemaVersion = 1;
|
||||
|
||||
// --- World ---
|
||||
// Campaign being played (see CampaignRegistry).
|
||||
std::string campaignId = "main";
|
||||
// The campaign's loaded location(s); only the one the player is in is kept loaded.
|
||||
LocationMap locations;
|
||||
// Saved state (Location::save) of the campaign's locations that are not loaded,
|
||||
// applied when one is loaded again. save()/load() cover both maps.
|
||||
std::unordered_map<std::string, nlohmann::json> storedLocationStates;
|
||||
// Pure authoritative location data. Live Location/render/script instances are
|
||||
// private runtime storage in InternalGameServer and never enter GameState.
|
||||
std::unordered_map<std::string, LocationGameState> locations;
|
||||
// Players are campaign-level entities and remain stable across locations.
|
||||
PlayerStateMap players;
|
||||
std::string currentLocationName;
|
||||
bool isDarklands = false;
|
||||
bool isNight = false;
|
||||
@@ -73,6 +73,11 @@ struct GameState : public ISaveable {
|
||||
// Moves to the next step if `step` is the current one; returns whether it did.
|
||||
bool completeTutorialStep(TutorialStep step);
|
||||
|
||||
CharacterState* findCharacter(
|
||||
const std::string& locationId, const std::string& characterId);
|
||||
const CharacterState* findCharacter(
|
||||
const std::string& locationId, const std::string& characterId) const;
|
||||
|
||||
// --- Phone / chat ---
|
||||
bool chatUnread[3] = { true, true, true };
|
||||
std::string chatPreviewMsg[3];
|
||||
|
||||
+290
-783
File diff suppressed because it is too large.
Load diff
+37
-42
@@ -16,13 +16,15 @@
|
||||
#include "TeleportZone.h"
|
||||
#include "LocationEditor.h"
|
||||
#include "LocationState.h"
|
||||
#include "ISaveable.h"
|
||||
#include "LocationGameState.h"
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
#include "Character.h"
|
||||
|
||||
namespace FRG
|
||||
{
|
||||
class IGameServer;
|
||||
|
||||
struct PointLight
|
||||
{
|
||||
@@ -35,7 +37,7 @@ namespace FRG
|
||||
// Rectangular limits — keep light confined to a room-sized AABB centred on the light
|
||||
float limitX = 99999.f; // shader fragment limit: half-width in world X
|
||||
float limitY = 99999.f; // shader fragment limit: half-width in world Z
|
||||
float autoLightLimitX = -1.0f; // activation rect half-width X; <0 = use legacy nearest-light logic
|
||||
float autoLightLimitX = -1.0f; // activation rect half-width X; <0 = use nearest-light logic
|
||||
float autoLightLimitY = -1.0f; // activation rect half-width Z
|
||||
};
|
||||
|
||||
@@ -55,7 +57,7 @@ namespace FRG
|
||||
std::string interactiveObjectsJsonPath;
|
||||
std::string npcsJsonPath;
|
||||
std::string dialoguesJsonPath;
|
||||
std::vector<std::string> navigationJsonPaths;
|
||||
//std::vector<std::string> navigationJsonPaths;
|
||||
std::string scriptPath;
|
||||
std::string teleportsJsonPath;
|
||||
std::string triggerZonesJsonPath;
|
||||
@@ -64,17 +66,17 @@ namespace FRG
|
||||
// Player shared by all locations of a campaign (see Location::createPlayer);
|
||||
// setup() creates a private one when this is null.
|
||||
std::shared_ptr<Character> player;
|
||||
LocationGameState* gameState = nullptr;
|
||||
};
|
||||
|
||||
class Location : public ISaveable
|
||||
class Location
|
||||
{
|
||||
public:
|
||||
Location(Renderer& iRenderer, Inventory& iInventory);
|
||||
Location(Renderer& iRenderer, Inventory& iInventory, LocationState& state, IGameServer& server,
|
||||
std::string locationId);
|
||||
// Detaches the (shared, longer-lived) player from this location.
|
||||
~Location();
|
||||
|
||||
// ---- All serialisable runtime state ----
|
||||
LocationState state;
|
||||
const std::string& locationId() const { return locationId_; }
|
||||
|
||||
std::unordered_map<std::string, LoadedGameObject> gameObjects;
|
||||
|
||||
@@ -88,10 +90,11 @@ namespace FRG
|
||||
std::vector<std::unique_ptr<Character>> npcs;
|
||||
|
||||
// Builds the player character (models, animations, weapon, textures).
|
||||
static std::shared_ptr<Character> createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer);
|
||||
static std::shared_ptr<Character> createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer,
|
||||
CharacterState& state, IGameServer& server);
|
||||
|
||||
std::vector<PathFinder> navigationMaps;
|
||||
PathFinder* navigation = nullptr;
|
||||
//std::vector<PathFinder> navigationMaps;
|
||||
//int state.activeNavigationIndex = 0;
|
||||
|
||||
std::shared_ptr<ShadowMap> shadowMap;
|
||||
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
|
||||
@@ -114,10 +117,6 @@ namespace FRG
|
||||
|
||||
std::function<void()> onPlayerTaxiRequired;
|
||||
|
||||
// Optional, set by Game. While it returns true the player can't pick up /
|
||||
// use interactive objects (the dorm tutorial unlocks them step by step).
|
||||
std::function<bool()> areInteractiveObjectsLocked;
|
||||
|
||||
// Set by Game after setup(). Lua and onDeathAnimComplete call this to
|
||||
// request the transition without coupling Location to Game directly.
|
||||
std::function<bool()> requestDarklandsTransition;
|
||||
@@ -140,7 +139,7 @@ namespace FRG
|
||||
EditorMode editorMode = EditorMode::None;
|
||||
LocationEditor editor;
|
||||
|
||||
std::vector<std::string> navigationMapPaths;
|
||||
//std::vector<std::string> navigationMapPaths;
|
||||
|
||||
// Set by Game when the user's primary pointer (left mouse / single touch)
|
||||
// has crossed the tap-vs-drag threshold and is now rotating the camera.
|
||||
@@ -162,8 +161,20 @@ namespace FRG
|
||||
|
||||
|
||||
void setup(const LocationSetup& params, Quest::QuestJournal* journal, AudioPlayerAsync* audioPlayer);
|
||||
void setupNavigation(const std::vector<std::string>& paths);
|
||||
//void setupNavigation(const std::vector<std::string>& paths);
|
||||
bool switchNavigation(int index);
|
||||
void setCharacterTarget(Character& character, const Eigen::Vector3f& target,
|
||||
std::string callbackName = "");
|
||||
void updateCharacterState(
|
||||
Character& character,
|
||||
const std::function<void(CharacterState&)>& mutation);
|
||||
void clearCharacterAttackTarget(Character& character);
|
||||
void stopCharacter(Character& character);
|
||||
void faceCharacter(Character& character, std::string targetId);
|
||||
void replanCharacter(Character& character);
|
||||
void resetCharacterAfterDeath(Character& character);
|
||||
void tryCharacterAttackCombo(Character& character);
|
||||
void setCharacterHp(Character& character, float hp);
|
||||
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
|
||||
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
|
||||
|
||||
@@ -177,8 +188,7 @@ namespace FRG
|
||||
|
||||
bool setNavigationAreaAvailable(const std::string& areaName, bool available);
|
||||
|
||||
void update(int64_t deltaMs);
|
||||
void updateNpcCar(int64_t deltaMs);
|
||||
void update(int64_t deltaMs, const LocationSimulationEvents& events);
|
||||
// Purely visual feedback for click-triggered player routes. Disabling it
|
||||
// also cancels an animation that is currently playing.
|
||||
void setPathVisualizationEnabled(bool enabled);
|
||||
@@ -194,26 +204,21 @@ namespace FRG
|
||||
void setDialogueFlag(const std::string& flag, int value);
|
||||
int getDialogueFlag(const std::string& flag) const;
|
||||
|
||||
void onTaxiCalled();
|
||||
|
||||
// ---- Save / Load ----
|
||||
// Serialises LocationState + all Character/InteractiveObject sub-states + Lua globals.
|
||||
void save(nlohmann::json& out) const override;
|
||||
// Restores state from JSON written by save.
|
||||
// Navigation pointer and triggerZone.playerInside are re-synced automatically.
|
||||
void load(const nlohmann::json& in) override;
|
||||
protected:
|
||||
friend class LocationEditor;
|
||||
friend class Game;
|
||||
friend class ScriptEngine;
|
||||
friend class InternalGameServer;
|
||||
Renderer& renderer;
|
||||
Inventory& inventory;
|
||||
AudioPlayerAsync* audioPlayer;
|
||||
|
||||
private:
|
||||
LocationState& state; // non-owning binding to LocationGameState
|
||||
IGameServer& gameServer_;
|
||||
std::string locationId_;
|
||||
// ---- Pointer-to-index helpers ----
|
||||
InteractiveObject* getTargetInteractiveObject();
|
||||
TeleportZone* getTargetTeleportZone();
|
||||
int findInteractiveObjectIndex(const InteractiveObject* obj) const;
|
||||
int findTeleportZoneIndex(const TeleportZone* tz) const;
|
||||
|
||||
enum class InstancedTexture { None, Color, Darklands };
|
||||
|
||||
@@ -223,26 +228,16 @@ namespace FRG
|
||||
void drawInstancedStatics(const std::string& instancedShaderName, InstancedTexture textureKind,
|
||||
const std::function<void()>& setUniforms = {});
|
||||
|
||||
void resolveCharacterCollisions();
|
||||
void updateDynamicReplans(int64_t deltaMs);
|
||||
void loadTeleportZones(const std::string& jsonPath, const char* zipFile);
|
||||
void loadTriggerZones(const std::string& jsonPath, const char* zipFile);
|
||||
void loadPointLights(const std::string& jsonPath, const char* zipFile);
|
||||
void updateTriggerZones(const Eigen::Vector3f& playerPos);
|
||||
void nudgeCharacterAside(Character* standing, const Eigen::Vector3f& awayFrom);
|
||||
int findNpcIndex(const Character* npc) const;
|
||||
void fireBumpCallbacks(Character* mover, Character* standing);
|
||||
void captureAuxiliaryState(LocationGameState& out) const;
|
||||
void applyAuxiliaryState(const LocationGameState& in);
|
||||
|
||||
PathVisualization pathVisualization;
|
||||
|
||||
std::unordered_map<Character*, Eigen::Vector3f> lastCharacterPositions;
|
||||
std::unordered_map<Character*, int64_t> replanCooldownRemainingMs;
|
||||
std::unordered_map<Character*, int> stuckFrameCounts;
|
||||
std::unordered_map<Character*, float> stuckPauseRemainingS;
|
||||
|
||||
static constexpr float NPC_BUMP_CALLBACK_COOLDOWN = 5.0f;
|
||||
std::unordered_map<int, float> npcBumpedByPlayerCooldown;
|
||||
std::unordered_map<int, float> npcBumpsPlayerCooldown;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+20
-16
@@ -24,10 +24,9 @@ namespace FRG
|
||||
void LocationEditor::buildNavMeshes()
|
||||
{
|
||||
navigationEditorNavMeshes.clear();
|
||||
if (!loc.navigation) return;
|
||||
const float y = loc.navigation->getFloorY() + 0.02f;
|
||||
const float y = loc.state.navigationMaps[loc.state.activeNavigationIndex].getFloorY() + 0.02f;
|
||||
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
||||
for (const auto& obs : loc.navigation->getObstaclePolygons()) {
|
||||
for (const auto& obs : loc.state.navigationMaps[loc.state.activeNavigationIndex].getObstaclePolygons()) {
|
||||
if (obs.polygon.size() < 3) continue;
|
||||
VertexRenderStruct mesh;
|
||||
mesh.data = CreatePolygonFloor(obs.polygon, y, red);
|
||||
@@ -51,7 +50,7 @@ namespace FRG
|
||||
{
|
||||
VertexDataStruct data;
|
||||
const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f);
|
||||
const float y = loc.navigation ? loc.navigation->getFloorY() + 0.05f : 0.05f;
|
||||
const float y = loc.state.navigationMaps[loc.state.activeNavigationIndex].getFloorY() + 0.05f;
|
||||
const float s = 0.2f;
|
||||
|
||||
for (const auto& pt : navigationEditorPoints) {
|
||||
@@ -68,12 +67,19 @@ namespace FRG
|
||||
}
|
||||
|
||||
navigationEditorPointsMesh.data = std::move(data);
|
||||
navigationEditorPointsMesh.RefreshVBO();
|
||||
if (navigationEditorPointsMesh.data.PositionData.size() > 0)
|
||||
{
|
||||
navigationEditorPointsMesh.RefreshVBO();
|
||||
}
|
||||
}
|
||||
|
||||
void LocationEditor::drawPoints()
|
||||
{
|
||||
if (navigationEditorPoints.empty()) return;
|
||||
if (navigationEditorPointsMesh.data.PositionData.empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
loc.renderer.shaderManager.PushShader("defaultColor");
|
||||
loc.renderer.SetMatrix();
|
||||
loc.renderer.DrawVertexRenderStruct(navigationEditorPointsMesh);
|
||||
@@ -120,12 +126,12 @@ namespace FRG
|
||||
poly.polygon.emplace_back(pt.x(), pt.z());
|
||||
}
|
||||
|
||||
if (loc.navigation) loc.navigation->addObstaclePolygon(poly);
|
||||
loc.state.navigationMaps[loc.state.activeNavigationIndex].addObstaclePolygon(poly);
|
||||
logger() << "[NAV_EDITOR] Added obstacle '" << poly.name << "' with "
|
||||
<< poly.polygon.size() << " vertices\n";
|
||||
|
||||
{
|
||||
const float y = loc.navigation ? loc.navigation->getFloorY() + 0.02f : 0.02f;
|
||||
const float y = loc.state.navigationMaps[loc.state.activeNavigationIndex].getFloorY() + 0.02f;
|
||||
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
|
||||
VertexRenderStruct mesh;
|
||||
mesh.data = CreatePolygonFloor(poly.polygon, y, red);
|
||||
@@ -147,14 +153,12 @@ namespace FRG
|
||||
if (!std::filesystem::exists(baseName + ".json")) break;
|
||||
}
|
||||
|
||||
if (!loc.navigation) return;
|
||||
|
||||
if (loc.navigation->saveConfig(baseName + ".json"))
|
||||
if (loc.state.navigationMaps[loc.state.activeNavigationIndex].saveConfig(baseName + ".json"))
|
||||
logger() << "[NAV_EDITOR] Saved config to: " << baseName << ".json\n";
|
||||
else
|
||||
std::cerr << "[NAV_EDITOR] Failed to save config to: " << baseName << ".json\n";
|
||||
|
||||
if (loc.navigation->saveGrid(baseName + ".txt"))
|
||||
if (loc.state.navigationMaps[loc.state.activeNavigationIndex].saveGrid(baseName + ".txt"))
|
||||
logger() << "[NAV_EDITOR] Saved grid to: " << baseName << ".txt\n";
|
||||
else
|
||||
std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n";
|
||||
@@ -162,8 +166,8 @@ namespace FRG
|
||||
|
||||
void LocationEditor::reload()
|
||||
{
|
||||
loc.setupNavigation(loc.navigationMapPaths);
|
||||
logger() << "[NAV_EDITOR] Reloaded navigation maps (" << loc.navigationMapPaths.size() << " maps)\n";
|
||||
//loc.setupNavigation(loc.navigationMapPaths);
|
||||
//logger() << "[NAV_EDITOR] Reloaded navigation maps (" << loc.navigationMapPaths.size() << " maps)\n";
|
||||
}
|
||||
|
||||
static VertexDataStruct CreateBoundsBoxMesh(const Eigen::Vector3f corners[8], const Eigen::Vector3f& color)
|
||||
@@ -454,9 +458,9 @@ namespace FRG
|
||||
data.rotationX = 0.0f;
|
||||
data.rotationY = rotDist(rng);
|
||||
data.rotationZ = 0.0f;
|
||||
data.positionX = loc.player->state.position.x();
|
||||
data.positionY = loc.player->state.position.y();//-0.1;
|
||||
data.positionZ = loc.player->state.position.z();
|
||||
data.positionX = loc.player->characterState.position.x();
|
||||
data.positionY = loc.player->characterState.position.y();//-0.1;
|
||||
data.positionZ = loc.player->characterState.position.z();
|
||||
data.scale = scaleDist(rng);
|
||||
|
||||
GameObjectData data2;
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
#include "LocationGameState.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
namespace {
|
||||
constexpr float PI = 3.14159265358979323846f;
|
||||
constexpr float NPC_TALK_DISTANCE = 1.35f;
|
||||
}
|
||||
|
||||
CharacterState* LocationGameState::findCharacter(
|
||||
const std::string& id, PlayerStateMap& players)
|
||||
{
|
||||
if (id.empty()) return nullptr;
|
||||
auto entity = players.find(id);
|
||||
if (entity != players.end() && entity->second) return entity->second.get();
|
||||
auto npc = npcs.find(id);
|
||||
if (npc != npcs.end() && npc->second) return npc->second.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const CharacterState* LocationGameState::findCharacter(
|
||||
const std::string& id, const PlayerStateMap& players) const
|
||||
{
|
||||
if (id.empty()) return nullptr;
|
||||
auto entity = players.find(id);
|
||||
if (entity != players.end() && entity->second) return entity->second.get();
|
||||
auto npc = npcs.find(id);
|
||||
if (npc != npcs.end() && npc->second) return npc->second.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void LocationGameState::startTaxi()
|
||||
{
|
||||
location->taxiCar.waypoints = location->taxiCarWaypoints;
|
||||
location->taxiCar.currentWaypoint = 0;
|
||||
location->taxiCar.mode = NpcCar::Mode::FOLLOW_WAYPOINTS;
|
||||
}
|
||||
|
||||
void LocationGameState::resetTaxi()
|
||||
{
|
||||
location->taxiCar = location->taxiCarInitialState;
|
||||
location->taxiArrivedCallbackCalled = false;
|
||||
}
|
||||
|
||||
bool LocationGameState::updateNpcCar(std::int64_t deltaMs)
|
||||
{
|
||||
const float dt = static_cast<float>(deltaMs) / 1000.0f;
|
||||
NpcCar& car = location->taxiCar;
|
||||
|
||||
Eigen::Vector3f target = car.position;
|
||||
float throttle = 0.f;
|
||||
bool hasTarget = false;
|
||||
bool waypointsOver = false;
|
||||
|
||||
if (car.mode == NpcCar::Mode::FOLLOW_WAYPOINTS) {
|
||||
if (!car.waypoints.empty()) {
|
||||
if (car.currentWaypoint >= car.waypoints.size())
|
||||
car.currentWaypoint = 0;
|
||||
|
||||
Eigen::Vector3f toTarget =
|
||||
car.waypoints[car.currentWaypoint] - car.position;
|
||||
toTarget.y() = 0.f;
|
||||
if (toTarget.norm() < car.waypointReachRadius) {
|
||||
++car.currentWaypoint;
|
||||
if (car.currentWaypoint == car.waypoints.size())
|
||||
waypointsOver = true;
|
||||
}
|
||||
|
||||
if (waypointsOver) {
|
||||
car.mode = NpcCar::Mode::NONE;
|
||||
} else {
|
||||
target = car.waypoints[car.currentWaypoint];
|
||||
throttle = 1.f;
|
||||
hasTarget = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float targetHeading = car.rotation;
|
||||
if (hasTarget) {
|
||||
Eigen::Vector3f toTarget = target - car.position;
|
||||
toTarget.y() = 0.f;
|
||||
if (toTarget.squaredNorm() > 1e-4f)
|
||||
targetHeading = std::atan2(-toTarget.x(), -toTarget.z());
|
||||
}
|
||||
|
||||
float angleDiff = targetHeading - car.rotation;
|
||||
while (angleDiff > PI) angleDiff -= 2.f * PI;
|
||||
while (angleDiff < -PI) angleDiff += 2.f * PI;
|
||||
|
||||
const float turnIntent = (std::max)(-1.f, (std::min)(1.f, angleDiff * 2.f));
|
||||
const float targetSteer = turnIntent * NpcCar::maxSteerAngle;
|
||||
const float steerLerp = (std::min)(1.f, dt * 8.f);
|
||||
car.steeringAngle += (targetSteer - car.steeringAngle) * steerLerp;
|
||||
|
||||
if (throttle > 0.f) {
|
||||
car.velocity += NpcCar::acceleration * dt * throttle;
|
||||
} else {
|
||||
const float resistance = NpcCar::friction * dt;
|
||||
if (car.velocity > 0.f)
|
||||
car.velocity = (std::max)(0.f, car.velocity - resistance);
|
||||
else if (car.velocity < 0.f)
|
||||
car.velocity = (std::min)(0.f, car.velocity + resistance);
|
||||
}
|
||||
car.velocity = (std::max)(
|
||||
-NpcCar::maxReverseSpeed, (std::min)(NpcCar::maxSpeed, car.velocity));
|
||||
|
||||
if (std::abs(car.velocity) > 0.01f) {
|
||||
const float speedFactor = car.velocity / NpcCar::maxSpeed;
|
||||
car.rotation += turnIntent * NpcCar::turnRate * dt * speedFactor;
|
||||
}
|
||||
|
||||
const Eigen::Vector3f forward(
|
||||
-std::sin(car.rotation), 0.f, -std::cos(car.rotation));
|
||||
const Eigen::Vector3f totalDelta = forward * car.velocity * dt;
|
||||
const float totalDist = totalDelta.norm();
|
||||
constexpr float maxSubstep = 0.2f;
|
||||
const int substeps = (std::max)(
|
||||
1, static_cast<int>(std::ceil(totalDist / maxSubstep)));
|
||||
const Eigen::Vector3f stepDelta = totalDelta / static_cast<float>(substeps);
|
||||
for (int step = 0; step < substeps; ++step)
|
||||
car.position += stepDelta;
|
||||
|
||||
car.wheelAngle += car.velocity * 4.f * dt;
|
||||
while (car.wheelAngle > 2.f * PI)
|
||||
car.wheelAngle -= 2.f * PI;
|
||||
|
||||
if (!location->taxiArrivedCallbackCalled) {
|
||||
const Eigen::Vector3f toArrival =
|
||||
car.position - location->taxiCarTriggerPosition;
|
||||
if (toArrival.squaredNorm() < 9.f) {
|
||||
location->taxiArrivedCallbackCalled = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
LocationSimulationEvents LocationGameState::update(
|
||||
std::int64_t deltaMs, PlayerStateMap& players, bool interactiveObjectsLocked)
|
||||
{
|
||||
LocationSimulationEvents result;
|
||||
result.taxiArrived = updateNpcCar(deltaMs);
|
||||
std::vector<std::string> objectIds;
|
||||
objectIds.reserve(interactiveObjects.size());
|
||||
for (const auto& [id, object] : interactiveObjects)
|
||||
if (object) objectIds.push_back(id);
|
||||
std::sort(objectIds.begin(), objectIds.end());
|
||||
for (const std::string& id : objectIds) {
|
||||
InteractiveObjectUpdateEvents events =
|
||||
interactiveObjects.at(id)->update(deltaMs);
|
||||
if (events.onCompleteName.empty()) continue;
|
||||
result.interactiveObjects.push_back(
|
||||
{id, std::move(events.onCompleteName), false, {}});
|
||||
}
|
||||
|
||||
// All characters observe the same tick-start state. Damage is applied only
|
||||
// after every state has advanced, so vector order cannot suppress an attack.
|
||||
std::unordered_map<std::string, CharacterObservation> observations;
|
||||
for (const auto& [id, player] : players)
|
||||
if (player)
|
||||
observations.emplace(
|
||||
id, CharacterObservation{player->position, player->hp});
|
||||
for (const auto& [id, npc] : npcs)
|
||||
if (npc)
|
||||
observations.emplace(
|
||||
id, CharacterObservation{npc->position, npc->hp});
|
||||
|
||||
auto advance = [&](const std::string& id, CharacterState& state) {
|
||||
CharacterUpdateContext context;
|
||||
auto attack = observations.find(state.attackTargetId);
|
||||
if (attack != observations.end()) context.attackTarget = &attack->second;
|
||||
auto face = observations.find(state.faceTargetId);
|
||||
if (face != observations.end()) context.faceTarget = &face->second;
|
||||
CharacterUpdateEvents events = state.update(deltaMs, context);
|
||||
if (!events.arrivedCallbackName.empty() || events.attackImpact ||
|
||||
events.deathAnimationComplete || events.navigationRequested) {
|
||||
result.characters.push_back({id, std::move(events)});
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<std::string> playerIds;
|
||||
playerIds.reserve(players.size());
|
||||
for (const auto& [id, player] : players)
|
||||
if (player) playerIds.push_back(id);
|
||||
std::sort(playerIds.begin(), playerIds.end());
|
||||
for (const std::string& id : playerIds)
|
||||
advance(id, *players.at(id));
|
||||
std::vector<std::string> npcIds;
|
||||
npcIds.reserve(npcs.size());
|
||||
for (const auto& [id, npc] : npcs)
|
||||
if (npc) npcIds.push_back(id);
|
||||
std::sort(npcIds.begin(), npcIds.end());
|
||||
for (const std::string& id : npcIds)
|
||||
advance(id, *npcs.at(id));
|
||||
|
||||
for (const CharacterSimulationEvent& source : result.characters) {
|
||||
if (!source.events.attackImpact) continue;
|
||||
CharacterState* target = findCharacter(source.events.attackTargetId, players);
|
||||
if (!target) continue;
|
||||
constexpr float damage = 10.f;
|
||||
target->setHp(target->hp - damage);
|
||||
result.damage.push_back({
|
||||
source.events.attackTargetId,
|
||||
source.characterId,
|
||||
damage,
|
||||
target->hp,
|
||||
target->initialHp,
|
||||
source.events.attackDirection});
|
||||
}
|
||||
|
||||
auto player = players.find("player");
|
||||
if (!interactiveObjectsLocked &&
|
||||
!location->targetInteractiveObjectId.empty() &&
|
||||
player != players.end() && player->second) {
|
||||
auto target = interactiveObjects.find(location->targetInteractiveObjectId);
|
||||
const auto object =
|
||||
target == interactiveObjects.end() ? nullptr : target->second;
|
||||
if (object && !object->isAnimating) {
|
||||
const Eigen::Vector3f& approachTarget = object->hasInteractionPosition
|
||||
? object->interactionPosition : object->position;
|
||||
if ((player->second->position - approachTarget).norm() <= object->approachRadius) {
|
||||
result.interactiveObjects.push_back(
|
||||
{location->targetInteractiveObjectId, {}, true,
|
||||
object->activateFunctionName});
|
||||
location->targetInteractiveObjectId.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!location->targetInteractNpcId.empty() &&
|
||||
player != players.end() && player->second &&
|
||||
npcs.count(location->targetInteractNpcId) != 0 &&
|
||||
npcs.at(location->targetInteractNpcId)) {
|
||||
const auto& npc = npcs.at(location->targetInteractNpcId);
|
||||
const float distance =
|
||||
(player->second->position - npc->position).norm();
|
||||
if (distance <= NPC_TALK_DISTANCE) {
|
||||
player->second->stopInPlace();
|
||||
npc->faceTargetId = "player";
|
||||
result.npcInteractionId = location->targetInteractNpcId;
|
||||
location->targetInteractNpcId.clear();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void LocationGameState::save(nlohmann::json& out) const
|
||||
{
|
||||
location->save(out["state"]);
|
||||
|
||||
out["npcs"] = nlohmann::json::object();
|
||||
for (const auto& [id, npc] : npcs) {
|
||||
if (npc) {
|
||||
nlohmann::json value;
|
||||
npc->save(value);
|
||||
out["npcs"][id] = std::move(value);
|
||||
}
|
||||
}
|
||||
|
||||
out["interactiveObjects"] = nlohmann::json::object();
|
||||
for (const auto& [id, object] : interactiveObjects) {
|
||||
if (!object) continue;
|
||||
nlohmann::json value;
|
||||
object->save(value);
|
||||
out["interactiveObjects"][id] = std::move(value);
|
||||
}
|
||||
|
||||
out["dialogue"] = dialogue;
|
||||
out["scriptGlobals"] = scriptGlobals;
|
||||
out["triggerZones"] = nlohmann::json::object();
|
||||
for (const auto& [id, zone] : triggerZones)
|
||||
out["triggerZones"][id] = {{"enabled", zone.enabled}, {"playerInside", zone.playerInside}};
|
||||
out["teleportZones"] = nlohmann::json::object();
|
||||
for (const auto& [id, active] : teleportZones)
|
||||
out["teleportZones"][id] = active;
|
||||
}
|
||||
|
||||
void LocationGameState::load(const nlohmann::json& in)
|
||||
{
|
||||
hasPersistentState = true;
|
||||
if (!location) location = std::make_shared<LocationState>();
|
||||
location->load(in.at("state"));
|
||||
|
||||
const auto& npcValues = in.at("npcs");
|
||||
if (!npcValues.is_object())
|
||||
throw std::runtime_error("Location npcs must be an object");
|
||||
{
|
||||
const auto& values = npcValues;
|
||||
if (!npcs.empty()) {
|
||||
for (const auto& [id, npc] : npcs)
|
||||
if (!values.contains(id))
|
||||
throw std::runtime_error(
|
||||
"Saved location is missing NPC '" + id + "'");
|
||||
for (const auto& [id, value] : values.items())
|
||||
if (npcs.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"Saved location contains unknown NPC '" + id + "'");
|
||||
}
|
||||
for (const auto& [id, value] : values.items()) {
|
||||
auto& npc = npcs[id];
|
||||
if (!npc) npc = std::make_shared<CharacterState>();
|
||||
npc->load(value);
|
||||
}
|
||||
}
|
||||
|
||||
const auto& objectValues = in.at("interactiveObjects");
|
||||
if (!objectValues.is_object())
|
||||
throw std::runtime_error("Location interactiveObjects must be an object");
|
||||
{
|
||||
const auto& values = objectValues;
|
||||
if (!interactiveObjects.empty()) {
|
||||
for (const auto& [id, object] : interactiveObjects)
|
||||
if (!values.contains(id))
|
||||
throw std::runtime_error(
|
||||
"Saved location is missing interactive object '" + id + "'");
|
||||
for (const auto& [id, value] : values.items())
|
||||
if (interactiveObjects.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"Saved location contains unknown interactive object '" + id + "'");
|
||||
}
|
||||
for (const auto& [id, value] : values.items()) {
|
||||
auto& object = interactiveObjects[id];
|
||||
if (!object) object = std::make_shared<InteractiveObjectState>();
|
||||
object->load(value);
|
||||
}
|
||||
}
|
||||
|
||||
dialogue = in.at("dialogue");
|
||||
scriptGlobals = in.at("scriptGlobals");
|
||||
triggerZones.clear();
|
||||
const auto& triggerValues = in.at("triggerZones");
|
||||
if (!triggerValues.is_object())
|
||||
throw std::runtime_error("Location triggerZones must be an object");
|
||||
for (const auto& [id, value] : triggerValues.items())
|
||||
triggerZones[id] = {
|
||||
value.at("enabled").get<bool>(),
|
||||
value.at("playerInside").get<bool>()};
|
||||
teleportZones.clear();
|
||||
const auto& teleportValues = in.at("teleportZones");
|
||||
if (!teleportValues.is_object())
|
||||
throw std::runtime_error("Location teleportZones must be an object");
|
||||
for (const auto& [id, value] : teleportValues.items())
|
||||
teleportZones[id] = value.get<bool>();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "CharacterState.h"
|
||||
#include "LocationState.h"
|
||||
#include "items/InteractiveObjectState.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
struct TriggerZoneGameState {
|
||||
bool enabled = true;
|
||||
bool playerInside = false;
|
||||
};
|
||||
|
||||
struct CharacterSimulationEvent {
|
||||
std::string characterId;
|
||||
CharacterUpdateEvents events;
|
||||
};
|
||||
|
||||
struct CharacterDamageEvent {
|
||||
std::string targetId;
|
||||
std::string attackerId;
|
||||
float amount = 0.f;
|
||||
float hp = 0.f;
|
||||
float maxHp = 0.f;
|
||||
Eigen::Vector3f direction = Eigen::Vector3f::Zero();
|
||||
};
|
||||
|
||||
struct InteractiveObjectSimulationEvent {
|
||||
std::string objectId;
|
||||
std::string animationCompleteFunctionName;
|
||||
bool activated = false;
|
||||
std::string activateFunctionName;
|
||||
};
|
||||
|
||||
struct LocationSimulationEvents {
|
||||
std::vector<CharacterSimulationEvent> characters;
|
||||
std::vector<CharacterDamageEvent> damage;
|
||||
std::vector<InteractiveObjectSimulationEvent> interactiveObjects;
|
||||
std::string npcInteractionId;
|
||||
bool taxiArrived = false;
|
||||
};
|
||||
|
||||
// Pure authoritative state for one location. Shared ownership gives bound local
|
||||
// simulation objects stable addresses; runtime renderers and Location pointers
|
||||
// are deliberately absent, and public clients receive value-only snapshots.
|
||||
struct LocationGameState {
|
||||
std::shared_ptr<LocationState> location = std::make_shared<LocationState>();
|
||||
std::unordered_map<std::string, std::shared_ptr<CharacterState>> npcs;
|
||||
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>> interactiveObjects;
|
||||
bool lockInteractiveObjectsDuringTutorial = false;
|
||||
bool hasPersistentState = false;
|
||||
std::unordered_map<std::string, TriggerZoneGameState> triggerZones;
|
||||
std::unordered_map<std::string, bool> teleportZones;
|
||||
nlohmann::json dialogue = nlohmann::json::object();
|
||||
nlohmann::json scriptGlobals = nlohmann::json::object();
|
||||
|
||||
CharacterState* findCharacter(const std::string& id, PlayerStateMap& players);
|
||||
const CharacterState* findCharacter(
|
||||
const std::string& id, const PlayerStateMap& players) const;
|
||||
void startTaxi();
|
||||
void resetTaxi();
|
||||
LocationSimulationEvents update(
|
||||
std::int64_t deltaMs, PlayerStateMap& players, bool interactiveObjectsLocked);
|
||||
void save(nlohmann::json& out) const;
|
||||
void load(const nlohmann::json& in);
|
||||
|
||||
private:
|
||||
bool updateNpcCar(std::int64_t deltaMs);
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+83
-206
@@ -2,6 +2,76 @@
|
||||
|
||||
namespace FRG {
|
||||
|
||||
extern const char* CONST_ZIP_FILE;
|
||||
|
||||
namespace {
|
||||
nlohmann::json saveCar(const NpcCar& car)
|
||||
{
|
||||
nlohmann::json waypoints = nlohmann::json::array();
|
||||
for (const auto& waypoint : car.waypoints)
|
||||
waypoints.push_back(
|
||||
{{"x", waypoint.x()}, {"y", waypoint.y()}, {"z", waypoint.z()}});
|
||||
return {
|
||||
{"position", {car.position.x(), car.position.y(), car.position.z()}},
|
||||
{"rotation", car.rotation},
|
||||
{"velocity", car.velocity},
|
||||
{"steeringAngle", car.steeringAngle},
|
||||
{"wheelAngle", car.wheelAngle},
|
||||
{"mode", static_cast<int>(car.mode)},
|
||||
{"waypoints", std::move(waypoints)},
|
||||
{"currentWaypoint", car.currentWaypoint}
|
||||
};
|
||||
}
|
||||
|
||||
void loadCar(NpcCar& car, const nlohmann::json& value)
|
||||
{
|
||||
const auto& position = value.at("position");
|
||||
car.position = {
|
||||
position.at(0).get<float>(),
|
||||
position.at(1).get<float>(),
|
||||
position.at(2).get<float>()};
|
||||
car.rotation = value.at("rotation").get<float>();
|
||||
car.velocity = value.at("velocity").get<float>();
|
||||
car.steeringAngle = value.at("steeringAngle").get<float>();
|
||||
car.wheelAngle = value.at("wheelAngle").get<float>();
|
||||
car.mode = static_cast<NpcCar::Mode>(value.at("mode").get<int>());
|
||||
car.waypoints.clear();
|
||||
for (const auto& waypoint : value.at("waypoints"))
|
||||
car.waypoints.push_back({
|
||||
waypoint.at("x").get<float>(),
|
||||
waypoint.at("y").get<float>(),
|
||||
waypoint.at("z").get<float>()});
|
||||
car.currentWaypoint = value.at("currentWaypoint").get<std::size_t>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
std::vector<Eigen::Vector3f> LocationState::planPathExceptMyself(const Eigen::Vector3f& start, const Eigen::Vector3f& end, const std::string& entityId) const
|
||||
{
|
||||
return navigationMaps[activeNavigationIndex].findPathToNearest(start, end, {});
|
||||
}
|
||||
|
||||
void LocationState::initNavigation(const std::vector<std::string>& paths)
|
||||
{
|
||||
const int savedActiveNavigationIndex = activeNavigationIndex;
|
||||
//navigationMapPaths = paths;
|
||||
navigationMaps.clear();
|
||||
navigationMaps.resize(paths.size());
|
||||
|
||||
for (size_t i = 0; i < paths.size(); ++i) {
|
||||
// Service the OS message queue between builds so the window doesn't
|
||||
// get flagged as "not responding" while loading many navigation grids.
|
||||
SDL_PumpEvents();
|
||||
navigationMaps[i].build(paths[i], CONST_ZIP_FILE);
|
||||
}
|
||||
|
||||
activeNavigationIndex =
|
||||
savedActiveNavigationIndex >= 0 &&
|
||||
savedActiveNavigationIndex < static_cast<int>(navigationMaps.size())
|
||||
? savedActiveNavigationIndex
|
||||
: 0;
|
||||
}
|
||||
|
||||
void LocationState::save(nlohmann::json& out) const
|
||||
{
|
||||
out["cameraAzimuth"] = cameraAzimuth;
|
||||
@@ -9,99 +79,14 @@ void LocationState::save(nlohmann::json& out) const
|
||||
|
||||
out["activeNavigationIndex"] = activeNavigationIndex;
|
||||
|
||||
out["targetInteractiveObjectIndex"] = targetInteractiveObjectIndex;
|
||||
out["targetInteractNpcIndex"] = targetInteractNpcIndex;
|
||||
out["targetTeleportZoneIndex"] = targetTeleportZoneIndex;
|
||||
out["targetInteractiveObjectId"] = targetInteractiveObjectId;
|
||||
out["targetInteractNpcId"] = targetInteractNpcId;
|
||||
out["targetTeleportZoneId"] = targetTeleportZoneId;
|
||||
|
||||
out["isDarklands"] = isDarklands;
|
||||
out["isNight"] = isNight;
|
||||
out["isDawn"] = isDawn;
|
||||
|
||||
|
||||
|
||||
// Taxi stuff:
|
||||
nlohmann::json taxiJson;
|
||||
|
||||
taxiJson["taxiCar"] = {
|
||||
{"positionX", taxiCar.position.x()},
|
||||
{"positionY", taxiCar.position.y()},
|
||||
{"positionZ", taxiCar.position.z()},
|
||||
{"rotation", taxiCar.rotation},
|
||||
{"velocity", taxiCar.velocity},
|
||||
{"steeringAngle", taxiCar.steeringAngle},
|
||||
{"mode", static_cast<int>(taxiCar.mode)},
|
||||
{"waypoints", nlohmann::json::array()},
|
||||
{"currentWaypoint", taxiCar.currentWaypoint},
|
||||
{"waypointReachRadius", taxiCar.waypointReachRadius}
|
||||
out["taxi"] = {
|
||||
{"car", saveCar(taxiCar)},
|
||||
{"arrivedCallbackCalled", taxiArrivedCallbackCalled}
|
||||
};
|
||||
|
||||
nlohmann::json taxiWaypoints = nlohmann::json::array();
|
||||
|
||||
for (int i = 0; i < taxiCar.waypoints.size(); ++i) {
|
||||
taxiWaypoints.push_back({
|
||||
{"x", taxiCar.waypoints[i].x()},
|
||||
{"y", taxiCar.waypoints[i].y()},
|
||||
{"z", taxiCar.waypoints[i].z()}
|
||||
});
|
||||
}
|
||||
|
||||
taxiJson["taxiCar"]["waypoints"] = std::move(taxiWaypoints);
|
||||
|
||||
taxiJson["taxiCarInitialState"] = {
|
||||
{"positionX", taxiCarInitialState.position.x()},
|
||||
{"positionY", taxiCarInitialState.position.y()},
|
||||
{"positionZ", taxiCarInitialState.position.z()},
|
||||
{"rotation", taxiCarInitialState.rotation},
|
||||
{"velocity", taxiCarInitialState.velocity},
|
||||
{"steeringAngle", taxiCarInitialState.steeringAngle},
|
||||
{"mode", static_cast<int>(taxiCarInitialState.mode)},
|
||||
{"waypoints", nlohmann::json::array()},
|
||||
{"currentWaypoint", taxiCarInitialState.currentWaypoint},
|
||||
{"waypointReachRadius", taxiCarInitialState.waypointReachRadius}
|
||||
};
|
||||
|
||||
nlohmann::json taxiInitialWaypoints = nlohmann::json::array();
|
||||
|
||||
for (int i = 0; i < taxiCarInitialState.waypoints.size(); ++i) {
|
||||
taxiInitialWaypoints.push_back({
|
||||
{"x", taxiCarInitialState.waypoints[i].x()},
|
||||
{"y", taxiCarInitialState.waypoints[i].y()},
|
||||
{"z", taxiCarInitialState.waypoints[i].z()}
|
||||
});
|
||||
}
|
||||
|
||||
taxiJson["taxiCarInitialState"]["waypoints"] = std::move(taxiInitialWaypoints);
|
||||
|
||||
nlohmann::json taxiDefaultWaypoints = nlohmann::json::array();
|
||||
|
||||
for (int i = 0; i < taxiCarWaypoints.size(); ++i) {
|
||||
taxiDefaultWaypoints.push_back({
|
||||
{"x", taxiCarWaypoints[i].x()},
|
||||
{"y", taxiCarWaypoints[i].y()},
|
||||
{"z", taxiCarWaypoints[i].z()}
|
||||
});
|
||||
}
|
||||
|
||||
taxiJson["taxiDefaultWaypoints"] = taxiDefaultWaypoints;
|
||||
|
||||
/* taxiCarTriggerPosition = Eigen::Vector3f(
|
||||
taxiJson.value("taxiCarTriggerPositionX", 0.f),
|
||||
taxiJson.value("taxiCarTriggerPositionY", 0.f),
|
||||
taxiJson.value("taxiCarTriggerPositionZ", 0.f)
|
||||
);*/
|
||||
|
||||
taxiJson["taxiCarTriggerPositionX"] = taxiCarTriggerPosition.x();
|
||||
taxiJson["taxiCarTriggerPositionY"] = taxiCarTriggerPosition.y();
|
||||
taxiJson["taxiCarTriggerPositionZ"] = taxiCarTriggerPosition.z();
|
||||
|
||||
// {"y", taxiCarTriggerPosition.y()},
|
||||
// {"z", taxiCarTriggerPosition.z()}
|
||||
//};
|
||||
|
||||
taxiJson["taxiArrivedCallbackCalled"] = taxiArrivedCallbackCalled;
|
||||
|
||||
//questJournal.save(questJson);
|
||||
out["taxi"] = std::move(taxiJson);
|
||||
}
|
||||
|
||||
void LocationState::load(const nlohmann::json& in)
|
||||
@@ -113,122 +98,14 @@ void LocationState::load(const nlohmann::json& in)
|
||||
|
||||
activeNavigationIndex = in.value("activeNavigationIndex", activeNavigationIndex);
|
||||
|
||||
targetInteractiveObjectIndex = in.value("targetInteractiveObjectIndex", -1);
|
||||
targetInteractNpcIndex = in.value("targetInteractNpcIndex", -1);
|
||||
targetTeleportZoneIndex = in.value("targetTeleportZoneIndex", -1);
|
||||
|
||||
isDarklands = in.value("isDarklands", false);
|
||||
isNight = in.value("isNight", false);
|
||||
isDawn = in.value("isDawn", false);
|
||||
|
||||
|
||||
//logger() << "LocationState::load step 2" << std::endl;
|
||||
|
||||
nlohmann::json taxiJson = in["taxi"];
|
||||
|
||||
taxiArrivedCallbackCalled = taxiJson.value("taxiArrivedCallbackCalled", false);
|
||||
|
||||
//----- Taxi State ----
|
||||
|
||||
nlohmann::json taxiCarJson = taxiJson["taxiCar"];
|
||||
|
||||
taxiCar.position = Eigen::Vector3f(
|
||||
taxiCarJson.value("positionX", 0.f),
|
||||
taxiCarJson.value("positionY", 0.f),
|
||||
taxiCarJson.value("positionZ", 0.f)
|
||||
);
|
||||
|
||||
//logger() << "LocationState::load step 3" << std::endl;
|
||||
|
||||
taxiCar.rotation = taxiCarJson.value("rotation", 0.f);
|
||||
|
||||
taxiCar.velocity = taxiCarJson.value("velocity", 0.f);
|
||||
|
||||
taxiCar.steeringAngle = taxiCarJson.value("steeringAngle", 0.f);
|
||||
|
||||
taxiCar.mode = static_cast<NpcCar::Mode>(taxiCarJson.value("mode", 0));
|
||||
|
||||
//logger() << "LocationState::load step 4" << std::endl;
|
||||
|
||||
taxiCar.waypoints.clear();
|
||||
if (taxiCarJson.contains("waypoints") && taxiCarJson["waypoints"].is_array()) {
|
||||
for (int i = 0; i < static_cast<int>(taxiCarJson["waypoints"].size()); ++i)
|
||||
taxiCar.waypoints.push_back(Eigen::Vector3f(
|
||||
taxiCarJson["waypoints"][i].value("x", 0.f),
|
||||
taxiCarJson["waypoints"][i].value("y", 0.f),
|
||||
taxiCarJson["waypoints"][i].value("z", 0.f)
|
||||
));
|
||||
}
|
||||
|
||||
//logger() << "LocationState::load step 5" << std::endl;
|
||||
|
||||
taxiCar.currentWaypoint = taxiCarJson.value("currentWaypoint", 0);
|
||||
taxiCar.waypointReachRadius = taxiCarJson.value("waypointReachRadius", 3.f);
|
||||
taxiCar.wheelAngle = taxiCarJson.value("wheelAngle", 0.f);
|
||||
|
||||
|
||||
//--------- Taxi initial state -----
|
||||
|
||||
//logger() << "LocationState::load step 6" << std::endl;
|
||||
|
||||
nlohmann::json taxiCarInitialStateJson = taxiJson["taxiCarInitialState"];
|
||||
|
||||
taxiCarInitialState.position = Eigen::Vector3f(
|
||||
taxiCarInitialStateJson.value("positionX", 0.f),
|
||||
taxiCarInitialStateJson.value("positionY", 0.f),
|
||||
taxiCarInitialStateJson.value("positionZ", 0.f)
|
||||
);
|
||||
|
||||
taxiCarInitialState.rotation = taxiCarInitialStateJson.value("rotation", 0.f);
|
||||
|
||||
taxiCarInitialState.velocity = taxiCarInitialStateJson.value("velocity", 0.f);
|
||||
|
||||
taxiCarInitialState.steeringAngle = taxiCarInitialStateJson.value("steeringAngle", 0.f);
|
||||
|
||||
taxiCarInitialState.mode = static_cast<NpcCar::Mode>(taxiCarInitialStateJson.value("mode", 0));
|
||||
|
||||
//logger() << "LocationState::load step 7" << std::endl;
|
||||
|
||||
|
||||
taxiCarInitialState.waypoints.clear();
|
||||
if (taxiCarInitialStateJson.contains("waypoints") && taxiCarInitialStateJson["waypoints"].is_array()) {
|
||||
for (int i = 0; i < static_cast<int>(taxiCarInitialStateJson["waypoints"].size()); ++i)
|
||||
taxiCarInitialState.waypoints.push_back(Eigen::Vector3f(
|
||||
taxiCarInitialStateJson["waypoints"][i].value("x", 0.f),
|
||||
taxiCarInitialStateJson["waypoints"][i].value("y", 0.f),
|
||||
taxiCarInitialStateJson["waypoints"][i].value("z", 0.f)
|
||||
));
|
||||
}
|
||||
|
||||
//logger() << "LocationState::load step 8" << std::endl;
|
||||
|
||||
taxiCarInitialState.currentWaypoint = taxiCarInitialStateJson.value("currentWaypoint", 0);
|
||||
taxiCarInitialState.waypointReachRadius = taxiCarInitialStateJson.value("waypointReachRadius", 3.f);
|
||||
taxiCarInitialState.wheelAngle = taxiCarInitialStateJson.value("wheelAngle", 0.f);
|
||||
|
||||
|
||||
|
||||
taxiCarWaypoints.clear();
|
||||
if (taxiJson.contains("taxiDefaultWaypoints") && taxiJson["taxiDefaultWaypoints"].is_array()) {
|
||||
for (int i = 0; i < static_cast<int>(taxiJson["taxiDefaultWaypoints"].size()); ++i)
|
||||
taxiCarWaypoints.push_back(Eigen::Vector3f(
|
||||
taxiJson["taxiDefaultWaypoints"][i].value("x", 0.f),
|
||||
taxiJson["taxiDefaultWaypoints"][i].value("y", 0.f),
|
||||
taxiJson["taxiDefaultWaypoints"][i].value("z", 0.f)
|
||||
));
|
||||
}
|
||||
|
||||
//logger() << "LocationState::load step 9" << std::endl;
|
||||
|
||||
taxiCarTriggerPosition = Eigen::Vector3f(
|
||||
taxiJson.value("taxiCarTriggerPositionX", 0.f),
|
||||
taxiJson.value("taxiCarTriggerPositionY", 0.f),
|
||||
taxiJson.value("taxiCarTriggerPositionZ", 0.f)
|
||||
);
|
||||
|
||||
//logger() << "LocationState::load step 10" << std::endl;
|
||||
|
||||
targetInteractiveObjectId = in.value("targetInteractiveObjectId", std::string());
|
||||
targetInteractNpcId = in.value("targetInteractNpcId", std::string());
|
||||
targetTeleportZoneId = in.value("targetTeleportZoneId", std::string());
|
||||
|
||||
const auto& taxi = in.at("taxi");
|
||||
loadCar(taxiCar, taxi.at("car"));
|
||||
taxiArrivedCallbackCalled =
|
||||
taxi.at("arrivedCallbackCalled").get<bool>();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
+12
-5
@@ -2,10 +2,18 @@
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "ISaveable.h"
|
||||
#include "NpcCar.h"
|
||||
#include "navigation/PathFinder.h"
|
||||
#include <string>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
struct LocationState : public ISaveable {
|
||||
//This value is populated from the JSON configs and should not be changed during the regular gameplay.
|
||||
//It might be changed only in the editor mode during development.
|
||||
std::vector<PathFinder> navigationMaps;
|
||||
std::vector<Eigen::Vector3f> planPathExceptMyself(const Eigen::Vector3f& start, const Eigen::Vector3f& end, const std::string& entityId) const;
|
||||
void initNavigation(const std::vector<std::string>& paths);
|
||||
|
||||
// ---- Camera ----
|
||||
float cameraAzimuth = -2.35f;
|
||||
float cameraInclination = 1.1036f;
|
||||
@@ -13,11 +21,10 @@ struct LocationState : public ISaveable {
|
||||
// ---- Navigation ----
|
||||
int activeNavigationIndex = 0;
|
||||
|
||||
// ---- Walk-to targets (-1 = none) ----
|
||||
// Mirrors CharacterState's kNoTarget convention; raw pointers are not serialisable.
|
||||
int targetInteractiveObjectIndex = -1;
|
||||
int targetInteractNpcIndex = -1;
|
||||
int targetTeleportZoneIndex = -1;
|
||||
// ---- Walk-to targets (empty = none) ----
|
||||
std::string targetInteractiveObjectId;
|
||||
std::string targetInteractNpcId;
|
||||
std::string targetTeleportZoneId;
|
||||
|
||||
// ---- World-mode flags (synced from Game after each transition) ----
|
||||
bool isDarklands = false;
|
||||
|
||||
+131
-114
@@ -201,9 +201,9 @@ namespace FRG {
|
||||
}
|
||||
#endif
|
||||
|
||||
MenuManager::MenuManager(Renderer& iRenderer, GameState& gameState, AudioPlayerAsync& audioPlayer) :
|
||||
MenuManager::MenuManager(Renderer& iRenderer, IGameServer& server, AudioPlayerAsync& audioPlayer) :
|
||||
renderer(iRenderer),
|
||||
gameState_(gameState),
|
||||
server_(server),
|
||||
audioPlayer_(audioPlayer)
|
||||
{
|
||||
//Localization
|
||||
@@ -338,9 +338,7 @@ namespace FRG {
|
||||
saveGameScreenRoot = loadLocalizedUi("resources/w/ui/screen_save.json", zipFile);
|
||||
}
|
||||
|
||||
void MenuManager::setup(Inventory& inv, const std::string& zipFile) {
|
||||
inventory = &inv;
|
||||
|
||||
void MenuManager::setup(const std::string& zipFile) {
|
||||
loadUiRoots(zipFile);
|
||||
|
||||
texObjectiveCompleted_ = renderer.textureManager.LoadFromPng("resources/w/ui/img/journal/quest_objective_completed.png", zipFile, true);
|
||||
@@ -360,19 +358,6 @@ namespace FRG {
|
||||
languageLoadingRu = renderer.textureManager.LoadFromPng("resources/loading_land003_ru.png", zipFile);
|
||||
languageLoadingEn = renderer.textureManager.LoadFromPng("resources/loading_land003_en.png", zipFile);
|
||||
|
||||
gameState_.questJournal.onQuestUnlocked = [this](const std::string& id) {
|
||||
const Quest::QuestState* q = gameState_.questJournal.findQuest(id);
|
||||
if (q) showToast(toastTextureQuestNew, q->definition.title);
|
||||
};
|
||||
gameState_.questJournal.onQuestCompleted = [this](const std::string& id) {
|
||||
const Quest::QuestState* q = gameState_.questJournal.findQuest(id);
|
||||
if (q) showToast(toastTextureQuestCompleted, q->definition.title);
|
||||
};
|
||||
gameState_.questJournal.onQuestFailed = [this](const std::string& id) {
|
||||
const Quest::QuestState* q = gameState_.questJournal.findQuest(id);
|
||||
if (q) showToast(toastTextureQuestFailed, q->definition.title);
|
||||
};
|
||||
|
||||
const std::string imgDir = "resources/w/ui/img/phone/";
|
||||
texBubbleInCenter_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_center.png", zipFile, true);
|
||||
texBubbleInLT_ = renderer.textureManager.LoadFromPng(imgDir + "bubble_in_corner_left_top.png", zipFile, true);
|
||||
@@ -419,9 +404,10 @@ namespace FRG {
|
||||
}
|
||||
|
||||
std::shared_ptr<UiNode> MenuManager::gameplayHudRoot() const {
|
||||
const std::string& loc = gameState_.currentLocationName;
|
||||
if (loc == "uni_exterior") return gameState_.isDarklands ? hudUniExtDarkRoot : hudUniExtRoot;
|
||||
if (loc == "uni_interior") return gameState_.isDarklands ? hudUniIntDarkRoot : hudUniIntRoot;
|
||||
const auto snapshot = server_.snapshot();
|
||||
const std::string& loc = snapshot->currentLocationId;
|
||||
if (loc == "uni_exterior") return snapshot->isDarklands ? hudUniExtDarkRoot : hudUniExtRoot;
|
||||
if (loc == "uni_interior") return snapshot->isDarklands ? hudUniIntDarkRoot : hudUniIntRoot;
|
||||
return hudDormRoot; // dorm, or a new game before the location is set
|
||||
}
|
||||
|
||||
@@ -434,21 +420,22 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::refreshHudVisibility() {
|
||||
if (!inventory) return; // update() already ticks during loading, before setup()
|
||||
const auto itemIds = server_.snapshot()->inventoryItemIds;
|
||||
|
||||
const TutorialStep step = gameState_.tutorialStep;
|
||||
const bool tutorial = gameState_.isTutorialActive();
|
||||
const bool hasPhone = inventory->hasItem("phone");
|
||||
const bool hasJournal = inventory->hasItem("journal");
|
||||
const bool hasNote = inventory->hasItem("note_spell");
|
||||
const bool dark = gameState_.isDarklands;
|
||||
const auto snapshot = server_.snapshot();
|
||||
const TutorialStep step = snapshot->tutorialStep;
|
||||
const bool tutorial = snapshot->isTutorialActive();
|
||||
const bool hasPhone = std::find(itemIds.begin(), itemIds.end(), "phone") != itemIds.end();
|
||||
const bool hasJournal = std::find(itemIds.begin(), itemIds.end(), "journal") != itemIds.end();
|
||||
const bool hasNote = std::find(itemIds.begin(), itemIds.end(), "note_spell") != itemIds.end();
|
||||
const bool dark = snapshot->isDarklands;
|
||||
|
||||
auto show = [this](std::initializer_list<const char*> names, bool visible) {
|
||||
for (const char* name : names) uiManager.setNodeVisible(name, visible);
|
||||
};
|
||||
|
||||
// --- HUD buttons: present while the matching item is in the inventory ---
|
||||
show({ "inventoryButton" }, inventory->getCount() > 0);
|
||||
show({ "inventoryButton" }, !itemIds.empty());
|
||||
show({ "phoneButton" }, hasPhone && !dark);
|
||||
show({ "journalButton" }, hasJournal);
|
||||
show({ "darklandsButton" }, hasNote && !dark);
|
||||
@@ -466,9 +453,9 @@ namespace FRG {
|
||||
show({ "hint4" }, step == TutorialStep::Zoom);
|
||||
show({ "hint5" }, step == TutorialStep::PickUpItem);
|
||||
// ...then the hints for the picked-up items, which can be done in any order.
|
||||
const bool chatHint = tutorial && !gameState_.tutorialPhoneChatScreenOpened;
|
||||
const bool journalHint = tutorial && !gameState_.tutorialJournalScreenOpened;
|
||||
const bool taxiHint = tutorial && gameState_.tutorialNeedOpenTaxiScreen;
|
||||
const bool chatHint = tutorial && !snapshot->tutorialPhoneChatScreenOpened;
|
||||
const bool journalHint = tutorial && !snapshot->tutorialJournalScreenOpened;
|
||||
const bool taxiHint = tutorial && snapshot->tutorialNeedOpenTaxiScreen;
|
||||
show({ "hint6a", "hint6aarrow" }, hasPhone && chatHint && !taxiHint);
|
||||
show({ "hint6b", "hint6barrow" }, hasJournal && journalHint);
|
||||
show({ "hint7", "hint7arrow" }, taxiHint);
|
||||
@@ -485,7 +472,7 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::showMainMenu() {
|
||||
gameState_.currentLocationName.clear();
|
||||
server_.submit(SetCurrentLocationCommand{""});
|
||||
|
||||
audioPlayer_.crossFadeMusicAsync("music/main menu final.ogg");
|
||||
lastMusicTrack = "music/main menu final.ogg";
|
||||
@@ -546,7 +533,6 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::showMenuAfterGameCompleted() {
|
||||
//gameState_.currentLocationName.clear();
|
||||
|
||||
audioPlayer_.crossFadeMusicAsync("music/main menu final.ogg");
|
||||
lastMusicTrack = "music/main menu final.ogg";
|
||||
@@ -1060,8 +1046,8 @@ namespace FRG {
|
||||
|
||||
steps.push_back([this]() {
|
||||
ItemRegistry::instance().loadFromJson("resources/config/items.json", CONST_ZIP_FILE);
|
||||
if (const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState_.campaignId))
|
||||
gameState_.questJournal.reloadDefinitionsPreservingState(campaign->questsPath, CONST_ZIP_FILE);
|
||||
if (const CampaignConfig* campaign = CampaignRegistry::instance().find(server_.snapshot()->campaignId))
|
||||
server_.submit(ReloadQuestDefinitionsCommand{campaign->questsPath, CONST_ZIP_FILE});
|
||||
});
|
||||
|
||||
steps.push_back([this]() {
|
||||
@@ -1197,7 +1183,7 @@ namespace FRG {
|
||||
|
||||
uiManager.setButtonCallback("inventoryMain", [this](const std::string&) {});
|
||||
|
||||
const auto& items = inventory->getItems();
|
||||
const auto items = inventoryItems();
|
||||
/*logger() << "Item count: " << items.size() << std::endl;
|
||||
for (int x = 0; x < items.size(); x++)
|
||||
{
|
||||
@@ -1236,7 +1222,7 @@ namespace FRG {
|
||||
|
||||
void MenuManager::selectInventoryItem(int index) {
|
||||
logger() << "MenuManager::selectInventoryItem: " << index << std::endl;
|
||||
const auto& items = inventory->getItems();
|
||||
const auto items = inventoryItems();
|
||||
if (index < 0 || index >= static_cast<int>(items.size())) return;
|
||||
|
||||
if (isPortraitMode() && inventoryMenuStackDepth_ == 1) {
|
||||
@@ -1288,7 +1274,10 @@ namespace FRG {
|
||||
|
||||
void MenuManager::openQuestJournal() {
|
||||
uiState_ = GameUiState::QuestJournal;
|
||||
gameState_.tutorialJournalScreenOpened = true;
|
||||
auto tutorial = server_.snapshot();
|
||||
server_.submit(SetTutorialFlagsCommand{
|
||||
tutorial->tutorialPhoneChatScreenOpened, true,
|
||||
tutorial->tutorialNeedOpenTaxiScreen, tutorial->tutorialTaxiHintDone});
|
||||
journalMenuStackDepth_ = 1;
|
||||
|
||||
const bool portrait = isPortraitMode();
|
||||
@@ -1380,9 +1369,10 @@ namespace FRG {
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
});
|
||||
|
||||
if (gameState_.isNight)
|
||||
const auto timeState = server_.snapshot();
|
||||
if (timeState->isNight)
|
||||
{
|
||||
if (gameState_.isDawn)
|
||||
if (timeState->isDawn)
|
||||
{
|
||||
uiManager.setNodeVisible("phoneTimeDay", false);
|
||||
uiManager.setNodeVisible("phoneTimeNight", false);
|
||||
@@ -1418,51 +1408,67 @@ namespace FRG {
|
||||
});
|
||||
uiManager.setButtonCallback("phoneMain", [this](const std::string&) {});
|
||||
uiManager.setTextButtonCallback("chat1button", [this](const std::string&) {
|
||||
gameState_.chatUnread[0] = false;
|
||||
setChatUnread(0, false);
|
||||
openPhoneChatFromList(0, isPortraitMode() ? verticalPhoneChat1Root : phoneChat1Root);
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
});
|
||||
uiManager.setTextButtonCallback("chat2button", [this](const std::string&) {
|
||||
gameState_.chatUnread[1] = false;
|
||||
setChatUnread(1, false);
|
||||
openPhoneChatFromList(1, isPortraitMode() ? verticalPhoneChat2Root : phoneChat2Root);
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
});
|
||||
uiManager.setTextButtonCallback("chat3button", [this](const std::string&) {
|
||||
gameState_.chatUnread[2] = false;
|
||||
setChatUnread(2, false);
|
||||
openPhoneChatFromList(2, isPortraitMode() ? verticalPhoneChat3Root : phoneChat3Root);
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
});
|
||||
}
|
||||
|
||||
void MenuManager::refreshChatUnreadIndicators() {
|
||||
const auto state = server_.snapshot();
|
||||
static const char* kUnreadNodes[3] = { "chat1Unread", "chat2Unread", "chat3Unread" };
|
||||
static const char* kMsgNodes[3] = { "chat1msg", "chat2msg", "chat3msg" };
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
uiManager.setNodeVisible(kUnreadNodes[i], gameState_.chatUnread[i]);
|
||||
if (!gameState_.chatPreviewMsg[i].empty())
|
||||
uiManager.setText(kMsgNodes[i], gameState_.chatPreviewMsg[i]);
|
||||
uiManager.setNodeVisible(kUnreadNodes[i], state->chatUnread[i]);
|
||||
if (!state->chatPreview[i].empty())
|
||||
uiManager.setText(kMsgNodes[i], state->chatPreview[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void MenuManager::setChatUnread(int chatIndex, bool unread, const std::string& previewMsg) {
|
||||
if (chatIndex < 0 || chatIndex > 2) return;
|
||||
gameState_.chatUnread[chatIndex] = unread;
|
||||
const auto state = server_.snapshot();
|
||||
std::string preview = state->chatPreview[chatIndex];
|
||||
if (!previewMsg.empty()) {
|
||||
gameState_.chatPreviewMsg[chatIndex] = trimChatPreview(previewMsg);
|
||||
} else if (!unread && !gameState_.chatHistory[chatIndex].empty()) {
|
||||
gameState_.chatPreviewMsg[chatIndex] = trimChatPreview(gameState_.chatHistory[chatIndex].back().text);
|
||||
preview = trimChatPreview(previewMsg);
|
||||
} else if (!unread && !state->chatHistory[chatIndex].empty()) {
|
||||
preview = trimChatPreview(state->chatHistory[chatIndex].back().text);
|
||||
}
|
||||
server_.submit(SetChatCommand{chatIndex, unread, std::move(preview)});
|
||||
}
|
||||
|
||||
void MenuManager::spendMoney(int amount) {
|
||||
gameState_.money -= amount;
|
||||
server_.submit(SpendMoneyCommand{amount});
|
||||
}
|
||||
|
||||
std::vector<Item> MenuManager::inventoryItems() const {
|
||||
std::vector<Item> items;
|
||||
for (const std::string& id : server_.snapshot()->inventoryItemIds) {
|
||||
if (const Item* item = ItemRegistry::instance().findById(id)) items.push_back(*item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
bool MenuManager::hasInventoryItem(const std::string& id) const {
|
||||
const auto& ids = server_.snapshot()->inventoryItemIds;
|
||||
return std::find(ids.begin(), ids.end(), id) != ids.end();
|
||||
}
|
||||
|
||||
|
||||
void MenuManager::openPhoneBank() {
|
||||
currentPhoneSubScreen_ = PhoneSubScreen::Bank;
|
||||
uiManager.pushMenuFromSavedRoot(isPortraitMode() ? verticalPhoneBankRoot : phoneBankRoot);
|
||||
uiManager.setText("balanceText", formatMoney(gameState_.money));
|
||||
uiManager.setText("balanceText", formatMoney(server_.snapshot()->money));
|
||||
|
||||
uiManager.setButtonCallback("phoneExitButton", [this](const std::string&) {
|
||||
closePhoneEntirely();
|
||||
@@ -1492,13 +1498,15 @@ namespace FRG {
|
||||
});
|
||||
uiManager.setButtonCallback("videoSkip", [this](const std::string&) {
|
||||
closePhoneEntirely();
|
||||
if (gameState_.isNight)
|
||||
const auto state = server_.snapshot();
|
||||
if (state->isNight)
|
||||
{
|
||||
startDialogueFunc("dialog_video002");
|
||||
}
|
||||
else
|
||||
{
|
||||
auto day = gameState_.globalInts["day"];
|
||||
auto dayIt = state->globalInts.find("day");
|
||||
const int day = dayIt == state->globalInts.end() ? 0 : dayIt->second;
|
||||
if (day == 0)
|
||||
{
|
||||
startDialogueFunc("dialog_video003");
|
||||
@@ -1513,10 +1521,11 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::openPhoneTaxi() {
|
||||
if (gameState_.currentLocationName == "uni_interior") {
|
||||
const std::string location = server_.snapshot()->currentLocationId;
|
||||
if (location == "uni_interior") {
|
||||
closePhoneEntirely();
|
||||
if (startDialogueFunc) startDialogueFunc("dialog_taxi003");
|
||||
} else if (gameState_.currentLocationName == "uni_exterior") {
|
||||
} else if (location == "uni_exterior") {
|
||||
currentPhoneSubScreen_ = PhoneSubScreen::MapUni;
|
||||
openPhoneMapScreen(isPortraitMode() ? verticalPhoneMapUniRoot : phoneMapUniRoot);
|
||||
} else {
|
||||
@@ -1539,17 +1548,18 @@ namespace FRG {
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
});
|
||||
uiManager.setButtonCallback("mapGo", [this](const std::string&) {
|
||||
gameState_.tutorialNeedOpenTaxiScreen = false;
|
||||
gameState_.tutorialTaxiHintDone = true;
|
||||
auto state = server_.snapshot();
|
||||
server_.submit(SetTutorialFlagsCommand{
|
||||
state->tutorialPhoneChatScreenOpened, state->tutorialJournalScreenOpened,
|
||||
false, true});
|
||||
|
||||
if (gameState_.taxiIsCalled == false)
|
||||
if (!state->taxiIsCalled)
|
||||
{
|
||||
gameState_.taxiIsCalled = true;
|
||||
if (callTaxiFunc)
|
||||
{
|
||||
callTaxiFunc();
|
||||
}
|
||||
gameState_.money -= 500;
|
||||
server_.submit(SetTaxiCalledCommand{true});
|
||||
server_.submit(StartTaxiCommand{state->currentLocationId});
|
||||
server_.submit(SetTriggerZoneEnabledCommand{
|
||||
state->currentLocationId, "taxi_zone001", false});
|
||||
server_.submit(SpendMoneyCommand{500});
|
||||
closePhoneEntirely();
|
||||
if (startDialogueFunc) startDialogueFunc("dialog_taxi002");
|
||||
audioPlayer_.playSoundAsync("resources/audio/537061__imafoley__message-pop-sound.ogg");
|
||||
@@ -1569,7 +1579,10 @@ namespace FRG {
|
||||
activeChatIndex_ = chatIndex;
|
||||
currentPhoneSubScreen_ = PhoneSubScreen::Chat;
|
||||
phoneChatVisibleBubbles_.clear();
|
||||
gameState_.tutorialPhoneChatScreenOpened = true;
|
||||
auto tutorial = server_.snapshot();
|
||||
server_.submit(SetTutorialFlagsCommand{
|
||||
true, tutorial->tutorialJournalScreenOpened,
|
||||
tutorial->tutorialNeedOpenTaxiScreen, tutorial->tutorialTaxiHintDone});
|
||||
uiManager.pushMenuFromSavedRoot(chatRoot);
|
||||
|
||||
// Build a dedicated chatUiManager root sized to the FBO chat area
|
||||
@@ -1616,11 +1629,7 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::returnToPhoneChatList() {
|
||||
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2
|
||||
&& !gameState_.chatHistory[activeChatIndex_].empty()) {
|
||||
gameState_.chatPreviewMsg[activeChatIndex_] =
|
||||
trimChatPreview(gameState_.chatHistory[activeChatIndex_].back().text);
|
||||
}
|
||||
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2) setChatUnread(activeChatIndex_, false);
|
||||
activeChatIndex_ = -1;
|
||||
currentPhoneSubScreen_ = PhoneSubScreen::ChatList;
|
||||
phoneChatVisibleBubbles_.clear();
|
||||
@@ -1634,11 +1643,7 @@ namespace FRG {
|
||||
|
||||
|
||||
void MenuManager::closePhoneEntirely() {
|
||||
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2
|
||||
&& !gameState_.chatHistory[activeChatIndex_].empty()) {
|
||||
gameState_.chatPreviewMsg[activeChatIndex_] =
|
||||
trimChatPreview(gameState_.chatHistory[activeChatIndex_].back().text);
|
||||
}
|
||||
if (activeChatIndex_ >= 0 && activeChatIndex_ <= 2) setChatUnread(activeChatIndex_, false);
|
||||
activeChatIndex_ = -1;
|
||||
currentPhoneSubScreen_ = PhoneSubScreen::None;
|
||||
uiState_ = GameUiState::Gameplay;
|
||||
@@ -1699,8 +1704,11 @@ namespace FRG {
|
||||
void MenuManager::tutorialShowTaxiHint()
|
||||
{
|
||||
logger() << "tutorialShowTaxiHint" << std::endl;
|
||||
if (!gameState_.isTutorialActive() || gameState_.tutorialTaxiHintDone) return;
|
||||
gameState_.tutorialNeedOpenTaxiScreen = true; // cleared when the taxi is ordered
|
||||
auto state = server_.snapshot();
|
||||
if (!state->isTutorialActive() || state->tutorialTaxiHintDone) return;
|
||||
server_.submit(SetTutorialFlagsCommand{
|
||||
state->tutorialPhoneChatScreenOpened, state->tutorialJournalScreenOpened,
|
||||
true, state->tutorialTaxiHintDone});
|
||||
refreshHudVisibility();
|
||||
}
|
||||
|
||||
@@ -1723,16 +1731,17 @@ namespace FRG {
|
||||
|
||||
void MenuManager::onLocationChanged(const std::string& locationName) {
|
||||
if (uiState_ != GameUiState::Gameplay) return;
|
||||
gameState_.currentLocationName = locationName;
|
||||
server_.submit(SetCurrentLocationCommand{locationName});
|
||||
const auto state = server_.snapshot();
|
||||
showGameplayHud();
|
||||
|
||||
if (locationName == "uni_exterior" || locationName == "uni_interior") {
|
||||
if (gameState_.isDarklands)
|
||||
if (state->isDarklands)
|
||||
{
|
||||
audioPlayer_.crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
||||
lastMusicTrack = "music/bishkek fight calm.ogg";
|
||||
}
|
||||
else if (gameState_.isNight)
|
||||
else if (state->isNight)
|
||||
{
|
||||
audioPlayer_.crossFadeMusicAsync("music/bishkek night.ogg");
|
||||
lastMusicTrack = "music/bishkek night.ogg";
|
||||
@@ -1743,7 +1752,7 @@ namespace FRG {
|
||||
lastMusicTrack = "music/bishkek univer day.ogg";
|
||||
}
|
||||
} else {
|
||||
if (gameState_.isNight)
|
||||
if (state->isNight)
|
||||
{
|
||||
audioPlayer_.crossFadeMusicAsync("music/bishkek night.ogg");
|
||||
lastMusicTrack = "music/bishkek night.ogg";
|
||||
@@ -1759,13 +1768,16 @@ namespace FRG {
|
||||
void MenuManager::onItemPickedUp(const std::string& itemId) {
|
||||
// The new item's HUD button appears through refreshHudVisibility().
|
||||
if (itemId == "phone" || itemId == "journal")
|
||||
gameState_.completeTutorialStep(TutorialStep::PickUpItem);
|
||||
server_.submit(CompleteTutorialStepCommand{TutorialStep::PickUpItem});
|
||||
refreshHudVisibility();
|
||||
}
|
||||
|
||||
void MenuManager::refreshQuestJournalUi() {
|
||||
visibleQuestIds.clear();
|
||||
auto quests = gameState_.questJournal.getVisibleQuests();
|
||||
const auto state = server_.snapshot();
|
||||
std::vector<const Quest::QuestState*> quests;
|
||||
quests.reserve(state->quests.size());
|
||||
for (const auto& quest : state->quests) quests.push_back(&quest);
|
||||
|
||||
std::sort(quests.begin(), quests.end(), [](const Quest::QuestState* a, const Quest::QuestState* b) {
|
||||
const int pa = questStatusPriority(a->status);
|
||||
@@ -1840,7 +1852,10 @@ namespace FRG {
|
||||
}
|
||||
|
||||
selectedQuestIndex = index;
|
||||
Quest::QuestState* quest = gameState_.questJournal.findQuest(visibleQuestIds[index]);
|
||||
const auto state = server_.snapshot();
|
||||
const Quest::QuestState* quest = nullptr;
|
||||
for (const auto& candidate : state->quests)
|
||||
if (candidate.definition.id == visibleQuestIds[index]) quest = &candidate;
|
||||
if (!quest) {
|
||||
return;
|
||||
}
|
||||
@@ -1888,7 +1903,7 @@ namespace FRG {
|
||||
}
|
||||
|
||||
void MenuManager::openPortraitInventoryItem(int index) {
|
||||
const auto& items = inventory->getItems();
|
||||
const auto items = inventoryItems();
|
||||
inventorySelectedIndex_ = index;
|
||||
inventoryMenuStackDepth_ = 2;
|
||||
uiManager.pushMenuFromSavedRoot(portraitInventoryItemRoot);
|
||||
@@ -1943,7 +1958,10 @@ namespace FRG {
|
||||
uiManager.setButtonCallback("journalMain", [this](const std::string&) {});
|
||||
|
||||
|
||||
Quest::QuestState* quest = gameState_.questJournal.findQuest(visibleQuestIds[index]);
|
||||
const auto state = server_.snapshot();
|
||||
const Quest::QuestState* quest = nullptr;
|
||||
for (const auto& candidate : state->quests)
|
||||
if (candidate.definition.id == visibleQuestIds[index]) quest = &candidate;
|
||||
if (!quest) return;
|
||||
const auto& def = quest->definition;
|
||||
|
||||
@@ -2070,7 +2088,8 @@ namespace FRG {
|
||||
phoneChatVisibleBubbles_.clear();
|
||||
|
||||
if (chatIndex < 0 || chatIndex > 2) return;
|
||||
for (const auto& msg : gameState_.chatHistory[chatIndex]) {
|
||||
const auto state = server_.snapshot();
|
||||
for (const auto& msg : state->chatHistory[chatIndex]) {
|
||||
const bool inc = msg.incoming;
|
||||
const std::string nodeName = chatUiManager.addChatBubble(
|
||||
"chatMessagesContainer", msg.text, inc,
|
||||
@@ -2100,11 +2119,7 @@ namespace FRG {
|
||||
void MenuManager::onChatBubbleReady(const std::string& text, bool incoming) {
|
||||
if (activeChatIndex_ < 0) return;
|
||||
|
||||
auto& history = gameState_.chatHistory[activeChatIndex_];
|
||||
if (static_cast<int>(history.size()) >= 5) {
|
||||
history.erase(history.begin());
|
||||
}
|
||||
history.push_back({ text, incoming });
|
||||
server_.submit(AppendChatMessageCommand{activeChatIndex_, text, incoming});
|
||||
|
||||
if (uiState_ != GameUiState::PhoneScreen) return;
|
||||
|
||||
@@ -2143,8 +2158,8 @@ namespace FRG {
|
||||
{
|
||||
logger() << "MenuManager::setDarklandsMode called" << std::endl;
|
||||
|
||||
if (enabled && gameState_.currentLocationName == "uni_interior" && inventory->hasItem("note_spell"))
|
||||
gameState_.completeTutorialStep(TutorialStep::EnterDarklands);
|
||||
if (enabled && server_.snapshot()->currentLocationId == "uni_interior" && hasInventoryItem("note_spell"))
|
||||
server_.submit(CompleteTutorialStepCommand{TutorialStep::EnterDarklands});
|
||||
|
||||
if (uiState_ == GameUiState::Gameplay)
|
||||
showGameplayHud(); // darklands has its own HUD layouts
|
||||
@@ -2152,30 +2167,31 @@ namespace FRG {
|
||||
|
||||
void MenuManager::onPlayerStartedWalking()
|
||||
{
|
||||
if (gameState_.currentLocationName == "uni_interior" && gameState_.isDarklands)
|
||||
gameState_.completeTutorialStep(TutorialStep::DarklandsHealth);
|
||||
const auto state = server_.snapshot();
|
||||
if (state->currentLocationId == "uni_interior" && state->isDarklands)
|
||||
server_.submit(CompleteTutorialStepCommand{TutorialStep::DarklandsHealth});
|
||||
}
|
||||
|
||||
void MenuManager::advanceUniIntDarklandsHud()
|
||||
{
|
||||
// The ghost zone can be reached without a floor tap (DarklandsHealth never completed).
|
||||
if (gameState_.tutorialStep == TutorialStep::DarklandsHealth
|
||||
|| gameState_.tutorialStep == TutorialStep::DarklandsExplore)
|
||||
gameState_.tutorialStep = TutorialStep::DarklandsAttack;
|
||||
const TutorialStep step = server_.snapshot()->tutorialStep;
|
||||
if (step == TutorialStep::DarklandsHealth || step == TutorialStep::DarklandsExplore)
|
||||
server_.submit(SetTutorialStepCommand{TutorialStep::DarklandsAttack});
|
||||
}
|
||||
|
||||
void MenuManager::onEnemyKilledInUniInterior()
|
||||
{
|
||||
// Every location's enemies report here; only the university interior drives the tutorial.
|
||||
if (gameState_.currentLocationName != "uni_interior") return;
|
||||
const auto state = server_.snapshot();
|
||||
if (state->currentLocationId != "uni_interior") return;
|
||||
// The last tutorial step; like the attack hint, also ends the earlier darklands hints.
|
||||
if (gameState_.tutorialStep >= TutorialStep::DarklandsHealth)
|
||||
gameState_.tutorialStep = TutorialStep::Completed;
|
||||
if (state->tutorialStep >= TutorialStep::DarklandsHealth)
|
||||
server_.submit(SetTutorialStepCommand{TutorialStep::Completed});
|
||||
}
|
||||
|
||||
void MenuManager::updateHealthBar(float hp, float maxHp) {
|
||||
gameState_.playerHp = hp;
|
||||
gameState_.playerMaxHp = maxHp;
|
||||
server_.submit(SetPlayerHealthCommand{hp, maxHp});
|
||||
applyCurrentHealthBar();
|
||||
}
|
||||
|
||||
@@ -2191,21 +2207,22 @@ namespace FRG {
|
||||
cutsceneHudActive_ = false;
|
||||
topUiManager.replaceRoot(nullptr);
|
||||
if (uiState_ == GameUiState::Gameplay)
|
||||
onLocationChanged(gameState_.currentLocationName);
|
||||
onLocationChanged(server_.snapshot()->currentLocationId);
|
||||
}
|
||||
|
||||
void MenuManager::applyCurrentHealthBar() {
|
||||
logger() << "MenuManager::applyCurrentHealthBar called step 1" << std::endl;
|
||||
logger() << "currentPlayerMaxHp_ is " << gameState_.playerMaxHp << std::endl;
|
||||
if (gameState_.playerMaxHp <= 0.f) return;
|
||||
const auto state = server_.snapshot();
|
||||
logger() << "currentPlayerMaxHp_ is " << state->playerMaxHp << std::endl;
|
||||
if (state->playerMaxHp <= 0.f) return;
|
||||
logger() << "MenuManager::applyCurrentHealthBar called step 2" << std::endl;
|
||||
logger() << "currentPlayerHp_ is " << gameState_.playerHp << std::endl;
|
||||
logger() << "currentPlayerHp_ is " << state->playerHp << std::endl;
|
||||
|
||||
const float fraction = std::clamp(gameState_.playerHp / gameState_.playerMaxHp, 0.f, 1.f);
|
||||
const float fraction = std::clamp(state->playerHp / state->playerMaxHp, 0.f, 1.f);
|
||||
|
||||
uiManager.setSliderValue("healthBarFill", fraction);
|
||||
std::string hpText = std::to_string(static_cast<int>(gameState_.playerHp)) + "/" +
|
||||
std::to_string(static_cast<int>(gameState_.playerMaxHp));
|
||||
std::string hpText = std::to_string(static_cast<int>(state->playerHp)) + "/" +
|
||||
std::to_string(static_cast<int>(state->playerMaxHp));
|
||||
if (hpText.size() < 7) hpText.insert(0, 7 - hpText.size(), ' ');
|
||||
uiManager.setText("healthValue", hpText);
|
||||
}
|
||||
@@ -2447,7 +2464,7 @@ namespace FRG {
|
||||
|
||||
openInventory();
|
||||
|
||||
const int itemCount = static_cast<int>(inventory->getItems().size());
|
||||
const int itemCount = static_cast<int>(inventoryItems().size());
|
||||
if (isPortraitMode() && wasOnItemDetail &&
|
||||
savedIndex >= 0 && savedIndex < itemCount) {
|
||||
selectInventoryItem(savedIndex);
|
||||
|
||||
+10
-9
@@ -3,8 +3,9 @@
|
||||
#include "Environment.h"
|
||||
#include "render/TextureManager.h"
|
||||
#include "UiManager.h"
|
||||
#include "Localization.h"
|
||||
#include "items/Item.h"
|
||||
#include "GameState.h"
|
||||
#include "game_server/IGameServer.h"
|
||||
#include <vector>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
@@ -39,9 +40,9 @@ namespace FRG {
|
||||
std::unordered_map<std::string,
|
||||
std::unordered_map<Language, std::string>> localizedLocationName;
|
||||
|
||||
MenuManager(Renderer& iRenderer, GameState& gameState, AudioPlayerAsync& audioPlayer);
|
||||
MenuManager(Renderer& iRenderer, IGameServer& server, AudioPlayerAsync& audioPlayer);
|
||||
|
||||
void setup(Inventory& inv, const std::string& zipFile);
|
||||
void setup(const std::string& zipFile);
|
||||
|
||||
void openInventory();
|
||||
void selectInventoryItem(int index);
|
||||
@@ -80,7 +81,7 @@ namespace FRG {
|
||||
void setChatUnread(int chatIndex, bool unread, const std::string& previewMsg = "");
|
||||
|
||||
void spendMoney(int amount);
|
||||
int getMoney() const { return gameState_.money; }
|
||||
int getMoney() const { return server_.snapshot()->money; }
|
||||
|
||||
// Starts a new game of the given campaign (see CampaignRegistry).
|
||||
std::function<void(const std::string& campaignId)> startGameFunc;
|
||||
@@ -89,7 +90,6 @@ namespace FRG {
|
||||
std::function<void()> startNightTransitionFunc;
|
||||
std::function<void(int)> chatOpenCallback;
|
||||
std::function<void()> skipCutsceneFunc;
|
||||
std::function<void()> callTaxiFunc;
|
||||
std::function<void(int)> onSaveGame;
|
||||
std::function<void(int)> onLoadGame;
|
||||
std::function<SaveSlotInfo(int)> getSlotInfoFunc;
|
||||
@@ -172,14 +172,14 @@ namespace FRG {
|
||||
Renderer& renderer;
|
||||
|
||||
private:
|
||||
GameState& gameState_;
|
||||
IGameServer& server_;
|
||||
AudioPlayerAsync& audioPlayer_;
|
||||
|
||||
void startNewGame(const std::string& campaignId);
|
||||
// HUD layout for the current location and darklands mode.
|
||||
std::shared_ptr<UiNode> gameplayHudRoot() const;
|
||||
// Single place that decides which HUD buttons and tutorial hints are visible:
|
||||
// buttons follow the inventory, hints follow gameState_.tutorialStep and the
|
||||
// buttons follow the inventory snapshot, hints follow the server tutorial state and
|
||||
// tutorial flags. Runs every frame; names absent from the shown screen are skipped.
|
||||
void refreshHudVisibility();
|
||||
void refreshQuestJournalUi();
|
||||
@@ -249,7 +249,8 @@ namespace FRG {
|
||||
static constexpr float TOAST_VISIBLE_MS = 2000.0f;
|
||||
|
||||
GameUiState uiState_ = GameUiState::Gameplay;
|
||||
Inventory* inventory = nullptr;
|
||||
std::vector<Item> inventoryItems() const;
|
||||
bool hasInventoryItem(const std::string& id) const;
|
||||
bool isPlayerInCreditsMenu = false;
|
||||
int inventorySelectedIndex_ = -1;
|
||||
|
||||
@@ -336,7 +337,7 @@ namespace FRG {
|
||||
enum class PhoneSubScreen { None, Main, Bank, Video, MapDorm, MapUni, ChatList, Chat };
|
||||
PhoneSubScreen currentPhoneSubScreen_ = PhoneSubScreen::None;
|
||||
|
||||
// Phone chat UI state (transient — rebuilt from gameState_.chatHistory on open)
|
||||
// Phone chat UI state (transient — rebuilt from the immutable server snapshot)
|
||||
struct PhoneChatBubbleInfo {
|
||||
std::string nodeName;
|
||||
float height;
|
||||
|
||||
+294
-270
@@ -1,5 +1,6 @@
|
||||
#include "ScriptEngine.h"
|
||||
#include "ScriptEngine.h"
|
||||
#include "Game.h"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
@@ -14,26 +15,11 @@ namespace FRG {
|
||||
|
||||
namespace {
|
||||
|
||||
// Wraps a sol::protected_function into a std::function<void()>, invoking it and
|
||||
// logging any Lua-side error to stderr. Shared by every binding that stores a
|
||||
// callback for later, one-shot invocation (npc_walk_to, move_object, etc.) and by
|
||||
// rehydrateNamedCallback, which re-wraps a callback looked up by name after load.
|
||||
std::function<void()> wrapLuaCallback(sol::protected_function fn) {
|
||||
return [fn]() mutable {
|
||||
auto result = fn();
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
std::cerr << "[script] callback error: " << err.what() << "\n";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Reverse-looks-up the global name a Lua function is registered under, by
|
||||
// comparing raw function pointers via lua_topointer. Returns "" if fn isn't
|
||||
// (or is no longer) reachable as a plain global — e.g. an anonymous function,
|
||||
// or one stored only in a local/upvalue. This lets callback-accepting bindings
|
||||
// remember a callback "by name" for save/load without changing the Lua-facing
|
||||
// API (scripts still just pass a function reference).
|
||||
// remember a callback by name while scripts continue to pass function references.
|
||||
std::string findGlobalFunctionName(lua_State* L, const sol::protected_function& fn) {
|
||||
if (!fn.valid()) return {};
|
||||
|
||||
@@ -58,6 +44,19 @@ namespace FRG {
|
||||
return found;
|
||||
}
|
||||
|
||||
Character* findNpc(Location* location, const std::string& id) {
|
||||
for (auto& npc : location->npcs)
|
||||
if (npc && npc->entityId() == id) return npc.get();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
InteractiveObject* findInteractiveObject(
|
||||
Location* location, const std::string& id) {
|
||||
for (auto& object : location->interactiveObjects)
|
||||
if (object.entityId() == id) return &object;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
struct ScriptEngine::Impl {
|
||||
@@ -65,11 +64,12 @@ namespace FRG {
|
||||
std::unordered_map<std::string, sol::protected_function> triggerEnterCallbacks;
|
||||
std::unordered_map<std::string, sol::protected_function> triggerExitCallbacks;
|
||||
std::unordered_map<std::string, sol::protected_function> cutsceneCompleteCallbacks;
|
||||
std::unordered_map<int, sol::protected_function> npcBumpedByPlayerCallbacks;
|
||||
std::unordered_map<int, sol::protected_function> npcBumpsPlayerCallbacks;
|
||||
std::unordered_map<std::string, sol::protected_function> npcBumpedByPlayerCallbacks;
|
||||
std::unordered_map<std::string, sol::protected_function> npcBumpsPlayerCallbacks;
|
||||
std::unordered_map<std::string, int>* globalInts = nullptr;
|
||||
std::unordered_map<std::string, float>* globalFloats = nullptr;
|
||||
Quest::QuestJournal* questJournal = nullptr;
|
||||
sol::protected_function locationInitCallback;
|
||||
sol::protected_function locationEnterCallback;
|
||||
sol::protected_function locationExitCallback;
|
||||
sol::protected_function darklandsEnterCallback;
|
||||
@@ -110,82 +110,92 @@ namespace FRG {
|
||||
|
||||
auto api = lua.create_named_table("game_api");
|
||||
|
||||
// npc_walk_to(index, x, y, z [, on_arrived])
|
||||
// npc_walk_to(npc_id, x, y, z [, on_arrived])
|
||||
// on_arrived is an optional Lua function called when the NPC reaches the target.
|
||||
// It can call npc_walk_to again (or anything else) to chain behaviour.
|
||||
api.set_function("npc_walk_to",
|
||||
[loc, this_impl = impl.get()](int index, float x, float y, float z, sol::object on_arrived) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_walk_to: index " << index
|
||||
<< " out of range (0.." << npcs.size() - 1 << ")\n";
|
||||
[loc, this_impl = impl.get()](const std::string& npcId, float x, float y, float z, sol::object on_arrived) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_walk_to: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (on_arrived.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = on_arrived.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] npc_walk_to: on_arrived callback has no global name, will not survive save/load\n";
|
||||
std::cerr << "[script] npc_walk_to: on_arrived must be a global Lua function\n";
|
||||
}
|
||||
npcs[index]->state.homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
npcs[index]->setTarget(Eigen::Vector3f(x, y, z), std::move(cb), std::move(cbName));
|
||||
loc->updateCharacterState(*npc, [x, z](CharacterState& workingState) {
|
||||
workingState.homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
});
|
||||
loc->setCharacterTarget(
|
||||
*npc, Eigen::Vector3f(x, y, z), std::move(cbName));
|
||||
});
|
||||
|
||||
|
||||
api.set_function("npc_rotate_to",
|
||||
[loc](int index, float angle) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_rotate_to: index " << index
|
||||
<< " out of range (0.." << npcs.size() - 1 << ")\n";
|
||||
[loc](const std::string& npcId, float angle) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_rotate_to: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
logger() << "[script] npc_rotate_to: index " << index << " angle " << angle << " degrees (" << rad << " radians)" << std::endl;
|
||||
npcs[index]->state.targetFacingAngle = rad;
|
||||
logger() << "[script] npc_rotate_to: " << npcId << " angle " << angle << " degrees (" << rad << " radians)" << std::endl;
|
||||
loc->updateCharacterState(*npc, [rad](CharacterState& workingState) {
|
||||
workingState.targetFacingAngle = rad;
|
||||
});
|
||||
});
|
||||
|
||||
api.set_function("player_rotate_to",
|
||||
[loc](float angle) {
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
logger() << "[script] player_rotate_to: angle " << angle << " degrees (" << rad << " radians)" << std::endl;
|
||||
loc->player->state.targetFacingAngle = rad;
|
||||
loc->updateCharacterState(*loc->player, [rad](CharacterState& workingState) {
|
||||
workingState.targetFacingAngle = rad;
|
||||
});
|
||||
});
|
||||
|
||||
api.set_function("player_stop_and_rotate_to_npc",
|
||||
[loc](int index) {
|
||||
loc->player->stopAndRotateToTarget(index);
|
||||
[loc](const std::string& npcId) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] player_stop_and_rotate_to_npc: unknown NPC id '"
|
||||
<< npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
loc->faceCharacter(*loc->player, npc->entityId());
|
||||
});
|
||||
|
||||
api.set_function("npc_stop_and_rotate_to_player",
|
||||
[loc](int index) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_stop_and_rotate_to_player: index " << index
|
||||
<< " out of range (0.." << npcs.size() - 1 << ")\n";
|
||||
[loc](const std::string& npcId) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_stop_and_rotate_to_player: unknown NPC id '"
|
||||
<< npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
npcs[index]->stopAndRotateToTarget(CharacterState::kPlayerIndex);
|
||||
loc->faceCharacter(*npc, "player");
|
||||
});
|
||||
|
||||
api.set_function("player_walk_to",
|
||||
[loc, this_impl = impl.get()](float x, float y, float z, sol::object on_arrived) {
|
||||
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (on_arrived.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = on_arrived.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] player_walk_to: on_arrived callback has no global name, will not survive save/load\n";
|
||||
std::cerr << "[script] player_walk_to: on_arrived must be a global Lua function\n";
|
||||
}
|
||||
loc->player->state.homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
loc->player->setTarget(Eigen::Vector3f(x, y, z), std::move(cb), std::move(cbName));
|
||||
loc->updateCharacterState(*loc->player, [x, z](CharacterState& workingState) {
|
||||
workingState.homePosition = Eigen::Vector3f(x, 0.f, z);
|
||||
});
|
||||
loc->setCharacterTarget(
|
||||
*loc->player, Eigen::Vector3f(x, y, z), std::move(cbName));
|
||||
});
|
||||
|
||||
|
||||
@@ -225,50 +235,44 @@ namespace FRG {
|
||||
inventory->removeItem(id);
|
||||
});
|
||||
|
||||
// deactivate_interactive_object(object_name)
|
||||
api.set_function("deactivate_interactive_object", [loc](const std::string& objectName) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name == objectName) {
|
||||
intObj.state.isActive = false;
|
||||
logger() << "[script] deactivate_interactive_object: " << objectName << std::endl;
|
||||
return;
|
||||
}
|
||||
api.set_function("deactivate_interactive_object", [loc](const std::string& objectId) {
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (object) {
|
||||
object->state.isActive = false;
|
||||
logger() << "[script] deactivate_interactive_object: " << objectId << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] deactivate_interactive_object: not found: " << objectName << std::endl;
|
||||
std::cerr << "[script] deactivate_interactive_object: unknown object id: " << objectId << std::endl;
|
||||
});
|
||||
|
||||
// activate_interactive_object(object_name)
|
||||
api.set_function("activate_interactive_object", [loc](const std::string& objectName) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name == objectName) {
|
||||
intObj.state.isActive = true;
|
||||
logger() << "[script] activate_interactive_object: " << objectName << std::endl;
|
||||
return;
|
||||
}
|
||||
api.set_function("activate_interactive_object", [loc](const std::string& objectId) {
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (object) {
|
||||
object->state.isActive = true;
|
||||
logger() << "[script] activate_interactive_object: " << objectId << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] activate_interactive_object: not found: " << objectName << std::endl;
|
||||
std::cerr << "[script] activate_interactive_object: unknown object id: " << objectId << std::endl;
|
||||
});
|
||||
|
||||
api.set_function("set_object_rotation", [loc](const std::string& objectName, float value) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name == objectName) {
|
||||
intObj.state.rotationY = value * static_cast<float>(M_PI) / 180.f;
|
||||
logger() << "[script] set_object_rotation: " << objectName << " " << value << std::endl;
|
||||
return;
|
||||
}
|
||||
api.set_function("set_object_rotation", [loc](const std::string& objectId, float value) {
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (object) {
|
||||
object->state.rotationY = value * static_cast<float>(M_PI) / 180.f;
|
||||
logger() << "[script] set_object_rotation: " << objectId << " " << value << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] set_object_rotation: not found: " << objectName << std::endl;
|
||||
std::cerr << "[script] set_object_rotation: unknown object id: " << objectId << std::endl;
|
||||
});
|
||||
|
||||
api.set_function("set_object_alpha", [loc](const std::string& objectName, float value) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name == objectName) {
|
||||
intObj.state.alpha = value;
|
||||
logger() << "[script] set_object_alpha: " <<objectName << " " << value << std::endl;
|
||||
return;
|
||||
}
|
||||
api.set_function("set_object_alpha", [loc](const std::string& objectId, float value) {
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (object) {
|
||||
object->state.alpha = value;
|
||||
logger() << "[script] set_object_alpha: " << objectId << " " << value << std::endl;
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] set_object_alpha: not found: " << objectName << std::endl;
|
||||
std::cerr << "[script] set_object_alpha: unknown object id: " << objectId << std::endl;
|
||||
});
|
||||
|
||||
// get_inventory_count()
|
||||
@@ -332,7 +336,15 @@ namespace FRG {
|
||||
// on_enter and on_exit are optional Lua functions (pass nil to omit).
|
||||
// Called when the player enters or exits the named trigger zone.
|
||||
api.set_function("set_trigger_zone_callbacks",
|
||||
[this_impl = impl.get()](const std::string& zoneId, sol::object onEnter, sol::object onExit) {
|
||||
[loc, this_impl = impl.get()](const std::string& zoneId, sol::object onEnter, sol::object onExit) {
|
||||
const bool exists = std::any_of(
|
||||
loc->triggerZones.begin(), loc->triggerZones.end(),
|
||||
[&zoneId](const TriggerZone& zone) { return zone.id == zoneId; });
|
||||
if (!exists) {
|
||||
std::cerr << "[script] set_trigger_zone_callbacks: unknown zone id '"
|
||||
<< zoneId << "'\n";
|
||||
return;
|
||||
}
|
||||
if (onEnter.is<sol::protected_function>())
|
||||
this_impl->triggerEnterCallbacks[zoneId] = onEnter.as<sol::protected_function>();
|
||||
if (onExit.is<sol::protected_function>())
|
||||
@@ -386,11 +398,15 @@ namespace FRG {
|
||||
loc->requestAdvanceDarklandsHud();
|
||||
});
|
||||
|
||||
// set_location_callbacks(on_enter, on_exit)
|
||||
// set_location_callbacks(on_init, on_enter, on_exit)
|
||||
// on_init() is called only when this location has no retained state.
|
||||
// on_enter() called once when the player arrives at this location.
|
||||
// on_exit() called once just before the player leaves this location.
|
||||
api.set_function("set_location_callbacks",
|
||||
[this_impl = impl.get()](sol::object onEnter, sol::object onExit) {
|
||||
[this_impl = impl.get()](
|
||||
sol::object onInit, sol::object onEnter, sol::object onExit) {
|
||||
if (onInit.is<sol::protected_function>())
|
||||
this_impl->locationInitCallback = onInit.as<sol::protected_function>();
|
||||
if (onEnter.is<sol::protected_function>())
|
||||
this_impl->locationEnterCallback = onEnter.as<sol::protected_function>();
|
||||
if (onExit.is<sol::protected_function>())
|
||||
@@ -415,11 +431,11 @@ namespace FRG {
|
||||
|
||||
api.set_function("get_player_x",
|
||||
[loc]() -> float {
|
||||
return loc->player ? loc->player->state.position.x() : 0.0f;
|
||||
return loc->player ? loc->player->characterState.position.x() : 0.0f;
|
||||
});
|
||||
api.set_function("get_player_z",
|
||||
[loc]() -> float {
|
||||
return loc->player ? loc->player->state.position.z() : 0.0f;
|
||||
return loc->player ? loc->player->characterState.position.z() : 0.0f;
|
||||
});
|
||||
|
||||
// set_player_hp(value) — sets the player's current HP directly.
|
||||
@@ -427,12 +443,14 @@ namespace FRG {
|
||||
[loc](float value) {
|
||||
if (loc->player)
|
||||
{
|
||||
loc->player->state.setHp(value);
|
||||
if (loc->player->state.currentState == AnimationState::ACTION_TO_DEATH || loc->player->state.currentState == AnimationState::DEATH_IDLE)
|
||||
{
|
||||
loc->player->state.currentState = AnimationState::STAND;
|
||||
loc->player->state.resetAnim = true;
|
||||
}
|
||||
loc->setCharacterHp(*loc->player, value);
|
||||
loc->updateCharacterState(*loc->player, [](CharacterState& workingState) {
|
||||
if (workingState.currentState == AnimationState::ACTION_TO_DEATH ||
|
||||
workingState.currentState == AnimationState::DEATH_IDLE) {
|
||||
workingState.currentState = AnimationState::STAND;
|
||||
workingState.resetAnim = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
// get_player_hp() → float (0 if no player)
|
||||
@@ -537,246 +555,253 @@ namespace FRG {
|
||||
});
|
||||
|
||||
api.set_function("set_trigger_zone_enabled",
|
||||
[loc](int index, bool value) {
|
||||
auto& triggerZones = loc->triggerZones;
|
||||
if (index < 0 || index >= static_cast<int>(triggerZones.size())) {
|
||||
std::cerr << "[script] set_trigger_zone_enabled: index " << index << " out of range\n";
|
||||
[loc](const std::string& zoneId, bool value) {
|
||||
for (auto& zone : loc->triggerZones) {
|
||||
if (zone.id != zoneId) continue;
|
||||
zone.enabled = value;
|
||||
return;
|
||||
}
|
||||
triggerZones[index].enabled = value;
|
||||
std::cerr << "[script] set_trigger_zone_enabled: unknown zone id '"
|
||||
<< zoneId << "'\n";
|
||||
});
|
||||
|
||||
// set_teleport_active(index, active) — activates or deactivates a teleport zone by index.
|
||||
// set_teleport_active(zone_id, active)
|
||||
// Swaps the particle effect texture to match the new state.
|
||||
api.set_function("set_teleport_active",
|
||||
[loc](int index, bool value) {
|
||||
auto& zones = loc->teleportZones;
|
||||
if (index < 0 || index >= static_cast<int>(zones.size())) {
|
||||
std::cerr << "[script] set_teleport_active: index " << index << " out of range\n";
|
||||
[loc](const std::string& zoneId, bool value) {
|
||||
for (auto& zone : loc->teleportZones) {
|
||||
if (zone.id != zoneId) continue;
|
||||
zone.setActive(value);
|
||||
return;
|
||||
}
|
||||
zones[index].setActive(value);
|
||||
std::cerr << "[script] set_teleport_active: unknown zone id '"
|
||||
<< zoneId << "'\n";
|
||||
});
|
||||
|
||||
// is_teleport_active(index) → bool
|
||||
// is_teleport_active(zone_id) → bool
|
||||
api.set_function("is_teleport_active",
|
||||
[loc](int index) -> bool {
|
||||
auto& zones = loc->teleportZones;
|
||||
if (index < 0 || index >= static_cast<int>(zones.size())) {
|
||||
std::cerr << "[script] is_teleport_active: index " << index << " out of range\n";
|
||||
return false;
|
||||
}
|
||||
return zones[index].active;
|
||||
[loc](const std::string& zoneId) -> bool {
|
||||
for (const auto& zone : loc->teleportZones)
|
||||
if (zone.id == zoneId) return zone.active;
|
||||
std::cerr << "[script] is_teleport_active: unknown zone id '"
|
||||
<< zoneId << "'\n";
|
||||
return false;
|
||||
});
|
||||
|
||||
|
||||
// npc_set_hp(index, value) — sets an NPC's current HP directly.
|
||||
// npc_set_hp(npc_id, value)
|
||||
api.set_function("npc_set_hp",
|
||||
[loc](int index, float value) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_set_hp: index " << index << " out of range\n";
|
||||
[loc](const std::string& npcId, float value) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_set_hp: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
npcs[index]->setHp(value);
|
||||
loc->setCharacterHp(*npc, value);
|
||||
});
|
||||
|
||||
// npc_set_position(index, x, y, z) — teleports an NPC instantly, no walking.
|
||||
// npc_set_position(npc_id, x, y, z)
|
||||
api.set_function("npc_set_position",
|
||||
[loc](int index, float x, float y, float z) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_set_position: index " << index << " out of range\n";
|
||||
[loc](const std::string& npcId, float x, float y, float z) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_set_position: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
Eigen::Vector3f pos(x, y, z);
|
||||
npcs[index]->state.position = pos;
|
||||
npcs[index]->state.homePosition = pos;
|
||||
npcs[index]->setTarget(pos);
|
||||
loc->updateCharacterState(*npc, [&pos](CharacterState& workingState) {
|
||||
workingState.position = pos;
|
||||
workingState.homePosition = pos;
|
||||
});
|
||||
loc->setCharacterTarget(*npc, pos);
|
||||
});
|
||||
|
||||
// npc_set_rotation(index, angle) — sets NPC facing angle around Y axis (degrees).
|
||||
// npc_set_rotation(npc_id, angle)
|
||||
api.set_function("npc_set_rotation",
|
||||
[loc](int index, float angle) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_set_rotation: index " << index << " out of range\n";
|
||||
[loc](const std::string& npcId, float angle) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_set_rotation: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
npcs[index]->state.facingAngle = rad;
|
||||
npcs[index]->state.targetFacingAngle = rad;
|
||||
loc->updateCharacterState(*npc, [rad](CharacterState& workingState) {
|
||||
workingState.facingAngle = rad;
|
||||
workingState.targetFacingAngle = rad;
|
||||
});
|
||||
});
|
||||
|
||||
// set_npc_enabled(index, enabled)
|
||||
// set_npc_enabled(npc_id, enabled)
|
||||
api.set_function("set_npc_enabled",
|
||||
[loc](int index, bool value) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] set_npc_enabled: index " << index << " out of range\n";
|
||||
[loc](const std::string& npcId, bool value) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] set_npc_enabled: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
npcs[index]->state.enabled = value;
|
||||
npcs[index]->state.attack = 0;
|
||||
npcs[index]->state.attack_cooldown = 0;
|
||||
npcs[index]->state.currentState = AnimationState::STAND;
|
||||
if (npcs[index]->state.canAttack)
|
||||
{
|
||||
npcs[index]->state.attackTargetIndex = CharacterState::kPlayerIndex;
|
||||
}
|
||||
npcs[index]->state.battle_state = 0;
|
||||
loc->updateCharacterState(*npc, [value](CharacterState& workingState) {
|
||||
workingState.enabled = value;
|
||||
workingState.attack = 0;
|
||||
workingState.attack_cooldown = 0;
|
||||
workingState.currentState = AnimationState::STAND;
|
||||
if (workingState.canAttack)
|
||||
workingState.attackTargetId = "player";
|
||||
workingState.battle_state = 0;
|
||||
});
|
||||
});
|
||||
|
||||
api.set_function("player_set_position",
|
||||
[loc](float x, float y, float z) {
|
||||
Eigen::Vector3f pos(x, y, z);
|
||||
loc->player->state.position = pos;
|
||||
loc->player->state.homePosition = pos;
|
||||
loc->player->setTarget(pos);
|
||||
loc->updateCharacterState(*loc->player, [&pos](CharacterState& workingState) {
|
||||
workingState.position = pos;
|
||||
workingState.homePosition = pos;
|
||||
});
|
||||
loc->setCharacterTarget(*loc->player, pos);
|
||||
});
|
||||
|
||||
api.set_function("player_set_rotation",
|
||||
[loc](float angle) {
|
||||
const float rad = angle * static_cast<float>(M_PI) / 180.f;
|
||||
loc->player->state.facingAngle = rad;
|
||||
loc->player->state.targetFacingAngle = rad;
|
||||
loc->updateCharacterState(*loc->player, [rad](CharacterState& workingState) {
|
||||
workingState.facingAngle = rad;
|
||||
workingState.targetFacingAngle = rad;
|
||||
});
|
||||
});
|
||||
|
||||
// move_object(name, x, y, z, duration_sec [, on_complete])
|
||||
api.set_function("move_object",
|
||||
[loc, this_impl = impl.get()](const std::string& name, float x, float y, float z,
|
||||
[loc, this_impl = impl.get()](const std::string& objectId, float x, float y, float z,
|
||||
float durationSec, sol::object onComplete) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name != name) continue;
|
||||
if (intObj.state.isAnimating) {
|
||||
std::cerr << "[script] move_object: '" << name << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] move_object: on_complete callback has no global name, will not survive save/load\n";
|
||||
}
|
||||
intObj.moveTo(Eigen::Vector3f(x, y, z), durationSec, std::move(cb), std::move(cbName));
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (!object) {
|
||||
std::cerr << "[script] move_object: unknown object id '" << objectId << "'\n";
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] move_object: object '" << name << "' not found\n";
|
||||
if (object->state.isAnimating) {
|
||||
std::cerr << "[script] move_object: '" << objectId << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] move_object: on_complete must be a global Lua function\n";
|
||||
}
|
||||
object->state.moveTo(
|
||||
Eigen::Vector3f(x, y, z), durationSec, std::move(cbName));
|
||||
});
|
||||
|
||||
// rotate_object(name, angle_deg, duration_sec [, on_complete])
|
||||
api.set_function("rotate_object",
|
||||
[loc, this_impl = impl.get()](const std::string& name, float angleDeg,
|
||||
[loc, this_impl = impl.get()](const std::string& objectId, float angleDeg,
|
||||
float durationSec, sol::object onComplete) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name != name) continue;
|
||||
if (intObj.state.isAnimating) {
|
||||
std::cerr << "[script] rotate_object: '" << name << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] rotate_object: on_complete callback has no global name, will not survive save/load\n";
|
||||
}
|
||||
const float angleRad = angleDeg * static_cast<float>(M_PI) / 180.f;
|
||||
intObj.rotateTo(intObj.state.rotationY + angleRad, durationSec, std::move(cb), std::move(cbName));
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (!object) {
|
||||
std::cerr << "[script] rotate_object: unknown object id '" << objectId << "'\n";
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] rotate_object: object '" << name << "' not found\n";
|
||||
if (object->state.isAnimating) {
|
||||
std::cerr << "[script] rotate_object: '" << objectId << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] rotate_object: on_complete must be a global Lua function\n";
|
||||
}
|
||||
const float angleRad = angleDeg * static_cast<float>(M_PI) / 180.f;
|
||||
object->state.rotateTo(
|
||||
object->state.rotationY + angleRad, durationSec, std::move(cbName));
|
||||
});
|
||||
|
||||
// fade_object(name, target_alpha, duration_sec [, on_complete])
|
||||
api.set_function("fade_object",
|
||||
[loc, this_impl = impl.get()](const std::string& name, float targetAlpha,
|
||||
[loc, this_impl = impl.get()](const std::string& objectId, float targetAlpha,
|
||||
float durationSec, sol::object onComplete) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name != name) continue;
|
||||
if (intObj.state.isAnimating) {
|
||||
std::cerr << "[script] fade_object: '" << name << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] fade_object: on_complete callback has no global name, will not survive save/load\n";
|
||||
}
|
||||
intObj.fadeTo(targetAlpha, durationSec, std::move(cb), std::move(cbName));
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (!object) {
|
||||
std::cerr << "[script] fade_object: unknown object id '" << objectId << "'\n";
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] fade_object: object '" << name << "' not found\n";
|
||||
if (object->state.isAnimating) {
|
||||
std::cerr << "[script] fade_object: '" << objectId << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] fade_object: on_complete must be a global Lua function\n";
|
||||
}
|
||||
object->state.fadeTo(targetAlpha, durationSec, std::move(cbName));
|
||||
});
|
||||
|
||||
// scale_object(name, target_scale, duration_sec [, on_complete])
|
||||
api.set_function("scale_object",
|
||||
[loc, this_impl = impl.get()](const std::string& name, float targetScale,
|
||||
[loc, this_impl = impl.get()](const std::string& objectId, float targetScale,
|
||||
float durationSec, sol::object onComplete) {
|
||||
for (auto& intObj : loc->interactiveObjects) {
|
||||
if (intObj.loadedObject.name != name) continue;
|
||||
if (intObj.state.isAnimating) {
|
||||
std::cerr << "[script] scale_object: '" << name << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::function<void()> cb;
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cb = wrapLuaCallback(fn);
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] scale_object: on_complete callback has no global name, will not survive save/load\n";
|
||||
}
|
||||
intObj.scaleTo(targetScale, durationSec, std::move(cb), std::move(cbName));
|
||||
InteractiveObject* object = findInteractiveObject(loc, objectId);
|
||||
if (!object) {
|
||||
std::cerr << "[script] scale_object: unknown object id '" << objectId << "'\n";
|
||||
return;
|
||||
}
|
||||
std::cerr << "[script] scale_object: object '" << name << "' not found\n";
|
||||
if (object->state.isAnimating) {
|
||||
std::cerr << "[script] scale_object: '" << objectId << "' is already animating\n";
|
||||
return;
|
||||
}
|
||||
std::string cbName;
|
||||
if (onComplete.is<sol::protected_function>()) {
|
||||
sol::protected_function fn = onComplete.as<sol::protected_function>();
|
||||
cbName = findGlobalFunctionName(this_impl->lua.lua_state(), fn);
|
||||
if (cbName.empty())
|
||||
std::cerr << "[script] scale_object: on_complete must be a global Lua function\n";
|
||||
}
|
||||
object->state.scaleTo(targetScale, durationSec, std::move(cbName));
|
||||
});
|
||||
|
||||
api.set_function("resetPlayerAfterDeath",
|
||||
[loc]() {
|
||||
if (loc->player) loc->player->resetPlayerAfterDeath();
|
||||
if (loc->player) loc->resetCharacterAfterDeath(*loc->player);
|
||||
});
|
||||
|
||||
// player_stop() — cancels the player's current walk target and stops them immediately.
|
||||
api.set_function("player_stop",
|
||||
[loc]() {
|
||||
if (loc->player) loc->player->stopInPlace();
|
||||
if (loc->player) loc->stopCharacter(*loc->player);
|
||||
});
|
||||
|
||||
// npc_stop(index) — cancels an NPC's current walk target and stops them immediately.
|
||||
// npc_stop(npc_id)
|
||||
api.set_function("npc_stop",
|
||||
[loc](int index) {
|
||||
auto& npcs = loc->npcs;
|
||||
if (index < 0 || index >= static_cast<int>(npcs.size())) {
|
||||
std::cerr << "[script] npc_stop: index " << index
|
||||
<< " out of range (0.." << npcs.size() - 1 << ")\n";
|
||||
[loc](const std::string& npcId) {
|
||||
Character* npc = findNpc(loc, npcId);
|
||||
if (!npc) {
|
||||
std::cerr << "[script] npc_stop: unknown NPC id '" << npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
npcs[index]->stopInPlace();
|
||||
loc->stopCharacter(*npc);
|
||||
});
|
||||
|
||||
// set_npc_bump_callbacks(npc_index, on_player_bumps_npc, on_npc_bumps_player)
|
||||
// set_npc_bump_callbacks(npc_id, on_player_bumps_npc, on_npc_bumps_player)
|
||||
// Both callbacks are optional (pass nil to omit).
|
||||
// on_player_bumps_npc() — player was moving and walked into the NPC.
|
||||
// on_npc_bumps_player() — the NPC was moving and walked into the standing player.
|
||||
// Each callback fires at most once every 5 seconds per NPC (cooldown managed by Location).
|
||||
api.set_function("set_npc_bump_callbacks",
|
||||
[this_impl = impl.get()](int index, sol::object onPlayerBumpsNpc, sol::object onNpcBumpsPlayer) {
|
||||
[loc, this_impl = impl.get()](const std::string& npcId, sol::object onPlayerBumpsNpc, sol::object onNpcBumpsPlayer) {
|
||||
if (!findNpc(loc, npcId)) {
|
||||
std::cerr << "[script] set_npc_bump_callbacks: unknown NPC id '"
|
||||
<< npcId << "'\n";
|
||||
return;
|
||||
}
|
||||
if (onPlayerBumpsNpc.is<sol::protected_function>())
|
||||
this_impl->npcBumpedByPlayerCallbacks[index] = onPlayerBumpsNpc.as<sol::protected_function>();
|
||||
this_impl->npcBumpedByPlayerCallbacks[npcId] = onPlayerBumpsNpc.as<sol::protected_function>();
|
||||
if (onNpcBumpsPlayer.is<sol::protected_function>())
|
||||
this_impl->npcBumpsPlayerCallbacks[index] = onNpcBumpsPlayer.as<sol::protected_function>();
|
||||
this_impl->npcBumpsPlayerCallbacks[npcId] = onNpcBumpsPlayer.as<sol::protected_function>();
|
||||
});
|
||||
|
||||
// quest_unlock(quest_id)
|
||||
@@ -896,7 +921,7 @@ namespace FRG {
|
||||
#endif
|
||||
}
|
||||
|
||||
void ScriptEngine::callNpcInteractCallback(int npcIndex) {
|
||||
void ScriptEngine::callNpcInteractCallback(const std::string& npcId) {
|
||||
if (!impl) {
|
||||
std::cerr << "[SCRIPT] Engine not initialized!" << std::endl;
|
||||
return;
|
||||
@@ -904,13 +929,13 @@ namespace FRG {
|
||||
sol::state& lua = impl->lua;
|
||||
sol::function fn = lua["on_npc_interact"];
|
||||
if (fn.valid()) {
|
||||
auto result = fn(npcIndex);
|
||||
auto result = fn(npcId);
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
std::cerr << "[SCRIPT] on_npc_interact error: " << err.what() << "\n";
|
||||
}
|
||||
else {
|
||||
logger() << "[SCRIPT] on_npc_interact called with index " << npcIndex << std::endl;
|
||||
logger() << "[SCRIPT] on_npc_interact called with id " << npcId << std::endl;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -926,26 +951,26 @@ namespace FRG {
|
||||
}
|
||||
}*/
|
||||
|
||||
void ScriptEngine::callActivateFunction(const std::string& functionName) {
|
||||
void ScriptEngine::callFunction(const std::string& functionName) {
|
||||
if (!impl) {
|
||||
throw std::runtime_error("[SCRIPT] Engine not initialized!");
|
||||
}
|
||||
|
||||
if (functionName.empty()) {
|
||||
throw std::runtime_error("[SCRIPT] Activate function name is empty!");
|
||||
throw std::runtime_error("[SCRIPT] Function name is empty!");
|
||||
}
|
||||
|
||||
sol::state& lua = impl->lua;
|
||||
logger() << "[SCRIPT] Looking for activate function: " << functionName << std::endl;
|
||||
logger() << "[SCRIPT] Looking for function: " << functionName << std::endl;
|
||||
|
||||
sol::function activateFunc = lua[functionName];
|
||||
sol::protected_function function = lua[functionName];
|
||||
|
||||
if (!activateFunc.valid()) {
|
||||
if (!function.valid()) {
|
||||
throw std::runtime_error("[SCRIPT] Lua function not found: " + functionName);
|
||||
}
|
||||
|
||||
logger() << "[SCRIPT] Found function! Calling: " << functionName << std::endl;
|
||||
auto result = activateFunc();
|
||||
auto result = function();
|
||||
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
@@ -955,6 +980,10 @@ namespace FRG {
|
||||
logger() << "[SCRIPT] Function executed successfully!" << std::endl;
|
||||
}
|
||||
|
||||
void ScriptEngine::callActivateFunction(const std::string& functionName) {
|
||||
callFunction(functionName);
|
||||
}
|
||||
|
||||
void ScriptEngine::callItemPickupCallback(const std::string& objectName) {
|
||||
if (!impl) {
|
||||
std::cerr << "[SCRIPT] impl is null!" << std::endl;
|
||||
@@ -1016,6 +1045,15 @@ namespace FRG {
|
||||
if (impl) impl->questJournal = journal;
|
||||
}
|
||||
|
||||
void ScriptEngine::callLocationInitCallback() {
|
||||
if (!impl || !impl->locationInitCallback.valid()) return;
|
||||
auto result = impl->locationInitCallback();
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
std::cerr << "[SCRIPT] location init callback error: " << err.what() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEngine::callLocationEnterCallback() {
|
||||
if (!impl || !impl->locationEnterCallback.valid()) return;
|
||||
auto result = impl->locationEnterCallback();
|
||||
@@ -1082,14 +1120,14 @@ namespace FRG {
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEngine::callNpcBumpedByPlayerCallback(int npcIndex) {
|
||||
void ScriptEngine::callNpcBumpedByPlayerCallback(const std::string& npcId) {
|
||||
if (!impl) return;
|
||||
auto it = impl->npcBumpedByPlayerCallbacks.find(npcIndex);
|
||||
auto it = impl->npcBumpedByPlayerCallbacks.find(npcId);
|
||||
if (it == impl->npcBumpedByPlayerCallbacks.end()) return;
|
||||
auto result = it->second();
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
std::cerr << "[SCRIPT] on_player_bumps_npc error for NPC " << npcIndex << ": " << err.what() << "\n";
|
||||
std::cerr << "[SCRIPT] on_player_bumps_npc error for NPC " << npcId << ": " << err.what() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1105,14 +1143,14 @@ namespace FRG {
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEngine::callNpcBumpsPlayerCallback(int npcIndex) {
|
||||
void ScriptEngine::callNpcBumpsPlayerCallback(const std::string& npcId) {
|
||||
if (!impl) return;
|
||||
auto it = impl->npcBumpsPlayerCallbacks.find(npcIndex);
|
||||
auto it = impl->npcBumpsPlayerCallbacks.find(npcId);
|
||||
if (it == impl->npcBumpsPlayerCallbacks.end()) return;
|
||||
auto result = it->second();
|
||||
if (!result.valid()) {
|
||||
sol::error err = result;
|
||||
std::cerr << "[SCRIPT] on_npc_bumps_player error for NPC " << npcIndex << ": " << err.what() << "\n";
|
||||
std::cerr << "[SCRIPT] on_npc_bumps_player error for NPC " << npcId << ": " << err.what() << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1177,18 +1215,4 @@ namespace FRG {
|
||||
}
|
||||
}
|
||||
|
||||
void ScriptEngine::rehydrateNamedCallback(const std::string& name, std::function<void()>& outCallback) const
|
||||
{
|
||||
outCallback = nullptr;
|
||||
if (!impl || name.empty()) return;
|
||||
|
||||
sol::protected_function fn = impl->lua[name];
|
||||
if (!fn.valid()) {
|
||||
std::cerr << "[SCRIPT] rehydrateNamedCallback: global function '" << name
|
||||
<< "' not found (script changed since save?)\n";
|
||||
return;
|
||||
}
|
||||
outCallback = wrapLuaCallback(fn);
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
+8
-16
@@ -2,7 +2,6 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "ISaveable.h"
|
||||
@@ -13,7 +12,7 @@ namespace FRG {
|
||||
class Location;
|
||||
class Inventory;
|
||||
|
||||
class ScriptEngine : public ISaveable {
|
||||
class ScriptEngine {
|
||||
public:
|
||||
ScriptEngine();
|
||||
~ScriptEngine();
|
||||
@@ -25,23 +24,24 @@ public:
|
||||
//void runScript(const std::string& path);
|
||||
|
||||
void callItemPickupCallback(const std::string& objectName);
|
||||
|
||||
void callFunction(const std::string& functionName);
|
||||
void callActivateFunction(const std::string& functionName);
|
||||
|
||||
void callNpcInteractCallback(int npcIndex);
|
||||
void callNpcInteractCallback(const std::string& npcId);
|
||||
|
||||
void callTriggerEnterCallback(const std::string& zoneId);
|
||||
void callTriggerExitCallback(const std::string& zoneId);
|
||||
|
||||
// Fired when the player (moving) physically bumps into a standing NPC.
|
||||
void callNpcBumpedByPlayerCallback(int npcIndex);
|
||||
void callNpcBumpedByPlayerCallback(const std::string& npcId);
|
||||
// Fired when a moving NPC physically bumps into the standing player.
|
||||
void callNpcBumpsPlayerCallback(int npcIndex);
|
||||
void callNpcBumpsPlayerCallback(const std::string& npcId);
|
||||
|
||||
void setGlobalStore(std::unordered_map<std::string, int>* store);
|
||||
void setGlobalFloatStore(std::unordered_map<std::string, float>* store);
|
||||
void setQuestJournal(Quest::QuestJournal* journal);
|
||||
|
||||
void callLocationInitCallback();
|
||||
void callLocationEnterCallback();
|
||||
void callLocationExitCallback();
|
||||
|
||||
@@ -63,16 +63,8 @@ public:
|
||||
// has already run and registered all callbacks.
|
||||
void loadScriptGlobals(const nlohmann::json& in);
|
||||
|
||||
// Re-wires an in-flight, one-shot callback (NPC/player walk-arrival, object
|
||||
// animation-complete) that was serialized by name rather than by value.
|
||||
// Looks up `name` as a global Lua function and assigns a wrapped invoker to
|
||||
// outCallback; leaves outCallback empty and logs an error if not found.
|
||||
// Call after init() (the script must already have run) and after the owning
|
||||
// state (CharacterState / InteractiveObjectState) has been loaded.
|
||||
void rehydrateNamedCallback(const std::string& name, std::function<void()>& outCallback) const;
|
||||
|
||||
void save(nlohmann::json& out) const override { saveScriptGlobals(out); }
|
||||
void load(const nlohmann::json& in) override { loadScriptGlobals(in); }
|
||||
void save(nlohmann::json& out) const { saveScriptGlobals(out); }
|
||||
void load(const nlohmann::json& in) { loadScriptGlobals(in); }
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
|
||||
+3
-3
@@ -51,13 +51,13 @@ namespace FRG {
|
||||
std::string shaderProgramName;
|
||||
|
||||
// Sparks spawn within this radius of each emission point. 0 means
|
||||
// "spawn exactly at the point" (legacy behavior).
|
||||
// "spawn exactly at the point" (old behavior).
|
||||
float emissionRadius;
|
||||
|
||||
bool configured;
|
||||
// Rebuilds the CPU-side quad/UV arrays. If cameraViewMatrix is identity,
|
||||
// particles are rendered as axis-aligned quads in world XY and sorted by
|
||||
// world Z (legacy behavior). Otherwise quads are billboarded toward the
|
||||
// world Z (old behavior). Otherwise quads are billboarded toward the
|
||||
// camera using its world-space right/up axes, and particles are sorted
|
||||
// back-to-front in eye space for correct alpha blending.
|
||||
void prepareDrawData(const Eigen::Matrix4f& cameraViewMatrix);
|
||||
@@ -106,7 +106,7 @@ namespace FRG {
|
||||
// Вызывать ДО draw() в начале кадра: готовит данные и загружает в VBO.
|
||||
// Pass the current camera view matrix (world → eye) to billboard sparks
|
||||
// toward the camera and sort them back-to-front. Identity (default)
|
||||
// keeps the legacy non-billboarded behavior.
|
||||
// keeps the old non-billboarded behavior.
|
||||
void prepareForDraw(const Eigen::Matrix4f& cameraViewMatrix = Eigen::Matrix4f::Identity());
|
||||
|
||||
void draw(Renderer& renderer, float zoom, int screenWidth, int screenHeight);
|
||||
|
||||
+2
-2
@@ -287,10 +287,10 @@ namespace FRG {
|
||||
float x1, u1;
|
||||
if (fillMode) {
|
||||
const float clamped = std::clamp(value, 0.0f, 1.0f);
|
||||
if (clamped <= 0.0f) {
|
||||
/*if (clamped <= 0.0f) {
|
||||
trackMesh.RefreshVBO();
|
||||
return;
|
||||
}
|
||||
}*/
|
||||
x1 = rect.x + rect.w * clamped;
|
||||
u1 = clamped;
|
||||
} else {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
namespace FRG::Dialogue {
|
||||
|
||||
class DialogueSystem : public FRG::ISaveable {
|
||||
class DialogueSystem {
|
||||
public:
|
||||
bool init(Renderer& renderer, const std::string& zipFile = "");
|
||||
|
||||
@@ -48,8 +48,8 @@ public:
|
||||
void saveDialogueState(nlohmann::json& out) const { dialogueRuntime.save(out); }
|
||||
void loadDialogueState(const nlohmann::json& in) { dialogueRuntime.load(in); }
|
||||
|
||||
void save(nlohmann::json& out) const override { saveDialogueState(out); }
|
||||
void load(const nlohmann::json& in) override { loadDialogueState(in); }
|
||||
void save(nlohmann::json& out) const { saveDialogueState(out); }
|
||||
void load(const nlohmann::json& in) { loadDialogueState(in); }
|
||||
|
||||
bool isActive() const { return dialogueRuntime.isActive() || cutsceneRuntime.isActive(); }
|
||||
bool isDialogueActive() const { return dialogueRuntime.isActive(); }
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
#pragma once
|
||||
|
||||
#include "../GameState.h"
|
||||
#include "../external/nlohmann/json.hpp"
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
// Value-only view of authoritative state. It deliberately contains no runtime
|
||||
// pointers, callbacks, renderer objects, or mutable references, so the same
|
||||
// contract can later be transported over a network.
|
||||
struct ChatMessageSnapshot {
|
||||
std::string text;
|
||||
bool incoming = false;
|
||||
};
|
||||
|
||||
struct CharacterSnapshot {
|
||||
std::string id;
|
||||
CharacterState state;
|
||||
};
|
||||
|
||||
struct InteractiveObjectSnapshot {
|
||||
std::string id;
|
||||
InteractiveObjectState state;
|
||||
};
|
||||
|
||||
struct LocationSnapshot {
|
||||
std::string id;
|
||||
LocationState state;
|
||||
std::unordered_map<std::string, CharacterSnapshot> npcs;
|
||||
std::unordered_map<std::string, InteractiveObjectSnapshot> interactiveObjects;
|
||||
std::unordered_map<std::string, TriggerZoneGameState> triggerZones;
|
||||
std::unordered_map<std::string, bool> teleportZones;
|
||||
nlohmann::json dialogue;
|
||||
nlohmann::json scriptGlobals;
|
||||
};
|
||||
|
||||
struct GameSnapshot {
|
||||
std::uint64_t revision = 0;
|
||||
std::string campaignId;
|
||||
std::string currentLocationId;
|
||||
bool isDarklands = false;
|
||||
bool isNight = false;
|
||||
bool isDawn = false;
|
||||
float playerHp = 0.f;
|
||||
float playerMaxHp = 0.f;
|
||||
int money = 0;
|
||||
TutorialStep tutorialStep = TutorialStep::Completed;
|
||||
bool tutorialPhoneChatScreenOpened = false;
|
||||
bool tutorialJournalScreenOpened = false;
|
||||
bool tutorialNeedOpenTaxiScreen = false;
|
||||
bool tutorialTaxiHintDone = false;
|
||||
bool taxiIsCalled = false;
|
||||
bool chatUnread[3] = {};
|
||||
std::string chatPreview[3];
|
||||
std::vector<ChatMessageSnapshot> chatHistory[3];
|
||||
std::vector<std::string> inventoryItemIds;
|
||||
nlohmann::json questJournal;
|
||||
std::unordered_map<std::string, int> globalInts;
|
||||
std::unordered_map<std::string, float> globalFloats;
|
||||
std::unordered_map<std::string, CharacterSnapshot> players;
|
||||
std::unordered_map<std::string, LocationSnapshot> locations;
|
||||
std::vector<Quest::QuestState> quests;
|
||||
|
||||
bool isTutorialActive() const { return tutorialStep != TutorialStep::Completed; }
|
||||
};
|
||||
|
||||
struct SetWorldModeCommand { bool darklands; bool night; bool dawn; };
|
||||
struct SetCurrentLocationCommand { std::string locationId; };
|
||||
struct SetPlayerHealthCommand { float hp; float maxHp; };
|
||||
struct SetMoneyCommand { int amount; };
|
||||
struct SpendMoneyCommand { int amount; };
|
||||
struct CompleteTutorialStepCommand { TutorialStep step; };
|
||||
struct SetTutorialStepCommand { TutorialStep step; };
|
||||
struct SetTutorialFlagsCommand {
|
||||
bool phoneChatOpened;
|
||||
bool journalOpened;
|
||||
bool needTaxiHint;
|
||||
bool taxiHintDone;
|
||||
};
|
||||
struct SetChatCommand {
|
||||
int index;
|
||||
bool unread;
|
||||
std::string preview;
|
||||
};
|
||||
struct AppendChatMessageCommand {
|
||||
int index;
|
||||
std::string text;
|
||||
bool incoming;
|
||||
};
|
||||
struct SetTaxiCalledCommand { bool called; };
|
||||
struct StartTaxiCommand { std::string locationId; };
|
||||
struct AddInventoryItemCommand { std::string itemId; };
|
||||
struct RemoveInventoryItemCommand { std::string itemId; };
|
||||
struct SetGlobalIntCommand { std::string key; int value; };
|
||||
struct SetGlobalFloatCommand { std::string key; float value; };
|
||||
struct SetCharacterTransformCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
Eigen::Vector3f position;
|
||||
float facingAngle;
|
||||
};
|
||||
struct SetCharacterTargetCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
std::string targetId;
|
||||
};
|
||||
struct ClearCharacterAttackTargetCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct SetCharacterDestinationCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
Eigen::Vector3f target;
|
||||
std::string callbackName;
|
||||
};
|
||||
struct StopCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct FaceCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
std::string targetId;
|
||||
};
|
||||
struct ReplanCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct ResetCharacterAfterDeathCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct TryCharacterAttackComboCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
};
|
||||
struct SetCharacterHealthCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
float hp;
|
||||
};
|
||||
struct SetCharacterAnimationMetadataCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
AnimationState animationState;
|
||||
int startFrame;
|
||||
int frameCount;
|
||||
};
|
||||
struct DamageCharacterCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
float amount;
|
||||
std::string attackerId;
|
||||
};
|
||||
struct CommitCharacterStateCommand {
|
||||
std::string locationId;
|
||||
std::string characterId;
|
||||
CharacterState state;
|
||||
};
|
||||
struct SetInteractiveObjectActiveCommand {
|
||||
std::string locationId;
|
||||
std::string objectId;
|
||||
bool active;
|
||||
};
|
||||
struct SetTriggerZoneEnabledCommand {
|
||||
std::string locationId;
|
||||
std::string zoneId;
|
||||
bool enabled;
|
||||
};
|
||||
struct ReloadQuestDefinitionsCommand { std::string path; std::string zipFile; };
|
||||
|
||||
using GameCommand = std::variant<
|
||||
SetWorldModeCommand,
|
||||
SetCurrentLocationCommand,
|
||||
SetPlayerHealthCommand,
|
||||
SetMoneyCommand,
|
||||
SpendMoneyCommand,
|
||||
CompleteTutorialStepCommand,
|
||||
SetTutorialStepCommand,
|
||||
SetTutorialFlagsCommand,
|
||||
SetChatCommand,
|
||||
AppendChatMessageCommand,
|
||||
SetTaxiCalledCommand,
|
||||
StartTaxiCommand,
|
||||
AddInventoryItemCommand,
|
||||
RemoveInventoryItemCommand,
|
||||
SetGlobalIntCommand,
|
||||
SetGlobalFloatCommand,
|
||||
SetCharacterTransformCommand,
|
||||
SetCharacterTargetCommand,
|
||||
ClearCharacterAttackTargetCommand,
|
||||
SetCharacterDestinationCommand,
|
||||
StopCharacterCommand,
|
||||
FaceCharacterCommand,
|
||||
ReplanCharacterCommand,
|
||||
ResetCharacterAfterDeathCommand,
|
||||
TryCharacterAttackComboCommand,
|
||||
SetCharacterHealthCommand,
|
||||
SetCharacterAnimationMetadataCommand,
|
||||
DamageCharacterCommand,
|
||||
CommitCharacterStateCommand,
|
||||
SetInteractiveObjectActiveCommand,
|
||||
SetTriggerZoneEnabledCommand,
|
||||
ReloadQuestDefinitionsCommand>;
|
||||
|
||||
class IGameServer {
|
||||
public:
|
||||
virtual ~IGameServer() = default;
|
||||
|
||||
// Each returned object is immutable and remains valid after later commands.
|
||||
virtual std::shared_ptr<const GameSnapshot> snapshot() const = 0;
|
||||
virtual void submit(const GameCommand& command) = 0;
|
||||
virtual void update(std::int64_t deltaMs) = 0;
|
||||
|
||||
// Persistence stays server-side. The JSON shape is GameState's established
|
||||
// save schema and is intentionally not the wire snapshot shape.
|
||||
virtual void save(nlohmann::json& out) const = 0;
|
||||
virtual void load(const nlohmann::json& in) = 0;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,295 @@
|
||||
#include "InternalGameServer.h"
|
||||
#include "../Location.h"
|
||||
#include <algorithm>
|
||||
#include <type_traits>
|
||||
|
||||
namespace FRG {
|
||||
|
||||
InternalGameServer::InternalGameServer() = default;
|
||||
|
||||
GameSnapshot InternalGameServer::buildSnapshot() const
|
||||
{
|
||||
GameSnapshot result;
|
||||
result.revision = revision_;
|
||||
result.campaignId = state_.campaignId;
|
||||
result.currentLocationId = state_.currentLocationName;
|
||||
result.isDarklands = state_.isDarklands;
|
||||
result.isNight = state_.isNight;
|
||||
result.isDawn = state_.isDawn;
|
||||
result.playerHp = state_.playerHp;
|
||||
result.playerMaxHp = state_.playerMaxHp;
|
||||
result.money = state_.money;
|
||||
result.tutorialStep = state_.tutorialStep;
|
||||
result.tutorialPhoneChatScreenOpened = state_.tutorialPhoneChatScreenOpened;
|
||||
result.tutorialJournalScreenOpened = state_.tutorialJournalScreenOpened;
|
||||
result.tutorialNeedOpenTaxiScreen = state_.tutorialNeedOpenTaxiScreen;
|
||||
result.tutorialTaxiHintDone = state_.tutorialTaxiHintDone;
|
||||
result.taxiIsCalled = state_.taxiIsCalled;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
result.chatUnread[i] = state_.chatUnread[i];
|
||||
result.chatPreview[i] = state_.chatPreviewMsg[i];
|
||||
result.chatHistory[i].reserve(state_.chatHistory[i].size());
|
||||
for (const StoredChatMessage& message : state_.chatHistory[i])
|
||||
result.chatHistory[i].push_back({message.text, message.incoming});
|
||||
}
|
||||
|
||||
nlohmann::json inventoryJson;
|
||||
state_.inventory.save(inventoryJson);
|
||||
if (inventoryJson.contains("items") && inventoryJson["items"].is_array()) {
|
||||
for (const auto& item : inventoryJson["items"])
|
||||
if (item.is_string()) result.inventoryItemIds.push_back(item.get<std::string>());
|
||||
}
|
||||
state_.questJournal.save(result.questJournal);
|
||||
result.globalInts = state_.globalInts;
|
||||
result.globalFloats = state_.globalFloats;
|
||||
for (const Quest::QuestState* quest : state_.questJournal.getVisibleQuests())
|
||||
if (quest) result.quests.push_back(*quest);
|
||||
for (const auto& [playerId, source] : state_.players) {
|
||||
if (!source) continue;
|
||||
CharacterState clean = *source;
|
||||
result.players.emplace(playerId, CharacterSnapshot{playerId, std::move(clean)});
|
||||
}
|
||||
for (const auto& [locationId, source] : state_.locations) {
|
||||
LocationSnapshot location;
|
||||
location.id = locationId;
|
||||
location.state = *source.location;
|
||||
for (const auto& [id, sourceNpc] : source.npcs) {
|
||||
if (!sourceNpc) continue;
|
||||
CharacterState clean = *sourceNpc;
|
||||
location.npcs.emplace(
|
||||
id, CharacterSnapshot{id, std::move(clean)});
|
||||
}
|
||||
for (const auto& [id, sourceObject] : source.interactiveObjects) {
|
||||
if (!sourceObject) continue;
|
||||
InteractiveObjectState clean = *sourceObject;
|
||||
location.interactiveObjects.emplace(
|
||||
id, InteractiveObjectSnapshot{id, std::move(clean)});
|
||||
}
|
||||
location.triggerZones = source.triggerZones;
|
||||
location.teleportZones = source.teleportZones;
|
||||
location.dialogue = source.dialogue;
|
||||
location.scriptGlobals = source.scriptGlobals;
|
||||
result.locations.emplace(locationId, std::move(location));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::shared_ptr<const GameSnapshot> InternalGameServer::snapshot() const
|
||||
{
|
||||
if (!cachedSnapshot_ || cachedRevision_ != revision_) {
|
||||
cachedSnapshot_ = std::make_shared<const GameSnapshot>(buildSnapshot());
|
||||
cachedRevision_ = revision_;
|
||||
}
|
||||
return cachedSnapshot_;
|
||||
}
|
||||
|
||||
void InternalGameServer::submit(const GameCommand& command)
|
||||
{
|
||||
std::visit([this](const auto& value) {
|
||||
using T = std::decay_t<decltype(value)>;
|
||||
if constexpr (std::is_same_v<T, SetWorldModeCommand>) {
|
||||
state_.isDarklands = value.darklands;
|
||||
state_.isNight = value.night;
|
||||
state_.isDawn = value.dawn;
|
||||
} else if constexpr (std::is_same_v<T, SetCurrentLocationCommand>) {
|
||||
state_.currentLocationName = value.locationId;
|
||||
} else if constexpr (std::is_same_v<T, SetPlayerHealthCommand>) {
|
||||
state_.playerHp = value.hp;
|
||||
state_.playerMaxHp = value.maxHp;
|
||||
} else if constexpr (std::is_same_v<T, SetMoneyCommand>) {
|
||||
state_.money = value.amount;
|
||||
} else if constexpr (std::is_same_v<T, SpendMoneyCommand>) {
|
||||
state_.money -= value.amount;
|
||||
} else if constexpr (std::is_same_v<T, CompleteTutorialStepCommand>) {
|
||||
state_.completeTutorialStep(value.step);
|
||||
} else if constexpr (std::is_same_v<T, SetTutorialStepCommand>) {
|
||||
state_.tutorialStep = value.step;
|
||||
} else if constexpr (std::is_same_v<T, SetTutorialFlagsCommand>) {
|
||||
state_.tutorialPhoneChatScreenOpened = value.phoneChatOpened;
|
||||
state_.tutorialJournalScreenOpened = value.journalOpened;
|
||||
state_.tutorialNeedOpenTaxiScreen = value.needTaxiHint;
|
||||
state_.tutorialTaxiHintDone = value.taxiHintDone;
|
||||
} else if constexpr (std::is_same_v<T, SetChatCommand>) {
|
||||
if (value.index >= 0 && value.index < 3) {
|
||||
state_.chatUnread[value.index] = value.unread;
|
||||
state_.chatPreviewMsg[value.index] = value.preview;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, AppendChatMessageCommand>) {
|
||||
if (value.index >= 0 && value.index < 3) {
|
||||
if (state_.chatHistory[value.index].size() >= 5)
|
||||
state_.chatHistory[value.index].erase(state_.chatHistory[value.index].begin());
|
||||
state_.chatHistory[value.index].push_back({value.text, value.incoming});
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, SetTaxiCalledCommand>) {
|
||||
state_.taxiIsCalled = value.called;
|
||||
} else if constexpr (std::is_same_v<T, StartTaxiCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end())
|
||||
location->second.startTaxi();
|
||||
} else if constexpr (std::is_same_v<T, AddInventoryItemCommand>) {
|
||||
state_.inventory.addItem(value.itemId);
|
||||
} else if constexpr (std::is_same_v<T, RemoveInventoryItemCommand>) {
|
||||
state_.inventory.removeItem(value.itemId);
|
||||
} else if constexpr (std::is_same_v<T, SetGlobalIntCommand>) {
|
||||
state_.globalInts[value.key] = value.value;
|
||||
} else if constexpr (std::is_same_v<T, SetGlobalFloatCommand>) {
|
||||
state_.globalFloats[value.key] = value.value;
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterTransformCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) {
|
||||
character->position = value.position;
|
||||
character->facingAngle = value.facingAngle;
|
||||
character->targetFacingAngle = value.facingAngle;
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterTargetCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->attackTargetId = value.targetId;
|
||||
} else if constexpr (std::is_same_v<T, ClearCharacterAttackTargetCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->attackTargetId.clear();
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterDestinationCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character && location != state_.locations.end() &&
|
||||
location->second.location) {
|
||||
character->setTarget(
|
||||
value.target, value.callbackName,
|
||||
*location->second.location, value.characterId);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, StopCharacterCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->stopInPlace();
|
||||
} else if constexpr (std::is_same_v<T, FaceCharacterCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character)
|
||||
character->stopAndRotateToTarget(value.targetId);
|
||||
} else if constexpr (std::is_same_v<T, ReplanCharacterCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character && location != state_.locations.end() &&
|
||||
location->second.location) {
|
||||
character->forceReplan(
|
||||
*location->second.location, value.characterId);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, ResetCharacterAfterDeathCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->resetPlayerAfterDeath();
|
||||
} else if constexpr (std::is_same_v<T, TryCharacterAttackComboCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->tryAttackCombo();
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterHealthCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->setHp(value.hp);
|
||||
} else if constexpr (std::is_same_v<T, SetCharacterAnimationMetadataCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) {
|
||||
character->setAnimationMetadata(
|
||||
value.animationState, value.startFrame, value.frameCount);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, DamageCharacterCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) character->hp = (std::max)(0.f, character->hp - (std::max)(0.f, value.amount));
|
||||
} else if constexpr (std::is_same_v<T, CommitCharacterStateCommand>) {
|
||||
CharacterState* character =
|
||||
state_.findCharacter(value.locationId, value.characterId);
|
||||
if (character) *character = value.state;
|
||||
} else if constexpr (std::is_same_v<T, SetInteractiveObjectActiveCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location == state_.locations.end()) return;
|
||||
auto object = location->second.interactiveObjects.find(value.objectId);
|
||||
if (object != location->second.interactiveObjects.end() && object->second)
|
||||
object->second->isActive = value.active;
|
||||
} else if constexpr (std::is_same_v<T, SetTriggerZoneEnabledCommand>) {
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end()) {
|
||||
location->second.triggerZones[value.zoneId].enabled = value.enabled;
|
||||
auto runtime = runtimeLocations_.find(value.locationId);
|
||||
if (runtime != runtimeLocations_.end() && runtime->second) {
|
||||
for (auto& zone : runtime->second->triggerZones)
|
||||
if (zone.id == value.zoneId) zone.enabled = value.enabled;
|
||||
}
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, ReloadQuestDefinitionsCommand>) {
|
||||
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
|
||||
}
|
||||
}, command);
|
||||
markHostMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::update(std::int64_t deltaMs)
|
||||
{
|
||||
auto runtime = runtimeLocations_.find(state_.currentLocationName);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) return;
|
||||
auto stored = state_.locations.find(state_.currentLocationName);
|
||||
if (stored == state_.locations.end()) return;
|
||||
const bool interactiveObjectsLocked =
|
||||
stored->second.lockInteractiveObjectsDuringTutorial &&
|
||||
state_.tutorialStep < TutorialStep::PickUpItem;
|
||||
LocationSimulationEvents events =
|
||||
stored->second.update(deltaMs, state_.players, interactiveObjectsLocked);
|
||||
runtime->second->update(deltaMs, events);
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
}
|
||||
|
||||
void InternalGameServer::save(nlohmann::json& out) const
|
||||
{
|
||||
const_cast<InternalGameServer*>(this)->synchronizeRuntimeAuxiliaryState();
|
||||
state_.save(out);
|
||||
}
|
||||
|
||||
void InternalGameServer::load(const nlohmann::json& in)
|
||||
{
|
||||
state_.load(in);
|
||||
applyStateToRuntime();
|
||||
markHostMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location)
|
||||
{
|
||||
runtimeLocations_[id] = std::move(location);
|
||||
auto stored = state_.locations.find(id);
|
||||
if (stored != state_.locations.end() && stored->second.hasPersistentState) {
|
||||
runtimeLocations_[id]->applyAuxiliaryState(stored->second);
|
||||
} else {
|
||||
runtimeLocations_[id]->captureAuxiliaryState(state_.locations[id]);
|
||||
}
|
||||
markHostMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::synchronizeRuntimeAuxiliaryState()
|
||||
{
|
||||
for (const auto& [id, location] : runtimeLocations_) {
|
||||
if (!location) continue;
|
||||
location->captureAuxiliaryState(state_.locations[id]);
|
||||
}
|
||||
markHostMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::applyStateToRuntime()
|
||||
{
|
||||
for (const auto& [id, stored] : state_.locations) {
|
||||
auto runtime = runtimeLocations_.find(id);
|
||||
if (runtime == runtimeLocations_.end() || !runtime->second) continue;
|
||||
runtime->second->applyAuxiliaryState(stored);
|
||||
}
|
||||
}
|
||||
|
||||
void InternalGameServer::markHostMutation()
|
||||
{
|
||||
++revision_;
|
||||
cachedSnapshot_.reset();
|
||||
}
|
||||
|
||||
} // namespace FRG
|
||||
@@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "IGameServer.h"
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class Game;
|
||||
class Character;
|
||||
class Location;
|
||||
|
||||
// In-process implementation used by single-player. GameState exists exactly
|
||||
// once, here; clients see immutable snapshots and mutate it with typed commands.
|
||||
class InternalGameServer final : public IGameServer {
|
||||
public:
|
||||
InternalGameServer();
|
||||
|
||||
std::shared_ptr<const GameSnapshot> snapshot() const override;
|
||||
void submit(const GameCommand& command) override;
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
private:
|
||||
friend class Game;
|
||||
|
||||
// Transitional host operations are intentionally available only to Game,
|
||||
// which owns location loading and old-save migration. They are never part of
|
||||
// IGameServer and therefore cannot leak mutable state to UI/network clients.
|
||||
GameState& hostState() { return state_; }
|
||||
const GameState& hostState() const { return state_; }
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>>& runtimeLocations() { return runtimeLocations_; }
|
||||
const std::unordered_map<std::string, std::shared_ptr<Location>>& runtimeLocations() const { return runtimeLocations_; }
|
||||
void bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location);
|
||||
void synchronizeRuntimeAuxiliaryState();
|
||||
void applyStateToRuntime();
|
||||
void markHostMutation();
|
||||
|
||||
GameSnapshot buildSnapshot() const;
|
||||
|
||||
GameState state_;
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
|
||||
std::uint64_t revision_ = 1;
|
||||
mutable std::uint64_t cachedRevision_ = 0;
|
||||
mutable std::shared_ptr<const GameSnapshot> cachedSnapshot_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <Eigen/Geometry>
|
||||
#include "../Character.h"
|
||||
#include <SDL.h>
|
||||
#include <unordered_set>
|
||||
|
||||
|
||||
namespace FRG {
|
||||
@@ -91,6 +92,9 @@ namespace FRG {
|
||||
|
||||
for (const auto& item : j["objects"]) {
|
||||
InteractiveObjectData data;
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty())
|
||||
throw std::runtime_error("Interactive object id must not be empty in " + jsonPath);
|
||||
data.base = parseGameObjectData(item);
|
||||
data.interactionRadius = item.value("interactionRadius", 2.0f);
|
||||
data.approachRadius = item.value("approachRadius", data.interactionRadius);
|
||||
@@ -293,6 +297,7 @@ namespace FRG {
|
||||
std::vector<InteractiveObject> GameObjectLoader::loadAndCreateInteractiveObjects(
|
||||
const std::string& jsonPath,
|
||||
Renderer& renderer,
|
||||
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>>& states,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
std::vector<InteractiveObject> interactiveObjects;
|
||||
@@ -300,12 +305,35 @@ namespace FRG {
|
||||
if (jsonPath.empty())
|
||||
return interactiveObjects;
|
||||
|
||||
for (const auto& data : loadInteractiveFromJson(jsonPath, zipPath)) {
|
||||
const auto definitions = loadInteractiveFromJson(jsonPath, zipPath);
|
||||
std::unordered_set<std::string> configuredIds;
|
||||
for (const auto& data : definitions) {
|
||||
if (!configuredIds.insert(data.id).second)
|
||||
throw std::runtime_error(
|
||||
"GameObjectLoader: duplicate interactive object id '" +
|
||||
data.id + "' in " + jsonPath);
|
||||
}
|
||||
for (const auto& [id, state] : states)
|
||||
if (configuredIds.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"GameObjectLoader: saved interactive object id '" + id +
|
||||
"' is not present in " + jsonPath);
|
||||
for (const auto& data : definitions) {
|
||||
SDL_PumpEvents();
|
||||
logger() << "Loading interactive object: " << data.base.name << std::endl;
|
||||
try {
|
||||
InteractiveObject intObj = InteractiveObject::createFromState(
|
||||
data.toInteractiveObjectState(), renderer, zipPath);
|
||||
InteractiveObjectState definition = data.toInteractiveObjectState();
|
||||
auto& state = states[data.id];
|
||||
if (!state) state = std::make_shared<InteractiveObjectState>(definition);
|
||||
else {
|
||||
nlohmann::json runtime;
|
||||
state->save(runtime);
|
||||
*state = std::move(definition);
|
||||
state->load(runtime);
|
||||
}
|
||||
InteractiveObject intObj =
|
||||
InteractiveObject::createFromState(*state, renderer, zipPath);
|
||||
intObj.id = data.id;
|
||||
intObj.loadedObject.name = data.base.name;
|
||||
|
||||
if (!data.activateFunctionName.empty())
|
||||
@@ -338,7 +366,9 @@ namespace FRG {
|
||||
for (const auto& item : j["npcs"]) {
|
||||
NpcData data;
|
||||
|
||||
data.id = item.value("id", "npc_unknown");
|
||||
data.id = item.at("id").get<std::string>();
|
||||
if (data.id.empty())
|
||||
throw std::runtime_error("NPC id must not be empty in " + jsonPath);
|
||||
data.name = item.value("name", "Unknown NPC");
|
||||
data.texturePath = item.value("texturePath", "");
|
||||
if (item.contains("meshTextures") && item["meshTextures"].is_object()) {
|
||||
@@ -376,6 +406,7 @@ namespace FRG {
|
||||
logger() << "Successfully loaded " << npcs.size() << " NPCs from " << jsonPath << std::endl;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error loading NPCs from JSON: " << e.what() << std::endl;
|
||||
throw;
|
||||
}
|
||||
|
||||
return npcs;
|
||||
@@ -385,9 +416,10 @@ namespace FRG {
|
||||
CharacterState st;
|
||||
|
||||
// Identity + game state
|
||||
st.npcId = id;
|
||||
st.npcName = name;
|
||||
st.position = Eigen::Vector3f(positionX, positionY, positionZ);
|
||||
st.homePosition = Eigen::Vector3f(positionX, 0.f, positionZ);
|
||||
st.stopInPlace();
|
||||
st.facingAngle = facingAngle;
|
||||
st.walkSpeed = walkSpeed;
|
||||
st.rotationSpeed = rotationSpeed;
|
||||
@@ -396,6 +428,7 @@ namespace FRG {
|
||||
st.initialHp = hp;
|
||||
st.visibleRange = visibleRange;
|
||||
st.canAttack = canAttack;
|
||||
if (canAttack) st.attackTargetId = "player";
|
||||
st.enabled = enabled;
|
||||
st.interactionRadius = interactionRadius;
|
||||
|
||||
@@ -433,29 +466,51 @@ namespace FRG {
|
||||
std::vector<std::unique_ptr<Character>> GameObjectLoader::loadAndCreate_Npcs(
|
||||
const std::string& jsonPath,
|
||||
Renderer& renderer,
|
||||
std::unordered_map<std::string, std::shared_ptr<CharacterState>>& states,
|
||||
IGameServer& server,
|
||||
const std::string& locationId,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
std::vector<std::unique_ptr<Character>> npcs;
|
||||
|
||||
int index = 0;
|
||||
|
||||
for (const auto& npcData : loadNpcsFromJson(jsonPath, zipPath)) {
|
||||
const auto definitions = loadNpcsFromJson(jsonPath, zipPath);
|
||||
std::unordered_set<std::string> seenIds;
|
||||
for (const auto& definition : definitions) {
|
||||
const std::string& id = definition.id;
|
||||
if (id == "player" || !seenIds.insert(id).second)
|
||||
throw std::runtime_error(
|
||||
"GameObjectLoader: duplicate or reserved NPC id '" + id +
|
||||
"' in " + jsonPath);
|
||||
}
|
||||
for (const auto& [id, state] : states)
|
||||
if (seenIds.count(id) == 0)
|
||||
throw std::runtime_error(
|
||||
"GameObjectLoader: saved NPC id '" + id +
|
||||
"' is not present in " + jsonPath);
|
||||
for (const auto& npcData : definitions) {
|
||||
SDL_PumpEvents();
|
||||
logger() << "Loading NPC: " << npcData.name << std::endl;
|
||||
try {
|
||||
auto npc = Character::createFromState(npcData.toCharacterState(), renderer, zipPath.c_str());
|
||||
CharacterState definition = npcData.toCharacterState();
|
||||
auto& state = states[npcData.id];
|
||||
if (!state) state = std::make_shared<CharacterState>(definition);
|
||||
else {
|
||||
nlohmann::json runtime;
|
||||
state->save(runtime);
|
||||
*state = std::move(definition);
|
||||
state->load(runtime);
|
||||
}
|
||||
auto npc = Character::createFromState(
|
||||
*state, server, locationId, npcData.id, renderer, zipPath.c_str());
|
||||
if (npc) {
|
||||
logger() << "Successfully loaded NPC: " << npcData.name << " at ("
|
||||
<< npcData.positionX << ", " << npcData.positionY << ", " << npcData.positionZ << ")" << std::endl;
|
||||
npc->index = index;
|
||||
npcs.push_back(std::move(npc));
|
||||
index = index + 1;
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "GameObjectLoader: Failed to load NPC '" << npcData.name << "': " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
logger() << "Total NPCs loaded: " << npcs.size() << std::endl;
|
||||
return npcs;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "../CharacterState.h"
|
||||
|
||||
namespace FRG {
|
||||
class IGameServer;
|
||||
|
||||
class Character;
|
||||
|
||||
@@ -41,6 +42,7 @@ namespace FRG {
|
||||
};
|
||||
|
||||
struct InteractiveObjectData {
|
||||
std::string id;
|
||||
GameObjectData base;
|
||||
float interactionRadius = 2.0f;
|
||||
float approachRadius = 2.0f;
|
||||
@@ -105,13 +107,17 @@ namespace FRG {
|
||||
static std::vector<InteractiveObject> loadAndCreateInteractiveObjects(
|
||||
const std::string& jsonPath,
|
||||
Renderer& renderer,
|
||||
std::unordered_map<std::string, std::shared_ptr<InteractiveObjectState>>& states,
|
||||
const std::string& zipPath = ""
|
||||
);
|
||||
|
||||
static std::vector<std::unique_ptr<FRG::Character>> loadAndCreate_Npcs(
|
||||
const std::string& jsonPath,
|
||||
Renderer& renderer,
|
||||
const std::string& zipPath = ""
|
||||
std::unordered_map<std::string, std::shared_ptr<CharacterState>>& states,
|
||||
IGameServer& server,
|
||||
const std::string& locationId,
|
||||
const std::string& zipPath
|
||||
);
|
||||
|
||||
static std::vector<NpcData> loadNpcsFromJson(const std::string& jsonPath, const std::string& zipPath = "");
|
||||
|
||||
@@ -19,12 +19,11 @@ namespace FRG {
|
||||
using std::max;
|
||||
#endif
|
||||
|
||||
InteractiveObject InteractiveObject::createFromState(InteractiveObjectState st,
|
||||
InteractiveObject InteractiveObject::createFromState(InteractiveObjectState& st,
|
||||
Renderer& renderer,
|
||||
const std::string& zipPath)
|
||||
{
|
||||
InteractiveObject obj;
|
||||
obj.state = std::move(st);
|
||||
InteractiveObject obj(st);
|
||||
|
||||
const LoadedGameObjectState& info = obj.state.objectInfo;
|
||||
|
||||
@@ -75,115 +74,115 @@ namespace FRG {
|
||||
}
|
||||
|
||||
|
||||
void InteractiveObject::moveTo(const Eigen::Vector3f& target, float durationSec, std::function<void()> onComplete, std::string callbackName) {
|
||||
if (state.isAnimating) return;
|
||||
void InteractiveObjectState::moveTo(
|
||||
const Eigen::Vector3f& target, float durationSec, std::string onCompleteName) {
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Move;
|
||||
task.startPos = state.position;
|
||||
task.startRotY = state.rotationY;
|
||||
task.startScale = state.scale;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = scale;
|
||||
task.targetPos = target;
|
||||
task.targetRotY = state.rotationY;
|
||||
task.targetScale = state.scale;
|
||||
task.targetRotY = rotationY;
|
||||
task.targetScale = scale;
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.elapsedMs = 0.f;
|
||||
task.onComplete = std::move(onComplete);
|
||||
task.onCompleteName = std::move(callbackName);
|
||||
state.animTask = std::move(task);
|
||||
state.isAnimating = true;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
void InteractiveObject::rotateTo(float targetRotY, float durationSec, std::function<void()> onComplete, std::string callbackName) {
|
||||
if (state.isAnimating) return;
|
||||
void InteractiveObjectState::rotateTo(
|
||||
float targetRotY, float durationSec, std::string onCompleteName) {
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Rotate;
|
||||
task.startPos = state.position;
|
||||
task.startRotY = state.rotationY;
|
||||
task.startScale = state.scale;
|
||||
task.targetPos = state.position;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = scale;
|
||||
task.targetPos = position;
|
||||
task.targetRotY = targetRotY;
|
||||
task.targetScale = state.scale;
|
||||
task.targetScale = scale;
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.elapsedMs = 0.f;
|
||||
task.onComplete = std::move(onComplete);
|
||||
task.onCompleteName = std::move(callbackName);
|
||||
state.animTask = std::move(task);
|
||||
state.isAnimating = true;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
void InteractiveObject::scaleTo(float targetScale, float durationSec, std::function<void()> onComplete, std::string callbackName) {
|
||||
if (state.isAnimating) return;
|
||||
void InteractiveObjectState::scaleTo(
|
||||
float targetScale, float durationSec, std::string onCompleteName) {
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Scale;
|
||||
task.startPos = state.position;
|
||||
task.startRotY = state.rotationY;
|
||||
task.startScale = state.scale;
|
||||
task.targetPos = state.position;
|
||||
task.targetRotY = state.rotationY;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = scale;
|
||||
task.targetPos = position;
|
||||
task.targetRotY = rotationY;
|
||||
task.targetScale = targetScale;
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.elapsedMs = 0.f;
|
||||
task.onComplete = std::move(onComplete);
|
||||
task.onCompleteName = std::move(callbackName);
|
||||
state.animTask = std::move(task);
|
||||
state.isAnimating = true;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
void InteractiveObject::fadeTo(float targetAlpha, float durationSec, std::function<void()> onComplete, std::string callbackName) {
|
||||
if (state.isAnimating) return;
|
||||
void InteractiveObjectState::fadeTo(
|
||||
float targetAlpha, float durationSec, std::string onCompleteName) {
|
||||
if (isAnimating) return;
|
||||
AnimTask task;
|
||||
task.type = AnimTask::Type::Fade;
|
||||
task.startPos = state.position;
|
||||
task.startRotY = state.rotationY;
|
||||
task.startScale = state.alpha; // reuse startScale to hold start alpha
|
||||
task.targetPos = state.position;
|
||||
task.targetRotY = state.rotationY;
|
||||
task.startPos = position;
|
||||
task.startRotY = rotationY;
|
||||
task.startScale = alpha; // reuse startScale to hold start alpha
|
||||
task.targetPos = position;
|
||||
task.targetRotY = rotationY;
|
||||
task.targetScale = targetAlpha; // reuse targetScale to hold target alpha
|
||||
task.durationMs = durationSec * 1000.f;
|
||||
task.elapsedMs = 0.f;
|
||||
task.onComplete = std::move(onComplete);
|
||||
task.onCompleteName = std::move(callbackName);
|
||||
state.animTask = std::move(task);
|
||||
state.isAnimating = true;
|
||||
task.onCompleteName = std::move(onCompleteName);
|
||||
animTask = std::move(task);
|
||||
isAnimating = true;
|
||||
}
|
||||
|
||||
void InteractiveObject::update(int64_t deltaMs) {
|
||||
if (!state.isAnimating || !state.animTask) return;
|
||||
InteractiveObjectUpdateEvents InteractiveObjectState::update(std::int64_t deltaMs) {
|
||||
InteractiveObjectUpdateEvents events;
|
||||
if (!isAnimating || !animTask) return events;
|
||||
|
||||
AnimTask& task = *state.animTask;
|
||||
AnimTask& task = *animTask;
|
||||
task.elapsedMs += static_cast<float>(deltaMs);
|
||||
|
||||
const float t = min(task.elapsedMs / task.durationMs, 1.0f);
|
||||
const float t = (std::min)(task.elapsedMs / task.durationMs, 1.0f);
|
||||
|
||||
switch (task.type) {
|
||||
case AnimTask::Type::Move:
|
||||
state.position = task.startPos + (task.targetPos - task.startPos) * t;
|
||||
position = task.startPos + (task.targetPos - task.startPos) * t;
|
||||
break;
|
||||
case AnimTask::Type::Rotate:
|
||||
state.rotationY = task.startRotY + (task.targetRotY - task.startRotY) * t;
|
||||
rotationY = task.startRotY + (task.targetRotY - task.startRotY) * t;
|
||||
break;
|
||||
case AnimTask::Type::Scale:
|
||||
state.scale = task.startScale + (task.targetScale - task.startScale) * t;
|
||||
scale = task.startScale + (task.targetScale - task.startScale) * t;
|
||||
break;
|
||||
case AnimTask::Type::Fade:
|
||||
state.alpha = task.startScale + (task.targetScale - task.startScale) * t;
|
||||
alpha = task.startScale + (task.targetScale - task.startScale) * t;
|
||||
break;
|
||||
}
|
||||
|
||||
if (t >= 1.0f) {
|
||||
state.position = task.targetPos;
|
||||
state.rotationY = task.targetRotY;
|
||||
position = task.targetPos;
|
||||
rotationY = task.targetRotY;
|
||||
if (task.type == AnimTask::Type::Fade)
|
||||
state.alpha = task.targetScale;
|
||||
alpha = task.targetScale;
|
||||
else
|
||||
state.scale = task.targetScale;
|
||||
scale = task.targetScale;
|
||||
|
||||
std::function<void()> cb = std::move(task.onComplete);
|
||||
state.animTask.reset();
|
||||
state.isAnimating = false;
|
||||
|
||||
if (cb) cb();
|
||||
events.onCompleteName = std::move(task.onCompleteName);
|
||||
animTask.reset();
|
||||
isAnimating = false;
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
void InteractiveObject::drawDarklands(Renderer& renderer) const {
|
||||
@@ -295,7 +294,6 @@ namespace FRG {
|
||||
task.durationMs = at.value("durationMs", 1000.f);
|
||||
task.elapsedMs = at.value("elapsedMs", 0.f);
|
||||
task.onCompleteName = at.value("onCompleteName", std::string());
|
||||
// onComplete is non-serializable; re-wired at runtime by Lua script callbacks.
|
||||
animTask = std::move(task);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
#include <Eigen/Core>
|
||||
#include "render/Renderer.h"
|
||||
#include "InteractiveObjectState.h"
|
||||
@@ -21,25 +18,28 @@ namespace FRG {
|
||||
};
|
||||
|
||||
struct InteractiveObject {
|
||||
explicit InteractiveObject(InteractiveObjectState& boundState) : state(boundState) {}
|
||||
std::string id;
|
||||
LoadedGameObject loadedObject; // rendering assets
|
||||
InteractiveObjectState state; // all mutable + creation state
|
||||
|
||||
// Factory: creates a fully-loaded InteractiveObject from a state descriptor.
|
||||
// Loads textures and mesh from state.objectInfo, computes mesh centering,
|
||||
// and sets state.position = jsonPosition + meshCenter.
|
||||
static InteractiveObject createFromState(InteractiveObjectState st,
|
||||
static InteractiveObject createFromState(InteractiveObjectState& state,
|
||||
Renderer& renderer,
|
||||
const std::string& zipPath = "");
|
||||
|
||||
// callbackName, if given, is the global Lua function name backing onComplete —
|
||||
// stored in the AnimTask so it survives save/load (see ScriptEngine::rehydrateNamedCallback).
|
||||
void moveTo(const Eigen::Vector3f& target, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
|
||||
void rotateTo(float targetRotY, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
|
||||
void scaleTo(float targetScale, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
|
||||
void fadeTo(float targetAlpha, float durationSec, std::function<void()> onComplete = {}, std::string callbackName = "");
|
||||
void update(int64_t deltaMs);
|
||||
void draw(Renderer& renderer) const;
|
||||
void drawDarklands(Renderer& renderer) const;
|
||||
const std::string& entityId() const { return id; }
|
||||
|
||||
private:
|
||||
friend class GameObjectLoader;
|
||||
friend class Location;
|
||||
friend class LocationEditor;
|
||||
friend class ScriptEngine;
|
||||
friend class InternalGameServer;
|
||||
InteractiveObjectState& state; // non-owning binding to LocationGameState
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
@@ -1,7 +1,7 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
#include <functional>
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/Geometry>
|
||||
#include "external/nlohmann/json.hpp"
|
||||
@@ -21,10 +21,11 @@ struct AnimTask {
|
||||
float targetScale = 1.f;
|
||||
float durationMs = 1000.f;
|
||||
float elapsedMs = 0.f;
|
||||
std::function<void()> onComplete; // non-serializable; re-wired at runtime by Lua callbacks
|
||||
// Global Lua function name backing onComplete, if it was sourced from a script
|
||||
// (see ScriptEngine::rehydrateNamedCallback). Serialized so the callback can be
|
||||
// re-looked-up by name after a save/load; empty if none or non-Lua.
|
||||
// Global Lua function called when the animation completes. Empty means none.
|
||||
std::string onCompleteName;
|
||||
};
|
||||
|
||||
struct InteractiveObjectUpdateEvents {
|
||||
std::string onCompleteName;
|
||||
};
|
||||
|
||||
@@ -70,6 +71,12 @@ public:
|
||||
bool isAnimating = false;
|
||||
std::optional<AnimTask> animTask;
|
||||
|
||||
void moveTo(const Eigen::Vector3f& target, float durationSec, std::string onCompleteName = {});
|
||||
void rotateTo(float targetRotY, float durationSec, std::string onCompleteName = {});
|
||||
void scaleTo(float targetScale, float durationSec, std::string onCompleteName = {});
|
||||
void fadeTo(float targetAlpha, float durationSec, std::string onCompleteName = {});
|
||||
InteractiveObjectUpdateEvents update(std::int64_t deltaMs);
|
||||
|
||||
// --- Serialisation (runtime mutable fields only) ---
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
+4
-7
@@ -3,6 +3,7 @@
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include "../utils/Utils.h"
|
||||
|
||||
namespace FRG {
|
||||
@@ -44,13 +45,9 @@ namespace FRG {
|
||||
itemIds.clear();
|
||||
if (!in.contains("items") || !in["items"].is_array()) return;
|
||||
for (const auto& j : in["items"]) {
|
||||
if (j.is_string()) {
|
||||
itemIds.push_back(j.get<std::string>());
|
||||
} else if (j.is_object()) {
|
||||
// Backward-compat with old save files that stored full item structs.
|
||||
const std::string id = j.value("id", "");
|
||||
if (!id.empty()) itemIds.push_back(id);
|
||||
}
|
||||
if (!j.is_string())
|
||||
throw std::runtime_error("Inventory item IDs must be strings");
|
||||
itemIds.push_back(j.get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,12 +101,12 @@ namespace FRG {
|
||||
}
|
||||
|
||||
std::vector<Eigen::Vector3f> route;
|
||||
route.reserve(player.state.pathWaypoints.size() + 1);
|
||||
route.push_back(Eigen::Vector3f(player.state.position.x(), 0.0f, player.state.position.z()));
|
||||
route.reserve(player.characterState.pathWaypoints.size() + 1);
|
||||
route.push_back(Eigen::Vector3f(player.characterState.position.x(), 0.0f, player.characterState.position.z()));
|
||||
const size_t firstWaypoint = (std::min)(
|
||||
player.state.currentWaypointIndex, player.state.pathWaypoints.size());
|
||||
for (size_t i = firstWaypoint; i < player.state.pathWaypoints.size(); ++i) {
|
||||
const Eigen::Vector3f& waypoint = player.state.pathWaypoints[i];
|
||||
player.characterState.currentWaypointIndex, player.characterState.pathWaypoints.size());
|
||||
for (size_t i = firstWaypoint; i < player.characterState.pathWaypoints.size(); ++i) {
|
||||
const Eigen::Vector3f& waypoint = player.characterState.pathWaypoints[i];
|
||||
route.push_back(Eigen::Vector3f(waypoint.x(), 0.0f, waypoint.z()));
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ namespace FRG {
|
||||
constexpr float kPlayerClearanceMs = 20.0f;
|
||||
constexpr float kPlayerMaximumSpeedMultiplier = 1.5f;
|
||||
const float playerSpeed = (std::max)(
|
||||
player.state.walkSpeed * kPlayerMaximumSpeedMultiplier, 0.01f);
|
||||
player.characterState.walkSpeed * kPlayerMaximumSpeedMultiplier, 0.01f);
|
||||
const float playerRouteTimeMs = totalLength / playerSpeed * 1000.0f;
|
||||
const float revealDurationMs = (std::min)(kMaximumRevealMs, playerRouteTimeMs / 3.0f);
|
||||
const float waveSpeed = totalLength / (revealDurationMs / 1000.0f);
|
||||
|
||||
Reference in new issue
Block a user