Updated how skills and upgrades work, more gui stuff

This commit is contained in:
wincent 2024-07-17 20:59:11 +02:00
parent 5e3312850c
commit 3178a702a7
27 changed files with 986 additions and 205 deletions

View File

@ -33,7 +33,7 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, Modifi
return total;
}
float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::vector<ModifierInstance>& additionalModifiers, ModifierOperator op, bool resistance) const {
float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, const std::vector<ModifierInstance>& additionalModifiers, ModifierOperator op, bool resistance) const {
float total = 0;
for (const auto& modifier : additionalModifiers) {
@ -71,8 +71,6 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, int32_
float multiplicative = CalculateModifier(type, ModifierOperator::Multiplicative, false);
std::cout << "Scaler: " << scaler << " Additive: " << additive << " Multiplicative: " << multiplicative << std::endl;
return (scaler + additive) * (1 + multiplicative / 100);
}
@ -80,7 +78,7 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type) const
return CalculateModifier(type, level);
}
float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::vector<ModifierInstance>& additionalModifiers, int32_t level) const
float nejlika::AdditionalEntityData::CalculateFinalModifier(ModifierType type, const std::vector<ModifierInstance>& additionalModifiers, int32_t level) const
{
const auto templateDataOpt = NejlikaData::GetEntityTemplate(lot);
@ -106,6 +104,42 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::v
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();
@ -118,8 +152,6 @@ float nejlika::AdditionalEntityData::CalculateModifier(ModifierType type, std::v
float total = (scaler + additive) * (1 + multiplicative / 100);
std::cout << "Scaler: " << scaler << " Additive: " << additive << " Multiplicative: " << multiplicative << " Total: " << total << std::endl;
return total;
}
@ -132,7 +164,7 @@ float nejlika::AdditionalEntityData::CalculateResistance(ModifierType type) cons
float nejlika::AdditionalEntityData::CalculateMultiplier(ModifierType type) const
{
return 1 + CalculateModifier(type, ModifierOperator::Multiplicative, false);
return 100 + CalculateModifier(type, ModifierOperator::Multiplicative, false);
}
std::vector<ModifierInstance> nejlika::AdditionalEntityData::TriggerUpgradeItems(UpgradeTriggerType triggerType, const TriggerParameters& params) {
@ -270,6 +302,53 @@ void nejlika::AdditionalEntityData::RemoveSkills(LOT lot) {
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();
@ -296,8 +375,40 @@ void nejlika::AdditionalEntityData::RollStandardModifiers(int32_t level) {
}
}
float nejlika::AdditionalEntityData::CalculateMultiplier(ModifierType type, std::vector<ModifierInstance>& additionalModifiers) const {
return 1 + CalculateModifier(type, additionalModifiers, ModifierOperator::Multiplicative, false);
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 {
@ -402,7 +513,15 @@ void nejlika::AdditionalEntityData::ApplyToEntity() {
const auto& itemModifiers = itemData.GetModifierInstances();
activeModifiers.insert(activeModifiers.end(), itemModifiers.begin(), itemModifiers.end());
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) {
@ -431,11 +550,11 @@ void nejlika::AdditionalEntityData::ApplyToEntity() {
}
}
destroyable->SetMaxHealth(static_cast<int32_t>(CalculateModifier(ModifierType::Health, level)));
destroyable->SetMaxArmor(static_cast<int32_t>(CalculateModifier(ModifierType::Armor, level)));
if (!entity->IsPlayer()) {
destroyable->SetMaxImagination(static_cast<int32_t>(CalculateModifier(ModifierType::Imagination, level)));
}
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;
@ -444,15 +563,17 @@ void nejlika::AdditionalEntityData::ApplyToEntity() {
destroyable->SetHealth(destroyable->GetMaxHealth());
destroyable->SetArmor(destroyable->GetMaxArmor());
if (!entity->IsPlayer()) {
//if (!entity->IsPlayer()) {
destroyable->SetImagination(destroyable->GetMaxImagination());
}
//}
if (entity->IsPlayer()) {
auto* controllablePhysicsComponent = entity->GetComponent<ControllablePhysicsComponent>();
if (controllablePhysicsComponent) controllablePhysicsComponent->SetSpeedMultiplier(CalculateMultiplier(ModifierType::Speed));
if (controllablePhysicsComponent) controllablePhysicsComponent->SetSpeedMultiplier(CalculateMultiplier(ModifierType::Speed) / 100.0f);
}
TriggerPassiveRegeneration();
initialized = true;
}

View File

@ -24,18 +24,18 @@ public:
float CalculateModifier(ModifierType type, ModifierOperator op, bool resistance) const;
float CalculateModifier(ModifierType type, std::vector<ModifierInstance>& additionalModifiers, 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 CalculateModifier(ModifierType type, std::vector<ModifierInstance>& additionalModifiers, int32_t level) 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 1.0.
* @brief Calculate the multiplier for a given modifier type. With a base value of 100 (%).
*
* @param type The modifier type.
* @return The multiplier.
@ -43,13 +43,13 @@ public:
float CalculateMultiplier(ModifierType type) const;
/**
* @brief Calculate the multiplier for a given modifier type. With a base value of 1.0.
* @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, std::vector<ModifierInstance>& additionalModifiers) const;
float CalculateMultiplier(ModifierType type, const std::vector<ModifierInstance>& additionalModifiers) const;
/**
* @brief Calculate damage conversation mapping.
@ -85,9 +85,15 @@ public:
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;

View File

@ -210,18 +210,31 @@ void nejlika::AdditionalItemData::RollModifiers(Item* item, int32_t level) {
const auto& itemTemplateVec = NejlikaData::GetModifierNameTemplates(ModifierNameType::Object);
const auto itemTemplateIt = std::find_if(itemTemplateVec.begin(), itemTemplateVec.end(), [item](const auto& it) {
return it.GetLOT() == static_cast<int32_t>(item->GetLot());
});
std::vector<const ModifierNameTemplate*> availableObjects;
if (itemTemplateIt != itemTemplateVec.end()) {
const auto& itemTemplate = *itemTemplateIt;
for (const auto& itemTemplate : itemTemplateVec) {
if (itemTemplate.GetMinLevel() > level || itemTemplate.GetMaxLevel() < level) {
continue;
}
const auto itemModifiers = itemTemplate.GenerateModifiers(level);
if (itemTemplate.GetLOT() != static_cast<int32_t>(item->GetLot())) {
continue;
}
modifierInstances.insert(modifierInstances.end(), itemModifiers.begin(), itemModifiers.end());
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

@ -22,6 +22,7 @@ set(DGAME_SOURCES "Character.cpp"
"UpgradeTemplate.cpp"
"UpgradeEffect.cpp"
"Lookup.cpp"
"NejlikaHelpers.cpp"
)
include_directories(

View File

@ -67,7 +67,8 @@ std::vector<nejlika::ModifierInstance> nejlika::EntityTemplate::GenerateModifier
ModifierCategory::Player,
0,
"",
ModifierType::Invalid
ModifierType::Invalid,
""
);
modifiers.push_back(modifier);

View File

@ -51,11 +51,26 @@ std::string nejlika::ModifierInstance::GenerateHtmlString(const std::vector<Modi
// 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;
}
@ -67,32 +82,89 @@ std::string nejlika::ModifierInstance::GenerateHtmlString(const std::vector<Modi
}
for (const auto& resistance : target.second) {
ss << "\n<font color=\"#D0AB62\">";
ss << ((resistance.first) ? "Resistances" : "Modifiers");
ss << ":</font>\n";
for (const auto& math : resistance.second) {
for (const auto& modifier : math.second) {
ss << "<font color=\"" << GetModifierTypeColor(modifier.first) << "\">";
ss << "<font color=\"#FFFFFF\">";
ss << magic_enum::enum_name<ModifierType>(modifier.first) << ": ";
ss << ((modifier.second > 0) ? "+" : "-");
ss << ((modifier.second > 0) ? (math.first == ModifierOperator::Multiplicative ? "+" : "") : "-");
ss << std::fixed << std::setprecision(0) << std::abs(modifier.second);
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();
}

View File

@ -16,8 +16,11 @@ class ModifierInstance
{
public:
ModifierInstance(
ModifierType type, float value, ModifierOperator op, bool isResistance, ModifierCategory category, uint32_t effectID, const std::string& effectType, ModifierType convertTo
) : type(type), value(value), op(op), isResistance(isResistance), category(category), effectID(effectID), effectType(effectType), convertTo(convertTo) {}
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.
@ -59,6 +62,8 @@ public:
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; }
@ -75,6 +80,8 @@ public:
void SetEffectType(const std::string& effectType) { this->effectType = effectType; }
void SetUpgradeName(const std::string& upgradeName) { this->upgradeName = upgradeName; }
private:
ModifierType type;
ModifierType convertTo;
@ -84,6 +91,7 @@ private:
ModifierCategory category;
uint32_t effectID;
std::string effectType;
std::string upgradeName;
};
}

View File

@ -57,7 +57,14 @@ nejlika::ModifierNameTemplate::ModifierNameTemplate(const nlohmann::json & json)
{
for (const auto& modifier : json["modifiers"])
{
modifiers.push_back(ModifierTemplate(modifier));
const auto modifierTemplate = ModifierTemplate(modifier);
if (modifierTemplate.GetTypes().empty() || modifierTemplate.GetTypes().at(0) == ModifierType::Invalid)
{
continue;
}
modifiers.push_back(modifierTemplate);
}
}
@ -68,6 +75,10 @@ nejlika::ModifierNameTemplate::ModifierNameTemplate(const nlohmann::json & json)
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
{

View File

@ -4,6 +4,10 @@ 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>();
}

View File

@ -3,6 +3,8 @@
#include <magic_enum.hpp>
#include <random>
#include <algorithm>
#include <sstream>
#include <iostream>
using namespace nejlika;
@ -133,6 +135,11 @@ nejlika::ModifierTemplate::ModifierTemplate(const nlohmann::json& config) {
category = ModifierCategory::Player;
}
}
if (config.contains("skill"))
{
upgradeName = config["skill"].get<std::string>();
}
}
nlohmann::json nejlika::ModifierTemplate::ToJson() const {
@ -191,6 +198,12 @@ nlohmann::json nejlika::ModifierTemplate::ToJson() const {
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;
}
@ -249,9 +262,24 @@ std::string nejlika::ModifierTemplate::GenerateHtmlString(const std::vector<Modi
// 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) {
if (type == ModifierType::Invalid || type == ModifierType::SkillModifier) {
continue;
}
@ -299,35 +327,37 @@ std::string nejlika::ModifierTemplate::GenerateHtmlString(const std::vector<Modi
}
for (const auto& resistance : target.second) {
ss << "\n<font color=\"#D0AB62\">";
ss << ((resistance.first) ? "Resistances" : "Modifiers");
ss << ":</font>\n";
for (const auto& math : resistance.second) {
for (const auto& modifier : math.second) {
ss << "<font color=\"" << GetModifierTypeColor(modifier.first) << "\">";
ss << magic_enum::enum_name<ModifierType>(modifier.first) << ": ";
ss << "<font color=\"#FFFFFF\">";
ss << ((modifier.second.first > 0) ? "+" : "-");
ss << ((modifier.second.first > 0) ? (math.first == ModifierOperator::Multiplicative ? "+" : "") : "-");
ss << std::fixed << std::setprecision(0) << std::abs(modifier.second.first);
ss << std::fixed << std::setprecision(1) << std::abs(modifier.second.first);
if (modifier.second.first != modifier.second.second)
{
ss << "/";
ss << ((modifier.second.second > 0) ? "+" : "-");
ss << std::fixed << std::setprecision(0) << std::abs(modifier.second.second);
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";
}
@ -335,6 +365,90 @@ std::string nejlika::ModifierTemplate::GenerateHtmlString(const std::vector<Modi
}
}
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();
}
@ -354,12 +468,13 @@ std::optional<ModifierInstance> nejlika::ModifierTemplate::GenerateModifier(Modi
power++;
}
return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo);
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()) {
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;
}
@ -369,7 +484,18 @@ std::optional<ModifierInstance> nejlika::ModifierTemplate::GenerateModifier(Modi
return std::nullopt;
}
float value = GeneralUtils::GenerateRandomNumber<float>(scale.GetMin(), scale.GetMax());
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);
return ModifierInstance(type, value, operatorType, isResistance, category, effectID, effectType, convertTo, upgradeName);
}

View File

@ -59,6 +59,8 @@ public:
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; }
@ -74,6 +76,8 @@ public:
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.
@ -98,6 +102,7 @@ private:
bool isResistance;
uint32_t effectID;
std::string effectType;
std::string upgradeName;
};

View File

@ -53,6 +53,27 @@ const std::unordered_map<ModifierType, std::string> nameMap = {
{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 = {
@ -115,6 +136,16 @@ const std::unordered_set<ModifierType> isOverTimeMap = {
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)
@ -179,3 +210,7 @@ const ModifierType nejlika::GetDurationType(ModifierType type) {
const bool nejlika::IsOverTimeType(ModifierType type) {
return isOverTimeMap.find(type) != isOverTimeMap.end();
}
const bool nejlika::IsDurationType(ModifierType type) {
return isDurationType.find(type) != isDurationType.end();
}

View File

@ -58,9 +58,20 @@ enum class ModifierType : uint8_t
Slow,
ArmorPiercing,
ReducedStunDuration,
SkillCooldownReduction,
SkillRecharge,
BlockRecovery,
BlockChance,
Block,
HealthRegen,
ImaginationRegen,
AttacksPerSecond,
ImaginationCost,
MainWeaponDamage,
Stun,
CriticalDamage,
ChanceToBlock,
HealthDrain,
Invalid
@ -76,6 +87,8 @@ 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);

View File

@ -117,18 +117,39 @@ void nejlika::NejlikaData::LoadNejlikaData()
modifierNameTemplates.clear();
// Load data from json file
const auto& filename = Game::config->GetValue("nejlika");
const auto& directory_name = Game::config->GetValue("nejlika");
if (filename.empty())
if (directory_name.empty())
{
return;
}
std::ifstream file(filename);
// 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", filename.c_str());
LOG("Failed to open nejlika data file: %s", path.c_str());
return;
}
@ -144,19 +165,21 @@ void nejlika::NejlikaData::LoadNejlikaData()
return;
}
if (!json.contains("modifier-templates"))
if (json.contains("modifier-templates"))
{
LOG("nejlika data file does not contain modifier-templates");
return;
}
const auto& modifierTemplates = json["modifier-templates"];
const auto& modifierTemplates = json["modifier-templates"];
for (const auto& value : modifierTemplates)
{
auto modifierTemplate = ModifierNameTemplate(value);
for (const auto& value : modifierTemplates)
{
auto modifierTemplate = ModifierNameTemplate(value);
if (modifierTemplate.GetModifiers().empty())
{
continue;
}
modifierNameTemplates[modifierTemplate.GetType()].push_back(modifierTemplate);
modifierNameTemplates[modifierTemplate.GetType()].push_back(modifierTemplate);
}
}
LOG("Loaded %d modifier templates", modifierNameTemplates.size());
@ -186,5 +209,7 @@ void nejlika::NejlikaData::LoadNejlikaData()
upgradeTemplates[upgradeTemplate.GetLot()] = upgradeTemplate;
}
}
LOG("Loaded %d upgrade templates", upgradeTemplates.size());
}

View File

@ -35,6 +35,8 @@ 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);
}

View File

@ -22,11 +22,16 @@
#include <MissionComponent.h>
#include <eMissionState.h>
#include "NejlikaHelpers.h"
#include "NejlikaData.h"
using namespace nejlika;
using namespace nejlika::NejlikaData;
namespace {
std::unordered_map<LWOOBJID, std::unordered_map<uint32_t, std::vector<ModifierInstance>>> uniqueSkillModifiers;
}
void nejlika::NejlikaHooks::InstallHooks() {
Command itemDescriptionCommand{
.help = "Special UI command, does nothing when used in chat.",
@ -37,26 +42,38 @@ void nejlika::NejlikaHooks::InstallHooks() {
};
SlashCommandHandler::RegisterCommand(itemDescriptionCommand);
Command modificationssCommand{
.help = "Displays active modifications.",
.info = "Displays active modifications.",
.aliases = {"stats"},
.handle = [](Entity* entity, const SystemAddress& sysAddr, const std::string args) {
auto entityDataOpt = GetAdditionalEntityData(entity->GetObjectID());
if (!entityDataOpt.has_value()) {
return;
}
auto& entityData = *entityDataOpt.value();
std::vector<ModifierInstance> modifiers = entityData.GetActiveModifiers();
std::stringstream ss;
ss << "Active modifications: " << std::endl;
ss << ModifierInstance::GenerateHtmlString(modifiers);
ChatPackets::SendSystemMessage(sysAddr, GeneralUtils::ASCIIToUTF16(ss.str()));
},
.requiredLevel = eGameMasterLevel::CIVILIAN
};
SlashCommandHandler::RegisterCommand(modificationssCommand);
LoadNejlikaData();
InventoryComponent::OnItemCreated += [](InventoryComponent* component, Item* item) {
const auto& itemType = static_cast<eItemType>(item->GetInfo().itemType);
/*
static const std::unordered_set<eItemType> listOfHandledItems {
eItemType::HAT,
eItemType::CHEST,
eItemType::LEGS,
eItemType::NECK,
eItemType::LEFT_HAND,
eItemType::RIGHT_HAND
};
if (listOfHandledItems.find(itemType) == listOfHandledItems.end()) {
return;
}
*/
// No to the Thinking Hat
if (item->GetLot() == 6086) {
return;
@ -133,11 +150,10 @@ void nejlika::NejlikaHooks::InstallHooks() {
auto& additionalData = *additionalDataOpt.value();
additionalData.ApplyToEntity();
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (!inventoryComponent) {
additionalData.ApplyToEntity();
return;
}
@ -158,6 +174,8 @@ void nejlika::NejlikaHooks::InstallHooks() {
}
}
additionalData.ApplyToEntity();
additionalData.InitializeSkills();
};
@ -166,8 +184,6 @@ void nejlika::NejlikaHooks::InstallHooks() {
return;
}
//GameMessages::SendAddSkill(entity, NejlikaData::GetLookup().GetValue("intro:skills:proxy:main"), BehaviorSlot::Head);
//GameMessages::SendAddSkill(entity, NejlikaData::GetLookup().GetValue("intro:skills:proxy:secondary"), BehaviorSlot::Offhand);
GameMessages::SendAddSkill(entity, NejlikaData::GetLookup().GetValue("intro:skills:proxy:tertiary"), BehaviorSlot::Neck);
auto* missionComponent = entity->GetComponent<MissionComponent>();
@ -300,7 +316,7 @@ void nejlika::NejlikaHooks::InstallHooks() {
entityData.ApplyToEntity();
};
SkillComponent::OnSkillCast += [](SkillComponent* skillComponent, uint32_t skillID, bool success) {
SkillComponent::OnSkillCast += [](SkillComponent* skillComponent, uint32_t skillID, bool& success, uint32_t skillUID) {
std::cout << "Skill cast: " << skillID << " - " << success << std::endl;
auto* inventoryComponent = skillComponent->GetParent()->GetComponent<InventoryComponent>();
@ -312,17 +328,155 @@ void nejlika::NejlikaHooks::InstallHooks() {
auto* entity = skillComponent->GetParent();
auto skills = inventoryComponent->GetSkills();
const auto primaryTrigger = NejlikaData::GetLookup().GetValue("intro:skills:proxy:main");
const auto secondaryTrigger = NejlikaData::GetLookup().GetValue("intro:skills:proxy:secondary");
const auto tertiaryTrigger = NejlikaData::GetLookup().GetValue("intro:skills:proxy:tertiary");
if (skillID == primaryTrigger || skillID == secondaryTrigger || skillID == tertiaryTrigger) {
} else {
if (skillID == tertiaryTrigger) {
inventoryComponent->RotateSkills();
}
const auto entityDataOpt = GetAdditionalEntityData(entity->GetObjectID());
if (!entityDataOpt.has_value()) {
return;
}
inventoryComponent->RotateSkills();
auto& entityData = *entityDataOpt.value();
const auto& skillTemplates = GetModifierNameTemplates(ModifierNameType::Skill);
const auto& skillTemplateIt = std::find_if(skillTemplates.begin(), skillTemplates.end(), [skillID](const auto& it) {
return it.GetLOT() == skillID;
});
std::vector<ModifierInstance> modifiers;
if (skillTemplateIt != skillTemplates.end()) {
const auto& skillTemplate = *skillTemplateIt;
const auto skillModifiers = skillTemplate.GenerateModifiers(entityData.GetLevel());
modifiers.insert(modifiers.end(), skillModifiers.begin(), skillModifiers.end());
}
LOT itemLot = 0;
LWOOBJID itemId = 0;
BehaviorSlot itemSlot = BehaviorSlot::Invalid;
if (inventoryComponent) {
const auto& equipped = inventoryComponent->GetEquippedItems();
// omg...
for (const auto& [equippedSlot, itemDetails] : equipped) {
std::cout << "Found equipped item: " << itemDetails.lot << std::endl;
const auto info = Inventory::FindItemComponent(itemDetails.lot);
const auto skill = InventoryComponent::FindSkill(itemDetails.lot);
if (skill != skillID) {
continue;
}
const auto itemBehaviorSlot = InventoryComponent::FindBehaviorSlot(static_cast<eItemType>(info.itemType));
itemLot = itemDetails.lot;
itemId = itemDetails.id;
itemSlot = itemBehaviorSlot;
std::cout << "Found item: " << itemLot << std::endl;
break;
}
}
TriggerParameters params;
params.SkillID = skillID;
params.SelectedBehaviorSlot = itemSlot;
// Upgrades
const auto upgradeModifiers = entityData.TriggerUpgradeItems(UpgradeTriggerType::OnCast, params);
auto unionModifiersUpgradeModifiers = modifiers;
unionModifiersUpgradeModifiers.insert(unionModifiersUpgradeModifiers.end(), upgradeModifiers.begin(), upgradeModifiers.end());
const auto& imaginationCost = entityData.CalculateFinalModifier(ModifierType::ImaginationCost, unionModifiersUpgradeModifiers, entityData.GetLevel());
auto* destroyable = entity->GetComponent<DestroyableComponent>();
if (destroyable != nullptr) {
if (destroyable->GetImagination() < imaginationCost) {
NejlikaHelpers::RenderDamageText("Insufficient imagination!", entity, entity, 4.0f);
}
else {
destroyable->SetImagination(destroyable->GetImagination() - imaginationCost);
Game::entityManager->SerializeEntity(entity);
// Insert into the unique skill modifiers
const auto& uniqueSkillModifiersIt = uniqueSkillModifiers.find(entity->GetObjectID());
if (uniqueSkillModifiersIt != uniqueSkillModifiers.end()) {
auto& uniqueSkillModifiersMap = uniqueSkillModifiersIt->second;
uniqueSkillModifiersMap[skillID] = upgradeModifiers;
}
else {
uniqueSkillModifiers[entity->GetObjectID()] = { { skillID, upgradeModifiers } };
}
entity->AddCallbackTimer(10.0f, [entity, skillID]() {
const auto& uniqueSkillModifiersIt = uniqueSkillModifiers.find(entity->GetObjectID());
if (uniqueSkillModifiersIt != uniqueSkillModifiers.end()) {
auto& uniqueSkillModifiersMap = uniqueSkillModifiersIt->second;
const auto& uniqueSkillModifiersSkillIt = uniqueSkillModifiersMap.find(skillID);
if (uniqueSkillModifiersSkillIt != uniqueSkillModifiersMap.end()) {
uniqueSkillModifiersMap.erase(uniqueSkillModifiersSkillIt);
}
}
});
modifiers.insert(modifiers.end(), upgradeModifiers.begin(), upgradeModifiers.end());
}
}
float attackSpeed = 0;
if (itemSlot == BehaviorSlot::Primary) {
auto attacksPerSecond = entityData.CalculateFinalModifier(ModifierType::AttacksPerSecond, modifiers, entityData.GetLevel());
attacksPerSecond = std::max(attacksPerSecond, 1.0f);
const auto attackSpeedMod = entityData.CalculateMultiplier(ModifierType::AttackSpeed, modifiers);
attackSpeed = (1.0f / attacksPerSecond) * ((attackSpeedMod / 100.0f)) / 2.0f; // 2.0f to account for animation times
LOG("Attack speed: %f, attacks per second: %f, attack speed mod: %f", attackSpeed, attacksPerSecond, attackSpeedMod);
}
else {
const auto skillRecharge = entityData.CalculateFinalModifier(ModifierType::SkillRecharge, modifiers, entityData.GetLevel());
const auto skillCooldownMod = entityData.CalculateMultiplier(ModifierType::SkillCooldownReduction, modifiers);
attackSpeed = skillRecharge * (skillCooldownMod / 100.0f);
LOG("Skill recharge: %f, skill cooldown mod: %f", skillRecharge, skillCooldownMod);
}
if (entity->IsPlayer()) {
entity->AddCallbackTimer(0.0f, [entity, skillID, attackSpeed]() {
CBITSTREAM;
CMSGHEADER;
const auto objectID = entity->GetObjectID();
bitStream.Write(objectID);
bitStream.Write(eGameMessageType::MODIFY_SKILL_COOLDOWN);
bitStream.Write1();
bitStream.Write<float>(attackSpeed);
bitStream.Write<int32_t>(static_cast<int32_t>(skillID));
LOG("Sending cooldown reduction for skill: %d: %f", skillID, attackSpeed);
SEND_PACKET_BROADCAST;
});
}
};
DestroyableComponent::OnDamageCalculation += [](Entity* damaged, LWOOBJID offender, uint32_t skillID, uint32_t& damage) {
@ -403,14 +557,24 @@ void nejlika::NejlikaHooks::InstallHooks() {
}
}
std::vector<ModifierInstance> modifiers;
if (itemSlot == BehaviorSlot::Primary) {
const auto mainWeaponDamage = offenderEntity.CalculateMainWeaponDamage();
for (const auto& modifier : mainWeaponDamage) {
std::cout << "Main weapon damage: " << magic_enum::enum_name(modifier.GetType()) << " - " << modifier.GetValue() << std::endl;
}
modifiers.insert(modifiers.end(), mainWeaponDamage.begin(), mainWeaponDamage.end());
}
const auto& skillTemplates = GetModifierNameTemplates(ModifierNameType::Skill);
const auto& skillTemplateIt = std::find_if(skillTemplates.begin(), skillTemplates.end(), [skillID](const auto& it) {
return it.GetLOT() == skillID;
});
std::vector<ModifierInstance> modifiers;
if (skillTemplateIt != skillTemplates.end()) {
const auto& skillTemplate = *skillTemplateIt;
@ -428,6 +592,20 @@ void nejlika::NejlikaHooks::InstallHooks() {
modifiers.insert(modifiers.end(), upgradeModifiers.begin(), upgradeModifiers.end());
const auto& uniqueSkillModifiersIt = uniqueSkillModifiers.find(offender);
if (uniqueSkillModifiersIt != uniqueSkillModifiers.end()) {
const auto& uniqueSkillModifiersMap = uniqueSkillModifiersIt->second;
const auto& uniqueSkillModifiersSkillIt = uniqueSkillModifiersMap.find(skillID);
if (uniqueSkillModifiersSkillIt != uniqueSkillModifiersMap.end()) {
const auto& uniqueSkillModifiersVec = uniqueSkillModifiersSkillIt->second;
modifiers.insert(modifiers.end(), uniqueSkillModifiersVec.begin(), uniqueSkillModifiersVec.end());
}
}
std::unordered_set<ModifierType> damageTypes;
for (const auto& modifier : modifiers) {
@ -444,14 +622,28 @@ void nejlika::NejlikaHooks::InstallHooks() {
}
}
uint32_t totalDamage = 0;
int32_t totalDamage = 0;
std::unordered_map<ModifierType, std::pair<float, float>> durationTypes;
std::unordered_map<ModifierType, float> tmpDamageValues;
const auto mainWeaponDamageModifier = (offenderEntity.CalculateMultiplier(ModifierType::MainWeaponDamage, modifiers) - 100.0f) / 100.0f;
if (mainWeaponDamageModifier > 0.0f) {
const auto mainWeaponDamage = offenderEntity.CalculateMainWeaponDamage();
for (auto modifier : mainWeaponDamage) {
const auto damageValue = modifier.GetValue();
modifier.SetValue(damageValue * mainWeaponDamageModifier);
modifiers.push_back(modifier);
}
}
for (const auto& type : damageTypes) {
if (nejlika::IsOverTimeType(type)) {
float damageValue = offenderEntity.CalculateModifier(type, modifiers, level);
float damageValue = offenderEntity.CalculateFinalModifier(type, modifiers, level);
// Calculate resistance, can't go below 20% of the original damage
const auto resistance = std::max(1 - (damagedEntity.CalculateResistance(type) / 100), 0.2f);
@ -460,7 +652,7 @@ void nejlika::NejlikaHooks::InstallHooks() {
const auto durationType = nejlika::GetDurationType(type);
const auto duration = offenderEntity.CalculateModifier(durationType, modifiers, level);
const auto duration = offenderEntity.CalculateFinalModifier(durationType, modifiers, level);
const auto durationResistance = std::max(1 - (damagedEntity.CalculateResistance(durationType) / 100), 0.2f);
@ -475,7 +667,7 @@ void nejlika::NejlikaHooks::InstallHooks() {
continue;
}
float damageValue = offenderEntity.CalculateModifier(type, modifiers, level);
float damageValue = offenderEntity.CalculateFinalModifier(type, modifiers, level);
tmpDamageValues[type] = damageValue;
}
@ -485,7 +677,7 @@ void nejlika::NejlikaHooks::InstallHooks() {
std::unordered_map<ModifierType, float> finalDamageValues;
for (const auto& [typeA, typeBMap] : converationMap) {
/*for (const auto& [typeA, typeBMap] : converationMap) {
const auto& typeAValue = tmpDamageValues.find(typeA);
if (typeAValue == tmpDamageValues.end()) {
@ -508,6 +700,13 @@ void nejlika::NejlikaHooks::InstallHooks() {
finalDamageValues[typeA] += typeAValueFloat - convertedValue;
finalDamageValues[typeB] += typeBValueFloat + convertedValue;
}
}*/
// Add the remaining values
for (const auto& [type, value] : tmpDamageValues) {
if (finalDamageValues.find(type) == finalDamageValues.end()) {
finalDamageValues[type] = value;
}
}
for (const auto& [type, damage] : finalDamageValues) {
@ -516,7 +715,7 @@ void nejlika::NejlikaHooks::InstallHooks() {
float reductedDamage = damage * resistance;
totalDamage += static_cast<uint32_t>(reductedDamage);
totalDamage += static_cast<int32_t>(reductedDamage);
std::cout << "Damage type: " << magic_enum::enum_name(type) << " - " << damage << std::endl;
std::cout << "Resistance: " << resistance << " - " << reductedDamage << std::endl;
@ -581,21 +780,25 @@ void nejlika::NejlikaHooks::InstallHooks() {
if (damageMultiplier > 1.0f) {
isCritical = true;
damageMultiplier *= offenderEntity.CalculateMultiplier(ModifierType::CriticalDamage, modifiers);
damageMultiplier *= (offenderEntity.CalculateMultiplier(ModifierType::CriticalDamage, modifiers) / 100.0f);
}
}
if (isHit) {
// Add a random +5% to the damage
totalDamage += static_cast<uint32_t>(totalDamage * (GeneralUtils::GenerateRandomNumber<int32_t>(0, 5) / 100.0f));
totalDamage += static_cast<int32_t>(totalDamage * (GeneralUtils::GenerateRandomNumber<int32_t>(0, 5) / 100.0f));
damage = totalDamage;
} else {
damage = totalDamage = 0;
}
if (totalDamage < 0 || damage < 0) {
totalDamage = damage = 0;
}
if (isCritical) {
totalDamage = static_cast<uint32_t>(totalDamage * damageMultiplier);
totalDamage = static_cast<int32_t>(totalDamage * damageMultiplier);
const auto effectName = std::to_string(GeneralUtils::GenerateRandomNumber<uint32_t>());
const auto damagedID = damaged->GetObjectID();
@ -616,26 +819,15 @@ void nejlika::NejlikaHooks::InstallHooks() {
});
}
auto attackSpeed = offenderEntity.CalculateModifier(ModifierType::AttackSpeed, modifiers, level);
if (damage == 0) {
return;
}
if (offfendEntity->IsPlayer()) {
offfendEntity->AddCallbackTimer(0.0f, [offfendEntity, skillID, attackSpeed]() {
CBITSTREAM;
CMSGHEADER;
const auto stunDuration = offenderEntity.CalculateFinalModifier(ModifierType::Stun, modifiers, level);
const auto entity = offfendEntity->GetObjectID();
bitStream.Write(entity);
bitStream.Write(eGameMessageType::MODIFY_SKILL_COOLDOWN);
bitStream.Write1();
bitStream.Write<float>(attackSpeed);
bitStream.Write<int32_t>(static_cast<int32_t>(skillID));
LOG("Sending cooldown reduction for skill: %d", skillID);
SEND_PACKET_BROADCAST;
});
if (baseCombatAIComponent && stunDuration > 0) {
LOG("Stunning for %f seconds", stunDuration);
baseCombatAIComponent->Stun(stunDuration, true);
}
// Apply over time damage.
@ -651,7 +843,7 @@ void nejlika::NejlikaHooks::InstallHooks() {
continue;
}
const auto damagePerTick = static_cast<int32_t>(damageDuration.first / duration);
const auto damagePerTick = static_cast<int32_t>(damageDuration.first);
auto* destroyable = damaged->GetComponent<DestroyableComponent>();
@ -661,54 +853,20 @@ void nejlika::NejlikaHooks::InstallHooks() {
for (size_t i = 0; i < duration; i++)
{
damaged->AddCallbackTimer(i * 0.5f, [offender, damaged, damagePerTick]() {
damaged->AddCallbackTimer(i * 0.5f, [type, offender, damaged, damagePerTick]() {
auto* offenderEntity = Game::entityManager->GetEntity(offender);
auto* destroyable = damaged->GetComponent<DestroyableComponent>();
if (!destroyable) {
return;
}
destroyable->Damage(offender, damagePerTick, 0, true, true);
destroyable->Damage(damagePerTick, offender, 0, true, true);
});
}
}
/* Moved to DestroyableComponent
std::stringstream damageUIMessage;
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;
int colorR = 255;
int colorG = 255;
int colorB = 255;
int colorA = 0;
if (damaged->IsPlayer()) {
// Make the damage red
colorR = 0;
colorG = 255;
colorB = 0;
colorA = 0;
}
const auto damageText = isHit ? std::to_string(totalDamage) : "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 (damaged->IsPlayer()) {
damaged->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
} else if (offfendEntity->IsPlayer()) {
offfendEntity->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
}*/
};
}
@ -744,34 +902,110 @@ void nejlika::NejlikaHooks::ItemDescription(Entity* entity, const SystemAddress&
std::stringstream name;
std::stringstream desc;
if (itemId == LWOOBJID_EMPTY) {
ChatPackets::SendSystemMessage(sysAddr, u"Invalid item ID.");
const auto& upgradeItemOpt = NejlikaData::GetUpgradeTemplate(lot);
if (upgradeItemOpt.has_value()) {
const auto& upgradeItem = *upgradeItemOpt.value();
auto* inventoryComponent = entity->GetComponent<InventoryComponent>();
if (!inventoryComponent) {
return;
}
auto amount = inventoryComponent->GetLotCount(lot);
if (itemId == LWOOBJID_EMPTY) {
amount++;
}
const auto& modifiers = upgradeItem.GenerateModifiers(amount);
name << "<font color=\"#D0AB62\">NAME</font>";
desc << "DESC" << "\n\n";
desc << "<font color=\"#D0AB62\">Level " << amount << "/" << upgradeItem.GetMaxLevel() << "</font>\n";
desc << ModifierInstance::GenerateHtmlString(modifiers);
const auto& passives = upgradeItem.GetPassives();
desc << UpgradeEffect::GenerateHtmlString(passives, amount);
} else if (itemId == LWOOBJID_EMPTY) {
const auto& itemTemplateVec = NejlikaData::GetModifierNameTemplates(ModifierNameType::Object);
auto* levelProgressionComponent = entity->GetComponent<LevelProgressionComponent>();
const auto itemTemplateIt = std::find_if(itemTemplateVec.begin(), itemTemplateVec.end(), [lot](const auto& it) {
return it.GetLOT() == static_cast<int32_t>(lot);
});
if (!levelProgressionComponent) {
return;
}
if (itemTemplateIt == itemTemplateVec.end()) {
name << "<font color=\"#D0AB62\">NAME</font>";
desc << "DESC";
} else {
const auto& itemTemplate = *itemTemplateIt;
std::vector<const nejlika::ModifierNameTemplate*> availableTemplates;
const nejlika::ModifierNameTemplate* lowestLevelTemplate = nullptr;
std::vector<const nejlika::ModifierNameTemplate*> lowestLevelTemplates;
const auto& modifiers = itemTemplate.GetModifiers();
for (const auto& itemTemplate : itemTemplateVec) {
auto level = static_cast<int32_t>(levelProgressionComponent->GetLevel());
// Get the entity level
auto* levelProgressionComponent = entity->GetComponent<LevelProgressionComponent>();
if (!levelProgressionComponent) {
return;
if (itemTemplate.GetLOT() != static_cast<int32_t>(lot)) {
continue;
}
auto level = levelProgressionComponent->GetLevel();
if (lowestLevelTemplate == nullptr) {
lowestLevelTemplate = &itemTemplate;
}
else if (lowestLevelTemplate->GetMinLevel() > level) {
lowestLevelTemplate = &itemTemplate;
lowestLevelTemplates.clear();
}
else if (lowestLevelTemplate->GetMinLevel() == level) {
lowestLevelTemplates.push_back(&itemTemplate);
}
if (itemTemplate.GetMinLevel() > level || itemTemplate.GetMaxLevel() < level) {
continue;
}
name << "<font color=\"#D0AB62\">NAME</font>";
desc << ModifierTemplate::GenerateHtmlString(modifiers, level);
availableTemplates.push_back(&itemTemplate);
}
// Add the lowest level template if no other template was found
if (availableTemplates.empty() && lowestLevelTemplate != nullptr) {
availableTemplates.push_back(lowestLevelTemplate);
// Add all templates with the same level
availableTemplates.insert(availableTemplates.end(), lowestLevelTemplates.begin(), lowestLevelTemplates.end());
}
name << "<font color=\"#D0AB62\">NAME</font>";
if (availableTemplates.empty()) {
desc << "DESC";
} else {
if (availableTemplates.size() > 1) {
desc << "<font color=\"#D0AB62\">One of:</font>\n";
}
for (size_t i = 0; i < availableTemplates.size(); i++) {
const auto& itemTemplate = *availableTemplates[i];
const auto& modifiers = itemTemplate.GetModifiers();
// Get the entity level
auto* levelProgressionComponent = entity->GetComponent<LevelProgressionComponent>();
if (!levelProgressionComponent) {
return;
}
auto level = std::max(static_cast<int32_t>(levelProgressionComponent->GetLevel()), itemTemplate.GetMinLevel());
desc << ModifierTemplate::GenerateHtmlString(modifiers, level);
if (i < availableTemplates.size() - 1) {
desc << "\n<font color=\"#D0AB62\">or</font>\n";
}
}
}
} else {
const auto itemDataOpt = GetAdditionalItemData(itemId);

View File

@ -306,3 +306,50 @@ void nejlika::UpgradeEffect::RemoveSkill(LWOOBJID origin) const {
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();
}

View File

@ -38,6 +38,15 @@ public:
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

View File

@ -10,7 +10,8 @@ enum class UpgradeTriggerType
OnHit,
Equip,
UnEquip,
Active
Active,
OnCast
};
}

View File

@ -767,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

@ -187,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

@ -675,7 +675,7 @@ void DestroyableComponent::Damage(uint32_t damage, const LWOOBJID source, uint32
if (m_Parent->IsPlayer()) {
m_Parent->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
} else if (attacker->IsPlayer()) {
} else if (attacker != nullptr && attacker->IsPlayer()) {
attacker->SetNetworkVar<std::string>(u"renderText", damageUIStr, UNASSIGNED_SYSTEM_ADDRESS);
}

View File

@ -31,7 +31,7 @@ ProjectileSyncEntry::ProjectileSyncEntry() {
std::unordered_map<uint32_t, uint32_t> SkillComponent::m_skillBehaviorCache = {};
Observable<SkillComponent*, uint32_t, bool> SkillComponent::OnSkillCast;
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());
@ -50,7 +50,11 @@ bool SkillComponent::CastPlayerSkill(const uint32_t behaviorId, const uint32_t s
context->ExecuteUpdates();
OnSkillCast(this, skillID, !context->failed);
bool success = !context->failed;
OnSkillCast(this, skillID, success, skillUid);
context->failed = !success;
return !context->failed;
}

View File

@ -185,8 +185,8 @@ public:
*/
uint32_t GetUniqueSkillId();
// SkillComponent, SkillID, Success
static Observable<SkillComponent*, uint32_t, bool> OnSkillCast;
// SkillComponent, SkillID, Success, skillUID
static Observable<SkillComponent*, uint32_t, bool&, uint32_t> OnSkillCast;
private:
/**

View File

@ -977,7 +977,7 @@ void GameMessages::SendResurrect(Entity* entity) {
int32_t imaginationToRestore = levelComponent->GetLevel() >= 45 ? 20 : 6;
if (imaginationToRestore > destroyableComponent->GetMaxImagination()) imaginationToRestore = destroyableComponent->GetMaxImagination();
destroyableComponent->SetImagination(imaginationToRestore);
destroyableComponent->SetImagination(destroyableComponent->GetMaxImagination());
}
}
});