mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-05-16 18:59:00 +00:00
rewrote the system with CraftingManager
This commit is contained in:
parent
947c8a0621
commit
5b9dc2c275
@ -1,63 +0,0 @@
|
||||
<?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;
|
||||
|
||||
final public function __construct(
|
||||
protected Item $base,
|
||||
protected Item $material,
|
||||
protected ?string $customName
|
||||
){ }
|
||||
|
||||
/**
|
||||
* Returns the XP cost requested for this action.
|
||||
* This XP cost will be summed up to the total XP cost of the anvil operation.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processing an action means applying the changes to the result item
|
||||
* and updating the XP cost property of the action.
|
||||
*/
|
||||
abstract public function process(Item $resultItem) : void;
|
||||
|
||||
/**
|
||||
* Returns whether this action is valid and can be applied.
|
||||
*/
|
||||
abstract public function canBeApplied() : bool;
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
<?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 floor;
|
||||
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 += (int) floor($costAddition * $levelDifference);
|
||||
$resultItem->addEnchantment($instance);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
<?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 = (int) floor($numberRepair * self::COST);
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
}
|
@ -23,10 +23,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\utils;
|
||||
|
||||
use pocketmine\block\anvil\AnvilActionsFactory;
|
||||
use pocketmine\crafting\AnvilCraftResult;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\player\Player;
|
||||
use function max;
|
||||
use pocketmine\Server;
|
||||
|
||||
final class AnvilHelper{
|
||||
private const COST_LIMIT = 39;
|
||||
@ -36,31 +36,29 @@ final 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 {
|
||||
$xpCost = 0;
|
||||
$resultItem = clone $base;
|
||||
public static function calculateResult(Player $player, Item $base, Item $material, ?string $customName = null) : ?AnvilCraftResult {
|
||||
|
||||
$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;
|
||||
$recipe = Server::getInstance()->getCraftingManager()->matchAnvilRecipe($base, $material);
|
||||
$result = $recipe->getResultFor($base, $material);
|
||||
|
||||
if($result !== null){
|
||||
$resultItem = $result->getResult();
|
||||
$xpCost = $result->getXpCost();
|
||||
if(($customName === null || $customName === "") && $resultItem->hasCustomName()){
|
||||
$xpCost++;
|
||||
$resultItem->clearCustomName();
|
||||
}elseif($resultItem->getCustomName() !== $customName){
|
||||
$xpCost++;
|
||||
$resultItem->setCustomName($customName);
|
||||
}
|
||||
$xpCost += $action->getXpCost();
|
||||
|
||||
$result = new AnvilCraftResult($xpCost, $resultItem);
|
||||
}
|
||||
|
||||
$xpCost += 2 ** $resultItem->getAnvilRepairCost() - 1;
|
||||
$xpCost += 2 ** $material->getAnvilRepairCost() - 1;
|
||||
$resultItem->setAnvilRepairCost(
|
||||
max($resultItem->getAnvilRepairCost(), $material->getAnvilRepairCost()) + $additionnalRepairCost
|
||||
);
|
||||
|
||||
if($xpCost <= 0 || ($xpCost > self::COST_LIMIT && !$player->isCreative())){
|
||||
if($result === null || $result->getXpCost() <= 0 || ($result->getXpCost() > self::COST_LIMIT && !$player->isCreative())){
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AnvilResult($xpCost, $resultItem);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
@ -21,21 +21,21 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\utils;
|
||||
namespace pocketmine\crafting;
|
||||
|
||||
use pocketmine\item\Item;
|
||||
|
||||
class AnvilResult{
|
||||
class AnvilCraftResult{
|
||||
public function __construct(
|
||||
private int $repairCost,
|
||||
private ?Item $result,
|
||||
private int $xpCost,
|
||||
private Item $result,
|
||||
){}
|
||||
|
||||
public function getRepairCost() : int{
|
||||
return $this->repairCost;
|
||||
public function getXpCost() : int{
|
||||
return $this->xpCost;
|
||||
}
|
||||
|
||||
public function getResult() : ?Item{
|
||||
public function getResult() : Item{
|
||||
return $this->result;
|
||||
}
|
||||
}
|
@ -21,29 +21,12 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\block\anvil;
|
||||
namespace pocketmine\crafting;
|
||||
|
||||
use pocketmine\item\Item;
|
||||
use function strlen;
|
||||
|
||||
final class RenameItemAction extends AnvilAction{
|
||||
private const COST = 1;
|
||||
interface AnvilRecipe{
|
||||
public function getXpCost() : int;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function getResultFor(Item $input, Item $material) : ?AnvilCraftResult;
|
||||
}
|
@ -76,6 +76,18 @@ class CraftingManager{
|
||||
*/
|
||||
private array $brewingRecipeCache = [];
|
||||
|
||||
/**
|
||||
* @var AnvilRecipe[]
|
||||
* @phpstan-var list<AnvilRecipe>
|
||||
*/
|
||||
private array $anvilRecipes = [];
|
||||
|
||||
/**
|
||||
* @var AnvilRecipe[][]
|
||||
* @phpstan-var array<int, array<int, AnvilRecipe>>
|
||||
*/
|
||||
private array $anvilRecipeCache = [];
|
||||
|
||||
/** @phpstan-var ObjectSet<\Closure() : void> */
|
||||
private ObjectSet $recipeRegisteredCallbacks;
|
||||
|
||||
@ -197,6 +209,14 @@ class CraftingManager{
|
||||
return $this->potionContainerChangeRecipes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AnvilRecipe[][]
|
||||
* @phpstan-return list<AnvilRecipe>
|
||||
*/
|
||||
public function getAnvilRecipes() : array{
|
||||
return $this->anvilRecipes;
|
||||
}
|
||||
|
||||
public function registerShapedRecipe(ShapedRecipe $recipe) : void{
|
||||
$this->shapedRecipes[self::hashOutputs($recipe->getResults())][] = $recipe;
|
||||
$this->craftingRecipeIndex[] = $recipe;
|
||||
@ -231,6 +251,14 @@ class CraftingManager{
|
||||
}
|
||||
}
|
||||
|
||||
public function registerAnvilRecipe(AnvilRecipe $recipe) : void{
|
||||
$this->anvilRecipes[] = $recipe;
|
||||
|
||||
foreach($this->recipeRegisteredCallbacks as $callback){
|
||||
$callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Item[] $outputs
|
||||
*/
|
||||
@ -304,4 +332,21 @@ class CraftingManager{
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function matchAnvilRecipe(Item $input, Item $material) : ?AnvilRecipe{
|
||||
$inputHash = $input->getStateId();
|
||||
$materialHash = $material->getStateId();
|
||||
$cached = $this->anvilRecipeCache[$inputHash][$materialHash] ?? null;
|
||||
if($cached !== null){
|
||||
return $cached;
|
||||
}
|
||||
|
||||
foreach($this->anvilRecipes as $recipe){
|
||||
if($recipe->getResultFor($input, $material) !== null){
|
||||
return $this->anvilRecipeCache[$inputHash][$materialHash] = $recipe;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ use pocketmine\data\bedrock\item\SavedItemStackData;
|
||||
use pocketmine\data\SavedDataLoadingException;
|
||||
use pocketmine\errorhandler\ErrorToExceptionHandler;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\VanillaItems;
|
||||
use pocketmine\nbt\LittleEndianNbtSerializer;
|
||||
use pocketmine\utils\Filesystem;
|
||||
use pocketmine\utils\Utils;
|
||||
@ -333,6 +334,18 @@ final class CraftingManagerFromDataHelper{
|
||||
));
|
||||
}
|
||||
|
||||
$result->registerAnvilRecipe(new MaterialRepairRecipe(
|
||||
new ExactRecipeIngredient(VanillaItems::DIAMOND_PICKAXE()),
|
||||
new ExactRecipeIngredient(VanillaItems::DIAMOND()),
|
||||
VanillaItems::DIAMOND_PICKAXE()
|
||||
));
|
||||
|
||||
$result->registerAnvilRecipe(new ItemCombineRecipe(
|
||||
new ExactRecipeIngredient(VanillaItems::DIAMOND_PICKAXE()),
|
||||
new ExactRecipeIngredient(VanillaItems::DIAMOND_PICKAXE()),
|
||||
VanillaItems::DIAMOND_PICKAXE()
|
||||
));
|
||||
|
||||
//TODO: smithing
|
||||
|
||||
return $result;
|
||||
|
139
src/crafting/ItemCombineRecipe.php
Normal file
139
src/crafting/ItemCombineRecipe.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?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\crafting;
|
||||
|
||||
use pocketmine\inventory\transaction\TransactionValidationException;
|
||||
use pocketmine\item\Durable;
|
||||
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 floor;
|
||||
use function max;
|
||||
use function min;
|
||||
|
||||
/**
|
||||
* Represent a recipe that repair an item with a material in an anvil.
|
||||
*/
|
||||
class ItemCombineRecipe implements AnvilRecipe{
|
||||
public function __construct(
|
||||
private RecipeIngredient $input,
|
||||
private RecipeIngredient $material,
|
||||
private Item $result
|
||||
){ }
|
||||
|
||||
public function getInput() : RecipeIngredient{
|
||||
return $this->input;
|
||||
}
|
||||
|
||||
public function getMaterial() : RecipeIngredient{
|
||||
return $this->material;
|
||||
}
|
||||
|
||||
public function getResult() : Item{
|
||||
return clone $this->result;
|
||||
}
|
||||
|
||||
public function getXpCost() : int{
|
||||
return 2;
|
||||
}
|
||||
|
||||
public function getResultFor(Item $input, Item $material) : ?AnvilCraftResult{
|
||||
if($input->equals($this->input->getItem()) && $material->equals($this->material->getItem())){
|
||||
$result = $this->getResult();
|
||||
$xpCost = 0;
|
||||
if($input instanceof Durable && $material instanceof Durable){
|
||||
$damage = $input->getDamage();
|
||||
if($damage !== 0){
|
||||
$baseMaxDurability = $input->getMaxDurability();
|
||||
$baseDurability = $baseMaxDurability - $damage;
|
||||
$materialDurability = $material->getMaxDurability() - $material->getDamage();
|
||||
$addDurability = (int) ($baseMaxDurability * 12 / 100);
|
||||
|
||||
$result->setDamage($baseMaxDurability - min($baseMaxDurability, $baseDurability + $materialDurability + $addDurability));
|
||||
}
|
||||
|
||||
$xpCost = 2;
|
||||
}
|
||||
|
||||
// setting base enchantments to result
|
||||
foreach($input->getEnchantments() as $enchantment){
|
||||
$result->addEnchantment($enchantment);
|
||||
}
|
||||
|
||||
// combining enchantments
|
||||
foreach($material->getEnchantments() as $instance){
|
||||
$enchantment = $instance->getType();
|
||||
$level = $instance->getLevel();
|
||||
if(!AvailableEnchantmentRegistry::getInstance()->isAvailableForItem($enchantment, $input)){
|
||||
continue;
|
||||
}
|
||||
if(($targetEnchantment = $input->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($input->getEnchantments() as $testedInstance){
|
||||
$testedEnchantment = $testedInstance->getType();
|
||||
if(!$testedEnchantment->isCompatibleWith($enchantment)){
|
||||
$xpCost++;
|
||||
//TODO: XP COST
|
||||
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($material instanceof EnchantedBook){
|
||||
// Enchanted books are half as expensive to combine
|
||||
$costAddition = max(1, $costAddition / 2);
|
||||
}
|
||||
$levelDifference = $instance->getLevel() - $input->getEnchantmentLevel($instance->getType());
|
||||
$xpCost += (int) floor($costAddition * $levelDifference);
|
||||
$result->addEnchantment($instance);
|
||||
|
||||
$xpCost += 2 ** $input->getAnvilRepairCost() - 1;
|
||||
$xpCost += 2 ** $material->getAnvilRepairCost() - 1;
|
||||
$result->setAnvilRepairCost(
|
||||
max($result->getAnvilRepairCost(), $material->getAnvilRepairCost()) + 1
|
||||
);
|
||||
}
|
||||
|
||||
return new AnvilCraftResult($xpCost, $result);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
74
src/crafting/MaterialRepairRecipe.php
Normal file
74
src/crafting/MaterialRepairRecipe.php
Normal file
@ -0,0 +1,74 @@
|
||||
<?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\crafting;
|
||||
|
||||
use pocketmine\item\Item;
|
||||
use function ceil;
|
||||
use function floor;
|
||||
use function max;
|
||||
use function min;
|
||||
|
||||
/**
|
||||
* Represent a recipe that repair an item with a material in an anvil.
|
||||
*/
|
||||
class MaterialRepairRecipe implements AnvilRecipe{
|
||||
public function __construct(
|
||||
private RecipeIngredient $input,
|
||||
private RecipeIngredient $material,
|
||||
private Item $result
|
||||
){ }
|
||||
|
||||
public function getInput() : RecipeIngredient{
|
||||
return $this->input;
|
||||
}
|
||||
|
||||
public function getMaterial() : RecipeIngredient{
|
||||
return $this->material;
|
||||
}
|
||||
|
||||
public function getResult() : Item{
|
||||
return clone $this->result;
|
||||
}
|
||||
|
||||
public function getXpCost() : int{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public function getResultFor(Item $input, Item $material) : ?Item{
|
||||
if($this->input->accepts($input) && $this->material->accepts($material)){
|
||||
$damage = $input->getDamage();
|
||||
if($damage !== 0){
|
||||
$quarter = min($damage, (int) floor($input->getMaxDurability() / 4));
|
||||
$numberRepair = min($material->getCount(), (int) ceil($damage / $quarter));
|
||||
if($numberRepair > 0){
|
||||
// TODO: remove the material
|
||||
$damage -= $quarter * $numberRepair;
|
||||
}
|
||||
return $this->getResult()->setDamage(max(0, $damage));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
@ -26,8 +26,8 @@ namespace pocketmine\inventory\transaction;
|
||||
use pocketmine\block\Anvil;
|
||||
use pocketmine\block\inventory\AnvilInventory;
|
||||
use pocketmine\block\utils\AnvilHelper;
|
||||
use pocketmine\block\utils\AnvilResult;
|
||||
use pocketmine\block\VanillaBlocks;
|
||||
use pocketmine\crafting\AnvilCraftResult;
|
||||
use pocketmine\event\player\PlayerUseAnvilEvent;
|
||||
use pocketmine\item\Item;
|
||||
use pocketmine\item\VanillaItems;
|
||||
@ -44,14 +44,14 @@ class AnvilTransaction extends InventoryTransaction{
|
||||
|
||||
public function __construct(
|
||||
Player $source,
|
||||
private readonly AnvilResult $expectedResult,
|
||||
private readonly AnvilCraftResult $expectedResult,
|
||||
private readonly ?string $customName
|
||||
) {
|
||||
parent::__construct($source);
|
||||
}
|
||||
|
||||
private function validateFiniteResources(int $xpSpent) : void{
|
||||
$expectedXpCost = $this->expectedResult->getRepairCost();
|
||||
$expectedXpCost = $this->expectedResult->getXpCost();
|
||||
if($xpSpent !== $expectedXpCost){
|
||||
throw new TransactionValidationException("Expected the amount of xp spent to be $expectedXpCost, but received $xpSpent");
|
||||
}
|
||||
@ -62,20 +62,20 @@ class AnvilTransaction extends InventoryTransaction{
|
||||
}
|
||||
}
|
||||
|
||||
private function validateInputs(Item $base, Item $material, Item $expectedOutput) : ?AnvilResult {
|
||||
private function validateInputs(Item $base, Item $material, Item $expectedOutput) : ?int {
|
||||
$calculAttempt = AnvilHelper::calculateResult($this->source, $base, $material, $this->customName);
|
||||
if($calculAttempt === null){
|
||||
return null;
|
||||
}
|
||||
$result = $calculAttempt->getResult();
|
||||
if($result === null || !$result->equalsExact($expectedOutput)){
|
||||
if(!$result->equalsExact($expectedOutput)){
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->baseItem = $base;
|
||||
$this->materialItem = $material;
|
||||
|
||||
return $calculAttempt;
|
||||
return $calculAttempt->getXpCost();
|
||||
}
|
||||
|
||||
public function validate() : void{
|
||||
@ -102,16 +102,16 @@ class AnvilTransaction extends InventoryTransaction{
|
||||
}
|
||||
|
||||
if(count($inputs) < 2){
|
||||
$attempt = $this->validateInputs($inputs[0], VanillaItems::AIR(), $outputItem) ??
|
||||
$xpCost = $this->validateInputs($inputs[0], VanillaItems::AIR(), $outputItem) ??
|
||||
throw new TransactionValidationException("Inputs do not match expected result");
|
||||
}else{
|
||||
$attempt = $this->validateInputs($inputs[0], $inputs[1], $outputItem) ??
|
||||
$xpCost = $this->validateInputs($inputs[0], $inputs[1], $outputItem) ??
|
||||
$this->validateInputs($inputs[1], $inputs[0], $outputItem) ??
|
||||
throw new TransactionValidationException("Inputs do not match expected result");
|
||||
}
|
||||
|
||||
if($this->source->hasFiniteResources()){
|
||||
$this->validateFiniteResources($attempt->getRepairCost());
|
||||
$this->validateFiniteResources($xpCost);
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,7 +150,7 @@ class AnvilTransaction extends InventoryTransaction{
|
||||
|
||||
$ev = new PlayerUseAnvilEvent($this->source, $this->baseItem, $this->materialItem, $this->expectedResult->getResult() ?? throw new \AssertionError(
|
||||
"Expected that the expected result is not null"
|
||||
), $this->customName, $this->expectedResult->getRepairCost());
|
||||
), $this->customName, $this->expectedResult->getXpCost());
|
||||
$ev->call();
|
||||
return !$ev->isCancelled();
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ use pocketmine\nbt\tag\CompoundTag;
|
||||
use pocketmine\nbt\tag\IntTag;
|
||||
use pocketmine\player\Player;
|
||||
use pocketmine\utils\Binary;
|
||||
use function in_array;
|
||||
use function lcg_value;
|
||||
use function mt_rand;
|
||||
|
||||
@ -173,8 +172,4 @@ class Armor extends Durable{
|
||||
$tag->setInt(self::TAG_CUSTOM_COLOR, Binary::signInt($this->customColor->toARGB())) :
|
||||
$tag->removeTag(self::TAG_CUSTOM_COLOR);
|
||||
}
|
||||
|
||||
public function isValidRepairMaterial(Item $material) : bool{
|
||||
return in_array($material->getTypeId(), $this->armorInfo->getMaterial()->getRepairMaterials(), true);
|
||||
}
|
||||
}
|
||||
|
@ -118,10 +118,6 @@ abstract class Durable extends Item{
|
||||
return $this->damage >= $this->getMaxDurability() || $this->isNull();
|
||||
}
|
||||
|
||||
public function isValidRepairMaterial(Item $material) : bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function deserializeCompoundTag(CompoundTag $tag) : void{
|
||||
parent::deserializeCompoundTag($tag);
|
||||
$this->unbreakable = $tag->getByte("Unbreakable", 0) !== 0;
|
||||
|
@ -23,8 +23,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\item;
|
||||
|
||||
use function in_array;
|
||||
|
||||
abstract class TieredTool extends Tool{
|
||||
protected ToolTier $tier;
|
||||
|
||||
@ -63,8 +61,4 @@ abstract class TieredTool extends Tool{
|
||||
public function isFireProof() : bool{
|
||||
return $this->tier === ToolTier::NETHERITE;
|
||||
}
|
||||
|
||||
public function isValidRepairMaterial(Item $material) : bool{
|
||||
return in_array($material->getTypeId(), $this->tier->getRepairMaterials(), true);
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace pocketmine\item;
|
||||
|
||||
use pocketmine\block\BlockTypeIds;
|
||||
use pocketmine\utils\LegacyEnumShimTrait;
|
||||
|
||||
/**
|
||||
@ -51,11 +50,10 @@ enum ToolTier{
|
||||
|
||||
/**
|
||||
* This function exists only to permit the use of named arguments and to make the code easier to read in PhpStorm.
|
||||
* @param int[] $repairMaterials The typeId of the items that can be used to repair this tool in the anvil.
|
||||
* @phpstan-return TMetadata
|
||||
*/
|
||||
private static function meta(int $harvestLevel, int $maxDurability, int $baseAttackPoints, int $baseEfficiency, int $enchantability, array $repairMaterials = []) : array{
|
||||
return [$harvestLevel, $maxDurability, $baseAttackPoints, $baseEfficiency, $enchantability, $repairMaterials];
|
||||
private static function meta(int $harvestLevel, int $maxDurability, int $baseAttackPoints, int $baseEfficiency, int $enchantability) : array{
|
||||
return [$harvestLevel, $maxDurability, $baseAttackPoints, $baseEfficiency, $enchantability];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,26 +61,12 @@ enum ToolTier{
|
||||
*/
|
||||
private function getMetadata() : array{
|
||||
return match($this){
|
||||
self::WOOD => self::meta(1, 60, 5, 2, 15, [
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::OAK_PLANKS),
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::SPRUCE_PLANKS),
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::BIRCH_PLANKS),
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::JUNGLE_PLANKS),
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::ACACIA_PLANKS),
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::DARK_OAK_PLANKS),
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::CRIMSON_PLANKS),
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::WARPED_PLANKS),
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::CHERRY_PLANKS),
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::MANGROVE_PLANKS)
|
||||
]),
|
||||
self::GOLD => self::meta(2, 33, 5, 12, 22, [ItemTypeIds::GOLD_INGOT]),
|
||||
self::STONE => self::meta(3, 132, 6, 4, 5, [
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::COBBLESTONE),
|
||||
ItemTypeIds::fromBlockTypeId(BlockTypeIds::COBBLED_DEEPSLATE)
|
||||
]),
|
||||
self::IRON => self::meta(4, 251, 7, 6, 14, [ItemTypeIds::IRON_INGOT]),
|
||||
self::DIAMOND => self::meta(5, 1562, 8, 8, 10, [ItemTypeIds::DIAMOND]),
|
||||
self::NETHERITE => self::meta(6, 2032, 9, 9, 15, [ItemTypeIds::NETHERITE_INGOT])
|
||||
self::WOOD => self::meta(1, 60, 5, 2, 15),
|
||||
self::GOLD => self::meta(2, 33, 5, 12, 22),
|
||||
self::STONE => self::meta(3, 132, 6, 4, 5),
|
||||
self::IRON => self::meta(4, 251, 7, 6, 14),
|
||||
self::DIAMOND => self::meta(5, 1562, 8, 8, 10),
|
||||
self::NETHERITE => self::meta(6, 2032, 9, 9, 15)
|
||||
};
|
||||
}
|
||||
|
||||
@ -111,13 +95,4 @@ enum ToolTier{
|
||||
public function getEnchantability() : int{
|
||||
return $this->getMetadata()[4];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the typeId of items that can be used to repair this tool in the anvil.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function getRepairMaterials() : array{
|
||||
return $this->getMetadata()[5];
|
||||
}
|
||||
}
|
||||
|
@ -38,8 +38,4 @@ class TurtleHelmet extends Armor{
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isValidRepairMaterial(Item $material) : bool{
|
||||
return $material->getTypeId() === ItemTypeIds::SCUTE;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user