mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 17:47:32 +00:00
Fixed / Improved Track Speed Handling on NPCs
This commit is contained in:
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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user