diff --git a/src/api b/src/api index 456bb2cbf..2b95d3ca4 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 456bb2cbfa7456d114bfc22caf49020353282e07 +Subproject commit 2b95d3ca47558e804a09e4f7b7f45aeb69fa7861 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index f8f620474..1878337b8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -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 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 // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 215b3a137..45772eedb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -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 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 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 defs) { + protected void addTypeDefinitions(List 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)); - } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 7cfb44428..871249885 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -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 entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + List 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 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 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 110bda19f..58c92009f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -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; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 6070342d7..8dbd13c92 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -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 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 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index c04f2e414..5a542ce7d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -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 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 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 defs) { - // Type tab - projectile count and fire delay + protected void addTypeDefinitions(List 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)); - } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 87fd21a7f..dc1b075e7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -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) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index e5dfde01e..26409d5fe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -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); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index d2714ddd2..6119e1340 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -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); } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index cf49cade6..367b534c7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -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 entities = world.getEntitiesWithinAABBExcludingEntity( + List 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 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) )); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 2bca3d681..3ce1e33fb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -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 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 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 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)); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 7bc4bd8ef..42f24757e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -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 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 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 defs) { - // Type tab - projectile count and fire delay + protected void addTypeDefinitions(List 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)); - } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index d79f783d1..46242fc71 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -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 } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 17fdecceb..1eebdab99 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -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 entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); + List 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 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) )); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 22fd99416..f8a6559ca 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -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; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 43a152f7a..c7cc9a1fa 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index d9e05c382..70228652e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 84bafc726..7766c3d45 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -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); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index d9d253d29..f969c27cd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -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 entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); + List 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; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java index 9a296e836..438fd11d4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -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 it = activeEntities.iterator(); while (it.hasNext()) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyProjectileAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyProjectileAbility.java new file mode 100644 index 000000000..da48a2d97 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbstractEnergyProjectileAbility.java @@ -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 The entity type this ability spawns + */ +public abstract class AbstractEnergyProjectileAbility + 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 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 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)); + } + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 488dfa39e..0fb58bcfe 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -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 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) { diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 04bf2b777..43fa459fc 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -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); } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 8a1325a27..a10dae0b8 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -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); } /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index c461e9575..c85b7be40 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -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); } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index e70b06009..9b30abab6 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -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); diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 9020b0ba8..8edcba628 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -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) { diff --git a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java index 7d856d916..6ed26666f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java +++ b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java @@ -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(); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 7807074f3..0f87ddf5a 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -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) {