From 0f774a68fc054e38ef4ef78c61dd56dd5242fa9b Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 14 Feb 2026 21:06:12 -0500 Subject: [PATCH 01/20] Refactor ability handling: replace damage handler with ability extenders for improved lifecycle management and damage routing --- .../controllers/data/ability/Ability.java | 11 +-- .../data/ability/AbilityController.java | 69 ++++++++++++++--- .../data/ability/IAbilityDamageHandler.java | 27 ------- .../data/ability/IAbilityExtender.java | 76 +++++++++++++++++++ .../npcs/entity/EntityAbilityProjectile.java | 18 ++--- src/main/java/noppes/npcs/DataAbilities.java | 18 +++++ .../controllers/data/PlayerAbilityData.java | 18 +++++ 7 files changed, 184 insertions(+), 53 deletions(-) delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityDamageHandler.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityExtender.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 1878337b8..f755bc370 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -329,13 +329,10 @@ public abstract class Ability implements IAbility { // Apply damage if (damage > 0) { - // Check for external damage handler first (e.g., DBC Addon) - IAbilityDamageHandler handler = AbilityController.Instance.getDamageHandler(); - boolean handled = false; - if (handler != null) { - handled = handler.handleDamage(this, caster, hitEntity, damage, knockback, knockbackUp, - knockbackDirX, knockbackDirZ); - } + // Check for ability extenders (e.g., DBC Addon damage routing) + boolean handled = AbilityController.Instance.fireOnAbilityDamage( + this, caster, hitEntity, damage, knockback, knockbackUp, + knockbackDirX, knockbackDirZ); if (!handled) { // Default damage path if (caster instanceof EntityNPCInterface) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index b65276211..c86e3e623 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -2,6 +2,7 @@ package kamkeel.npcs.controllers.data.ability; import kamkeel.npcs.controllers.data.ability.type.*; import kamkeel.npcs.controllers.SyncController; +import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; @@ -24,7 +25,7 @@ public class AbilityController implements IAbilityHandler { // ── Extension Points ───────────────────────────────────────────────────── private final Map> externalVariants = new LinkedHashMap<>(); private final List fieldProviders = new ArrayList<>(); - private IAbilityDamageHandler damageHandler = null; + private final List extenders = new ArrayList<>(); // ── Derived State ──────────────────────────────────────────────────────── private final Set builtInTypeIds = new HashSet<>(); @@ -350,14 +351,6 @@ public class AbilityController implements IAbilityHandler { return result; } - public void registerDamageHandler(IAbilityDamageHandler handler) { - this.damageHandler = handler; - } - - public IAbilityDamageHandler getDamageHandler() { - return damageHandler; - } - public void registerFieldProvider(IAbilityFieldProvider provider) { fieldProviders.add(provider); } @@ -366,6 +359,64 @@ public class AbilityController implements IAbilityHandler { return fieldProviders; } + public void registerExtender(IAbilityExtender extender) { + extenders.add(extender); + } + + public List getExtenders() { + return extenders; + } + + /** + * Fire onAbilityStart on all extenders. Returns false if ANY extender cancels. + */ + public boolean fireOnAbilityStart(Ability ability, EntityLivingBase caster, EntityLivingBase target) { + for (IAbilityExtender ext : extenders) { + if (!ext.onAbilityStart(ability, caster, target)) { + return false; + } + } + return true; + } + + /** + * Fire onAbilityTick on all extenders. Returns false if ANY extender says to interrupt. + */ + public boolean fireOnAbilityTick(Ability ability, EntityLivingBase caster, EntityLivingBase target, + AbilityPhase phase, int tick) { + for (IAbilityExtender ext : extenders) { + if (!ext.onAbilityTick(ability, caster, target, phase, tick)) { + return false; + } + } + return true; + } + + /** + * Fire onAbilityComplete on all extenders. + */ + public void fireOnAbilityComplete(Ability ability, EntityLivingBase caster, EntityLivingBase target, + boolean interrupted) { + for (IAbilityExtender ext : extenders) { + ext.onAbilityComplete(ability, caster, target, interrupted); + } + } + + /** + * Fire onAbilityDamage on all extenders. Chain of responsibility — first true wins. + */ + public boolean fireOnAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ) { + for (IAbilityExtender ext : extenders) { + if (ext.onAbilityDamage(ability, caster, target, damage, knockback, knockbackUp, + knockbackDirX, knockbackDirZ)) { + return true; + } + } + return false; + } + // ═══════════════════════════════════════════════════════════════════════════ // IAbilityHandler // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityDamageHandler.java b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityDamageHandler.java deleted file mode 100644 index 9a7f353c1..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityDamageHandler.java +++ /dev/null @@ -1,27 +0,0 @@ -package kamkeel.npcs.controllers.data.ability; - -import net.minecraft.entity.EntityLivingBase; - -/** - * External damage handler for abilities. - * When registered, called AFTER script events but BEFORE default attackEntityFrom. - * If handleDamage returns true, default damage is skipped. - */ -public interface IAbilityDamageHandler { - /** - * Handle damage from an ability. - * - * @param ability The ability dealing damage - * @param caster The entity executing the ability - * @param target The entity being hit - * @param damage The damage amount (after script event modifications) - * @param knockback The horizontal knockback - * @param knockbackUp The vertical knockback - * @param knockbackDirX The X component of knockback direction - * @param knockbackDirZ The Z component of knockback direction - * @return true if damage was handled (skip default), false to use default damage - */ - boolean handleDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, - float damage, float knockback, float knockbackUp, - double knockbackDirX, double knockbackDirZ); -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityExtender.java b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityExtender.java new file mode 100644 index 000000000..e3e7e5c0b --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/IAbilityExtender.java @@ -0,0 +1,76 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.entity.EntityLivingBase; + +/** + * General-purpose extension point for abilities. Multiple extenders can be registered. + * Covers lifecycle hooks (start/tick/complete) and damage handling. + *

+ * All methods have no-op defaults so extenders only override what they need. + */ +public interface IAbilityExtender { + + /** + * Called before an ability starts executing. + * Return false to cancel the ability (e.g., insufficient resources). + * + * @param ability The ability about to start + * @param caster The entity executing the ability (NPC or Player) + * @param target The target entity (may be null for player self-cast) + * @return true to allow, false to cancel + */ + default boolean onAbilityStart(Ability ability, EntityLivingBase caster, EntityLivingBase target) { + return true; + } + + /** + * Called each tick during ability execution. + * Return false to interrupt the ability (e.g., resource depleted). + * + * @param ability The executing ability + * @param caster The entity executing the ability + * @param target The target entity (may be null) + * @param phase The current ability phase (WINDUP, ACTIVE, DAZED, BURST_DELAY) + * @param tick The current tick within the phase + * @return true to continue, false to interrupt + */ + default boolean onAbilityTick(Ability ability, EntityLivingBase caster, EntityLivingBase target, + AbilityPhase phase, int tick) { + return true; + } + + /** + * Called when an ability completes (normally or via interruption). + * + * @param ability The completed ability + * @param caster The entity that was executing the ability + * @param target The target entity (may be null) + * @param interrupted true if the ability was interrupted, false if it completed normally + */ + default void onAbilityComplete(Ability ability, EntityLivingBase caster, EntityLivingBase target, + boolean interrupted) { + } + + /** + * Called when an ability deals damage, AFTER script events but BEFORE default attackEntityFrom. + * Return true to handle the damage (skips default damage application). + *

+ * Chain of responsibility: extenders are called in registration order. + * The first extender returning true claims the damage; remaining extenders are skipped. + * + * @param ability The ability dealing damage + * @param caster The entity executing the ability + * @param target The entity being hit + * @param damage The damage amount (after script event modifications) + * @param knockback The horizontal knockback + * @param knockbackUp The vertical knockback + * @param knockbackDirX The X component of knockback direction + * @param knockbackDirZ The Z component of knockback direction + * @return true if damage was handled (skip default), false to pass to next extender or default + */ + default boolean onAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ) { + return false; + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 9b30abab6..dee2ed292 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -8,7 +8,7 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.IAbilityDamageHandler; + import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; @@ -326,17 +326,15 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP if (previewMode) return; // Skip damage in preview mode Entity owner = getOwnerEntity(); - // Check for external damage handler (e.g., DBC Addon) + // Check for ability extenders (e.g., DBC Addon damage routing) boolean handled = false; if (sourceAbility != null && owner instanceof EntityLivingBase) { - IAbilityDamageHandler handler = AbilityController.Instance.getDamageHandler(); - if (handler != null) { - double dx = target.posX - posX; - double dz = target.posZ - posZ; - float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f; - handled = handler.handleDamage(sourceAbility, (EntityLivingBase) owner, target, - dmg, kb, kbUp, dx, dz); - } + double dx = target.posX - posX; + double dz = target.posZ - posZ; + float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f; + handled = AbilityController.Instance.fireOnAbilityDamage( + sourceAbility, (EntityLivingBase) owner, target, + dmg, kb, kbUp, dx, dz); } if (!handled) { diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 8edcba628..eb3082a59 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -171,6 +171,13 @@ public class DataAbilities { // 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: @@ -342,6 +349,9 @@ public class DataAbilities { // 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); @@ -501,6 +511,11 @@ public class DataAbilities { return false; } + // Fire extender start hook (e.g., resource cost checks) + if (!AbilityController.Instance.fireOnAbilityStart(ability, npc, target)) { + return false; + } + currentAbility = ability; lastTarget = target; ability.start(target); @@ -750,6 +765,9 @@ public class DataAbilities { 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); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 0f87ddf5a..aa8d4301d 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -147,6 +147,13 @@ public class PlayerAbilityData implements IPlayerAbilityData { // Tick advances time and possibly changes phase boolean phaseChanged = currentAbility.tick(); + // 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) { @@ -249,6 +256,9 @@ public class PlayerAbilityData implements IPlayerAbilityData { private void handleAbilityCompletion(EntityPlayer player) { if (currentAbility == null) return; + // Fire extender complete hook + AbilityController.Instance.fireOnAbilityComplete(currentAbility, player, currentTarget, false); + currentAbility.onComplete(player, currentTarget); // Release all locks @@ -318,6 +328,11 @@ public class PlayerAbilityData implements IPlayerAbilityData { // Check conditions (skip target-requiring ones) if (!ability.checkConditionsForPlayer(player)) return false; + // Fire extender start hook (e.g., resource cost checks) + if (!AbilityController.Instance.fireOnAbilityStart(ability, player, null)) { + return false; + } + // Start the ability currentAbility = ability; currentAbilityKey = key; @@ -517,6 +532,9 @@ public class PlayerAbilityData implements IPlayerAbilityData { public void interruptCurrentAbility() { if (currentAbility != null && currentAbility.isExecuting()) { + // Fire extender complete hook (interrupted) + AbilityController.Instance.fireOnAbilityComplete(currentAbility, playerData.player, currentTarget, true); + currentAbility.interrupt(); stopAbilityAnimation(); releaseRotationControl(); From 61405add979f55128e8e2940a0574d89ae43bc0f Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 15 Feb 2026 00:40:43 -0300 Subject: [PATCH 02/20] Added Ki Cost to Ability --- .../npcs/controllers/data/ability/Ability.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index f755bc370..a4dfcfe61 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -110,6 +110,9 @@ public abstract class Ability implements IAbility { protected boolean burstReplayAnimations = true; protected boolean burstOverlap = false; // if true, don't wait for entity death between bursts + // Ki + protected int kiCost = 0; + // ═══════════════════════════════════════════════════════════════════ // EXECUTION STATE (not saved, reset each combat) // ═══════════════════════════════════════════════════════════════════ @@ -1149,6 +1152,9 @@ public abstract class Ability implements IAbility { nbt.setBoolean("burstReplayAnimations", burstReplayAnimations); nbt.setBoolean("burstOverlap", burstOverlap); + // Ki + nbt.setInteger("kiCost", kiCost); + // Conditions NBTTagList condList = new NBTTagList(); for (Condition c : conditions) condList.appendTag(c.writeNBT()); @@ -1212,6 +1218,9 @@ public abstract class Ability implements IAbility { burstReplayAnimations = nbt.getBoolean("burstReplayAnimations"); burstOverlap = nbt.getBoolean("burstOverlap"); + // Ki + kiCost = nbt.getInteger("kiCost"); + // Conditions conditions.clear(); NBTTagList condList = nbt.getTagList("conditions", 10); @@ -1751,6 +1760,14 @@ public abstract class Ability implements IAbility { return burstIndex; } + public int getKiCost() { + return kiCost; + } + + public void setKiCost(int kiCost) { + this.kiCost = kiCost; + } + /** * Get display keys for rotation phase dropdown (excludes "None" since mode=FREE handles that). */ From e466ef67711806ede8ae1617415befdd037f0853 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 04:47:19 -0500 Subject: [PATCH 03/20] Remove Ki Cost from base Ability - DBC-specific concern Ki Cost belongs in the DBC Addon's DBCAbilityStats system, not in the base ability infrastructure. Co-Authored-By: Claude Opus 4.6 --- .../npcs/controllers/data/ability/Ability.java | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index a4dfcfe61..f755bc370 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -110,9 +110,6 @@ public abstract class Ability implements IAbility { protected boolean burstReplayAnimations = true; protected boolean burstOverlap = false; // if true, don't wait for entity death between bursts - // Ki - protected int kiCost = 0; - // ═══════════════════════════════════════════════════════════════════ // EXECUTION STATE (not saved, reset each combat) // ═══════════════════════════════════════════════════════════════════ @@ -1152,9 +1149,6 @@ public abstract class Ability implements IAbility { nbt.setBoolean("burstReplayAnimations", burstReplayAnimations); nbt.setBoolean("burstOverlap", burstOverlap); - // Ki - nbt.setInteger("kiCost", kiCost); - // Conditions NBTTagList condList = new NBTTagList(); for (Condition c : conditions) condList.appendTag(c.writeNBT()); @@ -1218,9 +1212,6 @@ public abstract class Ability implements IAbility { burstReplayAnimations = nbt.getBoolean("burstReplayAnimations"); burstOverlap = nbt.getBoolean("burstOverlap"); - // Ki - kiCost = nbt.getInteger("kiCost"); - // Conditions conditions.clear(); NBTTagList condList = nbt.getTagList("conditions", 10); @@ -1760,14 +1751,6 @@ public abstract class Ability implements IAbility { return burstIndex; } - public int getKiCost() { - return kiCost; - } - - public void setKiCost(int kiCost) { - this.kiCost = kiCost; - } - /** * Get display keys for rotation phase dropdown (excludes "None" since mode=FREE handles that). */ From 8c2ef1084a8e2dd9008e6d43864702c0bc6c5f4c Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 17:47:44 -0500 Subject: [PATCH 04/20] Fix Burst Refire --- .../controllers/data/ability/Ability.java | 6 ++-- .../type/AbstractEnergyProjectileAbility.java | 32 ++++++++++++++++--- src/main/java/noppes/npcs/DataAbilities.java | 2 +- .../controllers/data/PlayerAbilityData.java | 2 +- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index f755bc370..11ceffea8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -910,9 +910,9 @@ public abstract class Ability implements IAbility { if (phase == AbilityPhase.ACTIVE) { // Check if more burst iterations remain if (burstEnabled && burstAmount > 0 && burstIndex < burstAmount) { - if (!burstOverlap) { - cleanup(); - } + // Don't cleanup here - entities may still be in flight + // For non-overlap: cleaned up when next burst starts (in onExecute) + // For overlap: entities continue flying indefinitely burstIndex++; phase = AbilityPhase.BURST_DELAY; currentTick = 0; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyProjectileAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyProjectileAbility.java index 2be9256eb..f3ee572c2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyProjectileAbility.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyProjectileAbility.java @@ -258,9 +258,13 @@ public abstract class AbstractEnergyProjectileAbility 5 && entity.worldObj != null + && entity.worldObj.getEntityByID(entity.getEntityId()) != entity) { + entity.setDead(); + continue; + } + + allDead = false; + break; + } + + // Failsafe timeout: force completion if active phase exceeded max possible flight time + if (!allDead) { + int lastFireTick = fireDelay > 0 ? fireDelay * (projectileCount - 1) : 0; + int maxFlightTime = lifespanData.maxLifetime > 0 ? lifespanData.maxLifetime : 200; + if (tick > lastFireTick + maxFlightTime + 20) { + cleanup(); + allDead = true; } } + if (allDead) { signalCompletion(); } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index eb3082a59..78692162f 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -269,7 +269,7 @@ public class DataAbilities { // 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.isBurstOverlap() + if (currentAbility.isBurstEnabled() && currentAbility.getBurstIndex() < currentAbility.getBurstAmount() && currentAbility.getPhase() == AbilityPhase.ACTIVE && currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) { diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index aa8d4301d..f0a4c8c70 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -220,7 +220,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { } // Auto-complete for burst overlap mode (entities fly independently) - if (currentAbility.isBurstEnabled() && currentAbility.isBurstOverlap() + if (currentAbility.isBurstEnabled() && currentAbility.getBurstIndex() < currentAbility.getBurstAmount() && currentAbility.getPhase() == AbilityPhase.ACTIVE && currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) { From ff9f9b9660f0c942fe9f68cf5735af19a1b8136f Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 18:05:07 -0500 Subject: [PATCH 05/20] Enhance ability variant selection: inject a default "Base" variant when no built-in variants are available --- .../npcs/controllers/data/ability/AbilityController.java | 5 +++++ src/main/resources/assets/customnpcs/lang/en_US.lang | 1 + 2 files changed, 6 insertions(+) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index c86e3e623..992c56cd9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -346,6 +346,11 @@ public class AbilityController implements IAbilityHandler { } List ext = externalVariants.get(typeId); if (ext != null) { + // If no built-in variants exist but external ones do, + // inject a "Base" variant so the user always gets a choice + if (result.isEmpty()) { + result.add(new AbilityVariant("ability.variant.base", a -> {})); + } result.addAll(ext); } return result; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index a29759c31..f265f0fe9 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1910,6 +1910,7 @@ ability.modifyParent=Edit Parent # Ability Variant Selection ability.selectVariant=Select Variant +ability.variant.base=Base ability.variant.single=Single ability.variant.dual=Dual ability.variant.barrage=Barrage From 2dfdfb19d1aa7fd084cb7b9250ad35868fd1c8e4 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 18:15:56 -0500 Subject: [PATCH 06/20] Client-side trade validation and stock update Perform more complete client-side checks before predicting currency deductions in GuiNPCTrader.handleMouseClick. The method now verifies that the item can be given to the player (canGivePlayerClient), that the slot is enabled, and that stock is available, mirroring server-side checks. When a client-side purchase is accepted, availableStock is decremented if applicable. Added a private canGivePlayerClient(ItemStack) helper to check cursor-stack compatibility and max stack size. These changes prevent the UI from showing incorrect balances for trades the server would reject. --- .../npcs/client/gui/player/GuiNPCTrader.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java index 55f88ac8c..49edf72b6 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java @@ -337,18 +337,45 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { protected void handleMouseClick(Slot slot, int slotId, int mouseButton, int modifier) { // Client-side balance prediction: immediately subtract currency cost on click // so the displayed balance updates instantly without waiting for server response. + // Must mirror ALL server-side checks in ContainerNPCTrader.slotClick() to avoid + // predicting a deduction for trades the server will reject. if (slotId >= 0 && slotId < 18 && mouseButton == 0) { long cost = currencyCost[slotId]; if (cost > 0 && playerBalance >= cost) { ItemStack sold = role.inventorySold.items.get(slotId); - if (sold != null && container.canBuy(slotId, player)) { + if (sold != null + && canGivePlayerClient(sold) + && container.isSlotEnabled(slotId, player) + && (!stockEnabled || availableStock[slotId] > 0) + && container.canBuy(slotId, player)) { playerBalance -= cost; + if (stockEnabled && availableStock[slotId] < Integer.MAX_VALUE) { + availableStock[slotId]--; + } } } } super.handleMouseClick(slot, slotId, mouseButton, modifier); } + /** + * Client-side mirror of ContainerNPCTrader.canGivePlayer. + * Checks if the item can be added to the player's cursor stack. + */ + private boolean canGivePlayerClient(ItemStack item) { + ItemStack held = player.inventory.getItemStack(); + if (held == null) { + return true; + } + if (NoppesUtilPlayer.compareItems(held, item, false, false)) { + int k1 = item.stackSize; + if (k1 > 0 && k1 + held.stackSize <= held.getMaxStackSize()) { + return true; + } + } + return false; + } + @Override public void save() { } From 45d05b9157220d1d639bb40df37db08781da8f4f Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 19:19:57 -0500 Subject: [PATCH 07/20] Add trap presets & trigger-reveal visuals Introduce client-side trap presets and a visual "trigger reveal" for traps, plus related entity/renderer changes. - AbilityTrap: add TrapPreset enum and applyTrapPresetDefaults; wire a SubGuiTrapPresetSelector hook to apply presets from the GUI; pass visibility into created trap entities. - EntityAbilityZone: add visible flag, TRIGGER_FLASH_STATUS state, setEntityState usage to notify clients of triggers, handleHealthUpdate to start client-side flash, and adjust construction/serialization accordingly. - RenderAbilityZone: implement trigger-reveal visuals (revealFade, triggerReveal) that briefly show a burst when traps trigger, skip rendering for invisible traps unless revealing, and tweak flash/timing logic. - Minor cleanup: remove/condense comment blocks and refactor noisy helpers across AbilityZone/AbilityHazard, plus language entry update and new SubGuiTrapPresetSelector GUI class. These changes enable preset application for traps and improve client-side feedback when traps trigger. --- .../client/renderer/RenderAbilityZone.java | 114 ++++------ .../data/ability/type/AbilityHazard.java | 19 -- .../data/ability/type/AbilityTrap.java | 113 ++++++++-- .../data/ability/type/AbilityZone.java | 64 ------ .../npcs/entity/EntityAbilityZone.java | 206 +++++------------- .../client/gui/SubGuiTrapPresetSelector.java | 72 ++++++ .../assets/customnpcs/lang/en_US.lang | 6 + 7 files changed, 271 insertions(+), 323 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java index 9fae0e982..e9775c542 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java @@ -22,10 +22,6 @@ import java.util.HashMap; import java.util.Map; import java.util.Random; -/** - * Renders zone effects for Trap and Hazard abilities. - * Supports CIRCLE and SQUARE shapes with composable visual layers. - */ @SideOnly(Side.CLIENT) public class RenderAbilityZone extends Render { @@ -33,43 +29,35 @@ public class RenderAbilityZone extends Render { private static final int ACCENT_LINE_COUNT = 12; private static final Random RANDOM = new Random(); - // Track last particle spawn tick per entity to avoid duplicate spawns private final Map lastParticleTick = new HashMap<>(); - - // Persistent lightning state private final Map lightningStates = new HashMap<>(); - // ═══════════════════════════════════════════════════════════════════ - // RENDER CONTEXT - // ═══════════════════════════════════════════════════════════════════ + private static final int TRIGGER_REVEAL_TICKS = 10; private static class Ctx { EntityAbilityZone zone; ZoneShape shape; float radius, height; - float ir, ig, ib; // inner color + float ir, ig, ib; float outerR, outerG, outerB; boolean outerEnabled; float time, pulse, rotation, brightness; boolean flash, newTick; float particleDensity, particleScale, animSpeed, lightningDensity; - // Visual layer fields (always populated) boolean groundFill, rings, border, accents, lightning, particles; float groundAlpha, borderSpeed; int ringCount, accentStyle, particleMotion, particleSize; boolean particleGlow; String particleDir; + boolean trapVisible; + boolean triggerReveal; + float revealFade; } - // ═══════════════════════════════════════════════════════════════════ - // MAIN RENDER - // ═══════════════════════════════════════════════════════════════════ - @Override public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { EntityAbilityZone zone = (EntityAbilityZone) entity; - // Build context Ctx c = new Ctx(); c.zone = zone; c.shape = zone.getShape(); @@ -92,7 +80,6 @@ public class RenderAbilityZone extends Render { c.animSpeed = zone.getAnimSpeed(); c.lightningDensity = zone.getLightningDensity(); - // Visual layer fields — always populated c.groundFill = zone.isGroundFill(); c.groundAlpha = zone.getGroundAlpha(); c.rings = zone.isRings(); @@ -113,7 +100,27 @@ public class RenderAbilityZone extends Render { c.rotation = c.time * 1.5f * c.animSpeed; c.brightness = 1.0f; + c.trapVisible = true; + c.triggerReveal = false; + c.revealFade = 0.0f; + if (zone.getZoneType() == ZoneType.TRAP) { + c.trapVisible = zone.isVisible(); + + // Trigger reveal: 10-tick full visual burst that fades out + if (zone.getTriggerFlashTick() >= 0) { + float ticksSinceTrigger = zone.ticksExisted - zone.getTriggerFlashTick() + partialTicks; + if (ticksSinceTrigger < TRIGGER_REVEAL_TICKS) { + c.triggerReveal = true; + c.revealFade = Math.max(0.0f, 1.0f - ticksSinceTrigger / TRIGGER_REVEAL_TICKS); + } + } + + // Skip render entirely if invisible and not in reveal + if (!c.trapVisible && !c.triggerReveal) { + return; + } + if (!zone.isArmed()) { c.brightness = 0.4f; } else { @@ -121,16 +128,14 @@ public class RenderAbilityZone extends Render { } } - c.flash = zone.getTriggerFlashTick() >= 0 - && zone.ticksExisted - zone.getTriggerFlashTick() < 4; + // White flash during first 2 ticks of reveal + c.flash = c.triggerReveal && c.revealFade > 0.8f; c.newTick = isNewTick(zone); - // Render setupRenderState(); GL11.glPushMatrix(); GL11.glTranslated(x, y, z); - GL11.glDepthMask(false); renderZone(c); @@ -161,14 +166,21 @@ public class RenderAbilityZone extends Render { return state; } - // ═══════════════════════════════════════════════════════════════════ - // UNIFIED RENDER — Composable visual layers - // ═══════════════════════════════════════════════════════════════════ - private void renderZone(Ctx c) { + // During trigger reveal, override visual layers for dramatic burst + if (c.triggerReveal) { + c.brightness = c.revealFade; + c.groundFill = true; + c.groundAlpha = 0.35f * c.revealFade; + c.rings = true; + c.ringCount = 3; + c.accents = true; + c.accentStyle = 2; // FLICKERING + c.border = true; + } + float rR = ringR(c), rG = ringG(c), rB = ringB(c); - // 1. Ground fill if (c.groundFill) { if (c.flash) { renderShapeFill(c.shape, c.radius, 1, 1, 1, 0.5f); @@ -178,7 +190,6 @@ public class RenderAbilityZone extends Render { } } - // 2. Pulse rings if (c.rings) { int count = Math.max(1, Math.min(5, c.ringCount)); for (int i = 0; i < count; i++) { @@ -188,18 +199,15 @@ public class RenderAbilityZone extends Render { renderRingLine(c.shape, c.radius * frac, rR, rG, rB, ra, yOff); } - // Always include outer edge ring float edgeA = (0.5f + 0.4f * sin(c.pulse + count * Math.PI / 3)) * c.brightness; renderRingLine(c.shape, c.radius, rR, rG, rB, edgeA, 0.02f + count * 0.01f); } - // 3. Rotating border if (c.border) { renderRotatingBorder(c.shape, c.radius, rR, rG, rB, 0.7f * c.brightness, c.rotation * c.borderSpeed, 0.05f); } - // 4. Vertical accents if (c.accents) { float aa = 0.5f * c.brightness * (0.3f + 0.2f * sin(c.pulse * 0.7f)); AccentStyle style = AccentStyle.values()[Math.min(c.accentStyle, AccentStyle.values().length - 1)]; @@ -216,7 +224,6 @@ public class RenderAbilityZone extends Render { } } - // 5. Lightning if (c.lightning) { int innerColor = c.zone.getInnerColor(); int outerColor = c.outerEnabled ? c.zone.getOuterColor() : innerColor; @@ -246,10 +253,9 @@ public class RenderAbilityZone extends Render { } } state.render(); - GL11.glDepthMask(false); // Reset after lightning + GL11.glDepthMask(false); } - // 6. Particles if (c.particles && c.newTick && c.particleDensity > 0) { if (c.particleDir != null && !c.particleDir.isEmpty()) { if (c.particleDir.startsWith("mc:")) { @@ -263,7 +269,6 @@ public class RenderAbilityZone extends Render { } } - /** Spawn vanilla Minecraft particles using mc: prefix (e.g. "mc:flame"). */ private void spawnVanillaParticle(Ctx c) { String particleName = c.particleDir.substring(3); int count = Math.round(3 * c.particleDensity); @@ -276,7 +281,6 @@ public class RenderAbilityZone extends Render { } } - /** Spawn CustomFX particles using a texture directory. */ private void spawnCustomFXParticle(Ctx c) { int count = Math.round(3 * c.particleDensity); for (int i = 0; i < count; i++) { @@ -308,7 +312,6 @@ public class RenderAbilityZone extends Render { } } - /** Spawn default ZoneParticleFX with motion style from ParticleMotion enum. */ private void spawnDefaultParticles(Ctx c) { int count = Math.round(3 * c.particleDensity); ParticleMotion motion = ParticleMotion.values()[Math.min(c.particleMotion, ParticleMotion.values().length - 1)]; @@ -346,7 +349,6 @@ public class RenderAbilityZone extends Render { } } - /** Get motion vector for a CustomFX particle based on ParticleMotion ordinal. */ private double[] getMotionForStyle(int motionOrdinal) { ParticleMotion motion = ParticleMotion.values()[Math.min(motionOrdinal, ParticleMotion.values().length - 1)]; switch (motion) { @@ -371,18 +373,12 @@ public class RenderAbilityZone extends Render { } } - // ═══════════════════════════════════════════════════════════════════ - // STYLE HELPERS - // ═══════════════════════════════════════════════════════════════════ - - /** Get the ring/outer color (falls back to inner if outer not enabled). */ private float ringR(Ctx c) { return c.outerEnabled ? c.outerR : c.ir; } private float ringG(Ctx c) { return c.outerEnabled ? c.outerG : c.ig; } private float ringB(Ctx c) { return c.outerEnabled ? c.outerB : c.ib; } private static float sin(double v) { return (float) Math.sin(v); } - /** Vertical accents with sinusoidal sway per line. */ private void renderSwayingAccents(Ctx c, float r, float g, float b, float a, float height) { if (height <= 0.1f) return; float rotRad = (float) Math.toRadians(c.rotation * 0.2f); @@ -436,7 +432,6 @@ public class RenderAbilityZone extends Render { } } - /** Vertical accents with per-line random alpha flicker. */ private void renderFlickeringAccents(Ctx c, float r, float g, float b, float baseAlpha, float height) { if (height <= 0.1f) return; float rotRad = (float) Math.toRadians(c.rotation * 0.4f); @@ -492,7 +487,6 @@ public class RenderAbilityZone extends Render { } } - /** Pick a random position within the zone's area. Returns {x, y, z} offsets from zone center. */ private double[] randomPosInZone(Ctx c) { if (c.shape == ZoneShape.SQUARE) { double px = (RANDOM.nextDouble() * 2 - 1) * c.radius; @@ -507,7 +501,6 @@ public class RenderAbilityZone extends Render { } } - /** Pick a random position on the edge of the zone. Returns {x, z}. */ private double[] randomEdgePos(Ctx c) { if (c.shape == ZoneShape.SQUARE) { int side = RANDOM.nextInt(4); @@ -525,7 +518,6 @@ public class RenderAbilityZone extends Render { } } - /** Spawn a zone particle at world position. */ private void spawnParticle(EntityAbilityZone zone, double offsetX, double offsetY, double offsetZ, double mx, double my, double mz, float r, float g, float b, float alpha, @@ -537,10 +529,6 @@ public class RenderAbilityZone extends Render { Minecraft.getMinecraft().effectRenderer.addEffect(particle); } - // ═══════════════════════════════════════════════════════════════════ - // GL STATE - // ═══════════════════════════════════════════════════════════════════ - private void setupRenderState() { GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); GL11.glDisable(GL11.GL_TEXTURE_2D); @@ -561,10 +549,6 @@ public class RenderAbilityZone extends Render { GL11.glPopAttrib(); } - // ═══════════════════════════════════════════════════════════════════ - // SHAPE FILL (Ground plane) - // ═══════════════════════════════════════════════════════════════════ - private void renderShapeFill(ZoneShape shape, float radius, float r, float g, float b, float a) { GL11.glEnable(GL11.GL_POLYGON_OFFSET_FILL); GL11.glPolygonOffset(-1.0f, -1.0f); @@ -602,10 +586,6 @@ public class RenderAbilityZone extends Render { tess.draw(); } - // ═══════════════════════════════════════════════════════════════════ - // RING LINES (Concentric pulse rings) - // ═══════════════════════════════════════════════════════════════════ - private void renderRingLine(ZoneShape shape, float radius, float r, float g, float b, float a, float yOffset) { switch (shape) { @@ -640,18 +620,13 @@ public class RenderAbilityZone extends Render { tess.draw(); } - // ═══════════════════════════════════════════════════════════════════ - // ROTATING BORDER - // ═══════════════════════════════════════════════════════════════════ - private void renderRotatingBorder(ZoneShape shape, float radius, float r, float g, float b, float a, float rotationDeg, float yOffset) { int dashCount = 16; if (shape == ZoneShape.SQUARE) { - // Dashes along 4 rectangle edges with offset - float perimeter = radius * 8; // total perimeter = 4 * (2*radius) + float perimeter = radius * 8; float dashLen = perimeter / dashCount; float offset = (rotationDeg % 360.0f) / 360.0f * perimeter; @@ -691,7 +666,6 @@ public class RenderAbilityZone extends Render { } } - /** Convert a distance along the square perimeter to an x,z position. */ private double[] perimeterPos(float dist, float radius) { float sideLen = radius * 2; if (dist < sideLen) { @@ -713,10 +687,6 @@ public class RenderAbilityZone extends Render { return new double[]{-radius, radius - dist}; } - // ═══════════════════════════════════════════════════════════════════ - // VERTICAL ACCENT LINES - // ═══════════════════════════════════════════════════════════════════ - private void renderVerticalAccents(ZoneShape shape, float radius, float r, float g, float b, float a, float height, float rotationDeg) { @@ -725,7 +695,6 @@ public class RenderAbilityZone extends Render { Tessellator tess = Tessellator.instance; if (shape == ZoneShape.SQUARE) { - // Lines at corners + distributed along edges int perSide = Math.max(1, ACCENT_LINE_COUNT / 4); for (int side = 0; side < 4; side++) { for (int j = 0; j < perSide; j++) { @@ -745,7 +714,6 @@ public class RenderAbilityZone extends Render { tess.draw(); } } - // Corner accent lines float[][] corners = {{-radius, -radius}, {radius, -radius}, {radius, radius}, {-radius, radius}}; for (float[] corner : corners) { tess.startDrawing(GL11.GL_LINES); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 26409d5fe..7ca81c95a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -19,14 +19,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -/** - * Hazard ability: Creates persistent ground effect zones around the caster. - * Spawns one or more EntityAbilityZone entities in random positions - * within spawnRadius. Damage and zone logic handled by the entity. - */ public class AbilityHazard extends AbilityZone implements IAbilityHazard { - // Hazard-specific fields private float radius = 2.0f; private float damagePerSecond = 1.0f; private int damageInterval = 20; @@ -51,7 +45,6 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { public void onExecute(EntityLivingBase caster, EntityLivingBase target) { activeEntities.clear(); - // Use pre-calculated positions from telegraph phase, or generate new ones List positions; if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) { positions = new ArrayList<>(preCalculatedPositions); @@ -93,10 +86,6 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { } } - // ═══════════════════════════════════════════════════════════════════ - // NBT - // ═══════════════════════════════════════════════════════════════════ - @Override public void writeTypeNBT(NBTTagCompound nbt) { writeZoneNBT(nbt); @@ -117,10 +106,6 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { this.affectsCaster = nbt.getBoolean("affectsCaster"); } - // ═══════════════════════════════════════════════════════════════════ - // HAZARD-SPECIFIC GETTERS & SETTERS - // ═══════════════════════════════════════════════════════════════════ - public float getRadius() { return radius; } public void setRadius(float radius) { this.radius = radius; } @@ -136,10 +121,6 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { public boolean isAffectsCaster() { return affectsCaster; } public void setAffectsCaster(boolean affectsCaster) { this.affectsCaster = affectsCaster; } - // ═══════════════════════════════════════════════════════════════════ - // GUI FIELD DEFINITIONS - // ═══════════════════════════════════════════════════════════════════ - @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 7766c3d45..62471c051 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -1,16 +1,16 @@ package kamkeel.npcs.controllers.data.ability.type; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; import kamkeel.npcs.entity.EntityAbilityZone; import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; - - import noppes.npcs.api.ability.type.IAbilityTrap; - +import noppes.npcs.client.gui.SubGuiTrapPresetSelector; import noppes.npcs.client.gui.builder.FieldDef; -import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import noppes.npcs.constants.EnumPotionType; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; @@ -19,14 +19,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -/** - * Trap ability: Places proximity-triggered traps around the caster. - * Spawns one or more EntityAbilityZone entities in random positions - * within spawnRadius of the caster. Damage and trigger logic handled by the entity. - */ public class AbilityTrap extends AbilityZone implements IAbilityTrap { - // Trap-specific fields private float triggerRadius = 2.0f; private int armTime = 20; private int maxTriggers = 1; @@ -54,7 +48,6 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { public void onExecute(EntityLivingBase caster, EntityLivingBase target) { activeEntities.clear(); - // Use pre-calculated positions from telegraph phase, or generate new ones List positions; if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) { positions = new ArrayList<>(preCalculatedPositions); @@ -81,6 +74,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled, zoneHeight, particleDensity, particleScale, animSpeed, lightningDensity, + visible, getEffects()); applyVisualToEntity(entity); @@ -94,9 +88,81 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { } } - // ═══════════════════════════════════════════════════════════════════ - // NBT - // ═══════════════════════════════════════════════════════════════════ + @SideOnly(Side.CLIENT) + public enum TrapPreset { + HIDDEN, VENOM, EXPLOSIVE, CURSED, SHOCK, SNARE; + + @Override + public String toString() { + return "ability.preset." + name().toLowerCase(); + } + } + + private void applyTrapPresetDefaults(String preset) { + effects.clear(); + particleDir = ""; + + groundFill = true; + rings = false; + ringCount = 1; + border = false; + borderSpeed = 1.0f; + accents = false; + accentStyle = 0; + lightning = false; + particleGlow = false; + + switch (preset) { + case "VENOM": + groundAlpha = 0.04f; + particles = true; particleDensity = 0.3f; particleScale = 0.5f; + particleMotion = 1; particleDir = "mc:mobSpell"; + colorData.innerColor = 0x44DD44; colorData.outerColor = 0x116611; + windUpColor = 0x6044DD44; activeColor = 0xC044FF44; + effects.add(new AbilityEffect(EnumPotionType.Poison, 100, 0)); + break; + case "EXPLOSIVE": + groundAlpha = 0.04f; + particles = true; particleDensity = 0.3f; particleScale = 0.6f; + particleMotion = 0; particleDir = "mc:smoke"; + colorData.innerColor = 0xFF6611; colorData.outerColor = 0xCC2200; + windUpColor = 0x60FF6611; activeColor = 0xC0FF4400; + effects.add(new AbilityEffect(EnumPotionType.Fire, 60, 0)); + break; + case "CURSED": + groundAlpha = 0.04f; + particles = true; particleDensity = 0.3f; particleScale = 0.5f; + particleMotion = 1; particleDir = "mc:portal"; + colorData.innerColor = 0xAA44FF; colorData.outerColor = 0x6622BB; + windUpColor = 0x60AA44FF; activeColor = 0xC0CC66FF; + effects.add(new AbilityEffect(EnumPotionType.Weakness, 100, 0)); + break; + case "SHOCK": + groundAlpha = 0.04f; + particles = true; particleDensity = 0.3f; particleScale = 0.4f; + particleMotion = 2; particleDir = "mc:enchantmenttable"; + colorData.innerColor = 0x4488FF; colorData.outerColor = 0x2244BB; + windUpColor = 0x604488FF; activeColor = 0xC066AAFF; + effects.add(new AbilityEffect(EnumPotionType.MiningFatigue, 80, 1)); + break; + case "SNARE": + groundAlpha = 0.04f; + particles = true; particleDensity = 0.3f; particleScale = 0.5f; + particleMotion = 1; particleDir = "mc:snowshovel"; + colorData.innerColor = 0x88CCFF; colorData.outerColor = 0x4488CC; + windUpColor = 0x6088CCFF; activeColor = 0xC0AADDFF; + effects.add(new AbilityEffect(EnumPotionType.Slowness, 100, 1)); + break; + case "HIDDEN": + default: + groundAlpha = 0.03f; + particles = false; particleDensity = 0.0f; particleScale = 1.0f; + particleMotion = 0; + colorData.innerColor = 0x888888; colorData.outerColor = 0x444444; + windUpColor = 0x60888888; activeColor = 0xC0AAAAAA; + break; + } + } @Override public void writeTypeNBT(NBTTagCompound nbt) { @@ -124,10 +190,6 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { this.visible = nbt.getBoolean("visible"); } - // ═══════════════════════════════════════════════════════════════════ - // TRAP-SPECIFIC GETTERS & SETTERS - // ═══════════════════════════════════════════════════════════════════ - public float getTriggerRadius() { return triggerRadius; } public void setTriggerRadius(float triggerRadius) { this.triggerRadius = triggerRadius; } public int getArmTime() { return armTime; } @@ -145,9 +207,18 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { public boolean isVisible() { return visible; } public void setVisible(boolean visible) { this.visible = visible; } - // ═══════════════════════════════════════════════════════════════════ - // GUI FIELD DEFINITIONS - // ═══════════════════════════════════════════════════════════════════ + @SideOnly(Side.CLIENT) + @Override + protected void addPresetFieldDef(List defs) { + defs.add(FieldDef.subGuiField("gui.applyPreset", + SubGuiTrapPresetSelector::new, + gui -> { + SubGuiTrapPresetSelector selector = (SubGuiTrapPresetSelector) gui; + if (selector.selectedPreset != null) { + applyTrapPresetDefaults(selector.selectedPreset); + } + })); + } @SideOnly(Side.CLIENT) @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java index 438fd11d4..0d4b0ce8f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -31,27 +31,20 @@ import java.util.Iterator; import java.util.List; import java.util.Random; -/** - * Abstract base class for zone-based abilities (Trap, Hazard). - * Extracts shared fields: duration, shape, spawn, visual, lifecycle, and FieldDefs. - */ public abstract class AbilityZone extends Ability { - // Zone properties protected int durationTicks; protected ZoneShape zoneShape = ZoneShape.CIRCLE; protected float spawnRadius = 10.0f; protected int zoneCount = 3; protected float zoneHeight = 2.0f; - // Visual parameters protected float particleDensity = 1.0f; protected float particleScale = 1.0f; protected float animSpeed = 1.0f; protected float lightningDensity = 1.0f; protected EnergyDisplayData colorData; - // Visual layer fields (always active) protected boolean groundFill = true; protected float groundAlpha = 0.25f; protected boolean rings = true; @@ -67,7 +60,6 @@ public abstract class AbilityZone extends Ability { protected int particleSize = 32; protected boolean particleGlow = true; - // Runtime state protected static final Random RANDOM = new Random(); protected transient List activeEntities = new ArrayList<>(); protected transient List preCalculatedPositions = new ArrayList<>(); @@ -82,10 +74,6 @@ public abstract class AbilityZone extends Ability { this.allowedBy = UserType.NPC_ONLY; } - // ═════════════════════════════════════════════════════════════════ - // SHARED OVERRIDES - // ═════════════════════════════════════════════════════════════════ - @Override public boolean allowBurst() { return false; @@ -101,17 +89,12 @@ public abstract class AbilityZone extends Ability { return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } - /** - * Returns the actual zone effect radius for telegraph sizing. - * AbilityHazard returns its damage radius, AbilityTrap returns triggerRadius. - */ public abstract float getZoneRadius(); @Override public List createTelegraphs(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || windUpTicks <= 0) return new ArrayList<>(); - // Pre-calculate zone positions preCalculatedPositions.clear(); List placedPositions = new ArrayList<>(); float minSeparation = getZoneRadius() * 2.0f; @@ -122,7 +105,6 @@ public abstract class AbilityZone extends Ability { preCalculatedPositions.add(pos); } - // Create a telegraph for each zone position List telegraphs = new ArrayList<>(); float zoneRadius = getZoneRadius(); @@ -152,7 +134,6 @@ public abstract class AbilityZone extends Ability { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { - // Remove dead entities Iterator it = activeEntities.iterator(); while (it.hasNext()) { EntityAbilityZone entity = it.next(); @@ -192,13 +173,6 @@ public abstract class AbilityZone extends Ability { return durationTicks + 10; } - // ═════════════════════════════════════════════════════════════════ - // SHARED SPAWN HELPER - // ═════════════════════════════════════════════════════════════════ - - /** - * Find a non-overlapping spawn position within spawnRadius of the caster. - */ protected double[] findSpawnPosition(EntityLivingBase caster, List placedPositions, float minSeparation) { double spawnX = caster.posX; double spawnZ = caster.posZ; @@ -227,9 +201,6 @@ public abstract class AbilityZone extends Ability { return new double[]{spawnX, spawnZ}; } - /** - * Apply visual settings to a zone entity. - */ protected void applyVisualToEntity(EntityAbilityZone entity) { entity.applyVisual(groundFill, groundAlpha, rings, ringCount, border, borderSpeed, @@ -238,13 +209,6 @@ public abstract class AbilityZone extends Ability { particleSize, particleGlow); } - // ═════════════════════════════════════════════════════════════════ - // PRESET SUPPORT - // ═════════════════════════════════════════════════════════════════ - - /** - * Apply preset defaults by style name. - */ private void applyPresetDefaults(String styleName) { effects.clear(); particleDir = ""; @@ -307,10 +271,6 @@ public abstract class AbilityZone extends Ability { } } - /** - * Client-only preset enum for the GUI dropdown. - * Not persisted — presets simply set field values. - */ @SideOnly(Side.CLIENT) public enum ZonePreset { DEFAULT, TOXIC, INFERNO, ARCANE, ELECTRIC, FROST; @@ -321,10 +281,6 @@ public abstract class AbilityZone extends Ability { } } - // ═════════════════════════════════════════════════════════════════ - // SHARED NBT - // ═════════════════════════════════════════════════════════════════ - protected void writeZoneNBT(NBTTagCompound nbt) { nbt.setInteger("durationTicks", durationTicks); nbt.setString("zoneShape", zoneShape.name()); @@ -337,7 +293,6 @@ public abstract class AbilityZone extends Ability { nbt.setFloat("lightningDensity", lightningDensity); colorData.writeNBT(nbt); - // Visual layer fields — always written as top-level keys nbt.setBoolean("groundFill", groundFill); nbt.setFloat("groundAlpha", groundAlpha); nbt.setBoolean("rings", rings); @@ -367,7 +322,6 @@ public abstract class AbilityZone extends Ability { this.lightningDensity = nbt.getFloat("lightningDensity"); colorData.readNBT(nbt); - // Visual layer fields this.groundFill = nbt.getBoolean("groundFill"); this.groundAlpha = nbt.getFloat("groundAlpha"); this.rings = nbt.getBoolean("rings"); @@ -384,10 +338,6 @@ public abstract class AbilityZone extends Ability { this.particleGlow = nbt.getBoolean("particleGlow"); } - // ═════════════════════════════════════════════════════════════════ - // SHARED GETTERS & SETTERS - // ═════════════════════════════════════════════════════════════════ - public int getDurationTicks() { return durationTicks; } public void setDurationTicks(int durationTicks) { this.durationTicks = Math.max(1, durationTicks); } @@ -416,7 +366,6 @@ public abstract class AbilityZone extends Ability { public float getLightningDensity() { return lightningDensity; } public void setLightningDensity(float v) { this.lightningDensity = Math.max(0, Math.min(3, v)); } - // Color getters/setters public int getInnerColor() { return colorData.innerColor; } public void setInnerColor(int color) { colorData.innerColor = color; } public int getOuterColor() { return colorData.outerColor; } @@ -424,7 +373,6 @@ public abstract class AbilityZone extends Ability { public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } public void setOuterColorEnabled(boolean enabled) { colorData.outerColorEnabled = enabled; } - // Visual layer getters/setters public boolean isGroundFill() { return groundFill; } public void setGroundFill(boolean v) { this.groundFill = v; } public float getGroundAlpha() { return groundAlpha; } @@ -458,10 +406,6 @@ public abstract class AbilityZone extends Ability { public boolean isParticleGlow() { return particleGlow; } public void setParticleGlow(boolean v) { this.particleGlow = v; } - // ═════════════════════════════════════════════════════════════════ - // SHARED FIELD DEFINITIONS (client-only) - // ═════════════════════════════════════════════════════════════════ - @SideOnly(Side.CLIENT) protected void addPresetFieldDef(List defs) { defs.add(FieldDef.subGuiField("gui.applyPreset", @@ -477,42 +421,36 @@ public abstract class AbilityZone extends Ability { @SideOnly(Side.CLIENT) protected void addVisualFieldDefs(List defs) { defs.addAll(Arrays.asList( - // Ground Fill FieldDef.section("ability.section.ground").tab("ability.tab.visual"), FieldDef.row( FieldDef.boolField("gui.enabled", this::isGroundFill, this::setGroundFill), FieldDef.floatField("gui.alpha", this::getGroundAlpha, this::setGroundAlpha) ).tab("ability.tab.visual"), - // Rings FieldDef.section("ability.section.rings").tab("ability.tab.visual"), FieldDef.row( FieldDef.boolField("gui.enabled", this::isRings, this::setRings), FieldDef.intField("gui.count", this::getRingCount, this::setRingCount).range(1, 5) ).tab("ability.tab.visual"), - // Border FieldDef.section("ability.section.border").tab("ability.tab.visual"), FieldDef.row( FieldDef.boolField("gui.enabled", this::isBorder, this::setBorder), FieldDef.floatField("gui.speed", this::getBorderSpeed, this::setBorderSpeed) ).tab("ability.tab.visual"), - // Accents FieldDef.section("ability.section.accents").tab("ability.tab.visual"), FieldDef.row( FieldDef.boolField("gui.enabled", this::isAccents, this::setAccents), FieldDef.enumField("gui.style", AccentStyle.class, this::getAccentStyleEnum, this::setAccentStyleEnum) ).tab("ability.tab.visual"), - // Lightning FieldDef.section("ability.section.lightning").tab("ability.tab.visual"), FieldDef.row( FieldDef.boolField("gui.enabled", this::isLightning, this::setLightning), FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) ).tab("ability.tab.visual"), - // Particles FieldDef.section("ability.section.particles").tab("ability.tab.visual"), FieldDef.boolField("gui.enabled", this::isParticles, this::setParticles) .tab("ability.tab.visual"), @@ -527,12 +465,10 @@ public abstract class AbilityZone extends Ability { FieldDef.stringField("gui.texture", this::getParticleDir, this::setParticleDir) .tab("ability.tab.visual").visibleWhen(this::isParticles), - // Animation FieldDef.section("ability.section.animation").tab("ability.tab.visual"), FieldDef.floatField("gui.speed", this::getAnimSpeed, this::setAnimSpeed) .tab("ability.tab.visual"), - // Colors FieldDef.section("ability.section.colors").tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) .tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java index 2f2bc00e9..34156e362 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java @@ -26,7 +26,7 @@ import java.util.UUID; /** * Shared zone entity for Trap and Hazard abilities. - * Handles rendering, positioning, and damage logic. + * ZoneType distinguishes behavior: TRAP (proximity-triggered burst) vs HAZARD (persistent AoE). */ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnData { @@ -76,11 +76,11 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD } } - /** Tiny Y offset above ground to prevent z-clipping */ private static final double GROUND_OFFSET = 0.005; + private static final byte TRIGGER_FLASH_STATUS = 60; // ═══════════════════════════════════════════════════════════════════ - // COMMON PROPERTIES (synced via ByteBuf) + // COMMON PROPERTIES // ═══════════════════════════════════════════════════════════════════ private ZoneType zoneType = ZoneType.TRAP; @@ -99,13 +99,12 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD private float zoneHeight = 2.0f; - // Visual parameters (synced for renderer) private float particleDensity = 1.0f; private float particleScale = 1.0f; private float animSpeed = 1.0f; private float lightningDensity = 1.0f; - // Visual layer parameters (always active) + // Visual layer fields private boolean groundFill = true; private float groundAlpha = 0.25f; private boolean rings = true; @@ -113,18 +112,15 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD private boolean border = true; private float borderSpeed = 1.0f; private boolean accents = true; - private int accentStyle = 0; // AccentStyle ordinal + private int accentStyle = 0; private boolean lightning = false; private boolean particles = true; - private int particleMotion = 0; // ParticleMotion ordinal + private int particleMotion = 0; private String particleDir = ""; private int particleSize = 32; private boolean particleGlow = true; - // Lifetime private long deathWorldTime = -1; - - // Effects private List effects = new ArrayList<>(); // ═══════════════════════════════════════════════════════════════════ @@ -138,6 +134,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD private float damage = 6.0f; private float damageRadius = 0.0f; private float knockback = 0.5f; + private boolean visible = true; // ═══════════════════════════════════════════════════════════════════ // HAZARD-SPECIFIC PROPERTIES @@ -149,21 +146,18 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD private boolean affectsCaster = false; // ═══════════════════════════════════════════════════════════════════ - // RUNTIME STATE (transient) + // RUNTIME STATE // ═══════════════════════════════════════════════════════════════════ - // Trap runtime private transient boolean armed = false; private transient int triggerCount = 0; private transient int ticksSinceLastTrigger = 0; private transient Set triggeredEntities = new HashSet<>(); - private transient int triggerFlashTick = -1; // For visual flash on trigger + private transient int triggerFlashTick = -1; - // Hazard runtime private transient int ticksSinceDamage = 0; private transient Set damagedThisTick = new HashSet<>(); - // Preview mode private boolean previewMode = false; private EntityLivingBase previewOwner = null; @@ -179,11 +173,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.ignoreFrustumCheck = true; } - /** - * Full constructor for creating a zone entity. - * Use the static factory methods for cleaner creation. - * Position is snapped to ground level with a tiny offset to avoid z-clipping. - */ private EntityAbilityZone(World world, ZoneType type, EntityLivingBase owner, double x, double y, double z) { this(world); @@ -194,9 +183,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.setPosition(x, groundY, z); } - /** - * Create a Trap zone entity. - */ public static EntityAbilityZone createTrap(World world, EntityLivingBase owner, double x, double y, double z, ZoneShape shape, @@ -207,6 +193,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD float zoneHeight, float particleDensity, float particleScale, float animSpeed, float lightningDensity, + boolean visible, List effects) { EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.TRAP, owner, x, y, z); zone.shape = shape; @@ -228,19 +215,16 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD zone.particleScale = particleScale; zone.animSpeed = animSpeed; zone.lightningDensity = lightningDensity; + zone.visible = visible; if (effects != null) { for (AbilityEffect e : effects) { zone.effects.add(e.copy()); } } - // Ready to trigger immediately after arm time zone.ticksSinceLastTrigger = triggerCooldown; return zone; } - /** - * Create a Hazard zone entity. - */ public static EntityAbilityZone createHazard(World world, EntityLivingBase owner, double x, double y, double z, ZoneShape shape, @@ -275,7 +259,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD zone.effects.add(e.copy()); } } - // Ready to deal damage immediately zone.ticksSinceDamage = damageInterval; return zone; } @@ -285,8 +268,15 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD // ═══════════════════════════════════════════════════════════════════ @Override - protected void entityInit() { - // No DataWatcher needed + protected void entityInit() {} + + @Override + public void handleHealthUpdate(byte id) { + if (id == TRIGGER_FLASH_STATUS) { + this.triggerFlashTick = this.ticksExisted; + } else { + super.handleHealthUpdate(id); + } } @Override @@ -297,18 +287,15 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD super.onUpdate(); - // Set death time on first tick if (deathWorldTime < 0 && worldObj != null) { deathWorldTime = worldObj.getTotalWorldTime() + maxTicks; } - // Check lifetime if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { this.setDead(); return; } - // Server handles damage if (!worldObj.isRemote && !previewMode) { switch (zoneType) { case TRAP: @@ -322,11 +309,10 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD } // ═══════════════════════════════════════════════════════════════════ - // TRAP LOGIC (server-only) + // TRAP LOGIC // ═══════════════════════════════════════════════════════════════════ private void tickTrap() { - // Arm time countdown if (!armed) { if (ticksExisted >= armTime) { armed = true; @@ -336,18 +322,15 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD ticksSinceLastTrigger++; - // All triggers used if (maxTriggers > 0 && triggerCount >= maxTriggers) { this.setDead(); return; } - // Cooldown between triggers if (ticksSinceLastTrigger < triggerCooldown) { return; } - // Proximity detection AxisAlignedBB box = AxisAlignedBB.getBoundingBox( posX - triggerRadius, posY - 0.5, posZ - triggerRadius, posX + triggerRadius, posY + zoneHeight, posZ + triggerRadius @@ -419,7 +402,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD anyDamaged = true; } - // Apply knockback only if damage landed if (hit && knockback > 0) { double dx = entity.posX - posX; double dz = entity.posZ - posZ; @@ -430,23 +412,22 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD } } - // Apply effects only if damage landed if (hit) { applyEffects(entity); } } - // Only count the trigger if at least one entity was actually damaged if (anyDamaged) { triggerCount++; ticksSinceLastTrigger = 0; triggeredEntities.add(triggerer.getUniqueID()); triggerFlashTick = ticksExisted; + worldObj.setEntityState(this, TRIGGER_FLASH_STATUS); } } // ═══════════════════════════════════════════════════════════════════ - // HAZARD LOGIC (server-only) + // HAZARD LOGIC // ═══════════════════════════════════════════════════════════════════ private void tickHazard() { @@ -545,12 +526,12 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD @Override @SideOnly(Side.CLIENT) public boolean isInRangeToRenderDist(double distance) { - return distance < 16384.0D; // 128 blocks + return distance < 16384.0D; } @Override public boolean shouldRenderInPass(int pass) { - return pass == 1; // Translucent pass + return pass == 1; } @Override @@ -576,6 +557,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD public boolean isOuterColorEnabled() { return outerColorEnabled; } public float getZoneHeight() { return zoneHeight; } public boolean isArmed() { return armed || ticksExisted >= armTime; } + public boolean isVisible() { return visible; } public int getTriggerFlashTick() { return triggerFlashTick; } public int getDurationTicks() { return durationTicks; } public int getMaxTicks() { return maxTicks; } @@ -584,7 +566,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD public float getAnimSpeed() { return animSpeed; } public float getLightningDensity() { return lightningDensity; } - // Visual layer getters public boolean isGroundFill() { return groundFill; } public float getGroundAlpha() { return groundAlpha; } public boolean isRings() { return rings; } @@ -600,9 +581,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD public int getParticleSize() { return particleSize; } public boolean isParticleGlow() { return particleGlow; } - /** - * Apply visual settings from ability data. - */ public void applyVisual(boolean groundFill, float groundAlpha, boolean rings, int ringCount, boolean border, float borderSpeed, @@ -626,44 +604,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.particleGlow = particleGlow; } - /** - * Apply preset defaults by old style name (for backward compatibility). - */ - private void applyPresetDefaults(String styleName) { - switch (styleName) { - case "TOXIC": - groundFill = true; groundAlpha = 0.30f; rings = true; ringCount = 3; - border = false; borderSpeed = 1.0f; accents = true; accentStyle = 1; - lightning = false; particles = true; particleMotion = 0; particleGlow = true; - break; - case "INFERNO": - groundFill = true; groundAlpha = 0.35f; rings = true; ringCount = 3; - border = true; borderSpeed = 2.0f; accents = true; accentStyle = 2; - lightning = false; particles = true; particleMotion = 0; particleGlow = true; - break; - case "ARCANE": - groundFill = true; groundAlpha = 0.20f; rings = true; ringCount = 1; - border = true; borderSpeed = 0.8f; accents = true; accentStyle = 0; - lightning = false; particles = true; particleMotion = 1; particleGlow = true; - break; - case "ELECTRIC": - groundFill = true; groundAlpha = 0.15f; rings = false; ringCount = 1; - border = false; borderSpeed = 1.0f; accents = false; accentStyle = 0; - lightning = true; particles = true; particleMotion = 2; particleGlow = true; - break; - case "FROST": - groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3; - border = true; borderSpeed = 0.3f; accents = true; accentStyle = 0; - lightning = false; particles = true; particleMotion = 1; particleGlow = true; - break; - default: // DEFAULT - groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3; - border = true; borderSpeed = 1.0f; accents = true; accentStyle = 0; - lightning = false; particles = false; particleMotion = 0; particleGlow = true; - break; - } - } - // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ @@ -671,16 +611,9 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD @Override protected void readEntityFromNBT(NBTTagCompound nbt) { this.zoneType = ZoneType.values()[nbt.getInteger("ZoneType")]; - - // Shape: read new string key first, fall back to old ordinal (always CIRCLE for old RING/CONE) - if (nbt.hasKey("ShapeName")) { - try { - this.shape = ZoneShape.valueOf(nbt.getString("ShapeName")); - } catch (Exception e) { - this.shape = ZoneShape.CIRCLE; - } - } else { - // Legacy: old ordinal data — RING(1) and CONE(2) map to CIRCLE + try { + this.shape = ZoneShape.valueOf(nbt.getString("ShapeName")); + } catch (Exception e) { this.shape = ZoneShape.CIRCLE; } @@ -691,51 +624,28 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.innerColor = nbt.getInteger("InnerColor"); this.outerColor = nbt.getInteger("OuterColor"); this.outerColorEnabled = nbt.getBoolean("OuterColorEnabled"); - this.zoneHeight = nbt.hasKey("ZoneHeight") ? nbt.getFloat("ZoneHeight") : 2.0f; - this.particleDensity = nbt.hasKey("ParticleDensity") ? nbt.getFloat("ParticleDensity") : 1.0f; - this.particleScale = nbt.hasKey("ParticleScale") ? nbt.getFloat("ParticleScale") : 1.0f; - this.animSpeed = nbt.hasKey("AnimSpeed") ? nbt.getFloat("AnimSpeed") : 1.0f; - this.lightningDensity = nbt.hasKey("LightningDensity") ? nbt.getFloat("LightningDensity") : 1.0f; - this.deathWorldTime = nbt.hasKey("DeathWorldTime") ? nbt.getLong("DeathWorldTime") : -1; + this.zoneHeight = nbt.getFloat("ZoneHeight"); + this.particleDensity = nbt.getFloat("ParticleDensity"); + this.particleScale = nbt.getFloat("ParticleScale"); + this.animSpeed = nbt.getFloat("AnimSpeed"); + this.lightningDensity = nbt.getFloat("LightningDensity"); + this.deathWorldTime = nbt.getLong("DeathWorldTime"); - // Visual fields — backward compat with old ZoneStyle format - if (nbt.hasKey("ZoneStyle")) { - // OLD FORMAT: apply preset defaults, then overlay CustomVisual - applyPresetDefaults(nbt.getString("ZoneStyle")); - if (nbt.hasKey("CustomVisual")) { - NBTTagCompound cv = nbt.getCompoundTag("CustomVisual"); - this.groundFill = !cv.hasKey("GroundFill") || cv.getBoolean("GroundFill"); - this.groundAlpha = cv.hasKey("GroundAlpha") ? cv.getFloat("GroundAlpha") : 0.25f; - this.rings = !cv.hasKey("Rings") || cv.getBoolean("Rings"); - this.ringCount = cv.hasKey("RingCount") ? cv.getInteger("RingCount") : 3; - this.border = !cv.hasKey("Border") || cv.getBoolean("Border"); - this.borderSpeed = cv.hasKey("BorderSpeed") ? cv.getFloat("BorderSpeed") : 1.0f; - this.accents = !cv.hasKey("Accents") || cv.getBoolean("Accents"); - this.accentStyle = cv.hasKey("AccentStyle") ? cv.getInteger("AccentStyle") : 0; - this.lightning = cv.hasKey("Lightning") && cv.getBoolean("Lightning"); - this.particles = !cv.hasKey("Particles") || cv.getBoolean("Particles"); - this.particleMotion = cv.hasKey("ParticleMotion") ? cv.getInteger("ParticleMotion") : 0; - this.particleDir = cv.hasKey("ParticleDir") ? cv.getString("ParticleDir") : ""; - this.particleSize = cv.hasKey("ParticleSize") ? cv.getInteger("ParticleSize") : 32; - this.particleGlow = !cv.hasKey("ParticleGlow") || cv.getBoolean("ParticleGlow"); - } - } else { - // NEW FORMAT: read individual top-level keys - this.groundFill = !nbt.hasKey("GroundFill") || nbt.getBoolean("GroundFill"); - this.groundAlpha = nbt.hasKey("GroundAlpha") ? nbt.getFloat("GroundAlpha") : 0.25f; - this.rings = !nbt.hasKey("Rings") || nbt.getBoolean("Rings"); - this.ringCount = nbt.hasKey("RingCount") ? nbt.getInteger("RingCount") : 3; - this.border = !nbt.hasKey("Border") || nbt.getBoolean("Border"); - this.borderSpeed = nbt.hasKey("BorderSpeed") ? nbt.getFloat("BorderSpeed") : 1.0f; - this.accents = !nbt.hasKey("Accents") || nbt.getBoolean("Accents"); - this.accentStyle = nbt.hasKey("AccentStyle") ? nbt.getInteger("AccentStyle") : 0; - this.lightning = nbt.hasKey("Lightning") && nbt.getBoolean("Lightning"); - this.particles = !nbt.hasKey("Particles") || nbt.getBoolean("Particles"); - this.particleMotion = nbt.hasKey("ParticleMotion") ? nbt.getInteger("ParticleMotion") : 0; - this.particleDir = nbt.hasKey("ParticleDir") ? nbt.getString("ParticleDir") : ""; - this.particleSize = nbt.hasKey("ParticleSize") ? nbt.getInteger("ParticleSize") : 32; - this.particleGlow = !nbt.hasKey("ParticleGlow") || nbt.getBoolean("ParticleGlow"); - } + // Visual layer fields + this.groundFill = nbt.getBoolean("GroundFill"); + this.groundAlpha = nbt.getFloat("GroundAlpha"); + this.rings = nbt.getBoolean("Rings"); + this.ringCount = nbt.getInteger("RingCount"); + this.border = nbt.getBoolean("Border"); + this.borderSpeed = nbt.getFloat("BorderSpeed"); + this.accents = nbt.getBoolean("Accents"); + this.accentStyle = nbt.getInteger("AccentStyle"); + this.lightning = nbt.getBoolean("Lightning"); + this.particles = nbt.getBoolean("Particles"); + this.particleMotion = nbt.getInteger("ParticleMotion"); + this.particleDir = nbt.getString("ParticleDir"); + this.particleSize = nbt.getInteger("ParticleSize"); + this.particleGlow = nbt.getBoolean("ParticleGlow"); // Trap this.armTime = nbt.getInteger("ArmTime"); @@ -745,6 +655,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.damage = nbt.getFloat("Damage"); this.damageRadius = nbt.getFloat("DamageRadius"); this.knockback = nbt.getFloat("Knockback"); + this.visible = nbt.getBoolean("Visible"); // Hazard this.damagePerSecond = nbt.getFloat("DamagePerSecond"); @@ -783,7 +694,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD nbt.setFloat("LightningDensity", lightningDensity); nbt.setLong("DeathWorldTime", deathWorldTime); - // Visual fields — always written as top-level keys + // Visual layer fields nbt.setBoolean("GroundFill", groundFill); nbt.setFloat("GroundAlpha", groundAlpha); nbt.setBoolean("Rings", rings); @@ -807,6 +718,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD nbt.setFloat("Damage", damage); nbt.setFloat("DamageRadius", damageRadius); nbt.setFloat("Knockback", knockback); + nbt.setBoolean("Visible", visible); // Hazard nbt.setFloat("DamagePerSecond", damagePerSecond); @@ -843,7 +755,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD buffer.writeFloat(animSpeed); buffer.writeFloat(lightningDensity); - // Visual fields — always written individually + // Visual layer fields buffer.writeBoolean(groundFill); buffer.writeFloat(groundAlpha); buffer.writeBoolean(rings); @@ -859,10 +771,11 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD buffer.writeInt(particleSize); buffer.writeBoolean(particleGlow); - // Trap-specific (client needs armTime for visual state) + // Trap-specific buffer.writeInt(armTime); + buffer.writeBoolean(visible); - // Effects via NBT in buffer + // Effects NBTTagCompound effectsNbt = new NBTTagCompound(); NBTTagList effectList = new NBTTagList(); for (AbilityEffect effect : effects) { @@ -894,7 +807,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.animSpeed = buffer.readFloat(); this.lightningDensity = buffer.readFloat(); - // Visual fields — always read individually + // Visual layer fields this.groundFill = buffer.readBoolean(); this.groundAlpha = buffer.readFloat(); this.rings = buffer.readBoolean(); @@ -911,8 +824,9 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.particleGlow = buffer.readBoolean(); this.armTime = buffer.readInt(); + this.visible = buffer.readBoolean(); - // Effects via NBT + // Effects NBTTagCompound effectsNbt = ByteBufUtils.readTag(buffer); effects.clear(); if (effectsNbt != null && effectsNbt.hasKey("Effects")) { diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java b/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java new file mode 100644 index 000000000..b8fb9e376 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/SubGuiTrapPresetSelector.java @@ -0,0 +1,72 @@ +package noppes.npcs.client.gui; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.SubGuiInterface; + +@SideOnly(Side.CLIENT) +public class SubGuiTrapPresetSelector extends SubGuiInterface { + + public String selectedPreset = null; + + private static final String[] PRESET_NAMES = {"HIDDEN", "VENOM", "EXPLOSIVE", "CURSED", "SHOCK", "SNARE"}; + private static final String[] PRESET_LABELS = { + "ability.preset.hidden", "ability.preset.venom", "ability.preset.explosive", + "ability.preset.cursed", "ability.preset.shock", "ability.preset.snare" + }; + private static final int[] PRESET_COLORS = {0x888888, 0x44DD44, 0xFF6611, 0xAA44FF, 0x4488FF, 0x88CCFF}; + + public SubGuiTrapPresetSelector() { + xSize = 170; + ySize = 135; + setBackground("menubg.png"); + } + + @Override + public void initGui() { + super.initGui(); + int btnW = 72; + int btnH = 20; + int gapY = 4; + int col1 = guiLeft + 7; + int col2 = col1 + btnW + 6; + int startY = guiTop + 22; + + for (int i = 0; i < 6; i++) { + int x = (i % 2 == 0) ? col1 : col2; + int y = startY + (i / 2) * (btnH + gapY); + addButton(new GuiNpcButton(i, x, y, btnW, btnH, PRESET_LABELS[i])); + } + + int cancelY = startY + 3 * (btnH + gapY) + 4; + addButton(new GuiNpcButton(10, guiLeft + xSize / 2 - 36, cancelY, 72, btnH, "gui.cancel")); + } + + @Override + protected void actionPerformed(GuiButton btn) { + super.actionPerformed(btn); + if (btn.id >= 0 && btn.id < 6) { + selectedPreset = PRESET_NAMES[btn.id]; + close(); + } else if (btn.id == 10) { + close(); + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float f) { + super.drawScreen(mouseX, mouseY, f); + drawCenteredString(fontRendererObj, StatCollector.translateToLocal("gui.applyPreset"), guiLeft + xSize / 2, guiTop + 8, 0xFFFFFF); + for (int i = 0; i < 6; i++) { + GuiNpcButton btn = getButton(i); + if (btn != null && btn.visible) { + int x = btn.xPosition + 4; + int y = btn.yPosition + 5; + drawRect(x, y, x + 10, y + 10, 0xFF000000 | PRESET_COLORS[i]); + } + } + } +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index f265f0fe9..eaaf48a45 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1578,6 +1578,12 @@ ability.preset.inferno=Inferno ability.preset.arcane=Arcane ability.preset.electric=Electric ability.preset.frost=Frost +ability.preset.hidden=Hidden +ability.preset.venom=Venom +ability.preset.explosive=Explosive +ability.preset.cursed=Cursed +ability.preset.shock=Shock +ability.preset.snare=Snare # Accent Styles ability.accentStyle.static=Static From ce24c0a4ff4d63676d53396f096fb72df2390ab8 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 19:21:30 -0500 Subject: [PATCH 08/20] Enhance ability selection and display: add toggleable ability handling, improve ability name color coding, and update ability selection messaging --- .../kamkeel/npcs/command/AbilityCommand.java | 4 +-- .../data/ability/AbilityController.java | 18 +++++++--- .../controllers/data/ability/AbilitySlot.java | 34 ++++++++++++++++++- .../npcs/entity/EntityAbilityBeam.java | 20 +++++++++++ .../npcs/entity/EntityAbilityProjectile.java | 16 +++++---- .../noppes/npcs/client/ClientTickHandler.java | 4 ++- .../client/gui/advanced/GuiNPCAbilities.java | 6 ++-- 7 files changed, 86 insertions(+), 16 deletions(-) diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index 004274fc3..90d20ea1e 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -306,8 +306,8 @@ public class AbilityCommand extends CommandKamkeelBase { } /** - * Get list of player-usable ability keys (for tab completion). - * Uses registry keys for built-in abilities (no spaces) and UUIDs for custom. + * Get list of player-usable ability names (for tab completion). + * Uses registry keys for built-in abilities and display names for custom abilities. * Used by CommandKamkeel for usage token. */ public static List getPlayerAbilityNames() { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 992c56cd9..43af10b7c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -304,7 +304,13 @@ public class AbilityController implements IAbilityHandler { keys.add(id); } } - keys.addAll(customAbilities.keySet()); + // Use ability names for custom abilities (instead of UUIDs) for readable tab completion + for (Ability ability : customAbilities.values()) { + String name = ability.getName(); + if (name != null && !name.isEmpty()) { + keys.add(name); + } + } return keys; } @@ -318,9 +324,13 @@ public class AbilityController implements IAbilityHandler { } } } - for (Map.Entry entry : customAbilities.entrySet()) { - if (entry.getValue().getAllowedBy().allowsPlayer()) { - keys.add(entry.getKey()); + // Use ability names for custom abilities (instead of UUIDs) for readable tab completion + for (Ability ability : customAbilities.values()) { + if (ability.getAllowedBy().allowsPlayer()) { + String name = ability.getName(); + if (name != null && !name.isEmpty()) { + keys.add(name); + } } } return keys; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java index 5fde66586..4849e4dd4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java @@ -14,6 +14,9 @@ public class AbilitySlot { private Ability inlineAbility; private String referenceId; + /** Per-slot enabled override for reference slots (null = use master's default). */ + private Boolean enabledOverride; + private transient Ability cachedAbility; private transient int cachedVersion = -1; @@ -67,10 +70,32 @@ public class AbilitySlot { } cachedAbility = controller.resolveAbility(referenceId); + // Apply per-slot enabled override so NPC-specific enable/disable persists + if (cachedAbility != null && enabledOverride != null) { + cachedAbility.setEnabled(enabledOverride); + } cachedVersion = currentVersion; return cachedAbility; } + /** + * Set the per-slot enabled override for reference slots. + * For inline slots, delegates directly to the ability. + */ + public void setEnabled(boolean enabled) { + if (!isReference()) { + if (inlineAbility != null) { + inlineAbility.setEnabled(enabled); + } + } else { + this.enabledOverride = enabled; + // Also update the cached ability immediately if present + if (cachedAbility != null) { + cachedAbility.setEnabled(enabled); + } + } + } + /** * Get the inline ability directly (null for reference slots). */ @@ -104,6 +129,9 @@ public class AbilitySlot { if (isReference()) { NBTTagCompound nbt = new NBTTagCompound(); nbt.setString("Reference", referenceId); + if (enabledOverride != null) { + nbt.setBoolean("RefEnabled", enabledOverride); + } return nbt; } return inlineAbility != null ? inlineAbility.writeNBT() : new NBTTagCompound(); @@ -113,7 +141,11 @@ public class AbilitySlot { if (nbt == null) return null; if (nbt.hasKey("Reference")) { - return reference(nbt.getString("Reference")); + AbilitySlot slot = reference(nbt.getString("Reference")); + if (nbt.hasKey("RefEnabled")) { + slot.enabledOverride = nbt.getBoolean("RefEnabled"); + } + return slot; } // Inline ability diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 0fb58bcfe..6f5b3a64d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -696,6 +696,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { // Read trail points (relative to origin) trailPoints.clear(); + trailPointAges.clear(); if (nbt.hasKey("Trail")) { NBTTagList trailList = nbt.getTagList("Trail", 10); for (int i = 0; i < trailList.tagCount(); i++) { @@ -707,6 +708,18 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { )); } } + + // Read trail point ages (must match trailPoints size for fading trail) + if (nbt.hasKey("TrailAges")) { + int[] ages = nbt.getIntArray("TrailAges"); + for (int age : ages) { + trailPointAges.add(age); + } + } + // If trail points exist but ages don't (legacy data), initialize ages to 0 + while (trailPointAges.size() < trailPoints.size()) { + trailPointAges.add(0); + } } @Override @@ -733,5 +746,12 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { trailList.appendTag(pointNbt); } nbt.setTag("Trail", trailList); + + // Write trail point ages for fading trail sync + int[] ages = new int[trailPointAges.size()]; + for (int i = 0; i < trailPointAges.size(); i++) { + ages[i] = trailPointAges.get(i); + } + nbt.setIntArray("TrailAges", ages); } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index dee2ed292..9368c5269 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -896,10 +896,10 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP this.motionY = nbt.getDouble("MotionY"); this.motionZ = nbt.getDouble("MotionZ"); - if (this.worldObj != null && this.worldObj.isRemote) { - this.renderCurrentSize = this.size; - this.prevRenderSize = this.size; - } + // Always initialize render size from NBT size so entities + // loading in from outside render distance render correctly + this.renderCurrentSize = this.size; + this.prevRenderSize = this.size; anchorData.readNBT(nbt); combatData.readNBT(nbt); @@ -965,7 +965,9 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP NBTTagCompound compound = new NBTTagCompound(); this.writeEntityToNBT(compound); cpw.mods.fml.common.network.ByteBufUtils.writeTag(buffer, compound); - } catch (Exception ignored) {} + } catch (Exception e) { + noppes.npcs.LogWriter.error("Error writing ability projectile spawn data", e); + } } @Override @@ -975,7 +977,9 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP if (compound != null) { this.readEntityFromNBT(compound); } - } catch (Exception ignored) {} + } catch (Exception e) { + noppes.npcs.LogWriter.error("Error reading ability projectile spawn data", e); + } } // ==================== COLLISION SETTINGS ==================== diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index b4e8738c8..7d82ca0f1 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -63,7 +63,9 @@ public class ClientTickHandler { if (event.phase == Phase.START) { EntityPlayer player = mc.thePlayer; if (player != null) { - boolean specialKeyDown = ClientProxy.SpecialKey != null && Keyboard.isKeyDown(ClientProxy.SpecialKey.getKeyCode()); + // Only process special key when no GUI screen is open + boolean specialKeyDown = mc.currentScreen == null + && ClientProxy.SpecialKey != null && Keyboard.isKeyDown(ClientProxy.SpecialKey.getKeyCode()); if (specialKeyDown != lastSpecialKeyDown) { PlayerData data = CustomNpcs.proxy.getPlayerData(player); if (data != null) { diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index f8ed53c7c..e927b088f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -317,9 +317,11 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Toggle selected ability on/off if (id == 77) { if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { - Ability ability = npcSlots.get(selectedAbilityIndex).getAbility(); + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + Ability ability = slot.getAbility(); if (ability != null) { - ability.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); + boolean enabled = ((GuiNpcButton) guibutton).getValue() == 1; + slot.setEnabled(enabled); updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); From eda8931b5fa21737fc86a459837269f3399f607d Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 21:29:26 -0500 Subject: [PATCH 09/20] Refactor entity owner checks and enhance FULL_MODEL rotation support in ability entities --- .../npcs/entity/EntityAbilityBeam.java | 45 +++--- .../npcs/entity/EntityAbilityDisc.java | 8 +- .../npcs/entity/EntityAbilityLaser.java | 3 +- .../npcs/entity/EntityAbilityProjectile.java | 28 ++-- .../npcs/entity/EntityAbilitySweeper.java | 12 +- .../kamkeel/npcs/util/AnchorPointHelper.java | 136 +++++++++++++++++- 6 files changed, 175 insertions(+), 57 deletions(-) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 6f5b3a64d..bfdca5346 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -386,33 +386,26 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { chargeTick++; Entity owner = getOwnerEntity(); - if (owner == null || owner.isDead) { - setDead(); - return; + if (owner != null) { + Vec3 pos; + if (owner instanceof EntityLivingBase) { + pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance); + } else { + float yaw = (float) Math.toRadians(owner.rotationYaw); + double offsetX = -Math.sin(yaw) * chargeOffsetDistance; + double offsetZ = Math.cos(yaw) * chargeOffsetDistance; + pos = Vec3.createVectorHelper( + owner.posX + offsetX, + owner.posY + owner.getEyeHeight() * 0.7, + owner.posZ + offsetZ + ); + } + + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + startX = pos.xCoord; + startY = pos.yCoord; + startZ = pos.zCoord; } - - // Calculate position based on anchor point - Vec3 pos; - if (owner instanceof EntityLivingBase) { - pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance); - } else { - // Fallback for non-living entities (shouldn't happen normally) - float yaw = (float) Math.toRadians(owner.rotationYaw); - double offsetX = -Math.sin(yaw) * chargeOffsetDistance; - double offsetZ = Math.cos(yaw) * chargeOffsetDistance; - pos = Vec3.createVectorHelper( - owner.posX + offsetX, - owner.posY + owner.getEyeHeight() * 0.7, - owner.posZ + offsetZ - ); - } - - setPosition(pos.xCoord, pos.yCoord, pos.zCoord); - - // Also update origin for when firing starts - startX = pos.xCoord; - startY = pos.yCoord; - startZ = pos.zCoord; } /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 43fa459fc..c0f3cbe8d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -391,19 +391,13 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { protected void updateCharging() { chargeTick++; - Entity owner = getOwnerEntity(); - if (owner == null || owner.isDead) { - setDead(); - return; - } - // Grow size based on charge progress float progress = getChargeProgress(); this.discRadius = targetDiscRadius * progress; this.discThickness = targetDiscThickness * progress; this.size = this.discRadius; // Base size for interpolation - // Calculate position based on anchor point, offset downward by half radius to center + Entity owner = getOwnerEntity(); if (owner instanceof EntityLivingBase) { Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); setPosition(pos.xCoord, pos.yCoord, pos.zCoord); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index a10dae0b8..d17024680 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -156,8 +156,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { */ private void updateLaserOriginAndDirection() { Entity owner = getOwnerEntity(); - if (owner == null || owner.isDead) return; - if (!(owner instanceof EntityLivingBase)) return; + if (owner == null || !(owner instanceof EntityLivingBase)) return; EntityLivingBase livingOwner = (EntityLivingBase) owner; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 9368c5269..ac6add74d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -184,16 +184,20 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP // Skip lifetime/distance checks in preview mode if (!previewMode) { - // Failsafe: if owner entity is gone, dead, or NPC was killed/reset, self-destruct + // Only self-destruct if the owner is confirmed dead/killed. + // A null owner (unloaded/out of chunk) is NOT treated as dead — + // the projectile keeps acting normally and deathWorldTime handles lifetime. if (ownerEntityId >= 0 && ticksExisted > 5) { Entity owner = worldObj.getEntityByID(ownerEntityId); - if (owner == null || owner.isDead) { - this.setDead(); - return; - } - if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) { - this.setDead(); - return; + if (owner != null) { + if (owner.isDead) { + this.setDead(); + return; + } + if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) { + this.setDead(); + return; + } } } @@ -506,16 +510,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP * Update during charging state - grow size and follow anchor. * Default implementation grows {@code size} from 0 to {@code targetSize}. * Override in subclasses for type-specific charging (Disc, Beam). + * + * Note: Owner death checks are handled by onUpdate() before this is called. */ protected void updateCharging() { chargeTick++; - Entity owner = getOwnerEntity(); - if (owner == null || owner.isDead) { - setDead(); - return; - } float progress = getChargeProgress(); this.size = targetSize * progress; + Entity owner = getOwnerEntity(); if (owner instanceof EntityLivingBase) { Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); setPosition(pos.xCoord, pos.yCoord, pos.zCoord); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index 4d072090e..6f0068d26 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -144,15 +144,19 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa return; } - // Follow owner Entity owner = getOwner(); - if (owner == null) { + if (owner != null && owner.isDead) { + this.setDead(); + return; + } + if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) { this.setDead(); return; } - // Update position to follow owner - this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ); + if (owner != null) { + this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ); + } // Check if sweep is done if (completedRotations >= numberOfRotations) { diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 802841c2a..3c9b10f1d 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -119,6 +119,20 @@ public class AnchorPointHelper { y += rotatedOffset.yCoord; z += rotatedOffset.zCoord; + // Apply FULL_MODEL rotation if active + FramePart fullModel = getFullModelPart(entity); + if (fullModel != null) { + double dx = x - entity.posX; + double dy = y - entity.posY; + double dz = z - entity.posZ; + Vec3 rotated = applyFullModelToWorldOffset(fullModel, entity, dx, dy, dz, bodyYaw, scale); + return Vec3.createVectorHelper( + entity.posX + rotated.xCoord, + entity.posY + rotated.yCoord, + entity.posZ + rotated.zCoord + ); + } + return Vec3.createVectorHelper(x, y, z); } @@ -162,16 +176,32 @@ public class AnchorPointHelper { /** * Calculate hand position using animation data if available. + * Applies FULL_MODEL rotation if an active animation has it. */ private static Vec3 calculateHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { // Try to get animated position first - Vec3 animatedPos = getAnimatedHandPosition(entity, anchor, rightHand, scale); - if (animatedPos != null) { - return animatedPos; + Vec3 pos = getAnimatedHandPosition(entity, anchor, rightHand, scale); + if (pos == null) { + // Fallback to static position + pos = calculateFallbackHandPosition(entity, anchor, rightHand, scale); } - // Fallback to static position - return calculateFallbackHandPosition(entity, anchor, rightHand, scale); + // Apply FULL_MODEL rotation if active + FramePart fullModel = getFullModelPart(entity); + if (fullModel != null) { + double dx = pos.xCoord - entity.posX; + double dy = pos.yCoord - entity.posY; + double dz = pos.zCoord - entity.posZ; + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + Vec3 rotated = applyFullModelToWorldOffset(fullModel, entity, dx, dy, dz, bodyYaw, scale); + return Vec3.createVectorHelper( + entity.posX + rotated.xCoord, + entity.posY + rotated.yCoord, + entity.posZ + rotated.zCoord + ); + } + + return pos; } /** @@ -390,4 +420,100 @@ public class AnchorPointHelper { double worldZ = -offsetX * sin + offsetZ * cos; return Vec3.createVectorHelper(worldX, offsetY, worldZ); } + + // ==================== FULL_MODEL ANIMATION SUPPORT ==================== + + /** + * Get the FULL_MODEL FramePart if the entity has an active animation with it. + * Returns null if no FULL_MODEL animation is active. + */ + private static FramePart getFullModelPart(EntityLivingBase entity) { + AnimationData animData = AnimationData.getData(entity); + if (animData == null || !animData.isActive() || animData.animation == null) { + return null; + } + Frame frame = (Frame) animData.animation.currentFrame(); + if (frame == null) { + return null; + } + FramePart part = frame.frameParts.get(EnumAnimationPart.FULL_MODEL); + return part; + } + + /** + * Apply FULL_MODEL rotation to a world-space offset from entity position. + * Converts the offset to model space, applies FULL_MODEL rotation and pivots, + * then converts back to world space. + * + * The rendering pipeline is: entity_pos * body_yaw * scale(-1,-1,1) * FULL_MODEL * parts + * Combined body_yaw + scale maps model space to world space: + * worldX = mx*cos(yaw) + mz*sin(yaw) + * worldY = -my (model Y-down → world Y-up) + * worldZ = mx*sin(yaw) - mz*cos(yaw) + * This transform is its own inverse for X,Z. + * + * FULL_MODEL rotation order matches renderer: vertex sees Z, then Y, then X. + */ + private static Vec3 applyFullModelToWorldOffset(FramePart fullModel, EntityLivingBase entity, + double worldDX, double worldDY, double worldDZ, + float bodyYaw, float scale) { + float[] rotations = getRotations(fullModel, entity); + float[] pivots = getPivots(fullModel, entity); + + // Quick exit if no transform needed + boolean hasRotation = rotations[0] != 0 || rotations[1] != 0 || rotations[2] != 0; + boolean hasPivot = pivots[0] != 0 || pivots[1] != 0 || pivots[2] != 0; + if (!hasRotation && !hasPivot) { + return Vec3.createVectorHelper(worldDX, worldDY, worldDZ); + } + + // Convert world offset to model space + // Body yaw + scale combined transform is an involution (self-inverse) for X,Z + double cosYaw = Math.cos(bodyYaw); + double sinYaw = Math.sin(bodyYaw); + + double mx = worldDX * cosYaw + worldDZ * sinYaw; + double mz = worldDX * sinYaw - worldDZ * cosYaw; + double my = -worldDY; // Invert Y (world Y-up → model Y-down) + + // Apply FULL_MODEL rotations (vertex order: Z first, then Y, then X) + // Matches renderer: glRotatef(X), glRotatef(Y), glRotatef(Z) → vertex sees Z,Y,X + + // Z rotation + double cosZ = Math.cos(rotations[2]); + double sinZ = Math.sin(rotations[2]); + double nx = mx * cosZ - my * sinZ; + double ny = mx * sinZ + my * cosZ; + mx = nx; + my = ny; + + // Y rotation + double cosY = Math.cos(rotations[1]); + double sinY = Math.sin(rotations[1]); + nx = mx * cosY + mz * sinY; + double nz = -mx * sinY + mz * cosY; + mx = nx; + mz = nz; + + // X rotation + double cosX = Math.cos(rotations[0]); + double sinX = Math.sin(rotations[0]); + ny = my * cosX - mz * sinX; + nz = my * sinX + mz * cosX; + my = ny; + mz = nz; + + // Apply pivots in model space (after rotation, matching GL vertex ordering) + // Renderer: glTranslatef(px, -py, pz) → pivot Y is negated for model Y-down space + mx += pivots[0] * MODEL_SCALE * scale; + my -= pivots[1] * MODEL_SCALE * scale; + mz += pivots[2] * MODEL_SCALE * scale; + + // Convert back to world space (same involution for X,Z; invert Y back) + double newDX = mx * cosYaw + mz * sinYaw; + double newDZ = mx * sinYaw - mz * cosYaw; + double newDY = -my; + + return Vec3.createVectorHelper(newDX, newDY, newDZ); + } } From 3beaf6881508ab984f8179a775767309eae2f52a Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 21:44:36 -0500 Subject: [PATCH 10/20] Update GuiNPCAbilities.java --- .../client/gui/advanced/GuiNPCAbilities.java | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index e927b088f..2e89d3d49 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -491,8 +491,31 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC for (int i = 0; i < npcSlots.size(); i++) { list.add(getAbilityListEntry(i)); } + + // Sort: enabled first, then numerically by slot index + Collections.sort(list, new Comparator() { + @Override + public int compare(String a, String b) { + boolean aEnabled = a.startsWith("\u00A7a"); + boolean bEnabled = b.startsWith("\u00A7a"); + if (aEnabled != bEnabled) return aEnabled ? -1 : 1; + return extractIndex(a) - extractIndex(b); + } + + private int extractIndex(String entry) { + String stripped = entry.replaceAll("\u00A7.", "").trim(); + int dotIndex = stripped.indexOf("."); + if (dotIndex > 0) { + try { + return Integer.parseInt(stripped.substring(0, dotIndex).replaceAll("[^0-9]", "")); + } catch (NumberFormatException e) { } + } + return Integer.MAX_VALUE; + } + }); + if (npcAbilitiesScroll != null) { - npcAbilitiesScroll.setList(list); + npcAbilitiesScroll.setUnsortedList(list); } } From 248dbf66da17e3781e69d80ffd44b6e0f81fb4e2 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 22:03:32 -0500 Subject: [PATCH 11/20] Enhance ability animation handling: track ability animation state, clear orphaned animations on login, and ensure proper animation continuation --- .../controllers/data/PlayerAbilityData.java | 27 +++++++++++++++++++ .../npcs/controllers/data/PlayerData.java | 18 +++++++++---- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index f0a4c8c70..22ec80c4b 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -54,6 +54,13 @@ public class PlayerAbilityData implements IPlayerAbilityData { */ private int selectedIndex = 0; + /** + * Tracks whether the current animation was started by an ability. + * Persisted to NBT so orphaned ability animations can be detected and + * cleared on login (ability state is transient, but animation state is not). + */ + private boolean playingAbilityAnimation = false; + // ═══════════════════════════════════════════════════════════════════ // RUNTIME STATE (not saved) // ═══════════════════════════════════════════════════════════════════ @@ -779,6 +786,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { playerData.animationData.setEnabled(true); playerData.animationData.setAnimation(animation); playerData.animationData.updateClient(); + playingAbilityAnimation = true; } public void playAbilityAnimation(int animation) { @@ -800,6 +808,23 @@ public class PlayerAbilityData implements IPlayerAbilityData { 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) { @@ -819,6 +844,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { } compound.setTag("PlayerAbilities", list); compound.setInteger("PlayerAbilitySelected", selectedIndex); + compound.setBoolean("AbilityAnimating", playingAbilityAnimation); } public void readFromNBT(NBTTagCompound compound) { @@ -836,5 +862,6 @@ public class PlayerAbilityData implements IPlayerAbilityData { if (selectedIndex >= unlockedAbilities.size()) { selectedIndex = Math.max(0, unlockedAbilities.size() - 1); } + playingAbilityAnimation = compound.getBoolean("AbilityAnimating"); } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index fa4e7d961..02fd67563 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -95,13 +95,21 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { private boolean specialKeyDown = false; public void onLogin() { - // Continue playing animation for self when re-logging + // Handle animation state from previous session AnimationData animationData = this.animationData; if (animationData != null && animationData.isClientAnimating()) { - Animation currentAnimation = animationData.currentClientAnimation; - NBTTagCompound compound = currentAnimation.writeToNBT(); - animationData.viewAnimation(currentAnimation, animationData, compound, - animationData.isClientAnimating(), currentAnimation.currentFrame, currentAnimation.currentFrameTime); + if (abilityData.isPlayingAbilityAnimation()) { + // Ability animation was playing when player disconnected. + // Ability state is transient (lost on disconnect), so this animation + // is orphaned - clear it instead of replaying a stuck animation. + abilityData.clearOrphanedAbilityAnimation(); + } else { + // Non-ability animation (e.g. script-driven) - continue playing + Animation currentAnimation = animationData.currentClientAnimation; + NBTTagCompound compound = currentAnimation.writeToNBT(); + animationData.viewAnimation(currentAnimation, animationData, compound, + animationData.isClientAnimating(), currentAnimation.currentFrame, currentAnimation.currentFrameTime); + } } CustomEffectController controller = CustomEffectController.getInstance(); From 1fb52a4ae90ca035fdb7fdc3641999c823b62e67 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 22:04:47 -0500 Subject: [PATCH 12/20] Refactor movement abilities: extract shared logic into AbilityMovement class for Charge and Dash abilities --- .../data/ability/type/AbilityCharge.java | 138 ++------- .../data/ability/type/AbilityDash.java | 111 ++----- .../data/ability/type/AbilityMovement.java | 270 ++++++++++++++++++ 3 files changed, 316 insertions(+), 203 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 871249885..780ddc050 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -2,7 +2,6 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; @@ -12,8 +11,6 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.Vec3; -import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.client.gui.builder.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; @@ -25,9 +22,10 @@ import java.util.List; import java.util.Set; /** - * Charge ability: Rush attack where NPC charges in a line, damaging all targets hit. + * Charge ability: Rush attack where the caster charges in a line, damaging all targets hit. + * Extends AbilityMovement for shared direction locking, stall detection, and velocity application. */ -public class AbilityCharge extends Ability implements IAbilityCharge { +public class AbilityCharge extends AbilityMovement implements IAbilityCharge { // Type-specific parameters private float chargeSpeed = 0.8f; @@ -35,13 +33,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge { private float knockback = 3.0f; private float hitWidth = 1.5f; - // Runtime state (transient) - private transient double startX, startY, startZ; - private transient double prevTickX, prevTickZ; - private transient Vec3 chargeDirection; + // Type-specific runtime state private transient Set hitEntities = new HashSet<>(); - private transient float lockedYaw; - private transient int maxActiveTicks; public AbilityCharge() { this.typeId = "ability.cnpc.charge"; @@ -71,138 +64,68 @@ public class AbilityCharge extends Ability implements IAbilityCharge { return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } - @Override - public boolean hasAbilityMovement() { - return true; // This ability moves the NPC - } - /** * Called on the first tick of windup - lock direction here so it matches telegraph. */ @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, int tick) { if (tick == 1) { - // Lock direction on first windup tick (same time telegraph is created) - lockChargeDirection(caster, target); + lockDirection(caster, target); } - // Keep caster facing the locked direction during windup enforceLockedRotation(caster); } - /** - * Locks the charge direction. Called once at windup start — direction won't change even if target moves. - * NPC: charges toward aggro target. - * Player: charges in look direction. - */ - private void lockChargeDirection(EntityLivingBase caster, EntityLivingBase target) { - if (!isPlayerCaster(caster) && target != null) { - // NPC: charge toward aggro target - double dx = target.posX - caster.posX; - double dz = target.posZ - caster.posZ; - double len = Math.sqrt(dx * dx + dz * dz); - if (len > 0) { - chargeDirection = Vec3.createVectorHelper(dx / len, 0, dz / len); - } else { - float yaw = (float) Math.toRadians(caster.rotationYaw); - chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); - } - } else { - // Player: charge in look direction - float yaw = (float) Math.toRadians(caster.rotationYaw); - chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); - } - lockedYaw = (float) Math.toDegrees(Math.atan2(-chargeDirection.xCoord, chargeDirection.zCoord)); - } - @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target) { - // Initialize charge - direction was already locked during windup - startX = caster.posX; - startY = caster.posY; - startZ = caster.posZ; - prevTickX = caster.posX; - prevTickZ = caster.posZ; + initMovement(caster, maxRange, chargeSpeed); hitEntities.clear(); - // Safety timeout: expected ticks + buffer to prevent infinite charge - maxActiveTicks = chargeSpeed > 0 ? (int)(maxRange / chargeSpeed) + 10 : 10; - // If direction wasn't set during windup (shouldn't happen), set it now - if (chargeDirection == null) { - lockChargeDirection(caster, target); + if (movementDirection == null) { + lockDirection(caster, target); } enforceLockedRotation(caster); } - private void enforceLockedRotation(EntityLivingBase caster) { - caster.rotationYaw = lockedYaw; - caster.rotationYawHead = lockedYaw; - caster.prevRotationYaw = lockedYaw; - caster.prevRotationYawHead = lockedYaw; - caster.renderYawOffset = lockedYaw; - caster.prevRenderYawOffset = lockedYaw; - } - @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { - // Safety timeout or missing state: force-complete to prevent stuck NPC - if (!isPreview() && (chargeDirection == null || tick > maxActiveTicks)) { + if (checkTimeout(tick)) { stopMomentum(caster); signalCompletion(); return; } - if (chargeDirection == null) { + if (movementDirection == null) { signalCompletion(); return; } - // Enforce rotation every tick if (!isPreview()) { enforceLockedRotation(caster); } - // Stall detection: if entity hasn't moved since last tick, it's stuck against a wall - if (!isPreview() && tick > 1) { - double dx = caster.posX - prevTickX; - double dz = caster.posZ - prevTickZ; - if (dx * dx + dz * dz < 0.0001) { - stopMomentum(caster); - signalCompletion(); - return; - } + if (checkStall(caster, tick)) { + stopMomentum(caster); + signalCompletion(); + return; } - prevTickX = caster.posX; - prevTickZ = caster.posZ; + updatePrevPosition(caster); - // Calculate distance traveled - double distanceTraveled = Math.sqrt( - Math.pow(caster.posX - startX, 2) + - Math.pow(caster.posZ - startZ, 2) - ); - - // Check if reached max distance - if (distanceTraveled >= maxRange) { + if (getDistanceTraveled(caster) >= maxRange) { stopMomentum(caster); signalCompletion(); return; } - // Block detection (skip in preview - no real world collision) - if (!isPreview() && isChargeBlocked(caster)) { + if (checkBlocked(caster, chargeSpeed)) { stopMomentum(caster); signalCompletion(); return; } - // Move caster - caster.motionX = chargeDirection.xCoord * chargeSpeed; - caster.motionY = 0; - caster.motionZ = chargeDirection.zCoord * chargeSpeed; - if (!isPreview()) { - caster.velocityChanged = true; - } + // Move caster (flat: zero vertical motion) + applyVelocityFlat(caster, chargeSpeed); // Server-side collision damage (skip in preview) if (!caster.worldObj.isRemote && !isPreview()) { @@ -218,14 +141,11 @@ public class AbilityCharge extends Ability implements IAbilityCharge { EntityLivingBase livingEntity = (EntityLivingBase) entity; - // Hit this entity hitEntities.add(entity.getEntityId()); - // Apply damage with scripted event support boolean wasHit = applyAbilityDamageWithDirection(caster, livingEntity, damage, knockback, - chargeDirection.xCoord, chargeDirection.zCoord); + movementDirection.xCoord, movementDirection.zCoord); - // Apply effects and play impact sound if hit wasn't cancelled if (wasHit) { applyEffects(livingEntity); caster.worldObj.playSoundAtEntity(livingEntity, "random.explode", 0.5f, 1.2f); @@ -246,24 +166,10 @@ public class AbilityCharge extends Ability implements IAbilityCharge { super.onInterrupt(caster, source, damage); } - private void stopMomentum(EntityLivingBase caster) { - caster.motionX = 0; - caster.motionZ = 0; - if (!isPreview()) { - caster.velocityChanged = true; - } - } - - private boolean isChargeBlocked(EntityLivingBase caster) { - return isMovementBlocked(caster, chargeDirection.xCoord, chargeDirection.zCoord, chargeSpeed); - } - @Override - public void reset() { - super.reset(); - chargeDirection = null; + public void cleanup() { + super.cleanup(); hitEntities.clear(); - maxActiveTicks = 0; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 8dbd13c92..41c5faaa1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -2,16 +2,12 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; -import net.minecraft.util.Vec3; -import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.api.ability.type.IAbilityDash; @@ -22,9 +18,9 @@ import java.util.Random; /** * Dash ability: Quick evasive sidestep with NO telegraph. - * Defensive repositioning move to evade attacks. + * Extends AbilityMovement for shared direction locking, stall detection, and velocity application. */ -public class AbilityDash extends Ability implements IAbilityDash { +public class AbilityDash extends AbilityMovement implements IAbilityDash { /** * Dash behavior mode. @@ -88,12 +84,8 @@ public class AbilityDash extends Ability implements IAbilityDash { private float dashDistance = 4.0f; private float dashSpeed = 0.5f; - // Runtime state - private transient Vec3 dashDirection; - private transient double startX, startY, startZ; - private transient double prevTickX, prevTickZ; + // Type-specific runtime state private transient DashDirection chosenDirection; - private transient int maxActiveTicks; public AbilityDash() { this.typeId = "ability.cnpc.dash"; @@ -126,21 +118,9 @@ public class AbilityDash extends Ability implements IAbilityDash { return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } - @Override - public boolean hasAbilityMovement() { - return true; // This ability moves the NPC - } - @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target) { - startX = caster.posX; - startY = caster.posY; - startZ = caster.posZ; - prevTickX = caster.posX; - prevTickZ = caster.posZ; - - // Safety timeout: expected ticks + generous buffer to prevent infinite dash - maxActiveTicks = dashSpeed > 0 ? (int)(dashDistance / dashSpeed) + 10 : 10; + initMovement(caster, dashDistance, dashSpeed); // Choose random direction based on mode DashDirection[] directions = dashMode == DashMode.AGGRESSIVE @@ -148,26 +128,10 @@ public class AbilityDash extends Ability implements IAbilityDash { : DEFENSIVE_DIRECTIONS; chosenDirection = directions[RANDOM.nextInt(directions.length)]; - // NPC: dash direction is relative to aggro target facing - // Player: dash direction is relative to look direction - float baseYaw; - if (!isPlayerCaster(caster) && target != null) { - double dx = target.posX - caster.posX; - double dz = target.posZ - caster.posZ; - baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - } else { - baseYaw = caster.rotationYaw; - } - - // Apply direction offset + // Calculate direction: base yaw (toward target or look dir) + direction offset + float baseYaw = getBaseYaw(caster, target); float dashYaw = baseYaw + chosenDirection.getAngleOffset(); - float yawRad = (float) Math.toRadians(dashYaw); - - dashDirection = Vec3.createVectorHelper( - -Math.sin(yawRad), - 0, - Math.cos(yawRad) - ); + setDirectionFromYaw(dashYaw); // Small upward impulse for a skip/hop arc (gravity handles the descent) caster.motionY = 0.2; @@ -175,86 +139,59 @@ public class AbilityDash extends Ability implements IAbilityDash { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { - // Safety timeout or missing state: force-complete to prevent stuck NPC - if (!isPreview() && (dashDirection == null || tick > maxActiveTicks)) { - stopDash(caster); + if (checkTimeout(tick)) { + stopMomentum(caster); signalCompletion(); return; } - if (dashDirection == null) { + if (movementDirection == null) { signalCompletion(); return; } - // Stall detection: if entity hasn't moved since last tick, it's stuck against a wall - if (!isPreview() && tick > 1) { - double dx = caster.posX - prevTickX; - double dz = caster.posZ - prevTickZ; - if (dx * dx + dz * dz < 0.0001) { - stopDash(caster); - signalCompletion(); - return; - } + if (checkStall(caster, tick)) { + stopMomentum(caster); + signalCompletion(); + return; } - prevTickX = caster.posX; - prevTickZ = caster.posZ; + updatePrevPosition(caster); - // Check if reached max distance - double travelDx = caster.posX - startX; - double travelDz = caster.posZ - startZ; - if (travelDx * travelDx + travelDz * travelDz >= (double) dashDistance * dashDistance) { - stopDash(caster); + if (getDistanceTraveledSq(caster) >= (double) dashDistance * dashDistance) { + stopMomentum(caster); signalCompletion(); return; } - // Block detection (skip in preview - no real world collision) - if (!isPreview() && isDashBlocked(caster)) { - stopDash(caster); + if (checkBlocked(caster, dashSpeed)) { + stopMomentum(caster); signalCompletion(); return; } // Move caster (motionY left to gravity for skip arc) - caster.motionX = dashDirection.xCoord * dashSpeed; - caster.motionZ = dashDirection.zCoord * dashSpeed; - if (!isPreview()) { - caster.velocityChanged = true; + applyVelocity(caster, dashSpeed); + if (!isPreview()) { // Trail particles caster.worldObj.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0); } } - private void stopDash(EntityLivingBase caster) { - caster.motionX = 0; - caster.motionZ = 0; - if (!isPreview()) { - caster.velocityChanged = true; - } - } - @Override public void onComplete(EntityLivingBase caster, EntityLivingBase target) { - stopDash(caster); + stopMomentum(caster); } @Override public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { - stopDash(caster); + stopMomentum(caster); } @Override public void cleanup() { - dashDirection = null; + super.cleanup(); chosenDirection = null; - maxActiveTicks = 0; - } - - private boolean isDashBlocked(EntityLivingBase caster) { - if (dashDirection == null) return true; - return isMovementBlocked(caster, dashDirection.xCoord, dashDirection.zCoord, dashSpeed); } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java new file mode 100644 index 000000000..de2090cc3 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java @@ -0,0 +1,270 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import kamkeel.npcs.controllers.data.ability.Ability; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.util.Vec3; + +/** + * Abstract base class for abilities that move the caster along the ground + * in a specific direction (Charge, Dash, etc.). + *

+ * Provides shared movement logic: direction locking, stall detection with + * player-aware grace period, velocity application, distance tracking, + * rotation enforcement, and wall collision checking. + *

+ * NOT for ballistic/arc movement (Slam uses a different pattern). + */ +public abstract class AbilityMovement extends Ability { + + /** + * Grace period (in active ticks) before stall detection activates. + * Players need more time because: + * - Server sets motionX/Z and sends S12 velocity packet to client + * - Client receives S12 and starts moving + * - Client sends C03 position packet with updated position back to server + * Until the round trip completes, the server sees the old (stationary) position, + * which would falsely trigger stall detection. + */ + private static final int PLAYER_STALL_GRACE_TICKS = 5; + private static final int NPC_STALL_GRACE_TICKS = 2; + + // ═══════════════════════════════════════════════════════════════════ + // COMMON MOVEMENT STATE (transient, not saved to NBT) + // ═══════════════════════════════════════════════════════════════════ + + protected transient double startX, startY, startZ; + protected transient double prevTickX, prevTickZ; + protected transient Vec3 movementDirection; + protected transient float lockedYaw; + protected transient int maxActiveTicks; + + // ═══════════════════════════════════════════════════════════════════ + // OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + @Override + public boolean hasAbilityMovement() { + return true; + } + + @Override + public void cleanup() { + movementDirection = null; + maxActiveTicks = 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // DIRECTION HELPERS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Lock direction based on caster type. + * NPC: toward target. Player: look direction. + */ + protected void lockDirection(EntityLivingBase caster, EntityLivingBase target) { + if (!isPlayerCaster(caster) && target != null) { + lockDirectionToTarget(caster, target); + } else { + lockDirectionFromLook(caster); + } + } + + /** + * Lock direction to the caster's look direction (horizontal only). + */ + protected void lockDirectionFromLook(EntityLivingBase caster) { + float yaw = (float) Math.toRadians(caster.rotationYaw); + movementDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); + lockedYaw = computeYawFromDirection(); + } + + /** + * Lock direction toward a target entity. + * Falls back to look direction if target is at the same position. + */ + protected void lockDirectionToTarget(EntityLivingBase caster, EntityLivingBase target) { + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; + double len = Math.sqrt(dx * dx + dz * dz); + if (len > 0) { + movementDirection = Vec3.createVectorHelper(dx / len, 0, dz / len); + lockedYaw = computeYawFromDirection(); + } else { + lockDirectionFromLook(caster); + } + } + + /** + * Get the base yaw for direction calculation. + * NPC: yaw toward target. Player: current look yaw. + */ + protected float getBaseYaw(EntityLivingBase caster, EntityLivingBase target) { + if (!isPlayerCaster(caster) && target != null) { + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; + return (float) Math.toDegrees(Math.atan2(-dx, dz)); + } + return caster.rotationYaw; + } + + /** + * Set movement direction from a yaw angle (degrees). + */ + protected void setDirectionFromYaw(float yawDegrees) { + float yawRad = (float) Math.toRadians(yawDegrees); + movementDirection = Vec3.createVectorHelper(-Math.sin(yawRad), 0, Math.cos(yawRad)); + lockedYaw = yawDegrees; + } + + // ═══════════════════════════════════════════════════════════════════ + // MOVEMENT INITIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Initialize movement tracking. Call from onExecute(). + * + * @param caster The entity performing the movement + * @param distance Maximum travel distance (for timeout computation) + * @param speed Movement speed in blocks/tick (for timeout computation) + */ + protected void initMovement(EntityLivingBase caster, float distance, float speed) { + startX = caster.posX; + startY = caster.posY; + startZ = caster.posZ; + prevTickX = caster.posX; + prevTickZ = caster.posZ; + maxActiveTicks = speed > 0 ? (int) (distance / speed) + 10 : 10; + } + + // ═══════════════════════════════════════════════════════════════════ + // ACTIVE TICK CHECKS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Check for timeout or missing movement direction. + * + * @return true if the ability should stop + */ + protected boolean checkTimeout(int tick) { + return !isPreview() && (movementDirection == null || tick > maxActiveTicks); + } + + /** + * Check stall detection with appropriate grace period for player vs NPC. + * Players need a longer grace period because velocity sync via S12 packet + * takes time to reach the client, and C03 position packets lag behind. + * + * @return true if the entity is stalled (not moving) + */ + protected boolean checkStall(EntityLivingBase caster, int tick) { + if (isPreview()) return false; + int graceTicks = isPlayerCaster(caster) ? PLAYER_STALL_GRACE_TICKS : NPC_STALL_GRACE_TICKS; + if (tick <= graceTicks) return false; + + double dx = caster.posX - prevTickX; + double dz = caster.posZ - prevTickZ; + return dx * dx + dz * dz < 0.0001; + } + + /** + * Update previous tick position for stall detection. + * Call after stall check, before movement application. + */ + protected void updatePrevPosition(EntityLivingBase caster) { + prevTickX = caster.posX; + prevTickZ = caster.posZ; + } + + /** + * Get total linear distance traveled from start position. + */ + protected double getDistanceTraveled(EntityLivingBase caster) { + double dx = caster.posX - startX; + double dz = caster.posZ - startZ; + return Math.sqrt(dx * dx + dz * dz); + } + + /** + * Get total distance traveled squared (more efficient for comparison). + */ + protected double getDistanceTraveledSq(EntityLivingBase caster) { + double dx = caster.posX - startX; + double dz = caster.posZ - startZ; + return dx * dx + dz * dz; + } + + /** + * Check if movement direction is blocked by walls. + * + * @return true if blocked + */ + protected boolean checkBlocked(EntityLivingBase caster, float speed) { + if (isPreview() || movementDirection == null) return false; + return isMovementBlocked(caster, movementDirection.xCoord, movementDirection.zCoord, speed); + } + + // ═══════════════════════════════════════════════════════════════════ + // VELOCITY APPLICATION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Apply movement velocity to the caster (horizontal only, Y unchanged). + */ + protected void applyVelocity(EntityLivingBase caster, float speed) { + caster.motionX = movementDirection.xCoord * speed; + caster.motionZ = movementDirection.zCoord * speed; + if (!isPreview()) { + caster.velocityChanged = true; + } + } + + /** + * Apply movement velocity with zero vertical motion. + * Used for ground-locked movement like Charge. + */ + protected void applyVelocityFlat(EntityLivingBase caster, float speed) { + caster.motionX = movementDirection.xCoord * speed; + caster.motionY = 0; + caster.motionZ = movementDirection.zCoord * speed; + if (!isPreview()) { + caster.velocityChanged = true; + } + } + + /** + * Stop all horizontal momentum. + */ + protected void stopMomentum(EntityLivingBase caster) { + caster.motionX = 0; + caster.motionZ = 0; + if (!isPreview()) { + caster.velocityChanged = true; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // ROTATION + // ═══════════════════════════════════════════════════════════════════ + + /** + * Compute yaw angle (degrees) from current movement direction. + */ + protected float computeYawFromDirection() { + if (movementDirection == null) return 0; + return (float) Math.toDegrees(Math.atan2(-movementDirection.xCoord, movementDirection.zCoord)); + } + + /** + * Enforce locked rotation on all yaw fields. + * Sets rotationYaw, rotationYawHead, prevRotationYaw, prevRotationYawHead, + * renderYawOffset, and prevRenderYawOffset to lockedYaw. + */ + protected void enforceLockedRotation(EntityLivingBase caster) { + caster.rotationYaw = lockedYaw; + caster.rotationYawHead = lockedYaw; + caster.prevRotationYaw = lockedYaw; + caster.prevRotationYawHead = lockedYaw; + caster.renderYawOffset = lockedYaw; + caster.prevRenderYawOffset = lockedYaw; + } +} From 121563ba246c784c63702bc96bcd4fbef9467ba1 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 23:07:57 -0500 Subject: [PATCH 13/20] Enhance knockback mechanics: add methods for applying and forcefully applying knockback, respecting or ignoring knockback resistance --- src/api | 2 +- .../scripted/entity/ScriptLivingBase.java | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/api b/src/api index 2b95d3ca4..66fff09d2 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 2b95d3ca47558e804a09e4f7b7f45aeb69fa7861 +Subproject commit 66fff09d2078a9d85a6ef27ef98c02a5944eb325 diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptLivingBase.java b/src/main/java/noppes/npcs/scripted/entity/ScriptLivingBase.java index 15459194e..50f1914a7 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptLivingBase.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptLivingBase.java @@ -6,6 +6,7 @@ import net.minecraft.potion.Potion; import net.minecraft.potion.PotionEffect; import net.minecraft.util.DamageSource; import net.minecraft.util.EntityDamageSource; +import net.minecraft.util.MathHelper; import net.minecraft.util.Vec3; import noppes.npcs.api.IBlock; import noppes.npcs.api.IDamageSource; @@ -380,4 +381,53 @@ public class ScriptLivingBase extends ScriptEntity 0.4D) { + entity.motionY = 0.4D; + } + + entity.velocityChanged = true; + } } From 1ed012331bb4a8c215148f1991cb5cd879eea216 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 15 Feb 2026 23:15:50 -0500 Subject: [PATCH 14/20] Enhance ability input handling: block movement during ability locks, ensure proper input suppression, and cancel abilities on logout, dimension change, death, and respawn --- .../noppes/npcs/ScriptPlayerEventHandler.java | 22 +++++ .../npcs/client/ClientEventHandler.java | 13 ++- .../noppes/npcs/client/ClientTickHandler.java | 85 +++++++++++++++++-- .../controllers/data/PlayerAbilityData.java | 1 + .../npcs/controllers/data/PlayerData.java | 2 + 5 files changed, 116 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 909634a09..8c9bde6ac 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -106,6 +106,7 @@ public class ScriptPlayerEventHandler { Ability current = playerData.abilityData.getCurrentAbility(); if (current == null || !current.hasAbilityMovement()) { player.motionX = 0; + player.motionY = Math.min(player.motionY, 0); // Prevent jumping, allow gravity player.motionZ = 0; player.velocityChanged = true; } @@ -328,6 +329,12 @@ public class ScriptPlayerEventHandler { return; if (event.player.worldObj instanceof WorldServer && event.player instanceof EntityPlayerMP) { + // Cancel any executing ability on dimension change + PlayerData playerData = PlayerData.get(event.player); + if (playerData != null) { + playerData.abilityData.interruptCurrentAbility(); + } + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(event.player); IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(event.player); EventHooks.onPlayerChangeDim(handler, scriptPlayer, event.fromDim, event.toDim); @@ -478,6 +485,13 @@ public class ScriptPlayerEventHandler { if (event.entityLiving.worldObj instanceof WorldServer) { if (event.entityLiving instanceof EntityPlayerMP) { + // Cancel jump if movement is locked by an ability + PlayerData playerData = PlayerData.get((EntityPlayer) event.entityLiving); + if (playerData != null && playerData.abilityData.isMovementLocked()) { + event.entityLiving.motionY = 0; + event.entityLiving.velocityChanged = true; + } + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.entityLiving); IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(event.entityLiving); EventHooks.onPlayerJump(handler, scriptPlayer); @@ -532,6 +546,8 @@ public class ScriptPlayerEventHandler { EntityPlayer player = (EntityPlayer) event.entityLiving; PlayerData playerData = PlayerData.get(player); + // Cancel any executing ability on death + playerData.abilityData.interruptCurrentAbility(); if (ConfigScript.ClearActionsOnDeath) playerData.actionManager.clear(); } @@ -631,6 +647,12 @@ public class ScriptPlayerEventHandler { return; if (event.player.worldObj instanceof WorldServer && event.player instanceof EntityPlayerMP) { + // Cancel any executing ability on respawn + PlayerData playerData = PlayerData.get(event.player); + if (playerData != null) { + playerData.abilityData.interruptCurrentAbility(); + } + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(event.player); IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(event.player); EventHooks.onPlayerRespawn(handler, scriptPlayer); diff --git a/src/main/java/noppes/npcs/client/ClientEventHandler.java b/src/main/java/noppes/npcs/client/ClientEventHandler.java index a7a0f2b39..6fc690b5a 100644 --- a/src/main/java/noppes/npcs/client/ClientEventHandler.java +++ b/src/main/java/noppes/npcs/client/ClientEventHandler.java @@ -84,9 +84,20 @@ public class ClientEventHandler { @SubscribeEvent public void onMouse(MouseEvent event) { - if (Minecraft.getMinecraft().currentScreen != null) + Minecraft mc = Minecraft.getMinecraft(); + if (mc.currentScreen != null) return; + // Block mouse movement during ability rotation lock. + // Allow clicks (button >= 0) and scroll (dwheel != 0) to pass through. + if (mc.theWorld != null && mc.thePlayer != null + && ClientAbilityState.shouldLockRotation() + && (event.dx != 0 || event.dy != 0) + && event.button == -1 && event.dwheel == 0) { + event.setCanceled(true); + return; + } + ArrayList removeList = new ArrayList<>(); for (Map.Entry entry : disabledButtonTimes.entrySet()) { if (entry.getValue() > 0) { diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 7d82ca0f1..d12921756 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -13,6 +13,7 @@ import kamkeel.npcs.network.packets.player.InputDevicePacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.KeyBinding; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.ContainerPlayer; @@ -75,13 +76,27 @@ public class ClientTickHandler { lastSpecialKeyDown = specialKeyDown; } - // Suppress player input during ability-controlled phases - if (ClientAbilityState.shouldSuppressMovementInput()) { + // Suppress player input during ability-controlled phases. + // Only suppress when no GUI screen is open (screens already capture input). + if (mc.currentScreen == null && ClientAbilityState.shouldSuppressMovementInput()) { + // Unpress movement keybinds at the source BEFORE updatePlayerMoveState() reads them. + // This is more robust than just zeroing movementInput fields, because + // updatePlayerMoveState() reads directly from keybind pressed state. + KeyBinding.setKeyBindState(mc.gameSettings.keyBindForward.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindBack.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindLeft.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindRight.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindJump.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindSneak.getKeyCode(), false); + KeyBinding.setKeyBindState(mc.gameSettings.keyBindSprint.getKeyCode(), false); + + // Also zero the movementInput values directly as a safety net mc.thePlayer.movementInput.moveForward = 0; mc.thePlayer.movementInput.moveStrafe = 0; mc.thePlayer.movementInput.jump = false; + mc.thePlayer.movementInput.sneak = false; } - if (ClientAbilityState.shouldLockRotation()) { + if (mc.currentScreen == null && ClientAbilityState.shouldLockRotation()) { mc.thePlayer.rotationYaw = ClientAbilityState.lockedYaw; mc.thePlayer.rotationPitch = ClientAbilityState.lockedPitch; mc.thePlayer.prevRotationYaw = ClientAbilityState.lockedYaw; @@ -100,8 +115,42 @@ public class ClientTickHandler { } } if (event.phase == Phase.END) { - if (mc.thePlayer != null && mc.theWorld != null && !mc.isGamePaused() && ClientEventHandler.hasOverlays(mc.thePlayer)) { - renderCNPCPlayer.itemRenderer.updateEquippedItem(); + if (mc.thePlayer != null) { + // Re-enforce ability locks AFTER entity update. + // EntityPlayerSP.onLivingUpdate() calls updatePlayerMoveState() which + // overwrites Phase.START suppression with actual keyboard state, causing + // client-predicted movement that fights the server lock. Undo it here. + // Skip when a GUI screen is open (screens already capture all input). + if (mc.currentScreen == null && ClientAbilityState.shouldSuppressMovementInput()) { + mc.thePlayer.movementInput.moveForward = 0; + mc.thePlayer.movementInput.moveStrafe = 0; + mc.thePlayer.movementInput.jump = false; + mc.thePlayer.movementInput.sneak = false; + + // Zero horizontal motion (unless ability provides its own movement) + if (!ClientAbilityState.hasAbilityMovement) { + mc.thePlayer.motionX = 0; + mc.thePlayer.motionZ = 0; + } + + // Prevent jumping: clamp upward motion when locked (allow gravity) + if ((ClientAbilityState.movementLocked || ClientAbilityState.positionLocked) + && !ClientAbilityState.hasAbilityMovement) { + mc.thePlayer.motionY = Math.min(mc.thePlayer.motionY, 0); + } + } + + if (mc.currentScreen == null && ClientAbilityState.shouldLockRotation()) { + mc.thePlayer.rotationYaw = ClientAbilityState.lockedYaw; + mc.thePlayer.rotationPitch = ClientAbilityState.lockedPitch; + mc.thePlayer.prevRotationYaw = ClientAbilityState.lockedYaw; + mc.thePlayer.prevRotationPitch = ClientAbilityState.lockedPitch; + mc.thePlayer.rotationYawHead = ClientAbilityState.lockedYaw; + } + + if (mc.theWorld != null && !mc.isGamePaused() && ClientEventHandler.hasOverlays(mc.thePlayer)) { + renderCNPCPlayer.itemRenderer.updateEquippedItem(); + } } return; } @@ -159,8 +208,9 @@ public class ClientTickHandler { @SubscribeEvent public void onKey(InputEvent.KeyInputEvent event) { + Minecraft mc = Minecraft.getMinecraft(); + if (ClientProxy.NPCButton.isPressed()) { - Minecraft mc = Minecraft.getMinecraft(); if (mc.currentScreen == null) { InventoryTabCustomNpc.tabHelper(); } else if (mc.currentScreen instanceof GuiCNPCInventory) @@ -171,10 +221,33 @@ public class ClientTickHandler { int key = Keyboard.getEventKey(); boolean keyDown = Keyboard.isKeyDown(key); + // Block movement keys during ability lock (only when no GUI is open and world exists). + // Non-movement keys (ESC, chat, inventory, etc.) always pass through. + if (mc.theWorld != null && mc.currentScreen == null && keyDown + && ClientAbilityState.shouldSuppressMovementInput() && isMovementKey(key)) { + // Unpress the keybind so vanilla doesn't process it + KeyBinding.setKeyBindState(key, false); + return; + } + InputDevicePacket.sendKeyboard(key, keyDown); } } + /** + * Check if a key code corresponds to a movement keybind (WASD, jump, sneak, sprint). + */ + private boolean isMovementKey(int keyCode) { + Minecraft mc = Minecraft.getMinecraft(); + return keyCode == mc.gameSettings.keyBindForward.getKeyCode() + || keyCode == mc.gameSettings.keyBindBack.getKeyCode() + || keyCode == mc.gameSettings.keyBindLeft.getKeyCode() + || keyCode == mc.gameSettings.keyBindRight.getKeyCode() + || keyCode == mc.gameSettings.keyBindJump.getKeyCode() + || keyCode == mc.gameSettings.keyBindSneak.getKeyCode() + || keyCode == mc.gameSettings.keyBindSprint.getKeyCode(); + } + private boolean isIgnoredKey(int key) { int[] var2 = this.ignoreKeys; int var3 = var2.length; diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 22ec80c4b..6ced6f68a 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -699,6 +699,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { player.prevPosY = lockedPosY; player.prevPosZ = lockedPosZ; player.motionX = 0; + player.motionY = 0; // Full vertical freeze for position lock player.motionZ = 0; } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 02fd67563..7f326a344 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -124,6 +124,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { } public void onLogout() { + // Cancel any executing ability and release all locks + abilityData.interruptCurrentAbility(); this.partyInvites.clear(); this.actionManager.clear(); } From 071d89cc15ac5a3020d02779ed579b015a13dd2e Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 16 Feb 2026 01:03:05 -0500 Subject: [PATCH 15/20] Add energy projectile types and events: implement IEnergyOrb, IEnergyBeam, IEnergyDisc, and IEnergyLaser interfaces, along with their respective script handlers and event hooks --- .../npcs/entity/EntityAbilityBeam.java | 16 ++ .../npcs/entity/EntityAbilityDisc.java | 28 ++ .../npcs/entity/EntityAbilityLaser.java | 33 +++ .../kamkeel/npcs/entity/EntityAbilityOrb.java | 4 + .../npcs/entity/EntityAbilityProjectile.java | 216 ++++++++------- src/main/java/noppes/npcs/EventHooks.java | 134 ++++++++++ .../noppes/npcs/constants/EnumScriptType.java | 8 +- .../noppes/npcs/constants/ScriptContext.java | 12 +- .../controllers/ScriptHookController.java | 30 +++ .../controllers/data/PlayerAbilityData.java | 102 ++++++- .../java/noppes/npcs/scripted/NpcAPI.java | 16 ++ .../noppes/npcs/scripted/ScriptWorld.java | 50 ++++ .../npcs/scripted/constants/EntityType.java | 4 + .../scripted/entity/ScriptEnergyBeam.java | 69 +++++ .../scripted/entity/ScriptEnergyDisc.java | 75 ++++++ .../scripted/entity/ScriptEnergyLaser.java | 82 ++++++ .../npcs/scripted/entity/ScriptEnergyOrb.java | 53 ++++ .../entity/ScriptEnergyProjectile.java | 252 ++++++++++++++++++ .../npcs/scripted/entity/ScriptEntity.java | 15 ++ .../npcs/scripted/entity/ScriptNpc.java | 11 + .../npcs/scripted/entity/ScriptPlayer.java | 11 + .../scripted/event/EnergyProjectileEvent.java | 132 +++++++++ .../event/player/PlayerAbilityEvent.java | 104 ++++++++ 23 files changed, 1363 insertions(+), 94 deletions(-) create mode 100644 src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java create mode 100644 src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java create mode 100644 src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java create mode 100644 src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java create mode 100644 src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java create mode 100644 src/main/java/noppes/npcs/scripted/event/EnergyProjectileEvent.java diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index bfdca5346..db57660e4 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -3,6 +3,7 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; +import noppes.npcs.EventHooks; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -544,6 +545,9 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + if (!worldObj.isRemote) { + EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ); + } hasHit = true; if (isExplosive()) { posX = blockHit.hitVec.xCoord; @@ -593,10 +597,22 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { return beamWidth; } + public void setBeamWidth(float beamWidth) { + this.beamWidth = beamWidth; + } + public float getHeadSize() { return headSize; } + public void setHeadSize(float headSize) { + this.headSize = headSize; + } + + public void setAttachedToOwner(boolean attached) { + this.attachedToOwner = attached; + } + /** * Get interpolated head offset for smooth rendering. * Returns offset RELATIVE to origin. diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index c0f3cbe8d..dd499f122 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -3,6 +3,7 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; +import noppes.npcs.EventHooks; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; @@ -344,6 +345,9 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + if (!worldObj.isRemote) { + EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ); + } hasHit = true; if (isExplosive()) { posX = blockHit.hitVec.xCoord; @@ -413,14 +417,34 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return discRadius; } + public void setDiscRadius(float radius) { + this.discRadius = radius; + } + public float getDiscThickness() { return discThickness; } + public void setDiscThickness(float thickness) { + this.discThickness = thickness; + } + public boolean isBoomerang() { return boomerang; } + public void setBoomerang(boolean boomerang) { + this.boomerang = boomerang; + } + + public int getBoomerangDelay() { + return boomerangDelay; + } + + public void setBoomerangDelay(int delay) { + this.boomerangDelay = delay; + } + public boolean isReturning() { return returning; } @@ -429,6 +453,10 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return vertical; } + public void setVertical(boolean vertical) { + this.vertical = vertical; + } + /** * Get the yaw angle (degrees) from motion vector for vertical disc orientation. * During charging, uses owner's facing direction. diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index d17024680..5adabc4b6 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -3,6 +3,7 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; +import noppes.npcs.EventHooks; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; @@ -145,6 +146,9 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { endZ = startZ + dirZ * currentLength; if (ticksSinceFullExtension >= lingerTicks) { + if (!worldObj.isRemote) { + EventHooks.onEnergyProjectileExpired(this); + } this.setDead(); } } @@ -236,6 +240,9 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { MovingObjectPosition blockHit = worldObj.func_147447_a(start, end, false, true, false); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + if (!worldObj.isRemote) { + EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ); + } // Laser stops at block currentLength = (float) Math.sqrt( (blockHit.hitVec.xCoord - startX) * (blockHit.hitVec.xCoord - startX) + @@ -355,6 +362,32 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { return laserWidth; } + public void setLaserWidth(float width) { + this.laserWidth = width; + } + + public float getExpansionSpeed() { + return expansionSpeed; + } + + public void setExpansionSpeed(float speed) { + this.expansionSpeed = speed; + } + + public int getLingerTicks() { + return lingerTicks; + } + + public void setLingerTicks(int ticks) { + this.lingerTicks = ticks; + } + + public void setDirection(double x, double y, double z) { + this.dirX = x; + this.dirY = y; + this.dirZ = z; + } + public float getCurrentLength() { return currentLength; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index c85b7be40..2eb5c0084 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -3,6 +3,7 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; +import noppes.npcs.EventHooks; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; @@ -232,6 +233,9 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + if (!worldObj.isRemote) { + EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ); + } hasHit = true; if (isExplosive()) { posX = blockHit.hitVec.xCoord; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index ac6add74d..99c66edaa 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -20,13 +20,17 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; +import noppes.npcs.EventHooks; import noppes.npcs.NpcDamageSource; -import noppes.npcs.api.entity.IEnergyProjectile; import noppes.npcs.api.entity.IEntity; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; +import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; @@ -36,7 +40,38 @@ import java.util.Map; * * Design inspired by LouisXIV's energy attack system. */ -public abstract class EntityAbilityProjectile extends Entity implements IEnergyProjectile, IEntityAdditionalSpawnData { +public abstract class EntityAbilityProjectile extends Entity implements IEntityAdditionalSpawnData { + + // ==================== ACTIVE PROJECTILE TRACKING ==================== + private static final Map>> activeProjectiles = new HashMap<>(); + + public static void trackProjectile(EntityAbilityProjectile projectile) { + int ownerId = projectile.getOwnerEntityId(); + if (ownerId < 0) return; + List> refs = activeProjectiles.get(ownerId); + if (refs == null) { + refs = new ArrayList<>(); + activeProjectiles.put(ownerId, refs); + } + refs.add(new WeakReference<>(projectile)); + } + + public static List getActiveProjectiles(int ownerEntityId) { + List> refs = activeProjectiles.get(ownerEntityId); + if (refs == null) return Collections.emptyList(); + List result = new ArrayList<>(); + Iterator> it = refs.iterator(); + while (it.hasNext()) { + EntityAbilityProjectile p = it.next().get(); + if (p == null || p.isDead) { + it.remove(); + } else { + result.add(p); + } + } + if (refs.isEmpty()) activeProjectiles.remove(ownerEntityId); + return result; + } // ==================== VISUAL PROPERTIES ==================== protected float size = 1.0f; @@ -174,6 +209,11 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP // Skip super.onUpdate() in preview mode to avoid world checks if (!previewMode) { super.onUpdate(); + + // Track on first server tick + if (ticksExisted == 1 && !worldObj.isRemote) { + trackProjectile(this); + } } // Update rotation @@ -208,12 +248,18 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP // Check lifespan using world time (survives chunk unload/reload) if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { + if (!worldObj.isRemote) { + EventHooks.onEnergyProjectileExpired(this); + } this.setDead(); return; } // Check max distance (subclass can override if needed) if (checkMaxDistance()) { + if (!worldObj.isRemote) { + EventHooks.onEnergyProjectileExpired(this); + } this.setDead(); return; } @@ -226,6 +272,11 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP // Subclass-specific update updateProjectile(); + + // Fire tick event (server-side, non-preview only) + if (!previewMode && !worldObj.isRemote) { + EventHooks.onEnergyProjectileTick(this); + } } /** @@ -328,6 +379,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP protected void applyDamage(EntityLivingBase target, float dmg, float kb) { if (previewMode) return; // Skip damage in preview mode + + // Fire entity impact event (may cancel or modify damage) + if (!worldObj.isRemote) { + float result = EventHooks.onEnergyProjectileEntityImpact(this, target, dmg); + if (result < 0) return; // Event was cancelled + dmg = result; + } + Entity owner = getOwnerEntity(); // Check for ability extenders (e.g., DBC Addon damage routing) @@ -627,12 +686,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP return worldObj.getEntityByID(ownerEntityId); } - @Override public int getOwnerEntityId() { return ownerEntityId; } - @Override + public void setOwnerEntityId(int id) { + this.ownerEntityId = id; + } + public IEntity getOwner() { if (previewMode) return null; if (getOwnerEntity() == null) return null; @@ -647,17 +708,19 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP this.previewOwner = owner; } - @Override public int getTargetEntityId() { return targetEntityId; } + public void setTargetEntityId(int id) { + this.targetEntityId = id; + } + public Entity getTargetEntity() { if (targetEntityId == -1) return null; return worldObj.getEntityByID(targetEntityId); } - @Override public IEntity getTarget() { if (getTargetEntity() == null) return null; return NpcAPI.Instance().getIEntity(getTargetEntity()); @@ -686,162 +749,135 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP return false; } - // ==================== VISUAL GETTERS ==================== + // ==================== VISUAL GETTERS & SETTERS ==================== - @Override - public float getSize() { - return size; + public float getSize() { return size; } + public void setProjectileSize(float size) { + this.size = size; + this.renderCurrentSize = size; + this.prevRenderSize = size; } - @Override - public int getInnerColor() { - return displayData.getInnerColor(); + /** + * Set the start position for distance calculations. + * Used by factory methods when creating projectiles without the full constructor. + */ + public void setStartPosition(double x, double y, double z) { + this.startX = x; + this.startY = y; + this.startZ = z; } - @Override - public int getOuterColor() { - return displayData.getOuterColor(); - } + public int getInnerColor() { return displayData.getInnerColor(); } + public void setInnerColor(int color) { displayData.setInnerColor(color); } - @Override - public boolean isOuterColorEnabled() { - return displayData.isOuterColorEnabled(); - } + public int getOuterColor() { return displayData.getOuterColor(); } + public void setOuterColor(int color) { displayData.setOuterColor(color); } - @Override - public float getOuterColorWidth() { - return displayData.getOuterColorWidth(); - } + public boolean isOuterColorEnabled() { return displayData.isOuterColorEnabled(); } + public void setOuterColorEnabled(boolean enabled) { displayData.setOuterColorEnabled(enabled); } - @Override - public float getOuterColorAlpha() { - return displayData.getOuterColorAlpha(); - } + public float getOuterColorWidth() { return displayData.getOuterColorWidth(); } + public void setOuterColorWidth(float width) { displayData.setOuterColorWidth(width); } - @Override - public float getRotationSpeed() { - return displayData.getRotationSpeed(); - } + public float getOuterColorAlpha() { return displayData.getOuterColorAlpha(); } + public void setOuterColorAlpha(float alpha) { displayData.setOuterColorAlpha(alpha); } + + public float getRotationSpeed() { return displayData.getRotationSpeed(); } + public void setRotationSpeed(float speed) { displayData.setRotationSpeed(speed); } // ==================== ROTATION GETTERS ==================== - @Override public float getInterpolatedRotationX(float partialTicks) { return this.prevRotationValX + (this.rotationValX - this.prevRotationValX) * partialTicks; } - @Override public float getInterpolatedRotationY(float partialTicks) { return this.prevRotationValY + (this.rotationValY - this.prevRotationValY) * partialTicks; } - @Override public float getInterpolatedRotationZ(float partialTicks) { return this.prevRotationValZ + (this.rotationValZ - this.prevRotationValZ) * partialTicks; } - @Override public float getInterpolatedSize(float partialTicks) { return this.prevRenderSize + (this.renderCurrentSize - this.prevRenderSize) * partialTicks; } - // ==================== LIGHTNING GETTERS ==================== + // ==================== LIGHTNING GETTERS & SETTERS ==================== - @Override - public boolean hasLightningEffect() { - return lightningData.isLightningEffect(); - } + public boolean hasLightningEffect() { return lightningData.isLightningEffect(); } + public void setLightningEffect(boolean enabled) { lightningData.setLightningEffect(enabled); } - @Override - public float getLightningDensity() { - return lightningData.getLightningDensity(); - } + public float getLightningDensity() { return lightningData.getLightningDensity(); } + public void setLightningDensity(float density) { lightningData.setLightningDensity(density); } - @Override - public float getLightningRadius() { - return lightningData.getLightningRadius(); - } + public float getLightningRadius() { return lightningData.getLightningRadius(); } + public void setLightningRadius(float radius) { lightningData.setLightningRadius(radius); } - @Override - public int getLightningFadeTime() { - return lightningData.getLightningFadeTime(); - } + public int getLightningFadeTime() { return lightningData.getLightningFadeTime(); } + public void setLightningFadeTime(int ticks) { lightningData.setLightningFadeTime(ticks); } - // ==================== LIFESPAN GETTERS ==================== + // ==================== LIFESPAN GETTERS & SETTERS ==================== - @Override public float getMaxDistance() { return lifespanData.getMaxDistance(); } + public void setMaxDistance(float distance) { lifespanData.setMaxDistance(distance); } - @Override public int getMaxLifetime() { return lifespanData.getMaxLifetime(); } + public void setMaxLifetime(int ticks) { lifespanData.setMaxLifetime(ticks); } - // ==================== COMBAT GETTERS ==================== + // ==================== COMBAT GETTERS & SETTERS ==================== - @Override public float getDamage() { return combatData.getDamage(); } + public void setCombatDamage(float damage) { combatData.setDamage(damage); } - @Override public float getKnockback() { return combatData.knockback; } + public void setCombatKnockback(float knockback) { combatData.setKnockback(knockback); } - @Override public float getKnockbackUp() { return combatData.knockbackUp; } + public void setCombatKnockbackUp(float knockbackUp) { combatData.setKnockbackUp(knockbackUp); } - @Override public boolean isExplosive() { return combatData.isExplosive(); } + public void setExplosive(boolean explosive) { combatData.setExplosive(explosive); } - @Override public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float radius) { combatData.setExplosionRadius(radius); } - @Override public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float falloff) { combatData.setExplosionDamageFalloff(falloff); } - // ==================== HOMING GETTERS ==================== + // ==================== HOMING GETTERS & SETTERS ==================== - @Override public boolean isHoming() { return homingData.isHoming(); } + public void setHomingEnabled(boolean homing) { homingData.setHoming(homing); } - @Override public float getHomingStrength() { return homingData.getHomingStrength(); } + public void setHomingStrength(float strength) { homingData.setHomingStrength(strength); } - @Override public float getHomingRange() { return homingData.getHomingRange(); } + public void setHomingRange(float range) { homingData.setHomingRange(range); } - // ==================== TRAJECTORY GETTERS ==================== + // ==================== SPEED & ANCHOR GETTERS & SETTERS ==================== - - - // ==================== ANCHOR GETTERS ==================== - - @Override public float getSpeed() { return homingData.getSpeed(); } + public void setSpeed(float speed) { homingData.setSpeed(speed); } public AnchorPoint getAnchorPoint() { return anchorData.getAnchorPoint(); } - @Override public int getAnchor() { return anchorData.getAnchor(); } - - @Override public float getAnchorOffsetX() { return anchorData.getAnchorOffsetX(); } - - @Override public float getAnchorOffsetY() { return anchorData.getAnchorOffsetY(); } - - @Override public float getAnchorOffsetZ() { return anchorData.getAnchorOffsetZ(); } // ==================== MOVEMENT GETTERS ==================== - @Override public double getStartX() { return startX; } + public double getStartX() { return startX; } + public double getStartY() { return startY; } + public double getStartZ() { return startZ; } - @Override public double getStartY() { return startY; } + // ==================== STATE GETTERS ==================== - @Override public double getStartZ() { return startZ; } - - @Override public double getMotionX() { return motionX; } - - @Override public double getMotionY() { return motionY; } - - @Override public double getMotionZ() { return motionZ; } + public boolean getHasHit() { return hasHit; } // ==================== BRIGHTNESS ==================== diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 5f9d1388f..75b615671 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -1,6 +1,7 @@ package noppes.npcs; import cpw.mods.fml.common.eventhandler.Event; +import kamkeel.npcs.entity.EntityAbilityProjectile; import kamkeel.npcs.network.packets.data.AchievementPacket; import net.minecraft.command.ICommandSender; import net.minecraft.entity.Entity; @@ -16,7 +17,10 @@ import net.minecraft.world.World; import net.minecraft.world.WorldServer; import net.minecraftforge.event.entity.EntityEvent; import net.minecraftforge.event.world.WorldEvent; +import noppes.npcs.api.IBlock; +import noppes.npcs.api.IPos; import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEnergyProjectile; import noppes.npcs.api.entity.IAnimatable; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEntity; @@ -52,6 +56,7 @@ import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AnimationEvent; import noppes.npcs.scripted.event.BlockEvent; import noppes.npcs.scripted.event.CustomNPCsEvent; +import noppes.npcs.scripted.event.EnergyProjectileEvent; import noppes.npcs.scripted.event.ForgeEvent; import noppes.npcs.scripted.event.ItemEvent; import noppes.npcs.scripted.event.LinkedItemEvent; @@ -448,6 +453,110 @@ public class EventHooks { NpcAPI.EVENT_BUS.post(event); } + // ==================== ENERGY PROJECTILE EVENTS ==================== + + public static void onEnergyProjectileFired(EntityAbilityProjectile projectile) { + IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile); + EnergyProjectileEvent.FiredEvent event = new EnergyProjectileEvent.FiredEvent(wrapped); + ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_FIRED, event); + + Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId()); + if (owner instanceof EntityNPCInterface) { + ((EntityNPCInterface) owner).script.callScript(EnumScriptType.ENERGY_PROJECTILE_FIRED, event); + } else if (owner instanceof EntityPlayer) { + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner); + if (handler != null) { + handler.callScript(EnumScriptType.ENERGY_PROJECTILE_FIRED, event); + } + } + + NpcAPI.EVENT_BUS.post(event); + } + + public static void onEnergyProjectileTick(EntityAbilityProjectile projectile) { + IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile); + EnergyProjectileEvent.UpdateEvent event = new EnergyProjectileEvent.UpdateEvent(wrapped); + ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_TICK, event); + + Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId()); + if (owner instanceof EntityNPCInterface) { + ((EntityNPCInterface) owner).script.callScript(EnumScriptType.ENERGY_PROJECTILE_TICK, event); + } else if (owner instanceof EntityPlayer) { + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner); + if (handler != null) { + handler.callScript(EnumScriptType.ENERGY_PROJECTILE_TICK, event); + } + } + + NpcAPI.EVENT_BUS.post(event); + } + + /** + * Fires the entity impact event. Returns the (possibly modified) damage, or -1 if cancelled. + */ + public static float onEnergyProjectileEntityImpact(EntityAbilityProjectile projectile, EntityLivingBase target, float damage) { + IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile); + IEntity wrappedTarget = NpcAPI.Instance().getIEntity(target); + EnergyProjectileEvent.EntityImpactEvent event = new EnergyProjectileEvent.EntityImpactEvent(wrapped, wrappedTarget, damage); + ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_ENTITY_IMPACT, event); + + Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId()); + if (owner instanceof EntityNPCInterface) { + ((EntityNPCInterface) owner).script.callScript(EnumScriptType.ENERGY_PROJECTILE_ENTITY_IMPACT, event); + } else if (owner instanceof EntityPlayer) { + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner); + if (handler != null) { + handler.callScript(EnumScriptType.ENERGY_PROJECTILE_ENTITY_IMPACT, event); + } + } + + NpcAPI.EVENT_BUS.post(event); + + if (event.isCanceled()) { + return -1; + } + return event.getDamage(); + } + + public static void onEnergyProjectileBlockImpact(EntityAbilityProjectile projectile, int blockX, int blockY, int blockZ) { + IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile); + IPos pos = NpcAPI.Instance().getIPos(blockX, blockY, blockZ); + IWorld world = NpcAPI.Instance().getIWorld(projectile.worldObj); + IBlock block = NpcAPI.Instance().getIBlock(world, pos); + EnergyProjectileEvent.BlockImpactEvent event = new EnergyProjectileEvent.BlockImpactEvent(wrapped, block); + ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_BLOCK_IMPACT, event); + + Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId()); + if (owner instanceof EntityNPCInterface) { + ((EntityNPCInterface) owner).script.callScript(EnumScriptType.ENERGY_PROJECTILE_BLOCK_IMPACT, event); + } else if (owner instanceof EntityPlayer) { + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner); + if (handler != null) { + handler.callScript(EnumScriptType.ENERGY_PROJECTILE_BLOCK_IMPACT, event); + } + } + + NpcAPI.EVENT_BUS.post(event); + } + + public static void onEnergyProjectileExpired(EntityAbilityProjectile projectile) { + IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile); + EnergyProjectileEvent.ExpiredEvent event = new EnergyProjectileEvent.ExpiredEvent(wrapped); + ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_EXPIRED, event); + + Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId()); + if (owner instanceof EntityNPCInterface) { + ((EntityNPCInterface) owner).script.callScript(EnumScriptType.ENERGY_PROJECTILE_EXPIRED, event); + } else if (owner instanceof EntityPlayer) { + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner); + if (handler != null) { + handler.callScript(EnumScriptType.ENERGY_PROJECTILE_EXPIRED, event); + } + } + + NpcAPI.EVENT_BUS.post(event); + } + public static void onPlayerInit(PlayerDataScript handler, IPlayer player) { PlayerEvent.InitEvent event = new PlayerEvent.InitEvent(player); handler.callScript(EnumScriptType.INIT, event); @@ -693,6 +802,31 @@ public class EventHooks { return NpcAPI.EVENT_BUS.post(event); } + public static boolean onPlayerAbilityStart(PlayerDataScript handler, PlayerAbilityEvent.StartEvent event) { + handler.callScript(EnumScriptType.ABILITY_START, event); + return NpcAPI.EVENT_BUS.post(event); + } + + public static boolean onPlayerAbilityExecute(PlayerDataScript handler, PlayerAbilityEvent.ExecuteEvent event) { + handler.callScript(EnumScriptType.ABILITY_EXECUTE, event); + return NpcAPI.EVENT_BUS.post(event); + } + + public static void onPlayerAbilityTick(PlayerDataScript handler, PlayerAbilityEvent.TickEvent event) { + handler.callScript(EnumScriptType.ABILITY_TICK, event); + NpcAPI.EVENT_BUS.post(event); + } + + public static void onPlayerAbilityInterrupt(PlayerDataScript handler, PlayerAbilityEvent.InterruptEvent event) { + handler.callScript(EnumScriptType.ABILITY_INTERRUPT, event); + NpcAPI.EVENT_BUS.post(event); + } + + public static void onPlayerAbilityComplete(PlayerDataScript handler, PlayerAbilityEvent.CompleteEvent event) { + handler.callScript(EnumScriptType.ABILITY_COMPLETE, event); + NpcAPI.EVENT_BUS.post(event); + } + public static boolean onPlayerAbilityHit(PlayerDataScript handler, PlayerAbilityEvent.HitEvent event) { handler.callScript(EnumScriptType.ABILITY_HIT, event); return NpcAPI.EVENT_BUS.post(event); diff --git a/src/main/java/noppes/npcs/constants/EnumScriptType.java b/src/main/java/noppes/npcs/constants/EnumScriptType.java index 99a8b4091..da4e8f136 100644 --- a/src/main/java/noppes/npcs/constants/EnumScriptType.java +++ b/src/main/java/noppes/npcs/constants/EnumScriptType.java @@ -125,7 +125,13 @@ public enum EnumScriptType { ABILITY_INTERRUPT("abilityInterrupt"), ABILITY_COMPLETE("abilityComplete"), ABILITY_HIT("abilityHit"), - ABILITY_TICK("abilityTick"); + ABILITY_TICK("abilityTick"), + + ENERGY_PROJECTILE_FIRED("energyProjectileFired"), + ENERGY_PROJECTILE_TICK("energyProjectileTick"), + ENERGY_PROJECTILE_ENTITY_IMPACT("energyProjectileEntityImpact"), + ENERGY_PROJECTILE_BLOCK_IMPACT("energyProjectileBlockImpact"), + ENERGY_PROJECTILE_EXPIRED("energyProjectileExpired"); public String function; diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index dcc5b5d95..c5861927a 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -1,14 +1,17 @@ package noppes.npcs.constants; +import noppes.npcs.api.event.IAbilityEvent; import noppes.npcs.api.event.IAnimationEvent; import noppes.npcs.api.event.IBlockEvent; import noppes.npcs.api.event.ICustomGuiEvent; import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IEnergyProjectileEvent; import noppes.npcs.api.event.IFactionEvent; import noppes.npcs.api.event.IForgeEvent; import noppes.npcs.api.event.IItemEvent; import noppes.npcs.api.event.INpcEvent; import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IPlayerAbilityEvent; import noppes.npcs.api.event.IPlayerEvent; import noppes.npcs.api.event.IProjectileEvent; import noppes.npcs.api.event.IQuestEvent; @@ -52,7 +55,10 @@ public class ScriptContext { public static final ScriptContext NPC = register("NPC", "npc", INpcEvent.class, - IProjectileEvent.class + IProjectileEvent.class, + IEnergyProjectileEvent.class, + IAbilityEvent.class, + IAnimationEvent.class ); public static final ScriptContext PLAYER = register("PLAYER", "player", @@ -62,7 +68,9 @@ public class ScriptContext { IDialogEvent.class, IQuestEvent.class, IFactionEvent.class, - ICustomGuiEvent.class + ICustomGuiEvent.class, + IEnergyProjectileEvent.class, + IPlayerAbilityEvent.class ); public static final ScriptContext BLOCK = register("BLOCK", "block", diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index f3bad8e9d..88a12e20d 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -2,10 +2,12 @@ package noppes.npcs.controllers; import noppes.npcs.api.event.IAbilityEvent; import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IPlayerAbilityEvent; import noppes.npcs.api.event.IBlockEvent; import noppes.npcs.api.event.ICustomGuiEvent; import noppes.npcs.api.event.ICustomNPCsEvent; import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IEnergyProjectileEvent; import noppes.npcs.api.event.IFactionEvent; import noppes.npcs.api.event.IForgeEvent; import noppes.npcs.api.event.IItemEvent; @@ -106,6 +108,13 @@ public class ScriptHookController implements IScriptHookHandler { hook(NPC, PROJECTILE_TICK, IProjectileEvent.UpdateEvent.class); hook(NPC, PROJECTILE_IMPACT, IProjectileEvent.ImpactEvent.class); + // Energy Projectile + hook(NPC, ENERGY_PROJECTILE_FIRED, IEnergyProjectileEvent.FiredEvent.class); + hook(NPC, ENERGY_PROJECTILE_TICK, IEnergyProjectileEvent.UpdateEvent.class); + hook(NPC, ENERGY_PROJECTILE_ENTITY_IMPACT, IEnergyProjectileEvent.EntityImpactEvent.class); + hook(NPC, ENERGY_PROJECTILE_BLOCK_IMPACT, IEnergyProjectileEvent.BlockImpactEvent.class); + hook(NPC, ENERGY_PROJECTILE_EXPIRED, IEnergyProjectileEvent.ExpiredEvent.class); + // Ability hook(NPC, ABILITY_START, IAbilityEvent.StartEvent.class); hook(NPC, ABILITY_EXECUTE, IAbilityEvent.ExecuteEvent.class); @@ -113,6 +122,12 @@ public class ScriptHookController implements IScriptHookHandler { hook(NPC, ABILITY_TICK, IAbilityEvent.TickEvent.class); hook(NPC, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); hook(NPC, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); + + // Animation + hook(NPC, ANIMATION_START, IAnimationEvent.Started.class); + hook(NPC, ANIMATION_END, IAnimationEvent.Ended.class); + hook(NPC, ANIMATION_FRAME_ENTER, IAnimationEvent.IFrameEvent.Entered.class); + hook(NPC, ANIMATION_FRAME_EXIT, IAnimationEvent.IFrameEvent.Exited.class); } private void initializePlayerHooks() { @@ -208,6 +223,21 @@ public class ScriptHookController implements IScriptHookHandler { hook(PLAYER, ANIMATION_FRAME_ENTER, IAnimationEvent.IFrameEvent.Entered.class); hook(PLAYER, ANIMATION_FRAME_EXIT, IAnimationEvent.IFrameEvent.Exited.class); + // Energy Projectile + hook(PLAYER, ENERGY_PROJECTILE_FIRED, IEnergyProjectileEvent.FiredEvent.class); + hook(PLAYER, ENERGY_PROJECTILE_TICK, IEnergyProjectileEvent.UpdateEvent.class); + hook(PLAYER, ENERGY_PROJECTILE_ENTITY_IMPACT, IEnergyProjectileEvent.EntityImpactEvent.class); + hook(PLAYER, ENERGY_PROJECTILE_BLOCK_IMPACT, IEnergyProjectileEvent.BlockImpactEvent.class); + hook(PLAYER, ENERGY_PROJECTILE_EXPIRED, IEnergyProjectileEvent.ExpiredEvent.class); + + // Ability + hook(PLAYER, ABILITY_START, IPlayerAbilityEvent.StartEvent.class); + hook(PLAYER, ABILITY_EXECUTE, IPlayerAbilityEvent.ExecuteEvent.class); + hook(PLAYER, ABILITY_HIT, IPlayerAbilityEvent.HitEvent.class); + hook(PLAYER, ABILITY_TICK, IPlayerAbilityEvent.TickEvent.class); + hook(PLAYER, ABILITY_INTERRUPT, IPlayerAbilityEvent.InterruptEvent.class); + hook(PLAYER, ABILITY_COMPLETE, IPlayerAbilityEvent.CompleteEvent.class); + // Profile hook(PLAYER, PROFILE_CHANGE, IPlayerEvent.ProfileEvent.Changed.class); hook(PLAYER, PROFILE_REMOVE, IPlayerEvent.ProfileEvent.Removed.class); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 6ced6f68a..088017e62 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -15,9 +15,14 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.nbt.NBTTagString; import net.minecraft.util.DamageSource; +import noppes.npcs.EventHooks; import noppes.npcs.api.ability.IPlayerAbilityData; +import noppes.npcs.api.entity.IPlayer; import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.Animation; +import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; @@ -154,6 +159,9 @@ public class PlayerAbilityData implements IPlayerAbilityData { // 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())) { @@ -211,6 +219,11 @@ public class PlayerAbilityData implements IPlayerAbilityData { playAbilitySound(player, currentAbility.getActiveSound()); playAbilityAnimation(currentAbility.getActiveAnimation()); + // Fire execute event (cancelable) + if (firePlayerExecuteEvent(player, currentAbility, currentTarget)) { + return; // Cancelled + } + // Call onExecute currentAbility.onExecute(player, currentTarget); @@ -268,6 +281,9 @@ public class PlayerAbilityData implements IPlayerAbilityData { currentAbility.onComplete(player, currentTarget); + // Fire complete event + firePlayerCompleteEvent(player, currentAbility, currentTarget); + // Release all locks releaseRotationControl(); releaseLockedPosition(); @@ -340,6 +356,11 @@ public class PlayerAbilityData implements IPlayerAbilityData { return false; } + // Fire start event (cancelable) + if (firePlayerStartEvent(player, ability)) { + return false; // Cancelled + } + // Start the ability currentAbility = ability; currentAbilityKey = key; @@ -380,6 +401,11 @@ public class PlayerAbilityData implements IPlayerAbilityData { playAbilitySound(player, ability.getActiveSound()); playAbilityAnimation(ability.getActiveAnimation()); + // Fire execute event (cancelable) + if (firePlayerExecuteEvent(player, ability, currentTarget)) { + return; // Cancelled + } + // Call onExecute ability.onExecute(player, currentTarget); @@ -538,10 +564,17 @@ public class PlayerAbilityData implements IPlayerAbilityData { } 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(); releaseRotationControl(); @@ -594,7 +627,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { // Check for ability interruption if (currentAbility != null && currentAbility.canInterrupt(source)) { - interruptCurrentAbility(); + interruptCurrentAbility(source, amount); } return amount; @@ -834,6 +867,73 @@ public class PlayerAbilityData implements IPlayerAbilityData { } } + // ═══════════════════════════════════════════════════════════════════ + // SCRIPT EVENTS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Fire the ability start event. Returns true if cancelled. + */ + private boolean firePlayerStartEvent(EntityPlayer player, Ability ability) { + 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.StartEvent event = new PlayerAbilityEvent.StartEvent(iPlayer, ability, null); + 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 // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index c80c18d28..24f271f33 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -7,6 +7,10 @@ import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.TelegraphController; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityOrb; import net.minecraft.block.Block; import net.minecraft.command.CommandHandler; import net.minecraft.entity.Entity; @@ -119,6 +123,10 @@ import noppes.npcs.items.ItemScripted; import noppes.npcs.scripted.entity.ScriptAnimal; import noppes.npcs.scripted.entity.ScriptArrow; import noppes.npcs.scripted.entity.ScriptDBCPlayer; +import noppes.npcs.scripted.entity.ScriptEnergyBeam; +import noppes.npcs.scripted.entity.ScriptEnergyDisc; +import noppes.npcs.scripted.entity.ScriptEnergyLaser; +import noppes.npcs.scripted.entity.ScriptEnergyOrb; import noppes.npcs.scripted.entity.ScriptEntity; import noppes.npcs.scripted.entity.ScriptEntityItem; import noppes.npcs.scripted.entity.ScriptFishHook; @@ -454,6 +462,14 @@ public class NpcAPI extends AbstractNpcAPI { data = new ScriptEntityData(new ScriptArrow<>((EntityArrow) entity)); else if (entity instanceof EntityFishHook) data = new ScriptEntityData(new ScriptFishHook<>((EntityFishHook) entity)); + else if (entity instanceof EntityAbilityOrb) + data = new ScriptEntityData(new ScriptEnergyOrb<>((EntityAbilityOrb) entity)); + else if (entity instanceof EntityAbilityBeam) + data = new ScriptEntityData(new ScriptEnergyBeam<>((EntityAbilityBeam) entity)); + else if (entity instanceof EntityAbilityDisc) + data = new ScriptEntityData(new ScriptEnergyDisc<>((EntityAbilityDisc) entity)); + else if (entity instanceof EntityAbilityLaser) + data = new ScriptEntityData(new ScriptEnergyLaser<>((EntityAbilityLaser) entity)); else data = new ScriptEntityData(new ScriptEntity<>(entity)); entity.registerExtendedProperties("ScriptedObject", data); diff --git a/src/main/java/noppes/npcs/scripted/ScriptWorld.java b/src/main/java/noppes/npcs/scripted/ScriptWorld.java index c9b806ca9..b827315d2 100644 --- a/src/main/java/noppes/npcs/scripted/ScriptWorld.java +++ b/src/main/java/noppes/npcs/scripted/ScriptWorld.java @@ -21,6 +21,10 @@ import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.BlockSnapshot; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.event.world.BlockEvent; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityOrb; import noppes.npcs.CustomNpcs; import noppes.npcs.NoppesUtilServer; import noppes.npcs.api.IBlock; @@ -28,6 +32,10 @@ import noppes.npcs.api.IParticle; import noppes.npcs.api.IPos; import noppes.npcs.api.ITileEntity; import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEnergyBeam; +import noppes.npcs.api.entity.IEnergyDisc; +import noppes.npcs.api.entity.IEnergyLaser; +import noppes.npcs.api.entity.IEnergyOrb; import noppes.npcs.api.entity.IEntity; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.handler.data.ISound; @@ -1242,6 +1250,48 @@ public class ScriptWorld implements IWorld { return "DIM" + this.getDimensionID(); } + @Override + public IEnergyOrb createEnergyOrb(IEntity owner, double x, double y, double z, float size) { + EntityAbilityOrb entity = new EntityAbilityOrb(world); + entity.setPosition(x, y, z); + entity.setStartPosition(x, y, z); + entity.setOwnerEntityId(owner.getMCEntity().getEntityId()); + entity.setProjectileSize(size); + return (IEnergyOrb) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergyBeam createEnergyBeam(IEntity owner, double x, double y, double z, float beamWidth, float headSize) { + EntityAbilityBeam entity = new EntityAbilityBeam(world); + entity.setPosition(x, y, z); + entity.setStartPosition(x, y, z); + entity.setOwnerEntityId(owner.getMCEntity().getEntityId()); + entity.setBeamWidth(beamWidth); + entity.setHeadSize(headSize); + return (IEnergyBeam) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergyDisc createEnergyDisc(IEntity owner, double x, double y, double z, float radius, float thickness) { + EntityAbilityDisc entity = new EntityAbilityDisc(world); + entity.setPosition(x, y, z); + entity.setStartPosition(x, y, z); + entity.setOwnerEntityId(owner.getMCEntity().getEntityId()); + entity.setDiscRadius(radius); + entity.setDiscThickness(thickness); + return (IEnergyDisc) NpcAPI.Instance().getIEntity(entity); + } + + @Override + public IEnergyLaser createEnergyLaser(IEntity owner, double x, double y, double z, float laserWidth) { + EntityAbilityLaser entity = new EntityAbilityLaser(world); + entity.setPosition(x, y, z); + entity.setStartPosition(x, y, z); + entity.setOwnerEntityId(owner.getMCEntity().getEntityId()); + entity.setLaserWidth(laserWidth); + return (IEnergyLaser) NpcAPI.Instance().getIEntity(entity); + } + @Override public void broadcast(String message) { world.func_73046_m().getConfigurationManager().sendChatMsg(new ChatComponentText(message)); diff --git a/src/main/java/noppes/npcs/scripted/constants/EntityType.java b/src/main/java/noppes/npcs/scripted/constants/EntityType.java index 995969682..355173646 100644 --- a/src/main/java/noppes/npcs/scripted/constants/EntityType.java +++ b/src/main/java/noppes/npcs/scripted/constants/EntityType.java @@ -15,4 +15,8 @@ public class EntityType { public static final int THROWABLE = 11; public static final int FISH_HOOK = 12; public static final int ENERGY_PROJECTILE = 13; + public static final int ENERGY_ORB = 14; + public static final int ENERGY_BEAM = 15; + public static final int ENERGY_DISC = 16; + public static final int ENERGY_LASER = 17; } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java new file mode 100644 index 000000000..b42a32b1d --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java @@ -0,0 +1,69 @@ +package noppes.npcs.scripted.entity; + +import kamkeel.npcs.entity.EntityAbilityBeam; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.api.entity.IEnergyBeam; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.scripted.constants.EntityType; + +public class ScriptEnergyBeam extends ScriptEnergyProjectile implements IEnergyBeam { + + public ScriptEnergyBeam(T entity) { + super(entity); + } + + @Override + public int getType() { + return EntityType.ENERGY_BEAM; + } + + @Override + public boolean typeOf(int type) { + return type == EntityType.ENERGY_BEAM || super.typeOf(type); + } + + @Override + public int getEnergyType() { + return 1; + } + + // ==================== BEAM-SPECIFIC ==================== + + public float getBeamWidth() { return entity.getBeamWidth(); } + public void setBeamWidth(float width) { entity.setBeamWidth(width); } + + public float getHeadSize() { return entity.getHeadSize(); } + public void setHeadSize(float size) { entity.setHeadSize(size); } + + public boolean isAttachedToOwner() { return entity.isAttachedToOwner(); } + public void setAttachedToOwner(boolean attached) { entity.setAttachedToOwner(attached); } + + public boolean shouldRenderTailOrb() { return entity.shouldRenderTailOrb(); } + + // ==================== FIRE ==================== + + @Override + public void fire(IEntity target) { + ensureSpawned(); + EntityLivingBase mcTarget = null; + if (target != null && target.getMCEntity() instanceof EntityLivingBase) { + mcTarget = (EntityLivingBase) target.getMCEntity(); + } + entity.startFiring(mcTarget); + } + + @Override + public void fire(double x, double y, double z) { + ensureSpawned(); + double dx = x - entity.posX; + double dy = y - entity.posY; + double dz = z - entity.posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + entity.motionX = (dx / len) * entity.getSpeed(); + entity.motionY = (dy / len) * entity.getSpeed(); + entity.motionZ = (dz / len) * entity.getSpeed(); + } + entity.startFiring(null); + } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java new file mode 100644 index 000000000..62dde7979 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java @@ -0,0 +1,75 @@ +package noppes.npcs.scripted.entity; + +import kamkeel.npcs.entity.EntityAbilityDisc; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.api.entity.IEnergyDisc; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.scripted.constants.EntityType; + +public class ScriptEnergyDisc extends ScriptEnergyProjectile implements IEnergyDisc { + + public ScriptEnergyDisc(T entity) { + super(entity); + } + + @Override + public int getType() { + return EntityType.ENERGY_DISC; + } + + @Override + public boolean typeOf(int type) { + return type == EntityType.ENERGY_DISC || super.typeOf(type); + } + + @Override + public int getEnergyType() { + return 2; + } + + // ==================== DISC-SPECIFIC ==================== + + public float getDiscRadius() { return entity.getDiscRadius(); } + public void setDiscRadius(float radius) { entity.setDiscRadius(radius); } + + public float getDiscThickness() { return entity.getDiscThickness(); } + public void setDiscThickness(float thickness) { entity.setDiscThickness(thickness); } + + public boolean isVertical() { return entity.isVertical(); } + public void setVertical(boolean vertical) { entity.setVertical(vertical); } + + public boolean isBoomerang() { return entity.isBoomerang(); } + public void setBoomerang(boolean boomerang) { entity.setBoomerang(boomerang); } + + public int getBoomerangDelay() { return entity.getBoomerangDelay(); } + public void setBoomerangDelay(int ticks) { entity.setBoomerangDelay(ticks); } + + public boolean isReturning() { return entity.isReturning(); } + + // ==================== FIRE ==================== + + @Override + public void fire(IEntity target) { + ensureSpawned(); + EntityLivingBase mcTarget = null; + if (target != null && target.getMCEntity() instanceof EntityLivingBase) { + mcTarget = (EntityLivingBase) target.getMCEntity(); + } + entity.startMoving(mcTarget); + } + + @Override + public void fire(double x, double y, double z) { + ensureSpawned(); + double dx = x - entity.posX; + double dy = y - entity.posY; + double dz = z - entity.posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + entity.motionX = (dx / len) * entity.getSpeed(); + entity.motionY = (dy / len) * entity.getSpeed(); + entity.motionZ = (dz / len) * entity.getSpeed(); + } + entity.startMoving(null); + } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java new file mode 100644 index 000000000..976fd948d --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java @@ -0,0 +1,82 @@ +package noppes.npcs.scripted.entity; + +import kamkeel.npcs.entity.EntityAbilityLaser; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.api.entity.IEnergyLaser; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.scripted.constants.EntityType; + +public class ScriptEnergyLaser extends ScriptEnergyProjectile implements IEnergyLaser { + + public ScriptEnergyLaser(T entity) { + super(entity); + } + + @Override + public int getType() { + return EntityType.ENERGY_LASER; + } + + @Override + public boolean typeOf(int type) { + return type == EntityType.ENERGY_LASER || super.typeOf(type); + } + + @Override + public int getEnergyType() { + return 3; + } + + // ==================== LASER-SPECIFIC ==================== + + public float getLaserWidth() { return entity.getLaserWidth(); } + public void setLaserWidth(float width) { entity.setLaserWidth(width); } + + public float getExpansionSpeed() { return entity.getExpansionSpeed(); } + public void setExpansionSpeed(float speed) { entity.setExpansionSpeed(speed); } + + public int getLingerTicks() { return entity.getLingerTicks(); } + public void setLingerTicks(int ticks) { entity.setLingerTicks(ticks); } + + public float getCurrentLength() { return entity.getCurrentLength(); } + public boolean isFullyExtended() { return entity.isFullyExtended(); } + + public double getDirX() { return entity.getDirX(); } + public double getDirY() { return entity.getDirY(); } + public double getDirZ() { return entity.getDirZ(); } + public void setDirection(double x, double y, double z) { entity.setDirection(x, y, z); } + + public double getEndX() { return entity.getEndX(); } + public double getEndY() { return entity.getEndY(); } + public double getEndZ() { return entity.getEndZ(); } + + public float getLingerAlpha() { return entity.getLingerAlpha(); } + + public boolean isLockVerticalDirection() { return entity.isLockVerticalDirection(); } + public void setLockVerticalDirection(boolean lock) { entity.setLockVerticalDirection(lock); } + + // ==================== FIRE ==================== + + @Override + public void fire(IEntity target) { + ensureSpawned(); + EntityLivingBase mcTarget = null; + if (target != null && target.getMCEntity() instanceof EntityLivingBase) { + mcTarget = (EntityLivingBase) target.getMCEntity(); + } + entity.startMoving(mcTarget); + } + + @Override + public void fire(double x, double y, double z) { + ensureSpawned(); + double dx = x - entity.posX; + double dy = y - entity.posY; + double dz = z - entity.posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + entity.setDirection(dx / len, dy / len, dz / len); + } + entity.startMoving(null); + } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java new file mode 100644 index 000000000..1b6a95fb7 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java @@ -0,0 +1,53 @@ +package noppes.npcs.scripted.entity; + +import kamkeel.npcs.entity.EntityAbilityOrb; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.api.entity.IEnergyOrb; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.scripted.constants.EntityType; + +public class ScriptEnergyOrb extends ScriptEnergyProjectile implements IEnergyOrb { + + public ScriptEnergyOrb(T entity) { + super(entity); + } + + @Override + public int getType() { + return EntityType.ENERGY_ORB; + } + + @Override + public boolean typeOf(int type) { + return type == EntityType.ENERGY_ORB || super.typeOf(type); + } + + @Override + public int getEnergyType() { + return 0; + } + + @Override + public void fire(IEntity target) { + ensureSpawned(); + EntityLivingBase mcTarget = null; + if (target != null && target.getMCEntity() instanceof EntityLivingBase) { + mcTarget = (EntityLivingBase) target.getMCEntity(); + } + entity.startMoving(mcTarget); + } + + @Override + public void fire(double x, double y, double z) { + ensureSpawned(); + double dx = x - entity.posX; + double dy = y - entity.posY; + double dz = z - entity.posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + entity.motionX = (dx / len) * entity.getSpeed(); + entity.motionY = (dy / len) * entity.getSpeed(); + entity.motionZ = (dz / len) * entity.getSpeed(); + } + } +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java new file mode 100644 index 000000000..f614b8122 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java @@ -0,0 +1,252 @@ +package noppes.npcs.scripted.entity; + +import kamkeel.npcs.entity.EntityAbilityProjectile; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.api.entity.IEnergyProjectile; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.EventHooks; +import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.scripted.constants.EntityType; + +public class ScriptEnergyProjectile extends ScriptEntity implements IEnergyProjectile { + protected T entity; + + public ScriptEnergyProjectile(T entity) { + super(entity); + this.entity = entity; + } + + // ==================== TYPE ==================== + + @Override + public int getType() { + return EntityType.ENERGY_PROJECTILE; + } + + @Override + public boolean typeOf(int type) { + return type == EntityType.ENERGY_PROJECTILE || super.typeOf(type); + } + + public int getEnergyType() { + return -1; + } + + // ==================== OWNER & TARGET ==================== + + public int getOwnerEntityId() { + return entity.getOwnerEntityId(); + } + + public IEntity getOwner() { + return entity.getOwner(); + } + + public void setOwner(IEntity owner) { + if (owner != null) { + entity.setOwnerEntityId(owner.getMCEntity().getEntityId()); + } + } + + public int getTargetEntityId() { + return entity.getTargetEntityId(); + } + + public IEntity getTarget() { + return entity.getTarget(); + } + + public void setTarget(IEntity target) { + if (target != null) { + entity.setTargetEntityId(target.getMCEntity().getEntityId()); + } else { + entity.setTargetEntityId(-1); + } + } + + // ==================== SIZE ==================== + + public float getSize() { + return entity.getSize(); + } + + public void setSize(float size) { + entity.setProjectileSize(size); + } + + // ==================== DISPLAY ==================== + + public int getInnerColor() { return entity.getInnerColor(); } + public void setInnerColor(int color) { entity.setInnerColor(color); } + + public int getOuterColor() { return entity.getOuterColor(); } + public void setOuterColor(int color) { entity.setOuterColor(color); } + + public boolean isOuterColorEnabled() { return entity.isOuterColorEnabled(); } + public void setOuterColorEnabled(boolean enabled) { entity.setOuterColorEnabled(enabled); } + + public float getOuterColorWidth() { return entity.getOuterColorWidth(); } + public void setOuterColorWidth(float width) { entity.setOuterColorWidth(width); } + + public float getOuterColorAlpha() { return entity.getOuterColorAlpha(); } + public void setOuterColorAlpha(float alpha) { entity.setOuterColorAlpha(alpha); } + + public float getRotationSpeed() { return entity.getRotationSpeed(); } + public void setRotationSpeed(float speed) { entity.setRotationSpeed(speed); } + + // ==================== INTERPOLATION ==================== + + public float getInterpolatedRotationX(float partialTicks) { return entity.getInterpolatedRotationX(partialTicks); } + public float getInterpolatedRotationY(float partialTicks) { return entity.getInterpolatedRotationY(partialTicks); } + public float getInterpolatedRotationZ(float partialTicks) { return entity.getInterpolatedRotationZ(partialTicks); } + public float getInterpolatedSize(float partialTicks) { return entity.getInterpolatedSize(partialTicks); } + + // ==================== LIGHTNING ==================== + + public boolean hasLightningEffect() { return entity.hasLightningEffect(); } + public void setLightningEffect(boolean enabled) { entity.setLightningEffect(enabled); } + + public float getLightningDensity() { return entity.getLightningDensity(); } + public void setLightningDensity(float density) { entity.setLightningDensity(density); } + + public float getLightningRadius() { return entity.getLightningRadius(); } + public void setLightningRadius(float radius) { entity.setLightningRadius(radius); } + + public int getLightningFadeTime() { return entity.getLightningFadeTime(); } + public void setLightningFadeTime(int ticks) { entity.setLightningFadeTime(ticks); } + + // ==================== LIFESPAN ==================== + + public float getMaxDistance() { return entity.getMaxDistance(); } + public void setMaxDistance(float distance) { entity.setMaxDistance(distance); } + + public int getMaxLifetime() { return entity.getMaxLifetime(); } + public void setMaxLifetime(int ticks) { entity.setMaxLifetime(ticks); } + + // ==================== COMBAT ==================== + + public float getDamage() { return entity.getDamage(); } + public void setDamage(float damage) { entity.setCombatDamage(damage); } + + public float getKnockback() { return entity.getKnockback(); } + public void setKnockback(float knockback) { entity.setCombatKnockback(knockback); } + + public float getKnockbackUp() { return entity.getKnockbackUp(); } + public void setKnockbackUp(float knockbackUp) { entity.setCombatKnockbackUp(knockbackUp); } + + public boolean isExplosive() { return entity.isExplosive(); } + public void setExplosive(boolean explosive) { entity.setExplosive(explosive); } + + public float getExplosionRadius() { return entity.getExplosionRadius(); } + public void setExplosionRadius(float radius) { entity.setExplosionRadius(radius); } + + public float getExplosionDamageFalloff() { return entity.getExplosionDamageFalloff(); } + public void setExplosionDamageFalloff(float falloff) { entity.setExplosionDamageFalloff(falloff); } + + // ==================== MOVEMENT ==================== + + public float getSpeed() { return entity.getSpeed(); } + public void setSpeed(float speed) { entity.setSpeed(speed); } + + public boolean isHoming() { return entity.isHoming(); } + public void setHoming(boolean homing) { entity.setHomingEnabled(homing); } + + public float getHomingStrength() { return entity.getHomingStrength(); } + public void setHomingStrength(float strength) { entity.setHomingStrength(strength); } + + public float getHomingRange() { return entity.getHomingRange(); } + public void setHomingRange(float range) { entity.setHomingRange(range); } + + // ==================== ANCHOR ==================== + + public int getAnchor() { return entity.getAnchor(); } + public float getAnchorOffsetX() { return entity.getAnchorOffsetX(); } + public float getAnchorOffsetY() { return entity.getAnchorOffsetY(); } + public float getAnchorOffsetZ() { return entity.getAnchorOffsetZ(); } + + // ==================== POSITION ==================== + + public double getStartX() { return entity.getStartX(); } + public double getStartY() { return entity.getStartY(); } + public double getStartZ() { return entity.getStartZ(); } + + // ==================== STATE ==================== + + public boolean hasHit() { return entity.getHasHit(); } + public boolean isCharging() { return entity.isCharging(); } + public float getChargeProgress() { return entity.getChargeProgress(); } + + // ==================== FIRE ==================== + + public void fire(IEntity target) { + if (target != null) { + Entity mcTarget = target.getMCEntity(); + setMotionToward(mcTarget.posX, mcTarget.posY + mcTarget.getEyeHeight(), mcTarget.posZ); + } + ensureSpawned(); + } + + public void fire(double x, double y, double z) { + setMotionToward(x, y, z); + ensureSpawned(); + } + + public void fire(float yaw, float pitch) { + float yawRad = (float) Math.toRadians(yaw); + float pitchRad = (float) Math.toRadians(pitch); + entity.motionX = -Math.sin(yawRad) * Math.cos(pitchRad) * entity.getSpeed(); + entity.motionY = -Math.sin(pitchRad) * entity.getSpeed(); + entity.motionZ = Math.cos(yawRad) * Math.cos(pitchRad) * entity.getSpeed(); + ensureSpawned(); + } + + public void fire(IEntityLivingBase caster) { + if (caster == null) return; + initFromCaster(caster); + fire(caster.getRotation(), caster.getPitch()); + } + + public void fire(IEntityLivingBase caster, IEntity target) { + if (caster == null) return; + initFromCaster(caster); + fire(target); + } + + /** + * Sets owner from caster and positions projectile at caster's eye level. + */ + private void initFromCaster(IEntityLivingBase caster) { + EntityLivingBase mc = (EntityLivingBase) caster.getMCEntity(); + entity.setOwnerEntityId(mc.getEntityId()); + double eyeY = mc.posY + mc.getEyeHeight(); + entity.setPosition(mc.posX, eyeY, mc.posZ); + entity.setStartPosition(mc.posX, eyeY, mc.posZ); + } + + /** + * Calculates and sets motion from the projectile's current position toward a target point. + */ + private void setMotionToward(double x, double y, double z) { + double dx = x - entity.posX; + double dy = y - entity.posY; + double dz = z - entity.posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + entity.motionX = (dx / len) * entity.getSpeed(); + entity.motionY = (dy / len) * entity.getSpeed(); + entity.motionZ = (dz / len) * entity.getSpeed(); + } + } + + protected void ensureSpawned() { + if (!entity.addedToChunk && entity.worldObj != null) { + entity.worldObj.spawnEntityInWorld(entity); + if (!entity.worldObj.isRemote) { + EventHooks.onEnergyProjectileFired(entity); + } + } + } + +} diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEntity.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEntity.java index caac8d87d..65b2866af 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEntity.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEntity.java @@ -1,5 +1,10 @@ package noppes.npcs.scripted.entity; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityProjectile; import net.minecraft.block.material.Material; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityList; @@ -526,6 +531,16 @@ public class ScriptEntity implements IEntity { cls = EntityMob.class; else if (type == EntityType.NPC) cls = EntityNPCInterface.class; + else if (type == EntityType.ENERGY_PROJECTILE) + cls = EntityAbilityProjectile.class; + else if (type == EntityType.ENERGY_ORB) + cls = EntityAbilityOrb.class; + else if (type == EntityType.ENERGY_BEAM) + cls = EntityAbilityBeam.class; + else if (type == EntityType.ENERGY_DISC) + cls = EntityAbilityDisc.class; + else if (type == EntityType.ENERGY_LASER) + cls = EntityAbilityLaser.class; List entities = entity.worldObj.getEntitiesWithinAABB(cls, entity.boundingBox.expand(range, range, range)); List list = new ArrayList(); diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java index f534f9dbc..2379c2035 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java @@ -7,6 +7,7 @@ import noppes.npcs.api.IPos; import noppes.npcs.api.ITimers; import noppes.npcs.api.ability.IDataAbilities; import noppes.npcs.api.entity.ICustomNpc; +import noppes.npcs.api.entity.IEnergyProjectile; import noppes.npcs.api.entity.IEntityLivingBase; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.entity.data.IModelData; @@ -28,6 +29,7 @@ import noppes.npcs.constants.EnumMovingType; import noppes.npcs.constants.EnumNavType; import noppes.npcs.constants.EnumRoleType; import noppes.npcs.constants.EnumStandingType; +import kamkeel.npcs.entity.EntityAbilityProjectile; import noppes.npcs.controllers.FactionController; import noppes.npcs.controllers.data.DialogOption; import noppes.npcs.controllers.data.Line; @@ -1463,4 +1465,13 @@ public class ScriptNpc extends ScriptLiving imp public IDataAbilities getAbilityData() { return new ScriptDataAbilities(npc); } + + public IEnergyProjectile[] getActiveEnergyProjectiles() { + java.util.List entities = EntityAbilityProjectile.getActiveProjectiles(npc.getEntityId()); + IEnergyProjectile[] result = new IEnergyProjectile[entities.size()]; + for (int i = 0; i < entities.size(); i++) { + result[i] = (IEnergyProjectile) NpcAPI.Instance().getIEntity(entities.get(i)); + } + return result; + } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index a1b91e1db..3608b9d1e 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -1,6 +1,7 @@ package noppes.npcs.scripted.entity; import kamkeel.npcs.controllers.AttributeController; +import kamkeel.npcs.entity.EntityAbilityProjectile; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.packets.data.AchievementPacket; import kamkeel.npcs.network.packets.data.ChatAlertPacket; @@ -31,6 +32,7 @@ import noppes.npcs.api.IPos; import noppes.npcs.api.IScreenSize; import noppes.npcs.api.ITimers; import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEnergyProjectile; import noppes.npcs.api.entity.IEntity; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.gui.ICustomGui; @@ -933,4 +935,13 @@ public class ScriptPlayer extends ScriptLivingBase public String getFormattedCurrencyBalance() { return getData().tradeData.formatBalance(); } + + public IEnergyProjectile[] getActiveEnergyProjectiles() { + List entities = EntityAbilityProjectile.getActiveProjectiles(player.getEntityId()); + IEnergyProjectile[] result = new IEnergyProjectile[entities.size()]; + for (int i = 0; i < entities.size(); i++) { + result[i] = (IEnergyProjectile) NpcAPI.Instance().getIEntity(entities.get(i)); + } + return result; + } } diff --git a/src/main/java/noppes/npcs/scripted/event/EnergyProjectileEvent.java b/src/main/java/noppes/npcs/scripted/event/EnergyProjectileEvent.java new file mode 100644 index 000000000..4a1fab5a2 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/event/EnergyProjectileEvent.java @@ -0,0 +1,132 @@ +package noppes.npcs.scripted.event; + +import cpw.mods.fml.common.eventhandler.Cancelable; +import noppes.npcs.api.IBlock; +import noppes.npcs.api.entity.IEnergyProjectile; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.event.IEnergyProjectileEvent; +import noppes.npcs.constants.EnumScriptType; + +public class EnergyProjectileEvent extends CustomNPCsEvent implements IEnergyProjectileEvent { + public final IEnergyProjectile projectile; + public final IEntity owner; + + public EnergyProjectileEvent(IEnergyProjectile projectile) { + this.projectile = projectile; + this.owner = projectile.getOwner(); + } + + @Override + public IEnergyProjectile getProjectile() { + return projectile; + } + + @Override + public IEntity getOwner() { + return owner; + } + + @Override + public String getHookName() { + return "energyProjectileEvent"; + } + + /** + * energyProjectileFired + */ + public static class FiredEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.FiredEvent { + public FiredEvent(IEnergyProjectile projectile) { + super(projectile); + } + + @Override + public String getHookName() { + return EnumScriptType.ENERGY_PROJECTILE_FIRED.function; + } + } + + /** + * energyProjectileTick + */ + public static class UpdateEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.UpdateEvent { + public UpdateEvent(IEnergyProjectile projectile) { + super(projectile); + } + + @Override + public String getHookName() { + return EnumScriptType.ENERGY_PROJECTILE_TICK.function; + } + } + + /** + * energyProjectileEntityImpact + */ + @Cancelable + public static class EntityImpactEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.EntityImpactEvent { + private final IEntity target; + private float damage; + + public EntityImpactEvent(IEnergyProjectile projectile, IEntity target, float damage) { + super(projectile); + this.target = target; + this.damage = damage; + } + + @Override + public IEntity getTarget() { + return target; + } + + @Override + public float getDamage() { + return damage; + } + + @Override + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public String getHookName() { + return EnumScriptType.ENERGY_PROJECTILE_ENTITY_IMPACT.function; + } + } + + /** + * energyProjectileBlockImpact + */ + public static class BlockImpactEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.BlockImpactEvent { + private final IBlock block; + + public BlockImpactEvent(IEnergyProjectile projectile, IBlock block) { + super(projectile); + this.block = block; + } + + @Override + public IBlock getBlock() { + return block; + } + + @Override + public String getHookName() { + return EnumScriptType.ENERGY_PROJECTILE_BLOCK_IMPACT.function; + } + } + + /** + * energyProjectileExpired + */ + public static class ExpiredEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.ExpiredEvent { + public ExpiredEvent(IEnergyProjectile projectile) { + super(projectile); + } + + @Override + public String getHookName() { + return EnumScriptType.ENERGY_PROJECTILE_EXPIRED.function; + } + } +} diff --git a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java index cb7def405..b99aac1a1 100644 --- a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java @@ -3,6 +3,8 @@ package noppes.npcs.scripted.event.player; import cpw.mods.fml.common.eventhandler.Cancelable; import kamkeel.npcs.controllers.data.ability.Ability; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.util.DamageSource; +import noppes.npcs.api.IDamageSource; import noppes.npcs.api.ability.IAbility; import noppes.npcs.api.entity.IEntityLivingBase; import noppes.npcs.api.entity.IPlayer; @@ -38,6 +40,108 @@ public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEve return "playerAbilityEvent"; } + /** + * Fired when a player's ability starts executing (enters WINDUP phase). + */ + @Cancelable + public static class StartEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.StartEvent { + public StartEvent(IPlayer player, Ability ability, EntityLivingBase target) { + super(player, ability, target); + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_START.function; + } + } + + /** + * Fired when a player's ability enters the ACTIVE phase and performs its effect. + */ + @Cancelable + public static class ExecuteEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.ExecuteEvent { + public ExecuteEvent(IPlayer player, Ability ability, EntityLivingBase target) { + super(player, ability, target); + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_EXECUTE.function; + } + } + + /** + * Fired when a player's ability is interrupted by damage. + */ + public static class InterruptEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.InterruptEvent { + private final IDamageSource damageSource; + private final float damage; + + public InterruptEvent(IPlayer player, Ability ability, EntityLivingBase target, DamageSource source, float damage) { + super(player, ability, target); + this.damageSource = source != null ? NpcAPI.Instance().getIDamageSource(source) : null; + this.damage = damage; + } + + @Override + public IDamageSource getDamageSource() { + return damageSource; + } + + @Override + public float getDamage() { + return damage; + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_INTERRUPT.function; + } + } + + /** + * Fired when a player's ability completes its full execution cycle. + */ + public static class CompleteEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.CompleteEvent { + public CompleteEvent(IPlayer player, Ability ability, EntityLivingBase target) { + super(player, ability, target); + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_COMPLETE.function; + } + } + + /** + * Fired every tick while a player's ability is executing. + */ + public static class TickEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.TickEvent { + private final int abilityPhase; + private final int tick; + + public TickEvent(IPlayer player, Ability ability, EntityLivingBase target, int phase, int tick) { + super(player, ability, target); + this.abilityPhase = phase; + this.tick = tick; + } + + @Override + public int getAbilityPhase() { + return abilityPhase; + } + + @Override + public int getTick() { + return tick; + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_TICK.function; + } + } + /** * Fired when a player's ability hits an entity with damage. * Canceling prevents the damage from being applied. From e7222790090f71bf1d6a9f2ec9eee9c515bb8dae Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 16 Feb 2026 01:03:35 -0500 Subject: [PATCH 16/20] Update API hash to reflect recent changes --- src/api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api b/src/api index 66fff09d2..efa8721d3 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 66fff09d2078a9d85a6ef27ef98c02a5944eb325 +Subproject commit efa8721d3658b45146d58d6731a0b83c4860f02e From 21b1571868b8a7a48c95895b161406b9872bdac7 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 16 Feb 2026 01:15:52 -0500 Subject: [PATCH 17/20] Add flight checker to prevent abilities from affecting flying players --- .../data/ability/AbilityController.java | 16 ++++++++++++++++ .../noppes/npcs/ScriptPlayerEventHandler.java | 5 ++++- .../noppes/npcs/client/ClientTickHandler.java | 6 ++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 43af10b7c..545673e70 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -9,8 +9,11 @@ import noppes.npcs.LogWriter; import noppes.npcs.api.handler.IAbilityHandler; import noppes.npcs.util.NBTJsonUtil; +import net.minecraft.entity.player.EntityPlayer; + import java.io.File; import java.util.*; +import java.util.function.Predicate; import java.util.function.Supplier; public class AbilityController implements IAbilityHandler { @@ -26,6 +29,7 @@ public class AbilityController implements IAbilityHandler { private final Map> externalVariants = new LinkedHashMap<>(); private final List fieldProviders = new ArrayList<>(); private final List extenders = new ArrayList<>(); + private final List> flightCheckers = new ArrayList<>(); // ── Derived State ──────────────────────────────────────────────────────── private final Set builtInTypeIds = new HashSet<>(); @@ -382,6 +386,18 @@ public class AbilityController implements IAbilityHandler { return extenders; } + public void registerFlightChecker(Predicate checker) { + flightCheckers.add(checker); + } + + public boolean isPlayerFlying(EntityPlayer player) { + if (player.capabilities.isFlying) return true; + for (Predicate checker : flightCheckers) { + if (checker.test(player)) return true; + } + return false; + } + /** * Fire onAbilityStart on all extenders. Returns false if ANY extender cancels. */ diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 8c9bde6ac..c0405c193 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -9,6 +9,7 @@ import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.util.AttributeAttackUtil; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; @@ -106,7 +107,9 @@ public class ScriptPlayerEventHandler { Ability current = playerData.abilityData.getCurrentAbility(); if (current == null || !current.hasAbilityMovement()) { player.motionX = 0; - player.motionY = Math.min(player.motionY, 0); // Prevent jumping, allow gravity + if (!AbilityController.Instance.isPlayerFlying(player)) { + player.motionY = Math.min(player.motionY, 0); + } player.motionZ = 0; player.velocityChanged = true; } diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index d12921756..d993762ee 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -6,6 +6,7 @@ import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; import kamkeel.npcs.client.renderer.lightning.LightningBolt; +import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; @@ -133,9 +134,10 @@ public class ClientTickHandler { mc.thePlayer.motionZ = 0; } - // Prevent jumping: clamp upward motion when locked (allow gravity) + // Prevent jumping when locked (allow gravity only if not flying) if ((ClientAbilityState.movementLocked || ClientAbilityState.positionLocked) - && !ClientAbilityState.hasAbilityMovement) { + && !ClientAbilityState.hasAbilityMovement + && !AbilityController.Instance.isPlayerFlying(mc.thePlayer)) { mc.thePlayer.motionY = Math.min(mc.thePlayer.motionY, 0); } } From cd28f69a0e108f026f693e398c078b547751b3f0 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 16 Feb 2026 01:39:14 -0500 Subject: [PATCH 18/20] Enhance ability validation: add method to check if an ability can be resolved without deep copy and prune invalid unlocked abilities during load --- .../data/ability/AbilityController.java | 31 +++++++++++++++++++ .../controllers/data/PlayerAbilityData.java | 23 ++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 43af10b7c..ae21a9598 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -340,6 +340,37 @@ public class AbilityController implements IAbilityHandler { return builtAbilities.containsKey(key) || customAbilities.containsKey(key); } + /** + * Check if a key can be resolved to a valid ability without creating a deep copy. + * Uses the same lookup chain as {@link #resolveAbility(String)}. + */ + public boolean canResolveAbility(String key) { + if (key == null || key.isEmpty()) return false; + + // Built-in: exact name + if (builtAbilities.containsKey(key)) return true; + + // Built-in: case-insensitive name + for (String name : builtAbilities.keySet()) { + if (name.equalsIgnoreCase(key)) return true; + } + + // Built-in: registry key / ID + for (Ability ability : builtAbilities.values()) { + if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) return true; + } + + // Custom: exact UUID + if (customAbilities.containsKey(key)) return true; + + // Custom: case-insensitive name + for (Ability ability : customAbilities.values()) { + if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) return true; + } + + return false; + } + // ═══════════════════════════════════════════════════════════════════════════ // EXTENSION POINTS // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 6ced6f68a..d7f543726 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -15,6 +15,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.nbt.NBTTagString; import net.minecraft.util.DamageSource; +import noppes.npcs.LogWriter; import noppes.npcs.api.ability.IPlayerAbilityData; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; @@ -23,6 +24,7 @@ import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; /** @@ -860,9 +862,30 @@ public class PlayerAbilityData implements IPlayerAbilityData { } } selectedIndex = compound.getInteger("PlayerAbilitySelected"); + + // Prune abilities that no longer exist (deleted custom abilities or removed built-ins) + validateUnlockedAbilities(); + if (selectedIndex >= unlockedAbilities.size()) { selectedIndex = Math.max(0, unlockedAbilities.size() - 1); } playingAbilityAnimation = compound.getBoolean("AbilityAnimating"); } + + /** + * Remove any unlocked ability keys that can no longer be resolved. + * Called during load to clean up references to deleted abilities. + */ + private void validateUnlockedAbilities() { + if (AbilityController.Instance == null) return; + + Iterator it = unlockedAbilities.iterator(); + while (it.hasNext()) { + String key = it.next(); + if (!AbilityController.Instance.canResolveAbility(key)) { + it.remove(); + LogWriter.info("Removed invalid ability reference from player data: " + key); + } + } + } } From 6c8e15c0148a1a3f2348b0a9b1f9a6b861ecb7d7 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 16 Feb 2026 01:40:53 -0500 Subject: [PATCH 19/20] Update api --- src/api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api b/src/api index 66fff09d2..efa8721d3 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 66fff09d2078a9d85a6ef27ef98c02a5944eb325 +Subproject commit efa8721d3658b45146d58d6731a0b83c4860f02e From 01e18e0221c1166c1cae67e500228035c99261fb Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 16 Feb 2026 01:49:57 -0500 Subject: [PATCH 20/20] Beta 3 --- build.gradle | 2 +- src/main/java/noppes/npcs/CustomNpcs.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7421f7cf7..0babda6bb 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'idea' } -version = "1.11-beta1" +version = "1.11-beta3" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 933450557..caf86c7b5 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -118,7 +118,7 @@ import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; -@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta1") +@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta3") public class CustomNpcs { @SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy")