diff --git a/src/block/Block.php b/src/block/Block.php index 0e045792f91..9f61982ff73 100644 --- a/src/block/Block.php +++ b/src/block/Block.php @@ -36,6 +36,9 @@ use pocketmine\data\runtime\RuntimeDataWriter; use pocketmine\entity\Entity; use pocketmine\entity\projectile\Projectile; +use pocketmine\item\enchantment\AvailableEnchantmentRegistry; +use pocketmine\item\enchantment\ItemEnchantmentTagRegistry; +use pocketmine\item\enchantment\ItemEnchantmentTags; use pocketmine\item\enchantment\VanillaEnchantments; use pocketmine\item\Item; use pocketmine\item\ItemBlock; @@ -422,6 +425,19 @@ public function getBreakInfo() : BlockBreakInfo{ return $this->typeInfo->getBreakInfo(); } + /** + * Returns tags that represent the type of item being enchanted and are used to determine + * what enchantments can be applied to the item of this block during in-game enchanting (enchanting table, anvil, fishing, etc.). + * @see ItemEnchantmentTags + * @see ItemEnchantmentTagRegistry + * @see AvailableEnchantmentRegistry + * + * @return string[] + */ + public function getEnchantmentTags() : array{ + return $this->typeInfo->getEnchantmentTags(); + } + /** * Do the actions needed so the block is broken with the Item * diff --git a/src/block/BlockTypeInfo.php b/src/block/BlockTypeInfo.php index eb6b89ad1aa..a1424ef3cf0 100644 --- a/src/block/BlockTypeInfo.php +++ b/src/block/BlockTypeInfo.php @@ -35,10 +35,12 @@ final class BlockTypeInfo{ /** * @param string[] $typeTags + * @param string[] $enchantmentTags */ public function __construct( private BlockBreakInfo $breakInfo, - array $typeTags = [] + array $typeTags = [], + private array $enchantmentTags = [] ){ $this->typeTags = array_fill_keys($typeTags, true); } @@ -49,4 +51,17 @@ public function getBreakInfo() : BlockBreakInfo{ return $this->breakInfo; } public function getTypeTags() : array{ return array_keys($this->typeTags); } public function hasTypeTag(string $tag) : bool{ return isset($this->typeTags[$tag]); } + + /** + * Returns tags that represent the type of item being enchanted and are used to determine + * what enchantments can be applied to the item of this block during in-game enchanting (enchanting table, anvil, fishing, etc.). + * @see ItemEnchantmentTags + * @see ItemEnchantmentTagRegistry + * @see AvailableEnchantmentRegistry + * + * @return string[] + */ + public function getEnchantmentTags() : array{ + return $this->enchantmentTags; + } } diff --git a/src/block/VanillaBlocks.php b/src/block/VanillaBlocks.php index d733b06c2b8..cb612031f41 100644 --- a/src/block/VanillaBlocks.php +++ b/src/block/VanillaBlocks.php @@ -59,6 +59,7 @@ use pocketmine\block\utils\WoodType; use pocketmine\crafting\FurnaceType; use pocketmine\entity\projectile\Projectile; +use pocketmine\item\enchantment\ItemEnchantmentTags as EnchantmentTags; use pocketmine\item\Item; use pocketmine\item\ToolTier; use pocketmine\math\Facing; @@ -966,7 +967,7 @@ public function getBreakTime(Item $item) : float{ $pumpkinBreakInfo = new Info(BreakInfo::axe(1.0)); self::register("pumpkin", new Pumpkin(new BID(Ids::PUMPKIN), "Pumpkin", $pumpkinBreakInfo)); - self::register("carved_pumpkin", new CarvedPumpkin(new BID(Ids::CARVED_PUMPKIN), "Carved Pumpkin", $pumpkinBreakInfo)); + self::register("carved_pumpkin", new CarvedPumpkin(new BID(Ids::CARVED_PUMPKIN), "Carved Pumpkin", new Info(BreakInfo::axe(1.0), enchantmentTags: [EnchantmentTags::MASK]))); self::register("lit_pumpkin", new LitPumpkin(new BID(Ids::LIT_PUMPKIN), "Jack o'Lantern", $pumpkinBreakInfo)); self::register("pumpkin_stem", new PumpkinStem(new BID(Ids::PUMPKIN_STEM), "Pumpkin Stem", new Info(BreakInfo::instant()))); @@ -1002,7 +1003,7 @@ public function getBreakTime(Item $item) : float{ self::register("sea_lantern", new SeaLantern(new BID(Ids::SEA_LANTERN), "Sea Lantern", new Info(new BreakInfo(0.3)))); self::register("sea_pickle", new SeaPickle(new BID(Ids::SEA_PICKLE), "Sea Pickle", new Info(BreakInfo::instant()))); - self::register("mob_head", new MobHead(new BID(Ids::MOB_HEAD, TileMobHead::class), "Mob Head", new Info(new BreakInfo(1.0)))); + self::register("mob_head", new MobHead(new BID(Ids::MOB_HEAD, TileMobHead::class), "Mob Head", new Info(new BreakInfo(1.0), enchantmentTags: [EnchantmentTags::MASK]))); self::register("slime", new Slime(new BID(Ids::SLIME), "Slime Block", new Info(BreakInfo::instant()))); self::register("snow", new Snow(new BID(Ids::SNOW), "Snow Block", new Info(BreakInfo::shovel(0.2, ToolTier::WOOD())))); self::register("snow_layer", new SnowLayer(new BID(Ids::SNOW_LAYER), "Snow Layer", new Info(BreakInfo::shovel(0.1, ToolTier::WOOD())))); diff --git a/src/block/inventory/EnchantInventory.php b/src/block/inventory/EnchantInventory.php index 2c682d134bf..5d7e4525951 100644 --- a/src/block/inventory/EnchantInventory.php +++ b/src/block/inventory/EnchantInventory.php @@ -23,9 +23,15 @@ namespace pocketmine\block\inventory; +use pocketmine\event\player\PlayerEnchantOptionsRequestEvent; use pocketmine\inventory\SimpleInventory; use pocketmine\inventory\TemporaryInventory; +use pocketmine\item\enchantment\EnchantmentHelper as Helper; +use pocketmine\item\enchantment\EnchantOption; +use pocketmine\item\Item; use pocketmine\world\Position; +use function array_values; +use function count; class EnchantInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{ use BlockInventoryTrait; @@ -33,8 +39,47 @@ class EnchantInventory extends SimpleInventory implements BlockInventory, Tempor public const SLOT_INPUT = 0; public const SLOT_LAPIS = 1; + /** @var EnchantOption[] $options */ + private array $options = []; + public function __construct(Position $holder){ $this->holder = $holder; parent::__construct(2); } + + protected function onSlotChange(int $index, Item $before) : void{ + if($index === self::SLOT_INPUT){ + foreach($this->viewers as $viewer){ + $this->options = []; + $item = $this->getInput(); + $options = Helper::getEnchantOptions($this->holder, $item, $viewer->getEnchantmentSeed()); + + $event = new PlayerEnchantOptionsRequestEvent($viewer, $this, $options); + $event->call(); + if(!$event->isCancelled() && count($event->getOptions()) > 0){ + $this->options = array_values($event->getOptions()); + $viewer->getNetworkSession()->getInvManager()?->syncEnchantingTableOptions($this->options); + } + } + } + + parent::onSlotChange($index, $before); + } + + public function getInput() : Item{ + return $this->getItem(self::SLOT_INPUT); + } + + public function getLapis() : Item{ + return $this->getItem(self::SLOT_LAPIS); + } + + public function getOutput(int $optionId) : ?Item{ + $option = $this->getOption($optionId); + return $option === null ? null : Helper::enchantItem($this->getInput(), $option->getEnchantments()); + } + + public function getOption(int $optionId) : ?EnchantOption{ + return $this->options[$optionId] ?? null; + } } diff --git a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php index 6f557ddb138..ccf430b9ccd 100644 --- a/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php +++ b/src/data/bedrock/item/ItemSerializerDeserializerRegistrar.php @@ -223,6 +223,7 @@ private function register1to1ItemMappings() : void{ $this->map1to1Item(Ids::ECHO_SHARD, Items::ECHO_SHARD()); $this->map1to1Item(Ids::EGG, Items::EGG()); $this->map1to1Item(Ids::EMERALD, Items::EMERALD()); + $this->map1to1Item(Ids::ENCHANTED_BOOK, Items::ENCHANTED_BOOK()); $this->map1to1Item(Ids::ENCHANTED_GOLDEN_APPLE, Items::ENCHANTED_GOLDEN_APPLE()); $this->map1to1Item(Ids::ENDER_PEARL, Items::ENDER_PEARL()); $this->map1to1Item(Ids::EXPERIENCE_BOTTLE, Items::EXPERIENCE_BOTTLE()); diff --git a/src/entity/Human.php b/src/entity/Human.php index ae1aa246491..30142b91dc4 100644 --- a/src/entity/Human.php +++ b/src/entity/Human.php @@ -76,7 +76,7 @@ use function array_merge; use function array_values; use function min; -use function random_int; +use function mt_rand; class Human extends Living implements ProjectileSource, InventoryHolder{ @@ -211,6 +211,18 @@ public function getXpManager() : ExperienceManager{ return $this->xpManager; } + public function getEnchantmentSeed() : int{ + return $this->xpSeed; + } + + public function setEnchantmentSeed(int $seed) : void{ + $this->xpSeed = $seed; + } + + public function generateEnchantmentSeed() : int{ + return mt_rand(Limits::INT32_MIN, Limits::INT32_MAX); + } + public function getXpDropAmount() : int{ //this causes some XP to be lost on death when above level 1 (by design), dropping at most enough points for //about 7.5 levels of XP. @@ -334,7 +346,7 @@ function(Inventory $unused, array $oldItems) use ($syncHeldItem) : void{ if(($xpSeedTag = $nbt->getTag(self::TAG_XP_SEED)) instanceof IntTag){ $this->xpSeed = $xpSeedTag->getValue(); }else{ - $this->xpSeed = random_int(Limits::INT32_MIN, Limits::INT32_MAX); + $this->xpSeed = $this->generateEnchantmentSeed(); } } diff --git a/src/event/player/PlayerEnchantOptionsRequestEvent.php b/src/event/player/PlayerEnchantOptionsRequestEvent.php new file mode 100644 index 00000000000..d0448bfd8e1 --- /dev/null +++ b/src/event/player/PlayerEnchantOptionsRequestEvent.php @@ -0,0 +1,75 @@ +player = $player; + } + + public function getEnchantInventory() : EnchantInventory{ + return $this->enchantInventory; + } + + /** + * @return EnchantOption[] + */ + public function getOptions() : array{ + return $this->options; + } + + /** + * @param EnchantOption[] $options + */ + public function setOptions(array $options) : void{ + Utils::validateArrayValueType($options, function(EnchantOption $_) : void{ }); + if(($optionCount = count($options)) > 3){ + throw new \LogicException("The maximum number of options for an enchanting table is 3, but $optionCount have been passed"); + } + + $this->options = $options; + } +} diff --git a/src/event/player/PlayerItemEnchantEvent.php b/src/event/player/PlayerItemEnchantEvent.php new file mode 100644 index 00000000000..9974ab40a27 --- /dev/null +++ b/src/event/player/PlayerItemEnchantEvent.php @@ -0,0 +1,85 @@ +player = $player; + } + + /** + * Returns the inventory transaction involved in this enchant event. + */ + public function getTransaction() : EnchantTransaction{ + return $this->transaction; + } + + /** + * Returns the enchantment option used. + */ + public function getOption() : EnchantOption{ + return $this->option; + } + + /** + * Returns the item to be enchanted. + */ + public function getInputItem() : Item{ + return clone $this->inputItem; + } + + /** + * Returns the enchanted item. + */ + public function getOutputItem() : Item{ + return clone $this->outputItem; + } + + /** + * Returns the number of XP levels and lapis that will be subtracted after enchanting + * if the player is not in creative mode. + */ + public function getCost() : int{ + return $this->cost; + } +} diff --git a/src/inventory/transaction/EnchantTransaction.php b/src/inventory/transaction/EnchantTransaction.php new file mode 100644 index 00000000000..f3760e479a1 --- /dev/null +++ b/src/inventory/transaction/EnchantTransaction.php @@ -0,0 +1,132 @@ +inputItem === null || $this->outputItem === null){ + throw new AssumptionFailedError("Expected that inputItem and outputItem are not null before validating output"); + } + + $enchantedInput = EnchantmentHelper::enchantItem($this->inputItem, $this->option->getEnchantments()); + if(!$this->outputItem->equalsExact($enchantedInput)){ + throw new TransactionValidationException("Invalid output item"); + } + } + + private function validateFiniteResources(int $lapisSpent) : void{ + if($lapisSpent !== $this->cost){ + throw new TransactionValidationException("Expected the amount of lapis lazuli spent to be $this->cost, but received $lapisSpent"); + } + + $xpLevel = $this->source->getXpManager()->getXpLevel(); + $requiredXpLevel = $this->option->getRequiredXpLevel(); + + if($xpLevel < $requiredXpLevel){ + throw new TransactionValidationException("Player's XP level $xpLevel is less than the required XP level $requiredXpLevel"); + } + if($xpLevel < $this->cost){ + throw new TransactionValidationException("Player's XP level $xpLevel is less than the XP level cost $this->cost"); + } + } + + public function validate() : void{ + if(count($this->actions) < 1){ + throw new TransactionValidationException("Transaction must have at least one action to be executable"); + } + + /** @var Item[] $inputs */ + $inputs = []; + /** @var Item[] $outputs */ + $outputs = []; + $this->matchItems($outputs, $inputs); + + $lapisSpent = 0; + foreach($inputs as $input){ + if($input->getTypeId() === ItemTypeIds::LAPIS_LAZULI){ + $lapisSpent = $input->getCount(); + }else{ + if($this->inputItem !== null){ + throw new TransactionValidationException("Received more than 1 items to enchant"); + } + $this->inputItem = $input; + } + } + + if($this->inputItem === null){ + throw new TransactionValidationException("No item to enchant received"); + } + + if(($outputCount = count($outputs)) !== 1){ + throw new TransactionValidationException("Expected 1 output item, but received $outputCount"); + } + $this->outputItem = $outputs[0]; + + $this->validateOutput(); + + if($this->source->hasFiniteResources()){ + $this->validateFiniteResources($lapisSpent); + } + } + + public function execute() : void{ + parent::execute(); + + if($this->source->hasFiniteResources()){ + $this->source->getXpManager()->subtractXpLevels($this->cost); + } + $this->source->setEnchantmentSeed($this->source->generateEnchantmentSeed()); + } + + protected function callExecuteEvent() : bool{ + if($this->inputItem === null || $this->outputItem === null){ + throw new AssumptionFailedError("Expected that inputItem and outputItem are not null before executing the event"); + } + + $event = new PlayerItemEnchantEvent($this->source, $this, $this->option, $this->inputItem, $this->outputItem, $this->cost); + $event->call(); + return !$event->isCancelled(); + } +} diff --git a/src/item/Armor.php b/src/item/Armor.php index 6fb538cd60e..e52732caf6d 100644 --- a/src/item/Armor.php +++ b/src/item/Armor.php @@ -44,8 +44,11 @@ class Armor extends Durable{ protected ?Color $customColor = null; - public function __construct(ItemIdentifier $identifier, string $name, ArmorTypeInfo $info){ - parent::__construct($identifier, $name); + /** + * @param string[] $enchantmentTags + */ + public function __construct(ItemIdentifier $identifier, string $name, ArmorTypeInfo $info, array $enchantmentTags = []){ + parent::__construct($identifier, $name, $enchantmentTags); $this->armorInfo = $info; } @@ -72,6 +75,14 @@ public function isFireProof() : bool{ return $this->armorInfo->isFireProof(); } + public function getMaterial() : ArmorMaterial{ + return $this->armorInfo->getMaterial(); + } + + public function getEnchantability() : int{ + return $this->armorInfo->getMaterial()->getEnchantability(); + } + /** * Returns the dyed colour of this armour piece. This generally only applies to leather armour. */ diff --git a/src/item/ArmorMaterial.php b/src/item/ArmorMaterial.php new file mode 100644 index 00000000000..c7915baccef --- /dev/null +++ b/src/item/ArmorMaterial.php @@ -0,0 +1,42 @@ +enchantability; + } +} diff --git a/src/item/ArmorTypeInfo.php b/src/item/ArmorTypeInfo.php index 580b73df338..dbb4ed06dfe 100644 --- a/src/item/ArmorTypeInfo.php +++ b/src/item/ArmorTypeInfo.php @@ -24,13 +24,18 @@ namespace pocketmine\item; class ArmorTypeInfo{ + private ArmorMaterial $material; + public function __construct( private int $defensePoints, private int $maxDurability, private int $armorSlot, private int $toughness = 0, - private bool $fireProof = false - ){} + private bool $fireProof = false, + ?ArmorMaterial $material = null + ){ + $this->material = $material ?? VanillaArmorMaterials::LEATHER(); + } public function getDefensePoints() : int{ return $this->defensePoints; @@ -51,4 +56,8 @@ public function getToughness() : int{ public function isFireProof() : bool{ return $this->fireProof; } + + public function getMaterial() : ArmorMaterial{ + return $this->material; + } } diff --git a/src/item/EnchantedBook.php b/src/item/EnchantedBook.php new file mode 100644 index 00000000000..5660de6f60a --- /dev/null +++ b/src/item/EnchantedBook.php @@ -0,0 +1,30 @@ +nbt = new CompoundTag(); } @@ -455,6 +458,29 @@ public function getVanillaName() : string{ return $this->name; } + /** + * Returns tags that represent the type of item being enchanted and are used to determine + * what enchantments can be applied to this item during in-game enchanting (enchanting table, anvil, fishing, etc.). + * @see ItemEnchantmentTags + * @see ItemEnchantmentTagRegistry + * @see AvailableEnchantmentRegistry + * + * @return string[] + */ + public function getEnchantmentTags() : array{ + return $this->enchantmentTags; + } + + /** + * Returns the value that defines how enchantable the item is. + * + * The higher an item's enchantability is, the more likely it will be to gain high-level enchantments + * or multiple enchantments upon being enchanted in an enchanting table. + */ + public function getEnchantability() : int{ + return 1; + } + final public function canBePlaced() : bool{ return $this->getBlock()->canBePlaced(); } diff --git a/src/item/ItemBlock.php b/src/item/ItemBlock.php index fbbe2efeb45..11bcb58d372 100644 --- a/src/item/ItemBlock.php +++ b/src/item/ItemBlock.php @@ -36,7 +36,7 @@ final class ItemBlock extends Item{ public function __construct( private Block $block ){ - parent::__construct(ItemIdentifier::fromBlock($block), $block->getName()); + parent::__construct(ItemIdentifier::fromBlock($block), $block->getName(), $block->getEnchantmentTags()); } protected function describeState(RuntimeDataDescriber $w) : void{ diff --git a/src/item/ItemTypeIds.php b/src/item/ItemTypeIds.php index f37426c56a3..82333976661 100644 --- a/src/item/ItemTypeIds.php +++ b/src/item/ItemTypeIds.php @@ -303,8 +303,9 @@ private function __construct(){ public const MANGROVE_BOAT = 20264; public const GLOW_BERRIES = 20265; public const CHERRY_SIGN = 20266; + public const ENCHANTED_BOOK = 20267; - public const FIRST_UNUSED_ITEM_ID = 20267; + public const FIRST_UNUSED_ITEM_ID = 20268; private static int $nextDynamicId = self::FIRST_UNUSED_ITEM_ID; diff --git a/src/item/StringToItemParser.php b/src/item/StringToItemParser.php index c8be97cadf2..d482e4bef9a 100644 --- a/src/item/StringToItemParser.php +++ b/src/item/StringToItemParser.php @@ -1277,6 +1277,7 @@ private static function registerItems(self $result) : void{ $result->register("egg", fn() => Items::EGG()); $result->register("elixir", fn() => Items::MEDICINE()->setType(MedicineType::ELIXIR())); $result->register("emerald", fn() => Items::EMERALD()); + $result->register("enchanted_book", fn() => Items::ENCHANTED_BOOK()); $result->register("enchanted_golden_apple", fn() => Items::ENCHANTED_GOLDEN_APPLE()); $result->register("enchanting_bottle", fn() => Items::EXPERIENCE_BOTTLE()); $result->register("ender_pearl", fn() => Items::ENDER_PEARL()); diff --git a/src/item/TieredTool.php b/src/item/TieredTool.php index e7d4f220153..dc00aebcf85 100644 --- a/src/item/TieredTool.php +++ b/src/item/TieredTool.php @@ -26,8 +26,11 @@ abstract class TieredTool extends Tool{ protected ToolTier $tier; - public function __construct(ItemIdentifier $identifier, string $name, ToolTier $tier){ - parent::__construct($identifier, $name); + /** + * @param string[] $enchantmentTags + */ + public function __construct(ItemIdentifier $identifier, string $name, ToolTier $tier, array $enchantmentTags = []){ + parent::__construct($identifier, $name, $enchantmentTags); $this->tier = $tier; } @@ -43,6 +46,10 @@ protected function getBaseMiningEfficiency() : float{ return $this->tier->getBaseEfficiency(); } + public function getEnchantability() : int{ + return $this->tier->getEnchantability(); + } + public function getFuelTime() : int{ if($this->tier->equals(ToolTier::WOOD())){ return 200; diff --git a/src/item/ToolTier.php b/src/item/ToolTier.php index 231e233c334..4ca910c0bb0 100644 --- a/src/item/ToolTier.php +++ b/src/item/ToolTier.php @@ -45,12 +45,12 @@ final class ToolTier{ protected static function setup() : void{ self::registerAll( - new self("wood", 1, 60, 5, 2), - new self("gold", 2, 33, 5, 12), - new self("stone", 3, 132, 6, 4), - new self("iron", 4, 251, 7, 6), - new self("diamond", 5, 1562, 8, 8), - new self("netherite", 6, 2032, 9, 9) + new self("wood", 1, 60, 5, 2, 15), + new self("gold", 2, 33, 5, 12, 22), + new self("stone", 3, 132, 6, 4, 5), + new self("iron", 4, 251, 7, 6, 14), + new self("diamond", 5, 1562, 8, 8, 10), + new self("netherite", 6, 2032, 9, 9, 15) ); } @@ -59,7 +59,8 @@ private function __construct( private int $harvestLevel, private int $maxDurability, private int $baseAttackPoints, - private int $baseEfficiency + private int $baseEfficiency, + private int $enchantability ){ $this->Enum___construct($name); } @@ -79,4 +80,14 @@ public function getBaseAttackPoints() : int{ public function getBaseEfficiency() : int{ return $this->baseEfficiency; } + + /** + * Returns the value that defines how enchantable the item is. + * + * The higher an item's enchantability is, the more likely it will be to gain high-level enchantments + * or multiple enchantments upon being enchanted in an enchanting table. + */ + public function getEnchantability() : int{ + return $this->enchantability; + } } diff --git a/src/item/VanillaArmorMaterials.php b/src/item/VanillaArmorMaterials.php new file mode 100644 index 00000000000..ab2909bce32 --- /dev/null +++ b/src/item/VanillaArmorMaterials.php @@ -0,0 +1,73 @@ + + */ + public static function getAll() : array{ + // phpstan doesn't support generic traits yet :( + /** @var ArmorMaterial[] $result */ + $result = self::_registryGetAll(); + return $result; + } + + protected static function setup() : void{ + self::register("leather", new ArmorMaterial(15)); + self::register("chainmail", new ArmorMaterial(12)); + self::register("iron", new ArmorMaterial(9)); + self::register("turtle", new ArmorMaterial(9)); + self::register("gold", new ArmorMaterial(25)); + self::register("diamond", new ArmorMaterial(10)); + self::register("netherite", new ArmorMaterial(15)); + } +} diff --git a/src/item/VanillaItems.php b/src/item/VanillaItems.php index b7c32ebc111..07ba398d58b 100644 --- a/src/item/VanillaItems.php +++ b/src/item/VanillaItems.php @@ -24,7 +24,6 @@ namespace pocketmine\item; use pocketmine\block\utils\RecordType; -use pocketmine\block\VanillaBlocks; use pocketmine\block\VanillaBlocks as Blocks; use pocketmine\entity\Entity; use pocketmine\entity\Location; @@ -32,8 +31,10 @@ use pocketmine\entity\Villager; use pocketmine\entity\Zombie; use pocketmine\inventory\ArmorInventory; +use pocketmine\item\enchantment\ItemEnchantmentTags as EnchantmentTags; use pocketmine\item\ItemIdentifier as IID; use pocketmine\item\ItemTypeIds as Ids; +use pocketmine\item\VanillaArmorMaterials as ArmorMaterials; use pocketmine\math\Vector3; use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\CloningRegistryTrait; @@ -151,6 +152,7 @@ * @method static Item ECHO_SHARD() * @method static Egg EGG() * @method static Item EMERALD() + * @method static EnchantedBook ENCHANTED_BOOK() * @method static GoldenAppleEnchanted ENCHANTED_GOLDEN_APPLE() * @method static EnderPearl ENDER_PEARL() * @method static ExperienceBottle EXPERIENCE_BOTTLE() @@ -337,7 +339,7 @@ protected static function setup() : void{ self::registerSpawnEggs(); self::registerTierToolItems(); - self::register("air", VanillaBlocks::AIR()->asItem()->setCount(0)); + self::register("air", Blocks::AIR()->asItem()->setCount(0)); self::register("acacia_sign", new ItemBlockWallOrFloor(new IID(Ids::ACACIA_SIGN), Blocks::ACACIA_SIGN(), Blocks::ACACIA_WALL_SIGN())); self::register("amethyst_shard", new Item(new IID(Ids::AMETHYST_SHARD), "Amethyst Shard")); @@ -355,8 +357,8 @@ protected static function setup() : void{ self::register("bleach", new Item(new IID(Ids::BLEACH), "Bleach")); self::register("bone", new Item(new IID(Ids::BONE), "Bone")); self::register("bone_meal", new Fertilizer(new IID(Ids::BONE_MEAL), "Bone Meal")); - self::register("book", new Book(new IID(Ids::BOOK), "Book")); - self::register("bow", new Bow(new IID(Ids::BOW), "Bow")); + self::register("book", new Book(new IID(Ids::BOOK), "Book", [EnchantmentTags::ALL])); + self::register("bow", new Bow(new IID(Ids::BOW), "Bow", [EnchantmentTags::BOW])); self::register("bowl", new Bowl(new IID(Ids::BOWL), "Bowl")); self::register("bread", new Bread(new IID(Ids::BREAD), "Bread")); self::register("brick", new Item(new IID(Ids::BRICK), "Brick")); @@ -408,7 +410,7 @@ protected static function setup() : void{ self::register("clownfish", new Clownfish(new IID(Ids::CLOWNFISH), "Clownfish")); self::register("coal", new Coal(new IID(Ids::COAL), "Coal")); self::register("cocoa_beans", new CocoaBeans(new IID(Ids::COCOA_BEANS), "Cocoa Beans")); - self::register("compass", new Compass(new IID(Ids::COMPASS), "Compass")); + self::register("compass", new Compass(new IID(Ids::COMPASS), "Compass", [EnchantmentTags::COMPASS])); self::register("cooked_chicken", new CookedChicken(new IID(Ids::COOKED_CHICKEN), "Cooked Chicken")); self::register("cooked_fish", new CookedFish(new IID(Ids::COOKED_FISH), "Cooked Fish")); self::register("cooked_mutton", new CookedMutton(new IID(Ids::COOKED_MUTTON), "Cooked Mutton")); @@ -429,15 +431,16 @@ protected static function setup() : void{ self::register("echo_shard", new Item(new IID(Ids::ECHO_SHARD), "Echo Shard")); self::register("egg", new Egg(new IID(Ids::EGG), "Egg")); self::register("emerald", new Item(new IID(Ids::EMERALD), "Emerald")); + self::register("enchanted_book", new EnchantedBook(new IID(Ids::ENCHANTED_BOOK), "Enchanted Book")); self::register("enchanted_golden_apple", new GoldenAppleEnchanted(new IID(Ids::ENCHANTED_GOLDEN_APPLE), "Enchanted Golden Apple")); self::register("ender_pearl", new EnderPearl(new IID(Ids::ENDER_PEARL), "Ender Pearl")); self::register("experience_bottle", new ExperienceBottle(new IID(Ids::EXPERIENCE_BOTTLE), "Bottle o' Enchanting")); self::register("feather", new Item(new IID(Ids::FEATHER), "Feather")); self::register("fermented_spider_eye", new Item(new IID(Ids::FERMENTED_SPIDER_EYE), "Fermented Spider Eye")); self::register("fire_charge", new FireCharge(new IID(Ids::FIRE_CHARGE), "Fire Charge")); - self::register("fishing_rod", new FishingRod(new IID(Ids::FISHING_ROD), "Fishing Rod")); + self::register("fishing_rod", new FishingRod(new IID(Ids::FISHING_ROD), "Fishing Rod", [EnchantmentTags::FISHING_ROD])); self::register("flint", new Item(new IID(Ids::FLINT), "Flint")); - self::register("flint_and_steel", new FlintSteel(new IID(Ids::FLINT_AND_STEEL), "Flint and Steel")); + self::register("flint_and_steel", new FlintSteel(new IID(Ids::FLINT_AND_STEEL), "Flint and Steel", [EnchantmentTags::FLINT_AND_STEEL])); 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")); @@ -521,7 +524,7 @@ public function isFireProof() : bool{ return true; } self::register("redstone_dust", new Redstone(new IID(Ids::REDSTONE_DUST), "Redstone")); self::register("rotten_flesh", new RottenFlesh(new IID(Ids::ROTTEN_FLESH), "Rotten Flesh")); self::register("scute", new Item(new IID(Ids::SCUTE), "Scute")); - self::register("shears", new Shears(new IID(Ids::SHEARS), "Shears")); + self::register("shears", new Shears(new IID(Ids::SHEARS), "Shears", [EnchantmentTags::SHEARS])); self::register("shulker_shell", new Item(new IID(Ids::SHULKER_SHELL), "Shulker Shell")); self::register("slimeball", new Item(new IID(Ids::SLIMEBALL), "Slimeball")); self::register("snowball", new Snowball(new IID(Ids::SNOWBALL), "Snowball")); @@ -577,67 +580,67 @@ public function createEntity(World $world, Vector3 $pos, float $yaw, float $pitc } private static function registerTierToolItems() : void{ - self::register("diamond_axe", new Axe(new IID(Ids::DIAMOND_AXE), "Diamond Axe", ToolTier::DIAMOND())); - self::register("golden_axe", new Axe(new IID(Ids::GOLDEN_AXE), "Golden Axe", ToolTier::GOLD())); - self::register("iron_axe", new Axe(new IID(Ids::IRON_AXE), "Iron Axe", ToolTier::IRON())); - self::register("netherite_axe", new Axe(new IID(Ids::NETHERITE_AXE), "Netherite Axe", ToolTier::NETHERITE())); - self::register("stone_axe", new Axe(new IID(Ids::STONE_AXE), "Stone Axe", ToolTier::STONE())); - self::register("wooden_axe", new Axe(new IID(Ids::WOODEN_AXE), "Wooden Axe", ToolTier::WOOD())); - self::register("diamond_hoe", new Hoe(new IID(Ids::DIAMOND_HOE), "Diamond Hoe", ToolTier::DIAMOND())); - self::register("golden_hoe", new Hoe(new IID(Ids::GOLDEN_HOE), "Golden Hoe", ToolTier::GOLD())); - self::register("iron_hoe", new Hoe(new IID(Ids::IRON_HOE), "Iron Hoe", ToolTier::IRON())); - self::register("netherite_hoe", new Hoe(new IID(Ids::NETHERITE_HOE), "Netherite Hoe", ToolTier::NETHERITE())); - self::register("stone_hoe", new Hoe(new IID(Ids::STONE_HOE), "Stone Hoe", ToolTier::STONE())); - self::register("wooden_hoe", new Hoe(new IID(Ids::WOODEN_HOE), "Wooden Hoe", ToolTier::WOOD())); - self::register("diamond_pickaxe", new Pickaxe(new IID(Ids::DIAMOND_PICKAXE), "Diamond Pickaxe", ToolTier::DIAMOND())); - self::register("golden_pickaxe", new Pickaxe(new IID(Ids::GOLDEN_PICKAXE), "Golden Pickaxe", ToolTier::GOLD())); - self::register("iron_pickaxe", new Pickaxe(new IID(Ids::IRON_PICKAXE), "Iron Pickaxe", ToolTier::IRON())); - self::register("netherite_pickaxe", new Pickaxe(new IID(Ids::NETHERITE_PICKAXE), "Netherite Pickaxe", ToolTier::NETHERITE())); - self::register("stone_pickaxe", new Pickaxe(new IID(Ids::STONE_PICKAXE), "Stone Pickaxe", ToolTier::STONE())); - self::register("wooden_pickaxe", new Pickaxe(new IID(Ids::WOODEN_PICKAXE), "Wooden Pickaxe", ToolTier::WOOD())); - self::register("diamond_shovel", new Shovel(new IID(Ids::DIAMOND_SHOVEL), "Diamond Shovel", ToolTier::DIAMOND())); - self::register("golden_shovel", new Shovel(new IID(Ids::GOLDEN_SHOVEL), "Golden Shovel", ToolTier::GOLD())); - self::register("iron_shovel", new Shovel(new IID(Ids::IRON_SHOVEL), "Iron Shovel", ToolTier::IRON())); - self::register("netherite_shovel", new Shovel(new IID(Ids::NETHERITE_SHOVEL), "Netherite Shovel", ToolTier::NETHERITE())); - self::register("stone_shovel", new Shovel(new IID(Ids::STONE_SHOVEL), "Stone Shovel", ToolTier::STONE())); - self::register("wooden_shovel", new Shovel(new IID(Ids::WOODEN_SHOVEL), "Wooden Shovel", ToolTier::WOOD())); - self::register("diamond_sword", new Sword(new IID(Ids::DIAMOND_SWORD), "Diamond Sword", ToolTier::DIAMOND())); - self::register("golden_sword", new Sword(new IID(Ids::GOLDEN_SWORD), "Golden Sword", ToolTier::GOLD())); - self::register("iron_sword", new Sword(new IID(Ids::IRON_SWORD), "Iron Sword", ToolTier::IRON())); - self::register("netherite_sword", new Sword(new IID(Ids::NETHERITE_SWORD), "Netherite Sword", ToolTier::NETHERITE())); - self::register("stone_sword", new Sword(new IID(Ids::STONE_SWORD), "Stone Sword", ToolTier::STONE())); - self::register("wooden_sword", new Sword(new IID(Ids::WOODEN_SWORD), "Wooden Sword", ToolTier::WOOD())); + self::register("diamond_axe", new Axe(new IID(Ids::DIAMOND_AXE), "Diamond Axe", ToolTier::DIAMOND(), [EnchantmentTags::AXE])); + self::register("golden_axe", new Axe(new IID(Ids::GOLDEN_AXE), "Golden Axe", ToolTier::GOLD(), [EnchantmentTags::AXE])); + self::register("iron_axe", new Axe(new IID(Ids::IRON_AXE), "Iron Axe", ToolTier::IRON(), [EnchantmentTags::AXE])); + self::register("netherite_axe", new Axe(new IID(Ids::NETHERITE_AXE), "Netherite Axe", ToolTier::NETHERITE(), [EnchantmentTags::AXE])); + self::register("stone_axe", new Axe(new IID(Ids::STONE_AXE), "Stone Axe", ToolTier::STONE(), [EnchantmentTags::AXE])); + self::register("wooden_axe", new Axe(new IID(Ids::WOODEN_AXE), "Wooden Axe", ToolTier::WOOD(), [EnchantmentTags::AXE])); + self::register("diamond_hoe", new Hoe(new IID(Ids::DIAMOND_HOE), "Diamond Hoe", ToolTier::DIAMOND(), [EnchantmentTags::HOE])); + self::register("golden_hoe", new Hoe(new IID(Ids::GOLDEN_HOE), "Golden Hoe", ToolTier::GOLD(), [EnchantmentTags::HOE])); + self::register("iron_hoe", new Hoe(new IID(Ids::IRON_HOE), "Iron Hoe", ToolTier::IRON(), [EnchantmentTags::HOE])); + self::register("netherite_hoe", new Hoe(new IID(Ids::NETHERITE_HOE), "Netherite Hoe", ToolTier::NETHERITE(), [EnchantmentTags::HOE])); + self::register("stone_hoe", new Hoe(new IID(Ids::STONE_HOE), "Stone Hoe", ToolTier::STONE(), [EnchantmentTags::HOE])); + self::register("wooden_hoe", new Hoe(new IID(Ids::WOODEN_HOE), "Wooden Hoe", ToolTier::WOOD(), [EnchantmentTags::HOE])); + self::register("diamond_pickaxe", new Pickaxe(new IID(Ids::DIAMOND_PICKAXE), "Diamond Pickaxe", ToolTier::DIAMOND(), [EnchantmentTags::PICKAXE])); + self::register("golden_pickaxe", new Pickaxe(new IID(Ids::GOLDEN_PICKAXE), "Golden Pickaxe", ToolTier::GOLD(), [EnchantmentTags::PICKAXE])); + self::register("iron_pickaxe", new Pickaxe(new IID(Ids::IRON_PICKAXE), "Iron Pickaxe", ToolTier::IRON(), [EnchantmentTags::PICKAXE])); + self::register("netherite_pickaxe", new Pickaxe(new IID(Ids::NETHERITE_PICKAXE), "Netherite Pickaxe", ToolTier::NETHERITE(), [EnchantmentTags::PICKAXE])); + self::register("stone_pickaxe", new Pickaxe(new IID(Ids::STONE_PICKAXE), "Stone Pickaxe", ToolTier::STONE(), [EnchantmentTags::PICKAXE])); + self::register("wooden_pickaxe", new Pickaxe(new IID(Ids::WOODEN_PICKAXE), "Wooden Pickaxe", ToolTier::WOOD(), [EnchantmentTags::PICKAXE])); + self::register("diamond_shovel", new Shovel(new IID(Ids::DIAMOND_SHOVEL), "Diamond Shovel", ToolTier::DIAMOND(), [EnchantmentTags::SHOVEL])); + self::register("golden_shovel", new Shovel(new IID(Ids::GOLDEN_SHOVEL), "Golden Shovel", ToolTier::GOLD(), [EnchantmentTags::SHOVEL])); + self::register("iron_shovel", new Shovel(new IID(Ids::IRON_SHOVEL), "Iron Shovel", ToolTier::IRON(), [EnchantmentTags::SHOVEL])); + self::register("netherite_shovel", new Shovel(new IID(Ids::NETHERITE_SHOVEL), "Netherite Shovel", ToolTier::NETHERITE(), [EnchantmentTags::SHOVEL])); + self::register("stone_shovel", new Shovel(new IID(Ids::STONE_SHOVEL), "Stone Shovel", ToolTier::STONE(), [EnchantmentTags::SHOVEL])); + self::register("wooden_shovel", new Shovel(new IID(Ids::WOODEN_SHOVEL), "Wooden Shovel", ToolTier::WOOD(), [EnchantmentTags::SHOVEL])); + self::register("diamond_sword", new Sword(new IID(Ids::DIAMOND_SWORD), "Diamond Sword", ToolTier::DIAMOND(), [EnchantmentTags::SWORD])); + self::register("golden_sword", new Sword(new IID(Ids::GOLDEN_SWORD), "Golden Sword", ToolTier::GOLD(), [EnchantmentTags::SWORD])); + self::register("iron_sword", new Sword(new IID(Ids::IRON_SWORD), "Iron Sword", ToolTier::IRON(), [EnchantmentTags::SWORD])); + self::register("netherite_sword", new Sword(new IID(Ids::NETHERITE_SWORD), "Netherite Sword", ToolTier::NETHERITE(), [EnchantmentTags::SWORD])); + self::register("stone_sword", new Sword(new IID(Ids::STONE_SWORD), "Stone Sword", ToolTier::STONE(), [EnchantmentTags::SWORD])); + self::register("wooden_sword", new Sword(new IID(Ids::WOODEN_SWORD), "Wooden Sword", ToolTier::WOOD(), [EnchantmentTags::SWORD])); } private static function registerArmorItems() : void{ - self::register("chainmail_boots", new Armor(new IID(Ids::CHAINMAIL_BOOTS), "Chainmail Boots", new ArmorTypeInfo(1, 196, ArmorInventory::SLOT_FEET))); - self::register("diamond_boots", new Armor(new IID(Ids::DIAMOND_BOOTS), "Diamond Boots", new ArmorTypeInfo(3, 430, ArmorInventory::SLOT_FEET, 2))); - self::register("golden_boots", new Armor(new IID(Ids::GOLDEN_BOOTS), "Golden Boots", new ArmorTypeInfo(1, 92, ArmorInventory::SLOT_FEET))); - self::register("iron_boots", new Armor(new IID(Ids::IRON_BOOTS), "Iron Boots", new ArmorTypeInfo(2, 196, ArmorInventory::SLOT_FEET))); - self::register("leather_boots", new Armor(new IID(Ids::LEATHER_BOOTS), "Leather Boots", new ArmorTypeInfo(1, 66, ArmorInventory::SLOT_FEET))); - self::register("netherite_boots", new Armor(new IID(Ids::NETHERITE_BOOTS), "Netherite Boots", new ArmorTypeInfo(3, 482, ArmorInventory::SLOT_FEET, 3, true))); + self::register("chainmail_boots", new Armor(new IID(Ids::CHAINMAIL_BOOTS), "Chainmail Boots", new ArmorTypeInfo(1, 196, ArmorInventory::SLOT_FEET, material: ArmorMaterials::CHAINMAIL()), [EnchantmentTags::BOOTS])); + self::register("diamond_boots", new Armor(new IID(Ids::DIAMOND_BOOTS), "Diamond Boots", new ArmorTypeInfo(3, 430, ArmorInventory::SLOT_FEET, 2, material: ArmorMaterials::DIAMOND()), [EnchantmentTags::BOOTS])); + self::register("golden_boots", new Armor(new IID(Ids::GOLDEN_BOOTS), "Golden Boots", new ArmorTypeInfo(1, 92, ArmorInventory::SLOT_FEET, material: ArmorMaterials::GOLD()), [EnchantmentTags::BOOTS])); + self::register("iron_boots", new Armor(new IID(Ids::IRON_BOOTS), "Iron Boots", new ArmorTypeInfo(2, 196, ArmorInventory::SLOT_FEET, material: ArmorMaterials::IRON()), [EnchantmentTags::BOOTS])); + self::register("leather_boots", new Armor(new IID(Ids::LEATHER_BOOTS), "Leather Boots", new ArmorTypeInfo(1, 66, ArmorInventory::SLOT_FEET, material: ArmorMaterials::LEATHER()), [EnchantmentTags::BOOTS])); + self::register("netherite_boots", new Armor(new IID(Ids::NETHERITE_BOOTS), "Netherite Boots", new ArmorTypeInfo(3, 482, ArmorInventory::SLOT_FEET, 3, true, material: ArmorMaterials::NETHERITE()), [EnchantmentTags::BOOTS])); - self::register("chainmail_chestplate", new Armor(new IID(Ids::CHAINMAIL_CHESTPLATE), "Chainmail Chestplate", new ArmorTypeInfo(5, 241, ArmorInventory::SLOT_CHEST))); - self::register("diamond_chestplate", new Armor(new IID(Ids::DIAMOND_CHESTPLATE), "Diamond Chestplate", new ArmorTypeInfo(8, 529, ArmorInventory::SLOT_CHEST, 2))); - self::register("golden_chestplate", new Armor(new IID(Ids::GOLDEN_CHESTPLATE), "Golden Chestplate", new ArmorTypeInfo(5, 113, ArmorInventory::SLOT_CHEST))); - self::register("iron_chestplate", new Armor(new IID(Ids::IRON_CHESTPLATE), "Iron Chestplate", new ArmorTypeInfo(6, 241, ArmorInventory::SLOT_CHEST))); - self::register("leather_tunic", new Armor(new IID(Ids::LEATHER_TUNIC), "Leather Tunic", new ArmorTypeInfo(3, 81, ArmorInventory::SLOT_CHEST))); - self::register("netherite_chestplate", new Armor(new IID(Ids::NETHERITE_CHESTPLATE), "Netherite Chestplate", new ArmorTypeInfo(8, 593, ArmorInventory::SLOT_CHEST, 3, true))); + self::register("chainmail_chestplate", new Armor(new IID(Ids::CHAINMAIL_CHESTPLATE), "Chainmail Chestplate", new ArmorTypeInfo(5, 241, ArmorInventory::SLOT_CHEST, material: ArmorMaterials::CHAINMAIL()), [EnchantmentTags::CHESTPLATE])); + self::register("diamond_chestplate", new Armor(new IID(Ids::DIAMOND_CHESTPLATE), "Diamond Chestplate", new ArmorTypeInfo(8, 529, ArmorInventory::SLOT_CHEST, 2, material: ArmorMaterials::DIAMOND()), [EnchantmentTags::CHESTPLATE])); + self::register("golden_chestplate", new Armor(new IID(Ids::GOLDEN_CHESTPLATE), "Golden Chestplate", new ArmorTypeInfo(5, 113, ArmorInventory::SLOT_CHEST, material: ArmorMaterials::GOLD()), [EnchantmentTags::CHESTPLATE])); + self::register("iron_chestplate", new Armor(new IID(Ids::IRON_CHESTPLATE), "Iron Chestplate", new ArmorTypeInfo(6, 241, ArmorInventory::SLOT_CHEST, material: ArmorMaterials::IRON()), [EnchantmentTags::CHESTPLATE])); + self::register("leather_tunic", new Armor(new IID(Ids::LEATHER_TUNIC), "Leather Tunic", new ArmorTypeInfo(3, 81, ArmorInventory::SLOT_CHEST, material: ArmorMaterials::LEATHER()), [EnchantmentTags::CHESTPLATE])); + self::register("netherite_chestplate", new Armor(new IID(Ids::NETHERITE_CHESTPLATE), "Netherite Chestplate", new ArmorTypeInfo(8, 593, ArmorInventory::SLOT_CHEST, 3, true, material: ArmorMaterials::NETHERITE()), [EnchantmentTags::CHESTPLATE])); - self::register("chainmail_helmet", new Armor(new IID(Ids::CHAINMAIL_HELMET), "Chainmail Helmet", new ArmorTypeInfo(2, 166, ArmorInventory::SLOT_HEAD))); - self::register("diamond_helmet", new Armor(new IID(Ids::DIAMOND_HELMET), "Diamond Helmet", new ArmorTypeInfo(3, 364, ArmorInventory::SLOT_HEAD, 2))); - self::register("golden_helmet", new Armor(new IID(Ids::GOLDEN_HELMET), "Golden Helmet", new ArmorTypeInfo(2, 78, ArmorInventory::SLOT_HEAD))); - self::register("iron_helmet", new Armor(new IID(Ids::IRON_HELMET), "Iron Helmet", new ArmorTypeInfo(2, 166, ArmorInventory::SLOT_HEAD))); - self::register("leather_cap", new Armor(new IID(Ids::LEATHER_CAP), "Leather Cap", new ArmorTypeInfo(1, 56, ArmorInventory::SLOT_HEAD))); - self::register("netherite_helmet", new Armor(new IID(Ids::NETHERITE_HELMET), "Netherite Helmet", new ArmorTypeInfo(3, 408, ArmorInventory::SLOT_HEAD, 3, true))); - self::register("turtle_helmet", new TurtleHelmet(new IID(Ids::TURTLE_HELMET), "Turtle Shell", new ArmorTypeInfo(2, 276, ArmorInventory::SLOT_HEAD))); + self::register("chainmail_helmet", new Armor(new IID(Ids::CHAINMAIL_HELMET), "Chainmail Helmet", new ArmorTypeInfo(2, 166, ArmorInventory::SLOT_HEAD, material: ArmorMaterials::CHAINMAIL()), [EnchantmentTags::HELMET])); + self::register("diamond_helmet", new Armor(new IID(Ids::DIAMOND_HELMET), "Diamond Helmet", new ArmorTypeInfo(3, 364, ArmorInventory::SLOT_HEAD, 2, material: ArmorMaterials::DIAMOND()), [EnchantmentTags::HELMET])); + self::register("golden_helmet", new Armor(new IID(Ids::GOLDEN_HELMET), "Golden Helmet", new ArmorTypeInfo(2, 78, ArmorInventory::SLOT_HEAD, material: ArmorMaterials::GOLD()), [EnchantmentTags::HELMET])); + self::register("iron_helmet", new Armor(new IID(Ids::IRON_HELMET), "Iron Helmet", new ArmorTypeInfo(2, 166, ArmorInventory::SLOT_HEAD, material: ArmorMaterials::IRON()), [EnchantmentTags::HELMET])); + self::register("leather_cap", new Armor(new IID(Ids::LEATHER_CAP), "Leather Cap", new ArmorTypeInfo(1, 56, ArmorInventory::SLOT_HEAD, material: ArmorMaterials::LEATHER()), [EnchantmentTags::HELMET])); + self::register("netherite_helmet", new Armor(new IID(Ids::NETHERITE_HELMET), "Netherite Helmet", new ArmorTypeInfo(3, 408, ArmorInventory::SLOT_HEAD, 3, true, material: ArmorMaterials::NETHERITE()), [EnchantmentTags::HELMET])); + self::register("turtle_helmet", new TurtleHelmet(new IID(Ids::TURTLE_HELMET), "Turtle Shell", new ArmorTypeInfo(2, 276, ArmorInventory::SLOT_HEAD, material: ArmorMaterials::TURTLE()), [EnchantmentTags::HELMET])); - self::register("chainmail_leggings", new Armor(new IID(Ids::CHAINMAIL_LEGGINGS), "Chainmail Leggings", new ArmorTypeInfo(4, 226, ArmorInventory::SLOT_LEGS))); - self::register("diamond_leggings", new Armor(new IID(Ids::DIAMOND_LEGGINGS), "Diamond Leggings", new ArmorTypeInfo(6, 496, ArmorInventory::SLOT_LEGS, 2))); - self::register("golden_leggings", new Armor(new IID(Ids::GOLDEN_LEGGINGS), "Golden Leggings", new ArmorTypeInfo(3, 106, ArmorInventory::SLOT_LEGS))); - self::register("iron_leggings", new Armor(new IID(Ids::IRON_LEGGINGS), "Iron Leggings", new ArmorTypeInfo(5, 226, ArmorInventory::SLOT_LEGS))); - self::register("leather_pants", new Armor(new IID(Ids::LEATHER_PANTS), "Leather Pants", new ArmorTypeInfo(2, 76, ArmorInventory::SLOT_LEGS))); - self::register("netherite_leggings", new Armor(new IID(Ids::NETHERITE_LEGGINGS), "Netherite Leggings", new ArmorTypeInfo(6, 556, ArmorInventory::SLOT_LEGS, 3, true))); + self::register("chainmail_leggings", new Armor(new IID(Ids::CHAINMAIL_LEGGINGS), "Chainmail Leggings", new ArmorTypeInfo(4, 226, ArmorInventory::SLOT_LEGS, material: ArmorMaterials::CHAINMAIL()), [EnchantmentTags::LEGGINGS])); + self::register("diamond_leggings", new Armor(new IID(Ids::DIAMOND_LEGGINGS), "Diamond Leggings", new ArmorTypeInfo(6, 496, ArmorInventory::SLOT_LEGS, 2, material: ArmorMaterials::DIAMOND()), [EnchantmentTags::LEGGINGS])); + self::register("golden_leggings", new Armor(new IID(Ids::GOLDEN_LEGGINGS), "Golden Leggings", new ArmorTypeInfo(3, 106, ArmorInventory::SLOT_LEGS, material: ArmorMaterials::GOLD()), [EnchantmentTags::LEGGINGS])); + self::register("iron_leggings", new Armor(new IID(Ids::IRON_LEGGINGS), "Iron Leggings", new ArmorTypeInfo(5, 226, ArmorInventory::SLOT_LEGS, material: ArmorMaterials::IRON()), [EnchantmentTags::LEGGINGS])); + self::register("leather_pants", new Armor(new IID(Ids::LEATHER_PANTS), "Leather Pants", new ArmorTypeInfo(2, 76, ArmorInventory::SLOT_LEGS, material: ArmorMaterials::LEATHER()), [EnchantmentTags::LEGGINGS])); + self::register("netherite_leggings", new Armor(new IID(Ids::NETHERITE_LEGGINGS), "Netherite Leggings", new ArmorTypeInfo(6, 556, ArmorInventory::SLOT_LEGS, 3, true, material: ArmorMaterials::NETHERITE()), [EnchantmentTags::LEGGINGS])); } } diff --git a/src/item/enchantment/AvailableEnchantmentRegistry.php b/src/item/enchantment/AvailableEnchantmentRegistry.php new file mode 100644 index 00000000000..af8484049a1 --- /dev/null +++ b/src/item/enchantment/AvailableEnchantmentRegistry.php @@ -0,0 +1,211 @@ +register(Enchantments::PROTECTION(), [Tags::ARMOR], []); + $this->register(Enchantments::FIRE_PROTECTION(), [Tags::ARMOR], []); + $this->register(Enchantments::FEATHER_FALLING(), [Tags::BOOTS], []); + $this->register(Enchantments::BLAST_PROTECTION(), [Tags::ARMOR], []); + $this->register(Enchantments::PROJECTILE_PROTECTION(), [Tags::ARMOR], []); + $this->register(Enchantments::THORNS(), [Tags::CHESTPLATE], [Tags::HELMET, Tags::LEGGINGS, Tags::BOOTS]); + $this->register(Enchantments::RESPIRATION(), [Tags::HELMET], []); + $this->register(Enchantments::SHARPNESS(), [Tags::SWORD, Tags::AXE], []); + $this->register(Enchantments::KNOCKBACK(), [Tags::SWORD], []); + $this->register(Enchantments::FIRE_ASPECT(), [Tags::SWORD], []); + $this->register(Enchantments::EFFICIENCY(), [Tags::DIG_TOOLS], [Tags::SHEARS]); + $this->register(Enchantments::FORTUNE(), [Tags::DIG_TOOLS], []); + $this->register(Enchantments::SILK_TOUCH(), [Tags::DIG_TOOLS], [Tags::SHEARS]); + $this->register( + Enchantments::UNBREAKING(), + [Tags::ARMOR, Tags::WEAPONS, Tags::FISHING_ROD], + [Tags::SHEARS, Tags::FLINT_AND_STEEL, Tags::SHIELD, Tags::CARROT_ON_STICK, Tags::ELYTRA, Tags::BRUSH] + ); + $this->register(Enchantments::POWER(), [Tags::BOW], []); + $this->register(Enchantments::PUNCH(), [Tags::BOW], []); + $this->register(Enchantments::FLAME(), [Tags::BOW], []); + $this->register(Enchantments::INFINITY(), [Tags::BOW], []); + $this->register( + Enchantments::MENDING(), + [], + [Tags::ARMOR, Tags::WEAPONS, Tags::FISHING_ROD, + Tags::SHEARS, Tags::FLINT_AND_STEEL, Tags::SHIELD, Tags::CARROT_ON_STICK, Tags::ELYTRA, Tags::BRUSH] + ); + $this->register(Enchantments::VANISHING(), [], [Tags::ALL]); + $this->register(Enchantments::SWIFT_SNEAK(), [], [Tags::LEGGINGS]); + } + + /** + * @param string[] $primaryItemTags + * @param string[] $secondaryItemTags + */ + public function register(Enchantment $enchantment, array $primaryItemTags, array $secondaryItemTags) : void{ + $this->enchantments[spl_object_id($enchantment)] = $enchantment; + $this->setPrimaryItemTags($enchantment, $primaryItemTags); + $this->setSecondaryItemTags($enchantment, $secondaryItemTags); + } + + public function unregister(Enchantment $enchantment) : void{ + unset($this->enchantments[spl_object_id($enchantment)]); + unset($this->primaryItemTags[spl_object_id($enchantment)]); + unset($this->secondaryItemTags[spl_object_id($enchantment)]); + } + + public function unregisterAll() : void{ + $this->enchantments = []; + $this->primaryItemTags = []; + $this->secondaryItemTags = []; + } + + public function isRegistered(Enchantment $enchantment) : bool{ + return isset($this->enchantments[spl_object_id($enchantment)]); + } + + /** + * Returns primary compatibility tags for the specified enchantment. + * + * An item matching at least one of these tags (or its descendents) can be: + * - Offered this enchantment in an enchanting table + * - Enchanted by any means allowed by secondary tags + * + * @return string[] + */ + public function getPrimaryItemTags(Enchantment $enchantment) : array{ + return $this->primaryItemTags[spl_object_id($enchantment)] ?? []; + } + + /** + * @param string[] $tags + */ + public function setPrimaryItemTags(Enchantment $enchantment, array $tags) : void{ + if(!$this->isRegistered($enchantment)){ + throw new \LogicException("Cannot set primary item tags for non-registered enchantment"); + } + $this->primaryItemTags[spl_object_id($enchantment)] = array_values($tags); + } + + /** + * Returns secondary compatibility tags for the specified enchantment. + * + * An item matching at least one of these tags (or its descendents) can be: + * - Combined with an enchanted book with this enchantment in an anvil + * - Obtained as loot with this enchantment, e.g. fishing, treasure chests, mob equipment, etc. + * + * @return string[] + */ + public function getSecondaryItemTags(Enchantment $enchantment) : array{ + return $this->secondaryItemTags[spl_object_id($enchantment)] ?? []; + } + + /** + * @param string[] $tags + */ + public function setSecondaryItemTags(Enchantment $enchantment, array $tags) : void{ + if(!$this->isRegistered($enchantment)){ + throw new \LogicException("Cannot set secondary item tags for non-registered enchantment"); + } + $this->secondaryItemTags[spl_object_id($enchantment)] = array_values($tags); + } + + /** + * Returns enchantments that can be applied to the specified item in an enchanting table (primary only). + * + * @return Enchantment[] + */ + public function getPrimaryEnchantmentsForItem(Item $item) : array{ + $itemTags = $item->getEnchantmentTags(); + if(count($itemTags) === 0 || $item->hasEnchantments()){ + return []; + } + + return array_filter( + $this->enchantments, + fn(Enchantment $e) => TagRegistry::getInstance()->isTagArrayIntersection($this->getPrimaryItemTags($e), $itemTags) + ); + } + + /** + * Returns all available enchantments compatible with the item. + * + * Warning: not suitable for obtaining enchantments for an enchanting table + * (use {@link AvailableEnchantmentRegistry::getPrimaryEnchantmentsForItem()} for that). + * + * @return Enchantment[] + */ + public function getAllEnchantmentsForItem(Item $item) : array{ + if(count($item->getEnchantmentTags()) === 0){ + return []; + } + + return array_filter( + $this->enchantments, + fn(Enchantment $enchantment) => $this->isAvailableForItem($enchantment, $item) + ); + } + + /** + * Returns whether the specified enchantment can be applied to the particular item. + * + * Warning: not suitable for checking the availability of enchantment for an enchanting table. + */ + public function isAvailableForItem(Enchantment $enchantment, Item $item) : bool{ + $itemTags = $item->getEnchantmentTags(); + $tagRegistry = TagRegistry::getInstance(); + + return $tagRegistry->isTagArrayIntersection($this->getPrimaryItemTags($enchantment), $itemTags) || + $tagRegistry->isTagArrayIntersection($this->getSecondaryItemTags($enchantment), $itemTags); + } + + /** + * @return Enchantment[] + */ + public function getAll() : array{ + return $this->enchantments; + } +} diff --git a/src/item/enchantment/EnchantOption.php b/src/item/enchantment/EnchantOption.php new file mode 100644 index 00000000000..b1c3a7af5a2 --- /dev/null +++ b/src/item/enchantment/EnchantOption.php @@ -0,0 +1,66 @@ +requiredXpLevel; + } + + /** + * Returns the name that will be translated to the 'Standard Galactic Alphabet' client-side. + * This can be any arbitrary text string, since the vanilla client cannot read the text anyway. + * Example: 'bless creature range free'. + */ + public function getDisplayName() : string{ + return $this->displayName; + } + + /** + * Returns the enchantments that will be applied to the item when this option is clicked. + * + * @return EnchantmentInstance[] + */ + public function getEnchantments() : array{ + return $this->enchantments; + } +} diff --git a/src/item/enchantment/Enchantment.php b/src/item/enchantment/Enchantment.php index c53dfab7c00..22c0cdb0156 100644 --- a/src/item/enchantment/Enchantment.php +++ b/src/item/enchantment/Enchantment.php @@ -23,9 +23,13 @@ namespace pocketmine\item\enchantment; +use DaveRandom\CallbackValidator\CallbackType; +use DaveRandom\CallbackValidator\ParameterType; +use DaveRandom\CallbackValidator\ReturnType; use pocketmine\lang\Translatable; use pocketmine\utils\NotCloneable; use pocketmine\utils\NotSerializable; +use pocketmine\utils\Utils; /** * Manages enchantment type data. @@ -34,13 +38,32 @@ class Enchantment{ use NotCloneable; use NotSerializable; + /** @var \Closure(int $level) : int $minEnchantingPower */ + private \Closure $minEnchantingPower; + + /** + * @phpstan-param null|(\Closure(int $level) : int) $minEnchantingPower + * + * @param int $primaryItemFlags @deprecated + * @param int $secondaryItemFlags @deprecated + * @param int $enchantingPowerRange Value used to calculate the maximum enchanting power (minEnchantingPower + enchantingPowerRange) + */ public function __construct( private Translatable|string $name, private int $rarity, private int $primaryItemFlags, private int $secondaryItemFlags, - private int $maxLevel - ){} + private int $maxLevel, + ?\Closure $minEnchantingPower = null, + private int $enchantingPowerRange = 50 + ){ + $this->minEnchantingPower = $minEnchantingPower ?? fn(int $level) : int => 1; + + Utils::validateCallableSignature(new CallbackType( + new ReturnType("int"), + new ParameterType("level", "int") + ), $this->minEnchantingPower); + } /** * Returns a translation key for this enchantment's name. @@ -58,6 +81,8 @@ public function getRarity() : int{ /** * Returns a bitset indicating what item types can have this item applied from an enchanting table. + * + * @deprecated */ public function getPrimaryItemFlags() : int{ return $this->primaryItemFlags; @@ -66,6 +91,8 @@ public function getPrimaryItemFlags() : int{ /** * Returns a bitset indicating what item types cannot have this item applied from an enchanting table, but can from * an anvil. + * + * @deprecated */ public function getSecondaryItemFlags() : int{ return $this->secondaryItemFlags; @@ -73,6 +100,8 @@ public function getSecondaryItemFlags() : int{ /** * Returns whether this enchantment can apply to the item type from an enchanting table. + * + * @deprecated */ public function hasPrimaryItemType(int $flag) : bool{ return ($this->primaryItemFlags & $flag) !== 0; @@ -80,6 +109,8 @@ public function hasPrimaryItemType(int $flag) : bool{ /** * Returns whether this enchantment can apply to the item type from an anvil, if it is not a primary item. + * + * @deprecated */ public function hasSecondaryItemType(int $flag) : bool{ return ($this->secondaryItemFlags & $flag) !== 0; @@ -92,5 +123,34 @@ public function getMaxLevel() : int{ return $this->maxLevel; } - //TODO: methods for min/max XP cost bounds based on enchantment level (not needed yet - enchanting is client-side) + /** + * Returns whether this enchantment can be applied to the item along with the given enchantment. + */ + public function isCompatibleWith(Enchantment $other) : bool{ + return IncompatibleEnchantmentRegistry::getInstance()->areCompatible($this, $other); + } + + /** + * Returns the minimum enchanting power value required for the particular level of the enchantment + * to be available in an enchanting table. + * + * Enchanting power is a random value based on the number of bookshelves around an enchanting table + * and the enchantability of the item being enchanted. It is only used when determining the available + * enchantments for the enchantment options. + */ + public function getMinEnchantingPower(int $level) : int{ + return ($this->minEnchantingPower)($level); + } + + /** + * Returns the maximum enchanting power value allowed for the particular level of the enchantment + * to be available in an enchanting table. + * + * Enchanting power is a random value based on the number of bookshelves around an enchanting table + * and the enchantability of the item being enchanted. It is only used when determining the available + * enchantments for the enchantment options. + */ + public function getMaxEnchantingPower(int $level) : int{ + return $this->getMinEnchantingPower($level) + $this->enchantingPowerRange; + } } diff --git a/src/item/enchantment/EnchantmentHelper.php b/src/item/enchantment/EnchantmentHelper.php new file mode 100644 index 00000000000..bd4b6896339 --- /dev/null +++ b/src/item/enchantment/EnchantmentHelper.php @@ -0,0 +1,217 @@ +getTypeId() === ItemTypeIds::BOOK ? Items::ENCHANTED_BOOK() : clone $item; + + foreach($enchantments as $enchantment){ + $resultItem->addEnchantment($enchantment); + } + + return $resultItem; + } + + /** + * @return EnchantOption[] + */ + public static function getEnchantOptions(Position $tablePos, Item $input, int $seed) : array{ + if($input->isNull() || $input->hasEnchantments()){ + return []; + } + + $random = new Random($seed); + + $bookshelfCount = self::countBookshelves($tablePos); + $baseRequiredLevel = $random->nextRange(1, 8) + ($bookshelfCount >> 1) + $random->nextRange(0, $bookshelfCount); + $topRequiredLevel = (int) floor(max($baseRequiredLevel / 3, 1)); + $middleRequiredLevel = (int) floor($baseRequiredLevel * 2 / 3 + 1); + $bottomRequiredLevel = max($baseRequiredLevel, $bookshelfCount * 2); + + return [ + self::createEnchantOption($random, $input, $topRequiredLevel), + self::createEnchantOption($random, $input, $middleRequiredLevel), + self::createEnchantOption($random, $input, $bottomRequiredLevel), + ]; + } + + private static function countBookshelves(Position $tablePos) : int{ + $bookshelfCount = 0; + $world = $tablePos->getWorld(); + + for($x = -2; $x <= 2; $x++){ + for($z = -2; $z <= 2; $z++){ + // We only check blocks at a distance of 2 blocks from the enchanting table + if(abs($x) !== 2 && abs($z) !== 2){ + continue; + } + + // Ensure the space between the bookshelf stack at this X/Z and the enchanting table is empty + for($y = 0; $y <= 1; $y++){ + // Calculate the coordinates of the space between the bookshelf and the enchanting table + $spaceX = max(min($x, 1), -1); + $spaceZ = max(min($z, 1), -1); + $spaceBlock = $world->getBlock($tablePos->add($spaceX, $y, $spaceZ)); + if($spaceBlock->getTypeId() !== BlockTypeIds::AIR){ + continue 2; + } + } + + // Finally, check the number of bookshelves at the current position + for($y = 0; $y <= 1; $y++){ + $block = $world->getBlock($tablePos->add($x, $y, $z)); + if($block->getTypeId() === BlockTypeIds::BOOKSHELF){ + $bookshelfCount++; + if($bookshelfCount === self::MAX_BOOKSHELF_COUNT){ + return $bookshelfCount; + } + } + } + } + } + + return $bookshelfCount; + } + + private static function createEnchantOption(Random $random, Item $inputItem, int $requiredXpLevel) : EnchantOption{ + $enchantingPower = $requiredXpLevel; + + $enchantability = $inputItem->getEnchantability(); + $enchantingPower = $enchantingPower + $random->nextRange(0, $enchantability >> 2) + $random->nextRange(0, $enchantability >> 2) + 1; + // Random bonus for enchanting power between 0.85 and 1.15 + $bonus = 1 + ($random->nextFloat() + $random->nextFloat() - 1) * 0.15; + $enchantingPower = (int) round($enchantingPower * $bonus); + + $resultEnchantments = []; + $availableEnchantments = self::getAvailableEnchantments($enchantingPower, $inputItem); + + $lastEnchantment = self::getRandomWeightedEnchantment($random, $availableEnchantments); + if($lastEnchantment !== null){ + $resultEnchantments[] = $lastEnchantment; + + // With probability (power + 1) / 50, continue adding enchantments + while($random->nextFloat() <= ($enchantingPower + 1) / 50){ + // Remove from the list of available enchantments anything that conflicts + // with previously-chosen enchantments + $availableEnchantments = array_filter( + $availableEnchantments, + function(EnchantmentInstance $e) use ($lastEnchantment){ + return $e->getType() !== $lastEnchantment->getType() && + $e->getType()->isCompatibleWith($lastEnchantment->getType()); + } + ); + + $lastEnchantment = self::getRandomWeightedEnchantment($random, $availableEnchantments); + if($lastEnchantment === null){ + break; + } + + $resultEnchantments[] = $lastEnchantment; + $enchantingPower >>= 1; + } + } + + return new EnchantOption($requiredXpLevel, self::getRandomOptionName($random), $resultEnchantments); + } + + /** + * @return EnchantmentInstance[] + */ + private static function getAvailableEnchantments(int $enchantingPower, Item $item) : array{ + $list = []; + + foreach(EnchantmentRegistry::getInstance()->getPrimaryEnchantmentsForItem($item) as $enchantment){ + for($lvl = $enchantment->getMaxLevel(); $lvl > 0; $lvl--){ + if($enchantingPower >= $enchantment->getMinEnchantingPower($lvl) && + $enchantingPower <= $enchantment->getMaxEnchantingPower($lvl) + ){ + $list[] = new EnchantmentInstance($enchantment, $lvl); + break; + } + } + } + + return $list; + } + + /** + * @param EnchantmentInstance[] $enchantments + */ + private static function getRandomWeightedEnchantment(Random $random, array $enchantments) : ?EnchantmentInstance{ + if(count($enchantments) === 0){ + return null; + } + + $totalWeight = 0; + foreach($enchantments as $enchantment){ + $totalWeight += $enchantment->getType()->getRarity(); + } + + $result = null; + $randomWeight = $random->nextRange(1, $totalWeight); + + foreach($enchantments as $enchantment){ + $randomWeight -= $enchantment->getType()->getRarity(); + + if($randomWeight <= 0){ + $result = $enchantment; + break; + } + } + + return $result; + } + + private static function getRandomOptionName(Random $random) : string{ + $name = ""; + for($i = $random->nextRange(5, 15); $i > 0; $i--){ + $name .= chr($random->nextRange(ord("a"), ord("z"))); + } + + return $name; + } +} diff --git a/src/item/enchantment/IncompatibleEnchantmentGroups.php b/src/item/enchantment/IncompatibleEnchantmentGroups.php new file mode 100644 index 00000000000..ed1141beea9 --- /dev/null +++ b/src/item/enchantment/IncompatibleEnchantmentGroups.php @@ -0,0 +1,34 @@ +> + * @var true[][] + */ + private array $incompatibilityMap = []; + + private function __construct(){ + $this->register(Groups::PROTECTION, [Enchantments::PROTECTION(), Enchantments::FIRE_PROTECTION(), Enchantments::BLAST_PROTECTION(), Enchantments::PROJECTILE_PROTECTION()]); + $this->register(Groups::BOW_INFINITE, [Enchantments::INFINITY(), Enchantments::MENDING()]); + $this->register(Groups::DIG_DROP, [Enchantments::FORTUNE(), Enchantments::SILK_TOUCH()]); + } + + /** + * Register incompatibility for an enchantment group. + * + * All enchantments belonging to the same group are incompatible with each other, + * i.e. they cannot be added together on the same item. + * + * @param Enchantment[] $enchantments + */ + public function register(string $tag, array $enchantments) : void{ + foreach($enchantments as $enchantment){ + $this->incompatibilityMap[spl_object_id($enchantment)][$tag] = true; + } + } + + /** + * Unregister incompatibility for some enchantments of a particular group. + * + * @param Enchantment[] $enchantments + */ + public function unregister(string $tag, array $enchantments) : void{ + foreach($enchantments as $enchantment){ + unset($this->incompatibilityMap[spl_object_id($enchantment)][$tag]); + } + } + + /** + * Unregister incompatibility for all enchantments of a particular group. + */ + public function unregisterAll(string $tag) : void{ + foreach($this->incompatibilityMap as $id => $tags){ + unset($this->incompatibilityMap[$id][$tag]); + } + } + + /** + * Returns whether two enchantments can be applied to the same item. + */ + public function areCompatible(Enchantment $first, Enchantment $second) : bool{ + $firstIncompatibilities = $this->incompatibilityMap[spl_object_id($first)] ?? []; + $secondIncompatibilities = $this->incompatibilityMap[spl_object_id($second)] ?? []; + return count(array_intersect_key($firstIncompatibilities, $secondIncompatibilities)) === 0; + } +} diff --git a/src/item/enchantment/ItemEnchantmentTagRegistry.php b/src/item/enchantment/ItemEnchantmentTagRegistry.php new file mode 100644 index 00000000000..9c607f9d28f --- /dev/null +++ b/src/item/enchantment/ItemEnchantmentTagRegistry.php @@ -0,0 +1,190 @@ +> + * @var string[][] + */ + private array $tagMap = []; + + private function __construct(){ + $this->register(Tags::ARMOR, [Tags::HELMET, Tags::CHESTPLATE, Tags::LEGGINGS, Tags::BOOTS]); + $this->register(Tags::SHIELD); + $this->register(Tags::SWORD); + $this->register(Tags::TRIDENT); + $this->register(Tags::BOW); + $this->register(Tags::CROSSBOW); + $this->register(Tags::SHEARS); + $this->register(Tags::FLINT_AND_STEEL); + $this->register(Tags::DIG_TOOLS, [Tags::AXE, Tags::PICKAXE, Tags::SHOVEL, Tags::HOE]); + $this->register(Tags::FISHING_ROD); + $this->register(Tags::CARROT_ON_STICK); + $this->register(Tags::COMPASS); + $this->register(Tags::MASK); + $this->register(Tags::ELYTRA); + $this->register(Tags::BRUSH); + $this->register(Tags::WEAPONS, [ + Tags::SWORD, + Tags::TRIDENT, + Tags::BOW, + Tags::CROSSBOW, + Tags::DIG_TOOLS, + ]); + } + + /** + * Register tag and its nested tags. + * + * @param string[] $nestedTags + */ + public function register(string $tag, array $nestedTags = []) : void{ + $this->assertNotInternalTag($tag); + + foreach($nestedTags as $nestedTag){ + if(!isset($this->tagMap[$nestedTag])){ + $this->register($nestedTag); + } + $this->tagMap[$tag][] = $nestedTag; + } + + if(!isset($this->tagMap[$tag])){ + $this->tagMap[$tag] = []; + $this->tagMap[Tags::ALL][] = $tag; + } + } + + public function unregister(string $tag) : void{ + if(!isset($this->tagMap[$tag])){ + return; + } + $this->assertNotInternalTag($tag); + + unset($this->tagMap[$tag]); + + foreach(Utils::stringifyKeys($this->tagMap) as $key => $nestedTags){ + if(($nestedKey = array_search($tag, $nestedTags, true)) !== false){ + unset($this->tagMap[$key][$nestedKey]); + } + } + } + + /** + * Remove specified nested tags. + * + * @param string[] $nestedTags + */ + public function removeNested(string $tag, array $nestedTags) : void{ + $this->assertNotInternalTag($tag); + $this->tagMap[$tag] = array_diff($this->tagMap[$tag], $nestedTags); + } + + /** + * Returns nested tags of a particular tag. + * + * @return string[] + */ + public function getNested(string $tag) : array{ + return $this->tagMap[$tag] ?? []; + } + + /** + * @param string[] $firstTags + * @param string[] $secondTags + */ + public function isTagArrayIntersection(array $firstTags, array $secondTags) : bool{ + if(count($firstTags) === 0 || count($secondTags) === 0){ + return false; + } + + $firstLeafTags = $this->getLeafTagsForArray($firstTags); + $secondLeafTags = $this->getLeafTagsForArray($secondTags); + + return count(array_intersect($firstLeafTags, $secondLeafTags)) !== 0; + } + + /** + * Returns all tags that are recursively nested within each tag in the array and do not have any nested tags. + * + * @param string[] $tags + * + * @return string[] + */ + private function getLeafTagsForArray(array $tags) : array{ + $leafTagArrays = []; + foreach($tags as $tag){ + $leafTagArrays[] = $this->getLeafTags($tag); + } + return array_unique(array_merge(...$leafTagArrays)); + } + + /** + * Returns all tags that are recursively nested within the given tag and do not have any nested tags. + * + * @return string[] + */ + private function getLeafTags(string $tag) : array{ + $result = []; + $tagsToHandle = [$tag]; + + while(count($tagsToHandle) !== 0){ + $currentTag = array_shift($tagsToHandle); + $nestedTags = $this->getNested($currentTag); + + if(count($nestedTags) === 0){ + $result[] = $currentTag; + }else{ + $tagsToHandle = array_merge($tagsToHandle, $nestedTags); + } + } + + return $result; + } + + private function assertNotInternalTag(string $tag) : void{ + if($tag === Tags::ALL){ + throw new \InvalidArgumentException( + "Cannot perform any operations on the internal item enchantment tag '$tag'" + ); + } + } +} diff --git a/src/item/enchantment/ItemEnchantmentTags.php b/src/item/enchantment/ItemEnchantmentTags.php new file mode 100644 index 00000000000..485cb5d0c1e --- /dev/null +++ b/src/item/enchantment/ItemEnchantmentTags.php @@ -0,0 +1,57 @@ +typeModifier = $typeModifier; if($applicableDamageTypes !== null){ diff --git a/src/item/enchantment/VanillaEnchantments.php b/src/item/enchantment/VanillaEnchantments.php index ac2f5c57e52..779098c770e 100644 --- a/src/item/enchantment/VanillaEnchantments.php +++ b/src/item/enchantment/VanillaEnchantments.php @@ -59,47 +59,224 @@ final class VanillaEnchantments{ use RegistryTrait; protected static function setup() : void{ - self::register("PROTECTION", new ProtectionEnchantment(KnownTranslationFactory::enchantment_protect_all(), Rarity::COMMON, ItemFlags::ARMOR, ItemFlags::NONE, 4, 0.75, null)); - self::register("FIRE_PROTECTION", new ProtectionEnchantment(KnownTranslationFactory::enchantment_protect_fire(), Rarity::UNCOMMON, ItemFlags::ARMOR, ItemFlags::NONE, 4, 1.25, [ - EntityDamageEvent::CAUSE_FIRE, - EntityDamageEvent::CAUSE_FIRE_TICK, - EntityDamageEvent::CAUSE_LAVA - //TODO: check fireballs - ])); - self::register("FEATHER_FALLING", new ProtectionEnchantment(KnownTranslationFactory::enchantment_protect_fall(), Rarity::UNCOMMON, ItemFlags::FEET, ItemFlags::NONE, 4, 2.5, [ - EntityDamageEvent::CAUSE_FALL - ])); - self::register("BLAST_PROTECTION", new ProtectionEnchantment(KnownTranslationFactory::enchantment_protect_explosion(), Rarity::RARE, ItemFlags::ARMOR, ItemFlags::NONE, 4, 1.5, [ - EntityDamageEvent::CAUSE_BLOCK_EXPLOSION, - EntityDamageEvent::CAUSE_ENTITY_EXPLOSION - ])); - self::register("PROJECTILE_PROTECTION", new ProtectionEnchantment(KnownTranslationFactory::enchantment_protect_projectile(), Rarity::UNCOMMON, ItemFlags::ARMOR, ItemFlags::NONE, 4, 1.5, [ - EntityDamageEvent::CAUSE_PROJECTILE - ])); - self::register("THORNS", new Enchantment(KnownTranslationFactory::enchantment_thorns(), Rarity::MYTHIC, ItemFlags::TORSO, ItemFlags::HEAD | ItemFlags::LEGS | ItemFlags::FEET, 3)); - self::register("RESPIRATION", new Enchantment(KnownTranslationFactory::enchantment_oxygen(), Rarity::RARE, ItemFlags::HEAD, ItemFlags::NONE, 3)); + self::register("PROTECTION", new ProtectionEnchantment( + KnownTranslationFactory::enchantment_protect_all(), + Rarity::COMMON, + 0, + 0, + 4, + 0.75, + null, + fn(int $level) : int => 11 * ($level - 1) + 1, + 20 + )); + self::register("FIRE_PROTECTION", new ProtectionEnchantment( + KnownTranslationFactory::enchantment_protect_fire(), + Rarity::UNCOMMON, + 0, + 0, + 4, + 1.25, + [ + EntityDamageEvent::CAUSE_FIRE, + EntityDamageEvent::CAUSE_FIRE_TICK, + EntityDamageEvent::CAUSE_LAVA + //TODO: check fireballs + ], + fn(int $level) : int => 8 * ($level - 1) + 10, + 12 + )); + self::register("FEATHER_FALLING", new ProtectionEnchantment( + KnownTranslationFactory::enchantment_protect_fall(), + Rarity::UNCOMMON, + 0, + 0, + 4, + 2.5, + [ + EntityDamageEvent::CAUSE_FALL + ], + fn(int $level) : int => 6 * ($level - 1) + 5, + 10 + )); + self::register("BLAST_PROTECTION", new ProtectionEnchantment( + KnownTranslationFactory::enchantment_protect_explosion(), + Rarity::RARE, + 0, + 0, + 4, + 1.5, + [ + EntityDamageEvent::CAUSE_BLOCK_EXPLOSION, + EntityDamageEvent::CAUSE_ENTITY_EXPLOSION + ], + fn(int $level) : int => 8 * ($level - 1) + 5, + 12 + )); + self::register("PROJECTILE_PROTECTION", new ProtectionEnchantment( + KnownTranslationFactory::enchantment_protect_projectile(), + Rarity::UNCOMMON, + 0, + 0, + 4, + 1.5, + [ + EntityDamageEvent::CAUSE_PROJECTILE + ], + fn(int $level) : int => 6 * ($level - 1) + 3, + 15 + )); + self::register("THORNS", new Enchantment( + KnownTranslationFactory::enchantment_thorns(), + Rarity::MYTHIC, + 0, + 0, + 3, + fn(int $level) : int => 20 * ($level - 1) + 10, + 50 + )); + self::register("RESPIRATION", new Enchantment( + KnownTranslationFactory::enchantment_oxygen(), + Rarity::RARE, + 0, + 0, + 3, + fn(int $level) : int => 10 * $level, + 30 + )); - self::register("SHARPNESS", new SharpnessEnchantment(KnownTranslationFactory::enchantment_damage_all(), Rarity::COMMON, ItemFlags::SWORD, ItemFlags::AXE, 5)); - //TODO: smite, bane of arthropods (these don't make sense now because their applicable mobs don't exist yet) + self::register("SHARPNESS", new SharpnessEnchantment( + KnownTranslationFactory::enchantment_damage_all(), + Rarity::COMMON, + 0, + 0, + 5, + fn(int $level) : int => 11 * ($level - 1) + 1, + 20 + )); + self::register("KNOCKBACK", new KnockbackEnchantment( + KnownTranslationFactory::enchantment_knockback(), + Rarity::UNCOMMON, + 0, + 0, + 2, + fn(int $level) : int => 20 * ($level - 1) + 5, + 50 + )); + self::register("FIRE_ASPECT", new FireAspectEnchantment( + KnownTranslationFactory::enchantment_fire(), + Rarity::RARE, + 0, + 0, + 2, + fn(int $level) : int => 20 * ($level - 1) + 10, + 50 + )); + //TODO: smite, bane of arthropods, looting (these don't make sense now because their applicable mobs don't exist yet) - self::register("KNOCKBACK", new KnockbackEnchantment(KnownTranslationFactory::enchantment_knockback(), Rarity::UNCOMMON, ItemFlags::SWORD, ItemFlags::NONE, 2)); - self::register("FIRE_ASPECT", new FireAspectEnchantment(KnownTranslationFactory::enchantment_fire(), Rarity::RARE, ItemFlags::SWORD, ItemFlags::NONE, 2)); + self::register("EFFICIENCY", new Enchantment( + KnownTranslationFactory::enchantment_digging(), + Rarity::COMMON, + 0, + 0, + 5, + fn(int $level) : int => 10 * ($level - 1) + 1, + 50 + )); + self::register("FORTUNE", new Enchantment( + KnownTranslationFactory::enchantment_lootBonusDigger(), + Rarity::RARE, + 0, + 0, + 3, + fn(int $level) : int => 9 * ($level - 1) + 15, + 50 + )); + self::register("SILK_TOUCH", new Enchantment( + KnownTranslationFactory::enchantment_untouching(), + Rarity::MYTHIC, + 0, + 0, + 1, + fn(int $level) : int => 15, + 50 + )); + self::register("UNBREAKING", new Enchantment( + KnownTranslationFactory::enchantment_durability(), + Rarity::UNCOMMON, + 0, + 0, + 3, + fn(int $level) : int => 8 * ($level - 1) + 5, + 50 + )); - self::register("EFFICIENCY", new Enchantment(KnownTranslationFactory::enchantment_digging(), Rarity::COMMON, ItemFlags::DIG, ItemFlags::SHEARS, 5)); - self::register("FORTUNE", new Enchantment(KnownTranslationFactory::enchantment_lootBonusDigger(), Rarity::RARE, ItemFlags::DIG, ItemFlags::NONE, 3)); - self::register("SILK_TOUCH", new Enchantment(KnownTranslationFactory::enchantment_untouching(), Rarity::MYTHIC, ItemFlags::DIG, ItemFlags::SHEARS, 1)); - self::register("UNBREAKING", new Enchantment(KnownTranslationFactory::enchantment_durability(), Rarity::UNCOMMON, ItemFlags::DIG | ItemFlags::ARMOR | ItemFlags::FISHING_ROD | ItemFlags::BOW, ItemFlags::TOOL | ItemFlags::CARROT_STICK | ItemFlags::ELYTRA, 3)); + self::register("POWER", new Enchantment( + KnownTranslationFactory::enchantment_arrowDamage(), + Rarity::COMMON, + 0, + 0, + 5, + fn(int $level) : int => 10 * ($level - 1) + 1, + 15 + )); + self::register("PUNCH", new Enchantment( + KnownTranslationFactory::enchantment_arrowKnockback(), + Rarity::RARE, + 0, + 0, + 2, + fn(int $level) : int => 20 * ($level - 1) + 12, + 25 + )); + self::register("FLAME", new Enchantment( + KnownTranslationFactory::enchantment_arrowFire(), + Rarity::RARE, + 0, + 0, + 1, + fn(int $level) : int => 20, + 30 + )); + self::register("INFINITY", new Enchantment( + KnownTranslationFactory::enchantment_arrowInfinite(), + Rarity::MYTHIC, + 0, + 0, + 1, + fn(int $level) : int => 20, + 30 + )); - self::register("POWER", new Enchantment(KnownTranslationFactory::enchantment_arrowDamage(), Rarity::COMMON, ItemFlags::BOW, ItemFlags::NONE, 5)); - self::register("PUNCH", new Enchantment(KnownTranslationFactory::enchantment_arrowKnockback(), Rarity::RARE, ItemFlags::BOW, ItemFlags::NONE, 2)); - self::register("FLAME", new Enchantment(KnownTranslationFactory::enchantment_arrowFire(), Rarity::RARE, ItemFlags::BOW, ItemFlags::NONE, 1)); - self::register("INFINITY", new Enchantment(KnownTranslationFactory::enchantment_arrowInfinite(), Rarity::MYTHIC, ItemFlags::BOW, ItemFlags::NONE, 1)); + self::register("MENDING", new Enchantment( + KnownTranslationFactory::enchantment_mending(), + Rarity::RARE, + 0, + 0, + 1, + fn(int $level) : int => 25, + 50 + )); - self::register("MENDING", new Enchantment(KnownTranslationFactory::enchantment_mending(), Rarity::RARE, ItemFlags::NONE, ItemFlags::ALL, 1)); + self::register("VANISHING", new Enchantment( + KnownTranslationFactory::enchantment_curse_vanishing(), + Rarity::MYTHIC, + 0, + 0, + 1, + fn(int $level) : int => 25, + 25 + )); - self::register("VANISHING", new Enchantment(KnownTranslationFactory::enchantment_curse_vanishing(), Rarity::MYTHIC, ItemFlags::NONE, ItemFlags::ALL, 1)); - - self::register("SWIFT_SNEAK", new Enchantment(KnownTranslationFactory::enchantment_swift_sneak(), Rarity::MYTHIC, ItemFlags::NONE, ItemFlags::LEGS, 3)); + self::register("SWIFT_SNEAK", new Enchantment( + KnownTranslationFactory::enchantment_swift_sneak(), + Rarity::MYTHIC, + 0, + 0, + 3, + fn(int $level) : int => 10 * $level, + 5 + )); } protected static function register(string $name, Enchantment $member) : void{ diff --git a/src/network/mcpe/InventoryManager.php b/src/network/mcpe/InventoryManager.php index 254eff91079..bd925a5c00d 100644 --- a/src/network/mcpe/InventoryManager.php +++ b/src/network/mcpe/InventoryManager.php @@ -35,9 +35,12 @@ use pocketmine\block\inventory\SmithingTableInventory; use pocketmine\block\inventory\StonecutterInventory; use pocketmine\crafting\FurnaceType; +use pocketmine\data\bedrock\EnchantmentIdMap; use pocketmine\inventory\Inventory; use pocketmine\inventory\transaction\action\SlotChangeAction; use pocketmine\inventory\transaction\InventoryTransaction; +use pocketmine\item\enchantment\EnchantmentInstance; +use pocketmine\item\enchantment\EnchantOption; use pocketmine\network\mcpe\cache\CreativeInventoryCache; use pocketmine\network\mcpe\protocol\ClientboundPacket; use pocketmine\network\mcpe\protocol\ContainerClosePacket; @@ -46,7 +49,10 @@ use pocketmine\network\mcpe\protocol\InventoryContentPacket; use pocketmine\network\mcpe\protocol\InventorySlotPacket; use pocketmine\network\mcpe\protocol\MobEquipmentPacket; +use pocketmine\network\mcpe\protocol\PlayerEnchantOptionsPacket; use pocketmine\network\mcpe\protocol\types\BlockPosition; +use pocketmine\network\mcpe\protocol\types\Enchant; +use pocketmine\network\mcpe\protocol\types\EnchantOption as ProtocolEnchantOption; use pocketmine\network\mcpe\protocol\types\inventory\ContainerIds; use pocketmine\network\mcpe\protocol\types\inventory\ItemStack; use pocketmine\network\mcpe\protocol\types\inventory\ItemStackWrapper; @@ -58,6 +64,7 @@ use pocketmine\utils\AssumptionFailedError; use pocketmine\utils\ObjectSet; use function array_keys; +use function array_map; use function array_search; use function count; use function get_class; @@ -103,6 +110,12 @@ class InventoryManager{ private bool $fullSyncRequested = false; + /** @var int[] network recipe ID => enchanting table option index */ + private array $enchantingTableOptions = []; + //TODO: this should be based on the total number of crafting recipes - if there are ever 100k recipes, this will + //conflict with regular recipes + private int $nextEnchantingTableOptionId = 100000; + public function __construct( private Player $player, private NetworkSession $session @@ -382,6 +395,7 @@ public function onCurrentWindowRemove() : void{ throw new AssumptionFailedError("We should not have opened a new window while a window was waiting to be closed"); } $this->pendingCloseWindowId = $this->lastInventoryNetworkId; + $this->enchantingTableOptions = []; } } @@ -603,6 +617,39 @@ public function syncCreative() : void{ $this->session->sendDataPacket(CreativeInventoryCache::getInstance()->getCache($this->player->getCreativeInventory())); } + /** + * @param EnchantOption[] $options + */ + public function syncEnchantingTableOptions(array $options) : void{ + $protocolOptions = []; + + foreach($options as $index => $option){ + $optionId = $this->nextEnchantingTableOptionId++; + $this->enchantingTableOptions[$optionId] = $index; + + $protocolEnchantments = array_map( + fn(EnchantmentInstance $e) => new Enchant(EnchantmentIdMap::getInstance()->toId($e->getType()), $e->getLevel()), + $option->getEnchantments() + ); + // We don't pay attention to the $slotFlags, $heldActivatedEnchantments and $selfActivatedEnchantments + // as everything works fine without them (perhaps these values are used somehow in the BDS). + $protocolOptions[] = new ProtocolEnchantOption( + $option->getRequiredXpLevel(), + 0, $protocolEnchantments, + [], + [], + $option->getDisplayName(), + $optionId + ); + } + + $this->session->sendDataPacket(PlayerEnchantOptionsPacket::create($protocolOptions)); + } + + public function getEnchantingTableOptionIndex(int $recipeId) : ?int{ + return $this->enchantingTableOptions[$recipeId] ?? null; + } + private function newItemStackId() : int{ return $this->nextItemStackId++; } diff --git a/src/network/mcpe/handler/ItemStackRequestExecutor.php b/src/network/mcpe/handler/ItemStackRequestExecutor.php index 10787f84be0..8273068ab27 100644 --- a/src/network/mcpe/handler/ItemStackRequestExecutor.php +++ b/src/network/mcpe/handler/ItemStackRequestExecutor.php @@ -23,11 +23,13 @@ namespace pocketmine\network\mcpe\handler; +use pocketmine\block\inventory\EnchantInventory; use pocketmine\inventory\Inventory; use pocketmine\inventory\transaction\action\CreateItemAction; use pocketmine\inventory\transaction\action\DestroyItemAction; use pocketmine\inventory\transaction\action\DropItemAction; use pocketmine\inventory\transaction\CraftingTransaction; +use pocketmine\inventory\transaction\EnchantTransaction; use pocketmine\inventory\transaction\InventoryTransaction; use pocketmine\inventory\transaction\TransactionBuilder; use pocketmine\inventory\transaction\TransactionBuilderInventory; @@ -287,7 +289,7 @@ protected function takeCreatedItem(int $count) : Item{ * @throws ItemStackRequestProcessException */ private function assertDoingCrafting() : void{ - if(!$this->specialTransaction instanceof CraftingTransaction){ + if(!$this->specialTransaction instanceof CraftingTransaction && !$this->specialTransaction instanceof EnchantTransaction){ if($this->specialTransaction === null){ throw new ItemStackRequestProcessException("Expected CraftRecipe or CraftRecipeAuto action to precede this action"); }else{ @@ -333,7 +335,16 @@ protected function processItemStackRequestAction(ItemStackRequestAction $action) $this->setNextCreatedItem($item, true); }elseif($action instanceof CraftRecipeStackRequestAction){ - $this->beginCrafting($action->getRecipeId(), 1); + $window = $this->player->getCurrentWindow(); + if($window instanceof EnchantInventory){ + $optionId = $this->inventoryManager->getEnchantingTableOptionIndex($action->getRecipeId()); + if($optionId !== null && ($option = $window->getOption($optionId)) !== null){ + $this->specialTransaction = new EnchantTransaction($this->player, $option, $optionId + 1); + $this->setNextCreatedItem($window->getOutput($optionId)); + } + }else{ + $this->beginCrafting($action->getRecipeId(), 1); + } }elseif($action instanceof CraftRecipeAutoStackRequestAction){ $this->beginCrafting($action->getRecipeId(), $action->getRepetitions()); }elseif($action instanceof CraftingConsumeInputStackRequestAction){