diff --git a/build.gradle b/build.gradle index 46c55e203..35dede377 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ plugins { id "dts.typescript-generator" } -version = "1.11-beta2" +version = "1.11-beta3" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" diff --git a/src/api b/src/api index 2b95d3ca4..efa8721d3 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 2b95d3ca47558e804a09e4f7b7f45aeb69fa7861 +Subproject commit efa8721d3658b45146d58d6731a0b83c4860f02e 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/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/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 1878337b8..11ceffea8 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) { @@ -913,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/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index b65276211..c492cf50b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -2,14 +2,18 @@ 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; 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 { @@ -24,7 +28,8 @@ 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<>(); + private final List> flightCheckers = new ArrayList<>(); // ── Derived State ──────────────────────────────────────────────────────── private final Set builtInTypeIds = new HashSet<>(); @@ -303,7 +308,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; } @@ -317,9 +328,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; @@ -329,6 +344,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 // ═══════════════════════════════════════════════════════════════════════════ @@ -345,19 +391,16 @@ 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; } - public void registerDamageHandler(IAbilityDamageHandler handler) { - this.damageHandler = handler; - } - - public IAbilityDamageHandler getDamageHandler() { - return damageHandler; - } - public void registerFieldProvider(IAbilityFieldProvider provider) { fieldProviders.add(provider); } @@ -366,6 +409,76 @@ public class AbilityController implements IAbilityHandler { return fieldProviders; } + public void registerExtender(IAbilityExtender extender) { + extenders.add(extender); + } + + public List getExtenders() { + 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. + */ + 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/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/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/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/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/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; + } +} 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/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/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 0fb58bcfe..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; @@ -386,33 +387,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; } /** @@ -551,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; @@ -600,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. @@ -696,6 +705,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 +717,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 +755,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/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 43fa459fc..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; @@ -391,19 +395,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); @@ -419,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; } @@ -435,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 a10dae0b8..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(); } } @@ -156,8 +160,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; @@ -237,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) + @@ -356,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 9b30abab6..99c66edaa 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; @@ -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 @@ -184,16 +224,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; + } } } @@ -204,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; } @@ -222,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); + } } /** @@ -324,19 +379,25 @@ 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 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) { @@ -508,16 +569,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); @@ -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 ==================== @@ -898,10 +934,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); @@ -967,7 +1003,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 @@ -977,7 +1015,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/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/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/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); + } } diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 364598166..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-beta2") +@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta3") public class CustomNpcs { @SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy") diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 8edcba628..78692162f 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: @@ -262,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())) { @@ -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/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/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 909634a09..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,6 +107,9 @@ public class ScriptPlayerEventHandler { Ability current = playerData.abilityData.getCurrentAbility(); if (current == null || !current.hasAbilityMovement()) { player.motionX = 0; + if (!AbilityController.Instance.isPlayerFlying(player)) { + player.motionY = Math.min(player.motionY, 0); + } player.motionZ = 0; player.velocityChanged = true; } @@ -328,6 +332,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 +488,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 +549,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 +650,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 4c6b7a044..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; @@ -13,6 +14,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; @@ -63,7 +65,9 @@ public class ClientTickHandler { if (event.phase == Phase.START) { EntityPlayer player = mc.thePlayer; if (player != null) { - boolean specialKeyDown = mc.currentScreen == null && 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) { @@ -73,13 +77,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; @@ -98,8 +116,43 @@ 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 when locked (allow gravity only if not flying) + if ((ClientAbilityState.movementLocked || ClientAbilityState.positionLocked) + && !ClientAbilityState.hasAbilityMovement + && !AbilityController.Instance.isPlayerFlying(mc.thePlayer)) { + 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; } @@ -157,8 +210,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) @@ -169,10 +223,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/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/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index f8ed53c7c..2e89d3d49 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(); @@ -489,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); } } 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() { } 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 0f87ddf5a..60386b8bf 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -15,14 +15,21 @@ 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.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; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; /** @@ -54,6 +61,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) // ═══════════════════════════════════════════════════════════════════ @@ -147,6 +161,16 @@ 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())) { + interruptCurrentAbility(); + return; + } + switch (currentAbility.getPhase()) { case WINDUP: if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) { @@ -197,6 +221,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); @@ -213,7 +242,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())) { @@ -249,8 +278,14 @@ 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); + // Fire complete event + firePlayerCompleteEvent(player, currentAbility, currentTarget); + // Release all locks releaseRotationControl(); releaseLockedPosition(); @@ -318,6 +353,16 @@ 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; + } + + // Fire start event (cancelable) + if (firePlayerStartEvent(player, ability)) { + return false; // Cancelled + } + // Start the ability currentAbility = ability; currentAbilityKey = key; @@ -358,6 +403,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); @@ -516,7 +566,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(); @@ -569,7 +629,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { // Check for ability interruption if (currentAbility != null && currentAbility.canInterrupt(source)) { - interruptCurrentAbility(); + interruptCurrentAbility(source, amount); } return amount; @@ -674,6 +734,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; } @@ -761,6 +822,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { playerData.animationData.setEnabled(true); playerData.animationData.setAnimation(animation); playerData.animationData.updateClient(); + playingAbilityAnimation = true; } public void playAbilityAnimation(int animation) { @@ -782,6 +844,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) { @@ -790,6 +869,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 // ═══════════════════════════════════════════════════════════════════ @@ -801,6 +947,7 @@ public class PlayerAbilityData implements IPlayerAbilityData { } compound.setTag("PlayerAbilities", list); compound.setInteger("PlayerAbilitySelected", selectedIndex); + compound.setBoolean("AbilityAnimating", playingAbilityAnimation); } public void readFromNBT(NBTTagCompound compound) { @@ -815,8 +962,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); + } + } } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index fa4e7d961..7f326a344 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(); @@ -116,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(); } 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/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; + } } 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. diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index a29759c31..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 @@ -1910,6 +1916,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