Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ Magic connects a character's relationship with the elements to meditation, artif
- **Flow and Surge** — casting modes interact with equilibrium, tranquility, and corruption to shape a character's magical state over time.
- **Interactive meditation** — meditate around artifacts and engage with orbiting orbs, spending mental focus to develop resonance.
- **Artifacts and attunement** — artifacts carry elemental aura, with display, storage, and care affecting how that aura remains available.
- **Sealed artifacts** — sealed items crafted elsewhere break open into a rolled artifact, with a random or chosen school and weighted rarity set in `artifacts/sealed.yml`.
- **Crafted mage equipment** — assemble staffs, wands, and swords from parts, then work with charges and orbs to shape their elemental capabilities.
- **Casting consequences** — insufficient attunement can prevent a cast, while unstable equipment or carrying multiple staffs can cause a spell to fumble.

Expand Down
5 changes: 4 additions & 1 deletion src/main/java/net/tfminecraft/magic/Magic.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import net.tfminecraft.magic.listener.MagicSessionListener;
import net.tfminecraft.magic.listener.ResonanceCastListener;
import net.tfminecraft.magic.listener.SacrificeListener;
import net.tfminecraft.magic.listener.SealedArtifactListener;
import net.tfminecraft.magic.loader.ChargesLoader;
import net.tfminecraft.magic.loader.ConfigLoader;
import net.tfminecraft.magic.loader.ElementsLoader;
Expand Down Expand Up @@ -111,6 +112,7 @@ public void onEnable() {
getServer().getPluginManager().registerEvents(new ArtifactListener(), this);
getServer().getPluginManager().registerEvents(new ArtifactFillChestListener(), this);
getServer().getPluginManager().registerEvents(new SacrificeListener(), this);
getServer().getPluginManager().registerEvents(new SealedArtifactListener(), this);
GearStationListener gearStationListener = new GearStationListener();
getServer().getPluginManager().registerEvents(gearStationListener, this);
getServer().getPluginManager().registerEvents(gearStationListener.inventory(), this);
Expand Down Expand Up @@ -328,7 +330,8 @@ private void createConfigs() {
"artifacts/naming-schemes.yml",
"artifacts/adjectives.yml",
"artifacts/shrines.yml",
"artifacts/sacrifice.yml"
"artifacts/sacrifice.yml",
"artifacts/sealed.yml"
};
for (String path : defaultFiles) {
copyResourceIfMissing(path);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import net.tfminecraft.magic.Magic;
import net.tfminecraft.magic.artifact.sacrifice.SacrificeConfigLoader;
import net.tfminecraft.magic.artifact.sacrifice.SacrificeRegistry;
import net.tfminecraft.magic.artifact.sealed.SealedArtifactConfigLoader;
import net.tfminecraft.magic.artifact.sealed.SealedArtifactRegistry;
import net.tfminecraft.magic.artifact.shrine.ShrineConfigLoader;
import net.tfminecraft.magic.artifact.shrine.ShrineRegistry;
import net.tfminecraft.magic.registry.ElementRegistry;
Expand All @@ -40,6 +42,7 @@ public static boolean loadFolder(File folder) {
ok &= ArtifactAdjectiveLoader.load(new File(folder, "adjectives.yml"));
ok &= ShrineConfigLoader.load(new File(folder, "shrines.yml"));
ok &= SacrificeConfigLoader.load(new File(folder, "sacrifice.yml"));
ok &= SealedArtifactConfigLoader.load(new File(folder, "sealed.yml"));

StringBuilder groupSizes = new StringBuilder();
for (Map.Entry<String, List<String>> entry : ArtifactAffinityRegistry.getGroups().entrySet()) {
Expand All @@ -63,7 +66,8 @@ public static boolean loadFolder(File folder) {
+ ", model-schemes=" + ArtifactModelSchemeRegistry.size()
+ ", naming-schemes=" + ArtifactNamingSchemeRegistry.size()
+ ", shrines=" + ShrineRegistry.size()
+ ", sacrifice=" + SacrificeRegistry.size());
+ ", sacrifice=" + SacrificeRegistry.size()
+ ", sealed=" + SealedArtifactRegistry.size());
if (Cache.debug) {
log.info("[Magic] Debug: artifact types=" + ArtifactTypeRegistry.getAllIds());
for (ArtifactTypeDef type : ArtifactTypeRegistry.getAll()) {
Expand All @@ -84,6 +88,7 @@ private static void clearAll() {
ArtifactGeneratorCache.reset();
ShrineRegistry.clear();
SacrificeRegistry.clear();
SealedArtifactRegistry.clear();
}

private static boolean loadModelSchemes(File file) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package net.tfminecraft.magic.artifact.sealed;

import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Logger;

import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.InvalidConfigurationException;
import org.bukkit.configuration.file.YamlConfiguration;

import net.tfminecraft.magic.Magic;
import net.tfminecraft.magic.artifact.config.ArtifactRarityRegistry;
import net.tfminecraft.magic.artifact.config.ArtifactTypeDef;
import net.tfminecraft.magic.artifact.config.ArtifactTypeRegistry;

/**
* Loads artifacts/sealed.yml. Runs after generator.yml so rarities and types can be checked.
*
* <p>Like shrines.yml and sacrifice.yml, problems here never fail the artifact configs: a bad
* entry or weight is skipped with a warning, and an unreadable file turns sealed artifacts off.
*/
public final class SealedArtifactConfigLoader {

static final String RANDOM = "random";

private final Logger log;

private SealedArtifactConfigLoader(Logger log) {
this.log = log;
}

public static boolean load(File file) {
SealedArtifactRegistry.clear();
Logger log = Magic.plugin.getLogger();
if (file == null || !file.isFile()) {
log.info("[Magic] artifacts/sealed.yml missing; sealed artifacts disabled");
return true;
}
YamlConfiguration config = new YamlConfiguration();
try {
config.load(file);
} catch (IOException | InvalidConfigurationException ex) {
log.severe("[Magic] Failed to load sealed.yml, sealed artifacts disabled: " + ex.getMessage());
return true;
}
new SealedArtifactConfigLoader(log).read(config);
return true;
}

private void read(YamlConfiguration config) {
SealedArtifactRegistry.setSound(config.getString("sound", ""));
Map<String, Double> shared = readWeights(config.getConfigurationSection("rarities"), "rarities");
ConfigurationSection sealed = config.getConfigurationSection("sealed");
if (sealed == null) {
return;
}
for (String key : sealed.getKeys(false)) {
readEntry(key, sealed.getConfigurationSection(key), shared);
}
}

private void readEntry(String id, ConfigurationSection section, Map<String, Double> shared) {
if (section == null) {
warn("Sealed artifact '" + id + "' has no data");
return;
}
String item = section.getString("item", "").trim();
if (item.isEmpty()) {
warn("Sealed artifact '" + id + "' has no item");
return;
}
String element = section.getString("element", RANDOM).trim().toLowerCase(Locale.ROOT);
ArtifactTypeDef type = null;
if (!RANDOM.equals(element)) {
type = ArtifactTypeRegistry.getById(element);
if (type == null || !type.isEnabled()) {
warn("Sealed artifact '" + id + "' has unknown or disabled element '" + element + "'");
return;
}
}
Map<String, Double> weights = section.isConfigurationSection("rarities")
? readWeights(section.getConfigurationSection("rarities"), id + ".rarities")
: new LinkedHashMap<>(shared);
if (type != null) {
Iterator<String> rarities = weights.keySet().iterator();
while (rarities.hasNext()) {
String rarity = rarities.next();
if (!type.hasRarity(rarity)) {
warn("Sealed artifact '" + id + "': " + element + " has no " + rarity + " bracket");
rarities.remove();
}
}
}
if (weights.isEmpty()) {
warn("Sealed artifact '" + id + "' has no rarity with a weight above 0");
return;
}
SealedArtifactRegistry.register(new SealedArtifactDef(id, item, type == null ? null : element, weights));
}

private Map<String, Double> readWeights(ConfigurationSection section, String where) {
Map<String, Double> weights = new LinkedHashMap<>();
if (section == null) {
return weights;
}
for (String key : section.getKeys(false)) {
String rarity = key.trim().toLowerCase(Locale.ROOT);
double weight = section.getDouble(key, -1.0);
if (!ArtifactRarityRegistry.contains(rarity)) {
warn("Unknown rarity '" + key + "' in sealed.yml " + where);
} else if (weight < 0.0 || !Double.isFinite(weight)) {
warn("Rarity '" + key + "' in sealed.yml " + where + " needs a weight of 0 or more");
} else if (weight > 0.0) {
weights.put(rarity, weight);
}
}
return weights;
}

private void warn(String message) {
log.warning("[Magic] " + message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package net.tfminecraft.magic.artifact.sealed;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* One sealed artifact item from artifacts/sealed.yml.
*
* <p>A null element means the school is rolled from generator.yml's random_pool.
*/
public final class SealedArtifactDef {

private final String id;
private final String itemPath;
private final String elementId;
private final Map<String, Double> rarities;

public SealedArtifactDef(String id, String itemPath, String elementId, Map<String, Double> rarities) {
this.id = id;
this.itemPath = itemPath;
this.elementId = elementId;
this.rarities = Collections.unmodifiableMap(new LinkedHashMap<>(rarities));
}

public String getId() {
return id;
}

public String getItemPath() {
return itemPath;
}

/** The locked school, or null for a random school. */
public String getElementId() {
return elementId;
}

/** Rarity id to weight, every weight above zero. */
public Map<String, Double> getRarities() {
return rarities;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package net.tfminecraft.magic.artifact.sealed;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;

import net.tfminecraft.tlibs.TLibs;

/**
* Sealed artifact items loaded from artifacts/sealed.yml.
*/
public final class SealedArtifactRegistry {

private static final Map<String, SealedArtifactDef> defs = new LinkedHashMap<>();
private static Set<Material> materials;
private static String sound = "";

private SealedArtifactRegistry() {}

public static void clear() {
defs.clear();
materials = null;
sound = "";
}

public static void register(SealedArtifactDef def) {
defs.put(def.getId(), def);
materials = null;
}

public static List<SealedArtifactDef> getAll() {
return new ArrayList<>(defs.values());
}

public static int size() {
return defs.size();
}

/** Sound key played when a seal breaks; blank for none. */
public static String getSound() {
return sound;
}

public static void setSound(String value) {
sound = value == null ? "" : value.trim();
}

/**
* Matches an item against every sealed artifact.
*
* @return the matching definition, or null when the item is not sealed
*/
public static SealedArtifactDef match(ItemStack item) {
if (item == null || item.getType().isAir() || defs.isEmpty()) {
return null;
}
Set<Material> known = knownMaterials();
if (!known.isEmpty() && !known.contains(item.getType())) {
return null;
}
for (SealedArtifactDef def : defs.values()) {
try {
if (TLibs.getItemAPI().getChecker().checkItemWithPath(item, def.getItemPath())) {
return def;
}
} catch (Exception ignored) {
// bad path in config, skip this entry
}
}
return null;
}

/**
* Resolves each configured path once to learn its material, so most right-clicks
* skip the path checks.
*
* <p>An entry that fails to resolve yields an empty set, which disables the fast path
* rather than making that entry unmatchable.
*/
private static Set<Material> knownMaterials() {
if (materials != null) {
return materials;
}
Set<Material> found = EnumSet.noneOf(Material.class);
for (SealedArtifactDef def : defs.values()) {
ItemStack resolved;
try {
resolved = TLibs.getItemAPI().getCreator().getItemFromPath(def.getItemPath());
} catch (Exception ex) {
resolved = null;
}
if (resolved == null || resolved.getType().isAir()) {
materials = Set.of();
return materials;
}
found.add(resolved.getType());
}
materials = found;
return materials;
}
}
Loading
Loading