From 7dac095ced3ffad9b32c7235c164520c19878c8d Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:18:13 +0000 Subject: [PATCH 1/4] fix: restore gear crafting lost in the magic update The 2026-09-20 "magic update" commit was pushed from an older copy and dropped the gear work from the first commit: sneak-left-click craft abort with refunds, core part-limit slots, part stats, tiers, model schemes, the socket rarity prefix toggle, and the post-charge chat summary. Live gear configs still use all of these keys. Bring that code back on top of current main, keeping the TLibs package move, hidden unchargeable elements, and the spell modifier resync after charging. The gear station is also protected from ItemsAdder breaks while it holds a weapon, runs orbs, or has just been aborted, since orb hits and aborts are swings that ItemsAdder treats as a furniture break. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../java/net/tfminecraft/magic/Magic.java | 1 + .../tfminecraft/magic/gear/ArchetypeDef.java | 7 + .../net/tfminecraft/magic/gear/GearCache.java | 3 + .../net/tfminecraft/magic/gear/GearCosts.java | 27 + .../magic/gear/GearItemBuilder.java | 59 +- .../net/tfminecraft/magic/gear/GearKeys.java | 4 + .../magic/gear/GearModelResolver.java | 121 ++++ .../magic/gear/GearModelScheme.java | 38 ++ .../magic/gear/GearModelSchemeRegistry.java | 43 ++ .../magic/gear/GearProvenance.java | 36 ++ .../tfminecraft/magic/gear/GearRefresher.java | 29 +- .../magic/gear/GearStatApplicator.java | 105 ++++ .../magic/gear/GearStationListener.java | 91 +++ .../magic/gear/GearStationStore.java | 11 + .../magic/gear/MajorityTierResolver.java | 44 ++ .../net/tfminecraft/magic/gear/PartDef.java | 75 +++ .../net/tfminecraft/magic/gear/PartSlots.java | 59 ++ .../magic/gear/SocketColourRegistry.java | 6 +- .../magic/gear/WeaponAttunementChat.java | 44 ++ .../tfminecraft/magic/gear/WeaponLore.java | 8 +- .../magic/gear/WeaponResonanceDisplay.java | 47 ++ .../magic/gear/gui/GearInventoryManager.java | 67 ++- .../magic/gear/gui/SelectedPartsManager.java | 22 + .../magic/gear/orb/GearOrbService.java | 33 +- .../magic/loader/ConfigLoader.java | 2 + .../tfminecraft/magic/loader/GearLoader.java | 131 ++++- .../tfminecraft/magic/util/CostFormatter.java | 9 + src/main/resources/config.yml | 4 + src/main/resources/gear/archetypes.yml | 42 +- src/main/resources/gear/model-schemes.yml | 26 + src/main/resources/gear/part-types.yml | 14 +- src/main/resources/gear/parts.yml | 527 ++++++++++++++++-- src/main/resources/gear/socket-colours.yml | 16 +- src/main/resources/messages.yml | 20 +- 34 files changed, 1660 insertions(+), 111 deletions(-) create mode 100644 src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java create mode 100644 src/main/java/net/tfminecraft/magic/gear/GearModelScheme.java create mode 100644 src/main/java/net/tfminecraft/magic/gear/GearModelSchemeRegistry.java create mode 100644 src/main/java/net/tfminecraft/magic/gear/GearStatApplicator.java create mode 100644 src/main/java/net/tfminecraft/magic/gear/MajorityTierResolver.java create mode 100644 src/main/java/net/tfminecraft/magic/gear/PartSlots.java create mode 100644 src/main/java/net/tfminecraft/magic/gear/WeaponAttunementChat.java create mode 100644 src/main/java/net/tfminecraft/magic/gear/WeaponResonanceDisplay.java create mode 100644 src/main/resources/gear/model-schemes.yml diff --git a/src/main/java/net/tfminecraft/magic/Magic.java b/src/main/java/net/tfminecraft/magic/Magic.java index 683c4d8..1ee9d0a 100644 --- a/src/main/java/net/tfminecraft/magic/Magic.java +++ b/src/main/java/net/tfminecraft/magic/Magic.java @@ -300,6 +300,7 @@ private void createConfigs() { "gear/parts.yml", "gear/socket-colours.yml", "gear/orbs.yml", + "gear/model-schemes.yml", "elements/elements.yml", "artifacts/generator.yml", "artifacts/model-schemes.yml", diff --git a/src/main/java/net/tfminecraft/magic/gear/ArchetypeDef.java b/src/main/java/net/tfminecraft/magic/gear/ArchetypeDef.java index 6875beb..88ab08f 100644 --- a/src/main/java/net/tfminecraft/magic/gear/ArchetypeDef.java +++ b/src/main/java/net/tfminecraft/magic/gear/ArchetypeDef.java @@ -14,6 +14,7 @@ public final class ArchetypeDef { private final GearType type; private final String name; private final String template; + private final String icon; private final boolean melee; private final List required; private final Map slots; @@ -23,12 +24,14 @@ public ArchetypeDef( GearType type, String name, String template, + String icon, boolean melee, List required, Map slots) { this.type = type; this.name = name == null || name.isBlank() ? type.getDisplayName() : name; this.template = template == null ? "" : template.trim(); + this.icon = icon == null ? "" : icon.trim(); this.melee = melee; this.required = List.copyOf(required == null ? List.of() : required); Map copy = new LinkedHashMap<>(); @@ -55,6 +58,10 @@ public String getTemplate() { return template; } + public String getIcon() { + return icon; + } + public boolean isMelee() { return melee; } diff --git a/src/main/java/net/tfminecraft/magic/gear/GearCache.java b/src/main/java/net/tfminecraft/magic/gear/GearCache.java index fb0a2ac..10a033c 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearCache.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearCache.java @@ -13,6 +13,9 @@ public final class GearCache { public static boolean alignmentEnabled = false; + /** When false, gem socket colour strings omit Common/Rare/Epic/Legendary prefix. */ + public static boolean socketRarityPrefix = true; + /** Weapon tier band in the spell's element -> bonus. Never a penalty. */ private static final Map ALIGNMENT = new LinkedHashMap<>(); diff --git a/src/main/java/net/tfminecraft/magic/gear/GearCosts.java b/src/main/java/net/tfminecraft/magic/gear/GearCosts.java index f344306..8be4745 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearCosts.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearCosts.java @@ -4,10 +4,12 @@ import java.util.HashMap; import java.util.Map; +import org.bukkit.Location; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import net.tfminecraft.tlibs.TLibs; +import net.tfminecraft.magic.util.ItemRef; public final class GearCosts { @@ -82,4 +84,29 @@ public static void take(Player player, Collection parts) { } player.updateInventory(); } + + public static void refund(Player player, Collection parts, Location drop) { + if (player == null) { + return; + } + Location at = drop == null ? player.getLocation() : drop.clone().add(0.5, 1.0, 0.5); + for (Map.Entry entry : total(parts).entrySet()) { + int remaining = entry.getValue(); + while (remaining > 0) { + ItemStack stack = ItemRef.build(entry.getKey()); + if (stack == null || stack.getType().isAir()) { + break; + } + int give = Math.min(remaining, Math.max(1, stack.getMaxStackSize())); + stack.setAmount(give); + remaining -= give; + for (ItemStack leftover : player.getInventory().addItem(stack).values()) { + if (leftover != null && !leftover.getType().isAir() && at.getWorld() != null) { + at.getWorld().dropItem(at, leftover); + } + } + } + } + player.updateInventory(); + } } diff --git a/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java b/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java index c90af06..0358862 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearItemBuilder.java @@ -3,7 +3,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.Map; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -44,11 +43,13 @@ public static ItemStack rewriteSockets(ItemStack stack, int band) { if (archetype == null || GearProvenance.socketsLocked(stack)) { return stack; } - List colours = SocketLayout.colours(archetype, GearProvenance.resolveParts(stack), band); - ItemStack rewritten = applySockets(stack, colours); + List parts = GearProvenance.resolveParts(stack); + List colours = SocketLayout.colours(archetype, parts, band); + ItemStack rewritten = applyMmoData(stack, colours, parts); if (rewritten == null) { return stack; } + rewritten = GearModelResolver.apply(rewritten, type, parts); copyGearPdc(stack, rewritten); GearProvenance.lockSockets(rewritten); WeaponRequirement.fromItem(stack).persist(rewritten); @@ -77,19 +78,29 @@ private static ItemStack build(GearType type, Collection parts, boolean return decoratePreview(base, type, parts); } List colours = SocketLayout.colours(archetype, parts, band); - ItemStack withSockets = applySockets(base, colours); - if (withSockets == null) { - withSockets = base; + ItemStack withMmo = applyMmoData(base, colours, parts); + if (withMmo == null) { + withMmo = base; } - GearProvenance.stamp(withSockets, type, parts); - return WeaponLore.updateItem(withSockets); + withMmo = GearModelResolver.apply(withMmo, type, parts); + GearProvenance.stamp(withMmo, type, parts); + return WeaponLore.updateItem(withMmo); } private static boolean requiredPresent(ArchetypeDef archetype, Collection parts) { if (archetype.getRequired().isEmpty()) { return parts != null && !parts.isEmpty(); } - for (String required : archetype.getRequired()) { + PartDef core = null; + if (parts != null) { + for (PartDef part : parts) { + if (part != null && PartSlots.CORE.equalsIgnoreCase(part.getPartType())) { + core = part; + break; + } + } + } + for (String required : PartSlots.open(archetype, core)) { boolean found = false; if (parts != null) { for (PartDef part : parts) { @@ -106,17 +117,20 @@ private static boolean requiredPresent(ArchetypeDef archetype, Collection colours) { - if (stack == null || colours == null || colours.isEmpty() || !mmoItemsPresent()) { + private static ItemStack applyMmoData(ItemStack stack, List colours, Collection parts) { + if (stack == null || !mmoItemsPresent()) { return stack; } try { LiveMMOItem mmo = new LiveMMOItem(NBTItem.get(stack)); - mmo.setData(ItemStats.GEM_SOCKETS, new GemSocketsData(new ArrayList<>(colours))); + if (colours != null && !colours.isEmpty()) { + mmo.setData(ItemStats.GEM_SOCKETS, new GemSocketsData(new ArrayList<>(colours))); + } + GearStatApplicator.apply(mmo, parts); ItemStack built = mmo.newBuilder().build(); return built == null || built.getType().isAir() ? stack : built; } catch (Exception ex) { - Magic.plugin.getLogger().warning("[Magic] Failed to write GEM_SOCKETS: " + ex.getMessage()); + Magic.plugin.getLogger().warning("[Magic] Failed to write gear MMO data: " + ex.getMessage()); return stack; } } @@ -149,6 +163,14 @@ private static void copyGearPdc(ItemStack from, ItemStack to) { org.bukkit.persistence.PersistentDataType.INTEGER, archetypeRevision); } + Integer majority = fromMeta.getPersistentDataContainer().get( + GearKeys.majorityTier(), org.bukkit.persistence.PersistentDataType.INTEGER); + if (majority != null) { + toMeta.getPersistentDataContainer().set( + GearKeys.majorityTier(), + org.bukkit.persistence.PersistentDataType.INTEGER, + majority); + } to.setItemMeta(toMeta); } @@ -162,14 +184,13 @@ private static ItemStack decoratePreview( } List lore = meta.hasLore() ? new ArrayList<>(meta.getLore()) : new ArrayList<>(); lore.add(""); + int majority = MajorityTierResolver.resolve(parts); + if (majority > 0) { + lore.add("§eTier " + MajorityTierResolver.toRoman(majority)); + } lore.add("§6Sockets"); lore.addAll(SocketLayout.previewLines(ArchetypeRegistry.get(type), parts)); - Map costs = GearCosts.total(parts); - if (!costs.isEmpty()) { - lore.add(""); - lore.add("§aCost"); - lore.addAll(CostFormatter.getCostsFormatted(costs)); - } + CostFormatter.appendInput(lore, GearCosts.total(parts)); lore.add(""); lore.add("§eClick to prepare on the station"); if (!mmoItemsPresent()) { diff --git a/src/main/java/net/tfminecraft/magic/gear/GearKeys.java b/src/main/java/net/tfminecraft/magic/gear/GearKeys.java index 01efad3..08d7027 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearKeys.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearKeys.java @@ -36,6 +36,10 @@ public static NamespacedKey archetypeRevision() { return new NamespacedKey(Magic.plugin, "gear_archetype_revision"); } + public static NamespacedKey majorityTier() { + return new NamespacedKey(Magic.plugin, "majority_tier"); + } + public static NamespacedKey broken() { return new NamespacedKey(Magic.plugin, "gear_broken"); } diff --git a/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java b/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java new file mode 100644 index 0000000..c315700 --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java @@ -0,0 +1,121 @@ +package net.tfminecraft.magic.gear; + +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; + +import org.bukkit.Material; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import net.tfminecraft.tlibs.TLibs; +import net.tfminecraft.magic.Magic; +import net.tfminecraft.magic.util.ItemRef; + +public final class GearModelResolver { + + private GearModelResolver() {} + + public static String path(GearType type, Collection parts) { + GearModelScheme scheme = winner(parts); + if (scheme == null) { + return null; + } + return scheme.pathFor(type); + } + + public static GearModelScheme winner(Collection parts) { + if (parts == null) { + return null; + } + Map votes = new LinkedHashMap<>(); + String coreScheme = ""; + String firstScheme = ""; + for (PartDef part : parts) { + if (part == null || !part.hasModelScheme()) { + continue; + } + String id = part.getSchemeId(); + if (!GearModelSchemeRegistry.contains(id)) { + continue; + } + votes.merge(id, part.getSchemeWeight(), Integer::sum); + if (firstScheme.isEmpty()) { + firstScheme = id; + } + if (PartSlots.CORE.equalsIgnoreCase(part.getPartType())) { + coreScheme = id; + } + } + if (votes.isEmpty()) { + return null; + } + int best = -1; + String bestId = null; + boolean tie = false; + for (Map.Entry entry : votes.entrySet()) { + int weight = entry.getValue(); + if (weight > best) { + best = weight; + bestId = entry.getKey(); + tie = false; + } else if (weight == best) { + tie = true; + } + } + if (tie) { + if (!coreScheme.isEmpty() && votes.getOrDefault(coreScheme, 0) == best) { + bestId = coreScheme; + } else if (!firstScheme.isEmpty()) { + bestId = firstScheme; + } + } + return GearModelSchemeRegistry.get(bestId); + } + + public static ItemStack apply(ItemStack stack, GearType type, Collection parts) { + if (stack == null) { + return null; + } + String path = path(type, parts); + if (path == null || path.isBlank()) { + return stack; + } + String normalized = ItemRef.normalize(path); + String prefix = normalized.split("\\.")[0]; + try { + if (prefix.equalsIgnoreCase("ia")) { + return TLibs.getItemAPI().getArmorMerger().merge(stack, Optional.empty(), normalized); + } + if (prefix.equalsIgnoreCase("v")) { + return applyVanilla(stack, normalized); + } + Magic.plugin.getLogger().warning("[Magic] Unknown gear model path: " + path); + } catch (Exception ex) { + Magic.plugin.getLogger().warning("[Magic] Failed to apply gear model " + path + ": " + ex.getMessage()); + } + return stack; + } + + private static ItemStack applyVanilla(ItemStack stack, String path) { + String[] parts = path.split("\\."); + if (parts.length < 2) { + return stack; + } + Material material = Material.matchMaterial(parts[1].toUpperCase()); + if (material == null) { + Magic.plugin.getLogger().warning("[Magic] Unknown vanilla material in gear model: " + path); + return stack; + } + stack.setType(material); + if (parts.length >= 3) { + ItemMeta meta = stack.getItemMeta(); + if (meta != null) { + meta.setCustomModelData(Integer.parseInt(parts[2])); + stack.setItemMeta(meta); + } + } + return stack; + } +} diff --git a/src/main/java/net/tfminecraft/magic/gear/GearModelScheme.java b/src/main/java/net/tfminecraft/magic/gear/GearModelScheme.java new file mode 100644 index 0000000..9f2c6c9 --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/gear/GearModelScheme.java @@ -0,0 +1,38 @@ +package net.tfminecraft.magic.gear; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.Locale; +import java.util.Map; + +public final class GearModelScheme { + + private final String id; + private final Map paths; + + public GearModelScheme(String id, Map paths) { + this.id = id == null ? "" : id.trim().toLowerCase(Locale.ROOT); + EnumMap copy = new EnumMap<>(GearType.class); + if (paths != null) { + for (Map.Entry entry : paths.entrySet()) { + if (entry.getKey() == null || entry.getValue() == null || entry.getValue().isBlank()) { + continue; + } + copy.put(entry.getKey(), entry.getValue().trim()); + } + } + this.paths = Collections.unmodifiableMap(copy); + } + + public String getId() { + return id; + } + + public String pathFor(GearType type) { + if (type == null) { + return null; + } + String path = paths.get(type); + return path == null || path.isBlank() ? null : path; + } +} diff --git a/src/main/java/net/tfminecraft/magic/gear/GearModelSchemeRegistry.java b/src/main/java/net/tfminecraft/magic/gear/GearModelSchemeRegistry.java new file mode 100644 index 0000000..560d2c0 --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/gear/GearModelSchemeRegistry.java @@ -0,0 +1,43 @@ +package net.tfminecraft.magic.gear; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; + +public final class GearModelSchemeRegistry { + + private static final Map BY_ID = new LinkedHashMap<>(); + + private GearModelSchemeRegistry() {} + + public static void clear() { + BY_ID.clear(); + } + + public static void register(GearModelScheme scheme) { + if (scheme == null || scheme.getId().isEmpty()) { + return; + } + BY_ID.put(scheme.getId(), scheme); + } + + public static boolean contains(String id) { + return get(id) != null; + } + + public static GearModelScheme get(String id) { + if (id == null || id.isBlank()) { + return null; + } + return BY_ID.get(id.trim().toLowerCase(Locale.ROOT)); + } + + public static int size() { + return BY_ID.size(); + } + + public static Map all() { + return Collections.unmodifiableMap(BY_ID); + } +} diff --git a/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java b/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java index 69b606a..96be631 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearProvenance.java @@ -37,9 +37,45 @@ public static void stamp(ItemStack stack, GearType type, Collection par meta.getPersistentDataContainer().set( GearKeys.archetypeRevision(), PersistentDataType.INTEGER, archetype == null ? 1 : archetype.getRevision()); + writeMajority(meta, parts); stack.setItemMeta(meta); } + public static int majorityOf(ItemStack stack) { + if (stack == null || !stack.hasItemMeta()) { + return 0; + } + ItemMeta meta = stack.getItemMeta(); + if (meta == null) { + return 0; + } + Integer stored = meta.getPersistentDataContainer().get( + GearKeys.majorityTier(), PersistentDataType.INTEGER); + return stored == null ? 0 : stored; + } + + public static void applyMajority(ItemStack stack, Collection parts) { + if (stack == null || !stack.hasItemMeta()) { + return; + } + ItemMeta meta = stack.getItemMeta(); + if (meta == null) { + return; + } + writeMajority(meta, parts); + stack.setItemMeta(meta); + } + + private static void writeMajority(ItemMeta meta, Collection parts) { + int majority = MajorityTierResolver.resolve(parts); + if (majority > 0) { + meta.getPersistentDataContainer().set( + GearKeys.majorityTier(), PersistentDataType.INTEGER, majority); + } else { + meta.getPersistentDataContainer().remove(GearKeys.majorityTier()); + } + } + /** Stamped part ids that no longer exist in the live registry. */ public static List missingPartIds(ItemStack stack) { List missing = new ArrayList<>(); diff --git a/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java b/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java index cde2735..8b14938 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearRefresher.java @@ -19,10 +19,9 @@ /** * Brings a crafted weapon back in line with the live gear config. * - * Parts carry no stats today, only cost and socket counts, so this rewrites the socket - * layout rather than rebuilding the whole item. Runes already in the weapon are merged - * into the new layout; any that no longer fit are held by {@link GearBrokenMarker} - * instead of being dropped. + * Socket colours and part stats are rewritten from the stamped parts. Runes already in + * the weapon are merged into the new layout; any that no longer fit are held by + * {@link GearBrokenMarker} instead of being dropped. */ public final class GearRefresher { @@ -54,6 +53,12 @@ public static ItemStack refresh(ItemStack stack, Player holder, boolean force) { } if (!force && !GearProvenance.isOutdated(stack)) { + int live = MajorityTierResolver.resolve(GearProvenance.resolveParts(stack)); + if (live > 0 && GearProvenance.majorityOf(stack) != live) { + ItemStack clone = stack.clone(); + GearProvenance.applyMajority(clone, GearProvenance.resolveParts(clone)); + return WeaponLore.updateItem(clone); + } return null; } @@ -75,6 +80,7 @@ public static ItemStack refresh(ItemStack stack, Player holder, boolean force) { WeaponRequirement.fromItem(stack).persist(rebuilt); WeaponRift.copy(stack, rebuilt); GearProvenance.syncRevisions(rebuilt); + GearProvenance.applyMajority(rebuilt, GearProvenance.resolveParts(rebuilt)); rebuilt.setAmount(stack.getAmount()); if (!orphaned.isEmpty()) { @@ -92,7 +98,7 @@ public static ItemStack refresh(ItemStack stack, Player holder, boolean force) { /** * Writes the target socket colours while carrying existing runes across. Unlike - * {@code GearItemBuilder.applySockets}, which builds fresh socket data at craft time, + * {@code GearItemBuilder} craft, which builds fresh socket data at craft time, * this puts every gem it can back into a matching empty socket first. */ private static ItemStack rewrite(ItemStack stack, List colours, List orphaned) { @@ -114,8 +120,13 @@ private static ItemStack rewrite(ItemStack stack, List colours, List UNKNOWN_LOGGED = new HashSet<>(); + + private GearStatApplicator() {} + + public static Map sum(Collection parts) { + Map totals = new LinkedHashMap<>(); + if (parts == null) { + return totals; + } + for (PartDef part : parts) { + if (part == null || part.getStats().isEmpty()) { + continue; + } + for (Map.Entry entry : part.getStats().entrySet()) { + if (entry.getKey() == null || entry.getValue() == null) { + continue; + } + totals.merge(entry.getKey(), entry.getValue(), Double::sum); + } + } + return totals; + } + + public static Set managedIds() { + Set ids = new HashSet<>(); + for (PartDef part : PartRegistry.getAll()) { + ids.addAll(part.getStats().keySet()); + } + return ids; + } + + public static void apply(MMOItem mmo, Collection parts) { + if (mmo == null || !GearItemBuilder.mmoItemsPresent()) { + return; + } + Set managed = managedIds(); + if (managed.isEmpty()) { + return; + } + Map totals = sum(parts); + for (String statId : managed) { + ItemStat itemStat = resolve(statId); + if (itemStat == null) { + continue; + } + StatHistory hist = StatHistory.from(mmo, itemStat); + if (hist != null) { + hist.clearExternalData(); + Object og = hist.getOriginalData(); + if (og instanceof DoubleData doubleOg) { + doubleOg.setValue(0); + } + mmo.setStatHistory(itemStat, hist); + } + mmo.setData(itemStat, new DoubleData(0)); + } + for (String statId : managed) { + applyDouble(mmo, statId, totals.getOrDefault(statId, 0.0)); + } + } + + @SuppressWarnings("deprecation") + private static void applyDouble(MMOItem mmo, String statId, double value) { + ItemStat itemStat = resolve(statId); + if (itemStat == null) { + return; + } + DoubleData data = new DoubleData(value); + mmo.setData(itemStat, data); + StatHistory hist = StatHistory.from(mmo, itemStat); + if (hist != null) { + hist.registerExternalData(data); + mmo.setStatHistory(itemStat, hist); + } + } + + private static ItemStat resolve(String statId) { + if (statId == null || statId.isBlank()) { + return null; + } + ItemStat itemStat = MMOItems.plugin.getStats().get(statId.toUpperCase(Locale.ROOT)); + if (itemStat == null && UNKNOWN_LOGGED.add(statId.toLowerCase(Locale.ROOT))) { + Magic.plugin.getLogger().warning("[Magic] Unknown MMOItems stat: " + statId); + } + return itemStat; + } +} diff --git a/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java b/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java index 7376165..1074653 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java @@ -1,11 +1,15 @@ package net.tfminecraft.magic.gear; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.UUID; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.Sound; import org.bukkit.block.Block; +import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; @@ -15,6 +19,7 @@ import org.bukkit.inventory.EquipmentSlot; import org.bukkit.inventory.ItemStack; +import dev.lone.itemsadder.api.Events.FurnitureBreakEvent; import net.tfminecraft.tlibs.TLibs; import net.tfminecraft.magic.Messages; import net.tfminecraft.magic.charge.ChargeIds; @@ -24,14 +29,54 @@ public final class GearStationListener implements Listener { + /** ItemsAdder breaks furniture two ticks after the swing, so an abort swing needs cover. */ + private static final long ABORT_BREAK_GUARD_MILLIS = 1000L; + private final GearInventoryManager inventory = new GearInventoryManager(); + private final Map recentAborts = new HashMap<>(); public GearInventoryManager inventory() { return inventory; } + /** + * Left-clicks at the station (orb hits, aborts) are swings, and ItemsAdder breaks + * furniture on a swing. A station holding a weapon or running orbs must survive them. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onFurnitureBreak(FurnitureBreakEvent event) { + if (!isStationFurniture(event.getNamespacedID())) { + return; + } + Entity entity = event.getBukkitEntity(); + if (entity == null) { + return; + } + Location location = entity.getLocation().getBlock().getLocation(); + Long aborted = recentAborts.get(GearStationStore.key(location)); + boolean justAborted = aborted != null + && System.currentTimeMillis() - aborted < ABORT_BREAK_GUARD_MILLIS; + if (justAborted || GearStationStore.isOccupied(location) || GearOrbService.isActive(location)) { + event.setCancelled(true); + } + } + + private static boolean isStationFurniture(String namespacedId) { + String station = GearCache.station == null ? "" : GearCache.station.trim(); + int open = station.indexOf('('); + int close = station.indexOf(')', open + 1); + if (namespacedId == null || !station.toLowerCase().startsWith("iaf(") || close <= open) { + return false; + } + return station.substring(open + 1, close).equalsIgnoreCase(namespacedId); + } + @EventHandler(priority = EventPriority.HIGH) public void onInteract(PlayerInteractEvent event) { + if (event.getAction() == Action.LEFT_CLICK_BLOCK) { + tryAbort(event); + return; + } if (event.getAction() != Action.RIGHT_CLICK_BLOCK) { return; } @@ -150,4 +195,50 @@ private static void eject(Player player, Location location) { drop.getWorld().dropItem(drop, item); player.playSound(location, Sound.ENTITY_ITEM_PICKUP, 1f, 1f); } + + private void tryAbort(PlayerInteractEvent event) { + if (event.getHand() != EquipmentSlot.HAND) { + return; + } + if (!event.getPlayer().isSneaking()) { + return; + } + Block block = event.getClickedBlock(); + if (block == null) { + return; + } + try { + if (!TLibs.getBlockAPI().getChecker().checkBlock(block, GearCache.station)) { + return; + } + } catch (Exception ex) { + return; + } + event.setCancelled(true); + Player player = event.getPlayer(); + Location location = block.getLocation(); + GearStationStore.Occupancy occupancy = GearStationStore.get(location); + if (occupancy == null) { + return; + } + if (GearStationStore.isAttuned(occupancy.getItem())) { + return; + } + UUID owner = GearOrbService.sessionOwner(location); + if (owner != null && !owner.equals(player.getUniqueId())) { + player.sendMessage(Messages.get("gear.abort.not_yours")); + player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f); + return; + } + GearOrbService.abort(location); + ItemStack weapon = GearStationStore.takeForAbort(location); + if (weapon == null) { + return; + } + recentAborts.values().removeIf(at -> System.currentTimeMillis() - at >= ABORT_BREAK_GUARD_MILLIS); + recentAborts.put(GearStationStore.key(location), System.currentTimeMillis()); + GearCosts.refund(player, GearProvenance.resolveParts(weapon), location); + player.sendMessage(Messages.get("gear.abort.done")); + player.playSound(location, Sound.BLOCK_ANVIL_LAND, 0.6f, 1.4f); + } } diff --git a/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java b/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java index a3d5f34..478ff6c 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java @@ -98,6 +98,17 @@ public static ItemStack eject(Location location) { return item; } + /** Clears the station even if the weapon is unattuned or mid-orb. Does not return the staff. */ + public static ItemStack takeForAbort(Location location) { + Occupancy occupancy = get(location); + if (occupancy == null) { + return null; + } + ItemStack item = occupancy.getItem(); + clear(location, true); + return item; + } + /** An unattuned weapon never leaves the station, so band 0 gear cannot exist in the world. */ public static boolean isAttuned(ItemStack item) { return item != null && WeaponRequirement.fromItem(item).highestBand() > 0; diff --git a/src/main/java/net/tfminecraft/magic/gear/MajorityTierResolver.java b/src/main/java/net/tfminecraft/magic/gear/MajorityTierResolver.java new file mode 100644 index 0000000..d99fab0 --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/gear/MajorityTierResolver.java @@ -0,0 +1,44 @@ +package net.tfminecraft.magic.gear; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public final class MajorityTierResolver { + + private MajorityTierResolver() {} + + public static int resolve(Collection parts) { + if (parts == null || parts.isEmpty()) { + return 0; + } + Map votes = new HashMap<>(); + for (PartDef part : parts) { + if (part == null || !part.hasTier()) { + continue; + } + votes.merge(part.getTier(), 1, Integer::sum); + } + int bestTier = 0; + int bestCount = 0; + for (Map.Entry entry : votes.entrySet()) { + int tier = entry.getKey(); + int count = entry.getValue(); + if (count > bestCount || (count == bestCount && tier > bestTier)) { + bestCount = count; + bestTier = tier; + } + } + return bestTier; + } + + public static String toRoman(int tier) { + return switch (tier) { + case 1 -> "I"; + case 2 -> "II"; + case 3 -> "III"; + case 4 -> "IV"; + default -> String.valueOf(tier); + }; + } +} diff --git a/src/main/java/net/tfminecraft/magic/gear/PartDef.java b/src/main/java/net/tfminecraft/magic/gear/PartDef.java index 0a7b993..6716469 100644 --- a/src/main/java/net/tfminecraft/magic/gear/PartDef.java +++ b/src/main/java/net/tfminecraft/magic/gear/PartDef.java @@ -1,5 +1,6 @@ package net.tfminecraft.magic.gear; +import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; import java.util.LinkedHashMap; @@ -16,11 +17,16 @@ public final class PartDef { private final String id; private final String name; private final String partType; + private final int tier; private final Set types; private final String itemPath; private final Map cost; + private final List partLimit; + private final Map stats; private final Map sockets; private final List lore; + private final String schemeId; + private final int schemeWeight; private final boolean disabled; private int revision = 1; @@ -28,20 +34,40 @@ public PartDef( String id, String name, String partType, + int tier, Set types, String itemPath, Map cost, + List partLimit, + Map stats, Map sockets, List lore, + String schemeId, + int schemeWeight, boolean disabled) { this.id = id == null ? "" : id.trim().toLowerCase(Locale.ROOT); this.name = name == null || name.isBlank() ? this.id : name; this.partType = partType == null ? "" : partType.trim().toLowerCase(Locale.ROOT); + this.tier = tier < 1 ? 0 : tier; this.types = types == null || types.isEmpty() ? EnumSet.noneOf(GearType.class) : EnumSet.copyOf(types); this.itemPath = itemPath == null ? "" : itemPath.trim(); this.cost = cost == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(cost)); + List limitCopy = new ArrayList<>(); + if (partLimit != null) { + for (String category : partLimit) { + if (category == null || category.isBlank()) { + continue; + } + String key = category.trim().toLowerCase(Locale.ROOT); + if (!limitCopy.contains(key)) { + limitCopy.add(key); + } + } + } + this.partLimit = List.copyOf(limitCopy); + this.stats = stats == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(stats)); Map socketCopy = new LinkedHashMap<>(); if (sockets != null) { for (Map.Entry entry : sockets.entrySet()) { @@ -53,6 +79,8 @@ public PartDef( } this.sockets = Collections.unmodifiableMap(socketCopy); this.lore = lore == null ? List.of() : List.copyOf(lore); + this.schemeId = schemeId == null ? "" : schemeId.trim().toLowerCase(Locale.ROOT); + this.schemeWeight = schemeWeight < 1 ? 1 : schemeWeight; this.disabled = disabled; } @@ -68,6 +96,14 @@ public String getPartType() { return partType; } + public int getTier() { + return tier; + } + + public boolean hasTier() { + return tier > 0; + } + public Set getTypes() { return types; } @@ -88,6 +124,28 @@ public boolean hasCost() { return !cost.isEmpty(); } + public List getPartLimit() { + return partLimit; + } + + public boolean hasPartLimit() { + return !partLimit.isEmpty(); + } + + public boolean allowsPart(String category) { + if (!hasPartLimit()) { + return true; + } + if (category == null || category.isBlank()) { + return false; + } + return partLimit.contains(category.trim().toLowerCase(Locale.ROOT)); + } + + public Map getStats() { + return stats; + } + public Map getSockets() { return sockets; } @@ -111,6 +169,18 @@ public List getLore() { return lore; } + public String getSchemeId() { + return schemeId; + } + + public int getSchemeWeight() { + return schemeWeight; + } + + public boolean hasModelScheme() { + return !schemeId.isEmpty(); + } + public boolean isDisabled() { return disabled; } @@ -130,11 +200,16 @@ public void setRevision(int revision) { public String buildRevisionContent() { StringBuilder out = new StringBuilder(); out.append("type=").append(partType).append(';'); + out.append("tier=").append(tier).append(';'); out.append("gear=").append(new TreeSet<>(types.stream() .map(Enum::name).collect(Collectors.toList()))).append(';'); out.append("item=").append(itemPath).append(';'); out.append("cost=").append(new TreeMap<>(cost)).append(';'); + out.append("part-limit=").append(partLimit).append(';'); + out.append("stats=").append(new TreeMap<>(stats)).append(';'); out.append("sockets=").append(new TreeMap<>(sockets)).append(';'); + out.append("scheme=").append(schemeId).append(';'); + out.append("scheme-weight=").append(schemeWeight).append(';'); out.append("disabled=").append(disabled); return out.toString(); } diff --git a/src/main/java/net/tfminecraft/magic/gear/PartSlots.java b/src/main/java/net/tfminecraft/magic/gear/PartSlots.java new file mode 100644 index 0000000..f58071d --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/gear/PartSlots.java @@ -0,0 +1,59 @@ +package net.tfminecraft.magic.gear; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public final class PartSlots { + + public static final String CORE = "core"; + + private PartSlots() {} + + /** + * Part categories that are visible and mandatory for this archetype + core. + * Core is included when the archetype lists it. Extra slots are the intersection + * of archetype required and the core's part-limit. No limit means the full + * required list (never extra part-types.yml categories). + */ + public static List open(ArchetypeDef archetype, PartDef core) { + if (archetype == null) { + return List.of(); + } + List required = new ArrayList<>(); + for (String id : archetype.getRequired()) { + if (id == null || id.isBlank()) { + continue; + } + String key = id.trim().toLowerCase(Locale.ROOT); + if (!required.contains(key)) { + required.add(key); + } + } + if (core == null || !core.hasPartLimit()) { + return List.copyOf(required); + } + List open = new ArrayList<>(); + if (required.contains(CORE)) { + open.add(CORE); + } + for (String id : core.getPartLimit()) { + if (id == null || id.isBlank()) { + continue; + } + String key = id.trim().toLowerCase(Locale.ROOT); + if (CORE.equals(key) || open.contains(key) || !required.contains(key)) { + continue; + } + open.add(key); + } + return List.copyOf(open); + } + + public static boolean contains(List open, String category) { + if (open == null || category == null || category.isBlank()) { + return false; + } + return open.contains(category.trim().toLowerCase(Locale.ROOT)); + } +} diff --git a/src/main/java/net/tfminecraft/magic/gear/SocketColourRegistry.java b/src/main/java/net/tfminecraft/magic/gear/SocketColourRegistry.java index 62e6cd3..f417513 100644 --- a/src/main/java/net/tfminecraft/magic/gear/SocketColourRegistry.java +++ b/src/main/java/net/tfminecraft/magic/gear/SocketColourRegistry.java @@ -45,7 +45,11 @@ public static String colour(int band, String suffix) { if (suffix == null || suffix.isBlank()) { return ""; } - return prefix(band) + " " + suffix.trim(); + String trimmed = suffix.trim(); + if (!GearCache.socketRarityPrefix) { + return trimmed; + } + return prefix(band) + " " + trimmed; } public static Map prefixes() { diff --git a/src/main/java/net/tfminecraft/magic/gear/WeaponAttunementChat.java b/src/main/java/net/tfminecraft/magic/gear/WeaponAttunementChat.java new file mode 100644 index 0000000..d45db28 --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/gear/WeaponAttunementChat.java @@ -0,0 +1,44 @@ +package net.tfminecraft.magic.gear; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.entity.Player; +import org.bukkit.inventory.ItemStack; + +import net.tfminecraft.magic.Messages; + +public final class WeaponAttunementChat { + + private WeaponAttunementChat() {} + + public static void sendPostChargeSummary(Player player, ItemStack weapon) { + if (player == null || weapon == null) { + return; + } + WeaponRequirement requirement = WeaponRequirement.fromItem(weapon); + int rift = WeaponRift.get(weapon); + + player.sendMessage(""); + player.sendMessage(Messages.get("gear.orbs.summary_title")); + if (!requirement.hasStored()) { + player.sendMessage(Messages.get("gear.orbs.summary_unattuned")); + } else { + String prefix = Messages.get("gear.orbs.summary_line_prefix"); + List lines = new ArrayList<>(); + WeaponResonanceDisplay.appendElementLines(lines, requirement, prefix); + for (String line : lines) { + player.sendMessage(line); + } + } + if (rift > 0) { + player.sendMessage(Messages.get("gear.orbs.summary_rift", "rift", String.valueOf(rift))); + } + player.sendMessage(""); + if (rift > 0) { + player.sendMessage(Messages.get("gear.orbs.summary_hint_resonance_and_clean")); + } else { + player.sendMessage(Messages.get("gear.orbs.summary_hint_resonance")); + } + } +} diff --git a/src/main/java/net/tfminecraft/magic/gear/WeaponLore.java b/src/main/java/net/tfminecraft/magic/gear/WeaponLore.java index c9cf5aa..de3043d 100644 --- a/src/main/java/net/tfminecraft/magic/gear/WeaponLore.java +++ b/src/main/java/net/tfminecraft/magic/gear/WeaponLore.java @@ -31,6 +31,7 @@ public static void apply(ItemStack stack) { if (!GearProvenance.isGear(stack)) { return; } + GearProvenance.applyMajority(stack, GearProvenance.resolveParts(stack)); WeaponRequirement requirement = WeaponRequirement.fromItem(stack); ItemMeta meta = stack.getItemMeta(); if (meta == null) { @@ -39,6 +40,11 @@ public static void apply(ItemStack stack) { List lore = meta.hasLore() ? new ArrayList<>(meta.getLore()) : new ArrayList<>(); lore = stripBlock(lore); List block = new ArrayList<>(); + int majority = GearProvenance.majorityOf(stack); + if (majority > 0) { + block.add(MagicText.format("{color:label_muted}Tier " + + MajorityTierResolver.toRoman(majority))); + } if (!requirement.hasStored()) { block.add(MagicText.format("{color:label_muted}Unattuned")); block.add(MagicText.format("{color:label_muted}Apply a charged charge at the station")); @@ -91,7 +97,7 @@ private static List stripBlock(List lore) { } for (int i = 0; i < lore.size(); i++) { String p = plain(lore.get(i)).toLowerCase(Locale.ROOT); - if ("resonance".equals(p) || "unattuned".equals(p)) { + if ("resonance".equals(p) || "unattuned".equals(p) || p.startsWith("tier ")) { return new ArrayList<>(lore.subList(0, i)); } } diff --git a/src/main/java/net/tfminecraft/magic/gear/WeaponResonanceDisplay.java b/src/main/java/net/tfminecraft/magic/gear/WeaponResonanceDisplay.java new file mode 100644 index 0000000..21aa785 --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/gear/WeaponResonanceDisplay.java @@ -0,0 +1,47 @@ +package net.tfminecraft.magic.gear; + +import java.util.List; + +import net.tfminecraft.magic.charge.TierBands; +import net.tfminecraft.magic.model.ElementDef; +import net.tfminecraft.magic.model.ElementVisibility; +import net.tfminecraft.magic.registry.ElementRegistry; +import net.tfminecraft.magic.util.MagicText; + +public final class WeaponResonanceDisplay { + + private WeaponResonanceDisplay() {} + + /** + * One formatted line per imbued element. Caller supplies title / unattuned messaging. + */ + public static void appendElementLines(List out, WeaponRequirement requirement, String linePrefix) { + if (requirement == null || !requirement.hasStored()) { + return; + } + String prefix = linePrefix != null ? linePrefix : ""; + for (ElementDef element : ElementRegistry.getAll()) { + String elementId = element.getId(); + if (!ElementVisibility.shownOnCharge(elementId) || !includeElement(requirement, elementId)) { + continue; + } + double fill = requirement.aura().getFill(elementId); + String numeral = TierBands.numeralFor(elementId, fill); + out.add(MagicText.format(prefix) + MagicText.elementName(element) + + MagicText.format(" {color:label_muted}" + (numeral.isEmpty() ? "-" : numeral))); + } + } + + static boolean includeElement(WeaponRequirement requirement, String elementId) { + if (requirement == null || elementId == null) { + return false; + } + return includeAuraEntry( + requirement.aura().getCap(elementId), + requirement.aura().getFill(elementId)); + } + + static boolean includeAuraEntry(double cap, double fill) { + return cap > 0 || fill > 0; + } +} diff --git a/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java b/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java index 04c37b9..aea4e6b 100644 --- a/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java +++ b/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java @@ -27,6 +27,7 @@ import net.tfminecraft.magic.gear.GearType; import net.tfminecraft.magic.gear.PartDef; import net.tfminecraft.magic.gear.PartRegistry; +import net.tfminecraft.magic.gear.PartSlots; import net.tfminecraft.magic.gear.PartTypeDef; import net.tfminecraft.magic.gear.PartTypeRegistry; import net.tfminecraft.magic.gear.SocketLayout; @@ -44,14 +45,21 @@ public void openAssembly(Player player) { inv.setItem(i, filler); } GearType type = TypeSelectionManager.get(player); + ArchetypeDef archetype = ArchetypeRegistry.get(type); + PartDef core = selectedOrFirst(player, PartSlots.CORE, type); + List open = PartSlots.open(archetype, core); inv.setItem(0, typeButton(type)); Collection parts = collectParts(player, type); - for (PartTypeDef category : PartTypeRegistry.getAll()) { + for (String categoryId : open) { + PartTypeDef category = PartTypeRegistry.get(categoryId); + if (category == null) { + continue; + } int slot = category.getSlot(); if (slot <= 0 || slot >= inv.getSize()) { continue; } - PartDef part = selectedOrFirst(player, category.getId(), type); + PartDef part = selectedOrFirst(player, categoryId, type); if (part == null) { inv.setItem(slot, barrier("No part")); } else { @@ -68,11 +76,11 @@ public void openTypeSelection(Player player) { Inventory inv = Bukkit.createInventory(new TypeSelectionHolder(), 9, "§6Select Archetype"); int slot = 0; for (GearType type : GearType.values()) { - ItemStack item = new ItemStack(type.getIcon()); + ArchetypeDef def = ArchetypeRegistry.get(type); + ItemStack item = archetypeIcon(type, def); ItemMeta meta = item.getItemMeta(); if (meta != null) { meta.setDisplayName("§e" + type.getDisplayName()); - ArchetypeDef def = ArchetypeRegistry.get(type); List lore = new ArrayList<>(); lore.add("§7Click to choose"); if (def != null && def.isMelee()) { @@ -97,6 +105,12 @@ public void openTypeSelection(Player player) { @SuppressWarnings("deprecation") public void openPartSelection(Player player, String categoryId) { GearType type = TypeSelectionManager.get(player); + ArchetypeDef archetype = ArchetypeRegistry.get(type); + PartDef core = selectedOrFirst(player, PartSlots.CORE, type); + if (!PartSlots.contains(PartSlots.open(archetype, core), categoryId)) { + openAssembly(player); + return; + } List options = PartRegistry.matching(categoryId, type); int size = Math.max(9, Math.min(54, ((options.size() + 8) / 9) * 9)); Inventory inv = Bukkit.createInventory( @@ -124,21 +138,13 @@ public Collection collectParts(Player player, GearType type) { if (archetype == null) { return parts; } - for (String category : archetype.getRequired()) { + PartDef core = selectedOrFirst(player, PartSlots.CORE, type); + for (String category : PartSlots.open(archetype, core)) { PartDef part = selectedOrFirst(player, category, type); if (part != null) { parts.add(part); } } - for (PartTypeDef category : PartTypeRegistry.getAll()) { - if (archetype.getRequired().contains(category.getId())) { - continue; - } - PartDef part = selectedOrFirst(player, category.getId(), type); - if (part != null) { - parts.add(part); - } - } return parts; } @@ -154,6 +160,18 @@ private PartDef selectedOrFirst(Player player, String categoryId, GearType type) return PartRegistry.firstMatching(categoryId, type); } + private void pruneClosedSelections(Player player) { + GearType type = TypeSelectionManager.get(player); + List open = PartSlots.open( + ArchetypeRegistry.get(type), + selectedOrFirst(player, PartSlots.CORE, type)); + for (String category : SelectedPartsManager.categories(player)) { + if (!PartSlots.contains(open, category)) { + SelectedPartsManager.remove(player, category); + } + } + } + // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") @EventHandler @@ -181,6 +199,11 @@ public void onClick(InventoryClickEvent event) { } String category = PartTypeRegistry.idForSlot(event.getSlot()); if (category != null) { + GearType type = TypeSelectionManager.get(player); + PartDef core = selectedOrFirst(player, PartSlots.CORE, type); + if (!PartSlots.contains(PartSlots.open(ArchetypeRegistry.get(type), core), category)) { + return; + } openPartSelection(player, category); clickSound(player); } @@ -227,6 +250,9 @@ public void onClick(InventoryClickEvent event) { return; } SelectedPartsManager.set(player, holder.getCategoryId(), partId); + if (PartSlots.CORE.equalsIgnoreCase(holder.getCategoryId())) { + pruneClosedSelections(player); + } openAssembly(player); clickSound(player); } @@ -305,10 +331,17 @@ private static ItemStack barrier(String name) { return item; } + private static ItemStack archetypeIcon(GearType type, ArchetypeDef def) { + if (def == null) { + return new ItemStack(type.getIcon()); + } + return ItemRef.buildOrFallback(def.getIcon(), type.getIcon()); + } + // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") private static ItemStack typeButton(GearType type) { - ItemStack item = new ItemStack(Material.NETHER_STAR); + ItemStack item = archetypeIcon(type, ArchetypeRegistry.get(type)); ItemMeta meta = item.getItemMeta(); if (meta != null) { meta.setDisplayName("§6Archetype: §e" + type.getDisplayName()); @@ -335,9 +368,7 @@ private static ItemStack partIcon(PartDef part, boolean picker) { } } if (part.hasCost()) { - lore.add(""); - lore.add("§aCost"); - lore.addAll(CostFormatter.getCostsFormatted(part.getCost())); + CostFormatter.appendInput(lore, part.getCost()); } if (picker) { lore.add("§eClick to select"); diff --git a/src/main/java/net/tfminecraft/magic/gear/gui/SelectedPartsManager.java b/src/main/java/net/tfminecraft/magic/gear/gui/SelectedPartsManager.java index 1ca8523..f19ab2e 100644 --- a/src/main/java/net/tfminecraft/magic/gear/gui/SelectedPartsManager.java +++ b/src/main/java/net/tfminecraft/magic/gear/gui/SelectedPartsManager.java @@ -2,6 +2,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.UUID; import org.bukkit.entity.Player; @@ -28,6 +29,27 @@ public static void set(Player player, String categoryId, String partId) { .put(categoryId, partId); } + public static void remove(Player player, String categoryId) { + if (player == null || categoryId == null) { + return; + } + Map map = SELECTED.get(player.getUniqueId()); + if (map != null) { + map.remove(categoryId); + } + } + + public static Set categories(Player player) { + if (player == null) { + return Set.of(); + } + Map map = SELECTED.get(player.getUniqueId()); + if (map == null || map.isEmpty()) { + return Set.of(); + } + return Set.copyOf(map.keySet()); + } + public static void clear(Player player) { if (player != null) { SELECTED.remove(player.getUniqueId()); diff --git a/src/main/java/net/tfminecraft/magic/gear/orb/GearOrbService.java b/src/main/java/net/tfminecraft/magic/gear/orb/GearOrbService.java index c0906c6..3dce276 100644 --- a/src/main/java/net/tfminecraft/magic/gear/orb/GearOrbService.java +++ b/src/main/java/net/tfminecraft/magic/gear/orb/GearOrbService.java @@ -28,6 +28,7 @@ import net.tfminecraft.magic.gear.GearItemBuilder; import net.tfminecraft.magic.gear.GearProvenance; import net.tfminecraft.magic.gear.GearStationStore; +import net.tfminecraft.magic.gear.WeaponAttunementChat; import net.tfminecraft.magic.gear.WeaponLore; import net.tfminecraft.magic.gear.WeaponRequirement; import net.tfminecraft.magic.gear.WeaponRift; @@ -69,6 +70,31 @@ public static boolean isActive(Location station) { return station != null && SESSIONS.containsKey(GearStationStore.key(station)); } + public static UUID sessionOwner(Location station) { + if (station == null) { + return null; + } + GearOrbSession session = SESSIONS.get(GearStationStore.key(station)); + return session == null ? null : session.getPlayerId(); + } + + /** + * Stops the run without writing captured aura or rift. The charge stays spent. + */ + public static void abort(Location station) { + if (station == null) { + return; + } + GearOrbSession session = SESSIONS.remove(GearStationStore.key(station)); + if (session != null) { + session.end(); + } + GearStationStore.Occupancy occupancy = GearStationStore.get(station); + if (occupancy != null) { + occupancy.setOrbSessionActive(false); + } + } + /** * Begins a run. The caller has already consumed the charge and flagged the station, * so this never fails silently: a station that is already running is rejected first. @@ -170,9 +196,7 @@ private static void complete(GearOrbSession session, boolean announce) { "percent", String.valueOf((int) Math.round(session.capturedFraction() * 100)), "band", TierBands.numeral(capturedBand))); } - if (session.riftDelta() > 0) { - player.sendMessage(Messages.get("gear.orbs.rift", "rift", String.valueOf(newRift))); - } + WeaponAttunementChat.sendPostChargeSummary(player, weapon); } @EventHandler @@ -192,6 +216,9 @@ public void onSwing(PlayerAnimationEvent event) { } private static void tryHit(Player player) { + if (player.isSneaking()) { + return; + } GearOrbSession session = sessionOf(player.getUniqueId()); if (session == null || !session.claimSwing(tickCount) || !session.canHit(tickCount)) { return; diff --git a/src/main/java/net/tfminecraft/magic/loader/ConfigLoader.java b/src/main/java/net/tfminecraft/magic/loader/ConfigLoader.java index feca373..bb1f1d2 100644 --- a/src/main/java/net/tfminecraft/magic/loader/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/magic/loader/ConfigLoader.java @@ -136,6 +136,8 @@ private static void loadGear(ConfigurationSection section) { } long confirm = section.getLong("confirm_seconds", 5L); net.tfminecraft.magic.gear.GearCache.confirmMillis = Math.max(1L, confirm) * 1000L; + net.tfminecraft.magic.gear.GearCache.socketRarityPrefix = + section.getBoolean("socket_rarity_prefix", true); loadAlignment(section.getConfigurationSection("alignment")); } diff --git a/src/main/java/net/tfminecraft/magic/loader/GearLoader.java b/src/main/java/net/tfminecraft/magic/loader/GearLoader.java index d063713..4a1fe7f 100644 --- a/src/main/java/net/tfminecraft/magic/loader/GearLoader.java +++ b/src/main/java/net/tfminecraft/magic/loader/GearLoader.java @@ -20,6 +20,8 @@ import net.tfminecraft.magic.gear.orb.OrbCache; import net.tfminecraft.magic.gear.ArchetypeDef; import net.tfminecraft.magic.gear.ArchetypeRegistry; +import net.tfminecraft.magic.gear.GearModelScheme; +import net.tfminecraft.magic.gear.GearModelSchemeRegistry; import net.tfminecraft.magic.gear.GearType; import net.tfminecraft.magic.gear.PartDef; import net.tfminecraft.magic.gear.PartRegistry; @@ -36,6 +38,7 @@ public boolean loadFolder(File folder) { ArchetypeRegistry.clear(); PartTypeRegistry.clear(); PartRegistry.clear(); + GearModelSchemeRegistry.clear(); SocketColourRegistry.clear(); SocketLayout.clearLabels(); if (folder == null || !folder.exists()) { @@ -46,11 +49,13 @@ public boolean loadFolder(File folder) { ok &= loadPartTypes(new File(folder, "part-types.yml")); ok &= loadArchetypes(new File(folder, "archetypes.yml")); ok &= loadSocketColours(new File(folder, "socket-colours.yml")); + ok &= loadModelSchemes(new File(folder, "model-schemes.yml")); ok &= loadParts(new File(folder, "parts.yml")); ok &= loadOrbs(new File(folder, "orbs.yml")); Magic.plugin.getLogger().info("[Magic] Loaded " + ArchetypeRegistry.size() + " archetype(s), " + PartTypeRegistry.size() + " part type(s), " - + PartRegistry.size() + " part(s). Socket colours: " + + PartRegistry.size() + " part(s), " + GearModelSchemeRegistry.size() + + " model scheme(s). Socket colours: " + SocketColourRegistry.prefixes() + ". Socket labels: " + SocketLayout.labels()); return ok; @@ -109,6 +114,7 @@ private static boolean loadArchetypes(File file) { type, section.getString("name", type.getDisplayName()), section.getString("template", ""), + section.getString("icon", ""), section.getBoolean("melee", type == GearType.SWORD), section.getStringList("required"), slots); @@ -225,6 +231,58 @@ private static Particle particleOf(String name, Particle fallback) { } } + private static boolean loadModelSchemes(File file) { + YamlConfiguration config = read(file, "gear/model-schemes.yml"); + if (config == null) { + return false; + } + int skipped = 0; + for (String key : config.getKeys(false)) { + ConfigurationSection section = config.getConfigurationSection(key); + if (key == null || key.isBlank() || section == null) { + skipped++; + continue; + } + if (GearModelSchemeRegistry.contains(key)) { + Magic.plugin.getLogger().warning("[Magic] Duplicate model-scheme '" + key + "'"); + skipped++; + continue; + } + Map paths = new LinkedHashMap<>(); + for (String line : section.getStringList("models")) { + if (line == null || line.isBlank()) { + continue; + } + String trimmed = line.trim(); + int open = trimmed.indexOf('('); + int close = trimmed.lastIndexOf(')'); + if (open <= 0 || close <= open) { + Magic.plugin.getLogger().warning("[Magic] Invalid model '" + line + + "' in scheme '" + key + "'"); + continue; + } + GearType type = GearType.fromId(trimmed.substring(0, open).trim()); + String path = trimmed.substring(open + 1, close).trim(); + if (type == null || path.isEmpty()) { + Magic.plugin.getLogger().warning("[Magic] Invalid model '" + line + + "' in scheme '" + key + "'"); + continue; + } + paths.put(type, path); + } + if (paths.isEmpty()) { + Magic.plugin.getLogger().warning("[Magic] Model-scheme '" + key + "' has no models"); + skipped++; + continue; + } + GearModelSchemeRegistry.register(new GearModelScheme(key, paths)); + } + if (skipped > 0) { + Magic.plugin.getLogger().warning("[Magic] gear/model-schemes.yml skipped " + skipped); + } + return GearModelSchemeRegistry.size() > 0; + } + private static boolean loadParts(File file) { YamlConfiguration config = read(file, "gear/parts.yml"); if (config == null) { @@ -262,19 +320,63 @@ private static boolean loadParts(File file) { sockets.put(slotId, socketSection.getInt(slotId, 0)); } } + List partLimit = new ArrayList<>(); + for (String raw : section.getStringList("part-limit")) { + if (raw == null || raw.isBlank()) { + continue; + } + String category = raw.trim().toLowerCase(Locale.ROOT); + if (PartTypeRegistry.get(category) == null) { + Magic.plugin.getLogger().warning("[Magic] parts.yml: unknown part-limit '" + + raw + "' for '" + id + "'"); + continue; + } + if (!partLimit.contains(category)) { + partLimit.add(category); + } + } List lore = new ArrayList<>(); for (String line : section.getStringList("lore")) { lore.add(MagicText.format(line)); } + String schemeId = ""; + int schemeWeight = 1; + String schemeRaw = section.getString("model-scheme", ""); + if (schemeRaw != null && !schemeRaw.isBlank()) { + int start = schemeRaw.indexOf('('); + int end = schemeRaw.indexOf(')'); + if (start > 0 && end > start) { + schemeId = schemeRaw.substring(0, start).trim(); + try { + schemeWeight = Integer.parseInt(schemeRaw.substring(start + 1, end).trim()); + } catch (NumberFormatException ignored) { + schemeWeight = 1; + } + } else { + schemeId = schemeRaw.trim(); + } + schemeId = schemeId.toLowerCase(Locale.ROOT); + if (!GearModelSchemeRegistry.contains(schemeId)) { + Magic.plugin.getLogger().warning("[Magic] parts.yml: unknown model-scheme '" + + schemeRaw + "' for '" + id + "'"); + schemeId = ""; + schemeWeight = 1; + } + } PartDef def = new PartDef( id, MagicText.format(section.getString("name", id)), partType, + section.getInt("tier", 0), types, section.getString("item", "v.stone"), parseCost(section.getStringList("cost")), + partLimit, + parseStats(section.getStringList("stats")), sockets, lore, + schemeId, + schemeWeight, section.getBoolean("disabled", false)); if (def.totalSocketCount() > 4) { Magic.plugin.getLogger().warning("[Magic] part '" + id @@ -313,4 +415,31 @@ private static Map parseCost(List raw) { } return cost; } + + private static Map parseStats(List raw) { + Map stats = new LinkedHashMap<>(); + if (raw == null) { + return stats; + } + for (String entry : raw) { + if (entry == null || entry.isBlank()) { + continue; + } + int start = entry.indexOf('('); + int end = entry.indexOf(')'); + if (start <= 0 || end <= start) { + continue; + } + try { + String statId = entry.substring(0, start).trim().toLowerCase(Locale.ROOT); + if (statId.isEmpty()) { + continue; + } + stats.put(statId, Double.parseDouble(entry.substring(start + 1, end))); + } catch (NumberFormatException ignored) { + // skip malformed values + } + } + return stats; + } } diff --git a/src/main/java/net/tfminecraft/magic/util/CostFormatter.java b/src/main/java/net/tfminecraft/magic/util/CostFormatter.java index 203bf5c..4324d3a 100644 --- a/src/main/java/net/tfminecraft/magic/util/CostFormatter.java +++ b/src/main/java/net/tfminecraft/magic/util/CostFormatter.java @@ -15,6 +15,15 @@ public final class CostFormatter { private CostFormatter() {} + public static void appendInput(List lore, Map costs) { + if (lore == null || costs == null || costs.isEmpty()) { + return; + } + lore.add(""); + lore.add(StringFormatter.formatHex("#76de91§lInput:")); + lore.addAll(getCostsFormatted(costs)); + } + public static List getCostsFormatted(Map map) { List result = new ArrayList<>(); if (map == null || map.isEmpty()) { diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index b43d671..8c58c83 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -99,6 +99,10 @@ gear: output-slot: 16 confirm_seconds: 5 + # When false, rune socket colours use only the archetype suffix (e.g. "Minor Rune") + # without the band prefix (Common / Rare / Epic / Legendary). Band still gates attunement. + socket_rarity_prefix: true + # One-sided bonus for holding a weapon attuned to the element of the spell you cast. # Keyed by the weapon's tier band in that element. A weapon with no attunement in an # element contributes nothing; there is no penalty side, because a weapon that cannot diff --git a/src/main/resources/gear/archetypes.yml b/src/main/resources/gear/archetypes.yml index 353ac3b..a994e13 100644 --- a/src/main/resources/gear/archetypes.yml +++ b/src/main/resources/gear/archetypes.yml @@ -1,39 +1,51 @@ # Mage weapon archetypes. Templates are TLibs MMOItems paths Fran owns. # slots: id -> TLibs colour suffix. Prefix comes from socket-colours.yml. # Colour written = "{prefix} {suffix}", e.g. "Rare Staff Projectile Rune". +# icon: ItemRef path for picker/assembly GUIs (v.MATERIAL, m.TYPE.ID, ia.namespace:id). staff: name: Staff - template: m.mage_staffs.mage_iron_staff + template: m.mage_staffs.mage_custom_staff + icon: v.STICK melee: false required: - core - - form - - focus + - handle + - tome + - tome2 + - tome3 slots: - projectile: "Staff Projectile Rune" - support: "Staff Support Rune" - minor_spell: "Staff Minor Spell Rune" - major_spell: "Staff Major Spell Rune" + minor_rune: "Minor Rune" + lesser_rune: "Lesser Rune" + greater_rune: "Greater Rune" + ascendant_rune: "Ascedant Rune" wand: name: Wand - template: m.magic.wand_template + template: m.mage_wands.mage_custom_wand + icon: v.BLAZE_ROD melee: false required: - core - - focus + - handle + - tome slots: - minor_spell: "Wand Minor Spell Rune" - major_spell: "Wand Major Spell Rune" + minor_rune: "Minor Rune" + lesser_rune: "Lesser Rune" + greater_rune: "Greater Rune" + ascendant_rune: "Ascedant Rune" sword: name: Sword - template: m.magic.sword_template + template: m.mage_swords.mage_custom_sword + icon: v.IRON_SWORD melee: true required: - core - - form + - handle + - tome slots: - support: "Sword Support Rune" - spell: "Sword Spell Rune" + minor_rune: "Minor Rune" + lesser_rune: "Lesser Rune" + greater_rune: "Greater Rune" + ascendant_rune: "Ascedant Rune" diff --git a/src/main/resources/gear/model-schemes.yml b/src/main/resources/gear/model-schemes.yml new file mode 100644 index 0000000..98804b3 --- /dev/null +++ b/src/main/resources/gear/model-schemes.yml @@ -0,0 +1,26 @@ +# Placeholder gear skins. Fran replaces ia. paths. +# Each scheme maps archetype -> ItemRef path (ia.namespace:id or v.MATERIAL[.cmd]). + +iron: + models: + - staff(ia.tfmc_magic:iron_staff) + - wand(ia.tfmc_magic:iron_wand) + - sword(ia.tfmc_magic:iron_sword) + +steel: + models: + - staff(ia.tfmc_magic:steel_staff) + - wand(ia.tfmc_magic:steel_wand) + - sword(ia.tfmc_magic:steel_sword) + +abyssalite: + models: + - staff(ia.tfmc_magic:abyssalite_staff) + - wand(ia.tfmc_magic:abyssalite_wand) + - sword(ia.tfmc_magic:abyssalite_sword) + +mythril: + models: + - staff(ia.tfmc_magic:mythril_staff) + - wand(ia.tfmc_magic:mythril_wand) + - sword(ia.tfmc_magic:mythril_sword) diff --git a/src/main/resources/gear/part-types.yml b/src/main/resources/gear/part-types.yml index 5a614f2..3b711e1 100644 --- a/src/main/resources/gear/part-types.yml +++ b/src/main/resources/gear/part-types.yml @@ -3,9 +3,15 @@ core: slot: 10 name: Core -form: +handle: slot: 11 - name: Form -focus: + name: Handle +tome: slot: 12 - name: Focus + name: Tome +tome2: + slot: 13 + name: Tome2 +tome3: + slot: 14 + name: Tome3 \ No newline at end of file diff --git a/src/main/resources/gear/parts.yml b/src/main/resources/gear/parts.yml index 66e5299..8dd09fb 100644 --- a/src/main/resources/gear/parts.yml +++ b/src/main/resources/gear/parts.yml @@ -1,72 +1,527 @@ # PoC parts. Fran tunes socket counts, items, and costs. # Ceiling is 4 empty sockets on the finished weapon; extras are dropped at craft. +# part-limit: extra part categories a core unlocks (mandatory once that core is chosen). +# stats: MMOItems ids with doubles, same shape as cost — attack_damage(4). +# model-scheme: scheme id, optional weight like iron(10). Looked up in model-schemes.yml. -timber_core: - name: "§7Timber Core" +iron_staff_core: + name: "#d8d8d8Iron Magical Core" part-type: core + tier: 1 type: - staff + item: m.materials.iron_core + model-scheme: iron + cost: + - v.iron_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + sockets: {} + lore: + - "§7A basic magical core with one rune slot" + +steel_staff_core: + name: "#7f7d80Steel Magical Core" + part-type: core + tier: 2 + type: + - staff + item: m.materials.steel_core + model-scheme: steel + cost: + - m.materials.steel_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + - tome + sockets: {} + lore: + - "§7A more advanced magical core with two rune slots" + +abyssalite_staff_core: + name: "#3b4e60Abyssalite Magical Core" + part-type: core + tier: 3 + type: + - staff + item: m.materials.abyssalite_core + model-scheme: abyssalite + cost: + - m.materials.abyssalite_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + - tome + - tome2 + sockets: {} + lore: + - "§7An incredible magical core with three rune slots" + +mythril_staff_core: + name: "#9be1f3Mythril Magical Core" + part-type: core + tier: 4 + type: + - staff + item: m.materials.mythril_core + model-scheme: mythril + cost: + - m.materials.mythril_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + - tome + - tome2 + - tome3 + sockets: {} + lore: + - "§7An insanely powerful magical core with four rune slots" + +iron_wand_core: + name: "#d8d8d8Iron Magical Core" + part-type: core + tier: 1 + type: + - wand + item: m.materials.iron_core + model-scheme: iron + cost: + - v.iron_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + stats: + - attack_damage(4) + - attack_speed(1.2) + - mana_cost(2) + sockets: {} + lore: + - "§7A basic magical core with one rune slot and a simple way of mana channeling" + +steel_wand_core: + name: "#7f7d80Steel Magical Core" + part-type: core + tier: 2 + type: - wand + item: m.materials.steel_core + model-scheme: steel + cost: + - m.materials.steel_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + sockets: {} + lore: + - "§7An improved magical core with one rune slot and a better way of mana channeling" + +abyssalite_wand_core: + name: "#3b4e60Abyssalite Magical Core" + part-type: core + tier: 3 + type: + - wand + item: m.materials.abyssalite_core + model-scheme: abyssalite + cost: + - m.materials.abyssalite_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + - tome + sockets: {} + lore: + - "§7A good magical core with two rune slots and an advanced way of mana channeling" + +mythril_wand_core: + name: "#9be1f3Mythril Magical Core" + part-type: core + tier: 4 + type: + - wand + item: m.materials.mythril_core + model-scheme: mythril + cost: + - m.materials.mythril_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + - tome + stats: + - attack_damage(6) + - attack_speed(1.2) + - mana_cost(1) + sockets: {} + lore: + - "§7A great magical core with two rune slots and a powerful way of mana channeling" + +iron_sword_core: + name: "#d8d8d8Iron Magical Core" + part-type: core + tier: 1 + type: + - sword + item: m.materials.iron_core + model-scheme: iron + cost: + - v.iron_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + sockets: {} + lore: + - "§7A basic magical core with one rune slot and a simple balde edge" + +steel_sword_core: + name: "#7f7d80Steel Magical Core" + part-type: core + tier: 2 + type: + - sword + item: m.materials.steel_core + model-scheme: steel + cost: + - m.materials.steel_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + sockets: {} + lore: + - "§7An improved magical core with one rune slot and a better blade egde" + +abyssalite_sword_core: + name: "#3b4e60Abyssalite Magical Core" + part-type: core + tier: 3 + type: + - sword + item: m.materials.abyssalite_core + model-scheme: abyssalite + cost: + - m.materials.abyssalite_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + - tome + sockets: {} + lore: + - "§7A good magical core with two rune slots and an advanced blade edge" + +mythril_sword_core: + name: "#9be1f3Mythril Magical Core" + part-type: core + tier: 4 + type: - sword - item: v.oak_log + item: m.materials.mythril_core + model-scheme: mythril cost: - - v.oak_log(2) + - m.materials.mythril_ingot(4) + - m.currency.enchanted_dust(4) + part-limit: + - handle + - tome sockets: {} lore: - - "§8The spine of the piece" + - "§7A great magical core with two rune slots and a powerful blade edge" -oak_shaft: - name: "§7Oak Shaft" - part-type: form +oak_handle: + name: "§dOak Magical Handle" + part-type: handle + tier: 1 type: - staff + - wand + - sword item: v.stick cost: - - v.stick(4) + - v.stick(2) + - m.currency.enchanted_dust(2) + sockets: + minor_rune: 1 + lore: + - "§7A simple handle capable of channeling minor runes" + +basic_handle: + name: "§dBasic Magical Handle" + part-type: handle + tier: 2 + type: + - staff + - wand + - sword + item: m.materials.basic_handle + cost: + - v.stick(2) + - m.currency.enchanted_dust(4) + sockets: + lesser_rune: 1 + lore: + - "§7A better handle capable of channeling lesser runes" + +petty_handle: + name: "§dPetty Magical Handle" + part-type: handle + tier: 3 + type: + - staff + - wand + - sword + item: m.materials.petty_handle + cost: + - v.stick(2) + - m.currency.enchanted_dust(6) sockets: - projectile: 1 - support: 1 + greater_rune: 1 lore: - - "§8Staff form: projectile and support" + - "§7An advanced handle capable of channeling greater runes" -steel_blade: - name: "§7Steel Blade" - part-type: form +heavy_handle: + name: "§dHeavy Magical Handle" + part-type: handle + tier: 4 type: + - staff + - wand + - sword + item: m.materials.heavy_handle + cost: + - v.stick(2) + - m.currency.enchanted_dust(8) + sockets: + ascendant_rune: 1 + lore: + - "§7An amazing handle capable of channeling ascendant runes" + +normal_oak_tome: + name: "§dSimple Tome" + part-type: tome + tier: 1 + type: + - staff + - wand + - sword + item: v.book + cost: + - v.book(2) + lore: + - "§7A mundane book with no effect" + +oak_tome: + name: "§dSimple Magical Tome" + part-type: tome + tier: 1 + type: + - staff + - wand - sword - item: v.iron_ingot + item: v.book cost: - - v.iron_ingot(3) + - v.book(2) + - m.currency.enchanted_dust(2) sockets: - support: 1 - spell: 1 + minor_rune: 1 lore: - - "§8Sword form: support and spell" + - "§7A simple tome capable of channeling minor runes" -amber_focus: - name: "§7Amber Focus" - part-type: focus +basic_tome: + name: "§dBasic Magical Tome" + part-type: tome + tier: 2 type: - staff - item: v.glowstone_dust + - wand + - sword + item: m.materials.basic_tome cost: - - v.glowstone_dust(2) + - v.book(2) + - m.currency.enchanted_dust(4) sockets: - minor_spell: 1 - major_spell: 1 + lesser_rune: 1 lore: - - "§8Staff focus: two spell sockets" + - "§7A better tome capable of channeling lesser runes" -glass_focus: - name: "§7Glass Focus" - part-type: focus +petty_tome: + name: "§dPetty Magical Tome" + part-type: tome + tier: 3 type: + - staff - wand - item: v.glass + - sword + item: m.materials.petty_tome cost: - - v.glass(2) + - v.book(2) + - m.currency.enchanted_dust(6) sockets: - minor_spell: 1 - major_spell: 1 + greater_rune: 1 lore: - - "§8Wand focus: two spell sockets" + - "§7An advanced tome capable of channeling greater runes" + +heavy_tome: + name: "§dHeavy Magical Tome" + part-type: tome + tier: 4 + type: + - staff + - wand + - sword + item: m.materials.heavy_tome + cost: + - v.book(2) + - m.currency.enchanted_dust(8) + sockets: + ascendant_rune: 1 + lore: + - "§7An amazing tome capable of channeling ascendant runes" + +normal_oak_tome2: + name: "§dSimple Tome" + part-type: tome2 + tier: 1 + type: + - staff + - wand + - sword + item: v.book + cost: + - v.book(2) + lore: + - "§7A mundane book with no effect" + +oak_tome2: + name: "§dSimple Magical Tome" + part-type: tome2 + tier: 1 + type: + - staff + item: v.book + cost: + - v.book(2) + - m.currency.enchanted_dust(2) + sockets: + minor_rune: 1 + lore: + - "§7A simple tome capable of channeling minor runes" + +basic_tome2: + name: "§dBasic Magical Tome" + part-type: tome2 + tier: 2 + type: + - staff + item: m.materials.basic_tome + cost: + - v.book(2) + - m.currency.enchanted_dust(4) + sockets: + lesser_rune: 1 + lore: + - "§7A better tome capable of channeling lesser runes" + +petty_tome2: + name: "§dPetty Magical Tome" + part-type: tome2 + tier: 3 + type: + - staff + item: m.materials.petty_tome + cost: + - v.book(2) + - m.currency.enchanted_dust(6) + sockets: + greater_rune: 1 + lore: + - "§7An advanced tome capable of channeling greater runes" + +heavy_tome2: + name: "§dHeavy Magical Tome" + part-type: tome2 + tier: 4 + type: + - staff + item: m.materials.heavy_tome + cost: + - v.book(2) + - m.currency.enchanted_dust(8) + sockets: + ascendant_rune: 1 + lore: + - "§7An amazing tome capable of channeling ascendant runes" + +normal_oak_tome3: + name: "§dSimple Tome" + part-type: tome3 + tier: 1 + type: + - staff + - wand + - sword + item: v.book + cost: + - v.book(2) + lore: + - "§7A mundane book with no effect" + +oak_tome3: + name: "§dSimple Magical Tome" + part-type: tome3 + tier: 1 + type: + - staff + item: v.book + cost: + - v.book(2) + - m.currency.enchanted_dust(2) + sockets: + minor_rune: 1 + lore: + - "§7A simple tome capable of channeling minor runes" + +basic_tome3: + name: "§dBasic Magical Tome" + part-type: tome3 + tier: 2 + type: + - staff + item: m.materials.basic_tome + cost: + - v.book(2) + - m.currency.enchanted_dust(4) + sockets: + lesser_rune: 1 + lore: + - "§7A better tome capable of channeling lesser runes" + +petty_tom32: + name: "§dPetty Magical Tome" + part-type: tome3 + tier: 3 + type: + - staff + item: m.materials.petty_tome + cost: + - v.book(2) + - m.currency.enchanted_dust(6) + sockets: + greater_rune: 1 + lore: + - "§7An advanced tome capable of channeling greater runes" + +heavy_tome3: + name: "§dHeavy Magical Tome" + part-type: tome3 + tier: 4 + type: + - staff + item: m.materials.heavy_tome + cost: + - v.book(2) + - m.currency.enchanted_dust(8) + sockets: + ascendant_rune: 1 + lore: + - "§7An amazing tome capable of channeling ascendant runes" + + diff --git a/src/main/resources/gear/socket-colours.yml b/src/main/resources/gear/socket-colours.yml index 15513cf..4cbbaf5 100644 --- a/src/main/resources/gear/socket-colours.yml +++ b/src/main/resources/gear/socket-colours.yml @@ -1,6 +1,7 @@ # Resonance band -> TLibs colour prefix. -# Colour string = prefix + " " + archetype slot suffix. +# Colour string = prefix + " " + archetype slot suffix (when config gear.socket_rarity_prefix is true). # Must match tlibs socket-tier-groups keys exactly. +# Set gear.socket_rarity_prefix: false in config.yml to write suffix only (no Common/Rare/Epic/Legendary). prefix: default: Common @@ -11,8 +12,11 @@ prefix: # Socket slot id -> player-facing name in the assembly GUI. labels: - projectile: Projectile - support: Support - minor_spell: Minor Spell - major_spell: Major Spell - spell: Spell + minor_rune: Minor Rune + lesser_rune: Lesser Rune + greater_rune: Greater Rune + asecandt_rune: Ascendant Rune + minor_armor_rune: Minor Armor Rune + lesser_armor_rune: Lesser Armor Rune + greater_armor_rune: Greater Armor Rune + asecandt_armor_rune: Ascendant Armor Rune diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 5281cf0..d27d7ac 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -261,11 +261,29 @@ gear: rift: "#ff5555The weapon is left with {rift}% rift." + summary_title: "{color:label_accent}Imbued resonance" + + summary_line_prefix: "{color:label_muted} " + + summary_unattuned: "{color:label_muted}None yet" + + summary_rift: "{color:corruption}Rift {color:label_muted}{rift}%" + + summary_hint_resonance: "{color:label_muted}Apply another enchanted charge to raise resonance on this weapon." + + summary_hint_resonance_and_clean: "{color:label_muted}Apply another enchanted charge to raise resonance or clean rift from this weapon." + busy: "#aaaaaa§oThis station is already mid-attunement" eject: - unattuned: "#aaaaaa§oThe weapon is inert. It holds no element yet" + unattuned: "#aaaaaa§oThe weapon is inert. Shift-left-click the station to take the materials back" + + abort: + + done: "#aaaaaa§oThe craft comes apart. The charge, if any, is spent" + + not_yours: "#ff5555Someone else is attuning this weapon." broken: From 61383d30a1cd39709fb9d60212c9672a772d6144 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:19:12 +0000 Subject: [PATCH 2/4] fix: avoid deprecated stat history and model data calls in gear Co-Authored-By: Claude Opus 5.5 (1M context) --- .../java/net/tfminecraft/magic/gear/GearModelResolver.java | 5 ++++- .../java/net/tfminecraft/magic/gear/GearStatApplicator.java | 5 ++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java b/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java index c315700..b136f80 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java @@ -10,6 +10,7 @@ import org.bukkit.inventory.meta.ItemMeta; import net.tfminecraft.tlibs.TLibs; +import net.tfminecraft.magic.util.LegacyModelData; import net.tfminecraft.magic.Magic; import net.tfminecraft.magic.util.ItemRef; @@ -98,6 +99,8 @@ public static ItemStack apply(ItemStack stack, GearType type, Collection= 3) { ItemMeta meta = stack.getItemMeta(); if (meta != null) { - meta.setCustomModelData(Integer.parseInt(parts[2])); + LegacyModelData.set(meta, Integer.parseInt(parts[2])); stack.setItemMeta(meta); } } diff --git a/src/main/java/net/tfminecraft/magic/gear/GearStatApplicator.java b/src/main/java/net/tfminecraft/magic/gear/GearStatApplicator.java index 1a280bf..56b9673 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearStatApplicator.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearStatApplicator.java @@ -61,7 +61,7 @@ public static void apply(MMOItem mmo, Collection parts) { if (itemStat == null) { continue; } - StatHistory hist = StatHistory.from(mmo, itemStat); + StatHistory hist = mmo.computeStatHistory(itemStat); if (hist != null) { hist.clearExternalData(); Object og = hist.getOriginalData(); @@ -77,7 +77,6 @@ public static void apply(MMOItem mmo, Collection parts) { } } - @SuppressWarnings("deprecation") private static void applyDouble(MMOItem mmo, String statId, double value) { ItemStat itemStat = resolve(statId); if (itemStat == null) { @@ -85,7 +84,7 @@ private static void applyDouble(MMOItem mmo, String statId, double value) { } DoubleData data = new DoubleData(value); mmo.setData(itemStat, data); - StatHistory hist = StatHistory.from(mmo, itemStat); + StatHistory hist = mmo.computeStatHistory(itemStat); if (hist != null) { hist.registerExternalData(data); mmo.setStatHistory(itemStat, hist); From c6250b73965cbe0829ab680f04ec53c4d6c5337a Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:30:34 +0000 Subject: [PATCH 3/4] fix: restore spell tiers, rune keybinds and resonance locks The same "magic update" commit also dropped the spell side of the first commit. Bring back: - Spell tier gates. skills.yml tier is the floor for both the weapon's band and the caster's resonance, with separate weapon and spell refusal lines. - /magic rune keybind to rebind a held rune's abilities, with the magic.rune.keybind permission and runes.types config. - Element permissions. Locked schools stay hidden in the resonance menu, do not grow, and are skipped by admin resonance commands. - The cast mode switch chat line and "Click to Select" hint. The runes section merges with the cast triggers from #25. Element names keep the gradient colouring from #26. The per-cast durability debug log is not restored. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../java/net/tfminecraft/magic/Cache.java | 6 + .../generate/ArtifactItemBuilder.java | 5 +- .../attunement/AttunementCaptureService.java | 6 + .../magic/command/MagicCommand.java | 146 ++++++++++++++++-- .../tfminecraft/magic/gear/RuneKeybind.java | 89 +++++++++++ .../magic/gui/ArtifactCreateGuiBuilder.java | 3 +- .../magic/gui/ResonanceGuiBuilder.java | 9 +- .../magic/listener/ResonanceCastListener.java | 82 ++++++---- .../magic/listener/SpellTierGate.java | 38 +++++ .../magic/loader/ConfigLoader.java | 35 +++++ .../magic/loader/SkillsLoader.java | 12 +- .../magic/manager/ResonanceGuiManager.java | 30 ++++ .../tfminecraft/magic/model/ElementDef.java | 16 ++ .../magic/registry/SkillElementRegistry.java | 54 +++++-- .../magic/service/ResonanceService.java | 3 + .../net/tfminecraft/magic/util/MagicText.java | 14 +- src/main/resources/artifacts/sacrifice.yml | 6 +- src/main/resources/config.yml | 10 +- src/main/resources/elements/elements.yml | 52 ++++--- src/main/resources/messages.yml | 42 ++++- src/main/resources/plugin.yml | 26 +++- src/main/resources/skills.yml | 93 ++++++++++- 22 files changed, 674 insertions(+), 103 deletions(-) create mode 100644 src/main/java/net/tfminecraft/magic/gear/RuneKeybind.java create mode 100644 src/main/java/net/tfminecraft/magic/listener/SpellTierGate.java diff --git a/src/main/java/net/tfminecraft/magic/Cache.java b/src/main/java/net/tfminecraft/magic/Cache.java index 3061678..f3a850f 100644 --- a/src/main/java/net/tfminecraft/magic/Cache.java +++ b/src/main/java/net/tfminecraft/magic/Cache.java @@ -1,10 +1,13 @@ package net.tfminecraft.magic; import java.util.Collections; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Locale; import java.util.Set; +import io.lumine.mythic.lib.skill.trigger.TriggerType; + /** * Runtime flags from config.yml. */ @@ -36,6 +39,9 @@ public final class Cache { */ public static volatile Set castTriggers = Set.of(); + public static Set runeTypes = new HashSet<>(); + public static Set runeKeybinds = new LinkedHashSet<>(); + public static double tickIntervalSeconds() { return Math.max(1L, tickIntervalTicks) / 20.0; } diff --git a/src/main/java/net/tfminecraft/magic/artifact/generate/ArtifactItemBuilder.java b/src/main/java/net/tfminecraft/magic/artifact/generate/ArtifactItemBuilder.java index f6fbef7..0b5cfe8 100644 --- a/src/main/java/net/tfminecraft/magic/artifact/generate/ArtifactItemBuilder.java +++ b/src/main/java/net/tfminecraft/magic/artifact/generate/ArtifactItemBuilder.java @@ -217,10 +217,7 @@ public static String kindForPath(ArtifactTypeDef type, String path) { private static String buildDisplayName(ArtifactTypeDef type, String baseName) { String rolledName = baseName != null && !baseName.isBlank() ? baseName : type.getElementId(); ElementDef element = ElementRegistry.getById(type.getElementId()); - String elementColor = element != null && element.getColor() != null && !element.getColor().isBlank() - ? element.getColor() - : "#ffffff"; - return MagicText.format(elementColor + rolledName); + return MagicText.elementText(element, rolledName); } private static void applyName(MMOItem mmo, String displayName) { diff --git a/src/main/java/net/tfminecraft/magic/attunement/AttunementCaptureService.java b/src/main/java/net/tfminecraft/magic/attunement/AttunementCaptureService.java index 3099931..89a5a40 100644 --- a/src/main/java/net/tfminecraft/magic/attunement/AttunementCaptureService.java +++ b/src/main/java/net/tfminecraft/magic/attunement/AttunementCaptureService.java @@ -5,6 +5,8 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import net.tfminecraft.magic.model.ElementDef; +import net.tfminecraft.magic.registry.ElementRegistry; import net.tfminecraft.magic.session.ResonanceSession; public final class AttunementCaptureService { @@ -22,6 +24,10 @@ public static void credit( if (session == null || elementId == null || elementId.isBlank() || gain <= EPSILON) { return; } + ElementDef element = ElementRegistry.getById(elementId.toLowerCase(java.util.Locale.ROOT)); + if (element != null && !element.isUnlocked(meditator)) { + return; + } double before = session.getResonance(elementId); session.addResonance(elementId, gain); AuraLog.append( diff --git a/src/main/java/net/tfminecraft/magic/command/MagicCommand.java b/src/main/java/net/tfminecraft/magic/command/MagicCommand.java index 4341c4a..a1e9586 100644 --- a/src/main/java/net/tfminecraft/magic/command/MagicCommand.java +++ b/src/main/java/net/tfminecraft/magic/command/MagicCommand.java @@ -15,6 +15,7 @@ import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; +import io.lumine.mythic.lib.skill.trigger.TriggerType; import net.tfminecraft.magic.Cache; import net.tfminecraft.magic.Magic; import net.tfminecraft.magic.Messages; @@ -35,6 +36,7 @@ import net.tfminecraft.magic.artifact.shrine.ShrineChargeService; import net.tfminecraft.magic.attunement.AuraLog; import net.tfminecraft.magic.gear.GearRefresher; +import net.tfminecraft.magic.gear.RuneKeybind; import net.tfminecraft.magic.model.ElementDef; import net.tfminecraft.magic.profile.MagicProfileService; import net.tfminecraft.magic.registry.ElementRegistry; @@ -48,13 +50,23 @@ public final class MagicCommand implements CommandExecutor, TabCompleter { @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { - if (!hasAdmin(sender)) { - sender.sendMessage(Messages.get("admin.no_permission")); + if (args.length == 0) { + if (hasAdmin(sender)) { + sender.sendMessage(Messages.get("admin.usage")); + } else if (hasRuneKeybind(sender)) { + sender.sendMessage(Messages.get("rune.usage")); + } else { + sender.sendMessage(Messages.get("admin.no_permission")); + } return true; } - if (args.length == 0) { - sender.sendMessage(Messages.get("admin.usage")); + if ("rune".equalsIgnoreCase(args[0])) { + return handleRune(sender, args); + } + + if (!hasAdmin(sender)) { + sender.sendMessage(Messages.get("admin.no_permission")); return true; } @@ -97,6 +109,38 @@ public boolean onCommand(CommandSender sender, Command command, String label, St return true; } + private static boolean handleRune(CommandSender sender, String[] args) { + if (!(sender instanceof Player player)) { + sender.sendMessage(Messages.get("rune.players_only")); + return true; + } + if (!hasRuneKeybind(sender)) { + sender.sendMessage(Messages.get("rune.no_permission")); + return true; + } + if (args.length < 3 || !"keybind".equalsIgnoreCase(args[1])) { + sender.sendMessage(Messages.get("rune.usage")); + return true; + } + TriggerType trigger = resolveRuneKeybind(args[2]); + if (trigger == null) { + sender.sendMessage(Messages.get("rune.unknown_trigger")); + return true; + } + ItemStack held = player.getInventory().getItemInMainHand(); + RuneKeybind.Outcome outcome = RuneKeybind.apply(held, trigger); + switch (outcome.result()) { + case NOT_RUNE -> sender.sendMessage(Messages.get("rune.not_a_rune")); + case NO_ABILITIES -> sender.sendMessage(Messages.get("rune.no_abilities")); + case FAILED -> sender.sendMessage(Messages.get("rune.failed")); + case OK -> { + player.getInventory().setItemInMainHand(outcome.item()); + sender.sendMessage(Messages.get("rune.success", "trigger", trigger.name())); + } + } + return true; + } + private static boolean handleRefresh(CommandSender sender) { if (!(sender instanceof Player player)) { sender.sendMessage(Messages.get("gear.refresh.players_only")); @@ -198,7 +242,7 @@ private static boolean handleResonance(CommandSender sender, String[] args) { } if ("reset".equals(action)) { String elementArg = args.length >= 4 ? args[3] : "all"; - if (!applyResonance(session, elementArg, Cache.defaultResonance, false, sender)) { + if (!applyResonance(target, session, elementArg, Cache.defaultResonance, false, sender)) { return true; } persistResonance(target, session); @@ -219,7 +263,7 @@ private static boolean handleResonance(CommandSender sender, String[] args) { return true; } if ("set".equals(action)) { - if (!applyResonance(session, elementArg, amount, false, sender)) { + if (!applyResonance(target, session, elementArg, amount, false, sender)) { return true; } persistResonance(target, session); @@ -231,7 +275,7 @@ private static boolean handleResonance(CommandSender sender, String[] args) { return true; } if ("add".equals(action)) { - if (!applyResonance(session, elementArg, amount, true, sender)) { + if (!applyResonance(target, session, elementArg, amount, true, sender)) { return true; } persistResonance(target, session); @@ -287,6 +331,7 @@ private static boolean handleShrine(CommandSender sender, String[] args) { } private static boolean applyResonance( + Player target, ResonanceSession session, String elementArg, double amount, @@ -297,22 +342,49 @@ private static boolean applyResonance( return false; } if ("all".equalsIgnoreCase(elementArg)) { + List skipped = new ArrayList<>(); + int applied = 0; for (ElementDef element : ElementRegistry.getAll()) { - double next = add ? session.getResonance(element.getId()) + amount : amount; + double current = session.getResonance(element.getId()); + double next = add ? current + amount : amount; + if (isGain(current, next) && !element.isUnlocked(target)) { + skipped.add(element.getId()); + continue; + } session.setResonance(element.getId(), next); + applied++; } - return true; + if (!skipped.isEmpty()) { + sender.sendMessage(Messages.get( + "resonance.admin.locked_skipped", + "player", target.getName(), + "elements", String.join(", ", skipped))); + } + return applied > 0; } ElementDef element = ElementRegistry.getById(elementArg.toLowerCase(Locale.ROOT)); if (element == null) { sender.sendMessage(Messages.get("resonance.admin.unknown_element")); return false; } - double next = add ? session.getResonance(element.getId()) + amount : amount; + double current = session.getResonance(element.getId()); + double next = add ? current + amount : amount; + if (isGain(current, next) && !element.isUnlocked(target)) { + sender.sendMessage(Messages.get( + "resonance.admin.locked", + "player", target.getName(), + "permission", element.getPermission(), + "element", element.getId())); + return false; + } session.setResonance(element.getId(), next); return true; } + private static boolean isGain(double current, double next) { + return next > current + 0.0001; + } + private static void persistResonance(Player target, ResonanceSession session) { MagicProfileService profiles = Magic.plugin.getProfileService(); if (profiles != null) { @@ -533,13 +605,63 @@ private static boolean hasAdmin(CommandSender sender) { return sender.hasPermission("magic.admin") || sender.hasPermission("magic.admin.reload"); } + private static boolean hasRuneKeybind(CommandSender sender) { + return sender.hasPermission("magic.rune.keybind"); + } + + private static TriggerType resolveRuneKeybind(String raw) { + if (raw == null || raw.isBlank()) { + return null; + } + String want = raw.trim().toUpperCase(Locale.ROOT); + for (TriggerType type : Cache.runeKeybinds) { + if (type != null && want.equals(type.name())) { + return type; + } + } + return null; + } + @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { - if (!hasAdmin(sender) || args.length == 0) { + if (args.length == 0) { + return Collections.emptyList(); + } + boolean admin = hasAdmin(sender); + boolean rune = hasRuneKeybind(sender); + if (!admin && !rune) { return Collections.emptyList(); } if (args.length == 1) { - return filterPrefix(SUBCOMMANDS, args[0]); + List options = new ArrayList<>(); + if (rune) { + options.add("rune"); + } + if (admin) { + options.addAll(SUBCOMMANDS); + } + return filterPrefix(options, args[0]); + } + if ("rune".equalsIgnoreCase(args[0])) { + if (!rune) { + return Collections.emptyList(); + } + if (args.length == 2) { + return filterPrefix(List.of("keybind"), args[1]); + } + if (args.length == 3 && "keybind".equalsIgnoreCase(args[1])) { + List names = new ArrayList<>(); + for (TriggerType type : Cache.runeKeybinds) { + if (type != null && type.name() != null) { + names.add(type.name()); + } + } + return filterPrefix(names, args[2]); + } + return Collections.emptyList(); + } + if (!admin) { + return Collections.emptyList(); } if ("fillchest".equalsIgnoreCase(args[0])) { if (args.length == 2) { diff --git a/src/main/java/net/tfminecraft/magic/gear/RuneKeybind.java b/src/main/java/net/tfminecraft/magic/gear/RuneKeybind.java new file mode 100644 index 0000000..458fa04 --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/gear/RuneKeybind.java @@ -0,0 +1,89 @@ +package net.tfminecraft.magic.gear; + +import java.util.ArrayList; +import java.util.List; + +import org.bukkit.inventory.ItemStack; + +import io.lumine.mythic.lib.api.item.NBTItem; +import io.lumine.mythic.lib.skill.trigger.TriggerType; +import net.Indyuce.mmoitems.ItemStats; +import net.Indyuce.mmoitems.api.item.mmoitem.LiveMMOItem; +import net.Indyuce.mmoitems.stat.data.AbilityData; +import net.Indyuce.mmoitems.stat.data.AbilityListData; +import net.tfminecraft.magic.Cache; + +public final class RuneKeybind { + + public enum Result { + NOT_RUNE, + NO_ABILITIES, + FAILED, + OK + } + + public record Outcome(Result result, ItemStack item) { + public static Outcome of(Result result) { + return new Outcome(result, null); + } + + public static Outcome ok(ItemStack item) { + return new Outcome(Result.OK, item); + } + } + + private RuneKeybind() {} + + public static boolean isRune(ItemStack item) { + if (item == null || item.getType().isAir() || Cache.runeTypes.isEmpty()) { + return false; + } + if (!GearItemBuilder.mmoItemsPresent()) { + return false; + } + NBTItem nbt = NBTItem.get(item); + if (!nbt.hasType()) { + return false; + } + return Cache.runeTypes.contains(nbt.getType().toLowerCase()); + } + + public static Outcome apply(ItemStack held, TriggerType trigger) { + if (!isRune(held) || trigger == null) { + return Outcome.of(Result.NOT_RUNE); + } + try { + LiveMMOItem mmo = new LiveMMOItem(NBTItem.get(held)); + if (!mmo.hasData(ItemStats.ABILITIES)) { + return Outcome.of(Result.NO_ABILITIES); + } + AbilityListData current = (AbilityListData) mmo.getData(ItemStats.ABILITIES); + if (current == null || current.isEmpty()) { + return Outcome.of(Result.NO_ABILITIES); + } + List copies = new ArrayList<>(); + for (AbilityData old : current.getAbilities()) { + if (old == null || old.getAbility() == null) { + continue; + } + AbilityData copy = new AbilityData(old.getAbility(), trigger); + for (String modifier : old.getModifiers()) { + copy.setModifier(modifier, old.getParameter(modifier)); + } + copies.add(copy); + } + if (copies.isEmpty()) { + return Outcome.of(Result.NO_ABILITIES); + } + mmo.setData(ItemStats.ABILITIES, new AbilityListData(copies)); + ItemStack rebuilt = mmo.newBuilder().build(); + if (rebuilt == null || rebuilt.getType().isAir()) { + return Outcome.of(Result.FAILED); + } + rebuilt.setAmount(held.getAmount()); + return Outcome.ok(rebuilt); + } catch (Exception ex) { + return Outcome.of(Result.FAILED); + } + } +} diff --git a/src/main/java/net/tfminecraft/magic/gui/ArtifactCreateGuiBuilder.java b/src/main/java/net/tfminecraft/magic/gui/ArtifactCreateGuiBuilder.java index 1da969d..8243df8 100644 --- a/src/main/java/net/tfminecraft/magic/gui/ArtifactCreateGuiBuilder.java +++ b/src/main/java/net/tfminecraft/magic/gui/ArtifactCreateGuiBuilder.java @@ -13,6 +13,7 @@ import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; +import net.tfminecraft.magic.util.MagicText; import net.tfminecraft.magic.ArtifactCreateCache; import net.tfminecraft.magic.GuiCache; import net.tfminecraft.magic.artifact.create.ArtifactCreateSession; @@ -162,7 +163,7 @@ private static ItemStack buildElementItem(ElementDef element, ArtifactCreateSess if (meta == null) { return item; } - meta.setDisplayName(GuiText.format(element.getName())); + meta.setDisplayName(MagicText.elementName(element)); ElementRole role = session != null ? session.roleOf(element.getId()) : ElementRole.OFF; List lore = new ArrayList<>(); if (role != ElementRole.OFF) { diff --git a/src/main/java/net/tfminecraft/magic/gui/ResonanceGuiBuilder.java b/src/main/java/net/tfminecraft/magic/gui/ResonanceGuiBuilder.java index 71b2922..c52bdef 100644 --- a/src/main/java/net/tfminecraft/magic/gui/ResonanceGuiBuilder.java +++ b/src/main/java/net/tfminecraft/magic/gui/ResonanceGuiBuilder.java @@ -16,6 +16,7 @@ import net.tfminecraft.rpcharacters.objects.RPCharacter; import net.tfminecraft.rpcharacters.api.CharacterSkull; +import net.tfminecraft.magic.util.MagicText; import net.tfminecraft.magic.GuiCache; import net.tfminecraft.magic.integration.RpCharactersBridge; import net.tfminecraft.magic.model.CastModeDef; @@ -66,7 +67,7 @@ public static void populate(Inventory inventory, Player player, ResonanceSession buildCastModeItem(GuiCache.castModeRight, castModeId.equals(GuiCache.castModeRight.getId()), session)); for (ElementDef element : ElementRegistry.getAll()) { - if (element.getSlot() >= 0) { + if (element.getSlot() >= 0 && element.isUnlocked(player)) { inventory.setItem(element.getSlot(), buildElementItem(element, session)); } } @@ -165,8 +166,10 @@ private static ItemStack buildCastModeItem(CastModeDef mode, boolean selected, R lore.addAll(ModifierLore.linesForFlow(session)); } if (selected) { - lore.add(GuiText.text("label_accent", "Selected")); + lore.add(GuiText.format("{color:resonance_high}§lSelected")); meta.addEnchant(Enchantment.UNBREAKING, 1, true); + } else { + lore.add(GuiText.text("label_muted", "Click to Select")); } meta.setLore(lore); meta.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_ADDITIONAL_TOOLTIP, ItemFlag.HIDE_ENCHANTS); @@ -182,7 +185,7 @@ private static ItemStack buildElementItem(ElementDef element, ResonanceSession s if (meta == null) { return item; } - meta.setDisplayName(GuiText.format(element.getName())); + meta.setDisplayName(MagicText.elementName(element)); List lore = new ArrayList<>(); double resonance = session != null ? session.getResonance(element.getId()) : 0.0; lore.add(ResonanceBar.formatLore(element, resonance)); diff --git a/src/main/java/net/tfminecraft/magic/listener/ResonanceCastListener.java b/src/main/java/net/tfminecraft/magic/listener/ResonanceCastListener.java index e19c5af..576c04e 100644 --- a/src/main/java/net/tfminecraft/magic/listener/ResonanceCastListener.java +++ b/src/main/java/net/tfminecraft/magic/listener/ResonanceCastListener.java @@ -17,6 +17,7 @@ import io.lumine.mythic.lib.api.item.NBTItem; import io.lumine.mythic.lib.skill.Skill; import net.Indyuce.mmoitems.api.interaction.util.DurabilityItem; +import net.tfminecraft.magic.util.MagicText; import net.tfminecraft.magic.Cache; import net.tfminecraft.magic.Magic; import net.tfminecraft.magic.Messages; @@ -36,9 +37,11 @@ /** * The ways a mage weapon stops or taxes a spell. * - *

Refusal is a locked door. The weapon demands more of the spell's element - * than the caster carries, or was never attuned to that element at all, so the cast is - * cancelled before {@code whenCast} and costs nothing. It is never called a whiff. + *

Refusal is a locked door. The spell's {@code skills.yml} tier is the + * floor: the weapon must hold that element at that band, and the caster's resonance + * must meet the same band. A high-attuned staff does not block a lower-tier spell. + * Foreign element or a failed floor cancels before {@code whenCast} and costs + * nothing. It is never called a whiff. * *

Overload is carrying more than one staff. After refusal passes, the cast * always fumbles: mana and cooldown are spent, but the spell does not fire. @@ -52,7 +55,6 @@ */ public final class ResonanceCastListener implements Listener { - private static final double EPSILON = 0.0001; private static final int CHAT_KEYS_MAX = 512; private static final Map lastRefuseChat = new ConcurrentHashMap<>(); @@ -66,10 +68,12 @@ public void onPlayerCastSkill(PlayerCastSkillEvent event) { if (!SkillIdResolver.isActiveCast(cast)) { return; } - String elementId = SkillElementRegistry.elementOf(SkillIdResolver.resolveSkillId(cast)); + String skillId = SkillIdResolver.resolveSkillId(cast); + String elementId = SkillElementRegistry.elementOf(skillId); if (elementId == null) { return; } + int spellTier = SkillElementRegistry.tierOf(skillId); Player player = event.getPlayer(); if (player == null) { return; @@ -87,7 +91,7 @@ public void onPlayerCastSkill(PlayerCastSkillEvent event) { broken(player, weapon, elementId); return; } - if (refuse(player, weapon, elementId)) { + if (refuse(player, weapon, elementId, spellTier)) { event.setCancelled(true); return; } @@ -122,40 +126,60 @@ private static void wearWeapon(Player player, HeldSlot slot, ItemStack weapon) { GearHand.setHeld(player, slot, durability.decreaseDurability(1).toItem()); } - /** @return true when the weapon will not carry this element for this caster */ + /** @return true when this spell cannot fire on this weapon for this caster */ // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") - private static boolean refuse(Player player, ItemStack weapon, String elementId) { - double required = WeaponRequirement.fromItem(weapon).aura().getFill(elementId); + private static boolean refuse(Player player, ItemStack weapon, String elementId, int spellTier) { + double weaponFill = WeaponRequirement.fromItem(weapon).aura().getFill(elementId); ResonanceSession session = Magic.plugin.getResonanceGuiManager().getSessionManager().get(player); - double actual = session == null ? 0.0 : session.getResonance(elementId); - boolean foreign = required <= 0; - if (!foreign && actual + EPSILON >= required) { + double playerFill = session == null ? 0.0 : session.getResonance(elementId); + int weaponBand = TierBands.bandOf(elementId, weaponFill); + int playerBand = TierBands.bandOf(elementId, playerFill); + SpellTierGate.Refuse kind = SpellTierGate.refuse(weaponFill > 0, weaponBand, playerBand, spellTier); + if (kind == SpellTierGate.Refuse.NONE) { return false; } String elementName = elementName(elementId); - player.sendTitle( - Messages.get("cast.refuse_title"), - foreign - ? Messages.get("cast.refuse_sub_foreign", "element", elementName) - : Messages.get("cast.refuse_sub"), - 0, 25, 10); - player.playSound(player.getLocation(), Sound.BLOCK_BEACON_DEACTIVATE, SoundCategory.PLAYERS, 0.7f, 1.4f); - if (claimChat(player, weapon, elementId)) { - if (foreign) { - player.sendMessage(Messages.get("cast.refuse_chat_foreign", "element", elementName)); - } else { - String have = TierBands.numeralFor(elementId, actual); - player.sendMessage(Messages.get( - "cast.refuse_chat", + String need = numeralOrDash(spellTier); + String weaponHave = numeralOrDash(weaponBand); + String playerHave = numeralOrDash(playerBand); + String subtitle; + String chat; + switch (kind) { + case FOREIGN -> { + subtitle = Messages.get("cast.refuse_sub_foreign", "element", elementName); + chat = Messages.get("cast.refuse_chat_foreign", "element", elementName); + } + case WEAPON -> { + subtitle = Messages.get("cast.refuse_sub_weapon"); + chat = Messages.get( + "cast.refuse_chat_weapon", "element", elementName, - "need", TierBands.numeralFor(elementId, required), - "have", have.isEmpty() ? "-" : have)); + "have", weaponHave, + "need", need); } + default -> { + subtitle = Messages.get("cast.refuse_sub_spell"); + chat = Messages.get( + "cast.refuse_chat_spell", + "element", elementName, + "need", need, + "have", playerHave); + } + } + player.sendTitle(Messages.get("cast.refuse_title"), subtitle, 0, 25, 10); + player.playSound(player.getLocation(), Sound.BLOCK_BEACON_DEACTIVATE, SoundCategory.PLAYERS, 0.7f, 1.4f); + if (claimChat(player, weapon, elementId)) { + player.sendMessage(chat); } return true; } + private static String numeralOrDash(int band) { + String numeral = TierBands.numeral(band); + return numeral.isEmpty() ? "-" : numeral; + } + /** * A damaged weapon refuses everything, so nothing is spent here either. Shares the * refusal rate limiter so a player holding one is told once, not once per cast. @@ -216,7 +240,7 @@ private static boolean claimChat(Player player, ItemStack weapon, String element private static String elementName(String elementId) { ElementDef element = ElementRegistry.getById(elementId); - return element == null ? elementId : element.getName(); + return element == null ? elementId : MagicText.elementName(element); } public static void clearAll() { diff --git a/src/main/java/net/tfminecraft/magic/listener/SpellTierGate.java b/src/main/java/net/tfminecraft/magic/listener/SpellTierGate.java new file mode 100644 index 0000000..e6059b7 --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/listener/SpellTierGate.java @@ -0,0 +1,38 @@ +package net.tfminecraft.magic.listener; + +/** + * Per-spell floors: the weapon must hold the element at the spell's band, and the + * caster's resonance must meet the same band. A high-attuned staff does not block + * a lower-tier spell. + */ +public final class SpellTierGate { + + public enum Refuse { + NONE, + FOREIGN, + WEAPON, + SPELL + } + + private SpellTierGate() {} + + /** + * @param hasElement {@code false} when the weapon has no imbued fill for the spell's element + * @param weaponBand {@link net.tfminecraft.magic.charge.TierBands#bandOf} of that fill + * @param playerBand band of the caster's resonance in that element + * @param spellTier required band from skills.yml (1-4) + */ + public static Refuse refuse(boolean hasElement, int weaponBand, int playerBand, int spellTier) { + if (!hasElement) { + return Refuse.FOREIGN; + } + int need = Math.max(1, spellTier); + if (weaponBand < need) { + return Refuse.WEAPON; + } + if (playerBand < need) { + return Refuse.SPELL; + } + return Refuse.NONE; + } +} diff --git a/src/main/java/net/tfminecraft/magic/loader/ConfigLoader.java b/src/main/java/net/tfminecraft/magic/loader/ConfigLoader.java index bb1f1d2..1fa788a 100644 --- a/src/main/java/net/tfminecraft/magic/loader/ConfigLoader.java +++ b/src/main/java/net/tfminecraft/magic/loader/ConfigLoader.java @@ -4,12 +4,15 @@ import java.io.IOException; import java.util.List; +import java.util.Locale; + import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.InvalidConfigurationException; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; import net.tfminecraft.tlibs.interfaces.LoaderInterface; +import io.lumine.mythic.lib.skill.trigger.TriggerType; import net.tfminecraft.magic.Cache; import net.tfminecraft.magic.GuiCache; import net.tfminecraft.magic.Magic; @@ -110,6 +113,8 @@ public boolean loadSafe(File configFile) { loadMeditation(config.getConfigurationSection("meditation")); loadAttunement(config.getConfigurationSection("attunement")); loadGear(config.getConfigurationSection("gear")); + loadRuneTypes(config); + loadRuneKeybinds(config); return true; } @@ -122,6 +127,36 @@ private static void loadCastTriggers(List keybinds) { Magic.plugin.getLogger().info("[Magic] Cast triggers: CAST, API, " + String.join(", ", Cache.castTriggers)); } + private static void loadRuneTypes(FileConfiguration config) { + Cache.runeTypes.clear(); + for (String type : config.getStringList("runes.types")) { + if (type != null && !type.isBlank()) { + Cache.runeTypes.add(type.trim().toLowerCase()); + } + } + } + + private static void loadRuneKeybinds(FileConfiguration config) { + Cache.runeKeybinds.clear(); + for (String raw : config.getStringList("runes.keybinds")) { + if (raw == null || raw.isBlank()) { + continue; + } + String id = raw.trim().toUpperCase(Locale.ROOT); + TriggerType trigger; + try { + trigger = TriggerType.valueOf(id); + } catch (IllegalArgumentException | NullPointerException ex) { + trigger = null; + } + if (trigger == null) { + Magic.plugin.getLogger().warning("[Magic] Unknown rune keybind '" + raw + "'"); + continue; + } + Cache.runeKeybinds.add(trigger); + } + } + private static void loadGear(ConfigurationSection section) { if (section == null) { return; diff --git a/src/main/java/net/tfminecraft/magic/loader/SkillsLoader.java b/src/main/java/net/tfminecraft/magic/loader/SkillsLoader.java index e8c4447..a8ab8de 100644 --- a/src/main/java/net/tfminecraft/magic/loader/SkillsLoader.java +++ b/src/main/java/net/tfminecraft/magic/loader/SkillsLoader.java @@ -33,6 +33,7 @@ public boolean loadSafe(File configFile) { continue; } String elementId; + int tier = SkillElementRegistry.DEFAULT_TIER; if (config.isConfigurationSection(skillId)) { ConfigurationSection section = config.getConfigurationSection(skillId); if (section == null) { @@ -40,6 +41,15 @@ public boolean loadSafe(File configFile) { continue; } elementId = section.getString("element"); + if (section.contains("tier")) { + int raw = section.getInt("tier", SkillElementRegistry.DEFAULT_TIER); + int clamped = SkillElementRegistry.clampTier(raw); + if (raw != clamped) { + Magic.plugin.getLogger().warning("[Magic] skills.yml: tier " + raw + + " for skill '" + skillId + "' clamped to " + clamped); + } + tier = clamped; + } } else { elementId = config.getString(skillId); } @@ -54,7 +64,7 @@ public boolean loadSafe(File configFile) { skipped++; continue; } - SkillElementRegistry.register(skillId, normalized); + SkillElementRegistry.register(skillId, normalized, tier); } Magic.plugin.getLogger().info("[Magic] Loaded " + SkillElementRegistry.size() + " skill binding(s)" + (skipped > 0 ? " (" + skipped + " skipped)" : "") + "."); diff --git a/src/main/java/net/tfminecraft/magic/manager/ResonanceGuiManager.java b/src/main/java/net/tfminecraft/magic/manager/ResonanceGuiManager.java index 604eb38..cece7e9 100644 --- a/src/main/java/net/tfminecraft/magic/manager/ResonanceGuiManager.java +++ b/src/main/java/net/tfminecraft/magic/manager/ResonanceGuiManager.java @@ -1,6 +1,10 @@ package net.tfminecraft.magic.manager; +import java.util.Locale; + import org.bukkit.Bukkit; +import org.bukkit.Sound; +import org.bukkit.SoundCategory; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -85,10 +89,36 @@ public void onInventoryClick(InventoryClickEvent event) { } if (!previousMode.equals(session.getCastModeId())) { + notifyCastModeChanged(player, session.getCastModeId()); refreshGui(player, holder, session); } } + private static void notifyCastModeChanged(Player player, String modeId) { + player.sendMessage(Messages.get("cast_mode.switched", "mode", castModeChatLabel(modeId))); + boolean flow = GuiCache.castModeRight.getId().equalsIgnoreCase(modeId); + player.playSound( + player.getLocation(), + Sound.BLOCK_AMETHYST_BLOCK_CHIME, + SoundCategory.PLAYERS, + 0.85f, + flow ? 1.5f : 0.75f); + } + + private static String castModeChatLabel(String modeId) { + if (modeId != null && modeId.equalsIgnoreCase(GuiCache.castModeRight.getId())) { + return "Flow"; + } + if (modeId != null && modeId.equalsIgnoreCase(GuiCache.castModeLeft.getId())) { + return "Surge"; + } + if (modeId == null || modeId.isBlank()) { + return "Surge"; + } + return modeId.substring(0, 1).toUpperCase(Locale.ROOT) + + modeId.substring(1).toLowerCase(Locale.ROOT); + } + @EventHandler public void onInventoryDrag(InventoryDragEvent event) { if (!(event.getView().getTopInventory().getHolder() instanceof ResonanceGuiHolder)) { diff --git a/src/main/java/net/tfminecraft/magic/model/ElementDef.java b/src/main/java/net/tfminecraft/magic/model/ElementDef.java index 6183a0a..94cae54 100644 --- a/src/main/java/net/tfminecraft/magic/model/ElementDef.java +++ b/src/main/java/net/tfminecraft/magic/model/ElementDef.java @@ -7,6 +7,7 @@ import java.util.regex.Pattern; import org.bukkit.configuration.ConfigurationSection; +import org.bukkit.entity.Player; import net.tfminecraft.magic.Cache; import net.tfminecraft.magic.modifier.KeyframeCurve; @@ -23,6 +24,7 @@ public final class ElementDef { private final double maxResonance; private final Double decayPerHourOverride; private final double auraDecayPerHour; + private final String permission; private final KeyframeCurve resonance; public ElementDef(String id, ConfigurationSection config) { @@ -31,6 +33,8 @@ public ElementDef(String id, ConfigurationSection config) { this.icon = config.getString("icon", "v.BARRIER"); this.colors = readColors(config); this.slot = config.getInt("slot", -1); + String perm = config.getString("permission", ""); + this.permission = perm == null || perm.isBlank() ? null : perm.trim(); this.maxResonance = Math.max(1.0, config.getDouble("max_resonance", 100.0)); this.decayPerHourOverride = config.contains("decay_per_hour") ? config.getDouble("decay_per_hour") @@ -81,6 +85,18 @@ public int getSlot() { return slot; } + public String getPermission() { + return permission; + } + + /** Elements with a permission stay hidden and do not grow until the player holds it. */ + public boolean isUnlocked(Player player) { + if (permission == null) { + return true; + } + return player != null && player.hasPermission(permission); + } + public KeyframeCurve getResonanceCurve() { return resonance; } diff --git a/src/main/java/net/tfminecraft/magic/registry/SkillElementRegistry.java b/src/main/java/net/tfminecraft/magic/registry/SkillElementRegistry.java index 1a705da..eb877cc 100644 --- a/src/main/java/net/tfminecraft/magic/registry/SkillElementRegistry.java +++ b/src/main/java/net/tfminecraft/magic/registry/SkillElementRegistry.java @@ -7,34 +7,68 @@ public final class SkillElementRegistry { - private static final Map skillToElement = new LinkedHashMap<>(); + public static final int DEFAULT_TIER = 1; + public static final int MAX_TIER = 4; + + public record Binding(String elementId, int tier) {} + + private static final Map skills = new LinkedHashMap<>(); private SkillElementRegistry() {} public static void clear() { - skillToElement.clear(); + skills.clear(); } public static void register(String skillId, String elementId) { + register(skillId, elementId, DEFAULT_TIER); + } + + public static void register(String skillId, String elementId, int tier) { if (skillId == null || skillId.isBlank() || elementId == null || elementId.isBlank()) { return; } - skillToElement.put( - skillId.trim().toLowerCase(Locale.ROOT), elementId.trim().toLowerCase(Locale.ROOT)); + skills.put( + skillId.trim().toLowerCase(Locale.ROOT), + new Binding(elementId.trim().toLowerCase(Locale.ROOT), clampTier(tier))); } public static String elementOf(String skillId) { - if (skillId == null || skillId.isBlank()) { - return null; - } - return skillToElement.get(skillId.trim().toLowerCase(Locale.ROOT)); + Binding binding = bindingOf(skillId); + return binding == null ? null : binding.elementId(); + } + + public static int tierOf(String skillId) { + Binding binding = bindingOf(skillId); + return binding == null ? DEFAULT_TIER : binding.tier(); } public static Map bindings() { - return Collections.unmodifiableMap(skillToElement); + Map copy = new LinkedHashMap<>(); + for (Map.Entry entry : skills.entrySet()) { + copy.put(entry.getKey(), entry.getValue().elementId()); + } + return Collections.unmodifiableMap(copy); } public static int size() { - return skillToElement.size(); + return skills.size(); + } + + public static int clampTier(int tier) { + if (tier < DEFAULT_TIER) { + return DEFAULT_TIER; + } + if (tier > MAX_TIER) { + return MAX_TIER; + } + return tier; + } + + private static Binding bindingOf(String skillId) { + if (skillId == null || skillId.isBlank()) { + return null; + } + return skills.get(skillId.trim().toLowerCase(Locale.ROOT)); } } diff --git a/src/main/java/net/tfminecraft/magic/service/ResonanceService.java b/src/main/java/net/tfminecraft/magic/service/ResonanceService.java index 4a470ba..2c7c789 100644 --- a/src/main/java/net/tfminecraft/magic/service/ResonanceService.java +++ b/src/main/java/net/tfminecraft/magic/service/ResonanceService.java @@ -35,6 +35,9 @@ private static void tickSession(Player player, ResonanceSession session) { if (ratePerHour < 0.0 && current <= EPSILON) { continue; } + if (ratePerHour > 0.0 && !element.isUnlocked(player)) { + continue; + } session.addResonance(element.getId(), ratePerHour * dtHours); dirty = true; if (Cache.debug) { diff --git a/src/main/java/net/tfminecraft/magic/util/MagicText.java b/src/main/java/net/tfminecraft/magic/util/MagicText.java index d2a6e03..abc715c 100644 --- a/src/main/java/net/tfminecraft/magic/util/MagicText.java +++ b/src/main/java/net/tfminecraft/magic/util/MagicText.java @@ -41,12 +41,20 @@ public static String elementName(ElementDef element) { if (element == null) { return ""; } - String plain = visibleName(element); + return elementText(element, visibleName(element)); + } + + /** Any text in the element's colour, solid or gradient like {@link #elementName}. */ + public static String elementText(ElementDef element, String plain) { + String text = plain != null ? plain : ""; + if (element == null) { + return format(FALLBACK_COLOR + text); + } List colors = element.getColors(); if (colors.size() > 1) { - return StringFormatter.applyColourGradient(plain, colors); + return StringFormatter.applyColourGradient(text, colors); } - return format(element.getColor() + plain); + return format(element.getColor() + text); } private static String visibleName(ElementDef element) { diff --git a/src/main/resources/artifacts/sacrifice.yml b/src/main/resources/artifacts/sacrifice.yml index ce9c4bb..af84302 100644 --- a/src/main/resources/artifacts/sacrifice.yml +++ b/src/main/resources/artifacts/sacrifice.yml @@ -54,7 +54,7 @@ tiers: elements: bloodmagic: words: - - "by blood i claim this" + - "Lorin Vekar Drakun Talis" lore: pain: "Filled with the pain of {character}" screams: "Filled with the screams of {character}" @@ -62,7 +62,7 @@ elements: necromancy: enabled: false words: - - "by bone i claim this" + - "Vorthas Luneth... Taro Vekran" lore: pain: "Filled with the pain of {character}" screams: "Filled with the screams of {character}" @@ -70,7 +70,7 @@ elements: shadowmancy: enabled: false words: - - "by shadow i claim this" + - "Need to do" lore: pain: "Filled with the pain of {character}" screams: "Filled with the screams of {character}" diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 8c58c83..aa8ada1 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -25,10 +25,12 @@ equilibrium: 0: { mana: 0, damage: 0, cooldown: 0 } 100: { mana: -0.20, damage: 0.25, cooldown: -0.05 } -# MythicLib triggers the cast listener treats as a spell, besides CAST and API. -# These match the keybinds a rune can be set to. A weapon still has to be attuned -# to the spell's element. +# Runes: MMOItems type ids (item-types.yml) that /magic rune keybind can rebind. +# keybinds: triggers a rune can be set to. The cast listener also treats them as a +# spell, besides CAST and API. A weapon still has to be attuned to the spell's element. runes: + types: + - GEM_STONE keybinds: - RIGHT_CLICK - LEFT_CLICK @@ -95,7 +97,7 @@ attunement: off_per_hour: 0.04166666667 gear: - station: iaf(tfmc:weapon_station) + station: iaf(tfmc:magic_crafting_station) output-slot: 16 confirm_seconds: 5 diff --git a/src/main/resources/elements/elements.yml b/src/main/resources/elements/elements.yml index 2ab77f4..2fef260 100644 --- a/src/main/resources/elements/elements.yml +++ b/src/main/resources/elements/elements.yml @@ -5,102 +5,108 @@ # Row 4 (second from bottom): Shadowmancy, Necromancy, Bloodmagic cerrith: - name: "#55ff55Cerrith" + name: Cerrith icon: m.cerrith_runes.rune_of_cerrith_glyph - color: "#55ff55" + color: "#466629" slot: 28 - aura_decay_per_hour: -0.05 + aura_decay_per_hour: 0 resonance: 0: { mana: 0.20, damage: -0.20, cooldown: 0.20 } 100: { mana: -0.20, damage: 0.20, cooldown: -0.20 } seithr: - name: "#ffffffSeithr" + name: Seithr icon: m.seithr_runes.rune_of_cold_embrace - color: "#ffffff" + color: "#15E5FF" slot: 30 - aura_decay_per_hour: -0.05 + aura_decay_per_hour: 0 resonance: 0: { mana: 0.20, damage: -0.20, cooldown: 0.20 } 100: { mana: -0.20, damage: 0.20, cooldown: -0.20 } oseni: - name: "#ffaa00Oseni" + name: Oseni icon: m.oseni_runes.rune_of_oseni_glyph - color: "#ffaa00" + color: "#e29a00" slot: 32 - aura_decay_per_hour: -0.05 + aura_decay_per_hour: 0 resonance: 0: { mana: 0.20, damage: -0.20, cooldown: 0.20 } 100: { mana: -0.20, damage: 0.20, cooldown: -0.20 } mitlan: - name: "#5555ffMitlan" + name: Mitlan icon: v.LAPIS_LAZULI color: "#5555ff" slot: 34 - aura_decay_per_hour: -0.05 + aura_decay_per_hour: 0 resonance: 0: { mana: 0.20, damage: -0.20, cooldown: 0.20 } 100: { mana: -0.20, damage: 0.20, cooldown: -0.20 } shadowmancy: - name: "#555555Shadowmancy" + name: Shadowmancy icon: v.INK_SAC color: "#555555" slot: 40 - aura_decay_per_hour: -0.05 + permission: magic.shadowmancy + aura_decay_per_hour: 0 resonance: 0: { mana: 0.20, damage: -0.20, cooldown: 0.20 } 100: { mana: -0.20, damage: 0.20, cooldown: -0.20 } necromancy: - name: "#00aaaaNecromancy" + name: Necromancy icon: v.WITHER_SKELETON_SKULL color: "#00aaaa" slot: 38 - aura_decay_per_hour: -0.05 + permission: magic.necromancy + aura_decay_per_hour: 0 resonance: 0: { mana: 0.20, damage: -0.20, cooldown: 0.20 } 100: { mana: -0.20, damage: 0.20, cooldown: -0.20 } arcanum: - name: "#aa00aaArcanum" + name: Arcanum icon: v.ENCHANTED_BOOK color: "#aa00aa" slot: 22 + permission: magic.arcanum decay_per_hour: -0.035 - aura_decay_per_hour: -0.05 + aura_decay_per_hour: 0 resonance: 0: { mana: 0.20, damage: -0.20, cooldown: 0.20 } 100: { mana: -0.20, damage: 0.20, cooldown: -0.20 } spirit: - name: "#e8d5a3Spirit" + name: Spirit icon: v.FEATHER color: "#e8d5a3" slot: 20 - aura_decay_per_hour: -0.05 + permission: magic.spirit + aura_decay_per_hour: 0 resonance: 0: { mana: 0.20, damage: -0.20, cooldown: 0.20 } 100: { mana: -0.20, damage: 0.20, cooldown: -0.20 } illusion: - name: "#e070b0Illusion" + name: Illusion icon: v.ENDER_PEARL color: "#e070b0" slot: 24 - aura_decay_per_hour: -0.05 + permission: magic.illusion + aura_decay_per_hour: 0 resonance: 0: { mana: 0.20, damage: -0.20, cooldown: 0.20 } 100: { mana: -0.20, damage: 0.20, cooldown: -0.20 } bloodmagic: - name: "#aa0000Bloodmagic" + name: Bloodmagic icon: v.REDSTONE color: "#aa0000" slot: 42 - aura_decay_per_hour: -0.05 + permission: magic.bloodmagic + aura_decay_per_hour: 0 resonance: 0: { mana: 0.20, damage: -0.20, cooldown: 0.20 } 100: { mana: -0.20, damage: 0.20, cooldown: -0.20 } diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index d27d7ac..27a9334 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -4,6 +4,10 @@ reload: failed: "#ff5555[Magic] Reload failed - check console." +cast_mode: + + switched: "#55ff55Casting mode set to {mode}." + open: players_only: "#ff5555Only players can open the Resonance GUI." @@ -16,7 +20,25 @@ admin: no_permission: "#ff5555You do not have permission to use this command." - usage: "#aaaaaaUsage: /magic reload #555555| #aaaaaa/magic open #555555| #aaaaaa/magic resonance #555555| #aaaaaa/magic artifact roll #555555| #aaaaaa/magic artifact give #555555| #aaaaaa/magic artifact create #555555| #aaaaaa/magic artifact setfill #555555| #aaaaaa/magic fillchest #555555| #aaaaaa/magic shrine fill" + usage: "#aaaaaaUsage: /magic rune keybind #555555| #aaaaaa/magic reload #555555| #aaaaaa/magic open #555555| #aaaaaa/magic resonance #555555| #aaaaaa/magic artifact roll #555555| #aaaaaa/magic artifact give #555555| #aaaaaa/magic artifact create #555555| #aaaaaa/magic artifact setfill #555555| #aaaaaa/magic fillchest #555555| #aaaaaa/magic shrine fill" + +rune: + + usage: "#aaaaaaUsage: /magic rune keybind " + + players_only: "#ff5555Only players can rebind a held rune." + + no_permission: "#ff5555You do not have permission to rebind runes." + + not_a_rune: "#ff5555Hold a rune to rebind its abilities." + + no_abilities: "#ff5555That rune has no abilities to rebind." + + unknown_trigger: "#ff5555Unknown trigger." + + failed: "#ff5555Could not rebind that rune." + + success: "#55ff55Rebound all abilities on this rune to {trigger}." fillchest: @@ -38,17 +60,25 @@ cast: whiff: "§7*Whiff*" whiff_sub: "#aaaaaa§oRift {rift}%" - + whiff_sub_staffs: "#aaaaaa§oToo many staffs" # Refusal: the weapon never started. Nothing is spent, and it is never a whiff. refuse_title: "§7*Refused*" - refuse_sub: "#aaaaaa§oThis weapon is beyond you" + refuse_sub: "#aaaaaa§oThis spell is beyond you" + + refuse_sub_spell: "#aaaaaa§oThis spell is beyond you" + + refuse_sub_weapon: "#aaaaaa§oThis weapon cannot carry that spell" refuse_sub_foreign: "#aaaaaa§oThis weapon holds no {element}" - refuse_chat: "#aaaaaaThe weapon asks for {element} {need}. You carry {have}." + refuse_chat: "#aaaaaaThis spell needs {element} {need}. You carry {have}." + + refuse_chat_spell: "#aaaaaaThis spell needs {element} {need}. You carry {have}." + + refuse_chat_weapon: "#aaaaaaThe weapon holds {element} {have}. This spell needs {need}." refuse_chat_foreign: "#aaaaaaThis weapon was never attuned to {element}. Apply a {element} charge at a station." @@ -81,6 +111,10 @@ resonance: reset_ok: "#55ff55Reset {player} {element} resonance." + locked: "#ff5555{player} lacks {permission} ({element})." + + locked_skipped: "#ff5555Skipped locked elements for {player}: {elements}" + artifact: usage: "#aaaaaaUsage: /magic artifact roll #555555| #aaaaaa/magic artifact give #555555| #aaaaaa/magic artifact create #555555| #aaaaaa/magic artifact path #555555| #aaaaaa/magic artifact setfill" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 819825c..7db7c21 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -13,14 +13,16 @@ commands: aliases: [res] permission: magic.use magic: - description: Magic plugin admin commands - usage: /magic - permission: magic.admin + description: Magic plugin commands + usage: /magic permissions: magic.use: description: Allows using /resonance default: true + magic.rune.keybind: + description: Allows /magic rune keybind on a held rune + default: true magic.admin: description: Allows /magic admin commands (reload, open) default: op @@ -29,3 +31,21 @@ permissions: magic.admin.reload: description: Legacy permission alias for magic.admin default: op + magic.spirit: + description: Allows Spirit resonance + default: false + magic.arcanum: + description: Allows Arcanum resonance + default: false + magic.illusion: + description: Allows Illusion resonance + default: false + magic.shadowmancy: + description: Allows Shadowmancy resonance + default: false + magic.necromancy: + description: Allows Necromancy resonance + default: false + magic.bloodmagic: + description: Allows Bloodmagic resonance + default: false diff --git a/src/main/resources/skills.yml b/src/main/resources/skills.yml index 3bc04b7..c743332 100644 --- a/src/main/resources/skills.yml +++ b/src/main/resources/skills.yml @@ -1,4 +1,4 @@ -# skill id -> element id (MythicLib / MMOItems). Jar does not overwrite a live copy. +# skill id -> element + tier (MythicLib / MMOItems). Jar does not overwrite a live copy. # # The id is the MythicLib skill handler id, lowercase, which is what an MMOItems # ability reports. Flat or nested both work: @@ -7,6 +7,93 @@ # # arcanum_bolt: # element: arcanum +# tier: 1 # -# The element decides two things: which weapon requirement gates the cast, and which -# alignment band the weapon contributes. A skill with no binding is never gated. +# element: which weapon imbue and player resonance gate the cast, and which alignment +# band the weapon contributes. A skill with no binding is never gated. +# +# tier: 1-4 (Minor / Lesser / Greater / Ascendant). The weapon must hold that element +# at this band, and the caster's resonance must meet the same band. Omitted tier is 1. + +#Cerrith +Healing_Orb: + element: Cerrith + tier: 1 + +Shielding_Orb: + element: Cerrith + tier: 1 + +Hand_Cure: + element: Cerrith + tier: 2 + +Blessing_Of_Swiftness: + element: Cerrith + tier: 2 + +Cerrith_Glyph: + element: Cerrith + tier: 3 + +Restoration: + element: Cerrith + tier: 3 + +Blessing_Of_Healing: + element: Cerrith + tier: 4 + +Mana_Transfer: + element: Cerrith + tier: 4 + +#Oseni +Fire_Shard: + element: Oseni + tier: 1 + +Fire_Breath: + element: Oseni + tier: 2 + +Oseni_Glyph: + element: Oseni + tier: 3 + +Fire_Rain: + element: Oseni + tier: 4 + +#Seithr +Silencing_Shard: + element: Seithr + tier: 1 + +Ice_Shard: + element: Seithr + tier: 1 + +Ice_Shield: + element: Seithr + tier: 2 + +Ice_Wave: + element: Seithr + tier: 2 + +Seithr_Glyph: + element: Seithr + tier: 3 + +Cold_Embrace: + element: Seithr + tier: 3 + +Frostveil: + element: Seithr + tier: 4 + +Frozen_Tomb: + element: Seithr + tier: 4 From 025a756f51927a0e224f6a0e8a9a76cbc1e45c1d Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Thu, 24 Sep 2026 20:45:09 +0000 Subject: [PATCH 4/4] fix: tie gear aborts to the preparer and what they paid Anyone could abort another player's prepared craft and take its material refund, and cost-bypass players got refunds for materials never taken. Stations now record who prepared the craft and the exact materials charged. Only that player (or an admin) can abort it, and the refund is what was paid. Stations saved before this keep the old behaviour. Also break model-scheme ties between the tied leaders only, and fix the "Ascendant Rune" suffix, the petty_tome3 id and two lore typos in the bundled gear defaults. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../net/tfminecraft/magic/gear/GearCosts.java | 14 +++-- .../magic/gear/GearModelResolver.java | 13 +--- .../magic/gear/GearStationListener.java | 9 ++- .../magic/gear/GearStationStore.java | 59 +++++++++++++++++-- .../magic/gear/gui/GearInventoryManager.java | 4 +- src/main/resources/gear/archetypes.yml | 6 +- src/main/resources/gear/parts.yml | 6 +- src/main/resources/messages.yml | 2 +- 8 files changed, 83 insertions(+), 30 deletions(-) diff --git a/src/main/java/net/tfminecraft/magic/gear/GearCosts.java b/src/main/java/net/tfminecraft/magic/gear/GearCosts.java index 8be4745..c6b85fb 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearCosts.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearCosts.java @@ -31,8 +31,12 @@ public static Map total(Collection parts) { return costs; } + public static boolean bypasses(Player player) { + return player != null && player.hasPermission("magic.bypass_crafting_cost"); + } + public static boolean has(Player player, Collection parts) { - if (player != null && player.hasPermission("magic.bypass_crafting_cost")) { + if (bypasses(player)) { return true; } Map costs = total(parts); @@ -61,7 +65,7 @@ public static boolean has(Player player, Collection parts) { } public static void take(Player player, Collection parts) { - if (player == null || player.hasPermission("magic.bypass_crafting_cost")) { + if (player == null || bypasses(player)) { return; } for (Map.Entry entry : total(parts).entrySet()) { @@ -85,12 +89,12 @@ public static void take(Player player, Collection parts) { player.updateInventory(); } - public static void refund(Player player, Collection parts, Location drop) { - if (player == null) { + public static void refund(Player player, Map costs, Location drop) { + if (player == null || costs == null || costs.isEmpty()) { return; } Location at = drop == null ? player.getLocation() : drop.clone().add(0.5, 1.0, 0.5); - for (Map.Entry entry : total(parts).entrySet()) { + for (Map.Entry entry : costs.entrySet()) { int remaining = entry.getValue(); while (remaining > 0) { ItemStack stack = ItemRef.build(entry.getKey()); diff --git a/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java b/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java index b136f80..93873f0 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearModelResolver.java @@ -32,7 +32,6 @@ public static GearModelScheme winner(Collection parts) { } Map votes = new LinkedHashMap<>(); String coreScheme = ""; - String firstScheme = ""; for (PartDef part : parts) { if (part == null || !part.hasModelScheme()) { continue; @@ -42,9 +41,6 @@ public static GearModelScheme winner(Collection parts) { continue; } votes.merge(id, part.getSchemeWeight(), Integer::sum); - if (firstScheme.isEmpty()) { - firstScheme = id; - } if (PartSlots.CORE.equalsIgnoreCase(part.getPartType())) { coreScheme = id; } @@ -65,12 +61,9 @@ public static GearModelScheme winner(Collection parts) { tie = true; } } - if (tie) { - if (!coreScheme.isEmpty() && votes.getOrDefault(coreScheme, 0) == best) { - bestId = coreScheme; - } else if (!firstScheme.isEmpty()) { - bestId = firstScheme; - } + // bestId is already the first scheme seen among the tied leaders; the core wins a tie it is in. + if (tie && !coreScheme.isEmpty() && votes.getOrDefault(coreScheme, 0) == best) { + bestId = coreScheme; } return GearModelSchemeRegistry.get(bestId); } diff --git a/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java b/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java index 1074653..350f736 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearStationListener.java @@ -224,8 +224,8 @@ private void tryAbort(PlayerInteractEvent event) { if (GearStationStore.isAttuned(occupancy.getItem())) { return; } - UUID owner = GearOrbService.sessionOwner(location); - if (owner != null && !owner.equals(player.getUniqueId())) { + UUID owner = occupancy.getOwner() != null ? occupancy.getOwner() : GearOrbService.sessionOwner(location); + if (owner != null && !owner.equals(player.getUniqueId()) && !player.hasPermission("magic.admin")) { player.sendMessage(Messages.get("gear.abort.not_yours")); player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f); return; @@ -237,7 +237,10 @@ private void tryAbort(PlayerInteractEvent event) { } recentAborts.values().removeIf(at -> System.currentTimeMillis() - at >= ABORT_BREAK_GUARD_MILLIS); recentAborts.put(GearStationStore.key(location), System.currentTimeMillis()); - GearCosts.refund(player, GearProvenance.resolveParts(weapon), location); + Map charged = occupancy.getCharged() != null + ? occupancy.getCharged() + : GearCosts.total(GearProvenance.resolveParts(weapon)); + GearCosts.refund(player, charged, location); player.sendMessage(Messages.get("gear.abort.done")); player.playSound(location, Sound.BLOCK_ANVIL_LAND, 0.6f, 1.4f); } diff --git a/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java b/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java index 478ff6c..79167ee 100644 --- a/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java +++ b/src/main/java/net/tfminecraft/magic/gear/GearStationStore.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.IOException; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; @@ -31,10 +32,27 @@ public static final class Occupancy { private ItemStack item; private UUID displayId; private boolean orbSessionActive; + private final UUID owner; + private final Map charged; - Occupancy(ItemStack item, UUID displayId) { + Occupancy(ItemStack item, UUID displayId, UUID owner, Map charged) { this.item = item; this.displayId = displayId; + this.owner = owner; + this.charged = charged == null ? null : Map.copyOf(charged); + } + + /** Player who prepared the craft. Null for stations saved before owners were recorded. */ + public UUID getOwner() { + return owner; + } + + /** + * Materials taken when the craft was prepared, empty when costs were bypassed. + * Null for stations saved before charges were recorded. + */ + public Map getCharged() { + return charged; } public ItemStack getItem() { @@ -73,13 +91,13 @@ public static Occupancy get(Location location) { return location == null ? null : OCCUPIED.get(key(location)); } - public static Occupancy occupy(Location location, ItemStack item) { + public static Occupancy occupy(Location location, ItemStack item, UUID owner, Map charged) { if (location == null || item == null) { return null; } clear(location, false); UUID displayId = spawnDisplay(location, item); - Occupancy occupancy = new Occupancy(item, displayId); + Occupancy occupancy = new Occupancy(item, displayId, owner, charged); OCCUPIED.put(key(location), occupancy); save(); return occupancy; @@ -163,7 +181,28 @@ public static void load() { continue; } UUID displayId = spawnDisplay(location, item); - OCCUPIED.put(key(location), new Occupancy(item, displayId)); + UUID owner = null; + String rawOwner = section.getString("owner"); + if (rawOwner != null && !rawOwner.isBlank()) { + try { + owner = UUID.fromString(rawOwner); + } catch (IllegalArgumentException ignored) { + owner = null; + } + } + Map charged = null; + ConfigurationSection chargedSection = section.getConfigurationSection("charged"); + if (chargedSection != null) { + charged = new LinkedHashMap<>(); + for (String index : chargedSection.getKeys(false)) { + String path = chargedSection.getString(index + ".path"); + int amount = chargedSection.getInt(index + ".amount"); + if (path != null && !path.isBlank() && amount > 0) { + charged.merge(path, amount, Integer::sum); + } + } + } + OCCUPIED.put(key(location), new Occupancy(item, displayId, owner, charged)); } } @@ -182,6 +221,18 @@ public static void save() { config.set(path + ".y", location.getBlockY()); config.set(path + ".z", location.getBlockZ()); config.set(path + ".item", occupancy.getItem()); + if (occupancy.getOwner() != null) { + config.set(path + ".owner", occupancy.getOwner().toString()); + } + if (occupancy.getCharged() != null) { + config.createSection(path + ".charged"); + int chargedIndex = 0; + for (Map.Entry cost : occupancy.getCharged().entrySet()) { + String costPath = path + ".charged." + chargedIndex++; + config.set(costPath + ".path", cost.getKey()); + config.set(costPath + ".amount", cost.getValue()); + } + } } try { File file = file(); diff --git a/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java b/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java index aea4e6b..d3d6483 100644 --- a/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java +++ b/src/main/java/net/tfminecraft/magic/gear/gui/GearInventoryManager.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import org.bukkit.Bukkit; import org.bukkit.Material; @@ -287,8 +288,9 @@ private void tryPrepare(Player player) { player.playSound(player.getLocation(), Sound.ENTITY_VILLAGER_NO, 1f, 1f); return; } + Map charged = GearCosts.bypasses(player) ? Map.of() : GearCosts.total(parts); GearCosts.take(player, parts); - GearStationStore.occupy(station, prepared); + GearStationStore.occupy(station, prepared, player.getUniqueId(), charged); OpenStationManager.clear(player); player.closeInventory(); player.sendMessage(Messages.get("gear.craft.prepared")); diff --git a/src/main/resources/gear/archetypes.yml b/src/main/resources/gear/archetypes.yml index a994e13..d949c13 100644 --- a/src/main/resources/gear/archetypes.yml +++ b/src/main/resources/gear/archetypes.yml @@ -18,7 +18,7 @@ staff: minor_rune: "Minor Rune" lesser_rune: "Lesser Rune" greater_rune: "Greater Rune" - ascendant_rune: "Ascedant Rune" + ascendant_rune: "Ascendant Rune" wand: name: Wand @@ -33,7 +33,7 @@ wand: minor_rune: "Minor Rune" lesser_rune: "Lesser Rune" greater_rune: "Greater Rune" - ascendant_rune: "Ascedant Rune" + ascendant_rune: "Ascendant Rune" sword: name: Sword @@ -48,4 +48,4 @@ sword: minor_rune: "Minor Rune" lesser_rune: "Lesser Rune" greater_rune: "Greater Rune" - ascendant_rune: "Ascedant Rune" + ascendant_rune: "Ascendant Rune" diff --git a/src/main/resources/gear/parts.yml b/src/main/resources/gear/parts.yml index 8dd09fb..8b0023e 100644 --- a/src/main/resources/gear/parts.yml +++ b/src/main/resources/gear/parts.yml @@ -171,7 +171,7 @@ iron_sword_core: - handle sockets: {} lore: - - "§7A basic magical core with one rune slot and a simple balde edge" + - "§7A basic magical core with one rune slot and a simple blade edge" steel_sword_core: name: "#7f7d80Steel Magical Core" @@ -188,7 +188,7 @@ steel_sword_core: - handle sockets: {} lore: - - "§7An improved magical core with one rune slot and a better blade egde" + - "§7An improved magical core with one rune slot and a better blade edge" abyssalite_sword_core: name: "#3b4e60Abyssalite Magical Core" @@ -494,7 +494,7 @@ basic_tome3: lore: - "§7A better tome capable of channeling lesser runes" -petty_tom32: +petty_tome3: name: "§dPetty Magical Tome" part-type: tome3 tier: 3 diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml index 27a9334..ba8a21b 100644 --- a/src/main/resources/messages.yml +++ b/src/main/resources/messages.yml @@ -317,7 +317,7 @@ gear: done: "#aaaaaa§oThe craft comes apart. The charge, if any, is spent" - not_yours: "#ff5555Someone else is attuning this weapon." + not_yours: "#ff5555Only the player who prepared this craft can take it apart." broken: