CombatAI and Vendor

This commit is contained in:
David Markowitz 2023-06-26 00:15:25 -07:00
parent ec9278286b
commit 34cfd45d40
7 changed files with 173 additions and 218 deletions

View File

@ -1,8 +1,9 @@
#include "eMissionTaskType.h"
#ifndef __ACHIEVEMENTCACHEKEY__H__
#define __ACHIEVEMENTCACHEKEY__H__
#include "eMissionTaskType.h"
#include "GeneralUtils.h"
class AchievementCacheKey {
public:
AchievementCacheKey() {

View File

@ -1,6 +1,10 @@
#include "BaseCombatAIComponent.h"
#include <BitStream.h>
#include <algorithm>
#include <sstream>
#include <vector>
#include "BitStream.h"
#include "Entity.h"
#include "EntityManager.h"
#include "ControllablePhysicsComponent.h"
@ -15,10 +19,6 @@
#include "CDClientManager.h"
#include "DestroyableComponent.h"
#include <algorithm>
#include <sstream>
#include <vector>
#include "SkillComponent.h"
#include "QuickBuildComponent.h"
#include "DestroyableComponent.h"
@ -26,21 +26,25 @@
#include "CDComponentsRegistryTable.h"
#include "CDPhysicsComponentTable.h"
BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id): Component(parent) {
BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t componentId) : Component(parent) {
m_Target = LWOOBJID_EMPTY;
SetAiState(AiState::spawn);
m_ComponentId = componentId;
SetAiState(AiState::Spawn);
m_Timer = 1.0f;
m_StartPosition = parent->GetPosition();
m_MovementAI = nullptr;
m_Disabled = false;
m_SkillEntries = {};
m_MovementAI = nullptr;
m_SoftTimer = 5.0f;
m_dpEntity = nullptr;
m_dpEntityEnemy = nullptr;
}
void BaseCombatAIComponent::LoadTemplateData() {
//Grab the aggro information from BaseCombatAI:
auto componentQuery = CDClientDatabase::CreatePreppedStmt(
"SELECT aggroRadius, tetherSpeed, pursuitSpeed, softTetherRadius, hardTetherRadius FROM BaseCombatAIComponent WHERE id = ?;");
componentQuery.bind(1, (int)id);
componentQuery.bind(1, m_ComponentId);
auto componentResult = componentQuery.execQuery();
@ -63,21 +67,12 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id):
componentResult.finalize();
// Get aggro and tether radius from settings and use this if it is present. Only overwrite the
// radii if it is greater than the one in the database.
if (m_ParentEntity) {
auto aggroRadius = m_ParentEntity->GetVar<float>(u"aggroRadius");
m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius;
auto tetherRadius = m_ParentEntity->GetVar<float>(u"tetherRadius");
m_HardTetherRadius = tetherRadius != 0 ? tetherRadius : m_HardTetherRadius;
}
/*
* Find skills
*/
auto skillQuery = CDClientDatabase::CreatePreppedStmt(
"SELECT skillID, cooldown, behaviorID FROM SkillBehavior WHERE skillID IN (SELECT skillID FROM ObjectSkills WHERE objectTemplate = ?);");
skillQuery.bind(1, (int)parent->GetLOT());
skillQuery.bind(1, m_ParentEntity->GetLOT());
auto result = skillQuery.execQuery();
@ -90,35 +85,42 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id):
auto* behavior = Behavior::CreateBehavior(behaviorId);
std::stringstream behaviorQuery;
AiSkillEntry entry = { skillId, 0, abilityCooldown, behavior };
m_SkillEntries.push_back(entry);
m_SkillEntries.push_back(AiSkillEntry(skillId, 0, abilityCooldown, behavior));
result.nextRow();
}
}
void BaseCombatAIComponent::LoadConfigData() {
// Get aggro and tether radius from settings and use this if it is present. Only overwrite the
// radii if it is greater than the one in the database.
if (m_ParentEntity) {
auto aggroRadius = m_ParentEntity->GetVar<float>(u"aggroRadius");
m_AggroRadius = aggroRadius != 0 ? aggroRadius : m_AggroRadius;
auto tetherRadius = m_ParentEntity->GetVar<float>(u"tetherRadius");
m_HardTetherRadius = tetherRadius != 0 ? tetherRadius : m_HardTetherRadius;
}
}
void BaseCombatAIComponent::Startup() {
Stun(1.0f);
/*
* Add physics
*/
// Add physics
int32_t collisionGroup = (COLLISION_GROUP_DYNAMIC | COLLISION_GROUP_ENEMY);
CDComponentsRegistryTable* componentRegistryTable = CDClientManager::Instance().GetTable<CDComponentsRegistryTable>();
auto componentID = componentRegistryTable->GetByIDAndType(parent->GetLOT(), eReplicaComponentType::CONTROLLABLE_PHYSICS);
auto* componentRegistryTable = CDClientManager::Instance().GetTable<CDComponentsRegistryTable>();
if (!componentRegistryTable) return;
CDPhysicsComponentTable* physicsComponentTable = CDClientManager::Instance().GetTable<CDPhysicsComponentTable>();
auto componentID = componentRegistryTable->GetByIDAndType(m_ParentEntity->GetLOT(), eReplicaComponentType::CONTROLLABLE_PHYSICS);
auto* physicsComponentTable = CDClientManager::Instance().GetTable<CDPhysicsComponentTable>();
if (!physicsComponentTable) return;
if (physicsComponentTable != nullptr) {
auto* info = physicsComponentTable->GetByID(componentID);
if (info != nullptr) {
collisionGroup = info->bStatic ? COLLISION_GROUP_NEUTRAL : info->collisionGroup;
}
}
if (info) collisionGroup = info->bStatic ? COLLISION_GROUP_NEUTRAL : info->collisionGroup;
// Why are these new'd here and then deleted by the dpworld??
//Create a phantom physics volume so we can detect when we're aggro'd.
m_dpEntity = new dpEntity(m_ParentEntity->GetObjectID(), m_AggroRadius);
m_dpEntityEnemy = new dpEntity(m_ParentEntity->GetObjectID(), m_AggroRadius, false);
@ -135,11 +137,12 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id):
}
BaseCombatAIComponent::~BaseCombatAIComponent() {
if (m_dpEntity)
dpWorld::Instance().RemoveEntity(m_dpEntity);
if (m_dpEntity) dpWorld::Instance().RemoveEntity(m_dpEntity);
if (m_dpEntityEnemy)
dpWorld::Instance().RemoveEntity(m_dpEntityEnemy);
if (m_dpEntityEnemy) dpWorld::Instance().RemoveEntity(m_dpEntityEnemy);
m_MovementAI = nullptr;
m_dpEntity = nullptr;
m_dpEntityEnemy = nullptr;
}
void BaseCombatAIComponent::Update(const float deltaTime) {
@ -174,14 +177,12 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
if (m_SoftTimer <= 0.0f) {
EntityManager::Instance()->SerializeEntity(m_ParentEntity);
m_SoftTimer = 5.0f;
} else {
m_SoftTimer -= deltaTime;
}
if (m_Disabled || m_ParentEntity->IsDead())
return;
if (m_Disabled || m_ParentEntity->IsDead()) return;
bool stunnedThisFrame = m_Stunned;
CalculateCombat(deltaTime); // Putting this here for now
@ -189,16 +190,13 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
m_StartPosition = m_ParentEntity->GetPosition();
}
if (m_MovementAI == nullptr) {
if (!m_MovementAI) {
m_MovementAI = m_ParentEntity->GetComponent<MovementAIComponent>();
if (m_MovementAI == nullptr) {
return;
}
if (!m_MovementAI) return;
}
if (stunnedThisFrame) {
m_MovementAI->Stop();
return;
}
@ -208,24 +206,25 @@ void BaseCombatAIComponent::Update(const float deltaTime) {
}
switch (m_State) {
case AiState::spawn:
case AiState::Spawn:
Stun(2.0f);
SetAiState(AiState::idle);
SetAiState(AiState::Idle);
break;
case AiState::idle:
case AiState::Idle:
Wander();
break;
case AiState::aggro:
case AiState::Aggro:
OnAggro();
break;
case AiState::tether:
case AiState::Tether:
OnTether();
break;
default:
Game::logger->Log("BaseCombatAIComponent", "Entity %i is in an invalid state %i", m_ParentEntity->GetLOT(), m_State);
break;
}
}
@ -256,9 +255,7 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
auto* skillComponent = m_ParentEntity->GetComponent<SkillComponent>();
if (skillComponent == nullptr) {
return;
}
if (!skillComponent) return;
skillComponent->CalculateUpdate(deltaTime);
@ -330,19 +327,19 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
SetTarget(newTarget);
if (m_Target != LWOOBJID_EMPTY) {
if (m_State == AiState::idle) {
if (m_State == AiState::Idle) {
m_Timer = 0;
}
SetAiState(AiState::aggro);
SetAiState(AiState::Aggro);
} else {
SetAiState(AiState::idle);
SetAiState(AiState::Idle);
}
if (!hasSkillToCast) return;
if (m_Target == LWOOBJID_EMPTY) {
SetAiState(AiState::idle);
SetAiState(AiState::Idle);
return;
}
@ -367,7 +364,7 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
m_MovementAI->Stop();
}
SetAiState(AiState::aggro);
SetAiState(AiState::Aggro);
m_Timer = 0;
@ -383,8 +380,6 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
}
LWOOBJID BaseCombatAIComponent::FindTarget() {
//const auto reference = m_MovementAI == nullptr ? m_StartPosition : m_MovementAI->ApproximateLocation();
NiPoint3 reference = m_StartPosition;
if (m_MovementAI) reference = m_MovementAI->ApproximateLocation();
@ -510,23 +505,10 @@ std::vector<LWOOBJID> BaseCombatAIComponent::GetTargetWithinAggroRange() const {
return targets;
}
bool BaseCombatAIComponent::IsMech() {
switch (m_ParentEntity->GetLOT()) {
case 6253:
return true;
default:
return false;
}
return false;
}
void BaseCombatAIComponent::Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags) {
outBitStream->Write(m_DirtyStateOrTarget || bIsInitialUpdate);
if (m_DirtyStateOrTarget || bIsInitialUpdate) {
outBitStream->Write(uint32_t(m_State));
outBitStream->Write(m_State);
outBitStream->Write(m_Target);
m_DirtyStateOrTarget = false;
}
@ -542,7 +524,7 @@ void BaseCombatAIComponent::SetAiState(AiState newState) {
bool BaseCombatAIComponent::IsEnemy(LWOOBJID target) const {
auto* entity = EntityManager::Instance()->GetEntity(target);
if (entity == nullptr) {
if (!entity) {
Game::logger->Log("BaseCombatAIComponent", "Invalid entity for checking validity (%llu)!", target);
return false;
@ -550,13 +532,11 @@ bool BaseCombatAIComponent::IsEnemy(LWOOBJID target) const {
auto* destroyable = entity->GetComponent<DestroyableComponent>();
if (destroyable == nullptr) {
return false;
}
if (!destroyable) return false;
auto* referenceDestroyable = m_ParentEntity->GetComponent<DestroyableComponent>();
if (referenceDestroyable == nullptr) {
if (!referenceDestroyable) {
Game::logger->Log("BaseCombatAIComponent", "Invalid reference destroyable component on (%llu)!", m_ParentEntity->GetObjectID());
return false;
@ -564,7 +544,7 @@ bool BaseCombatAIComponent::IsEnemy(LWOOBJID target) const {
auto* quickbuild = entity->GetComponent<QuickBuildComponent>();
if (quickbuild != nullptr) {
if (quickbuild) {
const auto state = quickbuild->GetState();
if (state != eRebuildState::COMPLETED) {
@ -576,7 +556,7 @@ bool BaseCombatAIComponent::IsEnemy(LWOOBJID target) const {
auto candidateList = destroyable->GetFactionIDs();
for (auto value : candidateList) {
for (const auto value : candidateList) {
if (std::find(enemyList.begin(), enemyList.end(), value) != enemyList.end()) {
return true;
}
@ -598,8 +578,7 @@ Entity* BaseCombatAIComponent::GetTargetEntity() const {
void BaseCombatAIComponent::Taunt(LWOOBJID offender, float threat) {
// Can't taunt self
if (offender == m_ParentEntity->GetObjectID())
return;
if (offender == m_ParentEntity->GetObjectID()) return;
m_ThreatEntries[offender] += threat;
m_DirtyThreat = true;
@ -634,9 +613,7 @@ void BaseCombatAIComponent::ClearThreat() {
}
void BaseCombatAIComponent::Wander() {
if (!m_MovementAI->AtFinalWaypoint()) {
return;
}
if (!m_MovementAI->AtFinalWaypoint()) return;
m_MovementAI->SetHaltDistance(0);
@ -679,9 +656,7 @@ void BaseCombatAIComponent::OnAggro() {
auto* target = GetTargetEntity();
if (target == nullptr) {
return;
}
if (!target) return;
m_MovementAI->SetHaltDistance(m_AttackRadius);
@ -704,7 +679,7 @@ void BaseCombatAIComponent::OnAggro() {
m_MovementAI->SetDestination(targetPos);
SetAiState(AiState::tether);
SetAiState(AiState::Tether);
}
m_Timer += 0.5f;
@ -730,7 +705,7 @@ void BaseCombatAIComponent::OnTether() {
m_MovementAI->SetDestination(m_StartPosition);
SetAiState(AiState::aggro);
SetAiState(AiState::Aggro);
} else {
if (IsMech() && Vector3::DistanceSquared(targetPos, currentPos) > m_AttackRadius * m_AttackRadius * 3 * 3) return;
@ -742,64 +717,20 @@ void BaseCombatAIComponent::OnTether() {
m_Timer += 0.5f;
}
bool BaseCombatAIComponent::GetStunned() const {
return m_Stunned;
}
void BaseCombatAIComponent::SetStunned(const bool value) {
m_Stunned = value;
}
bool BaseCombatAIComponent::GetStunImmune() const {
return m_StunImmune;
}
void BaseCombatAIComponent::SetStunImmune(bool value) {
m_StunImmune = value;
}
float BaseCombatAIComponent::GetTetherSpeed() const {
return m_TetherSpeed;
}
void BaseCombatAIComponent::SetTetherSpeed(float value) {
m_TetherSpeed = value;
}
void BaseCombatAIComponent::Stun(const float time) {
if (m_StunImmune || m_StunTime > time) {
return;
}
if (m_StunImmune || m_StunTime > time) return;
m_StunTime = time;
m_Stunned = true;
}
float BaseCombatAIComponent::GetAggroRadius() const {
return m_AggroRadius;
}
void BaseCombatAIComponent::SetAggroRadius(const float value) {
m_AggroRadius = value;
}
void BaseCombatAIComponent::LookAt(const NiPoint3& point) {
if (m_Stunned) {
return;
}
if (m_Stunned) return;
m_ParentEntity->SetRotation(NiQuaternion::LookAt(m_ParentEntity->GetPosition(), point));
}
void BaseCombatAIComponent::SetDisabled(bool value) {
m_Disabled = value;
}
bool BaseCombatAIComponent::GetDistabled() const {
return m_Disabled;
}
void BaseCombatAIComponent::Sleep() {
m_dpEntity->SetSleeping(true);
m_dpEntityEnemy->SetSleeping(true);

View File

@ -20,20 +20,25 @@ class Entity;
/**
* The current state of the AI
*/
enum class AiState : int {
idle = 0, // Doing nothing
aggro, // Waiting for an enemy to cross / running back to spawn
tether, // Chasing an enemy
spawn, // Spawning into the world
dead // Killed
enum class AiState : int32_t {
Idle = 0, // Doing nothing
Aggro, // Waiting for an enemy to cross / running back to spawn
Tether, // Chasing an enemy
Spawn, // Spawning into the world
Dead // Killed
};
/**
* Represents a skill that can be cast by this enemy, including its cooldowns, which determines how often the skill
* may be cast.
*/
struct AiSkillEntry
{
struct AiSkillEntry {
AiSkillEntry(uint32_t skillId, float cooldown, float abilityCooldown, Behavior* behavior) {
this->skillId = skillId;
this->cooldown = cooldown;
this->abilityCooldown = abilityCooldown;
this->behavior = behavior;
}
uint32_t skillId;
float cooldown;
@ -53,6 +58,9 @@ public:
BaseCombatAIComponent(Entity* parentEntity, uint32_t id);
~BaseCombatAIComponent() override;
void LoadTemplateData() override;
void LoadConfigData() override;
void Startup() override;
void Update(float deltaTime) override;
void Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags);
@ -147,37 +155,37 @@ public:
* Gets whether or not the entity is currently stunned
* @return whether the entity is currently stunned
*/
bool GetStunned() const;
bool GetStunned() const { return m_Stunned; }
/**
* (un)stuns the entity, determining whether it'll be able to attack other entities
* @param value whether the enemy is stunned
*/
void SetStunned(bool value);
void SetStunned(bool value) { m_Stunned = value; }
/**
* Gets if this entity may be stunned
* @return if this entity may be stunned
*/
bool GetStunImmune() const;
bool GetStunImmune() const { return m_StunImmune; }
/**
* Set the stun immune value, determining if the entity may be stunned
* @param value
*/
void SetStunImmune(bool value);
void SetStunImmune(bool value) { m_StunImmune = value; }
/**
* Gets the current speed at which an entity runs when tethering
* @return the current speed at which an entity runs when tethering
*/
float GetTetherSpeed() const;
float GetTetherSpeed() const { return m_TetherSpeed; }
/**
* Sets the speed at which an entity will tether
* @param value the new tether speed
*/
void SetTetherSpeed(float value);
void SetTetherSpeed(float value) { m_TetherSpeed = value; }
/**
* Stuns the entity for a certain amount of time, will not work if the entity is stun immune
@ -189,13 +197,13 @@ public:
* Gets the radius that will cause this entity to get aggro'd, causing a target chase
* @return the aggro radius of the entity
*/
float GetAggroRadius() const;
float GetAggroRadius() const { return m_AggroRadius; }
/**
* Sets the aggro radius, causing the entity to start chasing enemies in this range
* @param value the aggro radius to set
*/
void SetAggroRadius(float value);
void SetAggroRadius(float value) { m_AggroRadius = value; }
/**
* Makes the entity look at a certain point in space
@ -207,13 +215,13 @@ public:
* (dis)ables the AI, causing it to stop/start attacking enemies
* @param value
*/
void SetDisabled(bool value);
void SetDisabled(bool value) { m_Disabled = value; }
/**
* Gets the current state of the AI, whether or not it's looking for enemies to attack
* @return
*/
bool GetDistabled() const;
bool GetDistabled() const { return m_Disabled; }
/**
* Turns the entity asleep, stopping updates to its physics volumes
@ -387,7 +395,9 @@ private:
* Whether the current entity is a mech enemy, needed as mechs tether radius works differently
* @return whether this entity is a mech
*/
bool IsMech();
bool IsMech() const { return m_ParentEntity->GetLOT() == 6253; };
int32_t m_ComponentId;
};
#endif // BASECOMBATAICOMPONENT_H

View File

@ -128,7 +128,7 @@ LWOSpawnComponent
LWOSpringpadComponent
LWOSwitchComponent
LWOTriggerComponent
LocalPlayer (not a component)
LocalPlayer - This is a function call in the client which, if the generated Entity is a player, the below components are added. This **must** be done for all players.
├~~ LWOInteractionManagerComponent
├~~ LWOUserControlComponent
├~~ LWOFriendsListComponent

View File

@ -14,6 +14,9 @@
VendorComponent::VendorComponent(Entity* parent) : Component(parent) {
m_HasStandardCostItems = false;
m_HasMultiCostItems = false;
}
void VendorComponent::Startup() {
SetupConstants();
RefreshInventory(true);
}
@ -31,24 +34,23 @@ void VendorComponent::OnUse(Entity* originator) {
GameMessages::SendVendorStatusUpdate(m_ParentEntity, originator->GetSystemAddress());
}
void VendorComponent::RefreshInventory(bool isCreation) {
SetHasStandardCostItems(false);
SetHasMultiCostItems(false);
//Custom code for Max vanity NPC
// Custom code for Max vanity NPC
if (m_ParentEntity->GetLOT() == 9749 && Game::server->GetZoneID() == 1201) {
if (!isCreation) return;
SetHasStandardCostItems(true);
m_Inventory.insert({ 11909, 0 }); // Top hat w frog
m_Inventory.insert({ 7785, 0 }); // Flash bulb
m_Inventory.insert({ 12764, 0 }); // Big fountain soda
m_Inventory.insert({ 12241, 0 }); // Hot cocoa (from fb)
m_Inventory.push_back(SoldItem(11909, 0)); // Top hat w frog
m_Inventory.push_back(SoldItem(7785, 0)); // Flash bulb
m_Inventory.push_back(SoldItem(12764, 0)); // Big fountain soda
m_Inventory.push_back(SoldItem(12241, 0)); // Hot cocoa (from fb)
return;
}
m_Inventory.clear();
auto* lootMatrixTable = CDClientManager::Instance().GetTable<CDLootMatrixTable>();
std::vector<CDLootMatrix> lootMatrices = lootMatrixTable->Query([=](CDLootMatrix entry) { return (entry.LootMatrixIndex == m_LootMatrixID); });
const auto lootMatrices = lootMatrixTable->Query([=](CDLootMatrix entry) { return (entry.LootMatrixIndex == m_LootMatrixID); });
if (lootMatrices.empty()) return;
// Done with lootMatrix table
@ -59,16 +61,16 @@ void VendorComponent::RefreshInventory(bool isCreation) {
for (const auto& lootMatrix : lootMatrices) {
int lootTableID = lootMatrix.LootTableIndex;
std::vector<CDLootTable> vendorItems = lootTableTable->Query([=](CDLootTable entry) { return (entry.LootTableIndex == lootTableID); });
auto vendorItems = lootTableTable->Query([=](CDLootTable entry) { return (entry.LootTableIndex == lootTableID); });
if (lootMatrix.maxToDrop == 0 || lootMatrix.minToDrop == 0) {
for (CDLootTable item : vendorItems) {
if (!m_HasStandardCostItems || !m_HasMultiCostItems){
for (const auto& item : vendorItems) {
if (!m_HasStandardCostItems || !m_HasMultiCostItems) {
auto itemComponentID = compRegistryTable->GetByIDAndType(item.itemid, eReplicaComponentType::ITEM);
auto itemComponent = itemComponentTable->GetItemComponentByID(itemComponentID);
if (!m_HasStandardCostItems && itemComponent.baseValue != -1) SetHasStandardCostItems(true);
if (!m_HasMultiCostItems && !itemComponent.currencyCosts.empty()) SetHasMultiCostItems(true);
}
m_Inventory.insert({ item.itemid, item.sortPriority });
m_Inventory.push_back(SoldItem(item.itemid, item.sortPriority));
}
} else {
auto randomCount = GeneralUtils::GenerateRandomNumber<int32_t>(lootMatrix.minToDrop, lootMatrix.maxToDrop);
@ -76,43 +78,47 @@ void VendorComponent::RefreshInventory(bool isCreation) {
for (size_t i = 0; i < randomCount; i++) {
if (vendorItems.empty()) break;
auto randomItemIndex = GeneralUtils::GenerateRandomNumber<int32_t>(0, vendorItems.size() - 1);
const auto& randomItem = vendorItems[randomItemIndex];
const auto& randomItem = vendorItems.at(randomItemIndex);
vendorItems.erase(vendorItems.begin() + randomItemIndex);
if (!m_HasStandardCostItems || !m_HasMultiCostItems){
auto itemComponentID = compRegistryTable->GetByIDAndType(randomItem.itemid, eReplicaComponentType::ITEM);
if (!m_HasStandardCostItems || !m_HasMultiCostItems) {
auto itemComponentID = compRegistryTable->GetByIDAndType(randomItem.itemid, eReplicaComponentType::ITEM, -1);
if (itemComponentID == -1) {
Game::logger->Log("VendorComponent", "Attempted to add item %i with ItemComponent ID -1 to vendor %i inventory. Not adding item!", itemComponentID, m_ParentEntity->GetLOT());
continue;
}
auto itemComponent = itemComponentTable->GetItemComponentByID(itemComponentID);
if (!m_HasStandardCostItems && itemComponent.baseValue != -1) SetHasStandardCostItems(true);
if (!m_HasMultiCostItems && !itemComponent.currencyCosts.empty()) SetHasMultiCostItems(true);
}
m_Inventory.insert({ randomItem.itemid, randomItem.sortPriority });
m_Inventory.push_back(SoldItem(randomItem.itemid, randomItem.sortPriority));
}
}
}
//Because I (Max) want a vendor to sell these cameras
// Because I (Max) want a vendor to sell these cameras
if (m_ParentEntity->GetLOT() == 13569) {
auto randomCamera = GeneralUtils::GenerateRandomNumber<int32_t>(0, 2);
LOT camera = 0;
DluAssert(randomCamera >= 0 && randomCamera <= 2);
switch (randomCamera) {
case 0:
m_Inventory.insert({ 16253, 0 }); //Grungagroid
camera = 16253; // Grungagroid
break;
case 1:
m_Inventory.insert({ 16254, 0 }); //Hipstabrick
camera = 16254; // Hipstabrick
break;
case 2:
m_Inventory.insert({ 16204, 0 }); //Megabrixel snapshot
break;
default:
camera = 16204; // Megabrixel snapshot
break;
}
m_Inventory.push_back(SoldItem(camera, 0)); //Megabrixel snapshot
}
// Callback timer to refresh this inventory.
m_ParentEntity->AddCallbackTimer(m_RefreshTimeSeconds, [this]() {
RefreshInventory();
}
);
});
EntityManager::Instance()->SerializeEntity(m_ParentEntity);
GameMessages::SendVendorStatusUpdate(m_ParentEntity, UNASSIGNED_SYSTEM_ADDRESS);
}
@ -124,10 +130,11 @@ void VendorComponent::SetupConstants() {
auto* vendorComponentTable = CDClientManager::Instance().GetTable<CDVendorComponentTable>();
std::vector<CDVendorComponent> vendorComps = vendorComponentTable->Query([=](CDVendorComponent entry) { return (entry.id == componentID); });
if (vendorComps.empty()) return;
m_BuyScalar = vendorComps[0].buyScalar;
m_SellScalar = vendorComps[0].sellScalar;
m_RefreshTimeSeconds = vendorComps[0].refreshTimeSeconds;
m_LootMatrixID = vendorComps[0].LootMatrixIndex;
auto vendorData = vendorComps.at(0);
m_BuyScalar = vendorData.buyScalar;
m_SellScalar = vendorData.sellScalar;
m_RefreshTimeSeconds = vendorData.refreshTimeSeconds;
m_LootMatrixID = vendorData.LootMatrixIndex;
}

View File

@ -2,6 +2,7 @@
#ifndef VENDORCOMPONENT_H
#define VENDORCOMPONENT_H
#include <functional>
#include "CDClientManager.h"
#include "Component.h"
#include "Entity.h"
@ -9,6 +10,15 @@
#include "RakNetTypes.h"
#include "eReplicaComponentType.h"
struct SoldItem {
SoldItem(const LOT lot, const int32_t sortPriority) {
this->lot = lot;
this->sortPriority = sortPriority;
};
LOT lot = 0;
int32_t sortPriority = 0;
};
/**
* A component for vendor NPCs. A vendor sells items to the player.
*/
@ -17,37 +27,31 @@ public:
inline static const eReplicaComponentType ComponentType = eReplicaComponentType::VENDOR;
VendorComponent(Entity* parent);
void Startup() override;
void Serialize(RakNet::BitStream* outBitStream, bool bIsInitialUpdate, unsigned int& flags);
void OnUse(Entity* originator) override;
float GetBuyScalar() const {
return m_BuyScalar;
}
float GetBuyScalar() const { return m_BuyScalar; }
float GetSellScalar() const {
return m_SellScalar;
}
float GetSellScalar() const { return m_SellScalar; }
void SetBuyScalar(float value) {
m_BuyScalar = value;
}
void SetBuyScalar(const float value) { m_BuyScalar = value; }
void SetSellScalar(float value) {
m_SellScalar = value;
}
void SetSellScalar(const float value) { m_SellScalar = value; }
std::map<LOT, int>& GetInventory() {
std::vector<SoldItem>& GetInventory() {
return m_Inventory;
}
void SetHasMultiCostItems(bool hasMultiCostItems) {
void SetHasMultiCostItems(const bool hasMultiCostItems) {
if (m_HasMultiCostItems == hasMultiCostItems) return;
m_HasMultiCostItems = hasMultiCostItems;
m_DirtyVendor = true;
}
void SetHasStandardCostItems(bool hasStandardCostItems) {
void SetHasStandardCostItems(const bool hasStandardCostItems) {
if (m_HasStandardCostItems == hasStandardCostItems) return;
m_HasStandardCostItems = hasStandardCostItems;
m_DirtyVendor = true;
@ -64,37 +68,39 @@ public:
void SetupConstants();
bool SellsItem(const LOT item) const {
return m_Inventory.find(item) != m_Inventory.end();
return std::count_if(m_Inventory.begin(), m_Inventory.end(), [item](const SoldItem& lhs) {
return lhs.lot == item;
}) > 0;
}
private:
/**
* The buy scalar.
*/
float m_BuyScalar;
float m_BuyScalar = 0.0f;
/**
* The sell scalar.
*/
float m_SellScalar;
float m_SellScalar = 0.0f;
/**
* The refresh time of this vendors' inventory.
*/
float m_RefreshTimeSeconds;
float m_RefreshTimeSeconds = 0.0f;
/**
* Loot matrix id of this vendor.
*/
uint32_t m_LootMatrixID;
uint32_t m_LootMatrixID = 0;
/**
* The list of items the vendor sells.
*/
std::map<LOT, int> m_Inventory;
std::vector<SoldItem> m_Inventory;
bool m_DirtyVendor;
bool m_HasStandardCostItems;
bool m_HasMultiCostItems;
bool m_DirtyVendor = false;
bool m_HasStandardCostItems = false;
bool m_HasMultiCostItems = false;
};
#endif // VENDORCOMPONENT_H

View File

@ -1286,7 +1286,7 @@ void GameMessages::SendVendorStatusUpdate(Entity* entity, const SystemAddress& s
auto* vendor = entity->GetComponent<VendorComponent>();
if (!vendor) return;
std::map<LOT, int> vendorItems = vendor->GetInventory();
auto vendorItems = vendor->GetInventory();
bitStream.Write(entity->GetObjectID());
bitStream.Write(eGameMessageType::VENDOR_STATUS_UPDATE);
@ -1294,9 +1294,9 @@ void GameMessages::SendVendorStatusUpdate(Entity* entity, const SystemAddress& s
bitStream.Write(bUpdateOnly);
bitStream.Write(static_cast<uint32_t>(vendorItems.size()));
for (std::pair<LOT, int> item : vendorItems) {
bitStream.Write(static_cast<int>(item.first));
bitStream.Write(static_cast<int>(item.second));
for (const auto&[lot, sortPriority] : vendorItems) {
bitStream.Write<int32_t>(lot);
bitStream.Write<int32_t>(sortPriority);
}
if (sysAddr == UNASSIGNED_SYSTEM_ADDRESS) SEND_PACKET_BROADCAST