mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 01:57:34 +00:00
Various Fixes
This commit is contained in:
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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user