mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 03:47:27 +00:00
Recovery -> Dazed. Better cooldown logic handling
This commit is contained in:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user