Implement Cave Vines & Glow Berries (#5424)

This commit is contained in:
ipad54 2023-05-08 21:24:23 +03:00 committed by GitHub
parent d834266635
commit fa719f37d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 320 additions and 6 deletions

View File

@ -713,8 +713,9 @@ final class BlockTypeIds{
public const AZALEA_LEAVES = 10686;
public const FLOWERING_AZALEA_LEAVES = 10687;
public const REINFORCED_DEEPSLATE = 10688;
public const CAVE_VINES = 10689;
public const FIRST_UNUSED_BLOCK_ID = 10689;
public const FIRST_UNUSED_BLOCK_ID = 10690;
private static int $nextDynamicId = self::FIRST_UNUSED_BLOCK_ID;

191
src/block/CaveVines.php Normal file
View File

@ -0,0 +1,191 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block;
use pocketmine\block\utils\SupportType;
use pocketmine\data\runtime\RuntimeDataDescriber;
use pocketmine\entity\Entity;
use pocketmine\event\block\BlockGrowEvent;
use pocketmine\item\Fertilizer;
use pocketmine\item\Item;
use pocketmine\item\VanillaItems;
use pocketmine\math\Facing;
use pocketmine\math\Vector3;
use pocketmine\player\Player;
use pocketmine\world\BlockTransaction;
use pocketmine\world\sound\GlowBerriesPickSound;
use function mt_rand;
class CaveVines extends Flowable{
public const MAX_AGE = 25;
protected int $age = 0;
protected bool $berries = false;
protected bool $head = false;
protected function describeState(RuntimeDataDescriber $w) : void{
$w->boundedInt(5, 0, self::MAX_AGE, $this->age);
$w->bool($this->berries);
$w->bool($this->head);
}
public function hasBerries() : bool{ return $this->berries; }
/** @return $this */
public function setBerries(bool $berries) : self{
$this->berries = $berries;
return $this;
}
public function isHead() : bool{ return $this->head; }
/** @return $this */
public function setHead(bool $head) : self{
$this->head = $head;
return $this;
}
public function getAge() : int{
return $this->age;
}
/** @return $this */
public function setAge(int $age) : self{
if($age < 0 || $age > self::MAX_AGE){
throw new \InvalidArgumentException("Age must be in range 0-" . self::MAX_AGE);
}
$this->age = $age;
return $this;
}
public function canClimb() : bool{
return true;
}
public function getLightLevel() : int{
return $this->berries ? 14 : 0;
}
private function canBeSupportedBy(Block $block) : bool{
return $block->getSupportType(Facing::DOWN)->equals(SupportType::FULL()) || $block->hasSameTypeId($this);
}
public function onNearbyBlockChange() : void{
if(!$this->canBeSupportedBy($this->getSide(Facing::UP))){
$this->position->getWorld()->useBreakOn($this->position);
}
}
public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{
if(!$this->canBeSupportedBy($blockReplace->getSide(Facing::UP))){
return false;
}
$this->age = mt_rand(0, self::MAX_AGE);
return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player);
}
public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{
if($this->berries){
$this->position->getWorld()->dropItem($this->position, $this->asItem());
$this->position->getWorld()->addSound($this->position, new GlowBerriesPickSound());
$this->position->getWorld()->setBlock($this->position, $this->setBerries(false));
return true;
}
if($item instanceof Fertilizer){
$ev = new BlockGrowEvent($this, (clone $this)
->setBerries(true)
->setHead(!$this->getSide(Facing::DOWN)->hasSameTypeId($this))
);
$ev->call();
if($ev->isCancelled()){
return false;
}
$item->pop();
$this->position->getWorld()->setBlock($this->position, $ev->getNewState());
return true;
}
return false;
}
public function onRandomTick() : void{
$head = !$this->getSide(Facing::DOWN)->hasSameTypeId($this);
if($head !== $this->head){
$this->position->getWorld()->setBlock($this->position, $this->setHead($head));
}
if($this->age < self::MAX_AGE && mt_rand(1, 10) === 1){
$growthPos = $this->position->getSide(Facing::DOWN);
$world = $growthPos->getWorld();
if($world->isInWorld($growthPos->getFloorX(), $growthPos->getFloorY(), $growthPos->getFloorZ())){
$block = $world->getBlock($growthPos);
if($block->getTypeId() === BlockTypeIds::AIR){
$ev = new BlockGrowEvent($block, VanillaBlocks::CAVE_VINES()
->setAge($this->age + 1)
->setBerries(mt_rand(1, 9) === 1)
);
$ev->call();
if(!$ev->isCancelled()){
$world->setBlock($growthPos, $ev->getNewState());
}
}
}
}
}
public function ticksRandomly() : bool{
return true;
}
protected function recalculateCollisionBoxes() : array{
return [];
}
public function hasEntityCollision() : bool{
return true;
}
public function onEntityInside(Entity $entity) : bool{
$entity->resetFallDistance();
return false;
}
public function getDropsForCompatibleTool(Item $item) : array{
return $this->berries ? [$this->asItem()] : [];
}
public function isAffectedBySilkTouch() : bool{
return true;
}
public function asItem() : Item{
return VanillaItems::GLOW_BERRIES();
}
public function getSupportType(int $facing) : SupportType{
return SupportType::NONE();
}
}

View File

@ -156,6 +156,7 @@ use function mb_strtolower;
* @method static CartographyTable CARTOGRAPHY_TABLE()
* @method static CarvedPumpkin CARVED_PUMPKIN()
* @method static Cauldron CAULDRON()
* @method static CaveVines CAVE_VINES()
* @method static Chain CHAIN()
* @method static ChemicalHeat CHEMICAL_HEAT()
* @method static Chest CHEST()
@ -1579,6 +1580,8 @@ final class VanillaBlocks{
self::register("cake_with_dyed_candle", new CakeWithDyedCandle(new BID(Ids::CAKE_WITH_DYED_CANDLE), "Cake With Dyed Candle", $cakeBreakInfo));
self::register("hanging_roots", new HangingRoots(new BID(Ids::HANGING_ROOTS), "Hanging Roots", new Info(BreakInfo::instant(ToolType::SHEARS, 1))));
self::register("cave_vines", new CaveVines(new BID(Ids::CAVE_VINES), "Cave Vines", new Info(BreakInfo::instant())));
}
private static function registerBlocksR18() : void{

View File

@ -44,6 +44,7 @@ use pocketmine\block\Candle;
use pocketmine\block\Carpet;
use pocketmine\block\Carrot;
use pocketmine\block\CarvedPumpkin;
use pocketmine\block\CaveVines;
use pocketmine\block\Chain;
use pocketmine\block\ChemistryTable;
use pocketmine\block\Chest;
@ -773,6 +774,17 @@ final class BlockObjectToStateSerializer implements BlockStateSerializer{
return Writer::create(Ids::CARVED_PUMPKIN)
->writeLegacyHorizontalFacing($block->getFacing());
});
$this->map(Blocks::CAVE_VINES(), function(CaveVines $block) : Writer{
//I have no idea why this only has 3 IDs - there are 4 in Java and 4 visually distinct states in Bedrock
return Writer::create($block->hasBerries() ?
($block->isHead() ?
Ids::CAVE_VINES_HEAD_WITH_BERRIES :
Ids::CAVE_VINES_BODY_WITH_BERRIES
) :
Ids::CAVE_VINES
)
->writeInt(StateNames::GROWING_PLANT_AGE, $block->getAge());
});
$this->map(Blocks::CHAIN(), function(Chain $block) : Writer{
return Writer::create(Ids::CHAIN)
->writePillarAxis($block->getAxis());

View File

@ -25,6 +25,7 @@ namespace pocketmine\data\bedrock\block\convert;
use pocketmine\block\Bamboo;
use pocketmine\block\Block;
use pocketmine\block\CaveVines;
use pocketmine\block\ChorusFlower;
use pocketmine\block\Light;
use pocketmine\block\Slab;
@ -611,6 +612,24 @@ final class BlockStateToObjectDeserializer implements BlockStateDeserializer{
return Blocks::CARVED_PUMPKIN()
->setFacing($in->readLegacyHorizontalFacing());
});
$this->map(Ids::CAVE_VINES, function(Reader $in) : CaveVines{
return Blocks::CAVE_VINES()
->setBerries(false)
->setHead(false)
->setAge($in->readBoundedInt(StateNames::GROWING_PLANT_AGE, 0, 25));
});
$this->map(Ids::CAVE_VINES_BODY_WITH_BERRIES, function(Reader $in) : CaveVines{
return Blocks::CAVE_VINES()
->setBerries(true)
->setHead(false)
->setAge($in->readBoundedInt(StateNames::GROWING_PLANT_AGE, 0, 25));
});
$this->map(Ids::CAVE_VINES_HEAD_WITH_BERRIES, function(Reader $in) : CaveVines{
return Blocks::CAVE_VINES()
->setBerries(true)
->setHead(true)
->setAge($in->readBoundedInt(StateNames::GROWING_PLANT_AGE, 0, 25));
});
$this->map(Ids::CHAIN, function(Reader $in) : Block{
return Blocks::CHAIN()
->setAxis($in->readPillarAxis());

View File

@ -233,6 +233,7 @@ final class ItemSerializerDeserializerRegistrar{
$this->map1to1Item(Ids::GHAST_TEAR, Items::GHAST_TEAR());
$this->map1to1Item(Ids::GLASS_BOTTLE, Items::GLASS_BOTTLE());
$this->map1to1Item(Ids::GLISTERING_MELON_SLICE, Items::GLISTERING_MELON());
$this->map1to1Item(Ids::GLOW_BERRIES, Items::GLOW_BERRIES());
$this->map1to1Item(Ids::GLOW_INK_SAC, Items::GLOW_INK_SAC());
$this->map1to1Item(Ids::GLOWSTONE_DUST, Items::GLOWSTONE_DUST());
$this->map1to1Item(Ids::GOLD_INGOT, Items::GOLD_INGOT());

42
src/item/GlowBerries.php Normal file
View File

@ -0,0 +1,42 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\item;
use pocketmine\block\Block;
use pocketmine\block\VanillaBlocks;
class GlowBerries extends Food{
public function getFoodRestore() : int{
return 2;
}
public function getSaturationRestore() : float{
return 0.4;
}
public function getBlock(?int $clickedFace = null) : Block{
return VanillaBlocks::CAVE_VINES();
}
}

View File

@ -301,8 +301,9 @@ final class ItemTypeIds{
public const TURTLE_HELMET = 20262;
public const MEDICINE = 20263;
public const MANGROVE_BOAT = 20264;
public const GLOW_BERRIES = 20265;
public const FIRST_UNUSED_ITEM_ID = 20265;
public const FIRST_UNUSED_ITEM_ID = 20266;
private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID;

View File

@ -206,6 +206,7 @@ final class StringToItemParser extends StringToTParser{
$result->registerBlock("carrots", fn() => Blocks::CARROTS());
$result->registerBlock("carved_pumpkin", fn() => Blocks::CARVED_PUMPKIN());
$result->registerBlock("cauldron", fn() => Blocks::CAULDRON());
$result->registerBlock("cave_vines", fn() => Blocks::CAVE_VINES());
$result->registerBlock("chain", fn() => Blocks::CHAIN());
$result->registerBlock("chemical_heat", fn() => Blocks::CHEMICAL_HEAT());
$result->registerBlock("chemistry_table", fn() => Blocks::COMPOUND_CREATOR());
@ -1275,6 +1276,7 @@ final class StringToItemParser extends StringToTParser{
$result->register("ghast_tear", fn() => Items::GHAST_TEAR());
$result->register("glass_bottle", fn() => Items::GLASS_BOTTLE());
$result->register("glistering_melon", fn() => Items::GLISTERING_MELON());
$result->register("glow_berries", fn() => Items::GLOW_BERRIES());
$result->register("glow_ink_sac", fn() => Items::GLOW_INK_SAC());
$result->register("glowstone_dust", fn() => Items::GLOWSTONE_DUST());
$result->register("gold_axe", fn() => Items::GOLDEN_AXE());

View File

@ -163,6 +163,7 @@ use pocketmine\world\World;
* @method static GlassBottle GLASS_BOTTLE()
* @method static Item GLISTERING_MELON()
* @method static Item GLOWSTONE_DUST()
* @method static GlowBerries GLOW_BERRIES()
* @method static Item GLOW_INK_SAC()
* @method static GoldenApple GOLDEN_APPLE()
* @method static Axe GOLDEN_AXE()
@ -438,6 +439,7 @@ final class VanillaItems{
self::register("ghast_tear", new Item(new IID(Ids::GHAST_TEAR), "Ghast Tear"));
self::register("glass_bottle", new GlassBottle(new IID(Ids::GLASS_BOTTLE), "Glass Bottle"));
self::register("glistering_melon", new Item(new IID(Ids::GLISTERING_MELON), "Glistering Melon"));
self::register("glow_berries", new GlowBerries(new IID(Ids::GLOW_BERRIES), "Glow Berries"));
self::register("glow_ink_sac", new Item(new IID(Ids::GLOW_INK_SAC), "Glow Ink Sac"));
self::register("glowstone_dust", new Item(new IID(Ids::GLOWSTONE_DUST), "Glowstone Dust"));
self::register("gold_ingot", new Item(new IID(Ids::GOLD_INGOT), "Gold Ingot"));

View File

@ -0,0 +1,35 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\world\sound;
use pocketmine\math\Vector3;
use pocketmine\network\mcpe\protocol\LevelSoundEventPacket;
use pocketmine\network\mcpe\protocol\types\LevelSoundEvent;
class GlowBerriesPickSound implements Sound{
public function encode(Vector3 $pos) : array{
return [LevelSoundEventPacket::nonActorSound(LevelSoundEvent::CAVE_VINES_PICK_BERRIES, $pos, false)];
}
}

File diff suppressed because one or more lines are too long

View File

@ -27,6 +27,7 @@ use PHPUnit\Framework\TestCase;
use pocketmine\block\BaseBanner;
use pocketmine\block\Bed;
use pocketmine\block\BlockTypeIds;
use pocketmine\block\CaveVines;
use pocketmine\block\RuntimeBlockStateRegistry;
use pocketmine\block\Skull;
use pocketmine\data\bedrock\block\BlockStateDeserializeException;
@ -61,9 +62,11 @@ final class BlockSerializerDeserializerTest extends TestCase{
}
//The following are workarounds for differences in blockstate representation in Bedrock vs PM
//In these cases, some properties are not stored in the blockstate (but rather in the block entity NBT), but
//they do form part of the internal blockstate hash in PM. This leads to inconsistencies when serializing
//and deserializing blockstates.
//In some cases, some properties are not stored in the blockstate (but rather in the block entity NBT), but
//they do form part of the internal blockstate hash in PM. In other cases, PM allows representing states
//that don't exist in Bedrock, such as the cave vines head without berries, which is a state that visually
//exists in Bedrock, but doesn't have its own ID.
//This leads to inconsistencies when serializing and deserializing blockstates which we need to correct for.
if(
($block instanceof BaseBanner && $newBlock instanceof BaseBanner) ||
($block instanceof Bed && $newBlock instanceof Bed)
@ -71,6 +74,8 @@ final class BlockSerializerDeserializerTest extends TestCase{
$newBlock->setColor($block->getColor());
}elseif($block instanceof Skull && $newBlock instanceof Skull){
$newBlock->setSkullType($block->getSkullType());
}elseif($block instanceof CaveVines && $newBlock instanceof CaveVines && !$block->hasBerries()){
$newBlock->setHead($block->isHead());
}
self::assertSame($block->getStateId(), $newBlock->getStateId(), "Mismatch of blockstate for " . $block->getName() . ", " . print_r($block, true) . " vs " . print_r($newBlock, true));