Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement anvil #6418

Open
wants to merge 12 commits into
base: minor-next
Choose a base branch
from
9 changes: 9 additions & 0 deletions src/block/inventory/AnvilInventory.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

use pocketmine\inventory\SimpleInventory;
use pocketmine\inventory\TemporaryInventory;
use pocketmine\item\Item;
use pocketmine\world\Position;

class AnvilInventory extends SimpleInventory implements BlockInventory, TemporaryInventory{
Expand All @@ -37,4 +38,12 @@ public function __construct(Position $holder){
$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);
}
}
189 changes: 189 additions & 0 deletions src/block/utils/AnvilHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?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{
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
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{
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")
};
}
}
41 changes: 41 additions & 0 deletions src/block/utils/AnvilResult.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
86 changes: 86 additions & 0 deletions src/event/player/PlayerUseAnvilEvent.php
ShockedPlot7560 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?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\event\player;

use pocketmine\event\Cancellable;
use pocketmine\event\CancellableTrait;
use pocketmine\item\Item;
use pocketmine\player\Player;

/**
* Called when a player uses an anvil (renaming, repairing, combining items).
* This event is called once per action even if multiple tasks are performed at once.
*/
class PlayerUseAnvilEvent extends PlayerEvent implements Cancellable{
use CancellableTrait;

public function __construct(
Player $player,
private Item $baseItem,
private ?Item $materialItem,
private Item $resultItem,
private ?string $customName,
private int $xpCost
){
$this->player = $player;
}

/**
* Returns the item that the player is using as the base item (left slot).
*/
public function getBaseItem() : Item{
return $this->baseItem;
}

/**
* Returns the item that the player is using as the material item (right slot), or null if there is no material item
* (e.g. when renaming an item).
*/
public function getMaterialItem() : ?Item{
return $this->materialItem;
}

/**
* Returns the item that the player will receive as a result of the anvil operation.
*/
public function getResultItem() : Item{
return $this->resultItem;
}

/**
* Returns the custom name that the player is setting on the item, or null if the player is not renaming the item.
*
* This value is defined when the base item is already renamed.
*/
public function getCustomName() : ?string{
return $this->customName;
}

/**
* Returns the amount of XP levels that the player will spend on this anvil operation.
*/
public function getXpCost() : int{
return $this->xpCost;
}
}
Loading
Loading