10 Commits

Author SHA1 Message Date
0919b4c4db Attempts to start proper implementation of the new action cost
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2025-12-17 14:25:14 +01:00
1b1dad4b8b Updates KnarLib and Spigot
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2025-09-16 17:25:11 +02:00
967791d275 Fixes merge problems
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2025-09-06 15:16:00 +02:00
bb46967892 Merge branch 'refs/heads/dev' into book-splitting
# Conflicts:
#	src/main/java/net/knarcraft/blacksmith/BlacksmithPlugin.java
#	src/main/java/net/knarcraft/blacksmith/command/blacksmith/BlackSmithConfigCommand.java
#	src/main/java/net/knarcraft/blacksmith/formatting/BlacksmithStringFormatter.java
#	src/main/java/net/knarcraft/blacksmith/trait/SalvageSession.java
#	src/main/java/net/knarcraft/blacksmith/trait/ScrapperTrait.java
2025-09-06 15:09:34 +02:00
1d17239247 Adds some code for displaying an item's cost
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2025-08-04 16:55:38 +02:00
ad5066af4b Adds more unfinished code for parsing and tab-completing advanced cost
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2025-02-03 19:34:57 +01:00
42ca42c571 Adds increased scrapper cost when used excessively
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-12-21 19:48:39 +01:00
7e17122bb2 Fixes colored NPC names, and cleans up a bit
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-12-15 14:20:03 +01:00
b01ccfc537 Finishes the ActionCost implementation
All checks were successful
EpicKnarvik97/Blacksmith/pipeline/head This commit looks good
2024-11-27 23:06:33 +01:00
e3dbeccc14 Broken commit implementing a lot of book-splitting code
Some checks failed
EpicKnarvik97/Blacksmith/pipeline/head There was a failure building this commit
2024-11-27 12:16:57 +01:00
30 changed files with 1135 additions and 63 deletions

19
pom.xml
View File

@@ -65,7 +65,7 @@
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
<version>1.20.6-R0.1-SNAPSHOT</version>
<version>1.21.8-R0.1-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -83,7 +83,7 @@
<dependency>
<groupId>net.knarcraft</groupId>
<artifactId>knarlib</artifactId>
<version>1.2.14</version>
<version>1.2.19</version>
<scope>compile</scope>
</dependency>
<dependency>
@@ -92,6 +92,11 @@
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.objecthunter</groupId>
<artifactId>exp4j</artifactId>
<version>0.4.8</version>
</dependency>
</dependencies>
<!-- Build information -->
@@ -127,6 +132,10 @@
<pattern>org.jetbrains.annotations</pattern>
<shadedPattern>net.knarcraft.blacksmith.lib.annotations</shadedPattern>
</relocation>
<relocation>
<pattern>net.objecthunter.exp4j</pattern>
<shadedPattern>net.knarcraft.blacksmith.lib.exp4j</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
@@ -141,6 +150,12 @@
<include>org/jetbrains/annotations/**</include>
</includes>
</filter>
<filter>
<artifact>net.objecthunter:exp4j</artifact>
<includes>
<include>net/objecthunter/exp4j/**</include>
</includes>
</filter>
</filters>
</configuration>
</execution>

View File

@@ -15,10 +15,12 @@ import net.knarcraft.blacksmith.command.scrapper.ScrapperEditCommand;
import net.knarcraft.blacksmith.command.scrapper.ScrapperEditTabCompleter;
import net.knarcraft.blacksmith.config.blacksmith.GlobalBlacksmithSettings;
import net.knarcraft.blacksmith.config.scrapper.GlobalScrapperSettings;
import net.knarcraft.blacksmith.container.ActionCost;
import net.knarcraft.blacksmith.formatting.Translatable;
import net.knarcraft.blacksmith.listener.NPCClickListener;
import net.knarcraft.blacksmith.listener.PlayerListener;
import net.knarcraft.blacksmith.manager.EconomyManager;
import net.knarcraft.blacksmith.manager.PlayerUsageManager;
import net.knarcraft.blacksmith.trait.BlacksmithTrait;
import net.knarcraft.blacksmith.trait.ScrapperTrait;
import net.knarcraft.knarlib.formatting.FormatBuilder;
@@ -28,7 +30,9 @@ import net.knarcraft.knarlib.formatting.Translator;
import net.knarcraft.knarlib.plugin.ConfigCommentPlugin;
import net.knarcraft.knarlib.util.ConfigHelper;
import net.knarcraft.knarlib.util.UpdateChecker;
import org.bukkit.Bukkit;
import org.bukkit.configuration.file.FileConfiguration;
import org.bukkit.configuration.serialization.ConfigurationSerialization;
import org.bukkit.event.Event;
import org.bukkit.plugin.PluginDescriptionFile;
import org.bukkit.plugin.PluginManager;
@@ -122,6 +126,8 @@ public class BlacksmithPlugin extends ConfigCommentPlugin {
instance = this;
ConfigHelper.saveDefaults(this);
ConfigurationSerialization.registerClass(ActionCost.class);
//Migrate from an earlier configuration file syntax if necessary
if (getConfig().getString("scrapper.defaults.dropItem") == null) {
net.knarcraft.knarlib.util.ConfigHelper.migrateConfig(this);
@@ -148,6 +154,10 @@ public class BlacksmithPlugin extends ConfigCommentPlugin {
//Alert about an update in the console
UpdateChecker.checkForUpdate(this, "https://api.spigotmc.org/legacy/update.php?resource=105938",
() -> this.getDescription().getVersion(), null);
// Remove expired scrapper usage data
Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> PlayerUsageManager.removeExpiredData(
System.currentTimeMillis() - 3600000), 36000, 36000);
}
/**

View File

@@ -12,6 +12,7 @@ import net.knarcraft.blacksmith.util.InputParsingHelper;
import net.knarcraft.blacksmith.util.ItemHelper;
import net.knarcraft.blacksmith.util.TypeValidationHelper;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
@@ -100,8 +101,12 @@ public class BlackSmithConfigCommand implements CommandExecutor {
if (enchantment == null) {
return false;
}
NamespacedKey enchantmentKey = enchantment.getKeyOrNull();
if (enchantmentKey == null) {
return false;
}
Translatable.getItemCurrentValueMessage(setting.getCommandName(),
ItemType.ENCHANTMENT, enchantment.getKey().getKey(),
ItemType.ENCHANTMENT, enchantmentKey.getKey(),
String.valueOf(settings.getEnchantmentCost(enchantment))).success(sender);
return true;
} else {
@@ -152,6 +157,8 @@ public class BlackSmithConfigCommand implements CommandExecutor {
return false;
}
ItemType itemType = ItemType.ENCHANTMENT;
// Note: While depreciated in Spigot, the new method is not available for Paper
@SuppressWarnings("deprecation")
String itemChanged = enchantment.getKey().getKey();
settings.setEnchantmentCost(enchantment, newPrice);
Translatable.getItemValueChangedMessage(blacksmithSetting.getCommandName(), itemType, itemChanged, newValue).success(sender);

View File

@@ -55,4 +55,9 @@ public enum SettingValueType {
*/
ENCHANTMENT_LIST,
/**
* Advanced cost, that supports either a simple double, or specifying money cost, permission requirement, exp cost and an item cost
*/
ADVANCED_COST,
}

View File

