feat: initial CMagicGem reimplementation
This commit is contained in:
@@ -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", "bloodsuck", "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)));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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}"
|
||||
@@ -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}
|
||||
@@ -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}"
|
||||
@@ -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
@@ -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骷髅剑没有技能, 所以这把武器废了(滑稽)}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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下一页"
|
||||
@@ -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
@@ -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."
|
||||
@@ -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请先放入装备。"
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user