1
0
Fork 0

Add correct implementation of crops (#4802)

* [FIX] Add correct implementation of seed drops.

> Official percentage of drops has been implemented

* Fix C++ conventions

* Change "Vals" variable to "m_Vals"

* [FIX] Add correct implementation of Carrots, Potatoes, Wheat & Beetroots seed

* Add Fortune support with crops

Add fortune support with Wheat, Carrots, Potatoes & Beetroots seeds

* [FIX] Right-clicking on a grown Beetroot in survival consume 2 bone meals

Fix #4805

* Add documentation for "cWorld::IsFullGrownPlantAt" method

* Fix dispenser that full grown a plant

> Change methods cItemDyeHandler::FertilizePlant & cItemDyeHandler::growPlantsAround to static

* Display particle even if tree doesn't grow

* When right-clicking on a full grown melon / pumpkin seed, no longer produce a melon / pumpkin

Before this commit, when you right-click on a melon or a pumpkin seed, a melon / pumpkin block spawned.
With this commit, it no longer spawns

* [FIX] Do not create melon / pumpkin block when right-clicking with a bone meal

This fix will prevent the creation of a melon / pumpkin block when you right-click with a bone meal on a melon / pumpkin plant
- It just detect if the plant is full grown. if yes, the method "Grow" is not called

- Remove IsFullGrownPlant

Co-authored-by: 12xx12 <12xx12100@gmail.com>
Co-authored-by: Tiger Wang <ziwei.tiger@outlook.com>
This commit is contained in:
0ddlyoko 2020-11-06 17:54:01 +01:00 committed by GitHub
parent 36a67df105
commit 672bb04570
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 218 additions and 306 deletions

View File

@ -2096,64 +2096,6 @@ function OnAllChunksAvailable()</pre> All return values from the callbacks are i
}, },
Notes = "Returns the total age of the world, in ticks. The age always grows, cannot be set by plugins and is unrelated to TimeOfDay.", Notes = "Returns the total age of the world, in ticks. The age always grows, cannot be set by plugins and is unrelated to TimeOfDay.",
}, },
GrowCactus =
{
Params =
{
{
Name = "BlockX",
Type = "number",
},
{
Name = "BlockY",
Type = "number",
},
{
Name = "BlockZ",
Type = "number",
},
{
Name = "NumBlocksToGrow",
Type = "number",
},
},
Returns =
{
{
Type = "number",
},
},
Notes = "OBSOLETE, use GrowPlantAt instead. Grows a cactus block at the specified coords, by up to the specified number of blocks. Adheres to the world's maximum cactus growth (GetMaxCactusHeight()). Returns the amount of blocks the cactus grew inside this call.",
},
GrowMelonPumpkin =
{
Params =
{
{
Name = "BlockX",
Type = "number",
},
{
Name = "BlockY",
Type = "number",
},
{
Name = "BlockZ",
Type = "number",
},
{
Name = "StemBlockType",
Type = "number",
},
},
Returns =
{
{
Type = "boolean",
},
},
Notes = "OBSOLETE, use GrowPlantAt instead. Grows a melon or pumpkin, based on the stem block type specified (assumed to be at the coords provided). Checks for normal melon / pumpkin growth conditions - stem not having another produce next to it and suitable ground below. Returns true if the melon or pumpkin grew successfully.",
},
GrowPlantAt = GrowPlantAt =
{ {
Params = Params =
@ -2176,76 +2118,21 @@ function OnAllChunksAvailable()</pre> All return values from the callbacks are i
Notes = "Grows the plant at the specified block by the specified number of stages. Returns the number of stages actually grown. Returns zero for non-growable blocks.", Notes = "Grows the plant at the specified block by the specified number of stages. Returns the number of stages actually grown. Returns zero for non-growable blocks.",
}, },
GrowRipePlant = GrowRipePlant =
{
{
Params =
{
{
Name = "BlockX",
Type = "number",
},
{
Name = "BlockY",
Type = "number",
},
{
Name = "BlockZ",
Type = "number",
},
},
Returns =
{
{
Type = "boolean",
},
},
Notes = "OBSOLETE, use the Vector3-based overload instead. Grows the plant at the specified coords to its full. Returns true if the plant was grown, false if not.",
},
{
Params =
{
{
Name = "BlockPos",
Type = "Vector3i",
},
},
Returns =
{
{
Type = "boolean",
},
},
Notes = "Grows the plant at the specified coords to its full. Returns true if the plant was grown, false if not.",
},
},
GrowSugarcane =
{ {
Params = Params =
{ {
{ {
Name = "BlockX", Name = "BlockPos",
Type = "number", Type = "Vector3i",
},
{
Name = "BlockY",
Type = "number",
},
{
Name = "BlockZ",
Type = "number",
},
{
Name = "NumBlocksToGrow",
Type = "number",
}, },
}, },
Returns = Returns =
{ {
{ {
Type = "number", Type = "boolean",
}, },
}, },
Notes = "OBSOLETE, use GrowPlantAt instead. Grows a sugarcane block at the specified coords, by up to the specified number of blocks. Adheres to the world's maximum sugarcane growth (GetMaxSugarcaneHeight()). Returns the amount of blocks the sugarcane grew inside this call.", Notes = "Grows the plant at the specified coords to its full. Returns true if the plant was grown, false if not.",
}, },
GrowTree = GrowTree =
{ {

View File

@ -10,6 +10,7 @@
#include "../Entities/ProjectileEntity.h" #include "../Entities/ProjectileEntity.h"
#include "../Simulator/FluidSimulator.h" #include "../Simulator/FluidSimulator.h"
#include "../Items/ItemSpawnEgg.h" #include "../Items/ItemSpawnEgg.h"
#include "../Items/ItemDye.h"
@ -25,24 +26,24 @@ cDispenserEntity::cDispenserEntity(BLOCKTYPE a_BlockType, NIBBLETYPE a_BlockMeta
void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum) void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
{ {
Vector3i dispRelCoord(GetRelPos()); Vector3i DispRelCoord(GetRelPos());
auto meta = a_Chunk.GetMeta(dispRelCoord); auto Meta = a_Chunk.GetMeta(DispRelCoord);
AddDropSpenserDir(dispRelCoord, meta); AddDropSpenserDir(DispRelCoord, Meta);
auto dispChunk = a_Chunk.GetRelNeighborChunkAdjustCoords(dispRelCoord); auto DispChunk = a_Chunk.GetRelNeighborChunkAdjustCoords(DispRelCoord);
if (dispChunk == nullptr) if (DispChunk == nullptr)
{ {
// Would dispense into / interact with a non-loaded chunk, ignore the tick // Would dispense into / interact with a non-loaded chunk, ignore the tick
return; return;
} }
BLOCKTYPE dispBlock = dispChunk->GetBlock(dispRelCoord); BLOCKTYPE DispBlock = DispChunk->GetBlock(DispRelCoord);
auto dispAbsCoord = dispChunk->RelativeToAbsolute(dispRelCoord); auto DispAbsCoord = DispChunk->RelativeToAbsolute(DispRelCoord);
// Dispense the item: // Dispense the item:
const cItem & SlotItem = m_Contents.GetSlot(a_SlotNum); const cItem & SlotItem = m_Contents.GetSlot(a_SlotNum);
if (ItemCategory::IsMinecart(SlotItem.m_ItemType) && IsBlockRail(dispBlock)) // only actually place the minecart if there are rails! if (ItemCategory::IsMinecart(SlotItem.m_ItemType) && IsBlockRail(DispBlock)) // only actually place the minecart if there are rails!
{ {
if (m_World->SpawnMinecart(dispAbsCoord.x + 0.5, dispAbsCoord.y + 0.5, dispAbsCoord.z + 0.5, SlotItem.m_ItemType) != cEntity::INVALID_ID) if (m_World->SpawnMinecart(DispAbsCoord.x + 0.5, DispAbsCoord.y + 0.5, DispAbsCoord.z + 0.5, SlotItem.m_ItemType) != cEntity::INVALID_ID)
{ {
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }
@ -52,15 +53,15 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
{ {
case E_ITEM_BUCKET: case E_ITEM_BUCKET:
{ {
LOGD("Dispensing empty bucket in slot %d; dispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(dispBlock).c_str(), dispBlock); LOGD("Dispensing empty bucket in slot %d; DispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(DispBlock).c_str(), DispBlock);
switch (dispBlock) switch (DispBlock)
{ {
case E_BLOCK_STATIONARY_WATER: case E_BLOCK_STATIONARY_WATER:
case E_BLOCK_WATER: case E_BLOCK_WATER:
{ {
if (ScoopUpLiquid(a_SlotNum, E_ITEM_WATER_BUCKET)) if (ScoopUpLiquid(a_SlotNum, E_ITEM_WATER_BUCKET))
{ {
dispChunk->SetBlock(dispRelCoord, E_BLOCK_AIR, 0); DispChunk->SetBlock(DispRelCoord, E_BLOCK_AIR, 0);
} }
break; break;
} }
@ -69,7 +70,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
{ {
if (ScoopUpLiquid(a_SlotNum, E_ITEM_LAVA_BUCKET)) if (ScoopUpLiquid(a_SlotNum, E_ITEM_LAVA_BUCKET))
{ {
dispChunk->SetBlock(dispRelCoord, E_BLOCK_AIR, 0); DispChunk->SetBlock(DispRelCoord, E_BLOCK_AIR, 0);
} }
break; break;
} }
@ -84,10 +85,10 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_WATER_BUCKET: case E_ITEM_WATER_BUCKET:
{ {
LOGD("Dispensing water bucket in slot %d; dispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(dispBlock).c_str(), dispBlock); LOGD("Dispensing water bucket in slot %d; DispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(DispBlock).c_str(), DispBlock);
if (EmptyLiquidBucket(dispBlock, a_SlotNum)) if (EmptyLiquidBucket(DispBlock, a_SlotNum))
{ {
dispChunk->SetBlock(dispRelCoord, E_BLOCK_WATER, 0); DispChunk->SetBlock(DispRelCoord, E_BLOCK_WATER, 0);
} }
else else
{ {
@ -98,10 +99,10 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_LAVA_BUCKET: case E_ITEM_LAVA_BUCKET:
{ {
LOGD("Dispensing lava bucket in slot %d; dispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(dispBlock).c_str(), dispBlock); LOGD("Dispensing lava bucket in slot %d; DispBlock is \"%s\" (%d).", a_SlotNum, ItemTypeToString(DispBlock).c_str(), DispBlock);
if (EmptyLiquidBucket(dispBlock, a_SlotNum)) if (EmptyLiquidBucket(DispBlock, a_SlotNum))
{ {
dispChunk->SetBlock(dispRelCoord, E_BLOCK_LAVA, 0); DispChunk->SetBlock(DispRelCoord, E_BLOCK_LAVA, 0);
} }
else else
{ {
@ -112,10 +113,10 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_SPAWN_EGG: case E_ITEM_SPAWN_EGG:
{ {
double MobX = 0.5 + dispAbsCoord.x; double MobX = 0.5 + DispAbsCoord.x;
double MobZ = 0.5 + dispAbsCoord.z; double MobZ = 0.5 + DispAbsCoord.z;
auto MonsterType = cItemSpawnEggHandler::ItemDamageToMonsterType(m_Contents.GetSlot(a_SlotNum).m_ItemDamage); auto MonsterType = cItemSpawnEggHandler::ItemDamageToMonsterType(m_Contents.GetSlot(a_SlotNum).m_ItemDamage);
if (m_World->SpawnMob(MobX, dispAbsCoord.y, MobZ, MonsterType, false) != cEntity::INVALID_ID) if (m_World->SpawnMob(MobX, DispAbsCoord.y, MobZ, MonsterType, false) != cEntity::INVALID_ID)
{ {
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }
@ -125,9 +126,9 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_BLOCK_TNT: case E_BLOCK_TNT:
{ {
// Spawn a primed TNT entity, if space allows: // Spawn a primed TNT entity, if space allows:
if (!cBlockInfo::IsSolid(dispBlock)) if (!cBlockInfo::IsSolid(DispBlock))
{ {
m_World->SpawnPrimedTNT(Vector3d(0.5, 0.5, 0.5) + dispAbsCoord, 80, 0); // 80 ticks fuse, no initial velocity m_World->SpawnPrimedTNT(Vector3d(0.5, 0.5, 0.5) + DispAbsCoord, 80, 0); // 80 ticks fuse, no initial velocity
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }
break; break;
@ -136,9 +137,9 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_FLINT_AND_STEEL: case E_ITEM_FLINT_AND_STEEL:
{ {
// Spawn fire if the block in front is air. // Spawn fire if the block in front is air.
if (dispBlock == E_BLOCK_AIR) if (DispBlock == E_BLOCK_AIR)
{ {
dispChunk->SetBlock(dispRelCoord, E_BLOCK_FIRE, 0); DispChunk->SetBlock(DispRelCoord, E_BLOCK_FIRE, 0);
bool ItemBroke = m_Contents.DamageItem(a_SlotNum, 1); bool ItemBroke = m_Contents.DamageItem(a_SlotNum, 1);
@ -152,7 +153,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_FIRE_CHARGE: case E_ITEM_FIRE_CHARGE:
{ {
if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkFireCharge, GetShootVector(meta) * 20) != cEntity::INVALID_ID) if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkFireCharge, GetShootVector(Meta) * 20) != cEntity::INVALID_ID)
{ {
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }
@ -161,7 +162,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_ARROW: case E_ITEM_ARROW:
{ {
if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkArrow, GetShootVector(meta) * 30 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkArrow, GetShootVector(Meta) * 30 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID)
{ {
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }
@ -170,7 +171,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_SNOWBALL: case E_ITEM_SNOWBALL:
{ {
if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkSnowball, GetShootVector(meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkSnowball, GetShootVector(Meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID)
{ {
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }
@ -179,7 +180,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_EGG: case E_ITEM_EGG:
{ {
if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkEgg, GetShootVector(meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkEgg, GetShootVector(Meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID)
{ {
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }
@ -188,7 +189,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_BOTTLE_O_ENCHANTING: case E_ITEM_BOTTLE_O_ENCHANTING:
{ {
if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkExpBottle, GetShootVector(meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID) if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkExpBottle, GetShootVector(Meta) * 20 + Vector3d(0, 1, 0)) != cEntity::INVALID_ID)
{ {
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }
@ -197,7 +198,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_POTION: case E_ITEM_POTION:
{ {
if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkSplashPotion, GetShootVector(meta) * 20 + Vector3d(0, 1, 0), &SlotItem) != cEntity::INVALID_ID) if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkSplashPotion, GetShootVector(Meta) * 20 + Vector3d(0, 1, 0), &SlotItem) != cEntity::INVALID_ID)
{ {
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }
@ -211,7 +212,9 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
DropFromSlot(a_Chunk, a_SlotNum); DropFromSlot(a_Chunk, a_SlotNum);
break; break;
} }
if (m_World->GrowRipePlant(dispAbsCoord.x, dispAbsCoord.y, dispAbsCoord.z, true))
// Simulate a right-click with bonemeal:
if (cItemDyeHandler::FertilizePlant(*m_World, DispAbsCoord))
{ {
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }
@ -225,13 +228,13 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_ACACIA_BOAT: case E_ITEM_ACACIA_BOAT:
case E_ITEM_DARK_OAK_BOAT: case E_ITEM_DARK_OAK_BOAT:
{ {
Vector3d spawnPos = dispAbsCoord; Vector3d spawnPos = DispAbsCoord;
if (IsBlockWater(dispBlock)) if (IsBlockWater(DispBlock))
{ {
// Water next to the dispenser, spawn a boat above the water block // Water next to the dispenser, spawn a boat above the water block
spawnPos.y += 1; spawnPos.y += 1;
} }
else if (IsBlockWater(dispChunk->GetBlock(dispRelCoord.addedY(-1)))) else if (IsBlockWater(DispChunk->GetBlock(DispRelCoord.addedY(-1))))
{ {
// Water one block below the dispenser, spawn a boat at the dispenser's Y level // Water one block below the dispenser, spawn a boat at the dispenser's Y level
// No adjustment needed // No adjustment needed
@ -243,7 +246,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
break; break;
} }
spawnPos += GetShootVector(meta) * 0.8; // A boat is bigger than one block. Add the shoot vector to put it outside the dispenser. spawnPos += GetShootVector(Meta) * 0.8; // A boat is bigger than one block. Add the shoot vector to put it outside the dispenser.
spawnPos += Vector3d(0.5, 0.5, 0.5); spawnPos += Vector3d(0.5, 0.5, 0.5);
if (m_World->SpawnBoat(spawnPos, cBoat::ItemToMaterial(SlotItem))) if (m_World->SpawnBoat(spawnPos, cBoat::ItemToMaterial(SlotItem)))
@ -255,7 +258,7 @@ void cDispenserEntity::DropSpenseFromSlot(cChunk & a_Chunk, int a_SlotNum)
case E_ITEM_FIREWORK_ROCKET: case E_ITEM_FIREWORK_ROCKET:
{ {
if (SpawnProjectileFromDispenser(dispAbsCoord, cProjectileEntity::pkFirework, GetShootVector(meta) * 20 + Vector3d(0, 1, 0), &SlotItem) != cEntity::INVALID_ID) if (SpawnProjectileFromDispenser(DispAbsCoord, cProjectileEntity::pkFirework, GetShootVector(Meta) * 20 + Vector3d(0, 1, 0), &SlotItem) != cEntity::INVALID_ID)
{ {
m_Contents.ChangeSlotCount(a_SlotNum, -1); m_Contents.ChangeSlotCount(a_SlotNum, -1);
} }

View File

@ -52,37 +52,42 @@ private:
} }
// Fully grown, drop the crop's produce: // Fully grown, drop the crop's produce:
cItems res; cItems Res;
switch (m_BlockType) switch (m_BlockType)
{ {
case E_BLOCK_BEETROOTS: case E_BLOCK_BEETROOTS:
{ {
const auto SeedCount = CalculateSeedCount(0, 3, ToolFortuneLevel(a_Tool)); const auto SeedCount = CalculateSeedCount(0, 3, ToolFortuneLevel(a_Tool));
res.Add(E_ITEM_BEETROOT_SEEDS, SeedCount); Res.Add(E_ITEM_BEETROOT_SEEDS, SeedCount);
res.Add(E_ITEM_BEETROOT); Res.Add(E_ITEM_BEETROOT);
break; break;
} }
case E_BLOCK_CROPS: case E_BLOCK_CROPS:
{ {
res.Add(E_ITEM_WHEAT); // https://minecraft.fandom.com/wiki/Seeds_(Wheat)
Res.Add(E_ITEM_WHEAT);
const auto SeedCount = CalculateSeedCount(1, 3, ToolFortuneLevel(a_Tool)); const auto SeedCount = CalculateSeedCount(1, 3, ToolFortuneLevel(a_Tool));
res.Add(E_ITEM_SEEDS, SeedCount); Res.Add(E_ITEM_SEEDS, SeedCount);
break; break;
} }
case E_BLOCK_CARROTS: case E_BLOCK_CARROTS:
{ {
// https://minecraft.gamepedia.com/Carrot#Breaking
const auto CarrotCount = CalculateSeedCount(1, 4, ToolFortuneLevel(a_Tool)); const auto CarrotCount = CalculateSeedCount(1, 4, ToolFortuneLevel(a_Tool));
res.Add(E_ITEM_CARROT, CarrotCount); Res.Add(E_ITEM_CARROT, CarrotCount);
break; break;
} }
case E_BLOCK_POTATOES: case E_BLOCK_POTATOES:
{ {
// https://minecraft.gamepedia.com/Potato#Breaking
const auto PotatoCount = CalculateSeedCount(2, 3, ToolFortuneLevel(a_Tool)); const auto PotatoCount = CalculateSeedCount(2, 3, ToolFortuneLevel(a_Tool));
res.Add(E_ITEM_POTATO, PotatoCount); Res.Add(E_ITEM_POTATO, PotatoCount);
if (rand.RandBool(0.02)) if (rand.RandBool(0.02))
{ {
// With a 2% chance, drop a poisonous potato as well // https://minecraft.gamepedia.com/Poisonous_Potato#Obtaining
res.Add(E_ITEM_POISONOUS_POTATO); // With a 2% chance, drop a poisonous potato as well:
Res.Add(E_ITEM_POISONOUS_POTATO);
} }
break; break;
} }
@ -92,7 +97,7 @@ private:
break; break;
} }
} // switch (m_BlockType) } // switch (m_BlockType)
return res; return Res;
} }
@ -101,16 +106,10 @@ private:
virtual int Grow(cChunk & a_Chunk, Vector3i a_RelPos, int a_NumStages = 1) const override virtual int Grow(cChunk & a_Chunk, Vector3i a_RelPos, int a_NumStages = 1) const override
{ {
auto oldMeta = a_Chunk.GetMeta(a_RelPos); const auto OldMeta = a_Chunk.GetMeta(a_RelPos);
if (oldMeta >= RipeMeta) const auto NewMeta = std::clamp<NIBBLETYPE>(static_cast<NIBBLETYPE>(OldMeta + a_NumStages), 0, RipeMeta);
{ a_Chunk.SetMeta(a_RelPos, NewMeta);
// Already ripe return NewMeta - OldMeta;
return 0;
}
auto newMeta = std::min<int>(oldMeta + a_NumStages, RipeMeta);
ASSERT(newMeta > oldMeta);
a_Chunk.GetWorld()->SetBlock(a_Chunk.RelativeToAbsolute(a_RelPos), m_BlockType, static_cast<NIBBLETYPE>(newMeta));
return newMeta - oldMeta;
} }
@ -131,8 +130,4 @@ private:
UNUSED(a_Meta); UNUSED(a_Meta);
return 7; return 7;
} }
} ; };

View File

@ -150,7 +150,10 @@ private:
{ {
case paGrowth: case paGrowth:
{ {
Grow(a_Chunk, a_RelPos); if (Grow(a_Chunk, a_RelPos) == 0)
{
BearFruit(a_Chunk, a_RelPos);
}
break; break;
} }
case paDeath: case paDeath:
@ -161,4 +164,10 @@ private:
case paStay: break; // do nothing case paStay: break; // do nothing
} }
} }
/** Grows the final produce next to the stem at the specified position. */
virtual void BearFruit(cChunk & a_Chunk, const Vector3i a_StemRelPos) const
{
// Nothing to do by default
}
}; };

View File

@ -24,7 +24,32 @@ private:
virtual cItems ConvertToPickups(NIBBLETYPE a_BlockMeta, const cEntity * a_Digger, const cItem * a_Tool) const override virtual cItems ConvertToPickups(NIBBLETYPE a_BlockMeta, const cEntity * a_Digger, const cItem * a_Tool) const override
{ {
return cItem(StemPickupType, 1, 0); /*
Use correct percent:
https://minecraft.gamepedia.com/Melon_Seeds#Breaking
https://minecraft.gamepedia.com/Pumpkin_Seeds#Breaking
*/
// Age > 7 (Impossible)
if (a_BlockMeta > 7)
{
return cItem(StemPickupType);
}
const auto Threshold = GetRandomProvider().RandReal<double>(100);
double Cumulative = 0;
char Count = 0;
for (; Count < 4; Count++)
{
Cumulative += m_AgeSeedDropProbability[static_cast<size_t>(a_BlockMeta)][static_cast<size_t>(Count)];
if (Cumulative > Threshold)
{
break;
}
}
return cItem(StemPickupType, Count);
} }
@ -52,60 +77,51 @@ private:
virtual int Grow(cChunk & a_Chunk, Vector3i a_RelPos, int a_NumStages = 1) const override virtual int Grow(cChunk & a_Chunk, Vector3i a_RelPos, int a_NumStages = 1) const override
{ {
auto oldMeta = a_Chunk.GetMeta(a_RelPos); const auto OldMeta = a_Chunk.GetMeta(a_RelPos);
auto meta = oldMeta + a_NumStages; const auto NewMeta = std::clamp<NIBBLETYPE>(static_cast<NIBBLETYPE>(OldMeta + a_NumStages), 0, 7);
a_Chunk.SetBlock(a_RelPos, m_BlockType, static_cast<NIBBLETYPE>(std::min(meta, 7))); // Update the stem a_Chunk.SetMeta(a_RelPos, NewMeta);
if (meta > 7) return NewMeta - OldMeta;
{
if (growProduce(a_Chunk, a_RelPos))
{
return 8 - oldMeta;
}
else
{
return 7 - oldMeta;
}
}
return meta - oldMeta;
} }
/** Grows the final produce next to the stem at the specified pos.
Returns true if successful, false if not. */
static bool growProduce(cChunk & a_Chunk, Vector3i a_StemRelPos)
virtual void BearFruit(cChunk & a_Chunk, const Vector3i a_StemRelPos) const override
{ {
auto & random = GetRandomProvider(); auto & Random = GetRandomProvider();
// Check if there's another produce around the stem, if so, abort: // Check if there's another produce around the stem, if so, abort:
static const Vector3i neighborOfs[] = static constexpr std::array<Vector3i, 4> NeighborOfs =
{ {
{ 1, 0, 0}, {
{-1, 0, 0}, { 1, 0, 0},
{ 0, 0, 1}, {-1, 0, 0},
{ 0, 0, -1}, { 0, 0, 1},
{ 0, 0, -1},
}
}; };
bool isValid;
BLOCKTYPE blockType[4]; std::array<BLOCKTYPE, 4> BlockType;
NIBBLETYPE blockMeta; // unused
isValid = a_Chunk.UnboundedRelGetBlock(a_StemRelPos + neighborOfs[0], blockType[0], blockMeta);
isValid = isValid && a_Chunk.UnboundedRelGetBlock(a_StemRelPos + neighborOfs[1], blockType[1], blockMeta);
isValid = isValid && a_Chunk.UnboundedRelGetBlock(a_StemRelPos + neighborOfs[2], blockType[2], blockMeta);
isValid = isValid && a_Chunk.UnboundedRelGetBlock(a_StemRelPos + neighborOfs[3], blockType[3], blockMeta);
if ( if (
!isValid || !a_Chunk.UnboundedRelGetBlockType(a_StemRelPos + NeighborOfs[0], BlockType[0]) ||
(blockType[0] == ProduceBlockType) || !a_Chunk.UnboundedRelGetBlockType(a_StemRelPos + NeighborOfs[1], BlockType[1]) ||
(blockType[1] == ProduceBlockType) || !a_Chunk.UnboundedRelGetBlockType(a_StemRelPos + NeighborOfs[2], BlockType[2]) ||
(blockType[2] == ProduceBlockType) || !a_Chunk.UnboundedRelGetBlockType(a_StemRelPos + NeighborOfs[3], BlockType[3]) ||
(blockType[3] == ProduceBlockType) (BlockType[0] == ProduceBlockType) ||
(BlockType[1] == ProduceBlockType) ||
(BlockType[2] == ProduceBlockType) ||
(BlockType[3] == ProduceBlockType)
) )
{ {
// Neighbors not valid or already taken by the same produce // Neighbors not valid or already taken by the same produce:
return false; return;
} }
// Pick a direction in which to place the produce: // Pick a direction in which to place the produce:
int x = 0, z = 0; int x = 0, z = 0;
int checkType = random.RandInt(3); // The index to the neighbors array which should be checked for emptiness const auto CheckType = Random.RandInt<size_t>(3); // The index to the neighbors array which should be checked for emptiness
switch (checkType) switch (CheckType)
{ {
case 0: x = 1; break; case 0: x = 1; break;
case 1: x = -1; break; case 1: x = -1; break;
@ -114,7 +130,7 @@ private:
} }
// Check that the block in that direction is empty: // Check that the block in that direction is empty:
switch (blockType[checkType]) switch (BlockType[CheckType])
{ {
case E_BLOCK_AIR: case E_BLOCK_AIR:
case E_BLOCK_SNOW: case E_BLOCK_SNOW:
@ -123,39 +139,72 @@ private:
{ {
break; break;
} }
default: return false; default: return;
} }
// Check if there's soil under the neighbor. We already know the neighbors are valid. Place produce if ok // Check if there's soil under the neighbor. We already know the neighbors are valid. Place produce if ok
BLOCKTYPE soilType; BLOCKTYPE SoilType;
auto produceRelPos = a_StemRelPos + Vector3i(x, 0, z); const auto ProduceRelPos = a_StemRelPos + Vector3i(x, 0, z);
VERIFY(a_Chunk.UnboundedRelGetBlock(produceRelPos.addedY(-1), soilType, blockMeta)); VERIFY(a_Chunk.UnboundedRelGetBlockType(ProduceRelPos.addedY(-1), SoilType));
switch (soilType)
switch (SoilType)
{ {
case E_BLOCK_DIRT: case E_BLOCK_DIRT:
case E_BLOCK_GRASS: case E_BLOCK_GRASS:
case E_BLOCK_FARMLAND: case E_BLOCK_FARMLAND:
{ {
// Place a randomly-facing produce: const NIBBLETYPE Meta = (ProduceBlockType == E_BLOCK_MELON) ? 0 : static_cast<NIBBLETYPE>(Random.RandInt(4) % 4);
NIBBLETYPE meta = (ProduceBlockType == E_BLOCK_MELON) ? 0 : static_cast<NIBBLETYPE>(random.RandInt(4) % 4);
auto produceAbsPos = a_Chunk.RelativeToAbsolute(produceRelPos);
FLOGD("Growing melon / pumpkin at {0} (<{1}, {2}> from stem), overwriting {3}, growing on top of {4}, meta {5}", FLOGD("Growing melon / pumpkin at {0} (<{1}, {2}> from stem), overwriting {3}, growing on top of {4}, meta {5}",
produceAbsPos, a_Chunk.RelativeToAbsolute(ProduceRelPos),
x, z, x, z,
ItemTypeToString(blockType[checkType]), ItemTypeToString(BlockType[CheckType]),
ItemTypeToString(soilType), ItemTypeToString(SoilType),
meta Meta
); );
a_Chunk.GetWorld()->SetBlock(produceAbsPos, ProduceBlockType, meta);
return true; // Place a randomly-facing produce:
a_Chunk.SetBlock(ProduceRelPos, ProduceBlockType, Meta);
} }
} }
return false;
} }
private:
// https://minecraft.gamepedia.com/Pumpkin_Seeds#Breaking
// https://minecraft.gamepedia.com/Melon_Seeds#Breaking
/** The array describes how many seed may be dropped at which age. The inner arrays describe the probability to drop 0, 1, 2, 3 seeds.
The outer describes the age of the stem. */
static constexpr std::array<std::array<double, 4>, 8> m_AgeSeedDropProbability
{
{
{
81.3, 17.42, 1.24, 0.03
},
{
65.1, 30.04, 4.62, 0.24
},
{
51.2, 38.4, 9.6, 0.8
},
{
39.44, 43.02, 15.64, 1.9
},
{
29.13, 44.44, 22.22, 3.7
},
{
21.6, 43.2, 28.8, 6.4
},
{
15.17, 39.82, 34.84, 10.16
},
{
10.16, 34.84, 39.82, 15.17
}
}
};
} ; } ;
using cBlockMelonStemHandler = cBlockStemsHandler<E_BLOCK_MELON, E_ITEM_MELON_SEEDS>; using cBlockMelonStemHandler = cBlockStemsHandler<E_BLOCK_MELON, E_ITEM_MELON_SEEDS>;
using cBlockPumpkinStemHandler = cBlockStemsHandler<E_BLOCK_PUMPKIN, E_ITEM_PUMPKIN_SEEDS>; using cBlockPumpkinStemHandler = cBlockStemsHandler<E_BLOCK_PUMPKIN, E_ITEM_PUMPKIN_SEEDS>;

View File

@ -820,10 +820,12 @@ void cChunk::TickBlocks(void)
// Tick random blocks, but the first one should be m_BlockToTick (so that SetNextBlockToTick() works) // Tick random blocks, but the first one should be m_BlockToTick (so that SetNextBlockToTick() works)
auto Idx = cChunkDef::MakeIndexNoCheck(m_BlockToTick); auto Idx = cChunkDef::MakeIndexNoCheck(m_BlockToTick);
auto & Random = GetRandomProvider();
for (int i = 0; i < 50; ++i) for (int i = 0; i < 50; ++i)
{ {
auto Pos = cChunkDef::IndexToCoordinate(static_cast<size_t>(Idx)); auto Pos = cChunkDef::IndexToCoordinate(static_cast<size_t>(Idx));
Idx = m_World->GetTickRandomNumber(cChunkDef::NumBlocks - 1); Idx = Random.RandInt(cChunkDef::NumBlocks - 1);
if (Pos.y > cChunkDef::GetHeight(m_HeightMap, Pos.x, Pos.z)) if (Pos.y > cChunkDef::GetHeight(m_HeightMap, Pos.x, Pos.z))
{ {
continue; // It's all air up here continue; // It's all air up here

View File

@ -102,13 +102,13 @@ public:
fertilization success is reported even in the case when the chance fails (bonemeal still needs to be consumed). */ fertilization success is reported even in the case when the chance fails (bonemeal still needs to be consumed). */
static bool FertilizePlant(cWorld & a_World, Vector3i a_BlockPos) static bool FertilizePlant(cWorld & a_World, Vector3i a_BlockPos)
{ {
BLOCKTYPE blockType; BLOCKTYPE BlockType;
NIBBLETYPE blockMeta; NIBBLETYPE BlockMeta;
if (!a_World.GetBlockTypeMeta(a_BlockPos, blockType, blockMeta)) if (!a_World.GetBlockTypeMeta(a_BlockPos, BlockType, BlockMeta))
{ {
return false; return false;
} }
switch (blockType) switch (BlockType)
{ {
case E_BLOCK_WHEAT: case E_BLOCK_WHEAT:
case E_BLOCK_CARROTS: case E_BLOCK_CARROTS:
@ -117,8 +117,8 @@ public:
case E_BLOCK_PUMPKIN_STEM: case E_BLOCK_PUMPKIN_STEM:
{ {
// Grow by 2 - 5 stages: // Grow by 2 - 5 stages:
auto numStages = GetRandomProvider().RandInt(2, 5); auto NumStages = GetRandomProvider().RandInt(2, 5);
if (a_World.GrowPlantAt(a_BlockPos, numStages) <= 0) if (a_World.GrowPlantAt(a_BlockPos, NumStages) <= 0)
{ {
return false; return false;
} }
@ -128,14 +128,21 @@ public:
case E_BLOCK_BEETROOTS: case E_BLOCK_BEETROOTS:
{ {
// 75% chance of 1-stage growth: if (a_World.GrowPlantAt(a_BlockPos, 1) <= 0)
if (GetRandomProvider().RandBool(0.75))
{ {
if (a_World.GrowPlantAt(a_BlockPos, 1) > 0) // Fix GH #4805 (bonemeal should only advance growth, not spawn produce):
{ return false;
a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0);
}
} }
a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0);
// 75% chance of 1-stage growth:
if (!GetRandomProvider().RandBool(0.75))
{
// Hit the 25%, rollback:
a_World.GrowPlantAt(a_BlockPos, -1);
}
return true; return true;
} // case beetroots } // case beetroots
@ -144,27 +151,25 @@ public:
// 45% chance of growing to the next stage / full tree: // 45% chance of growing to the next stage / full tree:
if (GetRandomProvider().RandBool(0.45)) if (GetRandomProvider().RandBool(0.45))
{ {
if (a_World.GrowPlantAt(a_BlockPos, 1) > 0) a_World.GrowPlantAt(a_BlockPos, 1);
{
a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0);
}
} }
a_World.BroadcastSoundParticleEffect(EffectID::PARTICLE_HAPPY_VILLAGER, a_BlockPos, 0);
return true; return true;
} } // case sapling
case E_BLOCK_BIG_FLOWER: case E_BLOCK_BIG_FLOWER:
{ {
// Drop the corresponding flower item without destroying the block: // Drop the corresponding flower item without destroying the block:
cItems pickups; cItems Pickups;
switch (blockMeta) switch (BlockMeta)
{ {
case E_META_BIG_FLOWER_SUNFLOWER: pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_SUNFLOWER); break; case E_META_BIG_FLOWER_SUNFLOWER: Pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_SUNFLOWER); break;
case E_META_BIG_FLOWER_LILAC: pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_LILAC); break; case E_META_BIG_FLOWER_LILAC: Pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_LILAC); break;
case E_META_BIG_FLOWER_ROSE_BUSH: pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_ROSE_BUSH); break; case E_META_BIG_FLOWER_ROSE_BUSH: Pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_ROSE_BUSH); break;
case E_META_BIG_FLOWER_PEONY: pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_PEONY); break; case E_META_BIG_FLOWER_PEONY: Pickups.Add(E_BLOCK_BIG_FLOWER, 1, E_META_BIG_FLOWER_PEONY); break;
} }
// TODO: Should we call any hook for this? // TODO: Should we call any hook for this?
a_World.SpawnItemPickups(pickups, a_BlockPos); a_World.SpawnItemPickups(Pickups, a_BlockPos);
return true; return true;
} // big flower } // big flower

View File

@ -1801,36 +1801,6 @@ bool cWorld::GrowRipePlant(Vector3i a_BlockPos)
int cWorld::GrowCactus(int a_BlockX, int a_BlockY, int a_BlockZ, int a_NumBlocksToGrow)
{
LOGWARNING("cWorld::GrowCactus is obsolete, use cWorld::GrowPlantAt instead");
return m_ChunkMap->GrowPlantAt({a_BlockX, a_BlockY, a_BlockZ});
}
bool cWorld::GrowMelonPumpkin(int a_BlockX, int a_BlockY, int a_BlockZ, BLOCKTYPE a_BlockType)
{
LOGWARNING("cWorld::GrowMelonPumpkin is obsolete, use cWorld::GrowPlantAt instead");
return (m_ChunkMap->GrowPlantAt({a_BlockX, a_BlockY, a_BlockZ}, 16) > 0); // 8 stages for the stem, 8 attempts for the produce
}
int cWorld::GrowSugarcane(int a_BlockX, int a_BlockY, int a_BlockZ, int a_NumBlocksToGrow)
{
LOGWARNING("cWorld::GrowSugarcane is obsolete, use cWorld::GrowPlantAt instead");
return m_ChunkMap->GrowPlantAt({a_BlockX, a_BlockY, a_BlockZ}, a_NumBlocksToGrow);
}
EMCSBiome cWorld::GetBiomeAt (int a_BlockX, int a_BlockZ) EMCSBiome cWorld::GetBiomeAt (int a_BlockX, int a_BlockZ)
{ {
return m_ChunkMap->GetBiomeAt(a_BlockX, a_BlockZ); return m_ChunkMap->GetBiomeAt(a_BlockX, a_BlockZ);

View File

@ -862,18 +862,10 @@ public:
bool GrowRipePlant(int a_BlockX, int a_BlockY, int a_BlockZ, bool a_IsByBonemeal = false) bool GrowRipePlant(int a_BlockX, int a_BlockY, int a_BlockZ, bool a_IsByBonemeal = false)
{ {
UNUSED(a_IsByBonemeal); UNUSED(a_IsByBonemeal);
return GrowRipePlant({a_BlockX, a_BlockY, a_BlockZ}); LOGWARNING("Warning: cWorld:GrowRipePlant function expects Vector3i-based coords rather than int-based coords. Emulating old-style call.");
return GrowRipePlant({ a_BlockX, a_BlockY, a_BlockZ });
} }
/** Grows a cactus present at the block specified by the amount of blocks specified, up to the max height specified in the config; returns the amount of blocks the cactus grew inside this call */
int GrowCactus(int a_BlockX, int a_BlockY, int a_BlockZ, int a_NumBlocksToGrow);
/** Grows a melon or a pumpkin next to the block specified (assumed to be the stem); returns true if the pumpkin or melon sucessfully grew. */
bool GrowMelonPumpkin(int a_BlockX, int a_BlockY, int a_BlockZ, BLOCKTYPE a_BlockType);
/** Grows a sugarcane present at the block specified by the amount of blocks specified, up to the max height specified in the config; returns the amount of blocks the sugarcane grew inside this call */
int GrowSugarcane(int a_BlockX, int a_BlockY, int a_BlockZ, int a_NumBlocksToGrow);
/** Returns the biome at the specified coords. Reads the biome from the chunk, if loaded, otherwise uses the world generator to provide the biome value */ /** Returns the biome at the specified coords. Reads the biome from the chunk, if loaded, otherwise uses the world generator to provide the biome value */
EMCSBiome GetBiomeAt(int a_BlockX, int a_BlockZ); EMCSBiome GetBiomeAt(int a_BlockX, int a_BlockZ);