Various Fixes

This commit is contained in:
Kam committed 2026-02-06 16:28:38 -05:00
1 parent 2e5c4c3bf4
commit af02b0ab1d
20 files changed
+599 -163

No files matched your search

@@ -43,7 +43,16 @@ public class RenderAbilityDisc extends RenderAbilityProjectile {
GL11.glPushMatrix();
GL11.glScalef(scale, scale, scale);
// Apply ONLY Y-axis rotation for flat spin
if (disc.isVertical()) {
// Vertical mode: thin edge faces direction of travel (like a saw blade)
// 1. Spin in local space (Y-axis)
// 2. Tilt 90° to stand up (X-axis)
// 3. Orient to face travel direction (Y-axis world yaw)
GL11.glRotatef(disc.getTravelYaw(), 0.0f, 1.0f, 0.0f);
GL11.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
}
// Apply Y-axis rotation for spin
GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f);
// Inner scale defines the core size
@@ -67,7 +67,8 @@ public abstract class Ability implements IAbility {
// Feedback
protected LockMovementType lockMovement = LockMovementType.WINDUP;
protected LockType lockType = LockType.BOTH;
protected RotationMode rotationMode = RotationMode.FREE;
protected LockMovementType rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
protected int windUpColor = 0x80FF4400; // Telegraph color during wind up
protected int activeColor = 0xC0FF0000; // Telegraph warning/active color
@@ -418,26 +419,33 @@ public abstract class Ability implements IAbility {
defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks)
.tab("General").range(0, 10000));
defs.add(FieldDef.section("ability.section.movement").tab("General"));
defs.add(FieldDef.row(
FieldDef.stringEnumField("ability.lockPosition", LockMovementType.getDisplayKeys(),
defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(),
() -> this.getLockMovement().getDisplayKey(),
v -> {
String[] keys = LockMovementType.getDisplayKeys();
for (int i = 0; i < keys.length; i++) {
if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; }
for (LockMovementType t : LockMovementType.values()) {
if (t.getDisplayKey().equals(v)) { this.setLockMovement(t); break; }
}
})
.hover("ability.hover.lockPosition"),
FieldDef.stringEnumField("ability.lockType", LockType.getDisplayKeys(),
() -> this.getLockType().getDisplayKey(),
.hover("ability.hover.lockMovement")
.tab("General"));
defs.add(FieldDef.row(
FieldDef.stringEnumField("ability.rotationMode", RotationMode.getDisplayKeys(),
() -> this.getRotationMode().getDisplayKey(),
v -> {
String[] keys = LockType.getDisplayKeys();
for (int i = 0; i < keys.length; i++) {
if (keys[i].equals(v)) { this.setLockType(LockType.fromOrdinal(i)); break; }
for (RotationMode m : RotationMode.values()) {
if (m.getDisplayKey().equals(v)) { this.setRotationMode(m); break; }
}
})
.hover("ability.hover.lockType")
.visibleWhen(() -> this.lockMovement != LockMovementType.NO)
.hover("ability.hover.rotationMode"),
FieldDef.stringEnumField("ability.rotationPhase", getRotationPhaseKeys(),
() -> this.getRotationPhase().getDisplayKey(),
v -> {
for (LockMovementType t : LockMovementType.values()) {
if (t.getDisplayKey().equals(v)) { this.setRotationPhase(t); break; }
}
})
.hover("ability.hover.rotationPhase")
.visibleWhen(() -> this.rotationMode != RotationMode.FREE)
).tab("General"));
defs.add(FieldDef.row(
FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible)
@@ -606,12 +614,12 @@ public abstract class Ability implements IAbility {
instance.setEntityIdToFollow(caster.getEntityId());
// For LINE/CONE telegraphs: track target direction during windup
// Only track if movement is NOT locked during windup (locked = NPC can't rotate)
// Only track if rotation is NOT locked during windup (NPC can still turn)
if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) {
if (!lockMovement.locksWindup()) {
if (!isRotationLockedDuringWindup()) {
instance.setTargetEntityId(target.getEntityId());
}
// If windup is locked, yaw stays fixed at creation time
// If rotation is locked, yaw stays fixed at creation time
}
} else if (target != null) {
// AOE_TARGET abilities: telegraph follows target during windup
@@ -954,7 +962,8 @@ public abstract class Ability implements IAbility {
nbt.setInteger("recovery", dazedTicks);
nbt.setBoolean("interruptible", interruptible);
nbt.setInteger("lockMovement", lockMovement.ordinal());
nbt.setInteger("lockType", lockType.ordinal());
nbt.setInteger("rotationMode", rotationMode.ordinal());
nbt.setInteger("rotationPhase", rotationPhase.ordinal());
nbt.setInteger("windUpColor", windUpColor);
nbt.setInteger("activeColor", activeColor);
nbt.setString("windUpSound", windUpSound);
@@ -1005,7 +1014,30 @@ public abstract class Ability implements IAbility {
dazedTicks = nbt.getInteger("recovery");
interruptible = nbt.getBoolean("interruptible");
lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement"));
lockType = nbt.hasKey("lockType") ? LockType.fromOrdinal(nbt.getInteger("lockType")) : LockType.BOTH;
if (nbt.hasKey("rotationMode")) {
// Current format
rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode"));
rotationPhase = LockMovementType.fromOrdinal(nbt.getInteger("rotationPhase"));
} else if (nbt.hasKey("lockType")) {
// Legacy format migration: lockType was LockType enum (0=MOVEMENT, 1=ROTATION, 2=BOTH)
int oldLockType = nbt.getInteger("lockType");
if (oldLockType == 0) {
// MOVEMENT only: movement locked, rotation was free
rotationMode = RotationMode.FREE;
} else if (oldLockType == 1) {
// ROTATION only: rotation locked same timing as old lockMovement, movement was free
rotationMode = RotationMode.LOCKED;
rotationPhase = lockMovement;
lockMovement = LockMovementType.NO;
} else {
// BOTH: both locked same timing
rotationMode = RotationMode.LOCKED;
rotationPhase = lockMovement;
}
} else {
rotationMode = RotationMode.FREE;
rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
}
windUpColor = nbt.getInteger("windUpColor");
activeColor = nbt.getInteger("activeColor");
windUpSound = nbt.getString("windUpSound");
@@ -1170,12 +1202,20 @@ public abstract class Ability implements IAbility {
this.lockMovement = lockMovement;
}
public LockType getLockType() {
return lockType;
public RotationMode getRotationMode() {
return rotationMode;
}
public void setLockType(LockType lockType) {
this.lockType = lockType;
public void setRotationMode(RotationMode rotationMode) {
this.rotationMode = rotationMode;
}
public LockMovementType getRotationPhase() {
return rotationPhase;
}
public void setRotationPhase(LockMovementType rotationPhase) {
this.rotationPhase = rotationPhase;
}
/**
@@ -1197,21 +1237,39 @@ public abstract class Ability implements IAbility {
}
/**
* API method: Get lock type as integer.
* @return 0=MOVEMENT, 1=ROTATION, 2=BOTH
* API method: Get rotation mode as integer.
* @return 0=FREE, 1=LOCKED, 2=TRACK
*/
@Override
public int getLockTypeOrdinal() {
return lockType.ordinal();
public int getRotationModeType() {
return rotationMode.ordinal();
}
/**
* API method: Set lock type from integer.
* @param type 0=MOVEMENT, 1=ROTATION, 2=BOTH
* API method: Set rotation mode from integer.
* @param type 0=FREE, 1=LOCKED, 2=TRACK
*/
@Override
public void setLockTypeOrdinal(int type) {
this.lockType = LockType.fromOrdinal(type);
public void setRotationModeType(int type) {
this.rotationMode = RotationMode.fromOrdinal(type);
}
/**
* API method: Get rotation phase as integer.
* @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
*/
@Override
public int getRotationPhaseType() {
return rotationPhase.ordinal();
}
/**
* API method: Set rotation phase from integer.
* @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
*/
@Override
public void setRotationPhaseType(int type) {
this.rotationPhase = LockMovementType.fromOrdinal(type);
}
/**
@@ -1219,7 +1277,7 @@ public abstract class Ability implements IAbility {
*/
@Override
public boolean isMovementLockedDuringWindup() {
return lockMovement.locksWindup() && lockType.locksMovement();
return lockMovement.locksWindup();
}
/**
@@ -1227,7 +1285,7 @@ public abstract class Ability implements IAbility {
*/
@Override
public boolean isMovementLockedDuringActive() {
return lockMovement.locksActive() && lockType.locksMovement();
return lockMovement.locksActive();
}
/**
@@ -1235,7 +1293,7 @@ public abstract class Ability implements IAbility {
*/
@Override
public boolean isRotationLockedDuringWindup() {
return lockMovement.locksWindup() && lockType.locksRotation();
return rotationMode == RotationMode.LOCKED && rotationPhase.locksWindup();
}
/**
@@ -1243,7 +1301,7 @@ public abstract class Ability implements IAbility {
*/
@Override
public boolean isRotationLockedDuringActive() {
return lockMovement.locksActive() && lockType.locksRotation();
return rotationMode == RotationMode.LOCKED && rotationPhase.locksActive();
}
/**
@@ -1274,6 +1332,21 @@ public abstract class Ability implements IAbility {
}
}
/**
* Check if hit scan (force face target) is active during the current phase.
*/
public boolean isHitScanForCurrentPhase() {
if (rotationMode != RotationMode.TRACK) return false;
switch (phase) {
case WINDUP:
return rotationPhase.locksWindup();
case ACTIVE:
return rotationPhase.locksActive();
default:
return false;
}
}
public int getWindUpColor() {
return windUpColor;
}
@@ -1457,6 +1530,17 @@ public abstract class Ability implements IAbility {
effects.clear();
}
/**
* Get display keys for rotation phase dropdown (excludes "None" since mode=FREE handles that).
*/
private static String[] getRotationPhaseKeys() {
return new String[]{
"ability.lockMove.windup",
"ability.lockMove.active",
"ability.lockMove.both"
};
}
// ═══════════════════════════════════════════════════════════════════
// SHIT FROM THE ASS
// ═══════════════════════════════════════════════════════════════════
@@ -0,0 +1,53 @@
package kamkeel.npcs.controllers.data.ability;
/**
* Defines what happens to NPC rotation during ability phases.
* Used with a LockMovementType to control WHEN it applies.
*/
public enum RotationMode {
/**
* NPC rotates normally via AI look helper.
*/
FREE,
/**
* Freeze rotation at the moment the phase starts.
* NPC stays facing the direction it was looking when the phase began.
*/
LOCKED,
/**
* Force NPC to continuously face its target (hit scan).
* Overrides AI look helper every tick for precise aiming.
*/
TRACK;
public String getDisplayKey() {
switch (this) {
case FREE:
return "ability.rotation.free";
case LOCKED:
return "ability.rotation.locked";
case TRACK:
return "ability.rotation.track";
default:
return "ability.rotation.free";
}
}
public static String[] getDisplayKeys() {
return new String[]{
"ability.rotation.free",
"ability.rotation.locked",
"ability.rotation.track"
};
}
public static RotationMode fromOrdinal(int ordinal) {
RotationMode[] values = values();
if (ordinal >= 0 && ordinal < values.length) {
return values[ordinal];
}
return FREE;
}
}
@@ -23,4 +23,5 @@ public class AbilityFieldDefs {
() -> getter.get(),
v -> setter.accept((List<AbilityEffect>) v));
}
}
@@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.RotationMode;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
@@ -57,6 +58,8 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
this.cooldownTicks = 0;
this.windUpTicks = 40;
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
this.rotationMode = RotationMode.TRACK;
this.rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animations
@@ -313,6 +316,12 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.tab("ability.tab.visual"),
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"),
@@ -439,6 +439,12 @@ public class AbilityBeamDual extends Ability {
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(0), v -> setInnerColor(0, v))
.tab("ability.tab.visual"),
@@ -475,6 +481,12 @@ public class AbilityBeamDual extends Ability {
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(1), v -> setInnerColor(1, v))
.tab("ability.tab.visual"),
@@ -32,6 +32,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
// Disc geometry
private float discRadius = 1.0f;
private float discThickness = 0.2f;
private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward)
// Boomerang properties
private boolean boomerang = false;
@@ -107,9 +108,9 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
boomerang, boomerangDelay);
if (isPreview()) {
discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks);
discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks, vertical);
} else {
discEntity.setupCharging(anchorData, windUpTicks);
discEntity.setupCharging(anchorData, windUpTicks, vertical);
}
discEntity.setEffects(this.effects);
@@ -176,6 +177,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("discRadius", discRadius);
nbt.setFloat("discThickness", discThickness);
nbt.setBoolean("vertical", vertical);
nbt.setBoolean("boomerang", boomerang);
nbt.setInteger("boomerangDelay", boomerangDelay);
anchorData.writeNBT(nbt);
@@ -190,6 +192,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
public void readTypeNBT(NBTTagCompound nbt) {
this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f;
this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f;
this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical");
this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang");
this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40;
anchorData.readNBT(nbt);
@@ -207,6 +210,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
public void setDiscRadius(float discRadius) { this.discRadius = discRadius; }
public float getDiscThickness() { return discThickness; }
public void setDiscThickness(float discThickness) { this.discThickness = discThickness; }
public boolean isVertical() { return vertical; }
public void setVertical(boolean vertical) { this.vertical = vertical; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
@@ -286,6 +291,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
),
FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical)
.hover("ability.hover.vertical"),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
@@ -309,6 +316,12 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.tab("ability.tab.visual"),
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"),
@@ -28,6 +28,7 @@ public class AbilityDiscDual extends Ability {
// Disc geometry
private float discRadius = 1.0f;
private float discThickness = 0.2f;
private boolean vertical = false;
// Boomerang properties
private boolean boomerang = false;
@@ -144,11 +145,11 @@ public class AbilityDiscDual extends Ability {
discEntity2.setSiblingEntityId(discEntity.getEntityId());
if (isPreview()) {
discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks);
discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks);
discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks, vertical);
discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks, vertical);
} else {
discEntity.setupCharging(anchorData[0], windUpTicks);
discEntity2.setupCharging(anchorData[1], windUpTicks);
discEntity.setupCharging(anchorData[0], windUpTicks, vertical);
discEntity2.setupCharging(anchorData[1], windUpTicks, vertical);
}
discEntity.setEffects(this.effects);
@@ -244,6 +245,7 @@ public class AbilityDiscDual extends Ability {
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("discRadius", discRadius);
nbt.setFloat("discThickness", discThickness);
nbt.setBoolean("vertical", vertical);
nbt.setBoolean("boomerang", boomerang);
nbt.setInteger("boomerangDelay", boomerangDelay);
nbt.setBoolean("dualFire", dualFire);
@@ -274,6 +276,7 @@ public class AbilityDiscDual extends Ability {
this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f;
this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f;
this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical");
this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang");
this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40;
this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire");
@@ -315,6 +318,8 @@ public class AbilityDiscDual extends Ability {
public void setDiscRadius(float discRadius) { this.discRadius = discRadius; }
public float getDiscThickness() { return discThickness; }
public void setDiscThickness(float discThickness) { this.discThickness = discThickness; }
public boolean isVertical() { return vertical; }
public void setVertical(boolean vertical) { this.vertical = vertical; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
@@ -426,6 +431,8 @@ public class AbilityDiscDual extends Ability {
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
),
FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical)
.hover("ability.hover.vertical"),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
@@ -455,6 +462,12 @@ public class AbilityDiscDual extends Ability {
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(0), v -> setInnerColor(0, v))
.tab("ability.tab.visual"),
@@ -491,6 +504,12 @@ public class AbilityDiscDual extends Ability {
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(1), v -> setInnerColor(1, v))
.tab("ability.tab.visual"),
@@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.RotationMode;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -38,7 +39,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
private final EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f);
private final EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100);
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT);
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Transient state for laser entity (used for movement locking)
@@ -53,6 +54,8 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
this.cooldownTicks = 0;
this.windUpTicks = 15;
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
this.rotationMode = RotationMode.TRACK;
this.rotationPhase = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
// Default built-in animations
@@ -118,10 +121,8 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
return;
}
laserEntity.setLockVerticalDirection(true);
laserEntity.startMoving(target);
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
}
@Override
@@ -269,6 +270,12 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.tab("ability.tab.visual"),
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"),
@@ -446,6 +446,12 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.tab("ability.tab.visual"),
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"),
@@ -581,6 +581,12 @@ public class AbilityOrbBarrage extends Ability {
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.tab("ability.tab.visual"),
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"),
@@ -511,6 +511,12 @@ public class AbilityOrbDual extends Ability {
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(0), v -> setInnerColor(0, v))
.tab("ability.tab.visual"),
@@ -547,6 +553,12 @@ public class AbilityOrbDual extends Ability {
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(1), v -> setInnerColor(1, v))
.tab("ability.tab.visual"),
@@ -31,6 +31,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
// Disc shape properties
private float discRadius = 1.0f; // Width of disc
private float discThickness = 0.2f; // Height of disc
private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward)
// Charging state (during windup)
private boolean charging = false;
@@ -122,11 +123,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
* The disc will grow from 0 to discRadius over chargeDuration ticks.
* Position follows the owner based on anchor point.
*/
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
public void setupCharging(EnergyAnchorData anchor, int chargeDuration, boolean vertical) {
setCharging(true);
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.anchorData = anchor;
this.vertical = vertical;
this.targetDiscRadius = this.discRadius;
this.targetDiscThickness = this.discThickness;
this.discRadius = 0.01f;
@@ -144,7 +146,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
* Follows anchor point and animations like in the real game.
* Can be fired when transitioning to active phase.
*/
public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) {
public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, boolean vertical) {
this.setPreviewMode(true);
this.setPreviewOwner(owner);
@@ -157,6 +159,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.anchorData = anchor;
this.vertical = vertical;
// Store target size and start at 0 for grow effect
this.targetDiscRadius = discRadius;
@@ -541,6 +544,28 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
return returning;
}
public boolean isVertical() {
return vertical;
}
/**
* Get the yaw angle (degrees) from motion vector for vertical disc orientation.
* During charging, uses owner's facing direction.
*/
public float getTravelYaw() {
if (isCharging()) {
Entity owner = previewMode ? previewOwner : getOwnerEntity();
if (owner != null) {
return owner.rotationYaw;
}
}
double speed = Math.sqrt(motionX * motionX + motionZ * motionZ);
if (speed > 0.001) {
return (float) (Math.atan2(-motionX, motionZ) * 180.0 / Math.PI);
}
return 0.0f;
}
// ==================== NBT ====================
@Override
@@ -549,6 +574,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
this.boomerangDelay = nbt.hasKey("BoomerangDelay") ? nbt.getInteger("BoomerangDelay") : 40;
this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f;
this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f;
this.vertical = nbt.hasKey("Vertical") ? nbt.getBoolean("Vertical") : false;
this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning");
// Charging state
boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
@@ -566,6 +592,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
nbt.setInteger("BoomerangDelay", boomerangDelay);
nbt.setFloat("DiscRadius", discRadius);
nbt.setFloat("DiscThickness", discThickness);
nbt.setBoolean("Vertical", vertical);
nbt.setBoolean("Returning", returning);
// Charging state
nbt.setBoolean("Charging", isCharging());
@@ -35,6 +35,9 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
// Direction (normalized)
private double dirX, dirY, dirZ;
// Lock vertical direction after firing (only update yaw, keep pitch fixed)
private boolean lockVerticalDirection = false;
// Track hit entities to avoid double-damage
private Set<Integer> hitEntities = new HashSet<>();
@@ -165,6 +168,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
return;
}
// Track owner's anchor position and rotation each tick
// so the laser follows the NPC's facing direction
updateLaserOriginAndDirection();
if (!fullyExtended) {
// Expand the laser
currentLength += expansionSpeed;
@@ -187,27 +194,76 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
} else {
// Laser is fully extended, count down linger time
ticksSinceFullExtension++;
// Update end point during linger (origin/direction may still change)
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
if (ticksSinceFullExtension >= lingerTicks) {
this.setDead();
}
}
}
/**
* Update laser origin and direction from the owner's current anchor position and rotation.
* Called each tick so the laser follows the NPC when tracking a target.
*/
private void updateLaserOriginAndDirection() {
Entity owner = getOwnerEntity();
if (owner == null || owner.isDead) return;
if (!(owner instanceof EntityLivingBase)) return;
EntityLivingBase livingOwner = (EntityLivingBase) owner;
// Update origin from anchor point
if (anchorData != null) {
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData);
startX = pos.xCoord;
startY = pos.yCoord;
startZ = pos.zCoord;
} else {
startX = owner.posX;
startY = owner.posY + owner.height * 0.7;
startZ = owner.posZ;
}
// Update direction from owner's current rotation
float yaw = (float) Math.toRadians(owner.rotationYaw);
if (lockVerticalDirection) {
// Only update horizontal direction (yaw), keep vertical (pitch) fixed
double horizontalLen = Math.sqrt(dirX * dirX + dirZ * dirZ);
if (horizontalLen < 0.001) horizontalLen = 1.0;
dirX = -Math.sin(yaw) * horizontalLen;
dirZ = Math.cos(yaw) * horizontalLen;
} else {
float pitch = (float) Math.toRadians(owner.rotationPitch);
dirX = -Math.sin(yaw) * Math.cos(pitch);
dirY = -Math.sin(pitch);
dirZ = Math.cos(yaw) * Math.cos(pitch);
}
// Keep entity positioned at origin
prevPosX = startX;
prevPosY = startY;
prevPosZ = startZ;
setPosition(startX, startY, startZ);
}
public void startMoving(EntityLivingBase target) {
if (!isCharging()) return;
setCharging(false);
// Update start position to current position
// Initialize from current anchor position
// updateLaserOriginAndDirection() will keep these updated each tick
startX = posX;
startY = posY;
startZ = posZ;
// Always derive direction from the owner's current yaw/pitch.
// This ensures the laser fires in the direction the NPC is visually facing,
// which matches the telegraph line direction regardless of Lock Movement mode.
// Set initial direction from owner's rotation
Entity owner = getOwnerEntity();
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
@@ -215,7 +271,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
this.dirY = -Math.sin(pitch);
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
} else if (target != null) {
// Fallback: aim at target if owner is somehow unavailable
double dx = target.posX - startX;
double dy = (target.posY + target.getEyeHeight() - 0.4) - startY;
double dz = target.posZ - startZ;
@@ -227,12 +282,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
}
}
setPosition(endX, endY, endZ);
// Initialize end point
this.endX = posX;
this.endY = posY;
this.endZ = posZ;
// Initialize end point at start (will expand from here)
this.endX = startX;
this.endY = startY;
this.endZ = startZ;
}
private void checkBlockCollision() {
@@ -308,14 +361,14 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
}
private void checkEntityCollisionAlongLine() {
// Check collision along the entire laser line
// Use a slightly expanded AABB for the full line
double minX = Math.min(startX, endX) - laserWidth;
double minY = Math.min(startY, endY) - laserWidth;
double minZ = Math.min(startZ, endZ) - laserWidth;
double maxX = Math.max(startX, endX) + laserWidth;
double maxY = Math.max(startY, endY) + laserWidth;
double maxZ = Math.max(startZ, endZ) + laserWidth;
// Expand search AABB by entity sizes to ensure we find all potential targets
double expand = Math.max(laserWidth, 1.0);
double minX = Math.min(startX, endX) - expand;
double minY = Math.min(startY, endY) - expand;
double minZ = Math.min(startZ, endZ) - expand;
double maxX = Math.max(startX, endX) + expand;
double maxY = Math.max(startY, endY) + expand;
double maxZ = Math.max(startZ, endZ) + expand;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ);
@@ -326,7 +379,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
if (shouldIgnoreEntity(entity)) continue;
if (hitEntities.contains(entity.getEntityId())) continue;
// Check if entity is close to the laser line
// Check if entity's bounding box intersects with the laser line
if (isEntityOnLine(entity)) {
hitEntities.add(entity.getEntityId());
applyDamage(entity);
@@ -337,42 +390,64 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
}
/**
* Check if an entity intersects with the laser line.
* Check if an entity's bounding box intersects with the laser line.
* Uses separate XZ (horizontal) and Y (vertical) checks against
* the entity's full bounding box rather than just its center point.
*/
private boolean isEntityOnLine(EntityLivingBase entity) {
// Get entity center
double ex = entity.posX;
double ey = entity.posY + entity.height * 0.5;
double ez = entity.posZ;
// Vector from start to entity
// Project entity XZ position onto the laser line direction
double vx = ex - startX;
double vy = ey - startY;
double vz = ez - startZ;
// Project onto line direction
double dot = vx * dirX + vy * dirY + vz * dirZ;
// Clamp to line segment
// Project using horizontal direction components only
double dirLenXZ = Math.sqrt(dirX * dirX + dirZ * dirZ);
double dot;
if (dirLenXZ > 0.001) {
dot = (vx * dirX + vz * dirZ) / (dirLenXZ * dirLenXZ) * dirLenXZ;
// Remap to full 3D parameter
dot = (vx * dirX + vz * dirZ + (entity.posY + entity.height * 0.5 - startY) * dirY);
} else {
// Laser fires nearly straight up/down - use full 3D projection
dot = vx * dirX + (entity.posY + entity.height * 0.5 - startY) * dirY + vz * dirZ;
}
dot = Math.max(0, Math.min(dot, currentLength));
// Closest point on line
// Closest point on laser line at this parameter
double closestX = startX + dirX * dot;
double closestY = startY + dirY * dot;
double closestZ = startZ + dirZ * dot;
// Distance from entity to closest point
double distSq = (ex - closestX) * (ex - closestX) +
(ey - closestY) * (ey - closestY) +
(ez - closestZ) * (ez - closestZ);
// XZ distance check: entity center vs laser line point
// Use minimum collision width so visually thin lasers still hit reliably
double xzDistSq = (ex - closestX) * (ex - closestX) + (ez - closestZ) * (ez - closestZ);
double effectiveWidth = Math.max(laserWidth, 0.5);
double hitRadiusXZ = effectiveWidth * 0.5 + entity.width * 0.5;
if (xzDistSq > hitRadiusXZ * hitRadiusXZ) return false;
// Hit if within laser width + entity radius
double hitRadius = laserWidth * 0.5 + entity.width * 0.5;
return distSq <= hitRadius * hitRadius;
// Y overlap check: laser point vs entity's full height range
double entityMinY = entity.boundingBox.minY;
double entityMaxY = entity.boundingBox.maxY;
double laserHalfWidth = effectiveWidth * 0.5;
// Check if laser Y is within entity's height range (expanded by laser half-width)
if (closestY < entityMinY - laserHalfWidth) return false;
if (closestY > entityMaxY + laserHalfWidth) return false;
return true;
}
// ==================== GETTERS FOR RENDERER ====================
public void setLockVerticalDirection(boolean lock) {
this.lockVerticalDirection = lock;
}
public boolean isLockVerticalDirection() {
return lockVerticalDirection;
}
public float getLaserWidth() {
return laserWidth;
}
@@ -100,9 +100,12 @@ public class AnchorPointHelper {
break;
}
x += anchorX * scale;
y += anchorY * scale;
z += anchorZ * scale;
// Rotate offsets relative to entity's body yaw
// +X = entity's right, +Y = up, +Z = entity's forward
Vec3 rotatedOffset = rotateOffsetByYaw(anchorX * scale, anchorY * scale, anchorZ * scale, bodyYaw);
x += rotatedOffset.xCoord;
y += rotatedOffset.yCoord;
z += rotatedOffset.zCoord;
return Vec3.createVectorHelper(x, y, z);
}
@@ -246,9 +249,11 @@ public class AnchorPointHelper {
double worldY = entity.posY + shoulderHeight - blockY;
double worldZ = entity.posZ + worldOffsetZ;
worldX += anchor.anchorOffsetX * scale;
worldY += anchor.anchorOffsetY * scale;
worldZ += anchor.anchorOffsetZ * scale;
// Rotate offsets relative to entity's body yaw
Vec3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw);
worldX += rotatedOffset.xCoord;
worldY += rotatedOffset.yCoord;
worldZ += rotatedOffset.zCoord;
return Vec3.createVectorHelper(worldX, worldY, worldZ);
}
@@ -317,9 +322,11 @@ public class AnchorPointHelper {
double y = entity.posY + entity.height * FALLBACK_ARM_HEIGHT + armLengthOffset;
double z = entity.posZ + lateralZ + forwardZ;
x += anchor.anchorOffsetX * scale;
y += anchor.anchorOffsetY * scale;
z += anchor.anchorOffsetZ * scale;
// Rotate offsets relative to entity's body yaw
Vec3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw);
x += rotatedOffset.xCoord;
y += rotatedOffset.yCoord;
z += rotatedOffset.zCoord;
return Vec3.createVectorHelper(x, y, z);
}
@@ -344,4 +351,17 @@ public class AnchorPointHelper {
return FRONT_HEIGHT;
}
}
/**
* Rotate an offset vector by the entity's body yaw so offsets are entity-relative.
* +X = entity's right, +Y = up, +Z = entity's forward.
* @param bodyYawRad body yaw in radians
*/
private static Vec3 rotateOffsetByYaw(double offsetX, double offsetY, double offsetZ, float bodyYawRad) {
double cos = Math.cos(bodyYawRad);
double sin = Math.sin(bodyYawRad);
double worldX = -offsetX * cos - offsetZ * sin;
double worldZ = -offsetX * sin + offsetZ * cos;
return Vec3.createVectorHelper(worldX, offsetY, worldZ);
}
}
+99 -56
View File
@@ -100,6 +100,14 @@ public class DataAbilities {
private transient double lockedPosY = 0;
private transient double lockedPosZ = 0;
/**
* Hit scan state - forces NPC to face target.
* Set during tickCurrentAbility(), applied after super.onLivingUpdate()
* to override AI look helper rotation.
*/
private transient boolean hitScanActive = false;
private transient EntityLivingBase hitScanTarget = null;
// ═══════════════════════════════════════════════════════════════════
// CONSTRUCTOR
// ═══════════════════════════════════════════════════════════════════
@@ -126,13 +134,13 @@ public class DataAbilities {
if (currentAbility != null) {
removeTelegraph(currentAbility);
stopAbilityAnimation();
releaseLockedRotation();
releaseRotationControl();
releaseLockedPosition();
currentAbility = null;
lastTarget = null;
} else {
// No ability but locks could be orphaned
if (rotationLocked) releaseLockedRotation();
if (rotationLocked || hitScanActive) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
}
return;
@@ -140,7 +148,7 @@ public class DataAbilities {
// Safety: release orphaned locks if no ability is actively executing
if (currentAbility == null || !currentAbility.isExecuting()) {
if (rotationLocked) releaseLockedRotation();
if (rotationLocked || hitScanActive) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
}
@@ -181,13 +189,13 @@ public class DataAbilities {
// Remove telegraph - it has served its purpose
removeTelegraph(currentAbility);
// Handle rotation lock transition from WINDUP to ACTIVE
// Handle rotation control transition from WINDUP to ACTIVE
if (currentAbility.isRotationLockedDuringActive()) {
if (!rotationLocked) {
captureLockedRotation();
}
} else if (rotationLocked) {
releaseLockedRotation();
releaseRotationControl();
}
// Handle position lock transition from WINDUP to ACTIVE
@@ -203,6 +211,11 @@ public class DataAbilities {
playAbilitySound(currentAbility.getActiveSound());
playAbilityAnimation(currentAbility.getActiveAnimation());
// Snap NPC to face target before execute so startMoving() reads correct rotation
if (currentAbility.isHitScanForCurrentPhase() && target != null) {
faceTarget(target);
}
// Fire execute event (cancelable)
AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent(
npc.wrappedNPC, currentAbility, target);
@@ -239,6 +252,15 @@ public class DataAbilities {
break;
}
// Update hit scan state - actual facing is deferred to applyRotationControl()
// which runs AFTER super.onLivingUpdate() to override AI look helper
if (currentAbility != null && currentAbility.isExecuting()
&& currentAbility.isHitScanForCurrentPhase() && target != null) {
enableHitScan(target);
} else if (hitScanActive) {
releaseRotationControl();
}
// Apply movement control if ability is still executing
if (currentAbility != null && currentAbility.isExecuting()) {
applyMovementControl();
@@ -284,7 +306,7 @@ public class DataAbilities {
NpcAPI.EVENT_BUS.post(completeEvent);
// Release locks before cleanup
releaseLockedRotation();
releaseRotationControl();
releaseLockedPosition();
// Clean up and roll cooldown (this sets currentAbility to null)
@@ -301,7 +323,7 @@ public class DataAbilities {
stopAbilityAnimation();
// Release locks
releaseLockedRotation();
releaseRotationControl();
releaseLockedPosition();
// Calculate cooldown: random(min, max) + ability offset
@@ -679,7 +701,7 @@ public class DataAbilities {
stopAbilityAnimation();
// Release locks
releaseLockedRotation();
releaseRotationControl();
releaseLockedPosition();
// Fire interrupt event
@@ -702,7 +724,7 @@ public class DataAbilities {
if (currentAbility != null) {
removeTelegraph(currentAbility);
stopAbilityAnimation();
releaseLockedRotation();
releaseRotationControl();
releaseLockedPosition();
currentAbility.interrupt();
currentAbility = null;
@@ -1000,15 +1022,15 @@ public class DataAbilities {
}
// ═══════════════════════════════════════════════════════════════════
// ROTATION LOCKING
// ROTATION CONTROL
// ═══════════════════════════════════════════════════════════════════
/** Bit flag for rotation lock in the existing data watcher slot 15 bitfield */
private static final int ROTATION_LOCKED_FLAG = 16;
/** Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 */
private static final int ROTATION_CONTROLLED_FLAG = 16;
/**
* Capture current rotation values to lock NPC's look direction.
* Called when entering a locked phase (WINDUP or ACTIVE depending on LockMovementType).
* Called when entering a LOCKED rotation phase.
*/
private void captureLockedRotation() {
lockedYaw = npc.rotationYaw;
@@ -1016,41 +1038,79 @@ public class DataAbilities {
lockedRenderYawOffset = npc.renderYawOffset;
lockedPitch = npc.rotationPitch;
rotationLocked = true;
npc.setBoolFlag(true, ROTATION_LOCKED_FLAG);
npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG);
}
/**
* Release the rotation lock.
* Called when leaving the locked phase or ability completes.
* Release rotation control (both locked and hit scan).
*/
private void releaseLockedRotation() {
private void releaseRotationControl() {
rotationLocked = false;
npc.setBoolFlag(false, ROTATION_LOCKED_FLAG);
hitScanActive = false;
hitScanTarget = null;
npc.setBoolFlag(false, ROTATION_CONTROLLED_FLAG);
}
/**
* Apply locked rotation values to the NPC.
* Called AFTER super.onLivingUpdate() in EntityNPCInterface to override
* any rotation changes made by the look helper, AI tasks, or body smoothing.
* Runs on both client and server.
* Enable hit scan tracking for the given target.
*/
public void applyLockedRotation() {
private void enableHitScan(EntityLivingBase target) {
if (!hitScanActive) {
hitScanActive = true;
npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG);
}
hitScanTarget = target;
}
/**
* Snap the NPC to face the target instantly.
*/
private void faceTarget(EntityLivingBase target) {
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);
npc.rotationYaw = yaw;
npc.rotationYawHead = yaw;
npc.renderYawOffset = yaw;
npc.rotationPitch = pitch;
npc.prevRotationYaw = yaw;
npc.prevRotationYawHead = yaw;
npc.prevRenderYawOffset = yaw;
npc.prevRotationPitch = pitch;
}
/**
* Apply rotation control after super.onLivingUpdate() and super.onUpdate().
* Handles both LOCKED (freeze at captured values) and TRACK (face target) modes.
*
* Server: computes the correct rotation (locked values or target facing).
* Client: trusts the server-synced rotation values and prevents body smoothing override.
*/
public void applyRotationControl() {
if (npc.worldObj.isRemote) {
// Client-side: check bit 16 in the existing flags data watcher (slot 15)
boolean flagActive = npc.getBoolFlag(ROTATION_LOCKED_FLAG);
if (!flagActive) {
// Client: check single flag for any rotation control
if (!npc.getBoolFlag(ROTATION_CONTROLLED_FLAG)) {
rotationLocked = false;
return;
}
// First tick the flag is active: capture current rotation values
if (!rotationLocked) {
lockedYaw = npc.rotationYaw;
lockedYawHead = npc.rotationYawHead;
lockedRenderYawOffset = npc.renderYawOffset;
lockedPitch = npc.rotationPitch;
rotationLocked = true;
}
// Force all rotation values back to locked state
// Trust server rotation, prevent body smoothing from overriding
npc.prevRotationYaw = npc.rotationYaw;
npc.prevRotationYawHead = npc.rotationYawHead;
npc.renderYawOffset = npc.rotationYawHead;
npc.prevRenderYawOffset = npc.rotationYawHead;
npc.prevRotationPitch = npc.rotationPitch;
return;
}
// Server: apply the appropriate rotation
if (hitScanActive && hitScanTarget != null && !hitScanTarget.isDead) {
faceTarget(hitScanTarget);
} else if (rotationLocked) {
npc.rotationYaw = lockedYaw;
npc.rotationYawHead = lockedYawHead;
npc.renderYawOffset = lockedRenderYawOffset;
@@ -1059,39 +1119,22 @@ public class DataAbilities {
npc.prevRotationYawHead = lockedYawHead;
npc.prevRenderYawOffset = lockedRenderYawOffset;
npc.prevRotationPitch = lockedPitch;
return;
}
// Server-side: use stored locked values
if (!rotationLocked) {
return;
}
// Force rotation back to locked values
npc.rotationYaw = lockedYaw;
npc.rotationYawHead = lockedYawHead;
npc.renderYawOffset = lockedRenderYawOffset;
npc.rotationPitch = lockedPitch;
// Also set prev values to prevent interpolation jitter
npc.prevRotationYaw = lockedYaw;
npc.prevRotationYawHead = lockedYawHead;
npc.prevRenderYawOffset = lockedRenderYawOffset;
npc.prevRotationPitch = lockedPitch;
}
/**
* Check if rotation is currently locked.
* Check if rotation is currently controlled (locked or tracking target).
* Used by AI tasks to determine if they should try to change NPC look direction.
*/
public boolean isRotationLocked() {
return rotationLocked;
return rotationLocked || hitScanActive;
}
// ═══════════════════════════════════════════════════════════════════
// POSITION LOCKING
// ═══════════════════════════════════════════════════════════════════
/** Bit flag for position lock in the existing data watcher slot 15 bitfield */
/** Bit flag for position lock in data watcher slot 15 */
private static final int POSITION_LOCKED_FLAG = 32;
/**
@@ -169,6 +169,9 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
public void pause() {
paused = true;
// Immediately sync prevPos to pos for NPC and all entities to prevent interpolation jitter
syncPrevPositions();
if (previewNpc != null) {
AnimationData data = previewNpc.display.animationData;
if (data.animation != null) {
@@ -177,6 +180,25 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
}
}
/**
* Sync prevPos to pos for the NPC and all preview entities.
* Prevents rendering interpolation jitter when standing still or paused.
*/
private void syncPrevPositions() {
if (previewNpc != null) {
previewNpc.prevPosX = previewNpc.posX;
previewNpc.prevPosY = previewNpc.posY;
previewNpc.prevPosZ = previewNpc.posZ;
}
for (Entity entity : previewEntities) {
if (entity != null && !entity.isDead) {
entity.prevPosX = entity.posX;
entity.prevPosY = entity.posY;
entity.prevPosZ = entity.posZ;
}
}
}
public void stop() {
// Clean up the ability
if (previewAbility != null) {
@@ -228,10 +250,21 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
* Tick the preview forward. Calls real ability methods.
*/
public void tick() {
if (!playing || paused || previewAbility == null || previewNpc == null) {
if (!playing || previewAbility == null || previewNpc == null) {
return;
}
if (paused) {
// Sync prevPos to pos for NPC and all entities to prevent interpolation jitter while paused
syncPrevPositions();
return;
}
// Sync NPC's prevPos to current pos BEFORE any updates to prevent jitter when standing still
previewNpc.prevPosX = previewNpc.posX;
previewNpc.prevPosY = previewNpc.posY;
previewNpc.prevPosZ = previewNpc.posZ;
currentTick++;
if (previewTelegraph != null) {
@@ -312,7 +312,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
// Re-apply locks after super.onUpdate() to override body rotation smoothing
// in EntityLivingBase.onUpdate() (func_110146_f) which runs AFTER onLivingUpdate()
// and overwrites renderYawOffset, causing a visible twitch each frame.
abilities.applyLockedRotation();
abilities.applyRotationControl();
abilities.applyLockedPosition();
if (!isRemote()) {
@@ -489,7 +489,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
super.onLivingUpdate();
// Apply locks after super.onLivingUpdate() to override AI/physics changes
abilities.applyLockedRotation();
abilities.applyRotationControl();
abilities.applyLockedPosition();
handleMountRiderState();