diff options
Diffstat (limited to 'src/Mobs')
-rw-r--r-- | src/Mobs/Monster.cpp | 6 | ||||
-rw-r--r-- | src/Mobs/Villager.cpp | 318 | ||||
-rw-r--r-- | src/Mobs/Villager.h | 133 |
3 files changed, 408 insertions, 49 deletions
diff --git a/src/Mobs/Monster.cpp b/src/Mobs/Monster.cpp index 0d433d861..86060583c 100644 --- a/src/Mobs/Monster.cpp +++ b/src/Mobs/Monster.cpp @@ -1135,14 +1135,14 @@ std::unique_ptr<cMonster> cMonster::NewMonsterFromType(eMonsterType a_MobType) } case mtVillager: { - int VillagerType = Random.RandInt(6); - if (VillagerType == 6) + int VillagerType = Random.RandInt(13); + if (VillagerType == 13) { // Give farmers a better chance of spawning VillagerType = 0; } - return cpp14::make_unique<cVillager>(static_cast<cVillager::eVillagerType>(VillagerType)); + return cpp14::make_unique<cVillager>(static_cast<cVillager::VillagerCareer>(VillagerType), 1U); } case mtHorse: { diff --git a/src/Mobs/Villager.cpp b/src/Mobs/Villager.cpp index 26462ba31..36b3d75f9 100644 --- a/src/Mobs/Villager.cpp +++ b/src/Mobs/Villager.cpp @@ -2,21 +2,53 @@ #include "Globals.h" // NOTE: MSVC stupidness requires this to be the same across all modules #include "Villager.h" +#include "../UI/VillagerTradeWindow.h" +#include "../Entities/Player.h" #include "../World.h" #include "../BlockArea.h" #include "../Blocks/BlockHandler.h" #include "../BlockInServerPluginInterface.h" +#include "../ClientHandle.h" -cVillager::cVillager(eVillagerType VillagerType) : +cVillager::cVillager(VillagerCareer Career, unsigned TradeTier) : super("Villager", mtVillager, "entity.villager.hurt", "entity.villager.death", 0.6, 1.8), + cEntityWindowOwner(this), m_ActionCountDown(-1), - m_Type(VillagerType), + m_Career(Career), + m_TradeTier(TradeTier), m_VillagerAction(false) { + UpdateTradeTier(m_TradeTier); +} + + + + + +cVillager::VillagerProfession cVillager::VillagerCareerToProfession(VillagerCareer Career) +{ + switch (Career) + { + case VillagerCareer::Farmer: + case VillagerCareer::Fisherman: + case VillagerCareer::Shepherd: + case VillagerCareer::Fletcher: return cVillager::VillagerProfession::Farmer; + case VillagerCareer::Librarian: + case VillagerCareer::Cartographer: return cVillager::VillagerProfession::Librarian; + case VillagerCareer::Cleric: return cVillager::VillagerProfession::Priest; + case VillagerCareer::Armorer: + case VillagerCareer::WeaponSmith: + case VillagerCareer::ToolSmith: return cVillager::VillagerProfession::Blacksmith; + case VillagerCareer::Butcher: + case VillagerCareer::Leatherworker: return cVillager::VillagerProfession::Butcher; + case VillagerCareer::Nitwit: return cVillager::VillagerProfession::Nitwit; + } + + throw std::runtime_error("Unhandled career"); } @@ -32,10 +64,7 @@ bool cVillager::DoTakeDamage(TakeDamageInfo & a_TDI) if ((a_TDI.Attacker != nullptr) && a_TDI.Attacker->IsPlayer()) { - if (GetRandomProvider().RandBool(1.0 / 6.0)) - { - m_World->BroadcastEntityStatus(*this, esVillagerAngry); - } + m_World->BroadcastEntityStatus(*this, esVillagerAngry); } if (a_TDI.DamageType == dtLightning) @@ -63,27 +92,18 @@ void cVillager::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) if (m_ActionCountDown > -1) { m_ActionCountDown--; - if (m_ActionCountDown == 0) + if ((m_ActionCountDown == 0) && (m_Career == VillagerCareer::Farmer)) { - switch (m_Type) - { - case vtFarmer: - { - HandleFarmerPlaceCrops(); - } - } + HandleFarmerPlaceCrops(); } return; } if (m_VillagerAction) { - switch (m_Type) + if (m_Career == VillagerCareer::Farmer) { - case vtFarmer: - { - HandleFarmerTryHarvestCrops(); - } + HandleFarmerTryHarvestCrops(); } m_VillagerAction = false; return; @@ -95,11 +115,169 @@ void cVillager::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) return; } - switch (m_Type) + if (m_Career == VillagerCareer::Farmer) + { + HandleFarmerPrepareFarmCrops(); + } +} + + + + + +void cVillager::OnRightClicked(cPlayer & InteractingPlayer) +{ + if (m_TradeOffers.empty()) + { + // Client handles this ok, but the contract of GetTradeOffer says it must return something + return; + } + + // If the window is not created, open it anew: + if (GetWindow() == nullptr) + { + OpenWindow(new cVillagerTradeWindow(*this)); + } + + InteractingPlayer.OpenWindow(*GetWindow()); + InteractingPlayer.GetClientHandle()->SendVillagerTradeList(*GetWindow(), m_TradeOffers); + GetWorld()->BroadcastSoundEffect("entity.villager.trading", GetPosX(), GetPosY(), GetPosZ(), 1, 1); +} + + + + + +VillagerTradeOffer & cVillager::GetTradeOffer(unsigned PlayerOfferIndex, const cItem & PrimarySlot, const cItem & SecondarySlot) +{ + if ((PlayerOfferIndex != 0) || GetTransactionMultiplier(m_TradeOffers[PlayerOfferIndex], PrimarySlot, SecondarySlot) != 0) + { + return m_TradeOffers[PlayerOfferIndex]; + } + + for (auto & Offer : m_TradeOffers) + { + if (GetTransactionMultiplier(Offer, PrimarySlot, SecondarySlot) != 0) + { + return Offer; + } + } + + return m_TradeOffers[PlayerOfferIndex]; +} + + + + + +unsigned cVillager::GetTransactionMultiplier(const VillagerTradeOffer & Trade, const cItem & PrimarySlot, const cItem & SecondarySlot) +{ + if (Trade.IsTradeExhausted()) + { + return 0; + } + + if ( + PrimarySlot.IsEqual(Trade.PrimaryDesire) && + (PrimarySlot.m_ItemCount >= Trade.PrimaryDesire.m_ItemCount) && + SecondarySlot.IsEqual(Trade.SecondaryDesire) && + (SecondarySlot.m_ItemCount >= Trade.SecondaryDesire.m_ItemCount) + ) + { + // Slots matched normally: find common denominator of transaction multiplier + return std::min( + { + static_cast<unsigned>(PrimarySlot.m_ItemCount / Trade.PrimaryDesire.m_ItemCount), + Trade.SecondaryDesire.IsEmpty() ? std::numeric_limits<unsigned>::max() : static_cast<unsigned>(SecondarySlot.m_ItemCount / Trade.SecondaryDesire.m_ItemCount), + static_cast<unsigned>(Trade.Recompense.GetMaxStackSize() / Trade.Recompense.m_ItemCount) + } + ); + } + else if ( + Trade.SecondaryDesire.IsEmpty() && + PrimarySlot.IsEmpty() && + SecondarySlot.IsEqual(Trade.PrimaryDesire) && + (SecondarySlot.m_ItemCount >= Trade.PrimaryDesire.m_ItemCount) + ) + { + // Slots crossed - primary desire in secondary slot: swap comparison accordingly + return std::min( + static_cast<unsigned>(SecondarySlot.m_ItemCount / Trade.PrimaryDesire.m_ItemCount), + static_cast<unsigned>(Trade.Recompense.GetMaxStackSize() / Trade.Recompense.m_ItemCount) + ); + } + + return 0; +} + + + + + +void cVillager::HandleTradeAvailable() const +{ + GetWorld()->BroadcastSoundEffect("entity.villager.yes", GetPosX(), GetPosY(), GetPosZ(), 1, 1); +} + + + + + +void cVillager::HandleTradeInProgress() const +{ + GetWorld()->BroadcastSoundEffect("entity.villager.trading", GetPosX(), GetPosY(), GetPosZ(), 1, 1); +} + + + + + +void cVillager::HandleTradeUnavailable() const +{ + GetWorld()->BroadcastSoundEffect("entity.villager.no", GetPosX(), GetPosY(), GetPosZ(), 1, 1); +} + + + + + +void cVillager::HandleTransaction(VillagerTradeOffer & Trade, unsigned TransactionMultiplier) +{ + GetWorld()->BroadcastSoundEffect("entity.villager.yes", GetPosX(), GetPosY(), GetPosZ(), 1, 1); + Trade.Transactions += TransactionMultiplier; + + for (unsigned Try = 0; Try != TransactionMultiplier; Try++) { - case vtFarmer: + // One singular transaction has a 10% chance of upgrading the villager's trading tier + + if (GetRandomProvider().RandBool(0.1)) { - HandleFarmerPrepareFarmCrops(); + UpdateTradeTier(m_TradeTier + 1); + + class SendNewTradesCallback : public cItemCallback<cClientHandle> + { + public: + SendNewTradesCallback(const cWindow & VillagerTradeWindow, const std::vector<VillagerTradeOffer> & VillagerTradeOffers) : + TradeWindow(VillagerTradeWindow), + TradeOffers(VillagerTradeOffers) + { + } + + bool Item(cClientHandle * Handle) + { + Handle->SendVillagerTradeList(TradeWindow, TradeOffers); + return false; + } + + private: + const cWindow & TradeWindow; + const std::vector<VillagerTradeOffer> & TradeOffers; + + } SNTC(*GetWindow(), m_TradeOffers); + GetWindow()->ForEachClient(SNTC); + + m_World->BroadcastEntityStatus(*this, esVillagerHappy); + break; } } } @@ -107,6 +285,7 @@ void cVillager::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) + //////////////////////////////////////////////////////////////////////////////// // Farmer functions: @@ -195,6 +374,101 @@ void cVillager::HandleFarmerPlaceCrops() +void cVillager::UpdateTradeTier(size_t DesiredTier) +{ + struct CareerHash + { + size_t operator()(const VillagerCareer & Career) const + { + return static_cast<size_t>(Career); + } + }; + using Offer = VillagerTradeOffer; + + static const std::unordered_map< + VillagerCareer, + std::vector< + // Element index: trading tier (0-based) + std::vector<Offer> // Trade offer + >, + CareerHash // TODO for C++14: unneeded + > TradeMatrix = + { + { + VillagerCareer::Farmer, + { + { // Tier 1 (equivalent) + Offer(cItem(E_ITEM_WHEAT, 18), cItem(E_ITEM_EMERALD)), + Offer(cItem(E_ITEM_POTATO, 15), cItem(E_ITEM_EMERALD)), + Offer(cItem(E_ITEM_CARROT, 15), cItem(E_ITEM_EMERALD)) + }, + { // Tier 2 + Offer(cItem(E_BLOCK_PUMPKIN, 8), cItem(E_ITEM_EMERALD)), + Offer(cItem(E_ITEM_EMERALD), cItem(E_ITEM_PUMPKIN_PIE, 2)) + }, + { // Tier 3 + Offer(cItem(E_BLOCK_MELON, 7), cItem(E_ITEM_EMERALD)), + Offer(cItem(E_ITEM_EMERALD), cItem(E_ITEM_RED_APPLE, 5)) + }, + { // Tier 4 + Offer(cItem(E_ITEM_EMERALD), cItem(E_ITEM_COOKIE, 6)), + Offer(cItem(E_ITEM_EMERALD), cItem(E_ITEM_CAKE)) + } + } + }, + { + VillagerCareer::Fisherman, {} + }, + { + VillagerCareer::Shepherd, {} + }, + { + VillagerCareer::Fletcher, {} + }, + { + VillagerCareer::Librarian, {} + }, + { + VillagerCareer::Cartographer, {} + }, + { + VillagerCareer::Cleric, {} + }, + { + VillagerCareer::Armorer, {} + }, + { + VillagerCareer::WeaponSmith, {} + }, + { + VillagerCareer::ToolSmith, {} + }, + { + VillagerCareer::Butcher, {} + }, + { + VillagerCareer::Leatherworker, {} + }, + { + VillagerCareer::Nitwit, {} + } + }; + + auto Tiers = TradeMatrix.find(m_Career)->second; + + m_TradeTier = std::min(Tiers.size(), DesiredTier); + m_TradeOffers.clear(); + + for (unsigned Tier = 0; Tier < m_TradeTier; Tier++) + { + m_TradeOffers.insert(m_TradeOffers.end(), Tiers[Tier].cbegin(), Tiers[Tier].cend()); + } +} + + + + + bool cVillager::IsBlockFarmable(BLOCKTYPE a_BlockType) { switch (a_BlockType) diff --git a/src/Mobs/Villager.h b/src/Mobs/Villager.h index 6f3e7b4e8..0d8672c46 100644 --- a/src/Mobs/Villager.h +++ b/src/Mobs/Villager.h @@ -3,37 +3,123 @@ #include "PassiveMonster.h" #include "Blocks/ChunkInterface.h" +#include "../UI/WindowOwner.h" + + + + + +struct VillagerTradeOffer +{ + VillagerTradeOffer(const cItem & PrimaryTradeDesire, const cItem & TradeRecompense) : + PrimaryDesire(PrimaryTradeDesire), + Recompense(TradeRecompense), + Transactions(0), + MaximumTransactions(10) + { + } + + VillagerTradeOffer(const cItem & PrimaryTradeDesire, const cItem & SecondaryTradeDesire, const cItem & TradeRecompense) : + PrimaryDesire(PrimaryTradeDesire), + SecondaryDesire(SecondaryTradeDesire), + Recompense(TradeRecompense), + Transactions(0), + MaximumTransactions(10) + { + } + + cItem PrimaryDesire; + cItem SecondaryDesire; + cItem Recompense; + + bool IsTradeExhausted() const + { + return Transactions == MaximumTransactions; + } + + unsigned Transactions; + unsigned MaximumTransactions; +}; + class cVillager : - public cPassiveMonster + public cPassiveMonster, + public cEntityWindowOwner { typedef cPassiveMonster super; public: - enum eVillagerType + enum class VillagerProfession + { + Farmer = 0, + Librarian = 1, + Priest = 2, + Blacksmith = 3, + Butcher = 4, + Nitwit = 5 + }; + + enum class VillagerCareer { - vtFarmer = 0, - vtLibrarian = 1, - vtPriest = 2, - vtBlacksmith = 3, - vtButcher = 4, - vtGeneric = 5, - vtMax - } ; + Farmer, + Fisherman, + Shepherd, + Fletcher, + Librarian, + Cartographer, + Cleric, + Armorer, + WeaponSmith, + ToolSmith, + Butcher, + Leatherworker, + Nitwit + }; + + cVillager(VillagerCareer, unsigned); + CLASS_PROTODEF(cVillager) - cVillager(eVillagerType VillagerType); + /** Converts internal career to equivalent Vanilla profession. + Returns an enumeration with Vanilla-compatible values. */ + static VillagerProfession VillagerCareerToProfession(VillagerCareer); - CLASS_PROTODEF(cVillager) + /** Returns the career of the villager. */ + VillagerCareer GetCareer() const { return m_Career; } + + /** Returns a trading offer of the villager subject to criteria. + If the player has selected the first trade offer, the first available trade offer matching the input is returned, or the first trade offer if nothing was found. + For any other offer selection, the selected offer is returned. */ + VillagerTradeOffer & GetTradeOffer(unsigned, const cItem &, const cItem &); + + /** Returns number of transactions of a single trade can be completed, given input and output items. */ + static unsigned GetTransactionMultiplier(const VillagerTradeOffer &, const cItem &, const cItem &); + + /** Handles events where players interact with an available trade. + This includes adding a desired item to fulfil or exceed the offer, or otherwise interacting with a trade when it can be performed. */ + void HandleTradeAvailable() const; + + /** Handles events where players interact with the trading window. + This event does not include interactions with the currently presented trade itself. */ + void HandleTradeInProgress() const; + + /** Handles events where players interact with an unavailable trade. + This includes adding a desired item to fulfil or exceed the offer, or otherwise interacting with a trade when it can be performed. */ + void HandleTradeUnavailable() const; + + /** Convenience function for setting correct item counts and upgrading trade tiers if necessary. */ + void HandleTransaction(VillagerTradeOffer &, unsigned TransactionMultiplier); // cEntity overrides virtual bool DoTakeDamage(TakeDamageInfo & a_TDI) override; - virtual void Tick (std::chrono::milliseconds a_Dt, cChunk & a_Chunk) override; + virtual void Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) override; + virtual void OnRightClicked(cPlayer &) override; + +private: - // cVillager functions /** return true if the given blocktype are: crops, potatoes or carrots. */ bool IsBlockFarmable(BLOCKTYPE a_BlockType); @@ -48,19 +134,18 @@ public: void HandleFarmerPlaceCrops(); // Get and set functions. - int GetVilType(void) const { return m_Type; } - Vector3i GetCropsPos(void) const { return m_CropsPos; } - bool DoesHaveActionActivated(void) const { return m_VillagerAction; } + Vector3i GetCropsPos(void) const { return m_CropsPos; } + bool DoesHaveActionActivated(void) const { return m_VillagerAction; } -private: + /** Levels-up the villager to the given tier, if not already at maximum for their career. + Levelling-up unlocks additional trades available at that level, and re-enables all previously exhausted trade offers. */ + void UpdateTradeTier(size_t); int m_ActionCountDown; - int m_Type; + VillagerCareer m_Career; + size_t m_TradeTier; + std::vector<VillagerTradeOffer> m_TradeOffers; bool m_VillagerAction; Vector3i m_CropsPos; -} ; - - - - +}; |