Fixed / Improved Track Speed Handling on NPCs

This commit is contained in:
Kam committed 2026-03-07 18:26:41 -05:00
1 parent 393abc08ed
commit 4e33891ae5
7 files changed
+145 -54

No files matched your search

+1 -1
Submodule src/api updated: 6faa544f6f...c46ca50683.
@@ -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;
@@ -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);
}
/**
@@ -49,7 +49,7 @@ public class AbilityLaser extends AbilityEnergyProjectile<EntityAbilityLaser> 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";
@@ -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();
+52 -24
View File
@@ -88,10 +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
@@ -222,9 +223,15 @@ 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);
}
}
}
@@ -237,7 +244,7 @@ public class DataAbilities extends AbstractDataAbilities {
// 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) {
@@ -260,7 +267,10 @@ public class DataAbilities extends AbstractDataAbilities {
@Override
protected void onBurstDelayReleaseLocks() {
if (hitScanActive) releaseRotationControl();
if (hitScanActive) {
preserveTrackedRotation = true;
releaseRotationControl();
}
}
@Override
@@ -887,34 +897,42 @@ 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);
// Capture current rotation as the starting point for lerp
trackedYaw = npc.rotationYawHead;
trackedPitch = npc.rotationPitch;
// 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;
@@ -926,17 +944,27 @@ public class DataAbilities extends AbstractDataAbilities {
npc.prevRenderYawOffset = targetYaw;
npc.prevRotationPitch = targetPitch;
} else {
// Only compute the lerp once per tick; subsequent calls just re-stamp values
if (!trackLerpedThisTick) {
trackLerpedThisTick = true;
float lerpFactor = 1.0f / (1.0f + trackDelay);
// Lerp from stored tracked rotation (not entity rotation, which AI may have overwritten)
// 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 yawDiff = wrapAngle(targetYaw - trackedYaw);
float pitchDiff = targetPitch - trackedPitch;
trackedYaw = trackedYaw + yawDiff * lerpFactor;
trackedPitch = trackedPitch + pitchDiff * lerpFactor;
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)
@@ -991,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;
@@ -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)