Refactoring for Code Cleanup and Projectile Abstract Class Handling

This commit is contained in:
Kam committed 2026-02-14 15:21:56 -05:00
1 parent a1f3f5184b
commit a23b199e7e
30 files changed
+1226 -2395

No files matched your search

@@ -7,6 +7,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.block.Block;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.DamageSource;
@@ -141,14 +142,17 @@ public abstract class Ability implements IAbility {
// ═══════════════════════════════════════════════════════════════════
/**
* Called first tick of ACTIVE phase
* Called first tick of ACTIVE phase.
* For burst without replay (burstReplayAnimations=false), this is called directly
* after BURST_DELAY without a preceding windup. Check isBurstRefire() or null-check
* entity state to detect and handle the burst refire case.
*/
public abstract void onExecute(EntityLivingBase caster, EntityLivingBase target, World world);
public abstract void onExecute(EntityLivingBase caster, EntityLivingBase target);
/**
* Called every tick of ACTIVE phase
*/
public abstract void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick);
public abstract void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick);
/**
* Write type-specific config to NBT
@@ -208,17 +212,15 @@ public abstract class Ability implements IAbility {
return true;
}
/**
* Called when burst re-fires without replay animations (burstReplayAnimations=false).
* Entity-spawning abilities should override to spawn and fire entities in one step,
* since onWindUpTick was not called (windup was skipped).
* Default: does nothing (ability goes directly to onExecute with existing state).
*/
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
// Override in entity-spawning abilities (Orb, Beam, Disc, LaserShot)
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
}
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
/**
* Whether this execution is a burst refire (not the first execution).
* Useful in onExecute() to detect when windup was skipped and entities need fresh creation.
*/
public boolean isBurstRefire() {
return burstEnabled && burstIndex > 0;
}
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
@@ -1051,11 +1053,11 @@ public abstract class Ability implements IAbility {
* Spawn an entity during ability execution.
* In preview mode, routes to PreviewEntityHandler instead of world.spawnEntityInWorld().
*/
protected void spawnAbilityEntity(World world, Entity entity) {
protected void spawnAbilityEntity(Entity entity) {
if (previewMode && previewEntityHandler != null) {
previewEntityHandler.onEntitySpawned(entity);
} else {
world.spawnEntityInWorld(entity);
entity.worldObj.spawnEntityInWorld(entity);
}
if (burstEnabled && burstOverlap) {
burstEntities.add(entity);
@@ -1800,6 +1802,52 @@ public abstract class Ability implements IAbility {
return totalDuration;
}
// ═══════════════════════════════════════════════════════════════════
// MOVEMENT COLLISION UTILITY
// ═══════════════════════════════════════════════════════════════════
/**
* Check if a caster's movement in a given direction is blocked by walls/blocks.
* Used by Charge and Dash abilities to detect wall collision.
*
* @param caster The entity moving
* @param dirX X component of normalized movement direction
* @param dirZ Z component of normalized movement direction
* @param speed Movement speed (blocks per tick)
* @return true if movement is blocked
*/
protected static boolean isMovementBlocked(EntityLivingBase caster, double dirX, double dirZ, double speed) {
double nextX = dirX * speed;
double nextZ = dirZ * speed;
AxisAlignedBB nextBox = caster.boundingBox.copy().offset(nextX, 0, nextZ);
double stepThreshold = nextBox.minY + Math.max(caster.stepHeight, 0.5);
int x1 = (int) Math.floor(nextBox.minX);
int x2 = (int) Math.floor(nextBox.maxX + 1.0);
int y1 = (int) Math.floor(nextBox.minY) - 1;
int y2 = (int) Math.floor(nextBox.maxY + 1.0);
int z1 = (int) Math.floor(nextBox.minZ);
int z2 = (int) Math.floor(nextBox.maxZ + 1.0);
java.util.ArrayList<AxisAlignedBB> collisionBoxes = new java.util.ArrayList<>();
for (int bx = x1; bx < x2; bx++) {
for (int bz = z1; bz < z2; bz++) {
for (int by = y1; by < y2; by++) {
Block block = caster.worldObj.getBlock(bx, by, bz);
if (!block.getMaterial().blocksMovement()) continue;
collisionBoxes.clear();
block.addCollisionBoxesToList(caster.worldObj, bx, by, bz, nextBox, collisionBoxes, caster);
for (AxisAlignedBB box : collisionBoxes) {
if (box.maxY > stepThreshold) {
return true;
}
}
}
}
}
return false;
}
// ═══════════════════════════════════════════════════════════════════
// IAbility API METHODS
// ═══════════════════════════════════════════════════════════════════
@@ -2,62 +2,41 @@ package kamkeel.npcs.controllers.data.ability.type;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.RotationMode;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.client.gui.builder.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import noppes.npcs.api.ability.type.IAbilityEnergyBeam;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.List;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
/**
* Energy Beam ability: A homing head with a trailing path.
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
*/
public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
public class AbilityBeam extends AbstractEnergyProjectileAbility<EntityAbilityBeam> implements IAbilityEnergyBeam {
private static final int MAX_PROJECTILES = 8;
// Beam shape properties (shared)
private float beamWidth = 0.4f;
private float headSize = 0.6f;
// Projectile count and fire stagger
private int projectileCount = 1;
private int fireDelay = 0;
// Shared visual data (all projectiles use these)
private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f);
private EnergyLightningData lightningData = new EnergyLightningData();
private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Shared combat/movement data
private final EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f);
private final EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f);
private final EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200);
// Per-projectile data (anchor + optional color override)
private ProjectileData[] projectiles;
// Transient entity state
private transient EntityAbilityBeam[] beamEntities;
public AbilityBeam() {
super(
new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f),
new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f),
new EnergyHomingData(0.4f, true, 0.1f, 15.0f),
new EnergyLifespanData(25.0f, 200)
);
this.typeId = "ability.cnpc.beam";
this.name = "Beam";
this.targetingMode = TargetingMode.AGGRO_TARGET;
@@ -72,56 +51,57 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
this.showTelegraph = true;
this.windUpAnimationName = "Ability_Beam_Windup";
this.activeAnimationName = "Ability_Beam_Active";
initProjectiles(1);
}
private void initProjectiles(int count) {
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
this.projectiles = new ProjectileData[projectileCount];
for (int i = 0; i < projectileCount; i++) {
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityAbilityBeam createEntity(EntityLivingBase caster, EntityLivingBase target,
Vec3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityAbilityBeam(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
beamWidth, headSize,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
lockMovement.locksActive());
}
@Override
protected void fireEntity(EntityAbilityBeam beam, EntityLivingBase target) {
if (isPreview()) {
beam.startPreviewFiring();
} else {
beam.startFiring(target);
}
}
private AnchorPoint getDefaultAnchor(int index) {
switch (index) {
case 0: return AnchorPoint.RIGHT_HAND;
case 1: return AnchorPoint.LEFT_HAND;
default: return AnchorPoint.FRONT;
}
@Override
protected void setupEntityCharging(EntityAbilityBeam beam, ProjectileData projData, int index) {
beam.setupCharging(projData.anchor, windUpTicks, 1.0f);
}
public void setProjectileCount(int count) {
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
if (newCount == projectileCount) return;
ProjectileData[] oldProjectiles = projectiles;
initProjectiles(newCount);
if (oldProjectiles != null) {
int copyCount = Math.min(oldProjectiles.length, newCount);
for (int i = 0; i < copyCount; i++) {
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
}
}
@Override
protected void setupEntityPreview(EntityAbilityBeam beam, EntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
beam.setupPreview(caster, beamWidth, headSize, resolved, lightningData, projData.anchor, windUpTicks, 1.0f);
}
public int getProjectileCount() {
return projectileCount;
@Override
protected EntityAbilityBeam[] createEntityArray(int size) {
return new EntityAbilityBeam[size];
}
public int getFireDelay() {
return fireDelay;
@Override
protected float getProjectileTelegraphRadius() {
return headSize * 2.0f;
}
public void setFireDelay(int delay) {
this.fireDelay = Math.max(0, delay);
@Override
protected Vec3 getSpawnPosition(EntityLivingBase caster, int index) {
return AnchorPointHelper.calculateAnchorPosition(caster, projectiles[index].anchor, 1.0f);
}
/** @deprecated Use {@link #getFireDelay()} */
public int getDualFireDelay() { return fireDelay; }
/** @deprecated Use {@link #setFireDelay(int)} */
public void setDualFireDelay(int delay) { setFireDelay(delay); }
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
@@ -134,7 +114,6 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
a.setName("Dual Beam");
beam.setProjectileCount(2);
beam.setFireDelay(0);
// Set color override for second beam
beam.projectiles[1].colorOverride = true;
beam.projectiles[1].innerColor = 0xFFFFFF;
beam.projectiles[1].outerColor = 0xFF0000;
@@ -145,244 +124,24 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
);
}
@Override
public boolean allowOverlap() {
return true;
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
/**
* Create beam entities.
* NPC: target is the aggro target — beams will home toward it.
* Player: target is null — beams fire in caster's look direction.
*/
private EntityAbilityBeam[] createBeamEntities(EntityLivingBase caster, EntityLivingBase target, World world) {
EntityAbilityBeam[] entities = new EntityAbilityBeam[projectileCount];
float offsetDist = 1.0f;
for (int i = 0; i < projectileCount; i++) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor, offsetDist);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
entities[i] = new EntityAbilityBeam(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
beamWidth, headSize,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
lockMovement.locksActive());
entities[i].setEffects(this.effects);
entities[i].setSourceAbility(this);
}
if (projectileCount == 2 && entities[0] != null && entities[1] != null) {
entities[0].setSiblingEntityId(entities[1].getEntityId());
entities[1].setSiblingEntityId(entities[0].getEntityId());
}
return entities;
}
private void fireBeamEntity(EntityAbilityBeam beam, EntityLivingBase target) {
if (beam == null || beam.isDead) return;
if (isPreview()) {
beam.startPreviewFiring();
} else {
beam.startFiring(target);
}
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
if (tick == 1) {
beamEntities = createBeamEntities(caster, target, world);
float offsetDist = 1.0f;
for (int i = 0; i < projectileCount; i++) {
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
if (isPreview()) {
beamEntities[i].setupPreview(caster, beamWidth, headSize, resolved, lightningData, projectiles[i].anchor, windUpTicks, offsetDist);
} else {
beamEntities[i].setupCharging(projectiles[i].anchor, windUpTicks, offsetDist);
}
spawnAbilityEntity(world, beamEntities[i]);
}
}
}
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
beamEntities = createBeamEntities(caster, target, world);
for (EntityAbilityBeam beam : beamEntities) {
spawnAbilityEntity(world, beam);
}
// Fire first projectile immediately, let onActiveTick handle the rest with fireDelay
fireBeamEntity(beamEntities[0], target);
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireBeamEntity(beamEntities[i], target);
}
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (beamEntities == null) return;
fireBeamEntity(beamEntities[0], target);
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireBeamEntity(beamEntities[i], target);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (beamEntities == null) {
signalCompletion();
return;
}
if (fireDelay > 0) {
for (int i = 1; i < projectileCount; i++) {
if (tick == fireDelay * i) {
fireBeamEntity(beamEntities[i], target);
}
}
}
boolean allDead = true;
for (EntityAbilityBeam beam : beamEntities) {
if (beam != null && !beam.isDead) {
allDead = false;
break;
}
}
if (allDead) {
signalCompletion();
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
@Override
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
if (beamEntities != null) {
for (int i = 0; i < beamEntities.length; i++) {
if (beamEntities[i] != null && !beamEntities[i].isDead) {
beamEntities[i].setDead();
}
beamEntities[i] = null;
}
}
beamEntities = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
// NPC: telegraph on aggro target position
// Player: no telegraph (beam fires in look direction, no fixed position to mark)
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
return null;
}
Telegraph telegraph = Telegraph.circle(headSize * 2.0f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return headSize * 2.0f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
protected void writeTypeSpecificNBT(NBTTagCompound nbt) {
nbt.setFloat("beamWidth", beamWidth);
nbt.setFloat("headSize", headSize);
nbt.setInteger("projectileCount", projectileCount);
nbt.setInteger("fireDelay", fireDelay);
// Shared visual data
displayData.writeNBT(nbt);
lightningData.writeNBT(nbt);
// Shared combat/movement data
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
NBTTagCompound projNbt = new NBTTagCompound();
projectiles[i].writeNBT(projNbt);
nbt.setTag("Projectile_" + i, projNbt);
}
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
protected void readTypeSpecificNBT(NBTTagCompound nbt) {
this.beamWidth = nbt.getFloat("beamWidth");
this.headSize = nbt.getFloat("headSize");
int count = nbt.getInteger("projectileCount");
initProjectiles(count);
this.fireDelay = nbt.getInteger("fireDelay");
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
// Shared visual data
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
}
// ═══════════════════════════════════════════════════════════════════
// API GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
// ==================== TYPE-SPECIFIC GETTERS ====================
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { homingData.speed = speed; }
public float getBeamWidth() { return beamWidth; }
public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; }
@@ -390,142 +149,16 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
public float getHeadSize() { return headSize; }
public void setHeadSize(float headSize) { this.headSize = headSize; }
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { this.homingData.speed = speed; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; }
public float getDamage() { return combatData.damage; }
public void setDamage(float damage) { this.combatData.damage = damage; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float knockback) { this.combatData.knockback = knockback; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean homing) { this.homingData.homing = homing; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; }
// Shared display data (primary colors)
public int getInnerColor() { return displayData.innerColor; }
public void setInnerColor(int innerColor) { displayData.innerColor = innerColor; }
public int getOuterColor() { return displayData.outerColor; }
public void setOuterColor(int outerColor) { displayData.outerColor = outerColor; }
public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
public void setOuterColorEnabled(boolean outerColorEnabled) { displayData.outerColorEnabled = outerColorEnabled; }
public float getOuterColorWidth() { return displayData.outerColorWidth; }
public void setOuterColorWidth(float outerColorWidth) { displayData.outerColorWidth = outerColorWidth; }
public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
public void setOuterColorAlpha(float outerColorAlpha) { displayData.outerColorAlpha = outerColorAlpha; }
public float getRotationSpeed() { return displayData.rotationSpeed; }
public void setRotationSpeed(float rotationSpeed) { displayData.rotationSpeed = rotationSpeed; }
// Shared lightning data
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
public void setLightningEffect(boolean lightningEffect) { lightningData.lightningEffect = lightningEffect; }
public float getLightningDensity() { return lightningData.lightningDensity; }
public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; }
public float getLightningRadius() { return lightningData.lightningRadius; }
public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; }
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
public void setLightningFadeTime(int lightningFadeTime) { lightningData.lightningFadeTime = lightningFadeTime; }
// Anchor - default to projectile 0
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { projectiles[0].anchor.anchorPoint = anchorPoint; }
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
@Override
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
@Override
public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
// Indexed API methods
public int getInnerColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.innerColor : displayData.innerColor;
}
public void setInnerColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.innerColor = color;
}
public int getOuterColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.outerColor : displayData.outerColor;
}
public void setOuterColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.outerColor = color;
}
public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); }
public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; }
public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; }
public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; }
private int clampIndex(int index) {
return Math.max(0, Math.min(index, projectileCount - 1));
}
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
// ═══════════════════════════════════════════════════════════════════
// GUI FIELD DEFINITIONS
// ═══════════════════════════════════════════════════════════════════
// ==================== TYPE-SPECIFIC GUI ====================
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
protected void addTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
@@ -550,63 +183,5 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
// Visual tab - shared primary colors
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"));
// Visual tab - shared effects
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual"));
// Visual tab - per-projectile sections
for (int i = 0; i < MAX_PROJECTILES; i++) {
final int idx = i;
String sectionKey = "ability.section.projectile" + (i + 1);
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
.visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX,
v -> projectiles[idx].anchor.anchorOffsetX = v).min(Float.NEGATIVE_INFINITY),
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY,
v -> projectiles[idx].anchor.anchorOffsetY = v).min(Float.NEGATIVE_INFINITY)
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ,
v -> projectiles[idx].anchor.anchorOffsetZ = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount).min(Float.NEGATIVE_INFINITY));
// Color override (only when multiple projectiles)
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
}
}
}
@@ -13,7 +13,6 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -81,7 +80,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
* Called on the first tick of windup - lock direction here so it matches telegraph.
*/
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (tick == 1) {
// Lock direction on first windup tick (same time telegraph is created)
lockChargeDirection(caster, target);
@@ -116,7 +115,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
// Initialize charge - direction was already locked during windup
startX = caster.posX;
startY = caster.posY;
@@ -146,7 +145,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
// Safety timeout or missing state: force-complete to prevent stuck NPC
if (!isPreview() && (chargeDirection == null || tick > maxActiveTicks)) {
stopMomentum(caster);
@@ -206,11 +205,11 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
}
// Server-side collision damage (skip in preview)
if (!world.isRemote && !isPreview()) {
if (!caster.worldObj.isRemote && !isPreview()) {
AxisAlignedBB hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth);
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
List<Entity> entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
for (Entity entity : entities) {
if (!(entity instanceof EntityLivingBase)) continue;
@@ -226,9 +225,10 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
boolean wasHit = applyAbilityDamageWithDirection(caster, livingEntity, damage, knockback,
chargeDirection.xCoord, chargeDirection.zCoord);
// Play impact sound if hit wasn't cancelled
// Apply effects and play impact sound if hit wasn't cancelled
if (wasHit) {
world.playSoundAtEntity(livingEntity, "random.explode", 0.5f, 1.2f);
applyEffects(livingEntity);
caster.worldObj.playSoundAtEntity(livingEntity, "random.explode", 0.5f, 1.2f);
}
}
}
@@ -255,35 +255,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
}
private boolean isChargeBlocked(EntityLivingBase caster) {
double nextX = chargeDirection.xCoord * chargeSpeed;
double nextZ = chargeDirection.zCoord * chargeSpeed;
AxisAlignedBB nextBox = caster.boundingBox.copy().offset(nextX, 0, nextZ);
double stepThreshold = nextBox.minY + Math.max(caster.stepHeight, 0.5);
int x1 = (int) Math.floor(nextBox.minX);
int x2 = (int) Math.floor(nextBox.maxX + 1.0);
int y1 = (int) Math.floor(nextBox.minY) - 1;
int y2 = (int) Math.floor(nextBox.maxY + 1.0);
int z1 = (int) Math.floor(nextBox.minZ);
int z2 = (int) Math.floor(nextBox.maxZ + 1.0);
java.util.ArrayList<AxisAlignedBB> collisionBoxes = new java.util.ArrayList<>();
for (int bx = x1; bx < x2; bx++) {
for (int bz = z1; bz < z2; bz++) {
for (int by = y1; by < y2; by++) {
net.minecraft.block.Block block = caster.worldObj.getBlock(bx, by, bz);
if (!block.getMaterial().blocksMovement()) continue;
collisionBoxes.clear();
block.addCollisionBoxesToList(caster.worldObj, bx, by, bz, nextBox, collisionBoxes, caster);
for (AxisAlignedBB box : collisionBoxes) {
if (box.maxY > stepThreshold) {
return true;
}
}
}
}
}
return false;
return isMovementBlocked(caster, chargeDirection.xCoord, chargeDirection.zCoord, chargeSpeed);
}
@Override
@@ -154,7 +154,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
hitEntities.clear();
currentRotation = -arcAngle / 2.0f;
activeSoundPlayed = false;
@@ -175,8 +175,8 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (caster.worldObj.isRemote && !isPreview()) return;
switch (sweepMode) {
case SWIPE:
@@ -185,25 +185,25 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
// Play active sound when sweep reaches center (where target typically stands)
if (!activeSoundPlayed && currentRotation >= 0) {
playActiveSound(caster, world);
playActiveSound(caster, caster.worldObj);
}
if (currentRotation > arcAngle / 2.0f) {
if (!isPreview()) {
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f);
performSweepDamageRange(caster, caster.worldObj, innerRadius, range, prevRotation, arcAngle / 2.0f);
}
signalCompletion();
return;
}
if (!isPreview()) {
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation);
performSweepDamageRange(caster, caster.worldObj, innerRadius, range, prevRotation, currentRotation);
}
break;
case SPIN:
// Play active sound at spin start
if (!activeSoundPlayed) {
playActiveSound(caster, world);
playActiveSound(caster, caster.worldObj);
}
if (tick >= spinDurationTicks) {
@@ -214,7 +214,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
currentRotation = (currentRotation + sweepSpeed) % 360.0f;
if (!isPreview()) {
hitEntities.clear();
performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation);
performSweepDamageRange(caster, caster.worldObj, innerRadius, range, prevSpin, currentRotation);
}
break;
}
@@ -11,7 +11,6 @@ import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -133,7 +132,7 @@ public class AbilityDash extends Ability implements IAbilityDash {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
startX = caster.posX;
startY = caster.posY;
startZ = caster.posZ;
@@ -175,7 +174,7 @@ public class AbilityDash extends Ability implements IAbilityDash {
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
// Safety timeout or missing state: force-complete to prevent stuck NPC
if (!isPreview() && (dashDirection == null || tick > maxActiveTicks)) {
stopDash(caster);
@@ -224,7 +223,7 @@ public class AbilityDash extends Ability implements IAbilityDash {
caster.velocityChanged = true;
// Trail particles
world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0);
caster.worldObj.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0);
}
}
@@ -255,35 +254,7 @@ public class AbilityDash extends Ability implements IAbilityDash {
private boolean isDashBlocked(EntityLivingBase caster) {
if (dashDirection == null) return true;
double nextX = dashDirection.xCoord * dashSpeed;
double nextZ = dashDirection.zCoord * dashSpeed;
AxisAlignedBB nextBox = caster.boundingBox.copy().offset(nextX, 0, nextZ);
double stepThreshold = nextBox.minY + Math.max(caster.stepHeight, 0.5);
int x1 = (int) Math.floor(nextBox.minX);
int x2 = (int) Math.floor(nextBox.maxX + 1.0);
int y1 = (int) Math.floor(nextBox.minY) - 1;
int y2 = (int) Math.floor(nextBox.maxY + 1.0);
int z1 = (int) Math.floor(nextBox.minZ);
int z2 = (int) Math.floor(nextBox.maxZ + 1.0);
java.util.ArrayList<AxisAlignedBB> collisionBoxes = new java.util.ArrayList<>();
for (int bx = x1; bx < x2; bx++) {
for (int bz = z1; bz < z2; bz++) {
for (int by = y1; by < y2; by++) {
net.minecraft.block.Block block = caster.worldObj.getBlock(bx, by, bz);
if (!block.getMaterial().blocksMovement()) continue;
collisionBoxes.clear();
block.addCollisionBoxesToList(caster.worldObj, bx, by, bz, nextBox, collisionBoxes, caster);
for (AxisAlignedBB box : collisionBoxes) {
if (box.maxY > stepThreshold) {
return true;
}
}
}
}
}
return false;
return isMovementBlocked(caster, dashDirection.xCoord, dashDirection.zCoord, dashSpeed);
}
@Override
@@ -2,67 +2,42 @@ package kamkeel.npcs.controllers.data.ability.type;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.client.gui.builder.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import noppes.npcs.api.ability.type.IAbilityDisc;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.List;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
/**
* Disc ability: Spawns a flat spinning disc projectile.
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
* Has optional boomerang behavior to return to owner.
*/
public class AbilityDisc extends Ability implements IAbilityDisc {
public class AbilityDisc extends AbstractEnergyProjectileAbility<EntityAbilityDisc> implements IAbilityDisc {
private static final int MAX_PROJECTILES = 8;
// Disc geometry (shared)
private float discRadius = 1.0f;
private float discThickness = 0.2f;
private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward)
// Boomerang properties (shared)
private boolean vertical = false;
private boolean boomerang = false;
private int boomerangDelay = 40;
// Projectile count and fire stagger
private int projectileCount = 1;
private int fireDelay = 0;
// Shared visual data (all projectiles use these)
private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f);
private EnergyLightningData lightningData = new EnergyLightningData();
private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Shared combat/movement data
private final EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f);
private final EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f);
private final EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200);
// Per-projectile data (anchor + optional color override)
private ProjectileData[] projectiles;
// Transient entity state
private transient EntityAbilityDisc[] discEntities;
public AbilityDisc() {
super(
new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f),
new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f),
new EnergyHomingData(0.6f, true, 0.12f, 18.0f),
new EnergyLifespanData(35.0f, 200)
);
this.typeId = "ability.cnpc.disc";
this.name = "Disc";
this.targetingMode = TargetingMode.AGGRO_TARGET;
@@ -75,59 +50,52 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
this.showTelegraph = true;
this.windUpAnimationName = "Ability_Disc_Windup";
this.activeAnimationName = "Ability_Disc_Active";
initProjectiles(1);
}
private void initProjectiles(int count) {
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
this.projectiles = new ProjectileData[projectileCount];
for (int i = 0; i < projectileCount; i++) {
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityAbilityDisc createEntity(EntityLivingBase caster, EntityLivingBase target,
Vec3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityAbilityDisc(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
discRadius, discThickness,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
boomerang, boomerangDelay);
}
@Override
protected void fireEntity(EntityAbilityDisc disc, EntityLivingBase target) {
if (isPreview()) {
disc.startPreviewFiring();
} else {
disc.startMoving(target);
}
}
/**
* Disc anchor order is REVERSED from Orb/Beam: [0]=LEFT_HAND, [1]=RIGHT_HAND
*/
private AnchorPoint getDefaultAnchor(int index) {
switch (index) {
case 0: return AnchorPoint.RIGHT_HAND;
case 1: return AnchorPoint.LEFT_HAND;
default: return AnchorPoint.FRONT;
}
@Override
protected void setupEntityCharging(EntityAbilityDisc disc, ProjectileData projData, int index) {
disc.setupCharging(projData.anchor, windUpTicks, vertical);
}
public void setProjectileCount(int count) {
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
if (newCount == projectileCount) return;
ProjectileData[] oldProjectiles = projectiles;
initProjectiles(newCount);
if (oldProjectiles != null) {
int copyCount = Math.min(oldProjectiles.length, newCount);
for (int i = 0; i < copyCount; i++) {
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
}
}
@Override
protected void setupEntityPreview(EntityAbilityDisc disc, EntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
disc.setupPreview(caster, discRadius, discThickness, resolved, lightningData, projData.anchor, windUpTicks, vertical);
}
public int getProjectileCount() {
return projectileCount;
@Override
protected EntityAbilityDisc[] createEntityArray(int size) {
return new EntityAbilityDisc[size];
}
public int getFireDelay() {
return fireDelay;
@Override
protected float getProjectileTelegraphRadius() {
return discRadius * 1.5f;
}
public void setFireDelay(int delay) {
this.fireDelay = Math.max(0, delay);
}
/** @deprecated Use {@link #getFireDelay()} */
public int getDualFireDelay() { return fireDelay; }
/** @deprecated Use {@link #setFireDelay(int)} */
public void setDualFireDelay(int delay) { setFireDelay(delay); }
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
@@ -140,7 +108,6 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
a.setName("Dual Disc");
disc.setProjectileCount(2);
disc.setFireDelay(5);
// Set color override for second disc
disc.projectiles[1].colorOverride = true;
disc.projectiles[1].innerColor = 0xFFFFFF;
disc.projectiles[1].outerColor = 0x8800FF;
@@ -150,255 +117,27 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
);
}
@Override
public boolean allowOverlap() {
return true;
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
/**
* Create disc entities.
* NPC: target is the aggro target — discs will home toward it.
* Player: target is null — discs fire in caster's look direction.
*/
private EntityAbilityDisc[] createDiscEntities(EntityLivingBase caster, EntityLivingBase target, World world) {
EntityAbilityDisc[] entities = new EntityAbilityDisc[projectileCount];
for (int i = 0; i < projectileCount; i++) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
entities[i] = new EntityAbilityDisc(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
discRadius, discThickness,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
boomerang, boomerangDelay);
entities[i].setEffects(this.effects);
entities[i].setSourceAbility(this);
}
if (projectileCount == 2 && entities[0] != null && entities[1] != null) {
entities[0].setSiblingEntityId(entities[1].getEntityId());
entities[1].setSiblingEntityId(entities[0].getEntityId());
}
return entities;
}
private void fireDiscEntity(EntityAbilityDisc disc, EntityLivingBase target) {
if (disc == null || disc.isDead) return;
if (isPreview()) {
disc.startPreviewFiring();
} else {
disc.startMoving(target);
}
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
if (tick == 1) {
discEntities = createDiscEntities(caster, target, world);
for (int i = 0; i < projectileCount; i++) {
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
if (isPreview()) {
discEntities[i].setupPreview(caster, discRadius, discThickness, resolved, lightningData, projectiles[i].anchor, windUpTicks, vertical);
} else {
discEntities[i].setupCharging(projectiles[i].anchor, windUpTicks, vertical);
}
spawnAbilityEntity(world, discEntities[i]);
}
}
}
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
discEntities = createDiscEntities(caster, target, world);
for (EntityAbilityDisc disc : discEntities) {
spawnAbilityEntity(world, disc);
}
// Fire first projectile immediately, let onActiveTick handle the rest with fireDelay
fireDiscEntity(discEntities[0], target);
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireDiscEntity(discEntities[i], target);
}
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (discEntities == null) return;
// Fire first projectile immediately
fireDiscEntity(discEntities[0], target);
// Fire remaining projectiles if no delay
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireDiscEntity(discEntities[i], target);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (discEntities == null) {
signalCompletion();
return;
}
// Fire staggered projectiles
if (fireDelay > 0) {
for (int i = 1; i < projectileCount; i++) {
if (tick == fireDelay * i) {
fireDiscEntity(discEntities[i], target);
}
}
}
// Check if all entities are dead
boolean allDead = true;
for (EntityAbilityDisc disc : discEntities) {
if (disc != null && !disc.isDead) {
allDead = false;
break;
}
}
if (allDead) {
signalCompletion();
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
@Override
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
if (discEntities != null) {
for (int i = 0; i < discEntities.length; i++) {
if (discEntities[i] != null && !discEntities[i].isDead) {
discEntities[i].setDead();
}
discEntities[i] = null;
}
}
discEntities = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
// NPC: telegraph on aggro target position
// Player: no telegraph (discs fire in look direction, no fixed position to mark)
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
return null;
}
// Create circle telegraph at target position
Telegraph telegraph = Telegraph.circle(discRadius * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return discRadius * 1.5f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
protected void writeTypeSpecificNBT(NBTTagCompound nbt) {
nbt.setFloat("discRadius", discRadius);
nbt.setFloat("discThickness", discThickness);
nbt.setBoolean("vertical", vertical);
nbt.setBoolean("boomerang", boomerang);
nbt.setInteger("boomerangDelay", boomerangDelay);
nbt.setInteger("projectileCount", projectileCount);
nbt.setInteger("fireDelay", fireDelay);
// Shared visual data
displayData.writeNBT(nbt);
lightningData.writeNBT(nbt);
// Shared combat/movement data
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
NBTTagCompound projNbt = new NBTTagCompound();
projectiles[i].writeNBT(projNbt);
nbt.setTag("Projectile_" + i, projNbt);
}
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
protected void readTypeSpecificNBT(NBTTagCompound nbt) {
this.discRadius = nbt.getFloat("discRadius");
this.discThickness = nbt.getFloat("discThickness");
this.vertical = nbt.getBoolean("vertical");
this.boomerang = nbt.getBoolean("boomerang");
this.boomerangDelay = nbt.getInteger("boomerangDelay");
int count = nbt.getInteger("projectileCount");
initProjectiles(count);
this.fireDelay = nbt.getInteger("fireDelay");
// Shared combat/movement data
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
// Shared visual data
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
}
// ═══════════════════════════════════════════════════════════════════
// API GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
// ==================== TYPE-SPECIFIC GETTERS ====================
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { homingData.speed = speed; }
@@ -418,141 +157,16 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
public int getBoomerangDelay() { return boomerangDelay; }
public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float d) { lifespanData.maxDistance = d; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int t) { lifespanData.maxLifetime = t; }
public float getDamage() { return combatData.damage; }
public void setDamage(float d) { combatData.damage = d; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float k) { combatData.knockback = k; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float k) { combatData.knockbackUp = k; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean h) { homingData.homing = h; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float s) { homingData.homingStrength = s; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float r) { homingData.homingRange = r; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean e) { combatData.explosive = e; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float r) { combatData.explosionRadius = r; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float f) { combatData.explosionDamageFalloff = f; }
// Shared display data (primary colors)
public int getInnerColor() { return displayData.innerColor; }
public void setInnerColor(int c) { displayData.innerColor = c; }
public int getOuterColor() { return displayData.outerColor; }
public void setOuterColor(int c) { displayData.outerColor = c; }
public float getOuterColorWidth() { return displayData.outerColorWidth; }
public void setOuterColorWidth(float w) { displayData.outerColorWidth = w; }
public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
public void setOuterColorAlpha(float a) { displayData.outerColorAlpha = a; }
public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
public void setOuterColorEnabled(boolean e) { displayData.outerColorEnabled = e; }
public float getRotationSpeed() { return displayData.rotationSpeed; }
public void setRotationSpeed(float s) { displayData.rotationSpeed = s; }
// Shared lightning data
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
public void setLightningEffect(boolean e) { lightningData.lightningEffect = e; }
public float getLightningDensity() { return lightningData.lightningDensity; }
public void setLightningDensity(float d) { lightningData.lightningDensity = d; }
public float getLightningRadius() { return lightningData.lightningRadius; }
public void setLightningRadius(float r) { lightningData.lightningRadius = r; }
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
public void setLightningFadeTime(int t) { lightningData.lightningFadeTime = t; }
// Anchor - default to projectile 0
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
public void setAnchorPointEnum(AnchorPoint p) { projectiles[0].anchor.anchorPoint = p; }
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
@Override
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
@Override
public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
// Indexed API methods
public int getInnerColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.innerColor : displayData.innerColor;
}
public void setInnerColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.innerColor = color;
}
public int getOuterColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.outerColor : displayData.outerColor;
}
public void setOuterColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.outerColor = color;
}
public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); }
public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; }
public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; }
public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; }
private int clampIndex(int index) {
return Math.max(0, Math.min(index, projectileCount - 1));
}
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
// ═══════════════════════════════════════════════════════════════════
// GUI FIELD DEFINITIONS
// ═══════════════════════════════════════════════════════════════════
// ==================== TYPE-SPECIFIC GUI ====================
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
// Type tab - projectile count and fire delay
protected void addTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
// Type tab - disc properties
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
@@ -579,64 +193,5 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
// Visual tab - shared primary colors
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"));
// Visual tab - shared effects
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual"));
// Visual tab - per-projectile sections
for (int i = 0; i < MAX_PROJECTILES; i++) {
final int idx = i;
String sectionKey = "ability.section.projectile" + (i + 1);
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
.visibleWhen(() -> idx < projectileCount));
// Anchor point
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX,
v -> projectiles[idx].anchor.anchorOffsetX = v).min(Float.NEGATIVE_INFINITY),
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY,
v -> projectiles[idx].anchor.anchorOffsetY = v).min(Float.NEGATIVE_INFINITY)
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ,
v -> projectiles[idx].anchor.anchorOffsetZ = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount).min(Float.NEGATIVE_INFINITY));
// Color override (only when multiple projectiles)
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
}
}
}
@@ -95,7 +95,7 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
lastAttacker = null;
counterTriggered = false;
lastDamageTaken = 0.0f;
@@ -103,9 +103,9 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (canCounter && counterEligible && counterTriggered && lastAttacker != null && !world.isRemote) {
performCounter(caster, world);
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (canCounter && counterEligible && counterTriggered && lastAttacker != null && !caster.worldObj.isRemote) {
performCounter(caster, caster.worldObj);
}
if (tick > counterWindow && counterEligible) {
@@ -5,7 +5,7 @@ import kamkeel.npcs.entity.EntityAbilityZone;
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
import noppes.npcs.api.ability.type.IAbilityHazard;
@@ -48,12 +48,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
activeEntities.clear();
// Use pre-calculated positions from telegraph phase, or generate new ones
@@ -75,7 +70,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard {
for (int i = 0; i < zoneCount; i++) {
double[] pos = positions.get(i);
EntityAbilityZone entity = EntityAbilityZone.createHazard(world, caster,
EntityAbilityZone entity = EntityAbilityZone.createHazard(caster.worldObj, caster,
pos[0], caster.posY, pos[1],
zoneShape,
radius,
@@ -93,7 +88,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard {
entity.setupPreview(caster);
}
spawnAbilityEntity(world, entity);
spawnAbilityEntity(entity);
activeEntities.add(entity);
}
}
@@ -79,27 +79,27 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) return;
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
if (caster.worldObj.isRemote && !isPreview()) return;
if (!isPreview()) {
getHealedAllies().clear();
// Always find allies if we're healing them (needed for both instant and HoT)
if (healAllies && healRadius > 0) {
findAlliesInRadius(caster, world);
findAlliesInRadius(caster, caster.worldObj);
}
if (instantHeal) {
// Instant heal - apply all healing now
if (healSelf) {
healEntity(caster);
spawnHealParticles(world, caster);
spawnHealParticles(caster.worldObj, caster);
}
for (EntityLivingBase ally : getHealedAllies()) {
healEntity(ally);
spawnHealParticles(world, ally);
spawnHealParticles(caster.worldObj, ally);
}
}
}
@@ -112,8 +112,8 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if ((world.isRemote && !isPreview()) || instantHeal) return;
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if ((caster.worldObj.isRemote && !isPreview()) || instantHeal) return;
// Heal over time - distribute heal across duration ticks
if (tick % 10 == 0) {
@@ -128,7 +128,7 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
}
caster.heal(selfTickHeal);
if (tick % 20 == 0) {
spawnHealParticles(world, caster);
spawnHealParticles(caster.worldObj, caster);
}
}
@@ -142,7 +142,7 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
}
ally.heal(allyTickHeal);
if (tick % 20 == 0) {
spawnHealParticles(world, ally);
spawnHealParticles(caster.worldObj, ally);
}
}
}
@@ -9,7 +9,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
import noppes.npcs.client.gui.builder.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import noppes.npcs.api.ability.type.IAbilityHeavyHit;
@@ -28,6 +27,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
private float knockback = 2.0f;
private float hitLength = 4.0f; // How far in front of the caster the hit reaches
private float hitWidth = 3.0f; // How wide to each side (total width = hitWidth * 2)
private int activeDisplayTicks = 10; // How long active animation plays before completing
public AbilityHeavyHit() {
this.typeId = "ability.cnpc.heavy_hit";
@@ -67,8 +67,8 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
if (caster.worldObj.isRemote && !isPreview()) {
signalCompletion();
return;
}
@@ -84,7 +84,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
// Search area: AABB that encompasses the rectangle
float searchDist = Math.max(hitLength, hitWidth) + 1.0f;
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(
List<Entity> entities = caster.worldObj.getEntitiesWithinAABBExcludingEntity(
caster, caster.boundingBox.expand(searchDist, 2, searchDist));
boolean anyHit = false;
@@ -113,15 +113,14 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
// Play hit sound even if nothing was hit (the attack still happens)
if (!anyHit) {
world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f);
caster.worldObj.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Just enough delay to see the active animation
if (tick == 10)
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (tick >= activeDisplayTicks)
signalCompletion();
}
@@ -131,6 +130,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
nbt.setFloat("knockback", knockback);
nbt.setFloat("hitLength", hitLength);
nbt.setFloat("hitWidth", hitWidth);
nbt.setInteger("activeDisplayTicks", activeDisplayTicks);
}
@Override
@@ -139,6 +139,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
this.knockback = nbt.getFloat("knockback");
this.hitLength = nbt.getFloat("hitLength");
this.hitWidth = nbt.getFloat("hitWidth");
this.activeDisplayTicks = nbt.hasKey("activeDisplayTicks") ? nbt.getInteger("activeDisplayTicks") : 10;
}
// Getters & Setters
@@ -174,6 +175,14 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
this.hitWidth = hitWidth;
}
public int getActiveDisplayTicks() {
return activeDisplayTicks;
}
public void setActiveDisplayTicks(int activeDisplayTicks) {
this.activeDisplayTicks = activeDisplayTicks;
}
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
@@ -187,6 +196,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
FieldDef.floatField("ability.hitLength", this::getHitLength, this::setHitLength),
FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth)
),
FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
@@ -2,50 +2,42 @@ package kamkeel.npcs.controllers.data.ability.type;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.RotationMode;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import noppes.npcs.client.gui.builder.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.api.ability.type.IAbilityLaserShot;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.List;
/**
* Laser Shot ability: Fast expanding thin line that pierces through targets.
* Travels in a straight line from origin, damaging all entities it passes through.
* Single-projectile only (always projectileCount=1).
*/
public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
public class AbilityLaserShot extends AbstractEnergyProjectileAbility<EntityAbilityLaser> implements IAbilityLaserShot {
// Laser properties
private float laserWidth = 0.3f;
private float expansionSpeed = 3.0f;
private int lingerTicks = 8;
// Data classes
private final EnergyDisplayData colorData = new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f);
private final EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f);
private final EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100);
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT);
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Transient state for laser entity (used for movement locking)
private transient EntityAbilityLaser laserEntity = null;
public AbilityLaserShot() {
super(
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f),
new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f),
new EnergyHomingData(),
new EnergyLifespanData(40.0f, 100)
);
this.typeId = "ability.cnpc.laser_shot";
this.name = "Laser Shot";
this.targetingMode = TargetingMode.AGGRO_TARGET;
@@ -58,14 +50,15 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
this.rotationPhase = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
// Default built-in animations
this.windUpAnimationName = "Ability_Laser_Windup";
this.activeAnimationName = "Ability_Laser_Active";
}
// ==================== OVERRIDES ====================
@Override
public boolean allowOverlap() {
return true;
protected AnchorPoint getDefaultAnchor(int index) {
return AnchorPoint.FRONT;
}
@Override
@@ -74,8 +67,34 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60;
}
/**
* LINE telegraph: positioned at caster, aimed toward target.
* Overrides the circle telegraph from AbstractEnergyProjectileAbility.
*/
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster)) {
return null;
}
Telegraph telegraph = Telegraph.line(getTelegraphLength(), getTelegraphWidth());
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, caster.posY, caster.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(caster.getEntityId());
if (target != null) {
instance.setTargetEntityId(target.getEntityId());
}
return instance;
}
@Override
@@ -85,242 +104,99 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
@Override
public float getTelegraphWidth() {
return laserWidth * 2.0f; // Make telegraph slightly wider for visibility
return laserWidth * 2.0f;
}
/**
* Spawn laser entity.
* NPC: target is the aggro target — laser expands toward it.
* Player: target is null — laser expands in caster's look direction.
*/
private EntityAbilityLaser spawnLaserEntity(EntityLivingBase caster, EntityLivingBase target, World world) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
EntityAbilityLaser entity = new EntityAbilityLaser(
world, caster, target,
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityAbilityLaser createEntity(EntityLivingBase caster, EntityLivingBase target,
Vec3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityAbilityLaser(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
laserWidth,
colorData, combatData, lightningData, lifespanData, trajectoryData,
expansionSpeed, lingerTicks
);
entity.setEffects(this.effects);
entity.setSourceAbility(this);
spawnAbilityEntity(world, entity);
return entity;
resolved, combatData, lightningData, lifespanData, trajectoryData,
expansionSpeed, lingerTicks);
}
private void fireLaserEntity(EntityAbilityLaser laser, EntityLivingBase target) {
if (laser == null || laser.isDead) return;
@Override
protected void fireEntity(EntityAbilityLaser laser, EntityLivingBase target) {
laser.setLockVerticalDirection(true);
laser.startMoving(target);
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
if (tick == 1) {
laserEntity = spawnLaserEntity(caster, target, world);
if (isPreview()) {
laserEntity.setupPreview(caster, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance);
} else {
laserEntity.setupCharging(anchorData, windUpTicks);
}
}
protected void setupEntityCharging(EntityAbilityLaser laser, ProjectileData projData, int index) {
laser.setupCharging(projData.anchor, windUpTicks);
}
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
laserEntity = spawnLaserEntity(caster, target, world);
fireLaserEntity(laserEntity, target);
protected void setupEntityPreview(EntityAbilityLaser laser, EntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
laser.setupPreview(caster, laserWidth, resolved, lightningData, expansionSpeed, lifespanData.maxDistance);
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (laserEntity == null) {
signalCompletion();
return;
}
fireLaserEntity(laserEntity, target);
protected EntityAbilityLaser[] createEntityArray(int size) {
return new EntityAbilityLaser[size];
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (laserEntity == null || laserEntity.isDead) {
laserEntity = null;
signalCompletion();
}
protected float getProjectileTelegraphRadius() {
return laserWidth * 2.0f;
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
// Despawn laser entity if still alive
if (laserEntity != null && !laserEntity.isDead) {
laserEntity.setDead();
}
laserEntity = null;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
protected void writeTypeSpecificNBT(NBTTagCompound nbt) {
nbt.setFloat("laserWidth", laserWidth);
nbt.setFloat("expansionSpeed", expansionSpeed);
nbt.setInteger("lingerTicks", lingerTicks);
anchorData.writeNBT(nbt);
colorData.writeNBT(nbt);
combatData.writeNBT(nbt);
lightningData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
protected void readTypeSpecificNBT(NBTTagCompound nbt) {
this.laserWidth = nbt.getFloat("laserWidth");
this.expansionSpeed = nbt.getFloat("expansionSpeed");
this.lingerTicks = nbt.getInteger("lingerTicks");
anchorData.readNBT(nbt);
colorData.readNBT(nbt);
combatData.readNBT(nbt);
lightningData.readNBT(nbt);
lifespanData.readNBT(nbt);
}
// Getters & Setters - Standalone fields
// ==================== TYPE-SPECIFIC GETTERS ====================
public float getLaserWidth() { return laserWidth; }
public void setLaserWidth(float laserWidth) { this.laserWidth = laserWidth; }
public float getExpansionSpeed() { return expansionSpeed; }
public void setExpansionSpeed(float expansionSpeed) { this.expansionSpeed = expansionSpeed; }
public int getLingerTicks() { return lingerTicks; }
public void setLingerTicks(int lingerTicks) { this.lingerTicks = lingerTicks; }
// Getters & Setters - Lifespan data
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; }
// Getters & Setters - Combat data
public float getDamage() { return combatData.damage; }
public void setDamage(float damage) { this.combatData.damage = damage; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float knockback) { this.combatData.knockback = knockback; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; }
// Getters & Setters - Color data
public int getInnerColor() { return colorData.innerColor; }
public void setInnerColor(int innerColor) { this.colorData.innerColor = innerColor; }
public int getOuterColor() { return colorData.outerColor; }
public void setOuterColor(int outerColor) { this.colorData.outerColor = outerColor; }
public float getOuterColorWidth() { return colorData.outerColorWidth; }
public void setOuterColorWidth(float outerColorWidth) { this.colorData.outerColorWidth = outerColorWidth; }
public float getOuterColorAlpha() { return colorData.outerColorAlpha; }
public void setOuterColorAlpha(float outerColorAlpha) { this.colorData.outerColorAlpha = outerColorAlpha; }
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; }
// Getters & Setters - Lightning data
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
public void setLightningEffect(boolean lightningEffect) { this.lightningData.lightningEffect = lightningEffect; }
public float getLightningDensity() { return lightningData.lightningDensity; }
public void setLightningDensity(float lightningDensity) { this.lightningData.lightningDensity = lightningDensity; }
public float getLightningRadius() { return lightningData.lightningRadius; }
public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; }
public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; }
public float getAnchorOffsetX() { return anchorData.anchorOffsetX; }
public float getAnchorOffsetY() { return anchorData.anchorOffsetY; }
public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; }
public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; }
public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; }
public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; }
public int getAnchorPoint() {
return anchorData.anchorPoint.ordinal();
}
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60;
}
// ==================== TYPE-SPECIFIC GUI ====================
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.row(
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.floatField("ability.knockbackUp", this::getKnockbackUp, this::setKnockbackUp)
),
FieldDef.section("ability.section.beam"),
FieldDef.row(
FieldDef.floatField("ability.laserWidth", this::getLaserWidth, this::setLaserWidth),
FieldDef.floatField("ability.expansionSpeed", this::getExpansionSpeed, this::setExpansionSpeed)
),
FieldDef.row(
FieldDef.intField("ability.lingerTicks", this::getLingerTicks, this::setLingerTicks),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.section("ability.section.explosive"),
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"),
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
// Visual tab
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX)
.min(Float.NEGATIVE_INFINITY),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
.min(Float.NEGATIVE_INFINITY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.min(Float.NEGATIVE_INFINITY).tab("ability.tab.visual"),
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled),
FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"),
FieldDef.section("ability.section.effects").tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.range(0.01f, 100f).visibleWhen(this::hasLightningEffect),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual")
protected void addTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage));
defs.add(FieldDef.row(
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.floatField("ability.knockbackUp", this::getKnockbackUp, this::setKnockbackUp)
));
defs.add(FieldDef.section("ability.section.beam"));
defs.add(FieldDef.row(
FieldDef.floatField("ability.laserWidth", this::getLaserWidth, this::setLaserWidth),
FieldDef.floatField("ability.expansionSpeed", this::getExpansionSpeed, this::setExpansionSpeed)
));
defs.add(FieldDef.row(
FieldDef.intField("ability.lingerTicks", this::getLingerTicks, this::setLingerTicks),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
));
defs.add(FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
}
}
@@ -2,60 +2,37 @@ package kamkeel.npcs.controllers.data.ability.type;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.LockMovementType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.client.gui.builder.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import noppes.npcs.api.ability.type.IAbilityOrb;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.Arrays;
import java.util.List;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
/**
* Orb ability: Spawns homing projectile sphere(s) that track target.
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
*/
public class AbilityOrb extends Ability implements IAbilityOrb {
public class AbilityOrb extends AbstractEnergyProjectileAbility<EntityAbilityOrb> implements IAbilityOrb {
private static final int MAX_PROJECTILES = 8;
// Orb properties
private float orbSize = 1.0f;
// Projectile count and fire stagger
private int projectileCount = 1;
private int fireDelay = 0;
// Shared visual data (all projectiles use these)
private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f);
private EnergyLightningData lightningData = new EnergyLightningData();
private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Shared combat/movement data
private final EnergyCombatData combatData = new EnergyCombatData();
private final EnergyHomingData homingData = new EnergyHomingData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData();
// Per-projectile data (anchor + optional color override)
private ProjectileData[] projectiles;
// Transient entity state
private transient EntityAbilityOrb[] orbEntities;
public AbilityOrb() {
super(
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f),
new EnergyCombatData(),
new EnergyHomingData(),
new EnergyLifespanData()
);
this.typeId = "ability.cnpc.orb";
this.name = "Orb";
this.targetingMode = TargetingMode.AGGRO_TARGET;
@@ -68,57 +45,50 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
this.showTelegraph = true;
this.windUpAnimationName = "Ability_Orb_Windup";
this.activeAnimationName = "Ability_Orb_Active";
initProjectiles(1);
}
private void initProjectiles(int count) {
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
this.projectiles = new ProjectileData[projectileCount];
for (int i = 0; i < projectileCount; i++) {
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityAbilityOrb createEntity(EntityLivingBase caster, EntityLivingBase target,
Vec3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityAbilityOrb(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData);
}
@Override
protected void fireEntity(EntityAbilityOrb orb, EntityLivingBase target) {
if (isPreview()) {
orb.startPreviewFiring();
} else {
orb.startMoving(target);
}
}
private AnchorPoint getDefaultAnchor(int index) {
switch (index) {
case 0: return AnchorPoint.RIGHT_HAND;
case 1: return AnchorPoint.LEFT_HAND;
default: return AnchorPoint.FRONT;
}
@Override
protected void setupEntityCharging(EntityAbilityOrb orb, ProjectileData projData, int index) {
orb.setupCharging(projData.anchor, windUpTicks);
}
public void setProjectileCount(int count) {
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
if (newCount == projectileCount) return;
ProjectileData[] oldProjectiles = projectiles;
initProjectiles(newCount);
// Preserve existing projectile data
if (oldProjectiles != null) {
int copyCount = Math.min(oldProjectiles.length, newCount);
for (int i = 0; i < copyCount; i++) {
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
}
}
@Override
protected void setupEntityPreview(EntityAbilityOrb orb, EntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
orb.setupPreview(caster, orbSize, resolved, lightningData, projData.anchor, windUpTicks);
}
public int getProjectileCount() {
return projectileCount;
@Override
protected EntityAbilityOrb[] createEntityArray(int size) {
return new EntityAbilityOrb[size];
}
public int getFireDelay() {
return fireDelay;
@Override
protected float getProjectileTelegraphRadius() {
return orbSize * 1.5f;
}
public void setFireDelay(int delay) {
this.fireDelay = Math.max(0, delay);
}
/** @deprecated Use {@link #getFireDelay()} */
public int getDualFireDelay() { return fireDelay; }
/** @deprecated Use {@link #setFireDelay(int)} */
public void setDualFireDelay(int delay) { setFireDelay(delay); }
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
@@ -153,244 +123,19 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
);
}
@Override
public boolean allowOverlap() {
return true;
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
/**
* Create orb entities.
* NPC: target is the aggro target — orbs will home toward it.
* Player: target is null — orbs fire in caster's look direction.
*/
private EntityAbilityOrb[] createOrbEntities(EntityLivingBase caster, EntityLivingBase target, World world) {
EntityAbilityOrb[] entities = new EntityAbilityOrb[projectileCount];
for (int i = 0; i < projectileCount; i++) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
entities[i] = new EntityAbilityOrb(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData);
entities[i].setEffects(this.effects);
entities[i].setSourceAbility(this);
}
if (projectileCount == 2 && entities[0] != null && entities[1] != null) {
entities[0].setSiblingEntityId(entities[1].getEntityId());
entities[1].setSiblingEntityId(entities[0].getEntityId());
}
return entities;
}
private void fireOrbEntity(EntityAbilityOrb orb, EntityLivingBase target) {
if (orb == null || orb.isDead) return;
if (isPreview()) {
orb.startPreviewFiring();
} else {
orb.startMoving(target);
}
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
if (tick == 1) {
orbEntities = createOrbEntities(caster, target, world);
for (int i = 0; i < projectileCount; i++) {
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
if (isPreview()) {
orbEntities[i].setupPreview(caster, orbSize, resolved, lightningData, projectiles[i].anchor, windUpTicks);
} else {
orbEntities[i].setupCharging(projectiles[i].anchor, windUpTicks);
}
spawnAbilityEntity(world, orbEntities[i]);
}
}
}
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
orbEntities = createOrbEntities(caster, target, world);
for (EntityAbilityOrb orb : orbEntities) {
spawnAbilityEntity(world, orb);
}
// Fire first projectile immediately, let onActiveTick handle the rest with fireDelay
fireOrbEntity(orbEntities[0], target);
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireOrbEntity(orbEntities[i], target);
}
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (orbEntities == null) return;
// Fire first projectile immediately
fireOrbEntity(orbEntities[0], target);
// Fire remaining projectiles if no delay
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireOrbEntity(orbEntities[i], target);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (orbEntities == null) {
signalCompletion();
return;
}
// Fire staggered projectiles
if (fireDelay > 0) {
for (int i = 1; i < projectileCount; i++) {
if (tick == fireDelay * i) {
fireOrbEntity(orbEntities[i], target);
}
}
}
// Check if all entities are dead
boolean allDead = true;
for (EntityAbilityOrb orb : orbEntities) {
if (orb != null && !orb.isDead) {
allDead = false;
break;
}
}
if (allDead) {
signalCompletion();
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
@Override
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
if (orbEntities != null) {
for (int i = 0; i < orbEntities.length; i++) {
if (orbEntities[i] != null && !orbEntities[i].isDead) {
orbEntities[i].setDead();
}
orbEntities[i] = null;
}
}
orbEntities = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
// NPC: telegraph on aggro target position
// Player: no telegraph (orbs fire in look direction, no fixed position to mark)
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
return null;
}
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return orbSize * 1.5f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
protected void writeTypeSpecificNBT(NBTTagCompound nbt) {
nbt.setFloat("orbSize", orbSize);
nbt.setInteger("projectileCount", projectileCount);
nbt.setInteger("fireDelay", fireDelay);
// Shared visual data
displayData.writeNBT(nbt);
lightningData.writeNBT(nbt);
// Shared combat/movement data
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
NBTTagCompound projNbt = new NBTTagCompound();
projectiles[i].writeNBT(projNbt);
nbt.setTag("Projectile_" + i, projNbt);
}
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
protected void readTypeSpecificNBT(NBTTagCompound nbt) {
this.orbSize = nbt.getFloat("orbSize");
int count = nbt.getInteger("projectileCount");
initProjectiles(count);
this.fireDelay = nbt.getInteger("fireDelay");
// Shared combat/movement data
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
// Shared visual data
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
}
// ═══════════════════════════════════════════════════════════════════
// API GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
// ==================== TYPE-SPECIFIC GETTERS ====================
public float getOrbSpeed() { return homingData.speed; }
public void setOrbSpeed(float speed) { homingData.speed = speed; }
@@ -398,139 +143,16 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
public float getOrbSize() { return orbSize; }
public void setOrbSize(float size) { this.orbSize = size; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float d) { lifespanData.maxDistance = d; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int t) { lifespanData.maxLifetime = t; }
public float getDamage() { return combatData.damage; }
public void setDamage(float d) { combatData.damage = d; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float k) { combatData.knockback = k; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float k) { combatData.knockbackUp = k; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean h) { homingData.homing = h; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float s) { homingData.homingStrength = s; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float r) { homingData.homingRange = r; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean e) { combatData.explosive = e; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float r) { combatData.explosionRadius = r; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float f) { combatData.explosionDamageFalloff = f; }
// Shared display data (primary colors)
public int getInnerColor() { return displayData.innerColor; }
public void setInnerColor(int c) { displayData.innerColor = c; }
public int getOuterColor() { return displayData.outerColor; }
public void setOuterColor(int c) { displayData.outerColor = c; }
public float getOuterColorWidth() { return displayData.outerColorWidth; }
public void setOuterColorWidth(float w) { displayData.outerColorWidth = w; }
public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
public void setOuterColorAlpha(float a) { displayData.outerColorAlpha = a; }
public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
public void setOuterColorEnabled(boolean e) { displayData.outerColorEnabled = e; }
public float getRotationSpeed() { return displayData.rotationSpeed; }
public void setRotationSpeed(float s) { displayData.rotationSpeed = s; }
// Shared lightning data
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
public void setLightningEffect(boolean e) { lightningData.lightningEffect = e; }
public float getLightningDensity() { return lightningData.lightningDensity; }
public void setLightningDensity(float d) { lightningData.lightningDensity = d; }
public float getLightningRadius() { return lightningData.lightningRadius; }
public void setLightningRadius(float r) { lightningData.lightningRadius = r; }
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
public void setLightningFadeTime(int t) { lightningData.lightningFadeTime = t; }
// Anchor - default to projectile 0
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
public void setAnchorPointEnum(AnchorPoint p) { projectiles[0].anchor.anchorPoint = p; }
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
@Override
public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
// Indexed API methods
public int getInnerColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.innerColor : displayData.innerColor;
}
public void setInnerColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.innerColor = color;
}
public int getOuterColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.outerColor : displayData.outerColor;
}
public void setOuterColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.outerColor = color;
}
public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); }
public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; }
public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; }
public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; }
private int clampIndex(int index) {
return Math.max(0, Math.min(index, projectileCount - 1));
}
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
// ═══════════════════════════════════════════════════════════════════
// GUI FIELD DEFINITIONS
// ═══════════════════════════════════════════════════════════════════
// ==================== TYPE-SPECIFIC GUI ====================
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
// Type tab - projectile count and fire delay
protected void addTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
// Type tab - orb properties
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
@@ -550,64 +172,5 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
// Visual tab - shared primary colors
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"));
// Visual tab - shared effects
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual"));
// Visual tab - per-projectile sections
for (int i = 0; i < MAX_PROJECTILES; i++) {
final int idx = i;
String sectionKey = "ability.section.projectile" + (i + 1);
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
.visibleWhen(() -> idx < projectileCount));
// Anchor point
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX,
v -> projectiles[idx].anchor.anchorOffsetX = v).min(Float.NEGATIVE_INFINITY),
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY,
v -> projectiles[idx].anchor.anchorOffsetY = v).min(Float.NEGATIVE_INFINITY)
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ,
v -> projectiles[idx].anchor.anchorOffsetZ = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount).min(Float.NEGATIVE_INFINITY));
// Color override (only when multiple projectiles)
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
}
}
}
@@ -67,18 +67,18 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
if (caster.worldObj.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (isPlayerCaster(caster)) {
// Player: instant hit-scan in look direction
executePlayerProjectile(caster, world);
executePlayerProjectile(caster, caster.worldObj);
} else if (target != null) {
// NPC: instant hit-scan on aggro target
executeNpcProjectile(caster, target, world);
executeNpcProjectile(caster, target, caster.worldObj);
}
signalCompletion();
@@ -250,7 +250,7 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
// Projectile is instant, nothing to do per-tick
}
@@ -10,7 +10,6 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -30,6 +29,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
private float pushStrength = 1.5f;
private float damage = 8.0f;
private int maxTargets = 10;
private int activeDisplayTicks = 10;
public AbilityShockwave() {
this.typeId = "ability.cnpc.shockwave";
@@ -62,14 +62,14 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
if (!isPreview()) {
// Shockwave is instant - apply effect immediately after windup
// Get all entities in radius
AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
List<EntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
int count = 0;
for (EntityLivingBase entity : entities) {
@@ -112,8 +112,8 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (tick == 10)
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (tick >= activeDisplayTicks)
signalCompletion();
}
@@ -123,6 +123,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
nbt.setFloat("pushStrength", pushStrength);
nbt.setFloat("damage", damage);
nbt.setInteger("maxTargets", maxTargets);
nbt.setInteger("activeDisplayTicks", activeDisplayTicks);
}
@Override
@@ -131,6 +132,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
this.pushStrength = nbt.getFloat("pushStrength");
this.damage = nbt.getFloat("damage");
this.maxTargets = nbt.getInteger("maxTargets");
this.activeDisplayTicks = nbt.hasKey("activeDisplayTicks") ? nbt.getInteger("activeDisplayTicks") : 10;
}
// Getters & Setters
@@ -166,6 +168,14 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
this.maxTargets = maxTargets;
}
public int getActiveDisplayTicks() {
return activeDisplayTicks;
}
public void setActiveDisplayTicks(int activeDisplayTicks) {
this.activeDisplayTicks = activeDisplayTicks;
}
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
@@ -177,6 +187,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
FieldDef.floatField("gui.strength", this::getPushStrength, this::setPushStrength)
),
FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets),
FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
@@ -84,7 +84,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (isPlayerCaster(caster)) {
// Player: update target along look direction during windup
if (targetingMode == TargetingMode.AOE_SELF) {
@@ -115,7 +115,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
hasLaunched = false;
hasLanded = false;
airTicks = 0;
@@ -286,7 +286,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (!hasLaunched) return;
if (hasLanded) return;
@@ -300,14 +300,14 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
// Check for landing: hit the ground or hit a wall mid-flight
if (airTicks > 3) {
if (caster.onGround || caster.isCollidedHorizontally) {
onLanding(caster, world);
onLanding(caster, caster.worldObj);
return;
}
}
// Timeout protection - force landing after max air time
if (airTicks >= maxAirTicks) {
onLanding(caster, world);
onLanding(caster, caster.worldObj);
return;
}
@@ -14,7 +14,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilitySweeper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
import noppes.npcs.api.ability.type.IAbilitySweeper;
import java.util.Arrays;
@@ -77,16 +77,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
// Spawn the sweeper entity that handles BOTH visuals AND damage.
// NPC: target is the aggro target — lockOnTarget tracks it during sweep.
// Player: target is null — sweep rotates around caster's facing direction, lockOnTarget has no effect.
activeEntity = new EntityAbilitySweeper(world, caster, target,
activeEntity = new EntityAbilitySweeper(caster.worldObj, caster, target,
beamLength, beamWidth, beamHeight,
colorData,
sweepSpeed, numberOfRotations,
@@ -97,14 +92,14 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
activeEntity.setupPreview(caster);
}
spawnAbilityEntity(world, activeEntity);
spawnAbilityEntity(activeEntity);
// Ability stays active until entity dies (prevents firing another while projectile is alive)
// Movement locking is handled separately by the base class
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
// Signal completion when entity dies
if (activeEntity == null || activeEntity.isDead) {
activeEntity = null;
@@ -97,13 +97,13 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
currentBlink = 0;
ticksSinceLastBlink = blinkDelayTicks; // Trigger first blink immediately
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1;
if (currentBlink >= blinkLimit) {
signalCompletion();
@@ -113,7 +113,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
ticksSinceLastBlink++;
if (ticksSinceLastBlink >= blinkDelayTicks) {
performBlink(caster, target, world);
performBlink(caster, target, caster.worldObj);
currentBlink++;
ticksSinceLastBlink = 0;
@@ -5,7 +5,7 @@ import kamkeel.npcs.entity.EntityAbilityZone;
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
import noppes.npcs.api.ability.type.IAbilityTrap;
@@ -51,12 +51,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
activeEntities.clear();
// Use pre-calculated positions from telegraph phase, or generate new ones
@@ -78,7 +73,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
for (int i = 0; i < zoneCount; i++) {
double[] pos = positions.get(i);
EntityAbilityZone entity = EntityAbilityZone.createTrap(world, caster,
EntityAbilityZone entity = EntityAbilityZone.createTrap(caster.worldObj, caster,
pos[0], caster.posY, pos[1],
zoneShape,
triggerRadius, armTime, maxTriggers, triggerCooldown,
@@ -94,7 +89,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
entity.setupPreview(caster);
}
spawnAbilityEntity(world, entity);
spawnAbilityEntity(entity);
activeEntities.add(entity);
}
}
@@ -79,7 +79,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
getPulledEntities().clear();
pullComplete = false;
ticksSincePullDamage = 0;
@@ -88,7 +88,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
if (aoe) {
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
List<EntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
int count = 0;
for (EntityLivingBase entity : entities) {
@@ -116,7 +116,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (isPreview()) {
// No entities to pull in preview, just run animation for a duration
if (tick >= 60) signalCompletion();
@@ -136,7 +136,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
ticksSincePullDamage++;
for (UUID uuid : new HashSet<>(getPulledEntities())) {
EntityLivingBase entity = findEntity(caster, world, uuid);
EntityLivingBase entity = findEntity(caster, caster.worldObj, uuid);
if (entity == null || entity.isDead) {
getPulledEntities().remove(uuid);
continue;
@@ -149,7 +149,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
if (dist <= 1.5f) {
getPulledEntities().remove(uuid);
onTargetArrived(caster, entity, world);
onTargetArrived(caster, entity, caster.worldObj);
continue;
}
@@ -164,7 +164,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
double nextZ = dz * factor;
AxisAlignedBB nextBox = entity.boundingBox.copy().offset(nextX, nextY, nextZ);
if (!world.getCollidingBoundingBoxes(entity, nextBox).isEmpty()) {
if (!caster.worldObj.getCollidingBoundingBoxes(entity, nextBox).isEmpty()) {
getPulledEntities().remove(uuid);
continue;
}
@@ -17,7 +17,7 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.constants.EnumPotionType;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.client.gui.SubGuiZonePresetSelector;
import noppes.npcs.client.gui.builder.FieldDef;
@@ -151,7 +151,7 @@ public abstract class AbilityZone extends Ability {
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
// Remove dead entities
Iterator<EntityAbilityZone> it = activeEntities.iterator();
while (it.hasNext()) {
@@ -0,0 +1,590 @@
package kamkeel.npcs.controllers.data.ability.type;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityProjectile;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.util.Vec3;
import noppes.npcs.api.ability.type.IAbilityEnergyProjectile;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.List;
/**
* Abstract base for energy projectile abilities (Orb, Disc, Beam, LaserShot).
* Handles shared lifecycle (windup → execute → active → completion), multi-projectile
* support with staggered firing, telegraph creation, NBT persistence, and GUI definitions.
*
* Subclasses provide entity creation/firing/charging logic and type-specific fields.
*
* @param <E> The entity type this ability spawns
*/
public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityProjectile>
extends Ability implements IAbilityEnergyProjectile {
protected static final int MAX_PROJECTILES = 8;
// Multi-projectile support
protected int projectileCount = 1;
protected int fireDelay = 0;
// Shared visual data
protected EnergyDisplayData displayData;
protected EnergyLightningData lightningData;
protected EnergyTrajectoryData trajectoryData;
// Shared combat/movement data
protected final EnergyCombatData combatData;
protected final EnergyHomingData homingData;
protected final EnergyLifespanData lifespanData;
// Per-projectile data (anchor + optional color override)
protected ProjectileData[] projectiles;
// Transient entity state
protected transient E[] entities;
// ==================== CONSTRUCTOR ====================
protected AbstractEnergyProjectileAbility(
EnergyDisplayData displayData,
EnergyCombatData combatData,
EnergyHomingData homingData,
EnergyLifespanData lifespanData
) {
this.displayData = displayData;
this.combatData = combatData;
this.homingData = homingData;
this.lifespanData = lifespanData;
this.lightningData = new EnergyLightningData();
this.trajectoryData = new EnergyTrajectoryData();
initProjectiles(1);
}
// ==================== ABSTRACT METHODS ====================
/**
* Create a single entity for the given projectile index.
* Called during windup (tick 1) and burst refire (onExecute when entities are null).
*/
protected abstract E createEntity(EntityLivingBase caster, EntityLivingBase target,
Vec3 spawnPos, EnergyDisplayData resolved, int index);
/**
* Fire an entity (start moving toward target). Entity is guaranteed non-null and alive.
*/
protected abstract void fireEntity(E entity, EntityLivingBase target);
/**
* Setup entity for charging animation during windup (non-preview).
*/
protected abstract void setupEntityCharging(E entity, ProjectileData projData, int index);
/**
* Setup entity for preview mode during windup.
*/
protected abstract void setupEntityPreview(E entity, EntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index);
/**
* Create typed array (Java generics can't create generic arrays).
*/
protected abstract E[] createEntityArray(int size);
/**
* Get the telegraph radius for this projectile type.
*/
protected abstract float getProjectileTelegraphRadius();
/**
* Add type-specific GUI field definitions (Type tab).
* Called before shared visual tab definitions.
*/
@SideOnly(Side.CLIENT)
protected abstract void addTypeDefinitions(List<FieldDef> defs);
/**
* Write type-specific fields to NBT (e.g., orbSize, discRadius).
* Shared data classes are written by the base class.
*/
protected abstract void writeTypeSpecificNBT(NBTTagCompound nbt);
/**
* Read type-specific fields from NBT.
* Shared data classes are read by the base class.
*/
protected abstract void readTypeSpecificNBT(NBTTagCompound nbt);
// ==================== OVERRIDABLE HOOKS ====================
/**
* Get the default anchor point for a projectile index.
* Override for types with different anchor defaults.
*/
protected AnchorPoint getDefaultAnchor(int index) {
switch (index) {
case 0: return AnchorPoint.RIGHT_HAND;
case 1: return AnchorPoint.LEFT_HAND;
default: return AnchorPoint.FRONT;
}
}
/**
* Get the spawn position for a projectile. Override for types that need
* an offset distance (e.g., Beam uses offsetDist=1.0).
*/
protected Vec3 getSpawnPosition(EntityLivingBase caster, int index) {
return AnchorPointHelper.calculateAnchorPosition(caster, projectiles[index].anchor);
}
// ==================== PROJECTILE MANAGEMENT ====================
protected void initProjectiles(int count) {
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
this.projectiles = new ProjectileData[projectileCount];
for (int i = 0; i < projectileCount; i++) {
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
}
}
public void setProjectileCount(int count) {
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
if (newCount == projectileCount) return;
ProjectileData[] oldProjectiles = projectiles;
initProjectiles(newCount);
if (oldProjectiles != null) {
int copyCount = Math.min(oldProjectiles.length, newCount);
for (int i = 0; i < copyCount; i++) {
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
}
}
}
public int getProjectileCount() { return projectileCount; }
public int getFireDelay() { return fireDelay; }
public void setFireDelay(int delay) { this.fireDelay = Math.max(0, delay); }
/** @deprecated Use {@link #getFireDelay()} */
public int getDualFireDelay() { return fireDelay; }
/** @deprecated Use {@link #setFireDelay(int)} */
public void setDualFireDelay(int delay) { setFireDelay(delay); }
protected int clampIndex(int index) {
return Math.max(0, Math.min(index, projectileCount - 1));
}
// ==================== ENTITY CREATION ====================
/**
* Create all entities for this ability.
* Sets effects, source ability, and sibling links.
*/
protected E[] createAllEntities(EntityLivingBase caster, EntityLivingBase target) {
E[] newEntities = createEntityArray(projectileCount);
for (int i = 0; i < projectileCount; i++) {
Vec3 spawnPos = getSpawnPosition(caster, i);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
newEntities[i] = createEntity(caster, target, spawnPos, resolved, i);
newEntities[i].setEffects(this.effects);
newEntities[i].setSourceAbility(this);
}
return newEntities;
}
/**
* Fire an entity if it's valid.
*/
protected void fireEntitySafe(E entity, EntityLivingBase target) {
if (entity != null && !entity.isDead) {
fireEntity(entity, target);
}
}
// ==================== LIFECYCLE ====================
@Override
public boolean allowOverlap() {
return true;
}
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (caster.worldObj.isRemote && !isPreview()) return;
if (tick == 1) {
entities = createAllEntities(caster, target);
for (int i = 0; i < projectileCount; i++) {
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
if (isPreview()) {
setupEntityPreview(entities[i], caster, resolved, projectiles[i], i);
} else {
setupEntityCharging(entities[i], projectiles[i], i);
}
spawnAbilityEntity(entities[i]);
}
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
// Burst refire: entities not created during windup (it was skipped)
if (entities == null) {
entities = createAllEntities(caster, target);
for (E entity : entities) {
spawnAbilityEntity(entity);
}
}
// Fire first projectile immediately
fireEntitySafe(entities[0], target);
// Fire remaining projectiles if no delay
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireEntitySafe(entities[i], target);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (entities == null) {
signalCompletion();
return;
}
// Fire staggered projectiles
if (fireDelay > 0) {
for (int i = 1; i < projectileCount; i++) {
if (tick == fireDelay * i) {
fireEntitySafe(entities[i], target);
}
}
}
// Check if all entities are dead
boolean allDead = true;
for (E entity : entities) {
if (entity != null && !entity.isDead) {
allDead = false;
break;
}
}
if (allDead) {
signalCompletion();
}
}
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
if (entities != null) {
for (int i = 0; i < entities.length; i++) {
if (entities[i] != null && !entities[i].isDead) {
entities[i].setDead();
}
entities[i] = null;
}
}
entities = null;
}
// ==================== TELEGRAPH ====================
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
return null;
}
Telegraph telegraph = Telegraph.circle(getProjectileTelegraphRadius());
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return getProjectileTelegraphRadius();
}
// ==================== NBT ====================
@Override
public final void writeTypeNBT(NBTTagCompound nbt) {
// Type-specific fields
writeTypeSpecificNBT(nbt);
// Shared fields
nbt.setInteger("projectileCount", projectileCount);
nbt.setInteger("fireDelay", fireDelay);
// Shared data classes
displayData.writeNBT(nbt);
lightningData.writeNBT(nbt);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
NBTTagCompound projNbt = new NBTTagCompound();
projectiles[i].writeNBT(projNbt);
nbt.setTag("Projectile_" + i, projNbt);
}
}
@Override
public final void readTypeNBT(NBTTagCompound nbt) {
// Type-specific fields
readTypeSpecificNBT(nbt);
// Shared fields
int count = nbt.getInteger("projectileCount");
initProjectiles(count);
this.fireDelay = nbt.getInteger("fireDelay");
// Shared data classes
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
}
// ==================== PREVIEW ====================
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
// ==================== API GETTERS & SETTERS ====================
// Combat data
@Override public float getDamage() { return combatData.damage; }
@Override public void setDamage(float damage) { combatData.damage = damage; }
@Override public float getKnockback() { return combatData.knockback; }
@Override public void setKnockback(float knockback) { combatData.knockback = knockback; }
@Override public float getKnockbackUp() { return combatData.knockbackUp; }
@Override public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; }
@Override public boolean isExplosive() { return combatData.explosive; }
@Override public void setExplosive(boolean explosive) { combatData.explosive = explosive; }
@Override public float getExplosionRadius() { return combatData.explosionRadius; }
@Override public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; }
@Override public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
@Override public void setExplosionDamageFalloff(float falloff) { combatData.explosionDamageFalloff = falloff; }
// Lifespan data
@Override public float getMaxDistance() { return lifespanData.maxDistance; }
@Override public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; }
@Override public int getMaxLifetime() { return lifespanData.maxLifetime; }
@Override public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; }
// Homing data
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean homing) { homingData.homing = homing; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float strength) { homingData.homingStrength = strength; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float range) { homingData.homingRange = range; }
// Display data - primary colors
@Override public int getInnerColor() { return displayData.innerColor; }
@Override public void setInnerColor(int color) { displayData.innerColor = color; }
@Override public int getOuterColor() { return displayData.outerColor; }
@Override public void setOuterColor(int color) { displayData.outerColor = color; }
@Override public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
@Override public void setOuterColorEnabled(boolean enabled) { displayData.outerColorEnabled = enabled; }
@Override public float getOuterColorWidth() { return displayData.outerColorWidth; }
@Override public void setOuterColorWidth(float width) { displayData.outerColorWidth = width; }
@Override public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
@Override public void setOuterColorAlpha(float alpha) { displayData.outerColorAlpha = alpha; }
public float getRotationSpeed() { return displayData.rotationSpeed; }
public void setRotationSpeed(float speed) { displayData.rotationSpeed = speed; }
// Lightning data
@Override public boolean hasLightningEffect() { return lightningData.lightningEffect; }
@Override public void setLightningEffect(boolean enabled) { lightningData.lightningEffect = enabled; }
@Override public float getLightningDensity() { return lightningData.lightningDensity; }
@Override public void setLightningDensity(float density) { lightningData.lightningDensity = density; }
@Override public float getLightningRadius() { return lightningData.lightningRadius; }
@Override public void setLightningRadius(float radius) { lightningData.lightningRadius = radius; }
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
public void setLightningFadeTime(int fadeTime) { lightningData.lightningFadeTime = fadeTime; }
// Anchor - default to projectile 0
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
public void setAnchorPointEnum(AnchorPoint point) { projectiles[0].anchor.anchorPoint = point; }
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
@Override public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
@Override public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
// Indexed API methods
public int getInnerColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.innerColor : displayData.innerColor;
}
public void setInnerColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.innerColor = color;
}
public int getOuterColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.outerColor : displayData.outerColor;
}
public void setOuterColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.outerColor = color;
}
public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); }
public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; }
public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; }
public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; }
// ==================== GUI FIELD DEFINITIONS ====================
@SideOnly(Side.CLIENT)
@Override
public final void getAbilityDefinitions(List<FieldDef> defs) {
// Type-specific fields first
addTypeDefinitions(defs);
// Shared visual tab - primary colors
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"));
// Shared visual tab - effects
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual"));
// Shared visual tab - per-projectile sections
for (int i = 0; i < MAX_PROJECTILES; i++) {
final int idx = i;
String sectionKey = "ability.section.projectile" + (i + 1);
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
.visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v),
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v)
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
// Color override (only when multiple projectiles)
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
}
}
}
@@ -45,10 +45,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
// Whether to render tail orb (only when anchored)
private boolean renderTailOrb = true;
// Charging state (during windup)
private boolean charging = false;
private int chargeDuration = 40;
private int chargeTick = 0;
// Charging state (beam-specific)
private float chargeOffsetDistance = 1.0f;
// Trail fading for non-anchored beams (comet effect)
@@ -56,9 +53,6 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
private int trailFadeTime = 20; // Ticks for trail to fully fade
private List<Integer> trailPointAges = new ArrayList<>();
// Data watcher index for charging state (synced to clients)
private static final int DW_CHARGING = 20;
// Debug logging
private static final boolean DEBUG_LOGGING = false;
@@ -66,34 +60,6 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
super(world);
}
@Override
protected void entityInit() {
super.entityInit();
// Register data watcher for charging state
this.dataWatcher.addObject(DW_CHARGING, (byte) 0);
}
/**
* Check if beam is in charging state (synced via data watcher).
* In preview mode, uses local field since data watcher isn't synced.
*/
public boolean isCharging() {
if (previewMode) {
return this.charging;
}
return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1;
}
/**
* Set charging state (server only, synced to clients via data watcher).
*/
private void setCharging(boolean value) {
this.charging = value;
if (!worldObj.isRemote) {
this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0));
}
}
/**
* Full constructor with all parameters using data classes.
* @param anchoredMode If true, origin follows owner and tail orb is rendered.
@@ -137,23 +103,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
}
// Calculate initial velocity toward target
if (target != null) {
double dx = target.posX - x;
double dy = (target.posY + target.getEyeHeight()) - y;
double dz = target.posZ - z;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
this.motionX = (dx / len) * getSpeed();
this.motionY = (dy / len) * getSpeed();
this.motionZ = (dz / len) * getSpeed();
}
} else {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
this.motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
this.motionY = -Math.sin(pitch) * getSpeed();
this.motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
}
calculateInitialVelocity(owner, target, x, y, z);
}
public void setupCharging(EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) {
@@ -218,8 +168,6 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
* Start preview firing (simulates firing toward a point in front of NPC).
*/
public void startPreviewFiring() {
if (!isCharging()) return;
setCharging(false);
// Origin (tail) stays at the charged position
@@ -270,8 +218,6 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
* For non-anchored beams: origin fixed at charged position, head starts there
*/
public void startFiring(EntityLivingBase target) {
if (!isCharging()) return;
setCharging(false);
// For player casters, snap to look vector for crosshair-aligned launch
@@ -433,8 +379,10 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
/**
* Update during charging state - follow owner based on anchor point.
* Beam-specific: positions based on chargeOffsetDistance and updates startX/Y/Z.
*/
private void updateCharging() {
@Override
protected void updateCharging() {
chargeTick++;
Entity owner = getOwnerEntity();
@@ -548,7 +496,8 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
addTrailPoint();
}
private void updateHoming() {
@Override
protected void updateHoming() {
if (!isHoming()) return;
Entity target = getTargetEntity();
@@ -704,24 +653,6 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
return attachedToOwner;
}
/**
* Get the charge progress (0-1).
*/
public float getChargeProgress() {
if (chargeDuration <= 0) return 1.0f;
return Math.min(1.0f, (float) chargeTick / chargeDuration);
}
/**
* Get interpolated charge progress for smooth rendering.
*/
public float getInterpolatedChargeProgress(float partialTicks) {
if (chargeDuration <= 0) return 1.0f;
float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration);
float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration);
return prevProgress + (currProgress - prevProgress) * partialTicks;
}
/**
* Whether trail should fade (comet effect for non-anchored beams).
*/
@@ -757,12 +688,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
this.prevHeadOffsetZ = this.headOffsetZ;
this.attachedToOwner = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner");
this.renderTailOrb = !nbt.hasKey("RenderTailOrb") || nbt.getBoolean("RenderTailOrb");
// Read charging state and sync to data watcher
boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
this.charging = isCharging;
this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0));
this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40;
this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0;
readChargingNBT(nbt);
this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f;
this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail");
this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20;
@@ -792,15 +718,11 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
nbt.setDouble("HeadOffsetZ", headOffsetZ);
nbt.setBoolean("AttachedToOwner", attachedToOwner);
nbt.setBoolean("RenderTailOrb", renderTailOrb);
nbt.setBoolean("Charging", isCharging());
nbt.setInteger("ChargeDuration", chargeDuration);
nbt.setInteger("ChargeTick", chargeTick);
writeChargingNBT(nbt);
nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance);
nbt.setBoolean("FadeTrail", fadeTrail);
nbt.setInteger("TrailFadeTime", trailFadeTime);
this.anchorData.writeNBT(nbt);
// Write trail points
NBTTagList trailList = new NBTTagList();
for (Vec3 point : trailPoints) {
@@ -33,48 +33,14 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
private float discThickness = 0.2f; // Height of disc
private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward)
// Charging state (during windup)
private boolean charging = false;
private int chargeDuration = 40;
private int chargeTick = 0;
// Charging state (disc-specific target sizes)
private float targetDiscRadius = 1.0f; // Full radius to grow to during charging
private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging
// Data watcher index for charging state (synced to clients)
private static final int DW_CHARGING = 20;
public EntityAbilityDisc(World world) {
super(world);
}
@Override
protected void entityInit() {
super.entityInit();
// Register data watcher for charging state
this.dataWatcher.addObject(DW_CHARGING, (byte) 0);
}
/**
* Check if disc is in charging state (synced via data watcher).
* In preview mode, uses local field since data watcher isn't synced.
*/
public boolean isCharging() {
if (previewMode) {
return this.charging;
}
return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1;
}
/**
* Set charging state (server only, synced to clients via data watcher).
*/
private void setCharging(boolean value) {
this.charging = value;
if (!worldObj.isRemote) {
this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0));
}
}
/**
* Full constructor with all parameters using data classes.
*/
@@ -99,23 +65,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
this.discThickness = discThickness;
// Calculate initial velocity toward target
if (target != null) {
double dx = target.posX - x;
double dy = (target.posY + target.getEyeHeight()) - y;
double dz = target.posZ - z;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
this.motionX = (dx / len) * getSpeed();
this.motionY = (dy / len) * getSpeed();
this.motionZ = (dz / len) * getSpeed();
}
} else {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
this.motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
this.motionY = -Math.sin(pitch) * getSpeed();
this.motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
}
calculateInitialVelocity(owner, target, x, y, z);
}
/**
@@ -124,21 +74,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
* Position follows the owner based on anchor point.
*/
public void setupCharging(EnergyAnchorData anchor, int chargeDuration, boolean vertical) {
setCharging(true);
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.anchorData = anchor;
this.vertical = vertical;
this.targetDiscRadius = this.discRadius;
this.targetDiscThickness = this.discThickness;
super.setupCharging(anchor, chargeDuration);
this.vertical = vertical;
this.discRadius = 0.01f;
this.discThickness = 0.01f;
this.size = 0.01f;
this.renderCurrentSize = 0.01f;
this.prevRenderSize = 0.01f;
this.motionX = 0;
this.motionY = 0;
this.motionZ = 0;
}
/**
@@ -188,8 +129,6 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
* Start preview firing (simulates firing toward a point in front of NPC).
*/
public void startPreviewFiring() {
if (!isCharging()) return;
setCharging(false);
// Update start position to current position
@@ -222,8 +161,6 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
* Called by ability when windup ends.
*/
public void startMoving(EntityLivingBase target) {
if (!isCharging()) return;
setCharging(false);
// For player casters, snap to look vector for crosshair-aligned launch
@@ -359,43 +296,6 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
this.posZ += motionZ;
}
private void updateHoming() {
if (!isHoming()) return;
Entity target = getTargetEntity();
if (target == null || !target.isEntityAlive()) return;
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
double dz = target.posZ - posZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist <= getHomingRange() && dist > 0) {
// Calculate effective homing strength - increases when closer to commit to target
float effectiveStrength = getHomingStrength();
if (dist < getHomingRange() * 0.3) {
effectiveStrength = Math.min(1.0f, getHomingStrength() * 2.5f);
} else if (dist < getHomingRange() * 0.6) {
effectiveStrength = Math.min(0.8f, getHomingStrength() * 1.5f);
}
double desiredVX = (dx / dist) * getSpeed();
double desiredVY = (dy / dist) * getSpeed();
double desiredVZ = (dz / dist) * getSpeed();
motionX += (desiredVX - motionX) * effectiveStrength;
motionY += (desiredVY - motionY) * effectiveStrength;
motionZ += (desiredVZ - motionZ) * effectiveStrength;
double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ);
if (vLen > 0) {
motionX = (motionX / vLen) * getSpeed();
motionY = (motionY / vLen) * getSpeed();
motionZ = (motionZ / vLen) * getSpeed();
}
}
}
private void updateReturnToOwner() {
Entity owner = getOwnerEntity();
@@ -485,8 +385,10 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
/**
* Update during charging state - follow owner based on anchor point.
* Overrides base to grow disc-specific radius/thickness instead of just size.
*/
private void updateCharging() {
@Override
protected void updateCharging() {
chargeTick++;
Entity owner = getOwnerEntity();
@@ -511,24 +413,6 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
updateRotation();
}
/**
* Get the charge progress (0-1).
*/
public float getChargeProgress() {
if (chargeDuration <= 0) return 1.0f;
return Math.min(1.0f, (float) chargeTick / chargeDuration);
}
/**
* Get interpolated charge progress for smooth rendering.
*/
public float getInterpolatedChargeProgress(float partialTicks) {
if (chargeDuration <= 0) return 1.0f;
float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration);
float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration);
return prevProgress + (currProgress - prevProgress) * partialTicks;
}
// ==================== GETTERS ====================
public float getDiscRadius() {
@@ -579,12 +463,8 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f;
this.vertical = nbt.hasKey("Vertical") ? nbt.getBoolean("Vertical") : false;
this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning");
// Charging state
boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
this.charging = isCharging;
this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0));
this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40;
this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0;
// Charging state (common fields handled by base)
readChargingNBT(nbt);
this.targetDiscRadius = nbt.hasKey("TargetDiscRadius") ? nbt.getFloat("TargetDiscRadius") : this.discRadius;
this.targetDiscThickness = nbt.hasKey("TargetDiscThickness") ? nbt.getFloat("TargetDiscThickness") : this.discThickness;
}
@@ -597,10 +477,8 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
nbt.setFloat("DiscThickness", discThickness);
nbt.setBoolean("Vertical", vertical);
nbt.setBoolean("Returning", returning);
// Charging state
nbt.setBoolean("Charging", isCharging());
nbt.setInteger("ChargeDuration", chargeDuration);
nbt.setInteger("ChargeTick", chargeTick);
// Charging state (common fields handled by base)
writeChargingNBT(nbt);
nbt.setFloat("TargetDiscRadius", targetDiscRadius);
nbt.setFloat("TargetDiscThickness", targetDiscThickness);
}
@@ -44,47 +44,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
// End point for rendering
private double endX, endY, endZ;
// Charging state (during windup)
private boolean charging = false;
private int chargeDuration = 40;
private int chargeTick = 0;
private float targetSize = 1.0f; // Full size to grow to during charging
// Data watcher index for charging state (synced to clients)
private static final int DW_CHARGING = 20;
public EntityAbilityLaser(World world) {
super(world);
}
@Override
protected void entityInit() {
super.entityInit();
// Register data watcher for charging state
this.dataWatcher.addObject(DW_CHARGING, (byte) 0);
}
/**
* Check if orb is in charging state (synced via data watcher).
* In preview mode, uses local field since data watcher isn't synced.
*/
public boolean isCharging() {
if (previewMode) {
return this.charging;
}
return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1;
}
/**
* Set charging state (server only, synced to clients via data watcher).
*/
private void setCharging(boolean value) {
this.charging = value;
if (!worldObj.isRemote) {
this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0));
}
}
/**
* Full constructor with all parameters using data classes.
*/
@@ -131,25 +94,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
this.endZ = z;
}
/**
* Setup this orb in charging mode (for windup phase).
* The orb will grow from 0 to orbSize over chargeDuration ticks.
* Position follows the owner based on anchor point.
*/
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
setCharging(true);
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.anchorData = anchor;
this.targetSize = this.size;
this.size = 0.01f;
this.renderCurrentSize = 0.01f;
this.prevRenderSize = 0.01f;
this.motionX = 0;
this.motionY = 0;
this.motionZ = 0;
}
@Override
protected void updateRotation() {
// Laser doesn't rotate
@@ -252,8 +196,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
}
public void startMoving(EntityLivingBase target) {
if (!isCharging()) return;
setCharging(false);
// Initialize from current anchor position
@@ -322,44 +264,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
}
}
/**
* Update during charging state - follow owner based on anchor point.
*/
private void updateCharging() {
chargeTick++;
Entity owner = getOwnerEntity();
if (owner == null || owner.isDead) {
setDead();
return;
}
// Grow size based on charge progress
float progress = getChargeProgress();
this.size = targetSize * progress;
// Calculate position based on anchor point, offset downward by half size to center
if (owner instanceof EntityLivingBase) {
Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData);
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
}
}
/**
* Get the charge progress (0-1).
*/
public float getChargeProgress() {
if (chargeDuration <= 0) return 1.0f;
return Math.min(1.0f, (float) chargeTick / chargeDuration);
}
public float getInterpolatedChargeProgress(float partialTicks) {
if (chargeDuration <= 0) return 1.0f;
float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration);
float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration);
return prevProgress + (currProgress - prevProgress) * partialTicks;
}
private void checkEntityCollisionAlongLine() {
// Expand search AABB by entity sizes to ensure we find all potential targets
double expand = Math.max(laserWidth, 1.0);
@@ -520,13 +424,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
this.endY = nbt.getDouble("EndY");
this.endZ = nbt.getDouble("EndZ");
// Charging state
boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
this.charging = isCharging;
this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0));
this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40;
this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0;
this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size;
readChargingNBT(nbt);
}
@Override
@@ -543,11 +441,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
nbt.setDouble("EndY", endY);
nbt.setDouble("EndZ", endZ);
// Charging state
nbt.setBoolean("Charging", isCharging());
nbt.setInteger("ChargeDuration", chargeDuration);
nbt.setInteger("ChargeTick", chargeTick);
nbt.setFloat("TargetSize", targetSize);
writeChargingNBT(nbt);
}
/**
@@ -20,47 +20,10 @@ import java.util.List;
*/
public class EntityAbilityOrb extends EntityAbilityProjectile {
// Charging state (during windup)
private boolean charging = false;
private int chargeDuration = 40;
private int chargeTick = 0;
private float targetSize = 1.0f; // Full size to grow to during charging
// Data watcher index for charging state (synced to clients)
private static final int DW_CHARGING = 20;
public EntityAbilityOrb(World world) {
super(world);
}
@Override
protected void entityInit() {
super.entityInit();
// Register data watcher for charging state
this.dataWatcher.addObject(DW_CHARGING, (byte) 0);
}
/**
* Check if orb is in charging state (synced via data watcher).
* In preview mode, uses local field since data watcher isn't synced.
*/
public boolean isCharging() {
if (previewMode) {
return this.charging;
}
return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1;
}
/**
* Set charging state (server only, synced to clients via data watcher).
*/
private void setCharging(boolean value) {
this.charging = value;
if (!worldObj.isRemote) {
this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0));
}
}
/**
* Full constructor using data classes for grouped parameters.
*/
@@ -77,43 +40,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile {
this.homingData = homing;
// Calculate initial velocity toward target
if (target != null) {
double dx = target.posX - x;
double dy = (target.posY + target.getEyeHeight()) - y;
double dz = target.posZ - z;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
this.motionX = (dx / len) * homing.speed;
this.motionY = (dy / len) * homing.speed;
this.motionZ = (dz / len) * homing.speed;
}
} else {
// Fire in NPC's facing direction
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
this.motionX = -Math.sin(yaw) * Math.cos(pitch) * homing.speed;
this.motionY = -Math.sin(pitch) * homing.speed;
this.motionZ = Math.cos(yaw) * Math.cos(pitch) * homing.speed;
}
}
/**
* Setup this orb in charging mode (for windup phase).
* The orb will grow from 0 to orbSize over chargeDuration ticks.
* Position follows the owner based on anchor point.
*/
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
setCharging(true);
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.anchorData = anchor;
this.targetSize = this.size;
this.size = 0.01f;
this.renderCurrentSize = 0.01f;
this.prevRenderSize = 0.01f;
this.motionX = 0;
this.motionY = 0;
this.motionZ = 0;
calculateInitialVelocity(owner, target, x, y, z);
}
/**
@@ -161,8 +88,6 @@ public class EntityAbilityOrb extends EntityAbilityProjectile {
* Start preview firing (simulates firing toward a point in front of NPC).
*/
public void startPreviewFiring() {
if (!isCharging()) return;
setCharging(false);
// Update start position to current position
@@ -196,8 +121,6 @@ public class EntityAbilityOrb extends EntityAbilityProjectile {
* Called by ability when windup ends.
*/
public void startMoving(EntityLivingBase target) {
if (!isCharging()) return;
setCharging(false);
// For player casters, snap to look vector for crosshair-aligned launch
@@ -302,47 +225,6 @@ public class EntityAbilityOrb extends EntityAbilityProjectile {
return true;
}
private void updateHoming() {
if (!isHoming()) return;
Entity target = getTargetEntity();
if (target == null || !target.isEntityAlive()) return;
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
double dz = target.posZ - posZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist <= getHomingRange() && dist > 0) {
// Calculate effective homing strength - increases when closer to commit to target
// This prevents orbiting behavior at close range
float effectiveStrength = getHomingStrength();
if (dist < getHomingRange() * 0.3) {
// Within 30% of homing range, dramatically increase strength to commit
effectiveStrength = Math.min(1.0f, getHomingStrength() * 2.5f);
} else if (dist < getHomingRange() * 0.6) {
// Within 60% of homing range, moderately increase strength
effectiveStrength = Math.min(0.8f, getHomingStrength() * 1.5f);
}
double desiredVX = (dx / dist) * getSpeed();
double desiredVY = (dy / dist) * getSpeed();
double desiredVZ = (dz / dist) * getSpeed();
motionX += (desiredVX - motionX) * effectiveStrength;
motionY += (desiredVY - motionY) * effectiveStrength;
motionZ += (desiredVZ - motionZ) * effectiveStrength;
// Normalize to maintain speed
double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ);
if (vLen > 0) {
motionX = (motionX / vLen) * getSpeed();
motionY = (motionY / vLen) * getSpeed();
motionZ = (motionZ / vLen) * getSpeed();
}
}
}
private void checkBlockCollision() {
Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ);
Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ);
@@ -387,47 +269,6 @@ public class EntityAbilityOrb extends EntityAbilityProjectile {
}
}
/**
* Update during charging state - follow owner based on anchor point.
*/
private void updateCharging() {
chargeTick++;
Entity owner = getOwnerEntity();
if (owner == null || owner.isDead) {
setDead();
return;
}
// Grow size based on charge progress
float progress = getChargeProgress();
this.size = targetSize * progress;
// Calculate position based on anchor point, offset downward by half size to center
if (owner instanceof EntityLivingBase) {
Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData);
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
}
}
/**
* Get the charge progress (0-1).
*/
public float getChargeProgress() {
if (chargeDuration <= 0) return 1.0f;
return Math.min(1.0f, (float) chargeTick / chargeDuration);
}
/**
* Get interpolated charge progress for smooth rendering.
*/
public float getInterpolatedChargeProgress(float partialTicks) {
if (chargeDuration <= 0) return 1.0f;
float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration);
float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration);
return prevProgress + (currProgress - prevProgress) * partialTicks;
}
// ==================== GETTERS ====================
// Legacy getter for renderer compatibility
@@ -439,22 +280,11 @@ public class EntityAbilityOrb extends EntityAbilityProjectile {
@Override
protected void readProjectileNBT(NBTTagCompound nbt) {
// Charging state
boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
this.charging = isCharging;
this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0));
this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40;
this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0;
this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size;
readChargingNBT(nbt);
}
@Override
protected void writeProjectileNBT(NBTTagCompound nbt) {
// Charging state
nbt.setBoolean("Charging", isCharging());
nbt.setInteger("ChargeDuration", chargeDuration);
nbt.setInteger("ChargeTick", chargeTick);
anchorData.writeNBT(nbt);
nbt.setFloat("TargetSize", targetSize);
writeChargingNBT(nbt);
}
}
@@ -10,6 +10,7 @@ import kamkeel.npcs.controllers.data.ability.AbilityEffect;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.IAbilityDamageHandler;
import kamkeel.npcs.controllers.data.ability.data.*;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
@@ -74,7 +75,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
protected double startX, startY, startZ;
protected int ownerEntityId = -1;
protected int targetEntityId = -1;
protected int siblingEntityId = -1;
// ==================== STATE ====================
protected boolean hasHit = false;
@@ -84,6 +84,13 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
/** The ability that spawned this projectile. Transient, not saved to NBT. */
protected transient Ability sourceAbility = null;
// ==================== CHARGING STATE ====================
protected boolean charging = false;
protected int chargeDuration = 40;
protected int chargeTick = 0;
protected float targetSize = 1.0f;
protected static final int DW_CHARGING = 20;
// ==================== ROTATION INTERPOLATION ====================
public float prevRotationValX, prevRotationValY, prevRotationValZ;
public float rotationValX, rotationValY, rotationValZ;
@@ -108,7 +115,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
/**
* Initialize common properties using data classes. Call from subclass constructors.
*/
protected void initProjectile(EntityLivingBase owner, EntityLivingBase target, EntityAbilityProjectile sibling,
protected void initProjectile(EntityLivingBase owner, EntityLivingBase target,
double x, double y, double z, float size,
EnergyDisplayData display, EnergyCombatData combat,
EnergyLightningData lightning, EnergyLifespanData lifespan,
@@ -120,7 +127,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
this.ownerEntityId = owner.getEntityId();
this.targetEntityId = target != null ? target.getEntityId() : -1;
this.siblingEntityId = sibling != null ? sibling.getEntityId() : -1;
// Visual
this.size = size;
@@ -136,18 +142,9 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
this.prevRenderSize = size;
}
// Initialize projectile without a sibling
protected void initProjectile(EntityLivingBase owner, EntityLivingBase target,
double x, double y, double z, float size,
EnergyDisplayData display, EnergyCombatData combat,
EnergyLightningData lightning, EnergyLifespanData lifespan,
EnergyTrajectoryData trajectory) {
initProjectile(owner, target, null, x, y, z, size, display, combat, lightning, lifespan, trajectory);
}
@Override
protected void entityInit() {
// No DataWatcher needed - using IEntityAdditionalSpawnData with NBT
this.dataWatcher.addObject(DW_CHARGING, (byte) 0);
}
@Override
@@ -455,6 +452,170 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
prevPosZ = newZ;
}
// ==================== CHARGING METHODS ====================
/**
* Check if entity is in charging state (synced via data watcher).
* In preview mode, uses local field since data watcher isn't synced.
*/
public boolean isCharging() {
if (previewMode) return this.charging;
return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1;
}
/**
* Set charging state (server only, synced to clients via data watcher).
*/
protected void setCharging(boolean value) {
this.charging = value;
if (!worldObj.isRemote) {
this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0));
}
}
public float getChargeProgress() {
if (chargeDuration <= 0) return 1.0f;
return Math.min(1.0f, (float) chargeTick / chargeDuration);
}
public float getInterpolatedChargeProgress(float partialTicks) {
if (chargeDuration <= 0) return 1.0f;
float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration);
float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration);
return prevProgress + (currProgress - prevProgress) * partialTicks;
}
/**
* Setup this entity in charging mode (for windup phase).
* Grows from 0 to current size over chargeDuration ticks.
* Subclasses with different charging behavior should override or use their own method.
*/
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
setCharging(true);
this.chargeDuration = chargeDuration;
this.chargeTick = 0;
this.anchorData = anchor;
this.targetSize = this.size;
this.size = 0.01f;
this.renderCurrentSize = 0.01f;
this.prevRenderSize = 0.01f;
this.motionX = 0;
this.motionY = 0;
this.motionZ = 0;
}
/**
* Update during charging state - grow size and follow anchor.
* Default implementation grows {@code size} from 0 to {@code targetSize}.
* Override in subclasses for type-specific charging (Disc, Beam).
*/
protected void updateCharging() {
chargeTick++;
Entity owner = getOwnerEntity();
if (owner == null || owner.isDead) {
setDead();
return;
}
float progress = getChargeProgress();
this.size = targetSize * progress;
if (owner instanceof EntityLivingBase) {
Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData);
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
}
}
// ==================== HOMING ====================
/**
* Update homing toward target. Uses position-based homing with distance-scaled strength.
* Override in subclasses for type-specific homing (e.g. Beam uses head offset).
*/
protected void updateHoming() {
if (!isHoming()) return;
Entity target = getTargetEntity();
if (target == null || !target.isEntityAlive()) return;
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
double dz = target.posZ - posZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist <= getHomingRange() && dist > 0) {
float effectiveStrength = getHomingStrength();
if (dist < getHomingRange() * 0.3) {
effectiveStrength = Math.min(1.0f, getHomingStrength() * 2.5f);
} else if (dist < getHomingRange() * 0.6) {
effectiveStrength = Math.min(0.8f, getHomingStrength() * 1.5f);
}
double desiredVX = (dx / dist) * getSpeed();
double desiredVY = (dy / dist) * getSpeed();
double desiredVZ = (dz / dist) * getSpeed();
motionX += (desiredVX - motionX) * effectiveStrength;
motionY += (desiredVY - motionY) * effectiveStrength;
motionZ += (desiredVZ - motionZ) * effectiveStrength;
double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ);
if (vLen > 0) {
motionX = (motionX / vLen) * getSpeed();
motionY = (motionY / vLen) * getSpeed();
motionZ = (motionZ / vLen) * getSpeed();
}
}
}
// ==================== VELOCITY HELPERS ====================
/**
* Calculate initial velocity toward target or forward based on owner facing.
* Call from subclass constructors after initProjectile().
*/
protected void calculateInitialVelocity(EntityLivingBase owner, EntityLivingBase target,
double x, double y, double z) {
if (target != null) {
double dx = target.posX - x;
double dy = (target.posY + target.getEyeHeight()) - y;
double dz = target.posZ - z;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
this.motionX = (dx / len) * getSpeed();
this.motionY = (dy / len) * getSpeed();
this.motionZ = (dz / len) * getSpeed();
}
} else {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
this.motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
this.motionY = -Math.sin(pitch) * getSpeed();
this.motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
}
}
// ==================== CHARGING NBT ====================
/**
* Read common charging state from NBT. Call from subclass readProjectileNBT().
*/
protected void readChargingNBT(NBTTagCompound nbt) {
boolean isChargingVal = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
this.charging = isChargingVal;
this.dataWatcher.updateObject(DW_CHARGING, (byte) (isChargingVal ? 1 : 0));
this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40;
this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0;
this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size;
}
/**
* Write common charging state to NBT. Call from subclass writeProjectileNBT().
*/
protected void writeChargingNBT(NBTTagCompound nbt) {
nbt.setBoolean("Charging", isCharging());
nbt.setInteger("ChargeDuration", chargeDuration);
nbt.setInteger("ChargeTick", chargeTick);
nbt.setFloat("TargetSize", targetSize);
}
// ==================== ENTITY HELPERS ====================
public Entity getOwnerEntity() {
@@ -502,26 +663,9 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
return NpcAPI.Instance().getIEntity(getTargetEntity());
}
@Override
public int getSiblingEntityId() {
return siblingEntityId;
}
@Override
public void setSiblingEntityId(int siblingEntityId) {
if (!(worldObj.getEntityByID(siblingEntityId) instanceof EntityAbilityProjectile)) return;
this.siblingEntityId = siblingEntityId;
}
@Override
public IEnergyProjectile getSibling() {
if (siblingEntityId == -1) return null;
return (EntityAbilityProjectile) worldObj.getEntityByID(siblingEntityId);
}
/**
* Check if an entity should be ignored for collision.
* Ignores: the owner, same-faction NPCs, and other projectiles from the same caster.
*/
protected boolean shouldIgnoreEntity(Entity entity) {
Entity owner = getOwnerEntity();
@@ -533,8 +677,11 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
if (ownerNpc.faction.id == targetNpc.faction.id) return true;
}
if (entity.getEntityId() == getSiblingEntityId())
return true;
// Ignore other projectiles from the same caster (e.g. multi-projectile abilities)
if (entity instanceof EntityAbilityProjectile) {
EntityAbilityProjectile other = (EntityAbilityProjectile) entity;
if (other.ownerEntityId == this.ownerEntityId) return true;
}
return false;
}
@@ -746,7 +893,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
this.ownerEntityId = nbt.getInteger("OwnerId");
this.targetEntityId = nbt.getInteger("TargetId");
this.siblingEntityId = nbt.getInteger("SiblingId");
this.motionX = nbt.getDouble("MotionX");
this.motionY = nbt.getDouble("MotionY");
@@ -789,7 +935,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
nbt.setInteger("OwnerId", ownerEntityId);
nbt.setInteger("TargetId", targetEntityId);
nbt.setInteger("SiblingId", targetEntityId);
nbt.setDouble("MotionX", motionX);
nbt.setDouble("MotionY", motionY);
+4 -9
View File
@@ -186,7 +186,7 @@ public class DataAbilities {
playAbilitySound(currentAbility.getWindUpSound());
playAbilityAnimation(currentAbility.getWindUpAnimation());
}
currentAbility.onWindUpTick(npc, target, npc.worldObj, currentAbility.getCurrentTick());
currentAbility.onWindUpTick(npc, target, currentAbility.getCurrentTick());
break;
case ACTIVE:
@@ -221,11 +221,6 @@ public class DataAbilities {
releaseLockedPosition();
}
// Burst re-fire without replay: let entity-spawning abilities spawn+fire in one step
if (oldPhase == AbilityPhase.BURST_DELAY && !currentAbility.isBurstReplayAnimations()) {
currentAbility.onBurstRefire(npc, target, npc.worldObj);
}
// Play active sound and animation
// Abilities that keep telegraph during active manage their own sound timing
if (!currentAbility.keepTelegraphDuringActive()) {
@@ -247,7 +242,7 @@ public class DataAbilities {
}
// Call onExecute
currentAbility.onExecute(npc, target, npc.worldObj);
currentAbility.onExecute(npc, target);
// Check if ability completed during onExecute (signalCompletion was called)
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
@@ -255,7 +250,7 @@ public class DataAbilities {
return;
}
}
currentAbility.onActiveTick(npc, target, npc.worldObj, currentAbility.getCurrentTick());
currentAbility.onActiveTick(npc, target, currentAbility.getCurrentTick());
// Check if ability completed during onActiveTick (signalCompletion was called)
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
@@ -554,7 +549,7 @@ public class DataAbilities {
}
// Call onExecute
ability.onExecute(npc, target, npc.worldObj);
ability.onExecute(npc, target);
// Check if ability completed during onExecute
if (ability.getPhase() == AbilityPhase.IDLE) {
@@ -290,7 +290,7 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
private void tickWindup() {
// Call the REAL windup tick method
previewAbility.onWindUpTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick);
previewAbility.onWindUpTick(previewNpc, fakeTarget, currentTick);
if (currentTick >= previewAbility.getWindUpTicks()) {
transitionToActive();
@@ -299,7 +299,7 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
private void tickActive() {
// Call the REAL active tick method
previewAbility.onActiveTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick);
previewAbility.onActiveTick(previewNpc, fakeTarget, currentTick);
// Simulate physics for movement abilities
simulatePhysics();
@@ -330,7 +330,7 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler {
previewTelegraph = null;
// Call the REAL onExecute method (fires entities, initiates movement, etc.)
previewAbility.onExecute(previewNpc, fakeTarget, previewNpc.worldObj);
previewAbility.onExecute(previewNpc, fakeTarget);
// Start active animation
Animation activeAnim = previewAbility.getActiveAnimation();
@@ -161,7 +161,7 @@ public class PlayerAbilityData implements IPlayerAbilityData {
playAbilitySound(player, currentAbility.getWindUpSound());
playAbilityAnimation(currentAbility.getWindUpAnimation());
}
currentAbility.onWindUpTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick());
currentAbility.onWindUpTick(player, currentTarget, currentAbility.getCurrentTick());
break;
case ACTIVE:
@@ -170,7 +170,10 @@ public class PlayerAbilityData implements IPlayerAbilityData {
for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) {
telegraph.lockPosition();
}
removeTelegraph(currentAbility, player);
// Remove telegraph unless the ability keeps it during active phase
if (!currentAbility.keepTelegraphDuringActive()) {
removeTelegraph(currentAbility, player);
}
// Handle rotation control transition from WINDUP to ACTIVE
if (currentAbility.isRotationLockedDuringActive()) {
@@ -195,20 +198,28 @@ public class PlayerAbilityData implements IPlayerAbilityData {
playAbilityAnimation(currentAbility.getActiveAnimation());
// Call onExecute
currentAbility.onExecute(player, currentTarget, player.worldObj);
currentAbility.onExecute(player, currentTarget);
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(player);
return;
}
}
currentAbility.onActiveTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick());
currentAbility.onActiveTick(player, currentTarget, currentAbility.getCurrentTick());
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
handleAbilityCompletion(player);
return;
}
// Auto-complete for burst overlap mode (entities fly independently)
if (currentAbility.isBurstEnabled() && currentAbility.isBurstOverlap()
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
currentAbility.signalCompletion();
}
// Release locks during burst delay
if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) {
if (rotationLocked) releaseRotationControl();
@@ -348,7 +359,7 @@ public class PlayerAbilityData implements IPlayerAbilityData {
playAbilityAnimation(ability.getActiveAnimation());
// Call onExecute
ability.onExecute(player, currentTarget, player.worldObj);
ability.onExecute(player, currentTarget);
// Check if ability completed during onExecute
if (ability.getPhase() == AbilityPhase.IDLE) {