summaryrefslogtreecommitdiffstats
path: root/src/Mobs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/Mobs/Monster.cpp6
-rw-r--r--src/Mobs/Villager.cpp318
-rw-r--r--src/Mobs/Villager.h133
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;
-} ;
-
-
-
-
+};