@@ -523,6 +523,8 @@ public class GlobalBlacksmithSettings implements Settings<BlacksmithSetting> {
//Load all enchantment prices
ConfigurationSection enchantmentCostNode = getAndCreateSection(fileConfiguration, getBase(BlacksmithSetting.ENCHANTMENT_COST.getPath()));
for (Enchantment enchantment : this.enchantmentCosts.keySet()) {
// Note: While depreciated in Spigot, the new method is not available for Paper
//noinspection deprecation
enchantmentCostNode.set(unNormalizeName(enchantment.getKey().getKey()), this.enchantmentCosts.get(enchantment));
}

View File

@@ -3,6 +3,7 @@ package net.knarcraft.blacksmith.config.scrapper;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.container.ActionCost;
import net.knarcraft.blacksmith.util.ConfigHelper;
import net.knarcraft.blacksmith.util.ItemHelper;
import org.bukkit.Material;
@@ -113,18 +114,6 @@ public class GlobalScrapperSettings implements Settings<ScrapperSetting> {
return ConfigHelper.asBoolean(getValue(setting));
}
/**
* Gets the given value as a double
*
* <p>This will throw an exception if used for a non-double setting</p>
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a double</p>
*/
public double asDouble(@NotNull ScrapperSetting setting) {
return ConfigHelper.asDouble(getValue(setting));
}
/**
* Gets the value of a setting, using the default if not set
*
@@ -190,8 +179,9 @@ public class GlobalScrapperSettings implements Settings<ScrapperSetting> {
*
* @return <p>The cost of using a scrapper to salvage an item</p>
*/
public double getSalvageCost() {
return asDouble(ScrapperSetting.SALVAGE_COST);
@NotNull
public ActionCost getSalvageCost() {
return getCost(ScrapperSetting.SALVAGE_COST, "salvage");
}
/**
@@ -199,8 +189,9 @@ public class GlobalScrapperSettings implements Settings<ScrapperSetting> {
*
* @return <p>The cost of using a scrapper to remove armor trim</p>
*/
public double getArmorTrimSalvageCost() {
return asDouble(ScrapperSetting.ARMOR_TRIM_SALVAGE_COST);
@NotNull
public ActionCost getArmorTrimSalvageCost() {
return getCost(ScrapperSetting.ARMOR_TRIM_SALVAGE_COST, "armor trim salvage");
}
/**
@@ -208,8 +199,46 @@ public class GlobalScrapperSettings implements Settings<ScrapperSetting> {
*
* @return <p>The cost of using a scrapper to remove netherite from an item</p>
*/
public double getNetheriteSalvageCost() {
return asDouble(ScrapperSetting.NETHERITE_SALVAGE_COST);
@NotNull
public ActionCost getNetheriteSalvageCost() {
return getCost(ScrapperSetting.NETHERITE_SALVAGE_COST, "netherite salvage");
}
/**
* Gets the cost of using a scrapper to split an enchanted book
*
* @return <p>The cost of using a scrapper to split an enchanted book</p>
*/
@NotNull
public ActionCost getEnchantedBookSalvageCost() {
return getCost(ScrapperSetting.ENCHANTED_BOOK_SALVAGE_COST, "enchanted book salvage");
}
/**
* Gets the math formula for the increase in salvage cost
*
* @return <p>The salvage cost increase formula</p>
*/
public String getSalvageCostIncrease() {
return asString(ScrapperSetting.SALVAGE_COST_INCREASE);
}
/**
* Gets the math formula for the increase in salvage cooldown
*
* @return <p>The salvage cooldown increase formula</p>
*/
public String getSalvageCooldownIncrease() {
return asString(ScrapperSetting.SALVAGE_COOLDOWN_INCREASE);
}
/**
* Gets whether the enchanted book salvage price should be multiplied with the number of enchantments
*
* @return <p>Whether the enchanted book salvage price should be multiplied with the number of enchantments</p>
*/
public boolean multiplyEnchantedBokSalvagePrice() {
return asBoolean(ScrapperSetting.MULTIPLY_ENCHANTED_BOOK_SALVAGE_COST);
}
/**
@@ -275,4 +304,35 @@ public class GlobalScrapperSettings implements Settings<ScrapperSetting> {
}
}
/**
* Gets the string value of the given setting
*
* @param setting <p>The setting to get the value of</p>
* @return <p>The value of the given setting as a string</p>
*/
@NotNull
private String asString(@NotNull ScrapperSetting setting) {
return getValue(setting).toString();
}
/**
* Gets the action cost from the given scrapper setting
*
* @param setting <p>The setting to get cost from</p>
* @param costType <p>Cost identifier for error message</p>
* @return <p>The cost of the action</p>
*/
@NotNull
private ActionCost getCost(@NotNull ScrapperSetting setting, @NotNull String costType) {
Object value = getValue(setting);
if (value instanceof ActionCost actionCost) {
return actionCost;
} else if (value instanceof Double number) {
return new ActionCost(number);
} else {
BlacksmithPlugin.error("Unable to load " + costType + " cost! Cost set to free!");
return new ActionCost();
}
}
}

View File

@@ -83,6 +83,15 @@ public class ScrapperNPCSettings implements TraitSettings<ScrapperSetting> {
}
}
/**
* Gets whether this scrapper is able to split an enchanted book
*
* @return <p>True if this scrapper is able to split an enchanted book</p>
*/
public boolean splitEnchantedBook() {
return asBoolean(ScrapperSetting.SPLIT_ENCHANTED_BOOK);
}
/**
* Gets the raw current value of a setting
*
@@ -171,6 +180,15 @@ public class ScrapperNPCSettings implements TraitSettings<ScrapperSetting> {
return asString(ScrapperSetting.COST_MESSAGE_NETHERITE);
}
/**
* Gets the message to use for displaying enchanted book salvage cost
*
* @return <p>The message to use for displaying enchanted book salvage cost</p>
*/
public String getEnchantedBookCostMessage() {
return asString(ScrapperSetting.COST_MESSAGE_ENCHANTED_BOOK);
}
@Override
@NotNull
public String getCoolDownUnexpiredMessage() {
@@ -466,4 +484,24 @@ public class ScrapperNPCSettings implements TraitSettings<ScrapperSetting> {
return asString(ScrapperSetting.CANNOT_SALVAGE_NETHERITE_MESSAGE);
}
/**
* Gets the message to display when explaining that this scrapper is unable to salvage enchanted books
*
* @return <p>The message to display when explaining that this scrapper is unable to salvage enchanted books</p>
*/
@NotNull
public String getCannotSalvageEnchantedBookMessage() {
return asString(ScrapperSetting.CANNOT_SALVAGE_ENCHANTED_BOOK_MESSAGE);
}
/**
* Gets the message to display when explaining that the scrapper cannot salvage an enchanted book with a single enchantment
*
* @return <p>The message to display when explaining that the scrapper cannot salvage an enchanted book with a single enchantment</p>
*/
@NotNull
public String getCannotSplitEnchantedBookFurtherMessage() {
return asString(ScrapperSetting.CANNOT_SPLIT_ENCHANTED_BOOK_FURTHER_MESSAGE);
}
}

View File

@@ -91,6 +91,12 @@ public enum ScrapperSetting implements Setting {
*/
SALVAGE_NETHERITE("salvageNetherite", SettingValueType.BOOLEAN, true,
"Whether to enable salvaging of netherite items", true, false),
/**
* The setting for whether the NPC should allow salvaging an enchanted books with several enchantments into several books with one enchantment
*/
SPLIT_ENCHANTED_BOOK("splitEnchantedBook", SettingValueType.BOOLEAN, false, "Whether to enable " +
"splitting of enchanted books", true, false),
/*-----------
| Messages |
@@ -194,6 +200,13 @@ public enum ScrapperSetting implements Setting {
"&eIt will cost &a{cost}&e to salvage that &a{item}&e into diamond!",
"The message to display when explaining the shown item's netherite salvage cost", true, true),
/**
* The message displayed when displaying the cost of salvaging the player's held enchanted book
*/
COST_MESSAGE_ENCHANTED_BOOK("costMessageEnchantedBook", SettingValueType.STRING,
"&eIt will cost &a{cost}&e to salvage that enchanted book!",
"The message to display when explaining the shown enchanted book's salvage cost", true, true),
/**
* The message displayed when explaining that all items will be returned as salvage
*/
@@ -236,6 +249,20 @@ public enum ScrapperSetting implements Setting {
"&cI'm sorry, but I'm unable to salvage netherite items!",
"The message to display when asked to salvage netherite items, and that option is disabled", true, true),
/**
* The message displayed when explaining that enchanted book salvage is disabled
*/
CANNOT_SALVAGE_ENCHANTED_BOOK_MESSAGE("cannotSalvageEnchantedBookMessage", SettingValueType.STRING,
"&cI'm sorry, but I'm unable to salvage enchanted books!",
"The message to display when asked to salvage enchanted books, and the option is disabled", true, true),
/**
* The message displayed when explaining that a player cannot salvage an enchanted book containing a single enchantment
*/
CANNOT_SPLIT_ENCHANTED_BOOK_FURTHER_MESSAGE("cannotSplitEnchantedBookFurtherMessage", SettingValueType.STRING,
"&cI'm sorry, but I cannot salvage that enchanted book any further",
"The message displayed when a player attempts to salvage an enchanted book with a single enchantment", true, true),
/**
* The message displayed when clicking a scrapper with an empty hand
*/
@@ -249,21 +276,47 @@ public enum ScrapperSetting implements Setting {
/**
* The setting for the salvage cost
*/
SALVAGE_COST("salvagePrice", SettingValueType.POSITIVE_DOUBLE, 0,
SALVAGE_COST("salvagePrice", SettingValueType.ADVANCED_COST, 0,
"The cost of using a scrapper to salvage an item", false, false),
/**
* The setting for the armor trim salvage cost
*/
ARMOR_TRIM_SALVAGE_COST("armorTrimSalvagePrice", SettingValueType.POSITIVE_DOUBLE, 5,
ARMOR_TRIM_SALVAGE_COST("armorTrimSalvagePrice", SettingValueType.ADVANCED_COST, 5,
"The cost of using the scrapper to remove armor trim", false, false),
/**
* The setting for the netherite salvage cost
*/
NETHERITE_SALVAGE_COST("netheriteSalvagePrice", SettingValueType.POSITIVE_DOUBLE, 15,
NETHERITE_SALVAGE_COST("netheriteSalvagePrice", SettingValueType.ADVANCED_COST, 15,
"The cost of using the scrapper to remove netherite from an item", false, false),
/**
* The setting for the enchanted book salvage cost
*/
ENCHANTED_BOOK_SALVAGE_COST("enchantedBookSalvagePrice", SettingValueType.ADVANCED_COST, 15,
"The cost of using the scrapper to split an enchanted book", false, false),
/**
* The setting for whether to multiply enchanted book salvage cost
*/
MULTIPLY_ENCHANTED_BOOK_SALVAGE_COST("multiplyEnchantedBookSalvageCost", SettingValueType.BOOLEAN, false,
"Whether to multiply the enchanted book salvage cost with the number of enchantments", false, false),
/**
* The mathematical formula for increasing salvage cost
*/
SALVAGE_COST_INCREASE("salvageCostIncrease", SettingValueType.STRING, "{cost}*{timesUsed}",
"The mathematical formula for salvage cost increase when continually used within the same hour. " +
"This is necessary for some servers where items can be easily farmed. Set to {cost} to disable behavior.", false, false),
/**
* The mathematical formula for increasing salvage cooldown
*/
SALVAGE_COOLDOWN_INCREASE("salvageCooldownIncrease", SettingValueType.STRING, "{cooldown}*{timesUsed}",
"The mathematical formula for salvage cooldown increase when continually used within the same hour. " +
"This is necessary for some servers where items can be easily farmed. Set to {cooldown} to disable behavior.", false, false),
/**
* Whether to display exact time in minutes and seconds when displaying a remaining cool-down
*/

View File

@@ -0,0 +1,274 @@
package net.knarcraft.blacksmith.container;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.manager.EconomyManager;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.NamespacedKey;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
/**
* The cost of performing an action
*
* @param monetaryCost <p>The monetary cost of the action</p>
* @param expCost <p>The experience cost of the action</p>
* @param itemCost <p>The item-based cost of the action</p>
* @param requiredPermissions <p>The permission required for the action</p>
*/
public record ActionCost(double monetaryCost, int expCost, @Nullable ItemStack itemCost,
@NotNull Set<String> requiredPermissions) implements ConfigurationSerializable {
/**
* Instantiates a new empty action cost
*/
public ActionCost() {
this(0, 0, null, Set.of());
}
/**
* Instantiates a new action cost with the specified monetary cost
*
* @param monetaryCost <p>The monetary cost to set</p>
*/
public ActionCost(double monetaryCost) {
this(monetaryCost, 0, null, Set.of());
}
/**
* Changes the monetary cost of this action
*
* @param monetaryCost <p>The new monetary cost</p>
* @return <p>The resulting action cost</p>
*/
public ActionCost changeMonetaryCost(double monetaryCost) {
return new ActionCost(monetaryCost, this.expCost, this.itemCost, this.requiredPermissions);
}
/**
* Changes the experience cost of this action
*
* @param expCost <p>The new experience cost</p>
* @return <p>The resulting action cost</p>
*/
public ActionCost changeExpCost(int expCost) {
return new ActionCost(this.monetaryCost, expCost, this.itemCost, this.requiredPermissions);
}
/**
* Changes the item cost of this action
*
* @param itemCost <p>The new item cost</p>
* @return <p>The resulting action cost</p>
*/
public ActionCost changeItemCost(@Nullable ItemStack itemCost) {
return new ActionCost(this.monetaryCost, this.expCost, itemCost, this.requiredPermissions);
}
/**
* Changes the permission cost of this action
*
* @param requiredPermissions <p>The new permission cost</p>
* @return <p>The resulting action cost</p>
*/
public ActionCost changePermissionCost(@NotNull Set<String> requiredPermissions) {
return new ActionCost(this.monetaryCost, this.expCost, this.itemCost, requiredPermissions);
}
/**
* Gets this action cost's costs as a string
*
* @param player <p>The player to calculate the cost for</p>
* @return <p>The string representation of this action cost</p>
*/
@NotNull
public String getCostString(@NotNull Player player) {
StringBuilder builder = new StringBuilder();
if (monetaryCost > 0) {
builder.append(EconomyManager.format(monetaryCost)).append(", ").append("\n");
}
if (expCost > 0) {
builder.append(expCost).append("exp, ").append("\n");
}
if (itemCost != null) {
NamespacedKey itemName = itemCost.getType().getKeyOrNull();
if (itemName != null) {
builder.append(itemCost.getAmount()).append(" x ").append(itemName);
ItemMeta itemMeta = itemCost.getItemMeta();
if (itemMeta != null && itemMeta.hasDisplayName()) {
builder.append("(").append(itemMeta.getDisplayName()).append(")");
}
if (itemMeta != null && itemMeta.hasLore() && itemMeta.getLore() != null) {
for (String lore : itemMeta.getLore()) {
builder.append("\n").append(lore);
}
}
builder.append("\n");
}
}
if (!requiredPermissions().isEmpty()) {
for (String permission : requiredPermissions()) {
if (player.hasPermission(permission)) {
builder.append(ChatColor.DARK_GREEN).append("O ").append(permission).append("\n");
} else {
builder.append(ChatColor.DARK_RED).append("X ").append(permission).append("\n");
}
}
}
return builder.toString();
}
/**
* Checks whether the given player is able to pay this action cost
*
* @param player <p>The player to check</p>
* @return <p>True if the player is able to pay</p>
*/
public boolean canPay(@NotNull Player player) {
for (String permission : this.requiredPermissions) {
if (!player.hasPermission(permission)) {
return false;
}
}
if (player.getExp() < this.expCost || !EconomyManager.hasEnough(player, this.monetaryCost)) {
return false;
}
return hasEnoughValidItemsInInventory(player);
}
/**
* Takes exp, money and items from the player, according to this cost
*
* @param player <p>The player to take the payment from</p>
*/
public void takePayment(@NotNull Player player) {
player.giveExp(-expCost);
EconomyManager.withdraw(player, monetaryCost);
takeItemCost(player);
}
/**
* Checks whether the given player has enough items specified as the item cost
*
* @param player <p>The player to check</p>
* @return <p>True if the player has enough items in their inventory</p>
*/
private boolean hasEnoughValidItemsInInventory(@NotNull Player player) {
if (this.itemCost == null) {
return true;
}
int amountInInventory = 0;
for (Map.Entry<Integer, Integer> entry : getValidItems(player).entrySet()) {
amountInInventory += entry.getValue();
}
return this.itemCost.getAmount() >= amountInInventory;
}
/**
* Gets all items in a player's inventory equal to the specified item
*
* @param player <p>The player to get valid items for</p>
* @return <p>All valid items in the format: Inventory id -> Amount</p>
*/
@NotNull
private Map<Integer, Integer> getValidItems(@NotNull Player player) {
if (this.itemCost == null) {
return new HashMap<>();
}
ItemMeta targetMeta = this.itemCost.getItemMeta();
String displayName = null;
List<String> lore = null;
if (targetMeta != null) {
displayName = targetMeta.hasDisplayName() ? targetMeta.getDisplayName() : null;
lore = targetMeta.hasLore() ? targetMeta.getLore() : null;
}
Map<Integer, Integer> output = new HashMap<>();
for (Map.Entry<Integer, ? extends ItemStack> entry : player.getInventory().all(this.itemCost.getType()).entrySet()) {
if (targetMeta != null) {
// Only consider item if the name and lore is the same
ItemMeta meta = entry.getValue().getItemMeta();
if (meta == null || (displayName != null && (!meta.hasDisplayName() ||
!meta.getDisplayName().equals(displayName)) || lore != null && (!meta.hasLore() ||
meta.getLore() == null || !meta.getLore().equals(lore)))) {
continue;
}
}
output.put(entry.getKey(), entry.getValue().getAmount());
}
return output;
}
/**
* Takes the amount of items specified as the cost for this action
*
* @param player <p>The player to take the items from</p>
*/
private void takeItemCost(@NotNull Player player) {
if (this.itemCost == null) {
return;
}
int clearedAmount = 0;
for (Map.Entry<Integer, Integer> entry : getValidItems(player).entrySet()) {
int amount = entry.getValue();
if (amount <= this.itemCost.getAmount() - clearedAmount) {
clearedAmount += amount;
player.getInventory().clear(entry.getKey());
} else {
clearedAmount = this.itemCost.getAmount();
ItemStack item = player.getInventory().getItem(entry.getKey());
if (item != null) {
item.setAmount(amount - clearedAmount);
} else {
BlacksmithPlugin.error("An item changed after calculating item cost. Was unable to take " +
amount + " items");
}
}
if (clearedAmount >= this.itemCost.getAmount()) {
break;
}
}
}
/**
* Deserializes an action cost
*
* @param serialized <p>The serialized action cost</p>
* @return <p>The deserialized action cost</p>
*/
@SuppressWarnings({"unused", "unchecked"})
public static ActionCost deserialize(Map<String, Object> serialized) {
double monetaryCost = (double) serialized.get("monetaryCost");
int expCost = (int) serialized.get("expCost");
ItemStack itemCost = (ItemStack) serialized.get("itemCost");
Set<String> requiredPermissions = (Set<String>) serialized.get("requiredPermissions");
return new ActionCost(monetaryCost, expCost, itemCost, requiredPermissions);
}
@Override
public @NotNull Map<String, Object> serialize() {
Map<String, Object> serialized = new HashMap<>();
serialized.put("monetaryCost", Optional.of(this.monetaryCost));
serialized.put("expCost", Optional.of(this.expCost));
serialized.put("itemCost", itemCost);
serialized.put("requiredPermissions", this.requiredPermissions);
return serialized;
}
}

View File

@@ -15,10 +15,10 @@ public enum ItemType {
*
* @return <p>The name of this item type</p>
*/
public String getItemTypeName() {
public FormatBuilder getItemTypeName() {
return switch (this) {
case MATERIAL -> new FormatBuilder(Translatable.ITEM_TYPE_MATERIAL).toString();
case ENCHANTMENT -> new FormatBuilder(Translatable.ITEM_TYPE_ENCHANTMENT).toString();
case MATERIAL -> new FormatBuilder(Translatable.ITEM_TYPE_MATERIAL);
case ENCHANTMENT -> new FormatBuilder(Translatable.ITEM_TYPE_ENCHANTMENT);
};
}

View File

@@ -22,8 +22,8 @@ public final class NPCFormatter {
* @param message <p>The message to send</p>
*/
public static void sendNPCMessage(@NotNull NPC npc, @NotNull Player player, @NotNull String message) {
player.sendMessage(new FormatBuilder(Translatable.NPC_TALK_FORMAT).replace("{npc}",
npc.getName()).replace("{message}", message).color().toString());
new FormatBuilder(Translatable.NPC_TALK_FORMAT).replace("{npc}",
npc.getName()).replace("{message}", message).displayColored(player);
}
/**

View File

@@ -140,7 +140,12 @@ public enum Translatable implements TranslatableMessage {
/**
* The format to use for formatting any message spoken by a blacksmith NPC
*/
NPC_TALK_FORMAT;
NPC_TALK_FORMAT,
/**
* The text to display when explaining that a cost must be a positive double
*/
DOUBLE_COST_REQUIRED;
/**
* Gets the message to display when displaying the raw value of messages
@@ -150,7 +155,7 @@ public enum Translatable implements TranslatableMessage {
*/
@NotNull
public static FormatBuilder getRawValueMessage(@NotNull String rawValue) {
return new FormatBuilder(Translatable.RAW_VALUE).replace("{rawValue}", rawValue);
return new FormatBuilder(Translatable.RAW_VALUE).color().replace("{rawValue}", rawValue);
}
/**

View File

@@ -3,9 +3,14 @@ package net.knarcraft.blacksmith.manager;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.config.blacksmith.GlobalBlacksmithSettings;
import net.knarcraft.blacksmith.config.scrapper.GlobalScrapperSettings;
import net.knarcraft.blacksmith.container.ActionCost;
import net.knarcraft.blacksmith.property.SalvageMethod;
import net.knarcraft.blacksmith.util.ItemHelper;
import net.knarcraft.blacksmith.util.SalvageHelper;
import net.knarcraft.knarlib.formatting.StringReplacer;
import net.milkbowl.vault.economy.Economy;
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import org.bukkit.Material;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
@@ -54,10 +59,12 @@ public class EconomyManager {
*
* @param player <p>The player holding an item</p>
* @param salvageMethod <p>The salvage method to check</p>
* @param item <p>The item to be salvaged</p>
* @return <p>Whether the player cannot pay for the salvage</p>
*/
public static boolean cannotPayForSalvage(@NotNull Player player, @NotNull SalvageMethod salvageMethod) {
return economy.getBalance(player) - getSalvageCost(salvageMethod) < 0;
public static boolean cannotPayForSalvage(@NotNull Player player, @NotNull SalvageMethod salvageMethod,
@NotNull ItemStack item) {
return !getSalvageCost(salvageMethod, item, player).canPay(player);
}
/**
@@ -76,26 +83,63 @@ public class EconomyManager {
* Gets the human-readable cost of salvaging an item
*
* @param salvageMethod <p>The salvage method to get the cost for</p>
* @param item <p>The item to be salvaged</p>
* @param player <p>The player to provide the cost to</p>
* @return <p>The formatted cost</p>
*/
@NotNull
public static String formatSalvageCost(@NotNull SalvageMethod salvageMethod) {
return economy.format(getSalvageCost(salvageMethod));
public static String formatSalvageCost(@NotNull SalvageMethod salvageMethod, @NotNull ItemStack item,
@NotNull Player player) {
return getSalvageCost(salvageMethod, item, player).getCostString(player);
}
/**
* Gets whether the given player has enough money to pay the given cost
*
* @param player <p>The player to check</p>
* @param cost <p>The required cost</p>
* @return <p>True if the player has enough money to cover the cost</p>
*/
public static boolean hasEnough(@NotNull Player player, double cost) {
return economy.getBalance(player) >= cost;
}
/**
* Formats a number as an economy cost
*
* @param cost <p>The cost to format</p>
* @return <p>The formatted cost</p>
*/
@NotNull
public static String format(double cost) {
return economy.format(cost);
}
/**
* Gets the cost of salvaging using the specified method
*
* @param salvageMethod <p>The salvage method to get cost for</p>
* @param item <p>The item to be salvaged</p>
* @return <p>The salvage cost</p>
*/
private static double getSalvageCost(@NotNull SalvageMethod salvageMethod) {
@NotNull
private static ActionCost getSalvageCost(@NotNull SalvageMethod salvageMethod, @NotNull ItemStack item,
@NotNull Player player) {
GlobalScrapperSettings settings = BlacksmithPlugin.getInstance().getGlobalScrapperSettings();
return switch (salvageMethod) {
ActionCost cost = switch (salvageMethod) {
case SALVAGE, EXTENDED_SALVAGE -> settings.getSalvageCost();
case NETHERITE -> settings.getNetheriteSalvageCost();
case ARMOR_TRIM -> settings.getArmorTrimSalvageCost();
case ENCHANTED_BOOK -> getEnchantedBookSalvageCost(item);
};
StringReplacer replacer = new StringReplacer(settings.getSalvageCostIncrease());
replacer.add("{cost}", String.valueOf(cost.monetaryCost()));
replacer.add("{timesUsed}", String.valueOf(PlayerUsageManager.getUsages(player,
System.currentTimeMillis() - 3600000)));
Expression expression = new ExpressionBuilder(replacer.replace()).build();
cost.changeMonetaryCost(Math.max(cost.monetaryCost(), expression.evaluate()));
return cost;
}
/**
@@ -118,13 +162,30 @@ public class EconomyManager {
* @param player <p>The player to withdraw from</p>
* @param salvageMethod <p>The salvage method to withdraw for</p>
*/
public static void withdrawScrapper(@NotNull Player player, @NotNull SalvageMethod salvageMethod) {
double cost = getSalvageCost(salvageMethod);
if (cost > 0) {
economy.withdrawPlayer(player, cost);
public static boolean withdrawScrapper(@NotNull Player player, @NotNull SalvageMethod salvageMethod) {
ActionCost cost = getSalvageCost(salvageMethod, player.getInventory().getItemInMainHand(), player);
if (cost.canPay(player)) {
cost.takePayment(player);
return true;
} else {
return false;
}
}
/**
* Withdraws money from a player
*
* @param player <p>The player to withdraw from</p>
* @param monetaryCost <p>The cost to withdraw</p>
* @throws IllegalArgumentException <p>If a negative cost is given</p>
*/
public static void withdraw(@NotNull Player player, double monetaryCost) {
if (monetaryCost < 0) {
throw new IllegalArgumentException("Cannot withdraw a negative amount");
}
economy.withdrawPlayer(player, monetaryCost);
}
/**
* Gets the cost of the item in the given player's main hand
*
@@ -183,6 +244,23 @@ public class EconomyManager {
return price;
}
/**
* Gets the cost of scrapping an enchanted book
*
* @param item <p>The enchanted book to calculate cost for</p>
* @return <p>The cost of scrapping the enchanted book</p>
*/
@NotNull
private static ActionCost getEnchantedBookSalvageCost(@NotNull ItemStack item) {
GlobalScrapperSettings settings = BlacksmithPlugin.getInstance().getGlobalScrapperSettings();
ActionCost cost = settings.getEnchantedBookSalvageCost();
if (settings.multiplyEnchantedBokSalvagePrice()) {
cost.changeMonetaryCost(cost.monetaryCost() * Math.max(SalvageHelper.getEnchantmentCount(item) - 1, 1));
}
return cost;
}
/**
* Sets up Vault for economy
*

View File

@@ -0,0 +1,75 @@
package net.knarcraft.blacksmith.manager;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* A manager for keeping track of players using scrappers
*/
public class PlayerUsageManager {
private static final Map<Player, Set<Long>> playerUsages = new HashMap<>();
/**
* Register that a player has used a scrapper
*
* @param player <p>The player using a scrapper</p>
*/
public static void registerUsage(@NotNull Player player) {
playerUsages.putIfAbsent(player, new HashSet<>());
Set<Long> usages = playerUsages.get(player);
usages.add(System.currentTimeMillis());
}
/**
* Gets how many times the given player has used a scrapper after the given timestamp
*
* @param player <p>The player to check usages for</p>
* @param afterTimestamp <p>The timestamp of when to start looking for usages</p>
* @return <p>The number of scrapper uses for the given player</p>
*/
public static int getUsages(@NotNull Player player, @NotNull Long afterTimestamp) {
Set<Long> usages = playerUsages.get(player);
if (usages == null) {
return 0;
}
int uses = 0;
Set<Long> expired = new HashSet<>();
for (Long usageTime : usages) {
if (usageTime < afterTimestamp) {
expired.add(usageTime);
} else {
uses++;
}
}
// Remove expired data
usages.removeAll(expired);
return uses;
}
/**
* Removes all expired timestamps
*
* @param afterTimestamp <p>The timestamp for the earliest time of valid timestamps</p>
*/
public static void removeExpiredData(@NotNull Long afterTimestamp) {
for (Set<Long> usageSet : playerUsages.values()) {
Set<Long> expired = new HashSet<>();
for (Long item : usageSet) {
if (item < afterTimestamp) {
expired.add(item);
}
}
usageSet.removeAll(expired);
}
}
}

View File

@@ -0,0 +1,43 @@
package net.knarcraft.blacksmith.property;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* An action for modifying cost
*/
public enum CostModifyAction {
/**
* Adds some cost to the existing cost, effectively modifying the total cost
*/
ADD,
/**
* Clears the cost completely
*/
CLEAR,
/**
* Sets the cost to the new cost, clearing any other existing costs
*/
SET,
;
/**
* Gets the action specified by the given string
*
* @param input <p>The input string to parse</p>
* @return <p>The action, or null if not matched</p>
*/
@Nullable
public static CostModifyAction fromString(@NotNull String input) {
for (CostModifyAction action : CostModifyAction.values()) {
if (input.toUpperCase().equals(action.name())) {
return action;
}
}
return null;
}
}

View File

@@ -0,0 +1,49 @@
package net.knarcraft.blacksmith.property;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* The type of costs applicable to an advanced cost configuration
*/
public enum CostType {
/**
* A monetary cost from an economy plugin
*/
ECONOMY,
/**
* A permission requirement, rather than a cost
*/
PERMISSION,
/**
* In-game experience levels
*/
EXP,
/**
* A specific item is consumed
*/
ITEM,
;
/**
* Parses a text string denoting a cost type
*
* @param input <p>The input to parse</p>
* @return <p>The parsed cost type, or null if not matched</p>
*/
@Nullable
public static CostType parse(@NotNull String input) {
String normalized = input.trim().toUpperCase();
for (CostType costType : CostType.values()) {
if (normalized.equals(costType.name())) {
return costType;
}
}
return null;
}
}

View File

@@ -25,4 +25,9 @@ public enum SalvageMethod {
*/
NETHERITE,
/**
* Splitting enchantments of an enchanted book
*/
ENCHANTED_BOOK,
}

View File

@@ -80,9 +80,8 @@ public class BlacksmithTrait extends CustomTrait<BlacksmithSetting> {
(!reforgeAbleItems.isEmpty() && !reforgeAbleItems.contains(hand.getType()));
if (notHoldingAnvil && notHoldingRepairable) {
String invalidMessage = new FormatBuilder(config.getInvalidItemMessage()).replace("{title}",
config.getBlacksmithTitle()).toString();
NPCFormatter.sendNPCMessage(this.npc, player, invalidMessage);
NPCFormatter.sendNPCMessage(this.npc, player, new FormatBuilder(config.getInvalidItemMessage()).
replace("{title}", config.getBlacksmithTitle()));
return;
}

View File

@@ -187,12 +187,14 @@ public abstract class CustomTrait<K extends Setting> extends Trait {
if (isBlacksmith) {
EconomyManager.withdrawBlacksmith(player);
} else if (this.session instanceof SalvageSession salvageSession) {
EconomyManager.withdrawScrapper(player, salvageSession.salvageMethod);
}
if (!EconomyManager.withdrawScrapper(player, salvageSession.salvageMethod)) {
return;
}
session.scheduleAction();
}
PlayerInventory playerInventory = player.getInventory();
ItemStack heldItem = player.getInventory().getItemInMainHand();
session.scheduleAction();
//Display the item in the NPC's hand
Entity entity = npc.getEntity();

View File

@@ -59,6 +59,8 @@ public class ReforgeSession extends Session implements Runnable {
enchantments = new ArrayList<>();
for (Enchantment enchantment : enchantmentRegistry) {
// Note: While depreciated in Spigot, the new method is not available for Paper
//noinspection deprecation
enchantments.add(enchantment.getKey().getKey());
}
}

View File

@@ -7,9 +7,13 @@ import net.knarcraft.blacksmith.event.ScrapperSalvageFailEvent;
import net.knarcraft.blacksmith.event.ScrapperSalvageSucceedEvent;
import net.knarcraft.blacksmith.formatting.NPCFormatter;
import net.knarcraft.blacksmith.manager.EconomyManager;
import net.knarcraft.blacksmith.manager.PlayerUsageManager;
import net.knarcraft.blacksmith.property.SalvageMethod;
import net.knarcraft.blacksmith.util.ItemHelper;
import net.knarcraft.blacksmith.util.SalvageHelper;
import net.knarcraft.knarlib.formatting.StringReplacer;
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import org.bukkit.Material;
import org.bukkit.Sound;
import org.bukkit.entity.LivingEntity;
@@ -69,7 +73,7 @@ public class SalvageSession extends Session implements Runnable {
return true;
}
if (EconomyManager.cannotPayForSalvage(this.player, this.salvageMethod)) {
if (EconomyManager.cannotPayForSalvage(this.player, this.salvageMethod, this.itemToSalvage)) {
NPCFormatter.sendNPCMessage(this.npc, this.player, this.config.getInsufficientFundsMessage());
return true;
}
@@ -93,6 +97,7 @@ public class SalvageSession extends Session implements Runnable {
case EXTENDED_SALVAGE -> Material.CRAFTING_TABLE;
case SALVAGE -> Material.ANVIL;
case NETHERITE, ARMOR_TRIM -> Material.SMITHING_TABLE;
case ENCHANTED_BOOK -> Material.ENCHANTING_TABLE;
};
}
@@ -120,8 +125,14 @@ public class SalvageSession extends Session implements Runnable {
// Start cool-down
Calendar wait = Calendar.getInstance();
wait.add(Calendar.SECOND, this.config.getSalvageCoolDown());
StringReplacer replacer = new StringReplacer(BlacksmithPlugin.getInstance().getGlobalScrapperSettings().getSalvageCooldownIncrease());
replacer.add("{cooldown}", String.valueOf(this.config.getSalvageCoolDown()));
replacer.add("{timesUsed}", String.valueOf(PlayerUsageManager.getUsages(player, System.currentTimeMillis() - 3600000)));
Expression expression = new ExpressionBuilder(replacer.replace()).build();
wait.add(Calendar.SECOND, Math.max((int) expression.evaluate(), this.config.getSalvageCoolDown()));
this.scrapperTrait.addCoolDown(this.player.getUniqueId(), wait);
PlayerUsageManager.registerUsage(this.player);
}
/**

View File

@@ -21,6 +21,7 @@ import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ArmorMeta;
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -116,7 +117,7 @@ public class ScrapperTrait extends CustomTrait<ScrapperSetting> {
return;
}
// Print the cost to the player
printCostMessage(player, itemInHand, EconomyManager.formatSalvageCost(result.salvageMethod()),
printCostMessage(player, itemInHand, EconomyManager.formatSalvageCost(result.salvageMethod(), itemInHand, player),
result.salvageMethod());
}
@@ -144,6 +145,13 @@ public class ScrapperTrait extends CustomTrait<ScrapperSetting> {
return result;
}
result = isEnchantedBookSalvage(player, itemInHand);
if (result.salvageState() == SalvageState.NO_SALVAGE) {
return null;
} else if (result.salvageState() == SalvageState.FOUND_SALVAGE) {
return result;
}
result = isNormalSalvage(player, itemInHand, extended);
if (result.salvageState() == SalvageState.NO_SALVAGE) {
return null;
@@ -173,7 +181,7 @@ public class ScrapperTrait extends CustomTrait<ScrapperSetting> {
return new SalvageResult(SalvageMethod.SALVAGE, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
}
// Check if the item is enchanted, and whether this blacksmith can salvage it
// Check if the item is enchanted, and whether this scrapper can salvage it
if (!itemInHand.getEnchantments().isEmpty() && !getSettings().salvageEnchanted()) {
NPCFormatter.sendNPCMessage(this.npc, player, getSettings().getCannotSalvageEnchantedMessage());
return new SalvageResult(SalvageMethod.SALVAGE, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
@@ -192,6 +200,37 @@ public class ScrapperTrait extends CustomTrait<ScrapperSetting> {
}
}
/**
* Gets the result of trying to salvage the item as an enchanted book
*
* @param player <p>The player trying to salvage the item</p>
* @param itemInHand <p>The item to be salvaged</p>
* @return <p>The result of attempting the salvage</p>
*/
private SalvageResult isEnchantedBookSalvage(@NotNull Player player, @NotNull ItemStack itemInHand) {
if (itemInHand.getType() != Material.ENCHANTED_BOOK ||
!(itemInHand.getItemMeta() instanceof EnchantmentStorageMeta)) {
return new SalvageResult(SalvageMethod.ENCHANTED_BOOK, new ArrayList<>(), SalvageState.INCORRECT_METHOD, 0);
}
if (!getSettings().splitEnchantedBook()) {
NPCFormatter.sendNPCMessage(this.npc, player, getSettings().getCannotSalvageEnchantedBookMessage());
return new SalvageResult(SalvageMethod.ENCHANTED_BOOK, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
}
if (SalvageHelper.getEnchantmentCount(itemInHand) <= 1) {
NPCFormatter.sendNPCMessage(this.npc, player, getSettings().getCannotSplitEnchantedBookFurtherMessage());
return new SalvageResult(SalvageMethod.ENCHANTED_BOOK, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
}
List<ItemStack> salvage = SalvageHelper.getEnchantedBookSalvage(itemInHand);
boolean noUsefulSalvage = salvage == null || salvage.isEmpty();
if (noUsefulSalvage) {
return new SalvageResult(SalvageMethod.ENCHANTED_BOOK, new ArrayList<>(), SalvageState.NO_SALVAGE, 0);
}
return new SalvageResult(SalvageMethod.ENCHANTED_BOOK, salvage, SalvageState.FOUND_SALVAGE, 1);
}
/**
* Gets the result of trying to salvage the item as a netherite applied item
*
@@ -271,6 +310,8 @@ public class ScrapperTrait extends CustomTrait<ScrapperSetting> {
NPCFormatter.sendNPCMessage(this.npc, player, replacer.replace(getSettings().getCostMessage()));
} else if (salvageMethod == SalvageMethod.NETHERITE) {
NPCFormatter.sendNPCMessage(this.npc, player, replacer.replace(getSettings().getNetheriteCostMessage()));
} else if (salvageMethod == SalvageMethod.ENCHANTED_BOOK) {
NPCFormatter.sendNPCMessage(this.npc, player, replacer.replace(getSettings().getEnchantedBookCostMessage()));
} else {
BlacksmithPlugin.error("Unrecognized salvage method " + salvageMethod);
}
@@ -290,8 +331,8 @@ public class ScrapperTrait extends CustomTrait<ScrapperSetting> {
* @return <p>True if the item can be theoretically salvaged</p>
*/
private boolean canBeSalvaged(@NotNull ItemStack item, @NotNull List<Material> salvageAbleItems, boolean extended) {
return (extended || ItemHelper.isRepairable(item)) &&
(salvageAbleItems.isEmpty() || salvageAbleItems.contains(item.getType()));
return item.getType() == Material.ENCHANTED_BOOK || ((extended || ItemHelper.isRepairable(item)) &&
(salvageAbleItems.isEmpty() || salvageAbleItems.contains(item.getType())));
}
/**

View File

@@ -5,7 +5,6 @@ import net.knarcraft.blacksmith.config.SettingValueType;
import net.knarcraft.blacksmith.config.Settings;
import net.knarcraft.blacksmith.formatting.Translatable;
import net.knarcraft.knarlib.formatting.FormatBuilder;
import net.md_5.bungee.api.ChatColor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
@@ -27,19 +26,20 @@ public final class ConfigCommandHelper {
/**
* Changes the value of the setting defined in the user's input
*
* @param args <p>The arguments given by the user</p>
* @param arguments <p>The arguments given by the user</p>
* @param detectedSetting <p>The setting recognized from the input, if any</p>
* @param settings <p>The global settings object to get settings from</p>
* @param sender <p>The command sender to display any output to</p>
*/
public static <K extends Setting> void changeValue(@NotNull String[] args,
public static <K extends Setting> void changeValue(@NotNull String[] arguments,
@NotNull K detectedSetting,
@NotNull Settings<K> settings,
@NotNull CommandSender sender) {
String newValue = args[1];
// TODO: Account for action cost
String newValue = arguments[1];
//This makes sure all arguments are treated as a sentence
if (detectedSetting.getValueType() == SettingValueType.STRING) {
newValue = String.join(" ", Arrays.asList(args).subList(1, args.length));
newValue = String.join(" ", Arrays.asList(arguments).subList(1, arguments.length));
}
settings.changeValue(detectedSetting, newValue);
getValueChangedMessage(detectedSetting.getCommandName(), newValue).success(sender);
@@ -77,7 +77,7 @@ public final class ConfigCommandHelper {
// Display the current value of the setting
getCurrentValueMessage(setting.getCommandName(), currentValue).neutral(sender);
if (printRawValue) {
if (!currentValue.equals(defaultValue) && printRawValue) {
displayRaw(sender, currentValue);
}
}
@@ -89,7 +89,7 @@ public final class ConfigCommandHelper {
* @param value <p>The value to display raw</p>
*/
public static void displayRaw(@NotNull CommandSender sender, @NotNull String value) {
sender.sendMessage(Translatable.getRawValueMessage(value.replace(ChatColor.COLOR_CHAR, '&')).toString());
new FormatBuilder(Translatable.RAW_VALUE).color().replace("{rawValue}", value).displayRaw(sender);
}
}

View File

@@ -0,0 +1,215 @@
package net.knarcraft.blacksmith.util;
import net.knarcraft.blacksmith.container.ActionCost;
import net.knarcraft.blacksmith.formatting.Translatable;
import net.knarcraft.blacksmith.property.CostModifyAction;
import net.knarcraft.blacksmith.property.CostType;
import net.knarcraft.knarlib.formatting.FormatBuilder;
import net.knarcraft.knarlib.util.TabCompletionHelper;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* A helper class for setting action costs
*/
public final class CostHelper {
private CostHelper() {
}
/**
* Parses a simple double cost
*
* @param commandSender <p>The command sender to notify of any problems</p>
* @param costString <p>The string to parse into a cost</p>
* @return <p>The parsed cost, or null if not a valid number</p>
*/
@Nullable
public static Double parseSimpleCost(@NotNull CommandSender commandSender,
@NotNull String costString) throws IllegalArgumentException {
try {
double cost = Double.parseDouble(costString);
if (cost < 0) {
throw new NumberFormatException();
}
return cost;
} catch (NumberFormatException exception) {
new FormatBuilder(Translatable.DOUBLE_COST_REQUIRED).error(commandSender);
return null;
}
}
/**
* Shows tab-completion actions for altering a cost
*
* @param arguments <p>The arguments given by the user</p>
* @return <p>The available tab-complete options</p>
*/
public static @Nullable List<String> tabCompleteCost(@NotNull String[] arguments) {
if (arguments.length == 1) {
return List.of("simple", "advanced");
} else if (arguments.length == 2) {
if (arguments[0].equalsIgnoreCase("simple")) {
return List.of();
} else {
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionHelper.getEnumList(CostModifyAction.class), arguments[1]);
}
} else if (arguments.length == 3) {
if (arguments[0].equalsIgnoreCase("simple")) {
return List.of();
} else {
return TabCompletionHelper.filterMatchingStartsWith(TabCompletionHelper.getEnumList(CostType.class), arguments[2]);
}
} else if (arguments.length == 4) {
CostType costType = CostType.parse(arguments[2]);
if (costType == null) {
return null;
}
return switch (costType) {
case PERMISSION -> TabCompletionHelper.tabCompletePermission(arguments[3]);
case ECONOMY -> List.of("0.5", "1", "10");
case EXP -> List.of("1, 5, 10");
case ITEM -> List.of();
};
}
return List.of();
}
/**
* Modifies the cost of an action
*
* @param costAction <p>The action to perform on the cost</p>
* @param costTypeName <p>The name of the cost to modify, or null if clearing costs</p>
* @param costValue <p>The value of the new cost, or null if clearing costs</p>
* @param oldCost <p>The previous cost of the action</p>
* @param player <p>The player attempting to alter the action cost</p>
* @return <p>The modified action cost, or null if something went wrong</p>
*/
@Nullable
public static ActionCost modifyActionCost(@NotNull String costAction, @Nullable String costTypeName,
@Nullable String costValue, @NotNull ActionCost oldCost,
@NotNull Player player) {
CostType costType;
if (costTypeName != null) {
costType = CostType.parse(costTypeName);
} else {
costType = null;
}
CostModifyAction action = CostModifyAction.fromString(costAction);
if (action == null) {
return null;
}
if (action != CostModifyAction.CLEAR && (costType == null || costValue == null)) {
player.sendMessage("Invalid cost type or value specified!");
return null;
}
return switch (action) {
case CLEAR -> clearActionCost(costType, oldCost);
case ADD -> addActionCost(costType, costValue, oldCost, player);
case SET -> setActionCost(costType, costValue, player);
};
}
/**
* Sets the cost to the specified value
*
* @param costType <p>The type of cost to set</p>
* @param costValue <p>The value of the new cost</p>
* @param player <p>The player to alert if the cost cannot be properly set</p>
* @return <p>The new action cost</p>
*/
public static ActionCost setActionCost(@NotNull CostType costType, @NotNull String costValue,
@NotNull Player player) {
return parseCost(costType, costValue, new ActionCost(), player);
}
/**
* Modifies a value for the specified action cost
*
* @param costType <p>The type of cost to modify</p>
* @param costValue <p>The new value of the cost</p>
* @param oldCost <p>The cost to modify</p>
* @param player <p>The player to alert if the cost cannot be properly set</p>
* @return <p>The new action cost</p>
*/
public static ActionCost addActionCost(@NotNull CostType costType, @NotNull String costValue,
@NotNull ActionCost oldCost, @NotNull Player player) {
return parseCost(costType, costValue, oldCost, player);
}
/**
* Clears an action cost
*
* @param costType <p>The type of action cost to clear, or null if clearing all action costs</p>
* @param oldCost <p>The old cost to modify, or null if clearing all action costs</p>
* @return <p>The modified cost</p>
*/
public static ActionCost clearActionCost(@Nullable CostType costType, @Nullable ActionCost oldCost) {
// Clears one or all fields of a cost
if (costType == null || oldCost == null) {
return new ActionCost();
} else {
return switch (costType) {
case EXP -> oldCost.changeExpCost(0);
case ITEM -> oldCost.changeItemCost(null);
case ECONOMY -> oldCost.changeMonetaryCost(0);
case PERMISSION -> oldCost.changePermissionCost(Set.of());
};
}
}
/**
* Parses an advanced action cost
*
* @param costType <p>The type of cost to parse</p>
* @param value <p>The value to parse</p>
* @param oldCost <p>The old cost to modify</p>
* @param player <p>The player changing the value</p>
* @return <p>The new action cost, or null if the process failed.</p>
*/
@Nullable
private static ActionCost parseCost(@NotNull CostType costType, @NotNull String value, @NotNull ActionCost oldCost,
@NotNull Player player) {
switch (costType) {
case ECONOMY:
double economyCost = Double.parseDouble(value);
if (economyCost < 0) {
new FormatBuilder("Cost cannot be negative!").error(player);
return null;
}
return oldCost.changeMonetaryCost(economyCost);
case ITEM:
ItemStack itemCost = player.getInventory().getItemInMainHand();
if (itemCost.getType().isAir()) {
new FormatBuilder("You have no item in your main hand").error(player);
return null;
}
return oldCost.changeItemCost(itemCost);
case PERMISSION:
Set<String> permissionSet = new HashSet<>(Arrays.asList(value.split(",")));
return oldCost.changePermissionCost(permissionSet);
case EXP:
int expCost = Integer.parseInt(value);
if (expCost < 0) {
new FormatBuilder("Exp cost cannot be negative!").error(player);
return null;
}
return oldCost.changeExpCost(expCost);
default:
return null;
}
}
}

View File

@@ -2,6 +2,7 @@ package net.knarcraft.blacksmith.util;
import net.knarcraft.blacksmith.BlacksmithPlugin;
import net.knarcraft.blacksmith.container.RecipeResult;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.Server;
import org.bukkit.enchantments.Enchantment;
@@ -10,6 +11,8 @@ import org.bukkit.inventory.Recipe;
import org.bukkit.inventory.ShapedRecipe;
import org.bukkit.inventory.ShapelessRecipe;
import org.bukkit.inventory.meta.ArmorMeta;
import org.bukkit.inventory.meta.EnchantmentStorageMeta;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.trim.ArmorTrim;
import org.bukkit.inventory.meta.trim.TrimMaterial;
import org.jetbrains.annotations.NotNull;
@@ -44,6 +47,51 @@ public final class SalvageHelper {
trimMaterialToMaterial.put(TrimMaterial.REDSTONE, Material.REDSTONE);
}
/**
* Gets salvage for an enchanted book
*
* @param itemStack <p>The enchanted book(s) to salvage</p>
* @return <p>The resulting salvage</p>
* @throws RuntimeException <p>If generated enchanted book metadata is null</p>
*/
@Nullable
public static List<ItemStack> getEnchantedBookSalvage(@NotNull ItemStack itemStack) {
ItemMeta meta = itemStack.getItemMeta();
List<ItemStack> output = new ArrayList<>();
if (!(meta instanceof EnchantmentStorageMeta enchantmentStorageMeta)) {
return null;
}
Map<Enchantment, Integer> enchantmentMap = enchantmentStorageMeta.getStoredEnchants();
for (Map.Entry<Enchantment, Integer> enchantmentEntry : enchantmentMap.entrySet()) {
EnchantmentStorageMeta enchantmentMeta = (EnchantmentStorageMeta) Bukkit.getServer().getItemFactory().getItemMeta(Material.ENCHANTED_BOOK);
if (enchantmentMeta == null) {
throw new RuntimeException("Unable to create enchanted book metadata.");
}
enchantmentMeta.addStoredEnchant(enchantmentEntry.getKey(), enchantmentEntry.getValue(), true);
ItemStack newBook = new ItemStack(Material.ENCHANTED_BOOK, 1);
newBook.setItemMeta(enchantmentMeta);
output.add(newBook);
}
return output;
}
/**
* Gets the number of enchantments applied to an enchanted book
*
* @param itemStack <p>The enchanted book to check</p>
* @return <p>The number of enchantments, or -1 if not an enchanted book</p>
*/
public static int getEnchantmentCount(@NotNull ItemStack itemStack) {
ItemMeta meta = itemStack.getItemMeta();
if (!(meta instanceof EnchantmentStorageMeta enchantmentStorageMeta)) {
return -1;
}
return enchantmentStorageMeta.getStoredEnchants().size();
}
/**
* Gets salvage for a given netherite item
*
@@ -88,6 +136,8 @@ public final class SalvageHelper {
} else {
return null;
}
// Note: While depreciated in Spigot, the new method is not available for Paper
@SuppressWarnings("deprecation")
Material patternMaterial = Material.matchMaterial(armorTrim.getPattern().getKey().getKey() +
"_ARMOR_TRIM_SMITHING_TEMPLATE");
if (patternMaterial != null) {

View File

@@ -38,6 +38,8 @@ public final class TabCompleteValuesHelper {
case MATERIAL -> getAllReforgeAbleMaterialNames();
case ENCHANTMENT, ENCHANTMENT_LIST -> getAllEnchantments();
case STRING_LIST -> List.of("*_SHOVEL,*_PICKAXE,*_AXE,*_HOE,*_SWORD:STICK,SMITHING_TABLE:*_PLANKS");
// TODO: Change this to something that makes sense
case ADVANCED_COST -> List.of();
};
}
@@ -77,6 +79,8 @@ public final class TabCompleteValuesHelper {
List<String> enchantments = new ArrayList<>();
for (Enchantment enchantment : enchantmentRegistry) {
// Note: While depreciated in Spigot, the new method is not available for Paper
//noinspection deprecation
enchantments.add(enchantment.getKey().getKey());
}
return enchantments;

View File

@@ -36,7 +36,8 @@ public final class TypeValidationHelper {
case STRING -> isNonEmptyString(value, sender);
case POSITIVE_INTEGER -> isPositiveInteger(value, sender);
case PERCENTAGE -> isPercentage(value, sender);
case BOOLEAN -> true;
// TODO: Implement proper advanced cost checking
case BOOLEAN, ADVANCED_COST -> true;
case REFORGE_ABLE_ITEMS, STRING_LIST -> isStringList(value, sender);
case MATERIAL -> isMaterial(value, sender);
case ENCHANTMENT -> isEnchantment(value, sender);

View File

@@ -141,6 +141,20 @@ scrapper:
# The cost of using the scrapper to remove netherite from an item
netheriteSalvagePrice: 15
# The cost of splitting an enchanted book into several books
enchantedBookSalvagePrice: 15
# The mathematical formula for salvage cost increase when continually used within the same hour. This is necessary
# for some servers where items can be easily farmed. Set to {cost} to disable behavior.
salvageCostIncrease: "{cost}*{timesUsed}"
# The mathematical formula for salvage cooldown increase when continually used within the same hour. This is
# necessary for some servers where items can be easily farmed. Set to {cooldown} to disable behavior.
salvageCooldownIncrease: "{cooldown}*{timesUsed}"
# Whether to multiply the enchanted book salvage price with the number of enchantments on the book
multiplyEnchantedBookSalvageCost: false
# The settings which are set to any new scrapper NPC. To change any of these settings for an existing NPC, you must
# change the Citizens NPC file, or use the /scrapper command
@@ -182,6 +196,9 @@ scrapper:
# Whether to enable salvaging of netherite items
salvageNetherite: true
# Whether to enable salvaging an enchanted books with several enchantments into several books with one enchantment
splitEnchantedBook: false
# Default values for messages used by NPCs
messages:
# The message to display when another player is using the scrapper
@@ -226,6 +243,9 @@ scrapper:
# The message to display when explaining the shown item's netherite salvage cost
costMessageNetherite: "&eIt will cost &a{cost}&e to salvage that &a{item}&e into diamond!"
# The message to display when explaining the shown enchanted book's salvage cost
costMessageEnchantedBook: "&eIt will cost &a{cost}&e to salvage that enchanted book!"
# The yield message to display if trying to salvage a non-damaged item
fullSalvageMessage: "&aI should be able to extract all components from that pristine item.&r"
@@ -244,5 +264,11 @@ scrapper:
# The message to display when asked to salvage netherite items, and that option is disabled
cannotSalvageNetheriteMessage: "&cI'm sorry, but I'm unable to salvage netherite items!"
# The message displayed when explaining that enchanted book salvage is disabled
cannotSalvageEnchantedBookMessage: "&cI'm sorry, but I'm unable to salvage enchanted books!"
# The message displayed when a player attempts to salvage an enchanted book with a single enchantment
cannotSplitEnchantedBookFurtherMessage: "&cI'm sorry, but I cannot salvage that enchanted book any further"
# The message to display when a scrapper is clicked with an empty hand
noItemMessage: "Please present the item you want to salvage"

View File

@@ -43,7 +43,7 @@ en:
# The format used when displaying any duration remaining
DURATION_FORMAT: "in {time} {unit}"
# The format used when NPCs talk to players ({npc} = The NPC's name, {message} is the actual message contents)
NPC_TALK_FORMAT: "&a[{npc}] -> You:&r {message}"
NPC_TALK_FORMAT: "&a[{npc}&r&a] -> You:&r {message}"
# Translation of the duration of less than a second
UNIT_NOW: "imminently"
# Translation of seconds in singular form
@@ -89,4 +89,6 @@ en:
# The text shown if the player has to wait for more than 5 minutes
INTERVAL_MORE_THAN_5_MINUTES: "in quite a while"
# The marker shown when displaying values overridden for the selected NPC (not shown if the NPC inherits the default value)
SETTING_OVERRIDDEN_MARKER: " [NPC]"
SETTING_OVERRIDDEN_MARKER: " [NPC]"
# The translation of the text displayed when a cost is expected, but a non-number is provided
DOUBLE_COST_REQUIRED: "You must supply a numeric (double) cost"