Compare commits

...

14 Commits

Author SHA1 Message Date
wincent
328743f1e1 Local work 2024-09-08 17:40:32 +02:00
wincent
3178a702a7 Updated how skills and upgrades work, more gui stuff 2024-07-17 20:59:11 +02:00
wincent
5e3312850c Refactor damage calculations and add additional modifiers 2024-07-06 00:02:30 +02:00
wincent
756dc4e44f Updated how upgrade adds skills 2024-06-08 17:31:22 +02:00
wincent
87613f287f Fix g++ 14 2024-06-06 09:37:49 +02:00
wincent
ac1b00fdaa Updates to upgrades 2024-06-06 09:37:24 +02:00
wincent
364bcf822a Added upgrades 2024-06-02 15:43:35 +02:00
wincent
d5b2278dc5 Refactor and combat changes 2024-06-02 09:53:56 +02:00
wincent
0bf5ee02e4 Merge remote-tracking branch 'origin/main' into nejlika 2024-05-30 10:56:58 +02:00
wincent
c2ad76ee66 Moved to a dedicated nejlika fle 2024-05-30 10:54:38 +02:00
wincent
c6528d444a Merge branch 'observable' into nejlika 2024-05-25 19:26:49 +02:00
wincent
78425aacb1 Proposal for observers and deferred implementations 2024-05-25 19:13:22 +02:00
wincent
b8d610987f Correct try-parse 2024-05-24 15:10:38 +02:00
wincent
7845131649 Test for reactive item descriptions 2024-05-24 15:06:40 +02:00
68 changed files with 30420 additions and 74 deletions

92
dCommon/Implementation.h Normal file
View File

@@ -0,0 +1,92 @@
#ifndef __IMPLEMENTATION_H__
#define __IMPLEMENTATION_H__
#include <functional>
#include <optional>
/**
* @brief A way to defer the implementation of an action.
*
* @tparam R The result of the action.
* @tparam T The types of the arguments that the implementation requires.
*/
template <typename R, typename... T>
class Implementation {
public:
typedef std::function<std::optional<R>(T...)> ImplementationFunction;
/**
* @brief Sets the implementation of the action.
*
* @param implementation The implementation of the action.
*/
void SetImplementation(const ImplementationFunction& implementation) {
this->implementation = implementation;
}
/**
* @brief Clears the implementation of the action.
*/
void ClearImplementation() {
implementation.reset();
}
/**
* @brief Checks if the implementation is set.
*
* @return true If the implementation is set.
* @return false If the implementation is not set.
*/
bool IsSet() const {
return implementation.has_value();
}
/**
* @brief Executes the implementation if it is set.
*
* @param args The arguments to pass to the implementation.
* @return std::optional<R> The optional result of the implementation. If the result is not set, it indicates that the default action should be taken.
*/
std::optional<R> Execute(T... args) const {
return IsSet() ? implementation.value()(args...) : std::nullopt;
}
/**
* @brief Exectues the implementation if it is set, otherwise returns a default value.
*
* @param args The arguments to pass to the implementation.
* @param defaultValue The default value to return if the implementation is not set or should not be deferred.
*/
R ExecuteWithDefault(T... args, const R& defaultValue) const {
return Execute(args...).value_or(defaultValue);
}
/**
* = operator overload.
*/
Implementation& operator=(const Implementation& other) {
implementation = other.implementation;
return *this;
}
/**
* = operator overload.
*/
Implementation& operator=(const ImplementationFunction& implementation) {
this->implementation = implementation;
return *this;
}
/**
* () operator overload.
*/
std::optional<R> operator()(T... args) {
return !IsSet() ? std::nullopt : implementation(args...);
}
private:
std::optional<ImplementationFunction> implementation;
};
#endif //!__IMPLEMENTATION_H__

73
dCommon/Observable.h Normal file
View File

@@ -0,0 +1,73 @@
#ifndef __OBSERVABLE_H__
#define __OBSERVABLE_H__
#include <vector>
#include <functional>
/**
* @brief An event which can be observed by multiple observers.
*
* @tparam T The types of the arguments to be passed to the observers.
*/
template <typename... T>
class Observable {
public:
typedef std::function<void(T...)> Observer;
/**
* @brief Adds an observer to the event.
*
* @param observer The observer to add.
*/
void AddObserver(const Observer& observer) {
observers.push_back(observer);
}
/**
* @brief Removes an observer from the event.
*
* @param observer The observer to remove.
*/
void RemoveObserver(const Observer& observer) {
observers.erase(std::remove(observers.begin(), observers.end(), observer), observers.end());
}
/**
* @brief Notifies all observers of the event.
*
* @param args The arguments to pass to the observers.
*/
void Notify(T... args) {
for (const auto& observer : observers) {
observer(args...);
}
}
/**
* += operator overload.
*/
Observable& operator+=(const Observer& observer) {
AddObserver(observer);
return *this;
}
/**
* -= operator overload.
*/
Observable& operator-=(const Observer& observer) {
RemoveObserver(observer);
return *this;
}
/**
* () operator overload.
*/
void operator()(T... args) {
Notify(args...);
}
private:
std::vector<Observer> observers;
};
#endif //!__OBSERVABLE_H__

View File

@@ -1,6 +1,7 @@
#include "dConfig.h"
#include <sstream>
#include <algorithm>
#include "BinaryPathFinder.h"
#include "GeneralUtils.h"

View File

@@ -0,0 +1,677 @@
#include "AdditionalEntityData.h"
#include "NejlikaData.h"
#include <DestroyableComponent.h>
#include <LevelProgressionComponent.h>
#include <InventoryComponent.h>
#include <BaseCombatAIComponent.h>
#include <TeamManager.h>
#include <ControllablePhysicsComponent.h>
#include <Item.h>
#include <queue>
using namespace nejlika;
float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, ModifierOperator op, bool resistance) const
{
float total = 0;
for (const auto& modifier : activeModifiers) {
if (modifier.GetConvertTo() != ModifierType::Invalid) {
continue;
}
if (modifier.GetType() != type || modifier.GetOperator() != op || modifier.IsResistance() != resistance) {
continue;
}
total += modifier.GetValue();
}
return total;
}
float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, const std::vector<ModifierInstance>& additionalModifiers, ModifierOperator op, bool resistance) const {
float total = 0;
for (const auto& modifier : additionalModifiers) {
if (modifier.GetConvertTo() != ModifierType::Invalid) {
continue;
}
if (modifier.GetType() != type || modifier.GetOperator() != op || modifier.IsResistance() != resistance) {
continue;
}
total += modifier.GetValue();
}
return total + CalculateModifier(type, op, resistance);
}
float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, int32_t level) const
{
const auto templateDataOpt = NejlikaData::GetEntityTemplate(lot);
if (!templateDataOpt.has_value()) {
return 0;
}
const auto& templateData = *templateDataOpt.value();
const auto scaler = templateData.GetScaler(type, false, level);
float additive = CalculateModifier(type, ModifierOperator::Additive, false);
if (scaler != 0 && additive >= scaler) {
additive -= scaler;
}
float multiplicative = CalculateModifier(type, ModifierOperator::Multiplicative, false);
return (scaler + additive) * (1 + multiplicative / 100);
}
float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type) const {
return CalculateModifier(type, level);
}
float nejlika::AdditionalEntityData::CalculateFinalModifier(ModifierType type, const std::vector<ModifierInstance>& additionalModifiers, int32_t level) const
{
const auto templateDataOpt = NejlikaData::GetEntityTemplate(lot);
if (!templateDataOpt.has_value()) {
return 0;
}
const auto& templateData = *templateDataOpt.value();
const auto scaler = templateData.GetScaler(type, false, level);
float additive = CalculateModifier(type, additionalModifiers, ModifierOperator::Additive, false);
if (scaler != 0 && additive >= scaler) {
additive -= scaler;
}
float multiplicative = CalculateModifier(type, additionalModifiers, ModifierOperator::Multiplicative, false);
static const std::unordered_set<ModifierType> elementalDamage = {
ModifierType::Fire,
ModifierType::Cold,
ModifierType::Lightning
};
if (type == ModifierType::Health) {
if (lot == 1) additive += 25;
additive += CalculateModifier(ModifierType::Physique, additionalModifiers, ModifierOperator::Additive, false) * 2.5f;
additive += CalculateModifier(ModifierType::Cunning, additionalModifiers, ModifierOperator::Additive, false) * 1.0f;
additive += CalculateModifier(ModifierType::Spirit, additionalModifiers, ModifierOperator::Additive, false) * 1.0f;
}
else if (type == ModifierType::Imagination) {
additive += CalculateModifier(ModifierType::Spirit, additionalModifiers, ModifierOperator::Additive, false) * 2.0f;
}
else if (type == ModifierType::Seperation || type == ModifierType::InternalDisassembly || type == ModifierType::Physical) {
multiplicative += CalculateModifier(ModifierType::Cunning, additionalModifiers, ModifierOperator::Additive, false) * 0.33f;
}
else if (type == ModifierType::Pierce) {
multiplicative += CalculateModifier(ModifierType::Cunning, additionalModifiers, ModifierOperator::Additive, false) * 0.285f;
}
else if (nejlika::IsOverTimeType(type) || nejlika::IsNormalDamageType(type)) {
multiplicative += CalculateModifier(ModifierType::Spirit, additionalModifiers, ModifierOperator::Additive, false) * 0.33f;
}
else if (type == ModifierType::ImaginationRegen) {
const auto spirit = CalculateModifier(ModifierType::Spirit, additionalModifiers, ModifierOperator::Additive, false);
additive += spirit * 0.01f + 1;
multiplicative += spirit * 0.25f;
}
else if (type == ModifierType::HealthRegen) {
const auto physique = CalculateModifier(ModifierType::Physique, additionalModifiers, ModifierOperator::Additive, false);
additive += physique * 0.04f;
}
else if (type == ModifierType::Defensive) {
additive += CalculateModifier(ModifierType::Physique, additionalModifiers, ModifierOperator::Additive, false) * 0.5f;
if (lot == 1) additive += level * 10;
}
else if (type == ModifierType::Offensive) {
additive += CalculateModifier(ModifierType::Cunning, additionalModifiers, ModifierOperator::Additive, false) * 0.5f;
if (lot == 1) additive += level * 10;
}
if (elementalDamage.contains(type)) {
additive += CalculateModifier(ModifierType::Elemental, additionalModifiers, ModifierOperator::Additive, false) / elementalDamage.size();
multiplicative += CalculateModifier(ModifierType::Elemental, additionalModifiers, ModifierOperator::Multiplicative, false) / elementalDamage.size();
}
if (nejlika::IsNormalDamageType(type) || nejlika::IsOverTimeType(type)) {
additive += CalculateModifier(ModifierType::Damage, additionalModifiers, ModifierOperator::Additive, false);
multiplicative += CalculateModifier(ModifierType::Damage, additionalModifiers, ModifierOperator::Multiplicative, false);
}
float total = (scaler + additive) * (1 + multiplicative / 100);
return total;
}
float nejlika::AdditionalEntityData::CalculateResistance(ModifierType type) const
{
type = nejlika::GetResistanceType(type);
return CalculateModifier(type, ModifierOperator::Multiplicative, true);
}
float nejlika::AdditionalEntityData::CalculateMultiplier(ModifierType type) const
{
return 100 + CalculateModifier(type, ModifierOperator::Multiplicative, false);
}
std::vector<ModifierInstance> nejlika::AdditionalEntityData::TriggerUpgradeItems(UpgradeTriggerType triggerType, const TriggerParameters& params) {
auto* entity = Game::entityManager->GetEntity(id);
if (entity == nullptr) {
return {};
}
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent == nullptr) {
return {};
}
std::vector<ModifierInstance> result;
for (const auto& itemID : upgradeItems) {
auto* item = inventoryComponent->FindItemById(itemID);
if (item == nullptr) {
continue;
}
const auto upgradeDataOpt = NejlikaData::GetUpgradeTemplate(item->GetLot());
if (!upgradeDataOpt.has_value()) {
continue;
}
const auto& upgradeData = *upgradeDataOpt.value();
const auto modifiers = upgradeData.Trigger(item->GetCount(), triggerType, id, params);
result.insert(result.end(), modifiers.begin(), modifiers.end());
}
return result;
}
std::vector<ModifierInstance> nejlika::AdditionalEntityData::TriggerUpgradeItems(UpgradeTriggerType triggerType) {
return TriggerUpgradeItems(triggerType, {});
}
void nejlika::AdditionalEntityData::InitializeSkills() {
auto* entity = Game::entityManager->GetEntity(id);
if (entity == nullptr) {
return;
}
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent == nullptr) {
return;
}
struct entry {
LWOOBJID id;
int32_t priority;
entry(LWOOBJID id, int32_t priority) : id(id), priority(priority) {}
};
std::vector<entry> items;
for (const auto& itemID : upgradeItems) {
auto* item = inventoryComponent->FindItemById(itemID);
if (item == nullptr) {
continue;
}
const auto priority = item->GetSlot();
items.push_back(entry(itemID, priority));
}
std::sort(items.begin(), items.end(), [](const entry& a, const entry& b) {
return a.priority < b.priority;
});
for (const auto& item : items) {
AddSkills(item.id);
}
}
void nejlika::AdditionalEntityData::AddSkills(LWOOBJID item) {
if (!upgradeItems.contains(item)) {
return;
}
auto* entity = Game::entityManager->GetEntity(id);
if (entity == nullptr) {
return;
}
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent == nullptr) {
return;
}
auto* itemData = inventoryComponent->FindItemById(item);
if (itemData == nullptr) {
return;
}
const auto upgradeDataOpt = NejlikaData::GetUpgradeTemplate(itemData->GetLot());
if (!upgradeDataOpt.has_value()) {
return;
}
const auto& upgradeData = *upgradeDataOpt.value();
LOG("Adding skills for item %i", id);
upgradeData.AddSkills(id);
}
void nejlika::AdditionalEntityData::RemoveSkills(LOT lot) {
const auto upgradeDataOpt = NejlikaData::GetUpgradeTemplate(lot);
if (!upgradeDataOpt.has_value()) {
return;
}
const auto& upgradeData = *upgradeDataOpt.value();
LOG("Removing skills for item %i", id);
upgradeData.RemoveSkills(id);
}
std::vector<ModifierInstance> nejlika::AdditionalEntityData::CalculateMainWeaponDamage() {
auto* entity = Game::entityManager->GetEntity(id);
if (entity == nullptr) {
return {};
}
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent == nullptr) {
return {};
}
Item* mainWeapon = nullptr;
for (const auto& [location, item] : inventoryComponent->GetEquippedItems()) {
if (location == "special_r") {
mainWeapon = inventoryComponent->FindItemById(item.id);
break;
}
}
if (mainWeapon == nullptr) {
return {};
}
const auto additionalItemDataOpt = NejlikaData::GetAdditionalItemData(mainWeapon->GetId());
if (!additionalItemDataOpt.has_value()) {
return {};
}
const auto& additionalItemData = *additionalItemDataOpt.value();
std::vector<ModifierInstance> result;
for (const auto& modifier : additionalItemData.GetModifierInstances()) {
if (nejlika::IsNormalDamageType(modifier.GetType()) || nejlika::IsOverTimeType(modifier.GetType()) || nejlika::IsDurationType(modifier.GetType())) {
if (modifier.GetOperator() == ModifierOperator::Additive && modifier.GetUpgradeName().empty()) {
result.push_back(modifier);
}
}
}
return result;
}
void nejlika::AdditionalEntityData::RollStandardModifiers(int32_t level) {
standardModifiers.clear();
const auto templateDataOpt = NejlikaData::GetEntityTemplate(lot);
if (templateDataOpt.has_value()) {
const auto& templateData = *templateDataOpt.value();
const auto modifiers = templateData.GenerateModifiers(level);
standardModifiers.insert(standardModifiers.end(), modifiers.begin(), modifiers.end());
}
const auto objectDataVec = NejlikaData::GetModifierNameTemplates(ModifierNameType::Object);
for (const auto& objectData : objectDataVec) {
if (objectData.GetLOT() != lot) {
continue;
}
const auto modifiers = objectData.GenerateModifiers(level);
standardModifiers.insert(standardModifiers.end(), modifiers.begin(), modifiers.end());
}
}
void nejlika::AdditionalEntityData::TriggerPassiveRegeneration() {
auto* entity = Game::entityManager->GetEntity(id);
if (entity == nullptr) {
return;
}
auto* destroyable = entity->GetComponent<DestroyableComponent>();
if (destroyable == nullptr) {
return;
}
const auto healthRegen = CalculateFinalModifier(ModifierType::HealthRegen, {}, level);
const auto imaginationRegen = CalculateFinalModifier(ModifierType::ImaginationRegen, {}, level);
if (healthRegen > 0) {
destroyable->SetHealth(std::min(destroyable->GetHealth() + static_cast<int32_t>(healthRegen), static_cast<int32_t>(destroyable->GetMaxHealth())));
}
if (imaginationRegen > 0) {
destroyable->SetImagination(std::min(destroyable->GetImagination() + static_cast<int32_t>(imaginationRegen), static_cast<int32_t>(destroyable->GetMaxImagination())));
}
Game::entityManager->SerializeEntity(entity);
// Trigger it again in 1 second
entity->AddCallbackTimer(1.0f, [this]() {
TriggerPassiveRegeneration();
});
}
float nejlika::AdditionalEntityData::CalculateMultiplier(ModifierType type, const std::vector<ModifierInstance>& additionalModifiers) const {
return 100 + CalculateModifier(type, additionalModifiers, ModifierOperator::Multiplicative, false);
}
std::unordered_map<ModifierType, std::unordered_map<ModifierType, float>> nejlika::AdditionalEntityData::CalculateDamageConversion(std::vector<ModifierInstance>& additionalModifiers) const {
std::unordered_map<ModifierType, std::unordered_map<ModifierType, float>> conversion;
for (const auto& modifier : activeModifiers) {
if (modifier.GetConvertTo() == ModifierType::Invalid) {
continue;
}
conversion[modifier.GetType()][modifier.GetConvertTo()] += modifier.GetValue();
}
for (const auto& modifier : additionalModifiers) {
if (modifier.GetConvertTo() == ModifierType::Invalid) {
continue;
}
conversion[modifier.GetType()][modifier.GetConvertTo()] += modifier.GetValue();
}
// Third pass: adjust bidirectional conversions
auto copy = conversion; // Create a copy to iterate over
for (const auto& [type, convertMap] : copy) {
for (const auto& [convertTo, value] : convertMap) {
if (conversion[convertTo][type] > 0) {
if (value > conversion[convertTo][type]) {
conversion[type][convertTo] -= conversion[convertTo][type];
conversion[convertTo][type] = 0; // Ensure no negative values
} else {
conversion[convertTo][type] -= value;
conversion[type][convertTo] = 0; // Ensure no negative values
}
}
}
}
// Fourth pass: if a type converts to multiple types, and the sum of the conversion values is greater than 100, normalize the values
for (const auto& [type, convertMap] : conversion) {
float sum = 0;
for (const auto& [convertTo, value] : convertMap) {
sum += value;
}
if (sum > 100) {
for (const auto& [convertTo, value] : convertMap) {
conversion[type][convertTo] = value / sum * 100;
}
}
}
return conversion;
}
void nejlika::AdditionalEntityData::ApplyToEntity() {
const auto templateDataOpt = NejlikaData::GetEntityTemplate(lot);
if (!templateDataOpt.has_value()) {
return;
}
const auto& templateData = *templateDataOpt.value();
auto* entity = Game::entityManager->GetEntity(id);
if (entity == nullptr) {
return;
}
auto* destroyable = entity->GetComponent<DestroyableComponent>();
if (destroyable == nullptr) {
return;
}
auto* levelProgression = entity->GetComponent<LevelProgressionComponent>();
if (levelProgression != nullptr) {
this->level = levelProgression->GetLevel();
}
else {
this->level = templateData.GetMinLevel();
}
if (!initialized) {
RollStandardModifiers(level);
}
activeModifiers = standardModifiers;
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent) {
for (const auto& [location, item] : inventoryComponent->GetEquippedItems()) {
const auto itemDataOpt = NejlikaData::GetAdditionalItemData(item.id);
if (!itemDataOpt.has_value()) {
continue;
}
const auto& itemData = *itemDataOpt.value();
const auto& itemModifiers = itemData.GetModifierInstances();
for (const auto& modifier : itemModifiers) {
if (nejlika::IsNormalDamageType(modifier.GetType()) || nejlika::IsOverTimeType(modifier.GetType()) || nejlika::IsDurationType(modifier.GetType())) {
if (modifier.GetOperator() == ModifierOperator::Additive && modifier.GetUpgradeName().empty()) {
continue;
}
}
activeModifiers.push_back(modifier);
}
}
for (const auto& upgradeItem : upgradeItems) {
auto* item = inventoryComponent->FindItemById(upgradeItem);
if (item == nullptr) {
continue;
}
LOG("Applying upgrade item %i", item->GetLot());
const auto itemDataOpt = NejlikaData::GetUpgradeTemplate(item->GetLot());
if (!itemDataOpt.has_value()) {
LOG("Upgrade item %i has no data", item->GetLot());
continue;
}
const auto& itemData = *itemDataOpt.value();
const auto& itemModifiers = itemData.GenerateModifiers(item->GetCount());
LOG("Upgrade item %i has %i modifiers with level %i", item->GetLot(), itemModifiers.size(), item->GetCount());
activeModifiers.insert(activeModifiers.end(), itemModifiers.begin(), itemModifiers.end());
}
}
destroyable->SetMaxHealth(static_cast<int32_t>(CalculateFinalModifier(ModifierType::Health, {}, level)));
destroyable->SetMaxArmor(static_cast<int32_t>(CalculateFinalModifier(ModifierType::Armor, {}, level)));
//if (!entity->IsPlayer()) {
destroyable->SetMaxImagination(static_cast<int32_t>(CalculateFinalModifier(ModifierType::Imagination, {}, level)));
//}
if (initialized) {
return;
}
destroyable->SetHealth(destroyable->GetMaxHealth());
destroyable->SetArmor(destroyable->GetMaxArmor());
//if (!entity->IsPlayer()) {
destroyable->SetImagination(destroyable->GetMaxImagination());
//}
if (entity->IsPlayer()) {
auto* controllablePhysicsComponent = entity->GetComponent<ControllablePhysicsComponent>();
if (controllablePhysicsComponent) controllablePhysicsComponent->SetSpeedMultiplier(CalculateMultiplier(ModifierType::Speed) / 100.0f);
}
TriggerPassiveRegeneration();
initialized = true;
}
void nejlika::AdditionalEntityData::CheckForRescale(AdditionalEntityData* other) {
auto* entity = Game::entityManager->GetEntity(id);
if (entity == nullptr) {
return;
}
if (entity->IsPlayer()) {
return;
}
auto* baseCombat = entity->GetComponent<BaseCombatAIComponent>();
if (baseCombat == nullptr) {
return;
}
const auto& threats = baseCombat->GetThreats();
int32_t totalThreats = 0;
int32_t totalLevel = 0;
for (const auto& [threat, _] : threats) {
const auto threatEntityOpt = NejlikaData::GetAdditionalEntityData(threat);
if (!threatEntityOpt.has_value()) {
continue;
}
const auto& threatEntity = *threatEntityOpt.value();
if (other->id == threatEntity.id) {
continue;
}
totalLevel += threatEntity.level;
totalThreats++;
}
if (other != nullptr) {
totalLevel += other->level;
totalThreats++;
auto* team = TeamManager::Instance()->GetTeam(other->id);
if (team != nullptr) {
for (const auto& member : team->members) {
const auto memberEntityOpt = NejlikaData::GetAdditionalEntityData(member);
if (!memberEntityOpt.has_value()) {
continue;
}
const auto& memberEntity = *memberEntityOpt.value();
if (other->id == memberEntity.id) {
continue;
}
totalLevel += memberEntity.level;
totalThreats++;
}
}
}
if (totalThreats == 0) {
return;
}
const auto averageLevel = totalLevel / totalThreats;
// Can't rescale to a lower level
if (averageLevel <= level) {
return;
}
level = averageLevel;
auto* destroyable = entity->GetComponent<DestroyableComponent>();
if (destroyable == nullptr) {
return;
}
float healthPercentage = destroyable->GetMaxHealth() == 0 ? 1 : static_cast<float>(destroyable->GetHealth()) / destroyable->GetMaxHealth();
float armorPercentage = destroyable->GetMaxArmor() == 0 ? 1 : static_cast<float>(destroyable->GetArmor()) / destroyable->GetMaxArmor();
float imaginationPercentage = destroyable->GetMaxImagination() == 0 ? 1 : static_cast<float>(destroyable->GetImagination()) / destroyable->GetMaxImagination();
RollStandardModifiers(level);
ApplyToEntity();
destroyable->SetHealth(static_cast<int32_t>(destroyable->GetMaxHealth() * healthPercentage));
destroyable->SetArmor(static_cast<int32_t>(destroyable->GetMaxArmor() * armorPercentage));
destroyable->SetImagination(static_cast<int32_t>(destroyable->GetMaxImagination() * imaginationPercentage));
LOG("Rescaled entity %i to level %d", entity->GetLOT(), level);
}

