Add Track Delay and Anchor Handling

This commit is contained in:
Kam committed 2026-03-02 20:09:32 -05:00
1 parent 36aab9e86e
commit dad49f1867
6 files changed
+119 -23

No files matched your search

@@ -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.
*/
@@ -49,6 +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.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
this.windUpAnimationName = "Ability_Laser_Windup";
@@ -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);
}
+69 -17
View File
@@ -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;