+ */
+public class StatCategoryGUI extends AbstractEditorGUI {
+
+ public StatCategoryGUI(Player player, ItemGeneratorReference itemGenerator) {
+ super(player, 1, "Editor/Item Stats - Category", itemGenerator);
+ }
+
+ @Override
+ public void setContents() {
+ // Slot 1 — General
+ setSlot(1, new Slot(createItem(Material.PAPER,
+ "&eGeneral Stats",
+ "&7Classic typed stats (critical rate, dodge, etc.)",
+ "",
+ "&6Left-Click: &eOpen")) {
+ @Override
+ public void onLeftClick() {
+ openSubMenu(new StatListGUI(player, itemGenerator, EditorGUI.ItemType.ITEM_STATS, "list"));
+ }
+ });
+
+ // Slot 3 — Damage %
+ setSlot(3, new Slot(createItem(Material.IRON_SWORD,
+ "&eDamage Buffs &6(%)",
+ "&7Per-damage-type % buff stats",
+ "",
+ "&6Left-Click: &eOpen")) {
+ @Override
+ public void onLeftClick() {
+ openSubMenu(new StatListGUI(player, itemGenerator, EditorGUI.ItemType.ITEM_STATS, "list-damage-buffs"));
+ }
+ });
+
+ // Slot 5 — Defense %
+ setSlot(5, new Slot(createItem(Material.IRON_CHESTPLATE,
+ "&eDefense Buffs &6(%)",
+ "&7Per-damage-type % defense buff stats",
+ "",
+ "&6Left-Click: &eOpen")) {
+ @Override
+ public void onLeftClick() {
+ openSubMenu(new StatListGUI(player, itemGenerator, EditorGUI.ItemType.ITEM_STATS, "list-defense-buffs"));
+ }
+ });
+
+ // Slot 7 — Penetration
+ setSlot(7, new Slot(createItem(Material.ARROW,
+ "&ePenetration",
+ "&7Per-damage-type penetration stats",
+ "",
+ "&6Left-Click: &eOpen")) {
+ @Override
+ public void onLeftClick() {
+ openSubMenu(new StatListGUI(player, itemGenerator, EditorGUI.ItemType.ITEM_STATS, "list-penetration"));
+ }
+ });
+ }
+}
diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatGUI.java b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatGUI.java
index e963b508..53cb7e5c 100644
--- a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatGUI.java
+++ b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatGUI.java
@@ -140,6 +140,37 @@ public void onRightClick() {
}
});
}
+
+ // Slot 6 — icon material (cosmetic; shown in StatListGUI)
+ String iconRaw = itemGenerator.getConfig().getString(ItemType.ICON.getPath(this.path), "PAPER");
+ Material iconMat = Material.PAPER;
+ try { iconMat = Material.valueOf(iconRaw.toUpperCase()); } catch (IllegalArgumentException ignored) {}
+ setSlot(6, new Slot(createItem(iconMat,
+ "&eIcon Material",
+ "&bCurrent: &a" + iconRaw,
+ "&6Left-Click: &eSet (type material name)",
+ "&6Right-Click: &eReset to PAPER")) {
+ @Override
+ public void onLeftClick() {
+ sendSetMessage(ItemType.ICON.getTitle(),
+ itemGenerator.getConfig().getString(ItemType.ICON.getPath(path), "PAPER"),
+ s -> {
+ try {
+ Material.valueOf(s.toUpperCase()); // validate
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("Unknown material: " + s);
+ }
+ itemGenerator.getConfig().set(ItemType.ICON.getPath(path), s.toUpperCase());
+ saveAndReopen();
+ });
+ }
+
+ @Override
+ public void onRightClick() {
+ itemGenerator.getConfig().set(ItemType.ICON.getPath(path), "PAPER");
+ saveAndReopen();
+ }
+ });
}
public enum ItemType {
@@ -149,6 +180,7 @@ public enum ItemType {
MAX("max"),
FLAT_RANGE("flat-range"),
ROUND("round"),
+ ICON("icon"),
;
private final String path;
diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatListGUI.java b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatListGUI.java
index 8e7b6b08..5ab2a4f6 100644
--- a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatListGUI.java
+++ b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatListGUI.java
@@ -17,17 +17,31 @@
public class StatListGUI extends AbstractEditorGUI {
private final EditorGUI.ItemType itemType;
+ /** Full config path to the list section, e.g. "generator.item-stats.list" */
+ private final String listSectionPath;
+ /**
+ * Opens the stat list for a specific sub-section (e.g. "list", "list-damage-buffs").
+ */
+ public StatListGUI(Player player, ItemGeneratorReference itemGenerator,
+ EditorGUI.ItemType itemType, String listSection) {
+ super(player, 6, "Editor/" + itemType.getTitle() + " (" + listSection + ")", itemGenerator);
+ this.itemType = itemType;
+ this.listSectionPath = itemType.getPath() + '.' + listSection;
+ }
+
+ /**
+ * Backward-compatible constructor — defaults to the standard "list" section.
+ */
public StatListGUI(Player player, ItemGeneratorReference itemGenerator, EditorGUI.ItemType itemType) {
- super(player, 6, "Editor/" + itemType.getTitle(), itemGenerator);
- this.itemType = itemType;
+ this(player, itemGenerator, itemType, "list");
}
@Override
public void setContents() {
JYML cfg = itemGenerator.getConfig();
List list = new ArrayList<>();
- ConfigurationSection section = cfg.getConfigurationSection(MainStatsGUI.ItemType.LIST.getPath(this.itemType));
+ ConfigurationSection section = cfg.getConfigurationSection(this.listSectionPath);
if (section != null) {
list.addAll(section.getKeys(false));
}
@@ -70,11 +84,22 @@ public void setContents() {
if (fabledHook != null) itemStack = fabledHook.getAttributeIndicator(entry);
break;
}
+ default: {
+ // For ITEM_STATS categories, read per-entry icon from config
+ String iconKey = this.listSectionPath + '.' + entry + ".icon";
+ String iconName = cfg.getString(iconKey, "PAPER");
+ try {
+ itemStack = new ItemStack(Material.valueOf(iconName.toUpperCase()));
+ } catch (IllegalArgumentException ignored) {
+ itemStack = new ItemStack(Material.PAPER);
+ }
+ break;
+ }
}
if (itemStack == null) {
itemStack = new ItemStack(Material.PAPER);
}
- String path = MainStatsGUI.ItemType.LIST.getPath(this.itemType) + '.' + entry + '.';
+ String path = this.listSectionPath + '.' + entry + '.';
String roundDisplay = this.itemType == EditorGUI.ItemType.FABLED_ATTRIBUTES
? ""
: "&bRound: &a" + cfg.getBoolean(path + "round", false);
@@ -90,13 +115,14 @@ public void setContents() {
roundDisplay,
"",
"&eModify");
+ final String entryPath = this.listSectionPath + '.' + entry;
setSlot(i, new Slot(itemStack) {
@Override
public void onLeftClick() {
openSubMenu(new StatGUI(player,
itemGenerator,
itemType,
- MainStatsGUI.ItemType.LIST.getPath(itemType) + '.' + entry));
+ entryPath));
}
});
diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/generators/DuplicableStatGenerator.java b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/generators/DuplicableStatGenerator.java
new file mode 100644
index 00000000..7f6df08c
--- /dev/null
+++ b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/generators/DuplicableStatGenerator.java
@@ -0,0 +1,142 @@
+package studio.magemonkey.divinity.modules.list.itemgenerator.generators;
+
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.jetbrains.annotations.NotNull;
+import studio.magemonkey.codex.config.api.JYML;
+import studio.magemonkey.codex.util.NumberUT;
+import studio.magemonkey.codex.util.StringUT;
+import studio.magemonkey.codex.util.random.Rnd;
+import studio.magemonkey.divinity.Divinity;
+import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager;
+import studio.magemonkey.divinity.modules.list.itemgenerator.api.AbstractAttributeGenerator;
+import studio.magemonkey.divinity.modules.list.itemgenerator.api.DamageInformation;
+import studio.magemonkey.divinity.stats.bonus.BonusCalculator;
+import studio.magemonkey.divinity.stats.bonus.StatBonus;
+import studio.magemonkey.divinity.stats.items.api.DuplicableItemLoreStat;
+import studio.magemonkey.divinity.stats.items.api.ItemLoreStat;
+import studio.magemonkey.divinity.utils.LoreUT;
+
+import java.util.*;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * Generator for DuplicableItemLoreStat subtypes (DynamicBuffStat, PenetrationStat).
+ * Each stat independently rolls against its own chance — no global min/max pool.
+ */
+public class DuplicableStatGenerator> extends AbstractAttributeGenerator {
+
+ private final Map attributes;
+
+ public DuplicableStatGenerator(
+ @NotNull Divinity plugin,
+ @NotNull ItemGeneratorManager.GeneratorItem generatorItem,
+ @NotNull String basePath,
+ @NotNull String listSection,
+ @NotNull Collection attributesAll,
+ @NotNull Function idExtractor,
+ @NotNull String placeholder
+ ) {
+ super(plugin, generatorItem, placeholder);
+
+ JYML cfg = generatorItem.getConfig();
+
+ String loreFormatKey = basePath + listSection + ".lore-format";
+ this.loreFormat = StringUT.color(cfg.getStringList(loreFormatKey));
+ this.attributes = new LinkedHashMap<>();
+
+ for (T att : attributesAll) {
+ String path2 = basePath + listSection + "." + idExtractor.apply(att) + ".";
+
+ cfg.addMissing(path2 + "chance", 0D);
+ cfg.addMissing(path2 + "scale-by-level", 1D);
+ cfg.addMissing(path2 + "min", 0D);
+ cfg.addMissing(path2 + "max", 0D);
+ cfg.addMissing(path2 + "flat-range", false);
+ cfg.addMissing(path2 + "round", false);
+
+ if (!this.loreFormat.contains(att.getPlaceholder())) {
+ this.loreFormat.add(att.getPlaceholder());
+ cfg.set(loreFormatKey, this.loreFormat);
+ }
+
+ double chance = cfg.getDouble(path2 + "chance", 0D);
+ double m1 = cfg.getDouble(path2 + "min", 0D);
+ double m2 = cfg.getDouble(path2 + "max", 0D);
+ if (m1 > m2) { double t = m1; m1 = m2; m2 = t; }
+ double scale = cfg.getDouble(path2 + "scale-by-level", 1D);
+ boolean flatRange = cfg.getBoolean(path2 + "flat-range", false);
+ boolean roundValues = cfg.getBoolean(path2 + "round", false);
+
+ this.attributes.put(att, new DamageInformation(chance, m1, m2, scale, flatRange, roundValues));
+ }
+ }
+
+ @Override
+ public void generate(@NotNull ItemStack item, int itemLevel) {
+ ItemMeta meta = item.getItemMeta();
+ if (meta == null) return;
+ List lore = meta.getLore();
+ if (lore == null) return;
+
+ int generatorPos = lore.indexOf(this.placeholder);
+ if (generatorPos < 0) return;
+
+ // Roll each stat independently against its own chance
+ List toApply = new ArrayList<>();
+ for (Map.Entry entry : this.attributes.entrySet()) {
+ DamageInformation info = entry.getValue();
+ if (info.getChance() <= 0) continue;
+ if (Rnd.get(true) < info.getChance()) {
+ toApply.add(entry.getKey());
+ }
+ }
+
+ if (toApply.isEmpty()) {
+ LoreUT.replacePlaceholder(item, this.placeholder, null);
+ return;
+ }
+
+ // Insert lore-format (stat placeholders) and remove the generator marker
+ for (String format : this.getLoreFormat()) {
+ generatorPos = LoreUT.addToLore(lore, generatorPos, format);
+ }
+ lore.remove(this.placeholder);
+ meta.setLore(lore);
+ item.setItemMeta(meta);
+
+ // Generate and write values for each rolled stat
+ for (T stat : toApply) {
+ if (!stat.hasPlaceholder(item)) continue;
+
+ DamageInformation info = this.attributes.get(stat);
+ if (info == null) continue;
+
+ BiFunction vMod =
+ generatorItem.getMaterialModifiers(item, (ItemLoreStat>) stat);
+
+ double vScale = generatorItem.getScaleOfLevel(info.getScaleByLevel(), itemLevel);
+ double vMin = BonusCalculator.SIMPLE_FULL.apply(info.getMin(), Arrays.asList(vMod)) * vScale;
+ double vMax = BonusCalculator.SIMPLE_FULL.apply(info.getMax(), Arrays.asList(vMod)) * vScale;
+ double vFin = NumberUT.round(Rnd.getDouble(vMin, vMax));
+ if (info.isRound()) {
+ vFin = Math.round(vFin);
+ }
+
+ if (vFin != 0) {
+ stat.add(item, new StatBonus(new double[]{vFin}, false, null), -1);
+ }
+
+ for (StatBonus bonus : generatorItem.getClassBonuses((ItemLoreStat>) stat)) {
+ stat.add(item, bonus, -1);
+ }
+ for (StatBonus bonus : generatorItem.getRarityBonuses((ItemLoreStat>) stat)) {
+ stat.add(item, bonus, -1);
+ }
+ for (StatBonus bonus : generatorItem.getMaterialBonuses(item, (ItemLoreStat>) stat)) {
+ stat.add(item, bonus, -1);
+ }
+ }
+ }
+}
diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/generators/TypedStatGenerator.java b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/generators/TypedStatGenerator.java
index aa8b3bcb..faaf1700 100644
--- a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/generators/TypedStatGenerator.java
+++ b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/generators/TypedStatGenerator.java
@@ -232,6 +232,7 @@ public void generate(@NotNull ItemStack item, int itemLevel) {
} else if (stat instanceof DurabilityStat) {
DurabilityStat rStat = (DurabilityStat) stat;
rStat.add(item, new double[]{vFin, vFin}, -1);
+ rStat.syncVanillaBar(item);
}
}
diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/magicdust/MagicDustManager.java b/src/main/java/studio/magemonkey/divinity/modules/list/magicdust/MagicDustManager.java
index aaa54da4..a3ded9f4 100644
--- a/src/main/java/studio/magemonkey/divinity/modules/list/magicdust/MagicDustManager.java
+++ b/src/main/java/studio/magemonkey/divinity/modules/list/magicdust/MagicDustManager.java
@@ -171,7 +171,7 @@ public boolean isRateableItem(@NotNull ItemStack target) {
}
public void openGUIPaid(@NotNull Player player, @Nullable ItemStack target, boolean force) {
- if (!force && !Perms.has(player, Perms.MAGIC_DUST_GUI)) {
+ if (!force && !player.hasPermission(Perms.MAGIC_DUST_GUI)) {
plugin.lang().Error_NoPerm.send(player);
return;
}
diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/repair/RepairManager.java b/src/main/java/studio/magemonkey/divinity/modules/list/repair/RepairManager.java
index 5287400f..10c81c35 100644
--- a/src/main/java/studio/magemonkey/divinity/modules/list/repair/RepairManager.java
+++ b/src/main/java/studio/magemonkey/divinity/modules/list/repair/RepairManager.java
@@ -177,7 +177,7 @@ public boolean openAnvilGUI(
@Nullable RepairType type,
boolean isForce) {
- if (!isForce && !Perms.has(player, Perms.REPAIR_GUI)) {
+ if (!isForce && !player.hasPermission(Perms.REPAIR_GUI)) {
plugin.lang().Error_NoPerm.send(player);
return false;
}
@@ -211,6 +211,7 @@ ItemStack getResult(@NotNull ItemStack target, @NotNull Player player) {
double max = arr[1];
ItemStack result = new ItemStack(target);
this.duraStat.add(result, new double[]{max, max}, -1);
+ this.duraStat.syncVanillaBar(result);
return result;
}
@@ -382,6 +383,7 @@ protected boolean onDragDrop(
durNow = (int) Math.min(durMax, durNow + durMax * 1D * (rPerc * 1D / 100D));
this.duraStat.add(target, new double[]{durNow, durMax}, -1);
+ this.duraStat.syncVanillaBar(target);
e.setCurrentItem(target);
if (lost != null) {
diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/sell/SellManager.java b/src/main/java/studio/magemonkey/divinity/modules/list/sell/SellManager.java
index afe101b5..e9fbc5d5 100644
--- a/src/main/java/studio/magemonkey/divinity/modules/list/sell/SellManager.java
+++ b/src/main/java/studio/magemonkey/divinity/modules/list/sell/SellManager.java
@@ -74,7 +74,7 @@ public void shutdown() {
}
public void openSellGUI(@NotNull Player player, boolean isForce) {
- if (!isForce && !Perms.has(player, Perms.SELL_GUI)) {
+ if (!isForce && !player.hasPermission(Perms.SELL_GUI)) {
plugin.lang().Error_NoPerm.send(player);
return;
}
diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/soulbound/SoulboundManager.java b/src/main/java/studio/magemonkey/divinity/modules/list/soulbound/SoulboundManager.java
index 79613e30..17ba685b 100644
--- a/src/main/java/studio/magemonkey/divinity/modules/list/soulbound/SoulboundManager.java
+++ b/src/main/java/studio/magemonkey/divinity/modules/list/soulbound/SoulboundManager.java
@@ -210,7 +210,7 @@ public void onSoulStart(InventoryClickEvent e) {
}
} else {
if (this.hasOwner(item)) {
- if (!this.isOwner(item, p) && !Perms.has(p, Perms.BYPASS_REQ_USER_UNTRADEABLE)) {
+ if (!this.isOwner(item, p) && !p.hasPermission(Perms.BYPASS_REQ_USER_UNTRADEABLE)) {
e.setCancelled(true);
return;
}
diff --git a/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java b/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java
index 0ca1fa00..dd84c6a0 100644
--- a/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java
+++ b/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java
@@ -2,6 +2,7 @@
import lombok.Getter;
import org.bukkit.Material;
+import org.bukkit.attribute.Attribute;
import org.bukkit.attribute.AttributeInstance;
import org.bukkit.attribute.AttributeModifier;
import org.bukkit.block.Biome;
@@ -51,6 +52,9 @@
import studio.magemonkey.divinity.stats.items.attributes.DefenseAttribute;
import studio.magemonkey.divinity.stats.items.attributes.api.SimpleStat;
import studio.magemonkey.divinity.stats.items.attributes.api.TypedStat;
+import studio.magemonkey.divinity.hooks.EHook;
+import studio.magemonkey.divinity.hooks.external.FabledHook;
+import studio.magemonkey.divinity.stats.items.attributes.stats.DynamicBuffStat;
import studio.magemonkey.divinity.utils.ItemUtils;
import java.util.*;
@@ -411,7 +415,7 @@ public synchronized List getEquipment() {
return new ArrayList<>(this.inventory);
}
- private void updateInventory() {
+ public void updateInventory() {
this.inventory.clear();
ItemStack[] armor = new ItemStack[0];
@@ -438,6 +442,7 @@ private void updateInventory() {
}
public void updateAll() {
+ if(EngineCfg.LEGACY_COMBAT) return;
if (!EngineCfg.ATTRIBUTES_EFFECTIVE_FOR_MOBS && !this.isPlayer()) {
return;
}
@@ -575,6 +580,171 @@ private void updateBonusAttributes() {
this.applyBonusAttribute(nbt, value);
}
+
+ // MC 1.21+ vanilla attributes — handled separately, not in NBTAttribute enum
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("SCALE");
+ if (attr != null) this.applyScaleAttribute(attr, calcVanillaStatValue(TypedStat.Type.SCALE));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("WATER_MOVEMENT_EFFICIENCY");
+ if (attr != null) applyVanillaAttributeModifier(attr, WATER_MOV_EFF_MODIFIER_UUID,
+ "divinity.water_movement_efficiency", calcVanillaStatValue(TypedStat.Type.WATER_MOVEMENT_EFFICIENCY));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("MOVEMENT_EFFICIENCY");
+ if (attr != null) applyVanillaAttributeModifier(attr, MOV_EFF_MODIFIER_UUID,
+ "divinity.movement_efficiency", calcVanillaStatValue(TypedStat.Type.MOVEMENT_EFFICIENCY));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("SNEAKING_SPEED");
+ if (attr != null) applyVanillaAttributeModifier(attr, SNEAK_SPEED_MODIFIER_UUID,
+ "divinity.sneaking_speed", calcVanillaStatValue(TypedStat.Type.SNEAKING_SPEED));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("BLOCK_BREAK_SPEED");
+ if (attr != null) applyVanillaAttributeModifier(attr, BLOCK_BREAK_SPEED_UUID,
+ "divinity.block_break_speed", calcVanillaStatValue(TypedStat.Type.BLOCK_BREAK_SPEED));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("BLOCK_INTERACTION_RANGE");
+ if (attr != null) applyVanillaAttributeModifier(attr, BLOCK_INTERACT_RANGE_UUID,
+ "divinity.block_interaction_range", calcVanillaStatValue(TypedStat.Type.BLOCK_INTERACTION_RANGE));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("ENTITY_INTERACTION_RANGE");
+ if (attr != null) applyVanillaAttributeModifier(attr, ENTITY_INTERACT_RANGE_UUID,
+ "divinity.entity_interaction_range", calcVanillaStatValue(TypedStat.Type.ENTITY_INTERACTION_RANGE));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("EXPLOSION_KNOCKBACK_RESISTANCE");
+ if (attr != null) applyVanillaAttributeModifier(attr, EXPLOSION_KB_RES_UUID,
+ "divinity.explosion_knockback_resistance", calcVanillaStatValue(TypedStat.Type.EXPLOSION_KNOCKBACK_RESISTANCE));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("FALL_DAMAGE_MULTIPLIER");
+ if (attr != null) applyVanillaAttributeModifier(attr, FALL_DAMAGE_MULT_UUID,
+ "divinity.fall_damage_multiplier", calcVanillaStatValue(TypedStat.Type.FALL_DAMAGE_MULTIPLIER));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("FLYING_SPEED");
+ if (attr != null) applyVanillaAttributeModifier(attr, FLYING_SPEED_UUID,
+ "divinity.flying_speed", calcVanillaStatValue(TypedStat.Type.FLYING_SPEED));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("GRAVITY");
+ if (attr != null) applyVanillaAttributeModifier(attr, GRAVITY_UUID,
+ "divinity.gravity", calcVanillaStatValue(TypedStat.Type.GRAVITY));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("JUMP_STRENGTH");
+ if (attr != null) applyVanillaAttributeModifier(attr, JUMP_STRENGTH_UUID,
+ "divinity.jump_strength", calcVanillaStatValue(TypedStat.Type.JUMP_STRENGTH));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("MAX_ABSORPTION");
+ if (attr != null) applyVanillaAttributeModifier(attr, MAX_ABSORPTION_UUID,
+ "divinity.max_absorption", calcVanillaStatValue(TypedStat.Type.MAX_ABSORPTION));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("MINING_EFFICIENCY");
+ if (attr != null) applyVanillaAttributeModifier(attr, MINING_EFFICIENCY_UUID,
+ "divinity.mining_efficiency", calcVanillaStatValue(TypedStat.Type.MINING_EFFICIENCY));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("OXYGEN_BONUS");
+ if (attr != null) applyVanillaAttributeModifier(attr, OXYGEN_BONUS_UUID,
+ "divinity.oxygen_bonus", calcVanillaStatValue(TypedStat.Type.OXYGEN_BONUS));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("SAFE_FALL_DISTANCE");
+ if (attr != null) applyVanillaAttributeModifier(attr, SAFE_FALL_DISTANCE_UUID,
+ "divinity.safe_fall_distance", calcVanillaStatValue(TypedStat.Type.SAFE_FALL_DISTANCE));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("STEP_HEIGHT");
+ if (attr != null) applyVanillaAttributeModifier(attr, STEP_HEIGHT_UUID,
+ "divinity.step_height", calcVanillaStatValue(TypedStat.Type.STEP_HEIGHT));
+ } catch (Exception ignored) {}
+
+ try {
+ Attribute attr = (Attribute) VersionManager.getNms().getAttribute("SUBMERGED_MINING_SPEED");
+ if (attr != null) applyVanillaAttributeModifier(attr, SUBMERGED_MINING_SPEED_UUID,
+ "divinity.submerged_mining_speed", calcVanillaStatValue(TypedStat.Type.SUBMERGED_MINING_SPEED));
+ } catch (Exception ignored) {}
+ }
+
+ private static final UUID SCALE_MODIFIER_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000001");
+ private static final UUID WATER_MOV_EFF_MODIFIER_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000002");
+ private static final UUID MOV_EFF_MODIFIER_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000003");
+ private static final UUID SNEAK_SPEED_MODIFIER_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000004");
+ private static final UUID BLOCK_BREAK_SPEED_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000005");
+ private static final UUID BLOCK_INTERACT_RANGE_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000006");
+ private static final UUID ENTITY_INTERACT_RANGE_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000007");
+ private static final UUID EXPLOSION_KB_RES_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000008");
+ private static final UUID FALL_DAMAGE_MULT_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000009");
+ private static final UUID FLYING_SPEED_UUID = UUID.fromString("d141e000-5ca1-4000-0000-00000000000a");
+ private static final UUID GRAVITY_UUID = UUID.fromString("d141e000-5ca1-4000-0000-00000000000b");
+ private static final UUID JUMP_STRENGTH_UUID = UUID.fromString("d141e000-5ca1-4000-0000-00000000000c");
+ private static final UUID MAX_ABSORPTION_UUID = UUID.fromString("d141e000-5ca1-4000-0000-00000000000d");
+ private static final UUID MINING_EFFICIENCY_UUID = UUID.fromString("d141e000-5ca1-4000-0000-00000000000e");
+ private static final UUID OXYGEN_BONUS_UUID = UUID.fromString("d141e000-5ca1-4000-0000-00000000000f");
+ private static final UUID SAFE_FALL_DISTANCE_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000010");
+ private static final UUID STEP_HEIGHT_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000011");
+ private static final UUID SUBMERGED_MINING_SPEED_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000012");
+
+ @SuppressWarnings("deprecation")
+ private void applyVanillaAttributeModifier(@NotNull Attribute attr, @NotNull UUID modUuid, @NotNull String modName, double value) {
+ AttributeInstance attInst = this.entity.getAttribute(attr);
+ if (attInst == null) return;
+ for (AttributeModifier mod : new HashSet<>(attInst.getModifiers())) {
+ try {
+ if (modUuid.equals(mod.getUniqueId())) {
+ if (mod.getAmount() == value) return;
+ attInst.removeModifier(mod);
+ break;
+ }
+ } catch (Exception ignored) {}
+ }
+ if (value == 0D) return;
+ attInst.addModifier(new AttributeModifier(modUuid, modName, value, Operation.ADD_NUMBER));
+ }
+
+ @SuppressWarnings("deprecation")
+ private void applyScaleAttribute(@NotNull Attribute scaleAttr, double value) {
+ applyVanillaAttributeModifier(scaleAttr, SCALE_MODIFIER_UUID, "divinity.scale", value);
+ }
+
+ private double calcVanillaStatValue(@NotNull TypedStat.Type type) {
+ TypedStat typedStat = ItemStats.getStat(type);
+ if (!(typedStat instanceof SimpleStat)) return 0D;
+ SimpleStat ss = (SimpleStat) typedStat;
+ List> bonuses = new ArrayList<>();
+ for (ItemStack item : this.getEquipment()) {
+ if (item == null || item.getType().isAir()) continue;
+ bonuses.addAll(ss.get(item, player));
+ }
+ bonuses.addAll(this.getBonuses(ss));
+ double value = BonusCalculator.SIMPLE_FULL.apply(0D, bonuses);
+ value = this.getEffectBonus(ss, false).applyAsDouble(value);
+ if (ss.getCapability() >= 0 && value > ss.getCapability()) value = ss.getCapability();
+ return value / 100D;
}
private void applyBonusAttribute(@NotNull NBTAttribute att, double value) {
@@ -683,6 +853,10 @@ public Map getDamageTypes(boolean safe) {
double value = Rnd.getDouble(range[0], range[1]);
value *= dmgAtt.getDamageModifierByBiome(bio); // Multiply by Biome
value = this.getEffectBonus(dmgAtt, safe).applyAsDouble(value);
+ if (this.isPlayer()) {
+ FabledHook fHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API);
+ if (fHook != null) value = fHook.applyStatScale(this.player, "damage_" + dmgAtt.getId(), value);
+ }
if (value > 0D) {
map.put(dmgAtt, value);
@@ -716,6 +890,10 @@ public Map getDefenseTypes(boolean safe) {
double value = BonusCalculator.SIMPLE_FULL.apply(0D, bonuses);
value = this.getEffectBonus(dt, safe).applyAsDouble(value);
+ if (this.isPlayer()) {
+ FabledHook fHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API);
+ if (fHook != null) value = fHook.applyStatScale(this.player, "defense_" + dt.getId(), value);
+ }
if (value > 0D) {
map.put(dt, value);
}
@@ -778,6 +956,56 @@ public double getItemStat(@NotNull SimpleStat.Type type, boolean safe) {
}
}
+ // Apply Fabled attribute/stat scaling if player and Fabled is loaded
+ if (this.isPlayer()) {
+ FabledHook fHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API);
+ if (fHook != null) {
+ value = fHook.applyStatScale(this.player, type.name().toLowerCase(), value);
+ }
+ }
+
+ return value;
+ }
+
+ public double getPenetration(@NotNull studio.magemonkey.divinity.stats.items.attributes.stats.PenetrationStat pen) {
+ List equip = this.getEquipment();
+ List> bonuses = new ArrayList<>();
+ for (ItemStack item : equip) {
+ if (item == null || item.getType().isAir()) continue;
+ bonuses.addAll(pen.get(item, player));
+ }
+ double value = studio.magemonkey.divinity.stats.bonus.BonusCalculator.SIMPLE_FULL.apply(0D, bonuses);
+ if (pen.getCapacity() >= 0 && value > pen.getCapacity()) {
+ value = pen.getCapacity();
+ }
+ if (this.isPlayer()) {
+ FabledHook fHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API);
+ if (fHook != null) {
+ value = fHook.applyStatScale(this.player, "penetration_" + pen.getPenId(), value);
+ }
+ }
+ return value;
+ }
+
+ public double getDynamicBuff(@NotNull DynamicBuffStat buff) {
+ List equip = this.getEquipment();
+ List> bonuses = new ArrayList<>();
+ for (ItemStack item : equip) {
+ if (item == null || item.getType().isAir()) continue;
+ bonuses.addAll(buff.get(item, player));
+ }
+ double value = BonusCalculator.SIMPLE_FULL.apply(0D, bonuses);
+ if (buff.getCapacity() >= 0 && value > buff.getCapacity()) {
+ value = buff.getCapacity();
+ }
+ if (this.isPlayer()) {
+ FabledHook fHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API);
+ if (fHook != null) {
+ String buffKey = (buff.getBuffTarget() == DynamicBuffStat.BuffTarget.DAMAGE ? "damagebuff_" : "defensebuff_")
+ + buff.getBuffId();
+ value = fHook.applyStatScale(this.player, buffKey, value);
+ }
+ }
return value;
}
diff --git a/src/main/java/studio/magemonkey/divinity/stats/ProjectileStats.java b/src/main/java/studio/magemonkey/divinity/stats/ProjectileStats.java
index 58e4fc6c..e81038e1 100644
--- a/src/main/java/studio/magemonkey/divinity/stats/ProjectileStats.java
+++ b/src/main/java/studio/magemonkey/divinity/stats/ProjectileStats.java
@@ -27,8 +27,9 @@ public static ItemStack getSrcWeapon(@NotNull Projectile e) {
return ((Trident) e).getItem();
}
if (!e.hasMetadata(PROJECTILE_SOURCE_WEAPON)) return null;
-
- Object val = e.getMetadata(PROJECTILE_SOURCE_WEAPON).get(0).value();
+ var meta = e.getMetadata(PROJECTILE_SOURCE_WEAPON);
+ if (meta.isEmpty()) return null;
+ Object val = meta.get(0).value();
return (ItemStack) val;
}
@@ -38,8 +39,9 @@ public static void setPower(@NotNull Projectile e, double power) {
public static double getPower(@NotNull Projectile e) {
if (!e.hasMetadata(PROJECTILE_LAUNCH_POWER)) return 1D;
-
- return e.getMetadata(PROJECTILE_LAUNCH_POWER).get(0).asDouble();
+ var meta = e.getMetadata(PROJECTILE_LAUNCH_POWER);
+ if (meta.isEmpty()) return 1D;
+ return meta.get(0).asDouble();
}
public static void setPickable(@NotNull Entity pp, boolean b) {
@@ -48,7 +50,8 @@ public static void setPickable(@NotNull Entity pp, boolean b) {
public static boolean isPickable(@NotNull Entity pp) {
if (!pp.hasMetadata(PROJECTILE_PICKABLE)) return true;
-
- return pp.getMetadata(PROJECTILE_PICKABLE).get(0).asBoolean();
+ var meta = pp.getMetadata(PROJECTILE_PICKABLE);
+ if (meta.isEmpty()) return true;
+ return meta.get(0).asBoolean();
}
}
diff --git a/src/main/java/studio/magemonkey/divinity/stats/bonus/BonusMap.java b/src/main/java/studio/magemonkey/divinity/stats/bonus/BonusMap.java
index 9e4d26ac..65ad5383 100644
--- a/src/main/java/studio/magemonkey/divinity/stats/bonus/BonusMap.java
+++ b/src/main/java/studio/magemonkey/divinity/stats/bonus/BonusMap.java
@@ -14,6 +14,8 @@
import studio.magemonkey.divinity.stats.items.attributes.*;
import studio.magemonkey.divinity.stats.items.attributes.api.SimpleStat;
import studio.magemonkey.divinity.stats.items.attributes.api.TypedStat;
+import studio.magemonkey.divinity.stats.items.attributes.stats.DynamicBuffStat;
+import studio.magemonkey.divinity.stats.items.attributes.stats.PenetrationStat;
import java.util.Collection;
import java.util.HashMap;
@@ -198,6 +200,57 @@ public void loadAmmo(@NotNull JYML cfg, @NotNull String path) {
}
}
+ public void loadDamageBuffs(@NotNull JYML cfg, @NotNull String path) {
+ for (String id : cfg.getSection(path)) {
+ DynamicBuffStat stat = ItemStats.getDamageBuff(id);
+ if (stat == null) continue;
+
+ String sVal = cfg.getString(path + "." + id);
+ if (sVal == null) continue;
+
+ String[] split = sVal.split("%", 2);
+ boolean perc = split.length == 2 && split[1].isEmpty();
+ double val = StringUT.getDouble(split[0], 0, true);
+
+ BiFunction func = (isBonus, apply) -> perc == isBonus ? apply + val : apply;
+ this.bonus.put(stat, func);
+ }
+ }
+
+ public void loadDefenseBuffs(@NotNull JYML cfg, @NotNull String path) {
+ for (String id : cfg.getSection(path)) {
+ DynamicBuffStat stat = ItemStats.getDefenseBuff(id);
+ if (stat == null) continue;
+
+ String sVal = cfg.getString(path + "." + id);
+ if (sVal == null) continue;
+
+ String[] split = sVal.split("%", 2);
+ boolean perc = split.length == 2 && split[1].isEmpty();
+ double val = StringUT.getDouble(split[0], 0, true);
+
+ BiFunction func = (isBonus, apply) -> perc == isBonus ? apply + val : apply;
+ this.bonus.put(stat, func);
+ }
+ }
+
+ public void loadPenetrations(@NotNull JYML cfg, @NotNull String path) {
+ for (String id : cfg.getSection(path)) {
+ PenetrationStat stat = ItemStats.getPenetration(id);
+ if (stat == null) continue;
+
+ String sVal = cfg.getString(path + "." + id);
+ if (sVal == null) continue;
+
+ String[] split = sVal.split("%", 2);
+ boolean perc = split.length == 2 && split[1].isEmpty();
+ double val = StringUT.getDouble(split[0], 0, true);
+
+ BiFunction func = (isBonus, apply) -> perc == isBonus ? apply + val : apply;
+ this.bonus.put(stat, func);
+ }
+ }
+
public void loadHands(@NotNull JYML cfg, @NotNull String path) {
for (String id : cfg.getSection(path)) {
HandAttribute stat;
diff --git a/src/main/java/studio/magemonkey/divinity/stats/items/ItemStats.java b/src/main/java/studio/magemonkey/divinity/stats/items/ItemStats.java
index 0faa6fb2..59aa4954 100644
--- a/src/main/java/studio/magemonkey/divinity/stats/items/ItemStats.java
+++ b/src/main/java/studio/magemonkey/divinity/stats/items/ItemStats.java
@@ -1,6 +1,5 @@
package studio.magemonkey.divinity.stats.items;
-import org.bukkit.Bukkit;
import org.bukkit.Keyed;
import org.bukkit.NamespacedKey;
import org.bukkit.attribute.Attribute;
@@ -20,6 +19,7 @@
import studio.magemonkey.codex.modules.IModule;
import studio.magemonkey.codex.util.DataUT;
import studio.magemonkey.divinity.Divinity;
+import studio.magemonkey.divinity.config.EngineCfg;
import studio.magemonkey.divinity.modules.api.QModuleDrop;
import studio.magemonkey.divinity.stats.items.api.DuplicableItemLoreStat;
import studio.magemonkey.divinity.stats.items.api.DynamicStat;
@@ -29,6 +29,8 @@
import studio.magemonkey.divinity.stats.items.attributes.api.SimpleStat;
import studio.magemonkey.divinity.stats.items.attributes.api.TypedStat;
import studio.magemonkey.divinity.stats.items.attributes.stats.DurabilityStat;
+import studio.magemonkey.divinity.stats.items.attributes.stats.DynamicBuffStat;
+import studio.magemonkey.divinity.stats.items.attributes.stats.PenetrationStat;
import studio.magemonkey.divinity.utils.ItemUtils;
import java.util.*;
@@ -44,6 +46,9 @@ public class ItemStats {
private static final Map> ATTRIBUTES = new HashMap<>();
private static final Map> MULTI_ATTRIBUTES = new HashMap<>();
private static final Set DYNAMIC_STATS = new HashSet<>();
+ private static final Map DAMAGE_BUFFS = new LinkedHashMap<>();
+ private static final Map DEFENSE_BUFFS = new LinkedHashMap<>();
+ private static final Map PENETRATIONS = new LinkedHashMap<>();
private static final Divinity plugin = Divinity.getInstance();
private static final List KEY_ID =
List.of(new NamespacedKey(plugin, ItemTags.TAG_ITEM_ID),
@@ -95,6 +100,9 @@ public static void clear() {
MULTI_ATTRIBUTES.clear();
DAMAGE_DEFAULT = null;
DEFENSE_DEFAULT = null;
+ DAMAGE_BUFFS.clear();
+ DEFENSE_BUFFS.clear();
+ PENETRATIONS.clear();
}
public static void registerDamage(@NotNull DamageAttribute dmg) {
@@ -138,6 +146,48 @@ public static Collection getDynamicStats() {
return Collections.unmodifiableSet(DYNAMIC_STATS);
}
+ public static void registerDamageBuff(@NotNull DynamicBuffStat buff) {
+ DAMAGE_BUFFS.put(buff.getBuffId(), buff);
+ }
+
+ public static void registerDefenseBuff(@NotNull DynamicBuffStat buff) {
+ DEFENSE_BUFFS.put(buff.getBuffId(), buff);
+ }
+
+ @NotNull
+ public static Collection getDamageBuffs() {
+ return DAMAGE_BUFFS.values();
+ }
+
+ @NotNull
+ public static Collection getDefenseBuffs() {
+ return DEFENSE_BUFFS.values();
+ }
+
+ @Nullable
+ public static DynamicBuffStat getDamageBuff(@NotNull String id) {
+ return DAMAGE_BUFFS.get(id.toLowerCase());
+ }
+
+ @Nullable
+ public static DynamicBuffStat getDefenseBuff(@NotNull String id) {
+ return DEFENSE_BUFFS.get(id.toLowerCase());
+ }
+
+ public static void registerPenetration(@NotNull PenetrationStat pen) {
+ PENETRATIONS.put(pen.getPenId(), pen);
+ }
+
+ @NotNull
+ public static Collection getPenetrations() {
+ return PENETRATIONS.values();
+ }
+
+ @Nullable
+ public static PenetrationStat getPenetration(@NotNull String id) {
+ return PENETRATIONS.get(id.toLowerCase());
+ }
+
private static void updateDefenseByDefault() {
if (DAMAGES.isEmpty()) return;
@@ -308,13 +358,12 @@ public static boolean hasStat(@NotNull ItemStack item, @Nullable Player player,
// ----------------------------------------------------------------- //
public static void updateVanillaAttributes(@NotNull ItemStack item, @Nullable Player player) {
+ if(EngineCfg.FULL_LEGACY || EngineCfg.LEGACY_COMBAT) return;
+
addAttribute(item, player, NBTAttribute.MAX_HEALTH, getStat(item, player, TypedStat.Type.MAX_HEALTH));
addAttribute(item, player, NBTAttribute.MOVEMENT_SPEED, getStat(item, player, TypedStat.Type.MOVEMENT_SPEED));
addAttribute(item, player, NBTAttribute.ATTACK_SPEED, getStat(item, player, TypedStat.Type.ATTACK_SPEED));
- addAttribute(item,
- player,
- NBTAttribute.KNOCKBACK_RESISTANCE,
- getStat(item, player, TypedStat.Type.KNOCKBACK_RESISTANCE));
+ addAttribute(item, player, NBTAttribute.KNOCKBACK_RESISTANCE, getStat(item, player, TypedStat.Type.KNOCKBACK_RESISTANCE));
double vanilla = DamageAttribute.getVanillaDamage(item);
if (vanilla > 1) addAttribute(item, player, NBTAttribute.ATTACK_DAMAGE, vanilla);
@@ -328,16 +377,12 @@ public static void updateVanillaAttributes(@NotNull ItemStack item, @Nullable Pl
toughness == 0 ? DefenseAttribute.getVanillaToughness(item) : toughness);
}
ItemMeta im = item.getItemMeta();
- if (im == null) {
- im = Bukkit.getItemFactory().getItemMeta(item.getType());
- }
// For 1.20.4+, the HIDE_ATTRIBUTES flag doesn't work unless an attribute has been added that's not the default.
// Note: This only applies to Paper and its forks.
if (Version.CURRENT.isAtLeast(Version.V1_20_R4)) {
Attribute moveSpeed = VersionManager.getNms().getAttribute("MOVEMENT_SPEED");
- if (!im.hasAttributeModifiers()
- || im.getAttributeModifiers(VersionManager.getNms().getAttribute("MOVEMENT_SPEED")) == null) {
+ if (im.getAttributeModifiers(VersionManager.getNms().getAttribute("MOVEMENT_SPEED")) == null) {
//noinspection RedundantCast
im.addAttributeModifier(moveSpeed,
new AttributeModifier(((Keyed) moveSpeed).getKey().getKey(), 0, Operation.ADD_NUMBER));
@@ -352,6 +397,7 @@ private static void addAttribute(@NotNull ItemStack item,
@Nullable Player player,
@NotNull NBTAttribute att,
double value) {
+ //if(EngineCfg.LEGACY_COMBAT) return;
ItemMeta meta = item.getItemMeta();
if (meta == null) return;
diff --git a/src/main/java/studio/magemonkey/divinity/stats/items/ItemTags.java b/src/main/java/studio/magemonkey/divinity/stats/items/ItemTags.java
index 0dd2fc39..16c4a2fb 100644
--- a/src/main/java/studio/magemonkey/divinity/stats/items/ItemTags.java
+++ b/src/main/java/studio/magemonkey/divinity/stats/items/ItemTags.java
@@ -13,7 +13,10 @@ public class ItemTags {
public static final String TAG_ITEM_STAT = "ITEM_STAT_";
public static final String TAG_ITEM_DAMAGE = "ITEM_DAMAGE_";
public static final String TAG_ITEM_DEFENSE = "ITEM_DEFENSE_";
- public static final String TAG_ITEM_FABLED_ATTR = "ITEM_FABLED_ATTR_";
+ public static final String TAG_ITEM_FABLED_ATTR = "ITEM_FABLED_ATTR_";
+ public static final String TAG_ITEM_DAMAGE_BUFF = "ITEM_DAMAGE_BUFF_";
+ public static final String TAG_ITEM_DEFENSE_BUFF = "ITEM_DEFENSE_BUFF_";
+ public static final String TAG_ITEM_PENETRATION = "ITEM_PENETRATION_";
public static final String TAG_REQ_USER_LEVEL = "ITEM_USER_LEVEL";
diff --git a/src/main/java/studio/magemonkey/divinity/stats/items/attributes/api/TypedStat.java b/src/main/java/studio/magemonkey/divinity/stats/items/attributes/api/TypedStat.java
index a611f3d3..64fdb8d6 100644
--- a/src/main/java/studio/magemonkey/divinity/stats/items/attributes/api/TypedStat.java
+++ b/src/main/java/studio/magemonkey/divinity/stats/items/attributes/api/TypedStat.java
@@ -48,37 +48,79 @@ enum Type {
// DIRECT_DAMAGE(ItemType.WEAPON, true, false, true),
// Generic vanilla types
- ARMOR(SimpleStat.ItemType.ARMOR, false, true, true),
- ARMOR_TOUGHNESS(SimpleStat.ItemType.ARMOR, false, true, true),
+ ARMOR(SimpleStat.ItemType.BOTH, false, true, true),
+ ARMOR_TOUGHNESS(SimpleStat.ItemType.BOTH, false, true, true),
ATTACK_SPEED(SimpleStat.ItemType.BOTH, true, true, true),
BASE_ATTACK_SPEED(SimpleStat.ItemType.BOTH, false, true, true),
KNOCKBACK_RESISTANCE(SimpleStat.ItemType.BOTH, false, true, true),
MAX_HEALTH(SimpleStat.ItemType.BOTH, false, true, true),
- MOVEMENT_SPEED(SimpleStat.ItemType.ARMOR, true, true, true),
+ MOVEMENT_SPEED(SimpleStat.ItemType.BOTH, true, true, true),
// All the other types
- AOE_DAMAGE(SimpleStat.ItemType.WEAPON, true, false, true),
- PVP_DAMAGE(SimpleStat.ItemType.WEAPON, true, true, true),
- PVE_DAMAGE(SimpleStat.ItemType.WEAPON, true, true, true),
- DODGE_RATE(SimpleStat.ItemType.ARMOR, true, true, true),
- ACCURACY_RATE(SimpleStat.ItemType.WEAPON, true, true, true),
+ AOE_DAMAGE(SimpleStat.ItemType.BOTH, true, false, true),
+ PVP_DAMAGE(SimpleStat.ItemType.BOTH, true, true, true),
+ PVE_DAMAGE(SimpleStat.ItemType.BOTH, true, true, true),
+ DODGE_RATE(SimpleStat.ItemType.BOTH, true, true, true),
+ ACCURACY_RATE(SimpleStat.ItemType.BOTH, true, true, true),
BLOCK_RATE(SimpleStat.ItemType.BOTH, true, true, true),
- BLOCK_DAMAGE(SimpleStat.ItemType.ARMOR, true, true, true),
+ BLOCK_DAMAGE(SimpleStat.ItemType.BOTH, true, true, true),
LOOT_RATE(SimpleStat.ItemType.BOTH, true, true, true),
- BURN_RATE(SimpleStat.ItemType.WEAPON, true, true, true),
- PVP_DEFENSE(SimpleStat.ItemType.ARMOR, true, false, true),
- PVE_DEFENSE(SimpleStat.ItemType.ARMOR, true, true, true),
- CRITICAL_RATE(SimpleStat.ItemType.WEAPON, true, true, true),
- CRITICAL_DAMAGE(SimpleStat.ItemType.WEAPON, false, false, true),
+ BURN_RATE(SimpleStat.ItemType.BOTH, true, true, true),
+ PVP_DEFENSE(SimpleStat.ItemType.BOTH, true, false, true),
+ PVE_DEFENSE(SimpleStat.ItemType.BOTH, true, true, true),
+ CRITICAL_RATE(SimpleStat.ItemType.BOTH, true, true, true),
+ CRITICAL_DAMAGE(SimpleStat.ItemType.BOTH, false, false, true),
DURABILITY(SimpleStat.ItemType.BOTH, false, true, false),
- PENETRATION(SimpleStat.ItemType.WEAPON, true, true, true),
- VAMPIRISM(SimpleStat.ItemType.WEAPON, true, true, true),
- BLEED_RATE(SimpleStat.ItemType.WEAPON, true, true, true),
- DISARM_RATE(SimpleStat.ItemType.WEAPON, true, true, true),
+ PENETRATION(SimpleStat.ItemType.BOTH, true, true, true),
+ VAMPIRISM(SimpleStat.ItemType.BOTH, true, true, true),
+ BLEED_RATE(SimpleStat.ItemType.BOTH, true, true, true),
+ DISARM_RATE(SimpleStat.ItemType.BOTH, true, true, true),
SALE_PRICE(SimpleStat.ItemType.BOTH, true, true, false),
- THORNMAIL(SimpleStat.ItemType.ARMOR, true, false, true),
+ THORNMAIL(SimpleStat.ItemType.BOTH, true, false, true),
HEALTH_REGEN(SimpleStat.ItemType.BOTH, true, true, true),
MANA_REGEN(SimpleStat.ItemType.BOTH, true, true, true),
+ MAX_MANA(SimpleStat.ItemType.BOTH, false, false, true),
+ CC_RESISTANCE(SimpleStat.ItemType.BOTH, true, false, true),
+ CC_DURATION(SimpleStat.ItemType.BOTH, true, false, true),
+ HEALING_CAST(SimpleStat.ItemType.BOTH, true, false, true),
+ HEALING_RECEIVED(SimpleStat.ItemType.BOTH, true, false, true),
+ //PLACEHOLDERS
+ //MAGIC AND SUMMONS
+ SKILL_EFFECTIVNESS(SimpleStat.ItemType.BOTH, true, true, true),
+ SUMMON_POWER(SimpleStat.ItemType.BOTH, true, true, true),
+ SUMMON_HP(SimpleStat.ItemType.BOTH, true, true, true),
+ SUMMON_DURATION(SimpleStat.ItemType.BOTH, true, true, true),
+ //projectiles
+ PROJECTILE_COUNT(SimpleStat.ItemType.BOTH, false, true, true),
+ PROJECTILE_SPEED(SimpleStat.ItemType.BOTH, true, true, true),
+ //BLEED AND STUN
+ BLEED_STACKS(SimpleStat.ItemType.BOTH, false, true, true),
+ BLEED_DURATION(SimpleStat.ItemType.BOTH, false, true, true),
+ BLEED_DAMAGEBUFF(SimpleStat.ItemType.BOTH, true, true, true),
+ STUN_STACKS(SimpleStat.ItemType.BOTH, false, true, true),
+ STUN_DURATION(SimpleStat.ItemType.BOTH, true, true, true),
+ //
+ // Entity size (maps to generic.scale Bukkit attribute)
+ SCALE(SimpleStat.ItemType.BOTH, false, true, true),
+ // MC 1.21+ vanilla attributes
+ WATER_MOVEMENT_EFFICIENCY(SimpleStat.ItemType.BOTH, true, false, true),
+ MOVEMENT_EFFICIENCY(SimpleStat.ItemType.BOTH, true, false, true),
+ SNEAKING_SPEED(SimpleStat.ItemType.BOTH, true, false, true),
+ // MC 1.20.5+ vanilla attributes
+ BLOCK_BREAK_SPEED(SimpleStat.ItemType.BOTH, true, false, true),
+ BLOCK_INTERACTION_RANGE(SimpleStat.ItemType.BOTH, false, true, true),
+ ENTITY_INTERACTION_RANGE(SimpleStat.ItemType.BOTH, false, true, true),
+ EXPLOSION_KNOCKBACK_RESISTANCE(SimpleStat.ItemType.BOTH, true, false, true),
+ FALL_DAMAGE_MULTIPLIER(SimpleStat.ItemType.BOTH, true, true, true),
+ FLYING_SPEED(SimpleStat.ItemType.BOTH, false, false, true),
+ GRAVITY(SimpleStat.ItemType.BOTH, false, true, true),
+ JUMP_STRENGTH(SimpleStat.ItemType.BOTH, false, false, true),
+ MAX_ABSORPTION(SimpleStat.ItemType.BOTH, false, false, true),
+ MINING_EFFICIENCY(SimpleStat.ItemType.BOTH, false, false, true),
+ OXYGEN_BONUS(SimpleStat.ItemType.BOTH, false, false, true),
+ SAFE_FALL_DISTANCE(SimpleStat.ItemType.BOTH, false, false, true),
+ STEP_HEIGHT(SimpleStat.ItemType.BOTH, false, false, true),
+ SUBMERGED_MINING_SPEED(SimpleStat.ItemType.BOTH, true, false, true),
;
private final SimpleStat.ItemType type;
diff --git a/src/main/java/studio/magemonkey/divinity/stats/items/attributes/stats/DurabilityStat.java b/src/main/java/studio/magemonkey/divinity/stats/items/attributes/stats/DurabilityStat.java
index 3093139b..9e080f47 100644
--- a/src/main/java/studio/magemonkey/divinity/stats/items/attributes/stats/DurabilityStat.java
+++ b/src/main/java/studio/magemonkey/divinity/stats/items/attributes/stats/DurabilityStat.java
@@ -24,8 +24,12 @@
public class DurabilityStat extends ItemLoreStat implements TypedStat {
private double cap;
- public DurabilityStat(@NotNull String name, @NotNull String format, double cap) {
- super(TypedStat.Type.DURABILITY.name(),
+ public DurabilityStat(
+ @NotNull String name,
+ @NotNull String format,
+ double cap) {
+ super(
+ TypedStat.Type.DURABILITY.name(),
name,
format,
"%ITEM_STAT_" + TypedStat.Type.DURABILITY.name() + "%",
@@ -111,7 +115,9 @@ public boolean isBroken(@NotNull ItemStack item) {
return durability != null && durability[0] == 0 && !EngineCfg.ATTRIBUTES_DURABILITY_BREAK_ITEMS;
}
- public boolean reduceDurability(@NotNull LivingEntity li, @NotNull ItemStack item, int amount) {
+ public boolean reduceDurability(
+ @NotNull LivingEntity li, @NotNull ItemStack item, int amount) {
+
if (!(li instanceof Player) && !EngineCfg.ATTRIBUTES_DURABILITY_REDUCE_FOR_MOBS) return false;
if (this.isUnbreakable(item)) return false;
@@ -151,44 +157,38 @@ public boolean reduceDurability(@NotNull LivingEntity li, @NotNull ItemStack ite
}
boolean result = this.add(item, new double[]{lose, max}, -1);
-
- if (result) {
- syncVanillaBar(item, lose, max);
- }
-
+ if (result) syncVanillaBar(item);
return result;
-
- }
-
- @Override
- @NotNull
- public String formatValue(@NotNull ItemStack item, double[] values) {
- return EngineCfg.getDurabilityFormat((int) values[0], (int) values[1]);
}
/**
- * Syncs the durability stat with the vanilla durability bar. Should be called after any change to the durability stat.
- * Note: This method assumes that the durability stat is already updated with the new values before calling it.
- *
- * @param item the item that needs updating
- * @param current the current durability value on the item
- * @param maxCustom the max value possible to be set for the item
+ * Synchronizes the vanilla durability bar to reflect Divinity custom durability as a percentage.
+ * Safeguard: if vanilla bar would show 100% but Divinity dura is not max, vanilla bar shows at least 1 damage.
*/
- public void syncVanillaBar(@NotNull ItemStack item, double current, double maxCustom) {
- ItemMeta meta = item.getItemMeta();
- if (!(meta instanceof Damageable)) return;
-
- Damageable damageable = (Damageable) meta;
+ public void syncVanillaBar(@NotNull ItemStack item) {
+ double[] dur = this.getRaw(item);
+ if (dur == null || dur[1] <= 0) return;
+ if (!(item.getItemMeta() instanceof Damageable)) return;
+ double percent = dur[0] / dur[1];
int maxVanilla = item.getType().getMaxDurability();
if (maxVanilla <= 0) return;
- double percent = current / maxCustom;
- // Scale the vanilla value to the custom percentage
- int vanillaDamage = (int) ((1.0 - percent) * maxVanilla);
+ int vanillaDamage = (int) Math.round(maxVanilla * (1.0 - percent));
+
+ // Safeguard: don't show full vanilla bar when divinity dura is not max
+ if (vanillaDamage == 0 && dur[0] < dur[1]) {
+ vanillaDamage = 1;
+ }
- damageable.setDamage(vanillaDamage);
- item.setItemMeta(damageable);
+ Damageable meta = (Damageable) item.getItemMeta();
+ meta.setDamage(vanillaDamage);
+ item.setItemMeta((ItemMeta) meta);
}
-}
\ No newline at end of file
+ @Override
+ @NotNull
+ public String formatValue(@NotNull ItemStack item, double[] values) {
+ return EngineCfg.getDurabilityFormat((int) values[0], (int) values[1]);
+ }
+}
diff --git a/src/main/java/studio/magemonkey/divinity/stats/items/attributes/stats/DynamicBuffStat.java b/src/main/java/studio/magemonkey/divinity/stats/items/attributes/stats/DynamicBuffStat.java
new file mode 100644
index 00000000..ef72eb97
--- /dev/null
+++ b/src/main/java/studio/magemonkey/divinity/stats/items/attributes/stats/DynamicBuffStat.java
@@ -0,0 +1,156 @@
+package studio.magemonkey.divinity.stats.items.attributes.stats;
+
+import lombok.Getter;
+import org.bukkit.entity.Player;
+import org.bukkit.inventory.ItemStack;
+import org.bukkit.inventory.meta.ItemMeta;
+import org.bukkit.persistence.PersistentDataContainer;
+import org.bukkit.persistence.PersistentDataType;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import studio.magemonkey.codex.util.ItemUT;
+import studio.magemonkey.codex.util.NumberUT;
+import studio.magemonkey.codex.util.StringUT;
+import studio.magemonkey.divinity.config.EngineCfg;
+import studio.magemonkey.divinity.stats.bonus.BonusCalculator;
+import studio.magemonkey.divinity.stats.bonus.StatBonus;
+import studio.magemonkey.divinity.stats.items.ItemStats;
+import studio.magemonkey.divinity.stats.items.ItemTags;
+import studio.magemonkey.divinity.stats.items.api.DuplicableItemLoreStat;
+import studio.magemonkey.divinity.stats.items.api.DynamicStat;
+
+import java.util.*;
+import java.util.function.BiFunction;
+
+public class DynamicBuffStat extends DuplicableItemLoreStat implements DynamicStat {
+
+ public enum BuffTarget { DAMAGE, DEFENSE }
+
+ @Getter
+ private final BuffTarget buffTarget;
+ @Getter
+ private final String buffId;
+ @Getter
+ private final Set hooks;
+ @Getter
+ private final double capacity;
+
+ public DynamicBuffStat(
+ @NotNull BuffTarget buffTarget,
+ @NotNull String buffId,
+ @NotNull String name,
+ @NotNull String format,
+ @NotNull Set hooks,
+ double capacity
+ ) {
+ super(
+ buffTarget.name().toLowerCase() + "_buff_" + buffId.toLowerCase(),
+ name,
+ format,
+ "%" + buffTarget.name() + "_BUFF_" + buffId + "%",
+ buffTarget == BuffTarget.DAMAGE
+ ? ItemTags.TAG_ITEM_DAMAGE_BUFF
+ : ItemTags.TAG_ITEM_DEFENSE_BUFF,
+ StatBonus.DATA_TYPE
+ );
+ this.buffTarget = buffTarget;
+ this.buffId = buffId.toLowerCase();
+ this.hooks = hooks;
+ this.capacity = capacity;
+
+ ItemStats.registerDynamicStat(this);
+ }
+
+ @Override
+ @NotNull
+ public Class getParameterClass() {
+ return StatBonus.class;
+ }
+
+ public boolean isApplicableTo(@NotNull String typeId) {
+ return this.hooks.contains(typeId.toLowerCase());
+ }
+
+ public double getTotal(@NotNull ItemStack item, @Nullable Player player) {
+ return BonusCalculator.SIMPLE_FULL.apply(0D, get(item, player));
+ }
+
+ @NotNull
+ public List> get(@NotNull ItemStack item, @Nullable Player player) {
+ List> bonuses = new ArrayList<>();
+ double base = 0;
+ double percent = 0;
+
+ for (StatBonus bonus : this.getAllRaw(item)) {
+ if (!bonus.meetsRequirement(player)) continue;
+ double[] value = bonus.getValue();
+ if (value.length == 1 && bonus.isPercent()) {
+ percent += value[0];
+ } else {
+ base += value[0];
+ }
+ }
+
+ {
+ double finalBase = base;
+ bonuses.add((isPercent, input) -> isPercent ? input : input + finalBase);
+ double finalPercent = percent;
+ bonuses.add((isPercent, input) -> isPercent ? input + finalPercent : input);
+ }
+
+ return bonuses;
+ }
+
+ @Override
+ @NotNull
+ public String formatValue(@NotNull ItemStack item, @NotNull StatBonus statBonus) {
+ String sVal = NumberUT.format(statBonus.getValue()[0]);
+ if (statBonus.isPercent()) {
+ sVal += EngineCfg.LORE_CHAR_PERCENT;
+ }
+ return sVal;
+ }
+
+ @Override
+ @NotNull
+ public String getFormat(@Nullable Player p, @NotNull ItemStack item, @NotNull StatBonus value) {
+ StatBonus.Condition> condition = value.getCondition();
+ return StringUT.colorFix(super.getFormat(item, value)
+ .replace("%condition%", condition == null || !EngineCfg.LORE_STYLE_REQ_USER_DYN_UPDATE
+ ? ""
+ : condition.getFormat(p, item)));
+ }
+
+ @Override
+ @NotNull
+ public ItemStack updateItem(@Nullable Player p, @NotNull ItemStack item) {
+ ItemMeta meta = item.getItemMeta();
+ if (meta == null) return item;
+
+ int amount = this.getAmount(item);
+ if (amount == 0) return item;
+ List lore = meta.getLore();
+ if (lore == null) return item;
+
+ for (int i = 0; i < amount; i++) {
+ int loreIndex = -1;
+ String metaId = "";
+ for (org.bukkit.NamespacedKey key : this.keys) {
+ metaId = key.getKey() + i;
+ loreIndex = ItemUT.getLoreIndex(item, metaId);
+ if (loreIndex >= 0) break;
+ }
+ if (loreIndex < 0) continue;
+
+ @Nullable StatBonus arr = this.getRaw(item, i);
+ if (arr == null) continue;
+ String formatNew = this.getFormat(p, item, arr);
+ lore.set(loreIndex, formatNew);
+ meta.setLore(lore);
+ item.setItemMeta(meta);
+ ItemUT.addLoreTag(item, metaId, formatNew);
+ }
+
+ return item;
+ }
+}
diff --git a/src/main/java/studio/magemonkey/divinity/stats/items/attributes/stats/PenetrationStat.java b/src/main/java/studio/magemonkey/divinity/stats/items/attributes/stats/PenetrationStat.java
new file mode 100644
index 00000000..3d89fa64
--- /dev/null
+++ b/src/main/java/studio/magemonkey/divinity/stats/items/attributes/stats/PenetrationStat.java
@@ -0,0 +1,161 @@
+package studio.magemonkey.divinity.stats.items.attributes.stats;
+
+import lombok.Getter;
+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 studio.magemonkey.codex.util.ItemUT;
+import studio.magemonkey.codex.util.NumberUT;
+import studio.magemonkey.codex.util.StringUT;
+import studio.magemonkey.divinity.config.EngineCfg;
+import studio.magemonkey.divinity.stats.bonus.BonusCalculator;
+import studio.magemonkey.divinity.stats.bonus.StatBonus;
+import studio.magemonkey.divinity.stats.items.ItemStats;
+import studio.magemonkey.divinity.stats.items.ItemTags;
+import studio.magemonkey.divinity.stats.items.api.DuplicableItemLoreStat;
+import studio.magemonkey.divinity.stats.items.api.DynamicStat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.function.BiFunction;
+
+/**
+ * A configurable penetration stat read from penetration.yml.
+ *
+ *
+ *
{@code percent-pen: true} — reduces the victim's effective defense by a percentage.
+ * Works with LEGACY, CUSTOM and FACTOR defense formulas.
+ *
{@code percent-pen: false} — reduces the victim's effective defense by a flat value.
+ * Only applied under the CUSTOM defense formula; ignored for LEGACY/FACTOR.