diff --git a/dGame/dCinema/Recorder.cpp b/dGame/dCinema/Recorder.cpp index 033d803d..71ef5369 100644 --- a/dGame/dCinema/Recorder.cpp +++ b/dGame/dCinema/Recorder.cpp @@ -10,6 +10,7 @@ #include "ServerPreconditions.h" #include "MovementAIComponent.h" #include "BaseCombatAIComponent.h" +#include "MissionComponent.h" using namespace Cinema::Recording; @@ -84,7 +85,14 @@ void Recorder::ActingDispatch(Entity* actor, const std::vector& records } else if (!forkRecord->precondition.empty()) { auto precondtion = Preconditions::CreateExpression(forkRecord->precondition); - success = precondtion.Check(actor); + auto* playerEntity = Game::entityManager->GetEntity(variables->player); + + if (playerEntity != nullptr) { + success = precondtion.Check(playerEntity); + } + else { + success = true; + } } else { success = true; } @@ -224,6 +232,96 @@ void Recorder::ActingDispatch(Entity* actor, const std::vector& records } } + // Check if the record is a companion record + auto* companionRecord = dynamic_cast(record); + + if (companionRecord && variables != nullptr) { + EntityInfo info; + info.lot = actor->GetLOT(); + info.pos = actor->GetPosition(); + info.rot = actor->GetRotation(); + info.scale = 1; + info.spawner = nullptr; + info.spawnerID = variables->player; + info.spawnerNodeID = 0; + info.settings = { + new LDFData>(u"syncLDF", { u"custom_script_client" }), + new LDFData(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua") + }; + + // Spawn it + auto* companion = Game::entityManager->CreateEntity(info); + + // Construct it + Game::entityManager->ConstructEntity(companion); + + CompanionRecord::SetCompanion(companion, variables->player); + + if (!companionRecord->records.empty()) { + ActingDispatch(companion, companionRecord->records, 0, variables); + } + + variables->entities.emplace(companion->GetObjectID()); + } + + auto* spawnRecord = dynamic_cast(record); + + if (spawnRecord && variables != nullptr) { + EntityInfo info; + info.lot = spawnRecord->lot; + info.pos = spawnRecord->position; + info.rot = spawnRecord->rotation; + info.scale = 1; + info.spawner = nullptr; + info.spawnerID = variables->player; + info.spawnerNodeID = 0; + info.settings = { + new LDFData>(u"syncLDF", { u"custom_script_client" }), + new LDFData(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua") + }; + + // Spawn it + auto* entity = Game::entityManager->CreateEntity(info); + + // Construct it + Game::entityManager->ConstructEntity(entity); + + variables->entities.emplace(entity->GetObjectID()); + + if (!spawnRecord->onSpawnRecords.empty()) { + ActingDispatch(entity, spawnRecord->onSpawnRecords, 0, variables); + } + + entity->AddDieCallback([entity, variables, spawnRecord]() { + variables->entities.erase(entity->GetObjectID()); + + ActingDispatch(entity, spawnRecord->onDespawnRecords, 0, variables); + }); + + auto* combatAIComponent = entity->GetComponent(); + + if (combatAIComponent) { + combatAIComponent->SetAggroRadius(200); + combatAIComponent->SetSoftTetherRadius(200); + combatAIComponent->SetHardTetherRadius(200); + combatAIComponent->SetTarget(variables->player); + } + } + + auto* missionRecord = dynamic_cast(record); + + if (missionRecord && variables != nullptr) { + auto* playerEntity = Game::entityManager->GetEntity(variables->player); + + if (playerEntity) { + auto* missionComponent = playerEntity->GetComponent(); + + if (missionComponent) { + missionComponent->CompleteMission(missionRecord->mission); + } + } + } + // Check if the record is a visibility record auto* visibilityRecord = dynamic_cast(record); @@ -414,6 +512,12 @@ void Cinema::Recording::Recorder::LoadRecords(tinyxml2::XMLElement* root, std::v record = new PathFindRecord(); } else if (name == "CombatAIRecord") { record = new CombatAIRecord(); + } else if (name == "CompanionRecord") { + record = new CompanionRecord(); + } else if (name == "SpawnRecord") { + record = new SpawnRecord(); + } else if (name == "MissionRecord") { + record = new MissionRecord(); } else { LOG("Unknown record type: %s", name.c_str()); continue; @@ -1198,3 +1302,170 @@ void Cinema::Recording::CombatAIRecord::Deserialize(tinyxml2::XMLElement* elemen m_Delay = element->DoubleAttribute("t"); } + +void Cinema::Recording::CompanionRecord::Act(Entity* actor) { +} + +void Cinema::Recording::CompanionRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) { + auto* element = document.NewElement("CompanionRecord"); + + element->SetAttribute("t", m_Delay); + + parent->InsertEndChild(element); + +} + +void Cinema::Recording::CompanionRecord::Deserialize(tinyxml2::XMLElement* element) { + m_Delay = element->DoubleAttribute("t"); + + records.clear(); + + Recorder::LoadRecords(element, records); +} + +void CompanionProtcol(Entity* entity, LWOOBJID follow) { + auto* followEntity = Game::entityManager->GetEntity(follow); + + if (followEntity == nullptr) { + return; + } + + auto* movementAI = entity->GetComponent(); + + if (movementAI == nullptr) { + movementAI = entity->AddComponent(MovementAIInfo()); + } + + if (movementAI == nullptr) { + return; + } + + auto* combatAI = entity->GetComponent(); + + if (combatAI == nullptr) { + return; + } + + const auto distance = NiPoint3::Distance(entity->GetPosition(), followEntity->GetPosition()); + + combatAI->SetStartPosition(followEntity->GetPosition()); + combatAI->SetSoftTetherRadius(15.0f); + combatAI->SetHardTetherRadius(25.0f); + combatAI->SetFocusPosition(followEntity->GetPosition()); + combatAI->SetFocusRadius(5.0f); + auto& info = movementAI->GetInfo(); + info.wanderChance = 1.0f; + info.wanderDelayMin = 0.5f; + info.wanderDelayMax = 1.0f; + info.wanderSpeed = 1.0f; + + /*if (distance > 50.0f) { + movementAI->Warp(followEntity->GetPosition()); + + Game::entityManager->SerializeEntity(entity); + }*/ + + /*if (distance > 30.0f) { + movementAI->SetDestination(followEntity->GetPosition()); + movementAI->SetHaltDistance(5.0f); + movementAI->SetMaxSpeed(1.0f); + + if (combatAI) { + combatAI->SetDisabled(true); + } + } + else { + if (combatAI) { + combatAI->SetDisabled(false); + } + }*/ + + entity->AddCallbackTimer(1.0f, [entity, follow]() { + CompanionProtcol(entity, follow); + }); +} + +void Cinema::Recording::CompanionRecord::SetCompanion(Entity* actor, LWOOBJID player) { + CompanionProtcol(actor, player); +} + +Cinema::Recording::SpawnRecord::SpawnRecord(LOT lot, const NiPoint3& position, const NiQuaternion& rotation) { + this->lot = lot; + this->position = position; + this->rotation = rotation; +} + +void Cinema::Recording::SpawnRecord::Act(Entity* actor) { +} + +void Cinema::Recording::SpawnRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) { + auto* element = document.NewElement("SpawnRecord"); + + element->SetAttribute("lot", lot); + element->SetAttribute("x", position.x); + element->SetAttribute("y", position.y); + element->SetAttribute("z", position.z); + + element->SetAttribute("qx", rotation.x); + element->SetAttribute("qy", rotation.y); + element->SetAttribute("qz", rotation.z); + element->SetAttribute("qw", rotation.w); + + element->SetAttribute("t", m_Delay); + + parent->InsertEndChild(element); +} + +void Cinema::Recording::SpawnRecord::Deserialize(tinyxml2::XMLElement* element) { + lot = element->IntAttribute("lot"); + + position.x = element->FloatAttribute("x"); + position.y = element->FloatAttribute("y"); + position.z = element->FloatAttribute("z"); + + if (element->Attribute("qx")) { + rotation.x = element->FloatAttribute("qx"); + rotation.y = element->FloatAttribute("qy"); + rotation.z = element->FloatAttribute("qz"); + rotation.w = element->FloatAttribute("qw"); + } + + m_Delay = element->DoubleAttribute("t"); + + auto* onSpawn = element->FirstChildElement("OnSpawn"); + + if (onSpawn) { + Recorder::LoadRecords(onSpawn, onSpawnRecords); + } + + auto* onDespawn = element->FirstChildElement("OnDespawn"); + + if (onDespawn) { + Recorder::LoadRecords(onDespawn, onDespawnRecords); + } +} + +Cinema::Recording::MissionRecord::MissionRecord(const int32_t& mission) { + this->mission = mission; +} + +void Cinema::Recording::MissionRecord::Act(Entity* actor) { +} + +void Cinema::Recording::MissionRecord::Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) { + auto* element = document.NewElement("MissionRecord"); + + element->SetAttribute("mission", mission); + + element->SetAttribute("t", m_Delay); + + parent->InsertEndChild(element); +} + +void Cinema::Recording::MissionRecord::Deserialize(tinyxml2::XMLElement* element) { + mission = element->IntAttribute("mission"); + + m_Delay = element->DoubleAttribute("t"); +} + + diff --git a/dGame/dCinema/Recorder.h b/dGame/dCinema/Recorder.h index 48abca73..25f54fb3 100644 --- a/dGame/dCinema/Recorder.h +++ b/dGame/dCinema/Recorder.h @@ -398,5 +398,60 @@ public: void Deserialize(tinyxml2::XMLElement* element) override; }; +class CompanionRecord : public Record +{ +public: + std::vector records; + + CompanionRecord() = default; + + void Act(Entity* actor) override; + + void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override; + + void Deserialize(tinyxml2::XMLElement* element) override; + + static void SetCompanion(Entity* actor, LWOOBJID player); +}; + +class SpawnRecord : public Record +{ +public: + LOT lot = LOT_NULL; + + NiPoint3 position; + + NiQuaternion rotation; + + std::vector onSpawnRecords; + + std::vector onDespawnRecords; + + SpawnRecord() = default; + + SpawnRecord(LOT lot, const NiPoint3& position, const NiQuaternion& rotation); + + void Act(Entity* actor) override; + + void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override; + + void Deserialize(tinyxml2::XMLElement* element) override; +}; + +class MissionRecord : public Record +{ +public: + int32_t mission; + + MissionRecord() = default; + + MissionRecord(const int32_t& mission); + + void Act(Entity* actor) override; + + void Serialize(tinyxml2::XMLDocument& document, tinyxml2::XMLElement* parent) override; + + void Deserialize(tinyxml2::XMLElement* element) override; +}; } diff --git a/dGame/dCinema/Scene.cpp b/dGame/dCinema/Scene.cpp index 473c6dce..b74e0670 100644 --- a/dGame/dCinema/Scene.cpp +++ b/dGame/dCinema/Scene.cpp @@ -13,6 +13,8 @@ #include "SlashCommandHandler.h" #include "ChatPackets.h" #include "InventoryComponent.h" +#include "MovementAIComponent.h" +#include "Recorder.h" using namespace Cinema; @@ -185,6 +187,14 @@ void Cinema::Scene::AutoLoadScenesForZone(LWOMAPID zone) { .requiredLevel = eGameMasterLevel::LEAD_MODERATOR }); + SlashCommandHandler::RegisterCommand(Command{ + .help = "", + .info = "", + .aliases = { "companion" }, + .handle = CommandCompanion, + .requiredLevel = eGameMasterLevel::LEAD_MODERATOR + }); + const auto& scenesRoot = Game::config->GetValue("scenes_directory"); if (scenesRoot.empty()) { @@ -643,3 +653,47 @@ void Cinema::Scene::CommandSceneSetup(Entity* entity, const SystemAddress& sysAd scene.Rehearse(); } + +void Cinema::Scene::CommandCompanion(Entity* entity, const SystemAddress& sysAddr, const std::string args) { + const auto splitArgs = GeneralUtils::SplitString(args, ' '); + if (splitArgs.empty()) return; + + EntityInfo info; + info.lot = 0; + info.pos = entity->GetPosition(); + info.rot = entity->GetRotation(); + info.scale = 1; + info.spawner = nullptr; + info.spawnerID = entity->GetObjectID(); + info.spawnerNodeID = 0; + info.settings = { + new LDFData>(u"syncLDF", { u"custom_script_client" }), + new LDFData(u"custom_script_client", u"scripts\\ai\\SPEC\\MISSION_MINIGAME_CLIENT.lua") + }; + + // If there is an argument, set the lot + const auto lotOptional = GeneralUtils::TryParse(splitArgs[0]); + if (lotOptional) { + info.lot = lotOptional.value(); + } else { + ChatPackets::SendSystemMessage(sysAddr, u"Invalid lot."); + return; + } + + // Spawn it + auto* actor = Game::entityManager->CreateEntity(info); + + // If there is an argument, set the actors name + if (args.size() > 1) { + actor->SetVar(u"npcName", args[1]); + } + + // Construct it + Game::entityManager->ConstructEntity(actor); + + const auto follow = entity->GetObjectID(); + + actor->AddCallbackTimer(1.0f, [actor, follow]() { + Cinema::Recording::CompanionRecord::SetCompanion(actor, follow); + }); +} diff --git a/dGame/dCinema/Scene.h b/dGame/dCinema/Scene.h index b4223d0b..531b563a 100644 --- a/dGame/dCinema/Scene.h +++ b/dGame/dCinema/Scene.h @@ -133,6 +133,7 @@ private: static void CommandPrefabDestroy(Entity* entity, const SystemAddress& sysAddr, const std::string args); static void CommandSceneAct(Entity* entity, const SystemAddress& sysAddr, const std::string args); static void CommandSceneSetup(Entity* entity, const SystemAddress& sysAddr, const std::string args); + static void CommandCompanion(Entity* entity, const SystemAddress& sysAddr, const std::string args); }; } // namespace Cinema diff --git a/dGame/dComponents/BaseCombatAIComponent.cpp b/dGame/dComponents/BaseCombatAIComponent.cpp index bfb0bbfa..5b64596a 100644 --- a/dGame/dComponents/BaseCombatAIComponent.cpp +++ b/dGame/dComponents/BaseCombatAIComponent.cpp @@ -630,7 +630,23 @@ void BaseCombatAIComponent::ClearThreat() { m_DirtyThreat = true; } +void BaseCombatAIComponent::SetStartPosition(const NiPoint3& position) { + m_StartPosition = position; +} + void BaseCombatAIComponent::Wander() { + if (m_FocusPosition != NiPoint3Constant::ZERO) { + m_MovementAI->SetHaltDistance(m_FocusRadius); + + m_MovementAI->SetDestination(m_FocusPosition); + + m_MovementAI->SetMaxSpeed(m_TetherSpeed); + + m_Timer += 0.5f; + + return; + } + if (!m_MovementAI->AtFinalWaypoint()) { return; } @@ -781,6 +797,38 @@ void BaseCombatAIComponent::SetAggroRadius(const float value) { m_AggroRadius = value; } +float BaseCombatAIComponent::GetSoftTetherRadius() const { + return m_SoftTetherRadius; +} + +void BaseCombatAIComponent::SetSoftTetherRadius(const float value) { + m_SoftTetherRadius = value; +} + +void BaseCombatAIComponent::SetHardTetherRadius(const float value) { + m_HardTetherRadius = value; +} + +const NiPoint3& BaseCombatAIComponent::GetFocusPosition() const { + return m_FocusPosition; +} + +void BaseCombatAIComponent::SetFocusPosition(const NiPoint3& value) { + m_FocusPosition = value; +} + +float BaseCombatAIComponent::GetFocusRadius() const { + return m_FocusRadius; +} + +void BaseCombatAIComponent::SetFocusRadius(const float value) { + m_FocusRadius = value; +} + +float BaseCombatAIComponent::GetHardTetherRadius() const { + return m_HardTetherRadius; +} + void BaseCombatAIComponent::LookAt(const NiPoint3& point) { if (m_Stunned) { return; diff --git a/dGame/dComponents/BaseCombatAIComponent.h b/dGame/dComponents/BaseCombatAIComponent.h index 89985d64..70a8ab8b 100644 --- a/dGame/dComponents/BaseCombatAIComponent.h +++ b/dGame/dComponents/BaseCombatAIComponent.h @@ -120,6 +120,12 @@ public: */ const NiPoint3& GetStartPosition() const; + /** + * Sets the position where the entity spawned + * @param position the position where the entity spawned + */ + void SetStartPosition(const NiPoint3& position); + /** * Removes all threats for this entities, and thus chances for it attacking other entities */ @@ -196,6 +202,54 @@ public: */ void SetAggroRadius(float value); + /** + * Gets the soft tether radius + * @return the soft tether radius + */ + float GetSoftTetherRadius() const; + + /** + * Sets the soft tether radius + * @param value the soft tether radius + */ + void SetSoftTetherRadius(float value); + + /** + * Gets the hard tether radius + * @return the hard tether radius + */ + float GetHardTetherRadius() const; + + /** + * Sets the hard tether radius + * @param value the hard tether radius + */ + void SetHardTetherRadius(float value); + + /** + * Get the position that the entity should stay around + * @return the focus position + */ + const NiPoint3& GetFocusPosition() const; + + /** + * Set the position that the entity should stay around + * @param position the focus position + */ + void SetFocusPosition(const NiPoint3& position); + + /** + * Get the radius that the entity should stay around the focus position + * @return the focus radius + */ + float GetFocusRadius() const; + + /** + * Set the radius that the entity should stay around the focus position + * @param radius the focus radius + */ + void SetFocusRadius(float radius); + /** * Makes the entity look at a certain point in space * @param point the point to look at @@ -382,6 +436,16 @@ private: */ bool m_DirtyStateOrTarget = false; + /** + * A position that the entity should stay around + */ + NiPoint3 m_FocusPosition; + + /** + * How far the entity should stay from the focus position + */ + float m_FocusRadius = 0; + /** * Whether the current entity is a mech enemy, needed as mechs tether radius works differently * @return whether this entity is a mech diff --git a/dGame/dComponents/MovementAIComponent.cpp b/dGame/dComponents/MovementAIComponent.cpp index b6a16803..294a1537 100644 --- a/dGame/dComponents/MovementAIComponent.cpp +++ b/dGame/dComponents/MovementAIComponent.cpp @@ -182,6 +182,10 @@ const MovementAIInfo& MovementAIComponent::GetInfo() const { return m_Info; } +MovementAIInfo& MovementAIComponent::GetInfo() { + return m_Info; +} + bool MovementAIComponent::AdvanceWaypointIndex() { if (m_PathIndex >= m_InterpolatedWaypoints.size()) { return false; diff --git a/dGame/dComponents/MovementAIComponent.h b/dGame/dComponents/MovementAIComponent.h index 15b5aaed..6cfeedd9 100644 --- a/dGame/dComponents/MovementAIComponent.h +++ b/dGame/dComponents/MovementAIComponent.h @@ -74,6 +74,12 @@ public: */ const MovementAIInfo& GetInfo() const; + /** + * Returns a mutable reference to the basic settings that this entity uses to move around + * @return a mutable reference to the basic settings that this entity uses to move around + */ + MovementAIInfo& GetInfo(); + /** * Set a destination point for the entity to move towards * @param value the destination point to move towards