diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java index dafc38ee8..d9ab67646 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -202,9 +202,13 @@ public abstract class AbilityEnergyProjectile /** * Fire an entity if it's valid. + * Clears charging state first so the entity can move and check collisions. */ protected void fireEntitySafe(E entity, EntityLivingBase target) { if (entity != null && !entity.isDead) { + if (entity.isCharging()) { + entity.setCharging(false); + } fireEntity(entity, target); } } @@ -261,6 +265,18 @@ public abstract class AbilityEnergyProjectile cleanup(); } entities = createAllEntities(caster, target); + + // If there's a fire delay, put unfired entities in charging state + // to prevent collision checks before they're actually fired. + // Entity[0] will be fired immediately below, so skip it. + if (fireDelay > 0 && projectileCount > 1) { + for (int i = 1; i < projectileCount; i++) { + if (entities[i] != null) { + entities[i].setCharging(true); + } + } + } + for (E entity : entities) { spawnAbilityEntity(entity); } @@ -299,7 +315,8 @@ public abstract class AbilityEnergyProjectile if (entity == null || entity.isDead) continue; // Check if entity was removed from world (chunk unload, etc.) - if (tick > 5 && entity.worldObj != null + // Skip this check for preview entities - they're not registered in the world + if (!isPreview() && tick > 5 && entity.worldObj != null && entity.worldObj.getEntityByID(entity.getEntityId()) != entity) { entity.setDead(); continue; diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java index d7322e90e..c98fae96a 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java @@ -66,7 +66,7 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit /** * Set charging state (server only, synced to clients via data watcher). */ - protected void setCharging(boolean value) { + public void setCharging(boolean value) { this.charging = value; if (!worldObj.isRemote) { this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index 9b111bde9..8f58fada7 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -19,6 +19,9 @@ import net.minecraft.world.World; import noppes.npcs.EventHooks; import noppes.npcs.NpcDamageSource; import noppes.npcs.api.entity.IEntity; +import noppes.npcs.controllers.PartyController; +import noppes.npcs.controllers.data.Party; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; @@ -207,7 +210,8 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { } // Set death time on first tick if not already set (handles chunk load/unload) - if (deathWorldTime < 0 && worldObj != null) { + // Skip during charging phase - lifetime starts when the entity actually fires + if (deathWorldTime < 0 && worldObj != null && !isCharging()) { deathWorldTime = worldObj.getTotalWorldTime() + getMaxLifetime(); } @@ -643,24 +647,50 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { /** * Check if an entity should be ignored for collision. - * Ignores: the owner, same-faction NPCs, and other projectiles from the same caster. + * Ignores: the owner, same-faction NPCs, passive NPCs, NPCs friendly to player owner, + * party members (if friendly fire is off), and other projectiles from the same caster. */ protected boolean shouldIgnoreEntity(Entity entity) { Entity owner = getOwnerEntity(); if (entity == owner) return true; - if (owner instanceof EntityNPCInterface && entity instanceof EntityNPCInterface) { - EntityNPCInterface ownerNpc = (EntityNPCInterface) owner; - EntityNPCInterface targetNpc = (EntityNPCInterface) entity; - if (ownerNpc.faction.id == targetNpc.faction.id) return true; - } - // Ignore other projectiles from the same caster (e.g. multi-projectile abilities) if (entity instanceof EntityEnergyProjectile) { EntityEnergyProjectile other = (EntityEnergyProjectile) entity; if (other.ownerEntityId == this.ownerEntityId) return true; } + // NPC target checks + if (entity instanceof EntityNPCInterface) { + EntityNPCInterface targetNpc = (EntityNPCInterface) entity; + + // Ignore passive NPCs + if (targetNpc.faction.isPassive) return true; + + // NPC owner: ignore same-faction NPCs + if (owner instanceof EntityNPCInterface) { + EntityNPCInterface ownerNpc = (EntityNPCInterface) owner; + if (ownerNpc.faction.id == targetNpc.faction.id) return true; + } + + // Player owner: ignore NPCs whose faction is friendly to the player + if (owner instanceof EntityPlayer) { + if (targetNpc.faction.isFriendlyToPlayer((EntityPlayer) owner)) return true; + } + } + + // Party friendly fire check: player owner hitting another player + if (owner instanceof EntityPlayer && entity instanceof EntityPlayer) { + EntityPlayer ownerPlayer = (EntityPlayer) owner; + EntityPlayer targetPlayer = (EntityPlayer) entity; + PlayerData ownerData = PlayerData.get(ownerPlayer); + PlayerData targetData = PlayerData.get(targetPlayer); + if (ownerData.partyUUID != null && ownerData.partyUUID.equals(targetData.partyUUID)) { + Party party = PartyController.Instance().getParty(ownerData.partyUUID); + if (party != null && !party.friendlyFire()) return true; + } + } + return false; } diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java new file mode 100644 index 000000000..774e9661e --- /dev/null +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -0,0 +1,478 @@ +package noppes.npcs; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.util.DamageSource; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; + +import java.util.List; + +/** + * Shared base class for NPC and Player ability data management. + * Contains the common ability lifecycle logic: tick, phase transitions, + * animation/telegraph/sound management, cooldown state, and position/rotation locking. + *

