Files
MagicGem-Reforged/src/main/java/cn/infstar/magicgem/skill/SkillEngine.java
T

869 lines
46 KiB
Java
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) { }
}