diff --git a/apps/freeablo/CMakeLists.txt b/apps/freeablo/CMakeLists.txt index f3bf252ed..b97b6f097 100644 --- a/apps/freeablo/CMakeLists.txt +++ b/apps/freeablo/CMakeLists.txt @@ -45,8 +45,6 @@ add_library(freeablo_lib # split into a library so I can link to it from tests faworld/hoverstate.h faworld/inventory.cpp faworld/inventory.h - faworld/itemenums.cpp - faworld/itemenums.h faworld/itemfactory.cpp faworld/itemfactory.h faworld/itemmap.cpp @@ -176,7 +174,7 @@ add_library(freeablo_lib # split into a library so I can link to it from tests fasavegame/objectidmapper.h fasavegame/objectidmapper.cpp fasavegame/gameloader.h - fasavegame/gameloader.cpp farender/levelrenderer.cpp farender/levelrenderer.h engine/debugsettings.h engine/debugsettings.cpp faworld/item/golditembase.cpp faworld/item/golditembase.h faworld/item/golditem.cpp faworld/item/golditem.h) + fasavegame/gameloader.cpp farender/levelrenderer.cpp farender/levelrenderer.h engine/debugsettings.h engine/debugsettings.cpp faworld/item/golditembase.cpp faworld/item/golditembase.h faworld/item/golditem.cpp faworld/item/golditem.h faworld/magiceffects/magiceffectbase.cpp faworld/magiceffects/magiceffectbase.h faworld/item/itemprefixorsuffixbase.cpp faworld/item/itemprefixorsuffixbase.h faworld/magiceffects/magiceffect.cpp faworld/magiceffects/magiceffect.h faworld/magiceffects/simplebuffdebuffeffect.cpp faworld/magiceffects/simplebuffdebuffeffect.h faworld/magiceffects/simplebuffdebuffeffectbase.cpp faworld/magiceffects/simplebuffdebuffeffectbase.h faworld/item/itemprefixorsuffix.cpp faworld/item/itemprefixorsuffix.h) target_link_libraries(freeablo_lib PUBLIC NuklearMisc Render Audio Serial Input Random Image enet cxxopts fmt::fmt) target_include_directories(freeablo_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/apps/freeablo/engine/debugsettings.cpp b/apps/freeablo/engine/debugsettings.cpp index dffb8a621..f0f6c8037 100644 --- a/apps/freeablo/engine/debugsettings.cpp +++ b/apps/freeablo/engine/debugsettings.cpp @@ -4,4 +4,9 @@ namespace DebugSettings { bool DebugMissiles = false; bool DebugLevelTransitions = false; + bool Instakill = false; + bool EnemiesFrozen = false; + bool PlayersInvuln = false; + bool DisableMusic = false; + ItemGenerationType itemGenerationType = ItemGenerationType::Normal; } \ No newline at end of file diff --git a/apps/freeablo/engine/debugsettings.h b/apps/freeablo/engine/debugsettings.h index 6380c06bf..0caaa9dba 100644 --- a/apps/freeablo/engine/debugsettings.h +++ b/apps/freeablo/engine/debugsettings.h @@ -4,4 +4,16 @@ namespace DebugSettings { extern bool DebugMissiles; extern bool DebugLevelTransitions; + extern bool Instakill; + extern bool EnemiesFrozen; + extern bool PlayersInvuln; + extern bool DisableMusic; + + enum class ItemGenerationType + { + Normal, + AlwaysMagical, + }; + + extern ItemGenerationType itemGenerationType; } \ No newline at end of file diff --git a/apps/freeablo/engine/enginemain.cpp b/apps/freeablo/engine/enginemain.cpp index 32b437ab2..fb577f47f 100644 --- a/apps/freeablo/engine/enginemain.cpp +++ b/apps/freeablo/engine/enginemain.cpp @@ -98,8 +98,6 @@ namespace Engine mMultiplayer = std::make_unique(*mWorld, *mLocalInputHandler); player = mPlayerFactory->create(*mWorld, characterClass); - if (variables["invuln"].as() == "on") - player->mInvuln = true; } } else diff --git a/apps/freeablo/engine/threadmanager.cpp b/apps/freeablo/engine/threadmanager.cpp index dd1e2c7b8..879b3bd89 100644 --- a/apps/freeablo/engine/threadmanager.cpp +++ b/apps/freeablo/engine/threadmanager.cpp @@ -1,5 +1,6 @@ #include "threadmanager.h" #include "../farender/renderer.h" +#include "debugsettings.h" #include #include #include @@ -53,6 +54,9 @@ namespace Engine void ThreadManager::playMusic(const std::string& path) { + if (DebugSettings::DisableMusic) + return; + Message message = {}; message.type = ThreadState::PLAY_MUSIC; message.data.musicPath = new std::string(path); diff --git a/apps/freeablo/fa_main.cpp b/apps/freeablo/fa_main.cpp index 35d0f5676..9373b1ca5 100644 --- a/apps/freeablo/fa_main.cpp +++ b/apps/freeablo/fa_main.cpp @@ -16,7 +16,6 @@ bool parseOptions(int argc, char** argv, cxxopts::ParseResult& variables) // -1 represents the main menu ("l,level", "Level number to load (0-16)", cxxopts::value()->default_value("-1"))( "c,character", "Choose Warrior, Rogue or Sorceror", cxxopts::value()->default_value("Warrior"))( - "invuln", "on or off", cxxopts::value()->default_value("off"))( "connect", "Ip Address or hostname to connect to", cxxopts::value()->default_value(""))( "seed", "Seed for level generation", cxxopts::value()->default_value("0")); diff --git a/apps/freeablo/fagui/guimanager.cpp b/apps/freeablo/fagui/guimanager.cpp index 87da4fc4c..27d43d4e2 100644 --- a/apps/freeablo/fagui/guimanager.cpp +++ b/apps/freeablo/fagui/guimanager.cpp @@ -6,7 +6,6 @@ #include "../fasavegame/gameloader.h" #include "../faworld/actorstats.h" #include "../faworld/equiptarget.h" -#include "../faworld/itemenums.h" #include "../faworld/player.h" #include "../faworld/playerbehaviour.h" #include "../faworld/spells.h" diff --git a/apps/freeablo/faworld/actor.cpp b/apps/freeablo/faworld/actor.cpp index 230da3098..a203c168a 100644 --- a/apps/freeablo/faworld/actor.cpp +++ b/apps/freeablo/faworld/actor.cpp @@ -12,6 +12,7 @@ #include "world.h" #include #include +#include #include namespace FAWorld @@ -181,6 +182,12 @@ namespace FAWorld if (mInvuln) return; + if (DebugSettings::Instakill) + { + die(); + return; + } + // https://wheybags.gitlab.io/jarulfs-guide/#getting-hit int32_t blockChance = getStats().getCalculatedStats().blockChance; blockChance += 2 * (getStats().mLevel - attacker->getStats().mLevel); @@ -371,7 +378,7 @@ namespace FAWorld printf("%s melee attacks %s - ", mName.c_str(), enemy->mName.c_str()); #endif - if (roll < toHit) + if (roll < toHit || DebugSettings::Instakill) { int32_t damage = stats.meleeDamage; damage += mWorld.mRng->randomInRange(stats.meleeDamageBonusRange.start, stats.meleeDamageBonusRange.end); diff --git a/apps/freeablo/faworld/actorstats.h b/apps/freeablo/faworld/actorstats.h index 1090c4512..8530c2611 100644 --- a/apps/freeablo/faworld/actorstats.h +++ b/apps/freeablo/faworld/actorstats.h @@ -40,13 +40,20 @@ namespace FAWorld } }; - struct ItemStats + struct MagicStatModifiers { BaseStats baseStats; int32_t maxLife = 0; int32_t maxMana = 0; int32_t armorClass = 0; int32_t toHit = 0; + int32_t meleeDamageBonus = 0; + int32_t rangedDamageBonus = 0; + }; + + struct ItemStats + { + MagicStatModifiers magicStatModifiers; IntRange meleeDamageBonusRange = {0, 0}; IntRange rangedDamageBonusRange = {0, 0}; }; diff --git a/apps/freeablo/faworld/behaviour.cpp b/apps/freeablo/faworld/behaviour.cpp index 8013f5839..c96dc45a6 100644 --- a/apps/freeablo/faworld/behaviour.cpp +++ b/apps/freeablo/faworld/behaviour.cpp @@ -3,6 +3,7 @@ #include "actor.h" #include "player.h" #include +#include #include #include #include @@ -51,7 +52,7 @@ namespace FAWorld void BasicMonsterBehaviour::update() { - if (mActor->mTarget.getType() != Target::Type::None) + if (mActor->mTarget.getType() != Target::Type::None || DebugSettings::EnemiesFrozen) return; mTicksSinceLastAction++; diff --git a/apps/freeablo/faworld/enums.h b/apps/freeablo/faworld/enums.h index 6bd0bf728..85c01e8ff 100644 --- a/apps/freeablo/faworld/enums.h +++ b/apps/freeablo/faworld/enums.h @@ -5,6 +5,13 @@ namespace FAWorld { + enum class Difficulty + { + Normal, + Nightmare, + Hell, + }; + enum class ActorType : uint8_t { // Undead, Demon and Animal set up to match the values from Diablo diff --git a/apps/freeablo/faworld/gamelevel.cpp b/apps/freeablo/faworld/gamelevel.cpp index 9710f8942..e2bdc706b 100644 --- a/apps/freeablo/faworld/gamelevel.cpp +++ b/apps/freeablo/faworld/gamelevel.cpp @@ -169,7 +169,7 @@ namespace FAWorld Actor* blocking = nullptr; if (mActorMap2D.count(actor->getPos().current())) blocking = mActorMap2D[actor->getPos().current()]; - debug_assert(blocking == actor || blocking == nullptr || blocking->isDead()); + debug_assert(blocking == actor || blocking == nullptr || (blocking->getWorld()->mLoading || blocking->isDead())); mActorMap2D[actor->getPos().current()] = actor; @@ -177,7 +177,7 @@ namespace FAWorld { if (mActorMap2D.count(actor->getPos().next())) blocking = mActorMap2D[actor->getPos().next()]; - debug_assert(blocking == actor || blocking == nullptr || blocking->isDead()); + debug_assert(blocking == actor || blocking == nullptr || (blocking->getWorld()->mLoading || blocking->isDead())); mActorMap2D[actor->getPos().next()] = actor; } diff --git a/apps/freeablo/faworld/inventory.cpp b/apps/freeablo/faworld/inventory.cpp index 48ed657cf..546b1835e 100644 --- a/apps/freeablo/faworld/inventory.cpp +++ b/apps/freeablo/faworld/inventory.cpp @@ -2,14 +2,13 @@ #include "../fagui/guimanager.h" #include "../fasavegame/gameloader.h" #include "../faworld/actorstats.h" -#include "actorstats.h" #include "equiptarget.h" #include "item/equipmentitem.h" #include "item/equipmentitembase.h" #include "item/itembase.h" +#include "item/itemprefixorsuffix.h" #include "item/usableitem.h" #include "item/usableitembase.h" -#include "itemenums.h" #include "itemfactory.h" #include "player.h" #include @@ -17,8 +16,6 @@ #include #include #include -#include -#include #include namespace FAWorld @@ -113,6 +110,8 @@ namespace FAWorld bool BasicInventory::autoPlaceItem(std::unique_ptr& item) { + release_assert(item); + // TODO: the original game had some fancier methods of trying to fit specific size items // There used to be an implementation of this here, but it was buggy so I removed it, // as I didn't want to spend time debugging it. @@ -300,6 +299,8 @@ namespace FAWorld bool CharacterInventory::autoPlaceItem(std::unique_ptr& item) { + release_assert(item); + // auto-placing in belt if (item->getAsUsableItem() && item->getAsUsableItem()->getBase()->isBeltEquippable() && mBelt.autoPlaceItem(item)) return true; @@ -329,9 +330,14 @@ namespace FAWorld count = placeGold(count, Engine::EngineMain::get()->mWorld->getItemFactory()); if (count) + { release_assert(goldItem->trySetCount(count)); + } else + { item.reset(); + return true; + } } return mMainInventory.autoPlaceItem(item); @@ -486,6 +492,26 @@ namespace FAWorld // TODO: other stats } + + EquipTarget allEquipmentSlots[] = {MakeEquipTarget(), + MakeEquipTarget(), + MakeEquipTarget(), + MakeEquipTarget(), + MakeEquipTarget(), + MakeEquipTarget(), + MakeEquipTarget()}; + + for (const auto& slot : allEquipmentSlots) + { + const EquipmentItem* item = getItemAt(slot) ? getItemAt(slot)->getAsEquipmentItem() : nullptr; + if (item) + { + if (item->mPrefix) + item->mPrefix->apply(stats.magicStatModifiers); + if (item->mSuffix) + item->mSuffix->apply(stats.magicStatModifiers); + } + } } bool CharacterInventory::isRangedWeaponEquipped() const { return getItemsInHands().rangedWeapon.has_value(); } @@ -554,7 +580,7 @@ namespace FAWorld { if (!mMainInventory.getItem(x, y)) { - std::unique_ptr newItem = itemFactory.generateBaseItem(ItemId::gold); + std::unique_ptr newItem = itemFactory.generateBaseItem("gold"); GoldItem* goldItem = newItem->getAsGoldItem(); int32_t toPlace = std::min(quantity, goldItem->getBase()->mMaxCount); @@ -590,7 +616,7 @@ namespace FAWorld } const GoldItemBase* goldItemBase = - safe_downcast(Engine::EngineMain::get()->mWorld->getItemFactory().getItemBaseHolder().get("gold")); + safe_downcast(Engine::EngineMain::get()->mWorld->getItemFactory().getItemBaseHolder().getItemBase("gold")); // second part - filling the empty slots with gold for (int32_t x = 0; x != mMainInventory.width(); x++) @@ -652,7 +678,7 @@ namespace FAWorld mMainInventory.placeItem(goldFromInventory, x, y); } - std::unique_ptr cursorGold = itemFactory.generateBaseItem(ItemId::gold); + std::unique_ptr cursorGold = itemFactory.generateBaseItem("gold"); release_assert(cursorGold->getAsGoldItem()->trySetCount(amountToTransferToCursor)); setCursorHeld(std::move(cursorGold)); diff --git a/apps/freeablo/faworld/item/equipmentitem.cpp b/apps/freeablo/faworld/item/equipmentitem.cpp index c75004ebe..cc18507a5 100644 --- a/apps/freeablo/faworld/item/equipmentitem.cpp +++ b/apps/freeablo/faworld/item/equipmentitem.cpp @@ -1,6 +1,9 @@ #include "equipmentitem.h" #include "equipmentitembase.h" +#include "itemprefixorsuffix.h" +#include "itemprefixorsuffixbase.h" #include +#include #include #include @@ -14,27 +17,77 @@ namespace FAWorld mArmorClass = getBase()->mArmorClassRange.max; } - void EquipmentItem::save(FASaveGame::GameSaver& saver) const { saver.save(mArmorClass); } + void EquipmentItem::save(FASaveGame::GameSaver& saver) const + { + super::save(saver); + + saver.save(mArmorClass); + + saver.save(mPrefix != nullptr); + if (mPrefix != nullptr) + { + Serial::ScopedCategorySaver cat("Prefix", saver); + saver.save(mPrefix->getBase()->mId); + mPrefix->save(saver); + } + + saver.save(mSuffix != nullptr); + if (mSuffix != nullptr) + { + Serial::ScopedCategorySaver cat("Suffix", saver); + saver.save(mSuffix->getBase()->mId); + mSuffix->save(saver); + } + } void EquipmentItem::load(FASaveGame::GameLoader& loader) { + super::load(loader); + mArmorClass = Misc::clamp(loader.load(), getBase()->mArmorClassRange.min, getBase()->mArmorClassRange.max); + + if (loader.load()) + { + std::string prefixId = loader.load(); + mPrefix = loader.currentlyLoadingWorld->getItemFactory().getItemBaseHolder().getItemPrefixOrSuffixBase(prefixId)->create(); + mPrefix->load(loader); + } + + if (loader.load()) + { + std::string suffixId = loader.load(); + mSuffix = loader.currentlyLoadingWorld->getItemFactory().getItemBaseHolder().getItemPrefixOrSuffixBase(suffixId)->create(); + mSuffix->load(loader); + } } const EquipmentItemBase* EquipmentItem::getBase() const { return safe_downcast(mBase); } std::string EquipmentItem::getFullDescription() const { - std::string description = super::getFullDescription(); + std::string description; + if (mPrefix) + description += mPrefix->getBase()->mName + " "; + + description += getBase()->mName; + + if (mSuffix) + description += " of " + mSuffix->getBase()->mName; + + description += "\n"; if (getBase()->mClass == ItemClass::weapon) - description += fmt::format("\ndamage: {} - {}", getBase()->mDamageBonusRange.start, getBase()->mDamageBonusRange.end); + description += fmt::format("damage: {} - {}\n", getBase()->mDamageBonusRange.start, getBase()->mDamageBonusRange.end); else - description += fmt::format("\narmor: {}", mArmorClass); + description += fmt::format("armor: {}\n", mArmorClass); + + if (mPrefix) + description += mPrefix->getFullDescription(); + if (mSuffix) + description += mSuffix->getFullDescription(); // TODO: durability // TODO: charges - // TODO: prefix / suffix // TODO: requirements return description; diff --git a/apps/freeablo/faworld/item/equipmentitem.h b/apps/freeablo/faworld/item/equipmentitem.h index 807e94af8..42caa6ff4 100644 --- a/apps/freeablo/faworld/item/equipmentitem.h +++ b/apps/freeablo/faworld/item/equipmentitem.h @@ -4,6 +4,7 @@ namespace FAWorld { class EquipmentItemBase; + class ItemPrefixOrSuffix; class EquipmentItem final : public Item { @@ -27,5 +28,7 @@ namespace FAWorld public: int32_t mArmorClass = 0; + std::unique_ptr mPrefix; + std::unique_ptr mSuffix; }; } diff --git a/apps/freeablo/faworld/item/equipmentitembase.cpp b/apps/freeablo/faworld/item/equipmentitembase.cpp index 739c22a42..26501b142 100644 --- a/apps/freeablo/faworld/item/equipmentitembase.cpp +++ b/apps/freeablo/faworld/item/equipmentitembase.cpp @@ -1,4 +1,5 @@ #include "equipmentitembase.h" +#include #include #include diff --git a/apps/freeablo/faworld/item/itembase.cpp b/apps/freeablo/faworld/item/itembase.cpp index b0acfa755..2f7615f81 100644 --- a/apps/freeablo/faworld/item/itembase.cpp +++ b/apps/freeablo/faworld/item/itembase.cpp @@ -1,14 +1,15 @@ #include "itembase.h" -#include "../../farender/renderer.h" #include "item.h" #include +#include +#include namespace FAWorld { ItemBase::ItemBase(const DiabloExe::ExeItem& exeItem) : mId(exeItem.idName), mType(exeItem.type), mClass(exeItem.itemClass), mName(exeItem.name), mShortName(exeItem.shortName), - mSize(exeItem.invSizeX, exeItem.invSizeY), mPrice(exeItem.price), mDropItemSoundPath(exeItem.dropItemSoundPath), - mInventoryPlaceItemSoundPath(exeItem.invPlaceItemSoundPath) + mSize(exeItem.invSizeX, exeItem.invSizeY), mPrice(exeItem.price), mQualityLevel(exeItem.qualityLevel), mDropRate(exeItem.dropRate), + mDropItemSoundPath(exeItem.dropItemSoundPath), mInventoryPlaceItemSoundPath(exeItem.invPlaceItemSoundPath) { FARender::SpriteLoader& spriteLoader = FARender::Renderer::get()->mSpriteLoader; mDropItemAnimation = spriteLoader.getSprite(spriteLoader.mItemDrops[mId]); diff --git a/apps/freeablo/faworld/item/itembase.h b/apps/freeablo/faworld/item/itembase.h index 66be2229f..d8174ec37 100644 --- a/apps/freeablo/faworld/item/itembase.h +++ b/apps/freeablo/faworld/item/itembase.h @@ -1,6 +1,6 @@ #pragma once -#include #include +#include #include #include @@ -9,6 +9,11 @@ namespace Render class Cursor; } +namespace DiabloExe +{ + class ExeItem; +} + namespace FAWorld { class Item; @@ -32,6 +37,8 @@ namespace FAWorld Vec2i mSize; int32_t mPrice = 0; + int32_t mQualityLevel = 0; + int32_t mDropRate = 0; Render::SpriteGroup* mDropItemAnimation = nullptr; const Render::TextureReference* mInventoryIcon = nullptr; diff --git a/apps/freeablo/faworld/item/itembaseholder.cpp b/apps/freeablo/faworld/item/itembaseholder.cpp index 0fdadf4fb..493b9daff 100644 --- a/apps/freeablo/faworld/item/itembaseholder.cpp +++ b/apps/freeablo/faworld/item/itembaseholder.cpp @@ -2,7 +2,9 @@ #include "equipmentitembase.h" #include "golditembase.h" #include "usableitembase.h" +#include #include +#include #include #include @@ -30,6 +32,9 @@ namespace FAWorld } release_assert(goldItemCount <= 1); + + for (const auto& prefixOrSuffix : exe.getMagicItemEffects()) + mAllItemPrefixSuffixBases[prefixOrSuffix.mIdName] = std::make_unique(prefixOrSuffix); } std::unique_ptr ItemBaseHolder::createItem(const std::string& baseTypeId) const { return mAllItemBases.at(baseTypeId)->createItem(); } diff --git a/apps/freeablo/faworld/item/itembaseholder.h b/apps/freeablo/faworld/item/itembaseholder.h index 60e659c10..dfdbcf41e 100644 --- a/apps/freeablo/faworld/item/itembaseholder.h +++ b/apps/freeablo/faworld/item/itembaseholder.h @@ -1,5 +1,6 @@ #pragma once #include "itembase.h" +#include "itemprefixorsuffixbase.h" #include #include @@ -16,9 +17,18 @@ namespace FAWorld explicit ItemBaseHolder(const DiabloExe::DiabloExe& exe); std::unique_ptr createItem(const std::string& baseTypeId) const; - const ItemBase* get(const std::string& key) const { return mAllItemBases.at(key).get(); } + const ItemBase* getItemBase(const std::string& key) const { return mAllItemBases.at(key).get(); } + + const ItemPrefixOrSuffixBase* getItemPrefixOrSuffixBase(const std::string& key) const { return mAllItemPrefixSuffixBases.at(key).get(); } + + const std::unordered_map>& getAllItemBases() const { return mAllItemBases; } + const std::unordered_map>& getAllItemPrefixSuffixBases() const + { + return mAllItemPrefixSuffixBases; + } private: std::unordered_map> mAllItemBases; + std::unordered_map> mAllItemPrefixSuffixBases; }; } diff --git a/apps/freeablo/faworld/item/itemprefixorsuffix.cpp b/apps/freeablo/faworld/item/itemprefixorsuffix.cpp new file mode 100644 index 000000000..09cde7e70 --- /dev/null +++ b/apps/freeablo/faworld/item/itemprefixorsuffix.cpp @@ -0,0 +1,55 @@ +#include "itemprefixorsuffix.h" +#include "itemprefixorsuffixbase.h" +#include +#include +#include + +namespace FAWorld +{ + ItemPrefixOrSuffix::ItemPrefixOrSuffix(const ItemPrefixOrSuffixBase* base) : mBase(base) {} + + void ItemPrefixOrSuffix::init() + { + for (const auto& effectBase : getBase()->mEffects) + { + mEffects.emplace_back(effectBase->create()); + mEffects.back()->init(); + } + } + + std::string ItemPrefixOrSuffix::getFullDescription() const + { + std::string description; + for (const auto& effect : mEffects) + description += effect->getFullDescription() + "\n"; + + return description; + } + + void ItemPrefixOrSuffix::apply(MagicStatModifiers& modifiers) const + { + for (const auto& effect : mEffects) + effect->apply(modifiers); + } + + void ItemPrefixOrSuffix::save(FASaveGame::GameSaver& saver) const + { + for (const auto& effect : mEffects) + effect->save(saver); + } + + void ItemPrefixOrSuffix::load(FASaveGame::GameLoader& loader) + { + // TODO: deal with changing effects here, once we add mod support + + mEffects.reserve(getBase()->mEffects.size()); + for (const auto& effectBase : getBase()->mEffects) + { + std::unique_ptr effect = effectBase->create(); + effect->load(loader); + mEffects.emplace_back(std::move(effect)); + } + } + + ItemPrefixOrSuffix::~ItemPrefixOrSuffix() = default; +} diff --git a/apps/freeablo/faworld/item/itemprefixorsuffix.h b/apps/freeablo/faworld/item/itemprefixorsuffix.h new file mode 100644 index 000000000..5e8498609 --- /dev/null +++ b/apps/freeablo/faworld/item/itemprefixorsuffix.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include +#include + +namespace FAWorld +{ + class ItemPrefixOrSuffixBase; + class MagicEffect; + + class ItemPrefixOrSuffix + { + public: + explicit ItemPrefixOrSuffix(const ItemPrefixOrSuffixBase* base); + void init(); + ~ItemPrefixOrSuffix(); + + void save(FASaveGame::GameSaver& saver) const; + void load(FASaveGame::GameLoader& loader); + + const ItemPrefixOrSuffixBase* getBase() { return mBase; } + std::string getFullDescription() const; + void apply(MagicStatModifiers& modifiers) const; + + private: + const ItemPrefixOrSuffixBase* mBase = nullptr; + std::vector> mEffects; + }; +} diff --git a/apps/freeablo/faworld/item/itemprefixorsuffixbase.cpp b/apps/freeablo/faworld/item/itemprefixorsuffixbase.cpp new file mode 100644 index 000000000..9f722373d --- /dev/null +++ b/apps/freeablo/faworld/item/itemprefixorsuffixbase.cpp @@ -0,0 +1,135 @@ +#include "itemprefixorsuffixbase.h" +#include "equipmentitem.h" +#include "equipmentitembase.h" +#include "itemprefixorsuffix.h" +#include +#include + +namespace FAWorld +{ + ItemPrefixOrSuffixBase::ItemPrefixOrSuffixBase(const DiabloExe::ExeMagicItemEffect& exeEffect) + : mId(exeEffect.mIdName), mName(exeEffect.mName), mCursed(!exeEffect.mNotCursed), mIsPrefix(exeEffect.mIsPrefix), mQuality(exeEffect.mQualLevel), + mTargetTypesBitmask(exeEffect.mTargetTypesBitmask), mDropRate(exeEffect.mDoubleProbabilityForPrefixes ? 2 : 1) + { + switch (exeEffect.mEffect) + { + case ExeMagicEffectType::PlusStrength: + case ExeMagicEffectType::MinusStrength: + case ExeMagicEffectType::PlusMagic: + case ExeMagicEffectType::MinusMagic: + case ExeMagicEffectType::PlusDexterity: + case ExeMagicEffectType::MinusDexterity: + case ExeMagicEffectType::PlusVitality: + case ExeMagicEffectType::MinusVitality: + case ExeMagicEffectType::PlusToHit: + case ExeMagicEffectType::MinusToHit: + case ExeMagicEffectType::PlusLife: + case ExeMagicEffectType::MinusLife: + case ExeMagicEffectType::PlusMana: + case ExeMagicEffectType::MinusMana: + case ExeMagicEffectType::PlusDamage: + case ExeMagicEffectType::MinusArmor: + mEffects.emplace_back(new SimpleBuffDebuffEffectBase(exeEffect)); + break; + + case ExeMagicEffectType::PlusDamagePercent: + case ExeMagicEffectType::MinusDamagePercent: + case ExeMagicEffectType::PlusToHitAndDamagePercent: + case ExeMagicEffectType::MinusToHitAndDamagePercent: + case ExeMagicEffectType::PlusArmorPercent: + case ExeMagicEffectType::MinusArmorPercent: + case ExeMagicEffectType::PlusResistFire: + case ExeMagicEffectType::PlusResistLightning: + case ExeMagicEffectType::PlusResistMagic: + case ExeMagicEffectType::PlusResistAll: + case ExeMagicEffectType::PlusSpellLevels: + case ExeMagicEffectType::PlusCharges: + case ExeMagicEffectType::PlusDamageFire: + case ExeMagicEffectType::PlusDamageLightning: + case ExeMagicEffectType::PlusAllAttributes: + case ExeMagicEffectType::MinusAllAttributes: + case ExeMagicEffectType::PlusDamageTaken: + case ExeMagicEffectType::MinusDamageTaken: + case ExeMagicEffectType::PlusDurabilityPercent: + case ExeMagicEffectType::CurseDurabilityPercent: + case ExeMagicEffectType::Indestructible: + case ExeMagicEffectType::PlusLightPercent: + case ExeMagicEffectType::CurseLightPercent: + case ExeMagicEffectType::MultipleArrows: + case ExeMagicEffectType::PlusFireArrowDamage: + case ExeMagicEffectType::PlusLightningArrowDamage: + case ExeMagicEffectType::UniqueIcon: + case ExeMagicEffectType::Deal1To3DamageToAttackers: + case ExeMagicEffectType::ZeroMana: + case ExeMagicEffectType::UserCantHeal: + case ExeMagicEffectType::AbsorbHalfTrapDamage: + case ExeMagicEffectType::Knockback: + case ExeMagicEffectType::HitMonsterCantHeal: + case ExeMagicEffectType::StealMana: + case ExeMagicEffectType::StealLife: + case ExeMagicEffectType::DamageArmor: + case ExeMagicEffectType::FastAttack: + case ExeMagicEffectType::FastHitRecovery: + case ExeMagicEffectType::FastBlock: + case ExeMagicEffectType::RandomSpeedArrows: + case ExeMagicEffectType::SetItemDamage: + case ExeMagicEffectType::SetDurability: + case ExeMagicEffectType::NoStrengthRequirement: + case ExeMagicEffectType::SetCharges: + case ExeMagicEffectType::FastAttack2: + case ExeMagicEffectType::OneHanded: + case ExeMagicEffectType::Plus200PercentOnDemons: + case ExeMagicEffectType::AllResistancesZero: + case ExeMagicEffectType::ConstantlyLoseLife: + case ExeMagicEffectType::LifeSteal: + case ExeMagicEffectType::Infravision: + case ExeMagicEffectType::SetArmor: + case ExeMagicEffectType::AddArmorToLife: + case ExeMagicEffectType::Add10PercentOfManaToArmor: + case ExeMagicEffectType::PlusLevelDependentResistFire: + mEffects.emplace_back(new MagicEffectBase(exeEffect)); // Not yet handled, use default + break; + } + } + + ItemPrefixOrSuffixBase::~ItemPrefixOrSuffixBase() = default; + + std::unique_ptr ItemPrefixOrSuffixBase::create() const { return std::make_unique(this); } + + bool ItemPrefixOrSuffixBase::canBeAppliedTo(const EquipmentItem& item) const + { + switch (item.getBase()->mType) + { + case ItemType::sword: + case ItemType::axe: + case ItemType::mace: + return int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::OtherWeapons); + + case ItemType::bow: + return int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::Bow); + + case ItemType::staff: + return int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::Staff); + + case ItemType::shield: + return int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::Shield); + + case ItemType::lightArmor: + case ItemType::helm: + case ItemType::mediumArmor: + case ItemType::heavyArmor: + return int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::Armor); + + case ItemType::ring: + case ItemType::amulet: + return int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::Jewelery); + + case ItemType::gold: + case ItemType::none: + case ItemType::misc: + return false; + } + + invalid_enum(ItemEffectType, item.getBase()->mType); + } +} \ No newline at end of file diff --git a/apps/freeablo/faworld/item/itemprefixorsuffixbase.h b/apps/freeablo/faworld/item/itemprefixorsuffixbase.h new file mode 100644 index 000000000..93d9ef463 --- /dev/null +++ b/apps/freeablo/faworld/item/itemprefixorsuffixbase.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include +#include + +namespace DiabloExe +{ + class ExeMagicItemEffect; +} + +namespace FAWorld +{ + class MagicEffectBase; + class ItemPrefixOrSuffix; + class EquipmentItem; + + class ItemPrefixOrSuffixBase + { + public: + explicit ItemPrefixOrSuffixBase(const DiabloExe::ExeMagicItemEffect& exeEffect); + ~ItemPrefixOrSuffixBase(); + + std::unique_ptr create() const; + bool canBeAppliedTo(const EquipmentItem& item) const; + + public: + std::string mId; + + std::string mName; + bool mCursed = false; + bool mIsPrefix = false; + int32_t mQuality = 0; + MagicalItemTargetBitmask mTargetTypesBitmask = MagicalItemTargetBitmask::None; + int32_t mDropRate = 1; + + std::vector> mEffects; + }; +} diff --git a/apps/freeablo/faworld/item/usableitembase.cpp b/apps/freeablo/faworld/item/usableitembase.cpp index 07ac432d3..13181a334 100644 --- a/apps/freeablo/faworld/item/usableitembase.cpp +++ b/apps/freeablo/faworld/item/usableitembase.cpp @@ -1,4 +1,5 @@ #include "usableitembase.h" +#include #include #include diff --git a/apps/freeablo/faworld/itemenums.cpp b/apps/freeablo/faworld/itemenums.cpp deleted file mode 100644 index d7048102b..000000000 --- a/apps/freeablo/faworld/itemenums.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "itemenums.h" diff --git a/apps/freeablo/faworld/itemenums.h b/apps/freeablo/faworld/itemenums.h deleted file mode 100644 index 59264c3f2..000000000 --- a/apps/freeablo/faworld/itemenums.h +++ /dev/null @@ -1,340 +0,0 @@ -#pragma once -#include - -namespace FAWorld -{ - // TODO: synchronize names with: - // https://github.com/sanctuary/notes/blob/72a0772e0d187d29117c4ddd6e6265cafc774a50/enums.h#L128 - enum class ItemEffectType - { - IncPercentChanceToHit, - DecPercentChanceToHit, - IncPercentDamageDone, - DecPercentDamageDone, - IncPercentDamageDoneChanceToHit, - DecPercentDamageDoneChanceToHit, - IncPercentArmourClass, - DecPercentArmourClass, - IncPercentResistFire, - IncPercentResistLightning, - IncPercentResistMagic, - IncPercentResistAll, - Unknown6, - Unknown7, - ModSpellLevel, - IncCharges, - IncFireDamage, - IncLightningDamage, - Unknown8, - IncStrength, - DecStrength, - IncMagic, - DecMagic, - IncDexterity, - DecDexterity, - IncVitality, - DecVitality, - IncAllBasicStats, - DecAllBasicStats, - IncDamageTaken, - DecDamageTaken, - IncHP, - DecHP, - IncMana, - DecMana, - IncPercentDurability, - DecPercentDurability, - Indestructible, - IncLightRadius, - DecLightRadius, - Unknown0, - MultipleArrows, - IncPercentFireArrowDamage, - IncPercentLightningArrowDamage, - UniquePicture, - Thorns, - AllMana, - PlayerNoHeal, - Unknown1, - Unknown2, - Unknown3, - Unknown4, - HalfTrapDamage, - Knockback, - MonsterNoHeal, - PercentManaSteal, - PercentLifeSteal, - ArmourPenetration, - AttackSpeed0, - HitRecovery, - FastBlock, - IncDamageDone, - RandomArrowSpeed, - UnusualDamage, - AlteredDurability, - NoStrengthRequirment, - Spell, - AttackSpeed1, - OneHanded, - AntiDemon, - ZeroAllResist, - Unknown5, - ConstantLifeDrain, - PercentFixedLifeSteal, - Infravision, - SpecifiedArmourClass, - IncHPWithArmourClass, - IncArmourClassWithMana, - IncFireResistWithLevel, - DecArmourClass - }; - - enum class ItemQuality - { - normal = 0, - magic, - unique - }; - - enum class ItemId - { - gold = 0, - shortSword, - buckler, - club, - shortBow, - shortStaffOfChargedBolt, - cleaver, - theUndeadCrown, - empyreanBand, - magicRock, - opticAmulet, // 10 - ringOfTruth, - tavernSign, - harlequinCrest, - veilOfSteel, - goldenElixir, - anvilOfFury, - blackMushroom, - brain, - fungalTome, - spectralElixir, // 20 - bloodStone, - mapOfTheStars, - heart, - potionOfHealing, - potionOfMana, - scrollOfIdentify, - scrollOfTownPortal, - arkainesValor, - potionOfFullHealing, - potionOfFullMana, // 30 - griswoldsEdge, - lightforge, - staffOfLazarus, - scrollOfResurrect, - baseSkullCap, - baseHelm, - baseFullHelm, - baseCrown, - baseGreatHelm, - baseCape, // 40 - baseRags, - baseCloak, - baseRobe, - baseQuiltedArmor, - baseLeatherArmor, - baseHardLeatherArmor, - baseStuddedLeatherArmor, - baseRingMail, - baseChainMail, - baseScaleMail, // 50 - baseBreastPlate, - baseSplintMail, - basePlateMail, - baseFieldPlate, - baseGothicPlate, - baseFullPlateMail, - baseBuckler, - baseSmallShield, - baseLargeShield, - baseKiteShield, // 60 - baseTowerShield, - baseGothicShield, - basePotionOfHealing, - basePotionOfFullHealing, - basePotionOfMana, - basePotionOfFullMana, - basePotionOfRejuvenation, - basePotionOfFullRejuvenation, - baseElixirOfStrength, - baseElixirOfMagic, // 70 - baseElixirOfDexterity, - baseElixirOfVitality, - baseScrollOfHealing, - baseScrollOfLightning, - baseScrollOfIdentify, - baseScrollOfResurrect, - baseScrollOfFireWall, - baseScrollOfInferno, - baseScrollOfTownPortal, - baseScrollOfFlash, // 80 - baseScrollOfInfravision, - baseScrollOfPhasing, - baseScrollOfManaShield, - baseScrollOfFlameWave, - baseScrollOfFireball, - baseScrollOfStoneCurse, - baseScrollOfChainLightning, - baseScrollOfGuardian, - baseNonItem, - baseScrollOfNova, // 90 - baseScrollOfGolem, - baseScrollOfNone, - baseScrollOfTeleport, - baseScrollOfApocalypse, - baseBookQlvl2, - baseBookQlvl8, - baseBookQlvl14, - baseBookQlvl20, - baseDagger, - baseShortSword, // 100 - baseFalchion, - baseScimitar, - baseClaymore, - baseBlade, - baseSabre, - baseLongSword, - baseBroadSword, - baseBastardSword, - baseTwoHandedSword, - baseGreatSword, // 110 - baseSmallAxe, - baseAxe, - baseLargeAxe, - baseBroadAxe, - baseBattleAxe, - baseGreatAxe, - baseMace, - baseMorningStar, - baseWarHammer, - baseSpikedClub, - baseClub, - baseFlail, - baseMaul, - baseShortBow, - baseHuntersBow, - baseLongBow, - baseCompositeBow, - baseShortBattleBow, - baseLongBattleBow, - baseShortWarBow, - baseLongWarBow, - baseShortStaff, - baseLongStaff, - baseCompositeStaff, - baseQuarterStaff, - baseWarStaff, - baseRingQlvl5, - baseRingQlvl10, - baseRingQlvl15, - baseAmuletQlvl8, - baseAmuletQlvl16, - COUNT, - }; - - enum class UniqueItemId - { - theButchersCleaver = 0, - theUndeadCrown, - empyreanBand, - opticAmulet, - ringOfTruth, - harlequinCrest, - veilOfSteel, - arkainesValor, - griswoldsEdge, - lightforge, - theRiftBow, - theNeedler, - theCelestialBow, - deadlyHunter, - bowOfTheDead, - theBlackoakBow, - flamedart, - fleshstinger, - windforce, - eaglehorn, - gonnagalsDirk, - theDefender, - gryphonsClaw, - blackRazor, - gibbousMoon, - iceShank, - theExecutionersBlade, - theBonesaw, - shadowhawk, - wizardspike, - lightsabre, - theFalconsTalon, - inferno, - doombringer, - theGrizzly, - theGrandfather, - theMangler, - sharpBeak, - bloodslayer, - theCelestialAxe, - wickedAxe, - stonecleaver, - aguinarasHatchet, - hellslayer, - messerschmidtsReaver, - crackrust, - hammerOfJholm, - civerbsCudgel, - theCelestialStar, - baranarsStar, - gnarledRoot, - theCraniumBasher, - schaefersHammer, - dreamflange, - staffOfShadows, - immolator, - stormSpire, - gleamsong, - thundercall, - theProtector, - najsPuzzler, - mindcry, - rodOfOnan, - helmOfSpirits, - thinkingCap, - overlordsHelm, - foolsCrest, - gotterdamerung, - royalCirclet, - tornFleshOfSouls, - theGladiatorsBane, - theRainbowCloak, - leatherOfAut, - wisdomsWrap, - sparkingMail, - scavengerCarapace, - nightscape, - najsLightPlate, - demonspikeCoat, - theDeflector, - splitSkullShield, - dragonsBreach, - blackoakShield, - holyDefender, - stormshield, - bramble, - ringOfRegha, - theBleeder, - constrictingRing, - ringOfEngagement, - null, - }; -} diff --git a/apps/freeablo/faworld/itemfactory.cpp b/apps/freeablo/faworld/itemfactory.cpp index fdd781304..b98e2edaa 100644 --- a/apps/freeablo/faworld/itemfactory.cpp +++ b/apps/freeablo/faworld/itemfactory.cpp @@ -1,67 +1,143 @@ #include "itemfactory.h" #include "diabloexe/diabloexe.h" -#include "itemenums.h" +#include #include #include +#include +#include #include namespace FAWorld { - ItemFilter::Callback ItemFilter::maxQLvl(int32_t value) - { - return [value](const DiabloExe::ExeItem& item) { return static_cast(item.qualityLevel) <= value; }; - } + ItemFactory::ItemFactory(const DiabloExe::DiabloExe& exe, Random::Rng& rng) : mItemBaseHolder(exe), mRng(rng) {} - ItemFilter::Callback ItemFilter::sellableGriswoldBasic() + std::unique_ptr ItemFactory::generateBaseItem(const std::string& id) const { - return [](const DiabloExe::ExeItem& item) { - static const auto excludedTypes = {ItemType::misc, ItemType::gold, ItemType::staff, ItemType::ring, ItemType::amulet}; - return std::count(excludedTypes.begin(), excludedTypes.end(), static_cast(item.type)) == 0; - }; + std::unique_ptr newItem = mItemBaseHolder.createItem(id); + newItem->init(); + return newItem; } - ItemFactory::ItemFactory(const DiabloExe::DiabloExe& exe, Random::Rng& rng) : mItemBaseHolder(exe), mExe(exe), mRng(rng) + std::unique_ptr ItemFactory::generateRandomItem(int32_t itemLevel, ItemGenerationType generationType) const { - for (int i = 0; i < static_cast(mExe.getBaseItems().size()); ++i) - mUniqueBaseItemIdToItemId[mExe.getBaseItems()[i].uniqueBaseItemId] = static_cast(i); + return generateRandomItem(itemLevel, generationType, [](const ItemBase&) { return true; }); } - std::unique_ptr ItemFactory::generateBaseItem(ItemId id, const BaseItemGenOptions& /*options*/) const + std::unique_ptr ItemFactory::generateRandomItem(int32_t itemLevel, ItemGenerationType generationType, const ItemFilter& filter) const { - const std::string& lookup = Engine::EngineMain::get()->exe().getBaseItems()[int32_t(id)].idName; - std::unique_ptr newItem = mItemBaseHolder.createItem(lookup); - newItem->init(); + if (DebugSettings::itemGenerationType == DebugSettings::ItemGenerationType::AlwaysMagical) + generationType = ItemGenerationType::AlwaysMagical; - return newItem; + const ItemBase* itemBase = randomItemBase([&](const ItemBase& base) { + bool ok = filter(base) && base.mQualityLevel <= itemLevel; + + if (generationType != ItemGenerationType::Normal) + ok = ok && base.getEquipType() != ItemEquipType::none; + + return ok; + }); + + release_assert(itemBase); + + std::unique_ptr item = itemBase->createItem(); + item->init(); - // Item res; - // res.mEmpty = false; - // res.mIsReal = true; - // res.mInvX = 0; - // res.mInvY = 0; - // res.mBaseId = id; - // auto info = getInfo(id); - // res.mMaxDurability = res.mCurrentDurability = info.durability; - // res.mArmorClass = mRng.randomInRange(info.minArmorClass, info.maxArmorClass); - // return res; + if (EquipmentItem* equipmentItem = item->getAsEquipmentItem()) + { + bool magical = generationType == ItemGenerationType::AlwaysMagical || mRng.randomInRange(0, 99) <= 10 || mRng.randomInRange(0, 99) <= itemLevel; + + if (magical) + { + int32_t maxLevel = itemLevel; + int32_t minLevel = maxLevel / 2; + + applyRandomEnchantment(*equipmentItem, minLevel, maxLevel); + } + } + + return item; } - ItemId ItemFactory::randomItemId(const ItemFilter::Callback& filter) const + const ItemBase* ItemFactory::randomItemBase(const ItemFilter& filter) const { - static std::vector pool; + static std::vector pool; pool.clear(); - for (auto id : enum_range()) + for (const auto& pair : mItemBaseHolder.getAllItemBases()) { - const DiabloExe::ExeItem& info = getInfo(id); - if (!filter(info)) + const ItemBase* base = pair.second.get(); + if (!filter(*base)) continue; - for (int32_t i = 0; i < static_cast(info.dropRate); ++i) - pool.push_back(id); + + for (int32_t i = 0; i < base->mDropRate; ++i) + pool.push_back(base); + } + + if (!pool.empty()) + return pool[mRng.randomInRange(0, pool.size() - 1)]; + + return nullptr; + } + + const ItemPrefixOrSuffixBase* ItemFactory::randomPrefixOrSuffixBase(const ItemPrefixOrSuffixFilter& filter) const + { + std::vector pool; + + for (const auto& pair : mItemBaseHolder.getAllItemPrefixSuffixBases()) + { + const ItemPrefixOrSuffixBase* base = pair.second.get(); + + if (filter(*base)) + { + for (int32_t i = 0; i < base->mDropRate; i++) + pool.push_back(base); + } } - return pool[mRng.randomInRange(0, pool.size() - 1)]; + + if (!pool.empty()) + return pool[mRng.randomInRange(0, pool.size() - 1)]; + + return nullptr; } - const DiabloExe::ExeItem& ItemFactory::getInfo(ItemId id) const { return mExe.getBaseItems().at(static_cast(id)); } + void ItemFactory::applyRandomEnchantment(EquipmentItem& item, int32_t minLevel, int32_t maxLevel) const + { + bool prefix = mRng.randomInRange(0, 3) == 0; + bool suffix = mRng.randomInRange(0, 2) != 0; + + if (!prefix && !suffix) + { + if (mRng.randomInRange(0, 1) == 1) + suffix = true; + else + prefix = true; + } + + if (prefix) + { + const ItemPrefixOrSuffixBase* prefixBase = randomPrefixOrSuffixBase([&](const ItemPrefixOrSuffixBase& base) { + return base.mIsPrefix && base.canBeAppliedTo(item) && base.mQuality >= minLevel && base.mQuality <= maxLevel; + }); + + if (prefixBase) + { + item.mPrefix = prefixBase->create(); + item.mPrefix->init(); + } + } + + if (suffix) + { + const ItemPrefixOrSuffixBase* suffixBase = randomPrefixOrSuffixBase([&](const ItemPrefixOrSuffixBase& base) { + return !base.mIsPrefix && base.canBeAppliedTo(item) && base.mQuality >= minLevel && base.mQuality <= maxLevel; + }); + + if (suffixBase) + { + item.mSuffix = suffixBase->create(); + item.mSuffix->init(); + } + } + } void ItemFactory::saveItem(const Item& item, FASaveGame::GameSaver& saver) const { diff --git a/apps/freeablo/faworld/itemfactory.h b/apps/freeablo/faworld/itemfactory.h index 432f65bdb..f465843a7 100644 --- a/apps/freeablo/faworld/itemfactory.h +++ b/apps/freeablo/faworld/itemfactory.h @@ -1,25 +1,15 @@ #pragma once -#include "diabloexe/baseitem.h" -#include "itemenums.h" -#include "misc/enum_range.h" #include #include #include -#include #include #include -#include namespace DiabloExe { class DiabloExe; } -namespace Cel -{ - class CelFile; -} - namespace FASaveGame { class GameSaver; @@ -28,29 +18,28 @@ namespace FASaveGame namespace FAWorld { - class Item; - enum class ItemId; - enum class UniqueItemId; - - class BaseItemGenOptions - { - public: - using thisType = BaseItemGenOptions; - }; - - namespace ItemFilter - { - using Callback = std::function; - Callback maxQLvl(int32_t value); - Callback sellableGriswoldBasic(); - } + using ItemFilter = std::function; + using ItemPrefixOrSuffixFilter = std::function; class ItemFactory { public: explicit ItemFactory(const DiabloExe::DiabloExe& exe, Random::Rng& rng); - std::unique_ptr generateBaseItem(ItemId id, const BaseItemGenOptions& options = {}) const; - ItemId randomItemId(const ItemFilter::Callback& filter) const; + + std::unique_ptr generateBaseItem(const std::string& id) const; + + enum class ItemGenerationType + { + Normal, + OnlyBaseItems, + AlwaysMagical, + }; + std::unique_ptr generateRandomItem(int32_t itemLevel, ItemGenerationType generationType) const; + std::unique_ptr generateRandomItem(int32_t itemLevel, ItemGenerationType generationType, const ItemFilter& filter) const; + + const ItemBase* randomItemBase(const ItemFilter& filter) const; + const ItemPrefixOrSuffixBase* randomPrefixOrSuffixBase(const ItemPrefixOrSuffixFilter& filter) const; + void applyRandomEnchantment(EquipmentItem& item, int32_t minLevel, int32_t maxLevel) const; void saveItem(const Item& item, FASaveGame::GameSaver& saver) const; std::unique_ptr loadItem(FASaveGame::GameLoader& loader) const; @@ -58,11 +47,7 @@ namespace FAWorld const ItemBaseHolder& getItemBaseHolder() const { return mItemBaseHolder; } private: - const DiabloExe::ExeItem& getInfo(ItemId id) const; - ItemBaseHolder mItemBaseHolder; - std::map mUniqueBaseItemIdToItemId; - const DiabloExe::DiabloExe& mExe; Random::Rng& mRng; }; } diff --git a/apps/freeablo/faworld/magiceffects/magiceffect.cpp b/apps/freeablo/faworld/magiceffects/magiceffect.cpp new file mode 100644 index 000000000..31052021e --- /dev/null +++ b/apps/freeablo/faworld/magiceffects/magiceffect.cpp @@ -0,0 +1,9 @@ +#include "magiceffect.h" +#include "magiceffectbase.h" + +namespace FAWorld +{ + MagicEffect::MagicEffect(const MagicEffectBase* base) : mBase(base) {} + + std::string MagicEffect::getFullDescription() const { return getBase()->mDescriptionFormatString; } +} diff --git a/apps/freeablo/faworld/magiceffects/magiceffect.h b/apps/freeablo/faworld/magiceffects/magiceffect.h new file mode 100644 index 000000000..01e0c9c47 --- /dev/null +++ b/apps/freeablo/faworld/magiceffects/magiceffect.h @@ -0,0 +1,33 @@ +#pragma once +#include + +namespace FASaveGame +{ + class GameSaver; + class GameLoader; +} + +namespace FAWorld +{ + class MagicEffectBase; + struct MagicStatModifiers; + + class MagicEffect + { + public: + explicit MagicEffect(const MagicEffectBase* base); + virtual ~MagicEffect() = default; + virtual void init(){}; + + virtual void save(FASaveGame::GameSaver& saver) const { UNUSED_PARAM(saver); }; + virtual void load(FASaveGame::GameLoader& loader) { UNUSED_PARAM(loader); }; + + virtual void apply(MagicStatModifiers& modifiers) const { UNUSED_PARAM(modifiers); } + virtual std::string getFullDescription() const; + + const MagicEffectBase* getBase() const { return mBase; } + + protected: + const MagicEffectBase* mBase = nullptr; + }; +} diff --git a/apps/freeablo/faworld/magiceffects/magiceffectbase.cpp b/apps/freeablo/faworld/magiceffects/magiceffectbase.cpp new file mode 100644 index 000000000..62d1ea287 --- /dev/null +++ b/apps/freeablo/faworld/magiceffects/magiceffectbase.cpp @@ -0,0 +1,15 @@ +#include "magiceffectbase.h" +#include "magiceffect.h" +#include + +namespace FAWorld +{ + MagicEffectBase::MagicEffectBase(const DiabloExe::ExeMagicItemEffect& exeEffect) : mParameter1(exeEffect.mMinEffect), mParameter2(exeEffect.mMaxEffect) + { + mDescriptionFormatString = "+Unimplemented magic effect"; + } + + std::unique_ptr MagicEffectBase::create() const { return std::make_unique(this); } + + MagicEffectBase::~MagicEffectBase() = default; +} \ No newline at end of file diff --git a/apps/freeablo/faworld/magiceffects/magiceffectbase.h b/apps/freeablo/faworld/magiceffects/magiceffectbase.h new file mode 100644 index 000000000..e645e953e --- /dev/null +++ b/apps/freeablo/faworld/magiceffects/magiceffectbase.h @@ -0,0 +1,28 @@ +#pragma once +#include +#include +#include + +namespace DiabloExe +{ + class ExeMagicItemEffect; +} + +namespace FAWorld +{ + class MagicEffect; + + class MagicEffectBase + { + public: + explicit MagicEffectBase(const DiabloExe::ExeMagicItemEffect& exeEffect); + virtual ~MagicEffectBase(); + + virtual std::unique_ptr create() const; + + public: + int32_t mParameter1 = 0; + int32_t mParameter2 = 0; + std::string mDescriptionFormatString; + }; +} \ No newline at end of file diff --git a/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffect.cpp b/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffect.cpp new file mode 100644 index 000000000..2a5c2523c --- /dev/null +++ b/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffect.cpp @@ -0,0 +1,67 @@ +#include "simplebuffdebuffeffect.h" +#include "simplebuffdebuffeffectbase.h" +#include +#include +#include +#include + +namespace FAWorld +{ + SimpleBuffDebuffEffect::SimpleBuffDebuffEffect(const SimpleBuffDebuffEffectBase* base) : super(base) {} + + void SimpleBuffDebuffEffect::init() + { + mValue = getBase()->mParameter2; // TODO: this should be randomised + } + + void SimpleBuffDebuffEffect::save(FASaveGame::GameSaver& saver) const + { + super::save(saver); + saver.save(mValue); + } + + void SimpleBuffDebuffEffect::load(FASaveGame::GameLoader& loader) + { + super::load(loader); + mValue = loader.load(); + } + + void SimpleBuffDebuffEffect::apply(MagicStatModifiers& modifiers) const + { + switch (getBase()->mAttribute) + { + case SimpleBuffDebuffEffectBase::Attribute::Strength: + modifiers.baseStats.strength += mValue; + break; + case SimpleBuffDebuffEffectBase::Attribute::Magic: + modifiers.baseStats.magic += mValue; + break; + case SimpleBuffDebuffEffectBase::Attribute::Dexterity: + modifiers.baseStats.dexterity += mValue; + break; + case SimpleBuffDebuffEffectBase::Attribute::Vitality: + modifiers.baseStats.vitality += mValue; + break; + case SimpleBuffDebuffEffectBase::Attribute::ToHit: + modifiers.toHit += mValue; + break; + case SimpleBuffDebuffEffectBase::Attribute::Life: + modifiers.maxLife += mValue; + break; + case SimpleBuffDebuffEffectBase::Attribute::Mana: + modifiers.maxMana += mValue; + break; + case SimpleBuffDebuffEffectBase::Attribute::Damage: + modifiers.meleeDamageBonus += mValue; + modifiers.rangedDamageBonus += mValue; + break; + case SimpleBuffDebuffEffectBase::Attribute::Armor: + modifiers.armorClass += mValue; + break; + } + } + + const SimpleBuffDebuffEffectBase* SimpleBuffDebuffEffect::getBase() const { return safe_downcast(mBase); } + + std::string SimpleBuffDebuffEffect::getFullDescription() const { return fmt::format(getBase()->mDescriptionFormatString, mValue); } +} \ No newline at end of file diff --git a/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffect.h b/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffect.h new file mode 100644 index 000000000..87cb61036 --- /dev/null +++ b/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffect.h @@ -0,0 +1,28 @@ +#pragma once +#include "magiceffect.h" + +namespace FAWorld +{ + class SimpleBuffDebuffEffectBase; + + class SimpleBuffDebuffEffect : public MagicEffect + { + using super = MagicEffect; + + public: + explicit SimpleBuffDebuffEffect(const SimpleBuffDebuffEffectBase* base); + ~SimpleBuffDebuffEffect() override = default; + void init() override; + + void save(FASaveGame::GameSaver& saver) const override; + void load(FASaveGame::GameLoader& loader) override; + + void apply(MagicStatModifiers& modifiers) const override; + std::string getFullDescription() const override; + + const SimpleBuffDebuffEffectBase* getBase() const; + + private: + int32_t mValue = 0; + }; +} diff --git a/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffectbase.cpp b/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffectbase.cpp new file mode 100644 index 000000000..3e75ef4bb --- /dev/null +++ b/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffectbase.cpp @@ -0,0 +1,116 @@ +#include "simplebuffdebuffeffectbase.h" +#include "simplebuffdebuffeffect.h" +#include +#include + +namespace FAWorld +{ + SimpleBuffDebuffEffectBase::SimpleBuffDebuffEffectBase(const DiabloExe::ExeMagicItemEffect& exeEffect) : MagicEffectBase(exeEffect) + { + switch (exeEffect.mEffect) + { + case ExeMagicEffectType::MinusStrength: + mParameter1 = -mParameter1; + mParameter2 = -mParameter2; + [[fallthrough]]; + case ExeMagicEffectType::PlusStrength: + mAttribute = Attribute::Strength; + break; + + case ExeMagicEffectType::MinusMagic: + mParameter1 = -mParameter1; + mParameter2 = -mParameter2; + [[fallthrough]]; + case ExeMagicEffectType::PlusMagic: + mAttribute = Attribute::Magic; + break; + + case ExeMagicEffectType::MinusDexterity: + mParameter1 = -mParameter1; + mParameter2 = -mParameter2; + [[fallthrough]]; + case ExeMagicEffectType::PlusDexterity: + mAttribute = Attribute::Dexterity; + break; + + case ExeMagicEffectType::MinusVitality: + mParameter1 = -mParameter1; + mParameter2 = -mParameter2; + [[fallthrough]]; + case ExeMagicEffectType::PlusVitality: + mAttribute = Attribute::Vitality; + break; + + case ExeMagicEffectType::MinusToHit: + mParameter1 = -mParameter1; + mParameter2 = -mParameter2; + [[fallthrough]]; + case ExeMagicEffectType::PlusToHit: + mAttribute = Attribute::ToHit; + break; + + case ExeMagicEffectType::MinusLife: + mParameter1 = -mParameter1; + mParameter2 = -mParameter2; + [[fallthrough]]; + case ExeMagicEffectType::PlusLife: + mAttribute = Attribute::Life; + break; + + case ExeMagicEffectType::MinusMana: + mParameter1 = -mParameter1; + mParameter2 = -mParameter2; + [[fallthrough]]; + case ExeMagicEffectType::PlusMana: + mAttribute = Attribute::Mana; + break; + + case ExeMagicEffectType::PlusDamage: + mAttribute = Attribute::Damage; + break; + + case ExeMagicEffectType::MinusArmor: + mParameter1 = -mParameter1; + mParameter2 = -mParameter2; + mAttribute = Attribute::Armor; + break; + default: + invalid_enum(ExeMagicEffectType, exeEffect.mEffect); + } + + switch (mAttribute) + { + case Attribute::Strength: + mDescriptionFormatString = "{:+} to strength"; + break; + case Attribute::Magic: + mDescriptionFormatString = "{:+} to magic"; + break; + case Attribute::Dexterity: + mDescriptionFormatString = "{:+} to dexterity"; + break; + case Attribute::Vitality: + mDescriptionFormatString = "{:+} to vitality"; + break; + case Attribute::ToHit: + mDescriptionFormatString = "chance to hit : {:+}%"; + break; + case Attribute::Life: + mDescriptionFormatString = "Hit Points : {:+}"; + break; + case Attribute::Mana: + mDescriptionFormatString = "Mana : {:+}"; + break; + case Attribute::Damage: + mDescriptionFormatString = "{:+} points to damage"; + break; + case Attribute::Armor: + mDescriptionFormatString = "armor class: {:+}"; + break; + } + } + + SimpleBuffDebuffEffectBase::~SimpleBuffDebuffEffectBase() = default; + + std::unique_ptr SimpleBuffDebuffEffectBase::create() const { return std::unique_ptr(new SimpleBuffDebuffEffect(this)); } +} diff --git a/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffectbase.h b/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffectbase.h new file mode 100644 index 000000000..4d3b2d4bd --- /dev/null +++ b/apps/freeablo/faworld/magiceffects/simplebuffdebuffeffectbase.h @@ -0,0 +1,32 @@ +#pragma once +#include "magiceffectbase.h" + +namespace FAWorld +{ + class SimpleBuffDebuffEffectBase : public MagicEffectBase + { + using super = MagicEffectBase; + + public: + explicit SimpleBuffDebuffEffectBase(const DiabloExe::ExeMagicItemEffect& exeEffect); + ~SimpleBuffDebuffEffectBase() override; + + std::unique_ptr create() const override; + + public: + enum class Attribute + { + Strength, + Magic, + Dexterity, + Vitality, + ToHit, + Life, + Mana, + Damage, + Armor, + }; + + Attribute mAttribute = {}; + }; +} diff --git a/apps/freeablo/faworld/missile/missileactorengagement.cpp b/apps/freeablo/faworld/missile/missileactorengagement.cpp index 0b45bb09e..784cecafb 100644 --- a/apps/freeablo/faworld/missile/missileactorengagement.cpp +++ b/apps/freeablo/faworld/missile/missileactorengagement.cpp @@ -1,5 +1,6 @@ #include "faworld/player.h" #include "missile.h" +#include #include namespace FAWorld::Missile @@ -38,7 +39,7 @@ namespace FAWorld::Missile toHit = Misc::clamp(toHit, missile.mToHitMinMaxCap.min, missile.mToHitMinMaxCap.max); int32_t roll = world->mRng->randomInRange(0, 99); - if (roll < toHit) + if (roll < toHit || DebugSettings::Instakill) { int32_t damage = missile.mRangedDamage; damage += world->mRng->randomInRange(missile.mRangedDamageBonusRange.start, missile.mRangedDamageBonusRange.end); diff --git a/apps/freeablo/faworld/monster.cpp b/apps/freeablo/faworld/monster.cpp index 4b8bdc077..476255cf4 100644 --- a/apps/freeablo/faworld/monster.cpp +++ b/apps/freeablo/faworld/monster.cpp @@ -3,7 +3,12 @@ #include "actor.h" #include "diabloexe/monster.h" #include "itemfactory.h" +#include #include +#include +#include +#include +#include #include #include @@ -104,18 +109,44 @@ namespace FAWorld void Monster::spawnItem() { - // TODO: Spawn magic, unique and special/quest items. + // TODO: Spawn unique and special/quest items, set gold drop amount - if (mWorld.mRng->randomInRange(0, 99) > 40) + if (DebugSettings::itemGenerationType == DebugSettings::ItemGenerationType::Normal && mWorld.mRng->randomInRange(0, 99) > 40) return; - ItemId itemId; - if (mWorld.mRng->randomInRange(0, 99) > 25) - itemId = ItemId::gold; + std::unique_ptr item; + if (DebugSettings::itemGenerationType == DebugSettings::ItemGenerationType::Normal && mWorld.mRng->randomInRange(0, 99) > 25) + { + item = mWorld.getItemFactory().generateBaseItem("gold"); + + // https://wheybags.gitlab.io/jarulfs-guide/#item-properties + int32_t difficultyFactor = 0; + + Difficulty difficulty = Difficulty::Normal; + switch (difficulty) + { + case Difficulty::Normal: + difficultyFactor = 0; + break; + case Difficulty::Nightmare: + difficultyFactor = 16; + break; + case Difficulty::Hell: + difficultyFactor = 32; + break; + } + + // TODO: there should be some special case here for hell and crypt levels, see Jarulf's guide link above + int32_t baseAmount = difficultyFactor + getLevel()->getLevelIndex(); + int32_t goldCount = mWorld.mRng->randomInRange(5 * baseAmount, 15 * baseAmount - 1); + + release_assert(item->getAsGoldItem()->trySetCount(std::min(goldCount, item->getAsGoldItem()->getBase()->mMaxCount))); + } else - itemId = mWorld.getItemFactory().randomItemId(ItemFilter::maxQLvl(mStats.mLevel)); + { + item = mWorld.getItemFactory().generateRandomItem(mStats.mLevel, ItemFactory::ItemGenerationType::Normal); + } - std::unique_ptr item = mWorld.getItemFactory().generateBaseItem(itemId); getLevel()->dropItemClosestEmptyTile(item, *this, getPos().current(), Misc::Direction(Misc::Direction8::none)); } diff --git a/apps/freeablo/faworld/player.cpp b/apps/freeablo/faworld/player.cpp index 93bce399d..be47aa487 100644 --- a/apps/freeablo/faworld/player.cpp +++ b/apps/freeablo/faworld/player.cpp @@ -3,22 +3,18 @@ #include "../engine/threadmanager.h" #include "../fagui/dialogmanager.h" #include "../fagui/guimanager.h" -#include "../fasavegame/gameloader.h" #include "actorstats.h" #include "diabloexe/characterstats.h" #include "equiptarget.h" #include "item/equipmentitem.h" #include "item/equipmentitembase.h" -#include "itemenums.h" -#include "itemmap.h" #include "missile/missile.h" #include "playerbehaviour.h" #include "spells.h" #include "world.h" -#include +#include #include #include -#include #include #include @@ -109,6 +105,9 @@ namespace FAWorld }; updateSprites(); + + if (DebugSettings::PlayersInvuln) + mInvuln = true; } void Player::calculateStats(LiveActorStats& stats, const ActorStats& actorStats) const @@ -130,10 +129,10 @@ namespace FAWorld ItemStats itemStats; mInventory.calculateItemBonuses(itemStats); - stats.baseStats.strength = charStats.strength + itemStats.baseStats.strength; - stats.baseStats.magic = charStats.magic + itemStats.baseStats.magic; - stats.baseStats.dexterity = charStats.dexterity + itemStats.baseStats.dexterity; - stats.baseStats.vitality = charStats.vitality + itemStats.baseStats.vitality; + stats.baseStats.strength = charStats.strength + itemStats.magicStatModifiers.baseStats.strength; + stats.baseStats.magic = charStats.magic + itemStats.magicStatModifiers.baseStats.magic; + stats.baseStats.dexterity = charStats.dexterity + itemStats.magicStatModifiers.baseStats.dexterity; + stats.baseStats.vitality = charStats.vitality + itemStats.magicStatModifiers.baseStats.vitality; stats.toHitMelee.bonus = 0; stats.toHitRanged.bonus = 0; @@ -149,13 +148,15 @@ namespace FAWorld { case PlayerClass::warrior: { - stats.maxLife = (int32_t)(FixedPoint(2) * FixedPoint(charStats.vitality) + FixedPoint(2) * FixedPoint(itemStats.baseStats.vitality) + - FixedPoint(2) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.maxLife) + 18) - .floor(); + stats.maxLife = + (int32_t)(FixedPoint(2) * FixedPoint(charStats.vitality) + FixedPoint(2) * FixedPoint(itemStats.magicStatModifiers.baseStats.vitality) + + FixedPoint(2) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.magicStatModifiers.maxLife) + 18) + .floor(); - stats.maxMana = (int32_t)(FixedPoint(1) * FixedPoint(charStats.magic) + FixedPoint(1) * FixedPoint(itemStats.baseStats.magic) + - FixedPoint(1) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.maxMana) - 1) - .floor(); + stats.maxMana = + (int32_t)(FixedPoint(1) * FixedPoint(charStats.magic) + FixedPoint(1) * FixedPoint(itemStats.magicStatModifiers.baseStats.magic) + + FixedPoint(1) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.magicStatModifiers.maxMana) - 1) + .floor(); stats.meleeDamage = (int32_t)((FixedPoint(charStats.strength) * actorStats.mLevel) / FixedPoint(100)).floor(); stats.rangedDamage = (int32_t)((FixedPoint(charStats.strength) * actorStats.mLevel) / FixedPoint(200)).floor(); @@ -198,13 +199,15 @@ namespace FAWorld } case PlayerClass::rogue: { - stats.maxLife = (int32_t)(FixedPoint(1) * FixedPoint(charStats.vitality) + FixedPoint("1.5") * FixedPoint(itemStats.baseStats.vitality) + - FixedPoint(2) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.maxLife) + 23) - .floor(); + stats.maxLife = + (int32_t)(FixedPoint(1) * FixedPoint(charStats.vitality) + FixedPoint("1.5") * FixedPoint(itemStats.magicStatModifiers.baseStats.vitality) + + FixedPoint(2) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.magicStatModifiers.maxLife) + 23) + .floor(); - stats.maxMana = (int32_t)(FixedPoint(1) * FixedPoint(charStats.magic) + FixedPoint("1.5") * FixedPoint(itemStats.baseStats.magic) + - FixedPoint(2) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.maxMana) + 5) - .floor(); + stats.maxMana = + (int32_t)(FixedPoint(1) * FixedPoint(charStats.magic) + FixedPoint("1.5") * FixedPoint(itemStats.magicStatModifiers.baseStats.magic) + + FixedPoint(2) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.magicStatModifiers.maxMana) + 5) + .floor(); stats.meleeDamage = (int32_t)(((FixedPoint(charStats.strength) + FixedPoint(charStats.dexterity)) * actorStats.mLevel) / FixedPoint(100)).floor(); @@ -247,13 +250,15 @@ namespace FAWorld } case PlayerClass::sorceror: { - stats.maxLife = (int32_t)(FixedPoint(1) * FixedPoint(charStats.vitality) + FixedPoint(1) * FixedPoint(itemStats.baseStats.vitality) + - FixedPoint(1) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.maxLife) + 9) - .floor(); + stats.maxLife = + (int32_t)(FixedPoint(1) * FixedPoint(charStats.vitality) + FixedPoint(1) * FixedPoint(itemStats.magicStatModifiers.baseStats.vitality) + + FixedPoint(1) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.magicStatModifiers.maxLife) + 9) + .floor(); - stats.maxMana = (int32_t)(FixedPoint(2) * FixedPoint(charStats.magic) + FixedPoint(2) * FixedPoint(itemStats.baseStats.magic) + - FixedPoint(2) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.maxMana) - 2) - .floor(); + stats.maxMana = + (int32_t)(FixedPoint(2) * FixedPoint(charStats.magic) + FixedPoint(2) * FixedPoint(itemStats.magicStatModifiers.baseStats.magic) + + FixedPoint(2) * FixedPoint(actorStats.mLevel) + FixedPoint(itemStats.magicStatModifiers.maxMana) - 2) + .floor(); stats.meleeDamage = (int32_t)((FixedPoint(charStats.strength) * actorStats.mLevel) / FixedPoint(100)).floor(); stats.rangedDamage = (int32_t)((FixedPoint(charStats.strength) * actorStats.mLevel) / FixedPoint(200)).floor(); @@ -304,9 +309,9 @@ namespace FAWorld stats.toHitRanged.bonus += actorStats.mLevel; // TODOHELLFIRE: Add in bonuses for barbarians and monks here, see https://wheybags.gitlab.io/jarulfs-guide/#monster-versus-player - stats.armorClass = (int32_t)(FixedPoint(stats.baseStats.dexterity) / FixedPoint(5) + itemStats.armorClass).floor(); - stats.toHitMelee.base = (int32_t)(FixedPoint(50) + FixedPoint(stats.baseStats.dexterity) / FixedPoint(2) + itemStats.toHit).floor(); - stats.toHitRanged.base = (int32_t)(FixedPoint(50) + FixedPoint(stats.baseStats.dexterity) + itemStats.toHit).floor(); + stats.armorClass = (int32_t)(FixedPoint(stats.baseStats.dexterity) / FixedPoint(5) + itemStats.magicStatModifiers.armorClass).floor(); + stats.toHitMelee.base = (int32_t)(FixedPoint(50) + FixedPoint(stats.baseStats.dexterity) / FixedPoint(2) + itemStats.magicStatModifiers.toHit).floor(); + stats.toHitRanged.base = (int32_t)(FixedPoint(50) + FixedPoint(stats.baseStats.dexterity) + itemStats.magicStatModifiers.toHit).floor(); stats.toHitMagic.base = (int32_t)(FixedPoint(50) + FixedPoint(stats.baseStats.magic)).floor(); stats.toHitMinMaxCap = {5, 95}; @@ -401,15 +406,15 @@ namespace FAWorld switch (mInventory.getBody()->getBase()->mType) { case ItemType::heavyArmor: - armor = "heavy"; + armor = "heavy-armor"; break; case ItemType::mediumArmor: - armor = "medium"; + armor = "medium-armor"; break; case ItemType::lightArmor: - armor = "light"; + armor = "light-armor"; break; default: @@ -463,7 +468,7 @@ namespace FAWorld auto getAnimation = [&](const std::string& animation) { FARender::SpriteLoader::PlayerSpriteKey spriteLookupKey({{"animation", animation}, {"class", classCode}, {"armor", armor}, {"weapon", weapon}}); - return renderer->mSpriteLoader.getSprite(renderer->mSpriteLoader.mPlayerSpriteDefinitions[spriteLookupKey]); + return renderer->mSpriteLoader.getSprite(renderer->mSpriteLoader.mPlayerSpriteDefinitions.at(spriteLookupKey)); }; mAnimation.setAnimationSprites(AnimState::dead, getAnimation("dead")); diff --git a/apps/freeablo/faworld/playerfactory.cpp b/apps/freeablo/faworld/playerfactory.cpp index c4d97ed84..f2cf7c784 100644 --- a/apps/freeablo/faworld/playerfactory.cpp +++ b/apps/freeablo/faworld/playerfactory.cpp @@ -1,7 +1,8 @@ #include "playerfactory.h" #include "diabloexe/characterstats.h" #include "equiptarget.h" -#include "itemenums.h" +#include "item/equipmentitem.h" +#include "item/itemprefixorsuffix.h" #include "itemfactory.h" #include "player.h" @@ -36,18 +37,6 @@ namespace FAWorld return player; } - void PlayerFactory::loadTestingKit(Player* player) const - { - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::buckler)); - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::shortBow)); - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::baseRingQlvl5)); - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::baseRingQlvl5)); - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::baseAmuletQlvl8)); - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::baseHelm)); - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::baseRags)); - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::baseDagger)); - } - void PlayerFactory::fillWithGold(Player* player) const { // function for testing @@ -73,7 +62,7 @@ namespace FAWorld bool hasSlots = true; while (hasSlots) { - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::potionOfHealing)); + player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem("potion_of_healing")); hasSlots = false; for (const BasicInventoryBox& slot : inv) @@ -84,35 +73,35 @@ namespace FAWorld void PlayerFactory::addWarriorItems(Player* player) const { - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::shortSword)); - std::unique_ptr buckler = mItemFactory.generateBaseItem(ItemId::buckler); + player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem("short_sword")); + std::unique_ptr buckler = mItemFactory.generateBaseItem("buckler"); player->mInventory.forcePlaceItem(buckler, MakeEquipTarget()); - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::club)); + player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem("club")); player->mInventory.placeGold(100, mItemFactory); for (int32_t i = 0; i < 2; ++i) - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::potionOfHealing)); + player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem("potion_of_healing")); } void PlayerFactory::addRogueItems(Player* player) const { - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::shortBow)); + player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem("short_bow")); player->mInventory.placeGold(100, mItemFactory); for (int32_t i = 0; i < 2; ++i) - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::potionOfHealing)); + player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem("potion_of_healing")); } void PlayerFactory::addSorcerorItems(Player* player) const { { - auto item = mItemFactory.generateBaseItem(ItemId::shortStaffOfChargedBolt); + auto item = mItemFactory.generateBaseItem("short_staff_of_charged_bolt"); // item.mMaxCharges = item.mCurrentCharges = 40; player->mInventory.autoPlaceItem(item); } player->mInventory.placeGold(100, mItemFactory); for (int32_t i = 0; i < 2; ++i) - player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem(ItemId::potionOfHealing)); + player->mInventory.autoPlaceItem(mItemFactory.generateBaseItem("potion_of_healing")); } } diff --git a/apps/freeablo/faworld/storedata.cpp b/apps/freeablo/faworld/storedata.cpp index 6b1ca918d..9dc64a401 100644 --- a/apps/freeablo/faworld/storedata.cpp +++ b/apps/freeablo/faworld/storedata.cpp @@ -8,16 +8,18 @@ namespace FAWorld { StoreData::StoreData(const ItemFactory& itemFactory) : mItemFactory(itemFactory) {} - void StoreData::regenerateGriswoldBasicItems(int32_t ilvl, Random::Rng& rng) + void StoreData::generateGriswoldBasicItems(int32_t itemLevel, Random::Rng& rng) { int32_t count = rng.randomInRange(10, 20); griswoldBasicItems.resize(count); - for (auto& item : griswoldBasicItems) + for (StoreItem& item : griswoldBasicItems) { - ItemId itemId = mItemFactory.randomItemId( - [ilvl](const DiabloExe::ExeItem& item) { return ItemFilter::maxQLvl(ilvl)(item) || ItemFilter::sellableGriswoldBasic()(item); }); + item.item = mItemFactory.generateRandomItem(itemLevel, ItemFactory::ItemGenerationType::OnlyBaseItems, [&](const ItemBase& base) { + static const auto excludedTypes = {ItemType::misc, ItemType::gold, ItemType::staff, ItemType::ring, ItemType::amulet}; + return std::count(excludedTypes.begin(), excludedTypes.end(), base.mType) == 0; + }); - item.item = mItemFactory.generateBaseItem(itemId); + item.item->init(); item.storeId = mNextItemId; mNextItemId++; } diff --git a/apps/freeablo/faworld/storedata.h b/apps/freeablo/faworld/storedata.h index f07de8e6c..a642bf066 100644 --- a/apps/freeablo/faworld/storedata.h +++ b/apps/freeablo/faworld/storedata.h @@ -24,7 +24,7 @@ namespace FAWorld uint32_t storeId = 0; }; - /// class for storing and regenerating items sold in various stores + /// class for storing items sold in various stores class StoreData { public: @@ -33,7 +33,7 @@ namespace FAWorld void save(FASaveGame::GameSaver& saver) const; void load(FASaveGame::GameLoader& loader); - void regenerateGriswoldBasicItems(int32_t ilvl, Random::Rng& rng); + void generateGriswoldBasicItems(int32_t itemLevel, Random::Rng& rng); public: std::vector griswoldBasicItems; diff --git a/apps/freeablo/faworld/world.cpp b/apps/freeablo/faworld/world.cpp index 9f990ce80..e376fdd20 100644 --- a/apps/freeablo/faworld/world.cpp +++ b/apps/freeablo/faworld/world.cpp @@ -33,10 +33,10 @@ namespace FAWorld this->setupObjectIdMappers(); if (mDiabloExe.isLoaded()) - regenerateStoreItems(); + generateStoreItems(); } - void World::regenerateStoreItems() { mStoreData->regenerateGriswoldBasicItems(10 /*placeholder*/, *mRng.get()); } + void World::generateStoreItems() { mStoreData->generateGriswoldBasicItems(10 /*placeholder*/, *mRng.get()); } void World::load(FASaveGame::GameLoader& loader) { @@ -47,6 +47,7 @@ namespace FAWorld new (this) World(tmp, 0U); } + mLoading = true; loader.currentlyLoadingWorld = this; mRng->load(loader); @@ -73,6 +74,7 @@ namespace FAWorld loader.runFunctionsToRunAtEnd(); + mLoading = false; loader.currentlyLoadingWorld = nullptr; } diff --git a/apps/freeablo/faworld/world.h b/apps/freeablo/faworld/world.h index 74fddcef1..173f2f530 100644 --- a/apps/freeablo/faworld/world.h +++ b/apps/freeablo/faworld/world.h @@ -74,7 +74,7 @@ namespace FAWorld void setLevel(int32_t levelNum, bool upStairsPos = true); GameLevel* getLevel(size_t level); void insertLevel(size_t level, GameLevel* gameLevel); - void regenerateStoreItems(); + void generateStoreItems(); Actor* getActorAt(const Misc::Point& point); @@ -113,6 +113,8 @@ namespace FAWorld const DiabloExe::DiabloExe& mDiabloExe; // TODO: something better than this std::unique_ptr mRng; + bool mLoading = false; // not serialised, for obvious reasons + private: std::unique_ptr mLevelRng; std::map mLevels; diff --git a/changelog.md b/changelog.md index 2dc315764..69b4b66f0 100644 --- a/changelog.md +++ b/changelog.md @@ -2,14 +2,15 @@ ## v0.5 [?? ??? ????] +- Added magic item generation (but only some effects actually work so far) +- Added accurate simulation of arrow damage +- Added game zoom with scroll wheel - Added healing at Pepin - Added healing and other potions - Added ability to move through levels by clicking on stairs - Added town portal spell -- Refactored rendering, FPS greatly improved and there should be no stuttering now -- Added game zoom with scroll wheel -- Added accurate simulation of arrow damage - Added debug grid that can be toggled with F11 +- Refactored rendering, FPS greatly improved and there should be no stuttering now - Fixed bug where arrows would miss stationary targets depending on the relative positions of shooter and target - Fixed bug where player would stop moving if you clicked and held your mouse without wiggling it - Fixed bug where game would crash if you pressed certain keys while on main menu diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 377ab1557..105cd0ce5 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -147,8 +147,8 @@ add_library(DiabloExe diabloexe/uniqueitem.h diabloexe/characterstats.h diabloexe/characterstats.cpp - diabloexe/affix.cpp - diabloexe/affix.h + diabloexe/exemagicitemeffect.cpp + diabloexe/exemagicitemeffect.h diabloexe/talkdata.h) target_link_libraries(DiabloExe Misc FAIO) set_target_properties(DiabloExe PROPERTIES COMPILE_FLAGS "${FA_COMPILER_FLAGS}") diff --git a/components/diabloexe/affix.cpp b/components/diabloexe/affix.cpp deleted file mode 100644 index 0b97b36ce..000000000 --- a/components/diabloexe/affix.cpp +++ /dev/null @@ -1,55 +0,0 @@ -#include "affix.h" -#include -#include -#include -#include -namespace DiabloExe -{ - - Affix::Affix(FAIO::FAFileObject& exe, size_t codeOffset) - { - uint32_t nameTemp = exe.read32(); - mEffect = exe.read32(); - mMinEffect = exe.read32(); - mMaxEffect = exe.read32(); - mQualLevel = exe.read32(); - - mBowjewelProb = exe.read8(); - mWSProb = exe.read8(); - mASProb = exe.read8(); - mUnknown0 = exe.read8(); - - mExcludedCombination0 = exe.read32(); - mExcludedCombination1 = exe.read32(); - - mCursed = exe.read32(); - mMinGold = exe.read32(); - mMaxGold = exe.read32(); - mMultiplier = exe.read32(); - mName = exe.readCStringFromWin32Binary(nameTemp, codeOffset); - } - - std::string Affix::dump() const - { - std::stringstream ss; - ss << "{" << std::endl - << "\tmName: " << mName << std::endl - << "\tmEffect: " << mEffect << ", " << std::endl - << "\tmMinEffect: " << (size_t)mMinEffect << "," << std::endl - << "\tmMaxEffect: " << (size_t)mMaxEffect << "," << std::endl - << "\tmQualEffect: " << (size_t)mQualLevel << "," << std::endl - << "\tmBowjewelProb: " << (size_t)mBowjewelProb << "," << std::endl - << "\tmWSProb: " << (size_t)mWSProb << "," << std::endl - << "\tmASProb: " << (size_t)mASProb << "," << std::endl - << "\tmUnknown: " << (size_t)mUnknown0 << "," << std::endl - << "\tmExcludedCombination0: " << (size_t)mExcludedCombination0 << "," << std::endl - << "\tmExcludedCombination1: " << (size_t)mExcludedCombination1 << "," << std::endl - << "\tmCursed: " << (size_t)mCursed << "," << std::endl - << "\tmMinGold: " << (size_t)mMinGold << "," << std::endl - << "\tmMaxGold: " << (size_t)mMaxGold << "," << std::endl - << "\tmMultiplier: " << (size_t)mMultiplier << "," << std::endl - << "}" << std::endl; - - return ss.str(); - } -} diff --git a/components/diabloexe/affix.h b/components/diabloexe/affix.h deleted file mode 100644 index 22e782458..000000000 --- a/components/diabloexe/affix.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once -#include -#include -#include -#include - -namespace DiabloExe -{ - // Affix -- Part of a word or phrase can be before or after the object Prefix for before Suffix for afterwords. - class Affix - { - public: - std::string mName; - - uint32_t mEffect; - uint32_t mMinEffect; - uint32_t mMaxEffect; - uint32_t mQualLevel; - uint8_t mBowjewelProb; - uint8_t mWSProb; - uint8_t mASProb; - uint8_t mUnknown0; - uint32_t mExcludedCombination0; - uint32_t mExcludedCombination1; - uint32_t mCursed; - uint32_t mMinGold; - uint32_t mMaxGold; - uint32_t mMultiplier; - - std::string dump() const; - Affix() {} - - private: - Affix(FAIO::FAFileObject& exe, size_t codeOffset); - friend class DiabloExe; - }; -} diff --git a/components/diabloexe/baseitem.h b/components/diabloexe/baseitem.h index 65bcffa89..c90ef1922 100644 --- a/components/diabloexe/baseitem.h +++ b/components/diabloexe/baseitem.h @@ -2,48 +2,9 @@ #include #include #include +#include #include -enum class ItemType -{ - misc = 0, - sword, - axe, - bow, - mace, - shield, - lightArmor, - helm, - mediumArmor, - heavyArmor, - staff, - gold, - ring, - amulet, - none = -1, -}; - -enum class ItemEquipType -{ - none = 0, - oneHanded = 1, - twoHanded = 2, - chest = 3, - head = 4, - ring = 5, - amulet = 6, -}; - -enum class ItemClass -{ - none = 0, - weapon, - armor, - jewelryAndConsumable, - gold, - quest, -}; - enum class ItemMiscId { none = 0, diff --git a/components/diabloexe/diabloexe.cpp b/components/diabloexe/diabloexe.cpp index 52c302306..40eea58e2 100644 --- a/components/diabloexe/diabloexe.cpp +++ b/components/diabloexe/diabloexe.cpp @@ -6,7 +6,7 @@ #include "settings/settings.h" #include "talkdata.h" #include -#include +#include #include #include #include @@ -66,7 +66,7 @@ namespace DiabloExe loadSoundFilenames(exe, codeOffset); loadBaseItems(exe, codeOffset); loadUniqueItems(exe, codeOffset); - loadAffixes(exe, codeOffset); + loadMagicItemEffects(exe, codeOffset); loadMissileGraphicsTable(exe, codeOffset); loadMissileDataTable(exe); loadSpellsTable(exe, codeOffset); @@ -315,6 +315,32 @@ namespace DiabloExe } } + class SimpleIdGenerator + { + public: + std::string generateIdFromName(const std::string_view name) + { + std::string idBase(name); + Misc::StringUtils::toLower(idBase); + Misc::StringUtils::replace(idBase, " ", "_"); + Misc::StringUtils::replace(idBase, "-", "_"); + idBase.erase(std::remove(idBase.begin(), idBase.end(), '\''), idBase.end()); + + std::string idName = idBase; + for (int32_t j = 1; usedIds.count(idName); j++) + { + idName = idBase + "_" + std::to_string(j); + Misc::StringUtils::replace(idName, "__", "_"); + } + + usedIds.insert(idName); + return idName; + } + + private: + std::unordered_set usedIds; + }; + void DiabloExe::loadBaseItems(FAIO::FAFileObject& exe, size_t codeOffset) { size_t itemOffset = mSettings->get("BaseItems", "itemOffset"); @@ -337,7 +363,7 @@ namespace DiabloExe el[1] = exe.read32(); constexpr auto invCellSize = 28; - std::unordered_set usedIds; + SimpleIdGenerator idGenerator; for (size_t i = 0; i < count; i++) { @@ -347,20 +373,7 @@ namespace DiabloExe if (tmp.name.empty()) continue; - std::string idBase = tmp.name; - Misc::StringUtils::toLower(idBase); - Misc::StringUtils::replace(idBase, " ", "_"); - Misc::StringUtils::replace(idBase, "-", "_"); - - std::string idName = idBase; - for (int32_t j = 1; usedIds.count(idName); j++) - { - idName = idBase + "_" + std::to_string(j); - Misc::StringUtils::replace(idName, "__", "_"); - } - - usedIds.insert(idName); - tmp.idName = idName; + tmp.idName = idGenerator.generateIdFromName(tmp.name); size_t dropGraphicsId = itemGraphicsIdToDropGraphicsId[tmp.invGraphicsId]; tmp.dropItemGraphicsPath = "items/" + mItemDropGraphicsFilename[dropGraphicsId] + ".cel"; @@ -399,20 +412,24 @@ namespace DiabloExe } } - void DiabloExe::loadAffixes(FAIO::FAFileObject& exe, size_t codeOffset) + void DiabloExe::loadMagicItemEffects(FAIO::FAFileObject& exe, size_t codeOffset) { - size_t affixOffset = mSettings->get("Affix", "affixOffset"); - size_t count = mSettings->get("Affix", "count"); + size_t offset = mSettings->get("MagicItemEffect", "magicItemEffectOffset"); + size_t count = mSettings->get("MagicItemEffect", "count"); + + SimpleIdGenerator idGenerator; for (size_t i = 0; i < count; i++) { - exe.FAfseek(affixOffset + 48 * i, SEEK_SET); - Affix tmp(exe, codeOffset); + exe.FAfseek(offset + 48 * i, SEEK_SET); + ExeMagicItemEffect tmp(exe, codeOffset); if (Misc::StringUtils::containsNonPrint(tmp.mName)) continue; if (tmp.mName.empty()) continue; - mAffixes.push_back(tmp); + + tmp.mIdName = idGenerator.generateIdFromName((tmp.mIsPrefix ? "prefix_" : "suffix_") + tmp.mName); + mMagicItemEffects.push_back(tmp); } } @@ -665,38 +682,38 @@ namespace DiabloExe { std::stringstream ss; - // ss << "Monsters: " << mMonsters.size() << std::endl; - // for (std::map::const_iterator it = mMonsters.begin(); it != mMonsters.end(); ++it) - // { - // ss << it->second.dump(); - // } - // - // ss << "Npcs: " << mNpcs.size() << std::endl; - // for (std::map::const_iterator it = mNpcs.begin(); it != mNpcs.end(); ++it) - // { - // ss << it->first << std::endl << it->second.dump(); - // } - // - // ss << "Character Stats: " << mCharacters.size() << std::endl - // << "Warrior" << std::endl - // << mCharacters.at("Warrior").dump() << "Rogue" << std::endl - // << mCharacters.at("Rogue").dump() << "Sorceror" << std::endl - // << mCharacters.at("Sorceror").dump(); + ss << "Monsters: " << mMonsters.size() << std::endl; + for (const auto& mMonster : mMonsters) + { + ss << mMonster.second.dump(); + } + + ss << "Npcs: " << mNpcs.size() << std::endl; + for (const auto& mNpc : mNpcs) + { + ss << mNpc.first << std::endl << mNpc.second.dump(); + } + + ss << "Character Stats: " << mCharacters.size() << std::endl + << "Warrior" << std::endl + << mCharacters.at("Warrior").dump() << "Rogue" << std::endl + << mCharacters.at("Rogue").dump() << "Sorceror" << std::endl + << mCharacters.at("Sorceror").dump(); ss << "Base Items: " << mBaseItems.size() << std::endl; - for (auto& baseItem : mBaseItems) + for (const auto& baseItem : mBaseItems) ss << baseItem.dump(); - // ss << "Unique Items: " << mUniqueItems.size() << std::endl; - // for (auto& uniqueItem : mUniqueItems) - // ss << uniqueItem.dump(); - // - // ss << "Affixes: " << mAffixes.size() << std::endl; - // for (auto& affix : mAffixes) - // ss << affix.dump(); + ss << "Unique Items: " << mUniqueItems.size() << std::endl; + for (const auto& uniqueItem : mUniqueItems) + ss << uniqueItem.dump(); + + ss << "Magic Item Effects: " << mMagicItemEffects.size() << std::endl; + for (const auto& effect : mMagicItemEffects) + ss << effect.dump(); return ss.str(); } - bool DiabloExe::isLoaded() const { return !mMonsters.empty() && !mNpcs.empty() && !mBaseItems.empty() && !mAffixes.empty(); } + bool DiabloExe::isLoaded() const { return !mMonsters.empty() && !mNpcs.empty() && !mBaseItems.empty() && !mMagicItemEffects.empty(); } } diff --git a/components/diabloexe/diabloexe.h b/components/diabloexe/diabloexe.h index de0f130bf..43a5aea73 100644 --- a/components/diabloexe/diabloexe.h +++ b/components/diabloexe/diabloexe.h @@ -1,5 +1,4 @@ #pragma once -#include "../../apps/freeablo/faworld/itemenums.h" #include #include #include @@ -18,7 +17,7 @@ namespace DiabloExe class ExeItem; class CharacterStats; class UniqueItem; - class Affix; + class ExeMagicItemEffect; class FontData { @@ -110,7 +109,7 @@ namespace DiabloExe const FontData& getFontData(const char* fontName) const; const std::vector& getBaseItems() const { return mBaseItems; } const std::vector& getUniqueItems() const { return mUniqueItems; } - const std::vector& getAffixes() const { return mAffixes; } + const std::vector& getMagicItemEffects() const { return mMagicItemEffects; } const std::map& getMissileGraphicsTable() const { return mMissileGraphicsTable; } const std::map& getMissileDataTable() const { return mMissileDataTable; } const std::map& getSpellsDataTable() const { return mSpellsDataTable; } @@ -133,7 +132,7 @@ namespace DiabloExe void loadNpcs(FAIO::FAFileObject& exe); void loadBaseItems(FAIO::FAFileObject& exe, size_t codeOffset); void loadUniqueItems(FAIO::FAFileObject& exe, size_t codeOffset); - void loadAffixes(FAIO::FAFileObject& exe, size_t codeOffset); + void loadMagicItemEffects(FAIO::FAFileObject& exe, size_t codeOffset); void loadCharacterStats(FAIO::FAFileObject& exe); void loadTownerAnimation(FAIO::FAFileObject& exe); void loadMissileGraphicsTable(FAIO::FAFileObject& exe, size_t codeOffset); @@ -148,7 +147,7 @@ namespace DiabloExe std::map mCharacters; std::vector mBaseItems; std::vector mUniqueItems; - std::vector mAffixes; + std::vector mMagicItemEffects; std::vector> mTownerAnimation; std::vector mItemDropGraphicsFilename; std::vector mSoundFilename; diff --git a/components/diabloexe/exemagicitemeffect.cpp b/components/diabloexe/exemagicitemeffect.cpp new file mode 100644 index 000000000..1e24d9fcb --- /dev/null +++ b/components/diabloexe/exemagicitemeffect.cpp @@ -0,0 +1,162 @@ +#include "exemagicitemeffect.h" +#include +#include +#include + +std::string exeMagicEffectTypeToString(ExeMagicEffectType type) +{ +#define GENERATE(X) \ + case ExeMagicEffectType::X: \ + return #X; + + switch (type) + { + GENERATE(PlusToHit) + GENERATE(MinusToHit) + GENERATE(PlusDamagePercent) + GENERATE(MinusDamagePercent) + GENERATE(PlusToHitAndDamagePercent) + GENERATE(MinusToHitAndDamagePercent) + GENERATE(PlusArmorPercent) + GENERATE(MinusArmorPercent) + GENERATE(PlusResistFire) + GENERATE(PlusResistLightning) + GENERATE(PlusResistMagic) + GENERATE(PlusResistAll) + GENERATE(PlusSpellLevels) + GENERATE(PlusCharges) + GENERATE(PlusDamageFire) + GENERATE(PlusDamageLightning) + GENERATE(PlusStrength) + GENERATE(MinusStrength) + GENERATE(PlusMagic) + GENERATE(MinusMagic) + GENERATE(PlusDexterity) + GENERATE(MinusDexterity) + GENERATE(PlusVitality) + GENERATE(MinusVitality) + GENERATE(PlusAllAttributes) + GENERATE(MinusAllAttributes) + GENERATE(PlusDamageTaken) + GENERATE(MinusDamageTaken) + GENERATE(PlusLife) + GENERATE(MinusLife) + GENERATE(PlusMana) + GENERATE(MinusMana) + GENERATE(PlusDurabilityPercent) + GENERATE(CurseDurabilityPercent) + GENERATE(Indestructible) + GENERATE(PlusLightPercent) + GENERATE(CurseLightPercent) + GENERATE(MultipleArrows) + GENERATE(PlusFireArrowDamage) + GENERATE(PlusLightningArrowDamage) + GENERATE(UniqueIcon) + GENERATE(Deal1To3DamageToAttackers) + GENERATE(ZeroMana) + GENERATE(UserCantHeal) + GENERATE(AbsorbHalfTrapDamage) + GENERATE(Knockback) + GENERATE(HitMonsterCantHeal) + GENERATE(StealMana) + GENERATE(StealLife) + GENERATE(DamageArmor) + GENERATE(FastAttack) + GENERATE(FastHitRecovery) + GENERATE(FastBlock) + GENERATE(PlusDamage) + GENERATE(RandomSpeedArrows) + GENERATE(SetItemDamage) + GENERATE(SetDurability) + GENERATE(NoStrengthRequirement) + GENERATE(SetCharges) + GENERATE(FastAttack2) + GENERATE(OneHanded) + GENERATE(Plus200PercentOnDemons) + GENERATE(AllResistancesZero) + GENERATE(ConstantlyLoseLife) + GENERATE(LifeSteal) + GENERATE(Infravision) + GENERATE(SetArmor) + GENERATE(AddArmorToLife) + GENERATE(Add10PercentOfManaToArmor) + GENERATE(PlusLevelDependentResistFire) + GENERATE(MinusArmor) + } + + invalid_enum(ExeMagicEffectType, type); + +#undef GENERATE +} + +namespace DiabloExe +{ + ExeMagicItemEffect::ExeMagicItemEffect(FAIO::FAFileObject& exe, size_t codeOffset) + { + // https://web.archive.org/web/20151015004713/http://www.thedark5.com/info/mod/mod1.html + + uint32_t nameTemp = exe.read32(); + + mEffect = ExeMagicEffectType(exe.read32()); + mMinEffect = exe.read32(); + mMaxEffect = exe.read32(); + mQualLevel = exe.read32(); + mTargetTypesBitmask = MagicalItemTargetBitmask(exe.read32()); + mCompatibilityBitmask = CompatibilityBitMask(exe.read32()); + mDoubleProbabilityForPrefixes = bool(exe.read32()); + mNotCursed = bool(exe.read32()); + mMinGold = exe.read32(); + mMaxGold = exe.read32(); + mGoldMultiplier = exe.read32(); + + mName = exe.readCStringFromWin32Binary(nameTemp, codeOffset); + mIsPrefix = !mName.empty() && std::isupper(mName[0]); + } + + std::string ExeMagicItemEffect::dump() const + { + std::stringstream ss; + + std::string targetTypeBitmaskString; + { + if (int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::Jewelery)) + ss << "Jewelery | "; + if (int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::Bow)) + ss << "Bow | "; + if (int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::Staff)) + ss << "Staff | "; + if (int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::OtherWeapons)) + ss << "OtherWeapons | "; + if (int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::Shield)) + ss << "Shield | "; + if (int32_t(mTargetTypesBitmask) & int32_t(MagicalItemTargetBitmask::Armor)) + ss << "Armor | "; + + targetTypeBitmaskString = ss.str(); + if (targetTypeBitmaskString.empty()) + targetTypeBitmaskString = "None"; + else + targetTypeBitmaskString = targetTypeBitmaskString.substr(0, targetTypeBitmaskString.size() - 3); + } + + ss.str(""); + ss << "{" << std::endl + << "\tmName: " << mName << std::endl + << "\tmIdName: " << mIdName << std::endl + << "\tmEffectType: " << (mIsPrefix ? "Prefix" : "Suffix") << std::endl + << "\tmEffect: " << exeMagicEffectTypeToString(mEffect) << ", " << std::endl + << "\tmMinEffect: " << mMinEffect << "," << std::endl + << "\tmMaxEffect: " << mMaxEffect << "," << std::endl + << "\tmQualLevel: " << mQualLevel << "," << std::endl + << "\tmTargetTypeBitmask: " << targetTypeBitmaskString << "," << std::endl + << "\tmCompatibilityBitmask: " << int32_t(mCompatibilityBitmask) << "," << std::endl + << "\tmDoubleProbabilityForPrefixes: " << mDoubleProbabilityForPrefixes << "," << std::endl + << "\tmNotCursed: " << mNotCursed << "," << std::endl + << "\tmMinGold: " << mMinGold << "," << std::endl + << "\tmMaxGold: " << mMaxGold << "," << std::endl + << "\tmGoldMultiplier: " << mGoldMultiplier << "," << std::endl + << "}" << std::endl; + + return ss.str(); + } +} diff --git a/components/diabloexe/exemagicitemeffect.h b/components/diabloexe/exemagicitemeffect.h new file mode 100644 index 000000000..7c5b92113 --- /dev/null +++ b/components/diabloexe/exemagicitemeffect.h @@ -0,0 +1,131 @@ +#pragma once +#include +#include +#include +#include +#include + +enum class ExeMagicEffectType +{ + PlusToHit = 0x00, + MinusToHit = 0x01, + PlusDamagePercent = 0x02, + MinusDamagePercent = 0x03, + PlusToHitAndDamagePercent = 0x04, + MinusToHitAndDamagePercent = 0x05, + PlusArmorPercent = 0x06, + MinusArmorPercent = 0x07, + PlusResistFire = 0x08, + PlusResistLightning = 0x09, + PlusResistMagic = 0x0A, + PlusResistAll = 0x0B, + PlusSpellLevels = 0x0E, + PlusCharges = 0x0F, + PlusDamageFire = 0x10, + PlusDamageLightning = 0x11, + PlusStrength = 0x13, + MinusStrength = 0x14, + PlusMagic = 0x15, + MinusMagic = 0x16, + PlusDexterity = 0x17, + MinusDexterity = 0x18, + PlusVitality = 0x19, + MinusVitality = 0x1A, + PlusAllAttributes = 0x1B, + MinusAllAttributes = 0x1C, + PlusDamageTaken = 0x1D, + MinusDamageTaken = 0x1E, + PlusLife = 0x1F, + MinusLife = 0x20, + PlusMana = 0x21, + MinusMana = 0x22, + PlusDurabilityPercent = 0x23, + CurseDurabilityPercent = 0x24, + Indestructible = 0x25, + PlusLightPercent = 0x26, + CurseLightPercent = 0x27, + // Unused = 0x28, + MultipleArrows = 0x29, + PlusFireArrowDamage = 0x2A, + PlusLightningArrowDamage = 0x2B, + UniqueIcon = 0x2C, + Deal1To3DamageToAttackers = 0x2D, + ZeroMana = 0x2E, + UserCantHeal = 0x2F, + // Unused = 0x30, + // Unused = 0x31, + // Unused = 0x32, + // Unused = 0x33, + AbsorbHalfTrapDamage = 0x34, + Knockback = 0x35, + HitMonsterCantHeal = 0x36, + StealMana = 0x37, + StealLife = 0x38, + DamageArmor = 0x39, + FastAttack = 0x3A, + FastHitRecovery = 0x3B, + FastBlock = 0x3C, + PlusDamage = 0x3D, + RandomSpeedArrows = 0x3E, + SetItemDamage = 0x3F, + SetDurability = 0x40, + NoStrengthRequirement = 0x41, + SetCharges = 0x42, + FastAttack2 = 0x43, + OneHanded = 0x44, + Plus200PercentOnDemons = 0x45, + AllResistancesZero = 0x46, + // Unused = 0x47, + ConstantlyLoseLife = 0x48, + LifeSteal = 0x49, + Infravision = 0x4A, + SetArmor = 0x4B, + AddArmorToLife = 0x4C, + Add10PercentOfManaToArmor = 0x4D, + PlusLevelDependentResistFire = 0x4E, + MinusArmor = 0x4F, +}; + +std::string exeMagicEffectTypeToString(ExeMagicEffectType type); + +namespace DiabloExe +{ + class ExeMagicItemEffect + { + public: + std::string mName; + std::string mIdName; + bool mIsPrefix = false; + + ExeMagicEffectType mEffect = {}; + int32_t mMinEffect = 0; + int32_t mMaxEffect = 0; + int32_t mQualLevel = 0; + MagicalItemTargetBitmask mTargetTypesBitmask = MagicalItemTargetBitmask::None; + + enum class CompatibilityBitMask + { + All = 0, + A = 1, // I really can't tell what these should be called + B = 16, + }; + + // This determines the effects compatibility with other effects. + // eg: you can combine an A prefix with an All suffix, or an A suffix, + // but you cannot combine an A prefix with a B prefix, or vice versa. + CompatibilityBitMask mCompatibilityBitmask = CompatibilityBitMask::All; + + bool mDoubleProbabilityForPrefixes = false; + bool mNotCursed = false; + int mMinGold = 0; + int mMaxGold = 0; + int mGoldMultiplier = 0; + + std::string dump() const; + ExeMagicItemEffect() = default; + + private: + ExeMagicItemEffect(FAIO::FAFileObject& exe, size_t codeOffset); + friend class DiabloExe; + }; +} diff --git a/components/misc/commonenums.h b/components/misc/commonenums.h new file mode 100644 index 000000000..6f45004ca --- /dev/null +++ b/components/misc/commonenums.h @@ -0,0 +1,52 @@ +#pragma once + +enum class MagicalItemTargetBitmask +{ + None = 0x0, + Jewelery = 0x1, + Bow = 0x10, + Staff = 0x100, + OtherWeapons = 0x1000, + Shield = 0x10000, + Armor = 0x100000, +}; + +enum class ItemType +{ + misc = 0, + sword, + axe, + bow, + mace, + shield, + lightArmor, + helm, + mediumArmor, + heavyArmor, + staff, + gold, + ring, + amulet, + none = -1, +}; + +enum class ItemEquipType +{ + none = 0, + oneHanded = 1, + twoHanded = 2, + chest = 3, + head = 4, + ring = 5, + amulet = 6, +}; + +enum class ItemClass +{ + none = 0, + weapon, + armor, + jewelryAndConsumable, + gold, + quest, +}; \ No newline at end of file diff --git a/resources/exeversions/v109.ini b/resources/exeversions/v109.ini index 5ae562bb6..f0781decf 100644 --- a/resources/exeversions/v109.ini +++ b/resources/exeversions/v109.ini @@ -24,8 +24,8 @@ bigtgoldFrameCount=55 uniqueItemOffset=0x7BC68 count=88 -[Affix] -affixOffset=0x79AA8 +[MagicItemEffect] +magicItemEffectOffset=0x79AA8 count=180 [CharacterStats]