first look at anvil

This commit is contained in:
ShockedPlot7560 2024-08-10 19:04:17 +02:00
parent c4a2b6494d
commit e4f979dadf
No known key found for this signature in database
GPG Key ID: D7539B420F1FA86E
13 changed files with 407 additions and 17 deletions

View File

@ -25,6 +25,7 @@ namespace pocketmine\block\inventory;
use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\TemporaryInventory;
use pocketmine\item\Item;
use pocketmine\world\Position;
class AnvilInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
@ -37,4 +38,12 @@ class AnvilInventory extends SimpleInventory implements BlockInventory, Temporar
$this->holder = $holder;
parent::__construct(2);
}
public function getInput() : Item {
return $this->getItem(self::SLOT_INPUT);
}
public function getMaterial() : Item {
return $this->getItem(self::SLOT_MATERIAL);
}
}

View File

@ -0,0 +1,187 @@
<?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\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\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;
private const COST_LIMIT = 39;
/**
* Attempts to calculate the result of an anvil operation.
*
* 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;
$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);
}
}
// 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;
$resultItem->setRepairCost(
max($resultItem->getRepairCost(), $material->getRepairCost()) + $additionnalRepairCost
);
if($resultCost <= 0 || ($resultCost > 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{
$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")
};
}
}

View File

@ -0,0 +1,41 @@
<?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\utils;
use pocketmine\item\Item;
class AnvilResult{
public function __construct(
private int $repairCost,
private ?Item $result,
){}
public function getRepairCost() : int{
return $this->repairCost;
}
public function getResult() : ?Item{
return $this->result;
}
}

View File

@ -0,0 +1,60 @@
<?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\inventory\transaction;
use pocketmine\block\utils\AnvilResult;
use pocketmine\item\Item;
use pocketmine\player\Player;
use function count;
class AnvilTransaction extends InventoryTransaction{
public function __construct(
Player $source,
private AnvilResult $anvilResult
) {
parent::__construct($source);
}
public function validate() : void{
if(count($this->actions) < 1){
throw new TransactionValidationException("Transaction must have at least one action to be executable");
}
/** @var Item[] $inputs */
$inputs = [];
/** @var Item[] $outputs */
$outputs = [];
$this->matchItems($outputs, $inputs);
//TODO
}
public function execute() : void{
parent::execute();
if($this->source->hasFiniteResources()){
$this->source->getXpManager()->subtractXpLevels($this->anvilResult->getRepairCost());
}
}
}

View File

@ -33,6 +33,7 @@ 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;
@ -172,4 +173,8 @@ 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);
}
}

View File