View File

@@ -0,0 +1,109 @@
#pragma once
#include <cstdint>
#include <vector>
#include "Entity.h"
#include "ModifierInstance.h"
#include "EntityTemplate.h"
#include "UpgradeTriggerType.h"
#include "TriggerParameters.h"
#include <unordered_set>
namespace nejlika
{
class AdditionalEntityData
{
public:
AdditionalEntityData() = default;
AdditionalEntityData(LWOOBJID id, LOT lot) : id(id), lot(lot) {}
float CalculateModifier(ModifierType type, ModifierOperator op, bool resistance) const;
float CalculateModifier(ModifierType type, const std::vector<ModifierInstance>& additionalModifiers, ModifierOperator op, bool resistance) const;
float CalculateModifier(ModifierType type, int32_t level) const;
float CalculateModifier(ModifierType type) const;
float CalculateFinalModifier(ModifierType type, const std::vector<ModifierInstance>& additionalModifiers, int32_t level) const;
float CalculateResistance(ModifierType type) const;
/**
* @brief Calculate the multiplier for a given modifier type. With a base value of 100 (%).
*
* @param type The modifier type.
* @return The multiplier.
*/
float CalculateMultiplier(ModifierType type) const;
/**
* @brief Calculate the multiplier for a given modifier type. With a base value of 100 (%).
*
* @param type The modifier type.
* @param additionalModifiers Additional modifiers to apply.
* @return The multiplier.
*/
float CalculateMultiplier(ModifierType type, const std::vector<ModifierInstance>& additionalModifiers) const;
/**
* @brief Calculate damage conversation mapping.
*
* @param additionalModifiers Additional modifiers to apply.
*
* @return The damage conversion mapping.
*/
std::unordered_map<ModifierType, std::unordered_map<ModifierType, float>> CalculateDamageConversion(std::vector<ModifierInstance>& additionalModifiers) const;
void ApplyToEntity();
void CheckForRescale(AdditionalEntityData* other);
int32_t GetLevel() const { return level; }
LWOOBJID GetID() const { return id; }
LOT GetLOT() const { return lot; }
const std::unordered_set<LWOOBJID>& GetUpgradeItems() const { return upgradeItems; }
void AddUpgradeItem(LWOOBJID id) { upgradeItems.insert(id); }
void RemoveUpgradeItem(LWOOBJID id) { upgradeItems.erase(id); }
std::vector<ModifierInstance> TriggerUpgradeItems(UpgradeTriggerType triggerType, const TriggerParameters& params);
std::vector<ModifierInstance> TriggerUpgradeItems(UpgradeTriggerType triggerType);
void InitializeSkills();
void AddSkills(LWOOBJID item);
void RemoveSkills(LOT lot);
const std::vector<ModifierInstance>& GetActiveModifiers() const { return activeModifiers; }
std::vector<ModifierInstance> CalculateMainWeaponDamage();
private:
void RollStandardModifiers(int32_t level);
void TriggerPassiveRegeneration();
bool initialized = false;
std::vector<ModifierInstance> standardModifiers;
std::vector<ModifierInstance> activeModifiers;
std::unordered_set<LWOOBJID> upgradeItems;
LWOOBJID id;
LOT lot;
int32_t level = 1;
};
}

View File

@@ -0,0 +1,240 @@
#include "AdditionalItemData.h"
#include "Item.h"
#include "eItemType.h"
#include "NejlikaData.h"
using namespace nejlika;
nejlika::AdditionalItemData::AdditionalItemData(Item* item)
{
const auto& config = item->GetConfig();
for (const auto& entry : config)
{
if (entry->GetKey() != u"modifiers")
{
continue;
}
const auto str = entry->GetValueAsString();
if (str.empty())
{
continue;
}
try
{
const auto json = nlohmann::json::parse(str);
Load(json);
}
catch (const nlohmann::json::exception& e)
{
std::cout << "Failed to parse additional item data: " << e.what() << std::endl;
}
}
}
nejlika::AdditionalItemData::AdditionalItemData(const nlohmann::json& json) {
Load(json);
}
void nejlika::AdditionalItemData::Load(const nlohmann::json& json) {
if (json.contains("names"))
{
for (const auto& name : json["names"])
{
modifierNames.emplace_back(name);
}
}
if (json.contains("instances"))
{
for (const auto& instance : json["instances"])
{
modifierInstances.emplace_back(instance);
}
}
}
nlohmann::json nejlika::AdditionalItemData::ToJson() const {
nlohmann::json json;
json["names"] = nlohmann::json::array();
for (const auto& name : modifierNames)
{
json["names"].push_back(name.ToJson());
}
json["instances"] = nlohmann::json::array();
for (const auto& instance : modifierInstances)
{
json["instances"].push_back(instance.ToJson());
}
return json;
}
void nejlika::AdditionalItemData::Save(Item* item) {
auto& config = item->GetConfig();
// Remove the old data
for (size_t i = 0; i < config.size(); i++)
{
if (config[i]->GetKey() == u"modifiers")
{
config.erase(config.begin() + i);
break;
}
}
std::stringstream ss;
ss << ToJson().dump();
std::cout << ss.str() << std::endl;
config.push_back(new LDFData<std::string>(u"modifiers", ToJson().dump()));
}
void nejlika::AdditionalItemData::RollModifiers(Item* item, int32_t level) {
modifierNames.clear();
modifierInstances.clear();
const auto& info = item->GetInfo();
const auto itemType = static_cast<eItemType>(info.itemType);
const auto itemRarity = info.rarity == 0 ? 1 : info.rarity;
uint32_t rarityRollPrefix = 0;
uint32_t rarityRollSuffix = 0;
// Generate (itemRarity) amout of names and modifiers rolls, take the highest rarity. 0-1000
for (int i = 0; i < itemRarity; i++) {
auto roll = GeneralUtils::GenerateRandomNumber<uint32_t>() % 1000;
if (roll > rarityRollPrefix) {
rarityRollPrefix = roll;
}
roll = GeneralUtils::GenerateRandomNumber<uint32_t>() % 1000;
if (roll > rarityRollSuffix) {
rarityRollSuffix = roll;
}
}
const auto& templates = NejlikaData::GetModifierNameTemplates();
std::vector<ModifierNameTemplate> availablePrefixes;
std::vector<ModifierNameTemplate> availableSuffixes;
for (const auto& [type, nameTemplates] : templates) {
for (const auto& nameTemplate : nameTemplates) {
if (type != ModifierNameType::Prefix && type != ModifierNameType::Suffix) {
continue;
}
if (nameTemplate.GetMinLevel() > level || nameTemplate.GetMaxLevel() < level) {
continue;
}
const auto rarity = nameTemplate.GetRarity();
if (rarity == ModifierRarity::Common) {
continue;
}
const auto& itemTypes = nameTemplate.GetItemTypes();
if (std::find(itemTypes.begin(), itemTypes.end(), itemType) == itemTypes.end()) {
continue;
}
/*
Uncommon: rarityRoll > 500,
Rare: rarityRoll > 900,
Epic: rarityRoll > 990,
Legendary: rarityRoll = 999
*/
const auto roll = type == ModifierNameType::Prefix ? rarityRollPrefix : rarityRollSuffix;
if (rarity == ModifierRarity::Uncommon && roll > 900) {
continue;
}
if (rarity == ModifierRarity::Rare && (roll <= 900 || roll > 990)) {
continue;
}
if (rarity == ModifierRarity::Epic && (roll <= 990 || roll > 998)) {
continue;
}
if (rarity == ModifierRarity::Legendary && roll != 999) {
continue;
}
if (type == ModifierNameType::Prefix) {
availablePrefixes.push_back(nameTemplate);
}
else {
availableSuffixes.push_back(nameTemplate);
}
}
}
if (!availablePrefixes.empty()) {
const auto& prefix = availablePrefixes[GeneralUtils::GenerateRandomNumber<uint32_t>() % availablePrefixes.size()];
modifierNames.push_back(ModifierName(prefix));
const auto modifiers = prefix.GenerateModifiers(level);
modifierInstances.insert(modifierInstances.end(), modifiers.begin(), modifiers.end());
}
if (!availableSuffixes.empty()) {
const auto& suffix = availableSuffixes[GeneralUtils::GenerateRandomNumber<uint32_t>() % availableSuffixes.size()];
modifierNames.push_back(ModifierName(suffix));
const auto modifiers = suffix.GenerateModifiers(level);
modifierInstances.insert(modifierInstances.end(), modifiers.begin(), modifiers.end());
}
const auto& itemTemplateVec = NejlikaData::GetModifierNameTemplates(ModifierNameType::Object);
std::vector<const ModifierNameTemplate*> availableObjects;
for (const auto& itemTemplate : itemTemplateVec) {
if (itemTemplate.GetMinLevel() > level || itemTemplate.GetMaxLevel() < level) {
continue;
}
if (itemTemplate.GetLOT() != static_cast<int32_t>(item->GetLot())) {
continue;
}
availableObjects.push_back(&itemTemplate);
}
if (availableObjects.empty()) {
Save(item);
return;
}
const auto& itemTemplate = *availableObjects[GeneralUtils::GenerateRandomNumber<uint32_t>() % availableObjects.size()];
const auto itemModifiers = itemTemplate.GenerateModifiers(level);
modifierInstances.insert(modifierInstances.end(), itemModifiers.begin(), itemModifiers.end());
Save(item);
}

View File

@@ -0,0 +1,41 @@
#pragma once
#include <cstdint>
#include "ModifierName.h"
#include "ModifierInstance.h"
#include "json.hpp"
class Item;
namespace nejlika
{
class AdditionalItemData
{
public:
AdditionalItemData() = default;
AdditionalItemData(Item* item);
AdditionalItemData(const nlohmann::json& json);
nlohmann::json ToJson() const;
void Load(const nlohmann::json& json);
void Save(Item* item);
void RollModifiers(Item* item, int32_t level);
const std::vector<ModifierName>& GetModifierNames() const { return modifierNames; }
const std::vector<ModifierInstance>& GetModifierInstances() const { return modifierInstances; }
private:
std::vector<ModifierName> modifierNames;
std::vector<ModifierInstance> modifierInstances;
};
}

