More work on states, added bot

This commit is contained in:
Vladislav Khorev committed 2026-10-29 22:37:30 +06:00
1 parent 66e93d46f4
commit 5e9a663812
25 files changed
+1595 -740

No files matched your search

+85 -31
View File
@@ -12,7 +12,7 @@ The architecture has four layers:
1. `GameState` is the aggregate root for persistent, authoritative state. 1. `GameState` is the aggregate root for persistent, authoritative state.
2. `InternalGameServer` owns `GameState`, advances the local simulation, handles commands, and produces immutable snapshots. 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. 3. Runtime objects hold rendering resources. `Character` and `Location` read immutable server snapshots by stable ID, while `InteractiveObject` still uses a transitional non-owning binding 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`. 4. Client-facing systems such as `MenuManager` read immutable snapshots and submit typed commands through `IGameServer`.
```text ```text
@@ -47,6 +47,8 @@ IGameServer ---- immutable GameSnapshot ----> UI and other clients
- Inventory, money, player-health HUD values, quests, tutorial progress, chat data, taxi state, and script-global values. - 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`. Live `Location` pointers and rendering objects are no longer stored in `GameState`.
Embedded state models such as `Inventory` and `QuestJournal` contain only value
data and deterministic operations; they do not retain runtime callbacks.
### Campaign-level players ### Campaign-level players
@@ -54,6 +56,11 @@ Player `CharacterState` objects are stored in `GameState::players`. The current
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. 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.
Every player, including the one controlled by the current client, remains in this
same authoritative collection. `IGameServer::localPlayerId()` identifies which
entry is local to a particular client connection; it is not a separate state
object.
`LocationGameState` does not retain a player pointer or back-reference to `GameState`. `LocationGameState` does not retain a player pointer or back-reference to `GameState`.
Its update boundary receives `GameState::players` by reference, allowing character Its update boundary receives `GameState::players` by reference, allowing character
lookup and simulation to treat every campaign player by stable ID. lookup and simulation to treat every campaign player by stable ID.
@@ -70,7 +77,12 @@ Each location record owns:
- Serialized dialogue runtime state. - Serialized dialogue runtime state.
- Serialized Lua runtime globals. - 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. World-mode flags (`darklands`, `night`, and `dawn`) exist only at the aggregate
`GameState` level. Location rendering and scripts read those values from the
top-level snapshot rather than maintaining per-location mirrors.
State objects use `shared_ptr` internally to keep addresses stable for the
remaining local runtime bindings. These pointers never appear in public snapshots.
### Stable identity ### Stable identity
@@ -121,6 +133,7 @@ Snapshots have a monotonically increasing revision. `InternalGameServer` caches
- Chat history and unread state. - Chat history and unread state.
- Taxi state. - Taxi state.
- Inventory additions and removals. - Inventory additions and removals.
- Quest status, objective completion/visibility, and active-objective changes.
- Script globals. - Script globals.
- Character transforms, attack targets, navigation destinations, stopping, - Character transforms, attack targets, navigation destinations, stopping,
facing, replanning, health, damage, death reset, attack combos, and animation facing, replanning, health, damage, death reset, attack combos, and animation
@@ -131,6 +144,14 @@ Snapshots have a monotonically increasing revision. `InternalGameServer` caches
A future network implementation can serialize equivalent requests instead of mutating local state. A future network implementation can serialize equivalent requests instead of mutating local state.
### Presentation events
`IGameServer::drainEvents()` returns value-only events produced by successful
authoritative mutations. Inventory add/remove and quest unlock/complete/fail/
objective-complete events contain stable IDs only. `Game` drains these events and
performs local tutorial, toast, and audio presentation through `MenuManager`.
Loading and persistence restoration clear pending events and remain silent.
## Internal Server ## Internal Server
`InternalGameServer` is the current `IGameServer` implementation. It: `InternalGameServer` is the current `IGameServer` implementation. It:
@@ -143,10 +164,10 @@ A future network implementation can serialize equivalent requests instead of mut
- Builds and caches immutable snapshots. - Builds and caches immutable snapshots.
- Delegates save/load serialization to the aggregate state model. - Delegates save/load serialization to the aggregate state model.
`Game` remains a privileged host during this migration. It can access private `Game` remains a privileged lifecycle host, but it no longer holds or receives a
internal-server operations for campaign loading, location construction, callback mutable `GameState` reference. It uses private, focused internal-server operations
wiring, and persistence restoration. These host operations are not exposed by for campaign loading and location construction. Those operations publish revisions
`IGameServer`. themselves and are not exposed by `IGameServer`.
### Animation and combat ### Animation and combat
@@ -180,16 +201,25 @@ collision handling, and presentation-event processing.
## Runtime and Visual Object Bindings ## Runtime and Visual Object Bindings
`Location`, `Character`, and `InteractiveObject` no longer own their authoritative state objects. Runtime visual objects do not own their authoritative state objects.
- `Location` keeps a private non-owning reference to its `LocationState` and a stable location ID. - `Location` keeps only its `IGameServer` reference and stable location ID. It resolves its immutable `LocationState` view from `IGameServer::snapshot()`.
- `Character` keeps a private non-owning **const** reference named `characterState`, plus stable entity and location IDs. - `Character` keeps only its `IGameServer` reference plus stable entity and location IDs. It resolves the player-first character view from `IGameServer::snapshot()`.
- `InteractiveObject` keeps a private non-owning reference to its `InteractiveObjectState`; its configured object name is its stable ID. - `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 `Character::stateSnapshot()` and `Location::stateSnapshot()` return aliasing
server advances them directly before runtime presentation. Rendering reads the bound `shared_ptr` views. Each pointer addresses a state copy inside a `GameSnapshot`,
state; runtime input, scripts, and collision handling submit semantic commands that while shared ownership keeps that whole immutable snapshot alive. Update, input,
invoke `CharacterState` operations against the authoritative `LocationState`. editor, and draw operations capture one such pointer and use it consistently
throughout the operation. A missing or unloaded entity produces an empty pointer
and presentation or input work is skipped.
The authoritative state objects are owned by `GameState` or `LocationGameState`.
The internal server advances them before publishing a new snapshot revision.
Runtime input, scripts, and collision handling submit semantic commands that invoke
authoritative character and location operations. Location commands cover camera
orientation, pending interaction targets, navigation switching and area changes,
editor obstacles, and taxi reset. No whole-location commit command is exposed.
Navigation target selection, replanning, stopping, facing, death reset, health Navigation target selection, replanning, stopping, facing, death reset, health
assignment, and attack-combo clock changes are implemented by `CharacterState`. assignment, and attack-combo clock changes are implemented by `CharacterState`.
@@ -203,12 +233,19 @@ Arrival behavior is represented solely by the serialized Lua function name in
`CharacterState`; `Location` resolves and invokes that name through `ScriptEngine` `CharacterState`; `Location` resolves and invokes that name through `ScriptEngine`
when it consumes the arrival event. when it consumes the arrival event.
`Location` owns presentation-only runtime characters for all non-local entries in
`GameSnapshot::players`. It reconciles them by stable ID as snapshots change,
reuses the local-player assets and animation set, and includes them in all render,
shadow, lighting, animation, damage, and name-tag passes. The local entry is
excluded using `IGameServer::localPlayerId()` because its visual is already owned
by `Game`.
`GameObjectLoader` now: `GameObjectLoader` now:
1. Loads static creation data from location configuration. 1. Loads static creation data from location configuration.
2. Creates or reuses server-owned state objects. 2. Creates or reuses server-owned state objects.
3. Applies saved runtime fields over the configuration defaults. 3. Applies saved runtime fields over the configuration defaults.
4. Constructs visual objects bound to those state objects. 4. Constructs visual objects using stable IDs and state-backed creation data.
Configuration remains the source of visual assets, animation paths, meshes, textures, and fixed tuning data. Configuration remains the source of visual assets, animation paths, meshes, textures, and fixed tuning data.
@@ -225,11 +262,14 @@ The normal frame flow is:
back as semantic destination commands, whose server handler calls back as semantic destination commands, whose server handler calls
`CharacterState::setTarget()` with the authoritative `LocationState`; arrival `CharacterState::setTarget()` with the authoritative `LocationState`; arrival
function names are resolved through `ScriptEngine`. function names are resolved through `ScriptEngine`.
6. Runtime characters apply the authoritative animation frame to loaded models and 6. The server invalidates its cached snapshot and advances the revision so runtime
presentation can observe the completed simulation tick.
7. Runtime characters read one immutable snapshot per operation, apply the
authoritative animation frame to loaded models, and
play audio, particles, and presentation callbacks. play audio, particles, and presentation callbacks.
7. Auxiliary dialogue, Lua, trigger, and teleport state is captured into `LocationGameState`. 8. Auxiliary dialogue, Lua, trigger, and teleport state is captured into `LocationGameState`.
8. The server increments its revision and invalidates the cached snapshot. 9. The server publishes those auxiliary changes in a later revision.
9. Rendering reads the bound local state; UI and external-style consumers read an immutable snapshot. 10. Rendering, UI, and external-style consumers read immutable snapshots.
Commands that update bound character or object state do not reload the whole location. This avoids replaying dialogue-node effects or Lua callbacks. Commands that update bound character or object state do not reload the whole location. This avoids replaying dialogue-node effects or Lua callbacks.
@@ -244,7 +284,7 @@ When a campaign starts:
- Existing campaign state and runtime locations are cleared. - Existing campaign state and runtime locations are cleared.
- Quest definitions are loaded. - Quest definitions are loaded.
- A campaign-level player state is created under the `"player"` ID. - A campaign-level player state is created under the `"player"` ID.
- The visual player is created and bound to that state. - The visual player is created with its stable ID and resolves that state through snapshots.
### Location loading ### Location loading
@@ -254,6 +294,8 @@ When a location is loaded:
- Static configuration is loaded. - Static configuration is loaded.
- NPC and interactive-object visuals are created against state owned by the location record. - NPC and interactive-object visuals are created against state owned by the location record.
- The shared campaign player is attached. - The shared campaign player is attached.
- Runtime visuals are created for other player IDs currently present in the
snapshot and are subsequently spawned or removed as snapshots change.
- Dialogue, Lua, navigation, trigger, teleport, and presentation callbacks are initialized. - Dialogue, Lua, navigation, trigger, teleport, and presentation callbacks are initialized.
- The runtime `Location` is registered privately with `InternalGameServer`. - The runtime `Location` is registered privately with `InternalGameServer`.
@@ -278,8 +320,8 @@ unsupported versions are rejected rather than migrated.
- Pending interaction targets and character references use stable IDs. - Pending interaction targets and character references use stable IDs.
- Immutable content such as meshes, initial taxi configuration, and object - Immutable content such as meshes, initial taxi configuration, and object
definitions is reconstructed from campaign configuration. definitions is reconstructed from campaign configuration.
- Configuration is loaded first and mutable saved state is overlaid in place, so - Configuration is loaded first and mutable saved state is overlaid in place.
runtime render bindings keep stable addresses. Snapshot-backed runtime objects then resolve the resulting state by stable ID.
Old user save slots are intentionally unsupported. Bundled start-state snapshots Old user save slots are intentionally unsupported. Bundled start-state snapshots
must be regenerated with `Game::saveInitialState()` whenever the schema changes. must be regenerated with `Game::saveInitialState()` whenever the schema changes.
@@ -298,11 +340,14 @@ It now:
### Scripts ### Scripts
Lua remains an in-process single-player runtime. Character bindings submit semantic Lua remains an in-process single-player runtime. Character, inventory, quest, and
commands through `Location`; the server invokes `CharacterState` operations on the campaign-global bindings submit semantic commands through `Location`; the server
centrally owned state. NPC, interactive-object, trigger-zone, and teleport-zone invokes operations on centrally owned state. Snapshot queries provide global
bindings address entities by configured ID. Object, inventory, quest, and values, inventory counts, item presence, and synchronous quest validation for the
global-state bindings remain trusted local integrations. existing Lua API. NPC,
interactive-object, trigger-zone, and teleport-zone bindings address entities by
configured ID. Other object and global-state bindings remain trusted local
integrations.
Each location script registers initialization, enter, and exit callbacks. Each location script registers initialization, enter, and exit callbacks.
Initialization runs only when the location has no retained state; loading the Lua Initialization runs only when the location has no retained state; loading the Lua
@@ -313,13 +358,18 @@ The scalar Lua runtime state for each location is stored under `LocationGameStat
### Dialogue and cutscenes ### Dialogue and cutscenes
Dialogue runtime state is captured in each `LocationGameState`. Dialogue and cutscene rendering remain local presentation systems. Dialogue runtime state is captured in each `LocationGameState`. Dialogue global
flags use snapshot getter and command setter callbacks rather than retaining a
mutable map pointer. 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. 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 ### 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. These callback-free state systems remain part of `GameState` for single-player
compatibility. UI access, mutations, and presentation notifications cross the
server interface. They can remain empty or disabled in multiplayer mode as planned.
## Multiplayer Readiness ## Multiplayer Readiness
@@ -372,7 +422,11 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
location-local NPC ID). location-local NPC ID).
- Some top-level values remain mirrors for compatibility, notably player health used by the HUD and scripts. - 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. - 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. - `Game` uses focused privileged internal-server APIs for local content
construction, but cannot access the aggregate or its state maps directly. The
private location-construction path still supplies `LocationGameState` to the
local object loader. A remote client implementation will need a client-side
runtime-location factory driven by configuration and snapshots.
## Main Files ## Main Files
@@ -383,8 +437,8 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
- `src/Game.h` / `src/Game.cpp` — application host, campaign lifecycle, and runtime wiring. - `src/Game.h` / `src/Game.cpp` — application host, campaign lifecycle, and runtime wiring.
- `src/Location.h` / `src/Location.cpp` — loaded location runtime, command routing, - `src/Location.h` / `src/Location.cpp` — loaded location runtime, command routing,
interactions, collisions, scripts, and presentation-event consumption. interactions, collisions, scripts, and presentation-event consumption.
- `src/Character.h` / `src/Character.cpp` — passive character presentation bound to - `src/Character.h` / `src/Character.cpp` — passive character presentation resolving
a const authoritative `CharacterState`. immutable `CharacterState` snapshots by stable ID.
- `src/CharacterState.h` / `src/CharacterState.cpp` — deterministic character - `src/CharacterState.h` / `src/CharacterState.cpp` — deterministic character
simulation, navigation operations, combat state, and logical animation clock. simulation, navigation operations, combat state, and logical animation clock.
- `src/items/InteractiveObject.h` / `.cpp` — visual object runtime bound to central object state. - `src/items/InteractiveObject.h` / `.cpp` — visual object runtime bound to central object state.
+235 -138
View File
@@ -1,4 +1,4 @@
#include "Character.h" #include "Character.h"
#include "render/TextRenderer.h" #include "render/TextRenderer.h"
#include <cmath> #include <cmath>
#include <iostream> #include <iostream>
@@ -19,6 +19,44 @@ namespace FRG {
Dialogue::TranslationDatabase Character::nameTranslations; Dialogue::TranslationDatabase Character::nameTranslations;
std::shared_ptr<const CharacterState> Character::stateSnapshot() const {
auto snapshot = gameServer_.snapshot();
if (!snapshot) return {};
auto player = snapshot->players.find(entityId_);
if (player != snapshot->players.end()) {
return std::shared_ptr<const CharacterState>(
std::move(snapshot), &player->second.state);
}
auto location = snapshot->locations.find(locationId_);
if (location == snapshot->locations.end()) return {};
auto npc = location->second.npcs.find(entityId_);
if (npc == location->second.npcs.end()) return {};
return std::shared_ptr<const CharacterState>(
std::move(snapshot), &npc->second.state);
}
bool Character::isMoving() const {
auto state = stateSnapshot();
return state && state->isMoving();
}
Eigen::Vector3f Character::getRequestedWalkTarget() const {
auto state = stateSnapshot();
return state ? state->getRequestedWalkTarget() : Eigen::Vector3f::Zero();
}
Eigen::Vector3f Character::getCurrentNavigationTarget() const {
auto state = stateSnapshot();
return state ? state->getCurrentNavigationTarget() : Eigen::Vector3f::Zero();
}
float Character::getHp() const {
auto state = stateSnapshot();
return state ? state->getHp() : 0.f;
}
void Character::loadAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile) { void Character::loadAnimation(AnimationState animState, const std::string& filename, const std::string& zipFile) {
auto& data = animations[animState]; auto& data = animations[animState];
data.model.LoadFromFile(filename, zipFile); data.model.LoadFromFile(filename, zipFile);
@@ -46,7 +84,7 @@ void Character::loadBinaryAnimation(AnimationState animState, const std::string&
std::unique_ptr<Character> Character::createFromState(const CharacterState& st, IGameServer& server, std::unique_ptr<Character> Character::createFromState(const CharacterState& st, IGameServer& server,
const std::string& locationId, const std::string& entityId, const std::string& locationId, const std::string& entityId,
Renderer& renderer, const char* zipFile) { Renderer& renderer, const char* zipFile) {
auto c = std::make_unique<Character>(st, server, locationId, entityId); auto c = std::make_unique<Character>(server, locationId, entityId);
const std::string zip = zipFile ? zipFile : ""; const std::string zip = zipFile ? zipFile : "";
@@ -62,7 +100,7 @@ std::unique_ptr<Character> Character::createFromState(const CharacterState& st,
} }
}; };
for (const auto& [animState, path] : c->characterState.creationInfo.animationPaths) for (const auto& [animState, path] : st.creationInfo.animationPaths)
loadAnim(animState, path); loadAnim(animState, path);
auto loadTex = [&](const std::string& path) -> std::shared_ptr<Texture> { auto loadTex = [&](const std::string& path) -> std::shared_ptr<Texture> {
@@ -76,27 +114,27 @@ std::unique_ptr<Character> Character::createFromState(const CharacterState& st,
} }
}; };
if (!c->characterState.creationInfo.singleTexturePath.empty()) { if (!st.creationInfo.singleTexturePath.empty()) {
auto tex = loadTex(c->characterState.creationInfo.singleTexturePath); auto tex = loadTex(st.creationInfo.singleTexturePath);
if (tex) c->setTexture(tex); if (tex) c->setTexture(tex);
} }
for (const auto& [meshName, texPath] : c->characterState.creationInfo.meshTexturePaths) { for (const auto& [meshName, texPath] : st.creationInfo.meshTexturePaths) {
auto tex = loadTex(texPath); auto tex = loadTex(texPath);
if (tex) c->setTexture(meshName, tex); if (tex) c->setTexture(meshName, tex);
} }
if (!c->characterState.creationInfo.weaponMeshPath.empty()) { if (!st.creationInfo.weaponMeshPath.empty()) {
const std::string& wp = c->characterState.creationInfo.weaponMeshPath; const std::string& wp = st.creationInfo.weaponMeshPath;
try { try {
if (wp.size() >= 4 && wp.substr(wp.size() - 4) == ".bin") if (wp.size() >= 4 && wp.substr(wp.size() - 4) == ".bin")
c->weaponMesh.data = LoadModelFromBinFile(wp, zip); c->weaponMesh.data = LoadModelFromBinFile(wp, zip);
else else
c->weaponMesh.data = LoadFromTextFile02(wp, zip); c->weaponMesh.data = LoadFromTextFile02(wp, zip);
if (c->characterState.creationInfo.weaponMeshMove != Eigen::Vector3f::Zero()) if (st.creationInfo.weaponMeshMove != Eigen::Vector3f::Zero())
c->weaponMesh.data.Move(c->characterState.creationInfo.weaponMeshMove); c->weaponMesh.data.Move(st.creationInfo.weaponMeshMove);
if (c->characterState.creationInfo.weaponMeshScale != 1.0f) if (st.creationInfo.weaponMeshScale != 1.0f)
c->weaponMesh.data.Scale(c->characterState.creationInfo.weaponMeshScale); c->weaponMesh.data.Scale(st.creationInfo.weaponMeshScale);
c->weaponMesh.RefreshVBO(); c->weaponMesh.RefreshVBO();
} catch (const std::exception& e) { } catch (const std::exception& e) {
@@ -104,10 +142,10 @@ std::unique_ptr<Character> Character::createFromState(const CharacterState& st,
} }
} }
c->weaponTexture = loadTex(c->characterState.creationInfo.weaponTexturePath); c->weaponTexture = loadTex(st.creationInfo.weaponTexturePath);
c->weaponAttachBoneName = c->characterState.creationInfo.weaponAttachBoneName; c->weaponAttachBoneName = st.creationInfo.weaponAttachBoneName;
c->weaponInitialPosition = c->characterState.creationInfo.weaponInitialPosition; c->weaponInitialPosition = st.creationInfo.weaponInitialPosition;
c->weaponInitialRotation = c->characterState.creationInfo.weaponInitialRotation.toRotationMatrix(); c->weaponInitialRotation = st.creationInfo.weaponInitialRotation.toRotationMatrix();
return c; return c;
} }
@@ -124,8 +162,8 @@ void Character::setTexture(const std::string& meshName, std::shared_ptr<Texture>
meshTextures[meshName] = std::move(tex); meshTextures[meshName] = std::move(tex);
} }
AnimationState Character::resolveActiveState() const { AnimationState Character::resolveActiveState(const CharacterState& state) const {
if (animations.count(characterState.currentState)) return characterState.currentState; if (animations.count(state.currentState)) return state.currentState;
return AnimationState::STAND; return AnimationState::STAND;
} }
@@ -134,29 +172,33 @@ void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
hitSparkEmitter.update(static_cast<float>(deltaMs)); hitSparkEmitter.update(static_cast<float>(deltaMs));
} }
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (events) { if (events) {
if (events->deathAnimationComplete && onDeathAnimComplete) if (events->deathAnimationComplete && onDeathAnimComplete)
onDeathAnimComplete(); onDeathAnimComplete();
if (events->attackImpact && audioPlayer) { if (events->attackImpact && audioPlayer) {
audioPlayer->playSoundAsync(characterState.isPlayer audioPlayer->playSoundAsync(state.isPlayer
? "resources/audio/547041__cogfirestudios__hit-swing-sword-small-3.ogg" ? "resources/audio/547041__cogfirestudios__hit-swing-sword-small-3.ogg"
: "resources/audio/13925__adcbicycle__21.ogg"); : "resources/audio/13925__adcbicycle__21.ogg");
} }
} }
if (!characterState.enabled) return; if (!state.enabled) return;
auto it = animations.find(characterState.currentState); auto it = animations.find(state.currentState);
if (it == animations.end()) return; if (it == animations.end()) return;
auto& anim = it->second; auto& anim = it->second;
anim.currentFrame = (std::min)( anim.currentFrame = (std::min)(
characterState.animationFrame, static_cast<float>((std::max)(0, anim.totalFrames - 1))); state.animationFrame, static_cast<float>((std::max)(0, anim.totalFrames - 1)));
if (static_cast<int>(anim.currentFrame) != anim.lastFrame) { if (static_cast<int>(anim.currentFrame) != anim.lastFrame) {
anim.gpuSkinningShaderData.ComputeSkinningMatrices( anim.gpuSkinningShaderData.ComputeSkinningMatrices(
anim.model.startBones, anim.model.animations[0].keyFrames, anim.model.startBones, anim.model.animations[0].keyFrames,
static_cast<int>(anim.currentFrame)); static_cast<int>(anim.currentFrame));
if (!characterState.useGpuSkinning) { if (!state.useGpuSkinning) {
anim.model.Interpolate(static_cast<int>(anim.currentFrame)); anim.model.Interpolate(static_cast<int>(anim.currentFrame));
} }
anim.lastFrame = static_cast<int>(anim.currentFrame); anim.lastFrame = static_cast<int>(anim.currentFrame);
@@ -171,14 +213,17 @@ void Character::draw(Renderer& renderer) {
renderer.RenderUniform1f("uAlpha", 1.0f); renderer.RenderUniform1f("uAlpha", 1.0f);
glDisable(GL_BLEND); glDisable(GL_BLEND);
if (!characterState.enabled) return; auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (!state.enabled) return;
if (characterState.useGpuSkinning) { if (state.useGpuSkinning) {
drawGpuSkinning(renderer); drawGpuSkinning(renderer, state);
return; return;
} }
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return; if (it == animations.end()) return;
@@ -192,10 +237,10 @@ void Character::draw(Renderer& renderer) {
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
auto& anim = it->second; auto& anim = it->second;
for (const auto& name : anim.model.meshNamesOrdered) { for (const auto& name : anim.model.meshNamesOrdered) {
@@ -211,14 +256,14 @@ void Character::draw(Renderer& renderer) {
renderer.PopMatrix(); renderer.PopMatrix();
drawAttachedWeapon(renderer); drawAttachedWeapon(renderer, state);
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
} }
void Character::drawGpuSkinning(Renderer& renderer) { void Character::drawGpuSkinning(Renderer& renderer, const CharacterState& state) {
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return; if (it == animations.end()) return;
@@ -246,10 +291,10 @@ void Character::drawGpuSkinning(Renderer& renderer) {
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
static_cast<int>(anim.gpuSkinningShaderData.skinningMatrices.size()), false, static_cast<int>(anim.gpuSkinningShaderData.skinningMatrices.size()), false,
@@ -269,15 +314,15 @@ void Character::drawGpuSkinning(Renderer& renderer) {
renderer.shaderManager.PushShader("fog"); renderer.shaderManager.PushShader("fog");
renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkin); renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkin);
drawAttachedWeapon(renderer); drawAttachedWeapon(renderer, state);
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
} }
bool Character::prepareGpuSkinning() { bool Character::prepareGpuSkinning(const CharacterState& state) {
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return false; if (it == animations.end()) return false;
@@ -293,11 +338,11 @@ bool Character::prepareGpuSkinning() {
return true; return true;
} }
void Character::drawAttachedWeapon(Renderer& renderer) { void Character::drawAttachedWeapon(Renderer& renderer, const CharacterState& state) {
if (!characterState.showWeapon) return; if (!state.showWeapon) return;
if (weaponMesh.data.PositionData.size() == 0 || !weaponTexture) return; if (weaponMesh.data.PositionData.size() == 0 || !weaponTexture) return;
auto it = animations.find(resolveActiveState()); auto it = animations.find(resolveActiveState(state));
if (it == animations.end()) return; if (it == animations.end()) return;
auto& anim = it->second; auto& anim = it->second;
@@ -310,10 +355,10 @@ void Character::drawAttachedWeapon(Renderer& renderer) {
Eigen::Matrix4f currentBone = anim.gpuSkinningShaderData.skinningMatrices[boneIdx] * bindBone; Eigen::Matrix4f currentBone = anim.gpuSkinningShaderData.skinningMatrices[boneIdx] * bindBone;
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
renderer.TranslateMatrix(Eigen::Vector3f(currentBone.block<3, 1>(0, 3))); renderer.TranslateMatrix(Eigen::Vector3f(currentBone.block<3, 1>(0, 3)));
renderer.RotateMatrix(Eigen::Matrix3f(currentBone.block<3, 3>(0, 0))); renderer.RotateMatrix(Eigen::Matrix3f(currentBone.block<3, 3>(0, 0)));
renderer.TranslateMatrix(weaponInitialPosition); renderer.TranslateMatrix(weaponInitialPosition);
@@ -327,24 +372,27 @@ void Character::drawAttachedWeapon(Renderer& renderer) {
// ==================== Shadow depth pass ==================== // ==================== Shadow depth pass ====================
void Character::drawShadowDepth(Renderer& renderer) { void Character::drawShadowDepth(Renderer& renderer) {
if (!characterState.enabled) return; auto stateSnapshot = this->stateSnapshot();
if (characterState.useGpuSkinning) { if (!stateSnapshot) return;
drawShadowDepthGpuSkinning(renderer); const CharacterState& state = *stateSnapshot;
if (!state.enabled) return;
if (state.useGpuSkinning) {
drawShadowDepthGpuSkinning(renderer, state);
} else { } else {
drawShadowDepthCpu(renderer); drawShadowDepthCpu(renderer, state);
} }
} }
void Character::drawShadowDepthCpu(Renderer& renderer) { void Character::drawShadowDepthCpu(Renderer& renderer, const CharacterState& state) {
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return; if (it == animations.end()) return;
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
auto& anim = it->second; auto& anim = it->second;
for (const auto& name : anim.model.meshNamesOrdered) { for (const auto& name : anim.model.meshNamesOrdered) {
@@ -358,17 +406,17 @@ void Character::drawShadowDepthCpu(Renderer& renderer) {
} }
renderer.PopMatrix(); renderer.PopMatrix();
drawAttachedWeapon(renderer); drawAttachedWeapon(renderer, state);
} }
void Character::drawShadowDepthGpuSkinning(Renderer& renderer) { void Character::drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterState& state) {
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return; if (it == animations.end()) return;
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
if (!prepareGpuSkinning()) return; if (!prepareGpuSkinning(state)) return;
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
static const std::string shadowSkinningShader = "shadow_depth_skinning"; static const std::string shadowSkinningShader = "shadow_depth_skinning";
@@ -377,10 +425,10 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
renderer.shaderManager.PushShader(shadowSkinningShader); renderer.shaderManager.PushShader(shadowSkinningShader);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
@@ -400,14 +448,17 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
renderer.PopMatrix(); renderer.PopMatrix();
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
drawAttachedWeapon(renderer); drawAttachedWeapon(renderer, state);
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
} }
// ==================== Main pass with shadows ==================== // ==================== Main pass with shadows ====================
void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) { void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
if (!characterState.enabled) return; auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (!state.enabled) return;
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) { if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false); hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
} }
@@ -415,15 +466,15 @@ void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightF
renderer.RenderUniform1f("uAlpha", 1.0f); renderer.RenderUniform1f("uAlpha", 1.0f);
glDisable(GL_BLEND); glDisable(GL_BLEND);
if (characterState.useGpuSkinning) { if (state.useGpuSkinning) {
drawGpuSkinningWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera); drawGpuSkinningWithShadow(renderer, state, lightFromCamera, shadowMapTex, lightDirCamera);
} else { } else {
drawCpuWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera); drawCpuWithShadow(renderer, state, lightFromCamera, shadowMapTex, lightDirCamera);
} }
} }
void Character::drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) { void Character::drawCpuWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return; if (it == animations.end()) return;
@@ -446,10 +497,10 @@ void Character::drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lig
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
auto& anim = it->second; auto& anim = it->second;
for (const auto& name : anim.model.meshNamesOrdered) { for (const auto& name : anim.model.meshNamesOrdered) {
@@ -468,20 +519,20 @@ void Character::drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lig
renderer.shaderManager.PushShader("fog"); renderer.shaderManager.PushShader("fog");
renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosShadow); renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosShadow);
drawAttachedWeapon(renderer); drawAttachedWeapon(renderer, state);
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
} }
void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) { void Character::drawGpuSkinningWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return; if (it == animations.end()) return;
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
if (!prepareGpuSkinning()) return; if (!prepareGpuSkinning(state)) return;
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
static const std::string skinningShadowShader = "fog_skinning_shadow"; static const std::string skinningShadowShader = "fog_skinning_shadow";
@@ -508,10 +559,10 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
@@ -534,7 +585,7 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
renderer.shaderManager.PushShader("fog"); renderer.shaderManager.PushShader("fog");
renderer.RenderUniform1i(textureUniformName, 0); renderer.RenderUniform1i(textureUniformName, 0);
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkinShadow); renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkinShadow);
drawAttachedWeapon(renderer); drawAttachedWeapon(renderer, state);
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
@@ -554,7 +605,10 @@ void Character::drawNight(Renderer& renderer,
const Eigen::Matrix4f& cameraViewInverse, const Eigen::Matrix4f& cameraViewInverse,
const float* ambientColor, const float* fogColor) const float* ambientColor, const float* fogColor)
{ {
if (!characterState.enabled) return; auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (!state.enabled) return;
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) { if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false); hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
} }
@@ -562,25 +616,26 @@ void Character::drawNight(Renderer& renderer,
renderer.RenderUniform1f("uAlpha", 1.0f); renderer.RenderUniform1f("uAlpha", 1.0f);
glDisable(GL_BLEND); glDisable(GL_BLEND);
if (characterState.useGpuSkinning) { if (state.useGpuSkinning) {
drawNightGpuSkinning(renderer, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount, drawNightGpuSkinning(renderer, state, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor); pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor);
} else { } else {
drawNightCpu(renderer, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount, drawNightCpu(renderer, state, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor); pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor);
} }
} }
void Character::drawNightGpuSkinning(Renderer& renderer, void Character::drawNightGpuSkinning(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const float* ambientColor, const float* fogColor) const float* ambientColor, const float* fogColor)
{ {
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return; if (it == animations.end()) return;
if (!prepareGpuSkinning()) return; if (!prepareGpuSkinning(state)) return;
static const std::string nightSkinningShader = "night_fog_skinning"; static const std::string nightSkinningShader = "night_fog_skinning";
static const std::string boneMatricesUniform = "uBoneMatrices[0]"; static const std::string boneMatricesUniform = "uBoneMatrices[0]";
@@ -606,10 +661,10 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false, static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
@@ -639,7 +694,7 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ); renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ);
renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ); renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ);
} }
drawAttachedWeapon(renderer); drawAttachedWeapon(renderer, state);
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
@@ -647,11 +702,12 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
} }
void Character::drawNightCpu(Renderer& renderer, void Character::drawNightCpu(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const float* ambientColor, const float* fogColor) const float* ambientColor, const float* fogColor)
{ {
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return; if (it == animations.end()) return;
@@ -677,10 +733,10 @@ void Character::drawNightCpu(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
auto& anim = it->second; auto& anim = it->second;
for (const auto& name : anim.model.meshNamesOrdered) { for (const auto& name : anim.model.meshNamesOrdered) {
@@ -697,7 +753,7 @@ void Character::drawNightCpu(Renderer& renderer,
renderer.PopMatrix(); renderer.PopMatrix();
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight); renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight);
drawAttachedWeapon(renderer); drawAttachedWeapon(renderer, state);
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
@@ -712,35 +768,39 @@ void Character::drawNightWithShadow(Renderer& renderer,
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
const float* ambientColor, const float* fogColor) const float* ambientColor, const float* fogColor)
{ {
if (!characterState.enabled) return; auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (!state.enabled) return;
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) { if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false); hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
} }
renderer.RenderUniform1f("uAlpha", 1.0f); renderer.RenderUniform1f("uAlpha", 1.0f);
glDisable(GL_BLEND); glDisable(GL_BLEND);
if (characterState.useGpuSkinning) { if (state.useGpuSkinning) {
drawNightGpuSkinningWithShadow(renderer, plPosEye, plDirEye, plColors, plCount, drawNightGpuSkinningWithShadow(renderer, state, plPosEye, plDirEye, plColors, plCount,
plWorldXZ, plLimitXZ, cameraViewInverse, plWorldXZ, plLimitXZ, cameraViewInverse,
lightFromCamera, shadowMapTex, ambientColor, fogColor); lightFromCamera, shadowMapTex, ambientColor, fogColor);
} else { } else {
drawNightCpuWithShadow(renderer, plPosEye, plDirEye, plColors, plCount, drawNightCpuWithShadow(renderer, state, plPosEye, plDirEye, plColors, plCount,
plWorldXZ, plLimitXZ, cameraViewInverse, plWorldXZ, plLimitXZ, cameraViewInverse,
lightFromCamera, shadowMapTex, ambientColor, fogColor); lightFromCamera, shadowMapTex, ambientColor, fogColor);
} }
} }
void Character::drawNightGpuSkinningWithShadow(Renderer& renderer, void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const float* plWorldXZ, const float* plLimitXZ,
const Eigen::Matrix4f& cameraViewInverse, const Eigen::Matrix4f& cameraViewInverse,
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
const float* ambientColor, const float* fogColor) const float* ambientColor, const float* fogColor)
{ {
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return; if (it == animations.end()) return;
if (!prepareGpuSkinning()) return; if (!prepareGpuSkinning(state)) return;
static const std::string nightShadowSkinningShader = "night_fog_skinning_shadow"; static const std::string nightShadowSkinningShader = "night_fog_skinning_shadow";
static const std::string boneMatricesUniform = "uBoneMatrices[0]"; static const std::string boneMatricesUniform = "uBoneMatrices[0]";
@@ -774,10 +834,10 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform, renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false, static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
@@ -810,7 +870,7 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
} }
renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ); renderer.RenderUniform2fvArray("uPointLightWorldXZ[0]", plCount, plWorldXZ);
renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ); renderer.RenderUniform2fvArray("uPointLightLimitXZ[0]", plCount, plLimitXZ);
drawAttachedWeapon(renderer); drawAttachedWeapon(renderer, state);
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
@@ -818,13 +878,14 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
} }
void Character::drawNightCpuWithShadow(Renderer& renderer, void Character::drawNightCpuWithShadow(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const float* plWorldXZ, const float* plLimitXZ,
const Eigen::Matrix4f& cameraViewInverse, const Eigen::Matrix4f& cameraViewInverse,
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
const float* ambientColor, const float* fogColor) const float* ambientColor, const float* fogColor)
{ {
AnimationState drawState = resolveActiveState(); AnimationState drawState = resolveActiveState(state);
auto it = animations.find(drawState); auto it = animations.find(drawState);
if (it == animations.end()) return; if (it == animations.end()) return;
@@ -858,10 +919,10 @@ void Character::drawNightCpuWithShadow(Renderer& renderer,
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR); Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
renderer.PushMatrix(); renderer.PushMatrix();
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() }); renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
renderer.ScaleMatrix(characterState.modelScale); renderer.ScaleMatrix(state.modelScale);
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix()); renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
auto& anim = it->second; auto& anim = it->second;
for (const auto& name : anim.model.meshNamesOrdered) { for (const auto& name : anim.model.meshNamesOrdered) {
@@ -878,7 +939,7 @@ void Character::drawNightCpuWithShadow(Renderer& renderer,
renderer.PopMatrix(); renderer.PopMatrix();
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow); renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow);
drawAttachedWeapon(renderer); drawAttachedWeapon(renderer, state);
renderer.PopProjectionMatrix(); renderer.PopProjectionMatrix();
renderer.shaderManager.PopShader(); renderer.shaderManager.PopShader();
@@ -906,8 +967,11 @@ void Character::presentDamage(float hp, float maxHp,
onHpChanged(hp, maxHp, attackerId); onHpChanged(hp, maxHp, attackerId);
if (!hitSparkEmitter.isConfigured()) return; if (!hitSparkEmitter.isConfigured()) return;
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
Eigen::Vector3f emitPos = characterState.position + Eigen::Vector3f(0.f, 1.f, 0.f); Eigen::Vector3f emitPos = state.position + Eigen::Vector3f(0.f, 1.f, 0.f);
hitSparkEmitter.resetEmissionPoints({ emitPos }); hitSparkEmitter.resetEmissionPoints({ emitPos });
hitSparkEmitter.setEmissionDirection(attackDirection); hitSparkEmitter.setEmissionDirection(attackDirection);
@@ -924,16 +988,18 @@ void Character::prepareHitSparksForDraw(const Eigen::Matrix4f& cameraViewMatrix)
hitSparkEmitter.prepareForDraw(cameraViewMatrix); hitSparkEmitter.prepareForDraw(cameraViewMatrix);
} }
void Character::drawName(TextRenderer& textRenderer, static void drawCharacterNameLabel(
const Eigen::Matrix4f& cameraViewMatrix, TextRenderer& textRenderer,
const Eigen::Matrix4f& projectionMatrix) const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix,
const CharacterState& state,
const std::string& displayName,
const std::array<float, 4>& color)
{ {
if (characterState.isPlayer) return; if (!state.enabled || state.hp <= 0.f || displayName.empty()) return;
if (!characterState.enabled) return;
if (characterState.hp <= 0.f) return;
if (characterState.npcName.empty()) return;
Eigen::Vector4f worldPos(characterState.position.x(), characterState.position.y() + 1.83f, characterState.position.z(), 1.f); Eigen::Vector4f worldPos(
state.position.x(), state.position.y() + 1.83f, state.position.z(), 1.f);
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos; Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
if (clip.w() <= 0.f) return; if (clip.w() <= 0.f) return;
@@ -943,26 +1009,57 @@ void Character::drawName(TextRenderer& textRenderer,
float screenX = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth; float screenX = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
float screenY = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight; float screenY = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;
textRenderer.drawText(displayName, screenX, screenY, 1.0f, true, color);
}
std::array<float, 4> color = characterState.canAttack void Character::drawName(TextRenderer& textRenderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix)
{
auto stateSnapshot = this->stateSnapshot();
if (!stateSnapshot) return;
const CharacterState& state = *stateSnapshot;
if (state.isPlayer) return;
if (state.npcName.empty()) return;
std::array<float, 4> color = state.canAttack
? std::array<float, 4>{ 1.f, 0.f, 0.f, 1.f } ? std::array<float, 4>{ 1.f, 0.f, 0.f, 1.f }
: std::array<float, 4>{ 0.f, 1.f, 0.f, 1.f }; : std::array<float, 4>{ 0.f, 1.f, 0.f, 1.f };
const std::string& translatedName = Character::nameTranslations.translateRef(characterState.npcName, g_currentLanguage); const std::string& translatedName = Character::nameTranslations.translateRef(state.npcName, g_currentLanguage);
textRenderer.drawText(translatedName, screenX, screenY, 1.0f, true, color); drawCharacterNameLabel(
textRenderer, cameraViewMatrix, projectionMatrix,
state, translatedName, color);
}
void Character::drawPlayerName(
TextRenderer& textRenderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix,
const std::string& displayName)
{
auto snapshot = stateSnapshot();
if (!snapshot) return;
static const std::array<float, 4> color = {0.f, 1.f, 0.f, 1.f};
drawCharacterNameLabel(
textRenderer, cameraViewMatrix, projectionMatrix,
*snapshot, displayName, color);
} }
void Character::drawHealthBar(Renderer& renderer, void Character::drawHealthBar(Renderer& renderer,
const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix) const Eigen::Matrix4f& projectionMatrix)
{ {
if (characterState.isPlayer || !characterState.canAttack) return; auto stateSnapshot = this->stateSnapshot();
if (!characterState.enabled) return; if (!stateSnapshot) return;
if (characterState.hp <= 0.f) return; const CharacterState& state = *stateSnapshot;
if (characterState.initialHp <= 0.f) return; if (state.isPlayer || !state.canAttack) return;
if (!state.enabled) return;
if (state.hp <= 0.f) return;
if (state.initialHp <= 0.f) return;
Eigen::Vector4f worldPos(characterState.position.x(), characterState.position.y() + 1.75f, characterState.position.z(), 1.f); Eigen::Vector4f worldPos(state.position.x(), state.position.y() + 1.75f, state.position.z(), 1.f);
Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos; Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
if (clip.w() <= 0.f) return; if (clip.w() <= 0.f) return;
float ndcX = clip.x() / clip.w(); float ndcX = clip.x() / clip.w();
@@ -980,7 +1077,7 @@ void Character::drawHealthBar(Renderer& renderer,
const float bottom = sy - barH * 0.5f; const float bottom = sy - barH * 0.5f;
const float top = sy + barH * 0.5f; const float top = sy + barH * 0.5f;
float frac = characterState.hp / characterState.initialHp; float frac = state.hp / state.initialHp;
if (frac < 0.f) frac = 0.f; if (frac < 0.f) frac = 0.f;
if (frac > 1.f) frac = 1.f; if (frac > 1.f) frac = 1.f;
const float split = left + barW * frac; const float split = left + barW * frac;
+27 -18
View File
@@ -22,12 +22,14 @@ class IGameServer;
class Character { class Character {
public: public:
Character(const CharacterState& boundState, IGameServer& server, Character(IGameServer& server, std::string locationId, std::string entityId)
std::string locationId, std::string entityId) : gameServer_(server), locationId_(std::move(locationId)),
: characterState(boundState), gameServer_(server), entityId_(std::move(entityId)) {}
locationId_(std::move(locationId)), entityId_(std::move(entityId)){}
const std::string& entityId() const { return entityId_; } const std::string& entityId() const { return entityId_; }
void bindToLocation(std::string locationId) { locationId_ = std::move(locationId); } void bindToLocation(std::string locationId) { locationId_ = std::move(locationId); }
// The aliasing shared_ptr keeps the owning GameSnapshot alive for as long
// as the returned CharacterState is being read.
std::shared_ptr<const CharacterState> stateSnapshot() const;
static constexpr float kAttackRange = 1.25f; static constexpr float kAttackRange = 1.25f;
// ---- Asset loading ---- // ---- Asset loading ----
@@ -63,6 +65,10 @@ public:
void drawName(TextRenderer& textRenderer, void drawName(TextRenderer& textRenderer,
const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix); const Eigen::Matrix4f& projectionMatrix);
void drawPlayerName(TextRenderer& textRenderer,
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix,
const std::string& displayName);
void drawHealthBar(Renderer& renderer, void drawHealthBar(Renderer& renderer,
const Eigen::Matrix4f& cameraViewMatrix, const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix); const Eigen::Matrix4f& projectionMatrix);
@@ -84,11 +90,11 @@ public:
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
const float* ambientColor, const float* fogColor); const float* ambientColor, const float* fogColor);
// ---- Query helpers (delegate to state) ---- // ---- Query helpers (read immutable snapshots) ----
bool isMoving() const { return characterState.isMoving(); } bool isMoving() const;
const Eigen::Vector3f& getRequestedWalkTarget() const { return characterState.getRequestedWalkTarget(); } Eigen::Vector3f getRequestedWalkTarget() const;
Eigen::Vector3f getCurrentNavigationTarget() const { return characterState.getCurrentNavigationTarget(); } Eigen::Vector3f getCurrentNavigationTarget() const;
float getHp() const { return characterState.getHp(); } float getHp() const;
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ---- // ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
VertexRenderStruct weaponMesh; VertexRenderStruct weaponMesh;
@@ -109,7 +115,6 @@ private:
friend class LocationEditor; friend class LocationEditor;
friend class PathVisualization; friend class PathVisualization;
friend class InternalGameServer; friend class InternalGameServer;
const CharacterState& characterState; // private non-owning binding to server-owned storage
IGameServer& gameServer_; IGameServer& gameServer_;
std::string locationId_; std::string locationId_;
std::string entityId_; std::string entityId_;
@@ -120,28 +125,32 @@ private:
VertexRenderStruct healthBarMesh; VertexRenderStruct healthBarMesh;
std::unordered_map<std::string, std::shared_ptr<Texture>> meshTextures; std::unordered_map<std::string, std::shared_ptr<Texture>> meshTextures;
AnimationState resolveActiveState() const; AnimationState resolveActiveState(const CharacterState& state) const;
bool prepareGpuSkinning(); bool prepareGpuSkinning(const CharacterState& state);
void drawAttachedWeapon(Renderer& renderer); void drawAttachedWeapon(Renderer& renderer, const CharacterState& state);
void drawGpuSkinning(Renderer& renderer); void drawGpuSkinning(Renderer& renderer, const CharacterState& state);
void drawShadowDepthGpuSkinning(Renderer& renderer); void drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterState& state);
void drawShadowDepthCpu(Renderer& renderer); void drawShadowDepthCpu(Renderer& renderer, const CharacterState& state);
void drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); void drawGpuSkinningWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
void drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera); void drawCpuWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera);
void drawNightGpuSkinning(Renderer& renderer, void drawNightGpuSkinning(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const float* ambientColor, const float* fogColor); const float* ambientColor, const float* fogColor);
void drawNightCpu(Renderer& renderer, void drawNightCpu(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const float* ambientColor, const float* fogColor); const float* ambientColor, const float* fogColor);
void drawNightGpuSkinningWithShadow(Renderer& renderer, void drawNightGpuSkinningWithShadow(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor); const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
void drawNightCpuWithShadow(Renderer& renderer, void drawNightCpuWithShadow(Renderer& renderer,
const CharacterState& state,
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount, const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse, const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor); const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
+11
View File
@@ -12,6 +12,17 @@ constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
constexpr float PI = 3.14159265358979323846f; constexpr float PI = 3.14159265358979323846f;
} }
void CharacterState::configureAsPlayer() {
walkSpeed = 3.0f;
rotationSpeed = 8.0f;
modelScale = 1.f;
modelCorrectionRotation = Eigen::Quaternionf(
Eigen::AngleAxisf(PI, Eigen::Vector3f::UnitY()) *
Eigen::AngleAxisf(-PI * 0.5f, Eigen::Vector3f::UnitX()));
canAttack = true;
isPlayer = true;
}
bool CharacterState::isMoving() const { bool CharacterState::isMoving() const {
Eigen::Vector3f toTarget = walkTarget - position; Eigen::Vector3f toTarget = walkTarget - position;
toTarget.y() = 0.f; toTarget.y() = 0.f;
+1
View File
@@ -135,6 +135,7 @@ public:
CharacterCreationInfo creationInfo; CharacterCreationInfo creationInfo;
// --- Pure-state methods --- // --- Pure-state methods ---
void configureAsPlayer();
bool isMoving() const; bool isMoving() const;
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; } const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; }
Eigen::Vector3f getCurrentNavigationTarget() const; Eigen::Vector3f getCurrentNavigationTarget() const;
+168 -171
View File
@@ -13,6 +13,7 @@
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <functional> #include <functional>
#include <type_traits>
#ifdef __ANDROID__ #ifdef __ANDROID__
#include <android/log.h> #include <android/log.h>
#endif #endif
@@ -76,16 +77,14 @@ namespace FRG
#endif #endif
Location* Game::currentLocation() const { Location* Game::currentLocation() const {
if (gameState.currentLocationName.empty()) return nullptr; const auto state = stateSnapshot();
const auto& locations = internalGameServer.runtimeLocations(); if (!state || state->currentLocationId.empty()) return nullptr;
auto it = locations.find(gameState.currentLocationName); return internalGameServer.runtimeLocation(state->currentLocationId);
return (it != locations.end()) ? it->second.get() : nullptr;
} }
Game::Game() Game::Game()
: audioPlayer(std::make_unique<AudioPlayerAsync>()) : audioPlayer(std::make_unique<AudioPlayerAsync>())
, internalGameServer() , internalGameServer()
, gameState(internalGameServer.hostState())
, menuManager(renderer, internalGameServer, *audioPlayer) , menuManager(renderer, internalGameServer, *audioPlayer)
, newTickCount(0) , newTickCount(0)
, lastTickCount(0) , lastTickCount(0)
@@ -360,19 +359,6 @@ namespace FRG
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE); CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
loadSteps.push([this]() { loadSteps.push([this]() {
// Wire inventory callbacks: tutorial tracking + toast notifications.
gameState.inventory.onItemAdded = [this](const std::string& itemId) {
menuManager.onItemPickedUp(itemId);
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) menuManager.showToast(menuManager.toastTextureItemAdded, item->name);
audioPlayer->playSoundAsync("resources/audio/831642__ienba__generic-game-pick-up.ogg");
};
gameState.inventory.onItemRemoved = [this](const std::string& itemId) {
const Item* item = ItemRegistry::instance().findById(itemId);
if (item) menuManager.showToast(menuManager.toastTextureItemRemoved, item->name);
audioPlayer->playSoundAsync("resources/audio/831642__ienba__generic-game-pick-up.ogg");
};
// Wire phone dialogue start function so MenuManager can trigger dialogues. // Wire phone dialogue start function so MenuManager can trigger dialogues.
menuManager.startDialogueFunc = [this](const std::string& id) { menuManager.startDialogueFunc = [this](const std::string& id) {
if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id); if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id);
@@ -399,18 +385,6 @@ namespace FRG
loadSteps.push([this]() { loadSteps.push([this]() {
try { try {
menuManager.setup(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; logger() << "UI loaded successfully" << std::endl;
} }
catch (const std::exception& e) { catch (const std::exception& e) {
@@ -425,7 +399,8 @@ namespace FRG
performResetToInitialState(); performResetToInitialState();
applyTutorialSetting(); applyTutorialSetting();
const CampaignConfig& campaign = activeCampaign(); const CampaignConfig& campaign = activeCampaign();
gameState.currentLocationName = campaign.startLocation; internalGameServer.submit(
SetCurrentLocationCommand{campaign.startLocation});
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback(); if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback();
audioPlayer->crossFadeMusicAsync(campaign.startMusic); audioPlayer->crossFadeMusicAsync(campaign.startMusic);
@@ -516,7 +491,10 @@ namespace FRG
} }
const CampaignConfig& Game::activeCampaign() const { const CampaignConfig& Game::activeCampaign() const {
const CampaignConfig* campaign = CampaignRegistry::instance().find(gameState.campaignId); const auto state = stateSnapshot();
const CampaignConfig* campaign = state
? CampaignRegistry::instance().find(state->campaignId)
: nullptr;
if (!campaign) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId); if (!campaign) campaign = CampaignRegistry::instance().find(CampaignRegistry::kDefaultCampaignId);
if (!campaign) { if (!campaign) {
static const CampaignConfig empty; static const CampaignConfig empty;
@@ -527,7 +505,7 @@ namespace FRG
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const { bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
return player && loadedCampaignId == campaignId return player && loadedCampaignId == campaignId
&& internalGameServer.runtimeLocations().count(locationId) > 0; && internalGameServer.hasRuntimeLocation(locationId);
} }
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId, void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
@@ -567,15 +545,15 @@ namespace FRG
void Game::loadCampaign(const std::string& campaignId) { void Game::loadCampaign(const std::string& campaignId) {
loadedCampaignId = campaignId; loadedCampaignId = campaignId;
gameState.campaignId = campaignId; const CampaignConfig* campaign =
gameState.locations.clear(); CampaignRegistry::instance().find(campaignId);
gameState.players.clear(); if (!campaign) return;
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE); internalGameServer.initializeCampaign(
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load campaignId, campaign->questsPath, CONST_ZIP_FILE);
// The player stays loaded for the whole campaign; locations come and go around him. // The player stays loaded for the whole campaign; locations come and go around him.
auto& playerState = gameState.players["player"]; player = Location::createPlayer(
if (!playerState) playerState = std::make_shared<CharacterState>(); renderer, audioPlayer.get(), internalGameServer,
player = Location::createPlayer(renderer, audioPlayer.get(), *playerState, internalGameServer); internalGameServer.localPlayerId());
if (player) if (player)
player->onDeathAnimComplete = [this]() { startDarklandsTransition(); }; player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
} }
@@ -583,21 +561,21 @@ namespace FRG
void Game::unloadCampaign() { void Game::unloadCampaign() {
unloadLocation(false); unloadLocation(false);
player.reset(); player.reset();
gameState.players.clear();
loadedCampaignId.clear(); loadedCampaignId.clear();
gameState.locations.clear(); internalGameServer.clearCampaign();
renderer.textureManager.UnloadUnused(); // the player's textures renderer.textureManager.UnloadUnused(); // the player's textures
} }
void Game::loadLocation(const std::string& locationId) { void Game::loadLocation(const std::string& locationId) {
const LocationConfig* config = activeCampaign().findLocation(locationId); const LocationConfig* config = activeCampaign().findLocation(locationId);
if (!config) { if (!config) {
std::cerr << "[campaign] Campaign '" << gameState.campaignId << "' has no location '" const auto state = stateSnapshot();
std::cerr << "[campaign] Campaign '"
<< (state ? state->campaignId : std::string()) << "' has no location '"
<< locationId << "'" << std::endl; << locationId << "'" << std::endl;
return; return;
} }
const bool initializeLocation = const bool initializeLocation = internalGameServer.prepareLocation(*config);
!gameState.locations[locationId].hasPersistentState;
std::shared_ptr<Location> loc = createLocation(*config); std::shared_ptr<Location> loc = createLocation(*config);
wireLocation(loc.get()); wireLocation(loc.get());
if (shadowMap) loc->shadowMap = shadowMap; if (shadowMap) loc->shadowMap = shadowMap;
@@ -608,12 +586,7 @@ namespace FRG
} }
void Game::unloadLocation(bool storeState) { void Game::unloadLocation(bool storeState) {
if (storeState) internalGameServer.synchronizeRuntimeAuxiliaryState(); internalGameServer.clearRuntimeLocations(storeState);
else {
for (const auto& [name, loc] : internalGameServer.runtimeLocations())
gameState.locations.erase(name);
}
internalGameServer.runtimeLocations().clear();
// Textures only the location's models and characters used are freed now. // Textures only the location's models and characters used are freed now.
renderer.textureManager.UnloadUnused(); renderer.textureManager.UnloadUnused();
} }
@@ -631,8 +604,10 @@ namespace FRG
} }
// Runs from inside the current location's update(); the location itself is // Runs from inside the current location's update(); the location itself is
// released by a later loading step. // released by a later loading step.
enterLocationThen(gameState.campaignId, destName, [this, destName, destPos, destRotY]() { const auto state = stateSnapshot();
gameState.currentLocationName = destName; if (!state) return;
enterLocationThen(state->campaignId, destName, [this, destName, destPos, destRotY]() {
internalGameServer.submit(SetCurrentLocationCommand{destName});
Location* loc = currentLocation(); Location* loc = currentLocation();
if (!loc) return; if (!loc) return;
if (loc->player) { if (loc->player) {
@@ -643,17 +618,17 @@ namespace FRG
}); });
loc->stopCharacter(*loc->player); loc->stopCharacter(*loc->player);
} }
loc->state.cameraAzimuth = destRotY; auto locationState = loc->stateSnapshot();
loc->state.isDarklands = gameState.isDarklands; const float inclination = locationState
loc->state.isNight = gameState.isNight; ? locationState->cameraInclination
loc->state.isDawn = gameState.isDawn; : 1.1036f;
internalGameServer.submit(SetLocationCameraCommand{
destName, destRotY, inclination});
loc->scriptEngine.callLocationEnterCallback(); loc->scriptEngine.callLocationEnterCallback();
//Reset taxi //Reset taxi
auto locationState = gameState.locations.find(destName); internalGameServer.submit(ResetTaxiCommand{destName});
if (locationState != gameState.locations.end()) internalGameServer.submit(SetTaxiCalledCommand{false});
locationState->second.resetTaxi();
gameState.taxiIsCalled = false;
menuManager.onLocationChanged(destName); menuManager.onLocationChanged(destName);
}); });
@@ -661,34 +636,12 @@ namespace FRG
void Game::performFullReset() { void Game::performFullReset() {
gameState.currentLocationName.clear();
gameState.isDarklands = false;
gameState.isNight = false;
gameState.isDawn = false;
gameState.playerHp = 200.f;
gameState.playerMaxHp = 200.f;
gameState.money = 5500;
gameState.globalInts.clear();
gameState.globalFloats.clear();
gameState.globalFloats["player_hp"] = 200;
gameState.tutorialStep = TutorialStep::AdvanceDialogue;
gameState.tutorialPhoneChatScreenOpened = false;
gameState.tutorialJournalScreenOpened = false;
gameState.tutorialNeedOpenTaxiScreen = false;
gameState.tutorialTaxiHintDone = false;
for (int i = 0; i < 3; ++i) {
gameState.chatUnread[i] = true;
gameState.chatPreviewMsg[i].clear();
gameState.chatHistory[i].clear();
}
gameState.inventory.clear();
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
// Rebuild the loaded location from its config; the others have no stored state // 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. // any more, so they are built fresh when the player first goes there.
gameState.locations.clear(); std::vector<std::string> loadedIds =
std::vector<std::string> loadedIds; internalGameServer.loadedRuntimeLocationIds();
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loadedIds.push_back(name); internalGameServer.resetCampaign(
activeCampaign().questsPath, CONST_ZIP_FILE);
unloadLocation(false); unloadLocation(false);
for (const std::string& id : loadedIds) loadLocation(id); for (const std::string& id : loadedIds) loadLocation(id);
@@ -737,7 +690,8 @@ namespace FRG
// (if the campaign has one); a disabled tutorial is simply one that is already completed. // (if the campaign has one); a disabled tutorial is simply one that is already completed.
void Game::applyTutorialSetting() { void Game::applyTutorialSetting() {
if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting) if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting)
gameState.tutorialStep = TutorialStep::Completed; internalGameServer.submit(
SetTutorialStepCommand{TutorialStep::Completed});
} }
void Game::saveInitialState() { void Game::saveInitialState() {
@@ -760,12 +714,6 @@ namespace FRG
std::shared_ptr<Location> Game::createLocation(const LocationConfig& config) 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; LocationSetup params;
params.gameObjectsJsonPath = config.gameObjectsJsonPath; params.gameObjectsJsonPath = config.gameObjectsJsonPath;
params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath; params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath;
@@ -778,29 +726,30 @@ namespace FRG
params.lightsJsonPath = config.lightsJsonPath; params.lightsJsonPath = config.lightsJsonPath;
params.playerPosition = config.playerPosition; params.playerPosition = config.playerPosition;
params.player = player; params.player = player;
params.gameState = &authoritative;
auto loc = std::make_shared<Location>( auto loc = std::make_shared<Location>(
renderer, gameState.inventory, *authoritative.location, internalGameServer, config.id); renderer, internalGameServer, config.id);
loc->setup(params, &gameState.questJournal, audioPlayer.get()); internalGameServer.setupRuntimeLocation(
loc->scriptEngine.setGlobalStore(&gameState.globalInts); config.id, loc, std::move(params), audioPlayer.get());
loc->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) { loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
gameState.isNight = isNight; const auto state = stateSnapshot();
gameState.isDawn = isDawn; if (!state) return;
internalGameServer.submit(SetWorldModeCommand{
state->isDarklands, isNight, isDawn});
}; };
loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); }; loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); }; loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); }; loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
loc->requestReturnToMainMenu = [this]() { loc->requestReturnToMainMenu = [this]() {
gameState.currentLocationName.clear(); internalGameServer.submit(SetCurrentLocationCommand{std::string()});
menuManager.showMenuAfterGameCompleted(); menuManager.showMenuAfterGameCompleted();
}; };
loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) { loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
menuManager.setChatUnread(idx, unread, msg); menuManager.setChatUnread(idx, unread, msg);
}; };
for (auto& npc : loc->npcs) { for (auto& npc : loc->npcs) {
if (npc && npc->characterState.canAttack) { auto state = npc ? npc->stateSnapshot() : nullptr;
if (state && state->canAttack) {
npc->onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); }; npc->onDeathAnimComplete = [this]() { menuManager.onEnemyKilledInUniInterior(); };
} }
} }
@@ -813,25 +762,6 @@ namespace FRG
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg"; menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
}; };
authoritative.lockInteractiveObjectsDuringTutorial =
config.lockInteractiveObjectsDuringTutorial;
if (config.taxi) {
const LocationConfig::Taxi& taxi = *config.taxi;
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; return loc;
} }
@@ -846,20 +776,16 @@ namespace FRG
teleportPlayer(destName, destPos, destRotY); teleportPlayer(destName, destPos, destRotY);
}; };
// Share the global int store with all dialogue runtimes so flags set via
// dialogue JSON and via Lua set_dialogue_flag all see the same state.
{
loc->dialogueSystem.setGlobalFlagStore(&gameState.globalInts);
}
// Wire tutorial advance callbacks. // Wire tutorial advance callbacks.
// completeTutorialStep() only acts on the current step, so sharing is safe. // completeTutorialStep() only acts on the current step, so sharing is safe.
{ {
loc->dialogueSystem.setOnDialogueAdvanced([this]() { loc->dialogueSystem.setOnDialogueAdvanced([this]() {
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue); internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::AdvanceDialogue});
}); });
loc->onPlayerFloorWalk = [this]() { loc->onPlayerFloorWalk = [this]() {
gameState.completeTutorialStep(TutorialStep::Walk); internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::Walk});
menuManager.onPlayerStartedWalking(); menuManager.onPlayerStartedWalking();
}; };
} }
@@ -897,19 +823,23 @@ namespace FRG
} }
bool currentTargetDead = false; bool currentTargetDead = false;
auto playerState = loc->player->stateSnapshot();
if (!playerState) return;
for (const auto& npc : loc->npcs) { for (const auto& npc : loc->npcs) {
if (npc && npc->entityId() == loc->player->characterState.attackTargetId) { if (npc && npc->entityId() == playerState->attackTargetId) {
currentTargetDead = npc->characterState.hp <= 0.f; auto npcState = npc->stateSnapshot();
currentTargetDead = !npcState || npcState->hp <= 0.f;
break; break;
} }
} }
if ((loc->player->characterState.attackTargetId.empty() || currentTargetDead) if ((playerState->attackTargetId.empty() || currentTargetDead)
&& &&
loc->player->isMoving() == false loc->player->isMoving() == false
&& &&
loc->player->getHp() > 0) { loc->player->getHp() > 0) {
internalGameServer.submit(SetCharacterTargetCommand{ internalGameServer.submit(SetCharacterTargetCommand{
loc->locationId(), "player", attackerId}); loc->locationId(),
internalGameServer.localPlayerId(), attackerId});
} }
} }
@@ -931,7 +861,9 @@ namespace FRG
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
logger() << "Shadow map initialized" << std::endl; logger() << "Shadow map initialized" << std::endl;
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loc->shadowMap = shadowMap; for (const std::string& id : internalGameServer.loadedRuntimeLocationIds())
if (Location* loc = internalGameServer.runtimeLocation(id))
loc->shadowMap = shadowMap;
} }
void Game::destroyShadowMapAndProparateToLocations() void Game::destroyShadowMapAndProparateToLocations()
@@ -941,7 +873,9 @@ namespace FRG
shadowMap.reset(); shadowMap.reset();
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loc->shadowMap.reset(); for (const std::string& id : internalGameServer.loadedRuntimeLocationIds())
if (Location* loc = internalGameServer.runtimeLocation(id))
loc->shadowMap.reset();
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
} }
@@ -1014,16 +948,12 @@ namespace FRG
Location* loc = currentLocation(); Location* loc = currentLocation();
if (loc) if (loc)
{ {
// Sync global flags so Location's draw functions see them. const auto state = stateSnapshot();
loc->state.isDarklands = gameState.isDarklands; if (state && state->isDarklands) {
loc->state.isNight = gameState.isNight;
loc->state.isDawn = gameState.isDawn;
if (gameState.isDarklands) {
loc->drawGameDarklands(); loc->drawGameDarklands();
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
} }
else if (gameState.isNight) { else if (state && state->isNight) {
loc->drawGameNight(); loc->drawGameNight();
CheckGlError(__FILE__, __LINE__); CheckGlError(__FILE__, __LINE__);
} }
@@ -1206,11 +1136,67 @@ namespace FRG
// The authoritative implementation advances simulation; a future // The authoritative implementation advances simulation; a future
// network client can use the same call to poll/apply server updates. // network client can use the same call to poll/apply server updates.
internalGameServer.update(delta); internalGameServer.update(delta);
processServerEvents();
} }
} }
} }
void Game::processServerEvents()
{
for (GameServerEvent& event : internalGameServer.drainEvents()) {
std::visit([this](const auto& value) {
using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, InventoryItemAddedEvent>) {
menuManager.onItemPickedUp(value.itemId);
const Item* item =
ItemRegistry::instance().findById(value.itemId);
if (item)
menuManager.showToast(
menuManager.toastTextureItemAdded, item->name);
audioPlayer->playSoundAsync(
"resources/audio/831642__ienba__generic-game-pick-up.ogg");
} else if constexpr (std::is_same_v<T, InventoryItemRemovedEvent>) {
const Item* item =
ItemRegistry::instance().findById(value.itemId);
if (item)
menuManager.showToast(
menuManager.toastTextureItemRemoved, item->name);
audioPlayer->playSoundAsync(
"resources/audio/831642__ienba__generic-game-pick-up.ogg");
} else if constexpr (std::is_same_v<T, Quest::QuestEvent>) {
const auto state = stateSnapshot();
if (!state) return;
auto quest = std::find_if(
state->quests.begin(), state->quests.end(),
[&value](const Quest::QuestState& candidate) {
return candidate.definition.id == value.questId;
});
if (quest == state->quests.end()) return;
switch (value.type) {
case Quest::QuestEventType::Unlocked:
menuManager.showToast(
menuManager.toastTextureQuestNew,
quest->definition.title);
break;
case Quest::QuestEventType::Completed:
menuManager.showToast(
menuManager.toastTextureQuestCompleted,
quest->definition.title);
break;
case Quest::QuestEventType::Failed:
menuManager.showToast(
menuManager.toastTextureQuestFailed,
quest->definition.title);
break;
case Quest::QuestEventType::ObjectiveCompleted:
break;
}
}
}, event);
}
}
void Game::render() { void Game::render() {
FRG::CheckGlError(__FILE__, __LINE__); FRG::CheckGlError(__FILE__, __LINE__);
@@ -1256,10 +1242,7 @@ namespace FRG
} }
void Game::update() { void Game::update() {
// Host systems (locations, scripts and loading) still execute in-process. processServerEvents();
// 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; SDL_Event event;
const bool mobileIsPortrait = loadingCompleted && (Environment::width < Environment::height); const bool mobileIsPortrait = loadingCompleted && (Environment::width < Environment::height);
while (SDL_PollEvent(&event)) { while (SDL_PollEvent(&event)) {
@@ -1394,7 +1377,8 @@ namespace FRG
} }
//logger() << "Current zoom: " << Environment::zoom << std::endl; //logger() << "Current zoom: " << Environment::zoom << std::endl;
// Tutorial: any mouse-wheel scroll counts as the "zoom gesture". // Tutorial: any mouse-wheel scroll counts as the "zoom gesture".
gameState.completeTutorialStep(TutorialStep::Zoom); internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::Zoom});
} }
if (event.type == SDL_KEYDOWN && event.key.repeat == 0) { if (event.type == SDL_KEYDOWN && event.key.repeat == 0) {
@@ -1422,7 +1406,6 @@ namespace FRG
if (event.key.keysym.sym == SDLK_3) if (event.key.keysym.sym == SDLK_3)
{ {
currentLocation()->switchNavigation(2); currentLocation()->switchNavigation(2);
//logger() << "Player position: " << currentLocation()->player->characterState.position.transpose() << std::endl;
} }
if (event.key.keysym.sym == SDLK_n) if (event.key.keysym.sym == SDLK_n)
{ {
@@ -1435,8 +1418,6 @@ namespace FRG
{ {
currentLocation()->editorMode = EditorMode::None; currentLocation()->editorMode = EditorMode::None;
} }
//logger() << "Player position: " << currentLocation()->player->characterState.position.transpose() << std::endl;
} }
} }
@@ -1516,8 +1497,11 @@ namespace FRG
currentLocation()->lastMouseY = eventY; currentLocation()->lastMouseY = eventY;
// Snapshot current angles so we can measure how far the user rotates. // Snapshot current angles so we can measure how far the user rotates.
dragStartAzimuth = currentLocation()->state.cameraAzimuth; auto state = currentLocation()->stateSnapshot();
dragStartInclination = currentLocation()->state.cameraInclination; if (state) {
dragStartAzimuth = state->cameraAzimuth;
dragStartInclination = state->cameraInclination;
}
} }
} }
@@ -1577,7 +1561,8 @@ namespace FRG
// Tutorial: detect a significant pinch-zoom (≥ 2 zoom units). // Tutorial: detect a significant pinch-zoom (≥ 2 zoom units).
if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) { if (std::abs(Environment::zoom - pinchStartZoom) >= 2.0f) {
gameState.completeTutorialStep(TutorialStep::Zoom); internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::Zoom});
} }
} }
@@ -1720,12 +1705,17 @@ namespace FRG
// ~0.15 rad (≈8.6°) per axis ensures the user intentionally panned in 2D, // ~0.15 rad (≈8.6°) per axis ensures the user intentionally panned in 2D,
// not just nudged a single axis by accident. // not just nudged a single axis by accident.
static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f; static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f;
const auto game = stateSnapshot();
if (cameraDragging if (cameraDragging
&& gameState.tutorialStep == TutorialStep::RotateCamera) { && game
float deltaAz = std::abs(currentLocation()->state.cameraAzimuth - dragStartAzimuth); && game->tutorialStep == TutorialStep::RotateCamera) {
float deltaInc = std::abs(currentLocation()->state.cameraInclination - dragStartInclination); auto state = currentLocation()->stateSnapshot();
if (!state) return;
float deltaAz = std::abs(state->cameraAzimuth - dragStartAzimuth);
float deltaInc = std::abs(state->cameraInclination - dragStartInclination);
if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) { if (deltaAz >= TUTORIAL_ROTATION_THRESHOLD && deltaInc >= TUTORIAL_ROTATION_THRESHOLD) {
gameState.completeTutorialStep(TutorialStep::RotateCamera); internalGameServer.submit(
CompleteTutorialStepCommand{TutorialStep::RotateCamera});
} }
} }
} }
@@ -1733,13 +1723,14 @@ namespace FRG
bool Game::startDarklandsTransition() bool Game::startDarklandsTransition()
{ {
if (!gameState.isNight) const auto state = stateSnapshot();
if (!state || !state->isNight)
{ {
currentLocation()->dialogueSystem.startDialogue("darklands_day_dialog001"); currentLocation()->dialogueSystem.startDialogue("darklands_day_dialog001");
return false; return false;
} }
if (gameState.isDawn) if (state->isDawn)
{ {
currentLocation()->dialogueSystem.startDialogue("darklands_morning_dialog001"); currentLocation()->dialogueSystem.startDialogue("darklands_morning_dialog001");
return false; return false;
@@ -1773,11 +1764,13 @@ namespace FRG
if (darklandsFlashFadingIn) { if (darklandsFlashFadingIn) {
darklandsFlashAlpha = min(darklandsFlashAlpha + step, 1.0f); darklandsFlashAlpha = min(darklandsFlashAlpha + step, 1.0f);
if (darklandsFlashAlpha >= 1.0f) { if (darklandsFlashAlpha >= 1.0f) {
const auto before = stateSnapshot();
if (!before) return;
if (isNightTransition) { if (isNightTransition) {
gameState.isNight = !gameState.isNight; internalGameServer.submit(SetWorldModeCommand{
before->isDarklands, !before->isNight, before->isDawn});
if (currentLocation()) if (currentLocation())
{ {
currentLocation()->state.isNight = gameState.isNight;
currentLocation()->scriptEngine.callTriggerNightEnterCallback(); currentLocation()->scriptEngine.callTriggerNightEnterCallback();
} }
@@ -1785,12 +1778,15 @@ namespace FRG
menuManager.lastMusicTrack = "music/bishkek night.ogg"; menuManager.lastMusicTrack = "music/bishkek night.ogg";
} else { } else {
gameState.isDarklands = !gameState.isDarklands; internalGameServer.submit(SetWorldModeCommand{
!before->isDarklands, before->isNight, before->isDawn});
const auto after = stateSnapshot();
if (!after) return;
// Change HUD // Change HUD
menuManager.setDarklandsMode(gameState.isDarklands); menuManager.setDarklandsMode(after->isDarklands);
if (gameState.isDarklands) if (after->isDarklands)
{ {
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg"); audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg"; menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
@@ -1802,8 +1798,7 @@ namespace FRG
} }
if (currentLocation()) { if (currentLocation()) {
currentLocation()->state.isDarklands = gameState.isDarklands; if (after->isDarklands)
if (gameState.isDarklands)
currentLocation()->scriptEngine.callDarklandsEnterCallback(); currentLocation()->scriptEngine.callDarklandsEnterCallback();
else else
currentLocation()->scriptEngine.callDarklandsExitCallback(); currentLocation()->scriptEngine.callDarklandsExitCallback();
@@ -1935,19 +1930,21 @@ namespace FRG
void Game::playMusicForLoadedGame() void Game::playMusicForLoadedGame()
{ {
if (gameState.isDarklands) const auto state = stateSnapshot();
if (!state) return;
if (state->isDarklands)
{ {
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg"); audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg"; menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
} }
else if (gameState.isNight) else if (state->isNight)
{ {
audioPlayer->crossFadeMusicAsync("music/bishkek night.ogg"); audioPlayer->crossFadeMusicAsync("music/bishkek night.ogg");
menuManager.lastMusicTrack = "music/bishkek night.ogg"; menuManager.lastMusicTrack = "music/bishkek night.ogg";
} }
else else
{ {
if (gameState.currentLocationName == "location_dorm") { if (state->currentLocationId == "location_dorm") {
audioPlayer->crossFadeMusicAsync("music/obshaga.ogg"); audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
menuManager.lastMusicTrack = "music/obshaga.ogg"; menuManager.lastMusicTrack = "music/obshaga.ogg";
} }
+4 -4
View File
@@ -21,7 +21,6 @@
#include <unordered_set> #include <unordered_set>
#include "Location.h" #include "Location.h"
#include "AudioPlayerAsync.h" #include "AudioPlayerAsync.h"
#include "GameState.h"
#include "game_server/InternalGameServer.h" #include "game_server/InternalGameServer.h"
namespace FRG { namespace FRG {
@@ -84,9 +83,6 @@ namespace FRG {
private: private:
InternalGameServer internalGameServer; InternalGameServer internalGameServer;
// Host-only alias while location/script orchestration is moved behind typed
// commands. The referenced object is owned by internalGameServer.
GameState& gameState;
public: public:
IGameServer& server() { return internalGameServer; } IGameServer& server() { return internalGameServer; }
@@ -132,6 +128,9 @@ namespace FRG {
Location* currentLocation() const; Location* currentLocation() const;
std::shared_ptr<const GameSnapshot> stateSnapshot() const {
return internalGameServer.snapshot();
}
void saveGame(int slot); void saveGame(int slot);
void loadGame(int slot); void loadGame(int slot);
@@ -181,6 +180,7 @@ namespace FRG {
int64_t getSyncTimeMs(); int64_t getSyncTimeMs();
void processTickCount(); void processTickCount();
void processServerEvents();
void drawScene(); void drawScene();
void drawUI(); void drawUI();
+1
View File
@@ -215,6 +215,7 @@ void GameState::load(const nlohmann::json& in)
auto& player = players[id]; auto& player = players[id];
if (!player) player = std::make_shared<CharacterState>(); if (!player) player = std::make_shared<CharacterState>();
player->load(value); player->load(value);
player->configureAsPlayer();
} }
for (auto it = players.begin(); it != players.end();) { for (auto it = players.begin(); it != players.end();) {
if (!playerValues.contains(it->first)) it = players.erase(it); if (!playerValues.contains(it->first)) it = players.erase(it);
+455 -167
View File
File diff suppressed because it is too large. Load diff
+29 -9
View File
@@ -72,11 +72,19 @@ namespace FRG
class Location class Location
{ {
public: public:
Location(Renderer& iRenderer, Inventory& iInventory, LocationState& state, IGameServer& server, Location(Renderer& iRenderer, IGameServer& server,
std::string locationId); std::string locationId);
// Detaches the (shared, longer-lived) player from this location. // Detaches the (shared, longer-lived) player from this location.
~Location(); ~Location();
const std::string& locationId() const { return locationId_; } const std::string& locationId() const { return locationId_; }
std::shared_ptr<const LocationState> stateSnapshot() const;
bool isDarklands() const;
bool isNight() const;
bool isDawn() const;
int globalInt(const std::string& key) const;
float globalFloat(const std::string& key) const;
void setGlobalInt(const std::string& key, int value);
void setGlobalFloat(const std::string& key, float value);
std::unordered_map<std::string, LoadedGameObject> gameObjects; std::unordered_map<std::string, LoadedGameObject> gameObjects;
@@ -91,10 +99,7 @@ namespace FRG
// Builds the player character (models, animations, weapon, textures). // 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); IGameServer& server, const std::string& playerId);
//std::vector<PathFinder> navigationMaps;
//int state.activeNavigationIndex = 0;
std::shared_ptr<ShadowMap> shadowMap; std::shared_ptr<ShadowMap> shadowMap;
Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity(); Eigen::Matrix4f cameraViewMatrix = Eigen::Matrix4f::Identity();
@@ -160,7 +165,7 @@ namespace FRG
void setup(const LocationSetup& params, Quest::QuestJournal* journal, AudioPlayerAsync* audioPlayer); void setup(const LocationSetup& params, AudioPlayerAsync* audioPlayer);
//void setupNavigation(const std::vector<std::string>& paths); //void setupNavigation(const std::vector<std::string>& paths);
bool switchNavigation(int index); bool switchNavigation(int index);
void setCharacterTarget(Character& character, const Eigen::Vector3f& target, void setCharacterTarget(Character& character, const Eigen::Vector3f& target,
@@ -175,6 +180,18 @@ namespace FRG
void resetCharacterAfterDeath(Character& character); void resetCharacterAfterDeath(Character& character);
void tryCharacterAttackCombo(Character& character); void tryCharacterAttackCombo(Character& character);
void setCharacterHp(Character& character, float hp); void setCharacterHp(Character& character, float hp);
bool hasInventoryItem(const std::string& itemId) const;
std::size_t inventoryItemCount() const;
void addInventoryItem(std::string itemId);
void removeInventoryItem(std::string itemId);
bool unlockQuest(const std::string& questId);
bool completeQuest(const std::string& questId);
bool failQuest(const std::string& questId);
bool setQuestObjectiveCompleted(
const std::string& questId, const std::string& objectiveId, bool completed);
bool setQuestObjectiveVisible(
const std::string& questId, const std::string& objectiveId, bool visible);
bool setQuestActiveObjective(const std::string& questId, int objectiveIndex);
InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir); InteractiveObject* raycastInteractiveObjects(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir);
Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f); Character* raycastNpcs(const Eigen::Vector3f& rayOrigin, const Eigen::Vector3f& rayDir, float maxDistance = 100.0f);
@@ -210,15 +227,18 @@ namespace FRG
friend class ScriptEngine; friend class ScriptEngine;
friend class InternalGameServer; friend class InternalGameServer;
Renderer& renderer; Renderer& renderer;
Inventory& inventory;
AudioPlayerAsync* audioPlayer; AudioPlayerAsync* audioPlayer;
private: private:
LocationState& state; // non-owning binding to LocationGameState
IGameServer& gameServer_; IGameServer& gameServer_;
std::string locationId_; std::string locationId_;
std::unordered_map<std::string, std::shared_ptr<Character>> remotePlayers_;
void synchronizeRemotePlayers();
void drawRemotePlayerNames(
const Eigen::Matrix4f& cameraViewMatrix,
const Eigen::Matrix4f& projectionMatrix);
// ---- Pointer-to-index helpers ---- // ---- Pointer-to-index helpers ----
TeleportZone* getTargetTeleportZone(); TeleportZone* getTargetTeleportZone(const LocationState& state);
enum class InstancedTexture { None, Color, Darklands }; enum class InstancedTexture { None, Color, Darklands };
+36 -10
View File
@@ -1,6 +1,7 @@
#include "LocationEditor.h" #include "LocationEditor.h"
#include "Location.h" #include "Location.h"
#include "Character.h" #include "Character.h"
#include "game_server/IGameServer.h"
#include "utils/Utils.h" #include "utils/Utils.h"
#include "render/OpenGlExtensions.h" #include "render/OpenGlExtensions.h"
#include "TextModel.h" #include "TextModel.h"
@@ -24,9 +25,14 @@ namespace FRG
void LocationEditor::buildNavMeshes() void LocationEditor::buildNavMeshes()
{ {
navigationEditorNavMeshes.clear(); navigationEditorNavMeshes.clear();
const float y = loc.state.navigationMaps[loc.state.activeNavigationIndex].getFloorY() + 0.02f; auto state = loc.stateSnapshot();
if (!state || state->activeNavigationIndex < 0 ||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
const PathFinder& navigation =
state->navigationMaps[state->activeNavigationIndex];
const float y = navigation.getFloorY() + 0.02f;
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f); const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
for (const auto& obs : loc.state.navigationMaps[loc.state.activeNavigationIndex].getObstaclePolygons()) { for (const auto& obs : navigation.getObstaclePolygons()) {
if (obs.polygon.size() < 3) continue; if (obs.polygon.size() < 3) continue;
VertexRenderStruct mesh; VertexRenderStruct mesh;
mesh.data = CreatePolygonFloor(obs.polygon, y, red); mesh.data = CreatePolygonFloor(obs.polygon, y, red);
@@ -50,7 +56,11 @@ namespace FRG
{ {
VertexDataStruct data; VertexDataStruct data;
const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f); const Eigen::Vector3f yellow(1.0f, 1.0f, 0.0f);
const float y = loc.state.navigationMaps[loc.state.activeNavigationIndex].getFloorY() + 0.05f; auto state = loc.stateSnapshot();
if (!state || state->activeNavigationIndex < 0 ||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
const float y =
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.05f;
const float s = 0.2f; const float s = 0.2f;
for (const auto& pt : navigationEditorPoints) { for (const auto& pt : navigationEditorPoints) {
@@ -126,12 +136,17 @@ namespace FRG
poly.polygon.emplace_back(pt.x(), pt.z()); poly.polygon.emplace_back(pt.x(), pt.z());
} }
loc.state.navigationMaps[loc.state.activeNavigationIndex].addObstaclePolygon(poly); loc.gameServer_.submit(AddNavigationObstacleCommand{
loc.locationId_, poly.name, poly.polygon});
logger() << "[NAV_EDITOR] Added obstacle '" << poly.name << "' with " logger() << "[NAV_EDITOR] Added obstacle '" << poly.name << "' with "
<< poly.polygon.size() << " vertices\n"; << poly.polygon.size() << " vertices\n";
{ {
const float y = loc.state.navigationMaps[loc.state.activeNavigationIndex].getFloorY() + 0.02f; auto state = loc.stateSnapshot();
if (!state || state->activeNavigationIndex < 0 ||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) return;
const float y =
state->navigationMaps[state->activeNavigationIndex].getFloorY() + 0.02f;
const Eigen::Vector3f red(1.0f, 0.0f, 0.0f); const Eigen::Vector3f red(1.0f, 0.0f, 0.0f);
VertexRenderStruct mesh; VertexRenderStruct mesh;
mesh.data = CreatePolygonFloor(poly.polygon, y, red); mesh.data = CreatePolygonFloor(poly.polygon, y, red);
@@ -153,12 +168,21 @@ namespace FRG
if (!std::filesystem::exists(baseName + ".json")) break; if (!std::filesystem::exists(baseName + ".json")) break;
} }
if (loc.state.navigationMaps[loc.state.activeNavigationIndex].saveConfig(baseName + ".json")) auto state = loc.stateSnapshot();
if (!state || state->activeNavigationIndex < 0 ||
state->activeNavigationIndex >= static_cast<int>(state->navigationMaps.size())) {
std::cerr << "[NAV_EDITOR] No active navigation map to save\n";
return;
}
const PathFinder& navigation =
state->navigationMaps[state->activeNavigationIndex];
if (navigation.saveConfig(baseName + ".json"))
logger() << "[NAV_EDITOR] Saved config to: " << baseName << ".json\n"; logger() << "[NAV_EDITOR] Saved config to: " << baseName << ".json\n";
else else
std::cerr << "[NAV_EDITOR] Failed to save config to: " << baseName << ".json\n"; std::cerr << "[NAV_EDITOR] Failed to save config to: " << baseName << ".json\n";
if (loc.state.navigationMaps[loc.state.activeNavigationIndex].saveGrid(baseName + ".txt")) if (navigation.saveGrid(baseName + ".txt"))
logger() << "[NAV_EDITOR] Saved grid to: " << baseName << ".txt\n"; logger() << "[NAV_EDITOR] Saved grid to: " << baseName << ".txt\n";
else else
std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n"; std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n";
@@ -458,9 +482,11 @@ namespace FRG
data.rotationX = 0.0f; data.rotationX = 0.0f;
data.rotationY = rotDist(rng); data.rotationY = rotDist(rng);
data.rotationZ = 0.0f; data.rotationZ = 0.0f;
data.positionX = loc.player->characterState.position.x(); auto playerState = loc.player ? loc.player->stateSnapshot() : nullptr;
data.positionY = loc.player->characterState.position.y();//-0.1; if (!playerState) return;
data.positionZ = loc.player->characterState.position.z(); data.positionX = playerState->position.x();
data.positionY = playerState->position.y();//-0.1;
data.positionZ = playerState->position.z();
data.scale = scaleDist(rng); data.scale = scaleDist(rng);
GameObjectData data2; GameObjectData data2;
-6
View File
@@ -26,12 +26,6 @@ struct LocationState : public ISaveable {
std::string targetInteractNpcId; std::string targetInteractNpcId;
std::string targetTeleportZoneId; std::string targetTeleportZoneId;
// ---- World-mode flags (synced from Game after each transition) ----
bool isDarklands = false;
bool isNight = false;
bool isDawn = false;
NpcCar taxiCarInitialState; NpcCar taxiCarInitialState;
NpcCar taxiCar; NpcCar taxiCar;
bool taxiArrivedCallbackCalled = false; bool taxiArrivedCallbackCalled = false;
+38 -81
View File
@@ -66,9 +66,6 @@ namespace FRG {
std::unordered_map<std::string, sol::protected_function> cutsceneCompleteCallbacks; std::unordered_map<std::string, sol::protected_function> cutsceneCompleteCallbacks;
std::unordered_map<std::string, sol::protected_function> npcBumpedByPlayerCallbacks; std::unordered_map<std::string, sol::protected_function> npcBumpedByPlayerCallbacks;
std::unordered_map<std::string, sol::protected_function> npcBumpsPlayerCallbacks; 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 locationInitCallback;
sol::protected_function locationEnterCallback; sol::protected_function locationEnterCallback;
sol::protected_function locationExitCallback; sol::protected_function locationExitCallback;
@@ -82,7 +79,7 @@ namespace FRG {
ScriptEngine::ScriptEngine() = default; ScriptEngine::ScriptEngine() = default;
ScriptEngine::~ScriptEngine() = default; ScriptEngine::~ScriptEngine() = default;
void ScriptEngine::init(Location* loc, Inventory* inventory, AudioPlayerAsync* audioPlayer, const std::string& scriptPath) { void ScriptEngine::init(Location* loc, AudioPlayerAsync* audioPlayer, const std::string& scriptPath) {
impl = std::make_unique<Impl>(); impl = std::make_unique<Impl>();
sol::state& lua = impl->lua; sol::state& lua = impl->lua;
@@ -199,10 +196,10 @@ namespace FRG {
}); });
api.set_function("has_item", [inventory](const std::string& itemId) { api.set_function("has_item", [loc](const std::string& itemId) {
const Item* item = ItemRegistry::instance().findById(itemId); const Item* item = ItemRegistry::instance().findById(itemId);
if (item) { if (item) {
if (inventory->hasItem(itemId)) { if (loc->hasInventoryItem(itemId)) {
logger() << "[script] has_item: " << item->name << " returns true" << std::endl; logger() << "[script] has_item: " << item->name << " returns true" << std::endl;
return true; return true;
} }
@@ -219,10 +216,10 @@ namespace FRG {
}); });
// pickup_item(item_id) // pickup_item(item_id)
api.set_function("pickup_item", [inventory](const std::string& itemId) { api.set_function("pickup_item", [loc](const std::string& itemId) {
const Item* item = ItemRegistry::instance().findById(itemId); const Item* item = ItemRegistry::instance().findById(itemId);
if (item) { if (item) {
inventory->addItem(item->id); loc->addInventoryItem(item->id);
logger() << "[script] pickup_item: " << item->name << std::endl; logger() << "[script] pickup_item: " << item->name << std::endl;
} else { } else {
std::cerr << "[script] pickup_item: item '" << itemId << "' not found in ItemRegistry\n"; std::cerr << "[script] pickup_item: item '" << itemId << "' not found in ItemRegistry\n";
@@ -230,9 +227,9 @@ namespace FRG {
}); });
// remove_item(item_id) // remove_item(item_id)
api.set_function("remove_item", [loc, inventory](const std::string& id) { api.set_function("remove_item", [loc](const std::string& id) {
logger() << "[script] remove_item: " << id << std::endl; logger() << "[script] remove_item: " << id << std::endl;
inventory->removeItem(id); loc->removeInventoryItem(id);
}); });
api.set_function("deactivate_interactive_object", [loc](const std::string& objectId) { api.set_function("deactivate_interactive_object", [loc](const std::string& objectId) {
@@ -276,13 +273,13 @@ namespace FRG {
}); });
// get_inventory_count() // get_inventory_count()
api.set_function("get_inventory_count", [inventory]() { api.set_function("get_inventory_count", [loc]() {
return inventory->getCount(); return loc->inventoryItemCount();
}); });
// has_item(item_id) // has_item(item_id)
api.set_function("has_item", [inventory](const std::string& id) { api.set_function("has_item", [loc](const std::string& id) {
return inventory->hasItem(id); return loc->hasInventoryItem(id);
}); });
api.set_function("start_dialogue", api.set_function("start_dialogue",
@@ -381,12 +378,12 @@ namespace FRG {
api.set_function("is_night", api.set_function("is_night",
[loc]() { [loc]() {
return loc->state.isNight; return loc->isNight();
}); });
api.set_function("is_dawn", api.set_function("is_dawn",
[loc]() { [loc]() {
return loc->state.isDawn; return loc->isDawn();
}); });
// advance_darklands_hud() // advance_darklands_hud()
@@ -415,27 +412,26 @@ namespace FRG {
// is_darklands() → bool // is_darklands() → bool
api.set_function("is_darklands", api.set_function("is_darklands",
[loc]() { return loc->state.isDarklands; }); [loc]() { return loc->isDarklands(); });
api.set_function("setFloatValue", api.set_function("setFloatValue",
[this_impl = impl.get()](const std::string& key, float value) { [loc](const std::string& key, float value) {
if (this_impl->globalFloats) loc->setGlobalFloat(key, value);
(*this_impl->globalFloats)[key] = value;
}); });
api.set_function("getFloatValue", api.set_function("getFloatValue",
[this_impl = impl.get()](const std::string& key) -> float { [loc](const std::string& key) -> float {
if (!this_impl->globalFloats) return 0.0f; return loc->globalFloat(key);
auto it = this_impl->globalFloats->find(key);
return it != this_impl->globalFloats->end() ? it->second : 0.0f;
}); });
api.set_function("get_player_x", api.set_function("get_player_x",
[loc]() -> float { [loc]() -> float {
return loc->player ? loc->player->characterState.position.x() : 0.0f; auto state = loc->player ? loc->player->stateSnapshot() : nullptr;
return state ? state->position.x() : 0.0f;
}); });
api.set_function("get_player_z", api.set_function("get_player_z",
[loc]() -> float { [loc]() -> float {
return loc->player ? loc->player->characterState.position.z() : 0.0f; auto state = loc->player ? loc->player->stateSnapshot() : nullptr;
return state ? state->position.z() : 0.0f;
}); });
// set_player_hp(value) — sets the player's current HP directly. // set_player_hp(value) — sets the player's current HP directly.
@@ -462,15 +458,12 @@ namespace FRG {
// Global integer store — shared across all location scripts. // Global integer store — shared across all location scripts.
// setIntValue(key, value) / getIntValue(key) → int (0 if not set) // setIntValue(key, value) / getIntValue(key) → int (0 if not set)
api.set_function("setIntValue", api.set_function("setIntValue",
[this_impl = impl.get()](const std::string& key, int value) { [loc](const std::string& key, int value) {
if (this_impl->globalInts) loc->setGlobalInt(key, value);
(*this_impl->globalInts)[key] = value;
}); });
api.set_function("getIntValue", api.set_function("getIntValue",
[this_impl = impl.get()](const std::string& key) -> int { [loc](const std::string& key) -> int {
if (!this_impl->globalInts) return 0; return loc->globalInt(key);
auto it = this_impl->globalInts->find(key);
return it != this_impl->globalInts->end() ? it->second : 0;
}); });
// set_darklands_callbacks(on_enter, on_exit) // set_darklands_callbacks(on_enter, on_exit)
@@ -806,64 +799,40 @@ namespace FRG {
// quest_unlock(quest_id) // quest_unlock(quest_id)
api.set_function("quest_unlock", api.set_function("quest_unlock",
[this_impl = impl.get()](const std::string& questId) -> bool { [loc](const std::string& questId) -> bool {
if (!this_impl->questJournal) { return loc->unlockQuest(questId);
std::cerr << "[script] quest_unlock: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->unlockQuest(questId);
}); });
// quest_complete(quest_id) // quest_complete(quest_id)
api.set_function("quest_complete", api.set_function("quest_complete",
[this_impl = impl.get()](const std::string& questId) -> bool { [loc](const std::string& questId) -> bool {
if (!this_impl->questJournal) { return loc->completeQuest(questId);
std::cerr << "[script] quest_complete: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->completeQuest(questId);
}); });
// quest_fail(quest_id) // quest_fail(quest_id)
api.set_function("quest_fail", api.set_function("quest_fail",
[this_impl = impl.get()](const std::string& questId) -> bool { [loc](const std::string& questId) -> bool {
if (!this_impl->questJournal) { return loc->failQuest(questId);
std::cerr << "[script] quest_fail: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->failQuest(questId);
}); });
// quest_set_objective_completed(quest_id, objective_id [, completed]) // quest_set_objective_completed(quest_id, objective_id [, completed])
api.set_function("quest_set_objective_completed", api.set_function("quest_set_objective_completed",
[this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object completed) -> bool { [loc](const std::string& questId, const std::string& objId, sol::object completed) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_set_objective_completed: QuestJournal not set\n";
return false;
}
const bool val = completed.is<bool>() ? completed.as<bool>() : true; const bool val = completed.is<bool>() ? completed.as<bool>() : true;
return this_impl->questJournal->setObjectiveCompleted(questId, objId, val); return loc->setQuestObjectiveCompleted(questId, objId, val);
}); });
// quest_set_objective_visible(quest_id, objective_id [, visible]) // quest_set_objective_visible(quest_id, objective_id [, visible])
api.set_function("quest_set_objective_visible", api.set_function("quest_set_objective_visible",
[this_impl = impl.get()](const std::string& questId, const std::string& objId, sol::object visible) -> bool { [loc](const std::string& questId, const std::string& objId, sol::object visible) -> bool {
if (!this_impl->questJournal) {
std::cerr << "[script] quest_set_objective_visible: QuestJournal not set\n";
return false;
}
const bool val = visible.is<bool>() ? visible.as<bool>() : true; const bool val = visible.is<bool>() ? visible.as<bool>() : true;
return this_impl->questJournal->setObjectiveVisible(questId, objId, val); return loc->setQuestObjectiveVisible(questId, objId, val);
}); });
// quest_set_active_objective(quest_id, objective_index) // quest_set_active_objective(quest_id, objective_index)
api.set_function("quest_set_active_objective", api.set_function("quest_set_active_objective",
[this_impl = impl.get()](const std::string& questId, int index) -> bool { [loc](const std::string& questId, int index) -> bool {
if (!this_impl->questJournal) { return loc->setQuestActiveObjective(questId, index);
std::cerr << "[script] quest_set_active_objective: QuestJournal not set\n";
return false;
}
return this_impl->questJournal->setActiveObjective(questId, index);
}); });
api.set_function("call_tutorial_taxi_required", api.set_function("call_tutorial_taxi_required",
@@ -1033,18 +1002,6 @@ namespace FRG {
} }
} }
void ScriptEngine::setGlobalStore(std::unordered_map<std::string, int>* store) {
if (impl) impl->globalInts = store;
}
void ScriptEngine::setGlobalFloatStore(std::unordered_map<std::string, float>* store) {
if (impl) impl->globalFloats = store;
}
void ScriptEngine::setQuestJournal(Quest::QuestJournal* journal) {
if (impl) impl->questJournal = journal;
}
void ScriptEngine::callLocationInitCallback() { void ScriptEngine::callLocationInitCallback() {
if (!impl || !impl->locationInitCallback.valid()) return; if (!impl || !impl->locationInitCallback.valid()) return;
auto result = impl->locationInitCallback(); auto result = impl->locationInitCallback();
+1 -8
View File
@@ -1,8 +1,6 @@
#pragma once #pragma once
#include <string> #include <string>
#include <memory> #include <memory>
#include <unordered_map>
#include "quest/QuestJournal.h"
#include "external/nlohmann/json.hpp" #include "external/nlohmann/json.hpp"
#include "ISaveable.h" #include "ISaveable.h"
#include "AudioPlayerAsync.h" #include "AudioPlayerAsync.h"
@@ -10,7 +8,6 @@
namespace FRG { namespace FRG {
class Location; class Location;
class Inventory;
class ScriptEngine { class ScriptEngine {
public: public:
@@ -18,7 +15,7 @@ public:
~ScriptEngine(); ~ScriptEngine();
// Must be called once, after the Game's NPCs are ready. // Must be called once, after the Game's NPCs are ready.
void init(Location* game, Inventory* inventory, AudioPlayerAsync* audioPlayer, const std::string& scriptPath); void init(Location* game, AudioPlayerAsync* audioPlayer, const std::string& scriptPath);
// Execute a Lua script file. Logs errors to stderr. // Execute a Lua script file. Logs errors to stderr.
//void runScript(const std::string& path); //void runScript(const std::string& path);
@@ -37,10 +34,6 @@ public:
// Fired when a moving NPC physically bumps into the standing player. // Fired when a moving NPC physically bumps into the standing player.
void callNpcBumpsPlayerCallback(const std::string& npcId); 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 callLocationInitCallback();
void callLocationEnterCallback(); void callLocationEnterCallback();
void callLocationExitCallback(); void callLocationExitCallback();
+20 -14
View File
@@ -162,21 +162,24 @@ void DialogueRuntime::selectChoice(int index) {
} }
void DialogueRuntime::setFlag(const std::string& name, int value) { void DialogueRuntime::setFlag(const std::string& name, int value) {
if (flagStore) (*flagStore)[name] = value; if (setGlobalFlag) setGlobalFlag(name, value);
} }
int DialogueRuntime::getFlag(const std::string& name) const { int DialogueRuntime::getFlag(const std::string& name) const {
if (!flagStore) return 0; return getGlobalFlag ? getGlobalFlag(name) : 0;
auto it = flagStore->find(name);
return (it != flagStore->end()) ? it->second : 0;
} }
void DialogueRuntime::setGlobalFlagStore(std::unordered_map<std::string, int>* store) { void DialogueRuntime::setGlobalFlagAccessors(
flagStore = store; std::function<int(const std::string&)> getter,
std::function<void(const std::string&, int)> setter)
{
getGlobalFlag = std::move(getter);
setGlobalFlag = std::move(setter);
} }
void DialogueRuntime::setQuestJournal(Quest::QuestJournal* journal) { void DialogueRuntime::setQuestActionHandler(
questJournal = journal; std::function<void(const QuestActionRequest&)> handler) {
onQuestAction = std::move(handler);
} }
void DialogueRuntime::applyQuestActions( void DialogueRuntime::applyQuestActions(
@@ -184,17 +187,20 @@ void DialogueRuntime::applyQuestActions(
const std::string& questFail, const std::string& objectiveComplete, const std::string& questFail, const std::string& objectiveComplete,
const std::string& objectiveVisible) const std::string& objectiveVisible)
{ {
if (!questJournal) return; if (!onQuestAction) return;
if (!questUnlock.empty()) questJournal->unlockQuest(questUnlock); if (!questUnlock.empty())
if (!questComplete.empty()) questJournal->completeQuest(questComplete); onQuestAction({QuestActionType::Unlock, questUnlock, {}});
if (!questFail.empty()) questJournal->failQuest(questFail); if (!questComplete.empty())
onQuestAction({QuestActionType::Complete, questComplete, {}});
if (!questFail.empty())
onQuestAction({QuestActionType::Fail, questFail, {}});
if (!objectiveComplete.empty()) { if (!objectiveComplete.empty()) {
auto [qId, oId] = splitDot(objectiveComplete); auto [qId, oId] = splitDot(objectiveComplete);
questJournal->setObjectiveCompleted(qId, oId); onQuestAction({QuestActionType::CompleteObjective, qId, oId});
} }
if (!objectiveVisible.empty()) { if (!objectiveVisible.empty()) {
auto [qId, oId] = splitDot(objectiveVisible); auto [qId, oId] = splitDot(objectiveVisible);
questJournal->setObjectiveVisible(qId, oId); onQuestAction({QuestActionType::ShowObjective, qId, oId});
} }
} }
+22 -6
View File
@@ -2,17 +2,29 @@
#include "dialogue/DialogueDatabase.h" #include "dialogue/DialogueDatabase.h"
#include "dialogue/TranslationDatabase.h" #include "dialogue/TranslationDatabase.h"
#include "quest/QuestJournal.h"
#include "external/nlohmann/json.hpp" #include "external/nlohmann/json.hpp"
#include "ISaveable.h" #include "ISaveable.h"
#include <functional> #include <functional>
#include <string> #include <string>
#include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
namespace FRG::Dialogue { namespace FRG::Dialogue {
enum class QuestActionType {
Unlock,
Complete,
Fail,
CompleteObjective,
ShowObjective
};
struct QuestActionRequest {
QuestActionType type;
std::string questId;
std::string objectiveId;
};
class DialogueRuntime : public FRG::ISaveable { class DialogueRuntime : public FRG::ISaveable {
public: public:
void setDatabase(const DialogueDatabase* value); void setDatabase(const DialogueDatabase* value);
@@ -41,8 +53,11 @@ public:
void setFlag(const std::string& name, int value); void setFlag(const std::string& name, int value);
int getFlag(const std::string& name) const; int getFlag(const std::string& name) const;
void setGlobalFlagStore(std::unordered_map<std::string, int>* store); void setGlobalFlagAccessors(
void setQuestJournal(Quest::QuestJournal* journal); std::function<int(const std::string&)> getter,
std::function<void(const std::string&, int)> setter);
void setQuestActionHandler(
std::function<void(const QuestActionRequest&)> handler);
// Saves whether a dialogue is active, its id, and current node. // Saves whether a dialogue is active, its id, and current node.
// Cutscene state is not saved. // Cutscene state is not saved.
@@ -62,13 +77,14 @@ private:
std::function<void(const std::string&, const std::string&)> onCutsceneStartNeeded; std::function<void(const std::string&, const std::string&)> onCutsceneStartNeeded;
std::function<void(const std::string&)> onDialogueLineStarted; std::function<void(const std::string&)> onDialogueLineStarted;
std::function<void(const std::string&, bool)> onChatBubbleReady; std::function<void(const std::string&, bool)> onChatBubbleReady;
std::function<void(const QuestActionRequest&)> onQuestAction;
const DialogueDatabase* database = nullptr; const DialogueDatabase* database = nullptr;
const TranslationDatabase* translations = nullptr; const TranslationDatabase* translations = nullptr;
Quest::QuestJournal* questJournal = nullptr;
const DialogueDefinition* activeDialogue = nullptr; const DialogueDefinition* activeDialogue = nullptr;
std::unordered_map<std::string, int>* flagStore = nullptr; std::function<int(const std::string&)> getGlobalFlag;
std::function<void(const std::string&, int)> setGlobalFlag;
std::unordered_set<std::string> consumedChoices; std::unordered_set<std::string> consumedChoices;
std::string currentNodeId; std::string currentNodeId;
+8 -5
View File
@@ -6,7 +6,6 @@
#include "cutscene/CutsceneDatabase.h" #include "cutscene/CutsceneDatabase.h"
#include "cutscene/CutsceneOverlay.h" #include "cutscene/CutsceneOverlay.h"
#include "cutscene/CutsceneRuntime.h" #include "cutscene/CutsceneRuntime.h"
#include "quest/QuestJournal.h"
#include "ISaveable.h" #include "ISaveable.h"
#include <SDL.h> #include <SDL.h>
#include <functional> #include <functional>
@@ -58,11 +57,15 @@ public:
void setFlag(const std::string& name, int value) { dialogueRuntime.setFlag(name, value); } void setFlag(const std::string& name, int value) { dialogueRuntime.setFlag(name, value); }
int getFlag(const std::string& name) const { return dialogueRuntime.getFlag(name); } int getFlag(const std::string& name) const { return dialogueRuntime.getFlag(name); }
void setGlobalFlagStore(std::unordered_map<std::string, int>* store) { void setGlobalFlagAccessors(
dialogueRuntime.setGlobalFlagStore(store); std::function<int(const std::string&)> getter,
std::function<void(const std::string&, int)> setter) {
dialogueRuntime.setGlobalFlagAccessors(
std::move(getter), std::move(setter));
} }
void setQuestJournal(Quest::QuestJournal* journal) { void setQuestActionHandler(
dialogueRuntime.setQuestJournal(journal); std::function<void(const QuestActionRequest&)> handler) {
dialogueRuntime.setQuestActionHandler(std::move(handler));
} }
private: private:
+66
View File
@@ -94,8 +94,51 @@ struct AppendChatMessageCommand {
}; };
struct SetTaxiCalledCommand { bool called; }; struct SetTaxiCalledCommand { bool called; };
struct StartTaxiCommand { std::string locationId; }; struct StartTaxiCommand { std::string locationId; };
struct ResetTaxiCommand { std::string locationId; };
struct SetLocationCameraCommand {
std::string locationId;
float azimuth;
float inclination;
};
struct SetLocationTargetsCommand {
std::string locationId;
std::string interactiveObjectId;
std::string npcId;
std::string teleportZoneId;
};
struct SwitchLocationNavigationCommand {
std::string locationId;
int index;
};
struct SetNavigationAreaAvailableCommand {
std::string locationId;
std::string areaName;
bool available;
};
struct AddNavigationObstacleCommand {
std::string locationId;
std::string name;
std::vector<Eigen::Vector2f> polygon;
};
struct AddInventoryItemCommand { std::string itemId; }; struct AddInventoryItemCommand { std::string itemId; };
struct RemoveInventoryItemCommand { std::string itemId; }; struct RemoveInventoryItemCommand { std::string itemId; };
struct UnlockQuestCommand { std::string questId; };
struct CompleteQuestCommand { std::string questId; };
struct FailQuestCommand { std::string questId; };
struct SetQuestObjectiveCompletedCommand {
std::string questId;
std::string objectiveId;
bool completed;
};
struct SetQuestObjectiveVisibleCommand {
std::string questId;
std::string objectiveId;
bool visible;
};
struct SetQuestActiveObjectiveCommand {
std::string questId;
int objectiveIndex;
};
struct SetGlobalIntCommand { std::string key; int value; }; struct SetGlobalIntCommand { std::string key; int value; };
struct SetGlobalFloatCommand { std::string key; float value; }; struct SetGlobalFloatCommand { std::string key; float value; };
struct SetCharacterTransformCommand { struct SetCharacterTransformCommand {
@@ -188,8 +231,20 @@ using GameCommand = std::variant<
AppendChatMessageCommand, AppendChatMessageCommand,
SetTaxiCalledCommand, SetTaxiCalledCommand,
StartTaxiCommand, StartTaxiCommand,
ResetTaxiCommand,
SetLocationCameraCommand,
SetLocationTargetsCommand,
SwitchLocationNavigationCommand,
SetNavigationAreaAvailableCommand,
AddNavigationObstacleCommand,
AddInventoryItemCommand, AddInventoryItemCommand,
RemoveInventoryItemCommand, RemoveInventoryItemCommand,
UnlockQuestCommand,
CompleteQuestCommand,
FailQuestCommand,
SetQuestObjectiveCompletedCommand,
SetQuestObjectiveVisibleCommand,
SetQuestActiveObjectiveCommand,
SetGlobalIntCommand, SetGlobalIntCommand,
SetGlobalFloatCommand, SetGlobalFloatCommand,
SetCharacterTransformCommand, SetCharacterTransformCommand,
@@ -209,13 +264,24 @@ using GameCommand = std::variant<
SetTriggerZoneEnabledCommand, SetTriggerZoneEnabledCommand,
ReloadQuestDefinitionsCommand>; ReloadQuestDefinitionsCommand>;
struct InventoryItemAddedEvent { std::string itemId; };
struct InventoryItemRemovedEvent { std::string itemId; };
using GameServerEvent = std::variant<
InventoryItemAddedEvent,
InventoryItemRemovedEvent,
Quest::QuestEvent>;
class IGameServer { class IGameServer {
public: public:
virtual ~IGameServer() = default; virtual ~IGameServer() = default;
// Each returned object is immutable and remains valid after later commands. // Each returned object is immutable and remains valid after later commands.
virtual std::shared_ptr<const GameSnapshot> snapshot() const = 0; virtual std::shared_ptr<const GameSnapshot> snapshot() const = 0;
// Client-relative identity assigned by this server connection.
virtual const std::string& localPlayerId() const = 0;
virtual void submit(const GameCommand& command) = 0; virtual void submit(const GameCommand& command) = 0;
virtual std::vector<GameServerEvent> drainEvents() = 0;
virtual void update(std::int64_t deltaMs) = 0; virtual void update(std::int64_t deltaMs) = 0;
// Persistence stays server-side. The JSON shape is GameState's established // Persistence stays server-side. The JSON shape is GameState's established
+279 -8
View File
@@ -1,12 +1,192 @@
#include "InternalGameServer.h" #include "InternalGameServer.h"
#include "../Campaign.h"
#include "../Location.h" #include "../Location.h"
#include <algorithm> #include <algorithm>
#include <random>
#include <type_traits> #include <type_traits>
namespace FRG { namespace FRG {
namespace {
// Set this to false to disable the battle-zone remote-player test bot.
constexpr bool kEnableBattleZoneTestBot = true;
constexpr const char* kBattleZoneCampaignId = "battle_zone";
constexpr const char* kBattleZoneTestBotId = "battle_zone_test_bot";
constexpr std::int64_t kBattleZoneTestBotIntervalMs = 5000;
}
InternalGameServer::InternalGameServer() = default; InternalGameServer::InternalGameServer() = default;
void InternalGameServer::ensureBattleZoneTestBot()
{
if (state_.campaignId != kBattleZoneCampaignId) return;
if (!kEnableBattleZoneTestBot) {
state_.players.erase(kBattleZoneTestBotId);
return;
}
auto& bot = state_.players[kBattleZoneTestBotId];
if (!bot) bot = std::make_shared<CharacterState>();
bot->configureAsPlayer();
bot->npcName = "Test Bot";
}
void InternalGameServer::updateBattleZoneTestBot(std::int64_t deltaMs)
{
if (!kEnableBattleZoneTestBot ||
state_.campaignId != kBattleZoneCampaignId ||
state_.currentLocationName.empty() ||
state_.players.find(kBattleZoneTestBotId) == state_.players.end()) {
return;
}
battleZoneTestBotElapsedMs_ += deltaMs;
if (battleZoneTestBotElapsedMs_ < kBattleZoneTestBotIntervalMs) return;
battleZoneTestBotElapsedMs_ %= kBattleZoneTestBotIntervalMs;
static std::mt19937 randomEngine(std::random_device{}());
static std::uniform_real_distribution<float> coordinate(-10.0f, 10.0f);
submit(SetCharacterDestinationCommand{
state_.currentLocationName,
kBattleZoneTestBotId,
Eigen::Vector3f(coordinate(randomEngine), 0.f, coordinate(randomEngine)),
std::string()});
}
void InternalGameServer::initializeCampaign(
const std::string& campaignId, const std::string& questsPath,
const std::string& zipFile)
{
state_.campaignId = campaignId;
state_.currentLocationName.clear();
state_.locations.clear();
state_.players.clear();
state_.questJournal.loadFromFile(questsPath, zipFile);
state_.globalFloats["player_hp"] = 200.f;
auto player = std::make_shared<CharacterState>();
player->configureAsPlayer();
state_.players.emplace(localPlayerId_, std::move(player));
battleZoneTestBotElapsedMs_ = 0;
ensureBattleZoneTestBot();
publishMutation();
}
void InternalGameServer::clearCampaign()
{
runtimeLocations_.clear();
state_.currentLocationName.clear();
state_.players.clear();
state_.locations.clear();
battleZoneTestBotElapsedMs_ = 0;
publishMutation();
}
void InternalGameServer::resetCampaign(
const std::string& questsPath, const std::string& zipFile)
{
pendingEvents_.clear();
state_.currentLocationName.clear();
state_.isDarklands = false;
state_.isNight = false;
state_.isDawn = false;
state_.playerHp = 200.f;
state_.playerMaxHp = 200.f;
state_.money = 5500;
state_.globalInts.clear();
state_.globalFloats.clear();
state_.globalFloats["player_hp"] = 200.f;
state_.tutorialStep = TutorialStep::AdvanceDialogue;
state_.tutorialPhoneChatScreenOpened = false;
state_.tutorialJournalScreenOpened = false;
state_.tutorialNeedOpenTaxiScreen = false;
state_.tutorialTaxiHintDone = false;
for (int i = 0; i < 3; ++i) {
state_.chatUnread[i] = true;
state_.chatPreviewMsg[i].clear();
state_.chatHistory[i].clear();
}
state_.inventory.clear();
state_.questJournal.loadFromFile(questsPath, zipFile);
state_.locations.clear();
battleZoneTestBotElapsedMs_ = 0;
ensureBattleZoneTestBot();
publishMutation();
}
bool InternalGameServer::prepareLocation(const LocationConfig& config)
{
LocationGameState& authoritative = state_.locations[config.id];
const bool initializeLocation = !authoritative.hasPersistentState;
if (!authoritative.location)
authoritative.location = std::make_shared<LocationState>();
if (authoritative.location->navigationMaps.empty())
authoritative.location->initNavigation(config.navigationJsonPaths);
authoritative.lockInteractiveObjectsDuringTutorial =
config.lockInteractiveObjectsDuringTutorial;
if (config.taxi) {
const LocationConfig::Taxi& taxi = *config.taxi;
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 (initializeLocation) authoritative.resetTaxi();
}
publishMutation();
return initializeLocation;
}
void InternalGameServer::setupRuntimeLocation(
const std::string& id, const std::shared_ptr<Location>& location,
LocationSetup params, AudioPlayerAsync* audioPlayer)
{
auto stored = state_.locations.find(id);
if (stored == state_.locations.end())
throw std::runtime_error("Location state was not prepared");
params.gameState = &stored->second;
location->setup(params, audioPlayer);
publishMutation();
}
Location* InternalGameServer::runtimeLocation(const std::string& id) const
{
auto it = runtimeLocations_.find(id);
return it != runtimeLocations_.end() ? it->second.get() : nullptr;
}
bool InternalGameServer::hasRuntimeLocation(const std::string& id) const
{
return runtimeLocations_.count(id) > 0;
}
std::vector<std::string> InternalGameServer::loadedRuntimeLocationIds() const
{
std::vector<std::string> result;
result.reserve(runtimeLocations_.size());
for (const auto& [id, location] : runtimeLocations_) result.push_back(id);
return result;
}
void InternalGameServer::clearRuntimeLocations(bool retainState)
{
if (retainState) {
synchronizeRuntimeAuxiliaryState();
} else {
for (const auto& [id, location] : runtimeLocations_)
state_.locations.erase(id);
publishMutation();
}
runtimeLocations_.clear();
}
GameSnapshot InternalGameServer::buildSnapshot() const GameSnapshot InternalGameServer::buildSnapshot() const
{ {
GameSnapshot result; GameSnapshot result;
@@ -85,7 +265,11 @@ std::shared_ptr<const GameSnapshot> InternalGameServer::snapshot() const
void InternalGameServer::submit(const GameCommand& command) void InternalGameServer::submit(const GameCommand& command)
{ {
std::visit([this](const auto& value) { auto enqueueQuestEvents = [this](Quest::QuestMutationResult result) {
for (Quest::QuestEvent& event : result.events)
pendingEvents_.emplace_back(std::move(event));
};
std::visit([this, &enqueueQuestEvents](const auto& value) {
using T = std::decay_t<decltype(value)>; using T = std::decay_t<decltype(value)>;
if constexpr (std::is_same_v<T, SetWorldModeCommand>) { if constexpr (std::is_same_v<T, SetWorldModeCommand>) {
state_.isDarklands = value.darklands; state_.isDarklands = value.darklands;
@@ -126,10 +310,83 @@ void InternalGameServer::submit(const GameCommand& command)
auto location = state_.locations.find(value.locationId); auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end()) if (location != state_.locations.end())
location->second.startTaxi(); location->second.startTaxi();
} else if constexpr (std::is_same_v<T, ResetTaxiCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end())
location->second.resetTaxi();
} else if constexpr (std::is_same_v<T, SetLocationCameraCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
location->second.location->cameraAzimuth = value.azimuth;
location->second.location->cameraInclination = value.inclination;
}
} else if constexpr (std::is_same_v<T, SetLocationTargetsCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
location->second.location->targetInteractiveObjectId =
value.interactiveObjectId;
location->second.location->targetInteractNpcId = value.npcId;
location->second.location->targetTeleportZoneId =
value.teleportZoneId;
}
} else if constexpr (std::is_same_v<T, SwitchLocationNavigationCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location &&
value.index >= 0 &&
value.index < static_cast<int>(
location->second.location->navigationMaps.size())) {
LocationState& locationState = *location->second.location;
locationState.activeNavigationIndex = value.index;
for (const auto& [id, player] : state_.players)
if (player) player->forceReplan(locationState, id);
for (const auto& [id, npc] : location->second.npcs)
if (npc) npc->forceReplan(locationState, id);
}
} else if constexpr (std::is_same_v<T, SetNavigationAreaAvailableCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
LocationState& locationState = *location->second.location;
const int index = locationState.activeNavigationIndex;
if (index >= 0 &&
index < static_cast<int>(locationState.navigationMaps.size())) {
locationState.navigationMaps[index].setAreaAvailable(
value.areaName, value.available);
}
}
} else if constexpr (std::is_same_v<T, AddNavigationObstacleCommand>) {
auto location = state_.locations.find(value.locationId);
if (location != state_.locations.end() && location->second.location) {
LocationState& locationState = *location->second.location;
const int index = locationState.activeNavigationIndex;
if (index >= 0 &&
index < static_cast<int>(locationState.navigationMaps.size())) {
PathFinder::ObstaclePolygon polygon;
polygon.name = value.name;
polygon.polygon = value.polygon;
locationState.navigationMaps[index].addObstaclePolygon(polygon);
}
}
} else if constexpr (std::is_same_v<T, AddInventoryItemCommand>) { } else if constexpr (std::is_same_v<T, AddInventoryItemCommand>) {
state_.inventory.addItem(value.itemId); if (state_.inventory.addItem(value.itemId))
pendingEvents_.emplace_back(InventoryItemAddedEvent{value.itemId});
} else if constexpr (std::is_same_v<T, RemoveInventoryItemCommand>) { } else if constexpr (std::is_same_v<T, RemoveInventoryItemCommand>) {
state_.inventory.removeItem(value.itemId); if (state_.inventory.removeItem(value.itemId))
pendingEvents_.emplace_back(InventoryItemRemovedEvent{value.itemId});
} else if constexpr (std::is_same_v<T, UnlockQuestCommand>) {
enqueueQuestEvents(state_.questJournal.unlockQuest(value.questId));
} else if constexpr (std::is_same_v<T, CompleteQuestCommand>) {
enqueueQuestEvents(state_.questJournal.completeQuest(value.questId));
} else if constexpr (std::is_same_v<T, FailQuestCommand>) {
enqueueQuestEvents(state_.questJournal.failQuest(value.questId));
} else if constexpr (std::is_same_v<T, SetQuestObjectiveCompletedCommand>) {
enqueueQuestEvents(state_.questJournal.setObjectiveCompleted(
value.questId, value.objectiveId, value.completed));
} else if constexpr (std::is_same_v<T, SetQuestObjectiveVisibleCommand>) {
enqueueQuestEvents(state_.questJournal.setObjectiveVisible(
value.questId, value.objectiveId, value.visible));
} else if constexpr (std::is_same_v<T, SetQuestActiveObjectiveCommand>) {
enqueueQuestEvents(state_.questJournal.setActiveObjective(
value.questId, value.objectiveIndex));
} else if constexpr (std::is_same_v<T, SetGlobalIntCommand>) { } else if constexpr (std::is_same_v<T, SetGlobalIntCommand>) {
state_.globalInts[value.key] = value.value; state_.globalInts[value.key] = value.value;
} else if constexpr (std::is_same_v<T, SetGlobalFloatCommand>) { } else if constexpr (std::is_same_v<T, SetGlobalFloatCommand>) {
@@ -225,7 +482,14 @@ void InternalGameServer::submit(const GameCommand& command)
state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile); state_.questJournal.reloadDefinitionsPreservingState(value.path, value.zipFile);
} }
}, command); }, command);
markHostMutation(); publishMutation();
}
std::vector<GameServerEvent> InternalGameServer::drainEvents()
{
std::vector<GameServerEvent> result;
result.swap(pendingEvents_);
return result;
} }
void InternalGameServer::update(std::int64_t deltaMs) void InternalGameServer::update(std::int64_t deltaMs)
@@ -234,11 +498,15 @@ void InternalGameServer::update(std::int64_t deltaMs)
if (runtime == runtimeLocations_.end() || !runtime->second) return; if (runtime == runtimeLocations_.end() || !runtime->second) return;
auto stored = state_.locations.find(state_.currentLocationName); auto stored = state_.locations.find(state_.currentLocationName);
if (stored == state_.locations.end()) return; if (stored == state_.locations.end()) return;
updateBattleZoneTestBot(deltaMs);
const bool interactiveObjectsLocked = const bool interactiveObjectsLocked =
stored->second.lockInteractiveObjectsDuringTutorial && stored->second.lockInteractiveObjectsDuringTutorial &&
state_.tutorialStep < TutorialStep::PickUpItem; state_.tutorialStep < TutorialStep::PickUpItem;
LocationSimulationEvents events = LocationSimulationEvents events =
stored->second.update(deltaMs, state_.players, interactiveObjectsLocked); stored->second.update(deltaMs, state_.players, interactiveObjectsLocked);
// Runtime presentation reads through snapshots, so publish the simulation
// tick before forwarding its value-only events to the loaded location.
publishMutation();
runtime->second->update(deltaMs, events); runtime->second->update(deltaMs, events);
synchronizeRuntimeAuxiliaryState(); synchronizeRuntimeAuxiliaryState();
} }
@@ -251,9 +519,12 @@ void InternalGameServer::save(nlohmann::json& out) const
void InternalGameServer::load(const nlohmann::json& in) void InternalGameServer::load(const nlohmann::json& in)
{ {
pendingEvents_.clear();
state_.load(in); state_.load(in);
battleZoneTestBotElapsedMs_ = 0;
ensureBattleZoneTestBot();
applyStateToRuntime(); applyStateToRuntime();
markHostMutation(); publishMutation();
} }
void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location) void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location)
@@ -265,7 +536,7 @@ void InternalGameServer::bindRuntimeLocation(const std::string& id, std::shared_
} else { } else {
runtimeLocations_[id]->captureAuxiliaryState(state_.locations[id]); runtimeLocations_[id]->captureAuxiliaryState(state_.locations[id]);
} }
markHostMutation(); publishMutation();
} }
void InternalGameServer::synchronizeRuntimeAuxiliaryState() void InternalGameServer::synchronizeRuntimeAuxiliaryState()
@@ -274,7 +545,7 @@ void InternalGameServer::synchronizeRuntimeAuxiliaryState()
if (!location) continue; if (!location) continue;
location->captureAuxiliaryState(state_.locations[id]); location->captureAuxiliaryState(state_.locations[id]);
} }
markHostMutation(); publishMutation();
} }
void InternalGameServer::applyStateToRuntime() void InternalGameServer::applyStateToRuntime()
@@ -286,7 +557,7 @@ void InternalGameServer::applyStateToRuntime()
} }
} }
void InternalGameServer::markHostMutation() void InternalGameServer::publishMutation()
{ {
++revision_; ++revision_;
cachedSnapshot_.reset(); cachedSnapshot_.reset();
+27 -8
View File
@@ -2,11 +2,15 @@
#include "IGameServer.h" #include "IGameServer.h"
class AudioPlayerAsync;
namespace FRG { namespace FRG {
class Game; class Game;
class Character; class Character;
class Location; class Location;
struct LocationConfig;
struct LocationSetup;
// In-process implementation used by single-player. GameState exists exactly // In-process implementation used by single-player. GameState exists exactly
// once, here; clients see immutable snapshots and mutate it with typed commands. // once, here; clients see immutable snapshots and mutate it with typed commands.
@@ -15,7 +19,9 @@ public:
InternalGameServer(); InternalGameServer();
std::shared_ptr<const GameSnapshot> snapshot() const override; std::shared_ptr<const GameSnapshot> snapshot() const override;
const std::string& localPlayerId() const override { return localPlayerId_; }
void submit(const GameCommand& command) override; void submit(const GameCommand& command) override;
std::vector<GameServerEvent> drainEvents() override;
void update(std::int64_t deltaMs) override; void update(std::int64_t deltaMs) override;
void save(nlohmann::json& out) const override; void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override; void load(const nlohmann::json& in) override;
@@ -23,25 +29,38 @@ public:
private: private:
friend class Game; friend class Game;
// Transitional host operations are intentionally available only to Game, // Host-only lifecycle operations. They never expose authoritative state and
// which owns location loading and old-save migration. They are never part of // are intentionally absent from IGameServer.
// IGameServer and therefore cannot leak mutable state to UI/network clients. void initializeCampaign(
GameState& hostState() { return state_; } const std::string& campaignId, const std::string& questsPath,
const GameState& hostState() const { return state_; } const std::string& zipFile);
std::unordered_map<std::string, std::shared_ptr<Location>>& runtimeLocations() { return runtimeLocations_; } void clearCampaign();
const std::unordered_map<std::string, std::shared_ptr<Location>>& runtimeLocations() const { return runtimeLocations_; } void resetCampaign(const std::string& questsPath, const std::string& zipFile);
bool prepareLocation(const LocationConfig& config);
void setupRuntimeLocation(
const std::string& id, const std::shared_ptr<Location>& location,
LocationSetup params, AudioPlayerAsync* audioPlayer);
Location* runtimeLocation(const std::string& id) const;
bool hasRuntimeLocation(const std::string& id) const;
std::vector<std::string> loadedRuntimeLocationIds() const;
void clearRuntimeLocations(bool retainState);
void bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location); void bindRuntimeLocation(const std::string& id, std::shared_ptr<Location> location);
void synchronizeRuntimeAuxiliaryState(); void synchronizeRuntimeAuxiliaryState();
void applyStateToRuntime(); void applyStateToRuntime();
void markHostMutation(); void publishMutation();
void ensureBattleZoneTestBot();
void updateBattleZoneTestBot(std::int64_t deltaMs);
GameSnapshot buildSnapshot() const; GameSnapshot buildSnapshot() const;
GameState state_; GameState state_;
std::string localPlayerId_ = "player";
std::int64_t battleZoneTestBotElapsedMs_ = 0;
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_; std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
std::uint64_t revision_ = 1; std::uint64_t revision_ = 1;
mutable std::uint64_t cachedRevision_ = 0; mutable std::uint64_t cachedRevision_ = 0;
mutable std::shared_ptr<const GameSnapshot> cachedSnapshot_; mutable std::shared_ptr<const GameSnapshot> cachedSnapshot_;
std::vector<GameServerEvent> pendingEvents_;
}; };
} // namespace FRG } // namespace FRG
+5 -4
View File
@@ -8,19 +8,20 @@
namespace FRG { namespace FRG {
void Inventory::addItem(const std::string& itemId) { bool Inventory::addItem(const std::string& itemId) {
itemIds.push_back(itemId); itemIds.push_back(itemId);
logger() << "Item added to inventory: " << itemId << std::endl; logger() << "Item added to inventory: " << itemId << std::endl;
if (onItemAdded) onItemAdded(itemId); return true;
} }
void Inventory::removeItem(const std::string& itemId) { bool Inventory::removeItem(const std::string& itemId) {
auto it = std::find(itemIds.begin(), itemIds.end(), itemId); auto it = std::find(itemIds.begin(), itemIds.end(), itemId);
if (it != itemIds.end()) { if (it != itemIds.end()) {
logger() << "Item removed from inventory: " << itemId << std::endl; logger() << "Item removed from inventory: " << itemId << std::endl;
if (onItemRemoved) onItemRemoved(itemId);
itemIds.erase(it); itemIds.erase(it);
return true;
} }
return false;
} }
bool Inventory::hasItem(const std::string& itemId) const { bool Inventory::hasItem(const std::string& itemId) const {
+2 -7
View File
@@ -2,7 +2,6 @@
#include <string> #include <string>
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <functional>
#include "ISaveable.h" #include "ISaveable.h"
namespace FRG { namespace FRG {
@@ -22,8 +21,8 @@ namespace FRG {
class Inventory : public ISaveable { class Inventory : public ISaveable {
public: public:
void addItem(const std::string& itemId); bool addItem(const std::string& itemId);
void removeItem(const std::string& itemId); bool removeItem(const std::string& itemId);
// Resolves current items from ItemRegistry by id, so returned data // Resolves current items from ItemRegistry by id, so returned data
// always reflects the currently loaded (localized) definitions. // always reflects the currently loaded (localized) definitions.
std::vector<Item> getItems() const; std::vector<Item> getItems() const;
@@ -34,10 +33,6 @@ namespace FRG {
void save(nlohmann::json& out) const override; void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override; void load(const nlohmann::json& in) override;
// Called whenever an item is added/removed; receives the item id.
std::function<void(const std::string&)> onItemAdded;
std::function<void(const std::string&)> onItemRemoved;
private: private:
std::vector<std::string> itemIds; std::vector<std::string> itemIds;
}; };
+43 -26
View File
@@ -106,38 +106,48 @@ bool QuestJournal::setStatus(const std::string& questId, QuestStatus status) {
return true; return true;
} }
bool QuestJournal::unlockQuest(const std::string& questId) { QuestMutationResult QuestJournal::unlockQuest(const std::string& questId) {
QuestMutationResult result;
QuestState* quest = findQuest(questId); QuestState* quest = findQuest(questId);
if (!quest) return false; if (!quest) return result;
result.success = true;
if (quest->status == QuestStatus::Hidden) { if (quest->status == QuestStatus::Hidden) {
quest->status = QuestStatus::Available; quest->status = QuestStatus::Available;
if (onQuestUnlocked) onQuestUnlocked(questId); result.events.push_back({QuestEventType::Unlocked, questId, {}});
} }
return true; return result;
} }
bool QuestJournal::completeQuest(const std::string& questId) { QuestMutationResult QuestJournal::completeQuest(const std::string& questId) {
bool changed = setStatus(questId, QuestStatus::Completed); QuestMutationResult result;
if (changed && onQuestCompleted) onQuestCompleted(questId); result.success = setStatus(questId, QuestStatus::Completed);
return changed; if (result.success)
result.events.push_back({QuestEventType::Completed, questId, {}});
return result;
} }
bool QuestJournal::failQuest(const std::string& questId) { QuestMutationResult QuestJournal::failQuest(const std::string& questId) {
bool changed = setStatus(questId, QuestStatus::Failed); QuestMutationResult result;
if (changed && onQuestFailed) onQuestFailed(questId); result.success = setStatus(questId, QuestStatus::Failed);
return changed; if (result.success)
result.events.push_back({QuestEventType::Failed, questId, {}});
return result;
} }
bool QuestJournal::setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed) { QuestMutationResult QuestJournal::setObjectiveCompleted(
const std::string& questId, const std::string& objectiveId, bool completed) {
QuestMutationResult result;
QuestState* quest = findQuest(questId); QuestState* quest = findQuest(questId);
if (!quest) return false; if (!quest) return result;
for (auto& objective : quest->definition.objectives) { for (auto& objective : quest->definition.objectives) {
if (objective.id == objectiveId) { if (objective.id == objectiveId) {
objective.completed = completed; objective.completed = completed;
result.success = true;
if (completed) { if (completed) {
if (onObjectiveCompleted) onObjectiveCompleted(questId, objectiveId); result.events.push_back(
{QuestEventType::ObjectiveCompleted, questId, objectiveId});
} }
if (completed && quest->definition.autoComplete && quest->status == QuestStatus::Available) { if (completed && quest->definition.autoComplete && quest->status == QuestStatus::Available) {
@@ -147,41 +157,48 @@ bool QuestJournal::setObjectiveCompleted(const std::string& questId, const std::
[](const QuestObjective& o) { return o.completed; }); [](const QuestObjective& o) { return o.completed; });
if (allDone) { if (allDone) {
quest->status = QuestStatus::Completed; quest->status = QuestStatus::Completed;
if (onQuestCompleted) onQuestCompleted(questId); result.events.push_back(
{QuestEventType::Completed, questId, {}});
} }
} }
return true; return result;
} }
} }
return false; return result;
} }
bool QuestJournal::setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible) { QuestMutationResult QuestJournal::setObjectiveVisible(
const std::string& questId, const std::string& objectiveId, bool visible) {
QuestMutationResult result;
QuestState* quest = findQuest(questId); QuestState* quest = findQuest(questId);
if (!quest) return false; if (!quest) return result;
for (auto& objective : quest->definition.objectives) { for (auto& objective : quest->definition.objectives) {
if (objective.id == objectiveId) { if (objective.id == objectiveId) {
objective.visible = visible; objective.visible = visible;
return true; result.success = true;
return result;
} }
} }
return false; return result;
} }
bool QuestJournal::setActiveObjective(const std::string& questId, int objectiveIndex) { QuestMutationResult QuestJournal::setActiveObjective(
const std::string& questId, int objectiveIndex) {
QuestMutationResult result;
QuestState* quest = findQuest(questId); QuestState* quest = findQuest(questId);
if (!quest) return false; if (!quest) return result;
if (objectiveIndex < 0 || objectiveIndex >= static_cast<int>(quest->definition.objectives.size())) { if (objectiveIndex < 0 || objectiveIndex >= static_cast<int>(quest->definition.objectives.size())) {
return false; return result;
} }
quest->activeObjectiveIndex = objectiveIndex; quest->activeObjectiveIndex = objectiveIndex;
return true; result.success = true;
return result;
} }
QuestState* QuestJournal::findQuest(const std::string& questId) { QuestState* QuestJournal::findQuest(const std::string& questId) {
+24 -13
View File
@@ -6,10 +6,27 @@
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
#include <functional>
namespace FRG::Quest { namespace FRG::Quest {
enum class QuestEventType {
Unlocked,
Completed,
Failed,
ObjectiveCompleted
};
struct QuestEvent {
QuestEventType type;
std::string questId;
std::string objectiveId;
};
struct QuestMutationResult {
bool success = false;
std::vector<QuestEvent> events;
};
class QuestJournal : public FRG::ISaveable { class QuestJournal : public FRG::ISaveable {
public: public:
bool loadFromFile(const std::string& path, const std::string& zipFile = ""); bool loadFromFile(const std::string& path, const std::string& zipFile = "");
@@ -22,19 +39,13 @@ public:
void save(nlohmann::json& out) const override; void save(nlohmann::json& out) const override;
void load(const nlohmann::json& in) override; void load(const nlohmann::json& in) override;
// Event callbacks — set by the owner (MenuManager) to receive notifications. QuestMutationResult unlockQuest(const std::string& questId);
std::function<void(const std::string& questId)> onQuestUnlocked; QuestMutationResult completeQuest(const std::string& questId);
std::function<void(const std::string& questId)> onQuestCompleted; QuestMutationResult failQuest(const std::string& questId);
std::function<void(const std::string& questId)> onQuestFailed;
std::function<void(const std::string& questId, const std::string& objectiveId)> onObjectiveCompleted;
bool unlockQuest(const std::string& questId); QuestMutationResult setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed = true);
bool completeQuest(const std::string& questId); QuestMutationResult setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible = true);
bool failQuest(const std::string& questId); QuestMutationResult setActiveObjective(const std::string& questId, int objectiveIndex);
bool setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed = true);
bool setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible = true);
bool setActiveObjective(const std::string& questId, int objectiveIndex);
QuestState* findQuest(const std::string& questId); QuestState* findQuest(const std::string& questId);
const QuestState* findQuest(const std::string& questId) const; const QuestState* findQuest(const std::string& questId) const;
+8 -6
View File
@@ -100,13 +100,15 @@ namespace FRG {
break; break;
} }
auto state = player.stateSnapshot();
if (!state) return;
std::vector<Eigen::Vector3f> route; std::vector<Eigen::Vector3f> route;
route.reserve(player.characterState.pathWaypoints.size() + 1); route.reserve(state->pathWaypoints.size() + 1);
route.push_back(Eigen::Vector3f(player.characterState.position.x(), 0.0f, player.characterState.position.z())); route.push_back(Eigen::Vector3f(state->position.x(), 0.0f, state->position.z()));
const size_t firstWaypoint = (std::min)( const size_t firstWaypoint = (std::min)(
player.characterState.currentWaypointIndex, player.characterState.pathWaypoints.size()); state->currentWaypointIndex, state->pathWaypoints.size());
for (size_t i = firstWaypoint; i < player.characterState.pathWaypoints.size(); ++i) { for (size_t i = firstWaypoint; i < state->pathWaypoints.size(); ++i) {
const Eigen::Vector3f& waypoint = player.characterState.pathWaypoints[i]; const Eigen::Vector3f& waypoint = state->pathWaypoints[i];
route.push_back(Eigen::Vector3f(waypoint.x(), 0.0f, waypoint.z())); route.push_back(Eigen::Vector3f(waypoint.x(), 0.0f, waypoint.z()));
} }
@@ -127,7 +129,7 @@ namespace FRG {
constexpr float kPlayerClearanceMs = 20.0f; constexpr float kPlayerClearanceMs = 20.0f;
constexpr float kPlayerMaximumSpeedMultiplier = 1.5f; constexpr float kPlayerMaximumSpeedMultiplier = 1.5f;
const float playerSpeed = (std::max)( const float playerSpeed = (std::max)(
player.characterState.walkSpeed * kPlayerMaximumSpeedMultiplier, 0.01f); state->walkSpeed * kPlayerMaximumSpeedMultiplier, 0.01f);
const float playerRouteTimeMs = totalLength / playerSpeed * 1000.0f; const float playerRouteTimeMs = totalLength / playerSpeed * 1000.0f;
const float revealDurationMs = (std::min)(kMaximumRevealMs, playerRouteTimeMs / 3.0f); const float revealDurationMs = (std::min)(kMaximumRevealMs, playerRouteTimeMs / 3.0f);
const float waveSpeed = totalLength / (revealDurationMs / 1000.0f); const float waveSpeed = totalLength / (revealDurationMs / 1000.0f);