diff --git a/GAME_STATE_ARCHITECTURE.md b/GAME_STATE_ARCHITECTURE.md index 21b811c..d72cc4e 100644 --- a/GAME_STATE_ARCHITECTURE.md +++ b/GAME_STATE_ARCHITECTURE.md @@ -140,7 +140,9 @@ Snapshots have a monotonically increasing revision. `InternalGameServer` caches - Script globals. - Character transforms, attack targets, navigation destinations, stopping, facing, replanning, health, damage, death reset, attack combos, and animation - metadata. + metadata. Character position, facing angle, home position, face target, and + enabled state use separate semantic commands; no whole-character commit + command is exposed. - Interactive-object activation. - Trigger-zone state. - Quest-definition reloads. diff --git a/src/CharacterState.cpp b/src/CharacterState.cpp index 2a05403..b71d514 100644 --- a/src/CharacterState.cpp +++ b/src/CharacterState.cpp @@ -144,24 +144,6 @@ CharacterUpdateEvents CharacterState::update( CharacterUpdateEvents events; if (!enabled) return events; - if (slowMoTimeRemaining > 0.0f) { - slowMoTimeRemaining = (std::max)( - 0.0f, slowMoTimeRemaining - static_cast(deltaMs) / 1000.0f); - - float transitionProgress = 1.0f; - if (slowMoTimeRemaining > slowMoActiveTime - slowMoTransitionTime) { - const float timeInTransition = slowMoActiveTime - slowMoTimeRemaining; - transitionProgress = (std::min)(1.0f, timeInTransition / slowMoTransitionTime); - } else if (slowMoTimeRemaining < slowMoTransitionTime) { - transitionProgress = (std::max)(0.0f, slowMoTimeRemaining / slowMoTransitionTime); - } - const float speedMultiplier = isPlayer - ? 1.0f + 0.5f * transitionProgress - : 1.0f - 0.9f * transitionProgress; - deltaMs = static_cast( - static_cast(deltaMs) * speedMultiplier); - } - const CharacterObservation* attackTarget = context.attackTarget; const CharacterObservation* faceTarget = context.faceTarget; if (!attackTarget) { diff --git a/src/CharacterState.h b/src/CharacterState.h index 7e2d367..efb7bc5 100644 --- a/src/CharacterState.h +++ b/src/CharacterState.h @@ -97,11 +97,6 @@ public: Eigen::Vector3f homePosition = Eigen::Vector3f::Zero(); float maxHomeDrift = 3.0f; - // --- Slow-motion effect --- - float slowMoTimeRemaining = 0.0f; - float slowMoActiveTime = 6.0f; - float slowMoTransitionTime = 0.5f; - // --- Authoritative logical animation clock --- // Frame counts are immutable metadata copied from loaded clips during // character construction. Simulation advances animationFrame itself. diff --git a/src/Game.cpp b/src/Game.cpp index 2bd5e79..f451f02 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -649,11 +649,10 @@ namespace FRG Location* loc = currentLocation(); if (!loc) return; if (loc->player) { - loc->updateCharacterState(*loc->player, [&](CharacterState& workingState) { - workingState.position = destPos; - workingState.facingAngle = destRotY; - workingState.targetFacingAngle = destRotY; - }); + server().submit(SetCharacterPositionCommand{ + destName, loc->player->entityId(), destPos}); + server().submit(SetCharacterFacingAngleCommand{ + destName, loc->player->entityId(), destRotY, true}); loc->stopCharacter(*loc->player); } loc->localState().cameraAzimuth = destRotY; @@ -1536,26 +1535,6 @@ namespace FRG return n; } - void Game::activateSlowMoEffect() { - if (!currentLocation()) return; - - if (currentLocation()->player) { - currentLocation()->updateCharacterState( - *currentLocation()->player, [](CharacterState& workingState) { - workingState.slowMoTimeRemaining = workingState.slowMoActiveTime; - }); - } - - for (auto& npc : currentLocation()->npcs) { - if (npc) { - currentLocation()->updateCharacterState( - *npc, [](CharacterState& workingState) { - workingState.slowMoTimeRemaining = workingState.slowMoActiveTime; - }); - } - } - } - void Game::enterCameraDragMode(int eventX, int eventY) { cameraDragging = true; diff --git a/src/Game.h b/src/Game.h index 04ad440..e1cf7f6 100644 --- a/src/Game.h +++ b/src/Game.h @@ -105,8 +105,6 @@ namespace FRG { const IGameServer& server() const { return gameServer; } MenuManager menuManager; - - void activateSlowMoEffect(); #ifdef EMSCRIPTEN static Game* s_instance; diff --git a/src/Location.cpp b/src/Location.cpp index f43416d..275a342 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -132,18 +132,6 @@ namespace FRG locationId_, character.entityId(), target, std::move(callbackName)}); } - void Location::updateCharacterState( - Character& character, - const std::function& mutation) - { - auto snapshot = character.stateSnapshot(); - if (!snapshot) return; - CharacterState stateCopy = *snapshot; - mutation(stateCopy); - gameServer_.submit(CommitCharacterStateCommand{ - locationId_, character.entityId(), std::move(stateCopy)}); - } - void Location::clearCharacterAttackTarget(Character& character) { gameServer_.submit(ClearCharacterAttackTargetCommand{ @@ -1847,7 +1835,6 @@ namespace FRG void Location::update(int64_t delta, const LocationSimulationEvents& events) { - pathVisualization.update(delta); synchronizeRemotePlayers(); auto locationState = stateSnapshot(); if (!locationState) return; @@ -2022,7 +2009,7 @@ namespace FRG Eigen::Vector3f camRight(cosAzim, 0.f, sinAzim); Eigen::Vector3f camForward(sinAzim * cosIncl, -sinIncl, -cosAzim * cosIncl); Eigen::Vector3f camUp(sinAzim * sinIncl, cosIncl, -cosAzim * sinIncl); - const Eigen::Vector3f playerPos = playerState->position; + const Eigen::Vector3f playerPos = player->renderPosition(); Eigen::Vector3f camPos = playerPos + Eigen::Vector3f(-sinAzim * cosIncl, sinIncl, cosAzim * cosIncl) * Environment::zoom + Eigen::Vector3f(0,1,0); Eigen::Vector3f rayDir = (camForward + camRight * (ndcX * aspect * tanHalfFov) + camUp * (ndcY * tanHalfFov)).normalized(); @@ -2065,10 +2052,7 @@ namespace FRG ? clickedObject->state.interactionPosition : clickedObject->state.position; setCharacterTarget(*player, interactionTarget); - auto updatedPlayerState = player->stateSnapshot(); - if (updatedPlayerState && !updatedPlayerState->pathWaypoints.empty()) { - pathVisualization.start(*player, interactionTarget); - } + pathVisualization.start(*player, interactionTarget); clearCharacterAttackTarget(*player); //logger() << "[CLICK] Player moving to object..." << std::endl; } @@ -2101,21 +2085,17 @@ namespace FRG tryCharacterAttackCombo(*player); } const std::string targetId = clickedNpc->entityId(); - updateCharacterState(*player, [targetId](CharacterState& workingState) { - workingState.attackTargetId = targetId; - }); + gameServer_.submit(SetCharacterTargetCommand{ + locationId_, player->entityId(), targetId}); gameServer_.submit(SetLocationTargetsCommand{ locationId_, std::string(), std::string(), std::string()}); if (distance >= Character::kAttackRange) { setCharacterTarget(*player, clickedNpcState->position); - auto updatedPlayerState = player->stateSnapshot(); - if (updatedPlayerState && !updatedPlayerState->pathWaypoints.empty()) { - pathVisualization.start( - *player, - clickedNpcState->position, - PathVisualization::Style::Hostile); - } + pathVisualization.start( + *player, + clickedNpcState->position, + PathVisualization::Style::Hostile); } if (distance <= clickedNpcState->interactionRadius) { @@ -2131,9 +2111,9 @@ namespace FRG // Already in talk range — fire immediately, stop, and face the player. //logger() << "[CLICK] *** SUCCESS: Clicked on NPC index: " << npcIndex << " (in range) ***" << std::endl; setCharacterTarget(*player, playerState->position); - updateCharacterState(*npcs[npcIndex], [this](CharacterState& workingState) { - workingState.faceTargetId = gameServer_.localPlayerId(); - }); + gameServer_.submit(SetCharacterFaceTargetCommand{ + locationId_, npcs[npcIndex]->entityId(), + gameServer_.localPlayerId()}); scriptEngine.callNpcInteractCallback(clickedNpc->entityId()); gameServer_.submit(SetLocationTargetsCommand{ locationId_, std::string(), std::string(), std::string()}); @@ -2142,10 +2122,7 @@ namespace FRG //logger() << "[CLICK] NPC " << npcIndex << " out of talk range (distance " << distance // << " > " << NPC_TALK_DISTANCE << "); walking to NPC..." << std::endl; setCharacterTarget(*player, clickedNpcState->position); - auto updatedPlayerState = player->stateSnapshot(); - if (updatedPlayerState && !updatedPlayerState->pathWaypoints.empty()) { - pathVisualization.start(*player, clickedNpcState->position); - } + pathVisualization.start(*player, clickedNpcState->position); gameServer_.submit(SetLocationTargetsCommand{ locationId_, std::string(), clickedNpc->entityId(), std::string()}); @@ -2173,13 +2150,10 @@ namespace FRG locationId_, std::string(), std::string(), clickedTeleport->id}); setCharacterTarget(*player, clickedTeleport->position); - auto updatedPlayerState = player->stateSnapshot(); - if (updatedPlayerState && !updatedPlayerState->pathWaypoints.empty()) { - pathVisualization.start( - *player, - clickedTeleport->position, - PathVisualization::Style::Teleport); - } + pathVisualization.start( + *player, + clickedTeleport->position, + PathVisualization::Style::Teleport); clearCharacterAttackTarget(*player); } else { const Eigen::Vector3f clickedDestination(hit.x(), 0.f, hit.z()); @@ -2235,6 +2209,7 @@ namespace FRG void Location::updatePresentation(int64_t delta) { dialogueSystem.update(static_cast(delta)); + pathVisualization.update(delta); for (auto& tz : teleportZones) tz.update(static_cast(delta)); } diff --git a/src/Location.h b/src/Location.h index 1215c05..4734459 100644 --- a/src/Location.h +++ b/src/Location.h @@ -80,6 +80,7 @@ namespace FRG // Detaches the (shared, longer-lived) player from this location. ~Location(); const std::string& locationId() const { return locationId_; } + IGameServer& gameServer() { return gameServer_; } std::shared_ptr stateSnapshot() const; LocationLocalState& localState() { return *localState_; } const LocationLocalState& localState() const { return *localState_; } @@ -175,9 +176,6 @@ namespace FRG bool switchNavigation(int index); void setCharacterTarget(Character& character, const Eigen::Vector3f& target, std::string callbackName = ""); - void updateCharacterState( - Character& character, - const std::function& mutation); void clearCharacterAttackTarget(Character& character); void stopCharacter(Character& character); void faceCharacter(Character& character, std::string targetId); diff --git a/src/ScriptEngine.cpp b/src/ScriptEngine.cpp index b769d32..37e861b 100644 --- a/src/ScriptEngine.cpp +++ b/src/ScriptEngine.cpp @@ -124,9 +124,8 @@ namespace FRG { if (cbName.empty()) std::cerr << "[script] npc_walk_to: on_arrived must be a global Lua function\n"; } - loc->updateCharacterState(*npc, [x, z](CharacterState& workingState) { - workingState.homePosition = Eigen::Vector3f(x, 0.f, z); - }); + loc->gameServer().submit(SetCharacterHomePositionCommand{ + loc->locationId(), npc->entityId(), Eigen::Vector3f(x, 0.f, z)}); loc->setCharacterTarget( *npc, Eigen::Vector3f(x, y, z), std::move(cbName)); }); @@ -142,18 +141,16 @@ namespace FRG { const float rad = angle * static_cast(M_PI) / 180.f; logger() << "[script] npc_rotate_to: " << npcId << " angle " << angle << " degrees (" << rad << " radians)" << std::endl; - loc->updateCharacterState(*npc, [rad](CharacterState& workingState) { - workingState.targetFacingAngle = rad; - }); + loc->gameServer().submit(SetCharacterFacingAngleCommand{ + loc->locationId(), npc->entityId(), rad, false}); }); api.set_function("player_rotate_to", [loc](float angle) { const float rad = angle * static_cast(M_PI) / 180.f; logger() << "[script] player_rotate_to: angle " << angle << " degrees (" << rad << " radians)" << std::endl; - loc->updateCharacterState(*loc->player, [rad](CharacterState& workingState) { - workingState.targetFacingAngle = rad; - }); + loc->gameServer().submit(SetCharacterFacingAngleCommand{ + loc->locationId(), loc->player->entityId(), rad, false}); }); api.set_function("player_stop_and_rotate_to_npc", @@ -188,9 +185,9 @@ namespace FRG { if (cbName.empty()) std::cerr << "[script] player_walk_to: on_arrived must be a global Lua function\n"; } - loc->updateCharacterState(*loc->player, [x, z](CharacterState& workingState) { - workingState.homePosition = Eigen::Vector3f(x, 0.f, z); - }); + loc->gameServer().submit(SetCharacterHomePositionCommand{ + loc->locationId(), loc->player->entityId(), + Eigen::Vector3f(x, 0.f, z)}); loc->setCharacterTarget( *loc->player, Eigen::Vector3f(x, y, z), std::move(cbName)); }); @@ -440,13 +437,7 @@ namespace FRG { if (loc->player) { loc->setCharacterHp(*loc->player, value); - loc->updateCharacterState(*loc->player, [](CharacterState& workingState) { - if (workingState.currentState == AnimationState::ACTION_TO_DEATH || - workingState.currentState == AnimationState::DEATH_IDLE) { - workingState.currentState = AnimationState::STAND; - workingState.resetAnim = true; - } - }); + loc->resetCharacterAfterDeath(*loc->player); } }); // get_player_hp() → float (0 if no player) @@ -602,10 +593,8 @@ namespace FRG { return; } Eigen::Vector3f pos(x, y, z); - loc->updateCharacterState(*npc, [&pos](CharacterState& workingState) { - workingState.position = pos; - workingState.homePosition = pos; - }); + loc->gameServer().submit(SetCharacterPositionCommand{ + loc->locationId(), npc->entityId(), pos, true}); loc->setCharacterTarget(*npc, pos); }); @@ -618,10 +607,8 @@ namespace FRG { return; } const float rad = angle * static_cast(M_PI) / 180.f; - loc->updateCharacterState(*npc, [rad](CharacterState& workingState) { - workingState.facingAngle = rad; - workingState.targetFacingAngle = rad; - }); + loc->gameServer().submit(SetCharacterFacingAngleCommand{ + loc->locationId(), npc->entityId(), rad, true}); }); // set_npc_enabled(npc_id, enabled) @@ -632,34 +619,23 @@ namespace FRG { std::cerr << "[script] set_npc_enabled: unknown NPC id '" << npcId << "'\n"; return; } - loc->updateCharacterState(*npc, [value](CharacterState& workingState) { - workingState.enabled = value; - workingState.attack = 0; - workingState.attack_cooldown = 0; - workingState.currentState = AnimationState::STAND; - if (workingState.canAttack) - workingState.attackTargetId = "player"; - workingState.battle_state = 0; - }); + loc->gameServer().submit(SetCharacterEnabledCommand{ + loc->locationId(), npc->entityId(), value}); }); api.set_function("player_set_position", [loc](float x, float y, float z) { Eigen::Vector3f pos(x, y, z); - loc->updateCharacterState(*loc->player, [&pos](CharacterState& workingState) { - workingState.position = pos; - workingState.homePosition = pos; - }); + loc->gameServer().submit(SetCharacterPositionCommand{ + loc->locationId(), loc->player->entityId(), pos, true}); loc->setCharacterTarget(*loc->player, pos); }); api.set_function("player_set_rotation", [loc](float angle) { const float rad = angle * static_cast(M_PI) / 180.f; - loc->updateCharacterState(*loc->player, [rad](CharacterState& workingState) { - workingState.facingAngle = rad; - workingState.targetFacingAngle = rad; - }); + loc->gameServer().submit(SetCharacterFacingAngleCommand{ + loc->locationId(), loc->player->entityId(), rad, true}); }); // move_object(name, x, y, z, duration_sec [, on_complete]) diff --git a/src/game_server/IGameServer.h b/src/game_server/IGameServer.h index 17da003..54043eb 100644 --- a/src/game_server/IGameServer.h +++ b/src/game_server/IGameServer.h @@ -136,17 +136,38 @@ struct SetQuestActiveObjectiveCommand { }; struct SetGlobalIntCommand { std::string key; int value; }; struct SetGlobalFloatCommand { std::string key; float value; }; -struct SetCharacterTransformCommand { +struct SetCharacterPositionCommand { + std::string locationId; + std::string characterId; + Eigen::Vector3f position; + bool updateHomePosition = false; +}; +struct SetCharacterFacingAngleCommand { + std::string locationId; + std::string characterId; + float angle; + bool setCurrentAngle = false; +}; +struct SetCharacterHomePositionCommand { std::string locationId; std::string characterId; Eigen::Vector3f position; - float facingAngle; }; struct SetCharacterTargetCommand { std::string locationId; std::string characterId; std::string targetId; }; +struct SetCharacterFaceTargetCommand { + std::string locationId; + std::string characterId; + std::string targetId; +}; +struct SetCharacterEnabledCommand { + std::string locationId; + std::string characterId; + bool enabled; +}; struct ClearCharacterAttackTargetCommand { std::string locationId; std::string characterId; @@ -196,11 +217,6 @@ struct DamageCharacterCommand { float amount; std::string attackerId; }; -struct CommitCharacterStateCommand { - std::string locationId; - std::string characterId; - CharacterState state; -}; struct SetInteractiveObjectActiveCommand { std::string locationId; std::string objectId; @@ -241,8 +257,12 @@ using GameCommand = std::variant< SetQuestActiveObjectiveCommand, SetGlobalIntCommand, SetGlobalFloatCommand, - SetCharacterTransformCommand, + SetCharacterPositionCommand, + SetCharacterFacingAngleCommand, + SetCharacterHomePositionCommand, SetCharacterTargetCommand, + SetCharacterFaceTargetCommand, + SetCharacterEnabledCommand, ClearCharacterAttackTargetCommand, SetCharacterDestinationCommand, StopCharacterCommand, @@ -253,7 +273,6 @@ using GameCommand = std::variant< SetCharacterHealthCommand, SetCharacterAnimationMetadataCommand, DamageCharacterCommand, - CommitCharacterStateCommand, SetInteractiveObjectActiveCommand, SetTriggerZoneEnabledCommand, ReloadQuestDefinitionsCommand>; diff --git a/src/game_server/InternalGameServer.cpp b/src/game_server/InternalGameServer.cpp index 7aee684..b315b3a 100644 --- a/src/game_server/InternalGameServer.cpp +++ b/src/game_server/InternalGameServer.cpp @@ -378,18 +378,47 @@ void InternalGameServer::submit(const GameCommand& command) state_.globalInts[value.key] = value.value; } else if constexpr (std::is_same_v) { state_.globalFloats[value.key] = value.value; - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) { character->position = value.position; - character->facingAngle = value.facingAngle; - character->targetFacingAngle = value.facingAngle; + if (value.updateHomePosition) + character->homePosition = value.position; } + } else if constexpr (std::is_same_v) { + CharacterState* character = + state_.findCharacter(value.locationId, value.characterId); + if (character) { + character->targetFacingAngle = value.angle; + if (value.setCurrentAngle) + character->facingAngle = value.angle; + } + } else if constexpr (std::is_same_v) { + CharacterState* character = + state_.findCharacter(value.locationId, value.characterId); + if (character) + character->homePosition = value.position; } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) character->attackTargetId = value.targetId; + } else if constexpr (std::is_same_v) { + CharacterState* character = + state_.findCharacter(value.locationId, value.characterId); + if (character) character->faceTargetId = value.targetId; + } else if constexpr (std::is_same_v) { + CharacterState* character = + state_.findCharacter(value.locationId, value.characterId); + if (character) { + character->enabled = value.enabled; + character->attack = 0; + character->attack_cooldown = 0; + character->currentState = AnimationState::STAND; + if (character->canAttack) + character->attackTargetId = "player"; + character->battle_state = 0; + } } else if constexpr (std::is_same_v) { CharacterState* character = state_.findCharacter(value.locationId, value.characterId); @@ -445,10 +474,6 @@ void InternalGameServer::submit(const GameCommand& command) CharacterState* character = state_.findCharacter(value.locationId, value.characterId); if (character) character->hp = (std::max)(0.f, character->hp - (std::max)(0.f, value.amount)); - } else if constexpr (std::is_same_v) { - CharacterState* character = - state_.findCharacter(value.locationId, value.characterId); - if (character) *character = value.state; } else if constexpr (std::is_same_v) { auto location = state_.locations.find(value.locationId); if (location == state_.locations.end()) return; diff --git a/src/render/PathVisualization.cpp b/src/render/PathVisualization.cpp index 084991a..098b002 100644 --- a/src/render/PathVisualization.cpp +++ b/src/render/PathVisualization.cpp @@ -104,17 +104,28 @@ namespace FRG { if (!state) return; std::vector route; route.reserve(state->pathWaypoints.size() + 1); - route.push_back(Eigen::Vector3f(state->position.x(), 0.0f, state->position.z())); + const Eigen::Vector3f renderPosition = player.renderPosition(); + route.push_back(Eigen::Vector3f(renderPosition.x(), 0.0f, renderPosition.z())); const size_t firstWaypoint = (std::min)( 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())); } + const Eigen::Vector3f clickedPosition( + clickedDestination.x(), 0.0f, clickedDestination.z()); + if (route.size() == 1 || + (route.back() - clickedPosition).norm() > 0.5f) { + // A remote destination is not present in the replica yet, or the + // replica still contains the route for a previous click. Preview + // the new intent directly until authoritative waypoints catch up. + route.clear(); + route.push_back(Eigen::Vector3f( + renderPosition.x(), 0.0f, renderPosition.z())); + route.push_back(clickedPosition); + } - destination = route.size() > 1 - ? route.back() - : Eigen::Vector3f(clickedDestination.x(), 0.0f, clickedDestination.z()); + destination = clickedPosition; float totalLength = 0.0f; for (size_t i = 1; i < route.size(); ++i) {