More work on states, added bot

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

No files matched your search

+235 -138
View File
@@ -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
View File
@@ -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);
+11
View File
@@ -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;
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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();
+1
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+29 -9
View File
@@ -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
View File
@@ -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;
-6
View File
@@ -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
View File
@@ -66,9 +66,6 @@ namespace FRG {
std::unordered_map<std::string, sol::protected_function> cutsceneCompleteCallbacks;
std::unordered_map<std::string, sol::protected_function> 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
View File
@@ -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();
+20 -14
View File
@@ -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});
}
}
+22 -6
View File
@@ -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;
+8 -5
View File
@@ -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:
+66
View File
@@ -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
+279 -8
View File
@@ -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();
+27 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+8 -6
View File
@@ -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);