View File

@@ -6,7 +6,24 @@ set(DGAME_SOURCES "Character.cpp"
"TeamManager.cpp"
"TradingManager.cpp"
"User.cpp"
"UserManager.cpp")
"UserManager.cpp"
"ModifierTemplate.cpp"
"ModifierInstance.cpp"
"ModifierRarity.cpp"
"ModifierType.cpp"
"ModifierScale.cpp"
"ModifierName.cpp"
"ModifierNameTemplate.cpp"
"NejlikaData.cpp"
"AdditionalItemData.cpp"
"EntityTemplate.cpp"
"AdditionalEntityData.cpp"
"NejlikaHooks.cpp"
"UpgradeTemplate.cpp"
"UpgradeEffect.cpp"
"Lookup.cpp"
"NejlikaHelpers.cpp"
)
include_directories(
${PROJECT_SOURCE_DIR}/dScripts

View File

@@ -96,6 +96,9 @@
#include "CDSkillBehaviorTable.h"
#include "CDZoneTableTable.h"
Observable<Entity*, const PositionUpdate&> Entity::OnPlayerPositionUpdate;
Observable<Entity*> Entity::OnReadyForUpdates;
Entity::Entity(const LWOOBJID& objectID, EntityInfo info, User* parentUser, Entity* parentEntity) {
m_ObjectID = objectID;
m_TemplateID = info.lot;
@@ -1778,6 +1781,12 @@ void Entity::SetOwnerOverride(const LWOOBJID value) {
m_OwnerOverride = value;
}
void Entity::SetPlayerReadyForUpdates() {
m_PlayerIsReadyForUpdates = true;
OnReadyForUpdates(this);
}
bool Entity::GetIsGhostingCandidate() const {
return m_IsGhostingCandidate;
}
@@ -2133,6 +2142,8 @@ void Entity::ProcessPositionUpdate(PositionUpdate& update) {
Game::entityManager->QueueGhostUpdate(GetObjectID());
if (updateChar) Game::entityManager->SerializeEntity(this);
OnPlayerPositionUpdate.Notify(this, update);
}
const SystemAddress& Entity::GetSystemAddress() const {

View File

@@ -11,6 +11,7 @@
#include "NiQuaternion.h"
#include "LDFFormat.h"
#include "eKillType.h"
#include "Observable.h"
namespace Loot {
class Info;
@@ -116,7 +117,7 @@ public:
void SetOwnerOverride(LWOOBJID value);
void SetPlayerReadyForUpdates() { m_PlayerIsReadyForUpdates = true; }
void SetPlayerReadyForUpdates();
void SetObservers(int8_t value);
@@ -299,6 +300,13 @@ public:
// Scale will only be communicated to the client when the construction packet is sent
void SetScale(const float scale) { m_Scale = scale; };
/**
* @brief The observable for player entity position updates.
*/
static Observable<Entity*, const PositionUpdate&> OnPlayerPositionUpdate;
static Observable<Entity*> OnReadyForUpdates;
protected:
LWOOBJID m_ObjectID;

View File

@@ -26,6 +26,9 @@
#include "GhostComponent.h"
#include <ranges>
Observable<Entity*> EntityManager::OnEntityCreated;
Observable<Entity*> EntityManager::OnEntityDestroyed;
// Configure which zones have ghosting disabled, mostly small worlds.
std::vector<LWOMAPID> EntityManager::m_GhostingExcludedZones = {
// Small zones
@@ -138,6 +141,9 @@ Entity* EntityManager::CreateEntity(EntityInfo info, User* user, Entity* parentE
m_SpawnPoints.insert_or_assign(GeneralUtils::UTF16ToWTF8(spawnName), entity->GetObjectID());
}
// Notify observers that a new entity has been created
OnEntityCreated(entity);
return entity;
}
@@ -161,6 +167,9 @@ void EntityManager::DestroyEntity(Entity* entity) {
DestructEntity(entity);
}
// Notify observers that an entity is about to be destroyed
OnEntityDestroyed(entity);
// Delete this entity at the end of the frame
ScheduleForDeletion(id);
}

View File

@@ -7,6 +7,7 @@
#include <unordered_map>
#include "dCommonVars.h"
#include "Observable.h"
class Entity;
class EntityInfo;
@@ -72,6 +73,9 @@ public:
const bool GetHardcoreDropinventoryOnDeath() { return m_HardcoreDropinventoryOnDeath; };
const uint32_t GetHardcoreUscoreEnemiesMultiplier() { return m_HardcoreUscoreEnemiesMultiplier; };
static Observable<Entity*> OnEntityCreated;
static Observable<Entity*> OnEntityDestroyed;
private:
void SerializeEntities();
void KillEntities();

90
dGame/EntityTemplate.cpp Normal file
View File

@@ -0,0 +1,90 @@
#include "EntityTemplate.h"
#include <magic_enum.hpp>
nejlika::EntityTemplate::EntityTemplate(const nlohmann::json& json) {
lot = json["lot"].get<LOT>();
minLevel = json.contains("min-level") ? json["min-level"].get<int32_t>() : 1;
for (const auto& scaler : json["scaling"])
{
EntityTemplateScaler s;
s.type = magic_enum::enum_cast<ModifierType>(scaler["type"].get<std::string>()).value();
s.isResistance = scaler.contains("resistance") && scaler["resistance"].get<bool>();
s.polynomial = scaler["polynomial"].get<std::vector<float>>();
scalers.push_back(s);
}
}
nlohmann::json nejlika::EntityTemplate::ToJson() const {
nlohmann::json json;
json["lot"] = lot;
json["min-level"] = minLevel;
nlohmann::json scalersJson;
for (const auto& scaler : scalers)
{
nlohmann::json s;
s["type"] = magic_enum::enum_name(scaler.type);
s["resistance"] = scaler.isResistance;
s["polynomial"] = scaler.polynomial;
scalersJson.push_back(s);
}
json["scaling"] = scalersJson;
return json;
}
float nejlika::EntityTemplate::GetScaler(ModifierType type, bool isResistance, int32_t level) const {
for (const auto& scaler : scalers)
{
if (scaler.type == type && scaler.isResistance == isResistance)
{
return CalculateScaler(scaler, level);
}
}
return 0.0f;
}
std::vector<nejlika::ModifierInstance> nejlika::EntityTemplate::GenerateModifiers(int32_t level) const {
std::vector<ModifierInstance> modifiers;
for (const auto& scaler : scalers)
{
ModifierInstance modifier(
scaler.type,
CalculateScaler(scaler, level),
ModifierOperator::Additive,
scaler.isResistance,
ModifierCategory::Player,
0,
"",
ModifierType::Invalid,
""
);
modifiers.push_back(modifier);
}
return modifiers;
}
float nejlika::EntityTemplate::CalculateScaler(const EntityTemplateScaler& scaler, int32_t level) const {
float result = 0.0f;
for (size_t i = 0; i < scaler.polynomial.size(); ++i)
{
result += scaler.polynomial[i] * std::pow(level, i);
}
return result;
}

47
dGame/EntityTemplate.h Normal file
View File

@@ -0,0 +1,47 @@
#pragma once
#include <cstdint>
#include <vector>
#include "Entity.h"
#include "ModifierType.h"
#include "ModifierInstance.h"
#include "json.hpp"
namespace nejlika
{
class EntityTemplate
{
public:
EntityTemplate() = default;
EntityTemplate(const nlohmann::json& json);
nlohmann::json ToJson() const;
LOT GetLOT() const { return lot; }
int32_t GetMinLevel() const { return minLevel; }
float GetScaler(ModifierType type, bool isResistance, int32_t level) const;
std::vector<ModifierInstance> GenerateModifiers(int32_t level) const;
private:
struct EntityTemplateScaler
{
ModifierType type;
bool isResistance;
std::vector<float> polynomial;
};
float CalculateScaler(const EntityTemplateScaler& scaler, int32_t level) const;
LOT lot;
std::vector<EntityTemplateScaler> scalers;
int32_t minLevel;
};
}

181
dGame/Lookup.cpp Normal file
View File

@@ -0,0 +1,181 @@
#include "Lookup.h"
#include <fstream>
#include "json.hpp"
#include <iostream>
using namespace nejlika;
Lookup::Lookup(const std::filesystem::path& lookup)
{
m_Lookup = lookup;
// Check if the file exists.
if (!std::filesystem::exists(lookup))
{
// Empty lookup
return;
}
// Read the json file
std::ifstream file(lookup);
std::string json((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
// Parse the json
auto doc = nlohmann::json::parse(json);
// The document is a map, so we can iterate over it.
for (const auto& [key, v] : doc.items()) {
if (v.is_number_integer())
{
m_LookupMap[key] = v.get<int64_t>();
continue;
}
if (!v.is_object()) {
std::stringstream ss;
ss << "Invalid value for key \"" << key << "\" in lookup.";
throw std::runtime_error(ss.str());
}
// Get the data
auto data = v.get<nlohmann::json>();
// Get the id
auto id = data["id"].get<int64_t>();
m_LookupMap[key] = id;
// Get the metadata
auto metadata = data["metadata"].get<std::string>();
m_Metadata[key] = metadata;
}
}
nejlika::Lookup::Lookup(const Lookup &other)
{
m_Lookup = other.m_Lookup;
m_LookupMap = other.m_LookupMap;
m_Metadata = other.m_Metadata;
m_CoreSymbols = other.m_CoreSymbols;
}
id Lookup::GetValue(const name& symbol) const
{
id value;
if (IsCoreSymbol(symbol, value)) {
return value;
}
const auto& it = m_LookupMap.find(symbol);
if (it == m_LookupMap.end())
{
std::stringstream ss;
ss << "Symbol \"" << symbol << "\" does not exist in the lookup.";
throw std::runtime_error(ss.str());
}
return it->second;
}
bool nejlika::Lookup::Exists(const name &symbol) const
{
return IsCoreSymbol(symbol) || (m_CoreSymbols.find(symbol) != m_CoreSymbols.end()) || (m_LookupMap.find(symbol) != m_LookupMap.end());
}
bool nejlika::Lookup::Exists(id value) const
{
for (const auto& [k, v] : m_LookupMap)
{
if (v == value)
{
return true;
}
}
return false;
}
const std::string& Lookup::GetMetadata(const name& symbol) const
{
const auto& it = m_Metadata.find(symbol);
if (it == m_Metadata.end())
{
static std::string empty;
return empty;
}
return it->second;
}
const std::filesystem::path& Lookup::GetLookup() const
{
return m_Lookup;
}
const std::unordered_map<name, id> &nejlika::Lookup::GetMap() const
{
return m_LookupMap;
}
bool nejlika::Lookup::IsCoreSymbol(const name &symbol, id &value) const
{
try {
value = std::stoi(symbol);
return true;
} catch (...) {
// cont...
}
if (!symbol.starts_with(core_prefix)) {
return false;
}
// Check in the core symbols
const auto& it = m_CoreSymbols.find(symbol);
if (it != m_CoreSymbols.end())
{
value = it->second;
return true;
}
// In the format "core:<value>"
try {
value = std::stoi(symbol.substr(core_prefix.size() + 1));
} catch (...) {
return false;
}
return true;
}
bool nejlika::Lookup::IsCoreSymbol(const name &symbol)
{
// Check if it can be converted to an integer
try {
[[maybe_unused]] auto value = std::stoi(symbol);
return true;
} catch (...) {
// cont...
}
return symbol.starts_with(core_prefix);
}

117
dGame/Lookup.h Normal file
View File

@@ -0,0 +1,117 @@
#pragma once
#include <filesystem>
#include <functional>
#include <cstdint>
#include <unordered_map>
#include <unordered_set>
namespace nejlika {
typedef std::string name;
typedef int id;
/**
* @brief A one-way mapping between symbols (names) and their corresponding numerical values.
*
* This is an active lookup, meaning that it is possible to add new symbols and their values to the lookup.
*
* A method is provided to wait for a symbol to be added to the lookup.
*/
class Lookup
{
public:
inline static const name core_prefix = "lego-universe";
/**
* @brief Constructs a Lookup object with the specified lookup file path.
*
* @param lookup The path to the lookup file.
* @throw If the lookup file could not be parsed.
*/
Lookup(const std::filesystem::path& lookup);
Lookup(const Lookup& other);
Lookup() = default;
/**
* @brief Gets the value of the specified symbol.
*
* @param symbol The symbol to get the value of.
* @return The value of the specified symbol.
* @throw If the specified symbol does not exist.
*/
id GetValue(const name& symbol) const;
/**
* @brief Checks whether the specified symbol exists.
*
* @param symbol The symbol to check.
* @return Whether the specified symbol exists.
*/
bool Exists(const name& symbol) const;
/**
* @brief Checks whether any symbol has the specified value.
*
* @param value The value to check.
* @return Whether any symbol has the specified value.
*/
bool Exists(id value) const;
/**
* @brief Gets the metadata of a specified symbol.
*
* @param symbol The symbol to get metadata of.
* @return The metadata of the specified symbol or an empty string if the symbol does not exist.
*/
const std::string& GetMetadata(const name& symbol) const;
/**
* @brief Gets the path to the lookup file.
*
* @return The path to the lookup file.
*/
const std::filesystem::path& GetLookup() const;
/**
* @brief Gets the map of all symbols and their values.
*
* @return The map of all symbols and their values.
*/
const std::unordered_map<name, id>& GetMap() const;
/**
* @brief Checks whether the specified symbol is a core symbol.
*
* @param symbol The symbol to check.
* @param value The value of the core symbol.
* @return Whether the specified symbol is a core symbol.
*/
bool IsCoreSymbol(const name& symbol, id& value) const;
/**
* @brief Checks whether the specified symbol is a core symbol.
*
* A symbol is considered a core symbol if it is either:
* a number;
* or a string starting with the core_prefix.
*
* @param symbol The symbol to check.
* @return Whether the specified symbol is a core symbol.
*/
static bool IsCoreSymbol(const name& symbol);
private:
std::filesystem::path m_Lookup;
std::unordered_map<name, id> m_LookupMap;
std::unordered_map<name, std::string> m_Metadata;
std::unordered_map<name, id> m_CoreSymbols;
};
} // namespace nejlika

14
dGame/ModifierCategory.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include <cstdint>
namespace nejlika
{
enum class ModifierCategory : uint8_t
{
Player = 0 << 0,
Pet = 1 << 0
};
}

170
dGame/ModifierInstance.cpp Normal file
View File

@@ -0,0 +1,170 @@
#include "ModifierInstance.h"
#include <sstream>
#include <magic_enum.hpp>
nejlika::ModifierInstance::ModifierInstance(const nlohmann::json& config) {
type = magic_enum::enum_cast<ModifierType>(config["type"].get<std::string>()).value_or(ModifierType::Invalid);
convertTo = magic_enum::enum_cast<ModifierType>(config["convert-to"].get<std::string>()).value_or(ModifierType::Invalid);
value = config["value"].get<float>();
if (config.contains("op")) {
op = magic_enum::enum_cast<ModifierOperator>(config["op"].get<std::string>()).value_or(ModifierOperator::Additive);
}
else {
op = ModifierOperator::Additive;
}
isResistance = config.contains("resistance") ? config["resistance"].get<bool>() : false;
if (config.contains("category")) {
category = magic_enum::enum_cast<ModifierCategory>(config["category"].get<std::string>()).value_or(ModifierCategory::Player);
}
else {
category = ModifierCategory::Player;
}
effectID = config.contains("effect-id") ? config["effect-id"].get<uint32_t>() : 0;
effectType = config.contains("effect-type") ? config["effect-type"].get<std::string>() : "";
}
nlohmann::json nejlika::ModifierInstance::ToJson() const
{
nlohmann::json config;
config["type"] = magic_enum::enum_name(type);
config["convert-to"] = magic_enum::enum_name(convertTo);
config["value"] = value;
config["op"] = magic_enum::enum_name(op);
config["resistance"] = isResistance;
config["category"] = magic_enum::enum_name(category);
config["effect-id"] = effectID;
config["effect-type"] = effectType;
return config;
}
std::string nejlika::ModifierInstance::GenerateHtmlString(const std::vector<ModifierInstance>& modifiers)
{
std::stringstream ss;
// target -> resistance -> op -> type -> value
std::unordered_map<ModifierCategory, std::unordered_map<bool, std::unordered_map<ModifierOperator, std::unordered_map<ModifierType, float>>>> modifierMap;
bool hasConvertTo = false;
bool hasSkillModifier = false;
for (const auto& modifier : modifiers) {
if (modifier.type == ModifierType::Invalid) {
continue;
}
if (modifier.GetConvertTo() != ModifierType::Invalid)
{
hasConvertTo = true;
continue;
}
if (!modifier.GetUpgradeName().empty())
{
hasSkillModifier = true;
continue;
}
modifierMap[modifier.category][modifier.isResistance][modifier.op][modifier.type] = modifier.value;
}
// Resistances and addatives are not separated, pet and player are
// Summarize the resistances and addatives
for (const auto& target : modifierMap) {
if (target.first == ModifierCategory::Pet) {
ss << "\n<font color=\"#D0AB62\">Pets:</font>\n";
}
for (const auto& resistance : target.second) {
for (const auto& math : resistance.second) {
for (const auto& modifier : math.second) {
ss << "<font color=\"#FFFFFF\">";
ss << ((modifier.second > 0) ? (math.first == ModifierOperator::Multiplicative ? "+" : "") : "-");
ss << std::fixed << std::setprecision(1) << std::abs(modifier.second);
if (math.first == ModifierOperator::Multiplicative) {
ss << "%";
}
ss << "</font> <font color=\"#D0AB62\">";
ss << " " << nejlika::GetModifierTypeName(modifier.first);
if (resistance.first) {
// If the ss now ends with 'Damage' remove it
if (ss.str().substr(ss.str().size() - 7) == " Damage") {
ss.seekp(-7, std::ios_base::end);
}
ss << " " << "Resistance";
}
ss << "</font>\n";
}
}
}
}
if (hasSkillModifier)
{
for (const auto& modifier : modifiers) {
if (modifier.type != ModifierType::SkillModifier) {
continue;
}
ss << "<font color=\"" << GetModifierTypeColor(modifier.type) << "\">";
ss << ((modifier.value > 0) ? "+" : "-");
ss << std::fixed << std::setprecision(0) << std::abs(modifier.value);
ss << " to ";
ss << modifier.GetUpgradeName();
ss << "</font>\n";
}
}
if (hasConvertTo)
{
for (const auto& modifier : modifiers) {
if (modifier.GetConvertTo() == ModifierType::Invalid)
{
continue;
}
if (modifier.type == ModifierType::Invalid) {
continue;
}
ss << "<font color=\"#FFFFFF\">";
// +xx/yy% of T1 converted to T2
ss << ((modifier.value > 0) ? "" : "-");
ss << std::fixed << std::setprecision(0) << std::abs(modifier.value);
ss << "%</font> <font color=\"#D0AB62\">";
ss << " of ";
ss << nejlika::GetModifierTypeName(modifier.type);
ss << " converted to ";
ss << nejlika::GetModifierTypeName(modifier.GetConvertTo());
ss << "</font>\n";
}
}
return ss.str();
}

97
dGame/ModifierInstance.h Normal file
View File

@@ -0,0 +1,97 @@
#pragma once
#include "ModifierType.h"
#include "ModifierCategory.h"
#include "ModifierOperator.h"
#include <cstdint>
#include <string>
#include "json.hpp"
namespace nejlika
{
class ModifierInstance
{
public:
ModifierInstance(
ModifierType type, float value, ModifierOperator op, bool isResistance, ModifierCategory category, uint32_t effectID, const std::string& effectType, ModifierType convertTo,
const std::string& upgradeName
) : type(type), value(value), op(op), isResistance(isResistance), category(category), effectID(effectID), effectType(effectType), convertTo(convertTo),
upgradeName(upgradeName)
{}
/**
* @brief Construct a new Modifier Instance object from a json configuration.
*
* @param config The json configuration.
*/
ModifierInstance(const nlohmann::json& config);
/**
* @brief Convert the modifier instance to a json representation.
*
* @return The json representation.
*/
nlohmann::json ToJson() const;
/**
* @brief Generate a HTML string representation of a set of modifiers.
*
* @param modifiers The modifiers to generate the HTML string for.
* @return The HTML string.
*/
static std::string GenerateHtmlString(const std::vector<ModifierInstance>& modifiers);
// Getters and setters
ModifierType GetType() const { return type; }
ModifierType GetConvertTo() const { return convertTo; }
float GetValue() const { return value; }
ModifierOperator GetOperator() const { return op; }
bool IsResistance() const { return isResistance; }
ModifierCategory GetCategory() const { return category; }
uint32_t GetEffectID() const { return effectID; }
std::string GetEffectType() const { return effectType; }
std::string GetUpgradeName() const { return upgradeName; }
void SetType(ModifierType type) { this->type = type; }
void SetConvertTo(ModifierType convertTo) { this->convertTo = convertTo; }
void SetValue(float value) { this->value = value; }
void SetOperator(ModifierOperator op) { this->op = op; }
void SetIsResistance(bool isResistance) { this->isResistance = isResistance; }
void SetCategory(ModifierCategory category) { this->category = category; }
void SetEffectID(uint32_t effectID) { this->effectID = effectID; }
void SetEffectType(const std::string& effectType) { this->effectType = effectType; }
void SetUpgradeName(const std::string& upgradeName) { this->upgradeName = upgradeName; }
private:
ModifierType type;
ModifierType convertTo;
float value;
ModifierOperator op;
bool isResistance;
ModifierCategory category;
uint32_t effectID;
std::string effectType;
std::string upgradeName;
};
}

83
dGame/ModifierName.cpp Normal file
View File

@@ -0,0 +1,83 @@
#include "ModifierName.h"
#include <magic_enum.hpp>
using namespace nejlika;
nejlika::ModifierName::ModifierName(const nlohmann::json & json)
{
name = json["name"].get<std::string>();
if (json.contains("type"))
{
type = magic_enum::enum_cast<ModifierNameType>(json["type"].get<std::string>()).value_or(ModifierNameType::Prefix);
}
else
{
type = ModifierNameType::Prefix;
}
if (json.contains("rarity"))
{
rarity = magic_enum::enum_cast<ModifierRarity>(json["rarity"].get<std::string>()).value_or(ModifierRarity::Common);
}
else
{
rarity = ModifierRarity::Common;
}
}
nejlika::ModifierName::ModifierName(const ModifierNameTemplate& templateData) {
name = templateData.GetName();
type = templateData.GetType();
rarity = templateData.GetRarity();
}
nlohmann::json nejlika::ModifierName::ToJson() const
{
nlohmann::json json;
json["name"] = name;
json["type"] = magic_enum::enum_name(type);
json["rarity"] = magic_enum::enum_name(rarity);
return json;
}
std::string nejlika::ModifierName::GenerateHtmlString() const {
const auto& rarityColor = ModifierRarityHelper::GetModifierRarityColor(rarity);
std::stringstream ss;
ss << "<font color=\"" << rarityColor << "\">" << name << "</font>";
return ss.str();
}
std::string nejlika::ModifierName::GenerateHtmlString(const std::vector<ModifierName>& names)
{
std::stringstream ss;
for (const auto& name : names) {
if (name.GetType() == ModifierNameType::Prefix && !name.name.empty()) {
ss << name.GenerateHtmlString() << "\n";
}
}
ss << "<font color=\"#D0AB62\">NAME</font>";
for (const auto& name : names) {
if (name.GetType() == ModifierNameType::Suffix && !name.name.empty()) {
ss << "\n" << name.GenerateHtmlString();
}
}
// Remove the last newline
auto str = ss.str();
if (!str.empty() && str.back() == '\n') {
str.pop_back();
}
return str;
}

57
dGame/ModifierName.h Normal file
View File

@@ -0,0 +1,57 @@
#pragma once
#include <cstdint>
#include <string>
#include "ModifierNameType.h"
#include "ModifierRarity.h"
#include "ModifierNameTemplate.h"
#include "json.hpp"
namespace nejlika
{
class ModifierName
{
public:
ModifierName(const std::string& name, ModifierNameType type, ModifierRarity rarity) :
name(name), type(type), rarity(rarity) {}
ModifierName(const nlohmann::json& json);
ModifierName(const ModifierNameTemplate& templateData);
nlohmann::json ToJson() const;
std::string GenerateHtmlString() const;
/**
* @brief Generate a HTML string representation of a set of names.
*
* @param modifiers The names to generate the HTML string for.
* @return The HTML string.
*/
static std::string GenerateHtmlString(const std::vector<ModifierName>& names);
// Getters and setters
const std::string& GetName() const { return name; }
ModifierNameType GetType() const { return type; }
ModifierRarity GetRarity() const { return rarity; }
void SetName(const std::string& name) { this->name = name; }
void SetType(ModifierNameType type) { this->type = type; }
void SetRarity(ModifierRarity rarity) { this->rarity = rarity; }
private:
std::string name;
ModifierNameType type;
ModifierRarity rarity;
};
}

View File

@@ -0,0 +1,166 @@
#include "ModifierNameTemplate.h"
#include <iostream>
#include "magic_enum.hpp"
using namespace nejlika;
nejlika::ModifierNameTemplate::ModifierNameTemplate(const nlohmann::json & json)
{
name = json["name"].get<std::string>();
if (json.contains("lot"))
{
lot = json["lot"].get<int32_t>();
}
else
{
lot = 0;
}
if (json.contains("type"))
{
type = magic_enum::enum_cast<ModifierNameType>(json["type"].get<std::string>()).value_or(ModifierNameType::Prefix);
}
else
{
type = ModifierNameType::Prefix;
}
if (json.contains("items"))
{
for (const auto& itemType : json["items"])
{
std::string type = itemType.get<std::string>();
// Make uppercase
std::transform(type.begin(), type.end(), type.begin(), ::toupper);
// Replace spaces with underscores
std::replace(type.begin(), type.end(), ' ', '_');
const auto itemTypeEnum = magic_enum::enum_cast<eItemType>(type);
if (itemTypeEnum.has_value())
{
itemTypes.push_back(itemTypeEnum.value());
}
else
{
std::cout << "Invalid item type: " << type << std::endl;
}
}
}
if (json.contains("modifiers"))
{
for (const auto& modifier : json["modifiers"])
{
const auto modifierTemplate = ModifierTemplate(modifier);
if (modifierTemplate.GetTypes().empty() || modifierTemplate.GetTypes().at(0) == ModifierType::Invalid)
{
continue;
}
modifiers.push_back(modifierTemplate);
}
}
if (json.contains("levels"))
{
auto levels = json["levels"];
if (levels.contains("min"))
{
minLevel = levels["min"].get<int32_t>();
const auto rasio = 1; //45.0f / 85.0f;
minLevel = std::max(1, static_cast<int32_t>(minLevel * rasio));
}
else
{
minLevel = 1;
}
if (levels.contains("max"))
{
maxLevel = levels["max"].get<int32_t>();
}
else
{
maxLevel = 45;
}
}
if (json.contains("rarity"))
{
rarity = magic_enum::enum_cast<ModifierRarity>(json["rarity"].get<std::string>()).value_or(ModifierRarity::Common);
}
else
{
rarity = ModifierRarity::Common;
}
}
nlohmann::json nejlika::ModifierNameTemplate::ToJson() const {
nlohmann::json json;
json["name"] = name;
json["type"] = magic_enum::enum_name(type);
if (lot != 0)
{
json["lot"] = lot;
}
if (!itemTypes.empty())
{
nlohmann::json items;
for (const auto& itemType : itemTypes)
{
items.push_back(magic_enum::enum_name(itemType));
}
json["items"] = items;
}
if (!modifiers.empty())
{
nlohmann::json modifierTemplates;
for (const auto& modifier : modifiers)
{
modifierTemplates.push_back(modifier.ToJson());
}
json["modifiers"] = modifierTemplates;
}
nlohmann::json levels;
levels["min"] = minLevel;
levels["max"] = maxLevel;
json["levels"] = levels;
json["rarity"] = magic_enum::enum_name(rarity);
return json;
}
std::vector<ModifierInstance> nejlika::ModifierNameTemplate::GenerateModifiers(int32_t level) const
{
std::vector<ModifierInstance> result;
for (const auto& modifierTemplate : modifiers)
{
auto modifiers = modifierTemplate.GenerateModifiers(level);
result.insert(result.end(), modifiers.begin(), modifiers.end());
}
return result;
}

View File

@@ -0,0 +1,55 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "eItemType.h"
#include "ModifierNameType.h"
#include "ModifierRarity.h"
#include "ModifierTemplate.h"
#include "json.hpp"
namespace nejlika
{
class ModifierNameTemplate
{
public:
ModifierNameTemplate(const nlohmann::json& json);
nlohmann::json ToJson() const;
std::vector<ModifierInstance> GenerateModifiers(int32_t level) const;
// Getters
const std::string& GetName() const { return name; }
ModifierNameType GetType() const { return type; }
const std::vector<ModifierTemplate>& GetModifiers() const { return modifiers; }
const std::vector<eItemType>& GetItemTypes() const { return itemTypes; }
int32_t GetMinLevel() const { return minLevel; }
int32_t GetMaxLevel() const { return maxLevel; }
ModifierRarity GetRarity() const { return rarity; }
int32_t GetLOT() const { return lot; }
private:
std::string name;
int32_t lot;
ModifierNameType type;
std::vector<ModifierTemplate> modifiers;
std::vector<eItemType> itemTypes;
int32_t minLevel;
int32_t maxLevel;
ModifierRarity rarity;
};
}

16
dGame/ModifierNameType.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
namespace nejlika
{
enum class ModifierNameType : uint8_t
{
Prefix,
Suffix,
Object,
Skill
};
}

14
dGame/ModifierOperator.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include <cstdint>
namespace nejlika
{
enum class ModifierOperator : uint8_t
{
Additive,
Multiplicative
};
}

31
dGame/ModifierRarity.cpp Normal file
View File

@@ -0,0 +1,31 @@
#include "ModifierRarity.h"
#include <unordered_map>
using namespace nejlika;
namespace
{
const std::unordered_map<ModifierRarity, std::string> colorMap = {
{ModifierRarity::Common, "#FFFFFF"},
{ModifierRarity::Uncommon, "#B5AC15"},
{ModifierRarity::Rare, "#3EEA4A"},
{ModifierRarity::Epic, "#2F83C1"},
{ModifierRarity::Legendary, "#852DCA"},
{ModifierRarity::Relic, "#00FFFF"}
};
}
const std::string& nejlika::ModifierRarityHelper::GetModifierRarityColor(ModifierRarity rarity)
{
const auto color = colorMap.find(rarity);
if (color != colorMap.end()) {
return color->second;
}
static const std::string white = "#FFFFFF";
return white;
}

24
dGame/ModifierRarity.h Normal file
View File

@@ -0,0 +1,24 @@
#pragma once
#include <cstdint>
#include <string>
namespace nejlika
{
enum class ModifierRarity : uint8_t
{
Common,
Uncommon,
Rare,
Epic,
Legendary,
Relic
};
namespace ModifierRarityHelper
{
const std::string& GetModifierRarityColor(ModifierRarity rarity);
}
}

40
dGame/ModifierScale.cpp Normal file
View File

@@ -0,0 +1,40 @@
#include "ModifierScale.h"
nejlika::ModifierScale::ModifierScale(const nlohmann::json & json)
{
level = json["level"].get<int32_t>();
const auto rasio = 1; //45.0f / 85.0f;
level = std::max(1, static_cast<int32_t>(level * rasio));
if (json.contains("min")) {
min = json["min"].get<float>();
}
else {
min = 0.0f;
}
if (json.contains("max")) {
max = json["max"].get<float>();
}
else {
max = 0.0f;
}
if (json.contains("value")) {
min = json["value"].get<float>();
max = json["value"].get<float>();
}
}
nlohmann::json nejlika::ModifierScale::ToJson() const
{
nlohmann::json json;
json["level"] = level;
json["min"] = min;
json["max"] = max;
return json;
}

33
dGame/ModifierScale.h Normal file
View File

@@ -0,0 +1,33 @@
#pragma once
#include <cstdint>
#include "json.hpp"
namespace nejlika
{
class ModifierScale
{
public:
ModifierScale() = default;
ModifierScale(int32_t level, float min, float max) : level(level), min(min), max(max) {}
ModifierScale(const nlohmann::json& json);
nlohmann::json ToJson() const;
int32_t GetLevel() const { return level; }
float GetMin() const { return min; }
float GetMax() const { return max; }
private:
int32_t level = 0;
float min = 0.0f;
float max = 0.0f;
};
}

501
dGame/ModifierTemplate.cpp Normal file
View File

@@ -0,0 +1,501 @@
#include "ModifierTemplate.h"
#include <magic_enum.hpp>
#include <random>
#include <algorithm>
#include <sstream>
#include <iostream>
using namespace nejlika;
nejlika::ModifierTemplate::ModifierTemplate(const nlohmann::json& config) {
if (config.contains("type"))
{
selector = ModifierTemplateSelector::One;
const auto type = magic_enum::enum_cast<ModifierType>(config["type"].get<std::string>());
if (type.has_value())
{
types = {type.value()};
}
else
{
types = {};
}
}
else if (config.contains("all"))
{
selector = ModifierTemplateSelector::All;
types = {};
for (const auto& type : config["all"])
{
const auto t = magic_enum::enum_cast<ModifierType>(type.get<std::string>());
if (t.has_value())
{
types.push_back(t.value());
}
}
}
else if (config.contains("two-of"))
{
selector = ModifierTemplateSelector::Two;
types = {};
for (const auto& type : config["two-of"])
{
const auto t = magic_enum::enum_cast<ModifierType>(type.get<std::string>());
if (t.has_value())
{
types.push_back(t.value());
}
}
}
else
{
types = {};
}
if (config.contains("convert-to"))
{
convertTo = magic_enum::enum_cast<ModifierType>(config["convert-to"].get<std::string>()).value_or(ModifierType::Invalid);
}
else
{
convertTo = ModifierType::Invalid;
}
if (config.contains("scaling"))
{
const auto scaling = config["scaling"];
for (const auto& scaler : scaling)
{
scales.push_back(ModifierScale(scaler));
}
}
if (config.contains("polynomial"))
{
const auto polynomialConfig = config["polynomial"];
for (const auto& term : polynomialConfig)
{
polynomial.push_back(term.get<float>());
}
}
if (config.contains("category"))
{
category = magic_enum::enum_cast<ModifierCategory>(config["category"].get<std::string>()).value_or(ModifierCategory::Player);
}
else
{
category = ModifierCategory::Player;
}
isResistance = config.contains("resistance") ? config["resistance"].get<bool>() : false;
effectID = config.contains("effect-id") ? config["effect-id"].get<uint32_t>() : 0;
effectType = config.contains("effect-type") ? config["effect-type"].get<std::string>() : "";
if (config.contains("operator"))
{
operatorType = magic_enum::enum_cast<ModifierOperator>(config["operator"].get<std::string>()).value_or(ModifierOperator::Additive);
}
else
{
operatorType = ModifierOperator::Additive;
}
// Old format
if (config.contains("percentage"))
{
if (config["percentage"].get<bool>()) {
operatorType = ModifierOperator::Multiplicative;
}
}
if (config.contains("pet"))
{
if (config["pet"].get<bool>()) {
category = ModifierCategory::Pet;
}
}
if (config.contains("owner"))
{
if (config["owner"].get<bool>()) {
category = ModifierCategory::Player;
}
}
if (config.contains("skill"))
{
upgradeName = config["skill"].get<std::string>();
}
}
nlohmann::json nejlika::ModifierTemplate::ToJson() const {
nlohmann::json config;
if (selector == ModifierTemplateSelector::One)
{
config["type"] = magic_enum::enum_name(types[0]);
}
else if (selector == ModifierTemplateSelector::All)
{
config["all"] = true;
for (const auto& type : types)
{
config["types"].push_back(magic_enum::enum_name(type));
}
}
else if (selector == ModifierTemplateSelector::Two)
{
config["two-of"] = true;
for (const auto& type : types)
{
config["two-of"].push_back(magic_enum::enum_name(type));
}
}
if (!scales.empty())
{
nlohmann::json scaling;
for (const auto& scale : scales)
{
scaling.push_back(scale.ToJson());
}
config["scaling"] = scaling;
}
if (!polynomial.empty())
{
nlohmann::json polynomialConfig;
for (const auto& term : polynomial)
{
polynomialConfig.push_back(term);
}
config["polynomial"] = polynomialConfig;
}
config["convert-to"] = magic_enum::enum_name(convertTo);
config["category"] = magic_enum::enum_name(category);
config["resistance"] = isResistance;
config["effect-id"] = effectID;
config["effect-type"] = effectType;
config["operator"] = magic_enum::enum_name(operatorType);
if (!upgradeName.empty())
{
config["skill"] = upgradeName;
}
return config;
}
std::vector<ModifierInstance> nejlika::ModifierTemplate::GenerateModifiers(int32_t level) const {
std::vector<ModifierInstance> modifiers;
std::vector<ModifierType> selectedTypes;
if (types.empty())
{
return modifiers;
}
if (selector == ModifierTemplateSelector::One)
{
selectedTypes = {types[0]};
}
else if (selector == ModifierTemplateSelector::All)
{
selectedTypes = types;
}
else if (selector == ModifierTemplateSelector::Two)
{
if (types.size() < 2)
{
selectedTypes = types;
}
else
{
// Randomly select two types
selectedTypes = types;
std::shuffle(selectedTypes.begin(), selectedTypes.end(), std::mt19937(std::random_device()()));
selectedTypes.resize(2);
}
}
for (const auto& selectedType : selectedTypes)
{
auto modifierOpt = GenerateModifier(selectedType, level);
if (modifierOpt.has_value())
{
modifiers.push_back(modifierOpt.value());
}
}
return modifiers;
}
std::string nejlika::ModifierTemplate::GenerateHtmlString(const std::vector<ModifierTemplate>& modifiers, int32_t level) {
std::stringstream ss;
// target -> resistance -> op -> type -> (min, max)
std::unordered_map<ModifierCategory, std::unordered_map<bool, std::unordered_map<ModifierOperator, std::unordered_map<ModifierType, std::pair<float, float>>>>> modifierMap;
bool hasConvertTo = false;
bool hasSkillModifier = false;
for (const auto& modifier : modifiers) {
if (modifier.GetConvertTo() != ModifierType::Invalid)
{
hasConvertTo = true;
continue;
}
if (!modifier.GetUpgradeName().empty())
{
hasSkillModifier = true;
continue;
}
for (const auto& type : modifier.types) {
if (type == ModifierType::Invalid || type == ModifierType::SkillModifier) {
continue;
}
if (!modifier.polynomial.empty())
{
float value = 0.0f;
int32_t power = 0;
for (const auto& term : modifier.polynomial)
{
value += term * std::pow(level, power);
power++;
}
modifierMap[modifier.category][modifier.isResistance][modifier.operatorType][type] = {value, value};
continue;
}
ModifierScale scale;
bool found = false;
// Select the scale with the highest level that is less than or equal to the current level
for (const auto& s : modifier.scales) {
if (s.GetLevel() <= level && s.GetLevel() > scale.GetLevel()) {
scale = s;
found = true;
}
}
if (!found) {
continue;
}
modifierMap[modifier.category][modifier.isResistance][modifier.operatorType][type] = {scale.GetMin(), scale.GetMax()};
}
}
// Resistances and addatives are not separated, pet and player are
// Summarize the resistances and addatives
for (const auto& target : modifierMap) {
if (target.first == ModifierCategory::Pet) {
ss << "\n<font color=\"#D0AB62\">Pets:</font>\n";
}
for (const auto& resistance : target.second) {
for (const auto& math : resistance.second) {
for (const auto& modifier : math.second) {
ss << "<font color=\"#FFFFFF\">";
ss << ((modifier.second.first > 0) ? (math.first == ModifierOperator::Multiplicative ? "+" : "") : "-");
ss << std::fixed << std::setprecision(1) << std::abs(modifier.second.first);
if (modifier.second.first != modifier.second.second)
{
ss << "/";
ss << std::fixed << std::setprecision(1) << std::abs(modifier.second.second);
}
if (math.first == ModifierOperator::Multiplicative) {
ss << "%";
}
ss << "</font> <font color=\"#D0AB62\">";
ss << " " << nejlika::GetModifierTypeName(modifier.first);
if (resistance.first) {
// If the ss now ends with 'Damage' remove it
if (ss.str().substr(ss.str().size() - 6) == "Damage") {
ss.seekp(-6, std::ios_base::end);
}
ss << " " << "Resistance";
}
ss << "</font>\n";
}
}
}
}
if (hasSkillModifier)
{
for (const auto& modifier : modifiers) {
for (const auto& type : modifier.types) {
if (type != ModifierType::SkillModifier) {
continue;
}
const auto& scalors = modifier.GetScales();
if (scalors.empty())
{
continue;
}
const auto& m = scalors[0];
ss << "<font color=\"" << GetModifierTypeColor(type) << "\">";
ss << ((m.GetMin() > 0) ? "+" : "-");
ss << std::fixed << std::setprecision(0) << std::abs(m.GetMin());
ss << " to ";
ss << modifier.GetUpgradeName();
ss << "</font>\n";
}
}
}
if (hasConvertTo)
{
for (const auto& modifier : modifiers) {
if (modifier.GetConvertTo() == ModifierType::Invalid)
{
continue;
}
for (const auto& type : modifier.types) {
if (type == ModifierType::Invalid) {
continue;
}
const auto& scalors = modifier.GetScales();
auto m = scalors[0];
for (const auto& s : scalors) {
if (s.GetLevel() <= level && s.GetLevel() > m.GetLevel()) {
m = s;
}
}
ss << "<font color=\"#FFFFFF\">";
// +xx/yy% of T1 converted to T2
ss << ((m.GetMin() > 0) ? "" : "-");
ss << std::fixed << std::setprecision(0) << std::abs(m.GetMin());
if (m.GetMin() != m.GetMax())
{
ss << "/";
ss << std::fixed << std::setprecision(0) << std::abs(m.GetMax());
}
ss << "%</font> <font color=\"#D0AB62\">";
ss << " of ";
ss << nejlika::GetModifierTypeName(type);
ss << " converted to ";
ss << nejlika::GetModifierTypeName(modifier.GetConvertTo());
ss << "</font>\n";
}
}
}
return ss.str();
}
std::optional<ModifierInstance> nejlika::ModifierTemplate::GenerateModifier(ModifierType type, int32_t level) const {
ModifierScale scale;
bool found = false;
if (!polynomial.empty())
{
float value = 0.0f;
int32_t power = 0;
for (const auto& term : polynomial)
{
value += term * std::pow(level, power);
power++;
}
return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo, upgradeName);
}
// Select the scale with the highest level that is less than or equal to the current level
for (const auto& s : scales) {
if ((s.GetLevel() <= level) && (s.GetLevel() > scale.GetLevel())) {
std::cout << "Found scale: " << s.GetMin() << " - " << s.GetMax() << " for level " << s.GetLevel() << std::endl;
scale = s;
found = true;
}
}
if (!found) {
return std::nullopt;
}
float value = 0;
if (scale.GetMax() == scale.GetMin())
{
value = scale.GetMin();
}
else
{
value = (GeneralUtils::GenerateRandomNumber<uint32_t>(0, 100) / 100.0f) * (scale.GetMax() - scale.GetMin()) + scale.GetMin();
}
std::cout << "Generated modifier: " << value << " with level " << level << " for type: " << magic_enum::enum_name(type) << std::endl;
return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo, upgradeName);
}

109
dGame/ModifierTemplate.h Normal file
View File

@@ -0,0 +1,109 @@
#pragma once
#include <cstdint>
#include <vector>
#include <string>
#include <optional>
#include "ModifierInstance.h"
#include "ModifierScale.h"
namespace nejlika
{
enum ModifierTemplateSelector : uint8_t
{
One,
All,
Two
};
class ModifierTemplate
{
public:
ModifierTemplate(
const std::vector<ModifierType>& types, ModifierTemplateSelector selector, ModifierCategory category, bool isResistance, uint32_t effectID, const std::string& effectType
) : types(types), selector(selector), category(category), isResistance(isResistance), effectID(effectID), effectType(effectType) {}
/**
* @brief Construct a new Modifier Template object from a json configuration.
*
* @param config The json configuration.
*/
ModifierTemplate(const nlohmann::json& config);
/**
* @brief Convert the modifier template to a json representation.
*
* @return The json representation.
*/
nlohmann::json ToJson() const;
std::vector<ModifierInstance> GenerateModifiers(int32_t level) const;
// Getters and setters
const std::vector<ModifierType>& GetTypes() const { return types; }
ModifierType GetConvertTo() const { return convertTo; }
ModifierTemplateSelector GetSelector() const { return selector; }
const std::vector<ModifierScale>& GetScales() const { return scales; }
ModifierCategory GetCategory() const { return category; }
bool IsResistance() const { return isResistance; }
uint32_t GetEffectID() const { return effectID; }
std::string GetEffectType() const { return effectType; }
const std::string& GetUpgradeName() const { return upgradeName; }
void SetTypes(const std::vector<ModifierType>& types) { this->types = types; }
void SetConvertTo(ModifierType convertTo) { this->convertTo = convertTo; }
void SetSelector(ModifierTemplateSelector selector) { this->selector = selector; }
void SetScales(const std::vector<ModifierScale>& scales) { this->scales = scales; }
void SetCategory(ModifierCategory category) { this->category = category; }
void SetIsResistance(bool isResistance) { this->isResistance = isResistance; }
void SetEffectID(uint32_t effectID) { this->effectID = effectID; }
void SetEffectType(const std::string& effectType) { this->effectType = effectType; }
void SetUpgradeName(const std::string& upgradeName) { this->upgradeName = upgradeName; }
/**
* @brief Generate a HTML string representation of a set of modifier templates.
*
* @param modifiers The modifier templates to generate the HTML string for.
* @param level The level of the modifier templates.
* @return The HTML string.
*/
static std::string GenerateHtmlString(const std::vector<ModifierTemplate>& modifiers, int32_t level);
private:
std::optional<ModifierInstance> GenerateModifier(ModifierType type, int32_t level) const;
std::vector<ModifierType> types;
ModifierType convertTo;
ModifierTemplateSelector selector;
std::vector<ModifierScale> scales;
std::vector<float> polynomial;
ModifierCategory category;
ModifierOperator operatorType;
bool isResistance;
uint32_t effectID;
std::string effectType;
std::string upgradeName;
};
}

216
dGame/ModifierType.cpp Normal file
View File

@@ -0,0 +1,216 @@
#include "ModifierType.h"
#include <unordered_map>
#include <unordered_set>
using namespace nejlika;
namespace
{
const std::unordered_map<ModifierType, std::string> colorMap = {
{ModifierType::Health, "#750000"},
{ModifierType::Armor, "#525252"},
{ModifierType::Imagination, "#0077FF"},
{ModifierType::Offensive, "#71583B"},
{ModifierType::Defensive, "#71583B"},
{ModifierType::Physical, "#666666"},
{ModifierType::Pierce, "#4f4f4f"},
{ModifierType::Vitality, "#e84646"},
{ModifierType::Fire, "#ff0000"},
{ModifierType::Cold, "#94d0f2"},
{ModifierType::Lightning, "#00a2ff"},
{ModifierType::Corruption, "#3d00ad"},
{ModifierType::Psychic, "#4b0161"},
{ModifierType::Acid, "#00ff00"},
};
const std::unordered_map<ModifierType, std::string> nameMap = {
{ModifierType::Health, "Health"},
{ModifierType::Armor, "Armor"},
{ModifierType::Imagination, "Imagination"},
{ModifierType::Offensive, "Offensive Ability"},
{ModifierType::Defensive, "Defensive Ability"},
{ModifierType::Physical, "Physical Damage"},
{ModifierType::Pierce, "Pierce Damage"},
{ModifierType::Vitality, "Vitality Damage"},
{ModifierType::Fire, "Fire Damage"},
{ModifierType::Cold, "Cold Damage"},
{ModifierType::Lightning, "Lightning Damage"},
{ModifierType::Corruption, "Corruption Damage"},
{ModifierType::Psychic, "Psychic Damage"},
{ModifierType::Acid, "Acid Damage"},
{ModifierType::InternalDisassembly, "Internal Disassembly"},
{ModifierType::InternalDisassemblyDuration, "Internal Disassembly Duration"},
{ModifierType::Burn, "Burn"},
{ModifierType::BurnDuration, "Burn Duration"},
{ModifierType::Frostburn, "Frostburn"},
{ModifierType::FrostburnDuration, "Frostburn Duration"},
{ModifierType::Poison, "Poison"},
{ModifierType::PoisonDuration, "Poison Duration"},
{ModifierType::Electrocute, "Electrocute"},
{ModifierType::ElectrocuteDuration, "Electrocute Duration"},
{ModifierType::VitalityDecay, "Vitality Decay"},
{ModifierType::VitalityDecayDuration, "Vitality Decay Duration"},
{ModifierType::Seperation, "Seperation"},
{ModifierType::SeperationDuration, "Seperation Duration"},
{ModifierType::Elemental, "Elemental Damage"},
{ModifierType::Damage, "Damage"},
{ModifierType::Speed, "Speed"},
{ModifierType::AttackSpeed, "Attack Speed"},
{ModifierType::BlockRecovery, "Block Recovery Speed"},
{ModifierType::BlockChance, "Chance to Block"},
{ModifierType::Block, "Damage to Block"},
{ModifierType::CriticalDamage, "Critical-hit Damage"},
{ModifierType::HealthDrain, "Damage converted to Health"},
{ModifierType::ArmorPiercing, "Armor Piercing"},
{ModifierType::ReducedStunDuration, "Reduced Stun Duration"},
{ModifierType::SkillCooldownReduction, "Skill Cooldown Reduction"},
{ModifierType::SkillRecharge, "Skill Recharge Time"},
{ModifierType::Slow, "Slow"},
{ModifierType::Physique, "Physique"},
{ModifierType::Cunning, "Cunning"},
{ModifierType::Spirit, "Imagination"},
{ModifierType::AttacksPerSecond, "Attacks per Second"},
{ModifierType::ImaginationCost, "Imagination Cost"},
{ModifierType::MainWeaponDamage, "Main Weapon Damage"},
{ModifierType::Stun, "Target Stun Duration"}
};
const std::unordered_map<ModifierType, ModifierType> resistanceMap = {
{ModifierType::Physical, ModifierType::Physical},
{ModifierType::Pierce, ModifierType::Pierce},
{ModifierType::Vitality, ModifierType::Vitality},
{ModifierType::Fire, ModifierType::Fire},
{ModifierType::Cold, ModifierType::Cold},
{ModifierType::Lightning, ModifierType::Lightning},
{ModifierType::Corruption, ModifierType::Corruption},
{ModifierType::Psychic, ModifierType::Psychic},
{ModifierType::Acid, ModifierType::Acid},
{ModifierType::InternalDisassembly, ModifierType::Physical},
{ModifierType::Burn, ModifierType::Fire},
{ModifierType::Frostburn, ModifierType::Cold},
{ModifierType::Poison, ModifierType::Acid},
{ModifierType::VitalityDecay, ModifierType::Vitality},
{ModifierType::Electrocute, ModifierType::Lightning},
{ModifierType::Seperation, ModifierType::Seperation}
};
const std::unordered_set<ModifierType> normalDamageTypes = {
ModifierType::Physical,
ModifierType::Pierce,
ModifierType::Vitality,
ModifierType::Fire,
ModifierType::Cold,
ModifierType::Lightning,
ModifierType::Corruption,
ModifierType::Psychic,
ModifierType::Acid
};
const std::unordered_map<ModifierType, ModifierType> durationMap = {
{ModifierType::InternalDisassembly, ModifierType::InternalDisassemblyDuration},
{ModifierType::Burn, ModifierType::BurnDuration},
{ModifierType::Frostburn, ModifierType::FrostburnDuration},
{ModifierType::Poison, ModifierType::PoisonDuration},
{ModifierType::VitalityDecay, ModifierType::VitalityDecayDuration},
{ModifierType::Electrocute, ModifierType::ElectrocuteDuration},
{ModifierType::Seperation, ModifierType::SeperationDuration}
};
const std::unordered_map<ModifierType, ModifierType> overTimeMap = {
{ModifierType::Physical, ModifierType::InternalDisassembly},
{ModifierType::Fire, ModifierType::Burn},
{ModifierType::Cold, ModifierType::Frostburn},
{ModifierType::Poison, ModifierType::Poison},
{ModifierType::Vitality, ModifierType::VitalityDecay},
{ModifierType::Lightning, ModifierType::Electrocute}
};
const std::unordered_set<ModifierType> isOverTimeMap = {
ModifierType::InternalDisassembly,
ModifierType::Burn,
ModifierType::Frostburn,
ModifierType::Poison,
ModifierType::VitalityDecay,
ModifierType::Electrocute,
ModifierType::Seperation
};
const std::unordered_set<ModifierType> isDurationType = {
ModifierType::InternalDisassemblyDuration,
ModifierType::BurnDuration,
ModifierType::FrostburnDuration,
ModifierType::PoisonDuration,
ModifierType::VitalityDecayDuration,
ModifierType::ElectrocuteDuration,
ModifierType::SeperationDuration
};
}
const std::string& nejlika::GetModifierTypeColor(ModifierType type)
{
const auto color = colorMap.find(type);
if (color != colorMap.end()) {
return color->second;
}
static const std::string white = "#FFFFFF";
return white;
}
const std::string& nejlika::GetModifierTypeName(ModifierType type) {
const auto name = nameMap.find(type);
if (name != nameMap.end()) {
return name->second;
}
static const std::string invalid = "Invalid";
return invalid;
}
const ModifierType nejlika::GetResistanceType(ModifierType type) {
const auto resistance = resistanceMap.find(type);
if (resistance != resistanceMap.end()) {
return resistance->second;
}
return ModifierType::Invalid;
}
const bool nejlika::IsNormalDamageType(ModifierType type) {
return normalDamageTypes.find(type) != normalDamageTypes.end();
}
const ModifierType nejlika::GetOverTimeType(ModifierType type) {
const auto overTime = overTimeMap.find(type);
if (overTime != overTimeMap.end()) {
return overTime->second;
}
return ModifierType::Invalid;
}
const ModifierType nejlika::GetDurationType(ModifierType type) {
const auto duration = durationMap.find(type);
if (duration != durationMap.end()) {
return duration->second;
}
return ModifierType::Invalid;
}
const bool nejlika::IsOverTimeType(ModifierType type) {
return isOverTimeMap.find(type) != isOverTimeMap.end();
}
const bool nejlika::IsDurationType(ModifierType type) {
return isDurationType.find(type) != isDurationType.end();
}

96
dGame/ModifierType.h Normal file
View File

@@ -0,0 +1,96 @@
#pragma once
#include <cstdint>
#include <string>
namespace nejlika
{
enum class ModifierType : uint8_t
{
Health,
Armor,
Imagination,
Physique,
Cunning,
Spirit,
Offensive,
Defensive,
// Normal Types
Physical,
Fire,
Cold,
Lightning,
Acid,
Vitality,
Pierce,
Corruption, // Aether
Psychic, // Chaos
// Duration Types
InternalDisassembly, // Internal Trauma
InternalDisassemblyDuration,
Burn,
BurnDuration,
Frostburn,
FrostburnDuration,
Poison,
PoisonDuration,
VitalityDecay,
VitalityDecayDuration,
Electrocute,
ElectrocuteDuration,
Seperation, // Bleeding
SeperationDuration,
// Special
Elemental, // Even split between Fire, Cold, Lightning
Damage,
Speed,
AttackSpeed,
SkillModifier,
Slow,
ArmorPiercing,
ReducedStunDuration,
SkillCooldownReduction,
SkillRecharge,
BlockRecovery,
BlockChance,
Block,
HealthRegen,
ImaginationRegen,
AttacksPerSecond,
ImaginationCost,
MainWeaponDamage,
Stun,
CriticalDamage,
HealthDrain,
Invalid
};
const std::string& GetModifierTypeColor(ModifierType type);
const std::string& GetModifierTypeName(ModifierType type);
const ModifierType GetResistanceType(ModifierType type);
const bool IsNormalDamageType(ModifierType type);
const bool IsOverTimeType(ModifierType type);
const bool IsDurationType(ModifierType type);
const ModifierType GetOverTimeType(ModifierType type);
const ModifierType GetDurationType(ModifierType type);
}

215
dGame/NejlikaData.cpp Normal file
View File

@@ -0,0 +1,215 @@
#include "NejlikaData.h"
#include "Game.h"
#include "dConfig.h"
#include "json.hpp"
#include "Logger.h"
#include "Lookup.h"
#include <fstream>
using namespace nejlika;
namespace
{
std::unordered_map<nejlika::ModifierNameType, std::vector<nejlika::ModifierNameTemplate>> modifierNameTemplates;
std::unordered_map<LWOOBJID, nejlika::AdditionalItemData> additionalItemData;
std::unordered_map<LWOOBJID, nejlika::AdditionalEntityData> additionalEntityData;
std::unordered_map<LOT, nejlika::EntityTemplate> entityTemplates;
std::unordered_map<LOT, nejlika::UpgradeTemplate> upgradeTemplates;
nejlika::Lookup lookup;
}
const nejlika::Lookup& nejlika::NejlikaData::GetLookup()
{
return lookup;
}
const std::unordered_map<ModifierNameType,std::vector<ModifierNameTemplate>>& nejlika::NejlikaData::GetModifierNameTemplates()
{
return modifierNameTemplates;
}
const std::vector<nejlika::ModifierNameTemplate>& nejlika::NejlikaData::GetModifierNameTemplates(ModifierNameType type)
{
const auto it = modifierNameTemplates.find(type);
if (it != modifierNameTemplates.end()) {
return it->second;
}
static const std::vector<nejlika::ModifierNameTemplate> empty;
return empty;
}
const std::optional<AdditionalItemData*> nejlika::NejlikaData::GetAdditionalItemData(LWOOBJID id) {
const auto& it = additionalItemData.find(id);
if (it != additionalItemData.end()) {
return std::optional<AdditionalItemData*>(&it->second);
}
return std::nullopt;
}
const std::optional<AdditionalEntityData*> nejlika::NejlikaData::GetAdditionalEntityData(LWOOBJID id) {
const auto& it = additionalEntityData.find(id);
if (it != additionalEntityData.end()) {
return std::optional<AdditionalEntityData*>(&it->second);
}
return std::nullopt;
}
const std::optional<EntityTemplate*> nejlika::NejlikaData::GetEntityTemplate(LOT lot) {
const auto& it = entityTemplates.find(lot);
if (it != entityTemplates.end()) {
return std::optional<EntityTemplate*>(&it->second);
}
return std::nullopt;
}
const std::optional<UpgradeTemplate*> nejlika::NejlikaData::GetUpgradeTemplate(LOT lot) {
const auto& it = upgradeTemplates.find(lot);
if (it != upgradeTemplates.end()) {
return std::optional<UpgradeTemplate*>(&it->second);
}
return std::nullopt;
}
void nejlika::NejlikaData::SetAdditionalItemData(LWOOBJID id, AdditionalItemData data) {
additionalItemData[id] = data;
}
void nejlika::NejlikaData::SetAdditionalEntityData(LWOOBJID id, AdditionalEntityData data) {
additionalEntityData[id] = data;
}
void nejlika::NejlikaData::UnsetAdditionalItemData(LWOOBJID id) {
additionalItemData.erase(id);
}
void nejlika::NejlikaData::UnsetAdditionalEntityData(LWOOBJID id) {
additionalEntityData.erase(id);
}
void nejlika::NejlikaData::LoadNejlikaData()
{
const auto& lookupFile = Game::config->GetValue("lookup");
if (!lookupFile.empty())
{
lookup = Lookup(lookupFile);
}
modifierNameTemplates.clear();
// Load data from json file
const auto& directory_name = Game::config->GetValue("nejlika");
if (directory_name.empty())
{
return;
}
// Loop through all files in the directory
for (const auto& entry : std::filesystem::directory_iterator(directory_name))
{
if (!entry.is_regular_file())
{
continue;
}
// It has to end on .mod.json
const auto& path = entry.path().string();
if (path.size() < 9 || path.substr(path.size() - 9) != ".mod.json")
{
continue;
}
LoadNejlikaDataFile(entry.path().string());
}
}
void nejlika::NejlikaData::LoadNejlikaDataFile(const std::string& path) {
std::ifstream file(path);
if (!file.is_open())
{
LOG("Failed to open nejlika data file: %s", path.c_str());
return;
}
nlohmann::json json;
try
{
json = nlohmann::json::parse(file);
}
catch (const nlohmann::json::exception& e)
{
LOG("Failed to parse nejlika data file: %s", e.what());
return;
}
if (json.contains("modifier-templates"))
{
const auto& modifierTemplates = json["modifier-templates"];
for (const auto& value : modifierTemplates)
{
auto modifierTemplate = ModifierNameTemplate(value);
if (modifierTemplate.GetModifiers().empty())
{
continue;
}
modifierNameTemplates[modifierTemplate.GetType()].push_back(modifierTemplate);
}
}
LOG("Loaded %d modifier templates", modifierNameTemplates.size());
if (json.contains("entity-templates"))
{
const auto& entityTemplatesArray = json["entity-templates"];
for (const auto& value : entityTemplatesArray)
{
auto entityTemplate = EntityTemplate(value);
entityTemplates[entityTemplate.GetLOT()] = entityTemplate;
}
}
LOG("Loaded %d entity templates", entityTemplates.size());
if (json.contains("upgrade-templates"))
{
const auto& upgradeTemplatesArray = json["upgrade-templates"];
for (const auto& value : upgradeTemplatesArray)
{
auto upgradeTemplate = UpgradeTemplate(value);
upgradeTemplates[upgradeTemplate.GetLot()] = upgradeTemplate;
}
}
LOG("Loaded %d upgrade templates", upgradeTemplates.size());
}

42
dGame/NejlikaData.h Normal file
View File

@@ -0,0 +1,42 @@
#pragma once
#include <cstdint>
#include <vector>
#include "ModifierNameTemplate.h"
#include "EntityTemplate.h"
#include "AdditionalItemData.h"
#include "AdditionalEntityData.h"
#include "UpgradeTemplate.h"
#include "Lookup.h"
namespace nejlika::NejlikaData
{
const std::unordered_map<ModifierNameType, std::vector<ModifierNameTemplate>>& GetModifierNameTemplates();
const std::vector<ModifierNameTemplate>& GetModifierNameTemplates(ModifierNameType type);
const std::optional<AdditionalItemData*> GetAdditionalItemData(LWOOBJID id);
const std::optional<AdditionalEntityData*> GetAdditionalEntityData(LWOOBJID id);
const std::optional<EntityTemplate*> GetEntityTemplate(LOT lot);
const std::optional<UpgradeTemplate*> GetUpgradeTemplate(LOT lot);
void SetAdditionalItemData(LWOOBJID id, AdditionalItemData data);
void SetAdditionalEntityData(LWOOBJID id, AdditionalEntityData data);
void UnsetAdditionalItemData(LWOOBJID id);
void UnsetAdditionalEntityData(LWOOBJID id);
void LoadNejlikaData();
void LoadNejlikaDataFile(const std::string& path);
const Lookup& GetLookup();
}

33
dGame/NejlikaHelpers.cpp Normal file
View File

@@ -0,0 +1,33 @@
#include "NejlikaHelpers.h"
void nejlika::NejlikaHelpers::RenderDamageText(const std::string & text, Entity * attacker, Entity * damaged,
float scale, int32_t fontSize, int32_t colorR, int32_t colorG, int32_t colorB, int32_t colorA)
{
if (damaged == nullptr) {
return;
}
auto damagedPosition = damaged->GetPosition();
// Add a slight random offset to the damage position
damagedPosition.x += (rand() % 10 - 5) / 5.0f;
damagedPosition.y += (rand() % 10 - 5) / 5.0f;
damagedPosition.z += (rand() % 10 - 5) / 5.0f;
const auto& damageText = text;
std::stringstream damageUIMessage;
damageUIMessage << 0.0825 << ";" << 0.12 << ";" << damagedPosition.x << ";" << damagedPosition.y + 4.5f << ";" << damagedPosition.z << ";" << 0.1 << ";";
damageUIMessage << 200 * scale << ";" << 200 * scale << ";" << 0.5 << ";" << 1.0 << ";" << damageText << ";" << 4 << ";" << fontSize << ";" << colorR << ";" << colorG << ";" << colorB << ";";
damageUIMessage << colorA;
const auto damageUIStr = damageUIMessage.str();
if (damaged->IsPlayer()) {
damaged->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
} else if (attacker != nullptr && attacker->IsPlayer()) {
attacker->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
}
}

9
dGame/NejlikaHelpers.h Normal file
View File

@@ -0,0 +1,9 @@
#include <Entity.h>
namespace nejlika::NejlikaHelpers
{
void RenderDamageText(const std::string& text, Entity* attacker, Entity* damaged,
float scale = 1, int32_t fontSize = 4, int32_t colorR = 255, int32_t colorG = 255, int32_t colorB = 255, int32_t colorA = 0);
}

1048
dGame/NejlikaHooks.cpp Normal file

File diff suppressed because it is too large Load Diff

10
dGame/NejlikaHooks.h Normal file
View File

@@ -0,0 +1,10 @@
#pragma once
namespace nejlika::NejlikaHooks
{
void InstallHooks();
void ItemDescription(Entity* entity, const SystemAddress& sysAddr, const std::string args);
}

View File

@@ -18,6 +18,8 @@ void PlayerManager::AddPlayer(Entity* player) {
if (iter == m_Players.end()) {
m_Players.push_back(player);
OnPlayerAdded(player);
}
}
@@ -27,6 +29,8 @@ bool PlayerManager::RemovePlayer(Entity* player) {
const bool toReturn = iter != m_Players.end();
if (toReturn) {
m_Players.erase(iter);
OnPlayerRemoved(player);
}
return toReturn;

View File

@@ -2,6 +2,7 @@
#define __PLAYERMANAGER__H__
#include "dCommonVars.h"
#include "Observable.h"
#include <string>
@@ -20,6 +21,9 @@ namespace PlayerManager {
Entity* GetPlayer(LWOOBJID playerID);
const std::vector<Entity*>& GetAllPlayers();
static Observable<Entity*> OnPlayerAdded;
static Observable<Entity*> OnPlayerRemoved;
};
#endif //!__PLAYERMANAGER__H__

16
dGame/TriggerParameters.h Normal file
View File

@@ -0,0 +1,16 @@
#pragma once
#include <cstdint>
#include "BehaviorSlot.h"
namespace nejlika
{
class TriggerParameters
{
public:
int32_t SkillID = 0;
BehaviorSlot SelectedBehaviorSlot = BehaviorSlot::Invalid;
};
}

355
dGame/UpgradeEffect.cpp Normal file
View File

@@ -0,0 +1,355 @@
#include "UpgradeEffect.h"
#include "GeneralUtils.h"
#include "GameMessages.h"
#include "InventoryComponent.h"
#include <magic_enum.hpp>
#include <iostream>
using namespace nejlika;
nejlika::UpgradeEffect::UpgradeEffect(const nlohmann::json& json)
{
Load(json);
}
nlohmann::json nejlika::UpgradeEffect::ToJson() const
{
nlohmann::json json;
json["trigger-type"] = static_cast<int32_t>(triggerType);
nlohmann::json modifiersJson = nlohmann::json::array();
for (const auto& modifier : modifiers) {
modifiersJson.push_back(modifier.ToJson());
}
json["modifiers"] = modifiersJson;
if (!chance.empty()) {
nlohmann::json chanceJson = nlohmann::json::array();
for (const auto& scale : chance) {
chanceJson.push_back({
{"level", scale.level},
{"value", scale.value}
});
}
json["chance"] = chanceJson;
}
if (effectID != 0) {
json["effect-id"] = effectID;
}
if (!effectType.empty()) {
json["effect-type"] = effectType;
}
if (!conditions.empty()) {
nlohmann::json conditionsJson = nlohmann::json::array();
for (const auto& condition : conditions) {
conditionsJson.push_back(magic_enum::enum_name(condition));
}
json["conditions"] = conditionsJson;
}
if (equipSkillID != 0) {
json["grant-skill-id"] = equipSkillID;
}
return json;
}
std::vector<ModifierInstance> nejlika::UpgradeEffect::GenerateModifiers(int32_t level) const
{
std::vector<ModifierInstance> result;
for (const auto& modifier : modifiers) {
auto instances = modifier.GenerateModifiers(level);
result.insert(result.end(), instances.begin(), instances.end());
}
return result;
}
void nejlika::UpgradeEffect::Load(const nlohmann::json& json)
{
triggerType = magic_enum::enum_cast<UpgradeTriggerType>(json["trigger-type"].get<std::string>()).value_or(UpgradeTriggerType::OnHit);
modifiers.clear();
if (json.contains("modifiers")){
for (const auto& modifier : json["modifiers"]) {
ModifierTemplate effect(modifier);
modifiers.push_back(effect);
}
}
if (json.contains("chance")) {
chance.clear();
for (const auto& scale : json["chance"]) {
chance.push_back({
scale["level"].get<int32_t>(),
scale["value"].get<float>()
});
}
}
if (json.contains("effect-id")) {
effectID = json["effect-id"].get<int32_t>();
}
if (json.contains("effect-type")) {
effectType = json["effect-type"].get<std::string>();
}
if (json.contains("conditions")) {
conditions.clear();
for (const auto& condition : json["conditions"]) {
conditions.push_back(magic_enum::enum_cast<UpgradeTriggerCondition>(condition.get<std::string>()).value_or(UpgradeTriggerCondition::None));
}
}
if (json.contains("grant-skill-id")) {
equipSkillID = json["grant-skill-id"].get<int32_t>();
}
}
float nejlika::UpgradeEffect::CalculateChance(int32_t level) const {
if (chance.empty()) {
return 1;
}
// Find the highest level that is less than or equal to the given level
float value = 0;
for (const auto& scale : chance) {
if (scale.level <= level) {
value = scale.value;
}
}
return value;
}
bool nejlika::UpgradeEffect::CheckConditions(LWOOBJID origin, const TriggerParameters& params) const {
auto* entity = Game::entityManager->GetEntity(origin);
if (!entity) {
return false;
}
auto* inventory = entity->GetComponent<InventoryComponent>();
if (!inventory) {
return false;
}
const auto& skills = inventory->GetSkills();
const auto& equipped = inventory->GetEquippedItems();
for (const auto& condition : conditions) {
switch (condition) {
case UpgradeTriggerCondition::PrimaryAbility:
if (params.SelectedBehaviorSlot != BehaviorSlot::Primary) {
return false;
}
break;
case UpgradeTriggerCondition::UseSkill:
if (params.SkillID != equipSkillID) {
return false;
}
case UpgradeTriggerCondition::None:
break;
case UpgradeTriggerCondition::Unarmed:
if (equipped.contains("special_r")) {
return false;
}
break;
case UpgradeTriggerCondition::Melee:
if (!equipped.contains("special_r")) {
return false;
}
break;
case UpgradeTriggerCondition::TwoHanded:
{
if (!equipped.contains("special_r")) {
return false;
}
const auto& weaponLot = equipped.at("special_r").lot;
const auto& info = Inventory::FindItemComponent(weaponLot);
if (!info.isTwoHanded) {
return false;
}
break;
}
case UpgradeTriggerCondition::Shield:
if (!equipped.contains("special_l")) {
return false;
}
break;
default:
break;
}
}
return true;
}
void nejlika::UpgradeEffect::OnTrigger(LWOOBJID origin) const {
auto* entity = Game::entityManager->GetEntity(origin);
if (!entity) {
return;
}
auto* inventory = entity->GetComponent<InventoryComponent>();
if (!inventory) {
return;
}
}
std::vector<ModifierInstance> nejlika::UpgradeEffect::Trigger(const std::vector<UpgradeEffect>& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params) {
std::vector<ModifierInstance> result;
for (const auto& modifier : modifiers) {
if (modifier.GetTriggerType() != triggerType) {
continue;
}
if (!modifier.CheckConditions(origin, params)) {
continue;
}
float chanceRoll = GeneralUtils::GenerateRandomNumber<float>(0, 1);
if (chanceRoll > modifier.CalculateChance(level)) {
continue;
}
std::cout << "Triggering effect trigger type: " << magic_enum::enum_name(triggerType) << std::endl;
modifier.OnTrigger(origin);
auto instances = modifier.GenerateModifiers(level);
result.insert(result.end(), instances.begin(), instances.end());
if (modifier.effectID == 0) {
continue;
}
GameMessages::SendPlayFXEffect(
origin,
modifier.effectID,
GeneralUtils::UTF8ToUTF16(modifier.effectType),
std::to_string(GeneralUtils::GenerateRandomNumber<size_t>())
);
}
return result;
}
void nejlika::UpgradeEffect::AddSkill(LWOOBJID origin) const {
auto* entity = Game::entityManager->GetEntity(origin);
if (!entity) {
return;
}
auto* inventory = entity->GetComponent<InventoryComponent>();
if (!inventory) {
return;
}
if (triggerType != UpgradeTriggerType::Active) {
return;
}
if (equipSkillID != 0) {
inventory->AddSkill(equipSkillID);
}
}
void nejlika::UpgradeEffect::RemoveSkill(LWOOBJID origin) const {
auto* entity = Game::entityManager->GetEntity(origin);
if (!entity) {
return;
}
auto* inventory = entity->GetComponent<InventoryComponent>();
if (!inventory) {
return;
}
if (triggerType != UpgradeTriggerType::Active) {
return;
}
if (equipSkillID != 0) {
inventory->RemoveSkill(equipSkillID);
}
}
std::string nejlika::UpgradeEffect::GenerateHtmlString(const std::vector<UpgradeEffect>& effects, int32_t level) {
std::stringstream ss;
for (const auto& effect : effects) {
const auto chance = effect.CalculateChance(level);
for (const auto& condition : effect.conditions) {
if (condition == UpgradeTriggerCondition::None || condition == UpgradeTriggerCondition::UseSkill) {
continue;
}
ss << "<font color=\"#D0AB62\">";
switch (condition) {
case UpgradeTriggerCondition::PrimaryAbility:
ss << "On main-hand attack";
break;
case UpgradeTriggerCondition::Melee:
ss << "Requires melee weapon";
break;
case UpgradeTriggerCondition::TwoHanded:
ss << "Requires two-handed weapon";
break;
case UpgradeTriggerCondition::Shield:
ss << "Requires a shield";
break;
case UpgradeTriggerCondition::Unarmed:
ss << "Requires unarmed attack";
break;
}
ss << "</font>\n\n";
}
if (chance < 1.0f) {
ss << "<font color=\"#FFFFFF\">" << chance * 100 << "%</font><font color=\"#D0AB62\"> chance to trigger</font>\n";
}
std::cout << "Level " << level << " chance: " << chance << std::endl;
ss << ModifierInstance::GenerateHtmlString(effect.GenerateModifiers(level));
}
return ss.str();
}

69
dGame/UpgradeEffect.h Normal file
View File

@@ -0,0 +1,69 @@
#pragma once
#include "ModifierTemplate.h"
#include "UpgradeTriggerType.h"
#include "UpgradeTriggerCondition.h"
#include <InventoryComponent.h>
#include "TriggerParameters.h"
#include <dCommonVars.h>
namespace nejlika
{
class UpgradeEffect
{
public:
UpgradeEffect(const nlohmann::json& json);
nlohmann::json ToJson() const;
std::vector<ModifierInstance> GenerateModifiers(int32_t level) const;
void Load(const nlohmann::json& json);
float CalculateChance(int32_t level) const;
bool CheckConditions(LWOOBJID origin, const TriggerParameters& params) const;
void OnTrigger(LWOOBJID origin) const;
static std::vector<ModifierInstance> Trigger(const std::vector<UpgradeEffect>& modifiers, int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params);
// Getters
const std::vector<ModifierTemplate>& GetModifiers() const { return modifiers; }
UpgradeTriggerType GetTriggerType() const { return triggerType; }
void AddSkill(LWOOBJID origin) const;
void RemoveSkill(LWOOBJID origin) const;
/**
* @brief Generate a HTML string representation of a set of upgrade effects.
*
* @param modifiers The upgrade effects to generate the HTML string for.
* @param level The level of the upgrade effects.
* @return The HTML string.
*/
static std::string GenerateHtmlString(const std::vector<UpgradeEffect>& effects, int32_t level);
private:
struct UpgradeScale
{
int32_t level;
float value;
};
std::vector<UpgradeScale> chance;
std::vector<UpgradeTriggerCondition> conditions;
UpgradeTriggerType triggerType;
int32_t equipSkillID = 0;
std::vector<ModifierTemplate> modifiers;
int32_t effectID = 0;
std::string effectType = "";
std::vector<ModifierTemplate> debuffs;
std::vector<UpgradeScale> debuffDuration;
};
}

90
dGame/UpgradeTemplate.cpp Normal file
View File

@@ -0,0 +1,90 @@
#include "UpgradeTemplate.h"
#include "NejlikaData.h"
using namespace nejlika;
nejlika::UpgradeTemplate::UpgradeTemplate(const nlohmann::json& json)
{
Load(json);
}
nlohmann::json nejlika::UpgradeTemplate::ToJson() const
{
nlohmann::json json;
json["name"] = name;
json["lot"] = lot;
json["max-level"] = maxLevel;
nlohmann::json passivesJson = nlohmann::json::array();
for (const auto& passive : passives) {
passivesJson.push_back(passive.ToJson());
}
json["passives"] = passivesJson;
return json;
}
void nejlika::UpgradeTemplate::Load(const nlohmann::json& json)
{
name = json["name"].get<std::string>();
if (json["lot"].is_string()) {
lot = NejlikaData::GetLookup().GetValue(json["lot"].get<std::string>());
}
else {
lot = json["lot"].get<int32_t>();
}
maxLevel = json.contains("max-level") ? json["max-level"].get<int32_t>() : 1;
passives.clear();
if (json.contains("modifiers")) {
for (const auto& modifier : json["modifiers"]) {
ModifierTemplate modTemplate(modifier);
modifiers.push_back(modTemplate);
}
}
if (json.contains("passives")) {
for (const auto& passive : json["passives"]) {
UpgradeEffect effect(passive);
passives.push_back(effect);
}
}
}
std::vector<ModifierInstance> nejlika::UpgradeTemplate::Trigger(int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params) const {
level = std::min(level, maxLevel);
return UpgradeEffect::Trigger(passives, level, triggerType, origin, params);
}
std::vector<ModifierInstance> nejlika::UpgradeTemplate::GenerateModifiers(int32_t level) const {
level = std::min(level, maxLevel);
std::vector<ModifierInstance> result;
for (const auto& modifier : modifiers) {
auto instances = modifier.GenerateModifiers(level);
result.insert(result.end(), instances.begin(), instances.end());
}
return result;
}
void nejlika::UpgradeTemplate::AddSkills(LWOOBJID origin) const {
for (const auto& passive : passives) {
passive.AddSkill(origin);
}
}
void nejlika::UpgradeTemplate::RemoveSkills(LWOOBJID origin) const {
for (const auto& passive : passives) {
passive.RemoveSkill(origin);
}
}

48
dGame/UpgradeTemplate.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#include "json.hpp"
#include "UpgradeEffect.h"
#include "TriggerParameters.h"
namespace nejlika
{
class UpgradeTemplate
{
public:
UpgradeTemplate() = default;
UpgradeTemplate(const nlohmann::json& json);
nlohmann::json ToJson() const;
void Load(const nlohmann::json& json);
const std::string& GetName() const { return name; }
int32_t GetLot() const { return lot; }
int32_t GetMaxLevel() const { return maxLevel; }
const std::vector<UpgradeEffect>& GetPassives() const { return passives; }
const std::vector<ModifierTemplate>& GetModifiers() const { return modifiers; }
std::vector<ModifierInstance> Trigger(int32_t level, UpgradeTriggerType triggerType, LWOOBJID origin, const TriggerParameters& params) const;
std::vector<ModifierInstance> GenerateModifiers(int32_t level) const;
void AddSkills(LWOOBJID origin) const;
void RemoveSkills(LWOOBJID origin) const;
private:
std::string name = "";
int32_t lot = 0;
int32_t maxLevel = 0;
std::vector<UpgradeEffect> passives;
std::vector<ModifierTemplate> modifiers;
};
}

View File

@@ -0,0 +1,19 @@
#pragma once
#include <cstdint>
namespace nejlika
{
enum class UpgradeTriggerCondition
{
None,
Unarmed,
Melee,
TwoHanded,
Shield,
PrimaryAbility,
UseSkill
};
}

View File

@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
namespace nejlika
{
enum class UpgradeTriggerType
{
OnHit,
Equip,
UnEquip,
Active,
OnCast
};
}

View File

@@ -516,7 +516,7 @@ void UserManager::LoginCharacter(const SystemAddress& sysAddr, uint32_t playerID
Database::Get()->UpdateLastLoggedInCharacter(playerID);
uint32_t zoneID = character->GetZoneID();
if (zoneID == LWOZONEID_INVALID) zoneID = 1000; //Send char to VE
if (zoneID == LWOZONEID_INVALID) zoneID = 1100; //Send char to AG, skip VE
ZoneInstanceManager::Instance()->RequestZoneTransfer(Game::server, zoneID, character->GetZoneClone(), false, [=](bool mythranShift, uint32_t zoneID, uint32_t zoneInstance, uint32_t zoneClone, std::string serverIP, uint16_t serverPort) {
LOG("Transferring %s to Zone %i (Instance %i | Clone %i | Mythran Shift: %s) with IP %s and Port %i", character->GetName().c_str(), zoneID, zoneInstance, zoneClone, mythranShift == true ? "true" : "false", serverIP.c_str(), serverPort);

View File

@@ -24,7 +24,13 @@ void HealBehavior::Handle(BehaviorContext* context, RakNet::BitStream& bit_strea
return;
}
destroyable->Heal(this->m_health);
auto maxHealth = destroyable->GetMaxHealth();
// 1 health is 5% of the max health, minimum of 5 health
auto health = static_cast<int32_t>(maxHealth * 0.05f) * this->m_health;
health = std::max(5u, health);
destroyable->Heal(health);
}

View File

@@ -36,6 +36,7 @@ BaseCombatAIComponent::BaseCombatAIComponent(Entity* parent, const uint32_t id):
m_Disabled = false;
m_SkillEntries = {};
m_SoftTimer = 5.0f;
m_StunImmune = true;
//Grab the aggro information from BaseCombatAI:
auto componentQuery = CDClientDatabase::CreatePreppedStmt(
@@ -369,9 +370,9 @@ void BaseCombatAIComponent::CalculateCombat(const float deltaTime) {
m_Timer = 0;
m_SkillTime = result.skillTime;
m_SkillTime = result.skillTime / 2.0f;
entry.cooldown = entry.abilityCooldown + m_SkillTime;
entry.cooldown = 0.1f; //entry.abilityCooldown + m_SkillTime;
m_SkillEntries[i] = entry;
@@ -619,6 +620,10 @@ void BaseCombatAIComponent::SetThreat(LWOOBJID offender, float threat) {
m_DirtyThreat = true;
}
const std::map<LWOOBJID, float>& BaseCombatAIComponent::GetThreats() const {
return m_ThreatEntries;
}
const NiPoint3& BaseCombatAIComponent::GetStartPosition() const {
return m_StartPosition;
}
@@ -679,7 +684,7 @@ void BaseCombatAIComponent::OnAggro() {
return;
}
m_MovementAI->SetHaltDistance(m_AttackRadius);
m_MovementAI->SetHaltDistance(0);
NiPoint3 targetPos = target->GetPosition();
NiPoint3 currentPos = m_MovementAI->GetParent()->GetPosition();
@@ -713,7 +718,7 @@ void BaseCombatAIComponent::OnTether() {
return;
}
m_MovementAI->SetHaltDistance(m_AttackRadius);
m_MovementAI->SetHaltDistance(0);
NiPoint3 targetPos = target->GetPosition();
NiPoint3 currentPos = m_MovementAI->ApproximateLocation();
@@ -762,8 +767,8 @@ void BaseCombatAIComponent::SetTetherSpeed(float value) {
m_TetherSpeed = value;
}
void BaseCombatAIComponent::Stun(const float time) {
if (m_StunImmune || m_StunTime > time) {
void BaseCombatAIComponent::Stun(const float time, const bool ignoreImmunity) {
if ((!ignoreImmunity && m_StunImmune) || m_StunTime > time) {
return;
}

View File

@@ -114,6 +114,12 @@ public:
*/
void SetThreat(LWOOBJID offender, float threat);
/**
* Get all threats for this entity
* @return all threats for this entity
*/
const std::map<LWOOBJID, float>& GetThreats() const;
/**
* Gets the position where the entity spawned
* @return the position where the entity spawned
@@ -181,8 +187,9 @@ public:
/**
* Stuns the entity for a certain amount of time, will not work if the entity is stun immune
* @param time the time to stun the entity, if stunnable
* @param ignoreImmunity whether to ignore the stun immunity of the entity
*/
void Stun(float time);
void Stun(float time, bool ignoreImmunity = false);
/**
* Gets the radius that will cause this entity to get aggro'd, causing a target chase

View File

@@ -38,6 +38,8 @@
#include "CDComponentsRegistryTable.h"
Observable<Entity*, LWOOBJID, uint32_t, uint32_t&> DestroyableComponent::OnDamageCalculation;
DestroyableComponent::DestroyableComponent(Entity* parent) : Component(parent) {
m_iArmor = 0;
m_fMaxArmor = 0.0f;
@@ -554,7 +556,7 @@ void DestroyableComponent::Repair(const uint32_t armor) {
}
void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32_t skillID, bool echo) {
void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32_t skillID, bool echo, bool isDamageOverTime) {
if (GetHealth() <= 0) {
return;
}
@@ -570,6 +572,10 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32
return;
}
if (!isDamageOverTime) {
OnDamageCalculation(m_Parent, source, skillID, damage);
}
// If this entity has damage reduction, reduce the damage to a minimum of 1
if (m_DamageReduction > 0 && damage > 0) {
if (damage > m_DamageReduction) {
@@ -637,6 +643,42 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32
cb(attacker);
}
std::stringstream damageUIMessage;
auto damagedPosition = m_Parent->GetPosition();
// Add a slight random offset to the damage position
damagedPosition.x += (rand() % 10 - 5) / 5.0f;
damagedPosition.y += (rand() % 10 - 5) / 5.0f;
damagedPosition.z += (rand() % 10 - 5) / 5.0f;
int colorR = 255;
int colorG = 255;
int colorB = 255;
int colorA = 0;
if (m_Parent->IsPlayer()) {
// Make the damage red
colorR = 0;
colorG = 255;
colorB = 0;
colorA = 0;
}
const auto damageText = damage != 0 ? std::to_string(damage) : "Miss";
damageUIMessage << 0.0825 << ";" << 0.12 << ";" << damagedPosition.x << ";" << damagedPosition.y + 4.5f << ";" << damagedPosition.z << ";" << 0.1 << ";";
damageUIMessage << 200 << ";" << 200 << ";" << 0.5 << ";" << 1.0 << ";" << damageText << ";" << 4 << ";" << 4 << ";" << colorR << ";" << colorG << ";" << colorB << ";";
damageUIMessage << colorA;
const auto damageUIStr = damageUIMessage.str();
if (m_Parent->IsPlayer()) {
m_Parent->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
} else if (attacker != nullptr && attacker->IsPlayer()) {
attacker->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
}
if (health != 0) {
auto* combatComponent = m_Parent->GetComponent<BaseCombatAIComponent>();

View File

@@ -7,6 +7,7 @@
#include "Entity.h"
#include "Component.h"
#include "eReplicaComponentType.h"
#include "Observable.h"
namespace CppScripts {
class Script;
@@ -383,8 +384,9 @@ public:
* @param source the attacker that caused this damage
* @param skillID the skill that damaged this entity
* @param echo whether or not to serialize the damage
* @param isDamageOverTime whether or not this damage is over time
*/
void Damage(uint32_t damage, LWOOBJID source, uint32_t skillID = 0, bool echo = true);
void Damage(uint32_t damage, LWOOBJID source, uint32_t skillID = 0, bool echo = true, bool isDamageOverTime = false);
/**
* Smashes this entity, notifying all clients
@@ -463,6 +465,9 @@ public:
// handle hardcode mode drops
void DoHardcoreModeDrops(const LWOOBJID source);
// Damaged entity, offender, skillID, damage
static Observable<Entity*, LWOOBJID, uint32_t, uint32_t&> OnDamageCalculation;
private:
/**
* Whether or not the health should be serialized

View File

@@ -1,6 +1,7 @@
#include "InventoryComponent.h"
#include <sstream>
#include <algorithm>
#include "Entity.h"
#include "Item.h"
@@ -38,6 +39,13 @@
#include "CDObjectSkillsTable.h"
#include "CDSkillBehaviorTable.h"
Observable<InventoryComponent*, Item*> InventoryComponent::OnItemCreated;
Observable<InventoryComponent*, Item*> InventoryComponent::OnItemDestroyed;
Observable<InventoryComponent*, Item*> InventoryComponent::OnItemEquipped;
Observable<InventoryComponent*, Item*> InventoryComponent::OnItemUnequipped;
Observable<InventoryComponent*, Item*> InventoryComponent::OnItemLoaded;
Observable<InventoryComponent*, Item*> InventoryComponent::OnCountChanged;
InventoryComponent::InventoryComponent(Entity* parent) : Component(parent) {
this->m_Dirty = true;
this->m_Equipped = {};
@@ -287,6 +295,8 @@ void InventoryComponent::AddItem(
}
auto* item = new Item(lot, inventory, slot, size, {}, parent, showFlyingLoot, isModMoveAndEquip, subKey, false, lootSourceType);
OnItemCreated(this, item);
isModMoveAndEquip = false;
}
@@ -571,6 +581,8 @@ void InventoryComponent::LoadXml(const tinyxml2::XMLDocument& document) {
}
itemElement = itemElement->NextSiblingElement();
OnItemLoaded(this, item);
}
bag = bag->NextSiblingElement();
@@ -849,6 +861,8 @@ void InventoryComponent::EquipItem(Item* item, const bool skipChecks) {
EquipScripts(item);
OnItemEquipped(this, item);
Game::entityManager->SerializeEntity(m_Parent);
}
@@ -879,6 +893,8 @@ void InventoryComponent::UnEquipItem(Item* item) {
UnequipScripts(item);
OnItemUnequipped(this, item);
Game::entityManager->SerializeEntity(m_Parent);
// Trigger property event
@@ -1113,15 +1129,17 @@ void InventoryComponent::AddItemSkills(const LOT lot) {
const auto slot = FindBehaviorSlot(static_cast<eItemType>(info.itemType));
if (slot == BehaviorSlot::Invalid) {
if (slot != BehaviorSlot::Primary) {
return;
}
const auto index = m_Skills.find(slot);
if (m_PrimarySkill != 0) {
GameMessages::SendRemoveSkill(m_Parent, m_PrimarySkill);
}
m_PrimarySkill = FindSkill(lot);
const auto skill = FindSkill(lot);
SetSkill(slot, skill);
GameMessages::SendAddSkill(m_Parent, m_PrimarySkill, slot);
}
void InventoryComponent::RemoveItemSkills(const LOT lot) {
@@ -1129,27 +1147,15 @@ void InventoryComponent::RemoveItemSkills(const LOT lot) {
const auto slot = FindBehaviorSlot(static_cast<eItemType>(info.itemType));
if (slot == BehaviorSlot::Invalid) {
if (slot != BehaviorSlot::Primary) {
return;
}
const auto index = m_Skills.find(slot);
GameMessages::SendRemoveSkill(m_Parent, m_PrimarySkill);
if (index == m_Skills.end()) {
return;
}
m_PrimarySkill = 1;
const auto old = index->second;
GameMessages::SendRemoveSkill(m_Parent, old);
m_Skills.erase(slot);
if (slot == BehaviorSlot::Primary) {
m_Skills.insert_or_assign(BehaviorSlot::Primary, 1);
GameMessages::SendAddSkill(m_Parent, 1, BehaviorSlot::Primary);
}
GameMessages::SendAddSkill(m_Parent, m_PrimarySkill, BehaviorSlot::Primary);
}
void InventoryComponent::TriggerPassiveAbility(PassiveAbilityTrigger trigger, Entity* target) {
@@ -1326,6 +1332,71 @@ void InventoryComponent::SetNPCItems(const std::vector<LOT>& items) {
Game::entityManager->SerializeEntity(m_Parent);
}
bool InventoryComponent::AddSkill(uint32_t skillId) {
if (std::find(m_Skills.begin(), m_Skills.end(), skillId) != m_Skills.end()) {
return false;
}
m_Skills.push_back(skillId);
UpdateSkills();
return true;
}
bool InventoryComponent::RemoveSkill(uint32_t skillId) {
const auto index = std::find(m_Skills.begin(), m_Skills.end(), skillId);
if (index == m_Skills.end()) {
return false;
}
m_Skills.erase(index);
UpdateSkills();
return true;
}
void InventoryComponent::UpdateSkills() {
// There are two skills active at the same time. If the rotation index is greater than the amount of skills / 2, set it to the max number is can be.
// This is to prevent the rotation index from going out of bounds.
if (m_SkillRotationIndex * 2 >= m_Skills.size()) {
m_SkillRotationIndex = 0;
}
const auto startIndex = m_SkillRotationIndex * 2;
const auto activeSkillA = m_Skills.size() > startIndex ? m_Skills[startIndex] : 0;
const auto activeSkillB = m_Skills.size() > startIndex + 1 ? m_Skills[startIndex + 1] : 0;
std::cout << "Skill rotation index: " << m_SkillRotationIndex << " Active skills: " << activeSkillA << " " << activeSkillB << "\n";
GameMessages::SendRemoveSkill(m_Parent, m_ActiveSkills.first);
GameMessages::SendRemoveSkill(m_Parent, m_ActiveSkills.second);
m_ActiveSkills = { activeSkillA, activeSkillB };
if (activeSkillA != 0) {
GameMessages::SendAddSkill(m_Parent, activeSkillA, BehaviorSlot::Head);
}
if (activeSkillB != 0) {
GameMessages::SendAddSkill(m_Parent, activeSkillB, BehaviorSlot::Offhand);
}
}
void InventoryComponent::RotateSkills() {
m_SkillRotationIndex++;
if (m_SkillRotationIndex * 2 >= m_Skills.size()) {
m_SkillRotationIndex = 0;
}
UpdateSkills();
}
InventoryComponent::~InventoryComponent() {
for (const auto& inventory : m_Inventories) {
delete inventory.second;
@@ -1580,26 +1651,4 @@ void InventoryComponent::UpdatePetXml(tinyxml2::XMLDocument& document) {
}
bool InventoryComponent::SetSkill(int32_t slot, uint32_t skillId) {
BehaviorSlot behaviorSlot = BehaviorSlot::Invalid;
if (slot == 1) behaviorSlot = BehaviorSlot::Primary;
else if (slot == 2) behaviorSlot = BehaviorSlot::Offhand;
else if (slot == 3) behaviorSlot = BehaviorSlot::Neck;
else if (slot == 4) behaviorSlot = BehaviorSlot::Head;
else if (slot == 5) behaviorSlot = BehaviorSlot::Consumable;
else return false;
return SetSkill(behaviorSlot, skillId);
}
bool InventoryComponent::SetSkill(BehaviorSlot slot, uint32_t skillId) {
if (skillId == 0) return false;
const auto index = m_Skills.find(slot);
if (index != m_Skills.end()) {
const auto old = index->second;
GameMessages::SendRemoveSkill(m_Parent, old);
}
GameMessages::SendAddSkill(m_Parent, skillId, slot);
m_Skills.insert_or_assign(slot, skillId);
return true;
}

View File

@@ -22,6 +22,7 @@
#include "eInventoryType.h"
#include "eReplicaComponentType.h"
#include "eLootSourceType.h"
#include "Observable.h"
class Entity;
class ItemSet;
@@ -367,23 +368,42 @@ public:
*/
void UnequipScripts(Item* unequippedItem);
std::map<BehaviorSlot, uint32_t> GetSkills(){ return m_Skills; };
uint32_t GetPrimarySkill() const { return m_PrimarySkill; }
bool SetSkill(int slot, uint32_t skillId);
bool SetSkill(BehaviorSlot slot, uint32_t skillId);
const std::vector<uint32_t>& GetSkills(){ return m_Skills; };
bool AddSkill(uint32_t skillId);
bool RemoveSkill(uint32_t skillId);
void RotateSkills();
void UpdateSkills();
~InventoryComponent() override;
static Observable<InventoryComponent*, Item*> OnItemCreated;
static Observable<InventoryComponent*, Item*> OnItemDestroyed;
static Observable<InventoryComponent*, Item*> OnItemEquipped;
static Observable<InventoryComponent*, Item*> OnItemUnequipped;
static Observable<InventoryComponent*, Item*> OnItemLoaded;
static Observable<InventoryComponent*, Item*> OnCountChanged;
private:
/**
* All the inventory this entity possesses
*/
std::map<eInventoryType, Inventory*> m_Inventories;
std::map<BehaviorSlot, uint32_t> m_ActivatorSkills;
uint32_t m_PrimarySkill = 0;
/**
* The skills that this entity currently has active
*/
std::map<BehaviorSlot, uint32_t> m_Skills;
std::vector<uint32_t> m_Skills;
std::pair<uint32_t, uint32_t> m_ActiveSkills;
uint32_t m_SkillRotationIndex = 0;
/**
* The pets this entity has, mapped by object ID and pet info

View File

@@ -6,6 +6,8 @@
#include "CDRewardsTable.h"
Observable<LevelProgressionComponent*> LevelProgressionComponent::OnLevelUp;
LevelProgressionComponent::LevelProgressionComponent(Entity* parent) : Component(parent) {
m_Parent = parent;
m_Level = 1;
@@ -80,6 +82,8 @@ void LevelProgressionComponent::HandleLevelUp() {
}
// Tell the client we have finished sending level rewards.
if (rewardingItem) GameMessages::NotifyLevelRewards(m_Parent->GetObjectID(), m_Parent->GetSystemAddress(), m_Level, !rewardingItem);
OnLevelUp(this);
}
void LevelProgressionComponent::SetRetroactiveBaseSpeed(){

View File

@@ -5,6 +5,7 @@
#include "Component.h"
#include "eCharacterVersion.h"
#include "eReplicaComponentType.h"
#include "Observable.h"
/**
* Component that handles level progression and serilization.
@@ -81,6 +82,8 @@ public:
*/
void SetRetroactiveBaseSpeed();
static Observable<LevelProgressionComponent*> OnLevelUp;
private:
/**
* whether the level is dirty

View File

@@ -31,6 +31,8 @@ ProjectileSyncEntry::ProjectileSyncEntry() {
std::unordered_map<uint32_t, uint32_t> SkillComponent::m_skillBehaviorCache = {};
Observable<SkillComponent*, uint32_t, bool&, uint32_t> SkillComponent::OnSkillCast;
bool SkillComponent::CastPlayerSkill(const uint32_t behaviorId, const uint32_t skillUid, RakNet::BitStream& bitStream, const LWOOBJID target, uint32_t skillID) {
auto* context = new BehaviorContext(this->m_Parent->GetObjectID());
@@ -48,6 +50,12 @@ bool SkillComponent::CastPlayerSkill(const uint32_t behaviorId, const uint32_t s
context->ExecuteUpdates();
bool success = !context->failed;
OnSkillCast(this, skillID, success, skillUid);
context->failed = !success;
return !context->failed;
}

View File

@@ -14,6 +14,7 @@
#include "Entity.h"
#include "Logger.h"
#include "eReplicaComponentType.h"
#include "Observable.h"
struct ProjectileSyncEntry {
LWOOBJID id = LWOOBJID_EMPTY;
@@ -184,6 +185,9 @@ public:
*/
uint32_t GetUniqueSkillId();
// SkillComponent, SkillID, Success, skillUID
static Observable<SkillComponent*, uint32_t, bool&, uint32_t> OnSkillCast;
private:
/**
* All of the active skills mapped by their unique ID.

View File

@@ -973,11 +973,11 @@ void GameMessages::SendResurrect(Entity* entity) {
if (levelComponent) {
int32_t healthToRestore = levelComponent->GetLevel() >= 45 ? 8 : 4;
if (healthToRestore > destroyableComponent->GetMaxHealth()) healthToRestore = destroyableComponent->GetMaxHealth();
destroyableComponent->SetHealth(healthToRestore);
destroyableComponent->SetHealth(destroyableComponent->GetMaxHealth());
int32_t imaginationToRestore = levelComponent->GetLevel() >= 45 ? 20 : 6;
if (imaginationToRestore > destroyableComponent->GetMaxImagination()) imaginationToRestore = destroyableComponent->GetMaxImagination();
destroyableComponent->SetImagination(imaginationToRestore);
destroyableComponent->SetImagination(destroyableComponent->GetMaxImagination());
}
}
});

View File

@@ -212,6 +212,8 @@ void Inventory::AddManagedItem(Item* item) {
items.insert_or_assign(id, item);
free--;
component->OnItemLoaded(component, item);
}
void Inventory::RemoveManagedItem(Item* item) {
@@ -226,6 +228,8 @@ void Inventory::RemoveManagedItem(Item* item) {
items.erase(id);
free++;
component->OnItemDestroyed(component, item);
}
eInventoryType Inventory::FindInventoryTypeForLot(const LOT lot) {

View File

@@ -41,6 +41,7 @@ namespace {
{ "userModelMod", "um" },
{ "userModelOpt", "uo" },
{ "reforgedLOT", "rl" },
{ "modifiers", "mod" }
};
}
@@ -200,6 +201,8 @@ void Item::SetCount(const uint32_t value, const bool silent, const bool disassem
count = value;
InventoryComponent::OnCountChanged(inventory->GetComponent(), this);
if (count == 0) {
RemoveFromInventory();
}

View File

@@ -1347,23 +1347,17 @@ namespace DEVGMCommands {
void SetSkillSlot(Entity* entity, const SystemAddress& sysAddr, const std::string args) {
const auto splitArgs = GeneralUtils::SplitString(args, ' ');
if (splitArgs.size() < 2) return;
if (splitArgs.size() < 1) return;
auto* const inventoryComponent = entity->GetComponent<InventoryComponent>();
if (inventoryComponent) {
const auto slot = GeneralUtils::TryParse<BehaviorSlot>(splitArgs[0]);
if (!slot) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting slot.");
const auto skillId = GeneralUtils::TryParse<uint32_t>(splitArgs[0]);
if (!skillId) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting skill.");
return;
} else {
const auto skillId = GeneralUtils::TryParse<uint32_t>(splitArgs[1]);
if (!skillId) {
ChatPackets::SendSystemMessage(sysAddr, u"Error getting skill.");
return;
} else {
if (inventoryComponent->SetSkill(slot.value(), skillId.value())) ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot successfully");
else ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot failed");
}
if (inventoryComponent->AddSkill(skillId.value())) ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot successfully");
else ChatPackets::SendSystemMessage(sysAddr, u"Set skill to slot failed");
}
}
}

24765
dGame/json.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@
#include "MessageIdentifiers.h"
#include "BitStream.h"
#include <string>
#include <algorithm>
enum class eConnectionType : uint16_t;

View File

@@ -81,6 +81,8 @@
#include "eLoginResponse.h"
#include "SlashCommandHandler.h"
#include "NejlikaHooks.h"
namespace Game {
Logger* logger = nullptr;
dServer* server = nullptr;
@@ -315,6 +317,8 @@ int main(int argc, char** argv) {
// Register slash commands if not in zone 0
if (zoneID != 0) SlashCommandHandler::Startup();
nejlika::NejlikaHooks::InstallHooks();
Game::logger->Flush(); // once immediately before the main loop
while (true) {
Metrics::StartMeasurement(MetricVariable::Frame);

View File

@@ -23,6 +23,10 @@ if(NOT WIN32)
target_include_directories(bcrypt PRIVATE "libbcrypt/include/bcrypt")
endif()
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU")
target_compile_definitions(bcrypt PRIVATE "_POSIX_C_SOURCE=200809L")
endif()
target_include_directories(bcrypt INTERFACE "libbcrypt/include")
target_include_directories(bcrypt PRIVATE "libbcrypt/src")