Rename internal CMagicGem implementation

This commit is contained in:
Coldsmiles
2026-10-05 01:46:32 +08:00
parent 953c15e380
commit ff9b6ce4f1
68 changed files with 298 additions and 306 deletions
@@ -0,0 +1,868 @@
package cn.infstar.magicgem.skill;
import cn.infstar.magicgem.api.event.MagicEquipmentChangeEvent;
import cn.infstar.magicgem.api.event.MagicJumpEvent;
import cn.infstar.magicgem.api.event.MagicPotionBuffEvent;
import cn.infstar.magicgem.api.event.MagicSkillUseEvent;
import cn.infstar.magicgem.condition.ConditionEngine;
import cn.infstar.magicgem.dsl.Directive;
import cn.infstar.magicgem.dsl.DirectiveParser;
import cn.infstar.magicgem.expression.DynamicExpression;
import cn.infstar.magicgem.item.ItemCodec;
import cn.infstar.magicgem.integration.MythicBridge;
import cn.infstar.magicgem.integration.ProtectionBridge;
import cn.infstar.magicgem.reward.RewardContext;
import cn.infstar.magicgem.reward.RewardEngine;
import cn.infstar.magicgem.reward.RewardException;
import cn.infstar.magicgem.util.MessageDispatcher;
import cn.infstar.magicgem.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;
}
MagicSkillUseEvent useEvent = new MagicSkillUseEvent(player, skill, item);
Bukkit.getPluginManager().callEvent(useEvent);
if (useEvent.isCancelled()) return;
boolean executed = false;
for (SkillAction action : actions) {
String type = normalize(action.mechanic().type());
if (timed && !active && !type.equals("timestart")) continue;
if (timed && active && type.equals("timestart")) continue;
try {
mechanic(player, item, slot, skill, action, target, eventContext);
executed = true;
} catch (RewardException | RuntimeException exception) {
plugin.getLogger().warning("技能 " + skill.name() + " 执行失败: " + exception.getMessage());
}
}
if (executed && !active && skill.cooldown() > 0)
cooldowns.get(player.getUniqueId()).put(stateKey, now + (long) (skill.cooldown() * 1000));
if (executed) tip(player, skill.successTip());
}
private void mechanic(Player player, ItemStack item, String slot, SkillDefinition skill, SkillAction action,
Entity target, EventContext context) throws RewardException {
Directive mechanic = action.mechanic();
switch (normalize(mechanic.type())) {
case "reward" -> {
ItemStack changed = applyReward(player, item, mechanic, false);
if (changed != item) replaceMatchingItem(player, item, changed);
}
case "rewardswitch" -> { }
case "switch" -> switchSkill(player, item, skill, mechanic);
case "potionbuff" -> potionBuff(player, item, mechanic, action.target(), target, action.worldLimits());
case "blink" -> blink(player, item, mechanic, action.target(), target, context.location());
case "timestart" -> timeStart(player, item, skill, mechanic);
case "timeadd" -> timeAdd(player, item, skill, mechanic);
case "debuff" -> debuff(player, item, mechanic, action.target(), target);
case "bloodsuck", "bl​​oodsuck", "blooksuck" -> bloodSuck(player, item, mechanic, context.event());
case "shielddamage" -> shieldDamage(player, item, mechanic, action.target(), target);
case "damage" -> damage(player, item, mechanic, action.target(), target);
case "counter" -> counter(player, item, slot, mechanic, target, context);
case "cancelevent" -> { if (context.event() instanceof Cancellable cancellable) cancellable.setCancelled(true); }
default -> {
Location location = context.location() != null ? context.location()
: target == null ? player.getLocation() : target.getLocation();
if (!mythic.castInline(player, action.mechanicLine(), target, location)
&& !mythic.cast(player, mechanic.type(), target, location))
plugin.getLogger().warning("未知技能机制或 MythicMobs 技能: " + mechanic.type());
}
}
}
private ItemStack applyReward(Player player, ItemStack item, Directive mechanic, boolean removal)
throws RewardException {
String nestedSource = mechanic.positional().isEmpty() ? mechanic.argument("", "reward", "r")
: mechanic.positional().getFirst();
Directive nested = parser.parse(nestedSource);
RewardContext rewardContext = new RewardContext(player, null, item, removal);
rewards.applyDirective(rewardContext, nested);
return rewardContext.item();
}
private void switchSkill(Player player, ItemStack item, SkillDefinition source, Directive mechanic)
throws RewardException {
List<String> cycle = new ArrayList<>();
String first = mechanic.argument("", "skills", "list", "s");
if (!first.isBlank()) cycle.addAll(parser.splitTopLevel(first, ';'));
cycle.addAll(mechanic.positional());
if (cycle.isEmpty()) return;
List<String> current = new ArrayList<>(itemCodec.skills(item));
int selected = -1;
for (int index = 0; index < cycle.size(); index++) if (current.contains(cycle.get(index))) { selected = index; break; }
String previous = selected < 0 ? null : cycle.get(selected);
String next = cycle.get((selected + 1) % cycle.size());
ItemStack changed = item;
if (previous != null) {
SkillDefinition oldSkill = registry.find(previous).orElse(null);
if (oldSkill != null) for (SkillAction action : oldSkill.actions())
if (normalize(action.mechanic().type()).equals("rewardswitch"))
changed = applyReward(player, changed, action.mechanic(), true);
current.remove(previous);
}
if (!current.contains(next)) current.add(next);
changed = itemCodec.setSkills(changed, current);
SkillDefinition newSkill = registry.find(next).orElse(null);
if (newSkill != null) for (SkillAction action : newSkill.actions())
if (normalize(action.mechanic().type()).equals("rewardswitch"))
changed = applyReward(player, changed, action.mechanic(), false);
replaceMatchingItem(player, item, changed);
}
private void potionBuff(Player player, ItemStack item, Directive mechanic, String selector, Entity target,
Map<String, Integer> worldLimits)
throws RewardException {
LivingEntity living = selectedLiving(player, selector, target);
if (living == null) return;
if (debuffed(living, "POTIONBUFF")) return;
String name = mechanic.argument("", "type", "potion", "p", "name").toLowerCase(Locale.ROOT);
PotionEffectType type = PotionTypes.resolve(name);
if (type == null) throw new RewardException("未知药水效果: " + name);
double levelValue = mechanic.booleanArgument(false, "roman")
? expressions.evaluateRoman(mechanic.argument("1", "level", "l"), player, item, Map.of())
: value(mechanic.argument("1", "level", "l"), player, item);
int level = Math.max(1, (int) Math.round(levelValue));
String world = living.getWorld().getName().toLowerCase(Locale.ROOT);
Integer actionLimit = worldLimits.get(world);
String globalPath = "potion-global-limit." + living.getWorld().getName();
Integer globalLimit = plugin.getConfig().contains(globalPath) ? plugin.getConfig().getInt(globalPath) : null;
if (actionLimit != null) level = Math.min(level, Math.max(0, actionLimit));
if (globalLimit != null) level = Math.min(level, Math.max(0, globalLimit));
if (level <= 0) return;
int duration = Math.max(1, (int) Math.round(value(mechanic.argument("1", "duration", "d"), player, item) * 20));
int refresh = Math.max(0, (int) Math.round(value(mechanic.argument("0", "refresh"), player, item) * 20));
PotionEffect old = living.getPotionEffect(type);
if (old != null && (old.getAmplifier() > level - 1 || old.getDuration() > refresh)) return;
MagicPotionBuffEvent event = new MagicPotionBuffEvent(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 = "magicgem_reforged_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("MAGICGEM_REFORGED_RUNTIME_ID")
? nbt.getString("MAGICGEM_REFORGED_RUNTIME_ID") : "");
if (itemId.isBlank()) {
itemId = UUID.randomUUID().toString();
String generated = itemId;
NBT.modify(item, nbt -> { nbt.setString("MAGICGEM_REFORGED_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; }
MagicJumpEvent jump = new MagicJumpEvent(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(), MagicEquipmentChangeEvent.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()), MagicEquipmentChangeEvent.Cause.HELD, event);
}
@EventHandler(ignoreCancelled = true)
public void onSwap(PlayerSwapHandItemsEvent event) {
Player player = event.getPlayer();
equipmentChange(player, "mainhand", player.getInventory().getItemInMainHand(), event.getMainHandItem(),
MagicEquipmentChangeEvent.Cause.SWAP, event);
equipmentChange(player, "offhand", player.getInventory().getItemInOffHand(), event.getOffHandItem(),
MagicEquipmentChangeEvent.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,
MagicEquipmentChangeEvent.Cause.INVENTORY, event);
if (!sameItem(offHand, currentOffHand))
equipmentChange(player, "offhand", offHand, currentOffHand,
MagicEquipmentChangeEvent.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, MagicEquipmentChangeEvent.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,
MagicEquipmentChangeEvent.Cause cause, Event source) {
MagicEquipmentChangeEvent event = new MagicEquipmentChangeEvent(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) { }
}