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> cooldowns = new ConcurrentHashMap<>(); private final Map> durations = new ConcurrentHashMap<>(); private final Map debuffs = new ConcurrentHashMap<>(); private final Map 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 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 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 information, Entity target, EventContext eventContext) { List 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 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 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 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 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 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 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 skills(ItemStack item, String slot) { if (item == null || item.getType().isAir()) return List.of(); Set names = new LinkedHashSet<>(itemCodec.skills(item)); String display = item.hasItemMeta() && item.getItemMeta().hasDisplayName() ? strip(item.getItemMeta().getDisplayName()) : ""; List 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 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 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 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) { } }