diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 871249885..780ddc050 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -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 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 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 8dbd13c92..41c5faaa1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -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 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java new file mode 100644 index 000000000..de2090cc3 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java @@ -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.). + *

+ * Provides shared movement logic: direction locking, stall detection with + * player-aware grace period, velocity application, distance tracking, + * rotation enforcement, and wall collision checking. + *

+ * 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; + } +}