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..399877f1 --- /dev/null +++ b/src/main/java/studio/magemonkey/divinity/modules/command/MMobEquipCmd.java @@ -0,0 +1,223 @@ +package studio.magemonkey.divinity.modules.command; + +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Entity; +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.UUID; +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 [uuid] [amount] [material] [-noenchants]"; + } + + @Override + @NotNull + public String description() { + return "Equip a generated item on a mob. With [uuid]: targets by UUID (usable from console/MythicMobs). Without: targets the mob you are looking at (player only)."; + } + + @Override + public boolean playersOnly() { + return false; + } + + @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 (args.length < 4) { + this.printUsage(sender); + return; + } + + // Detect UUID mode: args[1] looks like a UUID → console/MythicMobs targeting + // Otherwise: ray-trace mode (requires Player sender) + LivingEntity mob; + int argOffset; // index where starts + + UUID targetUuid = tryParseUUID(args[1]); + if (targetUuid != null) { + // UUID mode — works from console, MythicMobs, or player + if (args.length < 5) { + this.printUsage(sender); + return; + } + Entity entity = Bukkit.getEntity(targetUuid); + if (!(entity instanceof LivingEntity)) { + sender.sendMessage("§cNo living entity found with UUID §f" + targetUuid + "§c."); + return; + } + mob = (LivingEntity) entity; + argOffset = 2; // slot = args[2], id = args[3], level = args[4], ... + } else { + // Ray-trace mode — player only + if (!(sender instanceof Player)) { + sender.sendMessage("§cProvide a target UUID when running this command from console/MythicMobs."); + sender.sendMessage("§cUsage: " + this.usage()); + 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; + } + mob = (LivingEntity) result.getHitEntity(); + argOffset = 1; // slot = args[1], id = args[2], level = args[3], ... + } + + EntityEquipment equip = mob.getEquipment(); + if (equip == null) { + sender.sendMessage("§cThis entity does not support equipment."); + return; + } + + String slotArg = args[argOffset].toLowerCase(); + String id = args[argOffset + 1]; + int level = this.getNumI(sender, args[argOffset + 2], -1, true); + boolean noEnchants = Arrays.stream(args).anyMatch(a -> a.equalsIgnoreCase("-noenchants")); + + int amount = 1; + int amountArgIdx = argOffset + 3; + if (args.length > amountArgIdx && !args[amountArgIdx].equalsIgnoreCase("-noenchants")) { + amount = this.getNumI(sender, args[amountArgIdx], 1); + } + + // Find material arg (after amount, skipping -noenchants) + ItemType material = null; + for (int j = argOffset + 4; 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; + } + } + + 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."); + } + + private static UUID tryParseUUID(String s) { + try { + return UUID.fromString(s); + } catch (IllegalArgumentException e) { + return null; + } + } +} 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/utils/LoreUT.java b/src/main/java/studio/magemonkey/divinity/utils/LoreUT.java index b79ec62a..d56ae642 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 { @@ -136,4 +138,42 @@ 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%", + 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/modules/item_generator/items/common.yml b/src/main/resources/modules/item_generator/items/common.yml index 22a57731..09f820c4 100644 --- a/src/main/resources/modules/item_generator/items/common.yml +++ b/src/main/resources/modules/item_generator/items/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%' @@ -265,6 +268,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%'