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 10 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like this class to be divided into actions, for example an AnvilRapairAction, in the actions would define properties such as the cost and process the item

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like the idea

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implement possibility to set item and not juste get ?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there's some client side prediction about recipe (I honestly dont know) which may prevent it to work

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, no posibility for custom recipes :(

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