feat: initial CMagicGem reimplementation

This commit is contained in:
Coldsmile_7
2026-10-01 14:51:20 +08:00
commit 536c02b57e
82 changed files with 16612 additions and 0 deletions
@@ -0,0 +1,139 @@
package cn.infstar.cmagicgem;
import cn.infstar.cmagicgem.api.event.CMagicReloadEvent;
import cn.infstar.cmagicgem.command.CMagicGemCommand;
import cn.infstar.cmagicgem.condition.ConditionEngine;
import cn.infstar.cmagicgem.config.PluginSettings;
import cn.infstar.cmagicgem.config.LanguageManager;
import cn.infstar.cmagicgem.dsl.DirectiveParser;
import cn.infstar.cmagicgem.expression.Expression;
import cn.infstar.cmagicgem.expression.DynamicExpression;
import cn.infstar.cmagicgem.gem.GemRegistry;
import cn.infstar.cmagicgem.gem.GemService;
import cn.infstar.cmagicgem.gui.GemBrowser;
import cn.infstar.cmagicgem.gui.GemGuiManager;
import cn.infstar.cmagicgem.integration.EconomyBridge;
import cn.infstar.cmagicgem.integration.MythicBridge;
import cn.infstar.cmagicgem.integration.PlaceholderBridge;
import cn.infstar.cmagicgem.integration.PlayerPointsBridge;
import cn.infstar.cmagicgem.integration.ProtectionBridge;
import cn.infstar.cmagicgem.item.ItemCodec;
import cn.infstar.cmagicgem.item.ItemMatcher;
import cn.infstar.cmagicgem.listener.GemUseListener;
import cn.infstar.cmagicgem.migration.LegacyDirectoryImporter;
import cn.infstar.cmagicgem.reward.RewardEngine;
import cn.infstar.cmagicgem.skill.SkillRegistry;
import cn.infstar.cmagicgem.skill.SkillEngine;
import cn.infstar.cmagicgem.util.MessageDispatcher;
import de.tr7zw.changeme.nbtapi.NBT;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.util.Objects;
public final class CMagicGem extends JavaPlugin {
private volatile PluginSettings settings;
private GemRegistry gemRegistry;
private SkillRegistry skillRegistry;
private GemGuiManager guiManager;
private LanguageManager languages;
private EconomyBridge economy;
private PlayerPointsBridge points;
private PlaceholderBridge placeholders;
private MythicBridge mythic;
@Override
public void onLoad() {
NBT.preloadApi();
}
@Override
public void onEnable() {
new LegacyDirectoryImporter(this).importIfPresent();
saveDefaultConfig();
for (String resource : BUNDLED_RESOURCES) saveBundledResource(resource);
languages = new LanguageManager(this);
languages.reload();
DirectiveParser directiveParser = new DirectiveParser();
Expression expression = new Expression();
placeholders = new PlaceholderBridge(this);
DynamicExpression dynamicExpression = new DynamicExpression(expression, placeholders);
economy = new EconomyBridge(this);
points = new PlayerPointsBridge(this);
mythic = new MythicBridge(this);
ProtectionBridge protection = new ProtectionBridge(this);
gemRegistry = new GemRegistry(this, directiveParser);
skillRegistry = new SkillRegistry(this, directiveParser);
ItemCodec itemCodec = new ItemCodec(this, gemRegistry);
ItemMatcher itemMatcher = new ItemMatcher(itemCodec);
MessageDispatcher messages = new MessageDispatcher(this, itemCodec, directiveParser, placeholders, languages);
ConditionEngine conditionEngine = new ConditionEngine(dynamicExpression, itemMatcher, languages);
RewardEngine rewardEngine = new RewardEngine(this, itemCodec, itemMatcher, gemRegistry, skillRegistry, directiveParser,
dynamicExpression, placeholders, economy, points, mythic);
GemService gemService = new GemService(gemRegistry, itemCodec, itemMatcher, rewardEngine,
dynamicExpression, conditionEngine, languages);
SkillEngine skillEngine = new SkillEngine(this, skillRegistry, itemCodec, directiveParser,
dynamicExpression, conditionEngine, rewardEngine, mythic, protection, messages);
mythic.enableExtensions(gemRegistry, itemCodec, protection, skillEngine::mythicKill);
rewardEngine.skillInvoker(skillEngine::invoke);
GemBrowser gemBrowser = new GemBrowser(gemRegistry, itemCodec, languages);
guiManager = new GemGuiManager(this, gemRegistry, gemService, itemCodec, itemMatcher, messages, languages);
reloadAll();
CMagicGemCommand command = new CMagicGemCommand(gemRegistry, gemService, itemCodec, gemBrowser,
guiManager, skillEngine, messages, languages, this::reloadScope);
PluginCommand pluginCommand = Objects.requireNonNull(getCommand("cmgem"));
pluginCommand.setExecutor(command);
pluginCommand.setTabCompleter(command);
getServer().getPluginManager().registerEvents(new GemUseListener(gemService, itemCodec, messages, () -> settings), this);
getServer().getPluginManager().registerEvents(gemBrowser, this);
getServer().getPluginManager().registerEvents(guiManager, this);
getServer().getPluginManager().registerEvents(skillEngine, this);
getLogger().info("Integrations: Vault=" + economy.status()
+ ", PlayerPoints=" + points.status()
+ ", PlaceholderAPI=" + placeholders.status()
+ ", MythicMobs=" + mythic.status());
getLogger().info("CMagicGem 已启用,命令 /cmgem,加载 " + gemRegistry.all().size() + " 颗宝石。");
}
public void reloadAll() {
reloadScope("all");
}
private void reloadScope(String scope) {
boolean all = scope.equalsIgnoreCase("all");
boolean gems = all || scope.equalsIgnoreCase("gem");
boolean skills = all || scope.equalsIgnoreCase("skill");
if (all) {
reloadConfig();
if (languages != null) languages.reload();
settings = PluginSettings.load(getConfig());
if (economy != null) economy.refresh();
if (points != null) points.refresh();
if (placeholders != null) placeholders.refresh();
if (mythic != null) mythic.refresh();
}
if (gems && gemRegistry != null) gemRegistry.reload();
if (skills && skillRegistry != null) {
if (settings.skillEngine()) skillRegistry.reload();
else skillRegistry.clear();
}
if ((all || gems) && guiManager != null) guiManager.reload();
if (isEnabled()) getServer().getPluginManager().callEvent(new CMagicReloadEvent());
}
private void saveBundledResource(String path) {
File file = new File(getDataFolder(), path);
if (!file.exists()) saveResource(path, false);
}
private static final String[] BUNDLED_RESOURCES = {
"gems/ExampleGem.yml", "gems/AttributeGem.yml", "gems/ButtonGem.yml", "gems/EmbedGem.yml",
"gems/EnchantGem.yml", "gems/FunctionGem.yml", "gems/LoreGem.yml", "gems/MythicGem.yml",
"gems/PlayerGem.yml", "gems/PotionGem.yml",
"skills/ExampleSkills.yml", "skills/DefaultBuffs.yml", "skills/DefaultMMSkills.yml",
"skills/DefaultSkills.yml", "gui/Default.yml", "gui/gui.yml", "gui/rune.yml",
"languages/en_US.yml", "languages/zh_CN.yml"
};
}
@@ -0,0 +1,36 @@
package cn.infstar.cmagicgem.api.event;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
public final class CMagicEquipmentChangeEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final String slot;
private final ItemStack oldItem;
private final ItemStack newItem;
private final Cause cause;
private boolean cancelled;
public CMagicEquipmentChangeEvent(Player player, String slot, ItemStack oldItem, ItemStack newItem, Cause cause) {
super(player);
this.slot = slot;
this.oldItem = oldItem == null ? null : oldItem.clone();
this.newItem = newItem == null ? null : newItem.clone();
this.cause = cause;
}
public String getSlot() { return slot; }
public ItemStack getOldItem() { return oldItem == null ? null : oldItem.clone(); }
public ItemStack getNewItem() { return newItem == null ? null : newItem.clone(); }
public Cause getCause() { return cause; }
@Override public boolean isCancelled() { return cancelled; }
@Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
@Override public @NotNull HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
public enum Cause { ARMOR, HELD, SWAP, INVENTORY, BREAK, DEATH, CUSTOM }
}
@@ -0,0 +1,28 @@
package cn.infstar.cmagicgem.api.event;
import cn.infstar.cmagicgem.gem.GemDefinition;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public final class CMagicGemUseEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final GemDefinition gem;
private final boolean use;
private boolean cancelled;
public CMagicGemUseEvent(Player player, GemDefinition gem, boolean use) {
super(player);
this.gem = gem;
this.use = use;
}
public GemDefinition getGem() { return gem; }
public boolean isUse() { return use; }
@Override public boolean isCancelled() { return cancelled; }
@Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
@Override public @NotNull HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
}
@@ -0,0 +1,30 @@
package cn.infstar.cmagicgem.api.event;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.jetbrains.annotations.NotNull;
public final class CMagicJumpEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private Location from;
private Location to;
private boolean cancelled;
public CMagicJumpEvent(Player player, Location from, Location to) {
super(player);
this.from = from.clone();
this.to = to.clone();
}
public Location getFrom() { return from.clone(); }
public void setFrom(Location from) { this.from = from.clone(); }
public Location getTo() { return to.clone(); }
public void setTo(Location to) { this.to = to.clone(); }
@Override public boolean isCancelled() { return cancelled; }
@Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
@Override public @NotNull HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
}
@@ -0,0 +1,38 @@
package cn.infstar.cmagicgem.api.event;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.bukkit.potion.PotionEffectType;
import org.jetbrains.annotations.NotNull;
public final class CMagicPotionBuffEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final LivingEntity target;
private final PotionEffectType type;
private int amplifier;
private int durationTicks;
private boolean cancelled;
public CMagicPotionBuffEvent(Player player, LivingEntity target, PotionEffectType type,
int amplifier, int durationTicks) {
super(player);
this.target = target;
this.type = type;
this.amplifier = amplifier;
this.durationTicks = durationTicks;
}
public LivingEntity getTarget() { return target; }
public PotionEffectType getType() { return type; }
public int getAmplifier() { return amplifier; }
public void setAmplifier(int amplifier) { this.amplifier = Math.max(0, amplifier); }
public int getDurationTicks() { return durationTicks; }
public void setDurationTicks(int durationTicks) { this.durationTicks = Math.max(1, durationTicks); }
@Override public boolean isCancelled() { return cancelled; }
@Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
@Override public @NotNull HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
}
@@ -0,0 +1,16 @@
package cn.infstar.cmagicgem.api.event;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public final class CMagicReloadEvent extends Event {
private static final HandlerList HANDLERS = new HandlerList();
public CMagicReloadEvent() {
super(false);
}
@Override public @NotNull HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
}
@@ -0,0 +1,26 @@
package cn.infstar.cmagicgem.api.event;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
public final class CMagicSkillLoadEvent extends Event implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final String name;
private final ConfigurationSection config;
private boolean cancelled;
public CMagicSkillLoadEvent(String name, ConfigurationSection config) {
this.name = name;
this.config = config;
}
public String getName() { return name; }
public ConfigurationSection getConfig() { return config; }
@Override public boolean isCancelled() { return cancelled; }
@Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
@Override public @NotNull HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
}
@@ -0,0 +1,29 @@
package cn.infstar.cmagicgem.api.event;
import cn.infstar.cmagicgem.skill.SkillDefinition;
import org.bukkit.entity.Player;
import org.bukkit.event.Cancellable;
import org.bukkit.event.HandlerList;
import org.bukkit.event.player.PlayerEvent;
import org.bukkit.inventory.ItemStack;
import org.jetbrains.annotations.NotNull;
public final class CMagicSkillUseEvent extends PlayerEvent implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final SkillDefinition skill;
private final ItemStack item;
private boolean cancelled;
public CMagicSkillUseEvent(Player player, SkillDefinition skill, ItemStack item) {
super(player);
this.skill = skill;
this.item = item;
}
public SkillDefinition getSkill() { return skill; }
public ItemStack getItem() { return item; }
@Override public boolean isCancelled() { return cancelled; }
@Override public void setCancelled(boolean cancelled) { this.cancelled = cancelled; }
@Override public @NotNull HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
}
@@ -0,0 +1,401 @@
package cn.infstar.cmagicgem.command;
import cn.infstar.cmagicgem.config.LanguageManager;
import cn.infstar.cmagicgem.gem.GemDefinition;
import cn.infstar.cmagicgem.gem.GemRegistry;
import cn.infstar.cmagicgem.gem.GemService;
import cn.infstar.cmagicgem.gem.GemUseResult;
import cn.infstar.cmagicgem.item.ItemCodec;
import cn.infstar.cmagicgem.gui.GemBrowser;
import cn.infstar.cmagicgem.gui.GemGuiManager;
import cn.infstar.cmagicgem.skill.SkillEngine;
import cn.infstar.cmagicgem.util.MessageDispatcher;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.NBTType;
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.PlayerInventory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
public final class CMagicGemCommand implements CommandExecutor, TabCompleter {
private final GemRegistry registry;
private final GemService gemService;
private final ItemCodec itemCodec;
private final Consumer<String> reload;
private final GemBrowser browser;
private final SkillEngine skillEngine;
private final GemGuiManager guiManager;
private final MessageDispatcher messages;
private final LanguageManager languages;
public CMagicGemCommand(GemRegistry registry, GemService gemService, ItemCodec itemCodec, GemBrowser browser,
GemGuiManager guiManager, SkillEngine skillEngine, MessageDispatcher messages,
LanguageManager languages, Consumer<String> reload) {
this.registry = registry;
this.gemService = gemService;
this.itemCodec = itemCodec;
this.reload = reload;
this.browser = browser;
this.skillEngine = skillEngine;
this.guiManager = guiManager;
this.messages = messages;
this.languages = languages;
}
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (args.length == 0 || args[0].equalsIgnoreCase("help")) return help(sender);
String subcommand = args[0].toLowerCase(Locale.ROOT);
try {
return switch (subcommand) {
case "reload" -> reload(sender, args);
case "give" -> give(sender, args);
case "apply" -> apply(sender, args);
case "remove" -> remove(sender, args);
case "list" -> list(sender);
case "get" -> browse(sender, true);
case "view" -> browse(sender, false);
case "material" -> inspectMaterial(sender);
case "lore" -> inspectLore(sender);
case "enchant" -> inspectEnchant(sender);
case "nbt" -> inspectNbt(sender, args);
case "signal" -> signal(sender, args);
case "embed" -> embed(sender, args);
case "addblock" -> addBlock(sender, args);
case "delblock" -> deleteBlock(sender);
default -> help(sender);
};
} catch (IllegalArgumentException exception) {
sender.sendMessage(itemCodec.color("&c" + exception.getMessage()));
return true;
}
}
private boolean help(CommandSender sender) {
sender.sendMessage(itemCodec.color("&6CMagicGem &7- &f/cmgem"));
List<String> help = languages.section("command.help");
if (help.isEmpty()) help = List.of("&e/cmgem give <玩家> <宝石> [数量]",
"&e/cmgem apply <玩家> <宝石> [槽位]", "&e/cmgem remove <玩家> <宝石> [槽位]",
"&e/cmgem list | material | lore | enchant | nbt | reload",
"&e/cmgem embed [界面] | signal <玩家> <信号>");
help.forEach(line -> sender.sendMessage(itemCodec.color(line)));
return true;
}
private boolean signal(CommandSender sender, String[] args) {
requireAdmin(sender);
if (args.length < 3) throw new IllegalArgumentException("用法: /cmgem signal <玩家> <信号>");
String signal = String.join(" ", Arrays.copyOfRange(args, 2, args.length));
if (args[1].equalsIgnoreCase("@all")) {
Bukkit.getOnlinePlayers().forEach(player -> skillEngine.signal(player, signal));
sender.sendMessage(itemCodec.color(languages.text("cmagic.command.signal_all", "&a已向所有在线玩家发送技能信号。")));
return true;
}
Player player = requirePlayer(args[1]);
skillEngine.signal(player, signal);
sender.sendMessage(itemCodec.color(languages.text("cmagic.command.signal_player", "&a已向 {player} 发送技能信号。",
"player", player.getName())));
return true;
}
private boolean embed(CommandSender sender, String[] args) {
if (!(sender instanceof Player player)) throw new IllegalArgumentException(languages.text("command.player_only", "&c该命令只能由玩家执行"));
if (!hasPermission(sender, "embed")) throw new IllegalArgumentException(languages.text("command.no_permission", "&c你没有权限"));
if (args.length >= 2 && args[1].equalsIgnoreCase("addblock"))
return addBlock(sender, Arrays.copyOfRange(args, 1, args.length));
if (args.length >= 2 && args[1].equalsIgnoreCase("delblock")) return deleteBlock(sender);
String name = args.length < 2 ? null : String.join(" ", Arrays.copyOfRange(args, 1, args.length));
if (!guiManager.open(player, name)) throw new IllegalArgumentException(languages.text("gem.embed.not_found", "&c未知镶嵌台: {gui}",
"gui", name == null ? "default" : name));
return true;
}
private boolean addBlock(CommandSender sender, String[] args) {
requireAdmin(sender);
if (!(sender instanceof Player player)) throw new IllegalArgumentException(languages.text("command.player_only", "&c该命令只能由玩家执行"));
if (args.length < 2) throw new IllegalArgumentException("用法: /cmgem addblock <界面>");
var block = player.getLocation().clone().subtract(0, 1, 0).getBlock();
String name = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
if (!guiManager.addBlock(name, block.getLocation())) throw new IllegalArgumentException("未知界面或保存失败: " + name);
sender.sendMessage(itemCodec.color(languages.text("command.embed.addblock", "&a已绑定镶嵌台方块。")));
return true;
}
private boolean deleteBlock(CommandSender sender) {
requireAdmin(sender);
if (!(sender instanceof Player player)) throw new IllegalArgumentException(languages.text("command.player_only", "&c该命令只能由玩家执行"));
var block = player.getLocation().clone().subtract(0, 1, 0).getBlock();
if (!guiManager.deleteBlock(block.getLocation())) throw new IllegalArgumentException("脚下方块未绑定镶嵌台");
sender.sendMessage(itemCodec.color(languages.text("command.embed.delblock", "&a已解除镶嵌台方块。")));
return true;
}
private boolean reload(CommandSender sender, String[] args) {
requireAdmin(sender);
String scope = args.length < 2 ? "all" : args[1].toLowerCase(Locale.ROOT);
if (!Set.of("all", "gem", "skill").contains(scope))
throw new IllegalArgumentException("用法: /cmgem reload [gem|skill]");
reload.accept(scope);
String fallback = switch (scope) {
case "gem" -> "&a宝石配置已重载,共加载 {amount} 颗宝石。";
case "skill" -> "&a技能配置已重载。";
default -> "&aCMagicGem 已安全重载,共加载 {amount} 颗宝石。";
};
sender.sendMessage(itemCodec.color(languages.text("cmagic.command.reload_" + scope, fallback,
"amount", registry.all().size())));
return true;
}
private boolean give(CommandSender sender, String[] args) {
requireAdmin(sender);
if (args.length < 3) throw new IllegalArgumentException("用法: /cmgem give <玩家> <宝石> [数量]");
Player player = requirePlayer(args[1]);
GemDefinition gem = requireGem(joinGemName(args, 2, args.length > 3 && integer(args[args.length - 1]) ? args.length - 1 : args.length));
int amount = args.length > 3 && integer(args[args.length - 1]) ? Integer.parseInt(args[args.length - 1]) : 1;
gemService.give(player, gem, amount);
sender.sendMessage(itemCodec.color(languages.text("cmagic.command.give", "&a已给予 {player} {amount} 个 {gem}",
"player", player.getName(), "amount", amount, "gem", gem.name())));
return true;
}
private boolean apply(CommandSender sender, String[] args) {
requireAdmin(sender);
if (args.length < 3) throw new IllegalArgumentException("用法: /cmgem apply <玩家> <宝石> [槽位]");
Player player = requirePlayer(args[1]);
String possibleSlot = args[args.length - 1];
EquipmentSlot slot = args.length > 3 ? parseSlot(possibleSlot) : EquipmentSlot.HAND;
int nameEnd = args.length > 3 && slot != null ? args.length - 1 : args.length;
if (slot == null) slot = EquipmentSlot.HAND;
GemDefinition gem = requireGem(joinGemName(args, 2, nameEnd));
ItemStack target = getSlot(player.getInventory(), slot);
GemUseResult result = gemService.use(player, gem, target);
if (result.successful() || result.consumed()) setSlot(player.getInventory(), slot, result.target());
messages.send(sender, result.message());
return true;
}
private boolean remove(CommandSender sender, String[] args) {
requireAdmin(sender);
if (args.length < 3) throw new IllegalArgumentException("用法: /cmgem remove <玩家> <宝石> [槽位]");
Player player = requirePlayer(args[1]);
EquipmentSlot slot = args.length > 3 ? parseSlot(args[args.length - 1]) : EquipmentSlot.HAND;
int nameEnd = args.length > 3 && slot != null ? args.length - 1 : args.length;
if (slot == null) slot = EquipmentSlot.HAND;
String gemName = joinGemName(args, 2, nameEnd);
ItemStack target = getSlot(player.getInventory(), slot);
GemUseResult result = gemService.remove(player, gemName, target);
if (result.successful() || result.target() != target) setSlot(player.getInventory(), slot, result.target());
messages.send(sender, result.message());
return true;
}
private boolean list(CommandSender sender) {
if (!hasPermission(sender, "view")) throw new IllegalArgumentException(languages.text("command.no_permission", "&c你没有权限"));
sender.sendMessage(itemCodec.color(languages.text("cmagic.command.list", "&6已加载宝石 (&e{amount}&6):",
"amount", registry.all().size())));
sender.sendMessage(itemCodec.color("&f" + String.join("&7, &f", registry.names())));
return true;
}
private boolean browse(CommandSender sender, boolean allowTake) {
if (!(sender instanceof Player player)) throw new IllegalArgumentException(languages.text("command.player_only", "&c该命令只能由玩家执行"));
if (allowTake) requireAdmin(sender);
else if (!hasPermission(sender, "view")) throw new IllegalArgumentException(languages.text("command.no_permission", "&c你没有权限"));
browser.open(player, allowTake, 0);
return true;
}
private boolean inspectMaterial(CommandSender sender) {
ItemStack item = hand(sender);
sender.sendMessage(itemCodec.color("&eMaterial: &f" + item.getType().name()));
return true;
}
private boolean inspectLore(CommandSender sender) {
ItemStack item = hand(sender);
if (!item.hasItemMeta() || !item.getItemMeta().hasLore() || item.getItemMeta().getLore() == null) sender.sendMessage("Lore: []");
else item.getItemMeta().getLore().forEach(line -> sender.sendMessage(line.replace('§', '&')));
return true;
}
private boolean inspectEnchant(CommandSender sender) {
ItemStack item = hand(sender);
if (item.getEnchantments().isEmpty()) sender.sendMessage("Enchantments: []");
item.getEnchantments().forEach((enchantment, level) -> sender.sendMessage(enchantment.getKey() + "=" + level));
return true;
}
private boolean inspectNbt(CommandSender sender, String[] args) {
if (!(sender instanceof Player player))
throw new IllegalArgumentException(languages.text("command.player_only", "&c该命令只能由玩家执行"));
ItemStack item = player.getInventory().getItemInMainHand();
if (item.getType().isAir()) NBT.get(player, nbt -> { sendNbt(sender, nbt, args); });
else NBT.get(item, nbt -> { sendNbt(sender, nbt, args); });
return true;
}
private void sendNbt(CommandSender sender, ReadableNBT nbt, String[] args) {
if (args.length >= 2) {
String key = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
if (!nbt.hasTag(key)) sender.sendMessage(itemCodec.color(languages.text("command.nbt_not_found", "&cNBT 不存在: {key}", "key", key)));
else sender.sendMessage(itemCodec.color("&e" + key + " &7(" + nbt.getType(key).getName()
+ ") &f= " + nbtValue(nbt, key)));
return;
}
if (nbt.getKeys().isEmpty()) sender.sendMessage("NBT: []");
else nbt.getKeys().stream().sorted().forEach(key -> sender.sendMessage(itemCodec.color(
"&e" + key + " &7(" + nbt.getType(key).getName() + ") &f= " + nbtValue(nbt, key))));
}
private String nbtValue(ReadableNBT nbt, String key) {
NBTType type = nbt.getType(key);
if (type == null) return "null";
return switch (type) {
case NBTTagByte -> Byte.toString(nbt.getByte(key));
case NBTTagShort -> Short.toString(nbt.getShort(key));
case NBTTagInt -> Integer.toString(nbt.getInteger(key));
case NBTTagLong -> Long.toString(nbt.getLong(key));
case NBTTagFloat -> Float.toString(nbt.getFloat(key));
case NBTTagDouble -> Double.toString(nbt.getDouble(key));
case NBTTagByteArray -> Arrays.toString(nbt.getByteArray(key));
case NBTTagIntArray -> Arrays.toString(nbt.getIntArray(key));
case NBTTagLongArray -> Arrays.toString(nbt.getLongArray(key));
case NBTTagString -> nbt.getString(key);
case NBTTagCompound -> String.valueOf(nbt.getCompound(key));
case NBTTagList -> "list<" + nbt.getListType(key).getName() + ">";
default -> type.getName();
};
}
private ItemStack hand(CommandSender sender) {
if (!(sender instanceof Player player)) throw new IllegalArgumentException(languages.text("command.player_only", "&c该命令只能由玩家执行"));
ItemStack item = player.getInventory().getItemInMainHand();
if (item.getType().isAir()) throw new IllegalArgumentException(languages.text("cmagic.command.empty_hand", "&c主手没有物品"));
return item;
}
private void requireAdmin(CommandSender sender) {
if (!hasPermission(sender, "admin")) throw new IllegalArgumentException(languages.text("command.no_permission", "&c你没有权限"));
}
private boolean hasPermission(CommandSender sender, String permission) {
return sender.hasPermission("cmagicgem.command." + permission)
|| sender.hasPermission("magicgem.command." + permission);
}
private Player requirePlayer(String name) {
Player player = Bukkit.getPlayerExact(name);
if (player == null) throw new IllegalArgumentException(languages.text("command.player_not_found", "&c玩家不在线: {player}", "player", name));
return player;
}
private GemDefinition requireGem(String name) {
return registry.find(name).orElseThrow(() -> new IllegalArgumentException(
languages.text("command.gem_not_found", "&c未知宝石: {gem}", "gem", name)));
}
private String joinGemName(String[] args, int start, int end) {
return String.join(" ", Arrays.copyOfRange(args, start, end));
}
private boolean integer(String value) {
try { Integer.parseInt(value); return true; } catch (NumberFormatException ignored) { return false; }
}
private EquipmentSlot parseSlot(String value) {
return switch (value.toLowerCase(Locale.ROOT)) {
case "mainhand", "hand" -> EquipmentSlot.HAND;
case "offhand" -> EquipmentSlot.OFF_HAND;
case "head", "helmet" -> EquipmentSlot.HEAD;
case "chest", "chestplate" -> EquipmentSlot.CHEST;
case "legs", "leggings" -> EquipmentSlot.LEGS;
case "feet", "boots" -> EquipmentSlot.FEET;
default -> null;
};
}
private ItemStack getSlot(PlayerInventory inventory, EquipmentSlot slot) {
return switch (slot) {
case HAND -> inventory.getItemInMainHand();
case OFF_HAND -> inventory.getItemInOffHand();
case HEAD -> inventory.getHelmet();
case CHEST -> inventory.getChestplate();
case LEGS -> inventory.getLeggings();
case FEET -> inventory.getBoots();
default -> inventory.getItemInMainHand();
};
}
private void setSlot(PlayerInventory inventory, EquipmentSlot slot, ItemStack item) {
switch (slot) {
case HAND -> inventory.setItemInMainHand(item);
case OFF_HAND -> inventory.setItemInOffHand(item);
case HEAD -> inventory.setHelmet(item);
case CHEST -> inventory.setChestplate(item);
case LEGS -> inventory.setLeggings(item);
case FEET -> inventory.setBoots(item);
default -> inventory.setItemInMainHand(item);
}
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
if (args.length == 1) return filter(List.of("help", "give", "apply", "remove", "list", "get", "view", "embed", "addblock", "delblock", "signal", "material", "lore", "enchant", "nbt", "reload"), args[0]);
if (args.length == 2 && Set.of("give", "apply", "remove", "signal").contains(args[0].toLowerCase(Locale.ROOT))) {
List<String> players = new ArrayList<>(Bukkit.getOnlinePlayers().stream().map(Player::getName).toList());
if (args[0].equalsIgnoreCase("signal")) players.add("@all");
return filter(players, args[1]);
}
if (args.length >= 3 && Set.of("give", "apply", "remove").contains(args[0].toLowerCase(Locale.ROOT)))
return filter(registry.names(), String.join(" ", Arrays.copyOfRange(args, 2, args.length)));
if (args.length == 2 && args[0].equalsIgnoreCase("embed")) {
List<String> values = new ArrayList<>(guiManager.names());
values.add("addblock");
values.add("delblock");
return filter(values, args[1]);
}
if (args.length == 2 && args[0].equalsIgnoreCase("reload"))
return filter(List.of("gem", "skill"), args[1]);
if (args.length >= 3 && args[0].equalsIgnoreCase("embed") && args[1].equalsIgnoreCase("addblock"))
return filter(new ArrayList<>(guiManager.names()), String.join(" ", Arrays.copyOfRange(args, 2, args.length)));
if (args.length >= 2 && args[0].equalsIgnoreCase("addblock"))
return filter(new ArrayList<>(guiManager.names()), String.join(" ", Arrays.copyOfRange(args, 1, args.length)));
if (args.length == 2 && sender instanceof Player player) {
ItemStack item = player.getInventory().getItemInMainHand();
return switch (args[0].toLowerCase(Locale.ROOT)) {
case "material" -> filter(List.of(item.getType().name()), args[1]);
case "lore" -> item.hasItemMeta() && item.getItemMeta().hasLore() && item.getItemMeta().getLore() != null
? filter(item.getItemMeta().getLore().stream().map(line -> line.replace('§', '&')).toList(), args[1])
: List.of();
case "enchant" -> filter(item.getEnchantments().keySet().stream()
.map(enchantment -> enchantment.getKey().toString()).toList(), args[1]);
case "nbt" -> item.getType().isAir()
? NBT.get(player, nbt -> { return filter(nbt.getKeys().stream().toList(), args[1]); })
: NBT.get(item, nbt -> { return filter(nbt.getKeys().stream().toList(), args[1]); });
default -> List.of();
};
}
return List.of();
}
private List<String> filter(List<String> values, String prefix) {
String lower = prefix.toLowerCase(Locale.ROOT);
return values.stream().filter(value -> value.toLowerCase(Locale.ROOT).startsWith(lower)).limit(100).toList();
}
}
@@ -0,0 +1,63 @@
package cn.infstar.cmagicgem.condition;
import cn.infstar.cmagicgem.config.LanguageManager;
import cn.infstar.cmagicgem.dsl.Directive;
import cn.infstar.cmagicgem.expression.DynamicExpression;
import cn.infstar.cmagicgem.item.ItemMatcher;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public final class ConditionEngine {
private final DynamicExpression expressions;
private final ItemMatcher itemMatcher;
private final LanguageManager languages;
public ConditionEngine(DynamicExpression expressions, ItemMatcher itemMatcher, LanguageManager languages) {
this.expressions = expressions;
this.itemMatcher = itemMatcher;
this.languages = languages;
}
public CheckResult check(List<Directive> conditions, Player player, ItemStack item) {
for (Directive condition : conditions) {
CheckResult result = check(condition, player, item);
if (!result.success()) return result;
}
return CheckResult.PASS;
}
public CheckResult check(Directive condition, Player player, ItemStack item) {
String type = condition.type().toLowerCase(Locale.ROOT).replace("_", "");
boolean passed;
try {
passed = switch (type) {
case "papi", "expression" -> expressions.test(condition.argument("false", "p", "expression", "condition", "c"), player, item, Map.of());
case "permission" -> player.hasPermission(condition.argument("", "p", "permission")) == condition.booleanArgument(true, "has");
case "invempty" -> emptySlots(player) >= condition.intArgument(1, "amount");
case "item", "require" -> itemMatcher.matches(item, condition.argument("", "type", "require", "r"));
default -> false;
};
} catch (RuntimeException exception) {
return new CheckResult(false, languages.text("cmagic.condition_parse", "&c条件解析失败: {condition}",
"condition", condition.source()));
}
return passed ? CheckResult.PASS : new CheckResult(false,
condition.argument(languages.text("cmagic.condition_failed", "&c条件不满足: {condition}",
"condition", condition.type()), "tip", "message", "m"));
}
private int emptySlots(Player player) {
int count = 0;
for (ItemStack content : player.getInventory().getStorageContents())
if (content == null || content.getType().isAir()) count++;
return count;
}
public record CheckResult(boolean success, String message) {
public static final CheckResult PASS = new CheckResult(true, "");
}
}
@@ -0,0 +1,86 @@
package cn.infstar.cmagicgem.config;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public final class LanguageManager {
private final JavaPlugin plugin;
private volatile YamlConfiguration messages = new YamlConfiguration();
private volatile String language = "zh_CN";
public LanguageManager(JavaPlugin plugin) {
this.plugin = plugin;
}
public void reload() {
String configured = plugin.getConfig().getString("language",
plugin.getConfig().getString("lang", "zh_CN"));
language = safeName(configured);
File file = new File(plugin.getDataFolder(), "languages/" + language + ".yml");
YamlConfiguration loaded = YamlConfiguration.loadConfiguration(file);
try (InputStream input = plugin.getResource("languages/" + language + ".yml")) {
if (input != null) {
YamlConfiguration bundled = YamlConfiguration.loadConfiguration(
new InputStreamReader(input, StandardCharsets.UTF_8));
loaded.setDefaults(bundled);
}
} catch (Exception exception) {
plugin.getLogger().warning("无法读取内置语言 " + language + ": " + exception.getMessage());
}
messages = loaded;
}
public String language() {
return language;
}
public String text(String path, String fallback, Object... replacements) {
String value = messages.getString(path, fallback);
if (value == null) value = fallback == null ? "" : fallback;
value = commandName(value);
for (int index = 0; index + 1 < replacements.length; index += 2) {
String key = String.valueOf(replacements[index]);
String replacement = String.valueOf(replacements[index + 1]);
value = value.replace("{" + key + "}", replacement).replace("%" + key + "%", replacement);
}
return value;
}
public String format(String path, String fallback, Object... arguments) {
String pattern = text(path, fallback);
try {
return String.format(Locale.ROOT, pattern, arguments);
} catch (RuntimeException exception) {
return pattern;
}
}
public List<String> section(String path) {
ConfigurationSection section = messages.getConfigurationSection(path);
if (section == null) return List.of();
List<String> result = new ArrayList<>();
for (String key : section.getKeys(false)) {
String value = section.getString(key);
if (value != null) result.add(commandName(value));
}
return List.copyOf(result);
}
private String commandName(String value) {
return value.replace("/mgem", "/cmgem").replace("/magicgem", "/cmagicgem");
}
private String safeName(String configured) {
if (configured == null || !configured.matches("[A-Za-z0-9_-]+")) return "zh_CN";
return configured;
}
}
@@ -0,0 +1,13 @@
package cn.infstar.cmagicgem.config;
import org.bukkit.configuration.file.FileConfiguration;
public record PluginSettings(boolean workbench, boolean backpack, boolean gemLore, boolean gemCraft,
boolean skillEngine, boolean skillEngineLoreColor, boolean legacyMigration) {
public static PluginSettings load(FileConfiguration config) {
return new PluginSettings(config.getBoolean("workbench", true), config.getBoolean("backpack", false),
config.getBoolean("gem-lore", false), config.getBoolean("gem-craft", false),
config.getBoolean("skill-engine", true), config.getBoolean("skill-engine-lore-color", false),
config.getBoolean("legacy-migration", true));
}
}
@@ -0,0 +1,34 @@
package cn.infstar.cmagicgem.dsl;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public record Directive(String type, Map<String, String> arguments, List<String> positional,
boolean ignorable, Phase phase, String source) {
public enum Phase {
ALWAYS, SUCCESS, FAIL, REMOVE
}
public String argument(String fallback, String... names) {
for (String name : names) {
String value = arguments.get(name.toLowerCase(Locale.ROOT));
if (value != null) {
return value;
}
}
return fallback;
}
public boolean booleanArgument(boolean fallback, String... names) {
return Boolean.parseBoolean(argument(Boolean.toString(fallback), names));
}
public int intArgument(int fallback, String... names) {
try {
return Integer.parseInt(argument(Integer.toString(fallback), names));
} catch (NumberFormatException ignored) {
return fallback;
}
}
}
@@ -0,0 +1,116 @@
package cn.infstar.cmagicgem.dsl;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public final class DirectiveParser {
public Directive parse(String input) {
String source = input == null ? "" : input.trim();
boolean ignorable = containsFlag(source, "$ignorable");
Directive.Phase phase = containsFlag(source, "$onSuccess") ? Directive.Phase.SUCCESS
: containsFlag(source, "$onFail") ? Directive.Phase.FAIL
: containsFlag(source, "$onRemove") ? Directive.Phase.REMOVE : Directive.Phase.ALWAYS;
String clean = source.replaceAll("(?i)\\s+\\$(ignorable|onSuccess|onFail|onRemove)\\b", "").trim();
int brace = clean.indexOf('{');
String type = (brace < 0 ? clean : clean.substring(0, brace)).trim();
String body = brace < 0 ? "" : extractBody(clean, brace);
Map<String, String> arguments = new LinkedHashMap<>();
List<String> positional = new ArrayList<>();
for (String token : splitTopLevel(body, ';')) {
String trimmed = token.trim();
if (trimmed.isEmpty()) {
continue;
}
int equals = indexOfTopLevel(trimmed, '=');
if (equals < 0) {
positional.add(unquote(trimmed));
} else {
arguments.put(trimmed.substring(0, equals).trim().toLowerCase(Locale.ROOT),
unquote(trimmed.substring(equals + 1).trim()));
}
}
return new Directive(type, Map.copyOf(arguments), List.copyOf(positional), ignorable, phase, source);
}
public List<String> splitTopLevel(String value, char separator) {
List<String> parts = new ArrayList<>();
int depth = 0;
int start = 0;
boolean quoted = false;
for (int index = 0; index < value.length(); index++) {
char current = value.charAt(index);
if (current == '"' && (index == 0 || value.charAt(index - 1) != '\\')) {
quoted = !quoted;
} else if (!quoted && current == '{') {
depth++;
} else if (!quoted && current == '}') {
depth--;
} else if (!quoted && depth == 0 && current == separator) {
parts.add(value.substring(start, index));
start = index + 1;
}
}
parts.add(value.substring(start));
return parts;
}
public List<String> delimitedArgument(Directive directive, Set<String> names, Set<String> stopKeys) {
String source = directive.source();
int open = source.indexOf('{');
int close = source.lastIndexOf('}');
if (open < 0 || close <= open) return List.of();
Set<String> normalizedNames = names.stream().map(value -> value.toLowerCase(Locale.ROOT)).collect(java.util.stream.Collectors.toSet());
Set<String> normalizedStops = stopKeys.stream().map(value -> value.toLowerCase(Locale.ROOT)).collect(java.util.stream.Collectors.toSet());
List<String> values = new ArrayList<>();
boolean collecting = false;
for (String token : splitTopLevel(source.substring(open + 1, close), ';')) {
String trimmed = token.trim();
int equals = indexOfTopLevel(trimmed, '=');
String key = equals < 0 ? "" : trimmed.substring(0, equals).trim().toLowerCase(Locale.ROOT);
if (normalizedNames.contains(key)) {
collecting = true;
values.add(unquote(trimmed.substring(equals + 1).trim()));
} else if (collecting && normalizedStops.contains(key)) {
break;
} else if (collecting) {
values.add(unquote(trimmed));
}
}
return List.copyOf(values);
}
private int indexOfTopLevel(String value, char target) {
int depth = 0;
for (int index = 0; index < value.length(); index++) {
char current = value.charAt(index);
if (current == '{') depth++;
if (current == '}') depth--;
if (current == target && depth == 0) return index;
}
return -1;
}
private String extractBody(String value, int brace) {
int end = value.lastIndexOf('}');
if (end < brace) {
throw new IllegalArgumentException("缺少右花括号: " + value);
}
return value.substring(brace + 1, end);
}
private boolean containsFlag(String value, String flag) {
return value.toLowerCase(Locale.ROOT).contains(flag.toLowerCase(Locale.ROOT));
}
private String unquote(String value) {
if (value.length() < 2) return value;
char quote = value.charAt(0);
if ((quote != '"' && quote != '\'') || value.charAt(value.length() - 1) != quote) return value;
return value.substring(1, value.length() - 1)
.replace("\\" + quote, Character.toString(quote));
}
}
@@ -0,0 +1,146 @@
package cn.infstar.cmagicgem.expression;
import cn.infstar.cmagicgem.integration.PlaceholderBridge;
import cn.infstar.cmagicgem.util.EnchantmentResolver;
import de.tr7zw.changeme.nbtapi.NBT;
import org.bukkit.ChatColor;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class DynamicExpression {
private static final Pattern ITEM_VARIABLE = Pattern.compile("\\$(LORE|WORD|NBT|ENCHANT):([^?$]+?)(?:\\?([^$]*))?\\$", Pattern.CASE_INSENSITIVE);
private static final Pattern NUMBER = Pattern.compile("[-+]?\\d+(?:\\.\\d+)?");
private final Expression expression;
private final PlaceholderBridge placeholders;
public DynamicExpression(Expression expression, PlaceholderBridge placeholders) {
this.expression = expression;
this.placeholders = placeholders;
}
public double evaluate(String source, Player player, ItemStack item, Map<String, Double> variables) {
return evaluate(source, player, item, variables, false);
}
public double evaluateRoman(String source, Player player, ItemStack item, Map<String, Double> variables) {
return evaluate(source, player, item, variables, true);
}
private double evaluate(String source, Player player, ItemStack item, Map<String, Double> variables, boolean roman) {
String resolved = placeholders.resolve(player, source == null ? "0" : source);
Matcher matcher = ITEM_VARIABLE.matcher(resolved);
StringBuffer buffer = new StringBuffer();
while (matcher.find()) {
double fallback = parseNumber(matcher.group(3), 0);
double value = resolveItem(matcher.group(1), matcher.group(2), item, fallback, roman);
matcher.appendReplacement(buffer, Matcher.quoteReplacement(Double.toString(value)));
}
matcher.appendTail(buffer);
Map<String, Double> normalized = new HashMap<>();
variables.forEach((key, value) -> {
normalized.put(key, value);
normalized.put(key.toLowerCase(Locale.ROOT), value);
});
return expression.evaluate(buffer.toString(), normalized);
}
public boolean test(String source, Player player, ItemStack item, Map<String, Double> variables) {
return Math.abs(evaluate(source, player, item, variables)) > 1.0E-9;
}
private double resolveItem(String type, String key, ItemStack item, double fallback, boolean roman) {
if (item == null || item.getType().isAir()) return fallback;
return switch (type.toUpperCase(Locale.ROOT)) {
case "LORE" -> lore(item, key, fallback, roman);
case "WORD" -> word(item, key, fallback);
case "NBT" -> nbt(item, key, fallback);
case "ENCHANT" -> enchant(item, key, fallback);
default -> fallback;
};
}
private double word(ItemStack item, String expected, double value) {
if (!item.hasItemMeta()) return 0;
String cleanExpected = strip(expected);
if (item.getItemMeta().hasDisplayName() && strip(item.getItemMeta().getDisplayName()).contains(cleanExpected)) return value;
if (item.getItemMeta().hasLore() && item.getItemMeta().getLore() != null) {
for (String line : item.getItemMeta().getLore()) if (strip(line).contains(cleanExpected)) return value;
}
return 0;
}
private double lore(ItemStack item, String marker, double fallback, boolean roman) {
if (!item.hasItemMeta() || !item.getItemMeta().hasLore() || item.getItemMeta().getLore() == null) return fallback;
String cleanMarker = strip(marker);
for (String line : item.getItemMeta().getLore()) {
String clean = strip(line);
int index = clean.indexOf(cleanMarker);
if (index < 0) continue;
Matcher number = NUMBER.matcher(clean.substring(index + cleanMarker.length()));
if (number.find()) return parseNumber(number.group(), fallback);
if (roman) {
Matcher numeral = Pattern.compile("(?i)\\b[IVXLCDM]+\\b")
.matcher(clean.substring(index + cleanMarker.length()));
if (numeral.find()) return parseRoman(numeral.group(), fallback);
}
}
return fallback;
}
private double nbt(ItemStack item, String key, double fallback) {
try {
return NBT.get(item, nbt -> {
if (!nbt.hasTag(key)) return fallback;
return switch (nbt.getType(key)) {
case NBTTagByte -> nbt.getByte(key).doubleValue();
case NBTTagShort -> nbt.getShort(key).doubleValue();
case NBTTagInt -> nbt.getInteger(key).doubleValue();
case NBTTagLong -> nbt.getLong(key).doubleValue();
case NBTTagFloat -> nbt.getFloat(key).doubleValue();
case NBTTagDouble -> nbt.getDouble(key);
default -> parseNumber(nbt.getString(key), fallback);
};
});
} catch (RuntimeException exception) {
return fallback;
}
}
private double enchant(ItemStack item, String key, double fallback) {
Enchantment enchantment = EnchantmentResolver.resolve(key);
return enchantment == null ? fallback : item.getEnchantmentLevel(enchantment);
}
private String strip(String value) {
return ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', value == null ? "" : value));
}
private double parseNumber(String value, double fallback) {
try { return value == null || value.isBlank() ? fallback : Double.parseDouble(value); }
catch (NumberFormatException ignored) { return fallback; }
}
private double parseRoman(String value, double fallback) {
if (value == null || value.isBlank()) return fallback;
int total = 0;
int previous = 0;
String normalized = value.toUpperCase(Locale.ROOT);
for (int index = normalized.length() - 1; index >= 0; index--) {
int current = switch (normalized.charAt(index)) {
case 'I' -> 1; case 'V' -> 5; case 'X' -> 10; case 'L' -> 50;
case 'C' -> 100; case 'D' -> 500; case 'M' -> 1000; default -> 0;
};
if (current == 0) return fallback;
total += current < previous ? -current : current;
previous = current;
}
return total;
}
}
@@ -0,0 +1,194 @@
package cn.infstar.cmagicgem.expression;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
public final class Expression {
public double evaluate(String source, Map<String, Double> variables) {
if (source == null || source.isBlank()) {
return 0;
}
Parser parser = new Parser(source, variables);
double result = parser.parseConditional();
parser.skipWhitespace();
if (!parser.finished()) {
throw new IllegalArgumentException("无法解析表达式位置 " + parser.index + ": " + source);
}
return result;
}
public boolean test(String source, Map<String, Double> variables) {
return Math.abs(evaluate(source, variables)) > 1.0E-9;
}
private static final class Parser {
private final String source;
private final Map<String, Double> variables;
private int index;
private Parser(String source, Map<String, Double> variables) {
this.source = source;
this.variables = variables;
}
private double parseOr() {
double left = parseAnd();
while (match("||") || match("|")) {
double right = parseAnd();
left = truth(left) || truth(right) ? 1 : 0;
}
return left;
}
private double parseConditional() {
double condition = parseOr();
if (!match("?")) return condition;
double whenTrue = parseConditional();
require(":");
double whenFalse = parseConditional();
return truth(condition) ? whenTrue : whenFalse;
}
private double parseAnd() {
double left = parseEquality();
while (match("&&") || match("&")) {
double right = parseEquality();
left = truth(left) && truth(right) ? 1 : 0;
}
return left;
}
private double parseEquality() {
double left = parseComparison();
while (true) {
if (match("==")) left = close(left, parseComparison()) ? 1 : 0;
else if (match("!=")) left = close(left, parseComparison()) ? 0 : 1;
else return left;
}
}
private double parseComparison() {
double left = parseTerm();
while (true) {
if (match(">=")) left = left >= parseTerm() ? 1 : 0;
else if (match("<=")) left = left <= parseTerm() ? 1 : 0;
else if (match(">")) left = left > parseTerm() ? 1 : 0;
else if (match("<")) left = left < parseTerm() ? 1 : 0;
else return left;
}
}
private double parseTerm() {
double left = parseFactor();
while (true) {
if (match("+")) left += parseFactor();
else if (match("-")) left -= parseFactor();
else return left;
}
}
private double parseFactor() {
double left = parsePower();
while (true) {
if (match("*")) left *= parsePower();
else if (match("/")) left /= parsePower();
else if (match("%")) left %= parsePower();
else return left;
}
}
private double parsePower() {
double left = parseUnary();
if (match("^")) return Math.pow(left, parsePower());
return left;
}
private double parseUnary() {
if (match("!")) return truth(parseUnary()) ? 0 : 1;
if (match("-")) return -parseUnary();
if (match("+")) return parseUnary();
return parsePrimary();
}
private double parsePrimary() {
if (match("(")) {
double value = parseConditional();
require(")");
return value;
}
skipWhitespace();
if (index < source.length() && (Character.isDigit(source.charAt(index)) || source.charAt(index) == '.')) {
int start = index++;
while (index < source.length() && (Character.isDigit(source.charAt(index)) || source.charAt(index) == '.')) index++;
return Double.parseDouble(source.substring(start, index));
}
match("$");
String name = readName();
if (name.isEmpty()) throw new IllegalArgumentException("表达式缺少值: " + source);
if (match("(")) {
double first = match(")") ? Double.NaN : parseConditional();
double second = Double.NaN;
if (match(",")) second = parseConditional();
if (!Double.isNaN(first)) require(")");
return function(name, first, second);
}
if (name.equalsIgnoreCase("true")) return 1;
if (name.equalsIgnoreCase("false")) return 0;
return variables.getOrDefault(name, variables.getOrDefault(name.toLowerCase(Locale.ROOT), 0.0));
}
private double function(String name, double first, double second) {
return switch (name.toUpperCase(Locale.ROOT)) {
case "RANDOM" -> Double.isNaN(first) ? ThreadLocalRandom.current().nextDouble()
: Double.isNaN(second) ? ThreadLocalRandom.current().nextDouble(first)
: ThreadLocalRandom.current().nextDouble(first, second);
case "RANDINT" -> ThreadLocalRandom.current().nextInt((int) first, (int) second);
case "ABS" -> Math.abs(first);
case "FLOOR" -> Math.floor(first);
case "LOG" -> Math.log(first);
case "POW" -> Math.pow(first, second);
case "CEIL" -> Math.ceil(first);
case "ROUND" -> Math.rint(first);
case "SQRT" -> Math.sqrt(first);
case "MIN" -> Math.min(first, second);
case "MAX" -> Math.max(first, second);
default -> throw new IllegalArgumentException("未知函数: " + name);
};
}
private String readName() {
skipWhitespace();
int start = index;
while (index < source.length() && (Character.isLetterOrDigit(source.charAt(index)) || source.charAt(index) == '_')) index++;
return source.substring(start, index);
}
private boolean match(String token) {
skipWhitespace();
if (!source.startsWith(token, index)) return false;
index += token.length();
return true;
}
private void require(String token) {
if (!match(token)) throw new IllegalArgumentException("缺少 " + token + ": " + source);
}
private void skipWhitespace() {
while (index < source.length() && Character.isWhitespace(source.charAt(index))) index++;
}
private boolean finished() {
return index >= source.length();
}
private boolean truth(double value) {
return Math.abs(value) > 1.0E-9;
}
private boolean close(double left, double right) {
return Math.abs(left - right) < 1.0E-9;
}
}
}
@@ -0,0 +1,21 @@
package cn.infstar.cmagicgem.gem;
import cn.infstar.cmagicgem.dsl.Directive;
import org.bukkit.Material;
import java.util.List;
import java.util.Set;
public record GemDefinition(String id, String name, GemType type, Material material, String display,
List<String> tips, String texture, Integer color, String effects,
Integer customModelData, boolean glow,
Set<String> requirements, List<String> allowedGuis, int embedLimit,
String successExpression, String downgradeExpression, String breakExpression,
String fastUse, boolean eat, boolean shift, double cooldownSeconds, String cooldownTip,
boolean giveRandomGem, List<WeightedGem> randomGems,
List<Directive> rewards, List<Directive> conditions,
String successTip, String failTip, String downgradeTip,
String removeTip, String breakTip, String loreIdentity) {
public record WeightedGem(String name, double weight) {
}
}
@@ -0,0 +1,169 @@
package cn.infstar.cmagicgem.gem;
import cn.infstar.cmagicgem.dsl.Directive;
import cn.infstar.cmagicgem.dsl.DirectiveParser;
import cn.infstar.cmagicgem.item.ItemCodec;
import cn.infstar.cmagicgem.util.MaterialResolver;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
public final class GemRegistry {
private final JavaPlugin plugin;
private final DirectiveParser parser;
private volatile Map<String, GemDefinition> gems = Map.of();
private volatile Map<String, List<GemDefinition>> groups = Map.of();
public GemRegistry(JavaPlugin plugin, DirectiveParser parser) {
this.plugin = plugin;
this.parser = parser;
}
public int reload() {
File directory = new File(plugin.getDataFolder(), "gems");
if (!directory.exists() && !directory.mkdirs()) {
throw new IllegalStateException("无法创建 gems 目录");
}
File[] files = directory.listFiles((ignored, name) -> name.toLowerCase(Locale.ROOT).endsWith(".yml"));
Map<String, GemDefinition> loaded = new LinkedHashMap<>();
Map<String, List<GemDefinition>> loadedGroups = new LinkedHashMap<>();
if (files != null) {
java.util.Arrays.sort(files, java.util.Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER));
for (File file : files) {
List<GemDefinition> group = new ArrayList<>();
loadFile(file, loaded, group);
if (!group.isEmpty()) loadedGroups.put(file.getName(), List.copyOf(group));
}
}
Map<String, String> randomErrors = RandomGemValidator.validate(loaded);
for (Map.Entry<String, String> error : randomErrors.entrySet()) {
GemDefinition invalid = loaded.remove(error.getKey());
if (invalid != null) plugin.getLogger().severe("无法加载随机宝石 " + invalid.name() + ": " + error.getValue());
}
if (!randomErrors.isEmpty()) loadedGroups.replaceAll((name, values) -> values.stream()
.filter(gem -> loaded.containsKey(gem.name().toLowerCase(Locale.ROOT))).toList());
loadedGroups.entrySet().removeIf(entry -> entry.getValue().isEmpty());
gems = Collections.unmodifiableMap(loaded);
groups = Collections.unmodifiableMap(loadedGroups);
return loaded.size();
}
private void loadFile(File file, Map<String, GemDefinition> target, List<GemDefinition> group) {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
for (String key : yaml.getKeys(false)) {
ConfigurationSection section = yaml.getConfigurationSection(key);
if (section == null) continue;
try {
GemDefinition gem = parseGem(key, section);
String normalized = gem.name().toLowerCase(Locale.ROOT);
if (target.containsKey(normalized)) throw new IllegalArgumentException("宝石名称重复: " + gem.name());
target.put(normalized, gem);
group.add(gem);
} catch (RuntimeException exception) {
plugin.getLogger().log(Level.SEVERE, "无法加载宝石 " + file.getName() + "/" + key + ": " + exception.getMessage());
}
}
}
private GemDefinition parseGem(String id, ConfigurationSection section) {
String name = section.getString("Name", id);
GemType type = GemType.parse(section.getString("Type", "ITEM_GEM"));
Material material = MaterialResolver.resolve(section.getString("Material", "PLAYER_HEAD"));
if (material == null) throw new IllegalArgumentException("未知材料 " + section.getString("Material"));
List<Directive> rewards = parseDirectives(section.getStringList("Rewards"));
List<Directive> conditions = parseDirectives(section.getStringList("Conditions"));
List<GemDefinition.WeightedGem> random = new ArrayList<>();
ConfigurationSection randomSection = section.getConfigurationSection("Gems");
if (randomSection != null) {
for (String randomName : randomSection.getKeys(false)) {
random.add(new GemDefinition.WeightedGem(randomName, randomSection.getDouble(randomName, 1)));
}
} else if (type == GemType.RANDOM_GEM) {
for (String entry : section.getStringList("Random")) {
int split = entry.lastIndexOf(':');
random.add(new GemDefinition.WeightedGem(split < 0 ? entry : entry.substring(0, split),
split < 0 ? 1 : Double.parseDouble(entry.substring(split + 1))));
}
}
return new GemDefinition(id, name, type, material, section.getString("Display", name),
List.copyOf(section.getStringList("Tips")), section.getString("Texture"), color(section.get("Color")),
section.getString("Effect"),
section.contains("CustomModelData") ? section.getInt("CustomModelData") : null,
section.getBoolean("Glow", false), upperSet(stringList(section, "Require")),
List.copyOf(stringList(section, "Gui")), section.getInt("Embed", 0),
section.getString("Success", "100"), section.getString("Downgrade", "0"),
section.getString("Break", "0"), fastUse(section),
section.getBoolean("Eat", false), section.getBoolean("Shift", false),
section.getDouble("Cooldown", 0), section.getString("CooldownTip"),
section.getBoolean("Give", false), List.copyOf(random),
rewards, conditions, section.getString("SuccessTip"), section.getString("FailTip"),
section.getString("DowngradeTip"), section.getString("RemoveTip"),
section.getString("BreakTip"), section.getString("Lore"));
}
private Integer color(Object value) {
return ItemCodec.parseColor(value);
}
private String fastUse(ConfigurationSection section) {
Object value = section.get("FastUse");
if (value instanceof Boolean enabled) return enabled ? "offhand" : null;
if (value == null) return null;
String slot = value.toString().trim();
return slot.isEmpty() || slot.equalsIgnoreCase("false") ? null : slot;
}
private List<Directive> parseDirectives(List<String> values) {
List<Directive> directives = new ArrayList<>();
for (String value : values) directives.add(parser.parse(value));
return List.copyOf(directives);
}
private Set<String> upperSet(List<String> values) {
Set<String> result = new LinkedHashSet<>();
for (String value : values) result.add(value.toUpperCase(Locale.ROOT));
return Set.copyOf(result);
}
private List<String> stringList(ConfigurationSection section, String path) {
Object value = section.get(path);
if (value instanceof String text) return text.isBlank() ? List.of() : List.of(text);
if (value instanceof Collection<?> collection) {
List<String> result = new ArrayList<>();
for (Object entry : collection) if (entry != null && !entry.toString().isBlank()) result.add(entry.toString());
return List.copyOf(result);
}
return List.of();
}
public Optional<GemDefinition> find(String name) {
if (name == null) return Optional.empty();
return Optional.ofNullable(gems.get(name.toLowerCase(Locale.ROOT)));
}
public Collection<GemDefinition> all() {
return gems.values();
}
public List<String> names() {
return gems.values().stream().map(GemDefinition::name).sorted().toList();
}
public Map<String, List<GemDefinition>> groups() {
return groups;
}
}
@@ -0,0 +1,252 @@
package cn.infstar.cmagicgem.gem;
import cn.infstar.cmagicgem.api.event.CMagicGemUseEvent;
import cn.infstar.cmagicgem.dsl.Directive;
import cn.infstar.cmagicgem.condition.ConditionEngine;
import cn.infstar.cmagicgem.config.LanguageManager;
import cn.infstar.cmagicgem.expression.DynamicExpression;
import cn.infstar.cmagicgem.item.ItemCodec;
import cn.infstar.cmagicgem.item.ItemMatcher;
import cn.infstar.cmagicgem.reward.RewardContext;
import cn.infstar.cmagicgem.reward.RewardEngine;
import cn.infstar.cmagicgem.reward.RewardException;
import org.bukkit.entity.Player;
import org.bukkit.Bukkit;
import org.bukkit.inventory.ItemStack;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public final class GemService {
private final GemRegistry registry;
private final ItemCodec itemCodec;
private final ItemMatcher matcher;
private final RewardEngine rewards;
private final DynamicExpression expression;
private final ConditionEngine conditions;
private final LanguageManager languages;
private final Map<UUID, Map<String, Long>> cooldowns = new ConcurrentHashMap<>();
public GemService(GemRegistry registry, ItemCodec itemCodec, ItemMatcher matcher,
RewardEngine rewards, DynamicExpression expression, ConditionEngine conditions,
LanguageManager languages) {
this.registry = registry;
this.itemCodec = itemCodec;
this.matcher = matcher;
this.rewards = rewards;
this.expression = expression;
this.conditions = conditions;
this.languages = languages;
}
public GemUseResult use(Player player, GemDefinition requested, ItemStack target) {
CMagicGemUseEvent event = new CMagicGemUseEvent(player, requested, true);
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) return GemUseResult.rejected(target,
languages.text("cmagic.use_cancelled", "&c宝石使用已被其他插件取消"));
GemDefinition gem = resolveRandom(requested);
if (gem == null) return GemUseResult.rejected(target,
languages.text("cmagic.random_invalid", "&c随机宝石没有有效目标"));
if (requested.type() == GemType.RANDOM_GEM && requested.giveRandomGem()) {
give(player, gem, 1);
return new GemUseResult(true, true, target, requested.successTip());
}
GemUseResult result = switch (gem.type()) {
case ITEM_GEM -> useItem(player, requested, gem, target);
case PLAYER_GEM -> usePlayer(player, requested, gem, target);
case RANDOM_GEM -> GemUseResult.rejected(target,
languages.text("cmagic.random_cycle", "&c随机宝石不能循环引用"));
};
if (requested.type() != GemType.RANDOM_GEM) return result;
return new GemUseResult(true, result.successful(), result.target(), result.message());
}
public GemType effectiveType(GemDefinition gem) {
return effectiveType(gem, new HashSet<>());
}
private GemType effectiveType(GemDefinition gem, Set<String> visiting) {
if (gem.type() != GemType.RANDOM_GEM) return gem.type();
if (!visiting.add(gem.name().toLowerCase(java.util.Locale.ROOT))) return GemType.RANDOM_GEM;
GemType resolved = null;
for (GemDefinition.WeightedGem weighted : gem.randomGems()) {
GemDefinition nested = registry.find(weighted.name()).orElse(null);
if (nested == null) continue;
GemType type = effectiveType(nested, new HashSet<>(visiting));
if (type == GemType.RANDOM_GEM || resolved != null && resolved != type) return GemType.RANDOM_GEM;
resolved = type;
}
return resolved == null ? GemType.RANDOM_GEM : resolved;
}
private GemUseResult useItem(Player player, GemDefinition consumedGem, GemDefinition gem, ItemStack target) {
if (!matcher.matches(target, gem.requirements())) return GemUseResult.rejected(target,
languages.text("gem.tip.wrong_type", "&c目标装备不符合宝石要求"));
ConditionEngine.CheckResult condition = conditions.check(gem.conditions(), player, target);
if (!condition.success()) return GemUseResult.rejected(target, condition.message());
Map<String, Integer> embedded = new LinkedHashMap<>(itemCodec.embedded(target));
if (gem.embedLimit() > 0 && embedded.getOrDefault(gem.name(), 0) >= gem.embedLimit())
return GemUseResult.rejected(target, languages.text("gem.tip.embed_limit", "&c已达到该宝石的镶嵌上限"));
boolean successful = roll(gem.successExpression(), player, target);
RewardContext context = new RewardContext(player, gem, target, false);
if (!successful) {
try { rewards.applyPlayerRewards(context, gem.rewards(), Directive.Phase.FAIL); }
catch (RewardException exception) { return GemUseResult.rejected(target, exception.getMessage()); }
if (embedded.getOrDefault(gem.name(), 0) > 0 && roll(gem.downgradeExpression(), player, target)) {
RewardContext downgrade = new RewardContext(player, gem, target, true);
try {
ItemStack changed = rewards.applyItemRewards(downgrade, gem.rewards(), Directive.Phase.REMOVE);
embedded.compute(gem.name(), (ignored, value) -> value == null || value <= 1 ? null : value - 1);
changed = itemCodec.setEmbedded(changed, embedded);
downgrade.item(changed);
return new GemUseResult(true, false, changed,
defaultMessage(gem.downgradeTip(), languages.text("gem.tip.downgrade", "&c强化失败,装备已降级")));
} catch (RewardException exception) {
return GemUseResult.rejected(target, exception.getMessage());
}
}
return new GemUseResult(true, false, target,
defaultMessage(gem.failTip(), languages.text("gem.tip.fail", "&c宝石使用失败")));
}
try {
ItemStack changed = rewards.applyItemRewards(context, gem.rewards(), Directive.Phase.SUCCESS);
context.item(changed);
if (gem.embedLimit() > 0) {
embedded.merge(gem.name(), 1, Integer::sum);
changed = itemCodec.setEmbedded(changed, embedded);
context.item(changed);
}
rewards.applyPlayerRewards(context, gem.rewards(), Directive.Phase.SUCCESS);
return new GemUseResult(true, true, context.item(),
defaultMessage(gem.successTip(), languages.text("gem.tip.success", "&a宝石使用成功")));
} catch (RewardException | RuntimeException exception) {
return GemUseResult.rejected(target, exception.getMessage());
}
}
private GemUseResult usePlayer(Player player, GemDefinition consumedGem, GemDefinition gem, ItemStack target) {
ConditionEngine.CheckResult condition = conditions.check(gem.conditions(), player, target);
if (!condition.success()) return GemUseResult.rejected(target, condition.message());
long now = System.currentTimeMillis();
Map<String, Long> playerCooldowns = cooldowns.computeIfAbsent(player.getUniqueId(), ignored -> new ConcurrentHashMap<>());
long ready = playerCooldowns.getOrDefault(gem.name(), 0L);
if (ready > now) return GemUseResult.rejected(target,
cooldownTip(gem.cooldownTip(), (ready - now) / 1000.0, gem.cooldownSeconds()));
boolean successful = roll(gem.successExpression(), player, target);
RewardContext context = new RewardContext(player, gem, target, false);
try {
rewards.applyPlayerRewards(context, gem.rewards(), successful ? Directive.Phase.SUCCESS : Directive.Phase.FAIL);
if (gem.cooldownSeconds() > 0) playerCooldowns.put(gem.name(), now + (long) (gem.cooldownSeconds() * 1000));
return new GemUseResult(true, successful, target, defaultMessage(successful ? gem.successTip() : gem.failTip(),
languages.text(successful ? "gem.tip.success" : "gem.tip.fail",
successful ? "&a宝石使用成功" : "&c宝石使用失败")));
} catch (RewardException exception) {
return GemUseResult.rejected(target, exception.getMessage());
}
}
public GemUseResult remove(Player player, String gemName, ItemStack target) {
Optional<GemDefinition> found = registry.find(gemName);
if (found.isEmpty()) return GemUseResult.rejected(target,
languages.text("cmagic.gem_not_found", "&c未知宝石: {gem}", "gem", gemName));
GemDefinition gem = found.get();
CMagicGemUseEvent event = new CMagicGemUseEvent(player, gem, false);
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) return GemUseResult.rejected(target,
languages.text("cmagic.remove_cancelled", "&c宝石拆卸已被其他插件取消"));
Map<String, Integer> embedded = new LinkedHashMap<>(itemCodec.embedded(target));
int count = embedded.getOrDefault(gem.name(), 0);
if (count <= 0) return GemUseResult.rejected(target,
languages.text("cmagic.not_embedded", "&c装备未镶嵌该宝石"));
boolean broken = roll(gem.breakExpression(), player, target);
RewardContext context = new RewardContext(player, gem, target, true);
try {
ItemStack changed = rewards.applyItemRewards(context, gem.rewards(), Directive.Phase.REMOVE);
embedded.compute(gem.name(), (ignored, value) -> value == null || value <= 1 ? null : value - 1);
changed = itemCodec.setEmbedded(changed, embedded);
context.item(changed);
RewardContext onRemove = new RewardContext(player, gem, changed, false);
rewards.applyPlayerRewards(onRemove, gem.rewards(), Directive.Phase.REMOVE);
if (!broken) give(player, gem, 1);
return new GemUseResult(false, !broken, changed, broken
? defaultMessage(gem.breakTip(), languages.text("gem.tip.break", "&c宝石在拆卸时破碎"))
: defaultMessage(gem.removeTip(), languages.text("gem.embed.success", "&a宝石拆卸成功")));
} catch (RewardException exception) {
return GemUseResult.rejected(target, exception.getMessage());
}
}
public void give(Player player, GemDefinition gem, int amount) {
int remaining = Math.max(1, amount);
while (remaining > 0) {
int stack = Math.min(remaining, gem.material().getMaxStackSize());
ItemStack item = itemCodec.createGem(gem, stack);
player.getInventory().addItem(item).values().forEach(left -> player.getWorld().dropItemNaturally(player.getLocation(), left));
remaining -= stack;
}
}
private GemDefinition resolveRandom(GemDefinition gem) {
return resolveRandom(gem, new HashSet<>());
}
private GemDefinition resolveRandom(GemDefinition gem, Set<String> visiting) {
if (gem.type() != GemType.RANDOM_GEM) return gem;
if (!visiting.add(gem.name().toLowerCase(java.util.Locale.ROOT))) return null;
List<GemDefinition.WeightedGem> candidates = gem.randomGems();
double total = candidates.stream().filter(value -> registry.find(value.name()).isPresent())
.mapToDouble(value -> Math.max(0, value.weight())).sum();
if (total <= 0) return null;
double roll = ThreadLocalRandom.current().nextDouble(total);
for (GemDefinition.WeightedGem candidate : candidates) {
GemDefinition selected = registry.find(candidate.name()).orElse(null);
if (selected == null || !Double.isFinite(candidate.weight()) || candidate.weight() <= 0) continue;
roll -= candidate.weight();
if (roll <= 0) return resolveRandom(selected, visiting);
}
return null;
}
private boolean roll(String source, Player player, ItemStack item) {
double chance;
try { chance = expression.evaluate(source, player, item, Map.of()); }
catch (RuntimeException ignored) { chance = 0; }
return ThreadLocalRandom.current().nextDouble(100) < chance;
}
private String defaultMessage(String configured, String fallback) {
return configured == null ? fallback : configured;
}
private String cooldownTip(String configured, double remaining, double total) {
if (configured == null || configured.isBlank())
return "&e冷却中,剩余 " + String.format(java.util.Locale.ROOT, "%.2f", remaining) + " 秒";
int open = configured.indexOf('{');
int close = configured.lastIndexOf('}');
if (open < 0 || close < open) return configured;
Map<String, String> arguments = new LinkedHashMap<>();
for (String token : configured.substring(open + 1, close).split(";")) {
int equals = token.indexOf('=');
if (equals > 0) arguments.put(token.substring(0, equals).trim().toLowerCase(java.util.Locale.ROOT),
token.substring(equals + 1));
}
int length;
try { length = Math.max(0, Integer.parseInt(arguments.getOrDefault("length", "0"))); }
catch (NumberFormatException ignored) { length = 0; }
int full = total <= 0 ? 0 : (int) Math.round(length * Math.max(0, Math.min(1, 1 - remaining / total)));
String suffix;
try { suffix = String.format(java.util.Locale.ROOT, arguments.getOrDefault("s", "%.2fs"), remaining); }
catch (RuntimeException ignored) { suffix = String.format(java.util.Locale.ROOT, "%.2fs", remaining); }
String message = arguments.getOrDefault("p", "")
+ arguments.getOrDefault("full", "|").repeat(full)
+ arguments.getOrDefault("empty", "|").repeat(Math.max(0, length - full)) + suffix;
return configured.substring(0, open) + "{m=" + message + '}';
}
}
@@ -0,0 +1,21 @@
package cn.infstar.cmagicgem.gem;
import java.util.Locale;
public enum GemType {
ITEM_GEM, PLAYER_GEM, RANDOM_GEM;
public static GemType parse(String value) {
if (value == null || value.isBlank()) return ITEM_GEM;
String normalized = value.trim().toUpperCase(Locale.ROOT)
.replace("_", "")
.replace("-", "")
.replace(" ", "");
return switch (normalized) {
case "ITEMGEM", "ITEM" -> ITEM_GEM;
case "PLAYERGEM", "PLAYER" -> PLAYER_GEM;
case "RANDOMGEM", "RANDOM" -> RANDOM_GEM;
default -> valueOf(value.trim().toUpperCase(Locale.ROOT));
};
}
}
@@ -0,0 +1,9 @@
package cn.infstar.cmagicgem.gem;
import org.bukkit.inventory.ItemStack;
public record GemUseResult(boolean consumed, boolean successful, ItemStack target, String message) {
public static GemUseResult rejected(ItemStack target, String message) {
return new GemUseResult(false, false, target, message);
}
}
@@ -0,0 +1,58 @@
package cn.infstar.cmagicgem.gem;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
public final class RandomGemValidator {
private RandomGemValidator() {
}
public static Map<String, String> validate(Map<String, GemDefinition> gems) {
Map<String, String> errors = new LinkedHashMap<>();
Map<String, Set<GemType>> resolved = new LinkedHashMap<>();
for (Map.Entry<String, GemDefinition> entry : gems.entrySet()) {
if (entry.getValue().type() != GemType.RANDOM_GEM) continue;
try {
resolve(entry.getKey(), gems, resolved, new ArrayDeque<>());
} catch (IllegalArgumentException exception) {
errors.put(entry.getKey(), exception.getMessage());
}
}
return Map.copyOf(errors);
}
private static Set<GemType> resolve(String name, Map<String, GemDefinition> gems,
Map<String, Set<GemType>> resolved, Deque<String> path) {
String normalized = name.toLowerCase(Locale.ROOT);
Set<GemType> cached = resolved.get(normalized);
if (cached != null) return cached;
if (path.contains(normalized)) {
String cycle = String.join(" -> ", path) + " -> " + normalized;
throw new IllegalArgumentException("随机宝石循环引用: " + cycle);
}
GemDefinition gem = gems.get(normalized);
if (gem == null) throw new IllegalArgumentException("随机宝石引用不存在: " + name);
if (gem.type() != GemType.RANDOM_GEM) return Set.of(gem.type());
path.addLast(normalized);
EnumSet<GemType> types = EnumSet.noneOf(GemType.class);
boolean candidateFound = false;
for (GemDefinition.WeightedGem weighted : gem.randomGems()) {
if (!Double.isFinite(weighted.weight()) || weighted.weight() <= 0) continue;
candidateFound = true;
String target = weighted.name().toLowerCase(Locale.ROOT);
if (!gems.containsKey(target)) throw new IllegalArgumentException("随机宝石引用不存在: " + weighted.name());
types.addAll(resolve(target, gems, resolved, path));
}
path.removeLast();
if (!candidateFound || types.isEmpty()) throw new IllegalArgumentException("随机宝石没有正权重目标: " + gem.name());
if (types.size() > 1) throw new IllegalArgumentException("随机宝石同时包含物品和玩家宝石: " + gem.name());
Set<GemType> result = Set.copyOf(types);
resolved.put(normalized, result);
return result;
}
}
@@ -0,0 +1,185 @@
package cn.infstar.cmagicgem.gui;
import cn.infstar.cmagicgem.config.LanguageManager;
import cn.infstar.cmagicgem.gem.GemDefinition;
import cn.infstar.cmagicgem.gem.GemRegistry;
import cn.infstar.cmagicgem.item.ItemCodec;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class GemBrowser implements Listener {
private static final int PAGE_SIZE = 45;
private final GemRegistry registry;
private final ItemCodec codec;
private final LanguageManager languages;
public GemBrowser(GemRegistry registry, ItemCodec codec, LanguageManager languages) {
this.registry = registry;
this.codec = codec;
this.languages = languages;
}
public void open(Player player, boolean allowTake, int requestedPage) {
openGroups(player, allowTake, requestedPage);
}
private void openGroups(Player player, boolean allowTake, int requestedPage) {
List<Map.Entry<String, List<GemDefinition>>> groups = new ArrayList<>(registry.groups().entrySet());
Page page = page(requestedPage, groups.size());
BrowserHolder holder = new BrowserHolder(allowTake, page.index(), null);
Inventory inventory = Bukkit.createInventory(holder, 54, codec.color(
languages.text(allowTake ? "gem.gui.admin" : "gem.gui.player",
allowTake ? "&8&l管理员-宝石列表" : "&8&l宝石预览")));
holder.inventory = inventory;
Map<Integer, String> entries = new LinkedHashMap<>();
int start = page.index() * PAGE_SIZE;
for (int slot = 0; slot < PAGE_SIZE && start + slot < groups.size(); slot++) {
Map.Entry<String, List<GemDefinition>> group = groups.get(start + slot);
entries.put(slot, group.getKey());
inventory.setItem(slot, groupIcon(group.getKey(), group.getValue()));
}
holder.entries = Map.copyOf(entries);
controls(inventory, page);
player.openInventory(inventory);
}
private void openGroup(Player player, boolean allowTake, String groupName, int requestedPage) {
List<GemDefinition> gems = registry.groups().get(groupName);
if (gems == null) { openGroups(player, allowTake, 0); return; }
Page page = page(requestedPage, gems.size());
BrowserHolder holder = new BrowserHolder(allowTake, page.index(), groupName);
Inventory inventory = Bukkit.createInventory(holder, 54,
codec.color("&8" + groupName + " &7" + (page.index() + 1) + '/' + page.count()));
holder.inventory = inventory;
Map<Integer, String> entries = new LinkedHashMap<>();
int start = page.index() * PAGE_SIZE;
for (int slot = 0; slot < PAGE_SIZE && start + slot < gems.size(); slot++) {
GemDefinition gem = gems.get(start + slot);
entries.put(slot, gem.name());
inventory.setItem(slot, gemIcon(gem, allowTake));
}
holder.entries = Map.copyOf(entries);
controls(inventory, page);
inventory.setItem(49, button(Material.BARRIER,
languages.text("cmagic.gui.back", "&c返回配置文件列表")));
player.openInventory(inventory);
}
private Page page(int requested, int size) {
int pages = Math.max(1, (size + PAGE_SIZE - 1) / PAGE_SIZE);
return new Page(Math.max(0, Math.min(requested, pages - 1)), pages);
}
private void controls(Inventory inventory, Page page) {
if (page.index() > 0) inventory.setItem(45, button(Material.ARROW,
languages.text("gem.gui.prev", "&e上一页")));
inventory.setItem(49, button(Material.PAPER, languages.text("cmagic.gui.page", "&f第 {page} / {pages} 页",
"page", page.index() + 1, "pages", page.count())));
if (page.index() + 1 < page.count()) inventory.setItem(53, button(Material.ARROW,
languages.text("gem.gui.next", "&e下一页")));
}
@EventHandler
public void onClick(InventoryClickEvent event) {
if (!(event.getView().getTopInventory().getHolder() instanceof BrowserHolder holder)) return;
event.setCancelled(true);
if (!(event.getWhoClicked() instanceof Player player)
|| event.getClickedInventory() != event.getView().getTopInventory()) return;
int slot = event.getRawSlot();
if (slot == 45) navigate(player, holder, holder.page - 1);
else if (slot == 53) navigate(player, holder, holder.page + 1);
else if (slot == 49 && holder.group != null) openGroups(player, holder.allowTake, 0);
else if (slot < PAGE_SIZE && holder.entries.containsKey(slot)) {
if (holder.group == null) openGroup(player, holder.allowTake, holder.entries.get(slot), 0);
else if (holder.allowTake) registry.find(holder.entries.get(slot)).ifPresent(gem -> {
int amount = Math.min(64, gem.material().getMaxStackSize());
event.setCursor(codec.createGem(gem, amount));
});
}
}
private void navigate(Player player, BrowserHolder holder, int page) {
if (holder.group == null) openGroups(player, holder.allowTake, page);
else openGroup(player, holder.allowTake, holder.group, page);
}
private ItemStack groupIcon(String groupName, List<GemDefinition> gems) {
ItemStack item = codec.createGem(gems.getFirst(), Math.min(gems.size(), gems.getFirst().material().getMaxStackSize()));
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(codec.color("&e" + groupName));
List<String> lore = meta.hasLore() && meta.getLore() != null ? new ArrayList<>(meta.getLore()) : new ArrayList<>();
lore.add(codec.color(languages.text("cmagic.gui.gem_count", "&7宝石数量: &f{amount}",
"amount", gems.size())));
lore.add(codec.color(languages.text("cmagic.gui.browse", "&a点击浏览")));
meta.setLore(lore);
item.setItemMeta(meta);
return item;
}
private ItemStack gemIcon(GemDefinition gem, boolean internalInfo) {
ItemStack item = codec.createGem(gem, 1);
if (!internalInfo) return item;
ItemMeta meta = item.getItemMeta();
List<String> lore = meta.hasLore() && meta.getLore() != null ? new ArrayList<>(meta.getLore()) : new ArrayList<>();
lore.add(codec.color("&8&m----------------"));
lore.add(codec.color(languages.text("gem.info.info", "&b宝石内部信息")));
lore.add(codec.color("&7" + languages.text("gem.info.name", "名称") + ": &f" + gem.name()));
lore.add(codec.color("&7" + languages.text("gem.info.type", "类型") + ": &f" + gem.type()));
lore.add(codec.color("&7" + languages.text("gem.info.success", "成功率") + ": &f" + gem.successExpression()));
lore.add(codec.color("&7" + languages.text("common.upper", "上限") + ": &f" + gem.embedLimit()));
if (!gem.requirements().isEmpty()) lore.add(codec.color("&7" + languages.text("gem.info.types", "可强化物品列表")
+ ": &f" + String.join(", ", gem.requirements())));
if (!gem.allowedGuis().isEmpty()) lore.add(codec.color("&7" + languages.text("gem.info.guis", "可用界面")
+ ": &f" + String.join(", ", gem.allowedGuis())));
for (var reward : gem.rewards()) lore.add(codec.color("&7" + languages.text("gem.info.reward", "强化奖励")
+ ": &f" + reward.source()));
for (var condition : gem.conditions()) lore.add(codec.color("&7" + languages.text("gem.info.condition", "强化条件")
+ ": &f" + condition.source()));
lore.add(codec.color(languages.text("gem.info.get", "&a单击获得宝石实物")));
meta.setLore(lore);
item.setItemMeta(meta);
return item;
}
private ItemStack button(Material material, String name) {
ItemStack item = new ItemStack(material);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(codec.color(name));
item.setItemMeta(meta);
return item;
}
private record Page(int index, int count) { }
private static final class BrowserHolder implements InventoryHolder {
private final boolean allowTake;
private final int page;
private final String group;
private Map<Integer, String> entries = Map.of();
private Inventory inventory;
private BrowserHolder(boolean allowTake, int page, String group) {
this.allowTake = allowTake;
this.page = page;
this.group = group;
}
@Override
public Inventory getInventory() {
return inventory;
}
}
}
@@ -0,0 +1,484 @@
package cn.infstar.cmagicgem.gui;
import cn.infstar.cmagicgem.config.LanguageManager;
import cn.infstar.cmagicgem.gem.GemDefinition;
import cn.infstar.cmagicgem.gem.GemRegistry;
import cn.infstar.cmagicgem.gem.GemService;
import cn.infstar.cmagicgem.gem.GemType;
import cn.infstar.cmagicgem.gem.GemUseResult;
import cn.infstar.cmagicgem.item.ItemCodec;
import cn.infstar.cmagicgem.item.ItemMatcher;
import cn.infstar.cmagicgem.util.MessageDispatcher;
import org.bukkit.Bukkit;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.InventoryCloseEvent;
import org.bukkit.event.inventory.InventoryDragEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
public final class GemGuiManager implements Listener {
private final JavaPlugin plugin;
private final GemRegistry gems;
private final GemService service;
private final ItemCodec codec;
private final ItemMatcher matcher;
private final MessageDispatcher messages;
private final LanguageManager languages;
private volatile Map<String, GuiDefinition> definitions = Map.of();
private volatile Map<String, String> locations = Map.of();
public GemGuiManager(JavaPlugin plugin, GemRegistry gems, GemService service, ItemCodec codec, ItemMatcher matcher,
MessageDispatcher messages, LanguageManager languages) {
this.plugin = plugin;
this.gems = gems;
this.service = service;
this.codec = codec;
this.matcher = matcher;
this.messages = messages;
this.languages = languages;
}
public int reload() {
closeAll();
File directory = new File(plugin.getDataFolder(), "gui");
if (!directory.exists() && !directory.mkdirs()) throw new IllegalStateException("无法创建 gui 目录");
Map<String, GuiDefinition> loaded = new LinkedHashMap<>();
Map<String, String> blocks = new HashMap<>();
File[] files = directory.listFiles((ignored, name) -> name.toLowerCase(Locale.ROOT).endsWith(".yml"));
if (files != null) for (File file : files) loadFile(file, loaded, blocks);
definitions = Collections.unmodifiableMap(loaded);
locations = Collections.unmodifiableMap(blocks);
return loaded.size();
}
private void loadFile(File file, Map<String, GuiDefinition> loaded, Map<String, String> blocks) {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
for (String name : yaml.getKeys(false)) {
ConfigurationSection section = yaml.getConfigurationSection(name);
if (section == null) continue;
try {
GuiDefinition definition = parse(file, name, section);
loaded.put(name.toLowerCase(Locale.ROOT), definition);
for (String location : section.getStringList("Locations")) {
String normalized = normalizeLocation(location);
if (normalized != null) blocks.put(normalized, definition.name());
}
} catch (RuntimeException exception) {
plugin.getLogger().log(Level.SEVERE, "无法加载镶嵌台 " + file.getName() + '/' + name, exception);
}
}
}
private GuiDefinition parse(File file, String name, ConfigurationSection section) {
List<String> lines = section.getStringList("Slots");
if (lines.isEmpty()) lines = List.of("XXXXXXXXX", "XEGGGGGGX", "XXXXXXXXX");
int rows = Math.max(1, Math.min(6, lines.size()));
StringBuilder layout = new StringBuilder(rows * 9);
for (int row = 0; row < rows; row++) {
String line = lines.get(row);
layout.append(line, 0, Math.min(9, line.length()));
layout.append(" ".repeat(Math.max(0, 9 - line.length())));
}
Map<Character, GuiIcon> icons = new LinkedHashMap<>();
ConfigurationSection items = section.getConfigurationSection("Items");
if (items != null) for (String key : items.getKeys(false)) {
if (key.isEmpty()) continue;
ConfigurationSection icon = items.getConfigurationSection(key);
if (icon == null) continue;
icons.put(key.charAt(0), new GuiIcon(parseType(icon.getString("Type", "TIP")),
icon.getString("Material", "GRAY_STAINED_GLASS_PANE"), icon.getString("Display", " "),
icon.getStringList("Lore").isEmpty() ? icon.getStringList("Tips") : icon.getStringList("Lore"),
icon.getString("Texture"), ItemCodec.parseColor(icon.get("Color")), icon.getString("Effect"),
icon.getBoolean("Glow", false), icon.contains("CustomModelData") ? icon.getInt("CustomModelData") : null,
Math.max(1, icon.getInt("Amount", 1)), stringSet(icon, "Require"),
icon.getString("Gem"), icon.getString("Gui")));
}
return new GuiDefinition(name, file, codec.color(section.getString("Title", "&8" + name)),
Math.max(1, section.getInt("Page", 1)), rows * 9, layout.toString(), Map.copyOf(icons));
}
private Set<String> stringSet(ConfigurationSection section, String path) {
Object value = section.get(path);
if (value instanceof String text) return text.isBlank() ? Set.of() : Set.of(text);
if (value instanceof Collection<?> collection) {
Set<String> result = new LinkedHashSet<>();
for (Object entry : collection) if (entry != null && !entry.toString().isBlank()) result.add(entry.toString());
return Set.copyOf(result);
}
return Set.of();
}
private IconType parseType(String input) {
try { return IconType.valueOf(input.toUpperCase(Locale.ROOT)); }
catch (IllegalArgumentException exception) { return IconType.TIP; }
}
public Collection<String> names() {
return definitions.values().stream().map(GuiDefinition::name).toList();
}
public boolean open(Player player, String name) {
GuiDefinition definition = name == null || name.isBlank() ? defaultGui() : definitions.get(name.toLowerCase(Locale.ROOT));
if (definition == null) return false;
open(player, definition, 0, null);
return true;
}
private GuiDefinition defaultGui() {
GuiDefinition named = definitions.get("default");
return named != null ? named : definitions.values().stream().findFirst().orElse(null);
}
private void open(Player player, GuiDefinition definition, int page, ItemStack equipment) {
int selectedPage = Math.max(0, Math.min(page, definition.pages() - 1));
WorkbenchHolder holder = new WorkbenchHolder(definition, selectedPage);
Inventory inventory = Bukkit.createInventory(holder, definition.size(),
definition.title().replace("%page%", Integer.toString(selectedPage + 1)));
holder.inventory = inventory;
render(holder, equipment);
player.openInventory(inventory);
}
private void render(WorkbenchHolder holder, ItemStack equipment) {
holder.inventory.clear();
holder.equipSlots.clear();
holder.gemSlots.clear();
holder.visibleGems.clear();
Map<Character, List<GemDefinition>> candidates = new HashMap<>();
Map<Character, Integer> gemIndexes = new HashMap<>();
boolean equipmentPlaced = false;
for (int slot = 0; slot < holder.definition.size(); slot++) {
char symbol = holder.definition.layout().charAt(slot);
GuiIcon icon = holder.definition.icons().get(symbol);
if (icon == null || icon.type() == IconType.EMPTY) continue;
switch (icon.type()) {
case EQUIP_SLOT -> {
holder.equipSlots.add(slot);
if (!equipmentPlaced && equipment != null && !equipment.getType().isAir()) {
holder.inventory.setItem(slot, equipment);
equipmentPlaced = true;
} else holder.inventory.setItem(slot, iconItem(icon));
}
case GEM_SLOT -> {
holder.gemSlots.add(slot);
List<GemDefinition> matching = candidates.computeIfAbsent(symbol,
ignored -> eligibleGems(holder.definition, equipment, icon));
int gemIndex = gemIndexes.computeIfAbsent(symbol,
ignored -> holder.page * countSymbol(holder.definition, symbol));
gemIndexes.put(symbol, gemIndex + 1);
GemDefinition gem = gemIndex < matching.size() ? matching.get(gemIndex) : null;
if (gem != null) {
holder.visibleGems.put(slot, gem.name());
holder.inventory.setItem(slot, codec.createGem(gem, icon.amount()));
} else holder.inventory.setItem(slot, iconItem(icon));
}
case USE_GEM -> holder.inventory.setItem(slot, icon.gem() == null ? iconItem(icon)
: gems.find(icon.gem()).map(gem -> codec.createGem(gem, 1)).orElseGet(() -> iconItem(icon)));
case PAGE_TIP -> holder.inventory.setItem(slot, pageTip(icon, holder.page + 1, holder.definition.pages()));
default -> holder.inventory.setItem(slot, iconItem(icon));
}
}
}
private List<GemDefinition> eligibleGems(GuiDefinition definition, ItemStack equipment, GuiIcon icon) {
if (equipment == null || equipment.getType().isAir()) return List.of();
return gems.all().stream()
.filter(gem -> !gem.giveRandomGem() && service.effectiveType(gem) == GemType.ITEM_GEM)
.filter(gem -> matcher.matches(equipment, gem.requirements()))
.filter(gem -> gem.allowedGuis().isEmpty() || gem.allowedGuis().stream()
.anyMatch(gui -> gui.equalsIgnoreCase(definition.name())))
.filter(gem -> icon.require().isEmpty() || icon.require().stream()
.anyMatch(prefix -> gem.name().startsWith(prefix)))
.toList();
}
private int countSymbol(GuiDefinition definition, char symbol) {
int count = 0;
for (int slot = 0; slot < definition.size(); slot++)
if (definition.layout().charAt(slot) == symbol) count++;
return Math.max(1, count);
}
private ItemStack iconItem(GuiIcon icon) {
try {
return codec.createItem(new ItemCodec.ItemAppearance(icon.material(), icon.amount(), icon.display(), icon.lore(),
icon.texture(), icon.color(), icon.effects(), icon.customModelData(), icon.glow()));
} catch (IllegalArgumentException ignored) {
return codec.createItem(new ItemCodec.ItemAppearance("GRAY_STAINED_GLASS_PANE", icon.amount(),
icon.display(), icon.lore(), null, null, null, icon.customModelData(), icon.glow()));
}
}
private ItemStack pageTip(GuiIcon icon, int page, int pages) {
ItemStack item = iconItem(icon);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(codec.color(icon.display().replace("%page%", Integer.toString(page))
.replace("%pages%", Integer.toString(pages))) + " " + page + '/' + pages);
item.setItemMeta(meta);
return item;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onClick(InventoryClickEvent event) {
if (!(event.getView().getTopInventory().getHolder() instanceof WorkbenchHolder holder)) return;
if (!(event.getWhoClicked() instanceof Player player)) return;
if (event.getRawSlot() >= holder.inventory.getSize()) return;
event.setCancelled(true);
int slot = event.getRawSlot();
GuiIcon icon = holder.definition.icons().get(holder.definition.layout().charAt(slot));
if (icon == null) return;
if (icon.type() == IconType.EQUIP_SLOT) {
ItemStack current = equipment(holder);
ItemStack cursor = event.getCursor();
if (cursor != null && !cursor.getType().isAir() && !icon.require().isEmpty()
&& icon.require().stream().noneMatch(require -> matcher.matches(cursor, require))) {
player.sendMessage(codec.color(languages.text("cmagic.gui.slot_rejected", "&c该物品不能放入这个装备槽。")));
return;
}
for (int equipSlot : holder.equipSlots) holder.inventory.setItem(equipSlot, null);
holder.inventory.setItem(slot, cursor == null || cursor.getType().isAir() ? null : cursor.clone());
event.setCursor(current == null || isPlaceholder(holder, current) ? null : current);
refresh(holder);
return;
}
switch (icon.type()) {
case GEM_SLOT -> gemSlot(player, holder, slot, event);
case USE_GEM -> useGem(player, holder, icon);
case PAGE_PREV -> navigate(player, holder, holder.definition, holder.page - 1);
case PAGE_NEXT -> navigate(player, holder, holder.definition, holder.page + 1);
case PAGE_JUMP -> {
GuiDefinition target = icon.gui() == null ? null : definitions.get(icon.gui().toLowerCase(Locale.ROOT));
if (target != null) navigate(player, holder, target, 0);
}
default -> { }
}
}
private void gemSlot(Player player, WorkbenchHolder holder, int slot, InventoryClickEvent event) {
ItemStack equipment = equipment(holder);
if (equipment == null || equipment.getType().isAir()) {
player.sendMessage(codec.color(languages.text("cmagic.gui.equipment_required", "&c请先放入装备。")));
return;
}
if (event.isRightClick()) {
String gemName = holder.visibleGems.get(slot);
if (gemName == null || codec.embedded(equipment).getOrDefault(gemName, 0) <= 0) return;
GemUseResult result = service.remove(player, gemName, equipment);
if (result.successful() || result.target() != equipment) setEquipment(holder, result.target());
messages.send(player, result.message());
refresh(holder);
return;
}
ItemStack cursor = event.getCursor();
GemDefinition gem = codec.readGem(cursor, true).orElse(null);
if (gem == null || gem.giveRandomGem() || service.effectiveType(gem) != GemType.ITEM_GEM) return;
String visible = holder.visibleGems.get(slot);
if (visible != null && !visible.equalsIgnoreCase(gem.name())) return;
GemUseResult result = service.use(player, gem, equipment);
if (result.consumed()) consumeCursor(event);
if (result.target() != null) setEquipment(holder, result.target());
messages.send(player, result.message());
refresh(holder);
}
private void useGem(Player player, WorkbenchHolder holder, GuiIcon icon) {
GemDefinition gem = gems.find(icon.gem()).orElse(null);
if (gem == null) return;
boolean itemGem = !gem.giveRandomGem() && service.effectiveType(gem) == GemType.ITEM_GEM;
ItemStack target = itemGem ? equipment(holder) : null;
if (itemGem && (target == null || target.getType().isAir())) {
player.sendMessage(codec.color(languages.text("cmagic.gui.equipment_required", "&c请先放入装备。")));
return;
}
GemUseResult result = service.use(player, gem, target);
if (result.target() != null && itemGem) setEquipment(holder, result.target());
messages.send(player, result.message());
refresh(holder);
}
private void consumeCursor(InventoryClickEvent event) {
ItemStack cursor = event.getCursor();
if (cursor.getAmount() <= 1) event.setCursor(null);
else {
ItemStack changed = cursor.clone();
changed.setAmount(cursor.getAmount() - 1);
event.setCursor(changed);
}
}
private void refresh(WorkbenchHolder holder) {
ItemStack equipment = equipment(holder);
render(holder, equipment == null || isPlaceholder(holder, equipment) ? null : equipment);
}
private ItemStack equipment(WorkbenchHolder holder) {
for (int slot : holder.equipSlots) {
ItemStack item = holder.inventory.getItem(slot);
if (item != null && !item.getType().isAir() && !isPlaceholder(holder, item)) return item;
}
return null;
}
private boolean isPlaceholder(WorkbenchHolder holder, ItemStack item) {
for (int slot : holder.equipSlots) {
GuiIcon icon = holder.definition.icons().get(holder.definition.layout().charAt(slot));
if (icon != null && item.isSimilar(iconItem(icon))) return true;
}
return false;
}
private void setEquipment(WorkbenchHolder holder, ItemStack item) {
if (holder.equipSlots.isEmpty()) return;
holder.inventory.setItem(holder.equipSlots.iterator().next(), item);
}
private void navigate(Player player, WorkbenchHolder holder, GuiDefinition target, int page) {
ItemStack equipment = equipment(holder);
holder.returned = true;
for (int slot : holder.equipSlots) holder.inventory.setItem(slot, null);
open(player, target, Math.max(0, Math.min(page, target.pages() - 1)), equipment);
}
@EventHandler(ignoreCancelled = false)
public void onDrag(InventoryDragEvent event) {
if (!(event.getView().getTopInventory().getHolder() instanceof WorkbenchHolder holder)) return;
for (int slot : event.getRawSlots()) if (slot < holder.inventory.getSize()) {
event.setCancelled(true);
return;
}
}
@EventHandler
public void onClose(InventoryCloseEvent event) {
if (!(event.getInventory().getHolder() instanceof WorkbenchHolder holder) || holder.returned) return;
holder.returned = true;
ItemStack equipment = equipment(holder);
if (equipment == null) return;
for (int slot : holder.equipSlots) holder.inventory.setItem(slot, null);
Player player = (Player) event.getPlayer();
player.getInventory().addItem(equipment).values()
.forEach(left -> player.getWorld().dropItemNaturally(player.getLocation(), left));
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onBlockUse(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null) return;
String gui = locations.get(locationKey(event.getClickedBlock().getLocation()));
if (gui == null) return;
event.setCancelled(true);
open(event.getPlayer(), gui);
}
@EventHandler(ignoreCancelled = true)
public void onBlockBreak(BlockBreakEvent event) {
if (locations.containsKey(locationKey(event.getBlock().getLocation()))) event.setCancelled(true);
}
public boolean addBlock(String guiName, Location location) {
GuiDefinition definition = definitions.get(guiName.toLowerCase(Locale.ROOT));
if (definition == null || location.getBlock().getType().isAir()
|| locations.containsKey(locationKey(location))) return false;
return updateLocation(definition, location, true);
}
public boolean deleteBlock(Location location) {
String name = locations.get(locationKey(location));
if (name == null) return false;
GuiDefinition definition = definitions.get(name.toLowerCase(Locale.ROOT));
return definition != null && updateLocation(definition, location, false);
}
private boolean updateLocation(GuiDefinition definition, Location location, boolean add) {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(definition.file());
String path = definition.name() + ".Locations";
List<String> values = new ArrayList<>(yaml.getStringList(path));
String encoded = locationKey(location);
values.removeIf(value -> encoded.equals(normalizeLocation(value)));
if (add) values.add(encoded);
yaml.set(path, values);
try {
yaml.save(definition.file());
reload();
return true;
} catch (IOException exception) {
plugin.getLogger().log(Level.SEVERE, "保存镶嵌台位置失败", exception);
return false;
}
}
private String normalizeLocation(String value) {
if (value == null) return null;
String[] parts = value.trim().split("[,;:]");
if (parts.length < 4) return null;
try { return parts[0] + ',' + Integer.parseInt(parts[1]) + ',' + Integer.parseInt(parts[2]) + ',' + Integer.parseInt(parts[3]); }
catch (NumberFormatException exception) { return null; }
}
private String locationKey(Location location) {
return location.getWorld().getName() + ',' + location.getBlockX() + ',' + location.getBlockY() + ',' + location.getBlockZ();
}
private void closeAll() {
for (Player player : Bukkit.getOnlinePlayers())
if (player.getOpenInventory().getTopInventory().getHolder() instanceof WorkbenchHolder) player.closeInventory();
}
private enum IconType { TIP, EMPTY, EQUIP_SLOT, GEM_SLOT, USE_GEM, PAGE_JUMP, PAGE_PREV, PAGE_NEXT, PAGE_TIP }
private record GuiIcon(IconType type, String material, String display, List<String> lore,
String texture, Integer color, String effects, boolean glow,
Integer customModelData, int amount, Set<String> require, String gem, String gui) { }
private record GuiDefinition(String name, File file, String title, int pages, int size, String layout,
Map<Character, GuiIcon> icons) { }
private static final class WorkbenchHolder implements GemWorkbenchHolder {
private final GuiDefinition definition;
private final int page;
private final Set<Integer> equipSlots = new LinkedHashSet<>();
private final Set<Integer> gemSlots = new LinkedHashSet<>();
private final Map<Integer, String> visibleGems = new HashMap<>();
private Inventory inventory;
private boolean returned;
private WorkbenchHolder(GuiDefinition definition, int page) {
this.definition = definition;
this.page = page;
}
@Override
public Inventory getInventory() {
return inventory;
}
}
}
@@ -0,0 +1,6 @@
package cn.infstar.cmagicgem.gui;
import org.bukkit.inventory.InventoryHolder;
public interface GemWorkbenchHolder extends InventoryHolder {
}
@@ -0,0 +1,60 @@
package cn.infstar.cmagicgem.integration;
import org.bukkit.OfflinePlayer;
import org.bukkit.plugin.RegisteredServiceProvider;
import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.Method;
public final class EconomyBridge {
private final JavaPlugin plugin;
private Object provider;
private Method getBalance;
private Method deposit;
private Method withdraw;
public EconomyBridge(JavaPlugin plugin) {
this.plugin = plugin;
refresh();
}
public void refresh() {
provider = null;
try {
Class<?> economy = Class.forName("net.milkbowl.vault.economy.Economy");
RegisteredServiceProvider<?> registration = plugin.getServer().getServicesManager().getRegistration(economy);
if (registration == null) return;
provider = registration.getProvider();
getBalance = economy.getMethod("getBalance", OfflinePlayer.class);
deposit = economy.getMethod("depositPlayer", OfflinePlayer.class, double.class);
withdraw = economy.getMethod("withdrawPlayer", OfflinePlayer.class, double.class);
} catch (ReflectiveOperationException ignored) {
}
}
public boolean available() {
return provider != null;
}
public String status() {
return available() ? provider.getClass().getName() : "unavailable";
}
public double balance(OfflinePlayer player) {
if (!available()) return 0;
try { return ((Number) getBalance.invoke(provider, player)).doubleValue(); }
catch (ReflectiveOperationException exception) { return 0; }
}
public boolean change(OfflinePlayer player, double amount) {
if (!available()) return false;
if (amount < 0 && balance(player) + amount < 0) return false;
try {
Object response = (amount >= 0 ? deposit : withdraw).invoke(provider, player, Math.abs(amount));
Method successful = response.getClass().getMethod("transactionSuccess");
return (boolean) successful.invoke(response);
} catch (ReflectiveOperationException exception) {
return false;
}
}
}
@@ -0,0 +1,62 @@
package cn.infstar.cmagicgem.integration;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.iface.ReadWriteNBT;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Locale;
public final class MMOItemsBridge {
private final JavaPlugin plugin;
public MMOItemsBridge(JavaPlugin plugin) {
this.plugin = plugin;
}
public ItemStack rebuildIfMMOItem(ItemStack source) {
Plugin dependency = plugin.getServer().getPluginManager().getPlugin("MMOItems");
if (dependency == null || !dependency.isEnabled()) return source;
try {
ClassLoader loader = dependency.getClass().getClassLoader();
Class<?> mmoItems = Class.forName("net.Indyuce.mmoitems.MMOItems", true, loader);
Method getId = mmoItems.getMethod("getID", ItemStack.class);
Object id = getId.invoke(null, source);
if (!(id instanceof String value) || value.isBlank()) return source;
ReadWriteNBT customData = cmagicData(source);
ItemMeta oldMeta = source.getItemMeta();
Class<?> liveType = Class.forName("net.Indyuce.mmoitems.api.item.mmoitem.LiveMMOItem", true, loader);
Constructor<?> constructor = liveType.getConstructor(ItemStack.class);
Object liveItem = constructor.newInstance(source);
Object builder = liveType.getMethod("newBuilder").invoke(liveItem);
ItemStack rebuilt = (ItemStack) builder.getClass().getMethod("build").invoke(builder);
rebuilt.setAmount(source.getAmount());
ItemMeta rebuiltMeta = rebuilt.getItemMeta();
oldMeta.getPersistentDataContainer().copyTo(rebuiltMeta.getPersistentDataContainer(), true);
rebuilt.setItemMeta(rebuiltMeta);
NBT.modify(rebuilt, nbt -> { nbt.mergeCompound(customData); });
return rebuilt;
} catch (ReflectiveOperationException | LinkageError | RuntimeException exception) {
throw new IllegalStateException("MMOItems 物品重建失败: " + exception.getMessage(), exception);
}
}
private ReadWriteNBT cmagicData(ItemStack source) {
return NBT.get(source, nbt -> {
ReadWriteNBT copy = NBT.createNBTObject();
copy.mergeCompound(nbt);
for (String key : new ArrayList<>(copy.getKeys())) {
String normalized = key.toUpperCase(Locale.ROOT);
if (!normalized.startsWith("CMAGICGEM_") && !normalized.equals("MYTHIC_TYPE")) copy.removeKey(key);
}
return copy;
});
}
}
@@ -0,0 +1,185 @@
package cn.infstar.cmagicgem.integration;
import org.bukkit.inventory.ItemStack;
import org.bukkit.event.Event;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.Location;
import cn.infstar.cmagicgem.gem.GemRegistry;
import cn.infstar.cmagicgem.item.ItemCodec;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
public final class MythicBridge {
private final JavaPlugin plugin;
private Object itemManager;
private Method getItemStack;
private Object apiHelper;
private Method castSkill;
private InlineMechanicExecutor inlineExecutor;
private boolean extensionsEnabled;
public MythicBridge(JavaPlugin plugin) {
this.plugin = plugin;
refresh();
}
public void refresh() {
itemManager = null;
getItemStack = null;
apiHelper = null;
castSkill = null;
inlineExecutor = null;
Plugin dependency = plugin.getServer().getPluginManager().getPlugin("MythicMobs");
if (dependency == null || !dependency.isEnabled()) return;
for (String apiName : new String[]{"io.lumine.mythic.bukkit.MythicBukkit", "io.lumine.xikage.mythicmobs.MythicMobs"}) {
try {
Class<?> apiClass = Class.forName(apiName, true, dependency.getClass().getClassLoader());
Object api = apiClass.getMethod("inst").invoke(null);
itemManager = api.getClass().getMethod("getItemManager").invoke(api);
getItemStack = itemManager.getClass().getMethod("getItemStack", String.class);
try {
apiHelper = api.getClass().getMethod("getAPIHelper").invoke(api);
for (Method method : apiHelper.getClass().getMethods()) {
if (!method.getName().equals("castSkill") || method.getParameterCount() < 2) continue;
Class<?>[] parameters = method.getParameterTypes();
if (Entity.class.isAssignableFrom(parameters[0]) && parameters[1] == String.class) {
if (isBetterCastMethod(method, castSkill))
castSkill = method;
}
}
} catch (ReflectiveOperationException ignored) {
}
return;
} catch (ReflectiveOperationException ignored) {
}
}
plugin.getLogger().warning("MythicMobs 已安装,但无法连接物品 API");
}
public boolean available() {
return itemManager != null && getItemStack != null;
}
public boolean skillApiAvailable() {
return apiHelper != null && castSkill != null;
}
public String status() {
if (!available()) return "unavailable";
return "items=" + itemManager.getClass().getName() + ", skillApi=" + skillApiAvailable();
}
public Optional<ItemStack> item(String name) {
if (!available()) return Optional.empty();
try {
Object value = getItemStack.invoke(itemManager, name);
if (value instanceof Optional<?> optional) value = optional.orElse(null);
return value instanceof ItemStack item ? Optional.of(item.clone()) : Optional.empty();
} catch (ReflectiveOperationException exception) {
return Optional.empty();
}
}
public boolean cast(Player caster, String skill, Entity target) {
return cast(caster, skill, target, target == null ? caster.getLocation() : target.getLocation());
}
public boolean cast(Player caster, String skill, Entity target, Location location) {
if (apiHelper == null || castSkill == null) return false;
try {
Object[] arguments = new Object[castSkill.getParameterCount()];
Class<?>[] types = castSkill.getParameterTypes();
arguments[0] = caster;
arguments[1] = skill;
int collectionIndex = 0;
for (int index = 2; index < arguments.length; index++) {
if (Entity.class.isAssignableFrom(types[index])) arguments[index] = target == null ? caster : target;
else if (Location.class.isAssignableFrom(types[index])) arguments[index] = location;
else if (Collection.class.isAssignableFrom(types[index])) {
arguments[index] = collectionIndex++ == 0
? (target == null ? List.of() : List.of(target))
: (location == null ? List.of() : List.of(location));
}
else if (types[index] == float.class || types[index] == Float.class) arguments[index] = 1.0F;
else if (types[index] == double.class || types[index] == Double.class) arguments[index] = 1.0D;
else if (types[index] == boolean.class || types[index] == Boolean.class) arguments[index] = false;
else arguments[index] = null;
}
Object result = castSkill.invoke(apiHelper, arguments);
return !(result instanceof Boolean success) || success;
} catch (ReflectiveOperationException | IllegalArgumentException exception) {
return false;
}
}
public boolean castInline(Player caster, String mechanicLine, Entity target, Location location) {
return inlineExecutor != null && inlineExecutor.castInline(caster, mechanicLine, target, location);
}
public void enableExtensions(GemRegistry gems, ItemCodec items, ProtectionBridge protection,
MythicDeathHandler deathHandler) {
if (extensionsEnabled) return;
Plugin dependency = plugin.getServer().getPluginManager().getPlugin("MythicMobs");
if (dependency == null || !dependency.isEnabled()) return;
String adapter = hasClass(dependency, "io.lumine.mythic.bukkit.MythicBukkit")
? "cn.infstar.cmagicgem.integration.mythic.Mythic5Adapter"
: "cn.infstar.cmagicgem.integration.mythic.Mythic4Adapter";
try {
Class<?> type = Class.forName(adapter, true, plugin.getClass().getClassLoader());
Object instance = type.getConstructor(JavaPlugin.class, GemRegistry.class, ItemCodec.class,
ProtectionBridge.class, MythicDeathHandler.class)
.newInstance(plugin, gems, items, protection, deathHandler);
type.getMethod("register").invoke(instance);
if (instance instanceof InlineMechanicExecutor executor) inlineExecutor = executor;
extensionsEnabled = true;
if (plugin.getConfig().getBoolean("integrations.mythicmobs.reload-on-enable", true)) {
plugin.getServer().getScheduler().runTask(plugin, () -> {
if (!plugin.getServer().getPluginManager().isPluginEnabled("MythicMobs")) return;
boolean dispatched = plugin.getServer().dispatchCommand(
plugin.getServer().getConsoleSender(), "mm reload");
if (!dispatched) plugin.getLogger().warning("无法自动重新加载 MythicMobs 扩展配置,请手动执行 /mm reload");
});
}
} catch (ReflectiveOperationException | LinkageError exception) {
plugin.getLogger().warning("无法加载 MythicMobs 扩展适配器: " + exception.getMessage());
}
}
private boolean hasClass(Plugin dependency, String name) {
try {
Class.forName(name, false, dependency.getClass().getClassLoader());
return true;
} catch (ClassNotFoundException ignored) {
return false;
}
}
private boolean isBetterCastMethod(Method candidate, Method current) {
if (current == null) return true;
boolean candidateHasConsumer = hasConsumer(candidate);
boolean currentHasConsumer = hasConsumer(current);
if (candidateHasConsumer != currentHasConsumer) return !candidateHasConsumer;
return candidate.getParameterCount() > current.getParameterCount();
}
private boolean hasConsumer(Method method) {
for (Class<?> type : method.getParameterTypes())
if (java.util.function.Consumer.class.isAssignableFrom(type)) return true;
return false;
}
@FunctionalInterface
public interface MythicDeathHandler {
void onDeath(Player killer, String mobId, Entity mob, Event event);
}
public interface InlineMechanicExecutor {
boolean castInline(Player caster, String mechanicLine, Entity target, Location location);
}
}
@@ -0,0 +1,46 @@
package cn.infstar.cmagicgem.integration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.Method;
public final class PlaceholderBridge {
private final JavaPlugin plugin;
private Method setPlaceholders;
public PlaceholderBridge(JavaPlugin plugin) {
this.plugin = plugin;
refresh();
}
public void refresh() {
setPlaceholders = null;
Plugin dependency = plugin.getServer().getPluginManager().getPlugin("PlaceholderAPI");
if (dependency == null || !dependency.isEnabled()) return;
try {
Class<?> api = Class.forName("me.clip.placeholderapi.PlaceholderAPI", true, dependency.getClass().getClassLoader());
setPlaceholders = api.getMethod("setPlaceholders", Player.class, String.class);
} catch (ReflectiveOperationException exception) {
plugin.getLogger().warning("PlaceholderAPI 存在但接口不可用: " + exception.getMessage());
}
}
public String resolve(Player player, String input) {
if (setPlaceholders == null || player == null || input == null) return input;
try {
return (String) setPlaceholders.invoke(null, player, input);
} catch (ReflectiveOperationException exception) {
return input;
}
}
public boolean available() {
return setPlaceholders != null;
}
public String status() {
return available() ? setPlaceholders.toGenericString() : "unavailable";
}
}
@@ -0,0 +1,65 @@
package cn.infstar.cmagicgem.integration;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.Method;
import java.util.UUID;
public final class PlayerPointsBridge {
private final JavaPlugin plugin;
private Object api;
private Method look;
private Method give;
private Method take;
public PlayerPointsBridge(JavaPlugin plugin) {
this.plugin = plugin;
refresh();
}
public void refresh() {
api = null;
Plugin dependency = plugin.getServer().getPluginManager().getPlugin("PlayerPoints");
if (dependency == null || !dependency.isEnabled()) return;
try {
api = dependency.getClass().getMethod("getAPI").invoke(dependency);
look = api.getClass().getMethod("look", UUID.class);
give = api.getClass().getMethod("give", UUID.class, int.class);
take = api.getClass().getMethod("take", UUID.class, int.class);
} catch (ReflectiveOperationException exception) {
api = null;
plugin.getLogger().warning("PlayerPoints 存在但接口不可用: " + exception.getMessage());
}
}
public boolean available() {
return api != null;
}
public String status() {
return available() ? api.getClass().getName() : "unavailable";
}
public boolean change(Player player, int amount) {
if (!available()) return false;
try {
int balance = ((Number) look.invoke(api, player.getUniqueId())).intValue();
if (amount < 0 && balance + amount < 0) return false;
Object result = (amount >= 0 ? give : take).invoke(api, player.getUniqueId(), Math.abs(amount));
return !(result instanceof Boolean value) || value;
} catch (ReflectiveOperationException exception) {
return false;
}
}
public int balance(Player player) {
if (!available()) return 0;
try {
return ((Number) look.invoke(api, player.getUniqueId())).intValue();
} catch (ReflectiveOperationException exception) {
return 0;
}
}
}
@@ -0,0 +1,100 @@
package cn.infstar.cmagicgem.integration;
import org.bukkit.Location;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import java.lang.reflect.Method;
public final class ProtectionBridge {
private final JavaPlugin plugin;
public ProtectionBridge(JavaPlugin plugin) {
this.plugin = plugin;
}
public boolean pvpEnabled(Player player) {
Plugin dependency = plugin.getServer().getPluginManager().getPlugin("PvPManager");
if (dependency == null || !dependency.isEnabled()) return false;
try {
Object source = dependency;
for (String accessor : new String[]{"getPlayerHandler", "getPvPPlayerManager", "getPlayerManager"}) {
Method method = find(source.getClass(), accessor);
if (method != null) { source = method.invoke(source); break; }
}
Object pvpPlayer = source;
for (String accessor : new String[]{"get", "getPlayer", "getPvPPlayer"}) {
Method method = find(source.getClass(), accessor, Player.class);
if (method != null) { pvpPlayer = method.invoke(source, player); break; }
}
for (String name : new String[]{"hasPvPEnabled", "isPvPEnabled", "isPvpEnabled", "hasPvpEnabled"}) {
Method method = find(pvpPlayer.getClass(), name);
if (method != null) return (boolean) method.invoke(pvpPlayer);
}
} catch (ReflectiveOperationException | RuntimeException ignored) {
}
return false;
}
public boolean residenceFlag(Player player, Location location, String flag) {
return residenceFlag((Entity) player, location, flag);
}
public boolean residenceFlag(Entity entity, Location location, String flag) {
Plugin dependency = plugin.getServer().getPluginManager().getPlugin("Residence");
if (dependency == null || !dependency.isEnabled()) return false;
try {
Method disabledWorld = find(dependency.getClass(), "isDisabledWorldListener", org.bukkit.World.class);
if (disabledWorld != null && (boolean) disabledWorld.invoke(dependency, location.getWorld())) return true;
Object directPermissions = residencePermissions(dependency, entity, location);
Boolean directResult = permissionValue(directPermissions, entity instanceof Player player ? player : null, flag);
if (directResult != null) return directResult;
Object manager = dependency.getClass().getMethod("getResidenceManager").invoke(dependency);
Object residence = manager.getClass().getMethod("getByLoc", Location.class).invoke(manager, location);
if (residence == null) return true;
Object permissions = residence.getClass().getMethod("getPermissions").invoke(residence);
Boolean result = permissionValue(permissions, entity instanceof Player player ? player : null, flag);
if (result != null) return result;
} catch (ReflectiveOperationException | RuntimeException ignored) {
}
return false;
}
private Object residencePermissions(Plugin dependency, Entity entity, Location location)
throws ReflectiveOperationException {
if (entity instanceof Player player) {
Method method = find(dependency.getClass(), "getPermsByLocForPlayer", Location.class, Player.class);
if (method != null) return method.invoke(dependency, location, player);
}
Method method = find(dependency.getClass(), "getPermsByLoc", Location.class);
return method == null ? null : method.invoke(dependency, location);
}
private Boolean permissionValue(Object permissions, Player player, String flag) throws ReflectiveOperationException {
if (permissions == null) return null;
for (Method method : permissions.getClass().getMethods()) {
if (player == null && !method.getName().equals("has")) continue;
if (player != null && !method.getName().equals("playerHas") && !method.getName().equals("has")) continue;
Object[] arguments = new Object[method.getParameterCount()];
Class<?>[] types = method.getParameterTypes();
boolean compatible = true;
for (int index = 0; index < arguments.length; index++) {
if (Player.class.isAssignableFrom(types[index]) && player != null) arguments[index] = player;
else if (types[index] == String.class) arguments[index] = flag;
else if (types[index] == boolean.class || types[index] == Boolean.class) arguments[index] = true;
else { compatible = false; break; }
}
if (!compatible) continue;
Object result = method.invoke(permissions, arguments);
if (result instanceof Boolean allowed) return allowed;
}
return null;
}
private Method find(Class<?> type, String name, Class<?>... parameters) {
try { return type.getMethod(name, parameters); }
catch (NoSuchMethodException ignored) { return null; }
}
}
@@ -0,0 +1,189 @@
package cn.infstar.cmagicgem.integration.mythic;
import cn.infstar.cmagicgem.gem.GemDefinition;
import cn.infstar.cmagicgem.gem.GemRegistry;
import cn.infstar.cmagicgem.integration.MythicBridge.MythicDeathHandler;
import cn.infstar.cmagicgem.integration.MythicBridge.InlineMechanicExecutor;
import cn.infstar.cmagicgem.integration.ProtectionBridge;
import cn.infstar.cmagicgem.item.ItemCodec;
import io.lumine.xikage.mythicmobs.adapters.AbstractEntity;
import io.lumine.xikage.mythicmobs.adapters.AbstractItemStack;
import io.lumine.xikage.mythicmobs.adapters.bukkit.BukkitAdapter;
import io.lumine.xikage.mythicmobs.api.bukkit.events.MythicConditionLoadEvent;
import io.lumine.xikage.mythicmobs.api.bukkit.events.MythicDropLoadEvent;
import io.lumine.xikage.mythicmobs.api.bukkit.events.MythicMechanicLoadEvent;
import io.lumine.xikage.mythicmobs.api.bukkit.events.MythicMobDeathEvent;
import io.lumine.xikage.mythicmobs.drops.Drop;
import io.lumine.xikage.mythicmobs.drops.DropMetadata;
import io.lumine.xikage.mythicmobs.drops.IItemDrop;
import io.lumine.xikage.mythicmobs.io.MythicLineConfig;
import io.lumine.xikage.mythicmobs.skills.ITargetedEntitySkill;
import io.lumine.xikage.mythicmobs.skills.Skill;
import io.lumine.xikage.mythicmobs.skills.SkillCaster;
import io.lumine.xikage.mythicmobs.skills.SkillCondition;
import io.lumine.xikage.mythicmobs.skills.SkillMechanic;
import io.lumine.xikage.mythicmobs.skills.SkillMetadata;
import io.lumine.xikage.mythicmobs.skills.SkillTrigger;
import io.lumine.xikage.mythicmobs.skills.conditions.IEntityCondition;
import io.lumine.xikage.mythicmobs.skills.conditions.ICasterCondition;
import io.lumine.xikage.mythicmobs.mobs.GenericCaster;
import org.bukkit.Location;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class Mythic4Adapter implements Listener, InlineMechanicExecutor {
private final JavaPlugin plugin;
private final GemRegistry gems;
private final ItemCodec items;
private final ProtectionBridge protection;
private final MythicDeathHandler deathHandler;
private final Map<String, Skill> inlineSkills = new ConcurrentHashMap<>();
public Mythic4Adapter(JavaPlugin plugin, GemRegistry gems, ItemCodec items, ProtectionBridge protection,
MythicDeathHandler deathHandler) {
this.plugin = plugin;
this.gems = gems;
this.items = items;
this.protection = protection;
this.deathHandler = deathHandler;
}
public void register() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean castInline(Player caster, String mechanicLine, Entity target, Location location) {
if (mechanicLine == null || mechanicLine.isBlank()) return false;
try {
Skill skill = inlineSkills.computeIfAbsent(mechanicLine, line -> new Skill(List.of(line)));
AbstractEntity casterEntity = BukkitAdapter.adapt(caster);
AbstractEntity trigger = BukkitAdapter.adapt(target == null ? caster : target);
SkillCaster skillCaster = new GenericCaster(casterEntity);
HashSet<AbstractEntity> entities = new HashSet<>();
if (target != null) entities.add(BukkitAdapter.adapt(target));
HashSet<io.lumine.xikage.mythicmobs.adapters.AbstractLocation> locations = new HashSet<>();
Location origin = location == null ? caster.getLocation() : location;
if (location != null) locations.add(BukkitAdapter.adapt(location));
SkillMetadata metadata = new SkillMetadata(SkillTrigger.API, skillCaster, trigger,
BukkitAdapter.adapt(origin), entities, locations, 1.0F);
if (!skill.usable(metadata, SkillTrigger.API)) return false;
skill.execute(metadata);
return true;
} catch (RuntimeException | LinkageError exception) {
plugin.getLogger().warning("无法执行 MythicMobs 4 内联机制 " + mechanicLine + ": " + exception.getMessage());
return false;
}
}
@EventHandler
public void onDropLoad(MythicDropLoadEvent event) {
if (!event.getDropName().equalsIgnoreCase("gem")) return;
MythicLineConfig config = event.getConfig();
String name = config.getString(new String[]{"name", "n"}, "");
GemDefinition gem = gems.find(name).orElse(null);
if (gem != null) event.register(new GemDrop(config.getLine(), config, gem, items));
}
@EventHandler
public void onMechanicLoad(MythicMechanicLoadEvent event) {
String name = normalize(event.getMechanicName());
if (name.equals("shielddamage") || name.equals("sheilddamage"))
event.register(new ShieldDamage(event.getConfig().getLine(), event.getConfig()));
}
@EventHandler
public void onConditionLoad(MythicConditionLoadEvent event) {
String name = normalize(event.getConditionName());
switch (name) {
case "noarmorstand" -> event.register(new NoArmorStand(event.getConfig().getLine()));
case "pvpmanager" -> event.register(new Pvp(event.getConfig().getLine(), protection));
case "residenceflag", "redidenseflag" -> event.register(new Residence(event.getConfig().getLine(), event.getConfig(), protection));
default -> { }
}
}
@EventHandler
public void onMobDeath(MythicMobDeathEvent event) {
if (event.getKiller() instanceof Player player)
deathHandler.onDeath(player, event.getMobType().getInternalName(), event.getEntity(), event);
}
private static String normalize(String input) {
return input.toLowerCase(Locale.ROOT).replace("_", "").replace("-", "");
}
private static final class GemDrop extends Drop implements IItemDrop {
private final GemDefinition gem;
private final ItemCodec items;
private GemDrop(String line, MythicLineConfig config, GemDefinition gem, ItemCodec items) {
super(line, config);
this.gem = gem;
this.items = items;
}
@Override
public AbstractItemStack getDrop(DropMetadata metadata) {
return BukkitAdapter.adapt(items.createGem(gem, Math.max(1, (int) Math.round(getAmount()))));
}
}
private static final class ShieldDamage extends SkillMechanic implements ITargetedEntitySkill {
private final double damage;
private ShieldDamage(String line, MythicLineConfig config) {
super(line, config);
damage = config.getDouble(new String[]{"damage", "d", "amount"}, 0);
}
@Override
public boolean castAtEntity(SkillMetadata metadata, AbstractEntity target) {
Entity entity = BukkitAdapter.adapt(target);
if (!(entity instanceof LivingEntity living)) return false;
living.setAbsorptionAmount(Math.max(0, living.getAbsorptionAmount() - Math.max(0, damage)));
return true;
}
}
private static final class NoArmorStand extends SkillCondition implements IEntityCondition {
private NoArmorStand(String line) { super(line); }
@Override public boolean check(AbstractEntity target) { return !(BukkitAdapter.adapt(target) instanceof ArmorStand); }
}
private static final class Pvp extends SkillCondition implements IEntityCondition, ICasterCondition {
private final ProtectionBridge protection;
private Pvp(String line, ProtectionBridge protection) { super(line); this.protection = protection; }
@Override public boolean check(AbstractEntity target) {
Entity entity = BukkitAdapter.adapt(target);
return entity instanceof Player player && protection.pvpEnabled(player);
}
@Override public boolean check(SkillCaster caster) { return check(caster.getEntity()); }
}
private static final class Residence extends SkillCondition implements IEntityCondition, ICasterCondition {
private final ProtectionBridge protection;
private final String flag;
private Residence(String line, MythicLineConfig config, ProtectionBridge protection) {
super(line);
this.protection = protection;
flag = config.getString(new String[]{"flag", "f"}, "");
}
@Override public boolean check(AbstractEntity target) {
Entity entity = BukkitAdapter.adapt(target);
return protection.residenceFlag(entity, entity.getLocation(), flag);
}
@Override public boolean check(SkillCaster caster) { return check(caster.getEntity()); }
}
}
@@ -0,0 +1,177 @@
package cn.infstar.cmagicgem.integration.mythic;
import cn.infstar.cmagicgem.gem.GemDefinition;
import cn.infstar.cmagicgem.gem.GemRegistry;
import cn.infstar.cmagicgem.integration.MythicBridge.MythicDeathHandler;
import cn.infstar.cmagicgem.integration.MythicBridge.InlineMechanicExecutor;
import cn.infstar.cmagicgem.integration.ProtectionBridge;
import cn.infstar.cmagicgem.item.ItemCodec;
import io.lumine.mythic.api.adapters.AbstractEntity;
import io.lumine.mythic.api.adapters.AbstractItemStack;
import io.lumine.mythic.api.config.MythicLineConfig;
import io.lumine.mythic.api.drops.DropMetadata;
import io.lumine.mythic.api.drops.IItemDrop;
import io.lumine.mythic.api.skills.ITargetedEntitySkill;
import io.lumine.mythic.api.skills.Skill;
import io.lumine.mythic.api.skills.SkillCaster;
import io.lumine.mythic.api.skills.SkillMetadata;
import io.lumine.mythic.api.skills.SkillResult;
import io.lumine.mythic.api.skills.conditions.IEntityCondition;
import io.lumine.mythic.api.skills.conditions.ICasterCondition;
import io.lumine.mythic.bukkit.BukkitAdapter;
import io.lumine.mythic.bukkit.MythicBukkit;
import io.lumine.mythic.bukkit.events.MythicConditionLoadEvent;
import io.lumine.mythic.bukkit.events.MythicDropLoadEvent;
import io.lumine.mythic.bukkit.events.MythicMechanicLoadEvent;
import io.lumine.mythic.bukkit.events.MythicMobDeathEvent;
import io.lumine.mythic.core.skills.SkillMetadataImpl;
import io.lumine.mythic.core.skills.SkillTriggers;
import org.bukkit.Location;
import org.bukkit.entity.ArmorStand;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.inventory.ItemStack;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class Mythic5Adapter implements Listener, InlineMechanicExecutor {
private final JavaPlugin plugin;
private final GemRegistry gems;
private final ItemCodec items;
private final ProtectionBridge protection;
private final MythicDeathHandler deathHandler;
private final Map<String, Skill> inlineSkills = new ConcurrentHashMap<>();
public Mythic5Adapter(JavaPlugin plugin, GemRegistry gems, ItemCodec items, ProtectionBridge protection,
MythicDeathHandler deathHandler) {
this.plugin = plugin;
this.gems = gems;
this.items = items;
this.protection = protection;
this.deathHandler = deathHandler;
}
public void register() {
plugin.getServer().getPluginManager().registerEvents(this, plugin);
}
@Override
public boolean castInline(Player caster, String mechanicLine, Entity target, Location location) {
if (mechanicLine == null || mechanicLine.isBlank()) return false;
try {
var manager = MythicBukkit.inst().getSkillManager();
Skill skill = inlineSkills.computeIfAbsent(mechanicLine,
line -> manager.getSkill(null, List.of(line)).orElse(null));
if (skill == null) return false;
AbstractEntity casterEntity = BukkitAdapter.adapt(caster);
AbstractEntity trigger = BukkitAdapter.adapt(target == null ? caster : target);
SkillCaster skillCaster = manager.getCaster(casterEntity);
List<AbstractEntity> entities = new ArrayList<>();
if (target != null) entities.add(BukkitAdapter.adapt(target));
List<io.lumine.mythic.api.adapters.AbstractLocation> locations = new ArrayList<>();
Location origin = location == null ? caster.getLocation() : location;
if (location != null) locations.add(BukkitAdapter.adapt(location));
SkillMetadata metadata = new SkillMetadataImpl(SkillTriggers.API, skillCaster, trigger,
BukkitAdapter.adapt(origin), entities, locations, 1.0F);
if (!skill.isUsable(metadata, SkillTriggers.API)) return false;
skill.execute(metadata);
return true;
} catch (RuntimeException | LinkageError exception) {
plugin.getLogger().warning("无法执行 MythicMobs 5 内联机制 " + mechanicLine + ": " + exception.getMessage());
return false;
}
}
@EventHandler
public void onDropLoad(MythicDropLoadEvent event) {
if (!event.getDropName().equalsIgnoreCase("gem")) return;
String name = event.getConfig().getString(new String[]{"name", "n"}, "");
GemDefinition gem = gems.find(name).orElse(null);
if (gem != null) event.register(new GemDrop(gem, items));
}
@EventHandler
public void onMechanicLoad(MythicMechanicLoadEvent event) {
String name = normalize(event.getMechanicName());
if (!name.equals("shielddamage") && !name.equals("sheilddamage")) return;
double damage = event.getConfig().getDouble(new String[]{"damage", "d", "amount"}, 0);
event.register(new ShieldDamage(damage));
}
@EventHandler
public void onConditionLoad(MythicConditionLoadEvent event) {
String name = normalize(event.getConditionName());
switch (name) {
case "noarmorstand" -> event.register((IEntityCondition) target ->
!(BukkitAdapter.adapt(target) instanceof ArmorStand));
case "pvpmanager" -> event.register(new Pvp(protection));
case "residenceflag", "redidenseflag" -> {
String flag = event.getConfig().getString(new String[]{"flag", "f"}, "");
event.register(new Residence(protection, flag));
}
default -> { }
}
}
@EventHandler
public void onMobDeath(MythicMobDeathEvent event) {
if (event.getKiller() instanceof Player player)
deathHandler.onDeath(player, event.getMobType().getInternalName(), event.getEntity(), event);
}
private static String normalize(String input) {
return input.toLowerCase(Locale.ROOT).replace("_", "").replace("-", "");
}
private record GemDrop(GemDefinition gem, ItemCodec items) implements IItemDrop {
@Override
public AbstractItemStack getDrop(DropMetadata metadata, double amount) {
ItemStack item = items.createGem(gem, Math.max(1, (int) Math.round(amount)));
return BukkitAdapter.adapt(item);
}
}
private record ShieldDamage(double damage) implements ITargetedEntitySkill {
@Override
public SkillResult castAtEntity(SkillMetadata metadata, AbstractEntity target) {
Entity entity = BukkitAdapter.adapt(target);
if (!(entity instanceof LivingEntity living)) return SkillResult.INVALID_TARGET;
living.setAbsorptionAmount(Math.max(0, living.getAbsorptionAmount() - Math.max(0, damage)));
return SkillResult.SUCCESS;
}
}
private record Pvp(ProtectionBridge protection) implements IEntityCondition, ICasterCondition {
@Override
public boolean check(AbstractEntity target) {
Entity entity = BukkitAdapter.adapt(target);
return entity instanceof Player player && protection.pvpEnabled(player);
}
@Override
public boolean check(SkillCaster caster) {
return check(caster.getEntity());
}
}
private record Residence(ProtectionBridge protection, String flag) implements IEntityCondition, ICasterCondition {
@Override
public boolean check(AbstractEntity target) {
Entity entity = BukkitAdapter.adapt(target);
return protection.residenceFlag(entity, entity.getLocation(), flag);
}
@Override
public boolean check(SkillCaster caster) {
return check(caster.getEntity());
}
}
}
@@ -0,0 +1,230 @@
package cn.infstar.cmagicgem.item;
import cn.infstar.cmagicgem.gem.GemDefinition;
import cn.infstar.cmagicgem.gem.GemRegistry;
import cn.infstar.cmagicgem.migration.LegacyItemMigrator;
import cn.infstar.cmagicgem.util.PotionTypes;
import cn.infstar.cmagicgem.util.MaterialResolver;
import org.bukkit.ChatColor;
import org.bukkit.Bukkit;
import org.bukkit.Color;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import com.destroystokyo.paper.profile.ProfileProperty;
import org.bukkit.enchantments.Enchantment;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.PotionMeta;
import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.persistence.PersistentDataContainer;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.java.JavaPlugin;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
public final class ItemCodec {
private final GemRegistry registry;
private final NamespacedKey gemNameKey;
private final NamespacedKey dataVersionKey;
private final NamespacedKey embeddedKey;
private final NamespacedKey skillsKey;
private final LegacyItemMigrator legacyMigrator;
public ItemCodec(JavaPlugin plugin, GemRegistry registry) {
this.registry = registry;
this.gemNameKey = new NamespacedKey(plugin, "gem_name");
this.dataVersionKey = new NamespacedKey(plugin, "data_version");
this.embeddedKey = new NamespacedKey(plugin, "embedded_gems");
this.skillsKey = new NamespacedKey(plugin, "skills");
this.legacyMigrator = new LegacyItemMigrator(registry);
}
public ItemStack createGem(GemDefinition gem, int amount) {
ItemStack item = createItem(new ItemAppearance(gem.material().name(), amount, gem.display(), gem.tips(),
gem.texture(), gem.color(), gem.effects(), gem.customModelData(), gem.glow()));
ItemMeta meta = item.getItemMeta();
meta.getPersistentDataContainer().set(gemNameKey, PersistentDataType.STRING, gem.name());
meta.getPersistentDataContainer().set(dataVersionKey, PersistentDataType.INTEGER, 1);
item.setItemMeta(meta);
return item;
}
public ItemStack createItem(ItemAppearance appearance) {
String materialName = appearance.material() == null || appearance.material().isBlank()
? appearance.texture() == null || appearance.texture().isBlank() ? "STONE" : "PLAYER_HEAD"
: appearance.material();
Material material = MaterialResolver.resolve(materialName);
if (material == null) throw new IllegalArgumentException("未知材料: " + materialName);
int amount = Math.max(1, Math.min(appearance.amount(), material.getMaxStackSize()));
ItemStack item = new ItemStack(material, amount);
ItemMeta meta = item.getItemMeta();
if (appearance.display() != null && !appearance.display().isBlank())
meta.setDisplayName(color(appearance.display()));
if (appearance.lore() != null && !appearance.lore().isEmpty())
meta.setLore(appearance.lore().stream().map(this::color).toList());
if (appearance.customModelData() != null) meta.setCustomModelData(appearance.customModelData());
if (meta instanceof SkullMeta skull && appearance.texture() != null && !appearance.texture().isBlank()) {
var profile = Bukkit.createProfile(UUID.randomUUID());
profile.setProperty(new ProfileProperty("textures", appearance.texture()));
skull.setPlayerProfile(profile);
}
if (meta instanceof PotionMeta potion) {
if (appearance.color() != null) potion.setColor(Color.fromRGB(appearance.color() & 0xFFFFFF));
addPotionEffects(potion, appearance.effects());
}
if (appearance.glow()) {
meta.addEnchant(Enchantment.UNBREAKING, 1, true);
meta.addItemFlags(ItemFlag.HIDE_ENCHANTS);
}
item.setItemMeta(meta);
return item;
}
private void addPotionEffects(PotionMeta potion, String effects) {
if (effects == null || effects.isBlank()) return;
for (String encoded : effects.split(";")) {
String[] parts = encoded.trim().split(":");
if (parts.length == 0) continue;
PotionEffectType type = PotionTypes.resolve(parts[0]);
if (type == null) continue;
int duration = parts.length > 1 ? parseInt(parts[1], 1) * 20 : 20;
int level = parts.length > 2 ? parseInt(parts[2], 1) : 1;
potion.addCustomEffect(new PotionEffect(type, duration, Math.max(0, level - 1)), true);
}
}
public static Integer parseColor(Object value) {
if (value instanceof Number number) return number.intValue();
if (value == null || value.toString().isBlank()) return null;
return switch (value.toString().toUpperCase()) {
case "WHITE" -> 0xFFFFFF; case "SILVER" -> 0xC0C0C0; case "GRAY" -> 0x808080;
case "BLACK" -> 0x000000; case "RED" -> 0xFF0000; case "MAROON" -> 0x800000;
case "YELLOW" -> 0xFFFF00; case "OLIVE" -> 0x808000; case "LIME" -> 0x00FF00;
case "GREEN" -> 0x008000; case "AQUA" -> 0x00FFFF; case "TEAL" -> 0x008080;
case "BLUE" -> 0x0000FF; case "NAVY" -> 0x000080; case "FUCHSIA" -> 0xFF00FF;
case "PURPLE" -> 0x800080; case "ORANGE" -> 0xFFA500; case "GOLD" -> 0xFFD700;
default -> {
try { yield Integer.decode(value.toString()); } catch (NumberFormatException ignored) { yield null; }
}
};
}
public record ItemAppearance(String material, int amount, String display, List<String> lore,
String texture, Integer color, String effects,
Integer customModelData, boolean glow) { }
private int parseInt(String value, int fallback) {
try { return Integer.parseInt(value); } catch (NumberFormatException ignored) { return fallback; }
}
public Optional<GemDefinition> readGem(ItemStack item, boolean allowLore) {
if (item == null || item.getType().isAir() || !item.hasItemMeta()) return Optional.empty();
ItemMeta meta = item.getItemMeta();
String name = meta.getPersistentDataContainer().get(gemNameKey, PersistentDataType.STRING);
Optional<GemDefinition> direct = registry.find(name);
if (direct.isPresent()) return direct;
LegacyItemMigrator.LegacyData legacy = legacyMigrator.inspect(item);
if (legacy.identity() != null) {
Optional<GemDefinition> migrated = registry.find(legacy.identity());
if (migrated.isPresent()) {
meta.getPersistentDataContainer().set(gemNameKey, PersistentDataType.STRING, migrated.get().name());
meta.getPersistentDataContainer().set(dataVersionKey, PersistentDataType.INTEGER, 1);
item.setItemMeta(meta);
return migrated;
}
}
if (!allowLore || !meta.hasLore()) return Optional.empty();
List<String> lore = meta.getLore();
if (lore == null) return Optional.empty();
for (GemDefinition gem : registry.all()) {
if (gem.loreIdentity() == null) continue;
String expected = strip(gem.loreIdentity());
if (lore.stream().map(this::strip).anyMatch(line -> line.contains(expected))) return Optional.of(gem);
}
return Optional.empty();
}
public Map<String, Integer> embedded(ItemStack item) {
if (item == null || !item.hasItemMeta()) return Map.of();
String encoded = item.getItemMeta().getPersistentDataContainer().get(embeddedKey, PersistentDataType.STRING);
Map<String, Integer> result = new LinkedHashMap<>();
if (encoded == null || encoded.isBlank()) {
LegacyItemMigrator.LegacyData legacy = legacyMigrator.inspect(item);
if (!legacy.embedded().isEmpty()) {
writeEmbedded(item, legacy.embedded());
return new LinkedHashMap<>(legacy.embedded());
}
return result;
}
for (String line : encoded.split("\\n")) {
int separator = line.lastIndexOf('=');
if (separator < 0) continue;
try {
result.put(URLDecoder.decode(line.substring(0, separator), StandardCharsets.UTF_8),
Integer.parseInt(line.substring(separator + 1)));
} catch (IllegalArgumentException ignored) {
}
}
return result;
}
public ItemStack setEmbedded(ItemStack item, Map<String, Integer> gems) {
ItemStack result = item.clone();
writeEmbedded(result, gems);
return result;
}
private void writeEmbedded(ItemStack item, Map<String, Integer> gems) {
ItemMeta meta = item.getItemMeta();
StringBuilder encoded = new StringBuilder();
gems.forEach((name, count) -> encoded.append(URLEncoder.encode(name, StandardCharsets.UTF_8))
.append('=').append(count).append('\n'));
meta.getPersistentDataContainer().set(embeddedKey, PersistentDataType.STRING, encoded.toString());
meta.getPersistentDataContainer().set(dataVersionKey, PersistentDataType.INTEGER, 1);
item.setItemMeta(meta);
}
public List<String> skills(ItemStack item) {
if (item == null || !item.hasItemMeta()) return new ArrayList<>();
String value = item.getItemMeta().getPersistentDataContainer().get(skillsKey, PersistentDataType.STRING);
if (value == null || value.isBlank()) {
List<String> legacy = new ArrayList<>(legacyMigrator.inspect(item).skills());
if (!legacy.isEmpty()) writeSkills(item, legacy);
return legacy;
}
return new ArrayList<>(List.of(value.split("\\n")));
}
public ItemStack setSkills(ItemStack item, List<String> skills) {
ItemStack result = item.clone();
writeSkills(result, skills);
return result;
}
private void writeSkills(ItemStack item, List<String> skills) {
ItemMeta meta = item.getItemMeta();
PersistentDataContainer data = meta.getPersistentDataContainer();
data.set(skillsKey, PersistentDataType.STRING, String.join("\n", skills));
item.setItemMeta(meta);
}
public String color(String value) {
return ChatColor.translateAlternateColorCodes('&', value == null ? "" : value);
}
public String strip(String value) {
return ChatColor.stripColor(color(value));
}
}
@@ -0,0 +1,81 @@
package cn.infstar.cmagicgem.item;
import de.tr7zw.changeme.nbtapi.NBT;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.Locale;
import java.util.Set;
public final class ItemMatcher {
private final ItemCodec codec;
public ItemMatcher(ItemCodec codec) {
this.codec = codec;
}
public boolean matches(ItemStack item, Set<String> requirements) {
if (item == null || item.getType().isAir()) return false;
if (requirements.isEmpty()) return true;
for (String requirement : requirements) {
if (matches(item, requirement)) return true;
}
return false;
}
public boolean matches(ItemStack item, String rawRequirement) {
if (item == null || item.getType().isAir()) return false;
String requirement = rawRequirement.toUpperCase(Locale.ROOT);
Material material = item.getType();
if (requirement.equals("NOTHING")) return false;
if (requirement.equals("ALL") || requirement.equals("ANY")) return true;
if (requirement.startsWith("LORE:")) return loreContains(item, rawRequirement.substring(5));
if (requirement.startsWith("NAME:")) {
ItemMeta meta = item.getItemMeta();
return meta.hasDisplayName() && strip(meta.getDisplayName()).contains(strip(rawRequirement.substring(5)));
}
if (requirement.startsWith("GEM:")) {
String expected = rawRequirement.substring(4);
return codec.embedded(item).entrySet().stream().anyMatch(entry -> entry.getValue() > 0
&& (expected.equalsIgnoreCase("ALL") || expected.equalsIgnoreCase("ANY")
|| entry.getKey().equalsIgnoreCase(expected)));
}
if (requirement.startsWith("NBT:")) {
String[] parts = rawRequirement.substring(4).split(":", 2);
return NBT.get(item, nbt -> {
if (!nbt.hasTag(parts[0])) return false;
return parts.length == 1 || nbt.getString(parts[0]).equals(parts[1]);
});
}
if (material.name().equals(requirement) || material.name().endsWith('_' + requirement)) return true;
return switch (requirement) {
case "ARMOR" -> material.name().endsWith("_HELMET") || material.name().endsWith("_CHESTPLATE")
|| material.name().endsWith("_LEGGINGS") || material.name().endsWith("_BOOTS")
|| material == Material.ELYTRA;
case "WEAPON" -> material.name().endsWith("_SWORD") || material.name().endsWith("_AXE")
|| material == Material.BOW || material == Material.CROSSBOW || material == Material.TRIDENT
|| material == Material.MACE;
case "TOOL" -> material.name().endsWith("_PICKAXE") || material.name().endsWith("_AXE")
|| material.name().endsWith("_SHOVEL") || material.name().endsWith("_HOE")
|| material == Material.SHEARS || material == Material.FISHING_ROD;
case "HELMET", "HEAD" -> material.name().endsWith("_HELMET");
case "CHEST", "CHESTPLATE" -> material.name().endsWith("_CHESTPLATE") || material == Material.ELYTRA;
case "LEGS", "LEGGINGS" -> material.name().endsWith("_LEGGINGS");
case "FEET", "BOOTS" -> material.name().endsWith("_BOOTS");
default -> false;
};
}
private boolean loreContains(ItemStack item, String expected) {
ItemMeta meta = item.getItemMeta();
if (!meta.hasLore() || meta.getLore() == null) return false;
String clean = strip(expected);
return meta.getLore().stream().map(this::strip).anyMatch(line -> line.contains(clean));
}
private String strip(String value) {
return ChatColor.stripColor(ChatColor.translateAlternateColorCodes('&', value == null ? "" : value));
}
}
@@ -0,0 +1,154 @@
package cn.infstar.cmagicgem.listener;
import cn.infstar.cmagicgem.config.PluginSettings;
import cn.infstar.cmagicgem.gem.GemDefinition;
import cn.infstar.cmagicgem.gem.GemService;
import cn.infstar.cmagicgem.gem.GemType;
import cn.infstar.cmagicgem.gem.GemUseResult;
import cn.infstar.cmagicgem.item.ItemCodec;
import cn.infstar.cmagicgem.gui.GemWorkbenchHolder;
import cn.infstar.cmagicgem.util.MessageDispatcher;
import org.bukkit.Material;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.event.inventory.PrepareItemCraftEvent;
import org.bukkit.event.inventory.InventoryType;
import org.bukkit.event.inventory.FurnaceBurnEvent;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemConsumeEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.PlayerInventory;
import org.bukkit.inventory.ItemStack;
import java.util.function.Supplier;
public final class GemUseListener implements Listener {
private final GemService gemService;
private final ItemCodec itemCodec;
private final Supplier<PluginSettings> settings;
private final MessageDispatcher messages;
public GemUseListener(GemService gemService, ItemCodec itemCodec, MessageDispatcher messages,
Supplier<PluginSettings> settings) {
this.gemService = gemService;
this.itemCodec = itemCodec;
this.settings = settings;
this.messages = messages;
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onInventoryClick(InventoryClickEvent event) {
if (event.getView().getTopInventory().getHolder() instanceof GemWorkbenchHolder) return;
boolean backpack = event.getClickedInventory() == event.getWhoClicked().getInventory();
boolean workbench = !backpack && event.getView().getTopInventory().getType() == InventoryType.WORKBENCH;
if (backpack && !settings.get().backpack() || workbench && !settings.get().workbench() || !backpack && !workbench) return;
ItemStack cursor = event.getCursor();
ItemStack target = event.getCurrentItem();
GemDefinition gem = itemCodec.readGem(cursor, settings.get().gemLore()).orElse(null);
if (gem == null || gem.giveRandomGem() || gemService.effectiveType(gem) != GemType.ITEM_GEM
|| target == null || target.getType().isAir()) return;
String gui = backpack ? "backpack" : "workbench";
if (!gem.allowedGuis().isEmpty() && gem.allowedGuis().stream().noneMatch(value -> value.equalsIgnoreCase(gui))) return;
event.setCancelled(true);
GemUseResult result = gemService.use((org.bukkit.entity.Player) event.getWhoClicked(), gem, target);
if (result.consumed()) consumeOne(cursor, event);
if (result.target() != null) event.setCurrentItem(result.target());
messages.send(event.getWhoClicked(), result.message());
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onInteract(PlayerInteractEvent event) {
if (event.getAction() != Action.RIGHT_CLICK_AIR && event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
ItemStack used = event.getItem();
GemDefinition gem = itemCodec.readGem(used, settings.get().gemLore()).orElse(null);
if (gem == null || gem.eat() && gem.type() == GemType.PLAYER_GEM) return;
GemType effectiveType = gemService.effectiveType(gem);
boolean giveaway = gem.type() == GemType.RANDOM_GEM && gem.giveRandomGem();
if (!giveaway && effectiveType == GemType.ITEM_GEM && gem.fastUse() == null) {
event.setCancelled(true);
return;
}
if (gem.shift() && !event.getPlayer().isSneaking()) return;
event.setCancelled(true);
EquipmentSlot targetSlot = !giveaway && effectiveType == GemType.ITEM_GEM ? slot(gem.fastUse()) : null;
ItemStack target = targetSlot == null ? null : get(event.getPlayer().getInventory(), targetSlot);
GemUseResult result = gemService.use(event.getPlayer(), gem, target);
if (result.consumed() && used != null) {
used.setAmount(used.getAmount() - 1);
if (result.target() != null && targetSlot != null) set(event.getPlayer().getInventory(), targetSlot, result.target());
}
messages.send(event.getPlayer(), result.message());
}
@EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true)
public void onConsume(PlayerItemConsumeEvent event) {
GemDefinition gem = itemCodec.readGem(event.getItem(), settings.get().gemLore()).orElse(null);
if (gem == null || gem.type() != GemType.PLAYER_GEM || !gem.eat()) return;
if (gem.shift() && !event.getPlayer().isSneaking()) {
event.setCancelled(true);
return;
}
event.setCancelled(true);
GemUseResult result = gemService.use(event.getPlayer(), gem, null);
if (result.consumed()) {
ItemStack held = event.getPlayer().getInventory().getItem(event.getHand());
held.setAmount(held.getAmount() - 1);
}
messages.send(event.getPlayer(), result.message());
}
private EquipmentSlot slot(String value) {
if (value == null) return EquipmentSlot.OFF_HAND;
return switch (value.toLowerCase(java.util.Locale.ROOT)) {
case "head", "helmet" -> EquipmentSlot.HEAD;
case "chest", "chestplate" -> EquipmentSlot.CHEST;
case "legs", "leggings" -> EquipmentSlot.LEGS;
case "feet", "boots" -> EquipmentSlot.FEET;
case "mainhand", "hand" -> EquipmentSlot.HAND;
default -> EquipmentSlot.OFF_HAND;
};
}
private ItemStack get(PlayerInventory inventory, EquipmentSlot slot) {
return inventory.getItem(slot);
}
private void set(PlayerInventory inventory, EquipmentSlot slot, ItemStack item) {
inventory.setItem(slot, item);
}
@EventHandler
public void onCraft(PrepareItemCraftEvent event) {
if (settings.get().gemCraft()) return;
for (ItemStack item : event.getInventory().getMatrix()) {
if (itemCodec.readGem(item, settings.get().gemLore()).isPresent()) {
event.getInventory().setResult(new ItemStack(Material.AIR));
return;
}
}
}
@EventHandler(ignoreCancelled = true)
public void onDrop(PlayerDropItemEvent event) {
if (itemCodec.readGem(event.getItemDrop().getItemStack(), settings.get().gemLore()).isPresent())
event.setCancelled(true);
}
@EventHandler(ignoreCancelled = true)
public void onFuel(FurnaceBurnEvent event) {
if (itemCodec.readGem(event.getFuel(), settings.get().gemLore()).isPresent()) event.setCancelled(true);
}
private void consumeOne(ItemStack cursor, InventoryClickEvent event) {
if (cursor.getAmount() <= 1) event.setCursor(null);
else {
ItemStack reduced = cursor.clone();
reduced.setAmount(cursor.getAmount() - 1);
event.setCursor(reduced);
}
}
}
@@ -0,0 +1,82 @@
package cn.infstar.cmagicgem.migration;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.logging.Level;
public final class LegacyDirectoryImporter {
private static final List<String> DATA_DIRECTORIES = List.of("gems", "skills", "gui", "languages");
private final JavaPlugin plugin;
public LegacyDirectoryImporter(JavaPlugin plugin) {
this.plugin = plugin;
}
public ImportResult importIfPresent() {
Path pluginsDirectory = plugin.getDataFolder().toPath().toAbsolutePath().getParent();
if (pluginsDirectory == null) return new ImportResult(0, false);
Path legacy = pluginsDirectory.resolve("MagicGem");
Path target = plugin.getDataFolder().toPath();
if (!Files.isDirectory(legacy) || legacy.equals(target)) return new ImportResult(0, false);
int copied = 0;
try {
Files.createDirectories(target);
for (String directory : DATA_DIRECTORIES) copied += copyDirectory(legacy.resolve(directory), target.resolve(directory));
boolean configImported = importConfig(legacy.resolve("config.yml"), target.resolve("config.yml"));
plugin.getLogger().info("发现旧 MagicGem 数据目录,安全导入 " + copied + " 个文件。原目录保持不变。");
return new ImportResult(copied, configImported);
} catch (IOException exception) {
plugin.getLogger().log(Level.SEVERE, "导入旧 MagicGem 数据失败,未删除任何旧文件", exception);
return new ImportResult(copied, false);
}
}
private int copyDirectory(Path source, Path target) throws IOException {
if (!Files.isDirectory(source)) return 0;
int copied = 0;
try (var paths = Files.walk(source)) {
for (Path path : paths.toList()) {
Path relative = source.relativize(path);
Path destination = target.resolve(relative).normalize();
if (!destination.startsWith(target.normalize())) continue;
if (Files.isDirectory(path)) Files.createDirectories(destination);
else if (!Files.exists(destination)) {
Files.createDirectories(destination.getParent());
Files.copy(path, destination, StandardCopyOption.COPY_ATTRIBUTES);
copied++;
}
}
}
return copied;
}
private boolean importConfig(Path source, Path destination) throws IOException {
if (!Files.isRegularFile(source)) return false;
YamlConfiguration oldConfig = YamlConfiguration.loadConfiguration(source.toFile());
YamlConfiguration newConfig = YamlConfiguration.loadConfiguration(destination.toFile());
boolean changed = false;
for (String key : List.of("lang", "workbench", "backpack", "gem-lore", "gem-craft", "skill-engine",
"skill-engine-lore-color", "potion-global-limit")) {
if (oldConfig.contains(key) && !newConfig.contains(key)) {
newConfig.set(key, oldConfig.get(key));
changed = true;
}
}
if (oldConfig.contains("lang") && !newConfig.contains("language")) {
newConfig.set("language", oldConfig.getString("lang", "zh_CN"));
changed = true;
}
if (changed) newConfig.save(destination.toFile());
return changed;
}
public record ImportResult(int copiedFiles, boolean configImported) {
}
}
@@ -0,0 +1,136 @@
package cn.infstar.cmagicgem.migration;
import cn.infstar.cmagicgem.gem.GemDefinition;
import cn.infstar.cmagicgem.gem.GemRegistry;
import de.tr7zw.changeme.nbtapi.NBT;
import de.tr7zw.changeme.nbtapi.NBTType;
import de.tr7zw.changeme.nbtapi.iface.ReadableNBT;
import org.bukkit.inventory.ItemStack;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class LegacyItemMigrator {
public static final String GEM_NAME_KEY = "MAGICGEM_NAME";
public static final String EMBEDDED_GEMS_KEY = "MAGICGEM_EMBED";
public static final String SKILLS_KEY = "MAGICGEM_SKILL";
private final GemRegistry registry;
public LegacyItemMigrator(GemRegistry registry) {
this.registry = registry;
}
public LegacyData inspect(ItemStack item) {
if (item == null || item.getType().isAir()) return LegacyData.EMPTY;
try {
return NBT.get(item, nbt -> {
Collector collector = new Collector();
readExact(nbt, collector);
scan(nbt, collector, 0);
return collector.result();
});
} catch (RuntimeException ignored) {
return LegacyData.EMPTY;
}
}
private void readExact(ReadableNBT nbt, Collector collector) {
if (nbt.hasTag(GEM_NAME_KEY) && nbt.getType(GEM_NAME_KEY) == NBTType.NBTTagString) {
registry.find(nbt.getString(GEM_NAME_KEY)).ifPresent(gem -> collector.identity = gem.name());
}
if (nbt.hasTag(EMBEDDED_GEMS_KEY) && nbt.getType(EMBEDDED_GEMS_KEY) == NBTType.NBTTagCompound) {
ReadableNBT embedded = nbt.getCompound(EMBEDDED_GEMS_KEY);
if (embedded != null) for (String name : embedded.getKeys()) {
NBTType type = embedded.getType(name);
if (!numeric(type)) continue;
registry.find(name).ifPresent(gem -> collector.embedded.put(gem.name(), number(embedded, name, type)));
}
}
if (nbt.hasTag(SKILLS_KEY) && nbt.getType(SKILLS_KEY) == NBTType.NBTTagList) {
var skills = nbt.getStringList(SKILLS_KEY);
for (int index = 0; index < skills.size(); index++) collector.skills.add(skills.get(index));
}
}
private void scan(ReadableNBT nbt, Collector collector, int depth) {
if (nbt == null || depth > 12) return;
for (String key : nbt.getKeys()) {
if (key.equalsIgnoreCase(GEM_NAME_KEY) || key.equalsIgnoreCase(EMBEDDED_GEMS_KEY)
|| key.equalsIgnoreCase(SKILLS_KEY)) continue;
NBTType type = nbt.getType(key);
try {
if (type == NBTType.NBTTagString) {
String value = nbt.getString(key);
registry.find(value).ifPresent(gem -> {
if (isIdentityKey(key)) collector.identity = gem.name();
else collector.embedded.merge(gem.name(), 1, Integer::sum);
});
if (isSkillKey(key) && !value.isBlank()) {
for (String skill : value.split("[,;\\n]")) if (!skill.isBlank()) collector.skills.add(skill.trim());
}
} else if (type == NBTType.NBTTagCompound) {
ReadableNBT child = nbt.getCompound(key);
registry.find(key).ifPresent(gem -> collector.embedded.merge(gem.name(), numericCount(child), Integer::sum));
scan(child, collector, depth + 1);
} else if (type == NBTType.NBTTagList && isSkillKey(key)) {
var list = nbt.getStringList(key);
for (int index = 0; index < list.size(); index++) collector.skills.add(list.get(index));
} else if (numeric(type)) {
registry.find(key).ifPresent(gem -> collector.embedded.merge(gem.name(), number(nbt, key, type), Integer::sum));
}
} catch (RuntimeException ignored) {
}
}
}
private int numericCount(ReadableNBT nbt) {
if (nbt == null) return 1;
for (String name : Set.of("amount", "count", "level", "value")) {
if (nbt.hasTag(name)) return Math.max(1, nbt.getInteger(name));
}
return 1;
}
private int number(ReadableNBT nbt, String key, NBTType type) {
if (type == NBTType.NBTTagByte) return Math.max(1, nbt.getByte(key));
if (type == NBTType.NBTTagShort) return Math.max(1, nbt.getShort(key));
if (type == NBTType.NBTTagLong) return (int) Math.max(1, nbt.getLong(key));
return Math.max(1, nbt.getInteger(key));
}
private boolean numeric(NBTType type) {
return type == NBTType.NBTTagByte || type == NBTType.NBTTagShort || type == NBTType.NBTTagInt || type == NBTType.NBTTagLong;
}
private boolean isIdentityKey(String key) {
String normalized = key.toLowerCase(Locale.ROOT);
return key.equalsIgnoreCase("MAGICGEM_NAME")
|| normalized.contains("gem") && (normalized.contains("name") || normalized.contains("id") || normalized.equals("gem"));
}
private boolean isSkillKey(String key) {
return key.toLowerCase(Locale.ROOT).contains("skill");
}
private final class Collector {
private String identity;
private final Map<String, Integer> embedded = new LinkedHashMap<>();
private final Set<String> skills = new LinkedHashSet<>();
private LegacyData result() {
return new LegacyData(identity, Map.copyOf(embedded), List.copyOf(skills));
}
}
public record LegacyData(String identity, Map<String, Integer> embedded, List<String> skills) {
public static final LegacyData EMPTY = new LegacyData(null, Map.of(), List.of());
public boolean found() {
return identity != null || !embedded.isEmpty() || !skills.isEmpty();
}
}
}
@@ -0,0 +1,48 @@
package cn.infstar.cmagicgem.reward;
import cn.infstar.cmagicgem.gem.GemDefinition;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.function.DoubleSupplier;
public final class RewardContext {
private final Player player;
private final GemDefinition gem;
private ItemStack item;
private final boolean removal;
private final Map<Object, Double> evaluatedNumbers = new IdentityHashMap<>();
public RewardContext(Player player, GemDefinition gem, ItemStack item, boolean removal) {
this.player = player;
this.gem = gem;
this.item = item;
this.removal = removal;
}
public Player player() {
return player;
}
public GemDefinition gem() {
return gem;
}
public ItemStack item() {
return item;
}
public void item(ItemStack item) {
this.item = item;
}
public boolean removal() {
return removal;
}
public double evaluatedNumber(Object key, DoubleSupplier supplier) {
return evaluatedNumbers.computeIfAbsent(key, ignored -> supplier.getAsDouble());
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
package cn.infstar.cmagicgem.reward;
public final class RewardException extends Exception {
public RewardException(String message) {
super(message);
}
public RewardException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,10 @@
package cn.infstar.cmagicgem.skill;
import cn.infstar.cmagicgem.dsl.Directive;
import java.util.List;
import java.util.Map;
public record SkillAction(Directive mechanic, String mechanicLine, TriggerType trigger, List<String> triggerFilters, String target,
Map<String, Integer> worldLimits) {
}
@@ -0,0 +1,13 @@
package cn.infstar.cmagicgem.skill;
import cn.infstar.cmagicgem.dsl.Directive;
import java.util.List;
import java.util.Set;
public record SkillDefinition(String name, String lore, String itemName, String mythicType, Set<String> slots,
String success, double cooldown, double duration, String cooldownTip,
String successTip, String failTip,
List<SkillAction> actions, List<Directive> conditions,
List<Directive> triggerConditions, List<Directive> targetConditions) {
}
@@ -0,0 +1,868 @@
package cn.infstar.cmagicgem.skill;
import cn.infstar.cmagicgem.api.event.CMagicEquipmentChangeEvent;
import cn.infstar.cmagicgem.api.event.CMagicJumpEvent;
import cn.infstar.cmagicgem.api.event.CMagicPotionBuffEvent;
import cn.infstar.cmagicgem.api.event.CMagicSkillUseEvent;
import cn.infstar.cmagicgem.condition.ConditionEngine;
import cn.infstar.cmagicgem.dsl.Directive;
import cn.infstar.cmagicgem.dsl.DirectiveParser;
import cn.infstar.cmagicgem.expression.DynamicExpression;
import cn.infstar.cmagicgem.item.ItemCodec;
import cn.infstar.cmagicgem.integration.MythicBridge;
import cn.infstar.cmagicgem.integration.ProtectionBridge;
import cn.infstar.cmagicgem.reward.RewardContext;
import cn.infstar.cmagicgem.reward.RewardEngine;
import cn.infstar.cmagicgem.reward.RewardException;
import cn.infstar.cmagicgem.util.MessageDispatcher;
import cn.infstar.cmagicgem.util.PotionTypes;
import de.tr7zw.changeme.nbtapi.NBT;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.entity.Entity;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;
import org.bukkit.entity.Projectile;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.block.Action;
import org.bukkit.event.block.BlockBreakEvent;
import org.bukkit.event.block.BlockPlaceEvent;
import org.bukkit.event.entity.EntityDamageByEntityEvent;
import org.bukkit.event.entity.EntityDamageEvent;
import org.bukkit.event.entity.EntityDeathEvent;
import org.bukkit.event.entity.EntityRegainHealthEvent;
import org.bukkit.event.entity.EntityShootBowEvent;
import org.bukkit.event.entity.EntityTameEvent;
import org.bukkit.event.entity.EntityTeleportEvent;
import org.bukkit.event.entity.LingeringPotionSplashEvent;
import org.bukkit.event.entity.PlayerDeathEvent;
import org.bukkit.event.entity.PotionSplashEvent;
import org.bukkit.event.entity.ProjectileHitEvent;
import org.bukkit.event.entity.ProjectileLaunchEvent;
import org.bukkit.event.inventory.InventoryClickEvent;
import com.destroystokyo.paper.event.player.PlayerArmorChangeEvent;
import com.destroystokyo.paper.event.player.PlayerJumpEvent;
import org.bukkit.event.player.PlayerAnimationEvent;
import org.bukkit.event.player.PlayerAnimationType;
import org.bukkit.event.player.PlayerDropItemEvent;
import org.bukkit.event.player.PlayerFishEvent;
import org.bukkit.event.player.PlayerInteractAtEntityEvent;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.event.player.PlayerItemConsumeEvent;
import org.bukkit.event.player.PlayerItemBreakEvent;
import org.bukkit.event.player.PlayerItemHeldEvent;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.event.player.PlayerRespawnEvent;
import org.bukkit.event.player.PlayerTeleportEvent;
import org.bukkit.event.player.PlayerToggleSneakEvent;
import org.bukkit.event.player.PlayerToggleFlightEvent;
import org.bukkit.event.player.PlayerSwapHandItemsEvent;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.persistence.PersistentDataType;
import org.bukkit.plugin.java.JavaPlugin;
import org.bukkit.potion.PotionEffect;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.projectiles.ProjectileSource;
import org.bukkit.util.Vector;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public final class SkillEngine implements Listener {
private final JavaPlugin plugin;
private final SkillRegistry registry;
private final ItemCodec itemCodec;
private final DirectiveParser parser;
private final DynamicExpression expressions;
private final ConditionEngine conditions;
private final RewardEngine rewards;
private final MythicBridge mythic;
private final ProtectionBridge protection;
private final MessageDispatcher messages;
private final Map<UUID, Map<String, Long>> cooldowns = new ConcurrentHashMap<>();
private final Map<UUID, Map<String, Long>> durations = new ConcurrentHashMap<>();
private final Map<String, Long> debuffs = new ConcurrentHashMap<>();
private final Map<String, CounterState> counters = new ConcurrentHashMap<>();
private long ticks;
public SkillEngine(JavaPlugin plugin, SkillRegistry registry, ItemCodec itemCodec, DirectiveParser parser,
DynamicExpression expressions, ConditionEngine conditions, RewardEngine rewards,
MythicBridge mythic, ProtectionBridge protection, MessageDispatcher messages) {
this.plugin = plugin;
this.registry = registry;
this.itemCodec = itemCodec;
this.parser = parser;
this.expressions = expressions;
this.conditions = conditions;
this.rewards = rewards;
this.mythic = mythic;
this.protection = protection;
this.messages = messages;
Bukkit.getScheduler().runTaskTimer(plugin, this::timer, 1, 1);
}
public void invoke(Player player, String skillName) {
SkillDefinition skill = registry.find(skillName).orElse(null);
if (skill == null) return;
ItemStack item = player.getInventory().getItemInMainHand();
execute(player, item, "mainhand", skill, TriggerType.DEFAULT, List.of(), null,
new EventContext(null, player.getLocation()));
}
public void signal(Player player, String signal) {
trigger(player, TriggerType.SIGNAL, List.of(signal), null, null, null);
}
public void mythicKill(Player player, String mobId, Entity mob, Event event) {
trigger(player, TriggerType.KILL_MYTHICMOBS, List.of(mobId), mob, mob.getLocation(), event);
}
private void timer() {
ticks++;
if (ticks % 1200 == 0) {
long now = System.currentTimeMillis();
debuffs.entrySet().removeIf(entry -> entry.getValue() <= now);
counters.entrySet().removeIf(entry -> now - entry.getValue().lastUsed() > 1_800_000L);
}
for (Player player : Bukkit.getOnlinePlayers())
trigger(player, TriggerType.TIMER, List.of(Long.toString(ticks)), null, null, null);
}
private void trigger(Player player, TriggerType trigger, List<String> information,
Entity target, Location targetLocation, Event event) {
if (debuffed(player, "SKILL")) return;
for (EquippedItem equipped : equipment(player)) {
for (SkillDefinition skill : skills(equipped.item(), equipped.slot()))
execute(player, equipped.item(), equipped.slot(), skill, trigger, information, target,
new EventContext(event, targetLocation));
}
}
private void triggerItem(Player player, ItemStack item, String slot, TriggerType trigger,
List<String> information, Entity target, Location targetLocation, Event event) {
if (debuffed(player, "SKILL")) return;
for (SkillDefinition skill : skills(item, slot))
execute(player, item, slot, skill, trigger, information, target, new EventContext(event, targetLocation));
}
private void execute(Player player, ItemStack item, String slot, SkillDefinition skill, TriggerType trigger,
List<String> information, Entity target, EventContext eventContext) {
List<SkillAction> actions = skill.actions().stream()
.filter(action -> action.trigger() == trigger)
.filter(action -> filter(action, information, trigger))
.toList();
if (actions.isEmpty()) return;
if (!checkConditions(skill.conditions(), player, player, item)) return;
if (!checkConditions(skill.triggerConditions(), player, player, item)) return;
if (!checkConditions(skill.targetConditions(), player, target, item)) return;
long now = System.currentTimeMillis();
String stateKey = skill.name().toLowerCase(Locale.ROOT);
boolean timed = skill.duration() > 0;
boolean active = durations.computeIfAbsent(player.getUniqueId(), ignored -> new ConcurrentHashMap<>())
.getOrDefault(stateKey, 0L) > now;
long ready = cooldowns.computeIfAbsent(player.getUniqueId(), ignored -> new ConcurrentHashMap<>())
.getOrDefault(stateKey, 0L);
if (!active && ready > now) {
messages.cooldown(player, skill.cooldownTip(), (ready - now) / 1000.0, skill.cooldown());
return;
}
if (!roll(skill.success(), player, item)) {
tip(player, skill.failTip());
return;
}
CMagicSkillUseEvent useEvent = new CMagicSkillUseEvent(player, skill, item);
Bukkit.getPluginManager().callEvent(useEvent);
if (useEvent.isCancelled()) return;
boolean executed = false;
for (SkillAction action : actions) {
String type = normalize(action.mechanic().type());
if (timed && !active && !type.equals("timestart")) continue;
if (timed && active && type.equals("timestart")) continue;
try {
mechanic(player, item, slot, skill, action, target, eventContext);
executed = true;
} catch (RewardException | RuntimeException exception) {
plugin.getLogger().warning("技能 " + skill.name() + " 执行失败: " + exception.getMessage());
}
}
if (executed && !active && skill.cooldown() > 0)
cooldowns.get(player.getUniqueId()).put(stateKey, now + (long) (skill.cooldown() * 1000));
if (executed) tip(player, skill.successTip());
}
private void mechanic(Player player, ItemStack item, String slot, SkillDefinition skill, SkillAction action,
Entity target, EventContext context) throws RewardException {
Directive mechanic = action.mechanic();
switch (normalize(mechanic.type())) {
case "reward" -> {
ItemStack changed = applyReward(player, item, mechanic, false);
if (changed != item) replaceMatchingItem(player, item, changed);
}
case "rewardswitch" -> { }
case "switch" -> switchSkill(player, item, skill, mechanic);
case "potionbuff" -> potionBuff(player, item, mechanic, action.target(), target, action.worldLimits());
case "blink" -> blink(player, item, mechanic, action.target(), target, context.location());
case "timestart" -> timeStart(player, item, skill, mechanic);
case "timeadd" -> timeAdd(player, item, skill, mechanic);
case "debuff" -> debuff(player, item, mechanic, action.target(), target);
case "bloodsuck", "bl​​oodsuck", "blooksuck" -> bloodSuck(player, item, mechanic, context.event());
case "shielddamage" -> shieldDamage(player, item, mechanic, action.target(), target);
case "damage" -> damage(player, item, mechanic, action.target(), target);
case "counter" -> counter(player, item, slot, mechanic, target, context);
case "cancelevent" -> { if (context.event() instanceof Cancellable cancellable) cancellable.setCancelled(true); }
default -> {
Location location = context.location() != null ? context.location()
: target == null ? player.getLocation() : target.getLocation();
if (!mythic.castInline(player, action.mechanicLine(), target, location)
&& !mythic.cast(player, mechanic.type(), target, location))
plugin.getLogger().warning("未知技能机制或 MythicMobs 技能: " + mechanic.type());
}
}
}
private ItemStack applyReward(Player player, ItemStack item, Directive mechanic, boolean removal)
throws RewardException {
String nestedSource = mechanic.positional().isEmpty() ? mechanic.argument("", "reward", "r")
: mechanic.positional().getFirst();
Directive nested = parser.parse(nestedSource);
RewardContext rewardContext = new RewardContext(player, null, item, removal);
rewards.applyDirective(rewardContext, nested);
return rewardContext.item();
}
private void switchSkill(Player player, ItemStack item, SkillDefinition source, Directive mechanic)
throws RewardException {
List<String> cycle = new ArrayList<>();
String first = mechanic.argument("", "skills", "list", "s");
if (!first.isBlank()) cycle.addAll(parser.splitTopLevel(first, ';'));
cycle.addAll(mechanic.positional());
if (cycle.isEmpty()) return;
List<String> current = new ArrayList<>(itemCodec.skills(item));
int selected = -1;
for (int index = 0; index < cycle.size(); index++) if (current.contains(cycle.get(index))) { selected = index; break; }
String previous = selected < 0 ? null : cycle.get(selected);
String next = cycle.get((selected + 1) % cycle.size());
ItemStack changed = item;
if (previous != null) {
SkillDefinition oldSkill = registry.find(previous).orElse(null);
if (oldSkill != null) for (SkillAction action : oldSkill.actions())
if (normalize(action.mechanic().type()).equals("rewardswitch"))
changed = applyReward(player, changed, action.mechanic(), true);
current.remove(previous);
}
if (!current.contains(next)) current.add(next);
changed = itemCodec.setSkills(changed, current);
SkillDefinition newSkill = registry.find(next).orElse(null);
if (newSkill != null) for (SkillAction action : newSkill.actions())
if (normalize(action.mechanic().type()).equals("rewardswitch"))
changed = applyReward(player, changed, action.mechanic(), false);
replaceMatchingItem(player, item, changed);
}
private void potionBuff(Player player, ItemStack item, Directive mechanic, String selector, Entity target,
Map<String, Integer> worldLimits)
throws RewardException {
LivingEntity living = selectedLiving(player, selector, target);
if (living == null) return;
if (debuffed(living, "POTIONBUFF")) return;
String name = mechanic.argument("", "type", "potion", "p", "name").toLowerCase(Locale.ROOT);
PotionEffectType type = PotionTypes.resolve(name);
if (type == null) throw new RewardException("未知药水效果: " + name);
double levelValue = mechanic.booleanArgument(false, "roman")
? expressions.evaluateRoman(mechanic.argument("1", "level", "l"), player, item, Map.of())
: value(mechanic.argument("1", "level", "l"), player, item);
int level = Math.max(1, (int) Math.round(levelValue));
String world = living.getWorld().getName().toLowerCase(Locale.ROOT);
Integer actionLimit = worldLimits.get(world);
String globalPath = "potion-global-limit." + living.getWorld().getName();
Integer globalLimit = plugin.getConfig().contains(globalPath) ? plugin.getConfig().getInt(globalPath) : null;
if (actionLimit != null) level = Math.min(level, Math.max(0, actionLimit));
if (globalLimit != null) level = Math.min(level, Math.max(0, globalLimit));
if (level <= 0) return;
int duration = Math.max(1, (int) Math.round(value(mechanic.argument("1", "duration", "d"), player, item) * 20));
int refresh = Math.max(0, (int) Math.round(value(mechanic.argument("0", "refresh"), player, item) * 20));
PotionEffect old = living.getPotionEffect(type);
if (old != null && (old.getAmplifier() > level - 1 || old.getDuration() > refresh)) return;
CMagicPotionBuffEvent event = new CMagicPotionBuffEvent(player, living, type, level - 1, duration);
Bukkit.getPluginManager().callEvent(event);
if (!event.isCancelled()) living.addPotionEffect(new PotionEffect(type, event.getDurationTicks(),
event.getAmplifier(), true, true, true), true);
}
private void blink(Player player, ItemStack item, Directive mechanic, String selector, Entity target,
Location eventLocation) {
double distance = Math.max(0, value(mechanic.argument("5", "distance", "d"), player, item));
Location destination = switch (selector.toLowerCase(Locale.ROOT)) {
case "entity", "target", "targetentity" -> target == null ? null : target.getLocation();
case "location" -> eventLocation;
case "bed" -> player.getBedSpawnLocation();
case "up" -> player.getLocation().add(0, distance, 0);
case "down" -> player.getLocation().add(0, -distance, 0);
case "left" -> directional(player, distance, -90);
case "right" -> directional(player, distance, 90);
case "backward", "back" -> directional(player, distance, 180);
default -> directional(player, distance, 0);
};
if (destination == null) return;
Location safe = safeDestination(player.getLocation(), destination, distance);
if (safe != null) player.teleport(safe);
}
private Location directional(Player player, double distance, double yawOffset) {
Location origin = player.getLocation();
Location facing = origin.clone();
facing.setYaw(facing.getYaw() + (float) yawOffset);
Vector direction = facing.getDirection().normalize();
return origin.clone().add(direction.multiply(distance));
}
private Location safeDestination(Location origin, Location requested, double limit) {
Vector delta = requested.toVector().subtract(origin.toVector());
if (limit > 0 && delta.length() > limit) requested = origin.clone().add(delta.normalize().multiply(limit));
for (int step = 0; step < 4; step++) {
Location candidate = requested.clone().add(0, step, 0);
if (candidate.getBlock().isPassable() && candidate.clone().add(0, 1, 0).getBlock().isPassable()) return candidate;
}
return null;
}
private void timeStart(Player player, ItemStack item, SkillDefinition skill, Directive mechanic) {
durations.computeIfAbsent(player.getUniqueId(), ignored -> new ConcurrentHashMap<>())
.put(skill.name().toLowerCase(Locale.ROOT), System.currentTimeMillis() + (long) (skill.duration() * 1000));
tip(player, mechanic.argument("", "tip"));
}
private void timeAdd(Player player, ItemStack item, SkillDefinition skill, Directive mechanic) {
long addition = (long) (value(mechanic.argument("0", "time", "duration"), player, item) * 1000);
durations.computeIfAbsent(player.getUniqueId(), ignored -> new ConcurrentHashMap<>())
.merge(skill.name().toLowerCase(Locale.ROOT), addition, Long::sum);
tip(player, mechanic.argument("", "tip"));
}
private void debuff(Player player, ItemStack item, Directive mechanic, String selector, Entity target) {
Entity selected = selector.equalsIgnoreCase("self") ? player : target;
if (selected == null) return;
String type = mechanic.argument("SKILL", "type").toUpperCase(Locale.ROOT);
long end = System.currentTimeMillis() + (long) (value(mechanic.argument("1", "duration", "d"), player, item) * 1000);
debuffs.put(selected.getUniqueId() + ":" + type, end);
}
private void bloodSuck(Player player, ItemStack item, Directive mechanic, Event event) {
if (!(event instanceof EntityDamageEvent damage)) return;
double factor = value(mechanic.argument("0", "factor", "f", "k"), player, item);
player.setHealth(Math.min(player.getMaxHealth(), player.getHealth() + Math.max(0, damage.getFinalDamage() * factor)));
}
private void shieldDamage(Player player, ItemStack item, Directive mechanic, String selector, Entity target) {
LivingEntity living = selectedLiving(player, selector, target);
if (living == null) return;
double damage = Math.max(0, value(mechanic.argument("0", "damage", "amount", "d"), player, item));
living.setAbsorptionAmount(Math.max(0, living.getAbsorptionAmount() - damage));
}
private void damage(Player player, ItemStack item, Directive mechanic, String selector, Entity target) {
LivingEntity living = selectedLiving(player, selector, target);
if (living == null) return;
living.damage(Math.max(0, value(mechanic.argument("0", "damage", "amount", "d"), player, item)), player);
}
private void counter(Player player, ItemStack item, String slot, Directive mechanic, Entity target,
EventContext context) {
String tag = mechanic.argument("default", "tag");
long count;
if (mechanic.booleanArgument(false, "persist", "p")) {
String key = "cmagicgem_counter_" + Integer.toHexString(tag.hashCode());
count = NBT.get(item, nbt -> { return nbt.hasTag(key) ? nbt.getLong(key) + 1 : 1L; });
long stored = count;
NBT.modify(item, nbt -> { nbt.setLong(key, stored); });
} else {
String itemId = NBT.get(item, nbt -> nbt.hasTag("CMAGICGEM_RUNTIME_ID")
? nbt.getString("CMAGICGEM_RUNTIME_ID") : "");
if (itemId.isBlank()) {
itemId = UUID.randomUUID().toString();
String generated = itemId;
NBT.modify(item, nbt -> { nbt.setString("CMAGICGEM_RUNTIME_ID", generated); });
}
String key = player.getUniqueId() + ":" + itemId + ':' + slot + ':'
+ Integer.toUnsignedString(mechanic.source().hashCode(), 36) + ':' + tag;
long now = System.currentTimeMillis();
CounterState state = counters.compute(key, (ignored, current) ->
new CounterState(current == null ? 1 : current.count() + 1, now));
count = state.count();
}
double dynamic = value(mechanic.argument("0", "dynamic", "d"), player, item);
if (!expressions.test(mechanic.argument("true", "condition", "c"), player, item,
Map.of("v", (double) count, "d", dynamic))) return;
List<String> information = List.of(tag);
if (mechanic.booleanArgument(false, "allslot"))
trigger(player, TriggerType.COUNTER, information, target, context.location(), context.event());
else triggerItem(player, item, slot, TriggerType.COUNTER, information, target, context.location(), context.event());
}
private boolean filter(SkillAction action, List<String> information, TriggerType trigger) {
if (action.triggerFilters().isEmpty()) return true;
if (trigger == TriggerType.TIMER) {
try {
long interval = Math.max(1, Long.parseLong(action.triggerFilters().getFirst()));
return ticks % interval == 0;
} catch (NumberFormatException ignored) { return false; }
}
for (String expected : action.triggerFilters())
for (String actual : information)
if (actual.equalsIgnoreCase(expected)) return true;
return false;
}
private boolean checkConditions(List<Directive> checks, Player caster, Entity subject, ItemStack item) {
for (Directive check : checks) {
String type = normalize(check.type());
boolean passed = switch (type) {
case "noarmorstand" -> subject == null || subject.getType() != org.bukkit.entity.EntityType.ARMOR_STAND;
case "pvpmanager" -> subject instanceof Player player && protection.pvpEnabled(player);
case "residenceflag", "redidenseflag" -> protection.residenceFlag(caster,
subject == null ? caster.getLocation() : subject.getLocation(), check.argument("", "flag", "f"));
default -> conditions.check(check, subject instanceof Player player ? player : caster, item).success();
};
if (!passed) return false;
}
return true;
}
private Collection<SkillDefinition> skills(ItemStack item, String slot) {
if (item == null || item.getType().isAir()) return List.of();
Set<String> names = new LinkedHashSet<>(itemCodec.skills(item));
String display = item.hasItemMeta() && item.getItemMeta().hasDisplayName() ? strip(item.getItemMeta().getDisplayName()) : "";
List<String> lore = item.hasItemMeta() && item.getItemMeta().hasLore() && item.getItemMeta().getLore() != null
? item.getItemMeta().getLore().stream().map(this::strip).toList() : List.of();
String mythicType = NBT.get(item, nbt -> { return nbt.hasTag("MYTHIC_TYPE") ? nbt.getString("MYTHIC_TYPE") : ""; });
for (SkillDefinition skill : registry.all()) {
if (skill.lore() != null && lore.stream().anyMatch(line -> line.startsWith(strip(skill.lore())))) names.add(skill.name());
if (skill.itemName() != null && display.equals(strip(skill.itemName()))) names.add(skill.name());
if (skill.mythicType() != null && skill.mythicType().equalsIgnoreCase(mythicType)) names.add(skill.name());
}
List<SkillDefinition> found = new ArrayList<>();
for (String name : names) registry.find(name).ifPresent(skill -> {
if (skill.slots().isEmpty() || skill.slots().stream().anyMatch(value -> slotMatches(value, slot))) found.add(skill);
});
return found;
}
private List<EquippedItem> equipment(Player player) {
return List.of(new EquippedItem(player.getInventory().getItemInMainHand(), "mainhand"),
new EquippedItem(player.getInventory().getItemInOffHand(), "offhand"),
new EquippedItem(player.getInventory().getHelmet(), "head"),
new EquippedItem(player.getInventory().getChestplate(), "chest"),
new EquippedItem(player.getInventory().getLeggings(), "legs"),
new EquippedItem(player.getInventory().getBoots(), "feet"));
}
private boolean slotMatches(String configured, String actual) {
String value = configured.toLowerCase(Locale.ROOT);
return value.equals(actual) || value.equals("all") || value.equals("any")
|| value.equals("hand") && actual.equals("mainhand")
|| value.equals("helmet") && actual.equals("head")
|| value.equals("chestplate") && actual.equals("chest")
|| value.equals("leggings") && actual.equals("legs")
|| value.equals("boots") && actual.equals("feet");
}
private LivingEntity selectedLiving(Player player, String selector, Entity target) {
Entity selected = selector.equalsIgnoreCase("self") ? player : target;
return selected instanceof LivingEntity living ? living : null;
}
private double value(String source, Player player, ItemStack item) {
try { return expressions.evaluate(source, player, item, Map.of()); }
catch (RuntimeException ignored) { return 0; }
}
private boolean roll(String source, Player player, ItemStack item) {
return ThreadLocalRandom.current().nextDouble(100) < value(source, player, item);
}
private boolean debuffed(Entity entity, String type) {
String key = entity.getUniqueId() + ":" + type.toUpperCase(Locale.ROOT);
long until = debuffs.getOrDefault(key, 0L);
if (until <= System.currentTimeMillis()) {
debuffs.remove(key);
return false;
}
return true;
}
private void tip(Player player, String message) {
messages.send(player, message);
}
private String strip(String value) {
String colored = ChatColor.translateAlternateColorCodes('&', value == null ? "" : value);
return plugin.getConfig().getBoolean("skill-engine-lore-color", false)
? colored : ChatColor.stripColor(colored);
}
private String normalize(String value) {
return value.toLowerCase(Locale.ROOT).replace("_", "").replace("-", "");
}
private void replaceMatchingItem(Player player, ItemStack original, ItemStack changed) {
for (EquipmentSlot slot : EquipmentSlot.values()) {
ItemStack equipped = player.getInventory().getItem(slot);
if (equipped == original) {
player.getInventory().setItem(slot, changed);
return;
}
}
}
private Player attackingPlayer(Entity damager) {
if (damager instanceof Player player) return player;
if (damager instanceof Projectile projectile && projectile.getShooter() instanceof Player player) return player;
return null;
}
@EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = false)
public void onDamage(EntityDamageEvent event) {
if (debuffed(event.getEntity(), "HEAL")) { }
if (event.getEntity() instanceof Player victim) {
List<String> info = new ArrayList<>();
info.add(event.getCause().name());
Entity combatTarget = event instanceof EntityDamageByEntityEvent by ? by.getDamager() : null;
if (event instanceof EntityDamageByEntityEvent by) {
Entity damager = by.getDamager() instanceof Projectile projectile && projectile.getShooter() instanceof Entity shooter
? shooter : by.getDamager();
info.add(damager.getType().name());
trigger(victim, TriggerType.DAMAGED_ENTITY, List.of(damager.getType().name()), damager, victim.getLocation(), event);
if (by.getDamager() instanceof Projectile projectile)
trigger(victim, TriggerType.DAMAGED_PROJECTILE, List.of(damager.getType().name()), damager, victim.getLocation(), event);
} else trigger(victim, TriggerType.DAMAGED_BLOCK, List.of(event.getCause().name()), null, victim.getLocation(), event);
trigger(victim, TriggerType.DAMAGED, info, combatTarget, victim.getLocation(), event);
trigger(victim, TriggerType.COMBAT, info, combatTarget, victim.getLocation(), event);
}
if (event instanceof EntityDamageByEntityEvent by) {
Player attacker = attackingPlayer(by.getDamager());
if (attacker != null) {
if (debuffed(attacker, "ATTACK")) { event.setCancelled(true); return; }
Entity target = by.getEntity();
trigger(attacker, TriggerType.ATTACK, List.of(target.getType().name()), target, target.getLocation(), event);
trigger(attacker, TriggerType.COMBAT, List.of(target.getType().name()), target, target.getLocation(), event);
if (by.getDamager() instanceof Projectile projectile) {
TriggerType projectileTrigger = switch (projectile.getType()) {
case ARROW, SPECTRAL_ARROW -> TriggerType.BOW_HIT;
case TRIDENT -> TriggerType.TRIDENT_ATTACK;
case SNOWBALL -> TriggerType.SNOWBALL_ATTACK;
case FIREBALL, SMALL_FIREBALL -> TriggerType.FIREBALL_ATTACK;
default -> TriggerType.PROJECTILE_ATTACK;
};
trigger(attacker, projectileTrigger, List.of(target.getType().name()), target, target.getLocation(), event);
}
}
Entity directDamager = by.getDamager() instanceof Projectile projectile
&& projectile.getShooter() instanceof Entity shooter ? shooter : by.getDamager();
if (attacker == null && debuffed(directDamager, "ATTACK")) event.setCancelled(true);
}
}
@EventHandler(ignoreCancelled = true)
public void onDeath(EntityDeathEvent event) {
Player killer = event.getEntity().getKiller();
if (killer != null) {
trigger(killer, TriggerType.KILL, List.of(event.getEntityType().name()), event.getEntity(), event.getEntity().getLocation(), event);
trigger(killer, TriggerType.KILL_ENTITY, List.of(event.getEntityType().name()), event.getEntity(), event.getEntity().getLocation(), event);
if (event.getEntity() instanceof Player)
trigger(killer, TriggerType.KILL_PLAYER, List.of(), event.getEntity(), event.getEntity().getLocation(), event);
}
if (event instanceof PlayerDeathEvent playerDeath) {
trigger(playerDeath.getEntity(), TriggerType.DEATH, List.of(), playerDeath.getEntity(), playerDeath.getEntity().getLocation(), event);
trigger(playerDeath.getEntity(), TriggerType.DESPAWN, List.of(), playerDeath.getEntity(), playerDeath.getEntity().getLocation(), event);
}
}
@EventHandler(ignoreCancelled = false)
public void onInteract(PlayerInteractEvent event) {
Player player = event.getPlayer();
Action action = event.getAction();
boolean right = action.isRightClick();
if (!right) return;
if (debuffed(player, "USE")) { event.setCancelled(true); return; }
trigger(player, player.isSneaking() ? TriggerType.SHIFT_USE : TriggerType.USE,
List.of(), null, event.getClickedBlock() == null ? null : event.getClickedBlock().getLocation(), event);
if (event.getClickedBlock() == null)
trigger(player, player.isSneaking() ? TriggerType.SHIFT_RIGHTCLICK : TriggerType.RIGHT_CLICK,
List.of(), null, player.getLocation(), event);
}
@EventHandler
public void onAnimation(PlayerAnimationEvent event) {
if (event.getAnimationType() != PlayerAnimationType.ARM_SWING) return;
Player player = event.getPlayer();
trigger(player, player.isSneaking() ? TriggerType.SHIFT_SWING : TriggerType.SWING,
List.of(), null, player.getLocation(), event);
}
@EventHandler(ignoreCancelled = false)
public void onMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
if (debuffed(player, "MOVE") && event.getTo() != null
&& (event.getFrom().getX() != event.getTo().getX() || event.getFrom().getZ() != event.getTo().getZ())) {
event.setCancelled(true);
return;
}
}
@EventHandler(ignoreCancelled = false)
public void onJump(PlayerJumpEvent event) {
Player player = event.getPlayer();
if (debuffed(player, "JUMP")) { event.setCancelled(true); return; }
CMagicJumpEvent jump = new CMagicJumpEvent(player, event.getFrom(), event.getTo());
Bukkit.getPluginManager().callEvent(jump);
if (jump.isCancelled()) { event.setCancelled(true); return; }
trigger(player, TriggerType.JUMP, List.of(), null, jump.getTo(), event);
}
@EventHandler(ignoreCancelled = false)
public void onTeleport(PlayerTeleportEvent event) {
if (debuffed(event.getPlayer(), "TELEPORT")) { event.setCancelled(true); return; }
trigger(event.getPlayer(), TriggerType.TELEPORT, List.of(), null, event.getTo(), event);
}
@EventHandler(ignoreCancelled = false)
public void onPlace(BlockPlaceEvent event) {
if (debuffed(event.getPlayer(), "PLACE")) { event.setCancelled(true); return; }
trigger(event.getPlayer(), TriggerType.BLOCK_PLACE, List.of(event.getBlockPlaced().getType().name()), null, event.getBlockPlaced().getLocation(), event);
trigger(event.getPlayer(), TriggerType.BLOCK, List.of(event.getBlockPlaced().getType().name()), null, event.getBlockPlaced().getLocation(), event);
}
@EventHandler(ignoreCancelled = false)
public void onBreak(BlockBreakEvent event) {
if (debuffed(event.getPlayer(), "BREAK")) { event.setCancelled(true); return; }
trigger(event.getPlayer(), TriggerType.BLOCK_BREAK, List.of(event.getBlock().getType().name()), null, event.getBlock().getLocation(), event);
}
@EventHandler(ignoreCancelled = false)
public void onConsume(PlayerItemConsumeEvent event) {
if (debuffed(event.getPlayer(), "CONSUME")) { event.setCancelled(true); return; }
trigger(event.getPlayer(), TriggerType.CONSUME, List.of(event.getItem().getType().name()), null, event.getPlayer().getLocation(), event);
}
@EventHandler(ignoreCancelled = false)
public void onShoot(EntityShootBowEvent event) {
if (!(event.getEntity() instanceof Player player)) return;
if (debuffed(player, "SHOOT")) { event.setCancelled(true); return; }
trigger(player, event.getProjectile().getType().name().equals("TRIDENT") ? TriggerType.TRIDENT_THROW : TriggerType.SHOOT,
List.of(), event.getProjectile(), event.getProjectile().getLocation(), event);
}
@EventHandler(ignoreCancelled = true)
public void onProjectileLaunch(ProjectileLaunchEvent event) {
ProjectileSource source = event.getEntity().getShooter();
if (source instanceof Entity entity && debuffed(entity, "SHOOT")) {
event.setCancelled(true);
return;
}
if (!(source instanceof Player player)) return;
if (event.getEntity().getType() == org.bukkit.entity.EntityType.TRIDENT)
trigger(player, TriggerType.TRIDENT_THROW, List.of(), event.getEntity(), event.getEntity().getLocation(), event);
}
@EventHandler(ignoreCancelled = true)
public void onPotionSplash(PotionSplashEvent event) {
if (event.getEntity().getShooter() instanceof Player player)
trigger(player, TriggerType.SPLASH_POTION, List.of(), event.getEntity(), event.getEntity().getLocation(), event);
}
@EventHandler(ignoreCancelled = true)
public void onLingeringPotionSplash(LingeringPotionSplashEvent event) {
if (event.getEntity().getShooter() instanceof Player player)
trigger(player, TriggerType.SPLASH_POTION, List.of(), event.getEntity(), event.getEntity().getLocation(), event);
}
@EventHandler(ignoreCancelled = true)
public void onProjectileHit(ProjectileHitEvent event) {
if (!(event.getEntity().getShooter() instanceof Player player)) return;
if (event.getHitBlock() == null) return;
Location location = event.getHitBlock().getLocation();
TriggerType type = event.getEntity().getType().name().equals("TRIDENT") ? TriggerType.TRIDENT_HIT : TriggerType.HIT;
trigger(player, type, List.of(event.getEntityType().name()), null, location, event);
}
@EventHandler(ignoreCancelled = true)
public void onDrop(PlayerDropItemEvent event) {
trigger(event.getPlayer(), TriggerType.DROP, List.of(event.getItemDrop().getItemStack().getType().name()),
event.getItemDrop(), event.getItemDrop().getLocation(), event);
}
@EventHandler(ignoreCancelled = true)
public void onTame(EntityTameEvent event) {
if (event.getOwner() instanceof Player player)
trigger(player, TriggerType.TAME, List.of(event.getEntityType().name()), event.getEntity(), event.getEntity().getLocation(), event);
}
@EventHandler(ignoreCancelled = true)
public void onEntityInteract(PlayerInteractAtEntityEvent event) {
trigger(event.getPlayer(), TriggerType.INTERACT, List.of(event.getRightClicked().getType().name()),
event.getRightClicked(), event.getRightClicked().getLocation(), event);
}
@EventHandler(ignoreCancelled = true)
public void onFish(PlayerFishEvent event) {
String information = event.getCaught() == null ? event.getState().name() : event.getCaught().getType().name();
trigger(event.getPlayer(), TriggerType.FISH, List.of(information), event.getCaught(), event.getHook().getLocation(), event);
TriggerType type = switch (event.getState()) {
case BITE -> TriggerType.FISH_BITE;
case CAUGHT_FISH -> TriggerType.FISH_CATCH_FISH;
case CAUGHT_ENTITY -> TriggerType.FISH_CATCH_ENTITY;
case IN_GROUND -> TriggerType.FISH_GROUND;
case REEL_IN -> TriggerType.FISH_REEL;
case FAILED_ATTEMPT -> TriggerType.FISH_FAIL;
default -> null;
};
if (type != null) trigger(event.getPlayer(), type, List.of(information), event.getCaught(), event.getHook().getLocation(), event);
}
@EventHandler(ignoreCancelled = true)
public void onSneak(PlayerToggleSneakEvent event) {
trigger(event.getPlayer(), event.isSneaking() ? TriggerType.CROUCH : TriggerType.UNCROUCH,
List.of(), null, event.getPlayer().getLocation(), event);
}
@EventHandler(ignoreCancelled = false)
public void onRegain(EntityRegainHealthEvent event) {
if (debuffed(event.getEntity(), "HEAL")) event.setCancelled(true);
}
@EventHandler(ignoreCancelled = false)
public void onEntityTeleport(EntityTeleportEvent event) {
if (debuffed(event.getEntity(), "TELEPORT")) event.setCancelled(true);
}
@EventHandler(ignoreCancelled = false)
public void onFlight(PlayerToggleFlightEvent event) {
if (event.isFlying() && debuffed(event.getPlayer(), "FLY")) event.setCancelled(true);
}
@EventHandler(ignoreCancelled = true)
public void onArmorChange(PlayerArmorChangeEvent event) {
equipmentChange(event.getPlayer(), event.getSlotType().name().toLowerCase(Locale.ROOT),
event.getOldItem(), event.getNewItem(), CMagicEquipmentChangeEvent.Cause.ARMOR, event);
}
@EventHandler(ignoreCancelled = true)
public void onHeld(PlayerItemHeldEvent event) {
Player player = event.getPlayer();
equipmentChange(player, "mainhand", player.getInventory().getItem(event.getPreviousSlot()),
player.getInventory().getItem(event.getNewSlot()), CMagicEquipmentChangeEvent.Cause.HELD, event);
}
@EventHandler(ignoreCancelled = true)
public void onSwap(PlayerSwapHandItemsEvent event) {
Player player = event.getPlayer();
equipmentChange(player, "mainhand", player.getInventory().getItemInMainHand(), event.getMainHandItem(),
CMagicEquipmentChangeEvent.Cause.SWAP, event);
equipmentChange(player, "offhand", player.getInventory().getItemInOffHand(), event.getOffHandItem(),
CMagicEquipmentChangeEvent.Cause.SWAP, event);
}
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
public void onInventoryClick(InventoryClickEvent event) {
if (!(event.getWhoClicked() instanceof Player player)) return;
ItemStack mainHand = snapshot(player.getInventory().getItemInMainHand());
ItemStack offHand = snapshot(player.getInventory().getItemInOffHand());
Bukkit.getScheduler().runTask(plugin, () -> {
ItemStack currentMainHand = snapshot(player.getInventory().getItemInMainHand());
ItemStack currentOffHand = snapshot(player.getInventory().getItemInOffHand());
if (!sameItem(mainHand, currentMainHand))
equipmentChange(player, "mainhand", mainHand, currentMainHand,
CMagicEquipmentChangeEvent.Cause.INVENTORY, event);
if (!sameItem(offHand, currentOffHand))
equipmentChange(player, "offhand", offHand, currentOffHand,
CMagicEquipmentChangeEvent.Cause.INVENTORY, event);
});
}
@EventHandler
public void onBreak(PlayerItemBreakEvent event) {
if (isArmor(event.getBrokenItem().getType())) return;
equipmentChange(event.getPlayer(), slotFor(event.getPlayer(), event.getBrokenItem()), event.getBrokenItem(),
null, CMagicEquipmentChangeEvent.Cause.BREAK, event);
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Bukkit.getScheduler().runTask(plugin, () -> trigger(event.getPlayer(), TriggerType.SPAWN, List.of(), null, event.getPlayer().getLocation(), event));
}
@EventHandler
public void onRespawn(PlayerRespawnEvent event) {
Bukkit.getScheduler().runTask(plugin, () -> trigger(event.getPlayer(), TriggerType.SPAWN, List.of(), null, event.getRespawnLocation(), event));
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
trigger(event.getPlayer(), TriggerType.DESPAWN, List.of(), null, event.getPlayer().getLocation(), event);
cooldowns.remove(event.getPlayer().getUniqueId());
durations.remove(event.getPlayer().getUniqueId());
String prefix = event.getPlayer().getUniqueId() + ":";
counters.keySet().removeIf(key -> key.startsWith(prefix));
debuffs.keySet().removeIf(key -> key.startsWith(prefix));
}
private void equipmentChange(Player player, String slot, ItemStack oldItem, ItemStack newItem,
CMagicEquipmentChangeEvent.Cause cause, Event source) {
CMagicEquipmentChangeEvent event = new CMagicEquipmentChangeEvent(player, slot, oldItem, newItem, cause);
Bukkit.getPluginManager().callEvent(event);
if (event.isCancelled()) return;
if (oldItem != null && !oldItem.getType().isAir())
triggerItem(player, oldItem, slot, TriggerType.ARMOR_UNEQUIP, List.of(), null, player.getLocation(), source);
if (newItem != null && !newItem.getType().isAir())
triggerItem(player, newItem, slot, TriggerType.ARMOR_EQUIP, List.of(), null, player.getLocation(), source);
}
private ItemStack snapshot(ItemStack item) {
return item == null || item.getType().isAir() ? null : item.clone();
}
private boolean sameItem(ItemStack first, ItemStack second) {
return first == null ? second == null : first.equals(second);
}
private String slotFor(Player player, ItemStack item) {
for (EquipmentSlot slot : EquipmentSlot.values())
if (player.getInventory().getItem(slot) == item
|| player.getInventory().getItem(slot) != null
&& player.getInventory().getItem(slot).isSimilar(item))
return slot.name().toLowerCase(Locale.ROOT);
return "mainhand";
}
private boolean isArmor(Material material) {
return material.name().endsWith("_HELMET") || material.name().endsWith("_CHESTPLATE")
|| material.name().endsWith("_LEGGINGS") || material.name().endsWith("_BOOTS")
|| material == Material.ELYTRA;
}
private record EquippedItem(ItemStack item, String slot) { }
private record EventContext(Event event, Location location) { }
private record CounterState(long count, long lastUsed) { }
}
@@ -0,0 +1,150 @@
package cn.infstar.cmagicgem.skill;
import cn.infstar.cmagicgem.api.event.CMagicSkillLoadEvent;
import cn.infstar.cmagicgem.dsl.Directive;
import cn.infstar.cmagicgem.dsl.DirectiveParser;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.bukkit.plugin.java.JavaPlugin;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
public final class SkillRegistry {
private final JavaPlugin plugin;
private final DirectiveParser parser;
private volatile Map<String, SkillDefinition> skills = Map.of();
public SkillRegistry(JavaPlugin plugin, DirectiveParser parser) {
this.plugin = plugin;
this.parser = parser;
}
public int reload() {
File directory = new File(plugin.getDataFolder(), "skills");
if (!directory.exists() && !directory.mkdirs()) throw new IllegalStateException("无法创建 skills 目录");
Map<String, SkillDefinition> loaded = new LinkedHashMap<>();
File[] files = directory.listFiles((ignored, name) -> name.toLowerCase(Locale.ROOT).endsWith(".yml"));
if (files != null) for (File file : files) loadFile(file, loaded);
skills = Collections.unmodifiableMap(loaded);
return loaded.size();
}
public void clear() {
skills = Map.of();
}
private void loadFile(File file, Map<String, SkillDefinition> loaded) {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
String dependency = yaml.getString("depend");
if (dependency != null && plugin.getServer().getPluginManager().getPlugin(dependency) == null) return;
for (String key : yaml.getKeys(false)) {
if (key.equalsIgnoreCase("depend")) continue;
ConfigurationSection section = yaml.getConfigurationSection(key);
if (section == null) continue;
try {
CMagicSkillLoadEvent event = new CMagicSkillLoadEvent(key, section);
plugin.getServer().getPluginManager().callEvent(event);
if (!event.isCancelled()) loaded.put(key.toLowerCase(Locale.ROOT), parse(key, event.getConfig()));
}
catch (RuntimeException exception) { plugin.getLogger().log(Level.SEVERE, "无法加载技能 " + file.getName() + "/" + key + ": " + exception.getMessage()); }
}
}
private SkillDefinition parse(String name, ConfigurationSection section) {
List<SkillAction> actions = new ArrayList<>();
for (String line : section.getStringList("Skills")) actions.add(parseAction(line));
Set<String> slots = new LinkedHashSet<>();
Object slotValue = section.get("Slot");
if (slotValue instanceof String value) slots.add(value.toLowerCase(Locale.ROOT));
else for (String value : section.getStringList("Slot")) slots.add(value.toLowerCase(Locale.ROOT));
return new SkillDefinition(name, section.getString("Lore"), section.getString("Name"), section.getString("MythicType"),
Set.copyOf(slots), section.getString("Success", "100"), section.getDouble("Cooldown", 0),
section.getDouble("Duration", 0), section.getString("CooldownTip"),
section.getString("SuccessTip"), section.getString("FailTip"), List.copyOf(actions),
parseList(section.getStringList("Conditions")), parseList(section.getStringList("TriggerConditions")),
parseList(section.getStringList("TargetConditions")));
}
SkillAction parseAction(String line) {
List<String> tokens = topLevelTokens(line == null ? "" : line.trim());
String mechanicText = tokens.isEmpty() ? "" : tokens.getFirst();
TriggerType trigger = TriggerType.DEFAULT;
List<String> filters = List.of();
String target = "self";
Map<String, Integer> worldLimits = new LinkedHashMap<>();
List<String> mechanicTokens = new ArrayList<>();
for (String token : tokens) {
if (token.toLowerCase(Locale.ROOT).startsWith("~on")) {
int separator = token.indexOf(':');
trigger = TriggerType.parse(separator < 0 ? token : token.substring(0, separator));
filters = separator < 0 ? List.of() : List.of(token.substring(separator + 1).split(";"));
} else if (worldLimit(token, worldLimits)) {
continue;
} else {
mechanicTokens.add(token);
if (token.startsWith("@")) {
int brace = token.indexOf('{');
target = token.substring(1, brace < 0 ? token.length() : brace).toLowerCase(Locale.ROOT);
}
}
}
String mechanicLine = String.join(" ", mechanicTokens);
return new SkillAction(parser.parse(mechanicText), mechanicLine, trigger, filters, target,
Map.copyOf(worldLimits));
}
private boolean worldLimit(String token, Map<String, Integer> worldLimits) {
if (!token.startsWith("$") || token.lastIndexOf(':') <= 1) return false;
int separator = token.lastIndexOf(':');
try {
worldLimits.put(token.substring(1, separator).toLowerCase(Locale.ROOT),
Integer.parseInt(token.substring(separator + 1)));
return true;
} catch (NumberFormatException ignored) {
return false;
}
}
private List<String> topLevelTokens(String line) {
List<String> tokens = new ArrayList<>();
int depth = 0;
int start = -1;
boolean quoted = false;
for (int index = 0; index < line.length(); index++) {
char current = line.charAt(index);
if (start < 0 && !Character.isWhitespace(current)) start = index;
if (current == '"' && (index == 0 || line.charAt(index - 1) != '\\')) quoted = !quoted;
else if (!quoted && current == '{') depth++;
else if (!quoted && current == '}') depth = Math.max(0, depth - 1);
else if (!quoted && depth == 0 && Character.isWhitespace(current) && start >= 0) {
tokens.add(line.substring(start, index));
start = -1;
}
}
if (start >= 0) tokens.add(line.substring(start));
return tokens;
}
private List<Directive> parseList(List<String> lines) {
return lines.stream().map(parser::parse).toList();
}
public Optional<SkillDefinition> find(String name) {
return name == null ? Optional.empty() : Optional.ofNullable(skills.get(name.toLowerCase(Locale.ROOT)));
}
public Collection<SkillDefinition> all() {
return skills.values();
}
}
@@ -0,0 +1,24 @@
package cn.infstar.cmagicgem.skill;
import java.util.Locale;
public enum TriggerType {
API, CUSTOM, JUMP, SIGNAL, COUNTER, SHIFT_SWING, SHIFT_USE, SHIFT_RIGHTCLICK,
KILL_ENTITY, KILL_MYTHICMOBS, DAMAGED_PROJECTILE, DAMAGED_BLOCK, DAMAGED_ENTITY,
DROP, TRIDENT_HIT, HIT, BOW_HIT, TRIDENT_ATTACK, SNOWBALL_ATTACK, FIREBALL_ATTACK,
PROJECTILE_ATTACK, COMBAT, TELEPORT, BLOCK, DEFAULT, TRIDENT_THROW, DESPAWN, TAME,
ATTACK, SHOOT, BLOCK_PLACE, BLOCK_BREAK, CONSUME, CROUCH, UNCROUCH, DAMAGED, DEATH,
INTERACT, KILL, KILL_PLAYER, SPAWN, SPLASH_POTION, SWING, TIMER, USE, RIGHT_CLICK,
FISH, FISH_BITE, FISH_CATCH_FISH, FISH_CATCH_ENTITY, FISH_GROUND, FISH_REEL,
FISH_FAIL, ARMOR_EQUIP, ARMOR_UNEQUIP;
public static TriggerType parse(String input) {
String normalized = input.replaceFirst("(?i)^~on", "")
.replaceAll("([a-z])([A-Z])", "$1_$2")
.replace('-', '_').toUpperCase(Locale.ROOT);
if (normalized.equals("RIGHTCLICK")) normalized = "RIGHT_CLICK";
if (normalized.equals("KILLPLAYER")) normalized = "KILL_PLAYER";
if (normalized.equals("DESPAWNED")) normalized = "DESPAWN";
return valueOf(normalized);
}
}
@@ -0,0 +1,45 @@
package cn.infstar.cmagicgem.util;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.enchantments.Enchantment;
import java.util.Locale;
import java.util.Map;
public final class EnchantmentResolver {
private static final Map<String, String> LEGACY_NAMES = Map.ofEntries(
Map.entry("protection_environmental", "protection"),
Map.entry("protection_fire", "fire_protection"),
Map.entry("protection_fall", "feather_falling"),
Map.entry("protection_explosions", "blast_protection"),
Map.entry("protection_projectile", "projectile_protection"),
Map.entry("oxygen", "respiration"), Map.entry("water_worker", "aqua_affinity"),
Map.entry("damage_all", "sharpness"), Map.entry("damage_undead", "smite"),
Map.entry("damage_arthropods", "bane_of_arthropods"), Map.entry("dig_speed", "efficiency"),
Map.entry("durability", "unbreaking"), Map.entry("loot_bonus_blocks", "fortune"),
Map.entry("loot_bonus_mobs", "looting"), Map.entry("arrow_damage", "power"),
Map.entry("arrow_knockback", "punch"), Map.entry("arrow_fire", "flame"),
Map.entry("arrow_infinite", "infinity"), Map.entry("luck", "luck_of_the_sea"));
private EnchantmentResolver() {
}
public static Enchantment resolve(String input) {
if (input == null || input.isBlank()) return null;
String normalized = input.toLowerCase(Locale.ROOT);
NamespacedKey direct = NamespacedKey.fromString(normalized);
if (direct != null) {
Enchantment found = Registry.ENCHANTMENT.get(direct);
if (found != null) return found;
}
String key = LEGACY_NAMES.getOrDefault(normalized.replace("minecraft:", ""),
normalized.replace("minecraft:", ""));
Enchantment vanilla = Registry.ENCHANTMENT.get(NamespacedKey.minecraft(key));
if (vanilla != null) return vanilla;
for (Enchantment enchantment : Registry.ENCHANTMENT)
if (enchantment.getKey().getKey().equalsIgnoreCase(key)
|| enchantment.getKey().toString().equalsIgnoreCase(input)) return enchantment;
return null;
}
}
@@ -0,0 +1,40 @@
package cn.infstar.cmagicgem.util;
import org.bukkit.Bukkit;
import org.bukkit.Material;
import org.bukkit.material.MaterialData;
import java.util.Locale;
public final class MaterialResolver {
private MaterialResolver() {
}
public static Material resolve(String input) {
if (input == null || input.isBlank()) return null;
String value = input.trim();
Material direct = Material.matchMaterial(value);
if (direct != null && !direct.isLegacy()) return direct;
int data = 0;
int separator = value.lastIndexOf(':');
if (separator > 0 && !value.regionMatches(true, 0, "minecraft:", 0, 10)) {
try {
data = Integer.parseInt(value.substring(separator + 1));
value = value.substring(0, separator);
} catch (NumberFormatException ignored) {
}
}
Material modern = Material.matchMaterial(value);
if (modern != null && !modern.isLegacy()) return modern;
Material legacy = Material.matchMaterial(value.toUpperCase(Locale.ROOT), true);
if (legacy == null || !legacy.isLegacy()) return legacy;
try {
Material converted = Bukkit.getUnsafe().fromLegacy(new MaterialData(legacy, (byte) data), true);
return converted == Material.AIR && legacy != Material.LEGACY_AIR ? null : converted;
} catch (RuntimeException exception) {
return null;
}
}
}
@@ -0,0 +1,109 @@
package cn.infstar.cmagicgem.util;
import cn.infstar.cmagicgem.config.LanguageManager;
import cn.infstar.cmagicgem.dsl.Directive;
import cn.infstar.cmagicgem.dsl.DirectiveParser;
import cn.infstar.cmagicgem.integration.PlaceholderBridge;
import cn.infstar.cmagicgem.item.ItemCodec;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import net.kyori.adventure.title.Title;
import org.bukkit.Bukkit;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.LinkedHashMap;
import java.util.Map;
public final class MessageDispatcher {
private final JavaPlugin plugin;
private final ItemCodec codec;
private final DirectiveParser parser;
private final PlaceholderBridge placeholders;
private final LanguageManager languages;
public MessageDispatcher(JavaPlugin plugin, ItemCodec codec, DirectiveParser parser, PlaceholderBridge placeholders,
LanguageManager languages) {
this.plugin = plugin;
this.codec = codec;
this.parser = parser;
this.placeholders = placeholders;
this.languages = languages;
}
public void send(CommandSender receiver, String source) {
if (source == null || source.isBlank() || source.equalsIgnoreCase("none")) return;
if (!(receiver instanceof Player player) || !source.contains("{")) {
receiver.sendMessage(codec.color(source));
return;
}
Directive directive;
try { directive = parser.parse(source); }
catch (RuntimeException ignored) { player.sendMessage(codec.color(source)); return; }
String type = directive.type().toLowerCase(Locale.ROOT);
List<String> messages = parser.splitTopLevel(directive.argument("", "messages", "message", "m"), ';');
int interval = Math.max(0, directive.intArgument(0, "interval"));
if (type.equals("title")) {
for (int index = 0; index < messages.size(); index += 2) {
String title = resolve(player, messages.get(index));
String subtitle = resolve(player, index + 1 < messages.size() ? messages.get(index + 1) : "");
int delay = interval * (index / 2);
later(delay, () -> player.showTitle(Title.title(component(title), component(subtitle), Title.Times.times(
Duration.ofMillis(directive.intArgument(10, "fadein") * 50L),
Duration.ofMillis(directive.intArgument(70, "stay") * 50L),
Duration.ofMillis(directive.intArgument(20, "fadeout") * 50L)))));
}
return;
}
for (int index = 0; index < messages.size(); index++) {
String message = resolve(player, messages.get(index));
int delay = interval * index;
later(delay, () -> {
if (type.equals("actionbar")) player.sendActionBar(component(message));
else player.sendMessage(codec.color(message));
});
}
}
public void cooldown(Player player, String configured, double remaining, double total) {
if (configured == null || configured.isBlank()) {
send(player, languages.text("cmagic.cooldown", "&e冷却中,剩余 {seconds} 秒",
"seconds", String.format(Locale.ROOT, "%.2f", remaining)));
return;
}
int open = configured.indexOf('{');
int close = configured.lastIndexOf('}');
if (open < 0 || close < open) { send(player, configured); return; }
Map<String, String> arguments = new LinkedHashMap<>();
for (String token : configured.substring(open + 1, close).split(";")) {
int equals = token.indexOf('=');
if (equals > 0) arguments.put(token.substring(0, equals).trim().toLowerCase(Locale.ROOT), token.substring(equals + 1));
}
int length;
try { length = Math.max(0, Integer.parseInt(arguments.getOrDefault("length", "0"))); }
catch (NumberFormatException ignored) { length = 0; }
int full = total <= 0 ? 0 : (int) Math.round(length * Math.max(0, Math.min(1, 1 - remaining / total)));
String suffix;
try { suffix = String.format(Locale.ROOT, arguments.getOrDefault("s", "%.2fs"), remaining); }
catch (RuntimeException ignored) { suffix = String.format(Locale.ROOT, "%.2fs", remaining); }
String message = arguments.getOrDefault("p", "") + arguments.getOrDefault("full", "|").repeat(full)
+ arguments.getOrDefault("empty", "|").repeat(Math.max(0, length - full)) + suffix;
send(player, configured.substring(0, open) + "{m=" + message + '}');
}
private String resolve(Player player, String message) {
return placeholders.resolve(player, message);
}
private void later(int delay, Runnable action) {
if (delay <= 0) action.run(); else Bukkit.getScheduler().runTaskLater(plugin, action, delay);
}
private Component component(String message) {
return LegacyComponentSerializer.legacyAmpersand().deserialize(message == null ? "" : message);
}
}
@@ -0,0 +1,55 @@
package cn.infstar.cmagicgem.util;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.potion.PotionEffectType;
import java.util.Locale;
import java.util.Map;
public final class PotionTypes {
private static final Map<String, String> ALIASES = Map.ofEntries(
Map.entry("absorb", "absorption"), Map.entry("omen_bad", "bad_omen"),
Map.entry("pillager", "bad_omen"), Map.entry("blind", "blindness"),
Map.entry("confusion", "nausea"), Map.entry("sickness", "nausea"),
Map.entry("damage_resistance", "resistance"), Map.entry("armor", "resistance"),
Map.entry("dmg_resist", "resistance"), Map.entry("dmg_resistance", "resistance"),
Map.entry("conduit", "conduit_power"), Map.entry("power_conduit", "conduit_power"),
Map.entry("dolphin", "dolphins_grace"), Map.entry("grace", "dolphins_grace"),
Map.entry("fast_digging", "haste"), Map.entry("dig_speed", "haste"),
Map.entry("quick_mine", "haste"), Map.entry("super_pick", "haste"), Map.entry("digfast", "haste"),
Map.entry("fire_resist", "fire_resistance"), Map.entry("resist_fire", "fire_resistance"),
Map.entry("glow", "glowing"), Map.entry("shine", "glowing"), Map.entry("shiny", "glowing"),
Map.entry("harm", "instant_damage"), Map.entry("damage", "instant_damage"),
Map.entry("injure", "instant_damage"), Map.entry("harming", "instant_damage"),
Map.entry("inflict", "instant_damage"), Map.entry("heal", "instant_health"),
Map.entry("health", "instant_health"), Map.entry("insta_heal", "instant_health"),
Map.entry("instant_heal", "instant_health"), Map.entry("insta_health", "instant_health"),
Map.entry("boost_health", "health_boost"), Map.entry("boost", "health_boost"),
Map.entry("hp", "health_boost"), Map.entry("hero", "hero_of_the_village"),
Map.entry("village_hero", "hero_of_the_village"), Map.entry("starve", "hunger"),
Map.entry("hungry", "hunger"), Map.entry("increase_damage", "strength"),
Map.entry("bull", "strength"), Map.entry("strong", "strength"), Map.entry("attack", "strength"),
Map.entry("invisible", "invisibility"), Map.entry("hide", "invisibility"),
Map.entry("vanish", "invisibility"), Map.entry("invis", "invisibility"),
Map.entry("disappear", "invisibility"),
Map.entry("jump", "jump_boost"), Map.entry("leap", "jump_boost"),
Map.entry("levitate", "levitation"), Map.entry("lucky", "luck"),
Map.entry("vision", "night_vision"), Map.entry("vision_night", "night_vision"),
Map.entry("venom", "poison"), Map.entry("regen", "regeneration"), Map.entry("food", "saturation"),
Map.entry("slow", "slowness"), Map.entry("sluggish", "slowness"),
Map.entry("slow_digging", "mining_fatigue"), Map.entry("dull", "mining_fatigue"),
Map.entry("fatigue", "mining_fatigue"), Map.entry("dig_slow", "mining_fatigue"),
Map.entry("digging", "mining_fatigue"), Map.entry("slow_dig", "mining_fatigue"),
Map.entry("slow_fall", "slow_falling"), Map.entry("fall_slow", "slow_falling"),
Map.entry("sprint", "speed"), Map.entry("run_fast", "speed"),
Map.entry("swift", "speed"), Map.entry("fast", "speed"));
private PotionTypes() { }
public static PotionEffectType resolve(String input) {
if (input == null || input.isBlank()) return null;
String name = input.toLowerCase(Locale.ROOT).replace("minecraft:", "");
return Registry.EFFECT.get(NamespacedKey.minecraft(ALIASES.getOrDefault(name, name)));
}
}
+12
View File
@@ -0,0 +1,12 @@
language: zh_CN
workbench: true
backpack: false
gem-lore: false
gem-craft: false
skill-engine: true
skill-engine-lore-color: false
legacy-migration: true
potion-global-limit: {}
integrations:
mythicmobs:
reload-on-enable: true
+232
View File
@@ -0,0 +1,232 @@
【Version: 1.05
内置-生命宝石:
Name: 生命宝石
Require:
- ARMOR
Display: "&8&l&k||&c&l生命宝石&8&l&k||"
Tips:
- "&c生命宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其最大生命上限+1(上限5)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDFhNmE0MTFiODAyYjIyZTUzMjQ3OTVjZTM0ZTNjYWU0YTgzNzk2YTE4ZDkyMzMyNjY2Y2JmMjE0ODhjMCJ9fX0=
SuccessTip: "&f你使用了&c&l生命宝石&f, 装备生命上限&c&l+1"
Rewards:
- Attribute{name=health;operation=0;slot=auto;var=v+1;limit=5}
内置-真生命宝石:
Name: 真·生命宝石
Require:
- ARMOR
Display: "&8&l&k||&c&l真·生命宝石&8&l&k||"
Tips:
- "&c&l真生命宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其最大生命上限+1(上限10)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDFhNmE0MTFiODAyYjIyZTUzMjQ3OTVjZTM0ZTNjYWU0YTgzNzk2YTE4ZDkyMzMyNjY2Y2JmMjE0ODhjMCJ9fX0=
SuccessTip: "&f你使用了&c&l真·生命宝石&f, 装备生命上限&c&l+1"
Rewards:
- Attribute{name=health;operation=0;slot=auto;var=v+1;limit=10}
内置-神生命宝石:
Name: 神·生命宝石
Require:
- ARMOR
Display: "&8&l&k||&c&l神·生命宝石&8&l&k||"
Success: 50
Tips:
- "&c&l神·生命宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其最大生命上限+2(上限20)"
- "&7成功率: &a50%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDFhNmE0MTFiODAyYjIyZTUzMjQ3OTVjZTM0ZTNjYWU0YTgzNzk2YTE4ZDkyMzMyNjY2Y2JmMjE0ODhjMCJ9fX0=
SuccessTip: "&f你使用了&c&l神·生命宝石&f, 装备生命上限&c&l+2"
Rewards:
- Attribute{name=health;operation=0;slot=auto;var=v+2;limit=20}
内置-速度宝石:
Name: 速度宝石
Require:
- BOOTS
Display: "&8&l&k||&a&l速度宝石&8&l&k||"
Tips:
- "&a速度宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的靴子"
- "&7即可为其速度属性+1%(上限6%)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzA1OGQwODBiZDYyMTU2ZTVmNmU1ZThkZmZhYTI4OTI2YmQ4MzM5MWRjMjZmZTc4MDY3M2VmNTJmMjZlYjA4YiJ9fX0=
SuccessTip: "&f你使用了&a&l速度宝石&f, 装备速度属性&a&l+1%"
Rewards:
- Attribute{name=move;operation=1;slot=auto;var=v+0.01;limit=0.06}
内置-真速度宝石:
Name: 真·速度宝石
Require:
- BOOTS
Display: "&8&l&k||&a&l真·速度宝石&8&l&k||"
Tips:
- "&a&l真速度宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的靴子"
- "&7即可为其速度属性+1%(上限15%)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzA1OGQwODBiZDYyMTU2ZTVmNmU1ZThkZmZhYTI4OTI2YmQ4MzM5MWRjMjZmZTc4MDY3M2VmNTJmMjZlYjA4YiJ9fX0=
SuccessTip: "&f你使用了&a&l真·速度宝石&f, 装备速度属性&a&l+1%"
Rewards:
- Attribute{name=move;operation=1;slot=auto;var=v+0.01;limit=0.15}
内置-神速度宝石:
Name: 神·速度宝石
Require:
- BOOTS
Display: "&8&l&k||&a&l神·速度宝石&8&l&k||"
Success: 50
Tips:
- "&a&l神·速度宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的靴子"
- "&7即可为其速度属性+2%(上限30%)"
- "&7成功率: &a50%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzA1OGQwODBiZDYyMTU2ZTVmNmU1ZThkZmZhYTI4OTI2YmQ4MzM5MWRjMjZmZTc4MDY3M2VmNTJmMjZlYjA4YiJ9fX0=
SuccessTip: "&f你使用了&a&l神·速度宝石&f, 装备速度属性&a&l+2%"
Rewards:
- Attribute{name=move;operation=1;slot=auto;var=v+0.02;limit=0.3}
内置-攻击伤害宝石:
Name: 攻击伤害宝石
Require:
- WEAPON
Display: "&8&l&k||&c&l攻击伤害宝石&8&l&k||"
Tips:
- "&c攻击伤害宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其攻击伤害属性+1(上限20)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzEzODc0ZTgyYmM0NjZhOWM5Zjc5YWFmYmE3MTUxN2UyZWNjMjk0MWM2YmM5Njc5MDYyYTk3YjkxYjJkYWUzOSJ9fX0=
SuccessTip: "&f你使用了&c&l攻击伤害宝石&f, 装备攻击伤害属性&c&l+1%"
Rewards:
- Attribute{name=damage;operation=0;slot=auto;var=v+1;limit=20}
内置-真攻击伤害宝石:
Name: 真·攻击伤害宝石
Require:
- WEAPON
Display: "&8&l&k||&c&l真·攻击伤害宝石&8&l&k||"
Tips:
- "&c&l真·攻击伤害宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其攻击伤害属性+1(上限80)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzEzODc0ZTgyYmM0NjZhOWM5Zjc5YWFmYmE3MTUxN2UyZWNjMjk0MWM2YmM5Njc5MDYyYTk3YjkxYjJkYWUzOSJ9fX0=
SuccessTip: "&f你使用了&c&l真·攻击伤害宝石&f, 装备攻击伤害属性&c&l+1"
Rewards:
- Attribute{name=damage;operation=0;slot=auto;var=v+1;limit=80}
内置-攻速宝石:
Name: 攻速宝石
Require:
- WEAPON
Display: "&8&l&k||&c&l攻速宝石&8&l&k||"
Tips:
- "&c攻速宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其攻击速度属性+5%(上限80%)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzg1YzE5YmVhMWUwYWExYjNmOGY5ODUzYzc1NGI1MzQ2NzcxOGNlMGY5MGRlODg2ZmQ0ZWYyYjQyMDk5NTVjNSJ9fX0=
SuccessTip: "&f你使用了&c&l攻速宝石&f, 装备攻击速度属性&c&l+5%"
Rewards:
- Attribute{name=attack_speed;operation=1;slot=auto;var=v+0.05;limit=0.8}
内置-真攻速宝石:
Name: 真·攻速宝石
Require:
- WEAPON
Display: "&8&l&k||&c&l真·攻速宝石&8&l&k||"
Tips:
- "&c&l真·攻速宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其攻击速度属性+10%(上限300%)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzg1YzE5YmVhMWUwYWExYjNmOGY5ODUzYzc1NGI1MzQ2NzcxOGNlMGY5MGRlODg2ZmQ0ZWYyYjQyMDk5NTVjNSJ9fX0=
SuccessTip: "&f你使用了&c&l真·攻速宝石&f, 装备攻击速度属性&c&l+10%"
Rewards:
- Attribute{name=attack_speed;operation=1;slot=auto;var=v+0.1;limit=3}
内置-攻击击退宝石:
Name: 攻击击退宝石
Require:
- WEAPON
Display: "&8&l&k||&8&l攻击击退宝石&8&l&k||"
Tips:
- "&8攻击击退宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其击退属性+1(上限10)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDk5ZjI4MzMyYmNjMzQ5ZjQyMDIzYzI5ZTZlNjQxZjRiMTBhNmIxZTQ4NzE4Y2FlNTU3NDY2ZDUxZWI5MjIifX19
SuccessTip: "&f你使用了&8&l攻击击退宝石&f, 武器击退属性&8&l+1"
Rewards:
- Attribute{name=attack_knockback;operation=0;slot=auto;var=v+1;limit=10}
内置-幸运宝石:
Name: 幸运宝石
Require:
- WEAPON
- ARMOR
- TOOL
Display: "&8&l&k||&2&l幸运宝石&8&l&k||"
Tips:
- "&2幸运宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的装备"
- "&7即可为其幸运属性+1(上限10)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYWEwZWYyMDNhM2MzODM3ZmQ0YTEwZTFjM2YxOWNjNDBlNjQ5N2NiOWI5NzgwNzRkNDcyYzY1ZDQ4MmY2YjY3MyJ9fX0=
SuccessTip: "&f你使用了&2&l幸运宝石&f, 装备幸运属性&2&l+1"
Rewards:
- Attribute{name=luck;operation=0;slot=auto;var=v+1;limit=10}
内置-护甲宝石:
Name: 护甲宝石
Require:
- ARMOR
Display: "&8&l&k||&7&l护甲宝石&8&l&k||"
Tips:
- "&7护甲宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其护甲属性+1(上限10)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMmJkMzFhZDFlOTNmMTY2Zjk3ODU5Y2Q5ZjJiODBkYTUyYTgyOTQ0MDA5N2Y1ZDY3YThjMjEwZDEyMmI5ZDVlNSJ9fX0=
SuccessTip: "&f你使用了&7&l护甲宝石&f, 盔甲护甲属性&7&l+1"
Rewards:
- Attribute{name=armor;operation=0;slot=auto;var=v+1;limit=10}
内置-盔甲韧性宝石:
Name: 盔甲韧性宝石
Require:
- ARMOR
Display: "&8&l&k||&7&l盔甲韧性宝石&8&l&k||"
Tips:
- "&7盔甲韧性宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其盔甲韧性属性+1(上限10)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvN2I5MjRiMzdmZTMyOTQyNzNhNzQzODZjODc4Y2EyMTBmYzg5ZjQ3ODcwMjk0M2EwMjcyZTcxMzk1NjMwYmVkYSJ9fX0=
SuccessTip: "&f你使用了&7&l盔甲韧性宝石&f, 盔甲盔甲韧性属性&7&l+1"
Rewards:
- Attribute{name=armor_toughness;operation=0;slot=auto;var=v+1;limit=10}
内置-击退抗性宝石:
Name: 击退抗性宝石
Require:
- ARMOR
Display: "&8&l&k||&8&l击退抗性宝石&8&l&k||"
Tips:
- "&8击退抗性宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其击退抗性属性+1(上限10)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMWNiNzdhOWNlYmUxYzE5NjIxMDUyN2EwNjk4OTNiOGY2Mzk1NGIzNDc1NzIwZjgwNDBmZTEyMzNhYWY4OWYwZiJ9fX0=
SuccessTip: "&f你使用了&8&l击退抗性宝石&f, 盔甲击退抗性属性&8&l+1"
Rewards:
- Attribute{name=knockback_resistance;operation=0;slot=auto;var=v+0.1;limit=1}
File diff suppressed because it is too large Load Diff
+131
View File
@@ -0,0 +1,131 @@
# 下面是可镶嵌宝石
# 需要镶嵌槽位的宝石 应如下配置:
Version: 1.05
内置-单行槽位开槽粉尘:
Name: 单行槽位开槽粉尘
Require:
- WEAPON
- ARMOR
- TOOL
Display: "&8&l&k||&9&l单行槽位开槽粉尘&8&l&k||"
Tips:
- "&9&l单行槽位开槽粉尘 &7品质: &a&l传说"
- "&7在工作台内点击此粉尘, 再点击您的装备"
- "&7即可为装备开启&9蓝宝石&f槽(仅限一次)"
Material: CYAN_DYE
Glow: true
SuccessTip: "&f您为装备开启了&9&l蓝宝石&f槽"
Rewards:
- LoreAdd{lore=&f&l[&9蓝宝石&f&l]&7&l⬛&7&l⬛&7&l⬛;mode=after;locator="宝石槽";limit=1}
内置-单格槽位开槽粉尘:
Name: 单格槽位开槽粉尘
Require:
- WEAPON
- ARMOR
- TOOL
Display: "&8&l&k||&9&l单格槽位开槽粉尘&8&l&k||"
Tips:
- "&9&l单格槽位开槽粉尘 &7品质: &a&l传说"
- "&7需要槽位&9&l蓝宝石槽"
- "&7在工作台内点击此粉尘, 再点击您的装备"
- "&7即可为其附加一个蓝宝石格 (上限7个)"
Material: CYAN_DYE
Glow: true
SuccessTip: "&f您为装备开启了一个&9&l蓝宝石槽"
Rewards:
- LoreAdd{lore=&7&l⬛;mode=line;locator=&f&l[&9蓝宝石&f&l];limit=7;force=false}
内置-行内暴击几率蓝宝石:
Name: 行内暴击几率蓝宝石
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&9&l行内暴击几率蓝宝石&8&l&k||"
Tips:
- "&9&l行内暴击几率蓝石 &7品质: &f普通"
- "&7需要槽位&9蓝宝石槽"
- "&7在镶嵌台内镶嵌此宝石"
- "&7即可使装备暴击几率属性+10%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvM2Y3M2EzZGQ5NGE4OTk5NDc1N2RmOWJlNjMzMGNjM2U4NDgzOTIzMGRlOTFkNDViZmRhYzE5MjcyNzJmYyJ9fX0=
SuccessTip: "&f你使用了&9&l暴击几率蓝宝石&f, 装备暴击几率&9&l+10%"
Embed: 8
Rewards:
- LoreReplace{old=&7&l⬛;lore=&9&l⬛;mode=line;locator=&f&l[&9蓝宝石&f&l]}
- "LoreVar{lore=&7&l■ 暴击几率: +&a&l;locator=宝石属性;mode=after;var=v+10;inv=v-10;format=%.2f%%}"
内置-行内物理伤害蓝宝石:
Name: 行内物理伤害蓝宝石
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&9&l行内物理伤害蓝宝石&8&l&k||"
Tips:
- "&9&l行内物理伤害蓝石 &7品质: &f普通"
- "&7需要槽位&9蓝宝石槽"
- "&7在镶嵌台内镶嵌此宝石"
- "&7即可使装备物理伤害属性+10"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvM2Y3M2EzZGQ5NGE4OTk5NDc1N2RmOWJlNjMzMGNjM2U4NDgzOTIzMGRlOTFkNDViZmRhYzE5MjcyNzJmYyJ9fX0=
SuccessTip: "&f你使用了&9&l物理伤害蓝宝石&f, 装备物理伤害&9&l+10"
Embed: 8
Rewards:
- LoreReplace{old=&7&l⬛;lore=&9&l⬛;mode=line;locator=&f&l[&9蓝宝石&f&l]}
- "LoreVar{lore=&7&l■ 物理伤害: +&a&l;locator=宝石属性;mode=after;var=v+10;inv=v-10;format=%.2f}"
内置-多行槽位开槽粉尘:
Name: 多行槽位开槽粉尘
Require:
- WEAPON
- ARMOR
- TOOL
Display: "&8&l&k||&c&l多行槽位开槽粉尘&8&l&k||"
Tips:
- "&c&l多行槽位开槽粉尘 &7品质: &a&l传说"
- "&7在工作台内点击此粉尘"
- "&7再点击您的装备"
- "&7即可为装备添加一个红宝石槽(上限三个)"
Material: REDSTONE
Glow: true
SuccessTip: "&f您为装备开启了一个&c&l红宝石槽"
Rewards:
- LoreAdd{lore=&c<----红宝石槽---->;locator=宝石槽;mode=after;limit=3}
内置-槽内物理伤害红宝石:
Name: 槽内物理伤害红宝石
Require:
- SWORD
- _AXE
Display: "&8&l&k||&c&l槽内物理伤害宝石&8&l&k||"
Tips:
- "&c&l槽内物理伤害宝石 &7品质: &a&l史诗"
- "&7需要槽位&c红宝石槽"
- "&7在镶嵌台内镶嵌此宝石"
- "&7即可为武器物理伤害+10"
- "&7拆卸需花费1000点券"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTcwZjZjNDBjYzdiNTZhNWU3MDFjZmNjZDMxYzE0MzVhYmFiZmM0NWViMzNkODE1YTViNDEyMDNmMjExNWEwYiJ9fX0=
SuccessTip: "&f你使用了&c&l槽内物理伤害宝石&f, 武器物理伤害&c&l+10"
Embed: 5
Rewards:
- "LoreVar{lore=&c<-&7&l■ 物理伤害: +&a&l;locator=&c<----红宝石槽---->;mode=line;var=v+10;inv=v-10;format=%.2f&c->}"
内置-槽外物理伤害宝石:
Name: 槽外物理伤害红宝石
Require:
- SWORD
- _AXE
Display: "&8&l&k||&c&l槽外物理伤害宝石&8&l&k||"
Tips:
- "&c&l槽外物理伤害宝石 &7品质: &a&l史诗"
- "&7需要槽位&c红宝石槽"
- "&7在镶嵌台内镶嵌此宝石"
- "&7即可为武器物理伤害+10"
- "&7拆卸需花费1000点券"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTcwZjZjNDBjYzdiNTZhNWU3MDFjZmNjZDMxYzE0MzVhYmFiZmM0NWViMzNkODE1YTViNDEyMDNmMjExNWEwYiJ9fX0=
SuccessTip: "&f你使用了&c&l槽外物理伤害宝石&f, 武器物理伤害&c&l+10"
Embed: 5
Rewards:
- LoreReplace{old=&c<----红宝石槽---->;new=&c<&8&l&k||&c&l物理伤害宝石&8&l&k||&c>}
- "LoreVar{lore=&7&l■ 物理伤害: +&a&l;var=v+10;inv=v-10;format=%.2f;mode=last}"
+751
View File
@@ -0,0 +1,751 @@
#
# 以下是升级原版附魔的宝石
# 你可以在游戏内用mgem enchant查看附魔的名称
# 支持GoldenEnchant/EcoEnchant的特殊附魔. 你可以自行添加
Version: 1.05
内置-耐久宝石:
Name: 耐久宝石
Require:
- WEAPON
- ARMOR
- TOOL
- SHIELD
- FISHING_ROD
- FLINT_AND_STEEL
Display: "&8&l&k||&b&l耐久宝石&8&l&k||"
Tips:
- "&b耐久宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的装备"
- "&7即可为其耐久等级+1(上限7级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTExNWRjODhlMzIxNGMzODI0M2Q3ODJkNjNlZGIwYTZlMDYyOTFlYjZkYThlNjAwYzdlMmVhMzZlN2Y2MWIzMSJ9fX0=
SuccessTip: "&f你使用了&b&l耐久宝石&f, 物品耐久等级&b&l+1"
Rewards:
- Enchant{name=DURABILITY;limit=7}
内置-真耐久宝石:
Name: 真·耐久宝石
Require:
- WEAPON
- ARMOR
- TOOL
- SHIELD
- FISHING_ROD
- FLINT_AND_STEEL
Display: "&8&l&k||&b&l真·耐久宝石&8&l&k||"
Tips:
- "&b&l真·耐久宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的装备"
- "&7即可为其耐久等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTExNWRjODhlMzIxNGMzODI0M2Q3ODJkNjNlZGIwYTZlMDYyOTFlYjZkYThlNjAwYzdlMmVhMzZlN2Y2MWIzMSJ9fX0=
SuccessTip: "&f你使用了&b&l真·耐久宝石&f, 物品耐久等级&b&l+1"
Rewards:
- Enchant{name=DURABILITY;limit=15}
内置-时运宝石:
Name: 时运宝石
Require:
- TOOL
Display: "&8&l&k||&d&l时运宝石&8&l&k||"
Tips:
- "&d时运宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的工具"
- "&7即可为其时运等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZGI2OTc1YWY3MDcyNGQ2YTQ0ZmQ1OTQ2ZTYwYjI3MTc3MzdkZmRiNTQ1YjRkYWIxODkzMzUxYTljOWRkMTgzYyJ9fX0=
SuccessTip: "&f你使用了&d&l时运宝石&f, 工具时运等级&d&l+1"
Rewards:
- Enchant{name=LOOT_BONUS_BLOCKS;limit=15}
内置-真时运宝石:
Name: 真·时运宝石
Require:
- TOOL
Display: "&8&l&k||&d&l真·时运宝石&8&l&k||"
Tips:
- "&d&l真·时运宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的工具"
- "&7即可为其时运等级+1(上限40级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZGI2OTc1YWY3MDcyNGQ2YTQ0ZmQ1OTQ2ZTYwYjI3MTc3MzdkZmRiNTQ1YjRkYWIxODkzMzUxYTljOWRkMTgzYyJ9fX0=
SuccessTip: "&f你使用了&d&l真·时运宝石&f, 工具时运等级&d&l+1"
Rewards:
- Enchant{name=LOOT_BONUS_BLOCKS;limit=40}
内置-神时运宝石:
Name: 神·时运宝石
Require:
- TOOL
Display: "&8&l&k||&d&l神·时运宝石&8&l&k||"
Success: 80
Tips:
- "&d&l神·时运宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的工具"
- "&7即可为其时运等级+1(无上限)"
- "&7成功率: &a80%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZGI2OTc1YWY3MDcyNGQ2YTQ0ZmQ1OTQ2ZTYwYjI3MTc3MzdkZmRiNTQ1YjRkYWIxODkzMzUxYTljOWRkMTgzYyJ9fX0=
SuccessTip: "&f你使用了&d&l神·时运宝石&f, 工具时运等级&d&l+1"
Rewards:
- Enchant{name=LOOT_BONUS_BLOCKS;limit=512}
内置-锋利宝石:
Name: 锋利宝石
Require:
- _AXE
- SWORD
Display: "&8&l&k||&c&l锋利宝石&8&l&k||"
Tips:
- "&c锋利宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的剑/斧"
- "&7即可为其锋利等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMmIwNTVjODEwYmRkZmQxNjI2NGVjOGQ0MzljNDMyODNlMzViY2E3MWE1MDk4M2UxNWUzNjRjZDhhYjdjNjY4ZiJ9fX0=
SuccessTip: "&f你使用了&c&l锋利宝石&f, 剑/斧锋利等级&c&l+1"
Rewards:
- Enchant{name=DAMAGE_ALL;limit=15}
内置-真锋利宝石:
Name: 真·锋利宝石
Require:
- _AXE
- SWORD
Display: "&8&l&k||&c&l真·锋利宝石&8&l&k||"
Tips:
- "&c&l真·锋利宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的剑/斧"
- "&7即可为其锋利等级+1(上限40级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMmIwNTVjODEwYmRkZmQxNjI2NGVjOGQ0MzljNDMyODNlMzViY2E3MWE1MDk4M2UxNWUzNjRjZDhhYjdjNjY4ZiJ9fX0=
SuccessTip: "&f你使用了&c&l真·锋利宝石&f, 剑/斧锋利等级&c&l+1"
Rewards:
- Enchant{name=DAMAGE_ALL;limit=40}
内置-神锋利宝石:
Name: 神·锋利宝石
Require:
- _AXE
- SWORD
Success: 80
Display: "&8&l&k||&c&l神·锋利宝石&8&l&k||"
Tips:
- "&c&l神·锋利宝石 &7品质: &b史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的剑/斧"
- "&7即可为其锋利等级+1(无上限)"
- "&7成功率: &a80%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMmIwNTVjODEwYmRkZmQxNjI2NGVjOGQ0MzljNDMyODNlMzViY2E3MWE1MDk4M2UxNWUzNjRjZDhhYjdjNjY4ZiJ9fX0=
SuccessTip: "&f你使用了&c&l神·锋利宝石&f, 剑/斧锋利等级&c&l+1"
Rewards:
- Enchant{name=DAMAGE_ALL}
内置-抢夺宝石:
Name: 抢夺宝石
Require:
- _AXE
- SWORD
Display: "&8&l&k||&8&l抢夺宝石&8&l&k||"
Tips:
- "&8抢夺宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的剑/斧"
- "&7即可为其抢夺等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjI0MDcwYzliNjY1OWVkMjViMmNhMTI2OTE1ZjRkODgyMGZhZmNlNDMyNGVkOWE4ZjRiOGE1MDYzNDUzMDdmIn19fQ==
SuccessTip: "&f你使用了&8&l抢夺宝石&f, 剑/斧抢夺等级&8&l+1"
Rewards:
- Enchant{name=LOOT_BONUS_MOBS;limit=15}
内置-真抢夺宝石:
Name: 真·抢夺宝石
Require:
- _AXE
- SWORD
Display: "&8&l&k||&8&l真·抢夺宝石&8&l&k||"
Tips:
- "&8&l真·抢夺宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其抢夺等级+1(上限30级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjI0MDcwYzliNjY1OWVkMjViMmNhMTI2OTE1ZjRkODgyMGZhZmNlNDMyNGVkOWE4ZjRiOGE1MDYzNDUzMDdmIn19fQ==
SuccessTip: "&f你使用了&8&l真·抢夺宝石&f, 武器抢夺等级&8&l+1"
Rewards:
- Enchant{name=LOOT_BONUS_MOBS;limit=30}
内置-神抢夺宝石:
Name: 神·抢夺宝石
Require:
- _AXE
- SWORD
Success: 80
Display: "&8&l&k||&8&l神·抢夺宝石&8&l&k||"
Tips:
- "&8&l神·抢夺宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其抢夺等级+1(无上限)"
- "&7成功率: &a80%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjI0MDcwYzliNjY1OWVkMjViMmNhMTI2OTE1ZjRkODgyMGZhZmNlNDMyNGVkOWE4ZjRiOGE1MDYzNDUzMDdmIn19fQ==
SuccessTip: "&f你使用了&8&l神·抢夺宝石&f, 武器抢夺等级&8&l+1"
Rewards:
- Enchant{name=LOOT_BONUS_MOBS}
内置-力量宝石:
Name: 力量宝石
Require:
- BOW
Display: "&8&l&k||&e&l力量宝石&8&l&k||"
Tips:
- "&e力量宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的弓"
- "&7即可为其力量等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTIwNzE2MjAzZGEwMzljYWZjYTI0YmJkOWYzZTliZDVjNjk4NWMzYzI1NTI3YmNkNTA2ZDM4OGU5OWJiN2FmZSJ9fX0=
SuccessTip: "&f你使用了&e&l力量宝石&f, 弓力量等级&e&l+1"
Rewards:
- Enchant{name=ARROW_DAMAGE;limit=15}
内置-真力量宝石:
Name: 真·力量宝石
Require:
- BOW
Display: "&8&l&k||&e&l真·&l力量宝石&8&l&k||"
Tips:
- "&e&l真·力量宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的弓"
- "&7即可为其力量等级+1(上限40级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTIwNzE2MjAzZGEwMzljYWZjYTI0YmJkOWYzZTliZDVjNjk4NWMzYzI1NTI3YmNkNTA2ZDM4OGU5OWJiN2FmZSJ9fX0=
SuccessTip: "&f你使用了&e&l真·力量宝石&f, 弓力量等级&e&l+1"
Rewards:
- Enchant{name=ARROW_DAMAGE;limit=40}
内置-神力量宝石:
Name: 神·力量宝石
Require:
- BOW
Success: 80
Display: "&8&l&k||&e&l神·力量宝石&8&l&k||"
Tips:
- "&e&l神·力量宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的弓"
- "&7即可为其力量等级+1(无上限)"
- "&7成功率: &a80%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTIwNzE2MjAzZGEwMzljYWZjYTI0YmJkOWYzZTliZDVjNjk4NWMzYzI1NTI3YmNkNTA2ZDM4OGU5OWJiN2FmZSJ9fX0=
SuccessTip: "&f你使用了&e&l神·力量宝石&f, 弓力量等级&e&l+1"
Rewards:
- Enchant{name=ARROW_DAMAGE}
内置-荆棘宝石:
Name: 荆棘宝石
Require:
- ARMOR
Display: "&8&l&k||&2&l荆棘宝石&8&l&k||"
Tips:
- "&2荆棘宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其荆棘等级+1(上限10级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjc1N2NmYzdjMTFiMWU2ZjEwMjhiODA0NjJkMWU0MzAyZGNhNWFiODU5MjIyNWVhOGM4Yjc2MzljNzBlNjFmOSJ9fX0=
SuccessTip: "&f你使用了&2&l荆棘宝石&f, 盔甲荆棘等级&2&l+1"
Rewards:
- Enchant{name=THORNS;limit=10}
内置-真荆棘宝石:
Name: 真·荆棘宝石
Require:
- ARMOR
Display: "&8&l&k||&2&l真·荆棘宝石&8&l&k||"
Tips:
- "&2&l真·荆棘宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其荆棘等级+1(上限30级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjc1N2NmYzdjMTFiMWU2ZjEwMjhiODA0NjJkMWU0MzAyZGNhNWFiODU5MjIyNWVhOGM4Yjc2MzljNzBlNjFmOSJ9fX0=
SuccessTip: "&f你使用了&2&l真·荆棘宝石&f, 盔甲荆棘等级&2&l+1"
Rewards:
- Enchant{name=THORNS;limit=30}
内置-神荆棘宝石:
Name: 神·荆棘宝石
Require:
- ARMOR
Success: 80
Display: "&8&l&k||&2&l神·荆棘宝石&8&l&k||"
Tips:
- "&2&l神·荆棘宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其荆棘等级+1(无上限)"
- "&7成功率: &a80%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjc1N2NmYzdjMTFiMWU2ZjEwMjhiODA0NjJkMWU0MzAyZGNhNWFiODU5MjIyNWVhOGM4Yjc2MzljNzBlNjFmOSJ9fX0=
SuccessTip: "&f你使用了&2&l神·荆棘宝石&f, 盔甲荆棘等级&2&l+1"
Rewards:
- Enchant{name=THORNS}
内置-激流宝石:
Name: 激流宝石
Require:
- TRIDENT
Display: "&8&l&k||&9&l激流宝石&8&l&k||"
Tips:
- "&9激流宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的三叉戟"
- "&7即可为其激流等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTA5NjcxOTAzZDcwZDhhMWQ1ZDliY2Q1ODI0MWQ1ZGMxYTIzZjEzN2QyNTNjMjgyNTYxOTc2YTY4NTEyODMifX19
SuccessTip: "&f你使用了&9&l激流宝石&f, 三叉戟激流等级&9&l+1"
Rewards:
- Enchant{name=RIPTIDE;limit=15}
内置-真激流宝石:
Name: 真·激流宝石
Require:
- TRIDENT
Display: "&8&l&k||&9&l真·激流宝石&8&l&k||"
Tips:
- "&9&l真·激流宝石 &7品质: &a&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的三叉戟"
- "&7即可为其激流等级+1(上限40级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTA5NjcxOTAzZDcwZDhhMWQ1ZDliY2Q1ODI0MWQ1ZGMxYTIzZjEzN2QyNTNjMjgyNTYxOTc2YTY4NTEyODMifX19
SuccessTip: "&f你使用了&9&l真·激流宝石&f, 三叉戟激流等级&9&l+1"
Rewards:
- Enchant{name=RIPTIDE;limit=40}
内置-保护宝石:
Name: 保护宝石
Require:
- ARMOR
Display: "&8&l&k||&3&l保护宝石&8&l&k||"
Tips:
- "&3保护宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其保护等级+1(上限10级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzI0ZDIzZjBiYjNlNzUwYWU0ZmUxMGEyYTg0OTBlOGEwMjg4MDZiOGE5NTRjNDU3YmUzNzlkMDJiN2Q0NjUwMiJ9fX0=
SuccessTip: "&f你使用了&3&l保护宝石&f, 盔甲保护等级&3&l+1"
Rewards:
- Enchant{name=PROTECTION_ENVIRONMENTAL;limit=10}
内置-真保护宝石:
Name: 真·保护宝石
Require:
- ARMOR
Display: "&8&l&k||&3&l真·保护宝石&8&l&k||"
Tips:
- "&3&l真·保护宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的盔甲"
- "&7即可为其保护等级+1(上限20级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzI0ZDIzZjBiYjNlNzUwYWU0ZmUxMGEyYTg0OTBlOGEwMjg4MDZiOGE5NTRjNDU3YmUzNzlkMDJiN2Q0NjUwMiJ9fX0=
SuccessTip: "&f你使用了&3&l真·保护宝石&f, 盔甲保护等级&3&l+1"
Rewards:
- Enchant{name=PROTECTION_ENVIRONMENTAL;limit=20}
内置-冰霜行者宝石:
Name: 冰霜行者宝石
Require:
- BOOTS
Display: "&8&l&k||&9&l冰霜行者宝石&8&l&k||"
Tips:
- "&9&l冰霜行者宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的靴子"
- "&7即可为其冰霜行者等级+1(上限7级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzY5ZTBhYmEzNWQ2ODUzZWU1NGM4Njc5OWY3NTRhZTE2NjlkZDM5YTBlMmYzZDY5NzMxZjMzMWFkZWRlIn19fQ==
SuccessTip: "&f你使用了&9&l冰霜行者宝石&f, 靴子冰霜行者等级&9&l+1"
Rewards:
- Enchant{name=FROST_WALKER;limit=7}
内置-真冰霜行者宝石:
Name: 真·冰霜行者宝石
Require:
- BOOTS
Display: "&8&l&k||&9&l真·冰霜行者宝石&8&l&k||"
Tips:
- "&9&l真·冰霜行者宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的靴子"
- "&7即可为其冰霜行者等级+1(上限14级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYzY5ZTBhYmEzNWQ2ODUzZWU1NGM4Njc5OWY3NTRhZTE2NjlkZDM5YTBlMmYzZDY5NzMxZjMzMWFkZWRlIn19fQ==
SuccessTip: "&f你使用了&9&l真·冰霜行者宝石&f, 靴子冰霜行者等级&9&l+1"
Rewards:
- Enchant{name=FROST_WALKER;limit=14}
内置-海之眷顾宝石:
Name: 海之眷顾宝石
Require:
- FISHING_ROD
Display: "&8&l&k||&9&l海之眷顾宝石&8&l&k||"
Tips:
- "&9海之眷顾宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的钓鱼竿"
- "&7即可为其海之眷顾等级+1(上限10级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZWY5ZjMwMTZjN2U1NzAzOThkNWIwODVhY2Y2YmMxYjJjMTA5NWVmZTBmNTFhN2I2MDVjODFmOTIwOWZiMzc2NyJ9fX0=
SuccessTip: "&f你使用了&9&l海之眷顾宝石&f, 钓竿海之眷顾等级&9&l+1"
Rewards:
- Enchant{name=LUCK;limit=10}
内置-真海之眷顾宝石:
Name: 真·海之眷顾宝石
Require:
- FISHING_ROD
Display: "&8&l&k||&9&l真·海之眷顾宝石&8&l&k||"
Tips:
- "&9&l真·海之眷顾宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的钓鱼竿"
- "&7即可为其海之眷顾等级+1(上限20级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZWY5ZjMwMTZjN2U1NzAzOThkNWIwODVhY2Y2YmMxYjJjMTA5NWVmZTBmNTFhN2I2MDVjODFmOTIwOWZiMzc2NyJ9fX0=
SuccessTip: "&f你使用了&9&l真·海之眷顾宝石&f, 钓竿海之眷顾等级&9&l+1"
Rewards:
- Enchant{name=LUCK;limit=24}
内置-效率宝石:
Name: 效率宝石
Require:
- AXE
- SHOVEL
Display: "&8&l&k||&a&l效率宝石&8&l&k||"
Tips:
- "&a&l效率宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的镐/斧/铲"
- "&7即可为其效率等级+1(上限10级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjg1YmNmN2Y4MmQzNGRiODlhOTVhZGRmOGU1MzI1M2UyZDk1NTRjNmZkMmYyZTM5ZTI0MzYyZDI0M2EwY2NmNyJ9fX0=
SuccessTip: "&f你使用了&a&l效率宝石&f, 工具效率等级&a&l+1"
Rewards:
- Enchant{name=DIG_SPEED;limit=10}
内置-真效率宝石:
Name: 真·效率宝石
Require:
- AXE
- SHOVEL
Display: "&8&l&k||&a&l真·效率宝石&8&l&k||"
Tips:
- "&a&l真·效率宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的镐/斧/铲"
- "&7即可为其效率等级+1(上限33级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjg1YmNmN2Y4MmQzNGRiODlhOTVhZGRmOGU1MzI1M2UyZDk1NTRjNmZkMmYyZTM5ZTI0MzYyZDI0M2EwY2NmNyJ9fX0=
SuccessTip: "&f你使用了&a&l真·效率宝石&f, 工具效率等级&a&l+1"
Rewards:
- Enchant{name=DIG_SPEED;limit=33}
内置-火焰附加宝石:
Name: 火焰附加宝石
Require:
- SWORD
Display: "&8&l&k||&6&l火焰附加宝石&8&l&k||"
Tips:
- "&6火焰附加宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的剑"
- "&7即可为其火焰附加等级+1(上限10级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzBhNDRkNTFlY2M3OWY2OTRjZmQ2MDIyOGM4ODQyODg0OGNhNjE4ZTM2YTY1OWE0MTZlOTI0NmQ4NDFhZWM4In19fQ==
SuccessTip: "&f你使用了&6&l火焰附加宝石&f, 剑火焰附加等级&6&l+1"
Rewards:
- Enchant{name=FIRE_ASPECT;limit=10}
内置-真火焰附加宝石:
Name: 真·火焰附加宝石
Require:
- SWORD
Display: "&8&l&k||&6&l真·火焰附加宝石&8&l&k||"
Tips:
- "&6&l真·火焰附加宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的剑"
- "&7即可为其火焰附加等级+1(上限25级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzBhNDRkNTFlY2M3OWY2OTRjZmQ2MDIyOGM4ODQyODg0OGNhNjE4ZTM2YTY1OWE0MTZlOTI0NmQ4NDFhZWM4In19fQ==
SuccessTip: "&f你使用了&6&l真·火焰附加宝石&f, 剑火焰附加等级&6&l+1"
Rewards:
- Enchant{name=FIRE_ASPECT;limit=25}
内置-神火焰附加宝石:
Name: 神·火焰附加宝石
Require:
- SWORD
Success: 80
Display: "&8&l&k||&6&l神·火焰附加宝石&8&l&k||"
Tips:
- "&6&l神·火焰附加宝石 &7品质: &b史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的剑"
- "&7即可为其火焰附加等级+1(无上限)"
- "&7成功率: &a80%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzBhNDRkNTFlY2M3OWY2OTRjZmQ2MDIyOGM4ODQyODg0OGNhNjE4ZTM2YTY1OWE0MTZlOTI0NmQ4NDFhZWM4In19fQ==
SuccessTip: "&f你使用了&6&l神·火焰附加宝石&f, 剑火焰附加等级&6&l+1"
Rewards:
- Enchant{name=FIRE_ASPECT}
内置-穿刺宝石:
Name: 穿刺宝石
Require:
- TRIDENT
Display: "&8&l&k||&3&l穿刺宝石&8&l&k||"
Tips:
- "&3穿刺宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的三叉戟"
- "&7即可为其穿刺等级+1(上限20级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNWVjZDk3MjdlOWQ5ZjJjMDczOTkzNGVmMWU2M2QxZDg5YzkyMzY5NDIxY2FmYmFlYTczM2NjZTM1YzNiNjc2OCJ9fX0=
SuccessTip: "&f你使用了&3&l穿刺宝石&f, 三叉戟穿刺等级&3&l+1"
Rewards:
- Enchant{name=CHANNELING;limit=20}
内置-忠诚宝石:
Name: 忠诚宝石
Require:
- TRIDENT
Display: "&8&l&k||&3&l忠诚宝石&8&l&k||"
Tips:
- "&3忠诚宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的三叉戟"
- "&7即可为其忠诚等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjc2OGVkYzI4ODUzYzQyNDRkYmM2ZWViNjNiZDQ5ZWQ1NjhjYTIyYTg1MmEwYTU3OGIyZjJmOWZhYmU3MCJ9fX0=
SuccessTip: "&f你使用了&3&l忠诚宝石&f, 三叉戟忠诚等级&3&l+1"
Rewards:
- Enchant{name=LOYALTY;limit=15}
内置-击退宝石:
Name: 击退宝石
Require:
- SWORD
- _AXE
Display: "&8&l&k||&8&l击退宝石&8&l&k||"
Tips:
- "&8&l击退宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的剑/斧"
- "&7即可为其击退等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDk5ZjI4MzMyYmNjMzQ5ZjQyMDIzYzI5ZTZlNjQxZjRiMTBhNmIxZTQ4NzE4Y2FlNTU3NDY2ZDUxZWI5MjIifX19
SuccessTip: "&f你使用了&8&l击退宝石&f, 剑/斧击退等级&8&l+1"
Rewards:
- Enchant{name=KNOCKBACK;limit=15}
内置-真击退宝石:
Name: 真·击退宝石
Require:
- _AXE
- SWORD
Display: "&8&l&k||&8&l真·击退宝石&8&l&k||"
Tips:
- "&8&l真·击退宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的剑/斧"
- "&7即可为其击退等级+1(上限30级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDk5ZjI4MzMyYmNjMzQ5ZjQyMDIzYzI5ZTZlNjQxZjRiMTBhNmIxZTQ4NzE4Y2FlNTU3NDY2ZDUxZWI5MjIifX19
SuccessTip: "&f你使用了&8&l真·击退宝石&f, 剑/斧击退等级&8&l+1"
Rewards:
- Enchant{name=KNOCKBACK;limit=30}
内置-冲击宝石:
Name: 冲击宝石
Require:
- BOW
Display: "&8&l&k||&8&l冲击宝石&8&l&k||"
Tips:
- "&8&l冲击宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的弓"
- "&7即可为其冲击等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzU0Nzk1MTA0MjJiMWM1ZGNjNzdmNzVmZGMzMzQ2ZWQ0ZDlkYmJjYzFlODg1YjRhMjk5MmEyNzM3MzM2NDZhOSJ9fX0=
SuccessTip: "&f你使用了&8&l冲击宝石&f, 弓冲击等级&8&l+1"
Rewards:
- Enchant{name=ARROW_KNOCKBACK;limit=15}
内置-真冲击宝石:
Name: 真·冲击宝石
Require:
- BOW
Display: "&8&l&k||&8&l真·冲击宝石&8&l&k||"
Tips:
- "&8&l真·冲击宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的弓"
- "&7即可为其冲击等级+1(上限30级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzU0Nzk1MTA0MjJiMWM1ZGNjNzdmNzVmZGMzMzQ2ZWQ0ZDlkYmJjYzFlODg1YjRhMjk5MmEyNzM3MzM2NDZhOSJ9fX0=
SuccessTip: "&f你使用了&8&l真·冲击宝石&f, 弓冲击等级&8&l+1"
Rewards:
- Enchant{name=ARROW_KNOCKBACK;limit=30}
内置-水下呼吸宝石:
Name: 水下呼吸宝石
Require:
- HELMET
Display: "&8&l&k||&b&l水下呼吸宝石&8&l&k||"
Tips:
- "&b&l水下呼吸宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的头盔"
- "&7即可为其水下呼吸等级+1(上限6级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvY2QwZjFkMGE3NDU5OTFlNjYxNGZmZDdkYzlmODBlMzc0NTU5NmU4MTg5Nzc5NmYyODAyNjJmMTJmMTVlYjRhZCJ9fX0=
SuccessTip: "&f你使用了&b&l水下呼吸宝石&f, 头盔水下呼吸等级&b&l+1"
Rewards:
- Enchant{name=OXYGEN;limit=6}
内置-真水下呼吸宝石:
Name: 真·水下呼吸宝石
Require:
- HELMET
Display: "&8&l&k||&b&l真·水下呼吸宝石&8&l&k||"
Tips:
- "&b&l真·水下呼吸宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的头盔"
- "&7即可为其水下呼吸等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvY2QwZjFkMGE3NDU5OTFlNjYxNGZmZDdkYzlmODBlMzc0NTU5NmU4MTg5Nzc5NmYyODAyNjJmMTJmMTVlYjRhZCJ9fX0=
SuccessTip: "&f你使用了&b&l真·水下呼吸宝石&f, 头盔水下呼吸等级&b&l+1"
Rewards:
- Enchant{name=OXYGEN;limit=15}
内置-亡灵杀手宝石:
Name: 亡灵杀手宝石
Require:
- _AXE
- SWORD
Display: "&8&l&k||&7&l亡灵杀手宝石&8&l&k||"
Tips:
- "&7亡灵杀手宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其亡灵杀手等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjRmODVkNmFlNzdlMTJlNTFlZTQzYWRlZjQzYzNjMzI0ZTRlMzc4NDg5OWI5MDM3NjE4MmZlN2Y0N2E1ZSJ9fX0=
SuccessTip: "&f你使用了&7&l亡灵杀手宝石&f, 武器亡灵杀手等级&7&l+1"
Rewards:
- Enchant{name=DAMAGE_UNDEAD;limit=15}
内置-真亡灵杀手宝石:
Name: 真·亡灵杀手宝石
Require:
- _AXE
- SWORD
Display: "&8&l&k||&7&l真·亡灵杀手宝石&8&l&k||"
Tips:
- "&7&l真·亡灵杀手宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其亡灵杀手等级+1(上限40级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjRmODVkNmFlNzdlMTJlNTFlZTQzYWRlZjQzYzNjMzI0ZTRlMzc4NDg5OWI5MDM3NjE4MmZlN2Y0N2E1ZSJ9fX0=
SuccessTip: "&f你使用了&7&l真·亡灵杀手宝石&f, 武器亡灵杀手等级&7&l+1"
Rewards:
- Enchant{name=DAMAGE_UNDEAD;limit=40}
内置-神亡灵杀手宝石:
Name: 神·亡灵杀手宝石
Require:
- _AXE
- SWORD
Success: 80
Display: "&8&l&k||&7&l神·亡灵杀手宝石&8&l&k||"
Tips:
- "&7&l神·亡灵杀手宝石 &7品质: &b史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其亡灵杀手等级+1(无上限)"
- "&7成功率: &a80%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjRmODVkNmFlNzdlMTJlNTFlZTQzYWRlZjQzYzNjMzI0ZTRlMzc4NDg5OWI5MDM3NjE4MmZlN2Y0N2E1ZSJ9fX0=
SuccessTip: "&f你使用了&7&l神·亡灵杀手宝石&f, 武器亡灵杀手等级&7&l+1"
Rewards:
- Enchant{name=DAMAGE_UNDEAD}
内置-节肢杀手宝石:
Name: 节肢杀手宝石
Require:
- _AXE
- SWORD
Display: "&8&l&k||&8&l节肢杀手宝石&8&l&k||"
Tips:
- "&8节肢杀手宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其节肢杀手等级+1(上限15级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODhkYWY1NGFjOGMwNjFhODMzMjY5N2ZkMDM4YjlmM2FmOGVjMzhkMTY4NDllNTI3MTI1ZDI5NzNkZTYyZTliIn19fQ==
SuccessTip: "&f你使用了&8&l节肢杀手宝石&f, 武器节肢杀手等级&8&l+1"
Rewards:
- Enchant{name=DAMAGE_ARTHROPODS;limit=15}
内置-真节肢杀手宝石:
Name: 真·节肢杀手宝石
Require:
- _AXE
- SWORD
Display: "&8&l&k||&8&l真·节肢杀手宝石&8&l&k||"
Tips:
- "&8&l真·节肢杀手宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其节肢杀手等级+1(上限40级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODhkYWY1NGFjOGMwNjFhODMzMjY5N2ZkMDM4YjlmM2FmOGVjMzhkMTY4NDllNTI3MTI1ZDI5NzNkZTYyZTliIn19fQ==
SuccessTip: "&f你使用了&8&l真·节肢杀手宝石&f, 武器节肢杀手等级&8&l+1"
Rewards:
- Enchant{name=DAMAGE_ARTHROPODS;limit=40}
内置-神节肢杀手宝石:
Name: 神·节肢杀手宝石
Require:
- _AXE
- SWORD
Success: 80
Display: "&8&l&k||&8&l神·节肢杀手宝石&8&l&k||"
Tips:
- "&8&l神·节肢杀手宝石 &7品质: &b史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的武器"
- "&7即可为其节肢杀手等级+1(无上限)"
- "&7成功率: &a80%"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODhkYWY1NGFjOGMwNjFhODMzMjY5N2ZkMDM4YjlmM2FmOGVjMzhkMTY4NDllNTI3MTI1ZDI5NzNkZTYyZTliIn19fQ==
SuccessTip: "&f你使用了&8&l神·节肢杀手宝石&f, 武器节肢杀手等级&8&l+1"
Rewards:
- Enchant{name=DAMAGE_ARTHROPODS}
内置-横扫之刃宝石:
Name: 横扫之刃宝石
Require:
- SWORD
Display: "&8&l&k||&c&l横扫之刃宝石&8&l&k||"
Tips:
- "&c横扫之刃宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石"
- "&7再点击您的剑"
- "&7即可为其横扫之刃等级+1(上限10级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTVkNzZkOTBiMzc4MDgzZDE0Nzc1NjgwNTA1ZGRiMWU2YzJjNmRjZjRkZGU3ZjliMWY1ODgwOWJlYzZjNjVjOCJ9fX0=
SuccessTip: "&f你使用了&c&l横扫之刃宝石&f, 武器横扫之刃等级&c&l+1"
Rewards:
- Enchant{name=SWEEPING_EDGE;limit=10}
内置-灵魂疾行宝石:
Name: 灵魂疾行宝石
Require:
- BOOTS
Display: "&8&l&k||&3&l灵魂疾行宝石&8&l&k||"
Tips:
- "&3&l灵魂疾行宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的靴子"
- "&7即可为其灵魂疾行等级+1(上限7级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7ImlkIjoiOTM4NDM1M2E0YWUwNGZlNDkzOTcxYjU4N2RmNzdmMTIiLCJ0eXBlIjoiU0tJTiIsInVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTU4MGU1Y2Q5NzBjMWRlZDk3M2QwYzEzOWJlNDE4YjA3NTVlMDcxMzM0NWQ3MTZlZDU5NDI0NGE1M2U2ZTEzZCIsInByb2ZpbGVJZCI6IjgwMThhYjAwYjJhZTQ0Y2FhYzliZjYwZWY5MGY0NWU1IiwidGV4dHVyZUlkIjoiOTU4MGU1Y2Q5NzBjMWRlZDk3M2QwYzEzOWJlNDE4YjA3NTVlMDcxMzM0NWQ3MTZlZDU5NDI0NGE1M2U2ZTEzZCJ9fSwiY2FwZSI6bnVsbCwic2tpbiI6eyJpZCI6IjkzODQzNTNhNGFlMDRmZTQ5Mzk3MWI1ODdkZjc3ZjEyIiwidHlwZSI6IlNLSU4iLCJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzk1ODBlNWNkOTcwYzFkZWQ5NzNkMGMxMzliZTQxOGIwNzU1ZTA3MTMzNDVkNzE2ZWQ1OTQyNDRhNTNlNmUxM2QiLCJwcm9maWxlSWQiOiI4MDE4YWIwMGIyYWU0NGNhYWM5YmY2MGVmOTBmNDVlNSIsInRleHR1cmVJZCI6Ijk1ODBlNWNkOTcwYzFkZWQ5NzNkMGMxMzliZTQxOGIwNzU1ZTA3MTMzNDVkNzE2ZWQ1OTQyNDRhNTNlNmUxM2QifX0=
SuccessTip: "&f你使用了&3&l灵魂疾行宝石&f, 靴子灵魂疾行等级&3&l+1"
Rewards:
- Enchant{name=SOUL_SPEED;limit=10}
内置-真灵魂疾行宝石:
Name: 真·灵魂疾行宝石
Require:
- BOOTS
Display: "&8&l&k||&3&l真·灵魂疾行宝石&8&l&k||"
Tips:
- "&3&l真·灵魂疾行宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石"
- "&7再点击您的靴子"
- "&7即可为其灵魂疾行等级+1(上限30级)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7ImlkIjoiOTM4NDM1M2E0YWUwNGZlNDkzOTcxYjU4N2RmNzdmMTIiLCJ0eXBlIjoiU0tJTiIsInVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTU4MGU1Y2Q5NzBjMWRlZDk3M2QwYzEzOWJlNDE4YjA3NTVlMDcxMzM0NWQ3MTZlZDU5NDI0NGE1M2U2ZTEzZCIsInByb2ZpbGVJZCI6IjgwMThhYjAwYjJhZTQ0Y2FhYzliZjYwZWY5MGY0NWU1IiwidGV4dHVyZUlkIjoiOTU4MGU1Y2Q5NzBjMWRlZDk3M2QwYzEzOWJlNDE4YjA3NTVlMDcxMzM0NWQ3MTZlZDU5NDI0NGE1M2U2ZTEzZCJ9fSwiY2FwZSI6bnVsbCwic2tpbiI6eyJpZCI6IjkzODQzNTNhNGFlMDRmZTQ5Mzk3MWI1ODdkZjc3ZjEyIiwidHlwZSI6IlNLSU4iLCJ1cmwiOiJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzk1ODBlNWNkOTcwYzFkZWQ5NzNkMGMxMzliZTQxOGIwNzU1ZTA3MTMzNDVkNzE2ZWQ1OTQyNDRhNTNlNmUxM2QiLCJwcm9maWxlSWQiOiI4MDE4YWIwMGIyYWU0NGNhYWM5YmY2MGVmOTBmNDVlNSIsInRleHR1cmVJZCI6Ijk1ODBlNWNkOTcwYzFkZWQ5NzNkMGMxMzliZTQxOGIwNzU1ZTA3MTMzNDVkNzE2ZWQ1OTQyNDRhNTNlNmUxM2QifX0=
SuccessTip: "&f你使用了&3&l真·灵魂疾行宝石&f, 靴子灵魂疾行等级&3&l+1"
Rewards:
- Enchant{name=SOUL_SPEED;limit=30}
+12
View File
@@ -0,0 +1,12 @@
示例生命宝石:
Name: 示例生命宝石
Type: ITEM_GEM
Material: EMERALD
Display: "&a示例生命宝石"
Tips:
- "&7用于验证 CMagicGem 基础功能"
Require:
- ARMOR
Success: 100
Rewards:
- "Attribute{name=health;operation=0;slot=auto;var=v+1;inv=v-1;limit=10}"
+169
View File
@@ -0,0 +1,169 @@
#
# 以下宝石演示了内置功能
Version: 1.05
内置-无限宝石:
Name: 无限宝石
Require:
- WEAPON
- ARMOR
- TOOL
- SHIELD
- FISHING_ROD
- FLINT_AND_STEEL
Display: "&8&l&k||&9&l无限宝石&8&l&k||"
Tips:
- "&9&l无限宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的装备"
- "&7即可为您的装备添加无限耐久"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTExNWRjODhlMzIxNGMzODI0M2Q3ODJkNjNlZGIwYTZlMDYyOTFlYjZkYThlNjAwYzdlMmVhMzZlN2Y2MWIzMSJ9fX0=
SuccessTip: "&f你使用了&9&l无限宝石&f, 装备已被设置为&9&l无法破坏"
Rewards:
- Unbreakable
内置-原版耐久修复石:
Name: 修复宝石
Require:
- WEAPON
- ARMOR
- TOOL
- SHIELD
- FISHING_ROD
- FLINT_AND_STEEL
Display: "&8&l&k||&a&l修复宝石&8&l&k||"
Tips:
- "&c&l修复宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石,"
- "&7再点击您的装备"
- "&7即可使装备耐久恢复1000点"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTg5MDFmNzE0MzRkNTM5MjA3NDc2OTRmNjgyZjVlNTNiOGY3NDQ4M2YyNjljMzg0YzY5MzZiN2Q4NjU4MiJ9fX0=
SuccessTip: "&f你使用了&a&l修复宝石&f, 装备耐久&a&l+1000"
Rewards:
- Durability{amount=1000}
内置-失败宝石:
Name: 败者宝石
Success: 1
Require:
- WEAPON
- ARMOR
- TOOL
- SHIELD
- FISHING_ROD
- FLINT_AND_STEEL
Display: "&8&l&k||&c&l败者宝石&8&l&k||"
Tips:
- "&c&l败者宝石 &7品质: &a&l励志"
- "&7在工作台内点击此宝石,"
- "&7再点击您的装备"
- "&7即可以99%的概率感受强化失败的滋味"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTc3NWQzZWM4MmJhNjE5NjQ5YTIzYjg1MDg3NTI3NWY1NTliNjRlNTNjYjgxNmM0NDhiZWVjNzkyYjQ5YzAifX19
SuccessTip: "&a你竟然成功了&f, 肯定是服务器出了bug(狗头)"
Rewards:
- Name{name=&8&l&k||&a&l天命之子&8&l&k||}
内置-精准时运切换宝石:
Name: 精准时运切换宝石
Require:
- AXE
- SHOVEL
Display: "&8&l&k||&a&l精准/时运切换宝石&8&l&k||"
Tips:
- "&a&l精准时运切换宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的镐子/斧头/铲子"
- "&7即可为其加上精准/时运切换功能"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzEyZDJmOTMxMDQ0NDFjYTU2NmY3YjcxOGMzZGQwMzgyY2IzOGY5Njc4YjVjYjE4MWVhMjQ4N2EyZjdmODI2YiJ9fX0=
SuccessTip: "&f你使用了&a&l精准/时运切换宝石&f, 工具添加&a&l精准/时运切换功能"
Rewards:
- LoreAdd{lore=&2Shift+右键切换模式(精准/时运);limit=1}
内置-忠诚激流切换宝石:
Name: 忠诚激流切换宝石
Require:
- TRIDENT
Display: "&8&l&k||&a&l忠诚/激流切换宝石&8&l&k||"
Tips:
- "&a&l忠诚/激流切换宝石 &7品质: &b&l史诗"
- "&7在工作台内点击此宝石"
- "&7再点击您的三叉戟"
- "&7即可为其加上忠诚/激流切换功能"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzEyZDJmOTMxMDQ0NDFjYTU2NmY3YjcxOGMzZGQwMzgyY2IzOGY5Njc4YjVjYjE4MWVhMjQ4N2EyZjdmODI2YiJ9fX0=
SuccessTip: "&f你使用了&a&l忠诚/激流切换宝石&f, 三叉戟添加&a&l忠诚/激流切换功能"
Rewards:
- LoreAdd{lore=&2Shift+左键切换模式(忠诚/激流);limit=1}
内置-巨无霸汉堡:
Name: 巨无霸汉堡
Require:
- 啥也不能打
Display: "&8&l&k||&6&l巨无霸汉堡&8&l&k||"
Tips:
- "&6&l巨无霸汉堡 &7品质: &a&l传说"
- "&7拿在手中或戴在头上"
- "&7即可获得饱和效果"
- "&6&l[&c&lBUFF&6&l] &b&l饱和"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTQxMWNiNTRhYWExZTNkYjU1NWM3ODVmNmI3NWJlMWJmOGU2OGIyOGU1Y2ZjNTljOWE4NzY4OTRmNjFjZGMxNyJ9fX0=
Rewards:
- Empty
内置-附魔隐藏粉尘:
Name: 附魔隐藏粉尘
Require:
- WEAPON
- ARMOR
- TOOL
- SHIELD
- FISHING_ROD
Display: "&8&l&k||&7&l附魔隐藏粉尘&8&l&k||"
Tips:
- "&7在镶嵌台上使用/取下此粉尘"
- "&7即可隐藏/显示装备的附魔"
SuccessTip: "&f你使用了&7&l附魔隐藏粉尘&f, 装备附魔已隐藏"
RemoveTip: "&f你取下了&7&l附魔隐藏粉尘&f, 装备附魔已显示"
Material: GUNPOWDER
Glow: true
Embed: 1
Rewards:
- ItemFlag{HIDE_ENCHANTS}
内置-属性隐藏粉尘:
Name: 属性隐藏粉尘
Require:
- WEAPON
- ARMOR
- TOOL
- SHIELD
- FISHING_ROD
Display: "&8&l&k||&9&l属性隐藏粉尘&8&l&k||"
Tips:
- "&7在镶嵌台上使用/取下此粉尘"
- "&7即可隐藏/显示装备的蓝字/绿字属性"
SuccessTip: "&f你使用了&9&l属性隐藏粉尘&f, 装备属性已隐藏"
RemoveTip: "&f你取下了&9&l属性隐藏粉尘&f, 装备属性已显示"
Material: SUGAR
Glow: true
Embed: 1
Rewards:
- ItemFlag{HIDE_ATTRIBUTES}
内置-技能隐藏粉尘:
Name: 技能隐藏粉尘
Require:
- WEAPON
- ARMOR
- TOOL
- SHIELD
- FISHING_ROD
Display: "&8&l&k||&6&l技能隐藏粉尘&8&l&k||"
Tips:
- "&7在镶嵌台上使用/取下此技能"
- "&7即可隐藏/显示装备的药水或技能"
SuccessTip: "&f你使用了&6&l技能隐藏粉尘&f, 装备技能已隐藏"
RemoveTip: "&f你取下了&6&l技能隐藏粉尘&f, 装备技能已显示"
Material: GLOWSTONE_DUST
Glow: true
Embed: 1
Rewards:
- SkillToNBT
File diff suppressed because it is too large Load Diff
+60
View File
@@ -0,0 +1,60 @@
#
# MMOItem/MythicMobs系列宝石
# 本宝石修改MMOITEMS_ATTACK_DAMAGE. 可以强化MMOItem的攻击伤害属性
# 仅供演示 不做生产用途
# 要制作更多的MMOItem属性强化石, 请在游戏内使用mgem nbt和mgem nbt读取属性名
Version: 1.05
内置-MMOItems物理伤害强化石:
Name: MMOItems物理伤害宝石
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&lMMOItems物理伤害宝石&8&l&k||"
Tips:
- "&c&lMMOItems物理伤害宝石 &7品质: &a&l传说"
- "&7在工作台内点击此宝石,"
- "&7再点击您的装备"
- "&7即可使装备物理伤害属性+2(上限40)"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTcwZjZjNDBjYzdiNTZhNWU3MDFjZmNjZDMxYzE0MzVhYmFiZmM0NWViMzNkODE1YTViNDEyMDNmMjExNWEwYiJ9fX0=
SuccessTip: "&f你使用了&c&lMMOItems物理伤害强化石&f, 装备物理伤害&c&l+2.0"
Rewards:
- NBTDouble{name=MMOITEMS_ATTACK_DAMAGE;var=v+2;limit=v>=0&&v<=40}
- "LoreVar{lore=&7&l■ 物理伤害: &a&l;var=v+2;mode=last}"
内置-MMOItems闪现技能添加强化宝石:
Name: 闪现宝石
Embed: true
Require:
- WEAPON
- TOOL
Display: "&8&l&k||&e&lMMOItems闪现宝石&8&l&k||"
Tips:
- "&e&lMMOItems闪现宝石 &7品质: &a&l史诗"
- "&7在工作台内点击此宝石,"
- "&7再点击您的装备"
- "&7即可使装备闪现技能+1"
- "&7可花费&e1000金币&7拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODJjZGUwNjhlOTlhNGY5OGMzMWY4N2I0Y2MwNmJlMTRiMjI5YWNhNGY3MjgxYTQxNmM3ZTJmNTUzMjIzZGI3NCJ9fX0=
SuccessTip: "&f你使用了&e&lMMOItems闪现宝石&f, 装备闪现技能等级&e&l+1"
Rewards:
- MMOAbility{type=blink;mode=RIGHT_CLICK;cooldown=60{var=v-2;inv=v+2};range=10{var=v+2;inv=v-2}}
- "LoreAdd{lore=&f右键使用技能: &b&l闪现;limit=1}"
- Money{amount=-1000}
内置-MythicMobs默认剑宝石:
Name: MythicMobs默认剑宝石
Require:
- SWORD
Display: "&8&l&k||&c&lMythicMobs默认剑宝石&8&l&k||"
Tips:
- "&c&lMythicMobs默认剑宝石 &7品质: &f普通"
- "&7在工作台内点击此宝石,"
- "&7再点击您的装备"
- "&7即可将装备识别为MythicMobs默认的骷髅剑"
- "&7MythicMob默认的骷髅剑没有技能"
- "&7所以这个宝石没有任何效果"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvM2E3Y2RhOTAwNGZjMTk3ZDY2YWZiYzJiMDAzYTViOWVmMTNjZjQ2MDBiMWZjNzQ5MDA2NzU5MGYwNDcxODFlIn19fQ==
SuccessTip: "&f你使用了&c&lMythicMobs默认剑宝石&f, 装备变成MythicMobs中的&c&l骷髅剑"
Rewards:
- MythicItem{name=SkeletonKingSword}
- LoreAdd{lore=&c=======MythicMobs技能=======}
- LoreAdd{lore=&f骷髅剑没有技能, 所以这把武器废了(滑稽)}
+213
View File
@@ -0,0 +1,213 @@
# 玩家直接使用的宝石
Version: 1.05
内置-点券兑换券-500:
Name: 点券兑换券-500
Type: PlayerGem
Display: "&8&l&k||&9&l点券兑换券&8&l&k||"
Tips:
- "&7获得&9500&f点券"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTc4NDRhNDU1NGY5MzYzZDY1OTkwNmQ2MzEyODE4YTExMjJhMWY5YTkyYTIzMjYyYjFlNjc3MDIyMzJiYjcifX19
Rewards:
- Point{amount=500} $onSuccess
内置-点券兑换券-1000:
Name: 点券兑换券-1000
Type: PlayerGem
Display: "&8&l&k||&9&l点券兑换券&8&l&k||"
Tips:
- "&7获得&91000&f点券"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTc4NDRhNDU1NGY5MzYzZDY1OTkwNmQ2MzEyODE4YTExMjJhMWY5YTkyYTIzMjYyYjFlNjc3MDIyMzJiYjcifX19
Rewards:
- Point{amount=1000} $onSuccess
内置-点券兑换券-2000:
Name: 点券兑换券-2000
Type: PlayerGem
Display: "&8&l&k||&9&l点券兑换券&8&l&k||"
Tips:
- "&7获得&92000&f点券"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTc4NDRhNDU1NGY5MzYzZDY1OTkwNmQ2MzEyODE4YTExMjJhMWY5YTkyYTIzMjYyYjFlNjc3MDIyMzJiYjcifX19
Rewards:
- Point{amount=2000} $onSuccess
内置-点券兑换券-10000:
Name: 点券兑换券-10000
Type: PlayerGem
Display: "&8&l&k||&9&l点券兑换券&8&l&k||"
Tips:
- "&7获得&910000&f点券"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTc4NDRhNDU1NGY5MzYzZDY1OTkwNmQ2MzEyODE4YTExMjJhMWY5YTkyYTIzMjYyYjFlNjc3MDIyMzJiYjcifX19
Rewards:
- Point{amount=10000} $onSuccess
内置-随机点券兑换券:
Name: 随机点券兑换券-500-10000
Type: RandomGem
Display: "&8&l&k||&9&l随机点券兑换券&8&l&k||"
Tips:
- "&7使用后按如下概率获得点券"
- "&7 - 1% 获得 &910000 &f点券"
- "&7 - 10% 获得 &92000 &f点券"
- "&7 - 20% 获得 &91000 &f点券"
- "&7 - 69% 获得 &9500 &f点券"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTc4NDRhNDU1NGY5MzYzZDY1OTkwNmQ2MzEyODE4YTExMjJhMWY5YTkyYTIzMjYyYjFlNjc3MDIyMzJiYjcifX19
Gems:
点券兑换券-10000: 1
点券兑换券-2000: 10
点券兑换券-1000: 20
点券兑换券-500: 69
内置-随机点券兑换券2:
Name: 随机点券兑换券-100-3000
Type: PlayerGem
Display: "&8&l&k||&9&l随机点券兑换券&8&l&k||"
Tips:
- "&7随机获得&9100&f-&93000&f点券"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTc4NDRhNDU1NGY5MzYzZDY1OTkwNmQ2MzEyODE4YTExMjJhMWY5YTkyYTIzMjYyYjFlNjc3MDIyMzJiYjcifX19
Rewards:
- Point{amount=100+2900*$RANDOM()} $onSuccess
内置-金币兑换券-1000:
Name: 金币兑换券-1000
Type: PlayerGem
Display: "&8&l&k||&e&l金币兑换券&8&l&k||"
Tips:
- "&7获得&e1000&f金币"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjM3Y2FlNWM1MWViMTU1OGVhODI4ZjU4ZTBkZmY4ZTZiN2IwYjFhMTgzZDczN2VlY2Y3MTQ2NjE3NjEifX19
Rewards:
- Money{amount=1000} $onSuccess
内置-金币兑换券-2000:
Name: 金币兑换券-2000
Type: PlayerGem
Display: "&8&l&k||&e&l金币兑换券&8&l&k||"
Tips:
- "&7获得&e2000&f金币"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjM3Y2FlNWM1MWViMTU1OGVhODI4ZjU4ZTBkZmY4ZTZiN2IwYjFhMTgzZDczN2VlY2Y3MTQ2NjE3NjEifX19
Rewards:
- Money{amount=2000} $onSuccess
内置-金币兑换券-5000:
Name: 金币兑换券-5000
Type: PlayerGem
Display: "&8&l&k||&e&l金币兑换券&8&l&k||"
Tips:
- "&7获得&e5000&f金币"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjM3Y2FlNWM1MWViMTU1OGVhODI4ZjU4ZTBkZmY4ZTZiN2IwYjFhMTgzZDczN2VlY2Y3MTQ2NjE3NjEifX19
Rewards:
- Money{amount=5000} $onSuccess
内置-金币兑换券-10000:
Name: 金币兑换券-10000
Type: PlayerGem
Display: "&8&l&k||&e&l金币兑换券&8&l&k||"
Tips:
- "&7获得&e10000&f金币"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjM3Y2FlNWM1MWViMTU1OGVhODI4ZjU4ZTBkZmY4ZTZiN2IwYjFhMTgzZDczN2VlY2Y3MTQ2NjE3NjEifX19
Rewards:
- Money{amount=10000} $onSuccess
内置-金币兑换券-100000:
Name: 金币兑换券-100000
Type: PlayerGem
Display: "&8&l&k||&e&l金币兑换券&8&l&k||"
Tips:
- "&7获得&e100000&f金币"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjM3Y2FlNWM1MWViMTU1OGVhODI4ZjU4ZTBkZmY4ZTZiN2IwYjFhMTgzZDczN2VlY2Y3MTQ2NjE3NjEifX19
Rewards:
- Money{amount=100000} $onSuccess
内置-随机金币兑换券:
Name: 随机金币兑换券-1000-100000
Type: RandomGem
Display: "&8&l&k||&e&l随机金币兑换券&8&l&k||"
Tips:
- "&7使用后按如下概率获得金币"
- "&7 - 1% 获得 &e100000 &f金币"
- "&7 - 10% 获得 &e10000 &f金币"
- "&7 - 30% 获得 &e2000 &f金币"
- "&7 - 59% 获得 &e1000 &f金币"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjM3Y2FlNWM1MWViMTU1OGVhODI4ZjU4ZTBkZmY4ZTZiN2IwYjFhMTgzZDczN2VlY2Y3MTQ2NjE3NjEifX19
Gems:
金币兑换券-100000: 1
金币兑换券-10000: 10
金币兑换券-2000: 30
金币兑换券-1000: 59
内置-随机金币兑换券2:
Name: 随机金币兑换券-1000-10000
Type: PlayerGem
Display: "&8&l&k||&e&l金币兑换券&8&l&k||"
Tips:
- "&7随机获得&e1000&f-&e10000&f金币"
- "&7(&c!&7) 右键使用"
SuccessTip: "none"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjM3Y2FlNWM1MWViMTU1OGVhODI4ZjU4ZTBkZmY4ZTZiN2IwYjFhMTgzZDczN2VlY2Y3MTQ2NjE3NjEifX19
Rewards:
- Money{amount=1000+9000*$RANDOM()} $onSuccess
内置-蛇胆:
Name: 蛇胆
Type: PlayerGem
Display: "&8&l&k||&c&l蛇胆&8&l&k||"
Tips:
- "&7饮用后永久增加&c1&7点生命"
- "&7上限为30点"
Material: "POTION"
Color: "MAROON"
Eat: true
SuccessTip: "&f您饮用了&c&l蛇胆&f, 生命上限永久&c&l+1"
Rewards:
- MaxHealth{amount=1;limit=30} $onSuccess
内置-龙胆:
Name: 龙胆
Type: PlayerGem
Display: "&8&l&k||&6&l龙胆&8&l&k||"
Tips:
- "&7饮用后永久增加&62&7点生命"
- "&7上限为60点"
Material: "POTION"
Color: "GOLD"
Eat: true
SuccessTip: "&f您饮用了&6&l龙胆&f, 生命上限永久&6&l+2"
Rewards:
- MaxHealth{amount=2;limit=60} $onSuccess
内置-经验药水:
Name: 经验药水
Type: PlayerGem
Display: "&8&l&k||&a&l经验药水&8&l&k||"
Tips:
- "&7饮用后立即增加&a50&7级经验"
Material: "POTION"
Color: "LIME"
Eat: true
SuccessTip: "&f您饮用了&a&l经验药水&f, 经验等级&a&l+50"
Rewards:
- ExpLevel{amount=50} $onSuccess
+760
View File
@@ -0,0 +1,760 @@
# 下面是一些内置药水
Version: 1.05
内置-发光药水:
Name: 发光药水
Embed: 1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&e&l发光药水&8&l&k||"
Tips:
- "&e&l发光药水 &7品质: &a&l传说"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加发光BUFF"
- "&7可花费1000点券拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZDY3ZGYzNWI3YTgwMjdkZGM4M2M5MTlkNzNmMGEzOTk4YWQ1NGViMGQ5NzNlZjcxNmFlNTA1YzY3NTU4MTBmNyJ9fX0=
SuccessTip: "&f你使用了&e&l发光药水&f, 装备添加&e&l发光&fBUFF"
Rewards:
- LoreAdd{lore=&6&l[&c&lBUFF&6&l] &b&l发光}
- Point{amount=-1000} $onRemove
内置-缓降药水:
Name: 缓降药水
Embed: 1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&7&l缓降药水&8&l&k||"
Tips:
- "&7&l缓降药水 &7品质: &a&l传说"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加缓降BUFF"
- "&7可花费500点券拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvZjAxYzI5M2RmNGM5ZGIzYjZmNjNlMzQzNDIwMWU2ZjQ1NGRkMmRkZjAxMDkyZDFhMjQ3NjczZDAzMDBiMjJiOCJ9fX0=
SuccessTip: "&f你使用了&7&l缓降药水&f, 装备添加&7&l缓降&fBUFF"
Rewards:
- LoreAdd{lore=&6&l[&c&lBUFF&6&l] &b&l缓降}
- Point{amount=-500} $onRemove
内置-潮涌能量药水:
Name: 潮涌能量药水
Embed: 1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&9&l潮涌能量药水&8&l&k||"
Tips:
- "&9&l潮涌能量药水 &7品质: &a&l传说"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加潮涌能量BUFF"
- "&7可花费2000点券拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvN2FmNmRjNjdjZTc1YjcyMjJhYzM5ZTA1MzZlMDkwNzI0OWY4NTUxNmVmZDEzODViZWM5MGJlMzNkZDUzZGUwNyJ9fX0=
SuccessTip: "&f你使用了&9&l潮涌能量药水&f, 装备添加&9&l潮涌能量&fBUFF"
Rewards:
- LoreAdd{lore=&6&l[&c&lBUFF&6&l] &b&l潮涌能量}
- Point{amount=-2000} $onRemove
内置-防火药水:
Name: 防火药水
Embed: 1
Require:
- ARMOR
Display: "&8&l&k||&6&l防火药水&8&l&k||"
Tips:
- "&6&l防火药水 &7品质: &b&l史诗"
- "&7在工作台内点击此药水,"
- "&7再点击您的盔甲"
- "&7即可为装备添加防火BUFF"
- "&7可花费5000点券拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTRiOTNmNDE1MzJmYzI3ZWY3ZGY5MzNjMTgxYWMzMTY2ZDU2MDM3ZDVjNWZmNzVkMmU4NWFmZTM3Y2EyNTdkMyJ9fX0=
SuccessTip: "&f你使用了&6&l防火药水&f, 装备添加&6&l防火&fBUFF"
Rewards:
- LoreAdd{lore=&6&l[&c&lBUFF&6&l] &b&l防火}
- Point{amount=-5000} $onRemove
内置-水下呼吸药水:
Name: 水下呼吸药水
Embed: 1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&b&l水下呼吸药水&8&l&k||"
Tips:
- "&b&l水下呼吸药水 &7品质: &b&l史诗"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加水下呼吸BUFF"
- "&7可花费2000点券拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNWRjOWZmMTg0YWU3NjdkM2NiZmQ5YzNhYTJjN2U4OGIxMGY5YjU5MTI5N2ZmNjc2ZGE2MzlmYjQ0NDYyMzhjOCJ9fX0=
SuccessTip: "&f你使用了&b&l水下呼吸药水&f, 装备添加&b&l水下呼吸&fBUFF"
Rewards:
- LoreAdd{lore=&6&l[&c&lBUFF&6&l] &b&l水下呼吸}
- Point{amount=-2000} $onRemove
内置-夜视药水:
Name: 夜视药水
Embed: 1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&9&l夜视药水&8&l&k||"
Tips:
- "&9&l夜视药水 &7品质: &b&l史诗"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加夜视BUFF"
- "&7可花费2000点券拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNTYyOWEzZDNmNTE5ZGNkMzg1YjU5OTJlZjM0YmZhOGE2OGMzNzQ0NDc1MGI5NGVkZTVjMmY1MjFlMTczNDIifX19
SuccessTip: "&f你使用了&9&l夜视药水&f, 装备添加&9&l夜视&fBUFF"
Rewards:
- LoreAdd{lore=&6&l[&c&lBUFF&6&l] &b&l夜视}
- Point{amount=-2000} $onRemove
内置-隐身药水:
Name: 隐身药水
Embed: 1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&7&l隐身药水&8&l&k||"
Tips:
- "&7隐身药水 &7品质: &f普通"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加隐身BUFF"
- "&7可花费500点券拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNjJmZDQ4MWU2OTNmYzgyNTRmZDYyNWI1ZDE2NjMwYTA2YTZmZGY2MTIwZDA2YjIzNDVlM2U2ZGRiNTE0N2Y5YSJ9fX0=
SuccessTip: "&f你使用了&7&l隐身药水&f, 装备添加&7&l隐身&fBUFF"
Rewards:
- LoreAdd{lore=&6&l[&c&lBUFF&6&l] &b&l隐身}
- Point{amount=-500} $onRemove
内置-生命恢复药水1:
Name: 生命恢复药水1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&l生命恢复 I&8&l&k||"
Tips:
- "&c生命恢复药水 &f等级 1"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加生命恢复BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTFlN2ViMmU0NjFlOTZlNjMxY2JhMGMwY2RhYTU0NDg4MDYzMDJlZGFlOTFiNjFkYWZjMjgxYWU1ODRkOCJ9fX0=
SuccessTip: "&f你使用了&c&l生命恢复药水&f, 装备添加&c&l生命恢复&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l生命恢复 ;var=v+1;format=%.0f;limit=v<=1;mode=last}
内置-生命恢复药水2:
Name: 生命恢复药水2
Success: 80
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&l生命恢复 II&8&l&k||"
Tips:
- "&c生命恢复药水 &f等级 &a&l2"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加生命恢复BUFF"
- "&7成功率 &a80%&7, &c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTFlN2ViMmU0NjFlOTZlNjMxY2JhMGMwY2RhYTU0NDg4MDYzMDJlZGFlOTFiNjFkYWZjMjgxYWU1ODRkOCJ9fX0=
SuccessTip: "&f你使用了&c&l生命恢复药水&f, 装备添加&c&l生命恢复II&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l生命恢复 ;var=v<2?2:3;format=%.0f;limit=v<=2;mode=last}
内置-生命恢复药水3:
Name: 生命恢复药水3
Success: 60
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&l生命恢复 III&8&l&k||"
Tips:
- "&c生命恢复药水 &f等级 &b&l3"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加生命恢复BUFF"
- "&7成功率 &e60%&7, &c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTFlN2ViMmU0NjFlOTZlNjMxY2JhMGMwY2RhYTU0NDg4MDYzMDJlZGFlOTFiNjFkYWZjMjgxYWU1ODRkOCJ9fX0=
SuccessTip: "&f你使用了&c&l生命恢复药水&f, 装备添加&c&l生命恢复III&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l生命恢复 ;var=v<3?3:4;format=%.0f;limit=v<=3;mode=last}
内置-生命恢复药水4:
Name: 生命恢复药水4
Success: 40
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&l生命恢复 IV&8&l&k||"
Tips:
- "&c生命恢复药水 &f等级 &c&l4"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加生命恢复BUFF"
- "&7成功率 &660%&7, &c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOTFlN2ViMmU0NjFlOTZlNjMxY2JhMGMwY2RhYTU0NDg4MDYzMDJlZGFlOTFiNjFkYWZjMjgxYWU1ODRkOCJ9fX0=
SuccessTip: "&f你使用了&c&l生命恢复药水&f, 装备添加&c&l生命恢复IV&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l生命恢复 ;var=v<4?4:5;format=%.0f;limit=v<=4;mode=last}
内置-抗性提升药水1:
Name: 抗性提升药水1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&7&l抗性提升 I&8&l&k||"
Tips:
- "&7抗性提升药水 &f等级 1"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加抗性提升BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWRhNGU3OWVlZDEzODY1NjcwOGJjMzI1YzQ0MjE5Mjc1MTdjMWMwOGNjOTM5YTc2MDNiOGQxNWE5ZjI0ODU0ZCJ9fX0=
SuccessTip: "&f你使用了&7&l抗性提升药水&f, 装备添加&7&l抗性提升&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l抗性提升 ;var=v+1;format=%.0f;limit=v<=1;mode=last}
内置-抗性提升药水2:
Name: 抗性提升药水2
Success: 90
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&7&l抗性提升 II&8&l&k||"
Tips:
- "&7抗性提升药水 &f等级 &a&l2"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加抗性提升BUFF"
- "&7成功率 &a90%&7, &c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWRhNGU3OWVlZDEzODY1NjcwOGJjMzI1YzQ0MjE5Mjc1MTdjMWMwOGNjOTM5YTc2MDNiOGQxNWE5ZjI0ODU0ZCJ9fX0=
SuccessTip: "&f你使用了&7&l抗性提升药水&f, 装备添加&7&l抗性提升II&fBUFF"
FailTip: "&c抗性提升II药水使用失败……脸有点黑呀"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l抗性提升 ;var=v<2?2:3;format=%.0f;limit=v<=2;mode=last}
内置-抗性提升药水3:
Name: 抗性提升药水3
Success: 50
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&8&l抗性提升 III&8&l&k||"
Tips:
- "&8抗性提升药水 &f等级 &b&l3"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加抗性提升BUFF"
- "&7成功率 &e50%&7, &c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWRhNGU3OWVlZDEzODY1NjcwOGJjMzI1YzQ0MjE5Mjc1MTdjMWMwOGNjOTM5YTc2MDNiOGQxNWE5ZjI0ODU0ZCJ9fX0=
SuccessTip: "&f你使用了&8&l抗性提升药水&f, 装备添加&8&l抗性提升III&fBUFF"
FailTip: "&c抗性提升药水使用失败, 再接再厉!"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l抗性提升 ;var=v<3?3:4;format=%.0f;limit=v<=3;mode=last}
内置-抗性提升药水4:
Name: 抗性提升药水4
Success: 10
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&8&l抗性提升 IV&8&l&k||"
Tips:
- "&8抗性提升药水 &f等级 &c&l4"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加抗性提升BUFF"
- "&7成功率 &c10%&7, &c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvOWRhNGU3OWVlZDEzODY1NjcwOGJjMzI1YzQ0MjE5Mjc1MTdjMWMwOGNjOTM5YTc2MDNiOGQxNWE5ZjI0ODU0ZCJ9fX0=
SuccessTip: "&f你使用了&8&l抗性提升药水&f, 装备添加&8&l抗性提升IV&fBUFF"
FailTip: "&c抗性提升IV使用失败, 请下次再来!"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l抗性提升 ;var=v<4?4:5;format=%.0f;limit=v<=4;mode=last}
- Command{commands=bc 恭喜 %player_name% 成功为装备添加抗性提升IV!;console=true} $onSuccess
内置-英雄的信物1:
Name: 英雄的信物
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&9&l英雄的信物I&8&l&k||"
Tips:
- "&9英雄的信物 &f等级 I"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可使装备村庄英雄BUFF+1(上限3)"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDU0MzVjMzdlZGFmZTE2NWM5MmFlMzczNmUxNWM0NjRhZDYyMWQ2MTIyMDQzNWQxYTM5YWRiNDcwNWZiODg2In19fQ==
SuccessTip: "&f你使用了&8&l英雄的信物&f, 装备&8&l村庄英雄&fBUFF等级&8+1"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l村庄英雄 ;var=v+1;format=%.0f;limit=v<=3;mode=last}
内置-英雄的信物2:
Name: 英雄的信物2
Success: 70
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&9&l英雄的信物II&8&l&k||"
Tips:
- "&9&l英雄的信物 &f等级 &a&lII"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可使装备村庄英雄BUFF+1(上限5)"
- "&7成功率 &a70%&7, &c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDU0MzVjMzdlZGFmZTE2NWM5MmFlMzczNmUxNWM0NjRhZDYyMWQ2MTIyMDQzNWQxYTM5YWRiNDcwNWZiODg2In19fQ==
SuccessTip: "&f你使用了&8&l英雄的信物&f, 装备&8&l村庄英雄&fBUFF等级&8+1"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l村庄英雄 ;var=v+1;format=%.0f;limit=v<=5;mode=last}
内置-英雄的信物3:
Name: 英雄的信物3
Success: 40
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&9&l英雄的信物III&8&l&k||"
Tips:
- "&9&l英雄的信物 &f等级 &b&lIII"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可使装备村庄英雄BUFF+1(上限8)"
- "&7成功率 &640%&7, &c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDU0MzVjMzdlZGFmZTE2NWM5MmFlMzczNmUxNWM0NjRhZDYyMWQ2MTIyMDQzNWQxYTM5YWRiNDcwNWZiODg2In19fQ==
SuccessTip: "&f你使用了&8&l英雄的信物&f, 装备&8&l村庄英雄&fBUFF等级&8+1"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l村庄英雄 ;var=v+1;format=%.0f;limit=v<=8;mode=last}
内置-英雄的信物4:
Name: 英雄的信物4
Success: 20
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&9&l英雄的信物IV&8&l&k||"
Tips:
- "&9&l英雄的信物 &f等级 &c&lIV"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可使装备村庄英雄BUFF+1(上限10)"
- "&7成功率 &c20%&7, &c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDU0MzVjMzdlZGFmZTE2NWM5MmFlMzczNmUxNWM0NjRhZDYyMWQ2MTIyMDQzNWQxYTM5YWRiNDcwNWZiODg2In19fQ==
SuccessTip: "&f你使用了&8&l英雄的信物&f, 装备&8&l村庄英雄&fBUFF等级&8+1"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l村庄英雄 ;var=v+1;format=%.0f;limit=v<=10;mode=last}
内置-跳跃提升药水1:
Name: 跳跃提升药水1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&d&l跳跃提升 I&8&l&k||"
Tips:
- "&d跳跃提升药水 &f等级 1"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加跳跃提升BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjNkMzU2Yjg2MTE4ZmY4YzQ4YzExOWVmM2QyYWM5MGMxYWQxMzkyODkzMDIyZjgwYjlkYzE2OGMzNmRiMmE1NiJ9fX0=
SuccessTip: "&f你使用了&d&l跳跃提升药水&f, 装备添加&d&l跳跃提升&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l跳跃提升 ;var=v+1;format=%.0f;limit=v<=1;mode=last}
内置-跳跃提升药水2:
Name: 跳跃提升药水2
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&d&l跳跃提升 II&8&l&k||"
Tips:
- "&d跳跃提升药水 &f等级 &a&l2"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加跳跃提升BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjNkMzU2Yjg2MTE4ZmY4YzQ4YzExOWVmM2QyYWM5MGMxYWQxMzkyODkzMDIyZjgwYjlkYzE2OGMzNmRiMmE1NiJ9fX0=
SuccessTip: "&f你使用了&d&l跳跃提升药水&f, 装备添加&d&l跳跃提升II&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l跳跃提升 ;var=v<2?2:3;format=%.0f;limit=v<=2;mode=last}
内置-跳跃提升药水3:
Name: 跳跃提升药水3
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&d&l跳跃提升 III&8&l&k||"
Tips:
- "&d跳跃提升药水 &f等级 &b&l3"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加跳跃提升BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYjNkMzU2Yjg2MTE4ZmY4YzQ4YzExOWVmM2QyYWM5MGMxYWQxMzkyODkzMDIyZjgwYjlkYzE2OGMzNmRiMmE1NiJ9fX0=
SuccessTip: "&f你使用了&d&l跳跃提升药水&f, 装备添加&d&l跳跃提升III&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l跳跃提升 ;var=v<3?3:4;format=%.0f;limit=v<=3;mode=last}
内置-速度药水1:
Name: 速度药水1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&a&l速度 I&8&l&k||"
Tips:
- "&d速度药水 &f等级 1"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加速度BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzNmYTkxZGUzMWUyZGRkYTI5Y2E0MTljMDhiMzRhNWEyZDZiNGIxYTFiNTliMjM2Y2RjMTRjYjEzMWRjYjgwNyJ9fX0=
SuccessTip: "&f你使用了&a&l速度药水&f, 装备添加&a&l速度&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l速度 ;var=v+1;format=%.0f;limit=v<=1;mode=last}
内置-速度药水2:
Name: 速度药水2
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&a&l速度 II&8&l&k||"
Tips:
- "&a速度药水 &f等级 &a&l2"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加速度BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzNmYTkxZGUzMWUyZGRkYTI5Y2E0MTljMDhiMzRhNWEyZDZiNGIxYTFiNTliMjM2Y2RjMTRjYjEzMWRjYjgwNyJ9fX0=
SuccessTip: "&f你使用了&a&l速度药水&f, 装备添加&a&l速度II&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l速度 ;var=v<2?2:3;format=%.0f;limit=v<=2;mode=last}
内置-速度药水3:
Name: 速度药水3
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&a&l速度 III&8&l&k||"
Tips:
- "&a速度药水 &f等级 &b&l3"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加速度BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMzNmYTkxZGUzMWUyZGRkYTI5Y2E0MTljMDhiMzRhNWEyZDZiNGIxYTFiNTliMjM2Y2RjMTRjYjEzMWRjYjgwNyJ9fX0=
SuccessTip: "&f你使用了&a&l速度药水&f, 装备添加&a&l速度III&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l速度 ;var=v<3?3:4;format=%.0f;limit=v<=3;mode=last}
内置-急迫药水1:
Name: 急迫药水1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&6&l急迫 I&8&l&k||"
Tips:
- "&6急迫药水 &f等级 1"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加急迫BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTBhNjQ0NDdkNGI3N2U1ZTEwNTU2MWM3MjgyMTZlNjAwNjE3ZmI0Zjg1N2YxYTE1MGY1MmM5MTdhODVjYTc0MyJ9fX0=
SuccessTip: "&f你使用了&6&l急迫药水&f, 装备添加&6&l急迫&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l急迫 ;var=v+1;format=%.0f;limit=v<=1;mode=last}
内置-急迫药水2:
Name: 急迫药水2
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&6&l急迫 II&8&l&k||"
Tips:
- "&6急迫药水 &f等级 &6&l2"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加急迫BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTBhNjQ0NDdkNGI3N2U1ZTEwNTU2MWM3MjgyMTZlNjAwNjE3ZmI0Zjg1N2YxYTE1MGY1MmM5MTdhODVjYTc0MyJ9fX0=
SuccessTip: "&f你使用了&6&l急迫药水&f, 装备添加&6&l急迫II&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l急迫 ;var=v<2?2:3;format=%.0f;limit=v<=2;mode=last}
内置-急迫药水3:
Name: 急迫药水3
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&6&l急迫 III&8&l&k||"
Tips:
- "&6急迫药水 &f等级 &b&l3"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加急迫BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYTBhNjQ0NDdkNGI3N2U1ZTEwNTU2MWM3MjgyMTZlNjAwNjE3ZmI0Zjg1N2YxYTE1MGY1MmM5MTdhODVjYTc0MyJ9fX0=
SuccessTip: "&f你使用了&6&l急迫药水&f, 装备添加&6&l急迫III&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l急迫 ;var=v<3?3:4;format=%.0f;limit=v<=3;mode=last}
内置-力量药水1:
Name: 力量药水1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&l力量 I&8&l&k||"
Tips:
- "&c力量药水 &f等级 1"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加力量BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTBhMTRhOGNmMDkyZDBjZDE2OTMzYmUyZGM5ZDQ0MGZiMzBiNTE1OTIxZmM2YjY4Y2RjYzE5YWJmODY0NmZjZiJ9fX0=
SuccessTip: "&f你使用了&c&l力量药水&f, 装备添加&c&l力量&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l力量 ;var=v+1;format=%.0f;limit=v<=1;mode=last}
内置-力量药水2:
Name: 力量药水2
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&l力量 II&8&l&k||"
Tips:
- "&c力量药水 &f等级 &a&l2"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加力量BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTBhMTRhOGNmMDkyZDBjZDE2OTMzYmUyZGM5ZDQ0MGZiMzBiNTE1OTIxZmM2YjY4Y2RjYzE5YWJmODY0NmZjZiJ9fX0=
SuccessTip: "&f你使用了&c&l力量药水&f, 装备添加&c&l力量II&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l力量 ;var=v<2?2:3;format=%.0f;limit=v<=2;mode=last}
内置-力量药水3:
Name: 力量药水3
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&l力量 III&8&l&k||"
Tips:
- "&c力量药水 &f等级 &b&l3"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加力量BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTBhMTRhOGNmMDkyZDBjZDE2OTMzYmUyZGM5ZDQ0MGZiMzBiNTE1OTIxZmM2YjY4Y2RjYzE5YWJmODY0NmZjZiJ9fX0=
SuccessTip: "&f你使用了&c&l力量药水&f, 装备添加&c&l力量III&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l力量 ;var=v<3?3:4;format=%.0f;limit=v<=3;mode=last}
内置-生命提升药水1:
Name: 生命提升药水1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&l生命提升 I&8&l&k||"
Tips:
- "&c生命提升药水 &f等级 1"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加生命提升BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTBhMTRhOGNmMDkyZDBjZDE2OTMzYmUyZGM5ZDQ0MGZiMzBiNTE1OTIxZmM2YjY4Y2RjYzE5YWJmODY0NmZjZiJ9fX0=
SuccessTip: "&f你使用了&c&l生命提升药水&f, 装备添加&c&l生命提升&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l生命提升 ;var=v+1;format=%.0f;limit=v<=1;mode=last}
内置-生命提升药水2:
Name: 生命提升药水2
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&l生命提升 II&8&l&k||"
Tips:
- "&c生命提升药水 &f等级 &a&l2"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加生命提升BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTBhMTRhOGNmMDkyZDBjZDE2OTMzYmUyZGM5ZDQ0MGZiMzBiNTE1OTIxZmM2YjY4Y2RjYzE5YWJmODY0NmZjZiJ9fX0=
SuccessTip: "&f你使用了&c&l生命提升药水&f, 装备添加&c&l生命提升II&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l生命提升 ;var=v<2?2:3;format=%.0f;limit=v<=2;mode=last}
内置-生命提升药水3:
Name: 生命提升药水3
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&c&l生命提升 III&8&l&k||"
Tips:
- "&c生命提升药水 &f等级 &b&l3"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加生命提升BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvMTBhMTRhOGNmMDkyZDBjZDE2OTMzYmUyZGM5ZDQ0MGZiMzBiNTE1OTIxZmM2YjY4Y2RjYzE5YWJmODY0NmZjZiJ9fX0=
SuccessTip: "&f你使用了&c&l生命提升药水&f, 装备添加&c&l生命提升III&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l生命提升 ;var=v<3?3:4;format=%.0f;limit=v<=3;mode=last}
内置-伤害吸收药水1:
Name: 伤害吸收药水1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&e&l伤害吸收 I&8&l&k||"
Tips:
- "&e伤害吸收药水 &f等级 1"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加伤害吸收BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDlmNDBlZjc3ZmYzMzlhN2MzYWNhOGVlYWRmMTVlNjMyYmU2YzI1NzRlNmNiYTJlNzFjMmNjNzBkYzNiYmU2NCJ9fX0=
SuccessTip: "&f你使用了&e&l伤害吸收药水&f, 装备添加&e&l伤害吸收&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l伤害吸收 ;var=v+1;format=%.0f;limit=v<=1;mode=last}
内置-伤害吸收药水2:
Name: 伤害吸收药水2
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&e&l伤害吸收 II&8&l&k||"
Tips:
- "&e伤害吸收药水 &f等级 &a&l2"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加伤害吸收BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDlmNDBlZjc3ZmYzMzlhN2MzYWNhOGVlYWRmMTVlNjMyYmU2YzI1NzRlNmNiYTJlNzFjMmNjNzBkYzNiYmU2NCJ9fX0=
SuccessTip: "&f你使用了&e&l伤害吸收药水&f, 装备添加&e&l伤害吸收II&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l伤害吸收 ;var=v<2?2:3;format=%.0f;limit=v<=2;mode=last}
内置-伤害吸收药水3:
Name: 伤害吸收药水3
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&e&l伤害吸收 III&8&l&k||"
Tips:
- "&e伤害吸收药水 &f等级 &b&l3"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加伤害吸收BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDlmNDBlZjc3ZmYzMzlhN2MzYWNhOGVlYWRmMTVlNjMyYmU2YzI1NzRlNmNiYTJlNzFjMmNjNzBkYzNiYmU2NCJ9fX0=
SuccessTip: "&f你使用了&e&l伤害吸收药水&f, 装备添加&e&l伤害吸收III&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l伤害吸收 ;var=v<3?3:4;format=%.0f;limit=v<=3;mode=last}
内置-幸运药水1:
Name: 幸运药水1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&2&l幸运 I&8&l&k||"
Tips:
- "&2幸运药水 &f等级 1"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加幸运BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODlkYWVmN2VmNjExMDU3YWZkOWY5NDA0NDRjMzBmZmJkNWQ1NDY1OGQzYjFjNjczNjJhNTc5NzY1ZTI4NzIzNSJ9fX0=
SuccessTip: "&f你使用了&2&l幸运药水&f, 装备添加&2&l幸运&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l幸运 ;var=v+1;format=%.0f;limit=v<=1;mode=last}
内置-幸运药水2:
Name: 幸运药水2
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&2&l幸运 II&8&l&k||"
Tips:
- "&2幸运药水 &f等级 &a&l2"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加幸运BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODlkYWVmN2VmNjExMDU3YWZkOWY5NDA0NDRjMzBmZmJkNWQ1NDY1OGQzYjFjNjczNjJhNTc5NzY1ZTI4NzIzNSJ9fX0=
SuccessTip: "&f你使用了&2&l幸运药水&f, 装备添加&2&l幸运II&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l幸运 ;var=v<2?2:3;format=%.0f;limit=v<=2;mode=last}
内置-幸运药水3:
Name: 幸运药水3
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&2&l幸运 III&8&l&k||"
Tips:
- "&2幸运药水 &f等级 &b&l3"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加幸运BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvODlkYWVmN2VmNjExMDU3YWZkOWY5NDA0NDRjMzBmZmJkNWQ1NDY1OGQzYjFjNjczNjJhNTc5NzY1ZTI4NzIzNSJ9fX0=
SuccessTip: "&f你使用了&2&l幸运药水&f, 装备添加&2&l幸运III&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l幸运 ;var=v<3?3:4;format=%.0f;limit=v<=3;mode=last}
内置-海豚的恩惠1:
Name: 海豚的恩惠1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&b&l海豚的恩惠 I&8&l&k||"
Tips:
- "&b海豚的恩惠 &f等级 1"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加海豚的恩惠BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYWMzODIxZDRmNjFiMTdmODJmMGQ3YThlNTMxMjYwOGZmNTBlZGUyOWIxYjRkYzg5ODQ3YmU5NDI3ZDM2In19fQ==
SuccessTip: "&f你使用了&b&l海豚的恩惠药水&f, 装备添加&b&l海豚的恩惠&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l海豚的恩惠 ;var=v+1;format=%.0f;limit=v<=1;mode=last}
内置-海豚的恩惠2:
Name: 海豚的恩惠2
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&b&l海豚的恩惠 II&8&l&k||"
Tips:
- "&b海豚的恩惠 &f等级 &a&l2"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加海豚的恩惠BUFF"
- "&c不可拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvYWMzODIxZDRmNjFiMTdmODJmMGQ3YThlNTMxMjYwOGZmNTBlZGUyOWIxYjRkYzg5ODQ3YmU5NDI3ZDM2In19fQ==
SuccessTip: "&f你使用了&b&l海豚的恩惠药水&f, 装备添加&b&l海豚的恩惠II&fBUFF"
Rewards:
- LoreVar{lore=&6&l[&c&lBUFF&6&l] &b&l海豚的恩惠 ;var=v<2?2:3;format=%.0f;limit=v<=2;mode=last}
内置-饱和药水:
Name: 饱和药水
Embed: 1
Require:
- WEAPON
- TOOL
- ARMOR
Display: "&8&l&k||&6&l饱和药水&8&l&k||"
Tips:
- "&6&l饱和药水 &7品质: &a&l传说"
- "&7在工作台内点击此药水,"
- "&7再点击您的装备"
- "&7即可为装备添加饱和BUFF"
- "&7可花费2000点券拆卸"
Texture: eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNDE1N2Y5MzRkMzM4MTllYTdjYzkxNTJhNWRjNzU4YzM4MmQwZjdmNmRhZTk0YWY4M2ZkNzk2YTJkY2I2MDNhZCJ9fX0=
SuccessTip: "&f你使用了&6&l饱和药水&f, 装备添加&6&l饱和&fBUFF"
Rewards:
- LoreAdd{lore=&6&l[&c&lBUFF&6&l] &b&l饱和}
- Point{amount=-2000} $onRemove
+38
View File
@@ -0,0 +1,38 @@
default:
Title: "&8CMagicGem 镶嵌台 &7%page%"
Page: 1
Locations: []
Slots:
- "XXXXXXXXX"
- "XGGGGGGGX"
- "XGGGEGGGX"
- "XGGGGGGGX"
- "PXXXXXXNX"
Items:
X:
Type: TIP
Material: BLACK_STAINED_GLASS_PANE
Display: " "
E:
Type: EQUIP_SLOT
Material: ANVIL
Display: "&e放入装备"
Lore:
- "&7放入需要镶嵌或拆卸宝石的装备"
Require:
- ALL
G:
Type: GEM_SLOT
Material: LIGHT_GRAY_STAINED_GLASS_PANE
Display: "&7可用宝石"
Lore:
- "&7左键使用背包中的宝石"
- "&7右键拆卸已镶嵌的宝石"
P:
Type: PAGE_PREV
Material: ARROW
Display: "&e上一页"
N:
Type: PAGE_NEXT
Material: ARROW
Display: "&e下一页"
+80
View File
@@ -0,0 +1,80 @@
default:
Locations:
Page: 10000
Slots:
- TTTTITTTT
- GGGGGGGGG
- GGGGGGGGG
- GGGGGGGGG
- GGGGGGGGG
- PEEEXEEEN
Items:
T:
Type: TIP
Material: BLACK_STAINED_GLASS_PANE
I:
Type: EQUIP_SLOT
G:
Type: GEM_SLOT
P:
Type: PAGE_PREV
Material: ARROW
E:
Type: EMPTY
N:
Type: PAGE_NEXT
Material: ARROW
X:
Type: PAGE_TIP
Material: PAPER
符文镶嵌台:
Locations:
Title: "&8&l&k||&6&l符文镶嵌台&8&l&k||"
Page: 1
Slots:
- LLBTITRKK
- LBBMGMRRK
- BBMGGGMRR
- BBMGGGMRR
- LBBMGMRRK
- LLBDSDRKK
Items:
L:
Type: TIP
Material: BLUE_STAINED_GLASS_PANE
Display: "&9在左侧镶嵌蓝色符文"
K:
Type: TIP
Material: RED_STAINED_GLASS_PANE
Display: "&c在右侧镶嵌红色符文"
M:
Type: TIP
Material: GREEN_STAINED_GLASS_PANE
Display: "&a在中间镶嵌绿色符文"
T:
Type: TIP
Material: BLACK_STAINED_GLASS_PANE
Display: "&f&l在中间放置装备"
D:
Type: TIP
Material: YELLOW_STAINED_GLASS_PANE
Display: "&e在中间镶嵌技能符文"
I:
Type: EQUIP_SLOT
R:
Type: GEM_SLOT
Require: 红色符文
Amount: 1
G:
Type: GEM_SLOT
Require: 绿色符文
Amount: 1
B:
Type: GEM_SLOT
Require: 蓝色符文
Amount: 1
S:
Type: GEM_SLOT
Require: 技能符文
Amount: 1
File diff suppressed because it is too large Load Diff
+291
View File
@@ -0,0 +1,291 @@
version: 1.10
plugin:
prefix: "&9&l[&e&lMagicGem&9&l]&r"
read_file: "&eReading "
dupe_gem: "&cDuplicated gems find! Name: "
extract: "&eExtracting resource files"
build_error: "&cError: Failed to load class builder"
build_no_dependency: "&cError: A dependency is lacking, some features are disabled."
no_license: "&cThe plugin has not been licensed yet. Please check the console."
connect_failed: "&cLicense server connecting failed"
ip_deny: "&cThe IP address is not owned by you (please also check your username)!"
port_deny: "&cYour port is not licensed. Please buy more ports."
accept: "&cPlugin licensed. Thank you for purchasing!"
busy: "&cThe license server is busy. Will try again later"
wrong_info: "&cLicense request failed, plugin file may be corrupted"
wrong_user: "&cPlease specify correct account in MagicGem/config.yml (For example, user: Bob), and then use /mgem reload."
help: "&aFor support, please contact QQ: 386029724 or email liyi20129@gmail.com"
gem:
tip:
unable: "&cYour equipment doesn't meet certain conditions"
wrong_type: "&cYour equipment type is not acceptable."
embed_limit: "&cYou have added too much gem to the item"
success: "&aSuccessfully added gem!"
fail: "&cYou have failed to add gem to your item"
break: "&cYour gem broke"
downgrade: "&c&lYour item downgraded"
multi_equip: "&cYou cannot add gem to more than 1 item!"
downgrade_disable: "&cWarning: Reward cannot be cancelled, downgrade disabled"
invalid_reward: "&cWarning: Reward loading failed"
empty_reward: "&cWarning: Empty reward"
invalid_embed: "&cWarning: This reward cannot be canceled"
econ_dupe: "&cWarning: Embed gems cannot have positive (in total) economic rewards"
random_conflict: "&cError: Random gems cannot contain both player gem(s) and item gem(s)"
random_no_gem: "&cError: This random gem doesn't contain gems"
info:
name: "Gem Name"
type: "Type"
item: "Gem for items"
player: "Gem for players"
item_random: "Random gem for items"
player_random: "Random gem for players"
embed: "Embed"
consume: "&cPermanent"
use: "Right-click to use"
eat: "&aCan eat"
success: "Success Probability"
break: "Broke Probability"
downgrade: "Downgrade Probability"
types: "Available Types"
guis: "Available GUIs"
reward: "Rewards"
condition: "Conditions"
ignorable: "Ignore when failed"
info: "&bHidden Information"
get: "&aClick to get the real gem"
gui:
admin: "&8&l&k||&b&lAdmin Gem List&8&l&k||"
player: "&8&l&k||&b&lGem Preview&8&l&k||"
prev: "&8Previous Page"
next: "&8Next Page"
embed:
title: "&8&l&k||&b&lEmbed&8&l&k||"
tip: "&b&lPut your equipment in the middle slot, left-click to embed, right-click to undo"
success: "&aGem removed successfully!"
fail: "&cFailed to remove the gem, it has damaged!"
type_error: "&cThis type of gem cannot be used in this embed table!"
slot_error: "&cThis type of gem cannot be used in this slot!"
not_found: "&cThe specified gem embedder doesn't exist"
gem_not_found: "&cWarning: The UseGem button points to an invalid gem"
no_equip_slot: "&cWarning: Embedder doesn' have an equip slot"
multi_equip_slot: "&cWarning: Embedder cannot have multiple equip slots"
common:
type: "Type"
name: "Name"
amount: "Amount"
level: "Level"
upper: "Upper Limit"
range: "Range"
expression: "Expression"
inv_expression: "Inv-Expression"
null_expression: "&cWarning: Null expression"
invalid_range: "&cWarning: Invalid range"
invalid_texture: "&cWarning: Invalid skull texture"
invalid_material: "&cWarning: Invalid material name"
use: "On Use"
undo: "On Cancel"
reward:
common:
override: "Override origin value"
before_success: "Before Success"
after_success: "After Success"
ability:
type: "MMOItems Ability"
attribute:
type: "AttributeModifier"
unknown: "Unknown attribute"
unknown_slot: "&cUnknown Slot"
command:
type: "Command"
console: "Run with console"
player: "Run by player"
empty: "&cWarning: Empty Command"
conditional:
type: "Conditional Reward"
condition: "Condition"
reward: "Reward"
destroy:
type: "&cDestroy target item"
durability:
type: "Durability"
invalid: "&cWarning: Invalid durability amount"
enchant:
type: "Enchantment"
unknown: "&cUnknown Enchantment"
empty:
type: "&cEmpty"
exp:
type_level: "EXP Level"
invalid: "Invalid exp amount"
poor: "&cYou don't have enough EXP"
heal:
type: "Health Value"
item:
give: "Give items"
take: "Consume items"
set: "Set to item"
edit: "Item edit"
edit_slot: "Slot"
material: "Material name"
displayname: "Display name"
poor: "&cYou don't have enough materials"
lore:
add: "Add Lore"
first: "First line"
last: "Last line"
before: "Before"
after: "After"
line: "Single Line Edit"
all: "All"
del: "&cDelete Lore"
replace: "Replace Lore"
var: "Lore Variable Edit"
var_name: "Variable name"
invalid: "&cInvalid lore"
invalid_format: "&cWarning: Invalid format"
format: "Format"
roman: "Roman number"
locator: "Lore locator"
material:
type: "Change Material"
maxhealth:
type: "Max Health"
poor: "&cYour max health is over the range"
invalid: "&cInvalid max health amount"
message:
chat: "Chat Message"
title: "Title"
actionbar: "Action Bar"
content: "Contents"
mmitem:
type: "Change to MythicMobs Item"
disable: "&cWarning: Neither of MythicArtifacts or MythicCrucible is found, MMItem reward is disabled"
invalid: "&cWarning: Specified MythicMobs item doesn't exist in MythicMobs item storage"
money:
type: "Money"
disable: "&cWarning: Vault not found, money rewards were disabled"
invalid: "&cInvalid money amount"
poor: "&cYou don't have enough money"
spend: "&cYou paid &e%.2f &cgold!"
get: "&fYou get &e%.2f &fgold!"
name:
type: "New Name"
nbt_double:
type: "NBT Double"
nbt_string:
type: "NBT String"
remove: "&cRemove"
content: "Content"
point:
type: "PlayerPoints"
disable: "&cWarning: PlayerPoints not found, point rewards were disabled"
invalid: "Invalid point amount"
poor: "&cYou don't have enough points"
spend: "&cYou paid &9%d&c points!"
get: "&fYou got &9%d&f points!"
potion:
type: "Potion Effect"
duration: "Duration"
skill:
add: "Add MagicGem Skill"
change: "Change MagicGem Skill"
remove: "Remove MaticGem Skill"
unbreakable:
type: "Unbreakable"
upgrade:
type: "Gem Embed Override"
list: "Override List"
condition:
common:
invalid: "&cWarning: Invalid condition"
invempty:
type: "Required Empty Slot"
poor: "&cYou don't have enough empty slots"
invalid: "&cInvalid empty slot amount"
permission:
type: "Permission Requirement"
poor: "&cYou don't have required permission"
has: "&cYou already have required permission"
papi:
type: "PlaceHolderAPI Check"
disable: "&cWarning: PlaceHolderAPI not found, PAPI checks disabled"
skill:
common:
disable: "&cSkill engine is disabled"
invalid: "&cWarning: Invalid skill"
invalid_target: "&cWarning: Invalid target"
cooldown_tip: "&cSkill Cooldown"
mm_disable: "&cWarning: MythicMobs not found, MythicSkill is not supported"
potion:
invalid: "&cWarning: Invalid potion"
mmskill:
load_error: "&cWarning: MythicMobs skill loading error"
debuff:
invalid: "&cWarning: Invalid de-buff type"
command:
no_permission: "&cYou have no permissions to run the command"
player_only: "&cOnly players can use the command"
player_not_found: "The specified player is not online or doesn't exist"
reload_all: "&aAll config files have been successfully reloaded"
reload_skill: "&aSkill config files have been successfully reloaded"
reload_gem: "&aGem config files have been successfully reloaded"
invalid_args: "&cInvalid argument size"
invalid_amount: "&cThe specified amount is out of range [1, 64]"
gem_not_found: "&cThe specified gem cannot be found"
give_gem: "&fYou received %d %s"
full_inventory: "&cYour inventory has no empty slot. The item is spawned at your location"
nbt_not_found: "The specified NBT tag cannot be found"
nbt_type: "NBT type"
nbt_name: "name"
nbt_value: "value"
no_nbt: "&cThe item has no NBT tags"
no_meta: "&cThe item has no ItemMeta"
no_lore: "&cThe item has no Lore"
no_enchant: "&cThe item has no enchantment"
enchant_list: "&cEnchantment List"
embed:
addblock: "&aAdd Embed GUI successfully"
delblock: "&aRemove Embed GUI successfully"
save_error: "&cError saving data"
add_fail: "&c Add failed! Please check the block position"
del_fail: "&cDelete failed, the specified Embed GUI block doesn't exist"
help:
reload: "&e/mgem reload (gem|skill) - Reload config files"
apply: "&e/mgem apply <player> <gemName> (slot) - Apply a gem to a player's equip slot (default mainhand)"
give: "&e/mgem give <player> <gemName> (amount) - Give a player some gems (default 1 gem)"
signal: "&e/mgem signal (player|@all) (signal info) - Trigger signal skill of a player or all players"
get: "&e/mgem get - Open the admin GUI to preview gems with hidden information"
view: "&e/mgem view - Open the player preview GUI"
embed: "&e/mgem embed - Open Embed GUi"
embed_addblock: "&e/mgem embed addblock (guiName) - Add the block under your feet to Embed GUI block"
embed_delblock: "&e/mgem embed delblock - Del the Embed GUI block under your feet"
enchant: "&e/mgem enchant - List the enchants of your main hand item"
lore: "&e/mgem lore - List the lores of your main hand item"
material: "&e/mgem material - show the material (and ID) of your main hand item"
nbt: "&e/mgem nbt - List the NBT tags and types of your main hand item. When empty, it list your player nbt"
nbt_read: "&e/mgem nbt <key> - Read the NBT value of the specified key"
tab_tip: " &ePress TAB, use shift and copy to clone the read values"
cmagic:
cooldown: "&eCooldown: {seconds} seconds remaining"
condition_parse: "&cFailed to parse condition: {condition}"
condition_failed: "&cCondition not met: {condition}"
use_cancelled: "&cGem use was cancelled by another plugin"
remove_cancelled: "&cGem removal was cancelled by another plugin"
random_invalid: "&cThe random gem has no valid target"
random_cycle: "&cRandom gems cannot contain circular references"
gem_not_found: "&cUnknown gem: {gem}"
not_embedded: "&cThat gem is not embedded in this item"
command:
reload: "&aCMagicGem reloaded safely; {amount} gems loaded."
give: "&aGave {player} {amount} {gem}"
list: "&6Loaded gems (&e{amount}&6):"
signal_all: "&aSent the skill signal to every online player."
signal_player: "&aSent the skill signal to {player}."
empty_hand: "&cYour main hand is empty"
gui:
back: "&cBack to configuration files"
page: "&fPage {page} / {pages}"
gem_count: "&7Gems: &f{amount}"
browse: "&aClick to browse"
slot_rejected: "&cThat item cannot be placed in this equipment slot."
equipment_required: "&cPlace equipment in the slot first."
+291
View File
@@ -0,0 +1,291 @@
version: 1.10
plugin:
prefix: "&9&l[&e&l魔术宝石&9&l]&r"
read_file: "&e读取"
dupe_gem: "&c发现重复的宝石: "
extract: "&e抽取资源文件"
build_error: "&c错误: 构建器加载失败"
build_no_dependency: "&c警告: 您缺少了下列依赖,某些功能将不起效果"
no_license: "&c插件尚未认证,请检查控制台输出"
connect_failed: "&c验证服务器连接失败"
ip_deny: "&c这个IP地址尚未购买, 或不属于您(请注意config.yml中的user是否填对!)"
port_deny: "&c您的端口尚未购买, 请购买端口(享受折扣)"
accept: "&a插件成功认证, 感谢您对作者劳动的支持!"
wrong_info: "&c验证发生未知错误, 插件损坏或不受支持"
wrong_user: "&c请在插件config.yml中配置你的用户名(比如user: ZhangSan), 然后输入/mgem reload"
help: "&a更多支持请联系QQ: 386029724 或 邮件 liyi20129@gmail.com"
busy: "&e验证服务器繁忙, 将稍后再试"
gem:
tip:
unable: "&c强化失败, 未满足强化条件"
wrong_type: "&c物品类型不正确"
embed_limit: "&c宝石镶嵌数量达到上限"
success: "&a强化成功!"
fail: "&c强化失败!"
break: "&c宝石破碎!"
downgrade: "&c装备降级!"
multi_equip: "&c你不能同时对多个物品使用宝石!"
downgrade_disable: "&c警告: 奖励不能取消, 宝石降级率强制归零"
invalid_reward: "&c警告: 奖励未能读取成功"
empty_reward: "&c警告: 宝石奖励为空"
invalid_embed: "&c警告:奖励不可拆卸, 宝石设为不可拆卸"
econ_dupe: "&c警告: 可拆卸宝石不能有总价值为正的奖励"
random_conflict: "&c错误: 随机宝石不能同时包含物品和玩家宝石"
random_no_gem: "&c错误: 该随机宝石没有包含任何宝石"
info:
name: "名称"
type: "类型"
item: "物品强化"
player: "玩家强化"
item_random: "随机物品强化"
player_random: "随机玩家强化"
embed: "&a可镶嵌"
consume: "&c一次性"
use: "右键使用"
eat: "&a可食用"
success: "成功率"
downgrade: "降级率"
break: "破碎率"
types: "可强化物品列表"
guis: "可使用的镶嵌台"
reward: "强化奖励"
condition: "强化条件"
ignorable: "允许失败"
info: "&b宝石内部信息"
get: "&a单击获得宝石实物"
gui:
admin: "&8&l&k||&b&l管理员-宝石列表&8&l&k||"
player: "&8&l&k||&b&l宝石预览&8&l&k||"
prev: "&8上一页"
next: "&8下一页"
embed:
title: "&8&l&k||&b&l宝石镶嵌台&8&l&k||"
tip: "&b&l在中央放入装备, 左键镶嵌, 右键拆除"
success: "&a宝石成功拆除!"
fail: "&c宝石拆除失败, 该宝石已经失效!"
type_error: "&c该类型的宝石不能在本台上使用!"
slot_error: "&c该类型的宝石不能在该格子中使用!"
not_found: "&c指定的镶嵌台不存在"
gem_not_found: "&c警告: 使用宝石的按钮未能找到指定的宝石"
no_equip_slot: "&c警告: 镶嵌台不能没有装备格子"
multi_equip_slot: "&c警告: 镶嵌台不能有两个放装备的格子, 已删去多余格子"
common:
type: "类型"
name: "名称"
amount: "数量"
level: "等级"
upper: "上限"
range: "范围"
expression: "表达式"
inv_expression: "逆表达式"
null_expression: "&c警告: 表达式为空"
invalid_range: "&c警告: 上下限范围不正确"
invalid_texture: "&c警告: 加载头颅贴图时出错"
invalid_material: "&c警告: 加载材料时出错"
use: "使用时"
undo: "拆卸时"
reward:
common:
override: "强制覆盖"
before_success: "成功前"
after_success: "成功后"
ability:
type: "MMOItems技能"
attribute:
type: "原版属性"
unknown: "&c未知属性!"
unknown_slot: "&c未知装备格"
command:
type: "指令"
console: "用控制台执行"
player: "玩家执行"
empty: "&c警告: 指令为空"
conditional:
type: "条件奖励"
condition: "&c条件"
reward: "&a奖励"
destroy:
type: "&c破坏该物品"
durability:
type: "耐久度恢复"
invalid: "&c警告: 耐久为零"
enchant:
type: "附魔"
unknown: "&c未知附魔"
empty:
type: "空奖励"
exp:
type_level: "经验等级"
invalid: "无效经验数目"
poor: "&c你没有足够的经验"
heal:
type: "生命值"
item:
give: "获取物品"
take: "消耗物品"
edit: "物品编辑"
edit_slot: "槽位"
set: "设置为物品"
material: "材料名"
displayname: "显示名"
poor: "&c您没有足够的物品可供消耗"
lore:
add: "新增Lore"
first: "第一行"
last: "最后一行"
before: "在前"
after: "在后"
line: "单行操作"
all: "全部"
del: "&c删除Lore"
replace: "替换Lore"
var: "Lore变量编辑"
var_name: "变量名"
invalid: "&c无效Lore条目"
invalid_format: "&c警告: format格式不正确"
format: "格式"
roman: "罗马数字"
locator: "定位Lore"
material:
type: "材料修改"
maxhealth:
type: "最大生命"
poor: "&c你的最大生命值不在允许的范围"
invalid: "&c无效的最大生命点数"
message:
chat: "聊天框信息"
title: "全屏标题"
actionbar: "动作栏信息"
content: "内容"
mmitem:
type: "修改为MythicMobs物品"
disable: "&c警告: 未安装MythicArtifacts/MythicCrucible之一, MMItem奖励已经禁用"
invalid: "&c警告: 指定的MythicMobs物品在MythicMobs武器库中不存在"
money:
type: "金币"
disable: "&c警告: 未检测到Vault插件, 金币相关已禁用"
invalid: "&c无效金币数目"
poor: "&c您没有足够的金币"
spend: "&c您花费了&e%.2f&c金币!"
get: "&f您获得了&e%.2f&f金币!"
name:
type: "新名称"
nbt_double:
type: "NBT浮点数"
nbt_string:
type: "NBT字符串"
remove: "&c移除"
content: "内容"
point:
type: "点券"
disable: "&c警告: 未检测到PlayerPoint插件, 点券相关已禁用"
invalid: "无效点券数目"
poor: "&c您没有足够的点券"
spend: "&c您花费了&9%d&c点券!"
get: "&f您获得了&9%d&f点券!"
potion:
type: "药水效果"
duration: "持续时间"
skill:
add: "添加技能"
change: "修改技能"
remove: "移除技能"
unbreakable:
type: "不可破坏"
upgrade:
type: "宝石镶嵌覆盖"
list: "覆盖列表"
condition:
common:
invalid: "&cWarning: Invalid condition"
invempty:
type: "背包空格数目条件"
poor: "&c所需背包空间不足"
invalid: "&c无效背包空格数目"
papi:
type: "PlaceholderAPI条件"
disable: "&c警告: 未找到PlaceholderAPI, PAPI条件检查已禁用"
permission:
type: "权限节点条件"
poor: "&c您没有所需的权限"
has: "&c您已经拥有所需权限"
skill:
common:
disable: "&c宝石技能引擎处于禁用状态"
invalid: "&c警告: 无效技能"
invalid_target: "&c警告: 目标类型错误"
cooldown_tip: "&c技能冷却进度"
mm_disable: "&c警告: MythicMobs未安装, 技能引擎将忽略MM技能"
potion:
invalid: "&c警告: 无效药水"
mmskill:
load_error: "&c警告: MythicMobs技能加载错误"
debuff:
invalid: "&c警告: 无效DEBUFF类型"
command:
no_permission: "&c你没有使用该命令的权限"
player_only: "&c玩家才能使用本命令"
player_not_found: "指定的玩家不存在或不在线"
reload_all: "&a全部配置已经重载"
reload_skill: "&a技能配置已经重载"
reload_gem: "&a宝石配置已经重载"
invalid_args: "&c参数数量不正确"
invalid_amount: "&c指定的数量不在[1, 64]范围内, 已重置"
gem_not_found: "&c指定的宝石不存在"
give_gem: "&f你收到了%d个%s"
full_inventory: "&c你的背包空间不足, 已将宝石置于脚下"
nbt_not_found: "指定的NBT属性不存在"
nbt_type: "NBT类型"
nbt_name: "名称"
nbt_value: "值"
no_lore: "&c该物品没有Lore"
no_nbt: "&c该物品没有NBT标签"
no_meta: "&c该物品没有Meta"
no_enchant: "&c该物品没有附魔"
enchant_list: "&c附魔列表"
embed:
addblock: "&a成功添加脚下方块为镶嵌台"
delblock: "&a成功删除脚下镶嵌台"
save_error: "&c配置文件存储错误"
add_fail: "&c添加失败, 方块为空或镶嵌台已存在"
del_fail: "&c删除失败, 镶嵌台不存在"
help:
reload: "&e/mgem reload (gem|skill)- 重载配置全部/宝石/技能文件"
apply: "&e/mgem apply <玩家> <宝石名> (槽位) - 给玩家的指定装备位置使用1颗宝石(默认主手),或直接使用玩家宝石"
give: "&e/mgem give <玩家> <宝石名> (数量) - 给玩家指定数量的某种宝石(默认1个)"
signal: "&e/mgem signal (玩家|@all) (信号内容) - 触发全体玩家或某个玩家的信号技能"
get: "&e/mgem get - 打开管理员GUI, 查看说明并获取宝石"
view: "&e/mgem view - 打开宝石预览页面"
embed: "&e/mgem embed - 打开镶嵌台, 镶嵌或拆卸装备宝石"
embed_addblock: "&e/mgem embed addblock (镶嵌台名) - 指定脚下方块为镶嵌台"
embed_delblock: "&e/mgem embed delblock - 删除脚下的镶嵌台方块"
enchant: "&e/mgem enchant - 列出主手中物品的附魔(包括插件附魔)"
lore: "&e/mgem lore - 列出主手中物品的Lore"
material: "&e/mgem material - 显示主手物品的材料名和子ID"
nbt: "&e/mgem nbt - 列出主手中物品的NBT标签和类型 (空手会显示玩家自身nbt)"
nbt_read: "&e/mgem nbt <属性名> - 读取主手物品的指定属性。"
tab_tip: " &e部分功能可以用Tab读到输入栏直接复制(颜色代码会转换成&)"
cmagic:
cooldown: "&e冷却中,剩余 {seconds} 秒"
condition_parse: "&c条件解析失败: {condition}"
condition_failed: "&c条件不满足: {condition}"
use_cancelled: "&c宝石使用已被其他插件取消"
remove_cancelled: "&c宝石拆卸已被其他插件取消"
random_invalid: "&c随机宝石没有有效目标"
random_cycle: "&c随机宝石不能循环引用"
gem_not_found: "&c未知宝石: {gem}"
not_embedded: "&c装备未镶嵌该宝石"
command:
reload: "&aCMagicGem 已安全重载,共加载 {amount} 颗宝石。"
give: "&a已给予 {player} {amount} 个 {gem}"
list: "&6已加载宝石 (&e{amount}&6):"
signal_all: "&a已向所有在线玩家发送技能信号。"
signal_player: "&a已向 {player} 发送技能信号。"
empty_hand: "&c主手没有物品"
gui:
back: "&c返回配置文件列表"
page: "&f第 {page} / {pages} 页"
gem_count: "&7宝石数量: &f{amount}"
browse: "&a点击浏览"
slot_rejected: "&c该物品不能放入这个装备槽。"
equipment_required: "&c请先放入装备。"
+19
View File
@@ -0,0 +1,19 @@
name: CMagicGem
version: '${version}'
description: Paper 1.21.x compatible MagicGem reimplementation
main: cn.infstar.cmagicgem.CMagicGem
api-version: '1.21'
authors: [ Coldsmile_7 ]
softdepend: [PlaceholderAPI, Vault, PlayerPoints, MythicMobs, MMOItems, MythicArtifacts, MythicCrucible, Residence, PvPManager, EcoEnchants, GoldenEnchants]
commands:
cmgem:
description: CMagicGem main command
usage: /cmgem help
aliases: [cmg]
permissions:
cmagicgem.command.admin:
default: op
cmagicgem.command.view:
default: op
cmagicgem.command.embed:
default: op
+149
View File
@@ -0,0 +1,149 @@
# 不写nbt时, 技能名将作为默认的nbt识别名
# 如果一个技能同时还有匹配的NBT技能, 则两者能够用宝石互相转换
发光:
Lore: "&6&l[&c&lBUFF&6&l] &b&l发光"
Skills:
- PotionBuff{type=GLOWING} ~onTimer @Self $example-world:1
缓降:
Lore: "&6&l[&c&lBUFF&6&l] &b&l缓降"
Skills:
- PotionBuff{type=SLOW_FALLING} ~onTimer
潮涌能量:
Lore: "&6&l[&c&lBUFF&6&l] &b&l潮涌能量"
Skills:
- PotionBuff{type=CONDUIT_POWER} ~onTimer
夜视:
Lore: "&6&l[&c&lBUFF&6&l] &b&l夜视"
Skills:
- PotionBuff{type=NIGHT_VISION;duration=30;refresh=15} ~onTimer
饱和:
Lore: "&6&l[&c&lBUFF&6&l] &b&l饱和"
Skills:
- PotionBuff{type=SATURATION} ~onTimer
隐身:
Lore: "&6&l[&c&lBUFF&6&l] &b&l隐身"
Skills:
- PotionBuff{type=INVISIBILITY} ~onTimer
防火:
Lore: "&6&l[&c&lBUFF&6&l] &b&l防火"
Skills:
- PotionBuff{type=FIRE_RESISTANCE} ~onTimer
不详之兆:
Lore: "&6&l[&c&lBUFF&6&l] &b&l不祥之兆"
Skills:
- PotionBuff{type=BAD_OMEN} ~onTimer
伤害吸收:
Lore: "&6&l[&c&lBUFF&6&l] &b&l伤害吸收 "
Skills:
- PotionBuff{type=ABSORPTION;duration=60;level=$LORE:[BUFF] 伤害吸收?1$} ~onTimer
抗性提升:
Lore: "&6&l[&c&lBUFF&6&l] &b&l抗性提升 "
Skills:
- PotionBuff{type=DAMAGE_RESISTANCE;level=$LORE:[BUFF] 抗性提升?1$} ~onTimer
海豚的恩惠:
Lore: "&6&l[&c&lBUFF&6&l] &b&l海豚的恩惠 "
Skills:
- PotionBuff{type=DOLPHINS_GRACE;level=$LORE:[BUFF] 海豚的恩惠?1$} ~onTimer
急迫:
Lore: "&6&l[&c&lBUFF&6&l] &b&l急迫 "
Skills:
- PotionBuff{type=FAST_DIGGING;level=$LORE:[BUFF] 急迫?1$} ~onTimer
生命提升:
Lore: "&6&l[&c&lBUFF&6&l] &b&l生命提升 "
Skills:
- PotionBuff{type=HEALTH_BOOST;level=$LORE:[BUFF] 生命提升?1$;duration=480} ~onTimer
村庄英雄:
Lore: "&6&l[&c&lBUFF&6&l] &b&l村庄英雄 "
Skills:
- PotionBuff{type=HERO_OF_THE_VILLAGE;level=$LORE:[BUFF] 村庄英雄?1$} ~onTimer
力量:
Lore: "&6&l[&c&lBUFF&6&l] &b&l力量 "
Skills:
- PotionBuff{type=INCREASE_DAMAGE;level=$LORE:[BUFF] 力量?1$} ~onTimer
跳跃提升:
Lore: "&6&l[&c&lBUFF&6&l] &b&l跳跃提升 "
Skills:
- PotionBuff{type=JUMP;level=$LORE:[BUFF] 跳跃提升?1$} ~onTimer
幸运:
Lore: "&6&l[&c&lBUFF&6&l] &b&l幸运 "
Skills:
- PotionBuff{type=LUCK;level=$LORE:[BUFF] 幸运?1$} ~onTimer
生命恢复:
Lore: "&6&l[&c&lBUFF&6&l] &b&l生命恢复 "
Skills:
- PotionBuff{type=REGENERATION;level=$LORE:[BUFF] 生命恢复?1$} ~onTimer
速度:
Lore: "&6&l[&c&lBUFF&6&l] &b&l速度 "
Skills:
- PotionBuff{type=SPEED;level=$LORE:[BUFF] 速度?1$} ~onTimer
水下呼吸:
Lore: "&6&l[&c&lBUFF&6&l] &b&l水下呼吸 "
Skills:
- PotionBuff{type=WATER_BREATHING;level=$LORE:[BUFF] 水下呼吸?1$} ~onTimer
NBT伤害吸收:
Skills:
- PotionBuff{type=ABSORPTION;duration=60;level=$NBT:伤害吸收?1$} ~onTimer
NBT抗性提升:
Skills:
- PotionBuff{type=DAMAGE_RESISTANCE;level=$NBT:抗性提升?1$} ~onTimer
NBT海豚的恩惠:
Skills:
- PotionBuff{type=DOLPHINS_GRACE;level=$NBT:海豚的恩惠?1$} ~onTimer
NBT急迫:
Skills:
- PotionBuff{type=FAST_DIGGING;level=$NBT:急迫?1$} ~onTimer
NBT生命提升:
Skills:
- PotionBuff{type=HEALTH_BOOST;level=$NBT:生命提升?1$;duration=480} ~onTimer
NBT村庄英雄:
Skills:
- PotionBuff{type=HERO_OF_THE_VILLAGE;level=$NBT:村庄英雄?1$} ~onTimer
NBT力量:
Skills:
- PotionBuff{type=INCREASE_DAMAGE;level=$NBT:力量?1$} ~onTimer
NBT跳跃提升:
Skills:
- PotionBuff{type=JUMP;level=$NBT:跳跃提升?1$} ~onTimer
NBT幸运:
Skills:
- PotionBuff{type=LUCK;level=$NBT:幸运?1$} ~onTimer
NBT生命恢复:
Skills:
- PotionBuff{type=REGENERATION;level=$NBT:生命恢复?1$} ~onTimer
NBT速度:
Skills:
- PotionBuff{type=SPEED;level=$NBT:速度?1$} ~onTimer
NBT水下呼吸:
Skills:
- PotionBuff{type=WATER_BREATHING;level=$NBT:水下呼吸?1$} ~onTimer
@@ -0,0 +1,219 @@
depend: MythicMobs
刀:
Skills:
- effect:particles{p=witchMagic;a=1} @origin
斩:
Skills:
- effect:sound{s=block.fire.ambient;v=5;p=1} @Self
- damage{amount=70;ia=true;i=false}
- effect:particles{p=largeexplode;a=100} @Target
刀斩:
Cooldown: 5
Skills:
- missile{ot=刀;oh=斩;v=5;i=0.01;hR=0.5;vR=0.5;}
拔刀:
Skills:
- effect:particles{p=sweepAttack;a=2;hs=0.2;vs=0.2;speed=2} @origin
拔刀2:
Skills:
- effect:particles{particle=dragonbreath;amount=30;hS=0.05;vS=0.05;speed=0.2} @origin
- damage{amount=75;i=false}
- effect:explosion
拔刀斩:
Cooldown: 10
Skills:
- message{m="&8[&e技能提示&8]&f玩家 &6<mob.name> &f发动了技能 &c拔刀斩 &f!"} @PlayersInRadius{r=20;targetself=true}
- delay 30
- missile{ot=拔刀;oh=拔刀2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 9
- missile{ot=拔刀;oh=拔刀2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 9
- missile{ot=拔刀;oh=拔刀2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 9
- missile{ot=拔刀;oh=拔刀2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 9
- missile{ot=拔刀;oh=拔刀2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 9
拔刀斩2:
Cooldown: 5
Skills:
- missile{ot=拔刀;oh=拔刀2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 9
- missile{ot=拔刀;oh=拔刀2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 9
- missile{ot=拔刀;oh=拔刀2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 9
- missile{ot=拔刀;oh=拔刀2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 9
- missile{ot=拔刀;oh=拔刀2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 9
范围切割斩:
Cooldown: 10
Skills:
- message{m="&8[&e技能提示&8]&f玩家 &6<mob.name> &f发动了技能 &c切割斩 &f!"} @PlayersInRadius{r=20;targetself=true}
- effect:particletornado{p=totem;mr=2;h=1;i=1;d=60;rs=1;sh=1;cs=0;ca=1;chs=1;cvs=1;cps=0.3;cyo=0.2} @Self
- message{m="&8[&e技能提示&8]&f你爆发了&b&l特殊能力 &c&n狂热 可以快速回血 &f!"} @Self
- heal{amount=15} @Self
- delay 1
- heal{amount=15} @Self
- delay 1
- heal{amount=15} @Self
- delay 1
- heal{amount=15} @Self
- delay 1
- heal{amount=15} @Self
- delay 1
- heal{amount=15} @Self
- delay 1
- heal{amount=15} @Self
- delay 1
- heal{amount=15} @Self
- delay 1
- throw{v=15;velocityY=3} @Self
- sound{s=block.fire.ambient;v=2;p=1} @Self
- sound{s=block.fire.ambient;v=2;p=1} @Self
- leap{velocity=430} @target
- jump{velocity=0.5} @Self
- effect:particletornado{p=witchMagic;mr=1;h=1;i=1;d=3;rs=1;sh=1;cs=0;ca=1;chs=1;cvs=1;cps=0.3;cyo=0.2} @LivingEntitiesInRadius{r=10}
- effect:particletornado{p=flame;mr=2;h=1;i=1;d=8;rs=1;sh=1;cs=0;ca=1;chs=1;cvs=1;cps=0.3;cyo=0.2} @LivingEntitiesInRadius{r=10}
- sound{s=block.fire.ambient;v=2;p=1} @Self
- effect:explosion @PlayersInRadius @LivingEntitiesInRadius{r=10}
- effect:particles{p=cloud;a=35;hS=0.5;vS=0.5} @Self
- sound{s=block.fire.ambient;v=2;p=1} @Self
- damage{a=1000} @LivingEntitiesInRadius{r=10}
范围切割斩2:
Cooldown: 30
Skills:
- effect:particletornado{p=totem;mr=1;h=1;i=1;d=40;rs=1;sh=1;cs=0;ca=1;chs=1;cvs=1;cps=0.3;cyo=0.2} @Self
- message{m="&8[&e技能提示&8]&f你爆发了&b&l特殊能力 &c&n狂热 可以快速回血 &f!"} @Self
- heal{amount=1500} @Self
- delay 1
- heal{amount=1500} @Self
- delay 1
- heal{amount=1500} @Self
- delay 1
- heal{amount=1500} @Self
- delay 1
- heal{amount=1500} @Self
- delay 1
- heal{amount=1500} @Self
- delay 1
- heal{amount=1500} @Self
- delay 1
- heal{amount=1500} @Self
- delay 1
- throw{v=15;velocityY=3} @Self
- sound{s=block.fire.ambient;v=2;p=1} @Self
- sound{s=block.fire.ambient;v=2;p=1} @Self
- leap{velocity=430} @target
- jump{velocity=0.5} @Self
- sound{s=block.fire.ambient;v=2;p=1} @Self
- effect:explosion @PlayersInRadius @LivingEntitiesInRadius{r=10}
- effect:particles{p=cloud;a=35;hS=0.5;vS=0.5} @Self
- sound{s=block.fire.ambient;v=2;p=1} @Self
- damage{a=300000} @LivingEntitiesInRadius{r=10}
剑意:
Skills:
- effect:particles{p=sweepAttack;a=2;hs=0.2;vs=0.2;speed=2} @origin
劈中:
Skills:
- effect:particles{particle=dragonbreath;amount=30;hS=0.05;vS=0.05;speed=0.2} @origin
- damage{amount=1550;i=false}
- effect:explosion
剑意2:
Skills:
- effect:particles{p=sweepAttack;a=2;hs=0.2;vs=0.2;speed=2} @origin
劈中2:
Skills:
- effect:particles{particle=dragonbreath;amount=30;hS=0.05;vS=0.05;speed=0.2} @origin
- damage{amount=1550;i=false}
- effect:explosion
终焉剑意:
Cooldown: 15
Skills:
- message{m="&8[&e技能提示&8]&f&6<mob.name> &f发动了技能 &b终焉剑意 &f!"} @PlayersInRadius{r=20;targetself=true}
- delay 30
- message{m="&8[&e技能提示&8]&b&l斩 &f!"} @Self
- missile{ot=剑意2;oh=劈中2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 5
- missile{ot=剑意2;oh=劈中2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 5
- missile{ot=剑意2;oh=劈中2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 5
- missile{ot=剑意2;oh=劈中2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 5
- missile{ot=剑意2;oh=劈中2;v=15;i=1;hR=0.5;vR=0.5;}
- delay 5
- message{m="&8[&e技能提示&8]&b&l划刀斩 &f!"} @Self
- effect:particletornado{p=totem;mr=2;h=1;i=1;d=60;rs=1;sh=1;cs=0;ca=1;chs=1;cvs=1;cps=0.3;cyo=0.2} @Self
- heal{amount=150} @Self
- delay 1
- heal{amount=150} @Self
- delay 1
- heal{amount=150} @Self
- delay 1
- heal{amount=150} @Self
- delay 1
- heal{amount=150} @Self
- delay 1
- heal{amount=150} @Self
- delay 1
- heal{amount=150} @Self
- delay 1
- heal{amount=150} @Self
- delay 1
- sound{s=block.fire.ambient;v=2;p=1} @Self
- sound{s=block.fire.ambient;v=2;p=1} @Self
- leap{velocity=430} @target
- jump{velocity=0.5} @Self
- sound{s=block.fire.ambient;v=2;p=1} @Self
- effect:explosion @PlayersInRadius @LivingEntitiesInRadius{r=10}
- effect:particles{p=cloud;a=35;hS=0.5;vS=0.5} @Self
- sound{s=block.fire.ambient;v=2;p=1} @Self
- damage{a=10000} @LivingEntitiesInRadius{r=10}
- delay 35
- message{m="&8[&e技能提示&8]&b&l湮灭斩 &f!"} @Self
- effect:particles{p=largeexplode;a=100} @Target
- damage{a=10000} @LivingEntitiesInRadius{r=10}
- throw{v=15;velocityY=15} @LivingEntitiesInRadius{r=10}
- delay 35
- sendtitle{title="&8[&7&l>&8&f &6<mob.name>&7&l<&8]";subtitle="&0&l&k2&e&l终焉剑意&8&l·&b圣剑奥义!&0&l&k2";d=50;fi=7;fo=10} @PlayersInRadius{r=50;targetself=true}
- damage{amount=15000;i=false} @LivingEntitiesInRadius{r=10}
- effect:particles{p=flame;a=15;hS=0.2;vS=0.2;y=1.0;s=2.5;s=0.5} @LivingEntitiesInRadius{r=10}
- effect:sound{s=entity.generic.explode;v=1.2;p=1.5} @LivingEntitiesInRadius{r=10}
- delay 20
- damage{amount=15000;i=false} @LivingEntitiesInRadius{r=10}
- effect:particles{p=flame;a=15;hS=0.2;vS=0.2;y=1.0;s=2.5;s=0.5} @LivingEntitiesInRadius{r=10}
- effect:sound{s=entity.generic.explode;v=1.2;p=1.5} @LivingEntitiesInRadius{r=10}
- delay 20
- damage{amount=15000;i=false} @LivingEntitiesInRadius{r=10}
- effect:particles{p=flame;a=15;hS=0.2;vS=0.2;y=1.0;s=2.5;s=0.5} @LivingEntitiesInRadius{r=10}
- effect:sound{s=entity.generic.explode;v=1.2;p=1.5} @LivingEntitiesInRadius{r=10}
- damage{amount=15000;i=false} @LivingEntitiesInRadius{r=10}
- effect:particles{p=flame;a=15;hS=0.2;vS=0.2;y=1.0;s=2.5;s=0.5} @LivingEntitiesInRadius{r=10}
- effect:sound{s=entity.generic.explode;v=1.2;p=1.5} @LivingEntitiesInRadius{r=10}
- delay 20
- damage{amount=15000;i=false} @LivingEntitiesInRadius{r=10}
- effect:particles{p=flame;a=15;hS=0.2;vS=0.2;y=1.0;s=2.5;s=0.5} @LivingEntitiesInRadius{r=10}
- effect:sound{s=entity.generic.explode;v=1.2;p=1.5} @LivingEntitiesInRadius{r=10}
- delay 20
- damage{amount=15000;i=false} @LivingEntitiesInRadius{r=10}
- effect:particles{p=flame;a=15;hS=0.2;vS=0.2;y=1.0;s=2.5;s=0.5} @LivingEntitiesInRadius{r=10}
- effect:sound{s=entity.generic.explode;v=1.2;p=1.5} @LivingEntitiesInRadius{r=10}
- damage{amount=15000;i=false} @LivingEntitiesInRadius{r=10}
- effect:particles{p=flame;a=15;hS=0.2;vS=0.2;y=1.0;s=2.5;s=0.5} @LivingEntitiesInRadius{r=10}
- effect:sound{s=entity.generic.explode;v=1.2;p=1.5} @LivingEntitiesInRadius{r=10}
- delay 20
- damage{amount=1500;i=false} @LivingEntitiesInRadius{r=10}
- effect:particles{p=flame;a=15;hS=0.2;vS=0.2;y=1.0;s=2.5;s=0.5} @LivingEntitiesInRadius{r=10}
- effect:sound{s=entity.generic.explode;v=1.2;p=1.5} @LivingEntitiesInRadius{r=10}
- delay 20
- damage{amount=15000;i=false} @LivingEntitiesInRadius{r=10}
- effect:particles{p=flame;a=15;hS=0.2;vS=0.2;y=1.0;s=2.5;s=0.5} @LivingEntitiesInRadius{r=10}
- effect:sound{s=entity.generic.explode;v=1.2;p=1.5} @LivingEntitiesInRadius{r=10}
测试MM技能:
Lore: "[技能] 终焉剑意"
Cooldown: 60.0
Skills:
- skill{s=终焉剑意} ~onUse
@@ -0,0 +1,79 @@
向前闪现:
Cooldown: 10.0
Lore: "右键即可闪现"
CooldownTip: ActionBar{p=&f闪现冷却时间;s=&a%.2fs;empty=&f|;full=&a|;length=75}
Skills:
- Blink{distance=$LORE:闪现距离?32$} ~onUse @Forward
飞雷神三叉戟:
Cooldown: 15.0
Lore: "飞雷神三叉戟"
CooldownTip: ActionBar{p=&a技能冷却时间;s=&a%.2fs;empty=&f|;full=&a|;length=75}
Skills:
- Blink{distance=120} ~onTridentHit @Location
- Reward{Title{m=&e飞雷神之术}} ~onTridentHit
跳跃之蛋:
Cooldown: 5.0
Lore: "扔出此蛋, 你将瞬移到落点"
Slot:
- mainhand
CooldownTip: ActionBar{p=&a技能冷却时间;s=&a%.2fs;empty=&f|;full=&a|;length=75}
Skills:
- Blink{distance=120} ~onHit:EGG @Location
- Reward{Chat{m=&e跳跃之蛋!}} ~onHit:EGG
旅行者之弓:
Cooldown: 5.0
Lore: "逐日之弓"
CooldownTip: ActionBar{p=&a技能冷却时间;s=&a%.2fs;empty=&f|;full=&a|;length=75}
Skills:
- Blink{distance=120} ~onHit:ARROW @Location
精准时运切换:
Lore: §2Shift+右键切换模式(精准/时运)
Cooldown: 4.0
CooldownTip: ActionBar{p=&c切换冷却时间;s=&a%.2fs;empty=&f|;full=&c|;length=75}
Skills:
- Switch{s=精准;时运} ~onShiftUse
精准:
Skills:
- RewardSwitch{Enchant{name=SILK_TOUCH;limit=1}}
- RewardSwitch{ActionBar{m=&b&l切换为精准模式}}
时运:
Skills:
- RewardSwitch{Enchant{name=LOOT_BONUS_BLOCKS;level=0}}
- RewardSwitch{ActionBar{m=&a&l切换为时运模式}}
忠诚/激流切换:
Lore: §2Shift+左键切换模式(忠诚/激流)
Cooldown: 4.0
CooldownTip: ActionBar{p=&c切换冷却时间;s=&a%.2fs;empty=&f|;full=&c|;length=75}
Skills:
- Switch{s=忠诚;激流} ~onShiftSwing
激流:
Skills:
- RewardSwitch{Enchant{name=RIPTIDE;level=0}}
- RewardSwitch{ActionBar{m=&b&l切换为激流模式}}
忠诚:
Skills:
- RewardSwitch{ActionBar{m=&a&l切换为时忠诚模式}}
禁飞DEBUFF:
Cooldown: 5.0
Lore: 禁飞
CooldownTip: ActionBar{p=&a技能冷却时间;s=&a%.2fs;empty=&f|;full=&a|;length=75}
Skills:
- Debuff{type=fly;duration=5} ~onAttack @Entity
- Reward{Chat{m=&c禁飞!}} ~onAttack
定身DEBUFF:
Cooldown: 5.0
Lore: 定身
CooldownTip: ActionBar{p=&a技能冷却时间;s=&a%.2fs;empty=&f|;full=&a|;length=75}
Skills:
- Debuff{type=move;duration=2} ~onAttack @Entity
- Reward{Chat{m=&c定身!}} ~onAttack
@@ -0,0 +1,5 @@
示例回复:
Lore: "[技能] 示例回复"
Cooldown: 5
Skills:
- "Reward{Heal{amount=2}} ~onUse @Self"
@@ -0,0 +1,65 @@
package cn.infstar.cmagicgem.dsl;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.List;
import java.util.Set;
class DirectiveParserTest {
@Test
void keepsNestedRewardTogether() {
Directive directive = new DirectiveParser().parse("Conditional{condition=v>1;reward=Enchant{name=sharpness;level=1}} $ignorable");
assertEquals("Conditional", directive.type());
assertEquals("Enchant{name=sharpness;level=1}", directive.argument("", "reward"));
assertTrue(directive.ignorable());
}
@Test
void parsesLegacyRewardPhases() {
DirectiveParser parser = new DirectiveParser();
assertEquals(Directive.Phase.SUCCESS, parser.parse("Money{amount=1} $onSuccess").phase());
assertEquals(Directive.Phase.FAIL, parser.parse("Chat{m=失败} $onFail").phase());
assertEquals(Directive.Phase.REMOVE, parser.parse("MaxHealth{amount=-1} $onRemove").phase());
}
@Test
void removesBalancedArgumentQuotes() {
Directive directive = new DirectiveParser().parse(
"Conditional{condition=\"v>=2&&v<=8\";reward='Chat{m=ok}'}");
assertEquals("v>=2&&v<=8", directive.argument("", "condition"));
assertEquals("Chat{m=ok}", directive.argument("", "reward"));
}
@Test
void retainsLegacyDelimitedMessageSegments() {
DirectiveParser parser = new DirectiveParser();
Directive directive = parser.parse("Title{messages=主标题;副标题;第二页;第二页副标题;interval=20;stay=60}");
assertEquals(List.of("主标题", "副标题", "第二页", "第二页副标题"),
parser.delimitedArgument(directive, Set.of("messages", "message", "m"),
Set.of("interval", "fadein", "fadeout", "stay")));
}
@Test
void retainsSwitchCycleAfterNamedFirstEntry() {
Directive directive = new DirectiveParser().parse("Switch{s=精准;时运}");
assertEquals("精准", directive.argument("", "s"));
assertEquals(List.of("时运"), directive.positional());
}
@Test
void splitsLegacyMultiLineLoreGroups() {
DirectiveParser parser = new DirectiveParser();
Directive directive = parser.parse("LoreMultiReplace{old=第一行;第二行;new=新一行;新二行;limit=v<5}");
assertEquals(List.of("第一行", "第二行"),
parser.delimitedArgument(directive, Set.of("old"), Set.of("lore", "new", "limit")));
assertEquals(List.of("新一行", "新二行"),
parser.delimitedArgument(directive, Set.of("new"), Set.of("limit")));
}
}
@@ -0,0 +1,35 @@
package cn.infstar.cmagicgem.expression;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ExpressionTest {
private final Expression expression = new Expression();
@Test
void evaluatesArithmeticAndVariables() {
assertEquals(7, expression.evaluate("v*2+1", Map.of("v", 3.0)));
assertEquals(9, expression.evaluate("(v+1)^2", Map.of("v", 2.0)));
}
@Test
void evaluatesConditions() {
assertTrue(expression.test("v>=2&&v<=8", Map.of("v", 5.0)));
assertFalse(expression.test("v%3==0", Map.of("v", 5.0)));
assertFalse(expression.test("false&&v>0", Map.of("v", 5.0)));
assertTrue(expression.test("true||v<0", Map.of("v", 5.0)));
}
@Test
void evaluatesLegacyFunctionsAndTernary() {
assertEquals(8, expression.evaluate("true?$POW(2,3):0", Map.of()));
assertEquals(2, expression.evaluate("false?1:$FLOOR(2.9)", Map.of()));
double random = expression.evaluate("$RANDOM()", Map.of());
assertTrue(random >= 0 && random < 1);
}
}
@@ -0,0 +1,29 @@
package cn.infstar.cmagicgem.gem;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class GemTypeTest {
@Test
void acceptsLegacyAndEnumSpellings() {
assertEquals(GemType.ITEM_GEM, GemType.parse("ItemGem"));
assertEquals(GemType.PLAYER_GEM, GemType.parse("PlayerGem"));
assertEquals(GemType.RANDOM_GEM, GemType.parse("RandomGem"));
assertEquals(GemType.PLAYER_GEM, GemType.parse("PLAYER_GEM"));
assertEquals(GemType.RANDOM_GEM, GemType.parse("random-gem"));
assertEquals(GemType.ITEM_GEM, GemType.parse("item gem"));
}
@Test
void defaultsBlankValuesToItemGems() {
assertEquals(GemType.ITEM_GEM, GemType.parse(null));
assertEquals(GemType.ITEM_GEM, GemType.parse(" "));
}
@Test
void rejectsUnknownValues() {
assertThrows(IllegalArgumentException.class, () -> GemType.parse("not-a-gem"));
}
}
@@ -0,0 +1,57 @@
package cn.infstar.cmagicgem.gem;
import org.bukkit.Material;
import org.junit.jupiter.api.Test;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
class RandomGemValidatorTest {
@Test
void acceptsNestedRandomGemsWithOneTerminalType() {
Map<String, GemDefinition> gems = gems(
gem("item", GemType.ITEM_GEM),
random("nested", new GemDefinition.WeightedGem("item", 1)),
random("root", new GemDefinition.WeightedGem("nested", 1)));
assertTrue(RandomGemValidator.validate(gems).isEmpty());
}
@Test
void rejectsCyclesUnknownReferencesAndMixedTypes() {
Map<String, GemDefinition> gems = gems(
gem("item", GemType.ITEM_GEM), gem("player", GemType.PLAYER_GEM),
random("mixed", new GemDefinition.WeightedGem("item", 1), new GemDefinition.WeightedGem("player", 1)),
random("unknown", new GemDefinition.WeightedGem("missing", 1)),
random("cycle-a", new GemDefinition.WeightedGem("cycle-b", 1)),
random("cycle-b", new GemDefinition.WeightedGem("cycle-a", 1)));
Map<String, String> errors = RandomGemValidator.validate(gems);
assertEquals(Set.of("mixed", "unknown", "cycle-a", "cycle-b"), errors.keySet());
}
private Map<String, GemDefinition> gems(GemDefinition... definitions) {
Map<String, GemDefinition> result = new LinkedHashMap<>();
for (GemDefinition definition : definitions) result.put(definition.name(), definition);
return result;
}
private GemDefinition random(String name, GemDefinition.WeightedGem... targets) {
return gem(name, GemType.RANDOM_GEM, List.of(targets));
}
private GemDefinition gem(String name, GemType type) {
return gem(name, type, List.of());
}
private GemDefinition gem(String name, GemType type, List<GemDefinition.WeightedGem> targets) {
return new GemDefinition(name, name, type, Material.STONE, name, List.of(), null, null, null,
null, false, Set.of(), List.of(), 0, "100", "0", "0", null, false,
false, 0, null, false, targets, List.of(), List.of(), null, null, null, null, null, null);
}
}
@@ -0,0 +1,23 @@
package cn.infstar.cmagicgem.reward;
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.assertEquals;
class RewardContextTest {
@Test
void evaluatesEachDynamicRewardValueOnce() {
RewardContext context = new RewardContext(null, null, null, false);
Object reward = new Object();
AtomicInteger evaluations = new AtomicInteger();
assertEquals(7.5, context.evaluatedNumber(reward, () -> {
evaluations.incrementAndGet();
return 7.5;
}));
assertEquals(7.5, context.evaluatedNumber(reward, () -> 99));
assertEquals(1, evaluations.get());
}
}
@@ -0,0 +1,93 @@
package cn.infstar.cmagicgem.skill;
import cn.infstar.cmagicgem.dsl.Directive;
import cn.infstar.cmagicgem.dsl.DirectiveParser;
import cn.infstar.cmagicgem.reward.RewardEngine;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.YamlConfiguration;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class BundledConfigurationTest {
private final DirectiveParser parser = new DirectiveParser();
@Test
void pluginDescriptorOnlyRegistersNewCommandNames() {
YamlConfiguration descriptor = YamlConfiguration.loadConfiguration(new File("src/main/resources/plugin.yml"));
assertNotNull(descriptor.getConfigurationSection("commands.cmgem"));
assertTrue(descriptor.getStringList("commands.cmgem.aliases").contains("cmg"));
assertNull(descriptor.getConfigurationSection("commands.mgem"));
assertNull(descriptor.getConfigurationSection("commands.magicgem"));
}
@Test
void everyBundledGemRewardAndReferenceLoads() throws Exception {
Path directory = Path.of("src/main/resources/gems");
Set<String> names = new LinkedHashSet<>();
try (var files = Files.list(directory)) {
for (Path file : files.filter(path -> path.toString().endsWith(".yml")).toList()) {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file.toFile());
for (String key : yaml.getKeys(false)) {
ConfigurationSection section = yaml.getConfigurationSection(key);
if (section == null) continue;
names.add(section.getString("Name", key).toLowerCase(Locale.ROOT));
for (String source : section.getStringList("Rewards")) {
Directive reward = parser.parse(source);
assertTrue(RewardEngine.supports(reward.type()), file + "/" + key + ": " + source);
}
for (String source : section.getStringList("Conditions")) assertFalse(parser.parse(source).type().isBlank());
}
}
}
try (var files = Files.list(directory)) {
for (Path file : files.filter(path -> path.toString().endsWith(".yml")).toList()) {
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file.toFile());
for (String key : yaml.getKeys(false)) {
ConfigurationSection random = yaml.getConfigurationSection(key + ".Gems");
if (random == null) continue;
for (String target : random.getKeys(false))
assertTrue(names.contains(target.toLowerCase(Locale.ROOT)), file + "/" + key + " -> " + target);
}
}
}
}
@Test
void everyBundledSkillActionParses() throws Exception {
SkillRegistry registry = new SkillRegistry(null, parser);
try (var files = Files.list(Path.of("src/main/resources/skills"))) {
for (Path path : files.filter(file -> file.toString().endsWith(".yml")).toList()) {
File file = path.toFile();
YamlConfiguration yaml = YamlConfiguration.loadConfiguration(file);
for (String key : yaml.getKeys(false)) {
if (key.equalsIgnoreCase("depend")) continue;
ConfigurationSection section = yaml.getConfigurationSection(key);
assertNotNull(section, file + "/" + key);
for (String source : section.getStringList("Skills")) {
SkillAction action = registry.parseAction(source);
assertFalse(action.mechanic().type().isBlank(), file + "/" + key + ": " + source);
if (action.mechanic().type().equalsIgnoreCase("Reward")
|| action.mechanic().type().equalsIgnoreCase("RewardSwitch")) {
String nested = action.mechanic().positional().isEmpty()
? action.mechanic().argument("", "reward", "r")
: action.mechanic().positional().getFirst();
assertTrue(RewardEngine.supports(parser.parse(nested).type()), file + "/" + key + ": " + source);
}
}
}
}
}
}
}
@@ -0,0 +1,35 @@
package cn.infstar.cmagicgem.skill;
import cn.infstar.cmagicgem.dsl.DirectiveParser;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class SkillRegistryTest {
private final SkillRegistry registry = new SkillRegistry(null, new DirectiveParser());
@Test
void preservesMythicMechanicAndTargeterWithSpaces() {
String line = "message{m=\"Player <mob.name> used a skill\"} @PlayersInRadius{r=20;targetself=true} ~onUse";
SkillAction action = registry.parseAction(line);
assertEquals("message", action.mechanic().type());
assertEquals("Player <mob.name> used a skill", action.mechanic().argument("", "m").replace("\"", ""));
assertEquals("message{m=\"Player <mob.name> used a skill\"} @PlayersInRadius{r=20;targetself=true}",
action.mechanicLine());
assertEquals(TriggerType.USE, action.trigger());
assertEquals("playersinradius", action.target());
}
@Test
void separatesTriggerFiltersAndWorldLimits() {
SkillAction action = registry.parseAction(
"PotionBuff{type=SPEED;level=$LORE:Speed:1$} ~onHit:ARROW;TRIDENT @Self $world:3");
assertEquals("PotionBuff{type=SPEED;level=$LORE:Speed:1$} @Self", action.mechanicLine());
assertEquals(TriggerType.HIT, action.trigger());
assertEquals(java.util.List.of("ARROW", "TRIDENT"), action.triggerFilters());
assertEquals(java.util.Map.of("world", 3), action.worldLimits());
}
}