diff --git a/src/features/editScene/components/ActionDebug.hpp b/src/features/editScene/components/ActionDebug.hpp index f1eab05..c0b0a91 100644 --- a/src/features/editScene/components/ActionDebug.hpp +++ b/src/features/editScene/components/ActionDebug.hpp @@ -5,30 +5,30 @@ #include "GoapBlackboard.hpp" #include #include +#include /** - * Configuration for one animation state machine / state pair. - * Used to map logical animation names (e.g. "idle", "walk", "run") - * to the actual state machine and state in the animation tree. + * Configuration for one path following animation state. + * + * Each state (e.g. "idle", "walk", "run", "swim-idle", "swim", "swim-fast") + * consists of unlimited name-value pairs where: + * - name = state machine name (e.g. "main", "locomotion") + * - value = state name within that machine (e.g. "Idle", "Walking") + * + * When a path following state is activated, ALL its name-value pairs + * are applied via AnimationTreeSystem::setState(). */ -struct AnimationStateConfig { - /** Logical name used by code (e.g. "idle", "walk", "run") */ +struct PathFollowingState { + /** Logical name (e.g. "idle", "walk", "run", "swim-idle", "swim", "swim-fast") */ Ogre::String name; - /** Name of the state machine in the animation tree */ - Ogre::String stateMachine = "Locomotion"; + /** State machine name -> state name pairs */ + std::vector > stateMachineStates; - /** Name of the state within the state machine */ - Ogre::String stateName = "Idle"; + PathFollowingState() = default; - AnimationStateConfig() = default; - - AnimationStateConfig(const Ogre::String &name_, - const Ogre::String &stateMachine_, - const Ogre::String &stateName_) + PathFollowingState(const Ogre::String &name_) : name(name_) - , stateMachine(stateMachine_) - , stateName(stateName_) { } }; @@ -57,13 +57,13 @@ struct ActionDebug { // Debug output Ogre::String lastResult; - // --- Animation state machine configuration --- - // List of animation state configs (name/stateMachine/stateName triples) + // --- Path following animation states --- + // Each state has unlimited state machine name -> state name pairs. // Default entries: idle, walk, run - std::vector animStates = { - { "idle", "Locomotion", "Idle" }, - { "walk", "Locomotion", "Walk" }, - { "run", "Locomotion", "Run" }, + std::vector pathFollowingStates = { + { "idle" }, + { "walk" }, + { "run" }, }; // Walk speed (m/s) used for root motion scaling @@ -78,21 +78,17 @@ struct ActionDebug { ActionDebug() = default; /** - * Get the state machine and state name for a given logical animation name. - * Returns true if found, false if not (caller should use defaults). + * Get the state machine/state pairs for a given path following state name. + * Returns nullptr if not found. */ - bool getAnimState(const Ogre::String &animName, - Ogre::String &outStateMachine, - Ogre::String &outStateName) const + const PathFollowingState * + findPathState(const Ogre::String &stateName) const { - for (const auto &cfg : animStates) { - if (cfg.name == animName) { - outStateMachine = cfg.stateMachine; - outStateName = cfg.stateName; - return true; - } + for (const auto &state : pathFollowingStates) { + if (state.name == stateName) + return &state; } - return false; + return nullptr; } }; diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index a1146e3..916e7ef 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -1936,6 +1936,9 @@ nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity) for (const auto &pair : cs.slots) json["slots"][pair.first] = pair.second; + // Serialize front axis + json["frontAxis"] = { cs.frontAxis.x, cs.frontAxis.y, cs.frontAxis.z }; + return json; } @@ -1949,6 +1952,16 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, for (auto &[slot, mesh] : json["slots"].items()) cs.slots[slot] = mesh.get(); } + // Deserialize front axis + if (json.contains("frontAxis") && json["frontAxis"].is_array() && + json["frontAxis"].size() >= 3) { + cs.frontAxis = Ogre::Vector3(json["frontAxis"][0].get(), + json["frontAxis"][1].get(), + json["frontAxis"][2].get()); + if (cs.frontAxis.squaredLength() > 0.0001f) + cs.frontAxis.normalise(); + } + cs.dirty = true; entity.set(cs); } @@ -3215,14 +3228,19 @@ nlohmann::json SceneSerializer::serializeActionDebug(flecs::entity entity) if (!debug.selectedGoalName.empty()) json["selectedGoalName"] = debug.selectedGoalName; - // Serialize animation state configs - json["animStates"] = nlohmann::json::array(); - for (const auto &cfg : debug.animStates) { + // Serialize path following animation states + json["pathFollowingStates"] = nlohmann::json::array(); + for (const auto &pfState : debug.pathFollowingStates) { nlohmann::json entry; - entry["name"] = cfg.name; - entry["stateMachine"] = cfg.stateMachine; - entry["stateName"] = cfg.stateName; - json["animStates"].push_back(entry); + entry["name"] = pfState.name; + entry["stateMachineStates"] = nlohmann::json::array(); + for (const auto &pair : pfState.stateMachineStates) { + nlohmann::json pairEntry; + pairEntry["stateMachine"] = pair.first; + pairEntry["stateName"] = pair.second; + entry["stateMachineStates"].push_back(pairEntry); + } + json["pathFollowingStates"].push_back(entry); } json["walkSpeed"] = debug.walkSpeed; @@ -3240,28 +3258,71 @@ void SceneSerializer::deserializeActionDebug(flecs::entity entity, debug.selectedActionName = json.value("selectedActionName", ""); debug.selectedGoalName = json.value("selectedGoalName", ""); - // Deserialize animation state configs (new format) - if (json.contains("animStates") && json["animStates"].is_array()) { - debug.animStates.clear(); + // Deserialize path following animation states + if (json.contains("pathFollowingStates") && + json["pathFollowingStates"].is_array()) { + debug.pathFollowingStates.clear(); + for (const auto &entry : json["pathFollowingStates"]) { + PathFollowingState pfState; + pfState.name = entry.value("name", ""); + if (entry.contains("stateMachineStates") && + entry["stateMachineStates"].is_array()) { + for (const auto &pairEntry : + entry["stateMachineStates"]) { + Ogre::String sm = pairEntry.value( + "stateMachine", "main"); + Ogre::String state = pairEntry.value( + "stateName", "Idle"); + pfState.stateMachineStates.push_back( + { sm, state }); + } + } + debug.pathFollowingStates.push_back(pfState); + } + } else if (json.contains("animStates") && + json["animStates"].is_array()) { + // Backward compatibility: old animStates format + debug.pathFollowingStates.clear(); for (const auto &entry : json["animStates"]) { - AnimationStateConfig cfg; - cfg.name = entry.value("name", ""); - cfg.stateMachine = - entry.value("stateMachine", "Locomotion"); - cfg.stateName = entry.value("stateName", "Idle"); - debug.animStates.push_back(cfg); + PathFollowingState pfState; + pfState.name = entry.value("name", ""); + Ogre::String mainSM = + entry.value("mainStateMachine", "main"); + Ogre::String subSM = + entry.value("subStateMachine", "locomotion"); + Ogre::String stateName = + entry.value("stateName", "Idle"); + pfState.stateMachineStates.push_back({ mainSM, subSM }); + pfState.stateMachineStates.push_back( + { subSM, stateName }); + debug.pathFollowingStates.push_back(pfState); } } else { - // Backward compatibility: old format with individual fields - debug.animStates.clear(); + // Backward compatibility: old individual fields format + debug.pathFollowingStates.clear(); Ogre::String sm = json.value("locomotionStateMachine", "Locomotion"); - debug.animStates.push_back( - { "idle", sm, json.value("idleStateName", "Idle") }); - debug.animStates.push_back( - { "walk", sm, json.value("walkStateName", "Walk") }); - debug.animStates.push_back( - { "run", sm, json.value("runStateName", "Run") }); + { + PathFollowingState idle("idle"); + idle.stateMachineStates.push_back({ "main", sm }); + idle.stateMachineStates.push_back( + { sm, json.value("idleStateName", "Idle") }); + debug.pathFollowingStates.push_back(idle); + } + { + PathFollowingState walk("walk"); + walk.stateMachineStates.push_back({ "main", sm }); + walk.stateMachineStates.push_back( + { sm, json.value("walkStateName", "Walk") }); + debug.pathFollowingStates.push_back(walk); + } + { + PathFollowingState run("run"); + run.stateMachineStates.push_back({ "main", sm }); + run.stateMachineStates.push_back( + { sm, json.value("runStateName", "Run") }); + debug.pathFollowingStates.push_back(run); + } } debug.walkSpeed = json.value("walkSpeed", 2.5f); diff --git a/src/features/editScene/systems/SmartObjectSystem.cpp b/src/features/editScene/systems/SmartObjectSystem.cpp index 3647f3f..3001f55 100644 --- a/src/features/editScene/systems/SmartObjectSystem.cpp +++ b/src/features/editScene/systems/SmartObjectSystem.cpp @@ -111,20 +111,24 @@ void SmartObjectSystem::setLocomotionState(flecs::entity e, if (!e.has()) return; - Ogre::String smName = "Locomotion"; - Ogre::String stateName = animName; - - // Try to get the state machine/state from ActionDebug's animStates list + // Look up the path following state in ActionDebug if (e.has()) { auto &debug = e.get(); - Ogre::String foundSM, foundState; - if (debug.getAnimState(animName, foundSM, foundState)) { - smName = foundSM; - stateName = foundState; + const PathFollowingState *pfState = + debug.findPathState(animName); + if (pfState) { + // Apply ALL state machine name -> state name pairs + for (const auto &pair : pfState->stateMachineStates) { + m_animTreeSystem->setState(e, pair.first, + pair.second, false); + } + return; } } - m_animTreeSystem->setState(e, smName, stateName, false); + // Fallback: if no config found, try to set the state directly + // using the animName as both state machine and state name + m_animTreeSystem->setState(e, "main", animName, false); } bool SmartObjectSystem::testSmartObjectAction(flecs::entity character, diff --git a/src/features/editScene/ui/ActionDebugEditor.cpp b/src/features/editScene/ui/ActionDebugEditor.cpp index af89e60..0b4ec12 100644 --- a/src/features/editScene/ui/ActionDebugEditor.cpp +++ b/src/features/editScene/ui/ActionDebugEditor.cpp @@ -153,47 +153,88 @@ void ActionDebugEditor::renderGoalTester(flecs::entity entity, void ActionDebugEditor::renderAnimationConfig(ActionDebug &debug) { - ImGui::Text("Animation State Configs:"); + ImGui::Text("Path Following Animation States:"); ImGui::TextDisabled( - "Map logical names (idle/walk/run) to state machine/state pairs"); + "Each state (idle/walk/run/swim-idle/swim/swim-fast) has unlimited\n" + "state machine name -> state name pairs applied when activated."); ImGui::Separator(); - // Render each animation state config - int removeIdx = -1; - for (int i = 0; i < (int)debug.animStates.size(); i++) { - auto &cfg = debug.animStates[i]; + // Render each path following state + int removeStateIdx = -1; + for (int i = 0; i < (int)debug.pathFollowingStates.size(); i++) { + auto &pfState = debug.pathFollowingStates[i]; ImGui::PushID(i); char buf[256]; - ImGui::Text("Entry %d:", i); + ImGui::Text("State %d:", i); ImGui::Indent(); // Logical name (e.g. "idle", "walk", "run") - strncpy(buf, cfg.name.c_str(), sizeof(buf) - 1); + strncpy(buf, pfState.name.c_str(), sizeof(buf) - 1); buf[sizeof(buf) - 1] = '\0'; if (ImGui::InputText("Name", buf, sizeof(buf))) { - cfg.name = buf; + pfState.name = buf; } - // State machine name - strncpy(buf, cfg.stateMachine.c_str(), sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; - if (ImGui::InputText("State Machine", buf, sizeof(buf))) { - cfg.stateMachine = buf; + ImGui::Text("State Machine -> State pairs:"); + ImGui::Indent(); + + // Render each name-value pair + int removePairIdx = -1; + for (int j = 0; j < (int)pfState.stateMachineStates.size(); + j++) { + auto &pair = pfState.stateMachineStates[j]; + ImGui::PushID(j); + + char smBuf[128]; + char stateBuf[128]; + + strncpy(smBuf, pair.first.c_str(), sizeof(smBuf) - 1); + smBuf[sizeof(smBuf) - 1] = '\0'; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputText("##sm", smBuf, sizeof(smBuf))) { + pair.first = smBuf; + } + + ImGui::SameLine(); + ImGui::Text("->"); + + ImGui::SameLine(); + strncpy(stateBuf, pair.second.c_str(), + sizeof(stateBuf) - 1); + stateBuf[sizeof(stateBuf) - 1] = '\0'; + ImGui::SetNextItemWidth(120.0f); + if (ImGui::InputText("##state", stateBuf, + sizeof(stateBuf))) { + pair.second = stateBuf; + } + + ImGui::SameLine(); + if (ImGui::SmallButton("X")) { + removePairIdx = j; + } + + ImGui::PopID(); } - // State name within the state machine - strncpy(buf, cfg.stateName.c_str(), sizeof(buf) - 1); - buf[sizeof(buf) - 1] = '\0'; - if (ImGui::InputText("State Name", buf, sizeof(buf))) { - cfg.stateName = buf; + if (removePairIdx >= 0) { + pfState.stateMachineStates.erase( + pfState.stateMachineStates.begin() + + removePairIdx); } - // Remove button (keep at least 1 entry) - if (debug.animStates.size() > 1) { - if (ImGui::SmallButton("Remove")) { - removeIdx = i; + if (ImGui::SmallButton("+ Add Pair")) { + pfState.stateMachineStates.push_back( + { "main", "Idle" }); + } + + ImGui::Unindent(); + + // Remove state button + if (debug.pathFollowingStates.size() > 1) { + if (ImGui::SmallButton("Remove State")) { + removeStateIdx = i; } } @@ -202,13 +243,15 @@ void ActionDebugEditor::renderAnimationConfig(ActionDebug &debug) ImGui::PopID(); } - if (removeIdx >= 0) { - debug.animStates.erase(debug.animStates.begin() + removeIdx); + if (removeStateIdx >= 0) { + debug.pathFollowingStates.erase( + debug.pathFollowingStates.begin() + removeStateIdx); } - // Add new entry button - if (ImGui::Button("Add Animation State")) { - debug.animStates.push_back(AnimationStateConfig()); + // Add new state button + if (ImGui::Button("Add Path Following State")) { + debug.pathFollowingStates.push_back( + PathFollowingState("new_state")); } ImGui::Separator(); diff --git a/src/features/editScene/ui/CharacterSlotsEditor.cpp b/src/features/editScene/ui/CharacterSlotsEditor.cpp index f84e9c7..7487d93 100644 --- a/src/features/editScene/ui/CharacterSlotsEditor.cpp +++ b/src/features/editScene/ui/CharacterSlotsEditor.cpp @@ -21,13 +21,13 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, CharacterSlotSystem::loadCatalog(); /* Age selector */ - std::vector ages = - CharacterSlotSystem::getAges(); + std::vector ages = CharacterSlotSystem::getAges(); Ogre::String currentAge = cs.age; if (ImGui::BeginCombo("Age", currentAge.c_str())) { for (const auto &age : ages) { bool isSelected = (currentAge == age); - if (ImGui::Selectable(age.c_str(), isSelected)) { + if (ImGui::Selectable(age.c_str(), + isSelected)) { cs.age = age; modified = true; cs.dirty = true; @@ -45,7 +45,8 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, if (ImGui::BeginCombo("Sex", currentSex.c_str())) { for (const auto &sex : sexes) { bool isSelected = (currentSex == sex); - if (ImGui::Selectable(sex.c_str(), isSelected)) { + if (ImGui::Selectable(sex.c_str(), + isSelected)) { cs.sex = sex; modified = true; cs.dirty = true; @@ -58,11 +59,43 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, ImGui::Separator(); + /* Front-facing axis */ + { + Ogre::Vector3 axis = cs.frontAxis; + float axisVals[3] = { axis.x, axis.y, axis.z }; + ImGui::Text("Front Axis:"); + ImGui::SameLine(); + ImGui::TextDisabled( + "(direction character faces, used by path following)"); + if (ImGui::DragFloat3("##frontAxis", axisVals, 0.1f, + -1.0f, 1.0f)) { + cs.frontAxis = Ogre::Vector3( + axisVals[0], axisVals[1], axisVals[2]); + if (cs.frontAxis.squaredLength() > 0.0001f) + cs.frontAxis.normalise(); + modified = true; + } + /* Quick presets */ + ImGui::SameLine(); + if (ImGui::SmallButton("-Z")) { + cs.frontAxis = Ogre::Vector3::NEGATIVE_UNIT_Z; + modified = true; + } + ImGui::SameLine(); + if (ImGui::SmallButton("+Z")) { + cs.frontAxis = Ogre::Vector3::UNIT_Z; + modified = true; + } + } + + ImGui::Separator(); + /* Collect available and configured slots */ std::vector availableSlots = CharacterSlotSystem::getSlots(cs.age, cs.sex); for (const auto &pair : cs.slots) { - if (std::find(availableSlots.begin(), availableSlots.end(), + if (std::find(availableSlots.begin(), + availableSlots.end(), pair.first) == availableSlots.end()) availableSlots.push_back(pair.first); } @@ -80,9 +113,8 @@ bool CharacterSlotsEditor::renderComponent(flecs::entity entity, slot); Ogre::String label = slot; - Ogre::String preview = currentMesh.empty() ? - "(none)" : - currentMesh; + Ogre::String preview = + currentMesh.empty() ? "(none)" : currentMesh; if (ImGui::BeginCombo(label.c_str(), preview.c_str())) { bool noneSelected = currentMesh.empty();