+ * Subclasses must implement abstract methods for entity-specific behavior: + * event firing, rotation/position application, telegraph packets, and cooldown rolling. + * + * @see DataAbilities - NPC implementation + * @see noppes.npcs.controllers.data.PlayerAbilityData - Player implementation + */ +public abstract class AbstractDataAbilities { + + // ═══════════════════════════════════════════════════════════════════ + // SHARED RUNTIME STATE (not saved to NBT) + // ═══════════════════════════════════════════════════════════════════ + + /** Currently executing ability (null if none) */ + protected Ability currentAbility; + + /** World time when cooldown ends */ + protected long cooldownEndTime = 0; + + /** Rotation lock state */ + protected boolean rotationLocked = false; + protected float lockedYaw = 0; + protected float lockedPitch = 0; + + /** Position lock state */ + protected boolean positionLocked = false; + protected double lockedPosX = 0; + protected double lockedPosY = 0; + protected double lockedPosZ = 0; + + // ═══════════════════════════════════════════════════════════════════ + // ABSTRACT METHODS - Subclasses must implement + // ═══════════════════════════════════════════════════════════════════ + + /** Get the entity this ability data belongs to (NPC or Player). */ + protected abstract EntityLivingBase getEntity(); + + /** Get the current target for ability execution. */ + protected abstract EntityLivingBase getTarget(); + + /** Get the current world time. */ + protected abstract long getWorldTime(); + + // -- Event Firing -- + + /** Fire tick event. Implementation differs between NPC (NpcAPI) and Player (ScriptController). */ + protected abstract void fireTickEvent(Ability ability, EntityLivingBase target); + + /** Fire execute event. Returns true if cancelled. */ + protected abstract boolean fireExecuteEvent(Ability ability, EntityLivingBase target); + + /** Fire complete event. */ + protected abstract void fireCompleteEvent(Ability ability, EntityLivingBase target); + + /** Fire interrupt event. */ + protected abstract void fireInterruptEvent(Ability ability, EntityLivingBase target, + DamageSource source, float damage); + + // -- Telegraph -- + + /** Spawn telegraphs for an ability. */ + protected abstract void spawnTelegraph(Ability ability, EntityLivingBase target); + + /** Remove telegraphs for an ability. */ + protected abstract void removeTelegraph(Ability ability); + + // -- Animation/Sound Data Access -- + + /** Set animation data on the entity. */ + protected abstract void setAnimationData(Animation animation); + + /** Clear animation data on the entity. */ + protected abstract void clearAnimationData(); + + /** Play a sound at the entity's location. */ + protected abstract void playAbilitySound(String sound); + + // -- Rotation (subclass handles extra fields and application) -- + + /** Capture rotation values for locking. NPC captures 4 fields, Player captures 2. */ + protected abstract void captureLockedRotation(); + + // -- Completion -- + + /** + * Roll cooldown after ability completes. + * NPC: random(min, max) + ability offset. Player: ability's base cooldownTicks. + */ + protected abstract void rollCooldown(Ability ability); + + /** Additional completion logic (NPC clears lastTarget, Player clears key/target and syncs). */ + protected abstract void onAbilityComplete(); + + + // -- Hooks (optional overrides) -- + + /** Called before onExecute in ACTIVE phase. NPC uses this for faceTarget/hitScan. */ + protected void onPreExecute(Ability ability, EntityLivingBase target) {} + + /** Called after the phase switch. NPC uses this for hit scan updates and movement control. */ + protected void onPostPhaseTick(Ability ability, EntityLivingBase target) {} + + /** Additional lock releases in BURST_DELAY. NPC releases hitScan. */ + protected void onBurstDelayReleaseLocks() {} + + // ═══════════════════════════════════════════════════════════════════ + // SHARED ANIMATION METHODS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Play an animation on the entity. + */ + public void playAbilityAnimation(Animation animation) { + if (animation == null) return; + if (AnimationController.Instance == null) return; + setAnimationData(animation); + } + + public void playAbilityAnimation(int animation) { + if (animation < 0) return; + if (AnimationController.Instance == null) return; + if (AnimationController.Instance.get(animation) == null) return; + playAbilityAnimation((Animation) AnimationController.Instance.get(animation)); + } + + public void playAbilityAnimation(String animation) { + if (animation.isEmpty()) return; + if (AnimationController.Instance == null) return; + if (AnimationController.Instance.get(animation, true) == null) return; + playAbilityAnimation((Animation) AnimationController.Instance.get(animation, true)); + } + + /** + * Stop any currently playing ability animation. + */ + protected void stopAbilityAnimation() { + clearAnimationData(); + } + + // ═══════════════════════════════════════════════════════════════════ + // STATE QUERIES + // ═══════════════════════════════════════════════════════════════════ + + /** Check if an ability is currently executing. */ + public boolean isExecutingAbility() { + return currentAbility != null && currentAbility.isExecuting(); + } + + /** Get the currently executing ability. */ + public Ability getCurrentAbility() { + return currentAbility; + } + + // ═══════════════════════════════════════════════════════════════════ + // COOLDOWN MANAGEMENT + // ═══════════════════════════════════════════════════════════════════ + + /** Check if on cooldown. */ + public boolean isOnCooldown() { + return getWorldTime() < cooldownEndTime; + } + + /** Get remaining cooldown ticks. */ + public long getRemainingCooldown() { + long remaining = cooldownEndTime - getWorldTime(); + return remaining > 0 ? remaining : 0; + } + + /** Reset cooldown (allow immediate ability use). */ + public void resetCooldown() { + cooldownEndTime = 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // POSITION LOCKING + // ═══════════════════════════════════════════════════════════════════ + + /** + * Capture current position to lock entity in place. + */ + protected void captureLockedPosition() { + EntityLivingBase entity = getEntity(); + lockedPosX = entity.posX; + lockedPosY = entity.posY; + lockedPosZ = entity.posZ; + positionLocked = true; + onPositionLockChanged(true); + } + + /** + * Release position lock. + */ + protected void releaseLockedPosition() { + positionLocked = false; + onPositionLockChanged(false); + } + + /** Hook for subclass to react to position lock state changes (e.g., NPC sets data watcher flag). */ + protected void onPositionLockChanged(boolean locked) {} + + // ═══════════════════════════════════════════════════════════════════ + // ROTATION LOCKING + // ═══════════════════════════════════════════════════════════════════ + + /** + * Release rotation control. + */ + protected void releaseRotationControl() { + rotationLocked = false; + onRotationLockChanged(false); + } + + /** Hook for subclass to react to rotation lock state changes (e.g., NPC sets flag, clears hitScan). */ + protected void onRotationLockChanged(boolean locked) {} + + // ═══════════════════════════════════════════════════════════════════ + // CORE TICK - Phase transition logic shared between NPC and Player + // ═══════════════════════════════════════════════════════════════════ + + /** + * Tick the currently executing ability. Handles all phase transitions, + * telegraph/animation/sound lifecycle, and burst management. + *

+ * This is the core shared logic between DataAbilities and PlayerAbilityData. + * Subclass-specific behavior is injected via abstract methods and hooks. + */ + protected void tickCurrentAbility() { + EntityLivingBase entity = getEntity(); + EntityLivingBase target = getTarget(); + AbilityPhase oldPhase = currentAbility.getPhase(); + + // Tick advances time and possibly changes phase + boolean phaseChanged = currentAbility.tick(); + + // Fire tick event + fireTickEvent(currentAbility, target); + + // Fire extender tick hook (e.g., per-tick resource drain) + if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, entity, target, + currentAbility.getPhase(), currentAbility.getCurrentTick())) { + interruptCurrentAbility(null, 0); + return; + } + + // Handle phase-specific logic + switch (currentAbility.getPhase()) { + case WINDUP: + if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) { + // Burst replay: re-enter windup - set up locks, telegraph, sound, animation + if (currentAbility.isRotationLockedDuringWindup()) { + captureLockedRotation(); + } + if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) { + captureLockedPosition(); + } + spawnTelegraph(currentAbility, target); + playAbilitySound(currentAbility.getWindUpSound()); + playAbilityAnimation(currentAbility.getWindUpAnimation()); + } + currentAbility.onWindUpTick(entity, target, currentAbility.getCurrentTick()); + break; + + case ACTIVE: + if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) { + // Just entered ACTIVE phase - lock telegraph positions + for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) { + telegraph.lockPosition(); + } + + // Remove telegraph unless the ability keeps it during active phase + if (!currentAbility.keepTelegraphDuringActive()) { + removeTelegraph(currentAbility); + } + + // Handle rotation control transition from WINDUP to ACTIVE + if (currentAbility.isRotationLockedDuringActive()) { + if (!rotationLocked) { + captureLockedRotation(); + } + } else if (rotationLocked) { + releaseRotationControl(); + } + + // Handle position lock transition from WINDUP to ACTIVE + if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) { + if (!positionLocked) { + captureLockedPosition(); + } + } else if (positionLocked) { + releaseLockedPosition(); + } + + // Play active sound and animation + if (!currentAbility.keepTelegraphDuringActive()) { + playAbilitySound(currentAbility.getActiveSound()); + } + playAbilityAnimation(currentAbility.getActiveAnimation()); + + // Pre-execute hook (NPC: snap to face target for hit scan) + onPreExecute(currentAbility, target); + + // Fire execute event (cancelable) + if (fireExecuteEvent(currentAbility, target)) { + return; // Cancelled + } + + // Call onExecute + currentAbility.onExecute(entity, target); + + // Check if ability completed during onExecute + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + return; + } + } + + currentAbility.onActiveTick(entity, target, currentAbility.getCurrentTick()); + + // Check if ability completed during onActiveTick + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + return; + } + + // Auto-complete for burst overlap mode + if (currentAbility.isBurstEnabled() + && currentAbility.getBurstIndex() < currentAbility.getBurstAmount() + && currentAbility.getPhase() == AbilityPhase.ACTIVE + && currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) { + currentAbility.signalCompletion(); + } + + // Check if ability entered burst delay + if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) { + if (rotationLocked) releaseRotationControl(); + if (positionLocked) releaseLockedPosition(); + onBurstDelayReleaseLocks(); + } + break; + + case BURST_DELAY: + // Free movement and rotation during burst delay + if (rotationLocked) releaseRotationControl(); + if (positionLocked) releaseLockedPosition(); + break; + + case DAZED: + // Dazed phase - just wait for phase transition + break; + + case IDLE: + // Ability completed (reached via tick() from DAZED) + handleAbilityCompletion(target); + break; + } + + // Post-phase hook (NPC: hit scan + movement control) + onPostPhaseTick(currentAbility, target); + } + + // ═══════════════════════════════════════════════════════════════════ + // ABILITY COMPLETION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Handle ability completion. Called when ability phase becomes IDLE. + */ + protected void handleAbilityCompletion(EntityLivingBase target) { + if (currentAbility == null) return; + + // Remove any remaining telegraphs + removeTelegraph(currentAbility); + + // Fire extender complete hook + AbilityController.Instance.fireOnAbilityComplete(currentAbility, getEntity(), target, false); + + // Call onComplete callback + currentAbility.onComplete(getEntity(), target); + + // Fire complete event + fireCompleteEvent(currentAbility, target); + + // Release locks + releaseRotationControl(); + releaseLockedPosition(); + + // Subclass-specific completion (cooldown, cleanup, sync) + rollCooldown(currentAbility); + stopAbilityAnimation(); + onAbilityComplete(); + } + + // ═══════════════════════════════════════════════════════════════════ + // EXECUTE IMMEDIATE + // ═══════════════════════════════════════════════════════════════════ + + /** + * Execute an ability immediately (no windup). Called when windUpTicks is 0. + */ + protected void executeImmediate(Ability ability, EntityLivingBase target) { + // Play active sound and animation + playAbilitySound(ability.getActiveSound()); + playAbilityAnimation(ability.getActiveAnimation()); + + // Fire execute event (cancelable) + if (fireExecuteEvent(ability, target)) { + return; // Cancelled + } + + // Call onExecute + ability.onExecute(getEntity(), target); + + // Check if ability completed during onExecute + if (ability.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // INTERRUPTION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Interrupt the currently executing ability. + * Shared interrupt logic: remove telegraph, play dazed animation, + * release locks, fire events, call ability.interrupt(). + */ + public void interruptCurrentAbility(DamageSource source, float damage) { + if (currentAbility != null && currentAbility.isExecuting()) { + // Remove telegraph + removeTelegraph(currentAbility); + + // Fire extender complete hook (interrupted) + AbilityController.Instance.fireOnAbilityComplete(currentAbility, getEntity(), getTarget(), true); + + // Fire interrupt event + fireInterruptEvent(currentAbility, getTarget(), source, damage); + + // Call ability callbacks + currentAbility.onInterrupt(getEntity(), source, damage); + currentAbility.interrupt(); + + // Play dazed animation + stopAbilityAnimation(); + playAbilityAnimation(currentAbility.getDazedAnimation()); + + // Release locks + releaseRotationControl(); + releaseLockedPosition(); + + // Let subclass handle cleanup + onInterruptComplete(); + } + } + + /** + * Hook called after interrupt completes. Subclass handles clearing state and syncing. + * NPC: keeps currentAbility (ticks through DAZED). Player: clears immediately. + */ + protected void onInterruptComplete() {} +} diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 22f7100ae..cb1e5104d 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -12,22 +12,20 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.DamageSource; -import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; - import java.util.ArrayList; import java.util.List; import java.util.Random; /** * Manages NPC abilities - storage, selection, execution, and cooldowns. - * Follows the DataStats/DataAI pattern for NPC data management. + * Extends AbstractDataAbilities for shared ability lifecycle logic. */ -public class DataAbilities { +public class DataAbilities extends AbstractDataAbilities { private final EntityNPCInterface npc; private final Random random = new Random(); @@ -57,19 +55,9 @@ public class DataAbilities { public int maxCooldown = 200; // ═══════════════════════════════════════════════════════════════════ - // RUNTIME STATE (not saved) + // NPC-SPECIFIC RUNTIME STATE (not saved) // ═══════════════════════════════════════════════════════════════════ - /** - * Currently executing ability (null if none) - */ - private transient Ability currentAbility; - - /** - * World time when NPC cooldown ends (can select next ability) - */ - private transient long cooldownEndTime = 0; - /** * Last target used for ability execution */ @@ -81,33 +69,26 @@ public class DataAbilities { private transient List recentHitTimes = new ArrayList<>(); /** - * Locked rotation values for ACTIVE phase when movement is locked. - * Stored when entering ACTIVE phase, applied every tick to prevent rotation changes. + * NPC-specific locked rotation fields (yawHead, renderYawOffset). + * Base class handles lockedYaw and lockedPitch. */ - private transient boolean rotationLocked = false; - private transient float lockedYaw = 0; private transient float lockedYawHead = 0; private transient float lockedRenderYawOffset = 0; - private transient float lockedPitch = 0; - - /** - * Locked position values for movement lock. - * Stored when entering a locked phase, applied after super.onLivingUpdate() - * to override any position changes from AI/physics. - */ - private transient boolean positionLocked = false; - private transient double lockedPosX = 0; - private transient double lockedPosY = 0; - private transient double lockedPosZ = 0; /** * Hit scan state - forces NPC to face target. - * Set during tickCurrentAbility(), applied after super.onLivingUpdate() + * Set during onPostPhaseTick(), applied after super.onLivingUpdate() * to override AI look helper rotation. */ private transient boolean hitScanActive = false; private transient EntityLivingBase hitScanTarget = null; + /** Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 */ + private static final int ROTATION_CONTROLLED_FLAG = 16; + + /** Bit flag for position lock in data watcher slot 15 */ + private static final int POSITION_LOCKED_FLAG = 32; + // ═══════════════════════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════════════════════ @@ -116,6 +97,170 @@ public class DataAbilities { this.npc = npc; } + // ═══════════════════════════════════════════════════════════════════ + // ABSTRACT METHOD IMPLEMENTATIONS + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected EntityLivingBase getEntity() { + return npc; + } + + @Override + protected EntityLivingBase getTarget() { + return lastTarget != null ? lastTarget : npc.getAttackTarget(); + } + + @Override + protected long getWorldTime() { + return npc.worldObj.getTotalWorldTime(); + } + + @Override + protected void fireTickEvent(Ability ability, EntityLivingBase target) { + AbilityEvent.TickEvent event = new AbilityEvent.TickEvent( + npc.wrappedNPC, ability, target, + ability.getPhase().ordinal(), ability.getCurrentTick()); + NpcAPI.EVENT_BUS.post(event); + } + + @Override + protected boolean fireExecuteEvent(Ability ability, EntityLivingBase target) { + AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent( + npc.wrappedNPC, ability, target); + return NpcAPI.EVENT_BUS.post(executeEvent); + } + + @Override + protected void fireCompleteEvent(Ability ability, EntityLivingBase target) { + AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent( + npc.wrappedNPC, ability, target); + NpcAPI.EVENT_BUS.post(completeEvent); + } + + @Override + protected void fireInterruptEvent(Ability ability, EntityLivingBase target, + DamageSource source, float damage) { + AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent( + npc.wrappedNPC, ability, target, source, damage); + NpcAPI.EVENT_BUS.post(interruptEvent); + } + + @Override + protected void spawnTelegraph(Ability ability, EntityLivingBase target) { + List telegraphs = ability.createTelegraphs(npc, target); + if (!telegraphs.isEmpty()) { + ability.setTelegraphInstances(telegraphs); + for (TelegraphInstance telegraph : telegraphs) { + TelegraphSpawnPacket.sendToTracking(telegraph, npc); + } + } + } + + @Override + protected void removeTelegraph(Ability ability) { + List telegraphs = ability.getTelegraphInstances(); + for (TelegraphInstance telegraph : telegraphs) { + TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), npc); + } + ability.setTelegraphInstances(null); + } + + @Override + protected void setAnimationData(Animation animation) { + npc.display.animationData.setEnabled(true); + npc.display.animationData.setAnimation(animation); + npc.display.animationData.updateClient(); + } + + @Override + protected void clearAnimationData() { + npc.display.animationData.setAnimation(null); + npc.display.animationData.updateClient(); + } + + @Override + protected void playAbilitySound(String sound) { + if (sound != null && !sound.isEmpty()) { + npc.worldObj.playSoundAtEntity(npc, sound, 1.0f, 1.0f); + } + } + + @Override + protected void captureLockedRotation() { + lockedYaw = npc.rotationYaw; + lockedYawHead = npc.rotationYawHead; + lockedRenderYawOffset = npc.renderYawOffset; + lockedPitch = npc.rotationPitch; + rotationLocked = true; + npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG); + } + + @Override + protected void rollCooldown(Ability ability) { + int baseCooldown = minCooldown; + if (maxCooldown > minCooldown) { + baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); + } + cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown; + // Add ability-specific cooldown offset + cooldownEndTime += ability.getCooldownTicks(); + } + + @Override + protected void onAbilityComplete() { + currentAbility = null; + lastTarget = null; + } + + // ═══════════════════════════════════════════════════════════════════ + // HOOK OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void onPreExecute(Ability ability, EntityLivingBase target) { + // Snap NPC to face target before execute so startMoving() reads correct rotation + if (ability.isHitScanForCurrentPhase() && target != null) { + faceTarget(target); + } + } + + @Override + protected void onPostPhaseTick(Ability ability, EntityLivingBase target) { + // Update hit scan state - actual facing is deferred to applyRotationControl() + // which runs AFTER super.onLivingUpdate() to override AI look helper + if (ability != null && ability.isExecuting() + && ability.isHitScanForCurrentPhase() && target != null) { + enableHitScan(target); + } else if (hitScanActive) { + releaseRotationControl(); + } + + // Apply movement control if ability is still executing + if (ability != null && ability.isExecuting()) { + applyMovementControl(); + } + } + + @Override + protected void onBurstDelayReleaseLocks() { + if (hitScanActive) releaseRotationControl(); + } + + @Override + protected void onPositionLockChanged(boolean locked) { + npc.setBoolFlag(locked, POSITION_LOCKED_FLAG); + } + + @Override + protected void onRotationLockChanged(boolean locked) { + if (!locked) { + hitScanActive = false; + hitScanTarget = null; + } + npc.setBoolFlag(locked, ROTATION_CONTROLLED_FLAG); + } + // ═══════════════════════════════════════════════════════════════════ // TICK - Called from EntityNPCInterface.onLivingUpdate() // ═══════════════════════════════════════════════════════════════════ @@ -152,173 +297,15 @@ public class DataAbilities { if (positionLocked) releaseLockedPosition(); } - // Tick current ability if executing + // Tick current ability if executing (shared logic from base class) if (currentAbility != null && currentAbility.isExecuting()) { tickCurrentAbility(); } } - /** - * Tick the currently executing ability. - */ - private void tickCurrentAbility() { - EntityLivingBase target = lastTarget != null ? lastTarget : npc.getAttackTarget(); - AbilityPhase oldPhase = currentAbility.getPhase(); - - // Tick advances time and possibly changes phase - boolean phaseChanged = currentAbility.tick(); - - // Fire tick event for scripts - fireTickEvent(currentAbility, target); - - // Fire extender tick hook (e.g., per-tick resource drain) - if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, npc, target, - currentAbility.getPhase(), currentAbility.getCurrentTick())) { - interruptCurrentAbility(null, 0); - return; - } - - // Handle phase-specific logic - switch (currentAbility.getPhase()) { - case WINDUP: - if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) { - // Burst replay: re-enter windup - set up telegraph, sound, animation, locks - if (currentAbility.isRotationLockedDuringWindup()) { - captureLockedRotation(); - } - if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) { - captureLockedPosition(); - } - spawnTelegraph(currentAbility, target); - playAbilitySound(currentAbility.getWindUpSound()); - playAbilityAnimation(currentAbility.getWindUpAnimation()); - } - currentAbility.onWindUpTick(npc, target, currentAbility.getCurrentTick()); - break; - - case ACTIVE: - if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) { - // Just entered ACTIVE phase - lock telegraph positions if they were following - // This commits the ability to its current target position - for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) { - telegraph.lockPosition(); - } - - // Remove telegraph unless the ability keeps it during active phase - // (e.g., sweeping attacks where the telegraph shows the ongoing danger zone) - if (!currentAbility.keepTelegraphDuringActive()) { - removeTelegraph(currentAbility); - } - - // Handle rotation control transition from WINDUP to ACTIVE - if (currentAbility.isRotationLockedDuringActive()) { - if (!rotationLocked) { - captureLockedRotation(); - } - } else if (rotationLocked) { - releaseRotationControl(); - } - - // Handle position lock transition from WINDUP to ACTIVE - if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) { - if (!positionLocked) { - captureLockedPosition(); - } - } else if (positionLocked) { - releaseLockedPosition(); - } - - // Play active sound and animation - // Abilities that keep telegraph during active manage their own sound timing - if (!currentAbility.keepTelegraphDuringActive()) { - playAbilitySound(currentAbility.getActiveSound()); - } - playAbilityAnimation(currentAbility.getActiveAnimation()); - - // Snap NPC to face target before execute so startMoving() reads correct rotation - if (currentAbility.isHitScanForCurrentPhase() && target != null) { - faceTarget(target); - } - - // Fire execute event (cancelable) - AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent( - npc.wrappedNPC, currentAbility, target); - if (NpcAPI.EVENT_BUS.post(executeEvent)) { - // Event was cancelled - skip execution but continue to recovery - return; - } - - // Call onExecute - currentAbility.onExecute(npc, target); - - // Check if ability completed during onExecute (signalCompletion was called) - if (currentAbility.getPhase() == AbilityPhase.IDLE) { - handleAbilityCompletion(target); - return; - } - } - currentAbility.onActiveTick(npc, target, currentAbility.getCurrentTick()); - - // Check if ability completed during onActiveTick (signalCompletion was called) - if (currentAbility.getPhase() == AbilityPhase.IDLE) { - handleAbilityCompletion(target); - return; - } - - // Auto-complete for burst overlap mode (entities fly independently) - // This allows entity-spawning abilities to immediately chain next burst - // without each type needing to check burst state in their onActiveTick() - // Wait until all staggered projectiles have been fired before auto-completing - if (currentAbility.isBurstEnabled() - && currentAbility.getBurstIndex() < currentAbility.getBurstAmount() - && currentAbility.getPhase() == AbilityPhase.ACTIVE - && currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) { - currentAbility.signalCompletion(); - } - - // Check if ability entered burst delay during onActiveTick or overlap auto-complete - if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) { - // Release all locks - free movement/rotation during delay - if (rotationLocked) releaseRotationControl(); - if (positionLocked) releaseLockedPosition(); - if (hitScanActive) releaseRotationControl(); - } - break; - - case BURST_DELAY: - // Free movement and rotation during burst delay - if (rotationLocked) releaseRotationControl(); - if (positionLocked) releaseLockedPosition(); - break; - - case DAZED: - // Dazed phase - NPC cannot attack, just wait for daze to end - break; - - case IDLE: - // Ability completed (reached via tick() phase transition from DAZED) - handleAbilityCompletion(target); - break; - } - - // Update hit scan state - actual facing is deferred to applyRotationControl() - // which runs AFTER super.onLivingUpdate() to override AI look helper - if (currentAbility != null && currentAbility.isExecuting() - && currentAbility.isHitScanForCurrentPhase() && target != null) { - enableHitScan(target); - } else if (hitScanActive) { - releaseRotationControl(); - } - - // Apply movement control if ability is still executing - if (currentAbility != null && currentAbility.isExecuting()) { - applyMovementControl(); - } - } - /** * Apply movement control based on lock movement settings. - * Called every tick during ability execution. + * Called every tick during ability execution via onPostPhaseTick(). */ private void applyMovementControl() { if (currentAbility == null) return; @@ -337,61 +324,6 @@ public class DataAbilities { } } - /** - * Handle ability completion - called when ability phase becomes IDLE. - * This can happen from: - * - signalCompletion() called in onExecute() or onActiveTick() - * - tick() transitioning from DAZED to IDLE - */ - private void handleAbilityCompletion(EntityLivingBase target) { - if (currentAbility == null) return; - - // Remove any remaining telegraphs (for abilities that keep telegraph during active) - removeTelegraph(currentAbility); - - // Fire extender complete hook - AbilityController.Instance.fireOnAbilityComplete(currentAbility, npc, target, false); - - // Call onComplete callback - currentAbility.onComplete(npc, target); - - // Fire complete event - AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent( - npc.wrappedNPC, currentAbility, target); - NpcAPI.EVENT_BUS.post(completeEvent); - - // Release locks before cleanup - releaseRotationControl(); - releaseLockedPosition(); - - // Clean up and roll cooldown (this sets currentAbility to null) - onAbilityComplete(); - } - - /** - * Called when an ability completes. - * Calculates cooldown as: random(minCooldown, maxCooldown) + ability's cooldown offset - */ - private void onAbilityComplete() { - if (currentAbility != null) { - // Stop any ability animation - stopAbilityAnimation(); - - // Release locks - releaseRotationControl(); - releaseLockedPosition(); - - // Calculate cooldown: random(min, max) + ability offset - int abilityCooldownOffset = currentAbility.getCooldownTicks(); - rollCooldown(); - // Add ability-specific cooldown offset - cooldownEndTime += abilityCooldownOffset; - - currentAbility = null; - lastTarget = null; - } - } - // ═══════════════════════════════════════════════════════════════════ // ABILITY SELECTION - Called from CombatHandler // ═══════════════════════════════════════════════════════════════════ @@ -521,7 +453,13 @@ public class DataAbilities { ability.start(target); if (ability.getPhase() == AbilityPhase.ACTIVE) { - // Windup was 0 — skip telegraph/windup and go straight to active + // Windup was 0 — capture locks for immediate active phase, then execute + if (ability.isRotationLockedDuringActive()) { + captureLockedRotation(); + } + if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) { + captureLockedPosition(); + } executeImmediate(ability, target); } else { // Normal windup flow @@ -539,137 +477,6 @@ public class DataAbilities { return true; } - /** - * Execute an ability immediately (no windup). - * Called when windUpTicks is 0. - */ - private void executeImmediate(Ability ability, EntityLivingBase target) { - // Lock rotation if rotation is locked during ACTIVE phase - if (ability.isRotationLockedDuringActive()) { - captureLockedRotation(); - } - if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) { - captureLockedPosition(); - } - - // Play active sound and animation - playAbilitySound(ability.getActiveSound()); - playAbilityAnimation(ability.getActiveAnimation()); - - // Fire execute event (cancelable) - AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent( - npc.wrappedNPC, ability, target); - if (NpcAPI.EVENT_BUS.post(executeEvent)) { - return; - } - - // Call onExecute - ability.onExecute(npc, target); - - // Check if ability completed during onExecute - if (ability.getPhase() == AbilityPhase.IDLE) { - handleAbilityCompletion(target); - } - } - - /** - * Play a sound at the NPC's location. - */ - private void playAbilitySound(String sound) { - if (sound != null && !sound.isEmpty()) { - npc.worldObj.playSoundAtEntity(npc, sound, 1.0f, 1.0f); - } - } - - /** - * Play an animation on the NPC by ID. - * Public so abilities can trigger animations directly if needed. - * - * @param animation The animation to play, or null if none - */ - public void playAbilityAnimation(Animation animation) { - if (animation == null) return; - if (AnimationController.Instance == null) return; - - npc.display.animationData.setEnabled(true); - npc.display.animationData.setAnimation(animation); - npc.display.animationData.updateClient(); - } - - public void playAbilityAnimation(int animation) { - if (animation < 0) return; - if (AnimationController.Instance == null) return; - if (AnimationController.Instance.get(animation) == null) return; - - playAbilityAnimation((Animation) AnimationController.Instance.get(animation)); - } - - public void playAbilityAnimation(String animation) { - if (animation.isEmpty()) return; - if (AnimationController.Instance == null) return; - if (AnimationController.Instance.get(animation, true) == null) return; - - playAbilityAnimation((Animation) AnimationController.Instance.get(animation, true)); - } - - /** - * Stop any currently playing ability animation. - */ - private void stopAbilityAnimation() { - npc.display.animationData.setAnimation(null); - npc.display.animationData.updateClient(); - } - - /** - * Spawn and send telegraphs for an ability. - */ - private void spawnTelegraph(Ability ability, EntityLivingBase target) { - List telegraphs = ability.createTelegraphs(npc, target); - if (!telegraphs.isEmpty()) { - ability.setTelegraphInstances(telegraphs); - for (TelegraphInstance telegraph : telegraphs) { - TelegraphSpawnPacket.sendToTracking(telegraph, npc); - } - } - } - - /** - * Remove telegraphs when ability ends or is interrupted. - */ - private void removeTelegraph(Ability ability) { - List telegraphs = ability.getTelegraphInstances(); - for (TelegraphInstance telegraph : telegraphs) { - TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), npc); - } - ability.setTelegraphInstances(null); - } - - // ═══════════════════════════════════════════════════════════════════ - // COOLDOWN MANAGEMENT - // ═══════════════════════════════════════════════════════════════════ - - /** - * Check if NPC is on cooldown (cannot use any ability). - */ - public boolean isOnCooldown() { - return npc.worldObj.getTotalWorldTime() < cooldownEndTime; - } - - /** - * Get remaining cooldown ticks. - */ - public long getRemainingCooldown() { - long remaining = cooldownEndTime - npc.worldObj.getTotalWorldTime(); - return remaining > 0 ? remaining : 0; - } - - /** - * Reset cooldown (allow immediate ability use). - */ - public void resetCooldown() { - cooldownEndTime = 0; - } - // ═══════════════════════════════════════════════════════════════════ // INTERRUPTION - Called from CombatHandler on damage // ═══════════════════════════════════════════════════════════════════ @@ -748,39 +555,6 @@ public class DataAbilities { return count; } - /** - * Interrupt the currently executing ability. - * If interrupted during WINDUP, the ability will transition to DAZED phase. - * When DAZED completes, the ability will move to IDLE and onAbilityComplete() is called. - */ - public void interruptCurrentAbility(DamageSource source, float damage) { - if (currentAbility != null) { - // Remove telegraph early - removeTelegraph(currentAbility); - - // Plays dazed animation - playAbilityAnimation(currentAbility.getDazedAnimation()); - - // Release locks - releaseRotationControl(); - releaseLockedPosition(); - - // Fire extender complete hook (interrupted) - AbilityController.Instance.fireOnAbilityComplete(currentAbility, npc, lastTarget, true); - - // Fire interrupt event - AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent( - npc.wrappedNPC, currentAbility, lastTarget, source, damage); - NpcAPI.EVENT_BUS.post(interruptEvent); - - currentAbility.onInterrupt(npc, source, damage); - currentAbility.interrupt(); // Transitions to DAZED if interrupted during WINDUP - - // Don't clear currentAbility - let it tick through DAZED phase - // When DAZED ends and phase becomes IDLE, onAbilityComplete() will be called - } - } - /** * Force stop the current ability (no interrupt event). */ @@ -849,16 +623,6 @@ public class DataAbilities { // SCRIPT EVENTS // ═══════════════════════════════════════════════════════════════════ - /** - * Fire the ability tick event for scripts. - */ - private void fireTickEvent(Ability ability, EntityLivingBase target) { - AbilityEvent.TickEvent event = new AbilityEvent.TickEvent( - npc.wrappedNPC, ability, target, - ability.getPhase().ordinal(), ability.getCurrentTick()); - NpcAPI.EVENT_BUS.post(event); - } - /** * Fire an ability hit event. Called by abilities when they hit an entity. * Returns null if the event was cancelled, otherwise returns the (possibly modified) event. @@ -902,7 +666,7 @@ public class DataAbilities { stopCurrentAbility(); // Roll cooldown so NPC doesn't immediately attack after reset - rollCooldown(); + rollCooldownOnReset(); // Reset execution state on all resolved abilities for (Ability ability : getAbilities()) { @@ -911,10 +675,10 @@ public class DataAbilities { } /** - * Roll a new cooldown using the min/max range. - * Called after ability completes and on reset. + * Roll a new cooldown using the min/max range (without ability offset). + * Called on reset when no ability is involved. */ - private void rollCooldown() { + private void rollCooldownOnReset() { int baseCooldown = minCooldown; if (maxCooldown > minCooldown) { baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); @@ -1002,23 +766,9 @@ public class DataAbilities { } // ═══════════════════════════════════════════════════════════════════ - // STATE QUERIES + // STATE QUERIES (NPC-specific) // ═══════════════════════════════════════════════════════════════════ - /** - * Check if NPC is currently executing an ability. - */ - public boolean isExecutingAbility() { - return currentAbility != null && currentAbility.isExecuting(); - } - - /** - * Get the currently executing ability. - */ - public Ability getCurrentAbility() { - return currentAbility; - } - /** * Check if ability is controlling NPC movement (AI pathfinding should be blocked). * Called by AI tasks to determine if they should try to path. @@ -1086,35 +836,9 @@ public class DataAbilities { } // ═══════════════════════════════════════════════════════════════════ - // ROTATION CONTROL + // ROTATION CONTROL (NPC-specific: hit scan + 4-field rotation) // ═══════════════════════════════════════════════════════════════════ - /** Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 */ - private static final int ROTATION_CONTROLLED_FLAG = 16; - - /** - * Capture current rotation values to lock NPC's look direction. - * Called when entering a LOCKED rotation phase. - */ - private void captureLockedRotation() { - lockedYaw = npc.rotationYaw; - lockedYawHead = npc.rotationYawHead; - lockedRenderYawOffset = npc.renderYawOffset; - lockedPitch = npc.rotationPitch; - rotationLocked = true; - npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG); - } - - /** - * Release rotation control (both locked and hit scan). - */ - private void releaseRotationControl() { - rotationLocked = false; - hitScanActive = false; - hitScanTarget = null; - npc.setBoolFlag(false, ROTATION_CONTROLLED_FLAG); - } - /** * Enable hit scan tracking for the given target. */ @@ -1195,33 +919,9 @@ public class DataAbilities { } // ═══════════════════════════════════════════════════════════════════ - // POSITION LOCKING + // POSITION LOCKING (NPC-specific: data watcher + client-side logic) // ═══════════════════════════════════════════════════════════════════ - /** Bit flag for position lock in data watcher slot 15 */ - private static final int POSITION_LOCKED_FLAG = 32; - - /** - * Capture current position to lock NPC in place. - * Called when entering a phase with movement lock. - */ - private void captureLockedPosition() { - lockedPosX = npc.posX; - lockedPosY = npc.posY; - lockedPosZ = npc.posZ; - positionLocked = true; - npc.setBoolFlag(true, POSITION_LOCKED_FLAG); - } - - /** - * Release the position lock. - * Called when leaving the locked phase or ability completes. - */ - private void releaseLockedPosition() { - positionLocked = false; - npc.setBoolFlag(false, POSITION_LOCKED_FLAG); - } - /** * Apply locked position values to the NPC. * Called AFTER super.onLivingUpdate() in EntityNPCInterface to override diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 04dadc76f..f5ffb7028 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -3,7 +3,6 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; -import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; @@ -15,6 +14,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.nbt.NBTTagString; import net.minecraft.util.DamageSource; +import noppes.npcs.AbstractDataAbilities; import noppes.npcs.LogWriter; import noppes.npcs.EventHooks; import noppes.npcs.api.ability.IPlayerAbilityData; @@ -35,6 +35,7 @@ import java.util.List; /** * Manages player ability data - unlocked abilities, selection, cooldowns, and execution. * Players reference abilities by key (built-in keys or preset names), never customized copies. + * Extends AbstractDataAbilities for shared ability lifecycle logic. *

* Key differences from NPC DataAbilities: * - Universal cooldown (shared across all abilities) @@ -42,7 +43,7 @@ import java.util.List; * - Conditions requiring a target are skipped * - Uses ability's base cooldownTicks only */ -public class PlayerAbilityData implements IPlayerAbilityData { +public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerAbilityData { private final PlayerData playerData; @@ -69,19 +70,9 @@ public class PlayerAbilityData implements IPlayerAbilityData { private boolean playingAbilityAnimation = false; // ═══════════════════════════════════════════════════════════════════ - // RUNTIME STATE (not saved) + // PLAYER-SPECIFIC RUNTIME STATE (not saved) // ═══════════════════════════════════════════════════════════════════ - /** - * Universal cooldown end time (shared across all abilities). - */ - private transient long cooldownEndTime = 0; - - /** - * Currently executing ability instance (null if none). - */ - private transient Ability currentAbility; - /** * Current ability's key (for cooldown tracking after completion). */ @@ -92,21 +83,6 @@ public class PlayerAbilityData implements IPlayerAbilityData { */ private transient EntityLivingBase currentTarget; - /** - * Rotation lock state - freezes player yaw/pitch during ability phases. - */ - private transient boolean rotationLocked = false; - private transient float lockedYaw = 0; - private transient float lockedPitch = 0; - - /** - * Position lock state - freezes player position during ability phases. - */ - private transient boolean positionLocked = false; - private transient double lockedPosX = 0; - private transient double lockedPosY = 0; - private transient double lockedPosZ = 0; - /** * Last synced state flags byte for change detection (avoids spamming packets). */ @@ -120,6 +96,160 @@ public class PlayerAbilityData implements IPlayerAbilityData { this.playerData = playerData; } + // ═══════════════════════════════════════════════════════════════════ + // ABSTRACT METHOD IMPLEMENTATIONS + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected EntityLivingBase getEntity() { + return playerData.player; + } + + @Override + protected EntityLivingBase getTarget() { + return currentTarget; + } + + @Override + protected long getWorldTime() { + EntityPlayer player = playerData.player; + return player != null ? player.worldObj.getTotalWorldTime() : 0; + } + + @Override + protected void fireTickEvent(Ability ability, EntityLivingBase target) { + EntityPlayer player = playerData.player; + if (ScriptController.Instance == null || player == null) return; + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + if (handler == null) return; + IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); + PlayerAbilityEvent.TickEvent event = new PlayerAbilityEvent.TickEvent( + iPlayer, ability, target, ability.getPhase().ordinal(), ability.getCurrentTick()); + EventHooks.onPlayerAbilityTick(handler, event); + } + + @Override + protected boolean fireExecuteEvent(Ability ability, EntityLivingBase target) { + EntityPlayer player = playerData.player; + if (ScriptController.Instance == null || player == null) return false; + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + if (handler == null) return false; + IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); + PlayerAbilityEvent.ExecuteEvent event = new PlayerAbilityEvent.ExecuteEvent(iPlayer, ability, target); + return EventHooks.onPlayerAbilityExecute(handler, event); + } + + @Override + protected void fireCompleteEvent(Ability ability, EntityLivingBase target) { + EntityPlayer player = playerData.player; + if (ScriptController.Instance == null || player == null) return; + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + if (handler == null) return; + IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); + PlayerAbilityEvent.CompleteEvent event = new PlayerAbilityEvent.CompleteEvent(iPlayer, ability, target); + EventHooks.onPlayerAbilityComplete(handler, event); + } + + @Override + protected void fireInterruptEvent(Ability ability, EntityLivingBase target, + DamageSource source, float damage) { + EntityPlayer player = playerData.player; + if (ScriptController.Instance == null || player == null) return; + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + if (handler == null) return; + IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); + PlayerAbilityEvent.InterruptEvent event = new PlayerAbilityEvent.InterruptEvent( + iPlayer, ability, target, source, damage); + EventHooks.onPlayerAbilityInterrupt(handler, event); + } + + @Override + protected void spawnTelegraph(Ability ability, EntityLivingBase target) { + EntityPlayer player = playerData.player; + List telegraphs = ability.createTelegraphs(player, target); + if (!telegraphs.isEmpty()) { + ability.setTelegraphInstances(telegraphs); + if (player instanceof EntityPlayerMP) { + for (TelegraphInstance telegraph : telegraphs) { + TelegraphSpawnPacket.sendToTracking(telegraph, player); + } + } + } + } + + @Override + protected void removeTelegraph(Ability ability) { + EntityPlayer player = playerData.player; + List telegraphs = ability.getTelegraphInstances(); + for (TelegraphInstance telegraph : telegraphs) { + if (player instanceof EntityPlayerMP) { + TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player); + } + } + ability.setTelegraphInstances(null); + } + + @Override + protected void setAnimationData(Animation animation) { + playerData.animationData.setEnabled(true); + playerData.animationData.setAnimation(animation); + playerData.animationData.updateClient(); + playingAbilityAnimation = true; + } + + @Override + protected void clearAnimationData() { + playerData.animationData.setAnimation(null); + playerData.animationData.updateClient(); + playingAbilityAnimation = false; + } + + @Override + protected void playAbilitySound(String sound) { + if (sound != null && !sound.isEmpty()) { + EntityPlayer player = playerData.player; + if (player != null) { + player.worldObj.playSoundAtEntity(player, sound, 1.0f, 1.0f); + } + } + } + + @Override + protected void captureLockedRotation() { + EntityPlayer player = playerData.player; + lockedYaw = player.rotationYaw; + lockedPitch = player.rotationPitch; + rotationLocked = true; + } + + @Override + protected void rollCooldown(Ability ability) { + if (!ability.isIgnoreCooldown()) { + cooldownEndTime = getWorldTime() + ability.getCooldownTicks(); + } + } + + @Override + protected void onAbilityComplete() { + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + syncAbilityStateClear(playerData.player); + } + + // ═══════════════════════════════════════════════════════════════════ + // HOOK OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void onInterruptComplete() { + // Player clears state immediately (unlike NPC which ticks through DAZED) + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + syncAbilityStateClear(playerData.player); + } + // ═══════════════════════════════════════════════════════════════════ // TICK - Called from player tick handler // ═══════════════════════════════════════════════════════════════════ @@ -134,7 +264,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { } if (currentAbility != null && currentAbility.isExecuting()) { - tickCurrentAbility(player); + tickCurrentAbility(); // Apply rotation and position locks after ability tick applyRotationControl(player); @@ -152,160 +282,6 @@ public class PlayerAbilityData implements IPlayerAbilityData { } } - /** - * Tick the currently executing ability. - */ - private void tickCurrentAbility(EntityPlayer player) { - AbilityPhase oldPhase = currentAbility.getPhase(); - - // Tick advances time and possibly changes phase - boolean phaseChanged = currentAbility.tick(); - - // Fire tick event for scripts - firePlayerTickEvent(player, currentAbility, currentTarget); - - // Fire extender tick hook (e.g., per-tick resource drain) - if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, player, currentTarget, - currentAbility.getPhase(), currentAbility.getCurrentTick())) { - interruptCurrentAbility(); - return; - } - - switch (currentAbility.getPhase()) { - case WINDUP: - if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) { - // Burst replay: re-enter windup - set up locks - if (currentAbility.isRotationLockedDuringWindup()) { - captureLockedRotation(player); - } - if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) { - captureLockedPosition(player); - } - spawnTelegraph(currentAbility, player, null); - playAbilitySound(player, currentAbility.getWindUpSound()); - playAbilityAnimation(currentAbility.getWindUpAnimation()); - } - currentAbility.onWindUpTick(player, currentTarget, currentAbility.getCurrentTick()); - break; - - case ACTIVE: - if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) { - // Just entered ACTIVE phase - lock all telegraph positions - for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) { - telegraph.lockPosition(); - } - // Remove telegraph unless the ability keeps it during active phase - if (!currentAbility.keepTelegraphDuringActive()) { - removeTelegraph(currentAbility, player); - } - - // Handle rotation control transition from WINDUP to ACTIVE - if (currentAbility.isRotationLockedDuringActive()) { - if (!rotationLocked) { - captureLockedRotation(player); - } - } else if (rotationLocked) { - releaseRotationControl(); - } - - // Handle position lock transition from WINDUP to ACTIVE - if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) { - if (!positionLocked) { - captureLockedPosition(player); - } - } else if (positionLocked) { - releaseLockedPosition(); - } - - // Play active sound and animation - playAbilitySound(player, currentAbility.getActiveSound()); - playAbilityAnimation(currentAbility.getActiveAnimation()); - - // Fire execute event (cancelable) - if (firePlayerExecuteEvent(player, currentAbility, currentTarget)) { - return; // Cancelled - } - - // Call onExecute - currentAbility.onExecute(player, currentTarget); - - if (currentAbility.getPhase() == AbilityPhase.IDLE) { - handleAbilityCompletion(player); - return; - } - } - currentAbility.onActiveTick(player, currentTarget, currentAbility.getCurrentTick()); - - if (currentAbility.getPhase() == AbilityPhase.IDLE) { - handleAbilityCompletion(player); - return; - } - - // Auto-complete for burst overlap mode (entities fly independently) - if (currentAbility.isBurstEnabled() - && currentAbility.getBurstIndex() < currentAbility.getBurstAmount() - && currentAbility.getPhase() == AbilityPhase.ACTIVE - && currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) { - currentAbility.signalCompletion(); - } - - // Release locks during burst delay - if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) { - if (rotationLocked) releaseRotationControl(); - if (positionLocked) releaseLockedPosition(); - } - break; - - case BURST_DELAY: - // Free movement and rotation during burst delay - if (rotationLocked) releaseRotationControl(); - if (positionLocked) releaseLockedPosition(); - break; - - case DAZED: - // Player is dazed - just wait - break; - - case IDLE: - handleAbilityCompletion(player); - break; - } - } - - /** - * Handle ability completion. - */ - private void handleAbilityCompletion(EntityPlayer player) { - if (currentAbility == null) return; - - // Fire extender complete hook - AbilityController.Instance.fireOnAbilityComplete(currentAbility, player, currentTarget, false); - - currentAbility.onComplete(player, currentTarget); - - // Fire complete event - firePlayerCompleteEvent(player, currentAbility, currentTarget); - - // Release all locks - releaseRotationControl(); - releaseLockedPosition(); - - // Apply universal cooldown (using base cooldownTicks only, not min/max random) - if (!currentAbility.isIgnoreCooldown()) { - cooldownEndTime = player.worldObj.getTotalWorldTime() + currentAbility.getCooldownTicks(); - } - - // Stop animation - stopAbilityAnimation(); - - currentAbility = null; - currentAbilityKey = null; - currentTarget = null; - - // Send cleared state to client - syncAbilityStateClear(player); - } - // ═══════════════════════════════════════════════════════════════════ // ABILITY ACTIVATION - Called on Special Key press // ═══════════════════════════════════════════════════════════════════ @@ -348,7 +324,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { if (!ability.getAllowedBy().allowsPlayer()) return false; // Check universal cooldown - if (!ability.isIgnoreCooldown() && isOnCooldown(player)) return false; + if (!ability.isIgnoreCooldown() && isOnCooldown()) return false; // Check conditions (skip target-requiring ones) if (!ability.checkConditionsForPlayer(player)) return false; @@ -370,53 +346,30 @@ public class PlayerAbilityData implements IPlayerAbilityData { ability.start(null); if (ability.getPhase() == AbilityPhase.ACTIVE) { - // Windup was 0 — skip telegraph/windup and go straight to active + // Windup was 0 — capture locks for immediate active phase, then execute if (ability.isRotationLockedDuringActive()) { - captureLockedRotation(player); + captureLockedRotation(); } if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) { - captureLockedPosition(player); + captureLockedPosition(); } - executeImmediate(ability, player); + executeImmediate(ability, currentTarget); } else { // Normal windup flow if (ability.isRotationLockedDuringWindup()) { - captureLockedRotation(player); + captureLockedRotation(); } if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) { - captureLockedPosition(player); + captureLockedPosition(); } - spawnTelegraph(ability, player, null); - playAbilitySound(player, ability.getWindUpSound()); + spawnTelegraph(ability, null); + playAbilitySound(ability.getWindUpSound()); playAbilityAnimation(ability.getWindUpAnimation()); } return true; } - /** - * Execute an ability immediately (no windup). - * Called when windUpTicks is 0. - */ - private void executeImmediate(Ability ability, EntityPlayer player) { - // Play active sound and animation - playAbilitySound(player, ability.getActiveSound()); - playAbilityAnimation(ability.getActiveAnimation()); - - // Fire execute event (cancelable) - if (firePlayerExecuteEvent(player, ability, currentTarget)) { - return; // Cancelled - } - - // Call onExecute - ability.onExecute(player, currentTarget); - - // Check if ability completed during onExecute - if (ability.getPhase() == AbilityPhase.IDLE) { - handleAbilityCompletion(player); - } - } - // ═══════════════════════════════════════════════════════════════════ // CLIENT SYNC // ═══════════════════════════════════════════════════════════════════ @@ -508,9 +461,12 @@ public class PlayerAbilityData implements IPlayerAbilityData { } // ═══════════════════════════════════════════════════════════════════ - // COOLDOWNS + // COOLDOWNS (Player-specific overloads) // ═══════════════════════════════════════════════════════════════════ + /** + * Check if on cooldown using player reference. + */ public boolean isOnCooldown(EntityPlayer player) { return player.worldObj.getTotalWorldTime() < cooldownEndTime; } @@ -543,6 +499,10 @@ public class PlayerAbilityData implements IPlayerAbilityData { cooldownEndTime = 0; } + // ═══════════════════════════════════════════════════════════════════ + // IPlayerAbilityData INTERFACE + // ═══════════════════════════════════════════════════════════════════ + @Override public boolean activateAbility() { return activateAbility(playerData.player); @@ -553,41 +513,36 @@ public class PlayerAbilityData implements IPlayerAbilityData { return activateAbility(playerData.player, key); } - // ═══════════════════════════════════════════════════════════════════ - // STATE QUERIES - // ═══════════════════════════════════════════════════════════════════ - - public boolean isExecutingAbility() { - return currentAbility != null && currentAbility.isExecuting(); + @Override + public boolean isOnCooldown() { + EntityPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(player); } - public Ability getCurrentAbility() { - return currentAbility; + @Override + public boolean isOnCooldown(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(key, player); } + @Override + public void resetCooldown() { + cooldownEndTime = 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // INTERRUPTION (convenience overload) + // ═══════════════════════════════════════════════════════════════════ + public void interruptCurrentAbility() { interruptCurrentAbility(null, 0); } - public void interruptCurrentAbility(DamageSource source, float damage) { - if (currentAbility != null && currentAbility.isExecuting()) { - // Fire extender complete hook (interrupted) - AbilityController.Instance.fireOnAbilityComplete(currentAbility, playerData.player, currentTarget, true); - - // Fire interrupt event - firePlayerInterruptEvent(playerData.player, currentAbility, currentTarget, source, damage); - - currentAbility.interrupt(); - stopAbilityAnimation(); - playAbilityAnimation(currentAbility.getDazedAnimation()); - releaseRotationControl(); - releaseLockedPosition(); - currentAbility = null; - currentAbilityKey = null; - currentTarget = null; - syncAbilityStateClear(playerData.player); - } - } + // ═══════════════════════════════════════════════════════════════════ + // DAMAGE HANDLING + // ═══════════════════════════════════════════════════════════════════ /** * Handle damage taken while an ability is executing. @@ -636,24 +591,9 @@ public class PlayerAbilityData implements IPlayerAbilityData { return amount; } - @Override - public boolean isOnCooldown() { - EntityPlayer player = playerData.player; - if (player == null) return false; - return isOnCooldown(player); - } - - @Override - public boolean isOnCooldown(String key) { - EntityPlayer player = playerData.player; - if (player == null) return false; - return isOnCooldown(key, player); - } - - @Override - public void resetCooldown() { - cooldownEndTime = 0; - } + // ═══════════════════════════════════════════════════════════════════ + // STATE QUERIES (Player-specific) + // ═══════════════════════════════════════════════════════════════════ /** * Check if the player's movement should be locked due to an executing ability. @@ -664,26 +604,26 @@ public class PlayerAbilityData implements IPlayerAbilityData { && currentAbility.isMovementLockedForCurrentPhase(); } - // ═══════════════════════════════════════════════════════════════════ - // ROTATION CONTROL - // ═══════════════════════════════════════════════════════════════════ - /** - * Capture current rotation values to lock player's look direction. + * Returns true if an ability-driven animation is currently playing. + * Used by PlayerData.onLogin() to detect orphaned ability animations. */ - private void captureLockedRotation(EntityPlayer player) { - lockedYaw = player.rotationYaw; - lockedPitch = player.rotationPitch; - rotationLocked = true; + public boolean isPlayingAbilityAnimation() { + return playingAbilityAnimation; } /** - * Release rotation lock. + * Clears an orphaned ability animation (e.g. after relog when the ability is gone). + * Stops the animation and resets the tracking flag. */ - private void releaseRotationControl() { - rotationLocked = false; + public void clearOrphanedAbilityAnimation() { + stopAbilityAnimation(); } + // ═══════════════════════════════════════════════════════════════════ + // ROTATION CONTROL (Player-specific application) + // ═══════════════════════════════════════════════════════════════════ + /** * Apply rotation control on the server side. * Enforces locked rotation values on the player entity. @@ -703,26 +643,9 @@ public class PlayerAbilityData implements IPlayerAbilityData { } // ═══════════════════════════════════════════════════════════════════ - // POSITION LOCKING + // POSITION LOCKING (Player-specific application) // ═══════════════════════════════════════════════════════════════════ - /** - * Capture current position to lock player in place. - */ - private void captureLockedPosition(EntityPlayer player) { - lockedPosX = player.posX; - lockedPosY = player.posY; - lockedPosZ = player.posZ; - positionLocked = true; - } - - /** - * Release position lock. - */ - private void releaseLockedPosition() { - positionLocked = false; - } - /** * Apply position lock on the server side. * Snaps player back to locked position and zeroes motion. @@ -730,12 +653,23 @@ public class PlayerAbilityData implements IPlayerAbilityData { private void applyPositionLock(EntityPlayer player) { if (!positionLocked) return; - player.setPosition(lockedPosX, lockedPosY, lockedPosZ); - player.prevPosX = lockedPosX; - player.prevPosY = lockedPosY; - player.prevPosZ = lockedPosZ; + boolean flying = AbilityController.Instance != null + && AbilityController.Instance.isPlayerFlying(player); + + if (flying) { + // Flying players: lock X/Z only, preserve Y freedom + player.setPosition(lockedPosX, player.posY, lockedPosZ); + player.prevPosX = lockedPosX; + player.prevPosZ = lockedPosZ; + } else { + // Grounded players: full position lock including Y + player.setPosition(lockedPosX, lockedPosY, lockedPosZ); + player.prevPosX = lockedPosX; + player.prevPosY = lockedPosY; + player.prevPosZ = lockedPosZ; + player.motionY = 0; + } player.motionX = 0; - player.motionY = 0; // Full vertical freeze for position lock player.motionZ = 0; } @@ -789,89 +723,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { } // ═══════════════════════════════════════════════════════════════════ - // TELEGRAPH - // ═══════════════════════════════════════════════════════════════════ - - private void spawnTelegraph(Ability ability, EntityPlayer player, EntityLivingBase target) { - List telegraphs = ability.createTelegraphs(player, target); - if (!telegraphs.isEmpty()) { - ability.setTelegraphInstances(telegraphs); - if (player instanceof EntityPlayerMP) { - for (TelegraphInstance telegraph : telegraphs) { - TelegraphSpawnPacket.sendToTracking(telegraph, player); - } - } - } - } - - private void removeTelegraph(Ability ability, EntityPlayer player) { - List telegraphs = ability.getTelegraphInstances(); - for (TelegraphInstance telegraph : telegraphs) { - if (player instanceof EntityPlayerMP) { - TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player); - } - } - ability.setTelegraphInstances(null); - } - - // ═══════════════════════════════════════════════════════════════════ - // ANIMATION - // ═══════════════════════════════════════════════════════════════════ - - public void playAbilityAnimation(Animation animation) { - if (animation == null || AnimationController.Instance == null) return; - playerData.animationData.setEnabled(true); - playerData.animationData.setAnimation(animation); - playerData.animationData.updateClient(); - playingAbilityAnimation = true; - } - - public void playAbilityAnimation(int animation) { - if (animation < 0) return; - if (AnimationController.Instance == null) return; - if (AnimationController.Instance.get(animation) == null) return; - - playAbilityAnimation((Animation) AnimationController.Instance.get(animation)); - } - - public void playAbilityAnimation(String animation) { - if (animation.isEmpty()) return; - if (AnimationController.Instance == null) return; - if (AnimationController.Instance.get(animation, true) == null) return; - - playAbilityAnimation((Animation) AnimationController.Instance.get(animation, true)); - } - - private void stopAbilityAnimation() { - playerData.animationData.setAnimation(null); - playerData.animationData.updateClient(); - playingAbilityAnimation = false; - } - - /** - * Returns true if an ability-driven animation is currently playing. - * Used by PlayerData.onLogin() to detect orphaned ability animations. - */ - public boolean isPlayingAbilityAnimation() { - return playingAbilityAnimation; - } - - /** - * Clears an orphaned ability animation (e.g. after relog when the ability is gone). - * Stops the animation and resets the tracking flag. - */ - public void clearOrphanedAbilityAnimation() { - stopAbilityAnimation(); - } - - private void playAbilitySound(EntityPlayer player, String sound) { - if (sound != null && !sound.isEmpty()) { - player.worldObj.playSoundAtEntity(player, sound, 1.0f, 1.0f); - } - } - - // ═══════════════════════════════════════════════════════════════════ - // SCRIPT EVENTS + // SCRIPT EVENTS (Player-specific: start event only, others via abstract) // ═══════════════════════════════════════════════════════════════════ /** @@ -886,57 +738,6 @@ public class PlayerAbilityData implements IPlayerAbilityData { return EventHooks.onPlayerAbilityStart(handler, event); } - /** - * Fire the ability execute event. Returns true if cancelled. - */ - private boolean firePlayerExecuteEvent(EntityPlayer player, Ability ability, EntityLivingBase target) { - if (ScriptController.Instance == null) return false; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return false; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.ExecuteEvent event = new PlayerAbilityEvent.ExecuteEvent(iPlayer, ability, target); - return EventHooks.onPlayerAbilityExecute(handler, event); - } - - /** - * Fire the ability tick event. - */ - private void firePlayerTickEvent(EntityPlayer player, Ability ability, EntityLivingBase target) { - if (ScriptController.Instance == null) return; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.TickEvent event = new PlayerAbilityEvent.TickEvent( - iPlayer, ability, target, ability.getPhase().ordinal(), ability.getCurrentTick()); - EventHooks.onPlayerAbilityTick(handler, event); - } - - /** - * Fire the ability interrupt event. - */ - private void firePlayerInterruptEvent(EntityPlayer player, Ability ability, EntityLivingBase target, - DamageSource source, float damage) { - if (ScriptController.Instance == null) return; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.InterruptEvent event = new PlayerAbilityEvent.InterruptEvent( - iPlayer, ability, target, source, damage); - EventHooks.onPlayerAbilityInterrupt(handler, event); - } - - /** - * Fire the ability complete event. - */ - private void firePlayerCompleteEvent(EntityPlayer player, Ability ability, EntityLivingBase target) { - if (ScriptController.Instance == null) return; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.CompleteEvent event = new PlayerAbilityEvent.CompleteEvent(iPlayer, ability, target); - EventHooks.onPlayerAbilityComplete(handler, event); - } - // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════