diff --git a/src/api b/src/api index 9fbd1ecef..e4ddefb25 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 9fbd1eceffaa32f088676ff89d13fe3ca4ba1155 +Subproject commit e4ddefb2578a5fed9f45578600f4089f08cdf3b2 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 270d4ede7..8812e2d35 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -91,6 +91,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 int windUpColor = 0x80FF4400; // Telegraph color during wind up protected int activeColor = 0xC0FF0000; // Telegraph warning/active color @@ -687,6 +688,11 @@ 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") + .visibleWhen(() -> this.rotationMode == RotationMode.TRACK) + .tab("General")); defs.add(FieldDef.row( FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) .hover("ability.hover.interruptible") @@ -1388,6 +1394,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.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); nbt.setString("windUpSound", windUpSound); @@ -1482,6 +1489,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; windUpColor = nbt.hasKey("windUpColor") ? nbt.getInteger("windUpColor") : 0x80FF4400; activeColor = nbt.hasKey("activeColor") ? nbt.getInteger("activeColor") : 0xC0FF0000; windUpSound = nbt.getString("windUpSound"); @@ -1817,6 +1825,14 @@ public abstract class Ability implements IAbility, IAbilityAction { this.rotationPhase = rotationPhase; } + public float getTrackDelay() { + return trackDelay; + } + + public void setTrackDelay(float trackDelay) { + this.trackDelay = trackDelay; + } + /** * API method: Get lock movement type as integer. * @@ -1877,6 +1893,25 @@ public abstract class Ability implements IAbility, IAbilityAction { this.rotationPhase = LockMode.fromOrdinal(type); } + /** + * API method: Get track delay for TRACK rotation mode. + * 0 = instant tracking (legacy). Higher = slower lerp toward target. + */ + @Override + public float getTrackDelayValue() { + return trackDelay; + } + + /** + * API method: Set track delay for TRACK rotation mode. + * + * @param delay 0 = instant tracking. Higher = slower lerp toward target. + */ + @Override + public void setTrackDelayValue(float delay) { + this.trackDelay = Math.max(0, delay); + } + /** * Check if movement (pathfinding/motion) should be locked during WINDUP phase. */ 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 918f493ba..e9c4e0c16 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,6 +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.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 e84e0df00..753f997a1 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -6,6 +6,7 @@ import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData; +import kamkeel.npcs.util.AnchorPointHelper; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import net.minecraft.entity.Entity; @@ -224,11 +225,16 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { dirZ = 0.0; } - // Keep origin centered on look vector while active and clear owner bbox. - Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ); - setLookVectorLaunchPosition(livingOwner, direction, false); - // Lower beam origin slightly so it doesn't obscure the player's crosshair - posY -= 0.15; + // Position: use anchor if launchFromAnchor, otherwise use look vector position + if (anchorData.launchFromAnchor) { + Vec3 anchorPos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData); + setPosition(anchorPos.xCoord, anchorPos.yCoord, anchorPos.zCoord); + } else { + Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ); + setLookVectorLaunchPosition(livingOwner, direction, false); + // Lower beam origin slightly so it doesn't obscure the player's crosshair + posY -= 0.15; + } syncStartPositionToCurrent(); syncPositionStateToCurrent(false); } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index e56bf772d..fa6913455 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -4,6 +4,7 @@ import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityAction; import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.enums.RotationMode; import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; import kamkeel.npcs.controllers.data.ability.data.IAbilityAction; import kamkeel.npcs.controllers.data.ability.data.entry.AbilityToggleEntry; @@ -87,6 +88,7 @@ public class DataAbilities extends AbstractDataAbilities { */ private transient boolean hitScanActive = false; private transient EntityLivingBase hitScanTarget = null; + private transient float currentTrackDelay = 0; /** * Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 @@ -227,9 +229,19 @@ public class DataAbilities extends AbstractDataAbilities { protected void onPostPhaseTick(Ability ability, EntityLivingBase target) { // Update hit scan state - actual facing is deferred to applyRotationControl() // which runs AFTER super.onLivingUpdate() to override AI look helper - if (ability != null && ability.isExecuting() - && ability.isHitScanForCurrentPhase() && target != null) { - enableHitScan(target); + if (ability != null && ability.isExecuting() && target != null) { + if (ability.isHitScanForCurrentPhase()) { + enableHitScan(target, ability.getTrackDelay()); + } else { + // Release hit scan if it was previously active + if (hitScanActive) { + releaseRotationControl(); + } + // FREE mode: use AI look helper during ability execution since combat AI may not run + if (ability.getRotationMode() == RotationMode.FREE) { + npc.getLookHelper().setLookPositionWithEntity(target, 30.0F, 30.0F); + } + } } else if (hitScanActive) { releaseRotationControl(); } @@ -867,34 +879,74 @@ public class DataAbilities extends AbstractDataAbilities { /** * Enable hit scan tracking for the given target. */ - private void enableHitScan(EntityLivingBase target) { + private void enableHitScan(EntityLivingBase target, float trackDelay) { if (!hitScanActive) { hitScanActive = true; npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG); } hitScanTarget = target; + currentTrackDelay = trackDelay; } /** - * Snap the NPC to face the target instantly. + * Face the target with optional track delay. + * When trackDelay <= 0, snaps instantly (legacy behavior). + * When trackDelay > 0, lerps toward the target each tick. */ - private void faceTarget(EntityLivingBase target) { + private void faceTarget(EntityLivingBase target, float trackDelay) { 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); + float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); + float targetPitch = (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; + if (trackDelay <= 0) { + // Instant snap (legacy behavior) + npc.rotationYaw = targetYaw; + npc.rotationYawHead = targetYaw; + npc.renderYawOffset = targetYaw; + npc.rotationPitch = targetPitch; + npc.prevRotationYaw = targetYaw; + npc.prevRotationYawHead = targetYaw; + npc.prevRenderYawOffset = targetYaw; + npc.prevRotationPitch = targetPitch; + } else { + float lerpFactor = 1.0f / (1.0f + trackDelay); + + // Wrap yaw difference to [-180, 180] for correct interpolation + float yawDiff = wrapAngle(targetYaw - npc.rotationYawHead); + float pitchDiff = targetPitch - npc.rotationPitch; + + float newYaw = npc.rotationYawHead + yawDiff * lerpFactor; + float newPitch = npc.rotationPitch + pitchDiff * lerpFactor; + + // Set prev to current before updating (for smooth client interpolation) + npc.prevRotationYaw = npc.rotationYaw; + npc.prevRotationYawHead = npc.rotationYawHead; + npc.prevRenderYawOffset = npc.renderYawOffset; + npc.prevRotationPitch = npc.rotationPitch; + + npc.rotationYaw = newYaw; + npc.rotationYawHead = newYaw; + npc.renderYawOffset = newYaw; + npc.rotationPitch = newPitch; + } + } + + /** + * Snap the NPC to face the target instantly (no delay). + */ + private void faceTarget(EntityLivingBase target) { + faceTarget(target, 0); + } + + private static float wrapAngle(float angle) { + angle = angle % 360.0f; + if (angle >= 180.0f) angle -= 360.0f; + if (angle < -180.0f) angle += 360.0f; + return angle; } /** @@ -922,7 +974,7 @@ public class DataAbilities extends AbstractDataAbilities { // Server: apply the appropriate rotation if (hitScanActive && hitScanTarget != null && !hitScanTarget.isDead) { - faceTarget(hitScanTarget); + faceTarget(hitScanTarget, currentTrackDelay); } else if (rotationLocked) { npc.rotationYaw = lockedYaw; npc.rotationYawHead = lockedYawHead; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 82f58177f..9a566859e 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -2051,6 +2051,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.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)