@ -27,9 +27,13 @@ use pocketmine\world\sound\Sound;
class ArmorMaterial{
/**
* @param int[] $repairMaterials
*/
public function __construct(
private readonly int $enchantability,
private readonly ?Sound $equipSound = null
private readonly ?Sound $equipSound = null,
private readonly array $repairMaterials = []
){
}
@ -49,4 +53,11 @@ class ArmorMaterial{
public function getEquipSound() : ?Sound{
return $this->equipSound;
}
/**
* Returns the items that can be used to repair the armor
*/
public function getRepairMaterials() : array{
return $this->repairMaterials;
}
}

View File

@ -118,6 +118,10 @@ 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;

View File

@ -69,6 +69,7 @@ class Item implements \JsonSerializable{
public const TAG_DISPLAY_NAME = "Name";
public const TAG_DISPLAY_LORE = "Lore";
public const TAG_REPAIR_COST = "RepairCost";
public const TAG_KEEP_ON_DEATH = "minecraft:keep_on_death";
@ -84,6 +85,7 @@ class Item implements \JsonSerializable{
protected string $customName = "";
/** @var string[] */
protected array $lore = [];
protected int $repairCost = 0;
/** TODO: this needs to die in a fire */
protected ?CompoundTag $blockEntityTag = null;
@ -282,6 +284,23 @@ class Item implements \JsonSerializable{
return $this;
}
/**
* Returns the repair cost of the item.
*/
public function getRepairCost() : int{
return $this->repairCost;
}
/**
* Sets the repair cost of the item.
*
* @return $this
*/
public function setRepairCost(int $cost) : self{
$this->repairCost = $cost;
return $this;
}
/**
* @throws NbtException
*/
@ -338,6 +357,7 @@ class Item implements \JsonSerializable{
}
$this->keepOnDeath = $tag->getByte(self::TAG_KEEP_ON_DEATH, 0) !== 0;
$this->repairCost = $tag->getInt(self::TAG_REPAIR_COST, 0);
}
protected function serializeCompoundTag(CompoundTag $tag) : void{
@ -406,6 +426,12 @@ class Item implements \JsonSerializable{
}else{
$tag->removeTag(self::TAG_KEEP_ON_DEATH);
}
if($this->repairCost){
$tag->setInt(self::TAG_REPAIR_COST, $this->repairCost);
}else{
$tag->removeTag(self::TAG_REPAIR_COST);
}
}
public function getCount() : int{

View File

@ -23,6 +23,8 @@ declare(strict_types=1);
namespace pocketmine\item;
use function in_array;
abstract class TieredTool extends Tool{
protected ToolTier $tier;
@ -61,4 +63,8 @@ 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);
}
}

View File

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace pocketmine\item;
use pocketmine\block\BlockTypeIds;
use pocketmine\utils\LegacyEnumShimTrait;
/**
@ -36,7 +37,7 @@ use pocketmine\utils\LegacyEnumShimTrait;
* @method static ToolTier STONE()
* @method static ToolTier WOOD()
*
* @phpstan-type TMetadata array{0: int, 1: int, 2: int, 3: int, 4: int}
* @phpstan-type TMetadata array{0: int, 1: int, 2: int, 3: int, 4: int, 5: int[]}
*/
enum ToolTier{
use LegacyEnumShimTrait;
@ -52,8 +53,8 @@ enum ToolTier{
* This function exists only to permit the use of named arguments and to make the code easier to read in PhpStorm.
* @phpstan-return TMetadata
*/
private static function meta(int $harvestLevel, int $maxDurability, int $baseAttackPoints, int $baseEfficiency, int $enchantability) : array{
return [$harvestLevel, $maxDurability, $baseAttackPoints, $baseEfficiency, $enchantability];
private static function meta(int $harvestLevel, int $maxDurability, int $baseAttackPoints, int $baseEfficiency, int $enchantability, array $repairMaterials = []) : array{
return [$harvestLevel, $maxDurability, $baseAttackPoints, $baseEfficiency, $enchantability, $repairMaterials];
}
/**
@ -61,12 +62,26 @@ enum ToolTier{
*/
private function getMetadata() : array{
return match($this){
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)
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])
};
}
@ -95,4 +110,13 @@ enum ToolTier{
public function getEnchantability() : int{
return $this->getMetadata()[4];
}
/**
* Returns the list of items that can be used to repair this tool.
*
* @return int[]
*/
public function getRepairMaterials() : array{
return $this->getMetadata()[5];
}
}

View File

@ -38,4 +38,8 @@ class TurtleHelmet extends Armor{
return false;
}
public function isValidRepairMaterial(Item $material) : bool{
return $material->getTypeId() === ItemTypeIds::SCUTE;
}
}

View File

