diff --git a/src/main/java/net/tfminecraft/magic/artifact/ArtifactLore.java b/src/main/java/net/tfminecraft/magic/artifact/ArtifactLore.java index b30257b..5a115e1 100644 --- a/src/main/java/net/tfminecraft/magic/artifact/ArtifactLore.java +++ b/src/main/java/net/tfminecraft/magic/artifact/ArtifactLore.java @@ -17,6 +17,7 @@ import net.tfminecraft.magic.artifact.sacrifice.SacrificeImprintStore; import net.tfminecraft.magic.artifact.sacrifice.SacrificeRegistry; import net.tfminecraft.magic.model.ElementDef; +import net.tfminecraft.magic.model.ElementVisibility; import net.tfminecraft.magic.registry.ElementRegistry; import net.tfminecraft.magic.util.MagicNumbers; import net.tfminecraft.magic.util.MagicText; @@ -54,13 +55,12 @@ public static void apply(ItemStack stack) { double cap = artifact.getCap(element.getId()); double fill = artifact.getFill(element.getId()); double shown = ArtifactCareStore.usableFill(fill, cap, muffle); - block.add(MagicText.format( - element.getColor() - + element.getName() - + " {color:label_muted}" - + formatAmount(shown) - + " / " - + formatAmount(cap))); + block.add(MagicText.elementName(element) + + MagicText.format( + " {color:label_muted}" + + formatAmount(shown) + + " / " + + formatAmount(cap))); } int attuneOffset = block.size(); List attuneLines = buildAttuneLines(stack, artifact); @@ -270,8 +270,8 @@ private static ElementDef matchElement(String before) { ElementDef best = null; int bestLen = 0; for (ElementDef element : ElementRegistry.getAll()) { - String name = plain(element.getName()).toLowerCase(Locale.ROOT); - if (name.isEmpty() || !lower.startsWith(name)) { + String name = loreName(element); + if (!auraLabel(lower, name)) { continue; } if (name.length() > bestLen) { @@ -424,14 +424,50 @@ private static boolean isFillLine(String line) { } String before = p.substring(0, slash).trim().toLowerCase(Locale.ROOT); for (ElementDef element : ElementRegistry.getAll()) { - String name = plain(element.getName()).toLowerCase(Locale.ROOT); - if (!name.isEmpty() && before.startsWith(name)) { + String name = loreName(element); + if (auraLabel(before, name)) { return true; } } return false; } + /** + * True when {@code before} is the element label followed only by its amount. + * A short id such as {@code fire} must not match {@code fire resistance 5}. + */ + private static boolean auraLabel(String before, String name) { + if (before == null || name == null || name.isEmpty() || !before.startsWith(name)) { + return false; + } + String rest = before.substring(name.length()).trim(); + if (rest.isEmpty()) { + return false; + } + boolean digit = false; + for (int i = 0; i < rest.length(); i++) { + char c = rest.charAt(i); + if (Character.isDigit(c)) { + digit = true; + continue; + } + if (c == '.' && digit) { + continue; + } + return false; + } + return digit; + } + + /** Same label {@link MagicText#elementName} writes, so a blank name still matches its id. */ + private static String loreName(ElementDef element) { + String name = plain(element.getName()).toLowerCase(Locale.ROOT); + if (!name.isEmpty()) { + return name; + } + return element.getId() == null ? "" : element.getId().toLowerCase(Locale.ROOT); + } + // Keep the existing legacy text representation, formatting, and exact-string comparisons. @SuppressWarnings("deprecation") private static String plain(String line) { @@ -455,7 +491,8 @@ private static List orderedAuraElements(ItemStack stack, Artifact ar String primary = primaryId(stack, artifact); List listed = new ArrayList<>(); for (ElementDef element : ElementRegistry.getAll()) { - if (artifact.getCap(element.getId()) > 0) { + if (artifact.getCap(element.getId()) > 0 + && ElementVisibility.shownOnArtifact(element.getId(), primary)) { listed.add(element); } } diff --git a/src/main/java/net/tfminecraft/magic/artifact/create/ArtifactCreateSession.java b/src/main/java/net/tfminecraft/magic/artifact/create/ArtifactCreateSession.java index 4f2bbea..673449d 100644 --- a/src/main/java/net/tfminecraft/magic/artifact/create/ArtifactCreateSession.java +++ b/src/main/java/net/tfminecraft/magic/artifact/create/ArtifactCreateSession.java @@ -18,6 +18,7 @@ import net.tfminecraft.magic.artifact.generate.ArtifactAuraSlot; import net.tfminecraft.magic.artifact.generate.ArtifactItemBuilder; import net.tfminecraft.magic.artifact.generate.ArtifactRoll; +import net.tfminecraft.magic.model.ElementVisibility; import net.tfminecraft.magic.util.MagicNumbers; public final class ArtifactCreateSession { @@ -121,6 +122,10 @@ public boolean cycleElement(String elementId) { } ElementRole role = roleOf(elementId); if (role == ElementRole.OFF) { + if (primaryId != null && !primaryId.isBlank() + && !ElementVisibility.shownOnArtifact(elementId, primaryId)) { + return false; + } if (!ArtifactAffinityRegistry.compatibleWith(caps.keySet(), elementId)) { return false; } @@ -181,6 +186,9 @@ public ArtifactRoll toRoll() { if (primaryId.equals(entry.getKey()) || entry.getValue() == null || entry.getValue() <= 0) { continue; } + if (!ElementVisibility.shownOnArtifact(entry.getKey(), primaryId)) { + continue; + } slots.add(new ArtifactAuraSlot(entry.getKey(), entry.getValue())); } return ArtifactRoll.ok(primaryId, rarityId, slots); @@ -240,6 +248,14 @@ private void assignPrimary(String next) { primaryId = next; if (next != null) { clampCapsToRarity(); + if (next.equals(primaryId)) { + for (String elementId : new ArrayList<>(caps.keySet())) { + if (!elementId.equals(next) + && !ElementVisibility.shownOnArtifact(elementId, next)) { + setCap(elementId, 0); + } + } + } } clearPreviewLock(); } diff --git a/src/main/java/net/tfminecraft/magic/artifact/generate/ArtifactRoller.java b/src/main/java/net/tfminecraft/magic/artifact/generate/ArtifactRoller.java index cea2f50..d1ae531 100644 --- a/src/main/java/net/tfminecraft/magic/artifact/generate/ArtifactRoller.java +++ b/src/main/java/net/tfminecraft/magic/artifact/generate/ArtifactRoller.java @@ -15,6 +15,7 @@ import net.tfminecraft.magic.artifact.config.ArtifactTypeRegistry; import net.tfminecraft.magic.artifact.config.CapRange; import net.tfminecraft.magic.artifact.path.ArtifactPathSpec; +import net.tfminecraft.magic.model.ElementVisibility; import net.tfminecraft.magic.util.MagicNumbers; public final class ArtifactRoller { @@ -68,6 +69,9 @@ public ArtifactRoll roll(String primaryId, String rarityId) { if (companion == null || !companion.isEnabled() || !companion.hasRarity(rarity.getId())) { continue; } + if (!ElementVisibility.shownOnArtifact(companion.getElementId(), type.getElementId())) { + continue; + } pool.add(companion); } @@ -139,6 +143,9 @@ private ArtifactRoll rollLockedPrimary(ArtifactPathSpec spec) { if (companion == null || !companion.isEnabled() || !companion.hasRarity(rarity.getId())) { continue; } + if (!ElementVisibility.shownOnArtifact(companion.getElementId(), type.getElementId())) { + continue; + } if (!ArtifactAffinityRegistry.compatibleWith(slotIds(slots), companion.getElementId())) { continue; } diff --git a/src/main/java/net/tfminecraft/magic/artifact/shrine/ShrineChargeService.java b/src/main/java/net/tfminecraft/magic/artifact/shrine/ShrineChargeService.java index c23a983..d2cbe5a 100644 --- a/src/main/java/net/tfminecraft/magic/artifact/shrine/ShrineChargeService.java +++ b/src/main/java/net/tfminecraft/magic/artifact/shrine/ShrineChargeService.java @@ -141,9 +141,8 @@ public static AdminStart startAdmin(Player player, String elementId, double amou continue; } sawArtifact = true; - // Admin fill names the element outright, so a charge can take on one the - // shrine would never score. This is the only way to reach the elements - // that have scenery charging disabled. + // Admin fill names the element outright. Schools players cannot charge + // are refused here, the same as a shrine imprint. if (artifact instanceof Charge charge && charge.getCap(id) <= 0 && charge.imprintElement(item, id)) { charge.write(item); diff --git a/src/main/java/net/tfminecraft/magic/charge/Charge.java b/src/main/java/net/tfminecraft/magic/charge/Charge.java index 6ffdcf1..737346d 100644 --- a/src/main/java/net/tfminecraft/magic/charge/Charge.java +++ b/src/main/java/net/tfminecraft/magic/charge/Charge.java @@ -14,6 +14,7 @@ import net.tfminecraft.magic.artifact.aura.VesselKind; import net.tfminecraft.magic.artifact.shrine.ShrineScore; import net.tfminecraft.magic.artifact.shrine.ShrineElementScore; +import net.tfminecraft.magic.model.ElementVisibility; /** * Crafting consumable that gathers aura at a shrine and is spent at the mage station. @@ -87,7 +88,7 @@ public boolean isBlank() { } /** - * Takes on every element the shrine scores, capped at this charge's tier. + * Takes on every playable element the shrine scores, capped at this charge's tier. * * @return true when at least one element was imprinted */ @@ -107,6 +108,9 @@ public boolean imprint(ItemStack stack, ShrineScore score) { if (elementScore == null || elementScore.getMaxAura() <= 0) { continue; } + if (!ElementVisibility.shownOnCharge(entry.getKey())) { + continue; + } setCap(entry.getKey(), cap); any = true; if (elementScore.getMaxAura() > bestScore) { @@ -130,6 +134,9 @@ public boolean imprintElement(ItemStack stack, String elementId) { if (stack == null || elementId == null || elementId.isBlank() || cap <= 0) { return false; } + if (!ElementVisibility.shownOnCharge(elementId)) { + return false; + } setCap(elementId, cap); stampTier(stack); if (primary.isBlank()) { diff --git a/src/main/java/net/tfminecraft/magic/charge/ChargeLore.java b/src/main/java/net/tfminecraft/magic/charge/ChargeLore.java index a5b674d..1e9ef9a 100644 --- a/src/main/java/net/tfminecraft/magic/charge/ChargeLore.java +++ b/src/main/java/net/tfminecraft/magic/charge/ChargeLore.java @@ -10,6 +10,7 @@ import net.tfminecraft.magic.Cache; import net.tfminecraft.magic.model.ElementDef; +import net.tfminecraft.magic.model.ElementVisibility; import net.tfminecraft.magic.registry.ElementRegistry; import net.tfminecraft.magic.util.MagicText; @@ -54,8 +55,8 @@ public static void apply(ItemStack stack) { String numeral = TierBands.numeralFor(element.getId(), charge.getFill(element.getId())); // An imprinted element below the first band still lists, so the player can // see which elements the charge took on before it has gathered anything. - block.add(MagicText.format(element.getColor() + element.getName() - + " {color:label_muted}" + (numeral.isEmpty() ? "-" : numeral))); + block.add(MagicText.elementName(element) + + MagicText.format(" {color:label_muted}" + (numeral.isEmpty() ? "-" : numeral))); } } block.set(0, BEGIN + block.get(0)); @@ -71,7 +72,7 @@ private static List orderedElements(Charge charge) { String primary = charge.primaryElementId(); List listed = new ArrayList<>(); for (ElementDef element : ElementRegistry.getAll()) { - if (charge.getCap(element.getId()) > 0) { + if (charge.getCap(element.getId()) > 0 && ElementVisibility.shownOnCharge(element.getId())) { listed.add(element); } } diff --git a/src/main/java/net/tfminecraft/magic/gear/WeaponLore.java b/src/main/java/net/tfminecraft/magic/gear/WeaponLore.java index 58b9e04..c9cf5aa 100644 --- a/src/main/java/net/tfminecraft/magic/gear/WeaponLore.java +++ b/src/main/java/net/tfminecraft/magic/gear/WeaponLore.java @@ -10,6 +10,7 @@ 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; @@ -44,13 +45,16 @@ public static void apply(ItemStack stack) { } else { block.add(MagicText.format("{color:label_muted}Resonance")); for (ElementDef element : ElementRegistry.getAll()) { + if (!ElementVisibility.shownOnCharge(element.getId())) { + continue; + } double fill = requirement.aura().getFill(element.getId()); if (requirement.aura().getCap(element.getId()) <= 0 && fill <= 0) { continue; } String numeral = TierBands.numeralFor(element.getId(), fill); - block.add(MagicText.format(element.getColor() + element.getName() - + " {color:label_muted}" + (numeral.isEmpty() ? "-" : numeral))); + block.add(MagicText.elementName(element) + + MagicText.format(" {color:label_muted}" + (numeral.isEmpty() ? "-" : numeral))); } } int rift = WeaponRift.get(stack); diff --git a/src/main/java/net/tfminecraft/magic/gear/WeaponRequirement.java b/src/main/java/net/tfminecraft/magic/gear/WeaponRequirement.java index feb40bf..47e16e7 100644 --- a/src/main/java/net/tfminecraft/magic/gear/WeaponRequirement.java +++ b/src/main/java/net/tfminecraft/magic/gear/WeaponRequirement.java @@ -9,6 +9,7 @@ import net.tfminecraft.magic.artifact.aura.AuraData; import net.tfminecraft.magic.charge.Charge; import net.tfminecraft.magic.charge.TierBands; +import net.tfminecraft.magic.model.ElementVisibility; public final class WeaponRequirement { @@ -64,6 +65,9 @@ public void mergeAmounts(Map amounts) { continue; } String elementId = entry.getKey(); + if (!ElementVisibility.shownOnCharge(elementId)) { + continue; + } double next = Math.max(aura.getFill(elementId), incoming); if (aura.getCap(elementId) < next) { aura.setCap(elementId, next); diff --git a/src/main/java/net/tfminecraft/magic/model/ElementDef.java b/src/main/java/net/tfminecraft/magic/model/ElementDef.java index 92cee91..6183a0a 100644 --- a/src/main/java/net/tfminecraft/magic/model/ElementDef.java +++ b/src/main/java/net/tfminecraft/magic/model/ElementDef.java @@ -1,5 +1,11 @@ package net.tfminecraft.magic.model; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + import org.bukkit.configuration.ConfigurationSection; import net.tfminecraft.magic.Cache; @@ -7,10 +13,12 @@ public final class ElementDef { + private static final Pattern HEX = Pattern.compile("#?[0-9a-fA-F]{6}"); + private final String id; private final String name; private final String icon; - private final String color; + private final List colors; private final int slot; private final double maxResonance; private final Double decayPerHourOverride; @@ -21,7 +29,7 @@ public ElementDef(String id, ConfigurationSection config) { this.id = id; this.name = config.getString("name", id); this.icon = config.getString("icon", "v.BARRIER"); - this.color = config.getString("color", "#ffffff"); + this.colors = readColors(config); this.slot = config.getInt("slot", -1); this.maxResonance = Math.max(1.0, config.getDouble("max_resonance", 100.0)); this.decayPerHourOverride = config.contains("decay_per_hour") @@ -47,8 +55,14 @@ public String getIcon() { return icon; } + /** First colour stop, {@code #rrggbb}. Used for particles and single-colour text. */ public String getColor() { - return color; + return colors.get(0); + } + + /** Colour stops in config order. One stop is a solid colour. Several form a gradient. */ + public List getColors() { + return colors; } public double getMaxResonance() { @@ -70,4 +84,37 @@ public int getSlot() { public KeyframeCurve getResonanceCurve() { return resonance; } + + /** + * {@code color} is either one {@code #rrggbb} or a list of stops. Reading the list + * with {@code getString} would stringify it to {@code [#rrggbb]} and print the brackets. + */ + private static List readColors(ConfigurationSection config) { + List parsed = new ArrayList<>(); + if (config.isList("color")) { + for (String entry : config.getStringList("color")) { + addColor(parsed, entry); + } + } else { + addColor(parsed, config.getString("color")); + } + if (parsed.isEmpty()) { + parsed.add("#ffffff"); + } + return Collections.unmodifiableList(parsed); + } + + private static void addColor(List parsed, String raw) { + if (raw == null) { + return; + } + String token = raw.trim(); + if (!HEX.matcher(token).matches()) { + return; + } + if (!token.startsWith("#")) { + token = "#" + token; + } + parsed.add(token.toLowerCase(Locale.ROOT)); + } } diff --git a/src/main/java/net/tfminecraft/magic/model/ElementVisibility.java b/src/main/java/net/tfminecraft/magic/model/ElementVisibility.java new file mode 100644 index 0000000..c3caad7 --- /dev/null +++ b/src/main/java/net/tfminecraft/magic/model/ElementVisibility.java @@ -0,0 +1,90 @@ +package net.tfminecraft.magic.model; + +import java.util.Locale; + +import net.tfminecraft.magic.artifact.config.ArtifactTypeDef; +import net.tfminecraft.magic.artifact.config.ArtifactTypeRegistry; +import net.tfminecraft.magic.artifact.sacrifice.SacrificeElementDef; +import net.tfminecraft.magic.artifact.sacrifice.SacrificeRegistry; +import net.tfminecraft.magic.artifact.shrine.ShrineChargeService; +import net.tfminecraft.magic.artifact.shrine.ShrineElementDef; +import net.tfminecraft.magic.artifact.shrine.ShrineRegistry; + +/** + * Which elements may be written onto artifacts and enchanted charges. + * + *

An element is playable when players can actually put aura into it: scenery + * charging is on, or its sacrifice rite is enabled. Schools that are switched off + * (Shadowmancy, and the other inert schools) stay in the resonance menu but are + * not stamped onto items. An artifact still names its own primary, so a staff-made + * item of that school keeps its line. Disabled artifact types never appear. + */ +public final class ElementVisibility { + + private ElementVisibility() {} + + public static boolean typeEnabled(String elementId) { + ArtifactTypeDef type = typeOf(elementId); + return type == null || type.isEnabled(); + } + + /** + * True when a player can fill this element at a shrine or through an enabled rite. + * Missing shrine data counts as playable so a failed config load does not blank items. + */ + public static boolean playerCanCharge(String elementId) { + if (elementId == null || elementId.isBlank() || !typeEnabled(elementId)) { + return false; + } + ShrineElementDef shrine = ShrineRegistry.getById(elementId); + boolean sceneryOpen = shrine == null || shrine.isSceneryCharge(); + if (sceneryOpen && !ShrineChargeService.blocksVanillaCharge(elementId)) { + return true; + } + if (!SacrificeRegistry.isEnabled()) { + return false; + } + SacrificeElementDef rite = SacrificeRegistry.getById(elementId); + return rite != null && rite.isEnabled(); + } + + /** Charges only list elements a player can fill. */ + public static boolean shownOnCharge(String elementId) { + return playerCanCharge(elementId); + } + + /** + * Artifact lines list playable elements, plus the artifact's own primary even when + * that school cannot be filled from a shrine. + */ + public static boolean shownOnArtifact(String elementId, String primaryId) { + if (!typeEnabled(elementId)) { + return false; + } + if (playerCanCharge(elementId)) { + return true; + } + return primaryId != null && !primaryId.isBlank() && primaryId.equalsIgnoreCase(elementId); + } + + private static ArtifactTypeDef typeOf(String elementId) { + if (elementId == null || elementId.isBlank()) { + return null; + } + ArtifactTypeDef exact = ArtifactTypeRegistry.getById(elementId); + if (exact != null) { + return exact; + } + String id = elementId.trim().toLowerCase(Locale.ROOT); + exact = ArtifactTypeRegistry.getById(id); + if (exact != null) { + return exact; + } + for (ArtifactTypeDef type : ArtifactTypeRegistry.getAll()) { + if (type.getElementId() != null && type.getElementId().equalsIgnoreCase(id)) { + return type; + } + } + return null; + } +} diff --git a/src/main/java/net/tfminecraft/magic/util/MagicText.java b/src/main/java/net/tfminecraft/magic/util/MagicText.java index aa48d47..d2a6e03 100644 --- a/src/main/java/net/tfminecraft/magic/util/MagicText.java +++ b/src/main/java/net/tfminecraft/magic/util/MagicText.java @@ -1,12 +1,15 @@ package net.tfminecraft.magic.util; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.bukkit.Color; +import net.md_5.bungee.api.ChatColor; import net.tfminecraft.tlibs.objects.api.subapi.StringFormatter; import net.tfminecraft.magic.GuiCache; +import net.tfminecraft.magic.model.ElementDef; /** * Formats all player-facing strings from config via TLibs {@link StringFormatter#formatHex}. @@ -30,6 +33,36 @@ public static String text(String colorKey, String plain) { return format(color(colorKey) + (plain != null ? plain : "")); } + /** + * Element name in its configured colour. A single stop is solid. Several stops + * run as a gradient. The name's own hex prefix is not repeated. + */ + public static String elementName(ElementDef element) { + if (element == null) { + return ""; + } + String plain = visibleName(element); + List colors = element.getColors(); + if (colors.size() > 1) { + return StringFormatter.applyColourGradient(plain, colors); + } + return format(element.getColor() + plain); + } + + private static String visibleName(ElementDef element) { + String raw = element.getName(); + if (raw == null || raw.isBlank()) { + raw = element.getId(); + } + @SuppressWarnings("deprecation") + String stripped = ChatColor.stripColor(raw); + if (stripped == null) { + stripped = raw; + } + stripped = stripped.replaceAll("(?i)#[0-9a-f]{6}", "").trim(); + return stripped.isEmpty() ? element.getId() : stripped; + } + /** * Piecewise RGB: 0 dark red, 0.5 yellow, 1 light green. Returns {@code #rrggbb}. */ diff --git a/src/main/resources/elements/elements.yml b/src/main/resources/elements/elements.yml index 04a0706..2ab77f4 100644 --- a/src/main/resources/elements/elements.yml +++ b/src/main/resources/elements/elements.yml @@ -1,4 +1,5 @@ # Magic element catalog. Each element has a fixed GUI slot (0-53). +# color is one "#rrggbb", or a list of hex stops for a gradient across the name. # Row 2: Spirit (20), Arcanum (22), Illusion (24) # Row 3 (third from bottom): Cerrith, Seithr, Oseni, Mitlan # Row 4 (second from bottom): Shadowmancy, Necromancy, Bloodmagic