mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 06:07:27 +00:00
Refactor movement abilities: extract shared logic into AbilityMovement class for Charge and Dash abilities
This commit is contained in:
1 parent
3beaf68815
commit
1fb52a4ae9
3 files changed
+316
-203
No files matched your search
@@ -2,7 +2,6 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
@@ -12,8 +11,6 @@ import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.Vec3;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
@@ -25,9 +22,10 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Charge ability: Rush attack where NPC charges in a line, damaging all targets hit.
|
||||
* Charge ability: Rush attack where the caster charges in a line, damaging all targets hit.
|
||||
* Extends AbilityMovement for shared direction locking, stall detection, and velocity application.
|
||||
*/
|
||||
public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
public class AbilityCharge extends AbilityMovement implements IAbilityCharge {
|
||||
|
||||
// Type-specific parameters
|
||||
private float chargeSpeed = 0.8f;
|
||||
@@ -35,13 +33,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
private float knockback = 3.0f;
|
||||
private float hitWidth = 1.5f;
|
||||
|
||||
// Runtime state (transient)
|
||||
private transient double startX, startY, startZ;
|
||||
private transient double prevTickX, prevTickZ;
|
||||
private transient Vec3 chargeDirection;
|
||||
// Type-specific runtime state
|
||||
private transient Set<Integer> hitEntities = new HashSet<>();
|
||||
private transient float lockedYaw;
|
||||
private transient int maxActiveTicks;
|
||||
|
||||
public AbilityCharge() {
|
||||
this.typeId = "ability.cnpc.charge";
|
||||
@@ -71,138 +64,68 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAbilityMovement() {
|
||||
return true; // This ability moves the NPC
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on the first tick of windup - lock direction here so it matches telegraph.
|
||||
*/
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
if (tick == 1) {
|
||||
// Lock direction on first windup tick (same time telegraph is created)
|
||||
lockChargeDirection(caster, target);
|
||||
lockDirection(caster, target);
|
||||
}
|
||||
// Keep caster facing the locked direction during windup
|
||||
enforceLockedRotation(caster);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the charge direction. Called once at windup start — direction won't change even if target moves.
|
||||
* NPC: charges toward aggro target.
|
||||
* Player: charges in look direction.
|
||||
*/
|
||||
private void lockChargeDirection(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!isPlayerCaster(caster) && target != null) {
|
||||
// NPC: charge toward aggro target
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
if (len > 0) {
|
||||
chargeDirection = Vec3.createVectorHelper(dx / len, 0, dz / len);
|
||||
} else {
|
||||
float yaw = (float) Math.toRadians(caster.rotationYaw);
|
||||
chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw));
|
||||
}
|
||||
} else {
|
||||
// Player: charge in look direction
|
||||
float yaw = (float) Math.toRadians(caster.rotationYaw);
|
||||
chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw));
|
||||
}
|
||||
lockedYaw = (float) Math.toDegrees(Math.atan2(-chargeDirection.xCoord, chargeDirection.zCoord));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
// Initialize charge - direction was already locked during windup
|
||||
startX = caster.posX;
|
||||
startY = caster.posY;
|
||||
startZ = caster.posZ;
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
initMovement(caster, maxRange, chargeSpeed);
|
||||
hitEntities.clear();
|
||||
|
||||
// Safety timeout: expected ticks + buffer to prevent infinite charge
|
||||
maxActiveTicks = chargeSpeed > 0 ? (int)(maxRange / chargeSpeed) + 10 : 10;
|
||||
|
||||
// If direction wasn't set during windup (shouldn't happen), set it now
|
||||
if (chargeDirection == null) {
|
||||
lockChargeDirection(caster, target);
|
||||
if (movementDirection == null) {
|
||||
lockDirection(caster, target);
|
||||
}
|
||||
|
||||
enforceLockedRotation(caster);
|
||||
}
|
||||
|
||||
private void enforceLockedRotation(EntityLivingBase caster) {
|
||||
caster.rotationYaw = lockedYaw;
|
||||
caster.rotationYawHead = lockedYaw;
|
||||
caster.prevRotationYaw = lockedYaw;
|
||||
caster.prevRotationYawHead = lockedYaw;
|
||||
caster.renderYawOffset = lockedYaw;
|
||||
caster.prevRenderYawOffset = lockedYaw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
// Safety timeout or missing state: force-complete to prevent stuck NPC
|
||||
if (!isPreview() && (chargeDirection == null || tick > maxActiveTicks)) {
|
||||
if (checkTimeout(tick)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (chargeDirection == null) {
|
||||
if (movementDirection == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enforce rotation every tick
|
||||
if (!isPreview()) {
|
||||
enforceLockedRotation(caster);
|
||||
}
|
||||
|
||||
// Stall detection: if entity hasn't moved since last tick, it's stuck against a wall
|
||||
if (!isPreview() && tick > 1) {
|
||||
double dx = caster.posX - prevTickX;
|
||||
double dz = caster.posZ - prevTickZ;
|
||||
if (dx * dx + dz * dz < 0.0001) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
if (checkStall(caster, tick)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
updatePrevPosition(caster);
|
||||
|
||||
// Calculate distance traveled
|
||||
double distanceTraveled = Math.sqrt(
|
||||
Math.pow(caster.posX - startX, 2) +
|
||||
Math.pow(caster.posZ - startZ, 2)
|
||||
);
|
||||
|
||||
// Check if reached max distance
|
||||
if (distanceTraveled >= maxRange) {
|
||||
if (getDistanceTraveled(caster) >= maxRange) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Block detection (skip in preview - no real world collision)
|
||||
if (!isPreview() && isChargeBlocked(caster)) {
|
||||
if (checkBlocked(caster, chargeSpeed)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move caster
|
||||
caster.motionX = chargeDirection.xCoord * chargeSpeed;
|
||||
caster.motionY = 0;
|
||||
caster.motionZ = chargeDirection.zCoord * chargeSpeed;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
// Move caster (flat: zero vertical motion)
|
||||
applyVelocityFlat(caster, chargeSpeed);
|
||||
|
||||
// Server-side collision damage (skip in preview)
|
||||
if (!caster.worldObj.isRemote && !isPreview()) {
|
||||
@@ -218,14 +141,11 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
|
||||
EntityLivingBase livingEntity = (EntityLivingBase) entity;
|
||||
|
||||
// Hit this entity
|
||||
hitEntities.add(entity.getEntityId());
|
||||
|
||||
// Apply damage with scripted event support
|
||||
boolean wasHit = applyAbilityDamageWithDirection(caster, livingEntity, damage, knockback,
|
||||
chargeDirection.xCoord, chargeDirection.zCoord);
|
||||
movementDirection.xCoord, movementDirection.zCoord);
|
||||
|
||||
// Apply effects and play impact sound if hit wasn't cancelled
|
||||
if (wasHit) {
|
||||
applyEffects(livingEntity);
|
||||
caster.worldObj.playSoundAtEntity(livingEntity, "random.explode", 0.5f, 1.2f);
|
||||
@@ -246,24 +166,10 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
super.onInterrupt(caster, source, damage);
|
||||
}
|
||||
|
||||
private void stopMomentum(EntityLivingBase caster) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isChargeBlocked(EntityLivingBase caster) {
|
||||
return isMovementBlocked(caster, chargeDirection.xCoord, chargeDirection.zCoord, chargeSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
chargeDirection = null;
|
||||
public void cleanup() {
|
||||
super.cleanup();
|
||||
hitEntities.clear();
|
||||
maxActiveTicks = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,16 +2,12 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.util.Vec3;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import noppes.npcs.api.ability.type.IAbilityDash;
|
||||
@@ -22,9 +18,9 @@ import java.util.Random;
|
||||
|
||||
/**
|
||||
* Dash ability: Quick evasive sidestep with NO telegraph.
|
||||
* Defensive repositioning move to evade attacks.
|
||||
* Extends AbilityMovement for shared direction locking, stall detection, and velocity application.
|
||||
*/
|
||||
public class AbilityDash extends Ability implements IAbilityDash {
|
||||
public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
|
||||
/**
|
||||
* Dash behavior mode.
|
||||
@@ -88,12 +84,8 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
private float dashDistance = 4.0f;
|
||||
private float dashSpeed = 0.5f;
|
||||
|
||||
// Runtime state
|
||||
private transient Vec3 dashDirection;
|
||||
private transient double startX, startY, startZ;
|
||||
private transient double prevTickX, prevTickZ;
|
||||
// Type-specific runtime state
|
||||
private transient DashDirection chosenDirection;
|
||||
private transient int maxActiveTicks;
|
||||
|
||||
public AbilityDash() {
|
||||
this.typeId = "ability.cnpc.dash";
|
||||
@@ -126,21 +118,9 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAbilityMovement() {
|
||||
return true; // This ability moves the NPC
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
startX = caster.posX;
|
||||
startY = caster.posY;
|
||||
startZ = caster.posZ;
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
|
||||
// Safety timeout: expected ticks + generous buffer to prevent infinite dash
|
||||
maxActiveTicks = dashSpeed > 0 ? (int)(dashDistance / dashSpeed) + 10 : 10;
|
||||
initMovement(caster, dashDistance, dashSpeed);
|
||||
|
||||
// Choose random direction based on mode
|
||||
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
|
||||
@@ -148,26 +128,10 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
: DEFENSIVE_DIRECTIONS;
|
||||
chosenDirection = directions[RANDOM.nextInt(directions.length)];
|
||||
|
||||
// NPC: dash direction is relative to aggro target facing
|
||||
// Player: dash direction is relative to look direction
|
||||
float baseYaw;
|
||||
if (!isPlayerCaster(caster) && target != null) {
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
} else {
|
||||
baseYaw = caster.rotationYaw;
|
||||
}
|
||||
|
||||
// Apply direction offset
|
||||
// Calculate direction: base yaw (toward target or look dir) + direction offset
|
||||
float baseYaw = getBaseYaw(caster, target);
|
||||
float dashYaw = baseYaw + chosenDirection.getAngleOffset();
|
||||
float yawRad = (float) Math.toRadians(dashYaw);
|
||||
|
||||
dashDirection = Vec3.createVectorHelper(
|
||||
-Math.sin(yawRad),
|
||||
0,
|
||||
Math.cos(yawRad)
|
||||
);
|
||||
setDirectionFromYaw(dashYaw);
|
||||
|
||||
// Small upward impulse for a skip/hop arc (gravity handles the descent)
|
||||
caster.motionY = 0.2;
|
||||
@@ -175,86 +139,59 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
// Safety timeout or missing state: force-complete to prevent stuck NPC
|
||||
if (!isPreview() && (dashDirection == null || tick > maxActiveTicks)) {
|
||||
stopDash(caster);
|
||||
if (checkTimeout(tick)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dashDirection == null) {
|
||||
if (movementDirection == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Stall detection: if entity hasn't moved since last tick, it's stuck against a wall
|
||||
if (!isPreview() && tick > 1) {
|
||||
double dx = caster.posX - prevTickX;
|
||||
double dz = caster.posZ - prevTickZ;
|
||||
if (dx * dx + dz * dz < 0.0001) {
|
||||
stopDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
if (checkStall(caster, tick)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
updatePrevPosition(caster);
|
||||
|
||||
// Check if reached max distance
|
||||
double travelDx = caster.posX - startX;
|
||||
double travelDz = caster.posZ - startZ;
|
||||
if (travelDx * travelDx + travelDz * travelDz >= (double) dashDistance * dashDistance) {
|
||||
stopDash(caster);
|
||||
if (getDistanceTraveledSq(caster) >= (double) dashDistance * dashDistance) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Block detection (skip in preview - no real world collision)
|
||||
if (!isPreview() && isDashBlocked(caster)) {
|
||||
stopDash(caster);
|
||||
if (checkBlocked(caster, dashSpeed)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move caster (motionY left to gravity for skip arc)
|
||||
caster.motionX = dashDirection.xCoord * dashSpeed;
|
||||
caster.motionZ = dashDirection.zCoord * dashSpeed;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
applyVelocity(caster, dashSpeed);
|
||||
|
||||
if (!isPreview()) {
|
||||
// Trail particles
|
||||
caster.worldObj.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopDash(EntityLivingBase caster) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
stopDash(caster);
|
||||
stopMomentum(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
stopDash(caster);
|
||||
stopMomentum(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
dashDirection = null;
|
||||
super.cleanup();
|
||||
chosenDirection = null;
|
||||
maxActiveTicks = 0;
|
||||
}
|
||||
|
||||
private boolean isDashBlocked(EntityLivingBase caster) {
|
||||
if (dashDirection == null) return true;
|
||||
return isMovementBlocked(caster, dashDirection.xCoord, dashDirection.zCoord, dashSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.util.Vec3;
|
||||
|
||||
/**
|
||||
* Abstract base class for abilities that move the caster along the ground
|
||||
* in a specific direction (Charge, Dash, etc.).
|
||||
* <p>
|
||||
* Provides shared movement logic: direction locking, stall detection with
|
||||
* player-aware grace period, velocity application, distance tracking,
|
||||
* rotation enforcement, and wall collision checking.
|
||||
* <p>
|
||||
* NOT for ballistic/arc movement (Slam uses a different pattern).
|
||||
*/
|
||||
public abstract class AbilityMovement extends Ability {
|
||||
|
||||
/**
|
||||
* Grace period (in active ticks) before stall detection activates.
|
||||
* Players need more time because:
|
||||
* - Server sets motionX/Z and sends S12 velocity packet to client
|
||||
* - Client receives S12 and starts moving
|
||||
* - Client sends C03 position packet with updated position back to server
|
||||
* Until the round trip completes, the server sees the old (stationary) position,
|
||||
* which would falsely trigger stall detection.
|
||||
*/
|
||||
private static final int PLAYER_STALL_GRACE_TICKS = 5;
|
||||
private static final int NPC_STALL_GRACE_TICKS = 2;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// COMMON MOVEMENT STATE (transient, not saved to NBT)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
protected transient double startX, startY, startZ;
|
||||
protected transient double prevTickX, prevTickZ;
|
||||
protected transient Vec3 movementDirection;
|
||||
protected transient float lockedYaw;
|
||||
protected transient int maxActiveTicks;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// OVERRIDES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
public boolean hasAbilityMovement() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
movementDirection = null;
|
||||
maxActiveTicks = 0;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// DIRECTION HELPERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Lock direction based on caster type.
|
||||
* NPC: toward target. Player: look direction.
|
||||
*/
|
||||
protected void lockDirection(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!isPlayerCaster(caster) && target != null) {
|
||||
lockDirectionToTarget(caster, target);
|
||||
} else {
|
||||
lockDirectionFromLook(caster);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock direction to the caster's look direction (horizontal only).
|
||||
*/
|
||||
protected void lockDirectionFromLook(EntityLivingBase caster) {
|
||||
float yaw = (float) Math.toRadians(caster.rotationYaw);
|
||||
movementDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw));
|
||||
lockedYaw = computeYawFromDirection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock direction toward a target entity.
|
||||
* Falls back to look direction if target is at the same position.
|
||||
*/
|
||||
protected void lockDirectionToTarget(EntityLivingBase caster, EntityLivingBase target) {
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
if (len > 0) {
|
||||
movementDirection = Vec3.createVectorHelper(dx / len, 0, dz / len);
|
||||
lockedYaw = computeYawFromDirection();
|
||||
} else {
|
||||
lockDirectionFromLook(caster);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base yaw for direction calculation.
|
||||
* NPC: yaw toward target. Player: current look yaw.
|
||||
*/
|
||||
protected float getBaseYaw(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!isPlayerCaster(caster) && target != null) {
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
return (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
}
|
||||
return caster.rotationYaw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set movement direction from a yaw angle (degrees).
|
||||
*/
|
||||
protected void setDirectionFromYaw(float yawDegrees) {
|
||||
float yawRad = (float) Math.toRadians(yawDegrees);
|
||||
movementDirection = Vec3.createVectorHelper(-Math.sin(yawRad), 0, Math.cos(yawRad));
|
||||
lockedYaw = yawDegrees;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// MOVEMENT INITIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Initialize movement tracking. Call from onExecute().
|
||||
*
|
||||
* @param caster The entity performing the movement
|
||||
* @param distance Maximum travel distance (for timeout computation)
|
||||
* @param speed Movement speed in blocks/tick (for timeout computation)
|
||||
*/
|
||||
protected void initMovement(EntityLivingBase caster, float distance, float speed) {
|
||||
startX = caster.posX;
|
||||
startY = caster.posY;
|
||||
startZ = caster.posZ;
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
maxActiveTicks = speed > 0 ? (int) (distance / speed) + 10 : 10;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ACTIVE TICK CHECKS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Check for timeout or missing movement direction.
|
||||
*
|
||||
* @return true if the ability should stop
|
||||
*/
|
||||
protected boolean checkTimeout(int tick) {
|
||||
return !isPreview() && (movementDirection == null || tick > maxActiveTicks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check stall detection with appropriate grace period for player vs NPC.
|
||||
* Players need a longer grace period because velocity sync via S12 packet
|
||||
* takes time to reach the client, and C03 position packets lag behind.
|
||||
*
|
||||
* @return true if the entity is stalled (not moving)
|
||||
*/
|
||||
protected boolean checkStall(EntityLivingBase caster, int tick) {
|
||||
if (isPreview()) return false;
|
||||
int graceTicks = isPlayerCaster(caster) ? PLAYER_STALL_GRACE_TICKS : NPC_STALL_GRACE_TICKS;
|
||||
if (tick <= graceTicks) return false;
|
||||
|
||||
double dx = caster.posX - prevTickX;
|
||||
double dz = caster.posZ - prevTickZ;
|
||||
return dx * dx + dz * dz < 0.0001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update previous tick position for stall detection.
|
||||
* Call after stall check, before movement application.
|
||||
*/
|
||||
protected void updatePrevPosition(EntityLivingBase caster) {
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total linear distance traveled from start position.
|
||||
*/
|
||||
protected double getDistanceTraveled(EntityLivingBase caster) {
|
||||
double dx = caster.posX - startX;
|
||||
double dz = caster.posZ - startZ;
|
||||
return Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total distance traveled squared (more efficient for comparison).
|
||||
*/
|
||||
protected double getDistanceTraveledSq(EntityLivingBase caster) {
|
||||
double dx = caster.posX - startX;
|
||||
double dz = caster.posZ - startZ;
|
||||
return dx * dx + dz * dz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if movement direction is blocked by walls.
|
||||
*
|
||||
* @return true if blocked
|
||||
*/
|
||||
protected boolean checkBlocked(EntityLivingBase caster, float speed) {
|
||||
if (isPreview() || movementDirection == null) return false;
|
||||
return isMovementBlocked(caster, movementDirection.xCoord, movementDirection.zCoord, speed);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// VELOCITY APPLICATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Apply movement velocity to the caster (horizontal only, Y unchanged).
|
||||
*/
|
||||
protected void applyVelocity(EntityLivingBase caster, float speed) {
|
||||
caster.motionX = movementDirection.xCoord * speed;
|
||||
caster.motionZ = movementDirection.zCoord * speed;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply movement velocity with zero vertical motion.
|
||||
* Used for ground-locked movement like Charge.
|
||||
*/
|
||||
protected void applyVelocityFlat(EntityLivingBase caster, float speed) {
|
||||
caster.motionX = movementDirection.xCoord * speed;
|
||||
caster.motionY = 0;
|
||||
caster.motionZ = movementDirection.zCoord * speed;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all horizontal momentum.
|
||||
*/
|
||||
protected void stopMomentum(EntityLivingBase caster) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ROTATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Compute yaw angle (degrees) from current movement direction.
|
||||
*/
|
||||
protected float computeYawFromDirection() {
|
||||
if (movementDirection == null) return 0;
|
||||
return (float) Math.toDegrees(Math.atan2(-movementDirection.xCoord, movementDirection.zCoord));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce locked rotation on all yaw fields.
|
||||
* Sets rotationYaw, rotationYawHead, prevRotationYaw, prevRotationYawHead,
|
||||
* renderYawOffset, and prevRenderYawOffset to lockedYaw.
|
||||
*/
|
||||
protected void enforceLockedRotation(EntityLivingBase caster) {
|
||||
caster.rotationYaw = lockedYaw;
|
||||
caster.rotationYawHead = lockedYaw;
|
||||
caster.prevRotationYaw = lockedYaw;
|
||||
caster.prevRotationYawHead = lockedYaw;
|
||||
caster.renderYawOffset = lockedYaw;
|
||||
caster.prevRenderYawOffset = lockedYaw;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user