Recovery -> Dazed. Better cooldown logic handling

This commit is contained in:
Kam committed 2026-01-27 04:07:02 -05:00
1 parent 93486d306b
commit 747aae8337
30 files changed
+577 -435

No files matched your search

@@ -119,8 +119,9 @@ public class AbilityCommand extends CommandKamkeelBase {
sendResult(sender, " Type: \u00A7d" + ability.getTypeId());
sendResult(sender, " Cooldown: \u00A7e" + ability.getCooldownTicks() + " ticks");
sendResult(sender, " Wind Up: \u00A7e" + ability.getWindUpTicks() + " ticks");
sendResult(sender, " Active: \u00A7e" + ability.getActiveTicks() + " ticks");
sendResult(sender, " Recovery: \u00A7e" + ability.getRecoveryTicks() + " ticks");
if (ability.isInterruptible()) {
sendResult(sender, " Dazed: \u00A7e" + ability.getDazedTicks() + " ticks");
}
sendResult(sender, " Range: \u00A7e" + ability.getMinRange() + " - " + ability.getMaxRange());
sendResult(sender, " Telegraph: \u00A7e" + (ability.isShowTelegraph() ? "Yes" : "No"));
}
@@ -47,18 +47,13 @@ public abstract class Ability implements IAbility {
protected float maxRange = 20;
// Timing (ticks)
protected int cooldownTicks = 100;
protected int cooldownTicks = 0; // Added to global cooldown after ability completes
protected int windUpTicks = 20;
protected int activeTicks = 10;
protected int recoveryTicks = 20;
protected int dazedTicks = 80; // Only used when interrupted during WINDUP (if interruptible)
// 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
@@ -507,11 +502,8 @@ public abstract class Ability implements IAbility {
/**
* 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.
* Normal flow: WINDUP -> ACTIVE -> IDLE
* Interrupted flow: WINDUP -> DAZED -> IDLE (dazed only on interrupt during WINDUP)
*
* @return true if phase changed this tick
*/
@@ -529,18 +521,13 @@ public abstract class Ability implements IAbility {
}
break;
case ACTIVE:
// 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;
}
// Abilities control their own completion by calling signalCompletion()
// No auto-complete - each ability determines when it's done
break;
case RECOVERY:
// Recovery phase is only entered on interrupt
// Count down recoveryTicks then go to IDLE
if (currentTick >= recoveryTicks) {
case DAZED:
// Dazed phase is only entered on interrupt during WINDUP
// NPC cannot attack during this time
if (currentTick >= dazedTicks) {
phase = AbilityPhase.IDLE;
currentTick = 0;
return true;
@@ -552,13 +539,14 @@ public abstract class Ability implements IAbility {
/**
* Signal that the ability has completed its active phase.
* Called by abilities with waitForCompletion=true when they are done.
* Called by abilities 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) {
cleanup();
phase = AbilityPhase.IDLE;
currentTick = 0;
return true;
@@ -567,13 +555,14 @@ public abstract class Ability implements IAbility {
}
/**
* Interrupt this ability. Transitions to RECOVERY phase if interruptible.
* Interrupt this ability. Transitions to DAZED phase if interruptible during WINDUP.
* Called when NPC takes direct damage during WINDUP.
*/
public void interrupt() {
cleanup();
if (interruptible && phase == AbilityPhase.WINDUP) {
// Interrupted during windup - go to recovery (dazed state)
phase = AbilityPhase.RECOVERY;
// Interrupted during windup - go to dazed state
phase = AbilityPhase.DAZED;
currentTick = 0;
} else {
// Force stop - go directly to IDLE
@@ -624,15 +613,25 @@ public abstract class Ability implements IAbility {
}
/**
* Reset all execution state (called on combat end)
* Reset all execution state (called on combat end, NPC death, target lost, etc.)
*/
public void reset() {
cleanup();
phase = AbilityPhase.IDLE;
currentTick = 0;
currentTarget = null;
telegraphInstance = null;
}
/**
* Cleanup any resources created by this ability.
* Override this to kill spawned entities, cancel telegraphs, etc.
* Called when ability ends for any reason (completion, interrupt, reset, NPC death).
*/
public void cleanup() {
// Override in subclasses to clean up spawned entities, etc.
}
// ═══════════════════════════════════════════════════════════════════
// CONDITION CHECKING
// ═══════════════════════════════════════════════════════════════════
@@ -661,10 +660,8 @@ public abstract class Ability implements IAbility {
nbt.setFloat("maxRange", maxRange);
nbt.setInteger("cooldown", cooldownTicks);
nbt.setInteger("windUp", windUpTicks);
nbt.setInteger("active", activeTicks);
nbt.setInteger("recovery", recoveryTicks);
nbt.setInteger("recovery", dazedTicks);
nbt.setBoolean("interruptible", interruptible);
nbt.setBoolean("waitForCompletion", waitForCompletion);
nbt.setBoolean("lockMovement", lockMovement);
nbt.setInteger("windUpColor", windUpColor);
nbt.setInteger("activeColor", activeColor);
@@ -708,10 +705,8 @@ public abstract class Ability implements IAbility {
maxRange = nbt.getFloat("maxRange");
cooldownTicks = nbt.getInteger("cooldown");
windUpTicks = nbt.getInteger("windUp");
activeTicks = nbt.getInteger("active");
recoveryTicks = nbt.getInteger("recovery");
dazedTicks = 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")) {
@@ -857,20 +852,12 @@ public abstract class Ability implements IAbility {
this.windUpTicks = Math.max(0, windUpTicks);
}
public int getActiveTicks() {
return activeTicks;
public int getDazedTicks() {
return dazedTicks;
}
public void setActiveTicks(int activeTicks) {
this.activeTicks = Math.max(1, activeTicks);
}
public int getRecoveryTicks() {
return recoveryTicks;
}
public void setRecoveryTicks(int recoveryTicks) {
this.recoveryTicks = Math.max(0, recoveryTicks);
public void setDazedTicks(int dazedTicks) {
this.dazedTicks = Math.max(0, dazedTicks);
}
public boolean isInterruptible() {
@@ -881,14 +868,6 @@ 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;
}
@@ -978,7 +957,7 @@ public abstract class Ability implements IAbility {
}
/**
* Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=RECOVERY).
* Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=DAZED).
* Required for IAbility interface.
*/
@Override
@@ -20,7 +20,7 @@ public enum AbilityAnimation {;
animationNames.put(IDLE, idle);
animationNames.put(WINDUP, windUp);
animationNames.put(ACTIVE, active);
animationNames.put(RECOVERY, recovery);
animationNames.put(DAZED, recovery);
}
AbilityAnimation(String idle, String windUp, String active) {
@@ -80,8 +80,8 @@ public enum AbilityAnimation {;
return getAnimation(ACTIVE);
}
public IAnimation recovery() {
return getAnimation(RECOVERY);
public IAnimation dazed() {
return getAnimation(DAZED);
}
private File getFileFromName(String fileName) {
@@ -4,5 +4,5 @@ public enum AbilityPhase {
IDLE, // Not executing
WINDUP, // Charging up (telegraph visible)
ACTIVE, // Executing (damage happens)
RECOVERY // Finishing animation
DAZED // Stunned after interrupt during WINDUP (cannot attack)
}
@@ -46,10 +46,8 @@ public class AbilityCharge extends Ability {
this.maxRange = 20.0f;
this.minRange = 4.0f;
this.lockMovement = false; // Movement IS the ability
this.cooldownTicks = 80;
this.cooldownTicks = 0;
this.windUpTicks = 20;
this.activeTicks = 40;
this.recoveryTicks = 10;
// LINE telegraph showing charge path
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
@@ -161,11 +159,13 @@ public class AbilityCharge extends Ability {
npc.motionX = 0;
npc.motionZ = 0;
npc.velocityChanged = true;
signalCompletion();
return;
}
if (isChargeBlocked(npc)) {
stopMomentum(npc);
signalCompletion();
return;
}
@@ -38,6 +38,7 @@ public class AbilityCutter extends Ability {
private SweepMode sweepMode = SweepMode.SWIPE;
private float sweepSpeed = 6.0f;
private int spinDurationTicks = 60; // Duration for SPIN mode
private boolean piercing = true;
private float innerRadius = 0.0f;
@@ -52,10 +53,8 @@ public class AbilityCutter extends Ability {
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 8.0f;
this.lockMovement = true;
this.cooldownTicks = 80;
this.cooldownTicks = 0;
this.windUpTicks = 20;
this.activeTicks = 15;
this.recoveryTicks = 15;
this.telegraphType = TelegraphType.CONE;
this.windUpSound = "random.bow";
this.activeSound = "random.break";
@@ -110,6 +109,7 @@ public class AbilityCutter extends Ability {
switch (sweepMode) {
case SWIPE:
if (currentRotation > arcAngle / 2.0f) {
signalCompletion(); // Swipe arc complete
return;
}
performSweepDamage(npc, world, innerRadius, range, currentRotation);
@@ -117,6 +117,10 @@ public class AbilityCutter extends Ability {
break;
case SPIN:
if (tick >= spinDurationTicks) {
signalCompletion(); // Spin duration complete
return;
}
currentRotation = (currentRotation + sweepSpeed) % 360.0f;
hitEntities.clear();
performSweepDamage(npc, world, innerRadius, range, currentRotation);
@@ -195,6 +199,7 @@ public class AbilityCutter extends Ability {
nbt.setFloat("knockback", knockback);
nbt.setString("sweepMode", sweepMode.name());
nbt.setFloat("sweepSpeed", sweepSpeed);
nbt.setInteger("spinDurationTicks", spinDurationTicks);
nbt.setBoolean("piercing", piercing);
nbt.setFloat("innerRadius", innerRadius);
}
@@ -211,6 +216,7 @@ public class AbilityCutter extends Ability {
this.sweepMode = SweepMode.SWIPE;
}
this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 6.0f;
this.spinDurationTicks = nbt.hasKey("spinDurationTicks") ? nbt.getInteger("spinDurationTicks") : 60;
this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing");
this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f;
}
@@ -264,6 +270,14 @@ public class AbilityCutter extends Ability {
this.sweepSpeed = sweepSpeed;
}
public int getSpinDurationTicks() {
return spinDurationTicks;
}
public void setSpinDurationTicks(int spinDurationTicks) {
this.spinDurationTicks = Math.max(1, spinDurationTicks);
}
public boolean isPiercing() {
return piercing;
}
@@ -87,10 +87,8 @@ public class AbilityDash extends Ability {
this.maxRange = 20.0f;
this.minRange = 0.0f;
this.lockMovement = false;
this.cooldownTicks = 40;
this.cooldownTicks = 0;
this.windUpTicks = 5;
this.activeTicks = 8;
this.recoveryTicks = 10;
// No telegraph for dash - it's a quick evasive move
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
@@ -174,6 +172,7 @@ public class AbilityDash extends Ability {
npc.motionX = 0;
npc.motionZ = 0;
npc.velocityChanged = true;
signalCompletion();
return;
}
@@ -181,6 +180,7 @@ public class AbilityDash extends Ability {
npc.motionX = 0;
npc.motionZ = 0;
npc.velocityChanged = true;
signalCompletion();
return;
}
@@ -72,7 +72,7 @@ public class AbilityDisc extends Ability {
private AnchorPoint anchorPoint = AnchorPoint.FRONT;
// Transient state for disc entity (used during windup charging)
private transient int discEntityId = -1;
private transient EntityAbilityDisc discEntity = null;
public AbilityDisc() {
this.typeId = "ability.cnpc.disc";
@@ -80,10 +80,8 @@ public class AbilityDisc extends Ability {
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 30.0f;
this.minRange = 5.0f;
this.cooldownTicks = 80;
this.cooldownTicks = 0;
this.windUpTicks = 20;
this.activeTicks = 1;
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
}
@@ -111,17 +109,19 @@ public class AbilityDisc extends Ability {
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (world.isRemote) return;
// Find and start moving the disc that was spawned during windup
if (discEntityId != -1) {
Entity entity = world.getEntityByID(discEntityId);
if (entity instanceof EntityAbilityDisc) {
EntityAbilityDisc disc = (EntityAbilityDisc) entity;
disc.startMoving(target);
}
discEntityId = -1;
if (world.isRemote) {
signalCompletion();
return;
}
// Start moving the disc that was spawned during windup
if (discEntity != null && !discEntity.isDead) {
discEntity.startMoving(target);
}
discEntity = null;
// Disc entity manages itself - ability is done
signalCompletion();
}
@Override
@@ -131,7 +131,7 @@ public class AbilityDisc extends Ability {
// Spawn disc in charging mode on first tick of windup
if (tick == 1) {
// Create disc in charging mode - follows NPC based on anchor point during windup
EntityAbilityDisc disc = EntityAbilityDisc.createCharging(
discEntity = EntityAbilityDisc.createCharging(
world, npc, target,
discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed,
damage, knockback, knockbackUp,
@@ -145,8 +145,7 @@ public class AbilityDisc extends Ability {
windUpTicks // Charge duration = windup duration
);
world.spawnEntityInWorld(disc);
discEntityId = disc.getEntityId();
world.spawnEntityInWorld(discEntity);
}
}
@@ -159,15 +158,12 @@ public class AbilityDisc extends Ability {
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
// Despawn charging disc if interrupted during windup
if (discEntityId != -1) {
Entity entity = npc.worldObj.getEntityByID(discEntityId);
if (entity != null) {
entity.setDead();
}
discEntityId = -1;
public void cleanup() {
// Despawn disc entity if still alive
if (discEntity != null && !discEntity.isDead) {
discEntity.setDead();
}
discEntity = null;
}
@Override
@@ -71,7 +71,7 @@ public class AbilityEnergyBeam extends Ability {
private AnchorPoint anchorPoint = AnchorPoint.FRONT;
// Transient state for beam entity (used during windup charging)
private transient int beamEntityId = -1;
private transient EntityAbilityBeam beamEntity = null;
public AbilityEnergyBeam() {
this.typeId = "ability.cnpc.beam";
@@ -79,10 +79,8 @@ public class AbilityEnergyBeam extends Ability {
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 20.0f;
this.minRange = 5.0f;
this.cooldownTicks = 150;
this.cooldownTicks = 0;
this.windUpTicks = 40;
this.activeTicks = 200; // Match maxLifetime to keep NPC locked while beam is active
this.recoveryTicks = 5;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
}
@@ -110,17 +108,19 @@ public class AbilityEnergyBeam extends Ability {
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (world.isRemote) return;
// Find and start firing the beam that was spawned during windup
if (beamEntityId != -1) {
Entity entity = world.getEntityByID(beamEntityId);
if (entity instanceof EntityAbilityBeam) {
EntityAbilityBeam beam = (EntityAbilityBeam) entity;
beam.startFiring(target);
}
beamEntityId = -1;
if (world.isRemote) {
signalCompletion();
return;
}
// Start firing the beam that was spawned during windup
if (beamEntity != null && !beamEntity.isDead) {
beamEntity.startFiring(target);
}
beamEntity = null;
// Beam entity manages itself - ability is done
signalCompletion();
}
@Override
@@ -132,7 +132,7 @@ public class AbilityEnergyBeam extends Ability {
float offsetDist = 1.0f;
// Create beam in charging mode - follows NPC based on anchor point during windup
EntityAbilityBeam beam = EntityAbilityBeam.createCharging(
beamEntity = EntityAbilityBeam.createCharging(
world, npc, target,
beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed,
damage, knockback, knockbackUp,
@@ -147,8 +147,7 @@ public class AbilityEnergyBeam extends Ability {
anchorPoint // Anchor point for charging position
);
world.spawnEntityInWorld(beam);
beamEntityId = beam.getEntityId();
world.spawnEntityInWorld(beamEntity);
}
}
@@ -161,15 +160,12 @@ public class AbilityEnergyBeam extends Ability {
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
// Despawn charging beam if interrupted during windup
if (beamEntityId != -1) {
Entity entity = npc.worldObj.getEntityByID(beamEntityId);
if (entity != null) {
entity.setDead();
}
beamEntityId = -1;
public void cleanup() {
// Despawn beam entity if still alive
if (beamEntity != null && !beamEntity.isDead) {
beamEntity.setDead();
}
beamEntity = null;
}
@Override
@@ -25,6 +25,7 @@ public class AbilityGuard extends Ability {
private static final Random RANDOM = new Random();
// Type-specific parameters
private int durationTicks = 60;
private float damageReduction = 0.5f;
private boolean canCounter = false;
private CounterType counterType = CounterType.FLAT;
@@ -48,10 +49,8 @@ public class AbilityGuard extends Ability {
this.name = "Guard";
this.targetingMode = TargetingMode.SELF;
this.lockMovement = true;
this.cooldownTicks = 120;
this.cooldownTicks = 0;
this.windUpTicks = 10;
this.activeTicks = 60;
this.recoveryTicks = 10;
this.interruptible = false; // Hard to interrupt while guarding
// No telegraph for guard - it's a defensive stance
this.telegraphType = TelegraphType.NONE;
@@ -95,6 +94,11 @@ public class AbilityGuard extends Ability {
if (canCounter && counterTriggered && lastAttacker != null && !world.isRemote) {
performCounter(npc, world);
}
// Check if guard duration has ended
if (tick >= durationTicks) {
signalCompletion();
}
}
/**
@@ -175,6 +179,7 @@ public class AbilityGuard extends Ability {
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setFloat("damageReduction", damageReduction);
nbt.setBoolean("canCounter", canCounter);
nbt.setString("counterType", counterType.name());
@@ -186,6 +191,7 @@ public class AbilityGuard extends Ability {
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60;
this.damageReduction = nbt.hasKey("damageReduction") ? nbt.getFloat("damageReduction") : 0.5f;
this.canCounter = nbt.hasKey("canCounter") && nbt.getBoolean("canCounter");
try {
@@ -206,6 +212,14 @@ public class AbilityGuard extends Ability {
}
// Getters & Setters
public int getDurationTicks() {
return durationTicks;
}
public void setDurationTicks(int durationTicks) {
this.durationTicks = Math.max(1, durationTicks);
}
public float getDamageReduction() {
return damageReduction;
}
@@ -42,6 +42,7 @@ public class AbilityHazard extends Ability {
FOLLOW_TARGET
}
private int durationTicks = 100;
private float radius = 4.0f;
private float innerRadius = 0.0f;
private float coneAngle = 45.0f;
@@ -81,10 +82,8 @@ public class AbilityHazard extends Ability {
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.lockMovement = true;
this.cooldownTicks = 200;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.activeTicks = 100;
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.CIRCLE;
}
@@ -232,6 +231,12 @@ public class AbilityHazard extends Ability {
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
// Check if hazard duration has ended
if (tick >= durationTicks) {
signalCompletion();
return;
}
damagedThisTick.clear();
ticksSinceDamage++;
@@ -342,6 +347,7 @@ public class AbilityHazard extends Ability {
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setFloat("radius", radius);
nbt.setFloat("innerRadius", innerRadius);
nbt.setFloat("coneAngle", coneAngle);
@@ -366,6 +372,7 @@ public class AbilityHazard extends Ability {
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 100;
this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 4.0f;
this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f;
this.coneAngle = nbt.hasKey("coneAngle") ? nbt.getFloat("coneAngle") : 45.0f;
@@ -404,6 +411,14 @@ public class AbilityHazard extends Ability {
}
// Getters & Setters
public int getDurationTicks() {
return durationTicks;
}
public void setDurationTicks(int durationTicks) {
this.durationTicks = Math.max(1, durationTicks);
}
public float getRadius() {
return radius;
}
@@ -25,6 +25,7 @@ import java.util.List;
public class AbilityHeal extends Ability {
// Type-specific parameters
private int durationTicks = 60;
private float healAmount = 10.0f;
private float healPercent = 0.0f;
private boolean healSelf = true;
@@ -40,10 +41,8 @@ public class AbilityHeal extends Ability {
this.name = "Heal";
this.targetingMode = TargetingMode.SELF;
this.lockMovement = true;
this.cooldownTicks = 200;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.activeTicks = 10;
this.recoveryTicks = 10;
// No telegraph for heal - it's a self/ally buff
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
@@ -92,6 +91,9 @@ public class AbilityHeal extends Ability {
healEntity(ally);
spawnHealParticles(world, ally);
}
// Instant heal completes immediately
signalCompletion();
}
// If not instant, HoT healing is applied in onActiveTick
}
@@ -100,16 +102,16 @@ public class AbilityHeal extends Ability {
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (world.isRemote || instantHeal) return;
// Heal over time - distribute heal across active ticks
// Heal over time - distribute heal across duration ticks
if (tick % 10 == 0) {
// Calculate tick-based heal for fixed amount
float tickHeal = (healAmount / (float) activeTicks) * 10;
float tickHeal = (healAmount / (float) durationTicks) * 10;
if (healSelf) {
float selfTickHeal = tickHeal;
// Add percentage-based heal portion
if (healPercent > 0) {
selfTickHeal += (npc.getMaxHealth() * healPercent / (float) activeTicks) * 10;
selfTickHeal += (npc.getMaxHealth() * healPercent / (float) durationTicks) * 10;
}
npc.heal(selfTickHeal);
if (tick % 20 == 0) {
@@ -123,7 +125,7 @@ public class AbilityHeal extends Ability {
float allyTickHeal = tickHeal;
// Add percentage-based heal portion for each ally
if (healPercent > 0) {
allyTickHeal += (ally.getMaxHealth() * healPercent / (float) activeTicks) * 10;
allyTickHeal += (ally.getMaxHealth() * healPercent / (float) durationTicks) * 10;
}
ally.heal(allyTickHeal);
if (tick % 20 == 0) {
@@ -133,6 +135,11 @@ public class AbilityHeal extends Ability {
}
}
}
// Check if heal duration has ended
if (tick >= durationTicks) {
signalCompletion();
}
}
/**
@@ -202,6 +209,7 @@ public class AbilityHeal extends Ability {
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setFloat("healAmount", healAmount);
nbt.setFloat("healPercent", healPercent);
nbt.setBoolean("healSelf", healSelf);
@@ -212,6 +220,7 @@ public class AbilityHeal extends Ability {
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60;
this.healAmount = nbt.hasKey("healAmount") ? nbt.getFloat("healAmount") : 10.0f;
this.healPercent = nbt.hasKey("healPercent") ? nbt.getFloat("healPercent") : 0.0f;
this.healSelf = !nbt.hasKey("healSelf") || nbt.getBoolean("healSelf");
@@ -221,6 +230,14 @@ public class AbilityHeal extends Ability {
}
// Getters & Setters
public int getDurationTicks() {
return durationTicks;
}
public void setDurationTicks(int durationTicks) {
this.durationTicks = Math.max(1, durationTicks);
}
public float getHealAmount() {
return healAmount;
}
@@ -29,10 +29,8 @@ public class AbilityHeavyHit extends Ability {
this.maxRange = 3.0f;
this.minRange = 0.0f;
this.lockMovement = true;
this.cooldownTicks = 80;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.activeTicks = 5;
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.POINT;
this.showTelegraph = false;
this.windUpSound = "random.anvil_use";
@@ -63,7 +61,10 @@ public class AbilityHeavyHit extends Ability {
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (world.isRemote || target == null) return;
if (world.isRemote || target == null) {
signalCompletion();
return;
}
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(npc, target, damage, knockback);
@@ -72,6 +73,9 @@ public class AbilityHeavyHit extends Ability {
if (wasHit) {
applyEffects(target);
}
// Heavy Hit is instant
signalCompletion();
}
@Override
@@ -60,10 +60,8 @@ public class AbilityLaserShot extends Ability {
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 35.0f;
this.minRange = 3.0f;
this.cooldownTicks = 60;
this.cooldownTicks = 0;
this.windUpTicks = 15;
this.activeTicks = 1;
this.recoveryTicks = 5;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
}
@@ -101,7 +99,10 @@ public class AbilityLaserShot extends Ability {
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (world.isRemote) return;
if (world.isRemote) {
signalCompletion();
return;
}
double spawnX = npc.posX;
double spawnY = npc.posY + npc.getEyeHeight() * 0.8;
@@ -120,10 +121,14 @@ public class AbilityLaserShot extends Ability {
);
world.spawnEntityInWorld(laser);
// Laser entity manages itself - ability is done
signalCompletion();
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
// Laser entity manages itself
}
@Override
@@ -69,7 +69,7 @@ public class AbilityOrb extends Ability {
private AnchorPoint anchorPoint = AnchorPoint.FRONT;
// Transient state for orb entity (used during windup charging)
private transient int orbEntityId = -1;
private transient EntityAbilityOrb orbEntity = null;
public AbilityOrb() {
this.typeId = "ability.cnpc.orb";
@@ -77,10 +77,8 @@ public class AbilityOrb extends Ability {
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 25.0f;
this.minRange = 5.0f;
this.cooldownTicks = 100;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.activeTicks = 1; // Minimal - just spawn and done
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
}
@@ -108,17 +106,19 @@ public class AbilityOrb extends Ability {
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (world.isRemote) return;
// Find and start moving the orb that was spawned during windup
if (orbEntityId != -1) {
Entity entity = world.getEntityByID(orbEntityId);
if (entity instanceof EntityAbilityOrb) {
EntityAbilityOrb orb = (EntityAbilityOrb) entity;
orb.startMoving(target);
}
orbEntityId = -1;
if (world.isRemote) {
signalCompletion();
return;
}
// Start moving the orb that was spawned during windup
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.startMoving(target);
}
orbEntity = null;
// Orb entity manages itself - ability is done
signalCompletion();
}
@Override
@@ -128,7 +128,7 @@ public class AbilityOrb extends Ability {
// Spawn orb in charging mode on first tick of windup
if (tick == 1) {
// Create orb in charging mode - follows NPC based on anchor point during windup
EntityAbilityOrb orb = EntityAbilityOrb.createCharging(
orbEntity = EntityAbilityOrb.createCharging(
world, npc, target,
orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed,
damage, knockback, knockbackUp,
@@ -141,8 +141,7 @@ public class AbilityOrb extends Ability {
windUpTicks // Charge duration = windup duration
);
world.spawnEntityInWorld(orb);
orbEntityId = orb.getEntityId();
world.spawnEntityInWorld(orbEntity);
}
}
@@ -157,15 +156,12 @@ public class AbilityOrb extends Ability {
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
// Despawn charging orb if interrupted during windup
if (orbEntityId != -1) {
Entity entity = npc.worldObj.getEntityByID(orbEntityId);
if (entity != null) {
entity.setDead();
}
orbEntityId = -1;
public void cleanup() {
// Despawn orb entity if still alive
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.setDead();
}
orbEntity = null;
}
@Override
@@ -38,10 +38,8 @@ public class AbilityProjectile extends Ability {
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.minRange = 5.0f;
this.maxRange = 20.0f;
this.cooldownTicks = 60;
this.cooldownTicks = 0;
this.windUpTicks = 15;
this.activeTicks = 5;
this.recoveryTicks = 10;
// No telegraph for projectile - it's a ranged attack
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
@@ -122,6 +120,9 @@ public class AbilityProjectile extends Ability {
// Spawn projectile particles (visual trail)
spawnProjectileParticles(world, npc, target);
// Projectile is instant
signalCompletion();
}
private void spawnProjectileParticles(World world, EntityNPCInterface npc, EntityLivingBase target) {
@@ -28,19 +28,14 @@ public class AbilityShockwave extends Ability {
private float damage = 8.0f;
private int maxTargets = 10;
// Runtime state
private transient boolean executed = false;
public AbilityShockwave() {
this.typeId = "ability.cnpc.shockwave";
this.name = "Shockwave";
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 15.0f;
this.lockMovement = true;
this.cooldownTicks = 100;
this.cooldownTicks = 0;
this.windUpTicks = 25;
this.activeTicks = 5;
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.CIRCLE;
this.windUpSound = "random.explode";
this.activeSound = "random.explode";
@@ -74,13 +69,7 @@ public class AbilityShockwave extends Ability {
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
executed = false;
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (executed) return;
executed = true;
// Shockwave is instant - apply effect immediately after windup
// Get all entities in radius
AxisAlignedBB box = npc.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
@@ -124,16 +113,14 @@ public class AbilityShockwave extends Ability {
applyEffects(entity);
}
}
// Shockwave completes instantly
signalCompletion();
}
@Override
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {
executed = false;
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
executed = false;
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
// Shockwave is instant - nothing to do per-tick
}
@Override
@@ -49,11 +49,8 @@ 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 (used as timeout if waitForCompletion fails)
this.recoveryTicks = 10;
this.cooldownTicks = 100;
this.cooldownTicks = 0;
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;
@@ -54,10 +54,8 @@ public class AbilitySweeper extends Ability {
this.maxRange = 15.0f;
this.minRange = 0.0f;
this.lockMovement = true;
this.cooldownTicks = 200;
this.cooldownTicks = 0;
this.windUpTicks = 60;
this.activeTicks = 400; // Long duration - entity controls actual end
this.recoveryTicks = 5; // Short recovery
// Circle telegraph to show range
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
@@ -86,36 +84,36 @@ public class AbilitySweeper extends Ability {
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (!world.isRemote) {
LogWriter.info("[Sweeper] onExecute called at NPC pos " + npc.posX + ", " + npc.posY + ", " + npc.posZ);
// Spawn the entity that handles BOTH visuals AND damage
activeEntity = new EntityAbilitySweeper(world, npc, target,
beamLength, beamWidth, beamHeight,
innerColor, outerColor, outerColorEnabled, outerColorWidth,
sweepSpeed, numberOfRotations,
damage, damageInterval, piercing,
lockOnTarget);
world.spawnEntityInWorld(activeEntity);
LogWriter.info("[Sweeper] Spawned sweeper entity");
if (world.isRemote) {
signalCompletion();
return;
}
LogWriter.info("[Sweeper] onExecute called at NPC pos " + npc.posX + ", " + npc.posY + ", " + npc.posZ);
// Spawn the entity that handles BOTH visuals AND damage
activeEntity = new EntityAbilitySweeper(world, npc, target,
beamLength, beamWidth, beamHeight,
innerColor, outerColor, outerColorEnabled, outerColorWidth,
sweepSpeed, numberOfRotations,
damage, damageInterval, piercing,
lockOnTarget);
world.spawnEntityInWorld(activeEntity);
LogWriter.info("[Sweeper] Spawned sweeper entity");
// Entity manages itself - completion signaled from onActiveTick when entity dies
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
// Entity handles everything - nothing to do here
// Check if entity died early (entity manages its own death based on rotations)
if (activeEntity != null && activeEntity.isDead) {
// Entity finished, ability can end early
// The ability system will handle the recovery phase
// Entity handles everything - check if entity finished its rotations
if (activeEntity == null || activeEntity.isDead) {
signalCompletion();
}
}
@Override
public void reset() {
super.reset();
public void cleanup() {
// Clean up entity if still alive
if (activeEntity != null && !activeEntity.isDead) {
activeEntity.setDead();
@@ -59,10 +59,8 @@ public class AbilityTeleport extends Ability {
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 30.0f;
this.minRange = 5.0f;
this.cooldownTicks = 100;
this.cooldownTicks = 0;
this.windUpTicks = 10;
this.activeTicks = 40;
this.recoveryTicks = 10;
// No telegraph for teleport - it's instant repositioning
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
@@ -101,7 +99,10 @@ public class AbilityTeleport extends Ability {
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1;
if (currentBlink >= blinkLimit) return;
if (currentBlink >= blinkLimit) {
signalCompletion();
return;
}
ticksSinceLastBlink++;
@@ -109,6 +110,11 @@ public class AbilityTeleport extends Ability {
performBlink(npc, target, world);
currentBlink++;
ticksSinceLastBlink = 0;
// Check if this was the last blink
if (currentBlink >= blinkLimit) {
signalCompletion();
}
}
}
@@ -34,6 +34,7 @@ public class AbilityTrap extends Ability {
AHEAD_OF_CASTER
}
private int durationTicks = 200;
private TrapPlacement placement = TrapPlacement.AT_TARGET;
private float placementDistance = 5.0f;
private float triggerRadius = 2.0f;
@@ -64,10 +65,8 @@ public class AbilityTrap extends Ability {
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.lockMovement = true;
this.cooldownTicks = 150;
this.cooldownTicks = 0;
this.windUpTicks = 20;
this.activeTicks = 200;
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.CIRCLE;
}
@@ -180,6 +179,12 @@ public class AbilityTrap extends Ability {
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
// Check if trap duration has ended
if (tick >= durationTicks) {
signalCompletion();
return;
}
if (!armed) {
if (tick >= armTime) {
armed = true;
@@ -190,6 +195,7 @@ public class AbilityTrap extends Ability {
ticksSinceLastTrigger++;
if (maxTriggers > 0 && triggerCount >= maxTriggers) {
signalCompletion(); // All triggers used, trap is done
return;
}
@@ -277,6 +283,7 @@ public class AbilityTrap extends Ability {
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setString("placement", placement.name());
nbt.setFloat("placementDistance", placementDistance);
nbt.setFloat("triggerRadius", triggerRadius);
@@ -294,6 +301,7 @@ public class AbilityTrap extends Ability {
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 200;
try {
this.placement = TrapPlacement.valueOf(nbt.getString("placement"));
} catch (Exception e) {
@@ -314,6 +322,14 @@ public class AbilityTrap extends Ability {
}
// Getters & Setters
public int getDurationTicks() {
return durationTicks;
}
public void setDurationTicks(int durationTicks) {
this.durationTicks = Math.max(1, durationTicks);
}
public TrapPlacement getPlacement() {
return placement;
}
@@ -46,10 +46,8 @@ public class AbilityVortex extends Ability {
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 15.0f;
this.lockMovement = true;
this.cooldownTicks = 120;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.activeTicks = 40;
this.recoveryTicks = 5;
this.telegraphType = TelegraphType.CIRCLE;
this.windUpSound = "mob.ghast.charge";
this.activeSound = "mob.ghast.fireball";
@@ -119,6 +117,7 @@ public class AbilityVortex extends Ability {
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (pullComplete || pulledEntities.isEmpty()) {
signalCompletion();
return;
}
@@ -174,6 +173,7 @@ public class AbilityVortex extends Ability {
if (!anyStillPulling && pulledEntities.isEmpty()) {
pullComplete = true;
signalCompletion();
}
}
@@ -7,104 +7,87 @@ import noppes.npcs.constants.EnumAnimationPart;
import noppes.npcs.controllers.data.AnimationData;
import noppes.npcs.controllers.data.Frame;
import noppes.npcs.controllers.data.FramePart;
import noppes.npcs.entity.EntityCustomNpc;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.entity.data.ModelScalePart;
/**
* Utility class for calculating world positions based on anchor points.
* Used for positioning charging effects on different body parts.
* Animation-aware: reads arm positions from NPC animation data when available.
* Supports both EntityNPCInterface and EntityPlayer with animation and model size awareness.
*/
public class AnchorPointHelper {
// ==================== TUNABLE CONSTANTS ====================
// These can be adjusted to fine-tune positioning
// Default biped model constants (in model units, 1 unit = 1/16 block)
private static final float MODEL_SCALE = 1f / 16f;
// Shoulder position in model units (default biped arm rotation points)
private static final float SHOULDER_X_RIGHT = -5.0f; // Right arm is on the right side (-X in model space)
private static final float SHOULDER_X_LEFT = 5.0f; // Left arm is on the left side (+X in model space)
private static final float SHOULDER_Y = 2.0f; // Shoulder height from model origin
private static final float SHOULDER_Z = 0.0f; // Shoulder depth
// Shoulder position relative to model origin
private static final float SHOULDER_Y = 2f;
private static final float SHOULDER_X_OFFSET = 5f; // Distance from center to shoulder
// Hand offset in model units (where items are held, relative to shoulder after rotation)
// In arm's local space: X = toward/away from body, Y = down the arm, Z = forward/back
private static float HAND_OFFSET_X_RIGHT = -2.0f; // Slightly toward body for right
private static float HAND_OFFSET_X_LEFT = 2.0f; // Slightly toward body for left
private static float HAND_OFFSET_Y = 14.0f; // Down the arm (arm is ~12 units long)
private static float HAND_OFFSET_Z = 0.0f; // Forward from arm
// Palm position (in arm's local space, relative to shoulder)
// Arm is 12 units long; palm is near the end
private static final float PALM_DOWN = 11f; // Distance down the arm to palm center
private static final float PALM_FORWARD = 2f; // Forward offset to palm surface (not center of hand)
private static final float PALM_INWARD = 0f; // Lateral offset (0 = centered on arm)
// Height of shoulder relative to entity position (feet)
private static final float SHOULDER_HEIGHT_BLOCKS = 1.35f;
// Default height offsets as fraction of entity height
private static final float FRONT_HEIGHT = 0.7f;
private static final float CENTER_HEIGHT = 0.5f;
private static final float ARM_HEIGHT = 0.75f;
private static final float ABOVE_HEAD_HEIGHT = 1.2f;
private static final float CHEST_HEIGHT = 0.65f;
// Model scale factor (1/16 - converts model units to blocks)
private static final float MODEL_SCALE = 0.0625f;
// Distance in front for FRONT anchor
private static final float DEFAULT_FRONT_DISTANCE = 1.0f;
// Fallback offsets (in blocks) when no animation
private static final float ARM_LATERAL_OFFSET = 0.35f;
private static final float ARM_FORWARD_OFFSET = 0.3f;
private static final float FRONT_OFFSET = 1.0f;
// Height multipliers relative to eye height
private static final float FRONT_HEIGHT_MULT = 0.7f;
private static final float CENTER_HEIGHT_MULT = 0.5f;
private static final float ARM_HEIGHT_MULT = 0.75f;
private static final float ABOVE_HEAD_HEIGHT_MULT = 1.3f;
private static final float CHEST_HEIGHT_MULT = 0.5f;
// Fallback arm positioning (when no animation)
private static final float FALLBACK_LATERAL = 0.35f;
private static final float FALLBACK_FORWARD = 0.3f;
/**
* Calculate the world position for a given anchor point on an entity.
* Calculate world position for an anchor point on an entity.
*/
public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor) {
return calculateAnchorPosition(entity, anchor, FRONT_OFFSET);
return calculateAnchorPosition(entity, anchor, DEFAULT_FRONT_DISTANCE);
}
/**
* Calculate the world position for a given anchor point on an entity.
* Calculate world position for an anchor point with custom front offset.
*/
public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor, float frontOffset) {
float scale = getModelScale(entity);
float height = entity.height;
double x = entity.posX;
double y = entity.posY;
double z = entity.posZ;
float eyeHeight = entity.getEyeHeight();
// Body yaw in radians (used for arm positioning)
// Minecraft: yaw 0 = south (+Z), 90 = west (-X), 180 = north (-Z), 270 = east (+X)
float bodyYawRad = (float) Math.toRadians(entity.renderYawOffset);
float headYawRad = (float) Math.toRadians(entity.rotationYawHead);
float headYaw = (float) Math.toRadians(entity.rotationYawHead);
float bodyYaw = (float) Math.toRadians(entity.renderYawOffset);
switch (anchor) {
case FRONT:
// In front of entity face (use head yaw)
x -= Math.sin(headYawRad) * frontOffset;
y += eyeHeight * FRONT_HEIGHT_MULT;
z += Math.cos(headYawRad) * frontOffset;
x -= Math.sin(headYaw) * frontOffset * scale;
y += height * FRONT_HEIGHT;
z += Math.cos(headYaw) * frontOffset * scale;
break;
case CENTER:
y += eyeHeight * CENTER_HEIGHT_MULT;
y += height * CENTER_HEIGHT;
break;
case RIGHT_HAND:
Vec3 rightPos = getAnimatedHandPosition(entity, true);
if (rightPos != null) {
return rightPos;
}
// Fallback
return calculateFallbackHandPosition(entity, bodyYawRad, true);
return calculateHandPosition(entity, true, scale);
case LEFT_HAND:
Vec3 leftPos = getAnimatedHandPosition(entity, false);
if (leftPos != null) {
return leftPos;
}
// Fallback
return calculateFallbackHandPosition(entity, bodyYawRad, false);
return calculateHandPosition(entity, false, scale);
case ABOVE_HEAD:
y += eyeHeight * ABOVE_HEAD_HEIGHT_MULT;
y += height * ABOVE_HEAD_HEIGHT;
break;
case CHEST:
y += eyeHeight * CHEST_HEIGHT_MULT;
y += height * CHEST_HEIGHT;
break;
}
@@ -112,17 +95,51 @@ public class AnchorPointHelper {
}
/**
* Calculate hand position using NPC animation data.
* Uses the same transformation logic as the renderer.
* Get model scale factor for an entity.
* NPCs have configurable model size, players use default scale.
*/
private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, boolean rightArm) {
if (!(entity instanceof EntityNPCInterface)) {
return null;
private static float getModelScale(EntityLivingBase entity) {
if (entity instanceof EntityNPCInterface) {
int modelSize = ((EntityNPCInterface) entity).display.modelSize;
return modelSize / 5f; // modelSize 5 = 100% scale
}
return 1f;
}
// Default arm scale (no scaling)
private static final ModelScalePart DEFAULT_ARM_SCALE = new ModelScalePart();
/**
* Get arm scale for an entity.
* EntityCustomNpc has per-part scaling, others use default (1,1,1).
*/
private static ModelScalePart getArmScale(EntityLivingBase entity) {
if (entity instanceof EntityCustomNpc) {
return ((EntityCustomNpc) entity).modelData.modelScale.arms;
}
return DEFAULT_ARM_SCALE;
}
/**
* Calculate hand position using animation data if available.
*/
private static Vec3 calculateHandPosition(EntityLivingBase entity, boolean rightHand, float scale) {
// Try to get animated position first
Vec3 animatedPos = getAnimatedHandPosition(entity, rightHand, scale);
if (animatedPos != null) {
return animatedPos;
}
EntityNPCInterface npc = (EntityNPCInterface) entity;
AnimationData animData = npc.display.animationData;
// Fallback to static position
return calculateFallbackHandPosition(entity, rightHand, scale);
}
/**
* Calculate hand position from animation data.
* Works for both NPCs and Players.
*/
private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, boolean rightHand, float scale) {
AnimationData animData = AnimationData.getData(entity);
if (animData == null || !animData.isActive() || animData.animation == null) {
return null;
}
@@ -132,146 +149,172 @@ public class AnchorPointHelper {
return null;
}
EnumAnimationPart armPart = rightArm ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM;
if (!frame.frameParts.containsKey(armPart)) {
return null;
}
EnumAnimationPart armPart = rightHand ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM;
FramePart part = frame.frameParts.get(armPart);
if (part == null) {
return null;
}
// Ensure interpolation is up to date
part.interpolateOffset();
part.interpolateAngles();
// Get arm scale for EntityCustomNpc
ModelScalePart armScale = getArmScale(entity);
// === STEP 1: Get shoulder position in model space ===
float shoulderX = rightArm ? SHOULDER_X_RIGHT : SHOULDER_X_LEFT;
// Get rotation and pivot values
float[] rotations = getRotations(part, entity);
float[] pivots = getPivots(part, entity);
// Shoulder position in model space
float shoulderX = rightHand ? -SHOULDER_X_OFFSET : SHOULDER_X_OFFSET;
float shoulderY = SHOULDER_Y;
float shoulderZ = SHOULDER_Z;
float shoulderZ = 0f;
// Add animation pivot offsets
if (part.prevPivots != null) {
shoulderX += part.prevPivots[0];
shoulderY += part.prevPivots[1];
shoulderZ += part.prevPivots[2];
}
// Add pivot offsets
shoulderX += pivots[0];
shoulderY += pivots[1];
shoulderZ += pivots[2];
// === STEP 2: Get hand offset in arm's local space ===
float handX = rightArm ? HAND_OFFSET_X_RIGHT : HAND_OFFSET_X_LEFT;
float handY = HAND_OFFSET_Y;
float handZ = HAND_OFFSET_Z;
// Palm offset in arm's local space (before rotation)
// Apply arm scaling to the palm position
float palmX = (rightHand ? -PALM_INWARD : PALM_INWARD) * armScale.scaleX;
float palmY = PALM_DOWN * armScale.scaleY;
float palmZ = PALM_FORWARD * armScale.scaleZ;
// === STEP 3: Apply arm rotations to hand offset (Z → Y → X order) ===
// Rotations are in radians (already converted by interpolateAngles)
float rotX = part.prevRotations != null ? part.prevRotations[0] : 0;
float rotY = part.prevRotations != null ? part.prevRotations[1] : 0;
float rotZ = part.prevRotations != null ? part.prevRotations[2] : 0;
// Apply arm rotations (order: Z -> Y -> X)
double hx = palmX, hy = palmY, hz = palmZ;
// Start with hand offset
double hx = handX;
double hy = handY;
double hz = handZ;
// Apply Z rotation first
double cosZ = Math.cos(rotZ);
double sinZ = Math.sin(rotZ);
// Z rotation
double cosZ = Math.cos(rotations[2]);
double sinZ = Math.sin(rotations[2]);
double newX = hx * cosZ - hy * sinZ;
double newY = hx * sinZ + hy * cosZ;
hx = newX;
hy = newY;
// Apply Y rotation
double cosY = Math.cos(rotY);
double sinY = Math.sin(rotY);
// Y rotation
double cosY = Math.cos(rotations[1]);
double sinY = Math.sin(rotations[1]);
newX = hx * cosY + hz * sinY;
double newZ = -hx * sinY + hz * cosY;
hx = newX;
hz = newZ;
// Apply X rotation
double cosX = Math.cos(rotX);
double sinX = Math.sin(rotX);
// X rotation
double cosX = Math.cos(rotations[0]);
double sinX = Math.sin(rotations[0]);
newY = hy * cosX - hz * sinX;
newZ = hy * sinX + hz * cosX;
hy = newY;
hz = newZ;
// === STEP 4: Combine shoulder + rotated hand offset (in model units) ===
// Combine shoulder + rotated hand offset
double modelX = shoulderX + hx;
double modelY = shoulderY + hy;
double modelZ = shoulderZ + hz;
// === STEP 5: Convert to blocks ===
double blockX = modelX * MODEL_SCALE;
double blockY = modelY * MODEL_SCALE;
double blockZ = modelZ * MODEL_SCALE;
// Convert to blocks and apply model scale
double blockX = modelX * MODEL_SCALE * scale;
double blockY = modelY * MODEL_SCALE * scale;
double blockZ = modelZ * MODEL_SCALE * scale;
// === STEP 6: Apply body yaw rotation ===
// In model space: +X = left, -X = right, +Z = back, -Z = front
// Need to rotate to align with entity's facing direction
// Apply body yaw rotation
float bodyYaw = (float) Math.toRadians(entity.renderYawOffset);
double cosYaw = Math.cos(bodyYaw);
double sinYaw = Math.sin(bodyYaw);
// Rotate around Y axis (body rotation)
double worldOffsetX = blockX * cosYaw + blockZ * sinYaw;
double worldOffsetZ = -blockX * sinYaw + blockZ * cosYaw;
// === STEP 7: Final world position ===
// Calculate shoulder height based on entity and scale
float shoulderHeight = entity.height * ARM_HEIGHT;
// Final world position
double worldX = entity.posX + worldOffsetX;
double worldY = entity.posY + SHOULDER_HEIGHT_BLOCKS - blockY; // Subtract because +Y in model is down
double worldY = entity.posY + shoulderHeight - blockY;
double worldZ = entity.posZ + worldOffsetZ;
return Vec3.createVectorHelper(worldX, worldY, worldZ);
}
/**
* Get rotation values from FramePart.
* Uses interpolated values on client, raw values on server.
*/
private static float[] getRotations(FramePart part, EntityLivingBase entity) {
if (entity.worldObj.isRemote) {
// Client side - use interpolated values (already in radians)
part.interpolateAngles();
return part.prevRotations;
} else {
// Server side - convert raw degrees to radians
float pi = (float) Math.PI / 180f;
return new float[]{
part.rotation[0] * pi,
part.rotation[1] * pi,
part.rotation[2] * pi
};
}
}
/**
* Get pivot values from FramePart.
* Uses interpolated values on client, raw values on server.
*/
private static float[] getPivots(FramePart part, EntityLivingBase entity) {
if (entity.worldObj.isRemote) {
// Client side - use interpolated values
part.interpolateOffset();
return part.prevPivots;
} else {
// Server side - use raw values
return part.pivot;
}
}
/**
* Calculate fallback hand position when no animation is active.
*/
private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, float bodyYawRad, boolean rightHand) {
double x = entity.posX;
double y = entity.posY;
double z = entity.posZ;
float eyeHeight = entity.getEyeHeight();
private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, boolean rightHand, float scale) {
float bodyYaw = (float) Math.toRadians(entity.renderYawOffset);
// Get arm scale for EntityCustomNpc
ModelScalePart armScale = getArmScale(entity);
// Perpendicular direction for lateral offset
float perpYaw = bodyYawRad + (float) (rightHand ? Math.PI / 2 : -Math.PI / 2);
float perpYaw = bodyYaw + (rightHand ? (float) (Math.PI / 2) : (float) (-Math.PI / 2));
// Lateral offset (perpendicular to body facing)
double lateralX = -Math.sin(perpYaw) * ARM_LATERAL_OFFSET;
double lateralZ = Math.cos(perpYaw) * ARM_LATERAL_OFFSET;
// Calculate offsets scaled by model size and arm scale
double lateralX = -Math.sin(perpYaw) * FALLBACK_LATERAL * scale * armScale.scaleX;
double lateralZ = Math.cos(perpYaw) * FALLBACK_LATERAL * scale * armScale.scaleX;
double forwardX = -Math.sin(bodyYaw) * FALLBACK_FORWARD * scale * armScale.scaleZ;
double forwardZ = Math.cos(bodyYaw) * FALLBACK_FORWARD * scale * armScale.scaleZ;
// Forward offset (in facing direction)
double forwardX = -Math.sin(bodyYawRad) * ARM_FORWARD_OFFSET;
double forwardZ = Math.cos(bodyYawRad) * ARM_FORWARD_OFFSET;
// Arm length affects vertical position
double armLengthOffset = (1 - armScale.scaleY) * 0.3;
x += lateralX + forwardX;
y += eyeHeight * ARM_HEIGHT_MULT;
z += lateralZ + forwardZ;
double x = entity.posX + lateralX + forwardX;
double y = entity.posY + entity.height * ARM_HEIGHT - armLengthOffset;
double z = entity.posZ + lateralZ + forwardZ;
return Vec3.createVectorHelper(x, y, z);
}
/**
* Get the height multiplier for a given anchor point.
* Get default height multiplier for an anchor point.
*/
public static float getHeightMultiplier(AnchorPoint anchor) {
switch (anchor) {
case FRONT:
return FRONT_HEIGHT_MULT;
return FRONT_HEIGHT;
case CENTER:
return CENTER_HEIGHT_MULT;
return CENTER_HEIGHT;
case RIGHT_HAND:
case LEFT_HAND:
return ARM_HEIGHT_MULT;
return ARM_HEIGHT;
case ABOVE_HEAD:
return ABOVE_HEAD_HEIGHT_MULT;
return ABOVE_HEAD_HEIGHT;
case CHEST:
return CHEST_HEIGHT_MULT;
return CHEST_HEIGHT;
default:
return FRONT_HEIGHT_MULT;
return FRONT_HEIGHT;
}
}
}
+32 -19
View File
@@ -170,8 +170,8 @@ public class DataAbilities {
currentAbility.onActiveTick(npc, target, npc.worldObj, currentAbility.getCurrentTick());
break;
case RECOVERY:
// Recovery phase - just wait
case DAZED:
// Dazed phase - NPC cannot attack, just wait for daze to end
break;
case IDLE:
@@ -222,12 +222,10 @@ public class DataAbilities {
releaseLockedRotation();
// 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;
int abilityCooldownOffset = currentAbility.getCooldownTicks();
rollCooldown();
// Add ability-specific cooldown offset
cooldownEndTime += abilityCooldownOffset;
currentAbility = null;
lastTarget = null;
@@ -539,8 +537,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.
* If interrupted during WINDUP, the ability will transition to DAZED phase.
* When DAZED completes, the ability will move to IDLE and onAbilityComplete() is called.
*/
public void interruptCurrentAbility(DamageSource source, float damage) {
if (currentAbility != null) {
@@ -559,10 +557,10 @@ public class DataAbilities {
NpcAPI.EVENT_BUS.post(interruptEvent);
currentAbility.onInterrupt(npc, source, damage);
currentAbility.interrupt(); // This now transitions to RECOVERY, not IDLE
currentAbility.interrupt(); // Transitions to DAZED if interrupted during WINDUP
// Don't clear currentAbility - let it tick through RECOVERY phase
// When RECOVERY ends and phase becomes IDLE, onAbilityComplete() will be called
// Don't clear currentAbility - let it tick through DAZED phase
// When DAZED ends and phase becomes IDLE, onAbilityComplete() will be called
}
}
@@ -631,10 +629,13 @@ public class DataAbilities {
/**
* Reset all runtime state (cooldowns, current ability).
* Called when NPC respawns or combat ends.
* Rolls cooldown so NPC doesn't immediately use an ability.
*/
public void reset() {
stopCurrentAbility();
resetCooldown();
// Roll cooldown so NPC doesn't immediately attack after reset
rollCooldown();
// Reset execution state on all abilities
for (Ability ability : abilities) {
@@ -642,6 +643,18 @@ public class DataAbilities {
}
}
/**
* Roll a new cooldown using the min/max range.
* Called after ability completes and on reset.
*/
private void rollCooldown() {
int baseCooldown = minCooldown;
if (maxCooldown > minCooldown) {
baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1);
}
cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown;
}
// ═══════════════════════════════════════════════════════════════════
// ABILITY LIST MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
@@ -706,7 +719,7 @@ public class DataAbilities {
* Rules:
* - WINDUP phase: Block AI if lockMovement is true
* - ACTIVE phase: Block AI if lockMovement OR hasAbilityMovement is true
* - RECOVERY phase: Block AI if lockMovement is true
* - DAZED phase: Always block AI (NPC is stunned)
*/
public boolean isAbilityControllingMovement() {
if (currentAbility == null || !currentAbility.isExecuting()) {
@@ -727,9 +740,9 @@ public class DataAbilities {
// - hasAbilityMovement is true (ability is moving the NPC itself)
return currentAbility.isLockMovement() || currentAbility.hasAbilityMovement();
case RECOVERY:
// During recovery, block if lockMovement is set
return currentAbility.isLockMovement();
case DAZED:
// During dazed phase, always block - NPC is stunned from interrupt
return true;
default:
return false;
@@ -739,7 +752,7 @@ 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.
* This allows the NPC to freely track target during WINDUP and DAZED.
*/
public boolean shouldLockLookDirection() {
if (currentAbility == null || !currentAbility.isExecuting()) {
@@ -61,8 +61,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
// Timing
protected int cooldownTicks;
protected int windUpTicks;
protected int activeTicks;
protected int recoveryTicks;
protected int dazedTicks;
// ═══════════════════════════════════════════════════════════════════════════
// CACHED VALUES - Target Tab
@@ -115,8 +114,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
// Timing
this.cooldownTicks = ability.getCooldownTicks();
this.windUpTicks = ability.getWindUpTicks();
this.activeTicks = ability.getActiveTicks();
this.recoveryTicks = ability.getRecoveryTicks();
this.dazedTicks = ability.getDazedTicks();
// Target
this.minRange = ability.getMinRange();
@@ -254,42 +252,36 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
y += 24;
// Row 3: Interruptible
// Row 3: Interruptible + Dazed Ticks (only if interruptible)
addLabel(new GuiNpcLabel(6, "ability.interruptible", col1LabelX, y + 5));
addButton(new GuiNpcButton(14, guiLeft + 85, y, 40, 20, new String[]{"gui.no", "gui.yes"}, interruptible ? 1 : 0));
// Only show dazed ticks if interruptible is enabled
if (interruptible) {
addLabel(new GuiNpcLabel(12, "ability.dazed", col2LabelX, y + 5));
GuiNpcTextField dazedField = new GuiNpcTextField(13, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(dazedTicks));
dazedField.setIntegersOnly();
dazedField.setMinMaxDefault(0, 1000, 80);
addTextField(dazedField);
}
y += 28;
// Separator - Timing section header
addLabel(new GuiNpcLabel(7, "ability.timing", col1LabelX, y));
y += 14;
// Row 4: Windup Ticks + Active Ticks
// Row 4: Windup Ticks + Cooldown Ticks
addLabel(new GuiNpcLabel(10, "ability.windup", col1LabelX, y + 5));
GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks));
windupField.setIntegersOnly();
windupField.setMinMaxDefault(0, 1000, 20);
addTextField(windupField);
addLabel(new GuiNpcLabel(11, "ability.active", col2LabelX, y + 5));
GuiNpcTextField activeField = new GuiNpcTextField(12, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(activeTicks));
activeField.setIntegersOnly();
activeField.setMinMaxDefault(1, 1000, 10);
addTextField(activeField);
y += 24;
// Row 5: Recovery Ticks + Cooldown Ticks
addLabel(new GuiNpcLabel(12, "ability.recovery", col1LabelX, y + 5));
GuiNpcTextField recoveryField = new GuiNpcTextField(13, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(recoveryTicks));
recoveryField.setIntegersOnly();
recoveryField.setMinMaxDefault(0, 1000, 20);
addTextField(recoveryField);
addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5));
GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks));
cooldownField.setIntegersOnly();
cooldownField.setMinMaxDefault(0, 10000, 100);
cooldownField.setMinMaxDefault(0, 10000, 0);
addTextField(cooldownField);
}
@@ -632,6 +624,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
// General tab buttons
else if (id == 14) {
interruptible = ((GuiNpcButton) guibutton).getValue() == 1;
initGui(); // Refresh to show/hide dazed ticks field
} else if (id == 16) {
lockMovement = ((GuiNpcButton) guibutton).getValue() == 1;
}
@@ -764,10 +757,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
cooldownTicks = textField.getInteger();
} else if (id == 11) {
windUpTicks = textField.getInteger();
} else if (id == 12) {
activeTicks = textField.getInteger();
} else if (id == 13) {
recoveryTicks = textField.getInteger();
dazedTicks = textField.getInteger();
}
// Visual-specific fields (200+) - delegate to subclass
@@ -865,8 +856,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
// Timing
ability.setCooldownTicks(cooldownTicks);
ability.setWindUpTicks(windUpTicks);
ability.setActiveTicks(activeTicks);
ability.setRecoveryTicks(recoveryTicks);
ability.setDazedTicks(dazedTicks);
// Target
ability.setMinRange(minRange);
@@ -914,8 +904,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
this.cooldownTicks = ability.getCooldownTicks();
this.windUpTicks = ability.getWindUpTicks();
this.activeTicks = ability.getActiveTicks();
this.recoveryTicks = ability.getRecoveryTicks();
this.dazedTicks = ability.getDazedTicks();
this.minRange = ability.getMinRange();
this.maxRange = ability.getMaxRange();
@@ -68,8 +68,15 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig {
y += 24;
// Row 5: Effects button
addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects"));
// Row 5: Spin Duration (only for SPIN mode) + Effects button
if (cutter.getSweepMode() == AbilityCutter.SweepMode.SPIN) {
addLabel(new GuiNpcLabel(108, "ability.duration", labelX, y + 5));
GuiNpcTextField durationField = createIntField(108, fieldX, y, 50, cutter.getSpinDurationTicks());
durationField.setMinMaxDefault(1, 1000, 60);
addTextField(durationField);
}
addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects"));
}
@Override
@@ -78,6 +85,7 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig {
switch (id) {
case 106:
cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]);
initGui(); // Refresh to show/hide duration field
break;
case 107:
cutter.setPiercing(value == 1);
@@ -121,6 +129,9 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig {
case 105:
cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed()));
break;
case 108:
cutter.setSpinDurationTicks(field.getInteger());
break;
}
}
}
@@ -31,9 +31,14 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig {
int col2LabelX = guiLeft + 165;
int col2FieldX = guiLeft + 225;
// Row 1: Damage Reduction
addLabel(new GuiNpcLabel(100, "ability.dmgReduce", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, guard.getDamageReduction()));
// Row 1: Duration + Damage Reduction
addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5));
GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, guard.getDurationTicks());
durationField.setMinMaxDefault(1, 1000, 60);
addTextField(durationField);
addLabel(new GuiNpcLabel(100, "ability.dmgReduce", col2LabelX, y + 5));
addTextField(createFloatField(100, col2FieldX, y, 50, guard.getDamageReduction()));
y += 24;
@@ -112,6 +117,9 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig {
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 99:
guard.setDurationTicks(field.getInteger());
break;
case 100:
guard.setDamageReduction(parseFloat(field, guard.getDamageReduction()));
break;
@@ -31,14 +31,22 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig {
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Shape + Placement
addLabel(new GuiNpcLabel(100, "ability.shape", labelX, y + 5));
String[] shapes = {"Circle", "Ring", "Cone"};
addButton(new GuiNpcButton(100, fieldX, y, 55, 20, shapes, hazard.getShape().ordinal()));
// Row 1: Duration + Shape
addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5));
GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, hazard.getDurationTicks());
durationField.setMinMaxDefault(1, 2000, 100);
addTextField(durationField);
addLabel(new GuiNpcLabel(101, "ability.placement", col2LabelX, y + 5));
addLabel(new GuiNpcLabel(100, "ability.shape", col2LabelX, y + 5));
String[] shapes = {"Circle", "Ring", "Cone"};
addButton(new GuiNpcButton(100, col2FieldX, y, 55, 20, shapes, hazard.getShape().ordinal()));
y += 24;
// Row 2: Placement
addLabel(new GuiNpcLabel(101, "ability.placement", labelX, y + 5));
String[] placements = {"Caster", "Target", "FollowCast", "FollowTgt"};
addButton(new GuiNpcButton(101, col2FieldX, y, 60, 20, placements, hazard.getPlacement().ordinal()));
addButton(new GuiNpcButton(101, fieldX, y, 70, 20, placements, hazard.getPlacement().ordinal()));
y += 24;
@@ -116,6 +124,9 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig {
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 99:
hazard.setDurationTicks(field.getInteger());
break;
case 102:
hazard.setRadius(parseFloat(field, hazard.getRadius()));
break;
@@ -27,7 +27,20 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig {
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Heal Amount + Heal Percent
// Row 1: Instant Heal + Duration (only shown if not instant)
addLabel(new GuiNpcLabel(103, "dialog.instant", labelX, y + 5));
addButton(new GuiNpcButton(103, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0));
if (!heal.isInstantHeal()) {
addLabel(new GuiNpcLabel(99, "ability.duration", col2LabelX, y + 5));
GuiNpcTextField durationField = createIntField(99, col2FieldX, y, 50, heal.getDurationTicks());
durationField.setMinMaxDefault(1, 1000, 60);
addTextField(durationField);
}
y += 24;
// Row 2: Heal Amount + Heal Percent
addLabel(new GuiNpcLabel(100, "ability.healAmount", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, heal.getHealAmount()));
@@ -36,16 +49,13 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig {
y += 24;
// Row 2: Heal Radius + Instant Heal
// Row 3: Heal Radius
addLabel(new GuiNpcLabel(102, "ability.healRadius", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, heal.getHealRadius()));
addLabel(new GuiNpcLabel(103, "dialog.instant", col2LabelX, y + 5));
addButton(new GuiNpcButton(103, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0));
y += 24;
// Row 3: Heal Self + Heal Allies
// Row 4: Heal Self + Heal Allies
addLabel(new GuiNpcLabel(104, "ability.healSelf", labelX, y + 5));
addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealSelf() ? 1 : 0));
@@ -59,6 +69,7 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig {
switch (id) {
case 103:
heal.setInstantHeal(value == 1);
initGui(); // Refresh to show/hide duration field
break;
case 104:
heal.setHealSelf(value == 1);
@@ -72,6 +83,9 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig {
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 99:
heal.setDurationTicks(field.getInteger());
break;
case 100:
heal.setHealAmount(parseFloat(field, heal.getHealAmount()));
break;
@@ -31,13 +31,21 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig {
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Placement + Trigger Radius
addLabel(new GuiNpcLabel(100, "ability.placement", labelX, y + 5));
String[] placements = {"Caster", "Target", "Ahead"};
addButton(new GuiNpcButton(100, fieldX, y, 55, 20, placements, trap.getPlacement().ordinal()));
// Row 1: Duration + Placement
addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5));
GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, trap.getDurationTicks());
durationField.setMinMaxDefault(1, 2000, 200);
addTextField(durationField);
addLabel(new GuiNpcLabel(101, "ability.triggerRad", col2LabelX, y + 5));
addTextField(createFloatField(101, col2FieldX, y, 50, trap.getTriggerRadius()));
addLabel(new GuiNpcLabel(100, "ability.placement", col2LabelX, y + 5));
String[] placements = {"Caster", "Target", "Ahead"};
addButton(new GuiNpcButton(100, col2FieldX, y, 55, 20, placements, trap.getPlacement().ordinal()));
y += 24;
// Row 2: Trigger Radius
addLabel(new GuiNpcLabel(101, "ability.triggerRad", labelX, y + 5));
addTextField(createFloatField(101, fieldX, y, 50, trap.getTriggerRadius()));
y += 24;
@@ -90,6 +98,9 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig {
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 99:
trap.setDurationTicks(field.getInteger());
break;
case 101:
trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius()));
break;