Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e4f979d
first look at anvil
ShockedPlot7560 Aug 10, 2024
44c3e03
fix PHPstan
ShockedPlot7560 Aug 10, 2024
54f746f
finalize anvil transaction
ShockedPlot7560 Aug 10, 2024
726e2cb
Add anvil event
ShockedPlot7560 Aug 10, 2024
804731d
fix PHPstan
ShockedPlot7560 Aug 10, 2024
654b444
add sound and anvil damage
ShockedPlot7560 Aug 10, 2024
b1a773c
Merge branch 'minor-next' into feat/anvil
ShockedPlot7560 Aug 10, 2024
1cc809c
Merge branch 'minor-next' into feat/anvil
dktapps Aug 19, 2024
7cfb6ee
first look at anvil actions
ShockedPlot7560 Aug 19, 2024
b9df798
made AnvilAction constructor final
ShockedPlot7560 Aug 19, 2024
c77a72f
some work on anvil
ShockedPlot7560 Nov 18, 2024
947c8a0
remove phpstan docs
ShockedPlot7560 Nov 18, 2024
5b9dc2c
rewrote the system with CraftingManager
ShockedPlot7560 Dec 14, 2024
b3f0ed2
Merge remote-tracking branch 'upstream/minor-next' into feat/anvil
ShockedPlot7560 Dec 14, 2024
51d45be
Merge branch 'minor-next' into feat/anvil
dktapps Mar 9, 2025
a1695a5
Merge remote-tracking branch 'upstream/feat/anvil' into feat/anvil
ShockedPlot7560 Mar 22, 2025
b4cb09f
continue refactoring, not finished, not tested
ShockedPlot7560 Mar 22, 2025
11135c2
remove dead code
ShockedPlot7560 Mar 22, 2025
f346799
fill in CraftingManager for MaterialRepair
ShockedPlot7560 Mar 22, 2025
eaab4b3
add unit testing on MaterialRepairRecipe
ShockedPlot7560 Mar 22, 2025
084feae
more work on ItemCombine
ShockedPlot7560 Mar 23, 2025
2c7043d
Add validation for negative XP cost in AnvilCraftResult
ShockedPlot7560 Nov 5, 2025
373d2d6
Merge branch 'minor-next' into feat/anvil
ShockedPlot7560 Nov 5, 2025
072e4ae
Add AnvilUseEvent
ShockedPlot7560 Nov 7, 2025
b402ce6
Introduce enchanted books
ShockedPlot7560 Nov 7, 2025
a9bd3fb
fix CS
ShockedPlot7560 Nov 7, 2025
996f3a5
Use MetaWildcard instead for enchanted book
ShockedPlot7560 Nov 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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{
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
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