More work on states, added bot
This commit is contained in:
1 parent
66e93d46f4
commit
5e9a663812
25 files changed
+1595
-740
No files matched your search
+85
-31
@@ -12,7 +12,7 @@ The architecture has four layers:
|
||||
|
||||
1. `GameState` is the aggregate root for persistent, authoritative state.
|
||||
2. `InternalGameServer` owns `GameState`, advances the local simulation, handles commands, and produces immutable snapshots.
|
||||
3. Runtime objects such as `Location`, `Character`, and `InteractiveObject` hold rendering resources and non-owning bindings to server-owned state. `Location` also translates local input, scripts, and collision results into semantic server commands.
|
||||
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`.
|
||||
|
||||
```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.
|
||||
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
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`.
|
||||
Its update boundary receives `GameState::players` by reference, allowing character
|
||||
lookup and simulation to treat every campaign player by stable ID.
|
||||
@@ -70,7 +77,12 @@ Each location record owns:
|
||||
- Serialized dialogue runtime state.
|
||||
- Serialized Lua runtime globals.
|
||||
|
||||
State objects use `shared_ptr` internally to keep their addresses stable while local runtime objects are bound to them. These pointers never appear in public snapshots.
|
||||
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
|
||||
|
||||
@@ -121,6 +133,7 @@ Snapshots have a monotonically increasing revision. `InternalGameServer` caches
|
||||
- Chat history and unread state.
|
||||
- Taxi state.
|
||||
- Inventory additions and removals.
|
||||
- Quest status, objective completion/visibility, and active-objective changes.
|
||||
- Script globals.
|
||||
- Character transforms, attack targets, navigation destinations, stopping,
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
`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.
|
||||
- Delegates save/load serialization to the aggregate state model.
|
||||
|
||||
`Game` remains a privileged host during this migration. It can access private
|
||||
internal-server operations for campaign loading, location construction, callback
|
||||
wiring, and persistence restoration. These host operations are not exposed by
|
||||
`IGameServer`.
|
||||
`Game` remains a privileged lifecycle host, but it no longer holds or receives a
|
||||
mutable `GameState` reference. It uses private, focused internal-server operations
|
||||
for campaign loading and location construction. Those operations publish revisions
|
||||
themselves and are not exposed by `IGameServer`.
|
||||
|
||||
### Animation and combat
|
||||
|
||||
@@ -180,16 +201,25 @@ collision handling, and presentation-event processing.
|
||||
|
||||
## 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.
|
||||
- `Character` keeps a private non-owning **const** reference named `characterState`, plus stable entity and location IDs.
|
||||
- `Location` keeps only its `IGameServer` reference and stable location ID. It resolves its immutable `LocationState` view from `IGameServer::snapshot()`.
|
||||
- `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.
|
||||
|
||||
The bound state objects are owned by `GameState` or `LocationGameState`. The internal
|
||||
server advances them directly before runtime presentation. Rendering reads the bound
|
||||
state; runtime input, scripts, and collision handling submit semantic commands that
|
||||
invoke `CharacterState` operations against the authoritative `LocationState`.
|
||||
`Character::stateSnapshot()` and `Location::stateSnapshot()` return aliasing
|
||||
`shared_ptr` views. Each pointer addresses a state copy inside a `GameSnapshot`,
|
||||
while shared ownership keeps that whole immutable snapshot alive. Update, input,
|
||||
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
|
||||
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`
|
||||
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:
|
||||
|
||||
1. Loads static creation data from location configuration.
|
||||
2. Creates or reuses server-owned state objects.
|
||||
3. Applies saved runtime fields over the configuration defaults.
|
||||
4. Constructs visual objects bound to those state objects.
|
||||
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.
|
||||
|
||||
@@ -225,11 +262,14 @@ The normal frame flow is:
|
||||
back as semantic destination commands, whose server handler calls
|
||||
`CharacterState::setTarget()` with the authoritative `LocationState`; arrival
|
||||
function names are resolved through `ScriptEngine`.
|
||||
6. Runtime characters apply the authoritative animation frame to loaded models and
|
||||
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.
|
||||
7. Auxiliary dialogue, Lua, trigger, and teleport state is captured into `LocationGameState`.
|
||||
8. The server increments its revision and invalidates the cached snapshot.
|
||||
9. Rendering reads the bound local state; UI and external-style consumers read an immutable snapshot.
|
||||
8. Auxiliary dialogue, Lua, trigger, and teleport state is captured into `LocationGameState`.
|
||||
9. The server publishes those auxiliary changes in a later revision.
|
||||
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.
|
||||
|
||||
@@ -244,7 +284,7 @@ When a campaign starts:
|
||||
- Existing campaign state and runtime locations are cleared.
|
||||
- Quest definitions are loaded.
|
||||
- A campaign-level player state is created under the `"player"` ID.
|
||||
- The visual player is created and bound to that state.
|
||||
- The visual player is created with its stable ID and resolves that state through snapshots.
|
||||
|
||||
### Location loading
|
||||
|
||||
@@ -254,6 +294,8 @@ When a location is loaded:
|
||||
- Static configuration is loaded.
|
||||
- NPC and interactive-object visuals are created against state owned by the location record.
|
||||
- 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.
|
||||
- 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.
|
||||
- Immutable content such as meshes, initial taxi configuration, and object
|
||||
definitions is reconstructed from campaign configuration.
|
||||
- Configuration is loaded first and mutable saved state is overlaid in place, so
|
||||
runtime render bindings keep stable addresses.
|
||||
- Configuration is loaded first and mutable saved state is overlaid in place.
|
||||
Snapshot-backed runtime objects then resolve the resulting state by stable ID.
|
||||
|
||||
Old user save slots are intentionally unsupported. Bundled start-state snapshots
|
||||
must be regenerated with `Game::saveInitialState()` whenever the schema changes.
|
||||
@@ -298,11 +340,14 @@ It now:
|
||||
|
||||
### Scripts
|
||||
|
||||
Lua remains an in-process single-player runtime. Character bindings submit semantic
|
||||
commands through `Location`; the server invokes `CharacterState` operations on the
|
||||
centrally owned state. NPC, interactive-object, trigger-zone, and teleport-zone
|
||||
bindings address entities by configured ID. Object, inventory, quest, and
|
||||
global-state bindings remain trusted local integrations.
|
||||
Lua remains an in-process single-player runtime. Character, inventory, quest, and
|
||||
campaign-global bindings submit semantic commands through `Location`; the server
|
||||
invokes operations on centrally owned state. Snapshot queries provide global
|
||||
values, inventory counts, item presence, and synchronous quest validation for the
|
||||
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.
|
||||
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 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.
|
||||
|
||||
### 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
|
||||
|
||||
@@ -372,7 +422,11 @@ cmake --build proj-windows/build --config Debug --target ShadowOverBishkekDemo -
|
||||
location-local NPC ID).
|
||||
- Some top-level values remain mirrors for compatibility, notably player health used by the HUD and scripts.
|
||||
- Dialogue and Lua are synchronized as auxiliary location state rather than fully networked subsystems.
|
||||
- `Game` currently uses privileged internal-server APIs for local content construction. A remote client implementation will need a client-side runtime-location factory driven by configuration and snapshots.
|
||||
- `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
|
||||
|
||||
@@ -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/Location.h` / `src/Location.cpp` — loaded location runtime, command routing,
|
||||
interactions, collisions, scripts, and presentation-event consumption.
|
||||
- `src/Character.h` / `src/Character.cpp` — passive character presentation bound to
|
||||
a const authoritative `CharacterState`.
|
||||
- `src/Character.h` / `src/Character.cpp` — passive character presentation resolving
|
||||
immutable `CharacterState` snapshots by stable ID.
|
||||
- `src/CharacterState.h` / `src/CharacterState.cpp` — deterministic character
|
||||
simulation, navigation operations, combat state, and logical animation clock.
|
||||
- `src/items/InteractiveObject.h` / `.cpp` — visual object runtime bound to central object state.
|
||||
|
||||
+235
-138
@@ -1,4 +1,4 @@
|
||||
#include "Character.h"
|
||||
#include "Character.h"
|
||||
#include "render/TextRenderer.h"
|
||||
#include <cmath>
|
||||
#include <iostream>
|
||||
@@ -19,6 +19,44 @@ namespace FRG {
|
||||
|
||||
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) {
|
||||
auto& data = animations[animState];
|
||||
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,
|
||||
const std::string& locationId, const std::string& entityId,
|
||||
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 : "";
|
||||
|
||||
@@ -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);
|
||||
|
||||
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()) {
|
||||
auto tex = loadTex(c->characterState.creationInfo.singleTexturePath);
|
||||
if (!st.creationInfo.singleTexturePath.empty()) {
|
||||
auto tex = loadTex(st.creationInfo.singleTexturePath);
|
||||
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);
|
||||
if (tex) c->setTexture(meshName, tex);
|
||||
}
|
||||
|
||||
if (!c->characterState.creationInfo.weaponMeshPath.empty()) {
|
||||
const std::string& wp = c->characterState.creationInfo.weaponMeshPath;
|
||||
if (!st.creationInfo.weaponMeshPath.empty()) {
|
||||
const std::string& wp = st.creationInfo.weaponMeshPath;
|
||||
try {
|
||||
if (wp.size() >= 4 && wp.substr(wp.size() - 4) == ".bin")
|
||||
c->weaponMesh.data = LoadModelFromBinFile(wp, zip);
|
||||
else
|
||||
c->weaponMesh.data = LoadFromTextFile02(wp, zip);
|
||||
|
||||
if (c->characterState.creationInfo.weaponMeshMove != Eigen::Vector3f::Zero())
|
||||
c->weaponMesh.data.Move(c->characterState.creationInfo.weaponMeshMove);
|
||||
if (c->characterState.creationInfo.weaponMeshScale != 1.0f)
|
||||
c->weaponMesh.data.Scale(c->characterState.creationInfo.weaponMeshScale);
|
||||
if (st.creationInfo.weaponMeshMove != Eigen::Vector3f::Zero())
|
||||
c->weaponMesh.data.Move(st.creationInfo.weaponMeshMove);
|
||||
if (st.creationInfo.weaponMeshScale != 1.0f)
|
||||
c->weaponMesh.data.Scale(st.creationInfo.weaponMeshScale);
|
||||
|
||||
c->weaponMesh.RefreshVBO();
|
||||
} 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->weaponAttachBoneName = c->characterState.creationInfo.weaponAttachBoneName;
|
||||
c->weaponInitialPosition = c->characterState.creationInfo.weaponInitialPosition;
|
||||
c->weaponInitialRotation = c->characterState.creationInfo.weaponInitialRotation.toRotationMatrix();
|
||||
c->weaponTexture = loadTex(st.creationInfo.weaponTexturePath);
|
||||
c->weaponAttachBoneName = st.creationInfo.weaponAttachBoneName;
|
||||
c->weaponInitialPosition = st.creationInfo.weaponInitialPosition;
|
||||
c->weaponInitialRotation = st.creationInfo.weaponInitialRotation.toRotationMatrix();
|
||||
|
||||
return c;
|
||||
}
|
||||
@@ -124,8 +162,8 @@ void Character::setTexture(const std::string& meshName, std::shared_ptr<Texture>
|
||||
meshTextures[meshName] = std::move(tex);
|
||||
}
|
||||
|
||||
AnimationState Character::resolveActiveState() const {
|
||||
if (animations.count(characterState.currentState)) return characterState.currentState;
|
||||
AnimationState Character::resolveActiveState(const CharacterState& state) const {
|
||||
if (animations.count(state.currentState)) return state.currentState;
|
||||
return AnimationState::STAND;
|
||||
}
|
||||
|
||||
@@ -134,29 +172,33 @@ void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
|
||||
hitSparkEmitter.update(static_cast<float>(deltaMs));
|
||||
}
|
||||
|
||||
auto stateSnapshot = this->stateSnapshot();
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
|
||||
if (events) {
|
||||
if (events->deathAnimationComplete && onDeathAnimComplete)
|
||||
onDeathAnimComplete();
|
||||
if (events->attackImpact && audioPlayer) {
|
||||
audioPlayer->playSoundAsync(characterState.isPlayer
|
||||
audioPlayer->playSoundAsync(state.isPlayer
|
||||
? "resources/audio/547041__cogfirestudios__hit-swing-sword-small-3.ogg"
|
||||
: "resources/audio/13925__adcbicycle__21.ogg");
|
||||
}
|
||||
}
|
||||
|
||||
if (!characterState.enabled) return;
|
||||
auto it = animations.find(characterState.currentState);
|
||||
if (!state.enabled) return;
|
||||
auto it = animations.find(state.currentState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
auto& anim = it->second;
|
||||
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) {
|
||||
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
|
||||
anim.model.startBones, anim.model.animations[0].keyFrames,
|
||||
static_cast<int>(anim.currentFrame));
|
||||
if (!characterState.useGpuSkinning) {
|
||||
if (!state.useGpuSkinning) {
|
||||
anim.model.Interpolate(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);
|
||||
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) {
|
||||
drawGpuSkinning(renderer);
|
||||
if (state.useGpuSkinning) {
|
||||
drawGpuSkinning(renderer, state);
|
||||
return;
|
||||
}
|
||||
|
||||
AnimationState drawState = resolveActiveState();
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
@@ -192,10 +237,10 @@ void Character::draw(Renderer& renderer) {
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
auto& anim = it->second;
|
||||
for (const auto& name : anim.model.meshNamesOrdered) {
|
||||
@@ -211,14 +256,14 @@ void Character::draw(Renderer& renderer) {
|
||||
|
||||
renderer.PopMatrix();
|
||||
|
||||
drawAttachedWeapon(renderer);
|
||||
drawAttachedWeapon(renderer, state);
|
||||
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
}
|
||||
|
||||
void Character::drawGpuSkinning(Renderer& renderer) {
|
||||
AnimationState drawState = resolveActiveState();
|
||||
void Character::drawGpuSkinning(Renderer& renderer, const CharacterState& state) {
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
@@ -246,10 +291,10 @@ void Character::drawGpuSkinning(Renderer& renderer) {
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
||||
static_cast<int>(anim.gpuSkinningShaderData.skinningMatrices.size()), false,
|
||||
@@ -269,15 +314,15 @@ void Character::drawGpuSkinning(Renderer& renderer) {
|
||||
renderer.shaderManager.PushShader("fog");
|
||||
renderer.RenderUniform1i(textureUniformName, 0);
|
||||
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkin);
|
||||
drawAttachedWeapon(renderer);
|
||||
drawAttachedWeapon(renderer, state);
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
}
|
||||
|
||||
bool Character::prepareGpuSkinning() {
|
||||
AnimationState drawState = resolveActiveState();
|
||||
bool Character::prepareGpuSkinning(const CharacterState& state) {
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
if (it == animations.end()) return false;
|
||||
|
||||
@@ -293,11 +338,11 @@ bool Character::prepareGpuSkinning() {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Character::drawAttachedWeapon(Renderer& renderer) {
|
||||
if (!characterState.showWeapon) return;
|
||||
void Character::drawAttachedWeapon(Renderer& renderer, const CharacterState& state) {
|
||||
if (!state.showWeapon) 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;
|
||||
|
||||
auto& anim = it->second;
|
||||
@@ -310,10 +355,10 @@ void Character::drawAttachedWeapon(Renderer& renderer) {
|
||||
Eigen::Matrix4f currentBone = anim.gpuSkinningShaderData.skinningMatrices[boneIdx] * bindBone;
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix(Eigen::Vector3f(currentBone.block<3, 1>(0, 3)));
|
||||
renderer.RotateMatrix(Eigen::Matrix3f(currentBone.block<3, 3>(0, 0)));
|
||||
renderer.TranslateMatrix(weaponInitialPosition);
|
||||
@@ -327,24 +372,27 @@ void Character::drawAttachedWeapon(Renderer& renderer) {
|
||||
// ==================== Shadow depth pass ====================
|
||||
|
||||
void Character::drawShadowDepth(Renderer& renderer) {
|
||||
if (!characterState.enabled) return;
|
||||
if (characterState.useGpuSkinning) {
|
||||
drawShadowDepthGpuSkinning(renderer);
|
||||
auto stateSnapshot = this->stateSnapshot();
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
if (!state.enabled) return;
|
||||
if (state.useGpuSkinning) {
|
||||
drawShadowDepthGpuSkinning(renderer, state);
|
||||
} else {
|
||||
drawShadowDepthCpu(renderer);
|
||||
drawShadowDepthCpu(renderer, state);
|
||||
}
|
||||
}
|
||||
|
||||
void Character::drawShadowDepthCpu(Renderer& renderer) {
|
||||
AnimationState drawState = resolveActiveState();
|
||||
void Character::drawShadowDepthCpu(Renderer& renderer, const CharacterState& state) {
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
auto& anim = it->second;
|
||||
for (const auto& name : anim.model.meshNamesOrdered) {
|
||||
@@ -358,17 +406,17 @@ void Character::drawShadowDepthCpu(Renderer& renderer) {
|
||||
}
|
||||
renderer.PopMatrix();
|
||||
|
||||
drawAttachedWeapon(renderer);
|
||||
drawAttachedWeapon(renderer, state);
|
||||
}
|
||||
|
||||
void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
|
||||
void Character::drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterState& state) {
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
AnimationState drawState = resolveActiveState();
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
if (!prepareGpuSkinning()) return;
|
||||
if (!prepareGpuSkinning(state)) return;
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
static const std::string shadowSkinningShader = "shadow_depth_skinning";
|
||||
@@ -377,10 +425,10 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
|
||||
renderer.shaderManager.PushShader(shadowSkinningShader);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
||||
@@ -400,14 +448,17 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {
|
||||
renderer.PopMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
drawAttachedWeapon(renderer);
|
||||
drawAttachedWeapon(renderer, state);
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
}
|
||||
|
||||
// ==================== Main pass with shadows ====================
|
||||
|
||||
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) {
|
||||
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);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
if (characterState.useGpuSkinning) {
|
||||
drawGpuSkinningWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera);
|
||||
if (state.useGpuSkinning) {
|
||||
drawGpuSkinningWithShadow(renderer, state, lightFromCamera, shadowMapTex, lightDirCamera);
|
||||
} 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) {
|
||||
AnimationState drawState = resolveActiveState();
|
||||
void Character::drawCpuWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
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);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
auto& anim = it->second;
|
||||
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.RenderUniform1i(textureUniformName, 0);
|
||||
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosShadow);
|
||||
drawAttachedWeapon(renderer);
|
||||
drawAttachedWeapon(renderer, state);
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
}
|
||||
|
||||
void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
|
||||
AnimationState drawState = resolveActiveState();
|
||||
void Character::drawGpuSkinningWithShadow(Renderer& renderer, const CharacterState& state, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
if (!prepareGpuSkinning()) return;
|
||||
if (!prepareGpuSkinning(state)) return;
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
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);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
||||
@@ -534,7 +585,7 @@ void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matri
|
||||
renderer.shaderManager.PushShader("fog");
|
||||
renderer.RenderUniform1i(textureUniformName, 0);
|
||||
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkinShadow);
|
||||
drawAttachedWeapon(renderer);
|
||||
drawAttachedWeapon(renderer, state);
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
@@ -554,7 +605,10 @@ void Character::drawNight(Renderer& renderer,
|
||||
const Eigen::Matrix4f& cameraViewInverse,
|
||||
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) {
|
||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||
}
|
||||
@@ -562,25 +616,26 @@ void Character::drawNight(Renderer& renderer,
|
||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
if (characterState.useGpuSkinning) {
|
||||
drawNightGpuSkinning(renderer, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
|
||||
if (state.useGpuSkinning) {
|
||||
drawNightGpuSkinning(renderer, state, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
|
||||
pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor);
|
||||
} else {
|
||||
drawNightCpu(renderer, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
|
||||
drawNightCpu(renderer, state, pointLightPosEye, pointLightDirEye, pointLightColors, pointLightCount,
|
||||
pointLightWorldXZ, pointLightLimitXZ, cameraViewInverse, ambientColor, fogColor);
|
||||
}
|
||||
}
|
||||
|
||||
void Character::drawNightGpuSkinning(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const float* ambientColor, const float* fogColor)
|
||||
{
|
||||
AnimationState drawState = resolveActiveState();
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
if (!prepareGpuSkinning()) return;
|
||||
if (!prepareGpuSkinning(state)) return;
|
||||
|
||||
static const std::string nightSkinningShader = "night_fog_skinning";
|
||||
static const std::string boneMatricesUniform = "uBoneMatrices[0]";
|
||||
@@ -606,10 +661,10 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
||||
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("uPointLightLimitXZ[0]", plCount, plLimitXZ);
|
||||
}
|
||||
drawAttachedWeapon(renderer);
|
||||
drawAttachedWeapon(renderer, state);
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
renderer.PopProjectionMatrix();
|
||||
@@ -647,11 +702,12 @@ void Character::drawNightGpuSkinning(Renderer& renderer,
|
||||
}
|
||||
|
||||
void Character::drawNightCpu(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const float* ambientColor, const float* fogColor)
|
||||
{
|
||||
AnimationState drawState = resolveActiveState();
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
@@ -677,10 +733,10 @@ void Character::drawNightCpu(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
auto& anim = it->second;
|
||||
for (const auto& name : anim.model.meshNamesOrdered) {
|
||||
@@ -697,7 +753,7 @@ void Character::drawNightCpu(Renderer& renderer,
|
||||
renderer.PopMatrix();
|
||||
|
||||
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNight);
|
||||
drawAttachedWeapon(renderer);
|
||||
drawAttachedWeapon(renderer, state);
|
||||
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
@@ -712,35 +768,39 @@ void Character::drawNightWithShadow(Renderer& renderer,
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
|
||||
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) {
|
||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||
}
|
||||
renderer.RenderUniform1f("uAlpha", 1.0f);
|
||||
glDisable(GL_BLEND);
|
||||
|
||||
if (characterState.useGpuSkinning) {
|
||||
drawNightGpuSkinningWithShadow(renderer, plPosEye, plDirEye, plColors, plCount,
|
||||
if (state.useGpuSkinning) {
|
||||
drawNightGpuSkinningWithShadow(renderer, state, plPosEye, plDirEye, plColors, plCount,
|
||||
plWorldXZ, plLimitXZ, cameraViewInverse,
|
||||
lightFromCamera, shadowMapTex, ambientColor, fogColor);
|
||||
} else {
|
||||
drawNightCpuWithShadow(renderer, plPosEye, plDirEye, plColors, plCount,
|
||||
drawNightCpuWithShadow(renderer, state, plPosEye, plDirEye, plColors, plCount,
|
||||
plWorldXZ, plLimitXZ, cameraViewInverse,
|
||||
lightFromCamera, shadowMapTex, ambientColor, fogColor);
|
||||
}
|
||||
}
|
||||
|
||||
void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ,
|
||||
const Eigen::Matrix4f& cameraViewInverse,
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
|
||||
const float* ambientColor, const float* fogColor)
|
||||
{
|
||||
AnimationState drawState = resolveActiveState();
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
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 boneMatricesUniform = "uBoneMatrices[0]";
|
||||
@@ -774,10 +834,10 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
|
||||
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("uPointLightLimitXZ[0]", plCount, plLimitXZ);
|
||||
drawAttachedWeapon(renderer);
|
||||
drawAttachedWeapon(renderer, state);
|
||||
renderer.shaderManager.PopShader();
|
||||
|
||||
renderer.PopProjectionMatrix();
|
||||
@@ -818,13 +878,14 @@ void Character::drawNightGpuSkinningWithShadow(Renderer& renderer,
|
||||
}
|
||||
|
||||
void Character::drawNightCpuWithShadow(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ,
|
||||
const Eigen::Matrix4f& cameraViewInverse,
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
|
||||
const float* ambientColor, const float* fogColor)
|
||||
{
|
||||
AnimationState drawState = resolveActiveState();
|
||||
AnimationState drawState = resolveActiveState(state);
|
||||
auto it = animations.find(drawState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
@@ -858,10 +919,10 @@ void Character::drawNightCpuWithShadow(Renderer& renderer,
|
||||
Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);
|
||||
|
||||
renderer.PushMatrix();
|
||||
renderer.TranslateMatrix({ characterState.position.x(), characterState.position.y(), characterState.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-characterState.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(characterState.modelScale);
|
||||
renderer.RotateMatrix(characterState.modelCorrectionRotation.toRotationMatrix());
|
||||
renderer.TranslateMatrix({ state.position.x(), state.position.y(), state.position.z() });
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-state.facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
renderer.ScaleMatrix(state.modelScale);
|
||||
renderer.RotateMatrix(state.modelCorrectionRotation.toRotationMatrix());
|
||||
|
||||
auto& anim = it->second;
|
||||
for (const auto& name : anim.model.meshNamesOrdered) {
|
||||
@@ -878,7 +939,7 @@ void Character::drawNightCpuWithShadow(Renderer& renderer,
|
||||
renderer.PopMatrix();
|
||||
|
||||
renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosNightShadow);
|
||||
drawAttachedWeapon(renderer);
|
||||
drawAttachedWeapon(renderer, state);
|
||||
|
||||
renderer.PopProjectionMatrix();
|
||||
renderer.shaderManager.PopShader();
|
||||
@@ -906,8 +967,11 @@ void Character::presentDamage(float hp, float maxHp,
|
||||
onHpChanged(hp, maxHp, attackerId);
|
||||
|
||||
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.setEmissionDirection(attackDirection);
|
||||
|
||||
@@ -924,16 +988,18 @@ void Character::prepareHitSparksForDraw(const Eigen::Matrix4f& cameraViewMatrix)
|
||||
hitSparkEmitter.prepareForDraw(cameraViewMatrix);
|
||||
}
|
||||
|
||||
void Character::drawName(TextRenderer& textRenderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix)
|
||||
static void drawCharacterNameLabel(
|
||||
TextRenderer& textRenderer,
|
||||
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 (!characterState.enabled) return;
|
||||
if (characterState.hp <= 0.f) return;
|
||||
if (characterState.npcName.empty()) return;
|
||||
if (!state.enabled || state.hp <= 0.f || displayName.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;
|
||||
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 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>{ 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,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix)
|
||||
{
|
||||
if (characterState.isPlayer || !characterState.canAttack) return;
|
||||
if (!characterState.enabled) return;
|
||||
if (characterState.hp <= 0.f) return;
|
||||
if (characterState.initialHp <= 0.f) return;
|
||||
auto stateSnapshot = this->stateSnapshot();
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
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;
|
||||
if (clip.w() <= 0.f) return;
|
||||
float ndcX = clip.x() / clip.w();
|
||||
@@ -980,7 +1077,7 @@ void Character::drawHealthBar(Renderer& renderer,
|
||||
const float bottom = 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 > 1.f) frac = 1.f;
|
||||
const float split = left + barW * frac;
|
||||
|
||||
+27
-18
@@ -22,12 +22,14 @@ class IGameServer;
|
||||
|
||||
class Character {
|
||||
public:
|
||||
Character(const CharacterState& boundState, IGameServer& server,
|
||||
std::string locationId, std::string entityId)
|
||||
: characterState(boundState), gameServer_(server),
|
||||
locationId_(std::move(locationId)), entityId_(std::move(entityId)){}
|
||||
Character(IGameServer& server, std::string locationId, std::string entityId)
|
||||
: gameServer_(server), locationId_(std::move(locationId)),
|
||||
entityId_(std::move(entityId)) {}
|
||||
const std::string& entityId() const { return entityId_; }
|
||||
void bindToLocation(std::string locationId) { locationId_ = std::move(locationId); }
|
||||
// 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;
|
||||
|
||||
// ---- Asset loading ----
|
||||
@@ -63,6 +65,10 @@ public:
|
||||
void drawName(TextRenderer& textRenderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix);
|
||||
void drawPlayerName(TextRenderer& textRenderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix,
|
||||
const std::string& displayName);
|
||||
void drawHealthBar(Renderer& renderer,
|
||||
const Eigen::Matrix4f& cameraViewMatrix,
|
||||
const Eigen::Matrix4f& projectionMatrix);
|
||||
@@ -84,11 +90,11 @@ public:
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex,
|
||||
const float* ambientColor, const float* fogColor);
|
||||
|
||||
// ---- Query helpers (delegate to state) ----
|
||||
bool isMoving() const { return characterState.isMoving(); }
|
||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return characterState.getRequestedWalkTarget(); }
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const { return characterState.getCurrentNavigationTarget(); }
|
||||
float getHp() const { return characterState.getHp(); }
|
||||
// ---- Query helpers (read immutable snapshots) ----
|
||||
bool isMoving() const;
|
||||
Eigen::Vector3f getRequestedWalkTarget() const;
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||
float getHp() const;
|
||||
|
||||
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
|
||||
VertexRenderStruct weaponMesh;
|
||||
@@ -109,7 +115,6 @@ private:
|
||||
friend class LocationEditor;
|
||||
friend class PathVisualization;
|
||||
friend class InternalGameServer;
|
||||
const CharacterState& characterState; // private non-owning binding to server-owned storage
|
||||
IGameServer& gameServer_;
|
||||
std::string locationId_;
|
||||
std::string entityId_;
|
||||
@@ -120,28 +125,32 @@ private:
|
||||
VertexRenderStruct healthBarMesh;
|
||||
std::unordered_map<std::string, std::shared_ptr<Texture>> meshTextures;
|
||||
|
||||
AnimationState resolveActiveState() const;
|
||||
bool prepareGpuSkinning();
|
||||
void drawAttachedWeapon(Renderer& renderer);
|
||||
AnimationState resolveActiveState(const CharacterState& state) const;
|
||||
bool prepareGpuSkinning(const CharacterState& state);
|
||||
void drawAttachedWeapon(Renderer& renderer, const CharacterState& state);
|
||||
|
||||
void drawGpuSkinning(Renderer& renderer);
|
||||
void drawShadowDepthGpuSkinning(Renderer& renderer);
|
||||
void drawShadowDepthCpu(Renderer& renderer);
|
||||
void drawGpuSkinningWithShadow(Renderer& renderer, 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 drawGpuSkinning(Renderer& renderer, const CharacterState& state);
|
||||
void drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterState& state);
|
||||
void drawShadowDepthCpu(Renderer& renderer, const CharacterState& state);
|
||||
void drawGpuSkinningWithShadow(Renderer& renderer, const CharacterState& state, 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,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const float* ambientColor, const float* fogColor);
|
||||
void drawNightCpu(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const float* ambientColor, const float* fogColor);
|
||||
void drawNightGpuSkinningWithShadow(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
|
||||
void drawNightCpuWithShadow(Renderer& renderer,
|
||||
const CharacterState& state,
|
||||
const float* plPosEye, const float* plDirEye, const float* plColors, int plCount,
|
||||
const float* plWorldXZ, const float* plLimitXZ, const Eigen::Matrix4f& cameraViewInverse,
|
||||
const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const float* ambientColor, const float* fogColor);
|
||||
|
||||
@@ -12,6 +12,17 @@ constexpr float TARGET_REPLAN_THRESHOLD = 0.25f;
|
||||
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 {
|
||||
Eigen::Vector3f toTarget = walkTarget - position;
|
||||
toTarget.y() = 0.f;
|
||||
|
||||
@@ -135,6 +135,7 @@ public:
|
||||
CharacterCreationInfo creationInfo;
|
||||
|
||||
// --- Pure-state methods ---
|
||||
void configureAsPlayer();
|
||||
bool isMoving() const;
|
||||
const Eigen::Vector3f& getRequestedWalkTarget() const { return requestedWalkTarget; }
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||
|
||||
+168
-171
@@ -13,6 +13,7 @@
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <type_traits>
|
||||
#ifdef __ANDROID__
|
||||
#include <android/log.h>
|
||||
#endif
|
||||
@@ -76,16 +77,14 @@ namespace FRG
|
||||
#endif
|
||||
|
||||
Location* Game::currentLocation() const {
|
||||
if (gameState.currentLocationName.empty()) return nullptr;
|
||||
const auto& locations = internalGameServer.runtimeLocations();
|
||||
auto it = locations.find(gameState.currentLocationName);
|
||||
return (it != locations.end()) ? it->second.get() : nullptr;
|
||||
const auto state = stateSnapshot();
|
||||
if (!state || state->currentLocationId.empty()) return nullptr;
|
||||
return internalGameServer.runtimeLocation(state->currentLocationId);
|
||||
}
|
||||
|
||||
Game::Game()
|
||||
: audioPlayer(std::make_unique<AudioPlayerAsync>())
|
||||
, internalGameServer()
|
||||
, gameState(internalGameServer.hostState())
|
||||
, menuManager(renderer, internalGameServer, *audioPlayer)
|
||||
, newTickCount(0)
|
||||
, lastTickCount(0)
|
||||
@@ -360,19 +359,6 @@ namespace FRG
|
||||
CampaignRegistry::instance().loadFromJson("resources/config/campaigns.json", CONST_ZIP_FILE);
|
||||
|
||||
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.
|
||||
menuManager.startDialogueFunc = [this](const std::string& id) {
|
||||
if (currentLocation()) currentLocation()->dialogueSystem.startDialogue(id);
|
||||
@@ -399,18 +385,6 @@ namespace FRG
|
||||
loadSteps.push([this]() {
|
||||
try {
|
||||
menuManager.setup(CONST_ZIP_FILE);
|
||||
gameState.questJournal.onQuestUnlocked = [this](const std::string& id) {
|
||||
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
|
||||
if (quest) menuManager.showToast(menuManager.toastTextureQuestNew, quest->definition.title);
|
||||
};
|
||||
gameState.questJournal.onQuestCompleted = [this](const std::string& id) {
|
||||
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
|
||||
if (quest) menuManager.showToast(menuManager.toastTextureQuestCompleted, quest->definition.title);
|
||||
};
|
||||
gameState.questJournal.onQuestFailed = [this](const std::string& id) {
|
||||
const Quest::QuestState* quest = gameState.questJournal.findQuest(id);
|
||||
if (quest) menuManager.showToast(menuManager.toastTextureQuestFailed, quest->definition.title);
|
||||
};
|
||||
logger() << "UI loaded successfully" << std::endl;
|
||||
}
|
||||
catch (const std::exception& e) {
|
||||
@@ -425,7 +399,8 @@ namespace FRG
|
||||
performResetToInitialState();
|
||||
applyTutorialSetting();
|
||||
const CampaignConfig& campaign = activeCampaign();
|
||||
gameState.currentLocationName = campaign.startLocation;
|
||||
internalGameServer.submit(
|
||||
SetCurrentLocationCommand{campaign.startLocation});
|
||||
menuManager.showGameplayHud(); // first HUD frame drawn from the fresh state
|
||||
if (currentLocation()) currentLocation()->scriptEngine.callLocationEnterCallback();
|
||||
audioPlayer->crossFadeMusicAsync(campaign.startMusic);
|
||||
@@ -516,7 +491,10 @@ namespace FRG
|
||||
}
|
||||
|
||||
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) {
|
||||
static const CampaignConfig empty;
|
||||
@@ -527,7 +505,7 @@ namespace FRG
|
||||
|
||||
bool Game::isLocationLoaded(const std::string& campaignId, const std::string& locationId) const {
|
||||
return player && loadedCampaignId == campaignId
|
||||
&& internalGameServer.runtimeLocations().count(locationId) > 0;
|
||||
&& internalGameServer.hasRuntimeLocation(locationId);
|
||||
}
|
||||
|
||||
void Game::enterLocationThen(const std::string& requestedCampaignId, const std::string& requestedLocationId,
|
||||
@@ -567,15 +545,15 @@ namespace FRG
|
||||
|
||||
void Game::loadCampaign(const std::string& campaignId) {
|
||||
loadedCampaignId = campaignId;
|
||||
gameState.campaignId = campaignId;
|
||||
gameState.locations.clear();
|
||||
gameState.players.clear();
|
||||
gameState.questJournal.loadFromFile(activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||
gameState.globalFloats["player_hp"] = 200; // read by location scripts as they load
|
||||
const CampaignConfig* campaign =
|
||||
CampaignRegistry::instance().find(campaignId);
|
||||
if (!campaign) return;
|
||||
internalGameServer.initializeCampaign(
|
||||
campaignId, campaign->questsPath, CONST_ZIP_FILE);
|
||||
// The player stays loaded for the whole campaign; locations come and go around him.
|
||||
auto& playerState = gameState.players["player"];
|
||||
if (!playerState) playerState = std::make_shared<CharacterState>();
|
||||
player = Location::createPlayer(renderer, audioPlayer.get(), *playerState, internalGameServer);
|
||||
player = Location::createPlayer(
|
||||
renderer, audioPlayer.get(), internalGameServer,
|
||||
internalGameServer.localPlayerId());
|
||||
if (player)
|
||||
player->onDeathAnimComplete = [this]() { startDarklandsTransition(); };
|
||||
}
|
||||
@@ -583,21 +561,21 @@ namespace FRG
|
||||
void Game::unloadCampaign() {
|
||||
unloadLocation(false);
|
||||
player.reset();
|
||||
gameState.players.clear();
|
||||
loadedCampaignId.clear();
|
||||
gameState.locations.clear();
|
||||
internalGameServer.clearCampaign();
|
||||
renderer.textureManager.UnloadUnused(); // the player's textures
|
||||
}
|
||||
|
||||
void Game::loadLocation(const std::string& locationId) {
|
||||
const LocationConfig* config = activeCampaign().findLocation(locationId);
|
||||
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;
|
||||
return;
|
||||
}
|
||||
const bool initializeLocation =
|
||||
!gameState.locations[locationId].hasPersistentState;
|
||||
const bool initializeLocation = internalGameServer.prepareLocation(*config);
|
||||
std::shared_ptr<Location> loc = createLocation(*config);
|
||||
wireLocation(loc.get());
|
||||
if (shadowMap) loc->shadowMap = shadowMap;
|
||||
@@ -608,12 +586,7 @@ namespace FRG
|
||||
}
|
||||
|
||||
void Game::unloadLocation(bool storeState) {
|
||||
if (storeState) internalGameServer.synchronizeRuntimeAuxiliaryState();
|
||||
else {
|
||||
for (const auto& [name, loc] : internalGameServer.runtimeLocations())
|
||||
gameState.locations.erase(name);
|
||||
}
|
||||
internalGameServer.runtimeLocations().clear();
|
||||
internalGameServer.clearRuntimeLocations(storeState);
|
||||
// Textures only the location's models and characters used are freed now.
|
||||
renderer.textureManager.UnloadUnused();
|
||||
}
|
||||
@@ -631,8 +604,10 @@ namespace FRG
|
||||
}
|
||||
// Runs from inside the current location's update(); the location itself is
|
||||
// released by a later loading step.
|
||||
enterLocationThen(gameState.campaignId, destName, [this, destName, destPos, destRotY]() {
|
||||
gameState.currentLocationName = destName;
|
||||
const auto state = stateSnapshot();
|
||||
if (!state) return;
|
||||
enterLocationThen(state->campaignId, destName, [this, destName, destPos, destRotY]() {
|
||||
internalGameServer.submit(SetCurrentLocationCommand{destName});
|
||||
Location* loc = currentLocation();
|
||||
if (!loc) return;
|
||||
if (loc->player) {
|
||||
@@ -643,17 +618,17 @@ namespace FRG
|
||||
});
|
||||
loc->stopCharacter(*loc->player);
|
||||
}
|
||||
loc->state.cameraAzimuth = destRotY;
|
||||
loc->state.isDarklands = gameState.isDarklands;
|
||||
loc->state.isNight = gameState.isNight;
|
||||
loc->state.isDawn = gameState.isDawn;
|
||||
auto locationState = loc->stateSnapshot();
|
||||
const float inclination = locationState
|
||||
? locationState->cameraInclination
|
||||
: 1.1036f;
|
||||
internalGameServer.submit(SetLocationCameraCommand{
|
||||
destName, destRotY, inclination});
|
||||
loc->scriptEngine.callLocationEnterCallback();
|
||||
|
||||
//Reset taxi
|
||||
auto locationState = gameState.locations.find(destName);
|
||||
if (locationState != gameState.locations.end())
|
||||
locationState->second.resetTaxi();
|
||||
gameState.taxiIsCalled = false;
|
||||
internalGameServer.submit(ResetTaxiCommand{destName});
|
||||
internalGameServer.submit(SetTaxiCalledCommand{false});
|
||||
|
||||
menuManager.onLocationChanged(destName);
|
||||
});
|
||||
@@ -661,34 +636,12 @@ namespace FRG
|
||||
|
||||
|
||||
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
|
||||
// any more, so they are built fresh when the player first goes there.
|
||||
gameState.locations.clear();
|
||||
std::vector<std::string> loadedIds;
|
||||
for (auto& [name, loc] : internalGameServer.runtimeLocations()) loadedIds.push_back(name);
|
||||
std::vector<std::string> loadedIds =
|
||||
internalGameServer.loadedRuntimeLocationIds();
|
||||
internalGameServer.resetCampaign(
|
||||
activeCampaign().questsPath, CONST_ZIP_FILE);
|
||||
unloadLocation(false);
|
||||
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.
|
||||
void Game::applyTutorialSetting() {
|
||||
if (!activeCampaign().tutorial || !menuManager.tutorialEnabledSetting)
|
||||
gameState.tutorialStep = TutorialStep::Completed;
|
||||
internalGameServer.submit(
|
||||
SetTutorialStepCommand{TutorialStep::Completed});
|
||||
}
|
||||
|
||||
void Game::saveInitialState() {
|
||||
@@ -760,12 +714,6 @@ namespace FRG
|
||||
|
||||
std::shared_ptr<Location> Game::createLocation(const LocationConfig& config)
|
||||
{
|
||||
LocationGameState& authoritative = gameState.locations[config.id];
|
||||
if (!authoritative.location) authoritative.location = std::make_shared<LocationState>();
|
||||
|
||||
if (authoritative.location->navigationMaps.empty())
|
||||
authoritative.location->initNavigation(config.navigationJsonPaths);
|
||||
|
||||
LocationSetup params;
|
||||
params.gameObjectsJsonPath = config.gameObjectsJsonPath;
|
||||
params.interactiveObjectsJsonPath = config.interactiveObjectsJsonPath;
|
||||
@@ -778,29 +726,30 @@ namespace FRG
|
||||
params.lightsJsonPath = config.lightsJsonPath;
|
||||
params.playerPosition = config.playerPosition;
|
||||
params.player = player;
|
||||
params.gameState = &authoritative;
|
||||
|
||||
auto loc = std::make_shared<Location>(
|
||||
renderer, gameState.inventory, *authoritative.location, internalGameServer, config.id);
|
||||
loc->setup(params, &gameState.questJournal, audioPlayer.get());
|
||||
loc->scriptEngine.setGlobalStore(&gameState.globalInts);
|
||||
loc->scriptEngine.setGlobalFloatStore(&gameState.globalFloats);
|
||||
renderer, internalGameServer, config.id);
|
||||
internalGameServer.setupRuntimeLocation(
|
||||
config.id, loc, std::move(params), audioPlayer.get());
|
||||
loc->requestNightDayTransition = [this](bool isNight, bool isDawn) {
|
||||
gameState.isNight = isNight;
|
||||
gameState.isDawn = isDawn;
|
||||
const auto state = stateSnapshot();
|
||||
if (!state) return;
|
||||
internalGameServer.submit(SetWorldModeCommand{
|
||||
state->isDarklands, isNight, isDawn});
|
||||
};
|
||||
loc->requestDarklandsTransition = [this]() { return startDarklandsTransition(); };
|
||||
loc->requestAdvanceDarklandsHud = [this]() { menuManager.advanceUniIntDarklandsHud(); };
|
||||
loc->requestClosePhone = [this]() { menuManager.closePhoneEntirely(); };
|
||||
loc->requestReturnToMainMenu = [this]() {
|
||||
gameState.currentLocationName.clear();
|
||||
internalGameServer.submit(SetCurrentLocationCommand{std::string()});
|
||||
menuManager.showMenuAfterGameCompleted();
|
||||
};
|
||||
loc->requestSetChatUnread = [this](int idx, bool unread, const std::string& msg) {
|
||||
menuManager.setChatUnread(idx, unread, msg);
|
||||
};
|
||||
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(); };
|
||||
}
|
||||
}
|
||||
@@ -813,25 +762,6 @@ namespace FRG
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -846,20 +776,16 @@ namespace FRG
|
||||
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.
|
||||
// completeTutorialStep() only acts on the current step, so sharing is safe.
|
||||
{
|
||||
loc->dialogueSystem.setOnDialogueAdvanced([this]() {
|
||||
gameState.completeTutorialStep(TutorialStep::AdvanceDialogue);
|
||||
internalGameServer.submit(
|
||||
CompleteTutorialStepCommand{TutorialStep::AdvanceDialogue});
|
||||
});
|
||||
loc->onPlayerFloorWalk = [this]() {
|
||||
gameState.completeTutorialStep(TutorialStep::Walk);
|
||||
internalGameServer.submit(
|
||||
CompleteTutorialStepCommand{TutorialStep::Walk});
|
||||
menuManager.onPlayerStartedWalking();
|
||||
};
|
||||
}
|
||||
@@ -897,19 +823,23 @@ namespace FRG
|
||||
}
|
||||
|
||||
bool currentTargetDead = false;
|
||||
auto playerState = loc->player->stateSnapshot();
|
||||
if (!playerState) return;
|
||||
for (const auto& npc : loc->npcs) {
|
||||
if (npc && npc->entityId() == loc->player->characterState.attackTargetId) {
|
||||
currentTargetDead = npc->characterState.hp <= 0.f;
|
||||
if (npc && npc->entityId() == playerState->attackTargetId) {
|
||||
auto npcState = npc->stateSnapshot();
|
||||
currentTargetDead = !npcState || npcState->hp <= 0.f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ((loc->player->characterState.attackTargetId.empty() || currentTargetDead)
|
||||
if ((playerState->attackTargetId.empty() || currentTargetDead)
|
||||
&&
|
||||
loc->player->isMoving() == false
|
||||
&&
|
||||
loc->player->getHp() > 0) {
|
||||
internalGameServer.submit(SetCharacterTargetCommand{
|
||||
loc->locationId(), "player", attackerId});
|
||||
loc->locationId(),
|
||||
internalGameServer.localPlayerId(), attackerId});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -931,7 +861,9 @@ namespace FRG
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
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()
|
||||
@@ -941,7 +873,9 @@ namespace FRG
|
||||
shadowMap.reset();
|
||||
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__);
|
||||
|
||||
}
|
||||
@@ -1014,16 +948,12 @@ namespace FRG
|
||||
Location* loc = currentLocation();
|
||||
if (loc)
|
||||
{
|
||||
// Sync global flags so Location's draw functions see them.
|
||||
loc->state.isDarklands = gameState.isDarklands;
|
||||
loc->state.isNight = gameState.isNight;
|
||||
loc->state.isDawn = gameState.isDawn;
|
||||
|
||||
if (gameState.isDarklands) {
|
||||
const auto state = stateSnapshot();
|
||||
if (state && state->isDarklands) {
|
||||
loc->drawGameDarklands();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
}
|
||||
else if (gameState.isNight) {
|
||||
else if (state && state->isNight) {
|
||||
loc->drawGameNight();
|
||||
CheckGlError(__FILE__, __LINE__);
|
||||
}
|
||||
@@ -1206,11 +1136,67 @@ namespace FRG
|
||||
// The authoritative implementation advances simulation; a future
|
||||
// network client can use the same call to poll/apply server updates.
|
||||
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() {
|
||||
FRG::CheckGlError(__FILE__, __LINE__);
|
||||
|
||||
@@ -1256,10 +1242,7 @@ namespace FRG
|
||||
}
|
||||
|
||||
void Game::update() {
|
||||
// Host systems (locations, scripts and loading) still execute in-process.
|
||||
// Publish any changes they made since the previous frame as a new immutable
|
||||
// revision before presentation clients query the server.
|
||||
internalGameServer.markHostMutation();
|
||||
processServerEvents();
|
||||
SDL_Event event;
|
||||
const bool mobileIsPortrait = loadingCompleted && (Environment::width < Environment::height);
|
||||
while (SDL_PollEvent(&event)) {
|
||||
@@ -1394,7 +1377,8 @@ namespace FRG
|
||||
}
|
||||
//logger() << "Current zoom: " << Environment::zoom << std::endl;
|
||||
// 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) {
|
||||
@@ -1422,7 +1406,6 @@ namespace FRG
|
||||
if (event.key.keysym.sym == SDLK_3)
|
||||
{
|
||||
currentLocation()->switchNavigation(2);
|
||||
//logger() << "Player position: " << currentLocation()->player->characterState.position.transpose() << std::endl;
|
||||
}
|
||||
if (event.key.keysym.sym == SDLK_n)
|
||||
{
|
||||
@@ -1435,8 +1418,6 @@ namespace FRG
|
||||
{
|
||||
currentLocation()->editorMode = EditorMode::None;
|
||||
}
|
||||
|
||||
//logger() << "Player position: " << currentLocation()->player->characterState.position.transpose() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1516,8 +1497,11 @@ namespace FRG
|
||||
currentLocation()->lastMouseY = eventY;
|
||||
|
||||
// Snapshot current angles so we can measure how far the user rotates.
|
||||
dragStartAzimuth = currentLocation()->state.cameraAzimuth;
|
||||
dragStartInclination = currentLocation()->state.cameraInclination;
|
||||
auto state = currentLocation()->stateSnapshot();
|
||||
if (state) {
|
||||
dragStartAzimuth = state->cameraAzimuth;
|
||||
dragStartInclination = state->cameraInclination;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1577,7 +1561,8 @@ namespace FRG
|
||||
|
||||
// Tutorial: detect a significant pinch-zoom (≥ 2 zoom units).
|
||||
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,
|
||||
// not just nudged a single axis by accident.
|
||||
static constexpr float TUTORIAL_ROTATION_THRESHOLD = 0.15f;
|
||||
const auto game = stateSnapshot();
|
||||
if (cameraDragging
|
||||
&& gameState.tutorialStep == TutorialStep::RotateCamera) {
|
||||
float deltaAz = std::abs(currentLocation()->state.cameraAzimuth - dragStartAzimuth);
|
||||
float deltaInc = std::abs(currentLocation()->state.cameraInclination - dragStartInclination);
|
||||
&& game
|
||||
&& game->tutorialStep == TutorialStep::RotateCamera) {
|
||||
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) {
|
||||
gameState.completeTutorialStep(TutorialStep::RotateCamera);
|
||||
internalGameServer.submit(
|
||||
CompleteTutorialStepCommand{TutorialStep::RotateCamera});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1733,13 +1723,14 @@ namespace FRG
|
||||
|
||||
bool Game::startDarklandsTransition()
|
||||
{
|
||||
if (!gameState.isNight)
|
||||
const auto state = stateSnapshot();
|
||||
if (!state || !state->isNight)
|
||||
{
|
||||
currentLocation()->dialogueSystem.startDialogue("darklands_day_dialog001");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (gameState.isDawn)
|
||||
if (state->isDawn)
|
||||
{
|
||||
currentLocation()->dialogueSystem.startDialogue("darklands_morning_dialog001");
|
||||
return false;
|
||||
@@ -1773,11 +1764,13 @@ namespace FRG
|
||||
if (darklandsFlashFadingIn) {
|
||||
darklandsFlashAlpha = min(darklandsFlashAlpha + step, 1.0f);
|
||||
if (darklandsFlashAlpha >= 1.0f) {
|
||||
const auto before = stateSnapshot();
|
||||
if (!before) return;
|
||||
if (isNightTransition) {
|
||||
gameState.isNight = !gameState.isNight;
|
||||
internalGameServer.submit(SetWorldModeCommand{
|
||||
before->isDarklands, !before->isNight, before->isDawn});
|
||||
if (currentLocation())
|
||||
{
|
||||
currentLocation()->state.isNight = gameState.isNight;
|
||||
currentLocation()->scriptEngine.callTriggerNightEnterCallback();
|
||||
}
|
||||
|
||||
@@ -1785,12 +1778,15 @@ namespace FRG
|
||||
menuManager.lastMusicTrack = "music/bishkek night.ogg";
|
||||
|
||||
} else {
|
||||
gameState.isDarklands = !gameState.isDarklands;
|
||||
internalGameServer.submit(SetWorldModeCommand{
|
||||
!before->isDarklands, before->isNight, before->isDawn});
|
||||
const auto after = stateSnapshot();
|
||||
if (!after) return;
|
||||
|
||||
// Change HUD
|
||||
menuManager.setDarklandsMode(gameState.isDarklands);
|
||||
menuManager.setDarklandsMode(after->isDarklands);
|
||||
|
||||
if (gameState.isDarklands)
|
||||
if (after->isDarklands)
|
||||
{
|
||||
audioPlayer->crossFadeMusicAsync("music/bishkek fight calm.ogg");
|
||||
menuManager.lastMusicTrack = "music/bishkek fight calm.ogg";
|
||||
@@ -1802,8 +1798,7 @@ namespace FRG
|
||||
}
|
||||
|
||||
if (currentLocation()) {
|
||||
currentLocation()->state.isDarklands = gameState.isDarklands;
|
||||
if (gameState.isDarklands)
|
||||
if (after->isDarklands)
|
||||
currentLocation()->scriptEngine.callDarklandsEnterCallback();
|
||||
else
|
||||
currentLocation()->scriptEngine.callDarklandsExitCallback();
|
||||
@@ -1935,19 +1930,21 @@ namespace FRG
|
||||
|
||||
void Game::playMusicForLoadedGame()
|
||||
{
|
||||
if (gameState.isDarklands)
|
||||
const auto state = stateSnapshot();
|
||||
if (!state) return;
|
||||
if (state->isDarklands)
|
||||
{
|
||||
audioPlayer->crossFadeMusicAsync("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");
|
||||
menuManager.lastMusicTrack = "music/bishkek night.ogg";
|
||||
}
|
||||
else
|
||||
{
|
||||
if (gameState.currentLocationName == "location_dorm") {
|
||||
if (state->currentLocationId == "location_dorm") {
|
||||
audioPlayer->crossFadeMusicAsync("music/obshaga.ogg");
|
||||
menuManager.lastMusicTrack = "music/obshaga.ogg";
|
||||
}
|
||||
|
||||
+4
-4
@@ -21,7 +21,6 @@
|
||||
#include <unordered_set>
|
||||
#include "Location.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
#include "GameState.h"
|
||||
#include "game_server/InternalGameServer.h"
|
||||
namespace FRG {
|
||||
|
||||
@@ -84,9 +83,6 @@ namespace FRG {
|
||||
|
||||
private:
|
||||
InternalGameServer internalGameServer;
|
||||
// Host-only alias while location/script orchestration is moved behind typed
|
||||
// commands. The referenced object is owned by internalGameServer.
|
||||
GameState& gameState;
|
||||
|
||||
public:
|
||||
IGameServer& server() { return internalGameServer; }
|
||||
@@ -132,6 +128,9 @@ namespace FRG {
|
||||
|
||||
|
||||
Location* currentLocation() const;
|
||||
std::shared_ptr<const GameSnapshot> stateSnapshot() const {
|
||||
return internalGameServer.snapshot();
|
||||
}
|
||||
|
||||
void saveGame(int slot);
|
||||
void loadGame(int slot);
|
||||
@@ -181,6 +180,7 @@ namespace FRG {
|
||||
|
||||
int64_t getSyncTimeMs();
|
||||
void processTickCount();
|
||||
void processServerEvents();
|
||||
void drawScene();
|
||||
void drawUI();
|
||||
|
||||
|
||||
@@ -215,6 +215,7 @@ void GameState::load(const nlohmann::json& in)
|
||||
auto& player = players[id];
|
||||
if (!player) player = std::make_shared<CharacterState>();
|
||||
player->load(value);
|
||||
player->configureAsPlayer();
|
||||
}
|
||||
for (auto it = players.begin(); it != players.end();) {
|
||||
if (!playerValues.contains(it->first)) it = players.erase(it);
|
||||
|
||||
+455
-167
File diff suppressed because it is too large.
Load diff
+29
-9
@@ -72,11 +72,19 @@ namespace FRG
|
||||
class Location
|
||||
{
|
||||
public:
|
||||
Location(Renderer& iRenderer, Inventory& iInventory, LocationState& state, IGameServer& server,
|
||||
Location(Renderer& iRenderer, IGameServer& server,
|
||||
std::string locationId);
|
||||
// Detaches the (shared, longer-lived) player from this location.
|
||||
~Location();
|
||||
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;
|
||||
|
||||
@@ -91,10 +99,7 @@ namespace FRG
|
||||
|
||||
// Builds the player character (models, animations, weapon, textures).
|
||||
static std::shared_ptr<Character> createPlayer(Renderer& renderer, AudioPlayerAsync* audioPlayer,
|
||||
CharacterState& state, IGameServer& server);
|
||||
|
||||
//std::vector<PathFinder> navigationMaps;
|
||||
//int state.activeNavigationIndex = 0;
|
||||
IGameServer& server, const std::string& playerId);
|
||||
|
||||
std::shared_ptr<ShadowMap> shadowMap;
|
||||
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);
|
||||
bool switchNavigation(int index);
|
||||
void setCharacterTarget(Character& character, const Eigen::Vector3f& target,
|
||||
@@ -175,6 +180,18 @@ namespace FRG
|
||||
void resetCharacterAfterDeath(Character& character);
|
||||
void tryCharacterAttackCombo(Character& character);
|
||||
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);
|
||||
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 InternalGameServer;
|
||||
Renderer& renderer;
|
||||
Inventory& inventory;
|
||||
AudioPlayerAsync* audioPlayer;
|
||||
|
||||
private:
|
||||
LocationState& state; // non-owning binding to LocationGameState
|
||||
IGameServer& gameServer_;
|
||||
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 ----
|
||||
TeleportZone* getTargetTeleportZone();
|
||||
TeleportZone* getTargetTeleportZone(const LocationState& state);
|
||||
|
||||
enum class InstancedTexture { None, Color, Darklands };
|
||||
|
||||
|
||||
+36
-10
@@ -1,6 +1,7 @@
|
||||
#include "LocationEditor.h"
|
||||
#include "Location.h"
|
||||
#include "Character.h"
|
||||
#include "game_server/IGameServer.h"
|
||||
#include "utils/Utils.h"
|
||||
#include "render/OpenGlExtensions.h"
|
||||
#include "TextModel.h"
|
||||
@@ -24,9 +25,14 @@ namespace FRG
|
||||
void LocationEditor::buildNavMeshes()
|
||||
{
|
||||
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);
|
||||
for (const auto& obs : loc.state.navigationMaps[loc.state.activeNavigationIndex].getObstaclePolygons()) {
|
||||
for (const auto& obs : navigation.getObstaclePolygons()) {
|
||||
if (obs.polygon.size() < 3) continue;
|
||||
VertexRenderStruct mesh;
|
||||
mesh.data = CreatePolygonFloor(obs.polygon, y, red);
|
||||
@@ -50,7 +56,11 @@ namespace FRG
|
||||
{
|
||||
VertexDataStruct data;
|
||||
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;
|
||||
|
||||
for (const auto& pt : navigationEditorPoints) {
|
||||
@@ -126,12 +136,17 @@ namespace FRG
|
||||
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 "
|
||||
<< 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);
|
||||
VertexRenderStruct mesh;
|
||||
mesh.data = CreatePolygonFloor(poly.polygon, y, red);
|
||||
@@ -153,12 +168,21 @@ namespace FRG
|
||||
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";
|
||||
else
|
||||
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";
|
||||
else
|
||||
std::cerr << "[NAV_EDITOR] Failed to save grid to: " << baseName << ".txt\n";
|
||||
@@ -458,9 +482,11 @@ namespace FRG
|
||||
data.rotationX = 0.0f;
|
||||
data.rotationY = rotDist(rng);
|
||||
data.rotationZ = 0.0f;
|
||||
data.positionX = loc.player->characterState.position.x();
|
||||
data.positionY = loc.player->characterState.position.y();//-0.1;
|
||||
data.positionZ = loc.player->characterState.position.z();
|
||||
auto playerState = loc.player ? loc.player->stateSnapshot() : nullptr;
|
||||
if (!playerState) return;
|
||||
data.positionX = playerState->position.x();
|
||||
data.positionY = playerState->position.y();//-0.1;
|
||||
data.positionZ = playerState->position.z();
|
||||
data.scale = scaleDist(rng);
|
||||
|
||||
GameObjectData data2;
|
||||
|
||||
@@ -26,12 +26,6 @@ struct LocationState : public ISaveable {
|
||||
std::string targetInteractNpcId;
|
||||
std::string targetTeleportZoneId;
|
||||
|
||||
// ---- World-mode flags (synced from Game after each transition) ----
|
||||
bool isDarklands = false;
|
||||
bool isNight = false;
|
||||
bool isDawn = false;
|
||||
|
||||
|
||||
NpcCar taxiCarInitialState;
|
||||
NpcCar taxiCar;
|
||||
bool taxiArrivedCallbackCalled = false;
|
||||
|
||||
+38
-81
@@ -66,9 +66,6 @@ namespace FRG {
|
||||
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> npcBumpsPlayerCallbacks;
|
||||
std::unordered_map<std::string, int>* globalInts = nullptr;
|
||||
std::unordered_map<std::string, float>* globalFloats = nullptr;
|
||||
Quest::QuestJournal* questJournal = nullptr;
|
||||
sol::protected_function locationInitCallback;
|
||||
sol::protected_function locationEnterCallback;
|
||||
sol::protected_function locationExitCallback;
|
||||
@@ -82,7 +79,7 @@ namespace FRG {
|
||||
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>();
|
||||
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);
|
||||
if (item) {
|
||||
if (inventory->hasItem(itemId)) {
|
||||
if (loc->hasInventoryItem(itemId)) {
|
||||
logger() << "[script] has_item: " << item->name << " returns true" << std::endl;
|
||||
return true;
|
||||
}
|
||||
@@ -219,10 +216,10 @@ namespace FRG {
|
||||
});
|
||||
|
||||
// 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);
|
||||
if (item) {
|
||||
inventory->addItem(item->id);
|
||||
loc->addInventoryItem(item->id);
|
||||
logger() << "[script] pickup_item: " << item->name << std::endl;
|
||||
} else {
|
||||
std::cerr << "[script] pickup_item: item '" << itemId << "' not found in ItemRegistry\n";
|
||||
@@ -230,9 +227,9 @@ namespace FRG {
|
||||
});
|
||||
|
||||
// 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;
|
||||
inventory->removeItem(id);
|
||||
loc->removeInventoryItem(id);
|
||||
});
|
||||
|
||||
api.set_function("deactivate_interactive_object", [loc](const std::string& objectId) {
|
||||
@@ -276,13 +273,13 @@ namespace FRG {
|
||||
});
|
||||
|
||||
// get_inventory_count()
|
||||
api.set_function("get_inventory_count", [inventory]() {
|
||||
return inventory->getCount();
|
||||
api.set_function("get_inventory_count", [loc]() {
|
||||
return loc->inventoryItemCount();
|
||||
});
|
||||
|
||||
// has_item(item_id)
|
||||
api.set_function("has_item", [inventory](const std::string& id) {
|
||||
return inventory->hasItem(id);
|
||||
api.set_function("has_item", [loc](const std::string& id) {
|
||||
return loc->hasInventoryItem(id);
|
||||
});
|
||||
|
||||
api.set_function("start_dialogue",
|
||||
@@ -381,12 +378,12 @@ namespace FRG {
|
||||
|
||||
api.set_function("is_night",
|
||||
[loc]() {
|
||||
return loc->state.isNight;
|
||||
return loc->isNight();
|
||||
});
|
||||
|
||||
api.set_function("is_dawn",
|
||||
[loc]() {
|
||||
return loc->state.isDawn;
|
||||
return loc->isDawn();
|
||||
});
|
||||
|
||||
// advance_darklands_hud()
|
||||
@@ -415,27 +412,26 @@ namespace FRG {
|
||||
|
||||
// is_darklands() → bool
|
||||
api.set_function("is_darklands",
|
||||
[loc]() { return loc->state.isDarklands; });
|
||||
[loc]() { return loc->isDarklands(); });
|
||||
|
||||
api.set_function("setFloatValue",
|
||||
[this_impl = impl.get()](const std::string& key, float value) {
|
||||
if (this_impl->globalFloats)
|
||||
(*this_impl->globalFloats)[key] = value;
|
||||
[loc](const std::string& key, float value) {
|
||||
loc->setGlobalFloat(key, value);
|
||||
});
|
||||
api.set_function("getFloatValue",
|
||||
[this_impl = impl.get()](const std::string& key) -> float {
|
||||
if (!this_impl->globalFloats) return 0.0f;
|
||||
auto it = this_impl->globalFloats->find(key);
|
||||
return it != this_impl->globalFloats->end() ? it->second : 0.0f;
|
||||
[loc](const std::string& key) -> float {
|
||||
return loc->globalFloat(key);
|
||||
});
|
||||
|
||||
api.set_function("get_player_x",
|
||||
[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",
|
||||
[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.
|
||||
@@ -462,15 +458,12 @@ namespace FRG {
|
||||
// Global integer store — shared across all location scripts.
|
||||
// setIntValue(key, value) / getIntValue(key) → int (0 if not set)
|
||||
api.set_function("setIntValue",
|
||||
[this_impl = impl.get()](const std::string& key, int value) {
|
||||
if (this_impl->globalInts)
|
||||
(*this_impl->globalInts)[key] = value;
|
||||
[loc](const std::string& key, int value) {
|
||||
loc->setGlobalInt(key, value);
|
||||
});
|
||||
api.set_function("getIntValue",
|
||||
[this_impl = impl.get()](const std::string& key) -> int {
|
||||
if (!this_impl->globalInts) return 0;
|
||||
auto it = this_impl->globalInts->find(key);
|
||||
return it != this_impl->globalInts->end() ? it->second : 0;
|
||||
[loc](const std::string& key) -> int {
|
||||
return loc->globalInt(key);
|
||||
});
|
||||
|
||||
// set_darklands_callbacks(on_enter, on_exit)
|
||||
@@ -806,64 +799,40 @@ namespace FRG {
|
||||
|
||||
// quest_unlock(quest_id)
|
||||
api.set_function("quest_unlock",
|
||||
[this_impl = impl.get()](const std::string& questId) -> bool {
|
||||
if (!this_impl->questJournal) {
|
||||
std::cerr << "[script] quest_unlock: QuestJournal not set\n";
|
||||
return false;
|
||||
}
|
||||
return this_impl->questJournal->unlockQuest(questId);
|
||||
[loc](const std::string& questId) -> bool {
|
||||
return loc->unlockQuest(questId);
|
||||
});
|
||||
|
||||
// quest_complete(quest_id)
|
||||
api.set_function("quest_complete",
|
||||
[this_impl = impl.get()](const std::string& questId) -> bool {
|
||||
if (!this_impl->questJournal) {
|
||||
std::cerr << "[script] quest_complete: QuestJournal not set\n";
|
||||
return false;
|
||||
}
|
||||
return this_impl->questJournal->completeQuest(questId);
|
||||
[loc](const std::string& questId) -> bool {
|
||||
return loc->completeQuest(questId);
|
||||
});
|
||||
|
||||
// quest_fail(quest_id)
|
||||
api.set_function("quest_fail",
|
||||
[this_impl = impl.get()](const std::string& questId) -> bool {
|
||||
if (!this_impl->questJournal) {
|
||||
std::cerr << "[script] quest_fail: QuestJournal not set\n";
|
||||
return false;
|
||||
}
|
||||
return this_impl->questJournal->failQuest(questId);
|
||||
[loc](const std::string& questId) -> bool {
|
||||
return loc->failQuest(questId);
|
||||
});
|
||||
|
||||
// quest_set_objective_completed(quest_id, objective_id [, completed])
|
||||
api.set_function("quest_set_objective_completed",
|
||||
[this_impl = impl.get()](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;
|
||||
}
|
||||
[loc](const std::string& questId, const std::string& objId, sol::object completed) -> bool {
|
||||
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])
|
||||
api.set_function("quest_set_objective_visible",
|
||||
[this_impl = impl.get()](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;
|
||||
}
|
||||
[loc](const std::string& questId, const std::string& objId, sol::object visible) -> bool {
|
||||
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)
|
||||
api.set_function("quest_set_active_objective",
|
||||
[this_impl = impl.get()](const std::string& questId, int index) -> bool {
|
||||
if (!this_impl->questJournal) {
|
||||
std::cerr << "[script] quest_set_active_objective: QuestJournal not set\n";
|
||||
return false;
|
||||
}
|
||||
return this_impl->questJournal->setActiveObjective(questId, index);
|
||||
[loc](const std::string& questId, int index) -> bool {
|
||||
return loc->setQuestActiveObjective(questId, index);
|
||||
});
|
||||
|
||||
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() {
|
||||
if (!impl || !impl->locationInitCallback.valid()) return;
|
||||
auto result = impl->locationInitCallback();
|
||||
|
||||
+1
-8
@@ -1,8 +1,6 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "ISaveable.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
@@ -10,7 +8,6 @@
|
||||
namespace FRG {
|
||||
|
||||
class Location;
|
||||
class Inventory;
|
||||
|
||||
class ScriptEngine {
|
||||
public:
|
||||
@@ -18,7 +15,7 @@ public:
|
||||
~ScriptEngine();
|
||||
|
||||
// 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.
|
||||
//void runScript(const std::string& path);
|
||||
@@ -37,10 +34,6 @@ public:
|
||||
// Fired when a moving NPC physically bumps into the standing player.
|
||||
void callNpcBumpsPlayerCallback(const std::string& npcId);
|
||||
|
||||
void setGlobalStore(std::unordered_map<std::string, int>* store);
|
||||
void setGlobalFloatStore(std::unordered_map<std::string, float>* store);
|
||||
void setQuestJournal(Quest::QuestJournal* journal);
|
||||
|
||||
void callLocationInitCallback();
|
||||
void callLocationEnterCallback();
|
||||
void callLocationExitCallback();
|
||||
|
||||
@@ -162,21 +162,24 @@ void DialogueRuntime::selectChoice(int index) {
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!flagStore) return 0;
|
||||
auto it = flagStore->find(name);
|
||||
return (it != flagStore->end()) ? it->second : 0;
|
||||
return getGlobalFlag ? getGlobalFlag(name) : 0;
|
||||
}
|
||||
|
||||
void DialogueRuntime::setGlobalFlagStore(std::unordered_map<std::string, int>* store) {
|
||||
flagStore = store;
|
||||
void DialogueRuntime::setGlobalFlagAccessors(
|
||||
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) {
|
||||
questJournal = journal;
|
||||
void DialogueRuntime::setQuestActionHandler(
|
||||
std::function<void(const QuestActionRequest&)> handler) {
|
||||
onQuestAction = std::move(handler);
|
||||
}
|
||||
|
||||
void DialogueRuntime::applyQuestActions(
|
||||
@@ -184,17 +187,20 @@ void DialogueRuntime::applyQuestActions(
|
||||
const std::string& questFail, const std::string& objectiveComplete,
|
||||
const std::string& objectiveVisible)
|
||||
{
|
||||
if (!questJournal) return;
|
||||
if (!questUnlock.empty()) questJournal->unlockQuest(questUnlock);
|
||||
if (!questComplete.empty()) questJournal->completeQuest(questComplete);
|
||||
if (!questFail.empty()) questJournal->failQuest(questFail);
|
||||
if (!onQuestAction) return;
|
||||
if (!questUnlock.empty())
|
||||
onQuestAction({QuestActionType::Unlock, questUnlock, {}});
|
||||
if (!questComplete.empty())
|
||||
onQuestAction({QuestActionType::Complete, questComplete, {}});
|
||||
if (!questFail.empty())
|
||||
onQuestAction({QuestActionType::Fail, questFail, {}});
|
||||
if (!objectiveComplete.empty()) {
|
||||
auto [qId, oId] = splitDot(objectiveComplete);
|
||||
questJournal->setObjectiveCompleted(qId, oId);
|
||||
onQuestAction({QuestActionType::CompleteObjective, qId, oId});
|
||||
}
|
||||
if (!objectiveVisible.empty()) {
|
||||
auto [qId, oId] = splitDot(objectiveVisible);
|
||||
questJournal->setObjectiveVisible(qId, oId);
|
||||
onQuestAction({QuestActionType::ShowObjective, qId, oId});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,29 @@
|
||||
|
||||
#include "dialogue/DialogueDatabase.h"
|
||||
#include "dialogue/TranslationDatabase.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "external/nlohmann/json.hpp"
|
||||
#include "ISaveable.h"
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
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 {
|
||||
public:
|
||||
void setDatabase(const DialogueDatabase* value);
|
||||
@@ -41,8 +53,11 @@ public:
|
||||
|
||||
void setFlag(const std::string& name, int value);
|
||||
int getFlag(const std::string& name) const;
|
||||
void setGlobalFlagStore(std::unordered_map<std::string, int>* store);
|
||||
void setQuestJournal(Quest::QuestJournal* journal);
|
||||
void setGlobalFlagAccessors(
|
||||
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.
|
||||
// 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&)> onDialogueLineStarted;
|
||||
std::function<void(const std::string&, bool)> onChatBubbleReady;
|
||||
std::function<void(const QuestActionRequest&)> onQuestAction;
|
||||
|
||||
const DialogueDatabase* database = nullptr;
|
||||
const TranslationDatabase* translations = nullptr;
|
||||
Quest::QuestJournal* questJournal = 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::string currentNodeId;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "cutscene/CutsceneDatabase.h"
|
||||
#include "cutscene/CutsceneOverlay.h"
|
||||
#include "cutscene/CutsceneRuntime.h"
|
||||
#include "quest/QuestJournal.h"
|
||||
#include "ISaveable.h"
|
||||
#include <SDL.h>
|
||||
#include <functional>
|
||||
@@ -58,11 +57,15 @@ public:
|
||||
void setFlag(const std::string& name, int value) { dialogueRuntime.setFlag(name, value); }
|
||||
int getFlag(const std::string& name) const { return dialogueRuntime.getFlag(name); }
|
||||
|
||||
void setGlobalFlagStore(std::unordered_map<std::string, int>* store) {
|
||||
dialogueRuntime.setGlobalFlagStore(store);
|
||||
void setGlobalFlagAccessors(
|
||||
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) {
|
||||
dialogueRuntime.setQuestJournal(journal);
|
||||
void setQuestActionHandler(
|
||||
std::function<void(const QuestActionRequest&)> handler) {
|
||||
dialogueRuntime.setQuestActionHandler(std::move(handler));
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
@@ -94,8 +94,51 @@ struct AppendChatMessageCommand {
|
||||
};
|
||||
struct SetTaxiCalledCommand { bool called; };
|
||||
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 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 SetGlobalFloatCommand { std::string key; float value; };
|
||||
struct SetCharacterTransformCommand {
|
||||
@@ -188,8 +231,20 @@ using GameCommand = std::variant<
|
||||
AppendChatMessageCommand,
|
||||
SetTaxiCalledCommand,
|
||||
StartTaxiCommand,
|
||||
ResetTaxiCommand,
|
||||
SetLocationCameraCommand,
|
||||
SetLocationTargetsCommand,
|
||||
SwitchLocationNavigationCommand,
|
||||
SetNavigationAreaAvailableCommand,
|
||||
AddNavigationObstacleCommand,
|
||||
AddInventoryItemCommand,
|
||||
RemoveInventoryItemCommand,
|
||||
UnlockQuestCommand,
|
||||
CompleteQuestCommand,
|
||||
FailQuestCommand,
|
||||
SetQuestObjectiveCompletedCommand,
|
||||
SetQuestObjectiveVisibleCommand,
|
||||
SetQuestActiveObjectiveCommand,
|
||||
SetGlobalIntCommand,
|
||||
SetGlobalFloatCommand,
|
||||
SetCharacterTransformCommand,
|
||||
@@ -209,13 +264,24 @@ using GameCommand = std::variant<
|
||||
SetTriggerZoneEnabledCommand,
|
||||
ReloadQuestDefinitionsCommand>;
|
||||
|
||||
struct InventoryItemAddedEvent { std::string itemId; };
|
||||
struct InventoryItemRemovedEvent { std::string itemId; };
|
||||
|
||||
using GameServerEvent = std::variant<
|
||||
InventoryItemAddedEvent,
|
||||
InventoryItemRemovedEvent,
|
||||
Quest::QuestEvent>;
|
||||
|
||||
class IGameServer {
|
||||
public:
|
||||
virtual ~IGameServer() = default;
|
||||
|
||||
// Each returned object is immutable and remains valid after later commands.
|
||||
virtual std::shared_ptr<const GameSnapshot> snapshot() const = 0;
|
||||
// Client-relative identity assigned by this server connection.
|
||||
virtual const std::string& localPlayerId() const = 0;
|
||||
virtual void submit(const GameCommand& command) = 0;
|
||||
virtual std::vector<GameServerEvent> drainEvents() = 0;
|
||||
virtual void update(std::int64_t deltaMs) = 0;
|
||||
|
||||
// Persistence stays server-side. The JSON shape is GameState's established
|
||||
|
||||
@@ -1,12 +1,192 @@
|
||||
#include "InternalGameServer.h"
|
||||
#include "../Campaign.h"
|
||||
#include "../Location.h"
|
||||
#include <algorithm>
|
||||
#include <random>
|
||||
#include <type_traits>
|
||||
|
||||
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;
|
||||
|
||||
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 result;
|
||||
@@ -85,7 +265,11 @@ std::shared_ptr<const GameSnapshot> InternalGameServer::snapshot() const
|
||||
|
||||
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)>;
|
||||
if constexpr (std::is_same_v<T, SetWorldModeCommand>) {
|
||||
state_.isDarklands = value.darklands;
|
||||
@@ -126,10 +310,83 @@ void InternalGameServer::submit(const GameCommand& command)
|
||||
auto location = state_.locations.find(value.locationId);
|
||||
if (location != state_.locations.end())
|
||||
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>) {
|
||||
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>) {
|
||||
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>) {
|
||||
state_.globalInts[value.key] = value.value;
|
||||
} 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);
|
||||
}
|
||||
}, command);
|
||||
markHostMutation();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
std::vector<GameServerEvent> InternalGameServer::drainEvents()
|
||||
{
|
||||
std::vector<GameServerEvent> result;
|
||||
result.swap(pendingEvents_);
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
auto stored = state_.locations.find(state_.currentLocationName);
|
||||
if (stored == state_.locations.end()) return;
|
||||
updateBattleZoneTestBot(deltaMs);
|
||||
const bool interactiveObjectsLocked =
|
||||
stored->second.lockInteractiveObjectsDuringTutorial &&
|
||||
state_.tutorialStep < TutorialStep::PickUpItem;
|
||||
LocationSimulationEvents events =
|
||||
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);
|
||||
synchronizeRuntimeAuxiliaryState();
|
||||
}
|
||||
@@ -251,9 +519,12 @@ void InternalGameServer::save(nlohmann::json& out) const
|
||||
|
||||
void InternalGameServer::load(const nlohmann::json& in)
|
||||
{
|
||||
pendingEvents_.clear();
|
||||
state_.load(in);
|
||||
battleZoneTestBotElapsedMs_ = 0;
|
||||
ensureBattleZoneTestBot();
|
||||
applyStateToRuntime();
|
||||
markHostMutation();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
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 {
|
||||
runtimeLocations_[id]->captureAuxiliaryState(state_.locations[id]);
|
||||
}
|
||||
markHostMutation();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::synchronizeRuntimeAuxiliaryState()
|
||||
@@ -274,7 +545,7 @@ void InternalGameServer::synchronizeRuntimeAuxiliaryState()
|
||||
if (!location) continue;
|
||||
location->captureAuxiliaryState(state_.locations[id]);
|
||||
}
|
||||
markHostMutation();
|
||||
publishMutation();
|
||||
}
|
||||
|
||||
void InternalGameServer::applyStateToRuntime()
|
||||
@@ -286,7 +557,7 @@ void InternalGameServer::applyStateToRuntime()
|
||||
}
|
||||
}
|
||||
|
||||
void InternalGameServer::markHostMutation()
|
||||
void InternalGameServer::publishMutation()
|
||||
{
|
||||
++revision_;
|
||||
cachedSnapshot_.reset();
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
#include "IGameServer.h"
|
||||
|
||||
class AudioPlayerAsync;
|
||||
|
||||
namespace FRG {
|
||||
|
||||
class Game;
|
||||
class Character;
|
||||
class Location;
|
||||
struct LocationConfig;
|
||||
struct LocationSetup;
|
||||
|
||||
// In-process implementation used by single-player. GameState exists exactly
|
||||
// once, here; clients see immutable snapshots and mutate it with typed commands.
|
||||
@@ -15,7 +19,9 @@ public:
|
||||
InternalGameServer();
|
||||
|
||||
std::shared_ptr<const GameSnapshot> snapshot() const override;
|
||||
const std::string& localPlayerId() const override { return localPlayerId_; }
|
||||
void submit(const GameCommand& command) override;
|
||||
std::vector<GameServerEvent> drainEvents() override;
|
||||
void update(std::int64_t deltaMs) override;
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
@@ -23,25 +29,38 @@ public:
|
||||
private:
|
||||
friend class Game;
|
||||
|
||||
// Transitional host operations are intentionally available only to Game,
|
||||
// which owns location loading and old-save migration. They are never part of
|
||||
// IGameServer and therefore cannot leak mutable state to UI/network clients.
|
||||
GameState& hostState() { return state_; }
|
||||
const GameState& hostState() const { return state_; }
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>>& runtimeLocations() { return runtimeLocations_; }
|
||||
const std::unordered_map<std::string, std::shared_ptr<Location>>& runtimeLocations() const { return runtimeLocations_; }
|
||||
// Host-only lifecycle operations. They never expose authoritative state and
|
||||
// are intentionally absent from IGameServer.
|
||||
void initializeCampaign(
|
||||
const std::string& campaignId, const std::string& questsPath,
|
||||
const std::string& zipFile);
|
||||
void clearCampaign();
|
||||
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 synchronizeRuntimeAuxiliaryState();
|
||||
void applyStateToRuntime();
|
||||
void markHostMutation();
|
||||
void publishMutation();
|
||||
void ensureBattleZoneTestBot();
|
||||
void updateBattleZoneTestBot(std::int64_t deltaMs);
|
||||
|
||||
GameSnapshot buildSnapshot() const;
|
||||
|
||||
GameState state_;
|
||||
std::string localPlayerId_ = "player";
|
||||
std::int64_t battleZoneTestBotElapsedMs_ = 0;
|
||||
std::unordered_map<std::string, std::shared_ptr<Location>> runtimeLocations_;
|
||||
std::uint64_t revision_ = 1;
|
||||
mutable std::uint64_t cachedRevision_ = 0;
|
||||
mutable std::shared_ptr<const GameSnapshot> cachedSnapshot_;
|
||||
std::vector<GameServerEvent> pendingEvents_;
|
||||
};
|
||||
|
||||
} // namespace FRG
|
||||
+5
-4
@@ -8,19 +8,20 @@
|
||||
|
||||
namespace FRG {
|
||||
|
||||
void Inventory::addItem(const std::string& itemId) {
|
||||
bool Inventory::addItem(const std::string& itemId) {
|
||||
itemIds.push_back(itemId);
|
||||
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);
|
||||
if (it != itemIds.end()) {
|
||||
logger() << "Item removed from inventory: " << itemId << std::endl;
|
||||
if (onItemRemoved) onItemRemoved(itemId);
|
||||
itemIds.erase(it);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Inventory::hasItem(const std::string& itemId) const {
|
||||
|
||||
+2
-7
@@ -2,7 +2,6 @@
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include "ISaveable.h"
|
||||
|
||||
namespace FRG {
|
||||
@@ -22,8 +21,8 @@ namespace FRG {
|
||||
|
||||
class Inventory : public ISaveable {
|
||||
public:
|
||||
void addItem(const std::string& itemId);
|
||||
void removeItem(const std::string& itemId);
|
||||
bool addItem(const std::string& itemId);
|
||||
bool removeItem(const std::string& itemId);
|
||||
// Resolves current items from ItemRegistry by id, so returned data
|
||||
// always reflects the currently loaded (localized) definitions.
|
||||
std::vector<Item> getItems() const;
|
||||
@@ -34,10 +33,6 @@ namespace FRG {
|
||||
void save(nlohmann::json& out) const 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:
|
||||
std::vector<std::string> itemIds;
|
||||
};
|
||||
|
||||
+43
-26
@@ -106,38 +106,48 @@ bool QuestJournal::setStatus(const std::string& questId, QuestStatus status) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QuestJournal::unlockQuest(const std::string& questId) {
|
||||
QuestMutationResult QuestJournal::unlockQuest(const std::string& questId) {
|
||||
QuestMutationResult result;
|
||||
QuestState* quest = findQuest(questId);
|
||||
if (!quest) return false;
|
||||
if (!quest) return result;
|
||||
result.success = true;
|
||||
if (quest->status == QuestStatus::Hidden) {
|
||||
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) {
|
||||
bool changed = setStatus(questId, QuestStatus::Completed);
|
||||
if (changed && onQuestCompleted) onQuestCompleted(questId);
|
||||
return changed;
|
||||
QuestMutationResult QuestJournal::completeQuest(const std::string& questId) {
|
||||
QuestMutationResult result;
|
||||
result.success = setStatus(questId, QuestStatus::Completed);
|
||||
if (result.success)
|
||||
result.events.push_back({QuestEventType::Completed, questId, {}});
|
||||
return result;
|
||||
}
|
||||
|
||||
bool QuestJournal::failQuest(const std::string& questId) {
|
||||
bool changed = setStatus(questId, QuestStatus::Failed);
|
||||
if (changed && onQuestFailed) onQuestFailed(questId);
|
||||
return changed;
|
||||
QuestMutationResult QuestJournal::failQuest(const std::string& questId) {
|
||||
QuestMutationResult result;
|
||||
result.success = setStatus(questId, QuestStatus::Failed);
|
||||
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);
|
||||
if (!quest) return false;
|
||||
if (!quest) return result;
|
||||
|
||||
for (auto& objective : quest->definition.objectives) {
|
||||
if (objective.id == objectiveId) {
|
||||
objective.completed = completed;
|
||||
result.success = true;
|
||||
|
||||
if (completed) {
|
||||
if (onObjectiveCompleted) onObjectiveCompleted(questId, objectiveId);
|
||||
result.events.push_back(
|
||||
{QuestEventType::ObjectiveCompleted, questId, objectiveId});
|
||||
}
|
||||
|
||||
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; });
|
||||
if (allDone) {
|
||||
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);
|
||||
if (!quest) return false;
|
||||
if (!quest) return result;
|
||||
|
||||
for (auto& objective : quest->definition.objectives) {
|
||||
if (objective.id == objectiveId) {
|
||||
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);
|
||||
if (!quest) return false;
|
||||
if (!quest) return result;
|
||||
|
||||
if (objectiveIndex < 0 || objectiveIndex >= static_cast<int>(quest->definition.objectives.size())) {
|
||||
return false;
|
||||
return result;
|
||||
}
|
||||
|
||||
quest->activeObjectiveIndex = objectiveIndex;
|
||||
return true;
|
||||
result.success = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
QuestState* QuestJournal::findQuest(const std::string& questId) {
|
||||
|
||||
+24
-13
@@ -6,10 +6,27 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
|
||||
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 {
|
||||
public:
|
||||
bool loadFromFile(const std::string& path, const std::string& zipFile = "");
|
||||
@@ -22,19 +39,13 @@ public:
|
||||
void save(nlohmann::json& out) const override;
|
||||
void load(const nlohmann::json& in) override;
|
||||
|
||||
// Event callbacks — set by the owner (MenuManager) to receive notifications.
|
||||
std::function<void(const std::string& questId)> onQuestUnlocked;
|
||||
std::function<void(const std::string& questId)> onQuestCompleted;
|
||||
std::function<void(const std::string& questId)> onQuestFailed;
|
||||
std::function<void(const std::string& questId, const std::string& objectiveId)> onObjectiveCompleted;
|
||||
QuestMutationResult unlockQuest(const std::string& questId);
|
||||
QuestMutationResult completeQuest(const std::string& questId);
|
||||
QuestMutationResult failQuest(const std::string& questId);
|
||||
|
||||
bool unlockQuest(const std::string& questId);
|
||||
bool completeQuest(const std::string& questId);
|
||||
bool failQuest(const std::string& questId);
|
||||
|
||||
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);
|
||||
QuestMutationResult setObjectiveCompleted(const std::string& questId, const std::string& objectiveId, bool completed = true);
|
||||
QuestMutationResult setObjectiveVisible(const std::string& questId, const std::string& objectiveId, bool visible = true);
|
||||
QuestMutationResult setActiveObjective(const std::string& questId, int objectiveIndex);
|
||||
|
||||
QuestState* findQuest(const std::string& questId);
|
||||
const QuestState* findQuest(const std::string& questId) const;
|
||||
|
||||
@@ -100,13 +100,15 @@ namespace FRG {
|
||||
break;
|
||||
}
|
||||
|
||||
auto state = player.stateSnapshot();
|
||||
if (!state) return;
|
||||
std::vector<Eigen::Vector3f> route;
|
||||
route.reserve(player.characterState.pathWaypoints.size() + 1);
|
||||
route.push_back(Eigen::Vector3f(player.characterState.position.x(), 0.0f, player.characterState.position.z()));
|
||||
route.reserve(state->pathWaypoints.size() + 1);
|
||||
route.push_back(Eigen::Vector3f(state->position.x(), 0.0f, state->position.z()));
|
||||
const size_t firstWaypoint = (std::min)(
|
||||
player.characterState.currentWaypointIndex, player.characterState.pathWaypoints.size());
|
||||
for (size_t i = firstWaypoint; i < player.characterState.pathWaypoints.size(); ++i) {
|
||||
const Eigen::Vector3f& waypoint = player.characterState.pathWaypoints[i];
|
||||
state->currentWaypointIndex, state->pathWaypoints.size());
|
||||
for (size_t i = firstWaypoint; i < state->pathWaypoints.size(); ++i) {
|
||||
const Eigen::Vector3f& waypoint = state->pathWaypoints[i];
|
||||
route.push_back(Eigen::Vector3f(waypoint.x(), 0.0f, waypoint.z()));
|
||||
}
|
||||
|
||||
@@ -127,7 +129,7 @@ namespace FRG {
|
||||
constexpr float kPlayerClearanceMs = 20.0f;
|
||||
constexpr float kPlayerMaximumSpeedMultiplier = 1.5f;
|
||||
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 revealDurationMs = (std::min)(kMaximumRevealMs, playerRouteTimeMs / 3.0f);
|
||||
const float waveSpeed = totalLength / (revealDurationMs / 1000.0f);
|
||||
|
||||
Reference in new issue
Block a user