diff --git a/src/api b/src/api index 5eb98d172..fff8a25e0 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 5eb98d172eedfa0727e8ec9769bc2edad36a1ce4 +Subproject commit fff8a25e0edfb4557b8ff1afa3b2fefeed94aa9f diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyLaser.java index 1befc0dcc..469076d17 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyLaser.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyLaser.java @@ -33,13 +33,13 @@ public class RenderEnergyLaser extends RenderEnergy { return; } - // Get laser parameters - double startX = laser.getStartX(); - double startY = laser.getStartY(); - double startZ = laser.getStartZ(); - double endX = laser.getEndX(); - double endY = laser.getEndY(); - double endZ = laser.getEndZ(); + // Get interpolated laser positions for smooth rendering between ticks + double startX = laser.getInterpolatedStartX(partialTicks); + double startY = laser.getInterpolatedStartY(partialTicks); + double startZ = laser.getInterpolatedStartZ(partialTicks); + double endX = laser.getInterpolatedEndX(partialTicks); + double endY = laser.getInterpolatedEndY(partialTicks); + double endZ = laser.getInterpolatedEndZ(partialTicks); float width = laser.getLaserWidth(); float alpha = 1.0f; 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 2ed1e54a1..0d62d9ae7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -106,7 +106,7 @@ public abstract class Ability implements IAbility, IAbilityAction { protected LockMode lockMovement = LockMode.WINDUP; protected RotationMode rotationMode = RotationMode.FREE; protected LockMode rotationPhase = LockMode.WINDUP_AND_ACTIVE; - protected float trackDelay = 0; // 0 = instant tracking (legacy). Higher = slower lerp toward target. + protected float trackSpeed = 0; // 0 = instant tracking. Linear tracking speed in blocks/second. protected int windUpColor = 0x80FF4400; // Telegraph color during wind up protected int activeColor = 0xC0FF0000; // Telegraph warning/active color @@ -771,9 +771,9 @@ public abstract class Ability implements IAbility, IAbilityAction { .hover("ability.hover.rotationPhase") .visibleWhen(() -> this.rotationMode != RotationMode.FREE) ).tab("General")); - defs.add(FieldDef.floatField("ability.trackDelay", this::getTrackDelay, this::setTrackDelay) - .range(0.0f, 20.0f) - .hover("ability.hover.trackDelay") + defs.add(FieldDef.floatField("ability.trackSpeed", this::getTrackSpeed, this::setTrackSpeed) + .range(0.0f, 12.0f) + .hover("ability.hover.trackSpeed") .visibleWhen(() -> this.rotationMode == RotationMode.TRACK) .tab("General")); defs.add(FieldDef.row( @@ -1516,7 +1516,7 @@ public abstract class Ability implements IAbility, IAbilityAction { nbt.setInteger("lockMovement", lockMovement.ordinal()); nbt.setInteger("rotationMode", rotationMode.ordinal()); nbt.setInteger("rotationPhase", rotationPhase.ordinal()); - nbt.setFloat("trackDelay", trackDelay); + nbt.setFloat("trackSpeed", trackSpeed); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); nbt.setString("windUpSound", windUpSound); @@ -1613,7 +1613,7 @@ public abstract class Ability implements IAbility, IAbilityAction { lockMovement = LockMode.fromOrdinal(nbt.getInteger("lockMovement")); rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode")); rotationPhase = LockMode.fromOrdinal(nbt.getInteger("rotationPhase")); - trackDelay = nbt.hasKey("trackDelay") ? nbt.getFloat("trackDelay") : 0; + trackSpeed = nbt.hasKey("trackSpeed") ? nbt.getFloat("trackSpeed") : 0; windUpColor = nbt.hasKey("windUpColor") ? nbt.getInteger("windUpColor") : 0x80FF4400; activeColor = nbt.hasKey("activeColor") ? nbt.getInteger("activeColor") : 0xC0FF0000; windUpSound = nbt.getString("windUpSound"); @@ -1958,14 +1958,15 @@ public abstract class Ability implements IAbility, IAbilityAction { this.rotationPhase = rotationPhase; } - public float getTrackDelay() { - return trackDelay; + public float getTrackSpeed() { + return trackSpeed; } - public void setTrackDelay(float trackDelay) { - this.trackDelay = trackDelay; + public void setTrackSpeed(float trackSpeed) { + this.trackSpeed = trackSpeed; } + /** * API method: Get lock movement type as integer. * @@ -2027,22 +2028,22 @@ public abstract class Ability implements IAbility, IAbilityAction { } /** - * API method: Get track delay for TRACK rotation mode. - * 0 = instant tracking (legacy). Higher = slower lerp toward target. + * API method: Get track speed for TRACK rotation mode (blocks/second). + * 0 = instant tracking. Comparable to player sprint speed (~5.6 blocks/sec). */ @Override - public float getTrackDelayValue() { - return trackDelay; + public float getTrackSpeedValue() { + return trackSpeed; } /** - * API method: Set track delay for TRACK rotation mode. + * API method: Set track speed for TRACK rotation mode (blocks/second). * - * @param delay 0 = instant tracking. Higher = slower lerp toward target. + * @param speed 0 = instant tracking. ~4 = dodgeable while sprinting. ~6 = barely undodgeable. */ @Override - public void setTrackDelayValue(float delay) { - this.trackDelay = Math.max(0, delay); + public void setTrackSpeedValue(float speed) { + this.trackSpeed = Math.max(0, speed); } /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java index e9c4e0c16..40d30be43 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java @@ -49,7 +49,7 @@ public class AbilityLaser extends AbilityEnergyProjectile im this.lockMovement = LockMode.WINDUP_AND_ACTIVE; this.rotationMode = RotationMode.TRACK; this.rotationPhase = LockMode.WINDUP_AND_ACTIVE; - this.trackDelay = 3; + this.trackSpeed = 4.0f; this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; this.windUpAnimationName = "Ability_Laser_Windup"; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 2e39d3c49..e0a1a24d3 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -47,6 +47,10 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { // End point for rendering private double endX, endY, endZ; + // Previous tick positions for partial tick interpolation + private double prevStartX, prevStartY, prevStartZ; + private double prevEndX, prevEndY, prevEndZ; + // Block hit deduplication — prevents event/explosion spam every tick private int lastBlockHitX = Integer.MIN_VALUE; private int lastBlockHitY = Integer.MIN_VALUE; @@ -107,10 +111,16 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { } } - // Initialize end point + // Initialize end point and prev positions this.endX = x; this.endY = y; this.endZ = z; + this.prevStartX = x; + this.prevStartY = y; + this.prevStartZ = z; + this.prevEndX = x; + this.prevEndY = y; + this.prevEndZ = z; } @Override @@ -138,6 +148,14 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { return; } + // Save previous positions for partial tick interpolation + prevStartX = startX; + prevStartY = startY; + prevStartZ = startZ; + prevEndX = endX; + prevEndY = endY; + prevEndZ = endZ; + // Origin/direction tracking runs on BOTH sides so the client can render the beam. // Once reflected, never resume owner-tracking — the laser flies in a fixed direction. if (!reflected) { @@ -285,6 +303,14 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { this.endX = startX; this.endY = startY; this.endZ = startZ; + + // Initialize prev positions to current so first frame doesn't jump + this.prevStartX = startX; + this.prevStartY = startY; + this.prevStartZ = startZ; + this.prevEndX = startX; + this.prevEndY = startY; + this.prevEndZ = startZ; } private boolean setDirectionTowardTarget(EntityLivingBase target, double sourceX, double sourceY, double sourceZ) { @@ -733,6 +759,30 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { return endZ; } + public double getInterpolatedStartX(float partialTicks) { + return prevStartX + (startX - prevStartX) * partialTicks; + } + + public double getInterpolatedStartY(float partialTicks) { + return prevStartY + (startY - prevStartY) * partialTicks; + } + + public double getInterpolatedStartZ(float partialTicks) { + return prevStartZ + (startZ - prevStartZ) * partialTicks; + } + + public double getInterpolatedEndX(float partialTicks) { + return prevEndX + (endX - prevEndX) * partialTicks; + } + + public double getInterpolatedEndY(float partialTicks) { + return prevEndY + (endY - prevEndY) * partialTicks; + } + + public double getInterpolatedEndZ(float partialTicks) { + return prevEndZ + (endZ - prevEndZ) * partialTicks; + } + public double getDirX() { return dirX; } @@ -761,6 +811,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { this.endX = posX; this.endY = posY; this.endZ = posZ; + this.prevStartX = posX; + this.prevStartY = posY; + this.prevStartZ = posZ; + this.prevEndX = posX; + this.prevEndY = posY; + this.prevEndZ = posZ; } // ==================== REFLECTION SYNC ==================== @@ -862,6 +918,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { this.endX = posX; this.endY = posY; this.endZ = posZ; + this.prevStartX = posX; + this.prevStartY = posY; + this.prevStartZ = posZ; + this.prevEndX = posX; + this.prevEndY = posY; + this.prevEndZ = posZ; // Precompute initial direction from owner look for launch. Vec3 look = owner == null ? null : owner.getLookVec(); diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java index 63f632b5b..244ed91d1 100644 --- a/src/main/java/noppes/npcs/AbstractDataAbilities.java +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -785,6 +785,9 @@ public abstract class AbstractDataAbilities { // Fire tick event fireTickEvent(currentAbility, target); + // Script may have completed/cancelled the ability during the tick event + if (currentAbility == null || !currentAbility.isExecuting()) return; + // Fire extender tick hook (e.g., per-tick resource drain) if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, entity, target, currentAbility.getPhase(), currentAbility.getCurrentTick())) { diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index a26f38544..74b8b057f 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -88,7 +88,11 @@ public class DataAbilities extends AbstractDataAbilities { */ private transient boolean hitScanActive = false; private transient EntityLivingBase hitScanTarget = null; - private transient float currentTrackDelay = 0; + private transient float currentTrackSpeed = 0; + private transient float trackedYaw = 0; + private transient float trackedPitch = 0; + private transient boolean trackLerpedThisTick = false; + private transient boolean preserveTrackedRotation = false; /** * Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 @@ -219,19 +223,28 @@ public class DataAbilities extends AbstractDataAbilities { @Override protected void onPreExecute(Ability ability, EntityLivingBase target) { - // Snap NPC to face target before execute so startMoving() reads correct rotation + // Face target before execute so startMoving() reads correct rotation + // Use trackSpeed so the NPC plays catchup rather than instant-snapping if (ability.isHitScanForCurrentPhase() && target != null) { - faceTarget(target); + float speed = ability.getTrackSpeed(); + if (speed <= 0) { + faceTarget(target); + } else { + faceTarget(target, speed); + } } } @Override protected void onPostPhaseTick(Ability ability, EntityLivingBase target) { + // Reset per-tick lerp flag so faceTarget only computes the lerp once per tick + trackLerpedThisTick = false; + // Update hit scan state - actual facing is deferred to applyRotationControl() // which runs AFTER super.onLivingUpdate() to override AI look helper if (ability != null && ability.isExecuting() && target != null) { if (ability.isHitScanForCurrentPhase()) { - enableHitScan(target, ability.getTrackDelay()); + enableHitScan(target, ability.getTrackSpeed()); } else { // Release hit scan if it was previously active if (hitScanActive) { @@ -254,7 +267,10 @@ public class DataAbilities extends AbstractDataAbilities { @Override protected void onBurstDelayReleaseLocks() { - if (hitScanActive) releaseRotationControl(); + if (hitScanActive) { + preserveTrackedRotation = true; + releaseRotationControl(); + } } @Override @@ -881,31 +897,44 @@ public class DataAbilities extends AbstractDataAbilities { /** * Enable hit scan tracking for the given target. */ - private void enableHitScan(EntityLivingBase target, float trackDelay) { + private void enableHitScan(EntityLivingBase target, float trackSpeed) { if (!hitScanActive) { hitScanActive = true; npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG); + // Preserve tracked rotation across burst delays so the NPC doesn't snap + if (!preserveTrackedRotation) { + trackedYaw = npc.rotationYawHead; + trackedPitch = npc.rotationPitch; + } + preserveTrackedRotation = false; } hitScanTarget = target; - currentTrackDelay = trackDelay; + currentTrackSpeed = trackSpeed; } + private static final double DEG_TO_RAD = Math.PI / 180.0; + private static final double RAD_TO_DEG = 180.0 / Math.PI; + /** - * Face the target with optional track delay. - * When trackDelay <= 0, snaps instantly (legacy behavior). - * When trackDelay > 0, lerps toward the target each tick. + * Face the target with optional track speed (linear speed in blocks/second). + * When trackSpeed <= 0, snaps instantly. + * When trackSpeed > 0, the NPC's aim point moves at trackSpeed blocks/second, + * converted to angular velocity based on distance. This makes tracking feel + * consistent regardless of range — directly comparable to player movement speed. */ - private void faceTarget(EntityLivingBase target, float trackDelay) { + private void faceTarget(EntityLivingBase target, float trackSpeed) { 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 targetYaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); - float targetPitch = (float) (-(Math.atan2(dy, distXZ)) * 180.0 / Math.PI); + float targetYaw = (float) (Math.atan2(-dx, dz) * RAD_TO_DEG); + float targetPitch = (float) (-(Math.atan2(dy, distXZ)) * RAD_TO_DEG); - if (trackDelay <= 0) { - // Instant snap (legacy behavior) + if (trackSpeed <= 0) { + // Instant snap + trackedYaw = targetYaw; + trackedPitch = targetPitch; npc.rotationYaw = targetYaw; npc.rotationYawHead = targetYaw; npc.renderYawOffset = targetYaw; @@ -915,14 +944,28 @@ public class DataAbilities extends AbstractDataAbilities { npc.prevRenderYawOffset = targetYaw; npc.prevRotationPitch = targetPitch; } else { - float lerpFactor = 1.0f / (1.0f + trackDelay); + if (!trackLerpedThisTick) { + trackLerpedThisTick = true; - // Wrap yaw difference to [-180, 180] for correct interpolation - float yawDiff = wrapAngle(targetYaw - npc.rotationYawHead); - float pitchDiff = targetPitch - npc.rotationPitch; + // Convert linear speed (blocks/sec) to angular speed (degrees/tick) + // using the actual distance to the target + double dist = Math.max(1.0, Math.sqrt(dx * dx + dy * dy + dz * dz)); + float blocksPerTick = trackSpeed / 20.0f; + float maxAngularSpeed = (float) (Math.atan2(blocksPerTick, dist) * RAD_TO_DEG); - float newYaw = npc.rotationYawHead + yawDiff * lerpFactor; - float newPitch = npc.rotationPitch + pitchDiff * lerpFactor; + float yawDiff = wrapAngle(targetYaw - trackedYaw); + float pitchDiff = targetPitch - trackedPitch; + + float totalAngle = (float) Math.sqrt(yawDiff * yawDiff + pitchDiff * pitchDiff); + if (totalAngle > maxAngularSpeed) { + float scale = maxAngularSpeed / totalAngle; + yawDiff *= scale; + pitchDiff *= scale; + } + + trackedYaw = trackedYaw + yawDiff; + trackedPitch = trackedPitch + pitchDiff; + } // Set prev to current before updating (for smooth client interpolation) npc.prevRotationYaw = npc.rotationYaw; @@ -930,10 +973,10 @@ public class DataAbilities extends AbstractDataAbilities { npc.prevRenderYawOffset = npc.renderYawOffset; npc.prevRotationPitch = npc.rotationPitch; - npc.rotationYaw = newYaw; - npc.rotationYawHead = newYaw; - npc.renderYawOffset = newYaw; - npc.rotationPitch = newPitch; + npc.rotationYaw = trackedYaw; + npc.rotationYawHead = trackedYaw; + npc.renderYawOffset = trackedYaw; + npc.rotationPitch = trackedPitch; } } @@ -976,7 +1019,7 @@ public class DataAbilities extends AbstractDataAbilities { // Server: apply the appropriate rotation if (hitScanActive && hitScanTarget != null && !hitScanTarget.isDead) { - faceTarget(hitScanTarget, currentTrackDelay); + faceTarget(hitScanTarget, currentTrackSpeed); } else if (rotationLocked) { npc.rotationYaw = lockedYaw; npc.rotationYawHead = lockedYawHead; diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java index 787c27ee3..88dbdeadd 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBeam.java @@ -3,7 +3,6 @@ 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 { @@ -60,27 +59,7 @@ public class ScriptEnergyBeam extends ScriptEnergyP // ==================== FIRE ==================== @Override - public void fireAt(IEntity target) { - ensureSpawned(); - EntityLivingBase mcTarget = null; - if (target != null && target.getMCEntity() instanceof EntityLivingBase) { - mcTarget = (EntityLivingBase) target.getMCEntity(); - } - entity.startFiring(mcTarget); - } - - @Override - public void fireAt(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); + protected void launchFromOwner(EntityLivingBase target) { + entity.startFiring(target); } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java index 34ebb6305..66244c2fd 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyDisc.java @@ -3,7 +3,6 @@ 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 { @@ -76,27 +75,7 @@ public class ScriptEnergyDisc extends ScriptEnergyP // ==================== FIRE ==================== @Override - public void fireAt(IEntity target) { - ensureSpawned(); - EntityLivingBase mcTarget = null; - if (target != null && target.getMCEntity() instanceof EntityLivingBase) { - mcTarget = (EntityLivingBase) target.getMCEntity(); - } - entity.startMoving(mcTarget); - } - - @Override - public void fireAt(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); + protected void launchFromOwner(EntityLivingBase target) { + entity.startMoving(target); } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java index eb6159bf5..485f1c666 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyLaser.java @@ -3,7 +3,6 @@ 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 { @@ -92,18 +91,12 @@ public class ScriptEnergyLaser extends ScriptEnerg // ==================== FIRE ==================== @Override - public void fireAt(IEntity target) { - ensureSpawned(); - EntityLivingBase mcTarget = null; - if (target != null && target.getMCEntity() instanceof EntityLivingBase) { - mcTarget = (EntityLivingBase) target.getMCEntity(); - } - entity.startMoving(mcTarget); + protected void launchFromOwner(EntityLivingBase target) { + entity.startMoving(target); } @Override public void fireAt(double x, double y, double z) { - ensureSpawned(); double dx = x - entity.posX; double dy = y - entity.posY; double dz = z - entity.posZ; @@ -111,6 +104,6 @@ public class ScriptEnergyLaser extends ScriptEnerg if (len > 0) { entity.setDirection(dx / len, dy / len, dz / len); } - entity.startMoving(null); + ensureSpawned(); } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java index 3821190cb..af290b8b3 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java @@ -3,7 +3,6 @@ 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 { @@ -28,27 +27,7 @@ public class ScriptEnergyOrb extends ScriptEnergyPro } @Override - public void fireAt(IEntity target) { - ensureSpawned(); - EntityLivingBase mcTarget = null; - if (target != null && target.getMCEntity() instanceof EntityLivingBase) { - mcTarget = (EntityLivingBase) target.getMCEntity(); - } - entity.startMoving(mcTarget); - } - - @Override - public void fireAt(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); + protected void launchFromOwner(EntityLivingBase target) { + entity.startMoving(target); } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java index a4ca6697b..2397f2a2e 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java @@ -1,7 +1,6 @@ package noppes.npcs.scripted.entity; import kamkeel.npcs.entity.EntityEnergyProjectile; -import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import noppes.npcs.EventHooks; import noppes.npcs.api.entity.IEnergyProjectile; @@ -261,8 +260,14 @@ public class ScriptEnergyProjectile extends Sc public void fireAt(IEntity target) { if (target != null) { - Entity mcTarget = target.getMCEntity(); - setMotionToward(mcTarget.posX, mcTarget.posY + mcTarget.getEyeHeight(), mcTarget.posZ); + EntityLivingBase living = resolveLivingTarget(target); + if (living != null) { + entity.setTargetEntityId(living.getEntityId()); + setMotionToward(living.posX, living.posY + living.getEyeHeight(), living.posZ); + } else { + net.minecraft.entity.Entity mcTarget = target.getMCEntity(); + setMotionToward(mcTarget.posX, mcTarget.posY + mcTarget.getEyeHeight(), mcTarget.posZ); + } } ensureSpawned(); } @@ -273,24 +278,22 @@ public class ScriptEnergyProjectile extends Sc } public void fireDirection(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(); + setMotionFromDirection(yaw, pitch); ensureSpawned(); } public void fireFrom(IEntityLivingBase caster) { if (caster == null) return; initFromCaster(caster); - fireDirection(caster.getRotation(), caster.getPitch()); + launchFromOwner(null); + ensureSpawned(); } public void fireFrom(IEntityLivingBase caster, IEntity target) { if (caster == null) return; initFromCaster(caster); - fireAt(target); + launchFromOwner(resolveLivingTarget(target)); + ensureSpawned(); } /** @@ -304,6 +307,37 @@ public class ScriptEnergyProjectile extends Sc entity.setStartPosition(mc.posX, eyeY, mc.posZ); } + /** + * Hook for subclasses to call their entity's full launch sequence (startMoving/startFiring). + * Called by fireFrom methods — handles look-vector snap, charge exit, homing init, etc. + * Default: sets motion toward target, or along owner's look direction if target is null. + */ + protected void launchFromOwner(EntityLivingBase target) { + if (target != null) { + setMotionToward(target.posX, target.posY + target.getEyeHeight(), target.posZ); + } else { + net.minecraft.entity.Entity owner = entity.getOwnerEntity(); + if (owner != null) { + setMotionFromDirection(owner.rotationYaw, owner.rotationPitch); + } + } + } + + private void setMotionFromDirection(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(); + } + + protected static EntityLivingBase resolveLivingTarget(IEntity target) { + if (target != null && target.getMCEntity() instanceof EntityLivingBase) { + return (EntityLivingBase) target.getMCEntity(); + } + return null; + } + /** * Calculates and sets motion from the projectile's current position toward a target point. */ diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java index 7614939dd..42ea79203 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java @@ -3,7 +3,6 @@ package noppes.npcs.scripted.entity; import kamkeel.npcs.entity.EntityEnergySlicer; import net.minecraft.entity.EntityLivingBase; import noppes.npcs.api.entity.IEnergySlicer; -import noppes.npcs.api.entity.IEntity; import noppes.npcs.scripted.constants.EntityType; public class ScriptEnergySlicer extends ScriptEnergyProjectile implements IEnergySlicer { @@ -48,27 +47,7 @@ public class ScriptEnergySlicer extends ScriptEner // ==================== FIRE ==================== @Override - public void fireAt(IEntity target) { - ensureSpawned(); - EntityLivingBase mcTarget = null; - if (target != null && target.getMCEntity() instanceof EntityLivingBase) { - mcTarget = (EntityLivingBase) target.getMCEntity(); - } - entity.startMoving(mcTarget); - } - - @Override - public void fireAt(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); + protected void launchFromOwner(EntityLivingBase target) { + entity.startMoving(target); } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index a2e6b7d5c..913a3a257 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -2071,8 +2071,8 @@ ability.hover.weight=Higher weight = more likely to be chosen when multiple abil 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.trackDelay=Track Delay -ability.hover.trackDelay=How sluggish tracking is. 0 = instant (impossible to dodge). Higher = slower rotation toward target. Default 3 for lasers. +ability.trackSpeed=Track Speed +ability.hover.trackSpeed=Tracking speed in blocks/sec. 0 = instant. Sprint speed is ~5.6. Default 4 for lasers (dodgeable while sprinting). ability.hover.interruptible=If enabled, ability can be interrupted by taking damage ability.hover.ignoreIFrames=Temporarily clears target hurt resistance before applying ability damage, then restores it. ability.hover.dazed=Stun duration in ticks if ability is interrupted (20 ticks = 1 second)