More Abilities

This commit is contained in:
Kam committed 2026-01-26 04:42:23 -05:00
1 parent de043f4a25
commit 651f2d0d84
24 files changed
+560 -220

No files matched your search

@@ -61,10 +61,10 @@ public class RenderAbilityBeam extends RenderAbilityProjectile {
GL11.glTranslated(renderOriginX, renderOriginY, renderOriginZ);
// Render tail orb at origin (0,0,0) - static, no rotation
renderTailOrb(headSize * 0.8f, innerColor, outerColor);
renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled());
// Render trail segments with smooth connections
renderSmoothTrail(trail, beamWidth, innerColor, outerColor);
renderSmoothTrail(trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth());
// Render head at interpolated offset position
renderHead(beam, headOffsetX, headOffsetY, headOffsetZ, headSize, innerColor, outerColor, partialTicks);
@@ -78,17 +78,19 @@ public class RenderAbilityBeam extends RenderAbilityProjectile {
* Render the tail orb at the origin (where the beam emanates from).
* This orb does not rotate - it's a stable anchor point.
*/
private void renderTailOrb(float size, int innerColor, int outerColor) {
private void renderTailOrb(float size, int innerColor, int outerColor, boolean outerColorEnabled) {
GL11.glPushMatrix();
// Already at origin (0,0,0)
// Render outer glow
GL11.glDepthMask(false);
GL11.glPushMatrix();
GL11.glScalef(size, size, size);
renderCube(outerColor, 0.4f, 0.5f);
GL11.glPopMatrix();
GL11.glDepthMask(true);
// Render outer glow only if enabled
if (outerColorEnabled) {
GL11.glDepthMask(false);
GL11.glPushMatrix();
GL11.glScalef(size, size, size);
renderCube(outerColor, 0.4f, 0.5f);
GL11.glPopMatrix();
GL11.glDepthMask(true);
}
// Render inner core (smaller, solid)
GL11.glPushMatrix();
@@ -103,7 +105,8 @@ public class RenderAbilityBeam extends RenderAbilityProjectile {
* Render the beam trail with smooth connections between segments.
* Uses averaged perpendicular vectors at connection points to prevent gaps.
*/
private void renderSmoothTrail(List<Vec3> trail, float width, int innerColor, int outerColor) {
private void renderSmoothTrail(List<Vec3> trail, float width, int innerColor, int outerColor,
boolean outerColorEnabled, float outerColorWidth) {
if (trail.size() < 2) return;
int trailSize = trail.size();
@@ -112,10 +115,12 @@ public class RenderAbilityBeam extends RenderAbilityProjectile {
// At connection points, average the perpendiculars for smooth transitions
List<double[]> perpFrames = computePerpendiculars(trail);
// Render outer glow trail (wider, translucent)
GL11.glDepthMask(false);
renderTrailTube(trail, perpFrames, width * 1.8f, outerColor, 0.3f, true);
GL11.glDepthMask(true);
// Render outer glow trail (wider, translucent) - only if enabled
if (outerColorEnabled) {
GL11.glDepthMask(false);
renderTrailTube(trail, perpFrames, width * outerColorWidth, outerColor, 0.3f, true);
GL11.glDepthMask(true);
}
// Render inner core trail
renderTrailTube(trail, perpFrames, width * 0.6f, innerColor, 0.9f, false);
@@ -331,10 +336,12 @@ public class RenderAbilityBeam extends RenderAbilityProjectile {
GL11.glPushMatrix();
GL11.glScalef(scale, scale, scale);
// Render outer glow
GL11.glDepthMask(false);
renderCube(outerColor, 0.5f, 0.5f);
GL11.glDepthMask(true);
// Render outer glow only if enabled
if (beam.isOuterColorEnabled()) {
GL11.glDepthMask(false);
renderCube(outerColor, 0.5f, 0.5f);
GL11.glDepthMask(true);
}
// Render inner core
float innerScale = 0.6f;
@@ -39,10 +39,13 @@ public class RenderAbilityDisc extends RenderAbilityProjectile {
// Apply ONLY Y-axis rotation for flat spin
GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f);
// Render outer square (translucent)
GL11.glDepthMask(false);
renderFlatSquare(disc.getOuterColor(), 0.5f, radius, thickness);
GL11.glDepthMask(true);
// Render outer square (translucent) - only if enabled
if (disc.isOuterColorEnabled()) {
float outerScale = disc.getOuterColorWidth() / 1.8f; // Normalize to default width
GL11.glDepthMask(false);
renderFlatSquare(disc.getOuterColor(), 0.5f, radius * outerScale, thickness);
GL11.glDepthMask(true);
}
// Render inner square (solid, smaller)
float innerScale = 0.6f;
@@ -84,19 +84,22 @@ public class RenderAbilityLaser extends RenderAbilityProjectile {
double vertY = dz * horzX - dx * horzZ;
double vertZ = dx * horzY - dy * horzX;
// Render outer rectangle (larger, outer color, translucent)
GL11.glDepthMask(false);
renderBeamRectangle(renderStartX, renderStartY, renderStartZ,
renderEndX, renderEndY, renderEndZ,
horzX, horzY, horzZ, vertX, vertY, vertZ,
width * 1.8f, laser.getOuterColor(), alpha * 0.4f);
// Render outer rectangle (larger, outer color, translucent) - only if enabled
if (laser.isOuterColorEnabled()) {
float outerScale = laser.getOuterColorWidth();
GL11.glDepthMask(false);
renderBeamRectangle(renderStartX, renderStartY, renderStartZ,
renderEndX, renderEndY, renderEndZ,
horzX, horzY, horzZ, vertX, vertY, vertZ,
width * outerScale, laser.getOuterColor(), alpha * 0.4f);
// Render middle layer
renderBeamRectangle(renderStartX, renderStartY, renderStartZ,
renderEndX, renderEndY, renderEndZ,
horzX, horzY, horzZ, vertX, vertY, vertZ,
width * 1.3f, laser.getOuterColor(), alpha * 0.7f);
GL11.glDepthMask(true);
// Render middle layer
renderBeamRectangle(renderStartX, renderStartY, renderStartZ,
renderEndX, renderEndY, renderEndZ,
horzX, horzY, horzZ, vertX, vertY, vertZ,
width * 1.3f, laser.getOuterColor(), alpha * 0.7f);
GL11.glDepthMask(true);
}
// Render inner core (smaller, inner color, solid)
renderBeamRectangle(renderStartX, renderStartY, renderStartZ,
@@ -69,11 +69,17 @@ public class RenderAbilityOrb extends Render {
GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f);
GL11.glRotatef(orb.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f);
// Render outer sphere (translucent)
GL11.glDepthMask(false);
this.bindTexture(WHITE_TEXTURE);
renderSphere(orb.getOuterColor(), 0.6f);
GL11.glDepthMask(true);
// Render outer sphere (translucent) - only if enabled
if (orb.isOuterColorEnabled()) {
GL11.glDepthMask(false);
this.bindTexture(WHITE_TEXTURE);
float outerScale = orb.getOuterColorWidth() / 1.8f; // Normalize to default width
GL11.glPushMatrix();
GL11.glScalef(outerScale, outerScale, outerScale);
renderSphere(orb.getOuterColor(), 0.6f);
GL11.glPopMatrix();
GL11.glDepthMask(true);
}
// Render inner sphere (more opaque)
float innerScale = 0.75f;
@@ -26,6 +26,8 @@ public class RenderAbilitySweeper extends Render {
float beamWidth = sweeper.getBeamWidth();
int innerColor = sweeper.getInnerColor();
int outerColor = sweeper.getOuterColor();
boolean outerColorEnabled = sweeper.isOuterColorEnabled();
float outerColorWidth = sweeper.getOuterColorWidth();
float angle = sweeper.getInterpolatedAngle(partialTicks);
GL11.glPushMatrix();
@@ -35,7 +37,7 @@ public class RenderAbilitySweeper extends Render {
GL11.glRotatef(-angle, 0, 1, 0); // Negative for correct direction
// Render the beam extending in the +Z direction (like beam trail segments)
renderBeam(beamLength, beamWidth, innerColor, outerColor);
renderBeam(beamLength, beamWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth);
GL11.glPopMatrix();
@@ -65,7 +67,8 @@ public class RenderAbilitySweeper extends Render {
* Render beam using the same style as the Beam entity trail.
* Three layers: outer glow, middle, inner core.
*/
private void renderBeam(float length, float width, int innerColor, int outerColor) {
private void renderBeam(float length, float width, int innerColor, int outerColor,
boolean outerColorEnabled, float outerColorWidth) {
// Extract colors
float outerR = ((outerColor >> 16) & 0xFF) / 255.0f;
float outerG = ((outerColor >> 8) & 0xFF) / 255.0f;
@@ -78,13 +81,15 @@ public class RenderAbilitySweeper extends Render {
// Render from origin (0,0,0) to (0,0,length) in local coords
// After rotation this extends in the correct world direction
// Outer glow (wider, translucent) - like beam trail
GL11.glDepthMask(false);
renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.8f, outerR, outerG, outerB, 0.3f);
GL11.glDepthMask(true);
// Outer glow (wider, translucent) - like beam trail - only if enabled
if (outerColorEnabled) {
GL11.glDepthMask(false);
renderBeamSegment(0, 0, 0, 0, 0, length, width * outerColorWidth, outerR, outerG, outerB, 0.3f);
GL11.glDepthMask(true);
// Middle layer
renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.3f, outerR, outerG, outerB, 0.6f);
// Middle layer
renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.3f, outerR, outerG, outerB, 0.6f);
}
// Inner core (solid) - like beam trail inner
renderBeamSegment(0, 0, 0, 0, 0, length, width * 0.6f, innerR, innerG, innerB, 1.0f);
@@ -53,6 +53,10 @@ public abstract class Ability implements IAbility {
// Interruption
protected boolean interruptible = true;
// Wait for completion - if true, ability controls when active phase ends (ignores activeTicks)
// Used for abilities like Slam that need to wait for landing, or Beam that runs until entity dies
protected boolean waitForCompletion = false;
// Feedback
protected boolean lockMovement = true;
protected int windUpColor = 0x80FF4400; // Telegraph color during wind up
@@ -133,7 +137,9 @@ public abstract class Ability implements IAbility {
*/
protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity,
float damage, float knockback) {
return applyAbilityDamage(npc, hitEntity, damage, knockback, 0.0f);
double dx = hitEntity.posX - npc.posX;
double dz = hitEntity.posZ - npc.posZ;
return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, 0.0f, dx, dz);
}
/**
@@ -149,6 +155,36 @@ public abstract class Ability implements IAbility {
*/
protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity,
float damage, float knockback, float knockbackUp) {
double dx = hitEntity.posX - npc.posX;
double dz = hitEntity.posZ - npc.posZ;
return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, knockbackUp, dx, dz);
}
/**
* Apply damage to an entity with ability hit event support and custom knockback direction.
* Fires the abilityHit script event, allowing scripts to modify or cancel the damage.
*
* @param npc The NPC executing the ability
* @param hitEntity The entity being hit
* @param damage The damage amount
* @param knockback The horizontal knockback
* @param knockbackDirX The X component of knockback direction
* @param knockbackDirZ The Z component of knockback direction
* @return true if damage was applied (not cancelled), false if cancelled
*/
protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity,
float damage, float knockback,
double knockbackDirX, double knockbackDirZ) {
return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, 0.0f, knockbackDirX, knockbackDirZ);
}
/**
* Internal unified damage application method.
* All public damage methods delegate to this.
*/
private boolean applyAbilityDamageInternal(EntityNPCInterface npc, EntityLivingBase hitEntity,
float damage, float knockback, float knockbackUp,
double knockbackDirX, double knockbackDirZ) {
DataAbilities dataAbilities = npc.abilities;
AbilityEvent.HitEvent event = dataAbilities.fireHitEvent(
this, currentTarget, hitEntity, damage, knockback, knockbackUp);
@@ -168,49 +204,6 @@ public abstract class Ability implements IAbility {
}
// Apply knockback if any
if (finalKnockback > 0 || finalKnockbackUp > 0) {
double dx = hitEntity.posX - npc.posX;
double dz = hitEntity.posZ - npc.posZ;
applyNpcKnockback(hitEntity, dx, dz, finalKnockback, finalKnockbackUp);
}
return true;
}
/**
* Apply damage to an entity with ability hit event support and custom knockback direction.
* Fires the abilityHit script event, allowing scripts to modify or cancel the damage.
*
* @param npc The NPC executing the ability
* @param hitEntity The entity being hit
* @param damage The damage amount
* @param knockback The horizontal knockback
* @param knockbackDirX The X component of knockback direction (normalized)
* @param knockbackDirZ The Z component of knockback direction (normalized)
* @return true if damage was applied (not cancelled), false if cancelled
*/
protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity,
float damage, float knockback,
double knockbackDirX, double knockbackDirZ) {
DataAbilities dataAbilities = npc.abilities;
AbilityEvent.HitEvent event = dataAbilities.fireHitEvent(
this, currentTarget, hitEntity, damage, knockback, 0.0f);
if (event == null) {
return false; // Cancelled
}
// Apply damage with potentially modified values
float finalDamage = event.getDamage();
float finalKnockback = event.getKnockback();
float finalKnockbackUp = event.getKnockbackUp();
// Apply damage - use NpcDamageSource for consistency
if (finalDamage > 0) {
hitEntity.attackEntityFrom(new NpcDamageSource("mob", npc), finalDamage);
}
// Apply knockback in specified direction
if (finalKnockback > 0 || finalKnockbackUp > 0) {
applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback, finalKnockbackUp);
}
@@ -383,6 +376,11 @@ public abstract class Ability implements IAbility {
if (positionAtNpc) {
// AOE_SELF abilities: telegraph follows NPC during windup
instance.setEntityIdToFollow(npc.getEntityId());
// For LINE/CONE telegraphs: track target direction during windup
if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) {
instance.setTargetEntityId(target.getEntityId());
}
} else if (target != null) {
// AOE_TARGET abilities: telegraph follows target during windup
instance.setEntityIdToFollow(target.getEntityId());
@@ -487,8 +485,15 @@ public abstract class Ability implements IAbility {
}
/**
* Tick the ability. Returns true if phase changed.
* Note: Cooldown is managed by DataAbilities when the ability completes.
* Tick the ability forward. Called every game tick while executing.
* <p>
* Normal flow: WINDUP -> ACTIVE -> IDLE (no recovery in normal completion)
* Interrupted flow: WINDUP -> RECOVERY -> IDLE (recovery only on interrupt)
* <p>
* If waitForCompletion is true, the ability controls when active ends
* by calling signalCompletion() instead of using activeTicks.
*
* @return true if phase changed this tick
*/
public boolean tick() {
if (phase == AbilityPhase.IDLE) return false;
@@ -504,16 +509,20 @@ public abstract class Ability implements IAbility {
}
break;
case ACTIVE:
if (currentTick >= activeTicks) {
phase = AbilityPhase.RECOVERY;
// If waitForCompletion is true, ability must call signalCompletion()
// Otherwise, auto-complete when activeTicks is reached
if (!waitForCompletion && currentTick >= activeTicks) {
phase = AbilityPhase.IDLE;
currentTick = 0;
return true;
}
break;
case RECOVERY:
// Recovery phase is only entered on interrupt
// Count down recoveryTicks then go to IDLE
if (currentTick >= recoveryTicks) {
phase = AbilityPhase.IDLE;
// Cooldown is managed by DataAbilities.onAbilityComplete()
currentTick = 0;
return true;
}
break;
@@ -522,11 +531,35 @@ public abstract class Ability implements IAbility {
}
/**
* Interrupt this ability
* Signal that the ability has completed its active phase.
* Called by abilities with waitForCompletion=true when they are done.
* For example, AbilitySlam calls this when the NPC lands.
*
* @return true if phase changed to IDLE
*/
public boolean signalCompletion() {
if (phase == AbilityPhase.ACTIVE) {
phase = AbilityPhase.IDLE;
currentTick = 0;
return true;
}
return false;
}
/**
* Interrupt this ability. Transitions to RECOVERY phase if interruptible.
* Called when NPC takes direct damage during WINDUP.
*/
public void interrupt() {
phase = AbilityPhase.IDLE;
currentTick = 0;
if (interruptible && phase == AbilityPhase.WINDUP) {
// Interrupted during windup - go to recovery (dazed state)
phase = AbilityPhase.RECOVERY;
currentTick = 0;
} else {
// Force stop - go directly to IDLE
phase = AbilityPhase.IDLE;
currentTick = 0;
}
currentTarget = null;
telegraphInstance = null;
}
@@ -611,6 +644,7 @@ public abstract class Ability implements IAbility {
nbt.setInteger("active", activeTicks);
nbt.setInteger("recovery", recoveryTicks);
nbt.setBoolean("interruptible", interruptible);
nbt.setBoolean("waitForCompletion", waitForCompletion);
nbt.setBoolean("lockMovement", lockMovement);
nbt.setInteger("windUpColor", windUpColor);
nbt.setInteger("activeColor", activeColor);
@@ -650,6 +684,7 @@ public abstract class Ability implements IAbility {
activeTicks = nbt.getInteger("active");
recoveryTicks = nbt.getInteger("recovery");
interruptible = nbt.getBoolean("interruptible");
waitForCompletion = nbt.getBoolean("waitForCompletion");
lockMovement = nbt.getBoolean("lockMovement");
// Support old telegraphColor key for backwards compatibility
if (nbt.hasKey("windUpColor")) {
@@ -807,6 +842,14 @@ public abstract class Ability implements IAbility {
this.interruptible = interruptible;
}
public boolean isWaitForCompletion() {
return waitForCompletion;
}
public void setWaitForCompletion(boolean waitForCompletion) {
this.waitForCompletion = waitForCompletion;
}
public boolean isLockMovement() {
return lockMovement;
}
@@ -4,6 +4,8 @@ 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.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityDisc;
import net.minecraft.entity.EntityLivingBase;
@@ -69,8 +71,8 @@ public class AbilityDisc extends Ability {
this.windUpTicks = 20;
this.activeTicks = 1;
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.POINT;
this.showTelegraph = false;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
}
@Override
@@ -105,7 +107,7 @@ public class AbilityDisc extends Ability {
EntityAbilityDisc disc = new EntityAbilityDisc(
world, npc, target,
spawnX, spawnY, spawnZ,
discRadius, discThickness, innerColor, outerColor, rotationSpeed,
discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed,
damage, knockback, knockbackUp,
speed, homing, homingStrength, homingRange,
boomerang, boomerangDelay,
@@ -129,6 +131,33 @@ public class AbilityDisc extends Ability {
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
}
@Override
public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create circle telegraph at target position
Telegraph telegraph = Telegraph.circle(discRadius * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw);
instance.setCasterEntityId(npc.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return discRadius * 1.5f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("speed", speed);
@@ -182,7 +211,7 @@ public class AbilityDisc extends Ability {
this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF8800;
this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f;
this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled");
this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 8.0f;
this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 5.0f;
}
// Getters & Setters
@@ -4,6 +4,8 @@ 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.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityBeam;
import net.minecraft.entity.EntityLivingBase;
@@ -68,8 +70,8 @@ public class AbilityEnergyBeam extends Ability {
this.windUpTicks = 40;
this.activeTicks = 200; // Match maxLifetime to keep NPC locked while beam is active
this.recoveryTicks = 5;
this.telegraphType = TelegraphType.POINT;
this.showTelegraph = false;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
}
@Override
@@ -104,7 +106,7 @@ public class AbilityEnergyBeam extends Ability {
EntityAbilityBeam beam = new EntityAbilityBeam(
world, npc, target,
spawnX, spawnY, spawnZ,
beamWidth, headSize, innerColor, outerColor, rotationSpeed,
beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed,
damage, knockback, knockbackUp,
speed, homing, homingStrength, homingRange,
explosive, explosionRadius, explosionDamageFalloff,
@@ -127,6 +129,33 @@ public class AbilityEnergyBeam extends Ability {
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
}
@Override
public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create circle telegraph at target position
Telegraph telegraph = Telegraph.circle(headSize * 2.0f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw);
instance.setCasterEntityId(npc.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return headSize * 2.0f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("speed", speed);
@@ -128,17 +128,8 @@ public class AbilityGuard extends Ability {
if (wasHit && counterSound != null && !counterSound.isEmpty()) {
world.playSoundAtEntity(npc, counterSound, 1.0f, 1.2f);
}
if (counterAnimationId >= 0) {
if (noppes.npcs.controllers.AnimationController.Instance != null) {
noppes.npcs.controllers.data.Animation animation =
noppes.npcs.controllers.AnimationController.Instance.animations.get(counterAnimationId);
if (animation != null) {
npc.display.animationData.setEnabled(true);
npc.display.animationData.setAnimation(animation);
npc.display.animationData.updateClient();
}
}
}
// Use shared animation utility
npc.abilities.playAbilityAnimation(counterAnimationId);
counterTriggered = false;
lastAttacker = null;
@@ -184,6 +184,11 @@ public class AbilityHeal extends Ability {
}
}
@Override
public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) {
healedAllies.clear();
}
@Override
public void reset() {
super.reset();
@@ -46,6 +46,8 @@ public class AbilityLaserShot extends Ability {
// Visual properties
private int innerColor = 0xFFFFFF;
private int outerColor = 0xFF0000;
private float outerColorWidth = 1.8f;
private boolean outerColorEnabled = true;
public AbilityLaserShot() {
this.typeId = "ability.cnpc.laser_shot";
@@ -103,7 +105,7 @@ public class AbilityLaserShot extends Ability {
EntityAbilityLaser laser = new EntityAbilityLaser(
world, npc, target,
spawnX, spawnY, spawnZ,
laserWidth, innerColor, outerColor,
laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth,
damage, knockback, knockbackUp,
expansionSpeed, lingerTicks,
explosive, explosionRadius, explosionDamageFalloff,
@@ -144,6 +146,8 @@ public class AbilityLaserShot extends Ability {
nbt.setInteger("slowLevel", slowLevel);
nbt.setInteger("innerColor", innerColor);
nbt.setInteger("outerColor", outerColor);
nbt.setFloat("outerColorWidth", outerColorWidth);
nbt.setBoolean("outerColorEnabled", outerColorEnabled);
}
@Override
@@ -164,6 +168,8 @@ public class AbilityLaserShot extends Ability {
this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0;
this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF;
this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000;
this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f;
this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled");
}
// Getters & Setters
@@ -199,4 +205,8 @@ public class AbilityLaserShot extends Ability {
public void setInnerColor(int innerColor) { this.innerColor = innerColor; }
public int getOuterColor() { return outerColor; }
public void setOuterColor(int outerColor) { this.outerColor = outerColor; }
public float getOuterColorWidth() { return outerColorWidth; }
public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; }
public boolean isOuterColorEnabled() { return outerColorEnabled; }
public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; }
}
@@ -4,6 +4,8 @@ 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.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityOrb;
import net.minecraft.entity.EntityLivingBase;
@@ -65,8 +67,8 @@ public class AbilityOrb extends Ability {
this.windUpTicks = 30;
this.activeTicks = 1; // Minimal - just spawn and done
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.POINT;
this.showTelegraph = false;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
}
@Override
@@ -103,7 +105,7 @@ public class AbilityOrb extends Ability {
EntityAbilityOrb orb = new EntityAbilityOrb(
world, npc, target,
spawnX, spawnY, spawnZ,
orbSize, innerColor, outerColor, rotationSpeed,
orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed,
damage, knockback, knockbackUp,
orbSpeed, homing, homingStrength, homingRange,
explosive, explosionRadius, explosionDamageFalloff,
@@ -129,6 +131,33 @@ public class AbilityOrb extends Ability {
// Nothing to clean up - entity manages itself
}
@Override
public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create small circle telegraph at target position
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw);
instance.setCasterEntityId(npc.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return orbSize * 1.5f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("orbSpeed", orbSpeed);
@@ -49,10 +49,11 @@ public class AbilitySlam extends Ability {
this.name = "Slam";
this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target
this.windUpTicks = 30;
this.activeTicks = 60; // Max time in air + landing
this.activeTicks = 60; // Max time in air (used as timeout if waitForCompletion fails)
this.recoveryTicks = 10;
this.cooldownTicks = 100;
this.lockMovement = true;
this.waitForCompletion = true; // Ability completes when NPC lands, not by activeTicks
this.minRange = 2.0f;
this.maxRange = 15.0f;
this.telegraphType = TelegraphType.CIRCLE;
@@ -258,12 +259,15 @@ public class AbilitySlam extends Ability {
}
/**
* Called when NPC lands - deal AOE damage.
* Called when NPC lands - deal AOE damage and signal completion.
*/
private void onLanding(EntityNPCInterface npc, World world) {
hasLanded = true;
npc.setNpcJumpingState(false);
// Signal that the ability has completed its active phase
signalCompletion();
// Reset fall distance to prevent fall damage on landing
npc.fallDistance = 0;
@@ -92,7 +92,7 @@ public class AbilitySweeper extends Ability {
// Spawn the entity that handles BOTH visuals AND damage
activeEntity = new EntityAbilitySweeper(world, npc, target,
beamLength, beamWidth, beamHeight,
innerColor, outerColor,
innerColor, outerColor, outerColorEnabled, outerColorWidth,
sweepSpeed, numberOfRotations,
damage, damageInterval, piercing,
lockOnTarget);
@@ -26,6 +26,7 @@ public class TelegraphInstance {
// Entity tracking
private int entityIdToFollow = -1;
private int casterEntityId = -1;
private int targetEntityId = -1; // For LINE telegraphs: entity to face (yaw tracking)
// Timing
private int remainingTicks;
@@ -81,6 +82,16 @@ public class TelegraphInstance {
}
}
// Update yaw to face target entity (for LINE telegraphs)
if (targetEntityId >= 0 && world != null) {
Entity target = world.getEntityByID(targetEntityId);
if (target != null) {
double dx = target.posX - this.x;
double dz = target.posZ - this.z;
this.yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI);
}
}
// Check for warning phase transition
if (!isWarning && telegraph != null) {
if (remainingTicks <= telegraph.getWarningStartTick()) {
@@ -108,7 +119,7 @@ public class TelegraphInstance {
}
/**
* Get alpha-modulated color for pulsing animation.
* Get alpha-modulated color for breathing animation.
*/
public int getAnimatedColor(float partialTicks) {
int baseColor = getCurrentColor();
@@ -116,9 +127,12 @@ public class TelegraphInstance {
return baseColor;
}
// Pulsing animation: modulate alpha based on time
// Breathing animation: slow, smooth modulation of alpha
float time = (totalTicks - remainingTicks) + partialTicks;
float pulse = (float) (Math.sin(time * 0.3) * 0.3 + 0.7); // 0.4 to 1.0 range
// Slower breathing: 0.08 for ~4 second cycle, smoother sine curve
float breathPhase = (float) Math.sin(time * 0.08);
// Smooth easing: square the sine for more organic feel, range 0.5 to 1.0
float pulse = 0.5f + (breathPhase * breathPhase * 0.5f * Math.signum(breathPhase));
int alpha = (baseColor >> 24) & 0xFF;
int r = (baseColor >> 16) & 0xFF;
@@ -149,6 +163,7 @@ public class TelegraphInstance {
nbt.setInteger("entityIdToFollow", entityIdToFollow);
nbt.setInteger("casterEntityId", casterEntityId);
nbt.setInteger("targetEntityId", targetEntityId);
nbt.setInteger("remainingTicks", remainingTicks);
nbt.setInteger("totalTicks", totalTicks);
@@ -173,6 +188,7 @@ public class TelegraphInstance {
this.entityIdToFollow = nbt.getInteger("entityIdToFollow");
this.casterEntityId = nbt.getInteger("casterEntityId");
this.targetEntityId = nbt.getInteger("targetEntityId");
this.remainingTicks = nbt.getInteger("remainingTicks");
this.totalTicks = nbt.getInteger("totalTicks");
@@ -241,11 +257,12 @@ public class TelegraphInstance {
}
/**
* Locks the telegraph at its current position.
* Locks the telegraph at its current position and direction.
* Stops following any entity - used when ability commits to action.
*/
public void lockPosition() {
this.entityIdToFollow = -1;
this.targetEntityId = -1;
}
public int getCasterEntityId() {
@@ -256,6 +273,14 @@ public class TelegraphInstance {
this.casterEntityId = casterEntityId;
}
public int getTargetEntityId() {
return targetEntityId;
}
public void setTargetEntityId(int targetEntityId) {
this.targetEntityId = targetEntityId;
}
public int getRemainingTicks() {
return remainingTicks;
}
@@ -59,7 +59,8 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
*/
public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target,
double x, double y, double z,
float beamWidth, float headSize, int innerColor, int outerColor, float rotationSpeed,
float beamWidth, float headSize, int innerColor, int outerColor,
boolean outerColorEnabled, float outerColorWidth, float rotationSpeed,
float damage, float knockback, float knockbackUp,
float speed, boolean homing, float homingStrength, float homingRange,
boolean explosive, float explosionRadius, float explosionDamageFalloff,
@@ -69,7 +70,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
// Initialize base properties
initProjectile(owner, target, x, y, z,
headSize, innerColor, outerColor, rotationSpeed,
headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed,
damage, knockback, knockbackUp,
explosive, explosionRadius, explosionDamageFalloff,
stunDuration, slowDuration, slowLevel,
@@ -44,7 +44,8 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
*/
public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target,
double x, double y, double z,
float discRadius, float discThickness, int innerColor, int outerColor, float rotationSpeed,
float discRadius, float discThickness, int innerColor, int outerColor,
boolean outerColorEnabled, float outerColorWidth, float rotationSpeed,
float damage, float knockback, float knockbackUp,
float speed, boolean homing, float homingStrength, float homingRange,
boolean boomerang, int boomerangDelay,
@@ -55,7 +56,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
// Initialize base properties (use discRadius as size for base)
initProjectile(owner, target, x, y, z,
discRadius, innerColor, outerColor, rotationSpeed,
discRadius, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed,
damage, knockback, knockbackUp,
explosive, explosionRadius, explosionDamageFalloff,
stunDuration, slowDuration, slowLevel,
@@ -50,6 +50,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target,
double x, double y, double z,
float laserWidth, int innerColor, int outerColor,
boolean outerColorEnabled, float outerColorWidth,
float damage, float knockback, float knockbackUp,
float expansionSpeed, int lingerTicks,
boolean explosive, float explosionRadius, float explosionDamageFalloff,
@@ -59,7 +60,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
// Initialize base properties
initProjectile(owner, target, x, y, z,
laserWidth, innerColor, outerColor, 0.0f, // No rotation for laser
laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, 0.0f, // No rotation for laser
damage, knockback, knockbackUp,
explosive, explosionRadius, explosionDamageFalloff,
stunDuration, slowDuration, slowLevel,
@@ -34,7 +34,8 @@ public class EntityAbilityOrb extends EntityAbilityProjectile {
*/
public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target,
double x, double y, double z,
float orbSize, int innerColor, int outerColor, float rotationSpeed,
float orbSize, int innerColor, int outerColor,
boolean outerColorEnabled, float outerColorWidth, float rotationSpeed,
float damage, float knockback, float knockbackUp,
float speed, boolean homing, float homingStrength, float homingRange,
boolean explosive, float explosionRadius, float explosionDamageFalloff,
@@ -44,7 +45,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile {
// Initialize base properties
initProjectile(owner, target, x, y, z,
orbSize, innerColor, outerColor, rotationSpeed,
orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed,
damage, knockback, knockbackUp,
explosive, explosionRadius, explosionDamageFalloff,
stunDuration, slowDuration, slowLevel,
@@ -29,6 +29,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
protected float size = 1.0f;
protected int innerColor = 0xFFFFFF;
protected int outerColor = 0x8888FF;
protected boolean outerColorEnabled = true;
protected float outerColorWidth = 1.8f;
protected float rotationSpeed = 4.0f;
// ==================== COMBAT PROPERTIES ====================
@@ -85,7 +87,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
*/
protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target,
double x, double y, double z,
float size, int innerColor, int outerColor, float rotationSpeed,
float size, int innerColor, int outerColor,
boolean outerColorEnabled, float outerColorWidth, float rotationSpeed,
float damage, float knockback, float knockbackUp,
boolean explosive, float explosionRadius, float explosionDamageFalloff,
int stunDuration, int slowDuration, int slowLevel,
@@ -102,6 +105,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
this.size = size;
this.innerColor = innerColor;
this.outerColor = outerColor;
this.outerColorEnabled = outerColorEnabled;
this.outerColorWidth = outerColorWidth;
this.rotationSpeed = rotationSpeed;
// Combat
@@ -384,6 +389,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
return outerColor;
}
public boolean isOuterColorEnabled() {
return outerColorEnabled;
}
public float getOuterColorWidth() {
return outerColorWidth;
}
public float getRotationSpeed() {
return rotationSpeed;
}
@@ -438,6 +451,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
this.size = nbt.getFloat("Size");
this.innerColor = nbt.getInteger("InnerColor");
this.outerColor = nbt.getInteger("OuterColor");
this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled");
this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 1.8f;
this.rotationSpeed = nbt.hasKey("RotationSpeed") ? nbt.getFloat("RotationSpeed") : 4.0f;
this.damage = nbt.getFloat("Damage");
@@ -480,6 +495,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA
nbt.setFloat("Size", size);
nbt.setInteger("InnerColor", innerColor);
nbt.setInteger("OuterColor", outerColor);
nbt.setBoolean("OuterColorEnabled", outerColorEnabled);
nbt.setFloat("OuterColorWidth", outerColorWidth);
nbt.setFloat("RotationSpeed", rotationSpeed);
nbt.setFloat("Damage", damage);
@@ -32,6 +32,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
private float beamHeight = 0.8f;
private int innerColor = 0xFF6600;
private int outerColor = 0xFF0000;
private boolean outerColorEnabled = true;
private float outerColorWidth = 1.8f;
// Combat properties
private float damage = 5.0f;
@@ -70,6 +72,7 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
public EntityAbilitySweeper(World world, EntityNPCInterface owner, EntityLivingBase target,
float beamLength, float beamWidth, float beamHeight,
int innerColor, int outerColor,
boolean outerColorEnabled, float outerColorWidth,
float sweepSpeed, int numberOfRotations,
float damage, int damageInterval, boolean piercing,
boolean lockOnTarget) {
@@ -82,6 +85,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
this.beamHeight = beamHeight;
this.innerColor = innerColor;
this.outerColor = outerColor;
this.outerColorEnabled = outerColorEnabled;
this.outerColorWidth = outerColorWidth;
this.sweepSpeed = sweepSpeed;
this.numberOfRotations = numberOfRotations;
this.damage = damage;
@@ -323,6 +328,14 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
return outerColor;
}
public boolean isOuterColorEnabled() {
return outerColorEnabled;
}
public float getOuterColorWidth() {
return outerColorWidth;
}
/**
* Get the full interpolated angle INCLUDING base yaw for rendering.
*/
@@ -353,6 +366,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
this.beamHeight = nbt.getFloat("BeamHeight");
this.innerColor = nbt.getInteger("InnerColor");
this.outerColor = nbt.getInteger("OuterColor");
this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled");
this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 1.8f;
this.sweepSpeed = nbt.getFloat("SweepSpeed");
this.numberOfRotations = nbt.getInteger("NumRotations");
this.completedRotations = nbt.getInteger("CompletedRotations");
@@ -375,6 +390,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
nbt.setFloat("BeamHeight", beamHeight);
nbt.setInteger("InnerColor", innerColor);
nbt.setInteger("OuterColor", outerColor);
nbt.setBoolean("OuterColorEnabled", outerColorEnabled);
nbt.setFloat("OuterColorWidth", outerColorWidth);
nbt.setFloat("SweepSpeed", sweepSpeed);
nbt.setInteger("NumRotations", numberOfRotations);
nbt.setInteger("CompletedRotations", completedRotations);
@@ -399,6 +416,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
buffer.writeFloat(beamHeight);
buffer.writeInt(innerColor);
buffer.writeInt(outerColor);
buffer.writeBoolean(outerColorEnabled);
buffer.writeFloat(outerColorWidth);
buffer.writeFloat(sweepSpeed);
buffer.writeInt(numberOfRotations);
buffer.writeInt(completedRotations);
@@ -420,6 +439,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
this.beamHeight = buffer.readFloat();
this.innerColor = buffer.readInt();
this.outerColor = buffer.readInt();
this.outerColorEnabled = buffer.readBoolean();
this.outerColorWidth = buffer.readFloat();
this.sweepSpeed = buffer.readFloat();
this.numberOfRotations = buffer.readInt();
this.completedRotations = buffer.readInt();
+90 -58
View File
@@ -18,9 +18,7 @@ import noppes.npcs.scripted.NpcAPI;
import noppes.npcs.scripted.event.AbilityEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
@@ -47,9 +45,14 @@ public class DataAbilities {
public boolean enabled = false;
/**
* Minimum ticks between ability selections (global cooldown)
* Minimum cooldown ticks between abilities (global minimum)
*/
public int globalCooldown = 20;
public int minCooldown = 80;
/**
* Maximum cooldown ticks between abilities (global maximum)
*/
public int maxCooldown = 200;
// ═══════════════════════════════════════════════════════════════════
// RUNTIME STATE (not saved)
@@ -61,14 +64,9 @@ public class DataAbilities {
private transient Ability currentAbility;
/**
* Per-ability cooldown timers (ability ID -> world time when cooldown ends)
* World time when NPC cooldown ends (can select next ability)
*/
private transient Map<String, Long> cooldowns = new HashMap<>();
/**
* Ticks until next ability can be selected (global cooldown timer)
*/
private transient int globalCooldownTimer = 0;
private transient long cooldownEndTime = 0;
/**
* Last target used for ability execution
@@ -101,11 +99,6 @@ public class DataAbilities {
return;
}
// Tick global cooldown
if (globalCooldownTimer > 0) {
globalCooldownTimer--;
}
// Tick current ability if executing
if (currentAbility != null && currentAbility.isExecuting()) {
tickCurrentAbility();
@@ -192,24 +185,33 @@ public class DataAbilities {
npc.motionZ = 0;
}
}
// Lock look direction at target during ACTIVE phase only
if (shouldLockLookDirection() && target != null && target.isEntityAlive()) {
npc.getLookHelper().setLookPositionWithEntity(target, 30.0F, 30.0F);
}
}
}
/**
* Called when an ability completes.
* Calculates cooldown as: random(minCooldown, maxCooldown) + ability's cooldown offset
*/
private void onAbilityComplete() {
if (currentAbility != null) {
// Stop any ability animation
stopAbilityAnimation();
// Start per-ability cooldown
startCooldown(currentAbility);
// Calculate cooldown: random(min, max) + ability offset
int baseCooldown = minCooldown;
if (maxCooldown > minCooldown) {
baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1);
}
int totalCooldown = baseCooldown + currentAbility.getCooldownTicks();
cooldownEndTime = npc.worldObj.getTotalWorldTime() + totalCooldown;
currentAbility = null;
lastTarget = null;
// Start global cooldown
globalCooldownTimer = globalCooldown;
}
}
@@ -247,7 +249,8 @@ public class DataAbilities {
if (currentAbility != null && currentAbility.isExecuting()) {
return false;
}
if (globalCooldownTimer > 0) {
// Check if still on cooldown
if (npc.worldObj.getTotalWorldTime() < cooldownEndTime) {
return false;
}
return true;
@@ -293,11 +296,6 @@ public class DataAbilities {
return false;
}
// Check cooldown
if (isOnCooldown(ability)) {
return false;
}
// Check if already executing
if (ability.isExecuting()) {
return false;
@@ -358,8 +356,11 @@ public class DataAbilities {
/**
* Play an animation on the NPC by ID.
* Public so abilities can trigger animations directly if needed.
*
* @param animationId The global animation ID to play, or -1 for none
*/
private void playAbilityAnimation(int animationId) {
public void playAbilityAnimation(int animationId) {
if (animationId < 0) return;
if (AnimationController.Instance == null) return;
@@ -407,37 +408,25 @@ public class DataAbilities {
// ═══════════════════════════════════════════════════════════════════
/**
* Check if an ability is on cooldown.
* Check if NPC is on cooldown (cannot use any ability).
*/
public boolean isOnCooldown(Ability ability) {
Long endTime = cooldowns.get(ability.getId());
if (endTime == null) {
return false;
}
return npc.worldObj.getTotalWorldTime() < endTime;
public boolean isOnCooldown() {
return npc.worldObj.getTotalWorldTime() < cooldownEndTime;
}
/**
* Start cooldown for an ability.
* Get remaining cooldown ticks.
*/
private void startCooldown(Ability ability) {
long endTime = npc.worldObj.getTotalWorldTime() + ability.getCooldownTicks();
cooldowns.put(ability.getId(), endTime);
public long getRemainingCooldown() {
long remaining = cooldownEndTime - npc.worldObj.getTotalWorldTime();
return remaining > 0 ? remaining : 0;
}
/**
* Reset cooldown for a specific ability.
* Reset cooldown (allow immediate ability use).
*/
public void resetCooldown(String abilityId) {
cooldowns.remove(abilityId);
}
/**
* Reset all cooldowns.
*/
public void resetAllCooldowns() {
cooldowns.clear();
globalCooldownTimer = 0;
public void resetCooldown() {
cooldownEndTime = 0;
}
// ═══════════════════════════════════════════════════════════════════
@@ -489,9 +478,18 @@ public class DataAbilities {
/**
* Record a hit for the hit count condition.
* Includes periodic cleanup to prevent memory leak.
*/
private void recordHit() {
recentHitTimes.add(npc.worldObj.getTotalWorldTime());
long currentTime = npc.worldObj.getTotalWorldTime();
recentHitTimes.add(currentTime);
// Cleanup old entries periodically to prevent unbounded growth
// Keep only hits from last 5 minutes (6000 ticks) max
if (recentHitTimes.size() > 50) {
long cutoff = currentTime - 6000;
recentHitTimes.removeIf(time -> time < cutoff);
}
}
/**
@@ -517,6 +515,8 @@ public class DataAbilities {
/**
* Interrupt the currently executing ability.
* The ability will transition to RECOVERY phase (dazed state) and continue ticking.
* When RECOVERY completes, the ability will move to IDLE and onAbilityComplete() is called.
*/
public void interruptCurrentAbility(DamageSource source, float damage) {
if (currentAbility != null) {
@@ -532,9 +532,10 @@ public class DataAbilities {
NpcAPI.EVENT_BUS.post(interruptEvent);
currentAbility.onInterrupt(npc, source, damage);
currentAbility.interrupt();
currentAbility = null;
lastTarget = null;
currentAbility.interrupt(); // This now transitions to RECOVERY, not IDLE
// Don't clear currentAbility - let it tick through RECOVERY phase
// When RECOVERY ends and phase becomes IDLE, onAbilityComplete() will be called
}
}
@@ -605,7 +606,7 @@ public class DataAbilities {
*/
public void reset() {
stopCurrentAbility();
resetAllCooldowns();
resetCooldown();
// Reset execution state on all abilities
for (Ability ability : abilities) {
@@ -707,6 +708,24 @@ public class DataAbilities {
}
}
/**
* Check if NPC's look direction should be locked to target.
* Only locks during ACTIVE phase when lockMovement is true.
* This allows the NPC to freely track target during WINDUP and RECOVERY.
*/
public boolean shouldLockLookDirection() {
if (currentAbility == null || !currentAbility.isExecuting()) {
return false;
}
// Only lock look direction during ACTIVE phase
if (currentAbility.getPhase() != AbilityPhase.ACTIVE) {
return false;
}
return currentAbility.isLockMovement();
}
/**
* Check if NPC should skip normal attacks during ability execution.
*/
@@ -724,7 +743,8 @@ public class DataAbilities {
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
compound.setBoolean("AbilitiesEnabled", enabled);
compound.setInteger("AbilityGlobalCooldown", globalCooldown);
compound.setInteger("AbilityMinCooldown", minCooldown);
compound.setInteger("AbilityMaxCooldown", maxCooldown);
NBTTagList abilityList = new NBTTagList();
for (Ability ability : abilities) {
@@ -737,8 +757,20 @@ public class DataAbilities {
public void readFromNBT(NBTTagCompound compound) {
enabled = compound.getBoolean("AbilitiesEnabled");
globalCooldown = compound.hasKey("AbilityGlobalCooldown") ?
compound.getInteger("AbilityGlobalCooldown") : 20;
// Support old globalCooldown for backwards compatibility
if (compound.hasKey("AbilityMinCooldown")) {
minCooldown = compound.getInteger("AbilityMinCooldown");
maxCooldown = compound.getInteger("AbilityMaxCooldown");
} else if (compound.hasKey("AbilityGlobalCooldown")) {
// Migrate old single cooldown to min/max range
int oldCooldown = compound.getInteger("AbilityGlobalCooldown");
minCooldown = oldCooldown;
maxCooldown = oldCooldown * 2; // Give some range
} else {
minCooldown = 20;
maxCooldown = 60;
}
abilities.clear();
NBTTagList abilityList = compound.getTagList("Abilities", 10);
@@ -51,7 +51,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
// Settings
private boolean abilitiesEnabled = false;
private int globalCooldown = 20;
private int minCooldown = 20;
private int maxCooldown = 60;
private String search = "";
private int selectedAbilityIndex = -1;
@@ -72,12 +73,18 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
addLabel(new GuiNpcLabel(100, "gui.enabled", guiLeft + 5, y + 5));
addButton(new GuiNpcButton(100, guiLeft + 55, y, 50, 20, new String[]{"gui.no", "gui.yes"}, abilitiesEnabled ? 1 : 0));
// Global cooldown (spaced out since combat only is removed)
addLabel(new GuiNpcLabel(101, "ability.globalCooldown", guiLeft + 140, y + 5));
GuiNpcTextField cooldownField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 230, y, 50, 20, String.valueOf(globalCooldown));
cooldownField.setIntegersOnly();
cooldownField.setMinMaxDefault(0, 10000, 20);
addTextField(cooldownField);
// Cooldown range: Min Cooldown and Max Cooldown
addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 115, y + 5));
GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 185, y, 40, 20, String.valueOf(minCooldown));
minField.setIntegersOnly();
minField.setMinMaxDefault(0, 10000, 20);
addTextField(minField);
addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 235, y + 5));
GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 305, y, 40, 20, String.valueOf(maxCooldown));
maxField.setIntegersOnly();
maxField.setMinMaxDefault(0, 10000, 60);
addTextField(maxField);
y += 28;
@@ -254,7 +261,19 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
@Override
public void setGuiData(NBTTagCompound compound) {
abilitiesEnabled = compound.getBoolean("AbilitiesEnabled");
globalCooldown = compound.hasKey("AbilityGlobalCooldown") ? compound.getInteger("AbilityGlobalCooldown") : 20;
// Support old globalCooldown for backwards compatibility
if (compound.hasKey("AbilityMinCooldown")) {
minCooldown = compound.getInteger("AbilityMinCooldown");
maxCooldown = compound.getInteger("AbilityMaxCooldown");
} else if (compound.hasKey("AbilityGlobalCooldown")) {
int oldCooldown = compound.getInteger("AbilityGlobalCooldown");
minCooldown = oldCooldown;
maxCooldown = oldCooldown * 2;
} else {
minCooldown = 20;
maxCooldown = 60;
}
npcAbilities.clear();
NBTTagList abilityList = compound.getTagList("Abilities", 10);
@@ -352,7 +371,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
public void save() {
NBTTagCompound compound = new NBTTagCompound();
compound.setBoolean("AbilitiesEnabled", abilitiesEnabled);
compound.setInteger("AbilityGlobalCooldown", globalCooldown);
compound.setInteger("AbilityMinCooldown", minCooldown);
compound.setInteger("AbilityMaxCooldown", maxCooldown);
NBTTagList abilityList = new NBTTagList();
for (Ability ability : npcAbilities) {
@@ -421,7 +441,20 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
@Override
public void unFocused(GuiNpcTextField textField) {
if (textField.id == 101) {
globalCooldown = textField.getInteger();
minCooldown = textField.getInteger();
// Ensure min <= max
if (minCooldown > maxCooldown) {
maxCooldown = minCooldown;
initGui();
}
save();
} else if (textField.id == 102) {
maxCooldown = textField.getInteger();
// Ensure min <= max
if (maxCooldown < minCooldown) {
minCooldown = maxCooldown;
initGui();
}
save();
}
}
@@ -15,13 +15,18 @@ import noppes.npcs.client.gui.util.SubGuiInterface;
public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig {
private final AbilityLaserShot laser;
private int editingTypeColorId = 0;
private int editingVisualColorId = 0;
public SubGuiAbilityLaserShot(AbilityLaserShot ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.laser = ability;
}
@Override
protected boolean hasVisualSettings() {
return true;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
@@ -72,21 +77,40 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig {
addLabel(new GuiNpcLabel(109, "ability.knockbackUp", col2LabelX, y + 5));
addTextField(createFloatField(109, col2FieldX, y, 50, laser.getKnockbackUp()));
}
y += 24;
@Override
protected void initVisualTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 180;
int col2FieldX = guiLeft + 260;
// Row 6: Inner Color + Outer Color
addLabel(new GuiNpcLabel(120, "ability.innerColor", labelX, y + 5));
// Row 1: Inner Color + Outer Color
addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5));
String innerHex = String.format("%06X", laser.getInnerColor() & 0xFFFFFF);
GuiNpcButton innerColorBtn = new GuiNpcButton(120, fieldX, y, 50, 20, innerHex);
GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex);
innerColorBtn.setTextColor(laser.getInnerColor() & 0xFFFFFF);
addButton(innerColorBtn);
addLabel(new GuiNpcLabel(121, "ability.outerColor", col2LabelX, y + 5));
addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5));
String outerHex = String.format("%06X", laser.getOuterColor() & 0xFFFFFF);
GuiNpcButton outerColorBtn = new GuiNpcButton(121, col2FieldX, y, 50, 20, outerHex);
GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex);
outerColorBtn.setTextColor(laser.getOuterColor() & 0xFFFFFF);
outerColorBtn.setEnabled(laser.isOuterColorEnabled());
addButton(outerColorBtn);
y += 24;
// Row 2: Outer Color Enabled + Outer Color Width
addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5));
addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.isOuterColorEnabled() ? 1 : 0));
addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5));
GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, laser.getOuterColorWidth());
widthField.setEnabled(laser.isOuterColorEnabled());
addTextField(widthField);
}
@Override
@@ -96,34 +120,54 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig {
case 106:
laser.setExplosive(value == 1);
break;
case 120:
editingTypeColorId = 120;
}
}
@Override
protected void handleVisualButton(int id, GuiNpcButton button) {
int value = button.getValue();
switch (id) {
case 200:
editingVisualColorId = 200;
setSubGui(new SubGuiColorSelector(laser.getInnerColor()));
break;
case 121:
editingTypeColorId = 121;
case 201:
editingVisualColorId = 201;
setSubGui(new SubGuiColorSelector(laser.getOuterColor()));
break;
case 202:
laser.setOuterColorEnabled(value == 1);
initGui();
break;
}
}
@Override
public void subGuiClosed(SubGuiInterface subgui) {
if (subgui instanceof SubGuiColorSelector && editingTypeColorId != 0) {
if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) {
SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui;
int rgb = colorSelector.color & 0x00FFFFFF;
if (editingTypeColorId == 120) {
if (editingVisualColorId == 200) {
laser.setInnerColor(rgb);
} else if (editingTypeColorId == 121) {
} else if (editingVisualColorId == 201) {
laser.setOuterColor(rgb);
}
editingTypeColorId = 0;
editingVisualColorId = 0;
initGui();
} else {
super.subGuiClosed(subgui);
}
}
@Override
protected void handleVisualTextField(int id, GuiNpcTextField field) {
switch (id) {
case 203:
laser.setOuterColorWidth(parseFloat(field, laser.getOuterColorWidth()));
break;
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {