diff --git a/src/main/java/studio/magemonkey/divinity/config/Config.java b/src/main/java/studio/magemonkey/divinity/config/Config.java index f2096441..509c3b02 100644 --- a/src/main/java/studio/magemonkey/divinity/config/Config.java +++ b/src/main/java/studio/magemonkey/divinity/config/Config.java @@ -18,6 +18,8 @@ import studio.magemonkey.divinity.stats.items.attributes.api.TypedStat; import studio.magemonkey.divinity.stats.items.attributes.stats.BleedStat; 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.stats.tiers.Tier; import studio.magemonkey.divinity.types.ItemGroup; import studio.magemonkey.divinity.types.ItemSubType; @@ -69,6 +71,9 @@ public void setupAttributes() { this.setupDamages(); this.setupDefense(); this.setupStats(); + this.setupDamageBuffs(); + this.setupDefenseBuffs(); + this.setupPenetrations(); this.setupHand(); this.setupAmmo(); this.setupSockets(); @@ -174,20 +179,14 @@ private void setupDefense() { private void setupStats() { JYML cfg; try { - cfg = JYML.loadOrExtract(plugin, "/item_stats/stats.yml"); + cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/general_stats.yml"); } catch (InvalidConfigurationException e) { this.plugin.error("Failed to load stats config (" + this.plugin.getName() - + "/item_stats/stats.yml): Configuration error"); + + "/item_stats/stats/general_stats.yml): Configuration error"); e.printStackTrace(); return; } - cfg.addMissing("ARMOR_TOUGHNESS.enabled", true); - cfg.addMissing("ARMOR_TOUGHNESS.name", "Armor Toughness"); - cfg.addMissing("ARMOR_TOUGHNESS.format", "&9▸ %name%: &f%value% %condition%"); - cfg.addMissing("ARMOR_TOUGHNESS.capacity", 100.0); - cfg.save(); - for (SimpleStat.Type statType : TypedStat.Type.values()) { String path2 = statType.name() + "."; if (!cfg.getBoolean(path2 + "enabled")) { @@ -214,6 +213,109 @@ private void setupStats() { } } + private void setupDamageBuffs() { + JYML cfg; + try { + cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/damage_buffs_percent.yml"); + } catch (InvalidConfigurationException e) { + this.plugin.error("Failed to load damage_buffs_percent config: Configuration error"); + e.printStackTrace(); + return; + } + + for (DamageAttribute dmg : ItemStats.getDamages()) { + String id = dmg.getId(); + String path = id + "."; + cfg.addMissing(path + "enabled", true); + cfg.addMissing(path + "name", dmg.getName() + " Buff %"); + cfg.addMissing(path + "format", "&3▸ %name%: &f%value%%condition%"); + cfg.addMissing(path + "capacity", -1.0); + cfg.addMissing(path + "hook", Collections.singletonList(id)); + } + cfg.saveChanges(); + + for (String buffId : cfg.getSection("")) { + if (!cfg.getBoolean(buffId + ".enabled")) continue; + String name = StringUT.color(cfg.getString(buffId + ".name", buffId)); + String format = StringUT.color(cfg.getString(buffId + ".format", "&3▸ %name%: &f%value%")); + double cap = cfg.getDouble(buffId + ".capacity", -1D); + Set hooks = new HashSet<>(cfg.getStringList(buffId + ".hook")); + + DynamicBuffStat buff = new DynamicBuffStat( + DynamicBuffStat.BuffTarget.DAMAGE, buffId, name, format, hooks, cap); + ItemStats.registerDamageBuff(buff); + } + } + + private void setupDefenseBuffs() { + JYML cfg; + try { + cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/defense_buffs_percent.yml"); + } catch (InvalidConfigurationException e) { + this.plugin.error("Failed to load defense_buffs_percent config: Configuration error"); + e.printStackTrace(); + return; + } + + for (DefenseAttribute def : ItemStats.getDefenses()) { + String id = def.getId(); + String path = id + "."; + cfg.addMissing(path + "enabled", true); + cfg.addMissing(path + "name", def.getName() + " Buff %"); + cfg.addMissing(path + "format", "&9▸ %name%: &f%value%%condition%"); + cfg.addMissing(path + "capacity", -1.0); + cfg.addMissing(path + "hook", Collections.singletonList(id)); + } + cfg.saveChanges(); + + for (String buffId : cfg.getSection("")) { + if (!cfg.getBoolean(buffId + ".enabled")) continue; + String name = StringUT.color(cfg.getString(buffId + ".name", buffId)); + String format = StringUT.color(cfg.getString(buffId + ".format", "&9▸ %name%: &f%value%")); + double cap = cfg.getDouble(buffId + ".capacity", -1D); + Set hooks = new HashSet<>(cfg.getStringList(buffId + ".hook")); + + DynamicBuffStat buff = new DynamicBuffStat( + DynamicBuffStat.BuffTarget.DEFENSE, buffId, name, format, hooks, cap); + ItemStats.registerDefenseBuff(buff); + } + } + + private void setupPenetrations() { + JYML cfg; + try { + cfg = JYML.loadOrExtract(plugin, "/item_stats/stats/penetration.yml"); + } catch (InvalidConfigurationException e) { + this.plugin.error("Failed to load penetration config: Configuration error"); + e.printStackTrace(); + return; + } + + // Auto-generate a flat-pen entry for every registered damage type (if missing) + for (DamageAttribute dmg : ItemStats.getDamages()) { + String id = dmg.getId() + "_pen"; + String path = id + "."; + cfg.addMissing(path + "enabled", true); + cfg.addMissing(path + "name", dmg.getName() + " Penetration"); + cfg.addMissing(path + "format", "&c▸ %name%: &f%value%%condition%"); + cfg.addMissing(path + "capacity", -1.0); + cfg.addMissing(path + "percent-pen", false); + cfg.addMissing(path + "hooks", Collections.singletonList(dmg.getId())); + } + cfg.saveChanges(); + + for (String penId : cfg.getSection("")) { + if (!cfg.getBoolean(penId + ".enabled")) continue; + String name = StringUT.color(cfg.getString(penId + ".name", penId)); + String format = StringUT.color(cfg.getString(penId + ".format", "&c▸ %name%: &f%value%")); + double cap = cfg.getDouble(penId + ".capacity", -1D); + boolean percentPen = cfg.getBoolean(penId + ".percent-pen", false); + Set hooks = new HashSet<>(cfg.getStringList(penId + ".hooks")); + + new PenetrationStat(penId, name, format, hooks, percentPen, cap); + } + } + private void setupHand() { JYML cfg; try { diff --git a/src/main/java/studio/magemonkey/divinity/config/EngineCfg.java b/src/main/java/studio/magemonkey/divinity/config/EngineCfg.java index ff940ab3..eaec0e04 100644 --- a/src/main/java/studio/magemonkey/divinity/config/EngineCfg.java +++ b/src/main/java/studio/magemonkey/divinity/config/EngineCfg.java @@ -57,6 +57,8 @@ public EngineCfg(@NotNull Divinity plugin) throws InvalidConfigurationException public static double COMBAT_SHIELD_BLOCK_BONUS_DAMAGE_MOD; public static int COMBAT_SHIELD_BLOCK_COOLDOWN; public static boolean LEGACY_COMBAT; + public static String DEFENSE_FORMULA_MODE; + public static String CUSTOM_DEFENSE_FORMULA; public static boolean FULL_LEGACY; public static boolean COMBAT_DISABLE_VANILLA_SWEEP; public static boolean COMBAT_REDUCE_PLAYER_HEALTH_BAR; @@ -110,8 +112,9 @@ public EngineCfg(@NotNull Divinity plugin) throws InvalidConfigurationException public static String LORE_STYLE_REQ_ITEM_MODULE_FORMAT_SEPAR; public static String LORE_STYLE_REQ_ITEM_MODULE_FORMAT_COLOR; - public static String LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN; - public static int LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN; + public static String LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN; + public static int LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN; + public static boolean LORE_STYLE_ENCHANTMENTS_ROMAN_SYSTEM; public static String LORE_STYLE_FABLED_ATTRIBUTE_FORMAT; @@ -211,6 +214,8 @@ public void setup() { path = "combat."; EngineCfg.LEGACY_COMBAT = cfg.getBoolean(path + "legacy-combat", false); + EngineCfg.DEFENSE_FORMULA_MODE = cfg.getString(path + "defense-formula", "FACTOR").toUpperCase(); + EngineCfg.CUSTOM_DEFENSE_FORMULA = cfg.getString(path + "custom-defense-formula", "damage*(25/(25+defense))"); EngineCfg.COMBAT_DISABLE_VANILLA_SWEEP = cfg.getBoolean(path + "disable-vanilla-sweep-attack"); EngineCfg.COMBAT_REDUCE_PLAYER_HEALTH_BAR = cfg.getBoolean(path + "compress-player-health-bar"); EngineCfg.COMBAT_FISHING_HOOK_DO_DAMAGE = cfg.getBoolean(path + "fishing-hook-do-damage"); @@ -418,9 +423,11 @@ public void setup() { path = "lore.stats.style.enchantments."; cfg.addMissing(path + "format.main", "&c▸ %name% %value%"); cfg.addMissing(path + "format.max-roman", 10); + cfg.addMissing(path + "roman-system", true); EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN = StringUT.color(cfg.getString(path + "format.main", "&c▸ %name% %value%")); EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN = cfg.getInt(path + "format.max-roman", 10); + EngineCfg.LORE_STYLE_ENCHANTMENTS_ROMAN_SYSTEM = cfg.getBoolean(path + "roman-system", true); path = "lore.stats.style.fabled-attribute-format"; cfg.addMissing(path, "&7%attrPre%&3%name%&7%attrPost%"); diff --git a/src/main/java/studio/magemonkey/divinity/hooks/external/FabledHook.java b/src/main/java/studio/magemonkey/divinity/hooks/external/FabledHook.java index 901c7cad..6177ece8 100644 --- a/src/main/java/studio/magemonkey/divinity/hooks/external/FabledHook.java +++ b/src/main/java/studio/magemonkey/divinity/hooks/external/FabledHook.java @@ -286,6 +286,20 @@ public void run() { }.runTaskLater(plugin, 1L); } + /** + * Scales a Divinity stat value using Fabled's attribute and stat modifier system. + * Fabled attributes.yml can reference Divinity stat names (lowercase type names, e.g. "critical_rate"). + */ + public double applyStatScale(@NotNull Player player, @NotNull String statId, double value) { + try { + PlayerData data = Fabled.getData(player); + if (data == null) return value; + return data.scaleStat(statId, value); + } catch (Exception ignored) { + return value; + } + } + public boolean isFakeDamage(EntityDamageByEntityEvent event) { return DefaultCombatProtection.isFakeDamageEvent(event); } diff --git a/src/main/java/studio/magemonkey/divinity/manager/damage/DamageManager.java b/src/main/java/studio/magemonkey/divinity/manager/damage/DamageManager.java index 242b3815..19e204dc 100644 --- a/src/main/java/studio/magemonkey/divinity/manager/damage/DamageManager.java +++ b/src/main/java/studio/magemonkey/divinity/manager/damage/DamageManager.java @@ -23,6 +23,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import studio.magemonkey.codex.api.items.PrefixHelper; +import studio.magemonkey.codex.util.eval.Evaluator; import studio.magemonkey.codex.hooks.Hooks; import studio.magemonkey.codex.manager.IListener; import studio.magemonkey.codex.registry.provider.DamageTypeProvider; @@ -46,6 +47,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.BleedStat; +import studio.magemonkey.divinity.stats.items.attributes.stats.DynamicBuffStat; +import studio.magemonkey.divinity.stats.items.attributes.stats.PenetrationStat; import java.util.*; import java.util.function.DoubleUnaryOperator; @@ -251,24 +254,105 @@ public void onDamageRPGStart(@NotNull DivinityDamageEvent.Start e) { if (!e.isExempt()) dmgType *= powerMod; dmgType *= blockMod; + // Apply damage buff % from attacker's equipment + if (statsDamager != null && dmgAtt != null) { + for (DynamicBuffStat buff : ItemStats.getDamageBuffs()) { + if (buff.isApplicableTo(dmgAtt.getId())) { + double buffPct = statsDamager.getDynamicBuff(buff); + if (buffPct != 0) dmgType *= (1.0 + buffPct / 100.0); + } + } + } + // Per-type penetration (PenetrationStat from penetration.yml) + // perTypePenMod: additional % pen multiplier (all formulas) + // perTypeFlatPen: flat defense reduction (CUSTOM formula only) + double perTypePenMod = 1.0; + double perTypeFlatPen = 0.0; + if (statsDamager != null && dmgAtt != null) { + for (PenetrationStat penStat : ItemStats.getPenetrations()) { + if (penStat.isApplicableTo(dmgAtt.getId())) { + double penValue = statsDamager.getPenetration(penStat); + if (penValue != 0) { + if (penStat.isPercentPen()) { + perTypePenMod *= Math.max(0D, 1.0 - penValue / 100.0); + } else { + perTypeFlatPen += penValue; + } + } + } + } + } double directType = dmgType * directMod; // Get direct value for this Damage Attribute dmgType = Math.max(0, dmgType - directType); // Deduct this value from damage if (dmgType > 0) { - DefenseAttribute defAtt = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; - if (defAtt != null && defenses.containsKey(defAtt)) { - double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod); - - double defCalced; - if (EngineCfg.LEGACY_COMBAT) { - defCalced = Math.max(0, dmgType * (1 - (def * defAtt.getProtectionFactor() * 0.01))); - } else { - defCalced = Math.max(0, + if (EngineCfg.LEGACY_COMBAT) { + // Legacy: 1:1, highest priority defense only + DefenseAttribute defAtt = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; + if (defAtt != null && defenses.containsKey(defAtt)) { + double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod * perTypePenMod); + // Apply defense buff % from victim's equipment + for (DynamicBuffStat dBuff : ItemStats.getDefenseBuffs()) { + if (dBuff.isApplicableTo(defAtt.getId())) { + double buffPct = statsVictim.getDynamicBuff(dBuff); + if (buffPct != 0) def *= (1.0 + buffPct / 100.0); + } + } + double defCalced = Math.max(0, dmgType * (1 - (def * defAtt.getProtectionFactor() * 0.01))); + meta.setDefendedDamage(defAtt, dmgType - defCalced); + dmgType = defCalced; + } + } else if ("CUSTOM".equals(EngineCfg.DEFENSE_FORMULA_MODE)) { + // Custom: collect ALL matching defenses (group sum + individual placeholders) + double totalDef = 0; + Map individualDefs = new HashMap<>(); + for (DefenseAttribute defAtt : ItemStats.getDefenses()) { + if (dmgAtt != null && defAtt.isBlockable(dmgAtt) && defenses.containsKey(defAtt)) { + double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod * perTypePenMod); + totalDef += def; + individualDefs.put(defAtt.getId(), def); + } + } + // Apply defense buff % from victim's equipment (on summed total) + if (totalDef > 0 && dmgAtt != null) { + for (DynamicBuffStat dBuff : ItemStats.getDefenseBuffs()) { + if (dBuff.isApplicableTo(dmgAtt.getId())) { + double buffPct = statsVictim.getDynamicBuff(dBuff); + if (buffPct != 0) totalDef *= (1.0 + buffPct / 100.0); + } + } + } + // Apply flat penetration to total defense (CUSTOM formula only) + if (perTypeFlatPen > 0) { + totalDef = Math.max(0, totalDef - perTypeFlatPen); + } + if (totalDef > 0) { + double defCalced = Math.max(0, evaluateDefenseFormula( + EngineCfg.CUSTOM_DEFENSE_FORMULA, dmgType, totalDef, toughness, individualDefs)); + DefenseAttribute primaryDef = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; + if (primaryDef != null) { + meta.setDefendedDamage(primaryDef, dmgType - defCalced); + } + dmgType = defCalced; + } + } else { + // Factor: 1:1, highest priority defense only (minecraft formula) + DefenseAttribute defAtt = dmgAtt != null ? dmgAtt.getAttachedDefense() : null; + if (defAtt != null && defenses.containsKey(defAtt)) { + double def = Math.max(0, defenses.get(defAtt) * pveDefenseMod * penetrateMod * perTypePenMod); + // Apply defense buff % from victim's equipment + for (DynamicBuffStat dBuff : ItemStats.getDefenseBuffs()) { + if (dBuff.isApplicableTo(defAtt.getId())) { + double buffPct = statsVictim.getDynamicBuff(dBuff); + if (buffPct != 0) def *= (1.0 + buffPct / 100.0); + } + } + double defCalced = Math.max(0, dmgType * (1 - Math.max(def / 5, def - 4 * dmgType / Math.max(1, toughness + 8)) * defAtt.getProtectionFactor() * 0.05)); + meta.setDefendedDamage(defAtt, dmgType - defCalced); + dmgType = defCalced; } - meta.setDefendedDamage(defAtt, dmgType - defCalced); - dmgType = defCalced; } } //Should we reactivate direct damage, remove directType here and deal the damage straight. @@ -569,4 +653,18 @@ public void onDamage(DivinityDamageEvent.BeforeScale event) { } return success[0]; } + + private static double evaluateDefenseFormula(String formula, double damage, double defense, + double toughness, Map individualDefs) { + String expr = formula + .replace("damage", String.valueOf(damage)) + .replace("toughness", String.valueOf(toughness)); + // Replace individual defense placeholders BEFORE the sum placeholder + // because "defense" is a prefix of "defense_" + for (Map.Entry entry : individualDefs.entrySet()) { + expr = expr.replace("defense_" + entry.getKey(), String.valueOf(entry.getValue())); + } + expr = expr.replace("defense", String.valueOf(defense)); + return Evaluator.eval(expr, 1); + } } diff --git a/src/main/java/studio/magemonkey/divinity/manager/listener/object/ItemDurabilityListener.java b/src/main/java/studio/magemonkey/divinity/manager/listener/object/ItemDurabilityListener.java index 42c3ee8d..203d2aa2 100644 --- a/src/main/java/studio/magemonkey/divinity/manager/listener/object/ItemDurabilityListener.java +++ b/src/main/java/studio/magemonkey/divinity/manager/listener/object/ItemDurabilityListener.java @@ -10,6 +10,7 @@ import org.bukkit.event.player.PlayerFishEvent; import org.bukkit.event.player.PlayerInteractEvent; import org.bukkit.event.player.PlayerItemDamageEvent; +import org.bukkit.event.player.PlayerItemMendEvent; import org.bukkit.inventory.EntityEquipment; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.NotNull; @@ -39,6 +40,22 @@ public void onDuraItemDamage(PlayerItemDamageEvent e) { } } + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) + public void onMending(PlayerItemMendEvent e) { + ItemStack item = e.getItem(); + if (!ItemStats.hasStat(item, null, TypedStat.Type.DURABILITY)) return; + + e.setCancelled(true); // block vanilla mending + + double[] dur = duraStat.getRaw(item); + if (dur == null || dur[1] <= 0 || dur[0] >= dur[1]) return; // full, unbreakable, or no data + + int repairAmount = e.getRepairAmount(); + double newCurrent = Math.min(dur[0] + repairAmount, dur[1]); + duraStat.add(item, new double[]{newCurrent, dur[1]}, -1); + duraStat.syncVanillaBar(item); + } + @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) public void onDuraBreak(BlockBreakEvent e) { Player player = e.getPlayer(); diff --git a/src/main/java/studio/magemonkey/divinity/manager/listener/object/VanillaWrapperListener.java b/src/main/java/studio/magemonkey/divinity/manager/listener/object/VanillaWrapperListener.java index f22bd4ff..f7d69c7d 100644 --- a/src/main/java/studio/magemonkey/divinity/manager/listener/object/VanillaWrapperListener.java +++ b/src/main/java/studio/magemonkey/divinity/manager/listener/object/VanillaWrapperListener.java @@ -167,6 +167,7 @@ public void onVanillaDamage(EntityDamageEvent e) { EntityStats statsDamager = null; EntityStats statsVictim = EntityStats.get(victim); + if (!(victim instanceof Player)) statsVictim.updateInventory(); DamageMeta meta = new DamageMeta(victim, damager, weapon, cause); statsVictim.setLastDamageMeta(meta); @@ -197,6 +198,7 @@ public void onVanillaDamage(EntityDamageEvent e) { meta.setDamager(damager); statsDamager = EntityStats.get(damager); + if (!(damager instanceof Player)) statsDamager.updateInventory(); statsDamager.setLastDamageMeta(meta); weapon = statsDamager.getItemInMainHand(); @@ -219,6 +221,7 @@ public void onVanillaDamage(EntityDamageEvent e) { damager = (LivingEntity) shooter; meta.setDamager(damager); statsDamager = EntityStats.get(damager); + if (!(damager instanceof Player)) statsDamager.updateInventory(); statsDamager.setLastDamageMeta(meta); weapon = ProjectileStats.getSrcWeapon(projectile); diff --git a/src/main/java/studio/magemonkey/divinity/modules/api/QModule.java b/src/main/java/studio/magemonkey/divinity/modules/api/QModule.java index a5d649e9..18a49560 100644 --- a/src/main/java/studio/magemonkey/divinity/modules/api/QModule.java +++ b/src/main/java/studio/magemonkey/divinity/modules/api/QModule.java @@ -6,6 +6,7 @@ import studio.magemonkey.divinity.Divinity; import studio.magemonkey.divinity.modules.api.socketing.ModuleSocket; import studio.magemonkey.divinity.modules.command.*; +import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager; public abstract class QModule extends IModule { @@ -29,6 +30,9 @@ protected void onPostSetup() { this.moduleCommand.addSubCommand(new MGiveCmd(md)); this.moduleCommand.addSubCommand(new MDropCmd(md)); this.moduleCommand.addSubCommand(new MListCmd(md)); + if (this instanceof ItemGeneratorManager) { + this.moduleCommand.addSubCommand(new MMobEquipCmd(md)); + } } this.moduleCommand.addSubCommand(new MReloadCmd(this)); } diff --git a/src/main/java/studio/magemonkey/divinity/modules/command/MDropCmd.java b/src/main/java/studio/magemonkey/divinity/modules/command/MDropCmd.java index e0b5ab02..62e1d27e 100644 --- a/src/main/java/studio/magemonkey/divinity/modules/command/MDropCmd.java +++ b/src/main/java/studio/magemonkey/divinity/modules/command/MDropCmd.java @@ -19,6 +19,8 @@ import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager; import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager.GeneratorItem; +import studio.magemonkey.divinity.utils.LoreUT; + import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -72,16 +74,20 @@ public List getTab(@NotNull Player player, int i, @NotNull String[] args if (i == 7) { return Arrays.asList("1", "10"); // Amount } - - // Support for material argument for ItemGenerator - if (i == 8 && this.module instanceof ItemGeneratorManager) { - ItemGeneratorManager itemGeneratorManager = (ItemGeneratorManager) this.module; - GeneratorItem generatorItem = itemGeneratorManager.getItemById(args[5]); - if (generatorItem != null) { - List list = generatorItem.getMaterialsList().stream() - .map(ItemType::getNamespacedID).collect(Collectors.toList()); - return list; + if (i == 8) { + List list = new java.util.ArrayList<>(); + if (this.module instanceof ItemGeneratorManager) { + ItemGeneratorManager itemGeneratorManager = (ItemGeneratorManager) this.module; + GeneratorItem generatorItem = itemGeneratorManager.getItemById(args[5]); + if (generatorItem != null) { + list.addAll(generatorItem.getMaterialsList().stream() + .map(ItemType::getNamespacedID).collect(Collectors.toList())); + } } + return list; + } + if (i == 9) { + return Arrays.asList("-noenchants"); } return super.getTab(player, i, args); } @@ -121,15 +127,20 @@ public void perform(@NotNull CommandSender sender, @NotNull String label, @NotNu amount = this.getNumI(sender, args[7], 1); } + boolean noEnchants = Arrays.stream(args).anyMatch(a -> a.equalsIgnoreCase("-noenchants")); + String id = args[5]; ItemStack item = null; Location loc = new Location(world, x, y, z); - ItemType material; - try { - material = args.length >= 9 ? CodexEngine.get().getItemManager().getItemType(args[8]) : null; - } catch (MissingProviderException | MissingItemException e) { - material = null; + // Find first material arg from position 8 onwards, skipping -noenchants + ItemType material = null; + for (int j = 8; j < args.length; j++) { + if (args[j].equalsIgnoreCase("-noenchants")) continue; + try { + material = CodexEngine.get().getItemManager().getItemType(args[j]); + break; + } catch (MissingProviderException | MissingItemException ignored) {} } ItemGeneratorManager itemGenerator = this.module instanceof ItemGeneratorManager ? (ItemGeneratorManager) this.module : null; @@ -146,7 +157,7 @@ public void perform(@NotNull CommandSender sender, @NotNull String label, @NotNu item = DivinityAPI.getItemByModule(this.module, id, iLevel, -1, -1); } if (item == null) continue; - + if (noEnchants) LoreUT.removeEnchants(item); world.dropItemNaturally(loc, item); String name = ItemUT.getItemName(item); diff --git a/src/main/java/studio/magemonkey/divinity/modules/command/MGiveCmd.java b/src/main/java/studio/magemonkey/divinity/modules/command/MGiveCmd.java index f75bfa0c..d44f0b62 100644 --- a/src/main/java/studio/magemonkey/divinity/modules/command/MGiveCmd.java +++ b/src/main/java/studio/magemonkey/divinity/modules/command/MGiveCmd.java @@ -17,6 +17,8 @@ import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager; import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager.GeneratorItem; +import studio.magemonkey.divinity.utils.LoreUT; + import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -61,17 +63,22 @@ public List getTab(@NotNull Player player, int i, @NotNull String[] args if (i == 4) { return Arrays.asList("1", "10"); } - - // Support for material argument for ItemGenerator - if (i == 5 && this.module instanceof ItemGeneratorManager) { - ItemGeneratorManager itemGeneratorManager = (ItemGeneratorManager) this.module; - GeneratorItem generatorItem = itemGeneratorManager.getItemById(args[2]); - if (generatorItem != null) { - List list = generatorItem.getMaterialsList().stream() - .map(ItemType::getNamespacedID).collect(Collectors.toList()); - return list; + if (i == 5) { + List list = new java.util.ArrayList<>(); + if (this.module instanceof ItemGeneratorManager) { + ItemGeneratorManager itemGeneratorManager = (ItemGeneratorManager) this.module; + GeneratorItem generatorItem = itemGeneratorManager.getItemById(args[2]); + if (generatorItem != null) { + list.addAll(generatorItem.getMaterialsList().stream() + .map(ItemType::getNamespacedID).collect(Collectors.toList())); + } } + return list; + } + if (i == 6) { + return Arrays.asList("-noenchants"); } + return super.getTab(player, i, args); } @@ -111,13 +118,18 @@ public void perform(@NotNull CommandSender sender, @NotNull String label, @NotNu amount = this.getNumI(sender, args[4], 1); } + boolean noEnchants = Arrays.stream(args).anyMatch(a -> a.equalsIgnoreCase("-noenchants")); + ItemStack item = null; - ItemType material; - try { - material = args.length >= 6 ? CodexEngine.get().getItemManager().getItemType(args[5]) : null; - } catch (MissingProviderException | MissingItemException e) { - material = null; + // Find first material arg from position 5 onwards, skipping -noenchants + ItemType material = null; + for (int j = 5; j < args.length; j++) { + if (args[j].equalsIgnoreCase("-noenchants")) continue; + try { + material = CodexEngine.get().getItemManager().getItemType(args[j]); + break; + } catch (MissingProviderException | MissingItemException ignored) {} } ItemGeneratorManager itemGenerator = this.module instanceof ItemGeneratorManager ? (ItemGeneratorManager) this.module : null; @@ -134,6 +146,7 @@ public void perform(@NotNull CommandSender sender, @NotNull String label, @NotNu item = DivinityAPI.getItemByModule(this.module, id, iLevel, -1, -1); } if (item == null) continue; + if (noEnchants) LoreUT.removeEnchants(item); ItemUT.addItem(p, item); String name = ItemUT.getItemName(item); diff --git a/src/main/java/studio/magemonkey/divinity/modules/command/MMobEquipCmd.java b/src/main/java/studio/magemonkey/divinity/modules/command/MMobEquipCmd.java new file mode 100644 index 00000000..49ce5fe8 --- /dev/null +++ b/src/main/java/studio/magemonkey/divinity/modules/command/MMobEquipCmd.java @@ -0,0 +1,184 @@ +package studio.magemonkey.divinity.modules.command; + +import org.bukkit.command.CommandSender; +import org.bukkit.entity.LivingEntity; +import org.bukkit.entity.Player; +import org.bukkit.inventory.EntityEquipment; +import org.bukkit.inventory.ItemStack; +import studio.magemonkey.divinity.utils.LoreUT; +import org.bukkit.util.RayTraceResult; +import org.jetbrains.annotations.NotNull; +import studio.magemonkey.codex.CodexEngine; +import studio.magemonkey.codex.api.items.ItemType; +import studio.magemonkey.codex.api.items.exception.MissingItemException; +import studio.magemonkey.codex.api.items.exception.MissingProviderException; +import studio.magemonkey.codex.util.random.Rnd; +import studio.magemonkey.divinity.Perms; +import studio.magemonkey.divinity.api.DivinityAPI; +import studio.magemonkey.divinity.modules.api.QModuleDrop; +import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager; +import studio.magemonkey.divinity.modules.list.itemgenerator.ItemGeneratorManager.GeneratorItem; +import studio.magemonkey.divinity.stats.EntityStats; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class MMobEquipCmd extends MCmd> { + + public MMobEquipCmd(@NotNull QModuleDrop m) { + super(m, new String[]{"mobequip"}, Perms.ADMIN); + } + + @Override + @NotNull + public String usage() { + return "/[module] mobequip [amount] [material] [-noenchants]"; + } + + @Override + @NotNull + public String description() { + return "Equip a generated item on the mob you are looking at."; + } + + @Override + public boolean playersOnly() { + return true; + } + + @Override + @NotNull + public List getTab(@NotNull Player player, int i, @NotNull String[] args) { + if (i == 1) { + return Arrays.asList("head", "chest", "legs", "feet", "hand", "offhand"); + } + if (i == 2) { + return module.getItemIds(); + } + if (i == 3) { + return Arrays.asList("[level]", "-1", "1"); + } + if (i == 4) { + return Arrays.asList("1"); + } + if (i == 5) { + List list = new java.util.ArrayList<>(); + if (this.module instanceof ItemGeneratorManager) { + ItemGeneratorManager igm = (ItemGeneratorManager) this.module; + GeneratorItem gi = igm.getItemById(args[2]); + if (gi != null) { + list.addAll(gi.getMaterialsList().stream() + .map(ItemType::getNamespacedID).collect(Collectors.toList())); + } + } + return list; + } + if (i == 6) { + return Arrays.asList("-noenchants"); + } + return super.getTab(player, i, args); + } + + @Override + public void perform(@NotNull CommandSender sender, @NotNull String label, @NotNull String[] args) { + if (!(sender instanceof Player)) return; + if (args.length < 4) { + this.printUsage(sender); + return; + } + + Player player = (Player) sender; + + RayTraceResult result = player.getWorld().rayTraceEntities( + player.getEyeLocation(), + player.getEyeLocation().getDirection(), + 10.0, + entity -> entity instanceof LivingEntity && !entity.equals(player) + ); + if (result == null || !(result.getHitEntity() instanceof LivingEntity)) { + sender.sendMessage("§cNo mob found in your line of sight (max 10 blocks)."); + return; + } + LivingEntity mob = (LivingEntity) result.getHitEntity(); + EntityEquipment equip = mob.getEquipment(); + if (equip == null) { + sender.sendMessage("§cThis entity does not support equipment."); + return; + } + + String slotArg = args[1].toLowerCase(); + String id = args[2]; + int level = this.getNumI(sender, args[3], -1, true); + boolean noEnchants = Arrays.stream(args).anyMatch(a -> a.equalsIgnoreCase("-noenchants")); + + int amount = 1; + if (args.length >= 5 && !args[4].equalsIgnoreCase("-noenchants")) { + amount = this.getNumI(sender, args[4], 1); + } + + // Find material arg (args[5+], skipping -noenchants) + ItemType material = null; + for (int j = 5; j < args.length; j++) { + if (args[j].equalsIgnoreCase("-noenchants")) continue; + try { + material = CodexEngine.get().getItemManager().getItemType(args[j]); + break; + } catch (MissingProviderException | MissingItemException ignored) {} + } + ItemGeneratorManager igm = this.module instanceof ItemGeneratorManager + ? (ItemGeneratorManager) this.module : null; + GeneratorItem generatorItem = igm != null ? igm.getItemById(id) : null; + + for (int i = 0; i < amount; i++) { + int iLevel = (level == -1) ? Rnd.get(1, 100) : level; + ItemStack item; + if (material != null && generatorItem != null) { + item = generatorItem.create(iLevel, -1, material); + } else { + item = DivinityAPI.getItemByModule(this.module, id, iLevel, -1, -1); + } + if (item == null) { + sender.sendMessage("§cFailed to generate item '" + id + "'."); + return; + } + if (noEnchants) LoreUT.removeEnchants(item); + + switch (slotArg) { + case "head": + case "helmet": + equip.setHelmet(item); + break; + case "chest": + case "chestplate": + equip.setChestplate(item); + break; + case "legs": + case "leggings": + equip.setLeggings(item); + break; + case "feet": + case "boots": + equip.setBoots(item); + break; + case "hand": + case "mainhand": + equip.setItemInMainHand(item); + break; + case "offhand": + equip.setItemInOffHand(item); + break; + default: + sender.sendMessage("§cUnknown slot '" + slotArg + "'. Use: head, chest, legs, feet, hand, offhand."); + return; + } + } + + // Refresh mob stats immediately so new equipment is reflected before next hit + EntityStats.get(mob).updateInventory(); + + sender.sendMessage("§aEquipped §f" + id + "§a (lv §f" + level + "§a) on §f" + + (mob.getCustomName() != null ? mob.getCustomName() : mob.getType().name()) + + "§a in slot §f" + slotArg + "§a."); + } +} diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/ItemGeneratorManager.java b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/ItemGeneratorManager.java index 00f4c4b8..4fd6c1c3 100644 --- a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/ItemGeneratorManager.java +++ b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/ItemGeneratorManager.java @@ -46,12 +46,15 @@ import studio.magemonkey.divinity.modules.list.itemgenerator.editor.AbstractEditorGUI; import studio.magemonkey.divinity.modules.list.itemgenerator.generators.AbilityGenerator; import studio.magemonkey.divinity.modules.list.itemgenerator.generators.AttributeGenerator; +import studio.magemonkey.divinity.modules.list.itemgenerator.generators.DuplicableStatGenerator; import studio.magemonkey.divinity.modules.list.itemgenerator.generators.SingleAttributeGenerator; import studio.magemonkey.divinity.modules.list.itemgenerator.generators.TypedStatGenerator; import studio.magemonkey.divinity.modules.list.sets.SetManager; import studio.magemonkey.divinity.stats.bonus.BonusMap; import studio.magemonkey.divinity.stats.bonus.StatBonus; import studio.magemonkey.divinity.stats.items.ItemStats; +import studio.magemonkey.divinity.stats.items.attributes.stats.DynamicBuffStat; +import studio.magemonkey.divinity.stats.items.attributes.stats.PenetrationStat; import studio.magemonkey.divinity.stats.items.ItemTags; import studio.magemonkey.divinity.stats.items.api.ItemLoreStat; import studio.magemonkey.divinity.stats.items.attributes.DamageAttribute; @@ -85,12 +88,15 @@ public class ItemGeneratorManager extends QModuleDrop { private static ResourceManager resourceManager; private ItemAbilityHandler abilityHandler; - public static final String PLACE_GEN_DAMAGE = "%GENERATOR_DAMAGE%"; - public static final String PLACE_GEN_DEFENSE = "%GENERATOR_DEFENSE%"; - public static final String PLACE_GEN_STATS = "%GENERATOR_STATS%"; - public static final String PLACE_GEN_SOCKETS = "%GENERATOR_SOCKETS_%TYPE%%"; - public static final String PLACE_GEN_ABILITY = "%GENERATOR_SKILLS%"; - public static final String PLACE_GEN_FABLED_ATTR = "%GENERATOR_FABLED_ATTR%"; + public static final String PLACE_GEN_DAMAGE = "%GENERATOR_DAMAGE%"; + public static final String PLACE_GEN_DEFENSE = "%GENERATOR_DEFENSE%"; + public static final String PLACE_GEN_STATS = "%GENERATOR_STATS%"; + public static final String PLACE_GEN_SOCKETS = "%GENERATOR_SOCKETS_%TYPE%%"; + public static final String PLACE_GEN_ABILITY = "%GENERATOR_SKILLS%"; + public static final String PLACE_GEN_FABLED_ATTR = "%GENERATOR_FABLED_ATTR%"; + public static final String PLACE_GEN_DAMAGE_BUFFS = "%GENERATOR_DAMAGE_BUFFS%"; + public static final String PLACE_GEN_DEFENSE_BUFFS = "%GENERATOR_DEFENSE_BUFFS%"; + public static final String PLACE_GEN_PENETRATION = "%GENERATOR_PENETRATION%"; public ItemGeneratorManager(@NotNull Divinity plugin) { super(plugin, GeneratorItem.class); @@ -617,6 +623,22 @@ public GeneratorItem(@NotNull Divinity plugin, @NotNull JYML cfg) { "generator.item-stats.", ItemStats.getStats(), ItemGeneratorManager.PLACE_GEN_STATS)); + + // DuplicableStatGenerators for damage buffs, defense buffs, and penetration. + // Each handles its own config section and auto-populates missing keys. + this.addAttributeGenerator(new DuplicableStatGenerator<>( + this.plugin, this, "generator.item-stats.", "list-damage-buffs", + ItemStats.getDamageBuffs(), DynamicBuffStat::getBuffId, + ItemGeneratorManager.PLACE_GEN_DAMAGE_BUFFS)); + this.addAttributeGenerator(new DuplicableStatGenerator<>( + this.plugin, this, "generator.item-stats.", "list-defense-buffs", + ItemStats.getDefenseBuffs(), DynamicBuffStat::getBuffId, + ItemGeneratorManager.PLACE_GEN_DEFENSE_BUFFS)); + this.addAttributeGenerator(new DuplicableStatGenerator<>( + this.plugin, this, "generator.item-stats.", "list-penetration", + ItemStats.getPenetrations(), PenetrationStat::getPenId, + ItemGeneratorManager.PLACE_GEN_PENETRATION)); + this.addAttributeGenerator( this.abilityGenerator = new AbilityGenerator(this.plugin, this, PLACE_GEN_ABILITY)); FabledHook fabledHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API); @@ -956,6 +978,9 @@ protected ItemStack build(int itemLvl, int uses, @Nullable ItemType mat) { LoreUT.replacePlaceholder(item, PLACE_GEN_DAMAGE, null); LoreUT.replacePlaceholder(item, PLACE_GEN_DEFENSE, null); + LoreUT.replacePlaceholder(item, PLACE_GEN_DAMAGE_BUFFS, null); + LoreUT.replacePlaceholder(item, PLACE_GEN_DEFENSE_BUFFS, null); + LoreUT.replacePlaceholder(item, PLACE_GEN_PENETRATION, null); LevelRequirement reqLevel = ItemRequirements.getUserRequirement(LevelRequirement.class); if (reqLevel != null) { @@ -1015,6 +1040,15 @@ protected ItemStack build(int itemLvl, int uses, @Nullable ItemType mat) { for (ItemLoreStat at : ItemStats.getDefenses()) { lore.remove(at.getPlaceholder()); } + for (DynamicBuffStat at : ItemStats.getDamageBuffs()) { + lore.remove(at.getPlaceholder()); + } + for (DynamicBuffStat at : ItemStats.getDefenseBuffs()) { + lore.remove(at.getPlaceholder()); + } + for (PenetrationStat at : ItemStats.getPenetrations()) { + lore.remove(at.getPlaceholder()); + } FabledHook fabledHook = (FabledHook) Divinity.getInstance().getHook(EHook.SKILL_API); if (fabledHook != null) { diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/MainStatsGUI.java b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/MainStatsGUI.java index 456ab784..5f97fbf9 100644 --- a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/MainStatsGUI.java +++ b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/MainStatsGUI.java @@ -147,7 +147,11 @@ public void onRightClick() { "&eModify")) { @Override public void onLeftClick() { - openSubMenu(new StatListGUI(player, itemGenerator, itemType)); + if (itemType == EditorGUI.ItemType.ITEM_STATS) { + openSubMenu(new StatCategoryGUI(player, itemGenerator)); + } else { + openSubMenu(new StatListGUI(player, itemGenerator, itemType)); + } } }); } diff --git a/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatCategoryGUI.java b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatCategoryGUI.java new file mode 100644 index 00000000..6ce0ff1f --- /dev/null +++ b/src/main/java/studio/magemonkey/divinity/modules/list/itemgenerator/editor/stats/StatCategoryGUI.java @@ -0,0 +1,77 @@ +package studio.magemonkey.divinity.modules.list.itemgenerator.editor.stats; + +import org.bukkit.Material; +import org.bukkit.entity.Player; +import studio.magemonkey.codex.manager.api.menu.Slot; +import studio.magemonkey.divinity.modules.list.itemgenerator.editor.AbstractEditorGUI; +import studio.magemonkey.divinity.modules.list.itemgenerator.editor.EditorGUI; + +/** + * Intermediate category-selection GUI shown when clicking the "List" button + * in the Item Stats editor (MainStatsGUI slot 3). + * + *

Presents four sub-categories: + *

    + *
  • General — classic TypedStat entries (list:)
  • + *
  • Damage % — DynamicBuffStat damage entries (list-damage-buffs:)
  • + *
  • Defense % — DynamicBuffStat defense entries (list-defense-buffs:)
  • + *
  • Penetration — PenetrationStat entries (list-penetration:)
  • + *
+ */ +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/repair/RepairManager.java b/src/main/java/studio/magemonkey/divinity/modules/list/repair/RepairManager.java index fb6a1214..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 @@ -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/stats/EntityStats.java b/src/main/java/studio/magemonkey/divinity/stats/EntityStats.java index 7c0d2bd8..e8cc0147 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]; @@ -576,6 +580,43 @@ private void updateBonusAttributes() { this.applyBonusAttribute(nbt, value); } + + // SCALE — handled separately because generic.scale only exists on MC 1.21+ + // and is not part of the NBTAttribute enum to avoid codex-api recompilation. + try { + Attribute scaleAttr = (Attribute) VersionManager.getNms().getAttribute("SCALE"); + if (scaleAttr != null) { + TypedStat scaleStat = ItemStats.getStat(TypedStat.Type.SCALE); + if (scaleStat instanceof SimpleStat) { + SimpleStat ss = (SimpleStat) scaleStat; + List> bonuses = 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(); + value = value / 100D; // stat 100 = +1.0 over base scale of 1.0 → 2× size + this.applyScaleAttribute(scaleAttr, value); + } + } + } catch (Exception ignored) { /* SCALE not supported on this server version */ } + } + + private static final UUID SCALE_MODIFIER_UUID = UUID.fromString("d141e000-5ca1-4000-0000-000000000001"); + + @SuppressWarnings("deprecation") + private void applyScaleAttribute(@NotNull Attribute scaleAttr, double value) { + AttributeInstance attInst = this.entity.getAttribute(scaleAttr); + if (attInst == null) return; + for (AttributeModifier mod : new HashSet<>(attInst.getModifiers())) { + try { + if (SCALE_MODIFIER_UUID.equals(mod.getUniqueId())) { + if (mod.getAmount() == value) return; + attInst.removeModifier(mod); + break; + } + } catch (Exception ignored) {} + } + if (value == 0D) return; + attInst.addModifier(new AttributeModifier(SCALE_MODIFIER_UUID, "divinity.scale", value, Operation.ADD_NUMBER)); } private void applyBonusAttribute(@NotNull NBTAttribute att, double value) { @@ -684,6 +725,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); @@ -717,6 +762,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); } @@ -779,6 +828,54 @@ 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) { + value = fHook.applyStatScale(this.player, buff.getId(), value); + } + } return value; } 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 409d85a5..59aa4954 100644 --- a/src/main/java/studio/magemonkey/divinity/stats/items/ItemStats.java +++ b/src/main/java/studio/magemonkey/divinity/stats/items/ItemStats.java @@ -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; 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..68982504 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 @@ -79,6 +79,21 @@ enum Type { THORNMAIL(SimpleStat.ItemType.ARMOR, true, false, true), HEALTH_REGEN(SimpleStat.ItemType.BOTH, true, true, true), MANA_REGEN(SimpleStat.ItemType.BOTH, true, true, true), + CC_RESISTANCE(SimpleStat.ItemType.ARMOR, true, false, true), + HEALING_CAST(SimpleStat.ItemType.WEAPON, true, false, true), + HEALING_RECEIVED(SimpleStat.ItemType.ARMOR, true, false, true), + + //PLACEHOLDERS + //MAGIC AND SUMMONS + SKILL_EFFECTIVNESS(SimpleStat.ItemType.BOTH, true, true, true), + PHYSICAL_SKILL_DAMAGE(SimpleStat.ItemType.BOTH, true, true, true), + MAGICIAL_SKILL_DAMAGE(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), + + // Entity size (maps to generic.scale Bukkit attribute) + SCALE(SimpleStat.ItemType.BOTH, false, true, 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 347a1e7c..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 @@ -6,6 +6,7 @@ import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.Damageable; import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -155,7 +156,34 @@ public boolean reduceDurability( } } - return this.add(item, new double[]{lose, max}, -1); + boolean result = this.add(item, new double[]{lose, max}, -1); + if (result) syncVanillaBar(item); + return result; + } + + /** + * 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[] 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; + + 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 meta = (Damageable) item.getItemMeta(); + meta.setDamage(vanillaDamage); + item.setItemMeta((ItemMeta) meta); } @Override 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.
  • + *
+ * + * Each stat declares {@code hooks} — a list of damage-type IDs (matching damage.yml keys) + * that this penetration applies to. + */ +public class PenetrationStat extends DuplicableItemLoreStat implements DynamicStat { + + @Getter private final String penId; + @Getter private final Set hooks; // damage-type IDs this pen applies to + @Getter private final boolean percentPen; // true = %, false = flat + @Getter private final double capacity; + + public PenetrationStat( + @NotNull String penId, + @NotNull String name, + @NotNull String format, + @NotNull Set hooks, + boolean percentPen, + double capacity + ) { + super( + "penetration_" + penId.toLowerCase(), + name, + format, + "%PENETRATION_" + penId.toUpperCase() + "%", + ItemTags.TAG_ITEM_PENETRATION, + StatBonus.DATA_TYPE + ); + this.penId = penId.toLowerCase(); + this.hooks = hooks; + this.percentPen = percentPen; + this.capacity = capacity; + + ItemStats.registerPenetration(this); + ItemStats.registerDynamicStat(this); + } + + @Override + @NotNull + public Class getParameterClass() { + return StatBonus.class; + } + + /** Returns true if this penetration applies to the given damage type. */ + public boolean isApplicableTo(@NotNull String damageTypeId) { + return this.hooks.contains(damageTypeId.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]; + } + } + + final double fb = base; + final double fp = percent; + bonuses.add((isPercent, input) -> isPercent ? input : input + fb); + bonuses.add((isPercent, input) -> isPercent ? input + fp : input); + + return bonuses; + } + + @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; + } + + @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; + } +} diff --git a/src/main/java/studio/magemonkey/divinity/utils/LoreUT.java b/src/main/java/studio/magemonkey/divinity/utils/LoreUT.java index b79ec62a..2d10276f 100644 --- a/src/main/java/studio/magemonkey/divinity/utils/LoreUT.java +++ b/src/main/java/studio/magemonkey/divinity/utils/LoreUT.java @@ -9,7 +9,9 @@ import studio.magemonkey.divinity.Divinity; import studio.magemonkey.divinity.config.EngineCfg; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; public class LoreUT { @@ -127,8 +129,10 @@ public static void replaceEnchants(@NotNull ItemStack item) { String value = EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN .replace("%name%", plugin.lang().getEnchantment(e)) .replace("%value%", - level > EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN ? String.valueOf(level) - : NumberUT.toRoman(level)); + !EngineCfg.LORE_STYLE_ENCHANTMENTS_ROMAN_SYSTEM ? String.valueOf(level) + : (level > EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN + ? String.valueOf(level) + : NumberUT.toRoman(level))); lore.add(pos, value); } meta.setLore(lore); @@ -136,4 +140,44 @@ public static void replaceEnchants(@NotNull ItemStack item) { replacePlaceholder(item, "%ENCHANTS%", null); } + + /** + * Removes all enchantments from both the item's NBT/meta and its lore. + * Mirrors the same format used by {@link #replaceEnchants} so the generated + * lore lines are identified and stripped correctly. + * Call this instead of the raw {@code meta.removeEnchant} loop when you + * want the lore to stay clean (e.g. the {@code -noenchants} command flag). + */ + public static void removeEnchants(@NotNull ItemStack item) { + ItemMeta meta = item.getItemMeta(); + if (meta == null) return; + + // Snapshot before clearing so we know which lore lines to remove + Map enchants = new HashMap<>(meta.getEnchants()); + + // Strip lore lines that replaceEnchants() would have added + List lore = meta.getLore(); + if (lore != null) { + for (Map.Entry entry : enchants.entrySet()) { + Enchantment e = entry.getKey(); + int level = entry.getValue(); + String line = EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAIN + .replace("%name%", plugin.lang().getEnchantment(e)) + .replace("%value%", + !EngineCfg.LORE_STYLE_ENCHANTMENTS_ROMAN_SYSTEM + ? String.valueOf(level) + : (level > EngineCfg.LORE_STYLE_ENCHANTMENTS_FORMAT_MAX_ROMAN + ? String.valueOf(level) + : NumberUT.toRoman(level))); + lore.remove(studio.magemonkey.codex.util.StringUT.color(line)); + lore.remove(line); // fallback – uncolored form + } + lore.remove("%ENCHANTS%"); // placeholder if still present (shouldn't happen after generation) + meta.setLore(lore); + } + + // Remove from meta enchant map + enchants.keySet().forEach(meta::removeEnchant); + item.setItemMeta(meta); + } } diff --git a/src/main/resources/engine.yml b/src/main/resources/engine.yml index 8f1ad776..38a6c138 100644 --- a/src/main/resources/engine.yml +++ b/src/main/resources/engine.yml @@ -69,6 +69,16 @@ attributes: combat: # Whether to use the old combat formula for calculating defenses legacy-combat: false + # Defense formula. Only used when legacy-combat is false. + # Options: FACTOR (minecraft formula), CUSTOM + defense-formula: FACTOR + # Custom defense formula. Only used when defense-formula is CUSTOM. + # Placeholders: damage, defense (sum of all matching defenses), + # defense_ (individual defense, e.g. defense_weapon, defense_physical), + # toughness + # In CUSTOM mode, ALL defenses with matching block-damage-types are summed. + # In FACTOR mode, only the highest priority defense is used (1:1). + custom-defense-formula: 'damage*(25/(25+defense))' # Shield settings. shield: block: @@ -228,6 +238,7 @@ lore: format: main: '&c▸ %name%: %value%' enchantments: + roman-system: true # true = roman numerals (I, II, III...), false = arabic numbers (1, 2, 3...) format: main: '&c▸ %name% %value%' max-roman: 10 diff --git a/src/main/resources/item_stats/stats.yml b/src/main/resources/item_stats/stats.yml index bdeaa360..ba9411bf 100644 --- a/src/main/resources/item_stats/stats.yml +++ b/src/main/resources/item_stats/stats.yml @@ -188,4 +188,42 @@ MANA_REGEN: enabled: true name: Mana Regen format: '&9▸ %name%: &f%value% %condition%' + capacity: -1.0 + +# IT DOES NOTHING, THEY ARE JUST A PLACEHOLDERS FOR FABLED + +SKILL_EFFECTIVNESS: + enabled: true + name: Skill effectivness + format: '&9▸ %name%: &f%value% %condition%' + capacity: -1.0 + +PHYSICAL_SKILL_DAMAGE: + enabled: true + name: Physical Skill Damage + format: '&9▸ %name%: &f%value% %condition%' + capacity: -1.0 + +MAGICIAL_SKILL_DAMAGE: + enabled: true + name: Magicial Skill Damage + format: '&9▸ %name%: &f%value% %condition%' + capacity: -1.0 + +SUMMON_POWER: + enabled: true + name: Summon Power + format: '&9▸ %name%: &f%value% %condition%' + capacity: -1.0 + +SUMMON_HP: + enabled: true + name: Summon hp + format: '&9▸ %name%: &f%value% %condition%' + capacity: -1.0 + +SUMMON_DURATION: + enabled: true + name: Summon draution + format: '&9▸ %name%: &f%value% %condition%' capacity: -1.0 \ No newline at end of file diff --git a/src/main/resources/item_stats/stats/damage_buffs_percent.yml b/src/main/resources/item_stats/stats/damage_buffs_percent.yml new file mode 100644 index 00000000..2b0d6365 --- /dev/null +++ b/src/main/resources/item_stats/stats/damage_buffs_percent.yml @@ -0,0 +1,15 @@ +# Damage Buff % stats — auto-generated from damage.yml on first server start. +# Each entry buffs the listed damage type(s) by a percentage. +# You can manually add cross-type buffs by listing multiple IDs under 'hook'. +# +# Example: +# slashing_and_piercing: +# enabled: true +# name: '&3Slashing & Piercing Buff %' +# format: '&3▸ %name%: &f%value%%condition%' +# capacity: -1 +# hook: +# - slashing +# - piercing +# PLACEHOLDER: %DAMAGE_BUFF_{ID}% + diff --git a/src/main/resources/item_stats/stats/defense_buffs_percent.yml b/src/main/resources/item_stats/stats/defense_buffs_percent.yml new file mode 100644 index 00000000..3db96034 --- /dev/null +++ b/src/main/resources/item_stats/stats/defense_buffs_percent.yml @@ -0,0 +1,14 @@ +# Defense Buff % stats — auto-generated from defense.yml on first server start. +# Each entry buffs the listed defense type(s) by a percentage. +# You can manually add cross-type buffs by listing multiple IDs under 'hook'. +# +# Example: +# physical_and_pierce: +# enabled: true +# name: '&9Physical & Pierce Defense Buff %' +# format: '&9▸ %name%: &f%value%%condition%' +# capacity: -1 +# hook: +# - physical +# - pierce +# PLACEHOLDER: %DEFENSE_BUFF_{ID}% diff --git a/src/main/resources/item_stats/stats/general_stats.yml b/src/main/resources/item_stats/stats/general_stats.yml new file mode 100644 index 00000000..a3c72521 --- /dev/null +++ b/src/main/resources/item_stats/stats/general_stats.yml @@ -0,0 +1,210 @@ +#DIRECT_DAMAGE: +# enabled: true +# name: Direct Damage +# format: '&f▸ %name%: &f%value%' +# capacity: 100.0 + +# Vanilla Stats +ARMOR: + enabled: true + name: Armor + format: '&3▸ %name%: &f%value% %condition%' + capacity: -1 + +ARMOR_TOUGHNESS: + enabled: true + name: Armor Toughness + format: '&9▸ %name%: &f%value% %condition%' + capacity: 100.0 + +BASE_ATTACK_SPEED: + enabled: true + name: Base Attack Speed + format: '&e▸ %name%: &f%value% %condition%' + capacity: -1 + +ATTACK_SPEED: + enabled: true + name: Attack Speed + format: '&e▸ %name%: &f%value% %condition%' + capacity: -1 + +KNOCKBACK_RESISTANCE: + enabled: true + name: Knockback Resistance + format: '&e▸ %name%: &f%value% %condition%' + capacity: -1 + +MOVEMENT_SPEED: + enabled: true + name: Movement Speed + format: '&e▸ %name%: &f%value% %condition%' + capacity: 70.0 + +MAX_HEALTH: + enabled: true + name: Max Health + format: '&c▸ %name%: &f%value% %condition%' + capacity: -1 + +# Custom Stats +AOE_DAMAGE: + enabled: true + name: AoE Damage + format: '&3▸ %name%: &f%value% %condition%' + capacity: -1 + +PVP_DAMAGE: + enabled: true + name: PvP Damage + format: '&b▸ %name%: &f%value% %condition%' + capacity: 200.0 + +PVE_DAMAGE: + enabled: true + name: PvE Damage + format: '&b▸ %name%: &f%value% %condition%' + capacity: 200.0 + +DODGE_RATE: + enabled: true + name: Dodge Rate + format: '&6▸ %name%: &f%value% %condition%' + capacity: 45.0 + +ACCURACY_RATE: + enabled: true + name: Accuracy Rate + format: '&6▸ %name%: &f%value% %condition%' + capacity: 30.0 + +BLOCK_RATE: + enabled: true + name: Block Rate + format: '&6▸ %name%: &f%value% %condition%' + capacity: 100.0 + +BLOCK_DAMAGE: + enabled: true + name: Block Damage + format: '&6▸ %name%: &f%value% %condition%' + capacity: 100.0 + +CRITICAL_RATE: + enabled: true + name: Crit. Strike Rate + format: '&a▸ %name%: &f%value% %condition%' + capacity: 100.0 + +CRITICAL_DAMAGE: + enabled: true + name: Crit. Strike Dmg + format: '&a▸ %name%: &f%value% %condition%' + capacity: 3.5 + +PVP_DEFENSE: + enabled: true + name: PvP Defense + format: '&b▸ %name%: &f%value% %condition%' + capacity: 100.0 + +PVE_DEFENSE: + enabled: true + name: PvE Defense + format: '&b▸ %name%: &f%value% %condition%' + capacity: 100.0 + +LOOT_RATE: + enabled: true + name: Loot Rate + format: '&e▸ %name%: &f%value% %condition%' + capacity: 250.0 + +DURABILITY: + enabled: true + name: Durability + format: '&7▸ %name%: &f%value%' + capacity: -1 + +PENETRATION: + enabled: true + name: Armor Penetration + format: '&c▸ %name%: &f%value% %condition%' + capacity: 60.0 + +VAMPIRISM: + enabled: true + name: Vampirism + format: '&c▸ %name%: &f%value% %condition%' + capacity: 35.0 + +BURN_RATE: + enabled: true + name: Chance to Burn + format: '&c▸ %name%: &f%value% %condition%' + capacity: 100.0 + +SALE_PRICE: + enabled: true + name: Sale Price + format: '&d▸ %name%: &f%value% %condition%' + capacity: -1 + +BLEED_RATE: + enabled: true + name: Chance to Open Wounds + format: '&c▸ %name%: &f%value% %condition%' + capacity: 75.0 + settings: + damage: '%damage% * 0.5' + of-max-health: false + duration: 10.0 + +DISARM_RATE: + enabled: true + name: Chance to Disarm + format: '&c▸ %name%: &f%value% %condition%' + capacity: 25.0 + +THORNMAIL: + enabled: true + name: Thornmail + format: '&c▸ %name%: &f%value% %condition%' + capacity: 35.0 + +HEALTH_REGEN: + enabled: true + name: Health Regen + format: '&c▸ %name%: &f%value% %condition%' + capacity: -1.0 + +MANA_REGEN: + enabled: true + name: Mana Regen + format: '&9▸ %name%: &f%value% %condition%' + capacity: -1.0 + +# CC & Healing Stats +CC_RESISTANCE: + enabled: true + name: CC Resistance + format: '&e▸ %name%: &f%value% %condition%' + capacity: 60.0 + +HEALING_CAST: + enabled: true + name: Healing Cast + format: '&a▸ %name%: &f%value% %condition%' + capacity: -1 + +HEALING_RECEIVED: + enabled: true + name: Healing Received + format: '&a▸ %name%: &f%value% %condition%' + capacity: -1 + +SCALE: + enabled: true + name: Scale + format: '&b▸ %name%: &f%value% %condition%' + capacity: 200.0 diff --git a/src/main/resources/item_stats/stats/penetration.yml b/src/main/resources/item_stats/stats/penetration.yml new file mode 100644 index 00000000..66b8aee1 --- /dev/null +++ b/src/main/resources/item_stats/stats/penetration.yml @@ -0,0 +1,76 @@ +# ============================================================ +# Penetration Stats — penetration.yml +# ============================================================ +# +# Each entry defines one penetration stat that players can roll +# on their items. On server start, an entry is auto-generated +# for every registered damage type (damage.yml) if missing. +# +# Fields: +# enabled — whether this penetration stat is active +# name — display name (supports color codes) +# format — lore line format. Placeholders: %name%, %value%, %condition% +# capacity — maximum value (-1 = unlimited) +# percent-pen — true = % penetration (reduces defense by a percentage) +# false = flat penetration (subtracts flat defense value) +# NOTE: flat penetration only works with CUSTOM defense formula. +# hooks — list of damage-type IDs (from damage.yml) this stat applies to +# +# Example: a sword enchanted with 30 physical_pen (flat) will subtract +# 30 from the target's physical defense before damage is calculated. +# PLACEHOLDER: %PENETRATION_{ID}% +# ============================================================ + +physical_pen: + enabled: true + name: '&cPhysical Penetration' + format: '&c▸ %name%: &f%value%%condition%' + capacity: -1 + percent-pen: false + hooks: + - physical + +magical_pen: + enabled: true + name: '&dMagical Penetration' + format: '&d▸ %name%: &f%value%%condition%' + capacity: 60 + percent-pen: true + hooks: + - magical + +fire_pen: + enabled: true + name: '&6Fire Penetration' + format: '&6▸ %name%: &f%value%%condition%' + capacity: -1 + percent-pen: false + hooks: + - fire + +poison_pen: + enabled: true + name: '&2Poison Penetration' + format: '&2▸ %name%: &f%value%%condition%' + capacity: -1 + percent-pen: false + hooks: + - poison + +water_pen: + enabled: true + name: '&bWater Penetration' + format: '&b▸ %name%: &f%value%%condition%' + capacity: -1 + percent-pen: false + hooks: + - water + +wind_pen: + enabled: true + name: '&fWind Penetration' + format: '&f▸ %name%: &f%value%%condition%' + capacity: -1 + percent-pen: false + hooks: + - wind diff --git a/src/main/resources/modules/item_generator/items/common.yml b/src/main/resources/modules/item_generator/items/common.yml index 22a57731..37ded3f2 100644 --- a/src/main/resources/modules/item_generator/items/common.yml +++ b/src/main/resources/modules/item_generator/items/common.yml @@ -16,7 +16,13 @@ lore: - '%GENERATOR_SKILLS%' - '%GENERATOR_DEFENSE%' - '%GENERATOR_DAMAGE%' + - '%GENERATOR_DAMAGE_BUFFS%' + - '%GENERATOR_DEFENSE_BUFFS%' + - '%GENERATOR_PENETRATION%' - '%GENERATOR_STATS%' + - '%GENERATOR_DAMAGE_BUFFS%' + - '%GENERATOR_DEFENSE_BUFFS%' + - '%GENERATOR_PENETRATION%' - '%GENERATOR_FABLED_ATTR%' - '%GENERATOR_SOCKETS_GEM%' - '%GENERATOR_SOCKETS_ESSENCE%' @@ -265,6 +271,36 @@ generator: min: 0 max: 0 flat-range: false + list-damage-buffs: + # Per-damage-type % damage buff stats (DynamicBuffStat DAMAGE) + # Add entries here matching IDs from damage_buffs_percent.yml + # Example: + # physical: + # chance: 10.0 + # scale-by-level: 1.025 + # min: 5.0 + # max: 15.0 + # flat-range: false + list-defense-buffs: + # Per-damage-type % defense buff stats (DynamicBuffStat DEFENSE) + # Add entries here matching IDs from defense_buffs_percent.yml + # Example: + # physical: + # chance: 10.0 + # scale-by-level: 1.025 + # min: 5.0 + # max: 15.0 + # flat-range: false + list-penetration: + # Per-damage-type penetration stats (PenetrationStat) + # Add entries here matching IDs from penetration.yml + # Example: + # physical_pen: + # chance: 6.0 + # scale-by-level: 1.025 + # min: 4.0 + # max: 10.0 + # flat-range: false fabled-attributes: minimum: 1 maximum: 4 diff --git a/src/test/resources/common.yml b/src/test/resources/common.yml index 2c17c913..89cf3fd6 100644 --- a/src/test/resources/common.yml +++ b/src/test/resources/common.yml @@ -16,6 +16,9 @@ lore: - '%GENERATOR_SKILLS%' - '%GENERATOR_DEFENSE%' - '%GENERATOR_DAMAGE%' + - '%GENERATOR_DAMAGE_BUFFS%' + - '%GENERATOR_DEFENSE_BUFFS%' + - '%GENERATOR_PENETRATION%' - '%GENERATOR_STATS%' - '%GENERATOR_FABLED_ATTR%' - '%GENERATOR_SOCKETS_GEM%'