diff --git a/src/block/anvil/AnvilAction.php b/src/block/anvil/AnvilAction.php new file mode 100644 index 00000000000..a925195df73 --- /dev/null +++ b/src/block/anvil/AnvilAction.php @@ -0,0 +1,52 @@ +xpCost; + } + + /** + * If only actions marked as free of repair cost is applied, the result item + * will not have any repair cost increase. + */ + public function isFreeOfRepairCost() : bool { + return false; + } + + abstract public function process(Item $resultItem) : void; + + abstract public function canBeApplied() : bool; +} diff --git a/src/block/anvil/AnvilActionsFactory.php b/src/block/anvil/AnvilActionsFactory.php new file mode 100644 index 00000000000..c53e3794cfe --- /dev/null +++ b/src/block/anvil/AnvilActionsFactory.php @@ -0,0 +1,71 @@ +, true> */ + private array $actions = []; + + private function __construct(){ + $this->register(RenameItemAction::class); + $this->register(CombineEnchantmentsAction::class); + $this->register(RepairWithSacrificeAction::class); + $this->register(RepairWithMaterialAction::class); + } + + /** + * @param class-string $class + */ + public function register(string $class) : void{ + if(!is_subclass_of($class, AnvilAction::class, true)){ + throw new \InvalidArgumentException("Class $class is not an AnvilAction"); + } + if(isset($this->actions[$class])){ + throw new \InvalidArgumentException("Class $class is already registered"); + } + $this->actions[$class] = true; + } + + /** + * Return all available actions for the given items. + * + * @return AnvilAction[] + */ + public function getActions(Item $base, Item $material, ?string $customName) : array{ + $actions = []; + foreach($this->actions as $class => $_){ + $action = new $class($base, $material, $customName); + if($action->canBeApplied()){ + $actions[] = $action; + } + } + return $actions; + } +} diff --git a/src/block/anvil/CombineEnchantmentsAction.php b/src/block/anvil/CombineEnchantmentsAction.php new file mode 100644 index 00000000000..6419c48c06a --- /dev/null +++ b/src/block/anvil/CombineEnchantmentsAction.php @@ -0,0 +1,81 @@ +material->hasEnchantments(); + } + + public function process(Item $resultItem) : void{ + foreach($this->material->getEnchantments() as $instance){ + $enchantment = $instance->getType(); + $level = $instance->getLevel(); + if(!AvailableEnchantmentRegistry::getInstance()->isAvailableForItem($enchantment, $this->base)){ + continue; + } + if(($targetEnchantment = $this->base->getEnchantment($enchantment)) !== null){ + // Enchant already present on the target item + $targetLevel = $targetEnchantment->getLevel(); + $newLevel = ($targetLevel === $level ? $targetLevel + 1 : max($targetLevel, $level)); + $level = min($newLevel, $enchantment->getMaxLevel()); + $instance = new EnchantmentInstance($enchantment, $level); + }else{ + // Check if the enchantment is compatible with the existing enchantments + foreach($this->base->getEnchantments() as $testedInstance){ + $testedEnchantment = $testedInstance->getType(); + if(!$testedEnchantment->isCompatibleWith($enchantment)){ + $this->xpCost++; + continue 2; + } + } + } + + $costAddition = match($enchantment->getRarity()){ + Rarity::COMMON => 1, + Rarity::UNCOMMON => 2, + Rarity::RARE => 4, + Rarity::MYTHIC => 8, + default => throw new TransactionValidationException("Invalid rarity " . $enchantment->getRarity() . " found") + }; + + if($this->material instanceof EnchantedBook){ + // Enchanted books are half as expensive to combine + $costAddition = max(1, $costAddition / 2); + } + $levelDifference = $instance->getLevel() - $this->base->getEnchantmentLevel($instance->getType()); + $this->xpCost += $costAddition * $levelDifference; + $resultItem->addEnchantment($instance); + } + } +} diff --git a/src/block/anvil/RenameItemAction.php b/src/block/anvil/RenameItemAction.php new file mode 100644 index 00000000000..ac8aae72f45 --- /dev/null +++ b/src/block/anvil/RenameItemAction.php @@ -0,0 +1,49 @@ +customName === null || strlen($this->customName) === 0){ + if($this->base->hasCustomName()){ + $this->xpCost += self::COST; + $resultItem->clearCustomName(); + } + }else{ + if($this->base->getCustomName() !== $this->customName){ + $this->xpCost += self::COST; + $resultItem->setCustomName($this->customName); + } + } + } +} diff --git a/src/block/anvil/RepairWithMaterialAction.php b/src/block/anvil/RepairWithMaterialAction.php new file mode 100644 index 00000000000..9d9b12b15cb --- /dev/null +++ b/src/block/anvil/RepairWithMaterialAction.php @@ -0,0 +1,58 @@ +base instanceof Durable && + $this->base->isValidRepairMaterial($this->material) && + $this->base->getDamage() > 0; + } + + public function process(Item $resultItem) : void{ + assert($resultItem instanceof Durable, "Result item must be durable"); + assert($this->base instanceof Durable, "Base item must be durable"); + + $damage = $this->base->getDamage(); + $quarter = min($damage, (int) floor($this->base->getMaxDurability() / 4)); + $numberRepair = min($this->material->getCount(), (int) ceil($damage / $quarter)); + if($numberRepair > 0){ + $this->material->pop($numberRepair); + $damage -= $quarter * $numberRepair; + } + $resultItem->setDamage(max(0, $damage)); + + $this->xpCost = $numberRepair * self::COST; + } +} diff --git a/src/block/anvil/RepairWithSacrificeAction.php b/src/block/anvil/RepairWithSacrificeAction.php new file mode 100644 index 00000000000..7575f7c8f3b --- /dev/null +++ b/src/block/anvil/RepairWithSacrificeAction.php @@ -0,0 +1,58 @@ +base instanceof Durable && + $this->material instanceof Durable && + $this->base->getTypeId() === $this->material->getTypeId(); + } + + public function process(Item $resultItem) : void{ + assert($resultItem instanceof Durable, "Result item must be durable"); + assert($this->base instanceof Durable, "Base item must be durable"); + assert($this->material instanceof Durable, "Material item must be durable"); + + if($this->base->getDamage() !== 0){ + $baseMaxDurability = $this->base->getMaxDurability(); + $baseDurability = $baseMaxDurability - $this->base->getDamage(); + $materialDurability = $this->material->getMaxDurability() - $this->material->getDamage(); + $addDurability = (int) ($baseMaxDurability * 12 / 100); + + $newDurability = min($baseMaxDurability, $baseDurability + $materialDurability + $addDurability); + + $resultItem->setDamage($baseMaxDurability - $newDurability); + + $this->xpCost = self::COST; + } + } +} diff --git a/src/block/utils/AnvilHelper.php b/src/block/utils/AnvilHelper.php index ed8a1bfd5f6..86542318ca6 100644 --- a/src/block/utils/AnvilHelper.php +++ b/src/block/utils/AnvilHelper.php @@ -23,25 +23,12 @@ namespace pocketmine\block\utils; -use pocketmine\inventory\transaction\TransactionValidationException; -use pocketmine\item\Durable; -use pocketmine\item\EnchantedBook; -use pocketmine\item\enchantment\AvailableEnchantmentRegistry; -use pocketmine\item\enchantment\Enchantment; -use pocketmine\item\enchantment\EnchantmentInstance; -use pocketmine\item\enchantment\Rarity; +use pocketmine\block\anvil\AnvilActionsFactory; use pocketmine\item\Item; use pocketmine\player\Player; -use function ceil; -use function floor; use function max; -use function min; -use function strlen; -class AnvilHelper{ - private const COST_REPAIR_MATERIAL = 1; - private const COST_REPAIR_SACRIFICE = 2; - private const COST_RENAME = 1; +final class AnvilHelper{ private const COST_LIMIT = 39; /** @@ -50,140 +37,30 @@ class AnvilHelper{ * Returns null if the operation can't do anything. */ public static function calculateResult(Player $player, Item $base, Item $material, ?string $customName = null) : ?AnvilResult { - $resultCost = 0; + $xpCost = 0; $resultItem = clone $base; - if($resultItem instanceof Durable && $resultItem->isValidRepairMaterial($material) && $resultItem->getDamage() > 0){ - $resultCost += self::repairWithMaterial($resultItem, $material); - }else{ - if($resultItem->getTypeId() === $material->getTypeId() && $resultItem instanceof Durable && $material instanceof Durable){ - $resultCost += self::repairWithSacrifice($resultItem, $material); - } - if($material->hasEnchantments()){ - $resultCost += self::combineEnchantments($resultItem, $material); + $additionnalRepairCost = 0; + foreach(AnvilActionsFactory::getInstance()->getActions($base, $material, $customName) as $action){ + $action->process($resultItem); + if(!$action->isFreeOfRepairCost() && $action->getXpCost() > 0){ + // Repair cost increment if the item has been processed + // and any of the action is not free of repair cost + $additionnalRepairCost = 1; } + $xpCost += $action->getXpCost(); } - // Repair cost increment if the item has been processed, the rename is free of penalty - $additionnalRepairCost = $resultCost > 0 ? 1 : 0; - $resultCost += self::renameItem($resultItem, $customName); - - $resultCost += 2 ** $resultItem->getRepairCost() - 1; - $resultCost += 2 ** $material->getRepairCost() - 1; + $xpCost += 2 ** $resultItem->getRepairCost() - 1; + $xpCost += 2 ** $material->getRepairCost() - 1; $resultItem->setRepairCost( max($resultItem->getRepairCost(), $material->getRepairCost()) + $additionnalRepairCost ); - if($resultCost <= 0 || ($resultCost > self::COST_LIMIT && !$player->isCreative())){ + if($xpCost <= 0 || ($xpCost > self::COST_LIMIT && !$player->isCreative())){ return null; } - return new AnvilResult($resultCost, $resultItem); - } - - /** - * @return int The XP cost of repairing the item - */ - private static function repairWithMaterial(Durable $result, Item $material) : int { - $damage = $result->getDamage(); - $quarter = min($damage, (int) floor($result->getMaxDurability() / 4)); - $numberRepair = min($material->getCount(), (int) ceil($damage / $quarter)); - if($numberRepair > 0){ - $material->pop($numberRepair); - $damage -= $quarter * $numberRepair; - } - $result->setDamage(max(0, $damage)); - - return $numberRepair * self::COST_REPAIR_MATERIAL; - } - - /** - * @return int The XP cost of repairing the item - */ - private static function repairWithSacrifice(Durable $result, Durable $sacrifice) : int{ - if($result->getDamage() === 0){ - return 0; - } - $baseDurability = $result->getMaxDurability() - $result->getDamage(); - $materialDurability = $sacrifice->getMaxDurability() - $sacrifice->getDamage(); - $addDurability = (int) ($result->getMaxDurability() * 12 / 100); - - $newDurability = min($result->getMaxDurability(), $baseDurability + $materialDurability + $addDurability); - - $result->setDamage($result->getMaxDurability() - $newDurability); - - return self::COST_REPAIR_SACRIFICE; - } - - /** - * @return int The XP cost of combining the enchantments - */ - private static function combineEnchantments(Item $base, Item $sacrifice) : int{ - $cost = 0; - foreach($sacrifice->getEnchantments() as $instance){ - $enchantment = $instance->getType(); - $level = $instance->getLevel(); - if(!AvailableEnchantmentRegistry::getInstance()->isAvailableForItem($enchantment, $base)){ - continue; - } - if(($targetEnchantment = $base->getEnchantment($enchantment)) !== null){ - // Enchant already present on the target item - $targetLevel = $targetEnchantment->getLevel(); - $newLevel = ($targetLevel === $level ? $targetLevel + 1 : max($targetLevel, $level)); - $level = min($newLevel, $enchantment->getMaxLevel()); - $instance = new EnchantmentInstance($enchantment, $level); - }else{ - // Check if the enchantment is compatible with the existing enchantments - foreach($base->getEnchantments() as $testedInstance){ - $testedEnchantment = $testedInstance->getType(); - if(!$testedEnchantment->isCompatibleWith($enchantment)){ - $cost++; - continue 2; - } - } - } - - $costAddition = self::getCostAddition($enchantment); - - if($sacrifice instanceof EnchantedBook){ - // Enchanted books are half as expensive to combine - $costAddition = max(1, $costAddition / 2); - } - $levelDifference = $instance->getLevel() - $base->getEnchantmentLevel($instance->getType()); - $cost += $costAddition * $levelDifference; - $base->addEnchantment($instance); - } - - return (int) $cost; - } - - /** - * @return int The XP cost of renaming the item - */ - private static function renameItem(Item $item, ?string $customName) : int{ - $resultCost = 0; - if($customName === null || strlen($customName) === 0){ - if($item->hasCustomName()){ - $resultCost += self::COST_RENAME; - $item->clearCustomName(); - } - }else{ - if($item->getCustomName() !== $customName){ - $resultCost += self::COST_RENAME; - $item->setCustomName($customName); - } - } - - return $resultCost; - } - - private static function getCostAddition(Enchantment $enchantment) : int { - return match($enchantment->getRarity()){ - Rarity::COMMON => 1, - Rarity::UNCOMMON => 2, - Rarity::RARE => 4, - Rarity::MYTHIC => 8, - default => throw new TransactionValidationException("Invalid rarity " . $enchantment->getRarity() . " found") - }; + return new AnvilResult($xpCost, $resultItem); } }