diff --git a/apps/freeablo/CMakeLists.txt b/apps/freeablo/CMakeLists.txt index 0e71fec36..f3bf252ed 100644 --- a/apps/freeablo/CMakeLists.txt +++ b/apps/freeablo/CMakeLists.txt @@ -74,6 +74,8 @@ add_library(freeablo_lib # split into a library so I can link to it from tests faworld/playerinput.h faworld/position.cpp faworld/position.h + faworld/potion.cpp + faworld/potion.h faworld/storedata.cpp faworld/storedata.h faworld/target.cpp diff --git a/apps/freeablo/fagui/guimanager.cpp b/apps/freeablo/fagui/guimanager.cpp index 5059a0e05..87da4fc4c 100644 --- a/apps/freeablo/fagui/guimanager.cpp +++ b/apps/freeablo/fagui/guimanager.cpp @@ -1,6 +1,7 @@ #include "guimanager.h" #include "../engine/enginemain.h" #include "../engine/localinputhandler.h" +#include "../engine/threadmanager.h" #include "../farender/renderer.h" #include "../fasavegame/gameloader.h" #include "../faworld/actorstats.h" @@ -27,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -184,6 +186,11 @@ namespace FAGui mGoldSplitTarget = target; mGoldSplitCnt = 0; } + if (item->getAsUsableItem()) + { + FAWorld::PlayerInput::UseItemData input{target}; + Engine::EngineMain::get()->getLocalInputHandler()->addInput(FAWorld::PlayerInput(input, mPlayer->getId())); + } } void GuiManager::item(nk_context* ctx, FAWorld::EquipTarget target, RectOrVec2 placement, ItemHighlightInfo highlight, bool checkerboarded) diff --git a/apps/freeablo/faworld/actor.cpp b/apps/freeablo/faworld/actor.cpp index b07eb553c..230da3098 100644 --- a/apps/freeablo/faworld/actor.cpp +++ b/apps/freeablo/faworld/actor.cpp @@ -226,6 +226,7 @@ namespace FAWorld void Actor::heal(int32_t toHeal) { mStats.getHp().add(toHeal); } void Actor::restoreMana() { mStats.getMana().current = mStats.getMana().max; } + void Actor::restoreMana(int32_t toRestore) { mStats.getMana().add(toRestore); } void Actor::stopMoving(std::optional direction) { mMoveHandler.stopMoving(*this, direction); } diff --git a/apps/freeablo/faworld/actor.h b/apps/freeablo/faworld/actor.h index dfc5958dd..4528ec8b1 100644 --- a/apps/freeablo/faworld/actor.h +++ b/apps/freeablo/faworld/actor.h @@ -67,6 +67,7 @@ namespace FAWorld void heal(); void heal(int32_t toHeal); void restoreMana(); + void restoreMana(int32_t toRestore); void stopMoving(std::optional direction = std::nullopt); virtual void die(); bool isDead() const; diff --git a/apps/freeablo/faworld/actorstats.h b/apps/freeablo/faworld/actorstats.h index 9bdc22c86..1090c4512 100644 --- a/apps/freeablo/faworld/actorstats.h +++ b/apps/freeablo/faworld/actorstats.h @@ -29,6 +29,11 @@ namespace FAWorld int32_t dexterity = 0; int32_t vitality = 0; + int32_t maxStrength = 0; + int32_t maxMagic = 0; + int32_t maxDexterity = 0; + int32_t maxVitality = 0; + bool operator==(const BaseStats& other) { return strength == other.strength && magic == other.magic && dexterity == other.dexterity && vitality == other.vitality; diff --git a/apps/freeablo/faworld/inventory.cpp b/apps/freeablo/faworld/inventory.cpp index 07da73d39..48ed657cf 100644 --- a/apps/freeablo/faworld/inventory.cpp +++ b/apps/freeablo/faworld/inventory.cpp @@ -392,7 +392,7 @@ namespace FAWorld case EquipTargetType::inventory: break; case EquipTargetType::belt: - ok = cursorItem->getAsMiscItem() && cursorItem->getAsMiscItem()->getBase()->isBeltEquippable(); + ok = cursorItem->getAsUsableItem() && cursorItem->getAsUsableItem()->getBase()->isBeltEquippable(); break; case EquipTargetType::head: ok = cursorItem->getBase()->getEquipType() == ItemEquipType::head; diff --git a/apps/freeablo/faworld/item/item.h b/apps/freeablo/faworld/item/item.h index 463a48595..e9725ce5e 100644 --- a/apps/freeablo/faworld/item/item.h +++ b/apps/freeablo/faworld/item/item.h @@ -39,7 +39,7 @@ namespace FAWorld virtual EquipmentItem* getAsEquipmentItem() { return nullptr; } const EquipmentItem* getAsEquipmentItem() const { return const_cast(this)->getAsEquipmentItem(); } virtual UsableItem* getAsUsableItem() { return nullptr; } - const UsableItem* getAsMiscItem() const { return const_cast(this)->getAsUsableItem(); } + const UsableItem* getAsUsableItem() const { return const_cast(this)->getAsUsableItem(); } virtual GoldItem* getAsGoldItem() { return nullptr; } const GoldItem* getAsGoldItem() const { return const_cast(this)->getAsGoldItem(); } diff --git a/apps/freeablo/faworld/item/usableitem.cpp b/apps/freeablo/faworld/item/usableitem.cpp index f310c3e43..27c8c582d 100644 --- a/apps/freeablo/faworld/item/usableitem.cpp +++ b/apps/freeablo/faworld/item/usableitem.cpp @@ -1,5 +1,6 @@ #include "usableitem.h" #include "usableitembase.h" +#include #include namespace FAWorld @@ -7,4 +8,12 @@ namespace FAWorld UsableItem::UsableItem(const UsableItemBase* base) : super(base) {} const UsableItemBase* UsableItem::getBase() const { return safe_downcast(mBase); } + + void UsableItem::applyEffect(Player& user) + { + if (!getBase()->mUseSoundPath.empty()) + Engine::ThreadManager::get()->playSound(getBase()->mUseSoundPath); + + getBase()->mEffect(user); + } } diff --git a/apps/freeablo/faworld/item/usableitem.h b/apps/freeablo/faworld/item/usableitem.h index 628e1dbb8..afe5647d3 100644 --- a/apps/freeablo/faworld/item/usableitem.h +++ b/apps/freeablo/faworld/item/usableitem.h @@ -4,6 +4,7 @@ namespace FAWorld { class UsableItemBase; + class Player; class UsableItem final : public Item { @@ -17,6 +18,8 @@ namespace FAWorld UsableItem* getAsUsableItem() override { return this; } + void applyEffect(Player& user); + const UsableItemBase* getBase() const; }; } diff --git a/apps/freeablo/faworld/item/usableitembase.cpp b/apps/freeablo/faworld/item/usableitembase.cpp index 51dfe083d..07ac432d3 100644 --- a/apps/freeablo/faworld/item/usableitembase.cpp +++ b/apps/freeablo/faworld/item/usableitembase.cpp @@ -1,9 +1,95 @@ #include "usableitembase.h" #include +#include namespace FAWorld { - UsableItemBase::UsableItemBase(const DiabloExe::ExeItem& exeItem) : super(exeItem) {} + UsableItemBase::UsableItemBase(const DiabloExe::ExeItem& exeItem) : super(exeItem) + { + switch (exeItem.miscId) + { + case ItemMiscId::potionOfHealing: + { + mEffect = Potion::restoreHp; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + case ItemMiscId::potionOfFullHealing: + { + mEffect = Potion::restoreHpFull; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + case ItemMiscId::potionOfMana: + { + mEffect = Potion::restoreMana; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + case ItemMiscId::potionOfFullMana: + { + mEffect = Potion::restoreManaFull; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + case ItemMiscId::potionOfRejuvenation: + { + mEffect = [](Player& player) { + Potion::restoreHp(player); + Potion::restoreMana(player); + }; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + case ItemMiscId::potionOfFullRejuvenation: + { + mEffect = [](Player& player) { + Potion::restoreHpFull(player); + Potion::restoreManaFull(player); + }; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + case ItemMiscId::elixirOfDexterity: + { + mEffect = [](Player& player) { Potion::increaseDexterity(player, 1); }; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + case ItemMiscId::elixirOfMagic: + { + mEffect = [](Player& player) { Potion::increaseMagic(player, 1); }; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + case ItemMiscId::elixirOfVitality: + { + mEffect = [](Player& player) { Potion::increaseVitality(player, 1); }; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + case ItemMiscId::elixirOfStrength: + { + mEffect = [](Player& player) { Potion::increaseStrength(player, 1); }; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + case ItemMiscId::spectralElixir: + { + mEffect = [](Player& player) { + Potion::increaseStrength(player, 3); + Potion::increaseVitality(player, 3); + Potion::increaseMagic(player, 3); + Potion::increaseDexterity(player, 3); + }; + mUseSoundPath = "sfx/items/invpot.wav"; + break; + } + default: + mEffect = [](Player&) {}; + break; + } + } bool UsableItemBase::isBeltEquippable() const { return mSize == Vec2i(1, 1); } diff --git a/apps/freeablo/faworld/item/usableitembase.h b/apps/freeablo/faworld/item/usableitembase.h index 32ca62d6b..d20ea3adc 100644 --- a/apps/freeablo/faworld/item/usableitembase.h +++ b/apps/freeablo/faworld/item/usableitembase.h @@ -1,19 +1,22 @@ #pragma once #include "itembase.h" +#include namespace FAWorld { + class Player; + class UsableItemBase final : public ItemBase { using super = ItemBase; public: explicit UsableItemBase(const DiabloExe::ExeItem& exeItem); - std::unique_ptr createItem() const override; - bool isBeltEquippable() const; public: + std::function mEffect; + std::string mUseSoundPath; }; } diff --git a/apps/freeablo/faworld/player.cpp b/apps/freeablo/faworld/player.cpp index a6726a88b..93bce399d 100644 --- a/apps/freeablo/faworld/player.cpp +++ b/apps/freeablo/faworld/player.cpp @@ -30,6 +30,36 @@ namespace FAWorld { mStats.initialise(initialiseActorStats(charStats)); mStats.mLevelXpCounts = charStats.mNextLevelExp; + switch (mPlayerClass) + { + // https://wheybags.gitlab.io/jarulfs-guide/#maximum-stats for max base stats numbers + case PlayerClass::warrior: + { + mStats.baseStats.maxStrength = 250; + mStats.baseStats.maxMagic = 50; + mStats.baseStats.maxDexterity = 60; + mStats.baseStats.maxVitality = 100; + break; + } + case PlayerClass::rogue: + { + mStats.baseStats.maxStrength = 50; + mStats.baseStats.maxMagic = 70; + mStats.baseStats.maxDexterity = 250; + mStats.baseStats.maxVitality = 80; + break; + } + case PlayerClass::sorceror: + { + mStats.baseStats.maxStrength = 45; + mStats.baseStats.maxMagic = 250; + mStats.baseStats.maxDexterity = 85; + mStats.baseStats.maxVitality = 80; + break; + } + case PlayerClass::none: + break; + } mFaction = Faction::heaven(); mMoveHandler.mPathRateLimit = World::getTicksInPeriod("0.1"); // allow players to repath much more often than other actors @@ -570,6 +600,11 @@ namespace FAWorld restoreMana(); } + void Player::addStrength(int32_t delta) { mStats.baseStats.strength = std::min(mStats.baseStats.strength + delta, mStats.baseStats.maxStrength); } + void Player::addMagic(int32_t delta) { mStats.baseStats.magic = std::min(mStats.baseStats.magic + delta, mStats.baseStats.maxMagic); } + void Player::addDexterity(int32_t delta) { mStats.baseStats.dexterity = std::min(mStats.baseStats.dexterity + delta, mStats.baseStats.maxDexterity); } + void Player::addVitality(int32_t delta) { mStats.baseStats.vitality = std::min(mStats.baseStats.vitality + delta, mStats.baseStats.maxVitality); } + BaseStats Player::initialiseActorStats(const DiabloExe::CharacterStats& from) { BaseStats baseStats; diff --git a/apps/freeablo/faworld/player.h b/apps/freeablo/faworld/player.h index 340697a14..1fe8f31ea 100644 --- a/apps/freeablo/faworld/player.h +++ b/apps/freeablo/faworld/player.h @@ -35,6 +35,11 @@ namespace FAWorld virtual void calculateStats(LiveActorStats& stats, const ActorStats& actorStats) const override; + void addStrength(int32_t delta); + void addMagic(int32_t delta); + void addDexterity(int32_t delta); + void addVitality(int32_t delta); + void moveToLevel(GameLevel* level, bool placeAtUpStairs); // This isn't serialised as it must be set before saving can occur. diff --git a/apps/freeablo/faworld/playerbehaviour.cpp b/apps/freeablo/faworld/playerbehaviour.cpp index f2e5ad4e1..d1459ba80 100644 --- a/apps/freeablo/faworld/playerbehaviour.cpp +++ b/apps/freeablo/faworld/playerbehaviour.cpp @@ -5,7 +5,9 @@ #include "fasavegame/gameloader.h" #include "input/inputmanager.h" #include "item/itembase.h" +#include "item/usableitem.h" #include "player.h" +#include "potion.h" #include "storedata.h" #include #include @@ -206,6 +208,17 @@ namespace FAWorld return; } + case PlayerInput::Type::UseItem: + { + if (mPlayer->mInventory.getItemAt(input.mData.dataUseItem.target) && + mPlayer->mInventory.getItemAt(input.mData.dataUseItem.target)->getAsUsableItem()) + { + std::unique_ptr item = mPlayer->mInventory.remove(input.mData.dataUseItem.target); + item->getAsUsableItem()->applyEffect(*mPlayer); + } + + return; + } case PlayerInput::Type::PlayerJoined: case PlayerInput::Type::PlayerLeft: { diff --git a/apps/freeablo/faworld/playerinput.cpp b/apps/freeablo/faworld/playerinput.cpp index c2ee4555f..c124b1e7d 100644 --- a/apps/freeablo/faworld/playerinput.cpp +++ b/apps/freeablo/faworld/playerinput.cpp @@ -186,6 +186,10 @@ namespace FAWorld shopkeeperId = loader.load(); } + void PlayerInput::UseItemData::save(Serial::Saver& saver) const { target.save(saver); } + + void PlayerInput::UseItemData::load(Serial::Loader& loader) { target.load(loader); } + void PlayerInput::removeUnnecessaryInputs(std::vector& inputs) { // This should remove all but the last of each input type, per player. diff --git a/apps/freeablo/faworld/playerinput.h b/apps/freeablo/faworld/playerinput.h index 403292fba..f555dba67 100644 --- a/apps/freeablo/faworld/playerinput.h +++ b/apps/freeablo/faworld/playerinput.h @@ -21,7 +21,8 @@ MACRO(PlayerJoined) \ MACRO(PlayerLeft) \ MACRO(BuyItem) \ - MACRO(SellItem) + MACRO(SellItem) \ + MACRO(UseItem) namespace Serial { @@ -156,6 +157,12 @@ namespace FAWorld void save(Serial::Saver& saver) const; void load(Serial::Loader& loader); }; + struct UseItemData + { + FAWorld::EquipTarget target; + void save(Serial::Saver& saver) const; + void load(Serial::Loader& loader); + }; // All this macro mess takes care of generating boilerplate code to wrap up the above structs into a union with a type enum, and // save/load functions. So, eg, if mType == TargetTile, then you can access mData.dataTargetType. You can also call .save() and .load() diff --git a/apps/freeablo/faworld/potion.cpp b/apps/freeablo/faworld/potion.cpp new file mode 100644 index 000000000..1c08a508c --- /dev/null +++ b/apps/freeablo/faworld/potion.cpp @@ -0,0 +1,81 @@ +#include "potion.h" +#include "player.h" +#include + +namespace FAWorld +{ + // https:// wheybags.gitlab.io/jarulfs-guide/#potions-and-elixirs + // Used for the formulas + void Potion::restoreHp(Player& player) + { + FixedPoint bonus; + switch (player.getClass()) + { + case FAWorld::PlayerClass::warrior: + { + bonus = FixedPoint(2); + break; + } + case FAWorld::PlayerClass::rogue: + { + bonus = FixedPoint("1.5"); + break; + } + case FAWorld::PlayerClass::sorceror: + { + bonus = FixedPoint(1); + break; + } + default: + break; + } + + int32_t min = (int32_t)(bonus * FixedPoint(player.getStats().getHp().max) / FixedPoint(8)).floor(); + int32_t max = min * 3; + int32_t toHeal = player.getWorld()->mRng->randomInRange(min, max); + player.heal(toHeal); + } + + void Potion::restoreHpFull(Player& player) { player.heal(); } + + void Potion::restoreMana(Player& player) + { + FixedPoint bonus; + + switch (player.getClass()) + { + case FAWorld::PlayerClass::warrior: + { + bonus = FixedPoint(1); + break; + } + case FAWorld::PlayerClass::rogue: + { + bonus = FixedPoint("1.5"); + break; + } + case FAWorld::PlayerClass::sorceror: + { + bonus = FixedPoint(2); + break; + } + default: + break; + } + + int32_t min = (int32_t)(bonus * FixedPoint(player.getStats().getHp().max) / FixedPoint(8)).floor(); + int32_t max = min * 3; + player.getWorld()->mRng->randomInRange(min, max); + player.restoreMana(); + } + + void Potion::restoreManaFull(Player& player) { player.restoreMana(); } + + void Potion::increaseStrength(Player& player, int32_t delta) { player.addStrength(delta); } + + void Potion::increaseMagic(Player& player, int32_t delta) { player.addMagic(delta); } + + void Potion::increaseDexterity(Player& player, int32_t delta) { player.addDexterity(delta); } + + void Potion::increaseVitality(Player& player, int32_t delta) { player.addVitality(delta); } +} diff --git a/apps/freeablo/faworld/potion.h b/apps/freeablo/faworld/potion.h new file mode 100644 index 000000000..f692ee6d2 --- /dev/null +++ b/apps/freeablo/faworld/potion.h @@ -0,0 +1,21 @@ +#pragma once +#include "player.h" + +namespace FAWorld +{ + // Class Potion for all potion functions + class Potion + { + public: + static void restoreHp(Player& player); + static void restoreMana(Player& player); + static void restoreHpFull(Player& player); + static void restoreManaFull(Player& player); + + // elixirs + static void increaseStrength(Player& player, int32_t delta); + static void increaseMagic(Player& player, int32_t delta); + static void increaseDexterity(Player& player, int32_t delta); + static void increaseVitality(Player& player, int32_t delta); + }; +} diff --git a/changelog.md b/changelog.md index 96988a826..2dc315764 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,7 @@ ## v0.5 [?? ??? ????] - 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