From fec64bb91c03c5e872a8f6fbc1a253f341373072 Mon Sep 17 00:00:00 2001 From: Persson-dev <66266021+Persson-dev@users.noreply.github.com> Date: Wed, 29 Dec 2021 20:28:41 +0100 Subject: Improved farmer AI & Fixed entity loading functions (#5351) * Allow villagers to pickup items * Add farmer villager harvesting * Use of auto keyword * Using for loop to check adjacent crops * Show particules when farmer harvest * Fix area comment * Move constants to header file * Removing unnecessary semicolon * Initialization of CropBlockType variable * Apply 12xx12 suggestion * Fixing area constant size * Refactor bounding box calculation, use vectors. * Add Api documentation * Update lua docs * Rework farmer ai * Fixing lua docs notes * Add missing capitalisation * Add villagers inventory save * Fixing loading entities from disk inconsistencies * Add farmer harvest animation * Fix beetroots grow state Co-authored-by: Alexander Harkness --- src/Mobs/Villager.cpp | 313 ++++++++++++++++++++++++++++++++++++++------------ src/Mobs/Villager.h | 61 ++++++++-- 2 files changed, 292 insertions(+), 82 deletions(-) (limited to 'src/Mobs') diff --git a/src/Mobs/Villager.cpp b/src/Mobs/Villager.cpp index 46dd613f1..aa5409e4d 100644 --- a/src/Mobs/Villager.cpp +++ b/src/Mobs/Villager.cpp @@ -15,7 +15,8 @@ cVillager::cVillager(eVillagerType VillagerType) : Super("Villager", mtVillager, "entity.villager.hurt", "entity.villager.death", "entity.villager.ambient", 0.6f, 1.95f), m_ActionCountDown(-1), m_Type(VillagerType), - m_VillagerAction(false) + m_FarmerAction(faIdling), + m_Inventory(8, 1) { } @@ -60,46 +61,12 @@ void cVillager::Tick(std::chrono::milliseconds a_Dt, cChunk & a_Chunk) return; } - if (m_ActionCountDown > -1) - { - m_ActionCountDown--; - if (m_ActionCountDown == 0) - { - switch (m_Type) - { - case vtFarmer: - { - HandleFarmerPlaceCrops(); - } - } - } - return; - } - - if (m_VillagerAction) - { - switch (m_Type) - { - case vtFarmer: - { - HandleFarmerTryHarvestCrops(); - } - } - m_VillagerAction = false; - return; - } - - // Don't always try to do a special action. Each tick has 1% to do a special action. - if (GetRandomProvider().RandBool(0.99)) - { - return; - } - switch (m_Type) { case vtFarmer: { - HandleFarmerPrepareFarmCrops(); + TickFarmer(); + break; } } } @@ -130,50 +97,111 @@ void cVillager::KilledBy(TakeDamageInfo & a_TDI) //////////////////////////////////////////////////////////////////////////////// // Farmer functions: -void cVillager::HandleFarmerPrepareFarmCrops() +void cVillager::TickFarmer() { + + // Don't harvest crops if you must not if (!m_World->VillagersShouldHarvestCrops()) { return; } - cBlockArea Surrounding; + // This is to prevent undefined behaviors + if (m_FinalDestination.y <= 0) + { + return; + } - // Read a 11x7x11 area: + if (!IsIdling()) + { + // Forcing the farmer to go to work spots. + MoveToPosition(static_cast(m_CropsPos) + Vector3d(0.5, 0, 0.5)); + + // Forcing the farmer to look at the work spots. + Vector3d Direction = (m_FinalDestination - (GetPosition() + Vector3d(0, 1.6, 0))); // We get the direction from the eyes of the farmer to the work spot. + Direction.Normalize(); + SetPitch(std::asin(-Direction.y) / M_PI * 180); + } + + // Updating the timer + if (m_ActionCountDown > -1) + { + m_ActionCountDown--; + } + + // Searching for work in blocks where the farmer goes. + if (IsHarvestable(m_FinalDestination.Floor())) + { + m_CropsPos = m_FinalDestination.Floor(); + m_FarmerAction = faHarvesting; + HandleFarmerTryHarvestCrops(); + return; + } + else if (IsPlantable(m_FinalDestination.Floor()) && CanPlantCrops()) + { + m_CropsPos = m_FinalDestination.Floor(); + m_FarmerAction = faPlanting; + HandleFarmerTryPlaceCrops(); + return; + } + else + { + m_FarmerAction = faIdling; // Returning to idling. + } + + + // Don't always try to do a special action. Each tick has 10% to do a special action. + if (GetRandomProvider().RandBool(FARMER_SPECIAL_ACTION_CHANCE)) + { + ScanAreaForWork(); + } + +} + + + + + +void cVillager::ScanAreaForWork() +{ + + auto Pos = GetPosition().Floor(); + auto MinPos = Pos - FARMER_SCAN_CROPS_DIST; + auto MaxPos = Pos + FARMER_SCAN_CROPS_DIST; + + // Read area to be checked for crops. + cBlockArea Surrounding; Surrounding.Read( *m_World, - FloorC(GetPosX()) - 5, - FloorC(GetPosX()) + 6, - FloorC(GetPosY()) - 3, - FloorC(GetPosY()) + 4, - FloorC(GetPosZ()) - 5, - FloorC(GetPosZ()) + 6 + MinPos, MaxPos ); - for (int I = 0; I < 5; I++) + for (int I = 0; I < FARMER_RANDOM_TICK_SPEED; I++) { - for (int Y = 0; Y < 6; Y++) + for (int Y = MinPos.y; Y <= MaxPos.y; Y++) { // Pick random coordinates and check for crops. - int X = m_World->GetTickRandomNumber(11); - int Z = m_World->GetTickRandomNumber(11); + Vector3i CandidatePos(MinPos.x + m_World->GetTickRandomNumber(MaxPos.x - MinPos.x - 1), Y, MinPos.z + m_World->GetTickRandomNumber(MaxPos.z - MinPos.z - 1)); - // A villager can't farm this. - if (!IsBlockFarmable(Surrounding.GetRelBlockType(X, Y, Z))) + // A villager can harvest this. + if (IsHarvestable(CandidatePos)) { - continue; + m_CropsPos = CandidatePos; + m_FarmerAction = faHarvesting; + MoveToPosition(static_cast(m_CropsPos) + Vector3d(0.5, 0, 0.5)); + return; } - if (Surrounding.GetRelBlockMeta(X, Y, Z) != 0x7) + // A villager can plant this. + else if (IsPlantable(CandidatePos) && CanPlantCrops()) { - continue; + m_CropsPos = CandidatePos; + m_FarmerAction = faHarvesting; + MoveToPosition(static_cast(m_CropsPos) + Vector3d(0.5, 0, 0.5)); + return; } - m_VillagerAction = true; - m_CropsPos = Vector3i(static_cast(GetPosX()) + X - 5, static_cast(GetPosY()) + Y - 3, static_cast(GetPosZ()) + Z - 5); - MoveToPosition(Vector3d(m_CropsPos.x + 0.5, m_CropsPos.y + 0.0, m_CropsPos.z + 0.5)); - return; - } // for Y loop. - } // Repeat the procces 5 times. + } // for Y + } // Repeat the proccess according to the random tick speed. } @@ -182,15 +210,22 @@ void cVillager::HandleFarmerPrepareFarmCrops() void cVillager::HandleFarmerTryHarvestCrops() { - // Harvest the crops if the villager isn't moving and if the crops are closer then 2 blocks. - if (!m_PathfinderActivated && (GetPosition() - m_CropsPos).Length() < 2) + if (m_ActionCountDown > 0) + { + // The farmer is still on cooldown + return; + } + + // Harvest the crops if it is closer than 1 block. + if ((GetPosition() - m_CropsPos).Length() < 1) { // Check if the blocks didn't change while the villager was walking to the coordinates. - BLOCKTYPE CropBlock = m_World->GetBlock(m_CropsPos); - if (IsBlockFarmable(CropBlock) && m_World->GetBlockMeta(m_CropsPos) == 0x7) + if (IsHarvestable(m_CropsPos)) { + m_World->BroadcastSoundParticleEffect(EffectID::PARTICLE_BLOCK_BREAK, m_CropsPos, m_World->GetBlock(m_CropsPos)); m_World->DropBlockAsPickups(m_CropsPos, this, nullptr); - m_ActionCountDown = 20; + // Applying 0.5 second cooldown. + m_ActionCountDown = 10; } } } @@ -199,12 +234,113 @@ void cVillager::HandleFarmerTryHarvestCrops() -void cVillager::HandleFarmerPlaceCrops() +void cVillager::CheckForNearbyCrops() { + + // Search for adjacent crops + + constexpr std::array Directions = { Vector3i{0, 0, -1}, {0, 0, 1}, {1, 0, 0}, {-1, 0, 0} }; + + for (Vector3i Direction : Directions) + { + if (IsHarvestable(m_CropsPos + Direction)) + { + m_CropsPos += Direction; + m_FarmerAction = faHarvesting; + MoveToPosition(static_cast(m_CropsPos) + Vector3d(0.5, 0, 0.5)); + return; + } + else if (IsPlantable(m_CropsPos + Direction) && CanPlantCrops()) + { + m_CropsPos += Direction; + m_FarmerAction = faPlanting; + MoveToPosition(static_cast(m_CropsPos) + Vector3d(0.5, 0, 0.5)); + return; + } + + } + + // There is no more work to do around the previous crops. + m_FarmerAction = faIdling; +} + + + + + +void cVillager::HandleFarmerTryPlaceCrops() +{ + + if ((GetPosition() - m_CropsPos).Length() > 1) + { + // The farmer is still to far from the final destination + return; + } + + if (m_ActionCountDown > 0) + { + // The farmer is still on cooldown + return; + } + // Check if there is still farmland at the spot where the crops were. - if (m_World->GetBlock(m_CropsPos.addedY(-1)) == E_BLOCK_FARMLAND) + if (IsPlantable(m_CropsPos)) { - m_World->SetBlock(m_CropsPos, E_BLOCK_CROPS, 0); + // Finding the item to use to plant a crop + int TargetSlot = -1; + BLOCKTYPE CropBlockType = E_BLOCK_AIR; + + for (int I = 0; I < m_Inventory.GetWidth() && TargetSlot < 0; I++) + { + const cItem & Slot = m_Inventory.GetSlot(I); + switch (Slot.m_ItemType) + { + case E_ITEM_SEEDS: + { + TargetSlot = I; + CropBlockType = E_BLOCK_CROPS; + break; + } + + case E_ITEM_BEETROOT_SEEDS: + { + TargetSlot = I; + CropBlockType = E_BLOCK_BEETROOTS; + break; + } + + case E_ITEM_POTATO: + { + TargetSlot = I; + CropBlockType = E_BLOCK_POTATOES; + break; + } + + case E_ITEM_CARROT: + { + TargetSlot = I; + CropBlockType = E_BLOCK_CARROTS; + break; + } + + default: + { + break; + } + } + } + + // Removing item from villager inventory + m_Inventory.RemoveOneItem(TargetSlot); + + // Placing crop block + m_World->SetBlock(m_CropsPos, CropBlockType, 0); + + // Applying 1 second cooldown + m_ActionCountDown = 20; + + // Try to do the same with adjacent crops. + CheckForNearbyCrops(); } } @@ -212,16 +348,33 @@ void cVillager::HandleFarmerPlaceCrops() -bool cVillager::IsBlockFarmable(BLOCKTYPE a_BlockType) +bool cVillager::CanPlantCrops() +{ + return m_Inventory.HasItems(cItem(E_ITEM_SEEDS)) || + m_Inventory.HasItems(cItem(E_ITEM_BEETROOT_SEEDS)) || + m_Inventory.HasItems(cItem(E_ITEM_POTATO)) || + m_Inventory.HasItems(cItem(E_ITEM_CARROT)); +} + + + + + +bool cVillager::IsBlockFarmable(BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta) { switch (a_BlockType) { case E_BLOCK_BEETROOTS: + { + // The crop must have fully grown up. + return a_BlockMeta == 0x03; + } case E_BLOCK_CROPS: case E_BLOCK_POTATOES: case E_BLOCK_CARROTS: { - return true; + // The crop must have fully grown up. + return a_BlockMeta == 0x07; } default: return false; } @@ -231,6 +384,24 @@ bool cVillager::IsBlockFarmable(BLOCKTYPE a_BlockType) +bool cVillager::IsHarvestable(Vector3i a_CropsPos) +{ + return IsBlockFarmable(m_World->GetBlock(a_CropsPos), m_World->GetBlockMeta(a_CropsPos)); +} + + + + + +bool cVillager::IsPlantable(Vector3i a_CropsPos) +{ + return (m_World->GetBlock(a_CropsPos.addedY(-1)) == E_BLOCK_FARMLAND) && (m_World->GetBlock(a_CropsPos) == E_BLOCK_AIR); +} + + + + + cVillager::eVillagerType cVillager::GetRandomProfession() { int Profession = GetRandomProvider().RandInt(cVillager::eVillagerType::vtMax - 1); diff --git a/src/Mobs/Villager.h b/src/Mobs/Villager.h index 4ac06765c..ef1c9e70f 100644 --- a/src/Mobs/Villager.h +++ b/src/Mobs/Villager.h @@ -3,6 +3,7 @@ #include "PassiveMonster.h" #include "../Blocks/ChunkInterface.h" +#include "../Inventory.h" @@ -38,30 +39,68 @@ public: virtual void KilledBy (TakeDamageInfo & a_TDI) override; // cVillager functions - /** return true if the given blocktype are: crops, potatoes or carrots. */ - bool IsBlockFarmable(BLOCKTYPE a_BlockType); + /** Returns the villager hidden inventory (8 slots). */ + cItemGrid & GetInventory(void) { return m_Inventory; } + const cItemGrid & GetInventory(void) const { return m_Inventory; } + + /** Returns true if the given blocktype are: crops, potatoes or carrots and they have full grown up. */ + bool IsBlockFarmable(BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta); + + /** Returns true if the block at the given location is a fully grown up crop. */ + bool IsHarvestable(Vector3i a_CropsPos); + + /** Returns true if seeds can be planted at a given location. */ + bool IsPlantable(Vector3i a_CropsPos); // Farmer functions - /** Searches in a 11x7x11 area for crops. If it found some it will navigate to them. */ - void HandleFarmerPrepareFarmCrops(); + enum eFarmerAction + { + faIdling, + faPlanting, + faHarvesting, + } ; + + static const int FARMER_RANDOM_TICK_SPEED = 5; + /** With 10% chance, it takes about 20 seconds to find a spot. */ + static constexpr double FARMER_SPECIAL_ACTION_CHANCE = 0.1; + /** This distance from the Villager makes for a 31x3x31 area. */ + static constexpr Vector3i FARMER_SCAN_CROPS_DIST {15, 1, 15}; - /** Looks if the farmer has reached it's destination, and if it's still crops and the destination is closer then 2 blocks it will harvest them. */ + /** Tick function for farmers. */ + void TickFarmer(); + + /** Searches in a 31x3x31 area to harvest crops or spaces to plant crops. If it found some it will navigate to them. */ + void ScanAreaForWork(); + + /** Looks if the farmer has reached it's destination, and if it's still crops and the destination is closer then 1 block it will harvest them. */ void HandleFarmerTryHarvestCrops(); - /** Replaces the crops he harvested. */ - void HandleFarmerPlaceCrops(); + /** Looks if the farmer has reached it's destination, and if it's still non obstructed farmland and the destination is closer then 1 block it will plant crops. */ + void HandleFarmerTryPlaceCrops(); + + /** Checking for harvesting or planting nearby crops */ + void CheckForNearbyCrops(); + + /** Returns whether the farmer has crops in his inventory to plant. */ + bool CanPlantCrops(); + + /** Returns whether the farmer is not working. */ + bool IsIdling() + { + return m_FarmerAction == faIdling; + } // 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; } + int GetVilType(void) const { return m_Type; } + eFarmerAction GetFarmerAction(void) const { return m_FarmerAction; } private: int m_ActionCountDown; int m_Type; - bool m_VillagerAction; + eFarmerAction m_FarmerAction; Vector3i m_CropsPos; + cItemGrid m_Inventory; } ; -- cgit v1.2.3