diff --git a/generated/block/VanillaBlocks.php b/generated/block/VanillaBlocks.php index ef1132284..a1ce3f909 100644 --- a/generated/block/VanillaBlocks.php +++ b/generated/block/VanillaBlocks.php @@ -216,6 +216,7 @@ final class VanillaBlocks{ private static Wall $_mCOBBLESTONE_WALL; private static Cobweb $_mCOBWEB; private static CocoaBlock $_mCOCOA_POD; + private static Composter $_mCOMPOSTER; private static ChemistryTable $_mCOMPOUND_CREATOR; private static Concrete $_mCONCRETE; private static ConcretePowder $_mCONCRETE_POWDER; @@ -755,6 +756,7 @@ final class VanillaBlocks{ private static Slab $_mSANDSTONE_SLAB; private static Stair $_mSANDSTONE_STAIRS; private static Wall $_mSANDSTONE_WALL; + private static Scaffolding $_mSCAFFOLDING; private static Sculk $_mSCULK; private static Seagrass $_mSEAGRASS; private static SeaLantern $_mSEA_LANTERN; @@ -1096,6 +1098,7 @@ private static function getInitAssigners() : array{ "cobblestone_wall" => fn(Wall $v) => self::$_mCOBBLESTONE_WALL = $v, "cobweb" => fn(Cobweb $v) => self::$_mCOBWEB = $v, "cocoa_pod" => fn(CocoaBlock $v) => self::$_mCOCOA_POD = $v, + "composter" => fn(Composter $v) => self::$_mCOMPOSTER = $v, "compound_creator" => fn(ChemistryTable $v) => self::$_mCOMPOUND_CREATOR = $v, "concrete" => fn(Concrete $v) => self::$_mCONCRETE = $v, "concrete_powder" => fn(ConcretePowder $v) => self::$_mCONCRETE_POWDER = $v, @@ -1635,6 +1638,7 @@ private static function getInitAssigners() : array{ "sandstone_slab" => fn(Slab $v) => self::$_mSANDSTONE_SLAB = $v, "sandstone_stairs" => fn(Stair $v) => self::$_mSANDSTONE_STAIRS = $v, "sandstone_wall" => fn(Wall $v) => self::$_mSANDSTONE_WALL = $v, + "scaffolding" => fn(Scaffolding $v) => self::$_mSCAFFOLDING = $v, "sculk" => fn(Sculk $v) => self::$_mSCULK = $v, "seagrass" => fn(Seagrass $v) => self::$_mSEAGRASS = $v, "sea_lantern" => fn(SeaLantern $v) => self::$_mSEA_LANTERN = $v, @@ -2684,6 +2688,11 @@ public static function COCOA_POD() : CocoaBlock{ return clone self::$_mCOCOA_POD; } + public static function COMPOSTER() : Composter{ + if(!isset(self::$_mCOMPOSTER)){ self::init(); } + return clone self::$_mCOMPOSTER; + } + public static function COMPOUND_CREATOR() : ChemistryTable{ if(!isset(self::$_mCOMPOUND_CREATOR)){ self::init(); } return clone self::$_mCOMPOUND_CREATOR; @@ -5379,6 +5388,11 @@ public static function SANDSTONE_WALL() : Wall{ return clone self::$_mSANDSTONE_WALL; } + public static function SCAFFOLDING() : Scaffolding{ + if(!isset(self::$_mSCAFFOLDING)){ self::init(); } + return clone self::$_mSCAFFOLDING; + } + public static function SCULK() : Sculk{ if(!isset(self::$_mSCULK)){ self::init(); } return clone self::$_mSCULK; diff --git a/src/block/BlockTypeIds.php b/src/block/BlockTypeIds.php index ab4895e94..0a3415430 100644 --- a/src/block/BlockTypeIds.php +++ b/src/block/BlockTypeIds.php @@ -903,8 +903,9 @@ private function __construct(){ public const SHORT_DRY_GRASS = 10870; public const TALL_DRY_GRASS = 10871; public const DRIED_GHAST = 10872; + public const COMPOSTER = 10873; - public const FIRST_UNUSED_BLOCK_ID = 10873; + public const FIRST_UNUSED_BLOCK_ID = 10874; private static int $nextDynamicId = self::FIRST_UNUSED_BLOCK_ID; diff --git a/src/block/Composter.php b/src/block/Composter.php new file mode 100644 index 000000000..ae9034f13 --- /dev/null +++ b/src/block/Composter.php @@ -0,0 +1,276 @@ +boundedIntAuto(self::MIN_FILL_LEVEL, self::MAX_FILL_LEVEL, $this->fillLevel); + } + + public function getFillLevel() : int{ + return $this->fillLevel; + } + + /** @return $this */ + public function setFillLevel(int $fillLevel) : self{ + if($fillLevel < self::MIN_FILL_LEVEL || $fillLevel > self::MAX_FILL_LEVEL){ + throw new \InvalidArgumentException("Fill level must be in range " . self::MIN_FILL_LEVEL . " ... " . self::MAX_FILL_LEVEL); + } + $this->fillLevel = $fillLevel; + return $this; + } + + public function isEmpty() : bool{ + return $this->fillLevel === self::MIN_FILL_LEVEL; + } + + /** + * Returns whether the composter finished composting and is ready to be emptied. + */ + public function isReady() : bool{ + return $this->fillLevel === self::MAX_FILL_LEVEL; + } + + /** + * Returns the height of the compost inside the composter. An empty composter still has a floor 2/16 thick. + */ + private function getContentHeight() : float{ + return min(16, max(2, 1 + ($this->fillLevel * 2))) / 16; + } + + protected function recalculateCollisionBoxes() : array{ + $result = [ + AxisAlignedBB::one()->trim(Facing::UP, 1 - $this->getContentHeight()) + ]; + + foreach(Facing::HORIZONTAL as $facing){ + $result[] = AxisAlignedBB::one()->trim($facing, 14 / 16); + } + return $result; + } + + public function getSupportType(int $facing) : SupportType{ + return $facing === Facing::UP ? SupportType::EDGE : SupportType::NONE; + } + + public function getFlameEncouragement() : int{ + return 5; + } + + public function getFlammability() : int{ + return 20; + } + + public function onInteract(Item $item, int $face, Vector3 $clickVector, ?Player $player = null, array &$returnedItems = []) : bool{ + $world = $this->position->getWorld(); + $soundPos = $this->position->add(0.5, 0.5, 0.5); + + if($this->isReady()){ + $world->setBlock($this->position, $this->setFillLevel(self::MIN_FILL_LEVEL)); + $world->addSound($soundPos, new ComposterEmptySound()); + $world->dropItem($this->position->add(0.5, 0.85, 0.5), VanillaItems::BONE_MEAL()); + return true; + } + + if($this->fillLevel >= self::FULL_FILL_LEVEL){ + return true; + } + + $chance = self::getCompostChance($item); + if($chance <= 0){ + return false; + } + + $item->pop(); + + if(mt_rand(1, 100) > $chance){ + $world->addSound($soundPos, new ComposterFillSound()); + return true; + } + + $world->setBlock($this->position, $this->setFillLevel($this->fillLevel + 1)); + if($this->fillLevel === self::FULL_FILL_LEVEL){ + $world->addSound($soundPos, new ComposterReadySound()); + $world->scheduleDelayedBlockUpdate($this->position, 20); + }else{ + $world->addSound($soundPos, new ComposterFillSuccessSound()); + } + + return true; + } + + public function onScheduledUpdate() : void{ + if($this->fillLevel === self::FULL_FILL_LEVEL){ + $this->position->getWorld()->setBlock($this->position, $this->setFillLevel(self::MAX_FILL_LEVEL)); + } + } + + /** + * Returns the percentage of chance that the given item has to raise the fill level of a composter, + * or 0 if the item cannot be composted at all. + */ + public static function getCompostChance(Item $item) : int{ + if($item instanceof ItemBlock){ + return self::getBlockCompostChance($item->getBlock()->getTypeId()); + } + + return match($item->getTypeId()){ + ItemTypeIds::BEETROOT_SEEDS, + ItemTypeIds::DRIED_KELP, + ItemTypeIds::GLOW_BERRIES, + ItemTypeIds::MELON_SEEDS, + ItemTypeIds::PITCHER_POD, + ItemTypeIds::PUMPKIN_SEEDS, + ItemTypeIds::SWEET_BERRIES, + ItemTypeIds::TORCHFLOWER_SEEDS, + ItemTypeIds::WHEAT_SEEDS => 30, + ItemTypeIds::MELON => 50, + ItemTypeIds::APPLE, + ItemTypeIds::BEETROOT, + ItemTypeIds::CARROT, + ItemTypeIds::COCOA_BEANS, + ItemTypeIds::POTATO => 65, + ItemTypeIds::BAKED_POTATO, + ItemTypeIds::BREAD, + ItemTypeIds::COOKIE => 85, + ItemTypeIds::PUMPKIN_PIE => 100, + default => 0 + }; + } + + private static function getBlockCompostChance(int $blockTypeId) : int{ + return match($blockTypeId){ + BlockTypeIds::ACACIA_LEAVES, + BlockTypeIds::ACACIA_SAPLING, + BlockTypeIds::AZALEA_LEAVES, + BlockTypeIds::BIRCH_LEAVES, + BlockTypeIds::BIRCH_SAPLING, + BlockTypeIds::CHERRY_LEAVES, + BlockTypeIds::CHERRY_SAPLING, + BlockTypeIds::DARK_OAK_LEAVES, + BlockTypeIds::DARK_OAK_SAPLING, + BlockTypeIds::GRASS, + BlockTypeIds::HANGING_ROOTS, + BlockTypeIds::JUNGLE_LEAVES, + BlockTypeIds::JUNGLE_SAPLING, + BlockTypeIds::MANGROVE_LEAVES, + BlockTypeIds::MANGROVE_ROOTS, + BlockTypeIds::OAK_LEAVES, + BlockTypeIds::OAK_SAPLING, + BlockTypeIds::PINK_PETALS, + BlockTypeIds::SEAGRASS, + BlockTypeIds::SMALL_DRIPLEAF, + BlockTypeIds::SPRUCE_LEAVES, + BlockTypeIds::SPRUCE_SAPLING, + BlockTypeIds::SWEET_BERRY_BUSH, + BlockTypeIds::TALL_GRASS => 30, + BlockTypeIds::CACTUS, + BlockTypeIds::DOUBLE_TALLGRASS, + BlockTypeIds::DRIED_KELP, + BlockTypeIds::FLOWERING_AZALEA_LEAVES, + BlockTypeIds::GLOW_LICHEN, + BlockTypeIds::NETHER_SPROUTS, + BlockTypeIds::SUGARCANE, + BlockTypeIds::TWISTING_VINES, + BlockTypeIds::VINES, + BlockTypeIds::WEEPING_VINES => 50, + BlockTypeIds::ALLIUM, + BlockTypeIds::AZALEA, + BlockTypeIds::AZURE_BLUET, + BlockTypeIds::BIG_DRIPLEAF_HEAD, + BlockTypeIds::BLUE_ORCHID, + BlockTypeIds::BROWN_MUSHROOM, + BlockTypeIds::CARVED_PUMPKIN, + BlockTypeIds::CORNFLOWER, + BlockTypeIds::CRIMSON_FUNGUS, + BlockTypeIds::CRIMSON_ROOTS, + BlockTypeIds::DANDELION, + BlockTypeIds::FERN, + BlockTypeIds::LARGE_FERN, + BlockTypeIds::LILY_OF_THE_VALLEY, + BlockTypeIds::LILY_PAD, + BlockTypeIds::MELON, + BlockTypeIds::MOSS_BLOCK, + BlockTypeIds::MUSHROOM_STEM, + BlockTypeIds::NETHER_WART, + BlockTypeIds::ORANGE_TULIP, + BlockTypeIds::OXEYE_DAISY, + BlockTypeIds::PINK_TULIP, + BlockTypeIds::POPPY, + BlockTypeIds::PUMPKIN, + BlockTypeIds::RED_MUSHROOM, + BlockTypeIds::RED_TULIP, + BlockTypeIds::SEA_PICKLE, + BlockTypeIds::SHROOMLIGHT, + BlockTypeIds::SPORE_BLOSSOM, + BlockTypeIds::WARPED_FUNGUS, + BlockTypeIds::WARPED_ROOTS, + BlockTypeIds::WHEAT, + BlockTypeIds::WHITE_TULIP, + BlockTypeIds::WITHER_ROSE => 65, + //BlockTypeIds::STRAW_BED => 65, //TODO: uncomment when bedrock-1.26.50 is merged + BlockTypeIds::BROWN_MUSHROOM_BLOCK, + BlockTypeIds::FLOWERING_AZALEA, + BlockTypeIds::HAY_BALE, + BlockTypeIds::NETHER_WART_BLOCK, + BlockTypeIds::PITCHER_PLANT, + BlockTypeIds::RED_MUSHROOM_BLOCK, + BlockTypeIds::TORCHFLOWER, + BlockTypeIds::WARPED_WART_BLOCK => 85, + BlockTypeIds::CAKE => 100, + default => 0 + }; + } +} diff --git a/src/block/Scaffolding.php b/src/block/Scaffolding.php new file mode 100644 index 000000000..080b83677 --- /dev/null +++ b/src/block/Scaffolding.php @@ -0,0 +1,153 @@ +boundedIntAuto(self::MIN_STABILITY, self::MAX_STABILITY, $this->stability); + } + + public function getStability() : int{ + return $this->stability; + } + + /** @return $this */ + public function setStability(int $stability) : self{ + if($stability < self::MIN_STABILITY || $stability > self::MAX_STABILITY){ + throw new \InvalidArgumentException("Stability must be in range " . self::MIN_STABILITY . " ... " . self::MAX_STABILITY); + } + $this->stability = $stability; + return $this; + } + + public function isSolid() : bool{ + return false; + } + + public function canClimb() : bool{ + return true; + } + + public function canBeFlowedInto() : bool{ + return false; + } + + /** + * Only the top face is solid enough to walk on. The rest of the block is hollow, which is what makes it + * possible to climb through a scaffolding tower. + */ + protected function recalculateCollisionBoxes() : array{ + return [AxisAlignedBB::one()->trim(Facing::DOWN, 14 / 16)]; + } + + public function getSupportType(int $facing) : SupportType{ + return $facing === Facing::UP ? SupportType::FULL : SupportType::NONE; + } + + public function getFlameEncouragement() : int{ + return 60; + } + + public function getFlammability() : int{ + return 20; + } + + public function place(BlockTransaction $tx, Item $item, Block $blockReplace, Block $blockClicked, int $face, Vector3 $clickVector, ?Player $player = null) : bool{ + $stability = $this->recalculateStability($blockReplace->getPosition()); + if($stability >= self::MAX_STABILITY){ + return false; + } + + $this->stability = $stability; + return parent::place($tx, $item, $blockReplace, $blockClicked, $face, $clickVector, $player); + } + + public function onNearbyBlockChange() : void{ + $stability = $this->recalculateStability($this->position); + if($stability >= self::MAX_STABILITY){ + $this->checkFall(); + return; + } + + if($stability !== $this->stability){ + $this->position->getWorld()->setBlock($this->position, $this->setStability($stability)); + } + } + + /** + * A scaffolding sitting on a solid block is fully stable. Otherwise it inherits the stability of the + * scaffolding below it, or the stability of the closest horizontal neighbour plus one, which is what limits + * how far a scaffolding bridge can reach away from its support. + */ + private function recalculateStability(Position $position) : int{ + $world = $position->getWorld(); + if($world->getBlock($position->getSide(Facing::DOWN))->isSolid()){ + return self::MIN_STABILITY; + } + + $stability = self::MAX_STABILITY; + foreach(Facing::ALL as $facing){ + if($facing === Facing::UP){ + continue; + } + + $neighbor = $world->getBlock($position->getSide($facing)); + if(!$neighbor instanceof Scaffolding){ + continue; + } + + $stability = min($stability, $facing === Facing::DOWN ? $neighbor->stability : $neighbor->stability + 1); + } + + return $stability; + } +} diff --git a/src/block/VanillaBlocksInputs.php b/src/block/VanillaBlocksInputs.php index 7b00aec1e..07f2640f3 100644 --- a/src/block/VanillaBlocksInputs.php +++ b/src/block/VanillaBlocksInputs.php @@ -200,6 +200,7 @@ public function getBreakTime(Item $item) : float{ self::register("cobweb", fn(BID $id) => new Cobweb($id, "Cobweb", new Info(new BreakInfo(4.0, ToolType::SWORD | ToolType::SHEARS, 1)))); self::register("cocoa_pod", fn(BID $id) => new CocoaBlock($id, "Cocoa Block", new Info(BreakInfo::axe(0.2, null, 15.0)))); + self::register("composter", fn(BID $id) => new Composter($id, "Composter", new Info(BreakInfo::axe(0.6)))); self::register("coral_block", fn(BID $id) => new CoralBlock($id, "Coral Block", new Info(BreakInfo::pickaxe(1.5, ToolTier::WOOD, 30.0)))); self::register("daylight_sensor", fn(BID $id) => new DaylightSensor($id, "Daylight Sensor", new Info(BreakInfo::axe(0.2))), TileDaylightSensor::class); self::register("dead_bush", fn(BID $id) => new DeadBush($id, "Dead Bush", new Info(BreakInfo::instant(ToolType::SHEARS, 1), [Tags::POTTABLE_PLANTS]))); @@ -386,6 +387,7 @@ public function getBreakTime(Item $item) : float{ self::register("sand", fn(BID $id) => new Sand($id, "Sand", $sandTypeInfo)); self::register("red_sand", fn(BID $id) => new Sand($id, "Red Sand", $sandTypeInfo)); + self::register("scaffolding", fn(BID $id) => new Scaffolding($id, "Scaffolding", new Info(BreakInfo::instant()))); self::register("sea_lantern", fn(BID $id) => new SeaLantern($id, "Sea Lantern", new Info(new BreakInfo(0.3)))); self::register("sea_pickle", fn(BID $id) => new SeaPickle($id, "Sea Pickle", new Info(BreakInfo::instant()))); self::register("seagrass", fn(BID $id) => new Seagrass($id, "Seagrass", new Info(BreakInfo::instant(ToolType::SHEARS, 1)))); diff --git a/src/data/bedrock/block/convert/VanillaBlockMappings.php b/src/data/bedrock/block/convert/VanillaBlockMappings.php index 836ccafa7..755c7a685 100644 --- a/src/data/bedrock/block/convert/VanillaBlockMappings.php +++ b/src/data/bedrock/block/convert/VanillaBlockMappings.php @@ -45,6 +45,7 @@ use pocketmine\block\ChiseledBookshelf; use pocketmine\block\ChorusFlower; use pocketmine\block\CocoaBlock; +use pocketmine\block\Composter; use pocketmine\block\Copper; use pocketmine\block\CopperLantern; use pocketmine\block\DaylightSensor; @@ -85,6 +86,7 @@ use pocketmine\block\RedstoneTorch; use pocketmine\block\RespawnAnchor; use pocketmine\block\Sapling; +use pocketmine\block\Scaffolding; use pocketmine\block\Seagrass; use pocketmine\block\SeaPickle; use pocketmine\block\SmallDripleaf; @@ -1397,6 +1399,9 @@ private static function register1to1CustomMappings(BlockSerializerDeserializerRe new IntProperty(StateNames::AGE, 0, 2, fn(CocoaBlock $b) => $b->getAge(), fn(CocoaBlock $b, int $v) => $b->setAge($v)), $commonProperties->horizontalFacingSWNEInverted ])); + $reg->mapModel(Model::create(Blocks::COMPOSTER(), Ids::COMPOSTER)->properties([ + new IntProperty(StateNames::COMPOSTER_FILL_LEVEL, Composter::MIN_FILL_LEVEL, Composter::MAX_FILL_LEVEL, fn(Composter $b) => $b->getFillLevel(), fn(Composter $b, int $v) => $b->setFillLevel($v)) + ])); //D $reg->mapModel(Model::create(Blocks::DECORATED_POT(), Ids::DECORATED_POT)->properties([ @@ -1536,6 +1541,10 @@ private static function register1to1CustomMappings(BlockSerializerDeserializerRe ])); //S + $reg->mapModel(Model::create(Blocks::SCAFFOLDING(), Ids::SCAFFOLDING)->properties([ + new IntProperty(StateNames::STABILITY, Scaffolding::MIN_STABILITY, Scaffolding::MAX_STABILITY, fn(Scaffolding $b) => $b->getStability(), fn(Scaffolding $b, int $v) => $b->setStability($v)), + new DummyProperty(StateNames::STABILITY_CHECK, false) //server-side only in Altay, stability is recalculated on block updates + ])); $reg->mapModel(Model::create(Blocks::SEA_PICKLE(), Ids::SEA_PICKLE)->properties([ new IntProperty(StateNames::CLUSTER_COUNT, 0, 3, fn(SeaPickle $b) => $b->getCount(), fn(SeaPickle $b, int $v) => $b->setCount($v), offset: 1), new BoolProperty(StateNames::DEAD_BIT, fn(SeaPickle $b) => $b->isUnderwater(), fn(SeaPickle $b, bool $v) => $b->setUnderwater($v), inverted: true) diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index 946c22942..c8c3071fa 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -311,6 +311,7 @@ private static function registerBlocks(self $result) : void{ $result->registerBlock("colored_torch_rg", fn() => Blocks::RED_TORCH()); $result->registerBlock("comparator", fn() => Blocks::REDSTONE_COMPARATOR()); $result->registerBlock("comparator_block", fn() => Blocks::REDSTONE_COMPARATOR()); + $result->registerBlock("composter", fn() => Blocks::COMPOSTER()); $result->registerBlock("compound_creator", fn() => Blocks::COMPOUND_CREATOR()); $result->registerBlock("concrete", fn() => Blocks::CONCRETE()); $result->registerBlock("concrete_powder", fn() => Blocks::CONCRETE_POWDER()); @@ -1065,6 +1066,7 @@ private static function registerBlocks(self $result) : void{ $result->registerBlock("sandstone_stairs", fn() => Blocks::SANDSTONE_STAIRS()); $result->registerBlock("sandstone_wall", fn() => Blocks::SANDSTONE_WALL()); $result->registerBlock("sapling", fn() => Blocks::OAK_SAPLING()); + $result->registerBlock("scaffolding", fn() => Blocks::SCAFFOLDING()); $result->registerBlock("sculk", fn() => Blocks::SCULK()); $result->registerBlock("sea_lantern", fn() => Blocks::SEA_LANTERN()); $result->registerBlock("sea_pickle", fn() => Blocks::SEA_PICKLE()); diff --git a/src/world/sound/ComposterEmptySound.php b/src/world/sound/ComposterEmptySound.php new file mode 100644 index 000000000..da902c3e4 --- /dev/null +++ b/src/world/sound/ComposterEmptySound.php @@ -0,0 +1,37 @@ +