From af02b0ab1d6dbd82daff76b7b43a10aea547064d Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Feb 2026 16:28:38 -0500 Subject: [PATCH] Various Fixes --- src/api | 2 +- .../client/renderer/RenderAbilityDisc.java | 11 +- .../controllers/data/ability/Ability.java | 154 +++++++++++++---- .../data/ability/RotationMode.java | 53 ++++++ .../data/ability/gui/AbilityFieldDefs.java | 1 + .../data/ability/type/AbilityBeam.java | 9 + .../data/ability/type/AbilityBeamDual.java | 12 ++ .../data/ability/type/AbilityDisc.java | 17 +- .../data/ability/type/AbilityDiscDual.java | 27 ++- .../data/ability/type/AbilityLaserShot.java | 15 +- .../data/ability/type/AbilityOrb.java | 6 + .../data/ability/type/AbilityOrbBarrage.java | 6 + .../data/ability/type/AbilityOrbDual.java | 12 ++ .../npcs/entity/EntityAbilityDisc.java | 31 +++- .../npcs/entity/EntityAbilityLaser.java | 153 ++++++++++++----- .../kamkeel/npcs/util/AnchorPointHelper.java | 38 ++++- src/main/java/noppes/npcs/DataAbilities.java | 155 +++++++++++------- .../gui/util/AbilityPreviewExecutor.java | 35 +++- .../npcs/entity/EntityNPCInterface.java | 4 +- .../assets/customnpcs/lang/en_US.lang | 21 ++- 20 files changed, 599 insertions(+), 163 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/RotationMode.java diff --git a/src/api b/src/api index f383fc303..fe1cec898 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit f383fc3037221791e2f22a272580df9be013810f +Subproject commit fe1cec89814290a745ac708b309703125b5a3c15 diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java index 20486d70e..8cb8cdaad 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -43,7 +43,16 @@ public class RenderAbilityDisc extends RenderAbilityProjectile { GL11.glPushMatrix(); GL11.glScalef(scale, scale, scale); - // Apply ONLY Y-axis rotation for flat spin + if (disc.isVertical()) { + // Vertical mode: thin edge faces direction of travel (like a saw blade) + // 1. Spin in local space (Y-axis) + // 2. Tilt 90° to stand up (X-axis) + // 3. Orient to face travel direction (Y-axis world yaw) + GL11.glRotatef(disc.getTravelYaw(), 0.0f, 1.0f, 0.0f); + GL11.glRotatef(90.0f, 1.0f, 0.0f, 0.0f); + } + + // Apply Y-axis rotation for spin GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); // Inner scale defines the core size 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 e6404a0cb..65d941327 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -67,7 +67,8 @@ public abstract class Ability implements IAbility { // Feedback protected LockMovementType lockMovement = LockMovementType.WINDUP; - protected LockType lockType = LockType.BOTH; + protected RotationMode rotationMode = RotationMode.FREE; + protected LockMovementType rotationPhase = LockMovementType.WINDUP_AND_ACTIVE; protected int windUpColor = 0x80FF4400; // Telegraph color during wind up protected int activeColor = 0xC0FF0000; // Telegraph warning/active color @@ -418,26 +419,33 @@ public abstract class Ability implements IAbility { defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks) .tab("General").range(0, 10000)); defs.add(FieldDef.section("ability.section.movement").tab("General")); - defs.add(FieldDef.row( - FieldDef.stringEnumField("ability.lockPosition", LockMovementType.getDisplayKeys(), + defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(), () -> this.getLockMovement().getDisplayKey(), v -> { - String[] keys = LockMovementType.getDisplayKeys(); - for (int i = 0; i < keys.length; i++) { - if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; } + for (LockMovementType t : LockMovementType.values()) { + if (t.getDisplayKey().equals(v)) { this.setLockMovement(t); break; } } }) - .hover("ability.hover.lockPosition"), - FieldDef.stringEnumField("ability.lockType", LockType.getDisplayKeys(), - () -> this.getLockType().getDisplayKey(), + .hover("ability.hover.lockMovement") + .tab("General")); + defs.add(FieldDef.row( + FieldDef.stringEnumField("ability.rotationMode", RotationMode.getDisplayKeys(), + () -> this.getRotationMode().getDisplayKey(), v -> { - String[] keys = LockType.getDisplayKeys(); - for (int i = 0; i < keys.length; i++) { - if (keys[i].equals(v)) { this.setLockType(LockType.fromOrdinal(i)); break; } + for (RotationMode m : RotationMode.values()) { + if (m.getDisplayKey().equals(v)) { this.setRotationMode(m); break; } } }) - .hover("ability.hover.lockType") - .visibleWhen(() -> this.lockMovement != LockMovementType.NO) + .hover("ability.hover.rotationMode"), + FieldDef.stringEnumField("ability.rotationPhase", getRotationPhaseKeys(), + () -> this.getRotationPhase().getDisplayKey(), + v -> { + for (LockMovementType t : LockMovementType.values()) { + if (t.getDisplayKey().equals(v)) { this.setRotationPhase(t); break; } + } + }) + .hover("ability.hover.rotationPhase") + .visibleWhen(() -> this.rotationMode != RotationMode.FREE) ).tab("General")); defs.add(FieldDef.row( FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) @@ -606,12 +614,12 @@ public abstract class Ability implements IAbility { instance.setEntityIdToFollow(caster.getEntityId()); // For LINE/CONE telegraphs: track target direction during windup - // Only track if movement is NOT locked during windup (locked = NPC can't rotate) + // Only track if rotation is NOT locked during windup (NPC can still turn) if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) { - if (!lockMovement.locksWindup()) { + if (!isRotationLockedDuringWindup()) { instance.setTargetEntityId(target.getEntityId()); } - // If windup is locked, yaw stays fixed at creation time + // If rotation is locked, yaw stays fixed at creation time } } else if (target != null) { // AOE_TARGET abilities: telegraph follows target during windup @@ -954,7 +962,8 @@ public abstract class Ability implements IAbility { nbt.setInteger("recovery", dazedTicks); nbt.setBoolean("interruptible", interruptible); nbt.setInteger("lockMovement", lockMovement.ordinal()); - nbt.setInteger("lockType", lockType.ordinal()); + nbt.setInteger("rotationMode", rotationMode.ordinal()); + nbt.setInteger("rotationPhase", rotationPhase.ordinal()); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); nbt.setString("windUpSound", windUpSound); @@ -1005,7 +1014,30 @@ public abstract class Ability implements IAbility { dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); - lockType = nbt.hasKey("lockType") ? LockType.fromOrdinal(nbt.getInteger("lockType")) : LockType.BOTH; + if (nbt.hasKey("rotationMode")) { + // Current format + rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode")); + rotationPhase = LockMovementType.fromOrdinal(nbt.getInteger("rotationPhase")); + } else if (nbt.hasKey("lockType")) { + // Legacy format migration: lockType was LockType enum (0=MOVEMENT, 1=ROTATION, 2=BOTH) + int oldLockType = nbt.getInteger("lockType"); + if (oldLockType == 0) { + // MOVEMENT only: movement locked, rotation was free + rotationMode = RotationMode.FREE; + } else if (oldLockType == 1) { + // ROTATION only: rotation locked same timing as old lockMovement, movement was free + rotationMode = RotationMode.LOCKED; + rotationPhase = lockMovement; + lockMovement = LockMovementType.NO; + } else { + // BOTH: both locked same timing + rotationMode = RotationMode.LOCKED; + rotationPhase = lockMovement; + } + } else { + rotationMode = RotationMode.FREE; + rotationPhase = LockMovementType.WINDUP_AND_ACTIVE; + } windUpColor = nbt.getInteger("windUpColor"); activeColor = nbt.getInteger("activeColor"); windUpSound = nbt.getString("windUpSound"); @@ -1170,12 +1202,20 @@ public abstract class Ability implements IAbility { this.lockMovement = lockMovement; } - public LockType getLockType() { - return lockType; + public RotationMode getRotationMode() { + return rotationMode; } - public void setLockType(LockType lockType) { - this.lockType = lockType; + public void setRotationMode(RotationMode rotationMode) { + this.rotationMode = rotationMode; + } + + public LockMovementType getRotationPhase() { + return rotationPhase; + } + + public void setRotationPhase(LockMovementType rotationPhase) { + this.rotationPhase = rotationPhase; } /** @@ -1197,21 +1237,39 @@ public abstract class Ability implements IAbility { } /** - * API method: Get lock type as integer. - * @return 0=MOVEMENT, 1=ROTATION, 2=BOTH + * API method: Get rotation mode as integer. + * @return 0=FREE, 1=LOCKED, 2=TRACK */ @Override - public int getLockTypeOrdinal() { - return lockType.ordinal(); + public int getRotationModeType() { + return rotationMode.ordinal(); } /** - * API method: Set lock type from integer. - * @param type 0=MOVEMENT, 1=ROTATION, 2=BOTH + * API method: Set rotation mode from integer. + * @param type 0=FREE, 1=LOCKED, 2=TRACK */ @Override - public void setLockTypeOrdinal(int type) { - this.lockType = LockType.fromOrdinal(type); + public void setRotationModeType(int type) { + this.rotationMode = RotationMode.fromOrdinal(type); + } + + /** + * API method: Get rotation phase as integer. + * @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public int getRotationPhaseType() { + return rotationPhase.ordinal(); + } + + /** + * API method: Set rotation phase from integer. + * @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public void setRotationPhaseType(int type) { + this.rotationPhase = LockMovementType.fromOrdinal(type); } /** @@ -1219,7 +1277,7 @@ public abstract class Ability implements IAbility { */ @Override public boolean isMovementLockedDuringWindup() { - return lockMovement.locksWindup() && lockType.locksMovement(); + return lockMovement.locksWindup(); } /** @@ -1227,7 +1285,7 @@ public abstract class Ability implements IAbility { */ @Override public boolean isMovementLockedDuringActive() { - return lockMovement.locksActive() && lockType.locksMovement(); + return lockMovement.locksActive(); } /** @@ -1235,7 +1293,7 @@ public abstract class Ability implements IAbility { */ @Override public boolean isRotationLockedDuringWindup() { - return lockMovement.locksWindup() && lockType.locksRotation(); + return rotationMode == RotationMode.LOCKED && rotationPhase.locksWindup(); } /** @@ -1243,7 +1301,7 @@ public abstract class Ability implements IAbility { */ @Override public boolean isRotationLockedDuringActive() { - return lockMovement.locksActive() && lockType.locksRotation(); + return rotationMode == RotationMode.LOCKED && rotationPhase.locksActive(); } /** @@ -1274,6 +1332,21 @@ public abstract class Ability implements IAbility { } } + /** + * Check if hit scan (force face target) is active during the current phase. + */ + public boolean isHitScanForCurrentPhase() { + if (rotationMode != RotationMode.TRACK) return false; + switch (phase) { + case WINDUP: + return rotationPhase.locksWindup(); + case ACTIVE: + return rotationPhase.locksActive(); + default: + return false; + } + } + public int getWindUpColor() { return windUpColor; } @@ -1457,6 +1530,17 @@ public abstract class Ability implements IAbility { effects.clear(); } + /** + * Get display keys for rotation phase dropdown (excludes "None" since mode=FREE handles that). + */ + private static String[] getRotationPhaseKeys() { + return new String[]{ + "ability.lockMove.windup", + "ability.lockMove.active", + "ability.lockMove.both" + }; + } + // ═══════════════════════════════════════════════════════════════════ // SHIT FROM THE ASS // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/RotationMode.java b/src/main/java/kamkeel/npcs/controllers/data/ability/RotationMode.java new file mode 100644 index 000000000..e1142b571 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/RotationMode.java @@ -0,0 +1,53 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines what happens to NPC rotation during ability phases. + * Used with a LockMovementType to control WHEN it applies. + */ +public enum RotationMode { + /** + * NPC rotates normally via AI look helper. + */ + FREE, + + /** + * Freeze rotation at the moment the phase starts. + * NPC stays facing the direction it was looking when the phase began. + */ + LOCKED, + + /** + * Force NPC to continuously face its target (hit scan). + * Overrides AI look helper every tick for precise aiming. + */ + TRACK; + + public String getDisplayKey() { + switch (this) { + case FREE: + return "ability.rotation.free"; + case LOCKED: + return "ability.rotation.locked"; + case TRACK: + return "ability.rotation.track"; + default: + return "ability.rotation.free"; + } + } + + public static String[] getDisplayKeys() { + return new String[]{ + "ability.rotation.free", + "ability.rotation.locked", + "ability.rotation.track" + }; + } + + public static RotationMode fromOrdinal(int ordinal) { + RotationMode[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return FREE; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java index bc1dc9ab8..6e419b943 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java @@ -23,4 +23,5 @@ public class AbilityFieldDefs { () -> getter.get(), v -> setter.accept((List) v)); } + } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 8e3bfc575..356c1c4e9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.RotationMode; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.controllers.data.telegraph.Telegraph; @@ -57,6 +58,8 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { this.cooldownTicks = 0; this.windUpTicks = 40; this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.rotationMode = RotationMode.TRACK; + this.rotationPhase = LockMovementType.WINDUP_AND_ACTIVE; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; // Default built-in animations @@ -313,6 +316,12 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), + FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) + .tab("ability.tab.visual"), 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/AbilityBeamDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java index 89c5b126f..daf04a446 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java @@ -439,6 +439,12 @@ public class AbilityBeamDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(0), v -> setInnerColor(0, v)) .tab("ability.tab.visual"), @@ -475,6 +481,12 @@ public class AbilityBeamDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(1), v -> setInnerColor(1, v)) .tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 77d137eb9..98fe9eaac 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -32,6 +32,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { // Disc geometry private float discRadius = 1.0f; private float discThickness = 0.2f; + private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward) // Boomerang properties private boolean boomerang = false; @@ -107,9 +108,9 @@ public class AbilityDisc extends Ability implements IAbilityDisc { boomerang, boomerangDelay); if (isPreview()) { - discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks); + discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks, vertical); } else { - discEntity.setupCharging(anchorData, windUpTicks); + discEntity.setupCharging(anchorData, windUpTicks, vertical); } discEntity.setEffects(this.effects); @@ -176,6 +177,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("discRadius", discRadius); nbt.setFloat("discThickness", discThickness); + nbt.setBoolean("vertical", vertical); nbt.setBoolean("boomerang", boomerang); nbt.setInteger("boomerangDelay", boomerangDelay); anchorData.writeNBT(nbt); @@ -190,6 +192,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void readTypeNBT(NBTTagCompound nbt) { this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical"); this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; anchorData.readNBT(nbt); @@ -207,6 +210,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } public float getDiscThickness() { return discThickness; } public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public boolean isVertical() { return vertical; } + public void setVertical(boolean vertical) { this.vertical = vertical; } public float getMaxDistance() { return lifespanData.maxDistance; } public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } public int getMaxLifetime() { return lifespanData.maxLifetime; } @@ -286,6 +291,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius), FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness) ), + FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical) + .hover("ability.hover.vertical"), FieldDef.row( FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) @@ -309,6 +316,12 @@ public class AbilityDisc extends Ability implements IAbilityDisc { // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), + FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) + .tab("ability.tab.visual"), 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/AbilityDiscDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java index 2fbd6cbda..87d7fffd5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java @@ -28,6 +28,7 @@ public class AbilityDiscDual extends Ability { // Disc geometry private float discRadius = 1.0f; private float discThickness = 0.2f; + private boolean vertical = false; // Boomerang properties private boolean boomerang = false; @@ -144,11 +145,11 @@ public class AbilityDiscDual extends Ability { discEntity2.setSiblingEntityId(discEntity.getEntityId()); if (isPreview()) { - discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks); - discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks); + discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks, vertical); + discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks, vertical); } else { - discEntity.setupCharging(anchorData[0], windUpTicks); - discEntity2.setupCharging(anchorData[1], windUpTicks); + discEntity.setupCharging(anchorData[0], windUpTicks, vertical); + discEntity2.setupCharging(anchorData[1], windUpTicks, vertical); } discEntity.setEffects(this.effects); @@ -244,6 +245,7 @@ public class AbilityDiscDual extends Ability { public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("discRadius", discRadius); nbt.setFloat("discThickness", discThickness); + nbt.setBoolean("vertical", vertical); nbt.setBoolean("boomerang", boomerang); nbt.setInteger("boomerangDelay", boomerangDelay); nbt.setBoolean("dualFire", dualFire); @@ -274,6 +276,7 @@ public class AbilityDiscDual extends Ability { this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical"); this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); @@ -315,6 +318,8 @@ public class AbilityDiscDual extends Ability { public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } public float getDiscThickness() { return discThickness; } public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public boolean isVertical() { return vertical; } + public void setVertical(boolean vertical) { this.vertical = vertical; } public float getMaxDistance() { return lifespanData.maxDistance; } public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } public int getMaxLifetime() { return lifespanData.maxLifetime; } @@ -426,6 +431,8 @@ public class AbilityDiscDual extends Ability { FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius), FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness) ), + FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical) + .hover("ability.hover.vertical"), FieldDef.row( FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) @@ -455,6 +462,12 @@ public class AbilityDiscDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(0), v -> setInnerColor(0, v)) .tab("ability.tab.visual"), @@ -491,6 +504,12 @@ public class AbilityDiscDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(1), v -> setInnerColor(1, v)) .tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index d211de6d2..d5b48f1fd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.RotationMode; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.data.*; import noppes.npcs.client.gui.builder.FieldDef; @@ -38,7 +39,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { private final EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f); private final EnergyLightningData lightningData = new EnergyLightningData(); private final EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100); - private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); + private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData(); // Transient state for laser entity (used for movement locking) @@ -53,6 +54,8 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { this.cooldownTicks = 0; this.windUpTicks = 15; this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.rotationMode = RotationMode.TRACK; + this.rotationPhase = LockMovementType.WINDUP; this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; // Default built-in animations @@ -118,10 +121,8 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { return; } + laserEntity.setLockVerticalDirection(true); laserEntity.startMoving(target); - - // Ability stays active until entity dies (prevents firing another while projectile is alive) - // Movement locking is handled separately by the base class } @Override @@ -269,6 +270,12 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), + FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) + .tab("ability.tab.visual"), FieldDef.section("ability.section.colors").tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"), FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 6a87b38b4..1f1cb5e7d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -446,6 +446,12 @@ public class AbilityOrb extends Ability implements IAbilityOrb { // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), + FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) + .tab("ability.tab.visual"), FieldDef.section("ability.section.colors").tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"), FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java index dda01f68a..f384c4b71 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java @@ -581,6 +581,12 @@ public class AbilityOrbBarrage extends Ability { // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), + FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) + .tab("ability.tab.visual"), FieldDef.section("ability.section.colors").tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"), FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java index 7b8362464..3cabf4892 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java @@ -511,6 +511,12 @@ public class AbilityOrbDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(0), v -> setInnerColor(0, v)) .tab("ability.tab.visual"), @@ -547,6 +553,12 @@ public class AbilityOrbDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(1), v -> setInnerColor(1, v)) .tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 3578ac9bf..edd7b3dff 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -31,6 +31,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { // Disc shape properties private float discRadius = 1.0f; // Width of disc private float discThickness = 0.2f; // Height of disc + private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward) // Charging state (during windup) private boolean charging = false; @@ -122,11 +123,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { * The disc will grow from 0 to discRadius over chargeDuration ticks. * Position follows the owner based on anchor point. */ - public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { + public void setupCharging(EnergyAnchorData anchor, int chargeDuration, boolean vertical) { setCharging(true); this.chargeDuration = chargeDuration; this.chargeTick = 0; this.anchorData = anchor; + this.vertical = vertical; this.targetDiscRadius = this.discRadius; this.targetDiscThickness = this.discThickness; this.discRadius = 0.01f; @@ -144,7 +146,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { * Follows anchor point and animations like in the real game. * Can be fired when transitioning to active phase. */ - public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { + public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, boolean vertical) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -157,6 +159,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.chargeDuration = chargeDuration; this.chargeTick = 0; this.anchorData = anchor; + this.vertical = vertical; // Store target size and start at 0 for grow effect this.targetDiscRadius = discRadius; @@ -541,6 +544,28 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return returning; } + public boolean isVertical() { + return vertical; + } + + /** + * Get the yaw angle (degrees) from motion vector for vertical disc orientation. + * During charging, uses owner's facing direction. + */ + public float getTravelYaw() { + if (isCharging()) { + Entity owner = previewMode ? previewOwner : getOwnerEntity(); + if (owner != null) { + return owner.rotationYaw; + } + } + double speed = Math.sqrt(motionX * motionX + motionZ * motionZ); + if (speed > 0.001) { + return (float) (Math.atan2(-motionX, motionZ) * 180.0 / Math.PI); + } + return 0.0f; + } + // ==================== NBT ==================== @Override @@ -549,6 +574,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.boomerangDelay = nbt.hasKey("BoomerangDelay") ? nbt.getInteger("BoomerangDelay") : 40; this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f; this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f; + this.vertical = nbt.hasKey("Vertical") ? nbt.getBoolean("Vertical") : false; this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning"); // Charging state boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); @@ -566,6 +592,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { nbt.setInteger("BoomerangDelay", boomerangDelay); nbt.setFloat("DiscRadius", discRadius); nbt.setFloat("DiscThickness", discThickness); + nbt.setBoolean("Vertical", vertical); nbt.setBoolean("Returning", returning); // Charging state nbt.setBoolean("Charging", isCharging()); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 9c09bd467..8a1325a27 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -35,6 +35,9 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { // Direction (normalized) private double dirX, dirY, dirZ; + // Lock vertical direction after firing (only update yaw, keep pitch fixed) + private boolean lockVerticalDirection = false; + // Track hit entities to avoid double-damage private Set hitEntities = new HashSet<>(); @@ -165,6 +168,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { return; } + // Track owner's anchor position and rotation each tick + // so the laser follows the NPC's facing direction + updateLaserOriginAndDirection(); + if (!fullyExtended) { // Expand the laser currentLength += expansionSpeed; @@ -187,27 +194,76 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } else { // Laser is fully extended, count down linger time ticksSinceFullExtension++; + + // Update end point during linger (origin/direction may still change) + endX = startX + dirX * currentLength; + endY = startY + dirY * currentLength; + endZ = startZ + dirZ * currentLength; + if (ticksSinceFullExtension >= lingerTicks) { this.setDead(); } } } + /** + * Update laser origin and direction from the owner's current anchor position and rotation. + * Called each tick so the laser follows the NPC when tracking a target. + */ + private void updateLaserOriginAndDirection() { + Entity owner = getOwnerEntity(); + if (owner == null || owner.isDead) return; + if (!(owner instanceof EntityLivingBase)) return; + + EntityLivingBase livingOwner = (EntityLivingBase) owner; + + // Update origin from anchor point + if (anchorData != null) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData); + startX = pos.xCoord; + startY = pos.yCoord; + startZ = pos.zCoord; + } else { + startX = owner.posX; + startY = owner.posY + owner.height * 0.7; + startZ = owner.posZ; + } + + // Update direction from owner's current rotation + float yaw = (float) Math.toRadians(owner.rotationYaw); + if (lockVerticalDirection) { + // Only update horizontal direction (yaw), keep vertical (pitch) fixed + double horizontalLen = Math.sqrt(dirX * dirX + dirZ * dirZ); + if (horizontalLen < 0.001) horizontalLen = 1.0; + dirX = -Math.sin(yaw) * horizontalLen; + dirZ = Math.cos(yaw) * horizontalLen; + } else { + float pitch = (float) Math.toRadians(owner.rotationPitch); + dirX = -Math.sin(yaw) * Math.cos(pitch); + dirY = -Math.sin(pitch); + dirZ = Math.cos(yaw) * Math.cos(pitch); + } + + // Keep entity positioned at origin + prevPosX = startX; + prevPosY = startY; + prevPosZ = startZ; + setPosition(startX, startY, startZ); + } + public void startMoving(EntityLivingBase target) { if (!isCharging()) return; setCharging(false); - // Update start position to current position + // Initialize from current anchor position + // updateLaserOriginAndDirection() will keep these updated each tick startX = posX; startY = posY; startZ = posZ; - // Always derive direction from the owner's current yaw/pitch. - // This ensures the laser fires in the direction the NPC is visually facing, - // which matches the telegraph line direction regardless of Lock Movement mode. + // Set initial direction from owner's rotation Entity owner = getOwnerEntity(); - if (owner != null) { float yaw = (float) Math.toRadians(owner.rotationYaw); float pitch = (float) Math.toRadians(owner.rotationPitch); @@ -215,7 +271,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { this.dirY = -Math.sin(pitch); this.dirZ = Math.cos(yaw) * Math.cos(pitch); } else if (target != null) { - // Fallback: aim at target if owner is somehow unavailable double dx = target.posX - startX; double dy = (target.posY + target.getEyeHeight() - 0.4) - startY; double dz = target.posZ - startZ; @@ -227,12 +282,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } } - setPosition(endX, endY, endZ); - - // Initialize end point - this.endX = posX; - this.endY = posY; - this.endZ = posZ; + // Initialize end point at start (will expand from here) + this.endX = startX; + this.endY = startY; + this.endZ = startZ; } private void checkBlockCollision() { @@ -308,14 +361,14 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } private void checkEntityCollisionAlongLine() { - // Check collision along the entire laser line - // Use a slightly expanded AABB for the full line - double minX = Math.min(startX, endX) - laserWidth; - double minY = Math.min(startY, endY) - laserWidth; - double minZ = Math.min(startZ, endZ) - laserWidth; - double maxX = Math.max(startX, endX) + laserWidth; - double maxY = Math.max(startY, endY) + laserWidth; - double maxZ = Math.max(startZ, endZ) + laserWidth; + // Expand search AABB by entity sizes to ensure we find all potential targets + double expand = Math.max(laserWidth, 1.0); + double minX = Math.min(startX, endX) - expand; + double minY = Math.min(startY, endY) - expand; + double minZ = Math.min(startZ, endZ) - expand; + double maxX = Math.max(startX, endX) + expand; + double maxY = Math.max(startY, endY) + expand; + double maxZ = Math.max(startZ, endZ) + expand; AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ); @@ -326,7 +379,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { if (shouldIgnoreEntity(entity)) continue; if (hitEntities.contains(entity.getEntityId())) continue; - // Check if entity is close to the laser line + // Check if entity's bounding box intersects with the laser line if (isEntityOnLine(entity)) { hitEntities.add(entity.getEntityId()); applyDamage(entity); @@ -337,42 +390,64 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } /** - * Check if an entity intersects with the laser line. + * Check if an entity's bounding box intersects with the laser line. + * Uses separate XZ (horizontal) and Y (vertical) checks against + * the entity's full bounding box rather than just its center point. */ private boolean isEntityOnLine(EntityLivingBase entity) { - // Get entity center double ex = entity.posX; - double ey = entity.posY + entity.height * 0.5; double ez = entity.posZ; - // Vector from start to entity + // Project entity XZ position onto the laser line direction double vx = ex - startX; - double vy = ey - startY; double vz = ez - startZ; - - // Project onto line direction - double dot = vx * dirX + vy * dirY + vz * dirZ; - - // Clamp to line segment + // Project using horizontal direction components only + double dirLenXZ = Math.sqrt(dirX * dirX + dirZ * dirZ); + double dot; + if (dirLenXZ > 0.001) { + dot = (vx * dirX + vz * dirZ) / (dirLenXZ * dirLenXZ) * dirLenXZ; + // Remap to full 3D parameter + dot = (vx * dirX + vz * dirZ + (entity.posY + entity.height * 0.5 - startY) * dirY); + } else { + // Laser fires nearly straight up/down - use full 3D projection + dot = vx * dirX + (entity.posY + entity.height * 0.5 - startY) * dirY + vz * dirZ; + } dot = Math.max(0, Math.min(dot, currentLength)); - // Closest point on line + // Closest point on laser line at this parameter double closestX = startX + dirX * dot; double closestY = startY + dirY * dot; double closestZ = startZ + dirZ * dot; - // Distance from entity to closest point - double distSq = (ex - closestX) * (ex - closestX) + - (ey - closestY) * (ey - closestY) + - (ez - closestZ) * (ez - closestZ); + // XZ distance check: entity center vs laser line point + // Use minimum collision width so visually thin lasers still hit reliably + double xzDistSq = (ex - closestX) * (ex - closestX) + (ez - closestZ) * (ez - closestZ); + double effectiveWidth = Math.max(laserWidth, 0.5); + double hitRadiusXZ = effectiveWidth * 0.5 + entity.width * 0.5; + if (xzDistSq > hitRadiusXZ * hitRadiusXZ) return false; - // Hit if within laser width + entity radius - double hitRadius = laserWidth * 0.5 + entity.width * 0.5; - return distSq <= hitRadius * hitRadius; + // Y overlap check: laser point vs entity's full height range + double entityMinY = entity.boundingBox.minY; + double entityMaxY = entity.boundingBox.maxY; + double laserHalfWidth = effectiveWidth * 0.5; + + // Check if laser Y is within entity's height range (expanded by laser half-width) + if (closestY < entityMinY - laserHalfWidth) return false; + if (closestY > entityMaxY + laserHalfWidth) return false; + + return true; } // ==================== GETTERS FOR RENDERER ==================== + public void setLockVerticalDirection(boolean lock) { + this.lockVerticalDirection = lock; + } + + public boolean isLockVerticalDirection() { + return lockVerticalDirection; + } + public float getLaserWidth() { return laserWidth; } diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 3ec5560ba..5ee5936b1 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -100,9 +100,12 @@ public class AnchorPointHelper { break; } - x += anchorX * scale; - y += anchorY * scale; - z += anchorZ * scale; + // Rotate offsets relative to entity's body yaw + // +X = entity's right, +Y = up, +Z = entity's forward + Vec3 rotatedOffset = rotateOffsetByYaw(anchorX * scale, anchorY * scale, anchorZ * scale, bodyYaw); + x += rotatedOffset.xCoord; + y += rotatedOffset.yCoord; + z += rotatedOffset.zCoord; return Vec3.createVectorHelper(x, y, z); } @@ -246,9 +249,11 @@ public class AnchorPointHelper { double worldY = entity.posY + shoulderHeight - blockY; double worldZ = entity.posZ + worldOffsetZ; - worldX += anchor.anchorOffsetX * scale; - worldY += anchor.anchorOffsetY * scale; - worldZ += anchor.anchorOffsetZ * scale; + // Rotate offsets relative to entity's body yaw + Vec3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw); + worldX += rotatedOffset.xCoord; + worldY += rotatedOffset.yCoord; + worldZ += rotatedOffset.zCoord; return Vec3.createVectorHelper(worldX, worldY, worldZ); } @@ -317,9 +322,11 @@ public class AnchorPointHelper { double y = entity.posY + entity.height * FALLBACK_ARM_HEIGHT + armLengthOffset; double z = entity.posZ + lateralZ + forwardZ; - x += anchor.anchorOffsetX * scale; - y += anchor.anchorOffsetY * scale; - z += anchor.anchorOffsetZ * scale; + // Rotate offsets relative to entity's body yaw + Vec3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw); + x += rotatedOffset.xCoord; + y += rotatedOffset.yCoord; + z += rotatedOffset.zCoord; return Vec3.createVectorHelper(x, y, z); } @@ -344,4 +351,17 @@ public class AnchorPointHelper { return FRONT_HEIGHT; } } + + /** + * Rotate an offset vector by the entity's body yaw so offsets are entity-relative. + * +X = entity's right, +Y = up, +Z = entity's forward. + * @param bodyYawRad body yaw in radians + */ + private static Vec3 rotateOffsetByYaw(double offsetX, double offsetY, double offsetZ, float bodyYawRad) { + double cos = Math.cos(bodyYawRad); + double sin = Math.sin(bodyYawRad); + double worldX = -offsetX * cos - offsetZ * sin; + double worldZ = -offsetX * sin + offsetZ * cos; + return Vec3.createVectorHelper(worldX, offsetY, worldZ); + } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 19dc59ecd..606df7742 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -100,6 +100,14 @@ public class DataAbilities { private transient double lockedPosY = 0; private transient double lockedPosZ = 0; + /** + * Hit scan state - forces NPC to face target. + * Set during tickCurrentAbility(), applied after super.onLivingUpdate() + * to override AI look helper rotation. + */ + private transient boolean hitScanActive = false; + private transient EntityLivingBase hitScanTarget = null; + // ═══════════════════════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════════════════════ @@ -126,13 +134,13 @@ public class DataAbilities { if (currentAbility != null) { removeTelegraph(currentAbility); stopAbilityAnimation(); - releaseLockedRotation(); + releaseRotationControl(); releaseLockedPosition(); currentAbility = null; lastTarget = null; } else { // No ability but locks could be orphaned - if (rotationLocked) releaseLockedRotation(); + if (rotationLocked || hitScanActive) releaseRotationControl(); if (positionLocked) releaseLockedPosition(); } return; @@ -140,7 +148,7 @@ public class DataAbilities { // Safety: release orphaned locks if no ability is actively executing if (currentAbility == null || !currentAbility.isExecuting()) { - if (rotationLocked) releaseLockedRotation(); + if (rotationLocked || hitScanActive) releaseRotationControl(); if (positionLocked) releaseLockedPosition(); } @@ -181,13 +189,13 @@ public class DataAbilities { // Remove telegraph - it has served its purpose removeTelegraph(currentAbility); - // Handle rotation lock transition from WINDUP to ACTIVE + // Handle rotation control transition from WINDUP to ACTIVE if (currentAbility.isRotationLockedDuringActive()) { if (!rotationLocked) { captureLockedRotation(); } } else if (rotationLocked) { - releaseLockedRotation(); + releaseRotationControl(); } // Handle position lock transition from WINDUP to ACTIVE @@ -203,6 +211,11 @@ public class DataAbilities { playAbilitySound(currentAbility.getActiveSound()); playAbilityAnimation(currentAbility.getActiveAnimation()); + // Snap NPC to face target before execute so startMoving() reads correct rotation + if (currentAbility.isHitScanForCurrentPhase() && target != null) { + faceTarget(target); + } + // Fire execute event (cancelable) AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent( npc.wrappedNPC, currentAbility, target); @@ -239,6 +252,15 @@ public class DataAbilities { break; } + // Update hit scan state - actual facing is deferred to applyRotationControl() + // which runs AFTER super.onLivingUpdate() to override AI look helper + if (currentAbility != null && currentAbility.isExecuting() + && currentAbility.isHitScanForCurrentPhase() && target != null) { + enableHitScan(target); + } else if (hitScanActive) { + releaseRotationControl(); + } + // Apply movement control if ability is still executing if (currentAbility != null && currentAbility.isExecuting()) { applyMovementControl(); @@ -284,7 +306,7 @@ public class DataAbilities { NpcAPI.EVENT_BUS.post(completeEvent); // Release locks before cleanup - releaseLockedRotation(); + releaseRotationControl(); releaseLockedPosition(); // Clean up and roll cooldown (this sets currentAbility to null) @@ -301,7 +323,7 @@ public class DataAbilities { stopAbilityAnimation(); // Release locks - releaseLockedRotation(); + releaseRotationControl(); releaseLockedPosition(); // Calculate cooldown: random(min, max) + ability offset @@ -679,7 +701,7 @@ public class DataAbilities { stopAbilityAnimation(); // Release locks - releaseLockedRotation(); + releaseRotationControl(); releaseLockedPosition(); // Fire interrupt event @@ -702,7 +724,7 @@ public class DataAbilities { if (currentAbility != null) { removeTelegraph(currentAbility); stopAbilityAnimation(); - releaseLockedRotation(); + releaseRotationControl(); releaseLockedPosition(); currentAbility.interrupt(); currentAbility = null; @@ -1000,15 +1022,15 @@ public class DataAbilities { } // ═══════════════════════════════════════════════════════════════════ - // ROTATION LOCKING + // ROTATION CONTROL // ═══════════════════════════════════════════════════════════════════ - /** Bit flag for rotation lock in the existing data watcher slot 15 bitfield */ - private static final int ROTATION_LOCKED_FLAG = 16; + /** Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 */ + private static final int ROTATION_CONTROLLED_FLAG = 16; /** * Capture current rotation values to lock NPC's look direction. - * Called when entering a locked phase (WINDUP or ACTIVE depending on LockMovementType). + * Called when entering a LOCKED rotation phase. */ private void captureLockedRotation() { lockedYaw = npc.rotationYaw; @@ -1016,41 +1038,79 @@ public class DataAbilities { lockedRenderYawOffset = npc.renderYawOffset; lockedPitch = npc.rotationPitch; rotationLocked = true; - npc.setBoolFlag(true, ROTATION_LOCKED_FLAG); + npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG); } /** - * Release the rotation lock. - * Called when leaving the locked phase or ability completes. + * Release rotation control (both locked and hit scan). */ - private void releaseLockedRotation() { + private void releaseRotationControl() { rotationLocked = false; - npc.setBoolFlag(false, ROTATION_LOCKED_FLAG); + hitScanActive = false; + hitScanTarget = null; + npc.setBoolFlag(false, ROTATION_CONTROLLED_FLAG); } /** - * Apply locked rotation values to the NPC. - * Called AFTER super.onLivingUpdate() in EntityNPCInterface to override - * any rotation changes made by the look helper, AI tasks, or body smoothing. - * Runs on both client and server. + * Enable hit scan tracking for the given target. */ - public void applyLockedRotation() { + private void enableHitScan(EntityLivingBase target) { + if (!hitScanActive) { + hitScanActive = true; + npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG); + } + hitScanTarget = target; + } + + /** + * Snap the NPC to face the target instantly. + */ + private void faceTarget(EntityLivingBase target) { + double dx = target.posX - npc.posX; + double dz = target.posZ - npc.posZ; + double dy = (target.posY + target.getEyeHeight() * 0.5) - (npc.posY + npc.getEyeHeight()); + double distXZ = Math.sqrt(dx * dx + dz * dz); + + float yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); + float pitch = (float) (-(Math.atan2(dy, distXZ)) * 180.0 / Math.PI); + + npc.rotationYaw = yaw; + npc.rotationYawHead = yaw; + npc.renderYawOffset = yaw; + npc.rotationPitch = pitch; + npc.prevRotationYaw = yaw; + npc.prevRotationYawHead = yaw; + npc.prevRenderYawOffset = yaw; + npc.prevRotationPitch = pitch; + } + + /** + * Apply rotation control after super.onLivingUpdate() and super.onUpdate(). + * Handles both LOCKED (freeze at captured values) and TRACK (face target) modes. + * + * Server: computes the correct rotation (locked values or target facing). + * Client: trusts the server-synced rotation values and prevents body smoothing override. + */ + public void applyRotationControl() { if (npc.worldObj.isRemote) { - // Client-side: check bit 16 in the existing flags data watcher (slot 15) - boolean flagActive = npc.getBoolFlag(ROTATION_LOCKED_FLAG); - if (!flagActive) { + // Client: check single flag for any rotation control + if (!npc.getBoolFlag(ROTATION_CONTROLLED_FLAG)) { rotationLocked = false; return; } - // First tick the flag is active: capture current rotation values - if (!rotationLocked) { - lockedYaw = npc.rotationYaw; - lockedYawHead = npc.rotationYawHead; - lockedRenderYawOffset = npc.renderYawOffset; - lockedPitch = npc.rotationPitch; - rotationLocked = true; - } - // Force all rotation values back to locked state + // Trust server rotation, prevent body smoothing from overriding + npc.prevRotationYaw = npc.rotationYaw; + npc.prevRotationYawHead = npc.rotationYawHead; + npc.renderYawOffset = npc.rotationYawHead; + npc.prevRenderYawOffset = npc.rotationYawHead; + npc.prevRotationPitch = npc.rotationPitch; + return; + } + + // Server: apply the appropriate rotation + if (hitScanActive && hitScanTarget != null && !hitScanTarget.isDead) { + faceTarget(hitScanTarget); + } else if (rotationLocked) { npc.rotationYaw = lockedYaw; npc.rotationYawHead = lockedYawHead; npc.renderYawOffset = lockedRenderYawOffset; @@ -1059,39 +1119,22 @@ public class DataAbilities { npc.prevRotationYawHead = lockedYawHead; npc.prevRenderYawOffset = lockedRenderYawOffset; npc.prevRotationPitch = lockedPitch; - return; } - - // Server-side: use stored locked values - if (!rotationLocked) { - return; - } - - // Force rotation back to locked values - npc.rotationYaw = lockedYaw; - npc.rotationYawHead = lockedYawHead; - npc.renderYawOffset = lockedRenderYawOffset; - npc.rotationPitch = lockedPitch; - - // Also set prev values to prevent interpolation jitter - npc.prevRotationYaw = lockedYaw; - npc.prevRotationYawHead = lockedYawHead; - npc.prevRenderYawOffset = lockedRenderYawOffset; - npc.prevRotationPitch = lockedPitch; } /** - * Check if rotation is currently locked. + * Check if rotation is currently controlled (locked or tracking target). + * Used by AI tasks to determine if they should try to change NPC look direction. */ public boolean isRotationLocked() { - return rotationLocked; + return rotationLocked || hitScanActive; } // ═══════════════════════════════════════════════════════════════════ // POSITION LOCKING // ═══════════════════════════════════════════════════════════════════ - /** Bit flag for position lock in the existing data watcher slot 15 bitfield */ + /** Bit flag for position lock in data watcher slot 15 */ private static final int POSITION_LOCKED_FLAG = 32; /** diff --git a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java index 2da4a1a7b..7d856d916 100644 --- a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java +++ b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java @@ -169,6 +169,9 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { public void pause() { paused = true; + // Immediately sync prevPos to pos for NPC and all entities to prevent interpolation jitter + syncPrevPositions(); + if (previewNpc != null) { AnimationData data = previewNpc.display.animationData; if (data.animation != null) { @@ -177,6 +180,25 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { } } + /** + * Sync prevPos to pos for the NPC and all preview entities. + * Prevents rendering interpolation jitter when standing still or paused. + */ + private void syncPrevPositions() { + if (previewNpc != null) { + previewNpc.prevPosX = previewNpc.posX; + previewNpc.prevPosY = previewNpc.posY; + previewNpc.prevPosZ = previewNpc.posZ; + } + for (Entity entity : previewEntities) { + if (entity != null && !entity.isDead) { + entity.prevPosX = entity.posX; + entity.prevPosY = entity.posY; + entity.prevPosZ = entity.posZ; + } + } + } + public void stop() { // Clean up the ability if (previewAbility != null) { @@ -228,10 +250,21 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { * Tick the preview forward. Calls real ability methods. */ public void tick() { - if (!playing || paused || previewAbility == null || previewNpc == null) { + if (!playing || previewAbility == null || previewNpc == null) { return; } + if (paused) { + // Sync prevPos to pos for NPC and all entities to prevent interpolation jitter while paused + syncPrevPositions(); + return; + } + + // Sync NPC's prevPos to current pos BEFORE any updates to prevent jitter when standing still + previewNpc.prevPosX = previewNpc.posX; + previewNpc.prevPosY = previewNpc.posY; + previewNpc.prevPosZ = previewNpc.posZ; + currentTick++; if (previewTelegraph != null) { diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 00de6d405..faaa2c9f5 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -312,7 +312,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti // Re-apply locks after super.onUpdate() to override body rotation smoothing // in EntityLivingBase.onUpdate() (func_110146_f) which runs AFTER onLivingUpdate() // and overwrites renderYawOffset, causing a visible twitch each frame. - abilities.applyLockedRotation(); + abilities.applyRotationControl(); abilities.applyLockedPosition(); if (!isRemote()) { @@ -489,7 +489,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti super.onLivingUpdate(); // Apply locks after super.onLivingUpdate() to override AI/physics changes - abilities.applyLockedRotation(); + abilities.applyRotationControl(); abilities.applyLockedPosition(); handleMountRiderState(); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 7317856c9..497134b16 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1632,11 +1632,12 @@ ability.cnpc.sweeper=Sweeper ability.weight=Weight ability.windUpTicks=Wind Up ability.cooldownTicks=Cooldown -ability.lockPosition=Lock Position -ability.lockType=Lock Type -ability.lockType.movement=Movement -ability.lockType.rotation=Rotation -ability.lockType.both=Both +ability.lockMovement=Lock Movement +ability.rotationMode=Rotation +ability.rotationPhase=Phase +ability.rotation.free=Free +ability.rotation.locked=Locked +ability.rotation.track=Track ability.interruptible=Interruptible ability.dazedTicks=Dazed Ticks ability.targetingMode=Targeting @@ -1724,6 +1725,10 @@ ability.effects=Effects ability.mode=Mode ability.duration=Duration ability.anchorPoint=Anchor Point +ability.anchor.offsetX=Offset X +ability.anchor.offsetY=Offset Y +ability.anchor.offsetZ=Offset Z +ability.vertical=Vertical ability.rotationSpeed=Rotation Speed # Slam @@ -1864,8 +1869,9 @@ ability.noTypeSettings=No type-specific settings # ═══════════════════════════════════════════════════════════════ ability.hover.weight=Higher weight = more likely to be chosen when multiple abilities available -ability.hover.lockPosition=Prevents NPC from moving or rotating while ability is charging or active -ability.hover.lockType=What to lock: Movement (pathfinding), Rotation (facing direction), or Both +ability.hover.lockMovement=Prevents NPC pathfinding during the selected phases (Windup, Active, or Both) +ability.hover.rotationMode=Free: normal AI rotation. Locked: freeze facing direction. Track: force face target for precise aiming. +ability.hover.rotationPhase=When the rotation mode applies (Windup, Active, or Both) ability.hover.interruptible=If enabled, ability can be interrupted by taking damage ability.hover.dazed=Stun duration in ticks if ability is interrupted (20 ticks = 1 second) ability.hover.windup=Charge-up time before ability activates (20 ticks = 1 second) @@ -1886,6 +1892,7 @@ ability.hover.piercing=Hits multiple targets instead of stopping on first hit ability.hover.lightning=Add electrical effect to visuals ability.hover.outerGlow=Enable outer glow layer around projectile ability.hover.anchorPoint=Where on the NPC the ability spawns from +ability.hover.vertical=Disc flies standing up with thin edge facing target (like a saw blade) ability.hover.dualCharge=Whether or not should create two anchor points ability.hover.damage=Base damage dealt to targets ability.hover.speed=Movement speed of projectile