first look at anvil actions

This commit is contained in:
ShockedPlot7560 2024-08-19 22:27:45 +02:00
parent 1cc809c332
commit 7cfb6eea51
No known key found for this signature in database
GPG Key ID: D7539B420F1FA86E
7 changed files with 384 additions and 138 deletions

View File

@ -0,0 +1,52 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\anvil;
use pocketmine\item\Item;
abstract class AnvilAction{
protected int $xpCost = 0;
public function __construct(
protected Item $base,
protected Item $material,
protected ?string $customName
){ }
final public function getXpCost() : int{
return $this->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;
}

View File

@ -0,0 +1,71 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\anvil;
use pocketmine\item\Item;
use pocketmine\utils\SingletonTrait;
use function is_subclass_of;
final class AnvilActionsFactory{
use SingletonTrait;
/** @var array<class-string<AnvilAction>, 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<AnvilAction> $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;
}
}

View File

@ -0,0 +1,81 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\anvil;
use pocketmine\inventory\transaction\TransactionValidationException;
use pocketmine\item\EnchantedBook;
use pocketmine\item\enchantment\AvailableEnchantmentRegistry;
use pocketmine\item\enchantment\EnchantmentInstance;
use pocketmine\item\enchantment\Rarity;
use pocketmine\item\Item;
use function max;
use function min;
final class CombineEnchantmentsAction extends AnvilAction{
public function canBeApplied() : bool{
return $this->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);
}
}
}

View File

@ -0,0 +1,49 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\anvil;
use pocketmine\item\Item;
use function strlen;
final class RenameItemAction extends AnvilAction{
private const COST = 1;
public function canBeApplied() : bool{
return true;
}
public function process(Item $resultItem) : void{
if($this->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);
}
}
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\anvil;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use function assert;
use function ceil;
use function floor;
use function max;
use function min;
final class RepairWithMaterialAction extends AnvilAction{
private const COST = 1;
public function canBeApplied() : bool{
return $this->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;
}
}

View File

@ -0,0 +1,58 @@
<?php
/*
*
* ____ _ _ __ __ _ __ __ ____
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* @author PocketMine Team
* @link http://www.pocketmine.net/
*
*
*/
declare(strict_types=1);
namespace pocketmine\block\anvil;
use pocketmine\item\Durable;
use pocketmine\item\Item;
use function assert;
use function min;
final class RepairWithSacrificeAction extends AnvilAction{
private const COST = 2;
public function canBeApplied() : bool{
return $this->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;
}
}
}

View File

@ -23,25 +23,12 @@ declare(strict_types=1);
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);
}
}