Implement enchanting using enchanting tables (#5953)

Co-authored-by: Dylan K. Taylor <dktapps@pmmp.io>
This commit is contained in:
S3v3Nice
2023-08-15 19:28:26 +03:00
committed by GitHub
parent e48b5b2ec0
commit 39867b97c5
34 changed files with 1892 additions and 128 deletions

View File

@@ -0,0 +1,211 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\item\enchantment;
use pocketmine\item\enchantment\ItemEnchantmentTagRegistry as TagRegistry;
use pocketmine\item\enchantment\ItemEnchantmentTags as Tags;
use pocketmine\item\enchantment\VanillaEnchantments as Enchantments;
use pocketmine\item\Item;
use pocketmine\utils\SingletonTrait;
use function array_filter;
use function array_values;
use function count;
use function spl_object_id;
/**
* Registry of enchantments that can be applied to items during in-game enchanting (enchanting table, anvil, fishing, etc.).
*/
final class AvailableEnchantmentRegistry{
use SingletonTrait;
/** @var Enchantment[] */
private array $enchantments = [];
/** @var string[][] */
private array $primaryItemTags = [];
/** @var string[][] */
private array $secondaryItemTags = [];
private function __construct(){
$this->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;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\item\enchantment;
/**
* Represents an option on the enchanting table menu.
* If selected, all the enchantments in the option will be applied to the item.
*/
class EnchantOption{
/**
* @param EnchantmentInstance[] $enchantments
*/
public function __construct(
private int $requiredXpLevel,
private string $displayName,
private array $enchantments
){}
/**
* Returns the minimum amount of XP levels required to select this enchantment option.
* It's NOT the number of XP levels that will be subtracted after enchanting.
*/
public function getRequiredXpLevel() : int{
return $this->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;
}
}

View File

@@ -23,9 +23,13 @@ declare(strict_types=1);
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 @@ class Enchantment{
/**
* 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 @@ class Enchantment{
/**
* 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 @@ class Enchantment{
/**
* 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 @@ class Enchantment{
/**
* 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 @@ class Enchantment{
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;
}
}

View File

@@ -0,0 +1,217 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\item\enchantment;
use pocketmine\block\BlockTypeIds;
use pocketmine\item\enchantment\AvailableEnchantmentRegistry as EnchantmentRegistry;
use pocketmine\item\Item;
use pocketmine\item\ItemTypeIds;
use pocketmine\item\VanillaItems as Items;
use pocketmine\utils\Random;
use pocketmine\world\Position;
use function abs;
use function array_filter;
use function chr;
use function count;
use function floor;
use function max;
use function min;
use function ord;
use function round;
final class EnchantmentHelper{
private const MAX_BOOKSHELF_COUNT = 15;
/**
* @param EnchantmentInstance[] $enchantments
*/
public static function enchantItem(Item $item, array $enchantments) : Item{
$resultItem = $item->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;
}
}

View File

@@ -0,0 +1,34 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\item\enchantment;
/**
* Constants for groupings of incompatible enchantments.
* Enchantments belonging to the same incompatibility group cannot be applied side-by-side on the same item.
*/
final class IncompatibleEnchantmentGroups{
public const PROTECTION = "protection";
public const BOW_INFINITE = "bow_infinite";
public const DIG_DROP = "dig_drop";
}

View File

@@ -0,0 +1,94 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\item\enchantment;
use pocketmine\item\enchantment\IncompatibleEnchantmentGroups as Groups;
use pocketmine\item\enchantment\VanillaEnchantments as Enchantments;
use pocketmine\utils\SingletonTrait;
use function array_intersect_key;
use function count;
use function spl_object_id;
/**
* Manages which enchantments are incompatible with each other.
* Enchantments can be added to groups to make them incompatible with all other enchantments already in that group.
*/
final class IncompatibleEnchantmentRegistry{
use SingletonTrait;
/**
* @phpstan-var array<int, array<string, true>>
* @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;
}
}

View File

@@ -0,0 +1,190 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\item\enchantment;
use pocketmine\item\enchantment\ItemEnchantmentTags as Tags;
use pocketmine\utils\SingletonTrait;
use pocketmine\utils\Utils;
use function array_diff;
use function array_intersect;
use function array_merge;
use function array_search;
use function array_shift;
use function array_unique;
use function count;
/**
* Manages known item enchantment tags and the relations between them.
* Used to determine which tags belong to which other tags, and to check if lists of tags intersect.
*/
final class ItemEnchantmentTagRegistry{
use SingletonTrait;
/**
* @phpstan-var array<string, list<string>>
* @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'"
);
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\item\enchantment;
/**
* Tags used by items and enchantments to determine which enchantments can be applied to which items.
* Some tags may contain other tags.
* @see ItemEnchantmentTagRegistry
*/
final class ItemEnchantmentTags{
public const ALL = "all";
public const ARMOR = "armor";
public const HELMET = "helmet";
public const CHESTPLATE = "chestplate";
public const LEGGINGS = "leggings";
public const BOOTS = "boots";
public const SHIELD = "shield";
public const SWORD = "sword";
public const TRIDENT = "trident";
public const BOW = "bow";
public const CROSSBOW = "crossbow";
public const SHEARS = "shears";
public const FLINT_AND_STEEL = "flint_and_steel";
public const DIG_TOOLS = "dig_tools";
public const AXE = "axe";
public const PICKAXE = "pickaxe";
public const SHOVEL = "shovel";
public const HOE = "hoe";
public const FISHING_ROD = "fishing_rod";
public const CARROT_ON_STICK = "carrot_on_stick";
public const COMPASS = "compass";
public const MASK = "mask";
public const ELYTRA = "elytra";
public const BRUSH = "brush";
public const WEAPONS = "weapons";
}

View File

@@ -23,14 +23,13 @@ declare(strict_types=1);
namespace pocketmine\item\enchantment;
/** @deprecated */
final class ItemFlags{
private function __construct(){
//NOOP
}
//TODO: this should probably move to protocol
public const NONE = 0x0;
public const ALL = 0xffff;
public const ARMOR = self::HEAD | self::TORSO | self::LEGS | self::FEET;

View File

@@ -36,10 +36,15 @@ class ProtectionEnchantment extends Enchantment{
/**
* ProtectionEnchantment constructor.
*
* @phpstan-param null|(\Closure(int $level) : int) $minEnchantingPower
*
* @param int $primaryItemFlags @deprecated
* @param int $secondaryItemFlags @deprecated
* @param int[]|null $applicableDamageTypes EntityDamageEvent::CAUSE_* constants which this enchantment type applies to, or null if it applies to all types of damage.
* @param int $enchantingPowerRange Value used to calculate the maximum enchanting power (minEnchantingPower + enchantingPowerRange)
*/
public function __construct(Translatable|string $name, int $rarity, int $primaryItemFlags, int $secondaryItemFlags, int $maxLevel, float $typeModifier, ?array $applicableDamageTypes){
parent::__construct($name, $rarity, $primaryItemFlags, $secondaryItemFlags, $maxLevel);
public function __construct(Translatable|string $name, int $rarity, int $primaryItemFlags, int $secondaryItemFlags, int $maxLevel, float $typeModifier, ?array $applicableDamageTypes, ?\Closure $minEnchantingPower = null, int $enchantingPowerRange = 50){
parent::__construct($name, $rarity, $primaryItemFlags, $secondaryItemFlags, $maxLevel, $minEnchantingPower, $enchantingPowerRange);
$this->typeModifier = $typeModifier;
if($applicableDamageTypes !== null){

View File

@@ -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{