From 4ef76c1ea3e4ecb9a1a45b621115ff165cb1d041 Mon Sep 17 00:00:00 2001 From: neveza Date: Thu, 11 Jun 2020 09:45:31 -0400 Subject: [PATCH] Potions and Elixers #492 (#496) * current addition of potion of healing. May include mana later. * For issue #492 : Added potion.cpp/.h for cleaner code instead of jamming all the logic in the triggerItem function which would have gotten very ugly very quickly. Also added FullHealing as well as mana and rejuvination equivalents since they had related features. * for #492, All potions and elixers added along with functions to add to attributes. * Update player.cpp * Update guimanager.cpp * Update player.cpp * added cases to satisfy the errors in appveyor * Update guimanager.cpp * Updated the add Attributes functions to use std::min() * Updated functions to use FixedPoint * Update potion.h * Update guimanager.cpp * Update potion.h * Update guimanager.cpp * Update potion.cpp * Update potion.cpp * Update playerinput.h * Update playerinput.cpp * Update playerbehaviour.cpp * Update guimanager.cpp * Update playerbehaviour.cpp * Update CMakeLists.txt * Update guimanager.cpp * compile fixes * clang-format * changelog Co-authored-by: neveza Co-authored-by: Tom Mason --- apps/freeablo/CMakeLists.txt | 2 + apps/freeablo/fagui/guimanager.cpp | 7 ++ apps/freeablo/faworld/actor.cpp | 1 + apps/freeablo/faworld/actor.h | 1 + apps/freeablo/faworld/actorstats.h | 5 ++ apps/freeablo/faworld/inventory.cpp | 2 +- apps/freeablo/faworld/item/item.h | 2 +- apps/freeablo/faworld/item/usableitem.cpp | 9 ++ apps/freeablo/faworld/item/usableitem.h | 3 + apps/freeablo/faworld/item/usableitembase.cpp | 88 ++++++++++++++++++- apps/freeablo/faworld/item/usableitembase.h | 7 +- apps/freeablo/faworld/player.cpp | 35 ++++++++ apps/freeablo/faworld/player.h | 5 ++ apps/freeablo/faworld/playerbehaviour.cpp | 13 +++ apps/freeablo/faworld/playerinput.cpp | 4 + apps/freeablo/faworld/playerinput.h | 9 +- apps/freeablo/faworld/potion.cpp | 81 +++++++++++++++++ apps/freeablo/faworld/potion.h | 21 +++++ changelog.md | 1 + 19 files changed, 290 insertions(+), 6 deletions(-) create mode 100644 apps/freeablo/faworld/potion.cpp create mode 100644 apps/freeablo/faworld/potion.h 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