@ -69,12 +69,12 @@ final class VanillaArmorMaterials{
}
protected static function setup() : void{
self::register("leather", new ArmorMaterial(15, new ArmorEquipLeatherSound()));
self::register("leather", new ArmorMaterial(15, new ArmorEquipLeatherSound(), [ItemTypeIds::LEATHER]));
self::register("chainmail", new ArmorMaterial(12, new ArmorEquipChainSound()));
self::register("iron", new ArmorMaterial(9, new ArmorEquipIronSound()));
self::register("turtle", new ArmorMaterial(9, new ArmorEquipGenericSound()));
self::register("gold", new ArmorMaterial(25, new ArmorEquipGoldSound()));
self::register("diamond", new ArmorMaterial(10, new ArmorEquipDiamondSound()));
self::register("netherite", new ArmorMaterial(15, new ArmorEquipNetheriteSound()));
self::register("iron", new ArmorMaterial(9, new ArmorEquipIronSound(), [ItemTypeIds::IRON_INGOT]));
self::register("turtle", new ArmorMaterial(9, new ArmorEquipGenericSound(), [ItemTypeIds::SCUTE]));
self::register("gold", new ArmorMaterial(25, new ArmorEquipGoldSound(), [ItemTypeIds::GOLD_INGOT]));
self::register("diamond", new ArmorMaterial(10, new ArmorEquipDiamondSound(), [ItemTypeIds::DIAMOND]));
self::register("netherite", new ArmorMaterial(15, new ArmorEquipNetheriteSound(), [ItemTypeIds::NETHERITE_INGOT]));
}
}

View File

@ -23,11 +23,14 @@ declare(strict_types=1);
namespace pocketmine\network\mcpe\handler;
use pocketmine\block\inventory\AnvilInventory;
use pocketmine\block\inventory\EnchantInventory;
use pocketmine\block\utils\AnvilHelper;
use pocketmine\inventory\Inventory;
use pocketmine\inventory\transaction\action\CreateItemAction;
use pocketmine\inventory\transaction\action\DestroyItemAction;
use pocketmine\inventory\transaction\action\DropItemAction;
use pocketmine\inventory\transaction\AnvilTransaction;
use pocketmine\inventory\transaction\CraftingTransaction;
use pocketmine\inventory\transaction\EnchantingTransaction;
use pocketmine\inventory\transaction\InventoryTransaction;
@ -39,6 +42,7 @@ use pocketmine\network\mcpe\protocol\types\inventory\ContainerUIIds;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingConsumeInputStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftingCreateSpecificResultStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeAutoStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeOptionalStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CraftRecipeStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\CreativeCreateStackRequestAction;
use pocketmine\network\mcpe\protocol\types\inventory\stackrequest\DeprecatedCraftingResultsStackRequestAction;
@ -289,7 +293,7 @@ class ItemStackRequestExecutor{
* @throws ItemStackRequestProcessException
*/
private function assertDoingCrafting() : void{
if(!$this->specialTransaction instanceof CraftingTransaction && !$this->specialTransaction instanceof EnchantingTransaction){
if(!$this->specialTransaction instanceof CraftingTransaction && !$this->specialTransaction instanceof EnchantingTransaction && !$this->specialTransaction instanceof AnvilTransaction){
if($this->specialTransaction === null){
throw new ItemStackRequestProcessException("Expected CraftRecipe or CraftRecipeAuto action to precede this action");
}else{
@ -347,6 +351,15 @@ class ItemStackRequestExecutor{
}
}elseif($action instanceof CraftRecipeAutoStackRequestAction){
$this->beginCrafting($action->getRecipeId(), $action->getRepetitions());
}elseif($action instanceof CraftRecipeOptionalStackRequestAction){
$window = $this->player->getCurrentWindow();
if($window instanceof AnvilInventory){
$result = AnvilHelper::calculateResult($this->player, $window->getInput(), $window->getMaterial(), $this->request->getFilterStrings()[0] ?? null);
if($result !== null){
$this->specialTransaction = new AnvilTransaction($this->player, $result);
$this->setNextCreatedItem($result->getResult());
}
}
}elseif($action instanceof CraftingConsumeInputStackRequestAction){
$this->assertDoingCrafting();
$this->removeItemFromSlot($action->getSource(), $action->getCount()); //output discarded - we allow CraftingTransaction to verify the balance