Burst and Dual Mode + Variant Ability Support

This commit is contained in:
Kam committed 2026-02-06 20:25:52 -05:00
1 parent af02b0ab1d
commit 74cae17ebd
27 files changed
+2055 -2982

No files matched your search

@@ -32,6 +32,7 @@ import noppes.npcs.scripted.event.player.PlayerAbilityEvent;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import net.minecraft.entity.Entity;
@@ -101,6 +102,13 @@ public abstract class Ability implements IAbility {
// Configurable potion effects
protected List<AbilityEffect> effects = new ArrayList<>();
// Burst
protected boolean burstEnabled = false;
protected int burstAmount = 0; // number of EXTRA repetitions
protected int burstDelay = 0; // ticks between bursts
protected boolean burstReplayAnimations = true;
protected boolean burstOverlap = false; // if true, don't wait for entity death between bursts
// ═══════════════════════════════════════════════════════════════════
// EXECUTION STATE (not saved, reset each combat)
// ═══════════════════════════════════════════════════════════════════
@@ -111,6 +119,10 @@ public abstract class Ability implements IAbility {
protected transient long executionStartTime;
protected transient TelegraphInstance telegraphInstance;
// Burst execution state
protected transient int burstIndex = 0;
protected transient List<Entity> burstEntities = new ArrayList<>();
// ═══════════════════════════════════════════════════════════════════
// PREVIEW STATE (client-side only, not saved)
// ═══════════════════════════════════════════════════════════════════
@@ -156,6 +168,50 @@ public abstract class Ability implements IAbility {
return true;
}
/**
* Override to provide variant templates for this ability type.
* When a user creates this ability type, they will be shown a selection dialog
* with the returned variants. If empty or single-entry, the dialog is skipped.
*/
public List<AbilityVariant> getVariants() {
return Collections.emptyList();
}
/**
* Whether this ability type supports the burst system.
* Override to return false for abilities that shouldn't repeat (Guard, Heal, Sweeper, etc.).
*/
public boolean allowBurst() {
return true;
}
/**
* Whether this ability supports overlap mode for burst (entities from previous bursts keep flying).
* Override to return true for entity-spawning projectile abilities (Orb, Beam, Disc, LaserShot).
*/
public boolean allowOverlap() {
return false;
}
/**
* Whether this ability is ready for auto-completion during burst overlap mode.
* Override to delay auto-completion until all staggered projectiles have been fired.
* @param activeTick the current tick within the ACTIVE phase
*/
public boolean isReadyForBurstCompletion(int activeTick) {
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, World world, int tick) {
}
@@ -454,6 +510,25 @@ public abstract class Ability implements IAbility {
.range(0, 1000).visibleWhen(this::isInterruptible)
).tab("General"));
// ── Burst section ────────────────────────────────────────────
if (allowBurst()) {
defs.add(FieldDef.section("ability.section.burst").tab("General"));
defs.add(FieldDef.boolField("ability.burstEnabled", this::isBurstEnabled, this::setBurstEnabled)
.tab("General"));
defs.add(FieldDef.row(
FieldDef.intField("ability.burstAmount", this::getBurstAmount, this::setBurstAmount).range(1, 100),
FieldDef.intField("ability.burstDelay", this::getBurstDelay, this::setBurstDelay).range(0, 1000)
).tab("General").visibleWhen(this::isBurstEnabled));
defs.add(FieldDef.boolField("ability.burstReplayAnimations", this::isBurstReplayAnimations, this::setBurstReplayAnimations)
.tab("General").visibleWhen(this::isBurstEnabled)
.hover("ability.hover.burstReplay"));
if (allowOverlap()) {
defs.add(FieldDef.boolField("ability.burstOverlap", this::isBurstOverlap, this::setBurstOverlap)
.tab("General").visibleWhen(this::isBurstEnabled)
.hover("ability.hover.burstOverlap"));
}
}
// ── Target tab ───────────────────────────────────────────────
defs.add(FieldDef.row(
FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)).range(0, 100),
@@ -722,6 +797,8 @@ public abstract class Ability implements IAbility {
this.currentTick = 0;
this.currentTarget = target;
this.executionStartTime = System.currentTimeMillis();
this.burstIndex = 0;
this.burstEntities.clear();
if (windUpTicks <= 0) {
this.phase = AbilityPhase.ACTIVE;
@@ -764,6 +841,19 @@ public abstract class Ability implements IAbility {
return true;
}
break;
case BURST_DELAY:
// Waiting between burst repetitions - free movement/rotation
if (currentTick >= burstDelay) {
if (burstReplayAnimations && getWindUpTicks() > 0) {
phase = AbilityPhase.WINDUP;
} else {
phase = AbilityPhase.ACTIVE;
}
currentTick = 0;
return true;
}
break;
}
return false;
}
@@ -777,6 +867,19 @@ public abstract class Ability implements IAbility {
*/
public boolean signalCompletion() {
if (phase == AbilityPhase.ACTIVE) {
// Check if more burst iterations remain
if (burstEnabled && burstAmount > 0 && burstIndex < burstAmount) {
if (!burstOverlap) {
cleanup();
}
burstIndex++;
phase = AbilityPhase.BURST_DELAY;
currentTick = 0;
return true;
}
// Final completion - let burst entities die naturally (don't force-kill)
burstEntities.clear();
cleanup();
phase = AbilityPhase.IDLE;
currentTick = 0;
@@ -790,9 +893,10 @@ public abstract class Ability implements IAbility {
* Called when NPC takes direct damage during WINDUP.
*/
public void interrupt() {
cleanupBurstEntities();
cleanup();
if (interruptible && phase == AbilityPhase.WINDUP) {
// Interrupted during windup - go to dazed state
if (interruptible && (phase == AbilityPhase.WINDUP || phase == AbilityPhase.BURST_DELAY)) {
// Interrupted during windup or burst delay - go to dazed state
phase = AbilityPhase.DAZED;
currentTick = 0;
} else {
@@ -800,6 +904,7 @@ public abstract class Ability implements IAbility {
phase = AbilityPhase.IDLE;
currentTick = 0;
}
burstIndex = 0;
currentTarget = null;
telegraphInstance = null;
}
@@ -820,7 +925,7 @@ public abstract class Ability implements IAbility {
* @return true if the ability should be interrupted
*/
public boolean canInterrupt(DamageSource source) {
if (!interruptible || phase != AbilityPhase.WINDUP) {
if (!interruptible || (phase != AbilityPhase.WINDUP && phase != AbilityPhase.BURST_DELAY)) {
return false;
}
@@ -847,9 +952,11 @@ public abstract class Ability implements IAbility {
* Reset all execution state (called on combat end, NPC death, target lost, etc.)
*/
public void reset() {
cleanupBurstEntities();
cleanup();
phase = AbilityPhase.IDLE;
currentTick = 0;
burstIndex = 0;
currentTarget = null;
telegraphInstance = null;
previewMode = false;
@@ -865,6 +972,18 @@ public abstract class Ability implements IAbility {
// Override in subclasses to clean up spawned entities, etc.
}
/**
* Kill all entities tracked during burst overlap and clear the list.
*/
protected void cleanupBurstEntities() {
for (Entity e : burstEntities) {
if (e != null && !e.isDead) {
killAbilityEntity(e);
}
}
burstEntities.clear();
}
// ═══════════════════════════════════════════════════════════════════
// PREVIEW MODE SUPPORT
// ═══════════════════════════════════════════════════════════════════
@@ -896,6 +1015,9 @@ public abstract class Ability implements IAbility {
} else {
world.spawnEntityInWorld(entity);
}
if (burstEnabled && burstOverlap) {
burstEntities.add(entity);
}
}
/**
@@ -979,6 +1101,13 @@ public abstract class Ability implements IAbility {
nbt.setInteger("allowedBy", allowedBy.ordinal());
nbt.setBoolean("ignoreCooldown", ignoreCooldown);
// Burst
nbt.setBoolean("burstEnabled", burstEnabled);
nbt.setInteger("burstAmount", burstAmount);
nbt.setInteger("burstDelay", burstDelay);
nbt.setBoolean("burstReplayAnimations", burstReplayAnimations);
nbt.setBoolean("burstOverlap", burstOverlap);
// Conditions
NBTTagList condList = new NBTTagList();
for (Condition c : conditions) condList.appendTag(c.writeNBT());
@@ -1059,6 +1188,13 @@ public abstract class Ability implements IAbility {
allowedBy = nbt.hasKey("allowedBy") ? UserType.fromOrdinal(nbt.getInteger("allowedBy")) : UserType.BOTH;
ignoreCooldown = nbt.hasKey("ignoreCooldown") && nbt.getBoolean("ignoreCooldown");
// Burst
burstEnabled = nbt.hasKey("burstEnabled") && nbt.getBoolean("burstEnabled");
burstAmount = nbt.hasKey("burstAmount") ? nbt.getInteger("burstAmount") : 0;
burstDelay = nbt.hasKey("burstDelay") ? nbt.getInteger("burstDelay") : 0;
burstReplayAnimations = !nbt.hasKey("burstReplayAnimations") || nbt.getBoolean("burstReplayAnimations");
burstOverlap = nbt.hasKey("burstOverlap") && nbt.getBoolean("burstOverlap");
// Conditions
conditions.clear();
NBTTagList condList = nbt.getTagList("conditions", 10);
@@ -1313,6 +1449,8 @@ public abstract class Ability implements IAbility {
return isMovementLockedDuringWindup();
case ACTIVE:
return isMovementLockedDuringActive();
case BURST_DELAY:
return false; // Free movement during burst delay
default:
return false;
}
@@ -1327,6 +1465,8 @@ public abstract class Ability implements IAbility {
return isRotationLockedDuringWindup();
case ACTIVE:
return isRotationLockedDuringActive();
case BURST_DELAY:
return false; // Free rotation during burst delay
default:
return false;
}
@@ -1342,6 +1482,8 @@ public abstract class Ability implements IAbility {
return rotationPhase.locksWindup();
case ACTIVE:
return rotationPhase.locksActive();
case BURST_DELAY:
return false; // Free rotation during burst delay
default:
return false;
}
@@ -1530,6 +1672,52 @@ public abstract class Ability implements IAbility {
effects.clear();
}
// Burst getters/setters
public boolean isBurstEnabled() {
return burstEnabled;
}
public void setBurstEnabled(boolean burstEnabled) {
this.burstEnabled = burstEnabled;
}
public int getBurstAmount() {
return burstAmount;
}
public void setBurstAmount(int burstAmount) {
this.burstAmount = Math.max(0, burstAmount);
}
public int getBurstDelay() {
return burstDelay;
}
public void setBurstDelay(int burstDelay) {
this.burstDelay = Math.max(0, burstDelay);
}
public boolean isBurstReplayAnimations() {
return burstReplayAnimations;
}
public void setBurstReplayAnimations(boolean burstReplayAnimations) {
this.burstReplayAnimations = burstReplayAnimations;
}
public boolean isBurstOverlap() {
return burstOverlap;
}
public void setBurstOverlap(boolean burstOverlap) {
this.burstOverlap = burstOverlap;
}
public int getBurstIndex() {
return burstIndex;
}
/**
* Get display keys for rotation phase dropdown (excludes "None" since mode=FREE handles that).
*/
@@ -45,6 +45,9 @@ public class AbilityController implements IAbilityHandler {
/** External field providers for ability GUI (e.g., DBC Addon). Client-side only. */
private final List<IAbilityFieldProvider> fieldProviders = new ArrayList<>();
/** External variant definitions registered by addons, keyed by typeId. */
private final Map<String, List<AbilityVariant>> externalVariants = new LinkedHashMap<>();
public AbilityController() {
registerBuiltinTypes();
}
@@ -432,13 +435,9 @@ public class AbilityController implements IAbilityHandler {
registerType("cnpc:sweeper", AbilitySweeper::new);
registerType("cnpc:projectile", AbilityProjectile::new);
registerType("cnpc:orb", AbilityOrb::new);
registerType("cnpc:dual_orb", AbilityOrbDual::new);
registerType("cnpc:orb_barrage", AbilityOrbBarrage::new);
registerType("cnpc:dual_disc", AbilityDisc::new);
registerType("cnpc:dual_disc", AbilityDiscDual::new);
registerType("cnpc:disc", AbilityDisc::new);
registerType("cnpc:laser_shot", AbilityLaserShot::new);
registerType("cnpc:beam", AbilityBeam::new);
registerType("cnpc:dual_beam", AbilityBeamDual::new);
// Movement abilities
registerType("cnpc:charge", AbilityCharge::new);
@@ -490,4 +489,41 @@ public class AbilityController implements IAbilityHandler {
public List<IAbilityFieldProvider> getFieldProviders() {
return fieldProviders;
}
// ═══════════════════════════════════════════════════════════════════════════
// EXTERNAL VARIANT REGISTRY
// ═══════════════════════════════════════════════════════════════════════════
/**
* Register an external variant for an ability type.
* External mods can call this to add variant presets that appear
* alongside built-in variants in the variant selection SubGui.
*
* @param typeId The ability typeId (e.g., "ability.cnpc.beam")
* @param variant The variant to register
*/
public void registerVariant(String typeId, AbilityVariant variant) {
externalVariants.computeIfAbsent(typeId, k -> new ArrayList<>()).add(variant);
}
/**
* Get all variants for a given ability type (built-in + external).
* Returns the combined list of variants from the ability's getVariants()
* and any externally registered variants.
*
* @param typeId The ability typeId
* @return Combined list of variants, empty if none
*/
public List<AbilityVariant> getVariantsForType(String typeId) {
List<AbilityVariant> result = new ArrayList<>();
Ability temp = create(typeId);
if (temp != null) {
result.addAll(temp.getVariants());
}
List<AbilityVariant> ext = externalVariants.get(typeId);
if (ext != null) {
result.addAll(ext);
}
return result;
}
}
@@ -4,5 +4,6 @@ public enum AbilityPhase {
IDLE, // Not executing
WINDUP, // Charging up (telegraph visible)
ACTIVE, // Executing (damage happens)
DAZED // Stunned after interrupt during WINDUP (cannot attack)
DAZED, // Stunned after interrupt during WINDUP (cannot attack)
BURST_DELAY // Waiting between burst repetitions (free movement/rotation)
}
@@ -0,0 +1,39 @@
package kamkeel.npcs.controllers.data.ability;
import java.util.function.Consumer;
/**
* Represents a variant/template for an ability type.
* When a user creates an ability that has registered variants,
* they are presented with a selection dialog before the ability is created.
*
* Each variant has a display name (lang key) and a configurator that
* applies preset values to the newly created ability.
*/
public class AbilityVariant {
private final String displayKey;
private final Consumer<Ability> configurator;
/**
* @param displayKey Lang key for the variant display name (e.g., "ability.variant.dual")
* @param configurator Lambda that configures the ability with preset values
*/
public AbilityVariant(String displayKey, Consumer<Ability> configurator) {
this.displayKey = displayKey;
this.configurator = configurator;
}
public String getDisplayKey() {
return displayKey;
}
/**
* Apply this variant's preset configuration to the given ability.
*/
public void apply(Ability ability) {
if (configurator != null) {
configurator.accept(ability);
}
}
}
@@ -0,0 +1,60 @@
package kamkeel.npcs.controllers.data.ability.data;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import net.minecraft.nbt.NBTTagCompound;
/**
* Per-projectile data: anchor point + optional color override.
* All projectiles share the primary EnergyDisplayData; this class
* allows individual projectiles to override just the inner/outer colors.
*/
public class ProjectileData {
public EnergyAnchorData anchor;
public boolean colorOverride = false;
public int innerColor = 0xFFFFFF;
public int outerColor = 0x8888FF;
public ProjectileData(AnchorPoint defaultAnchor) {
this.anchor = new EnergyAnchorData(defaultAnchor);
}
/**
* Returns the display data to use for this projectile.
* If colorOverride is false, returns the primary display data directly.
* If colorOverride is true, returns a copy with overridden inner/outer colors.
*/
public EnergyDisplayData resolveDisplay(EnergyDisplayData primary) {
if (!colorOverride) return primary;
EnergyDisplayData resolved = primary.copy();
resolved.innerColor = this.innerColor;
resolved.outerColor = this.outerColor;
return resolved;
}
public void writeNBT(NBTTagCompound nbt) {
anchor.writeNBT(nbt);
nbt.setBoolean("colorOverride", colorOverride);
if (colorOverride) {
nbt.setInteger("overrideInnerColor", innerColor);
nbt.setInteger("overrideOuterColor", outerColor);
}
}
public void readNBT(NBTTagCompound nbt) {
anchor.readNBT(nbt);
colorOverride = nbt.getBoolean("colorOverride");
if (colorOverride) {
innerColor = nbt.hasKey("overrideInnerColor") ? nbt.getInteger("overrideInnerColor") : 0xFFFFFF;
outerColor = nbt.hasKey("overrideOuterColor") ? nbt.getInteger("overrideOuterColor") : 0x8888FF;
}
}
public ProjectileData copy() {
ProjectileData copy = new ProjectileData(anchor.anchorPoint);
copy.anchor = anchor.copy();
copy.colorOverride = colorOverride;
copy.innerColor = innerColor;
copy.outerColor = outerColor;
return copy;
}
}
@@ -13,7 +13,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
@@ -24,30 +23,39 @@ import noppes.npcs.api.ability.type.IAbilityEnergyBeam;
import java.util.Arrays;
import java.util.List;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
/**
* Energy Beam ability: A homing head with a trailing path.
* The beam stays attached to the origin and curves as it homes in on target.
* Cannot cross over itself (self-intersection prevention).
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
*/
public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
// Shape properties (standalone)
private static final int MAX_PROJECTILES = 8;
// Beam shape properties (shared)
private float beamWidth = 0.4f;
private float headSize = 0.6f;
// Data classes
private final EnergyDisplayData colorData = new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.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, 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 EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200);
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Transient state for beam entity (used during windup charging)
private transient EntityAbilityBeam beamEntity = null;
// Per-projectile data (anchor + optional color override)
private ProjectileData[] projectiles;
// Transient entity state
private transient EntityAbilityBeam[] beamEntities;
public AbilityBeam() {
this.typeId = "ability.cnpc.beam";
@@ -62,9 +70,88 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
this.rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animations
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));
}
}
private AnchorPoint getDefaultAnchor(int index) {
switch (index) {
case 0: return AnchorPoint.RIGHT_HAND;
case 1: return AnchorPoint.LEFT_HAND;
default: return AnchorPoint.FRONT;
}
}
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);
}
// Legacy compat
public int getDualFireDelay() { return fireDelay; }
public void setDualFireDelay(int delay) { setFireDelay(delay); }
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Beam");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilityBeam beam = (AbilityBeam) a;
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;
a.setWindUpTicks(80);
a.setWindUpAnimationName("Ability_BeamDual_Windup");
a.setActiveAnimationName("Ability_BeamDual_Active");
})
);
}
@Override
public boolean allowOverlap() {
return true;
}
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
@@ -77,20 +164,34 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
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;
}
// Start firing the beam that was spawned during windup
if (beamEntity != null && !beamEntity.isDead) {
if (isPreview()) {
beamEntity.startPreviewFiring();
} else {
beamEntity.startFiring(target);
}
private void fireBeamEntity(EntityAbilityBeam beam, EntityLivingBase target) {
if (beam == null || beam.isDead) return;
if (isPreview()) {
beam.startPreviewFiring();
} else {
beam.startFiring(target);
}
}
@@ -98,35 +199,73 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
// Spawn beam in charging mode on first tick of windup
if (tick == 1) {
beamEntities = createBeamEntities(caster, target, world);
float offsetDist = 1.0f;
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist);
beamEntity = new EntityAbilityBeam(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
beamWidth, headSize,
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData,
lockMovement.locksActive());
if (isPreview()) {
beamEntity.setupPreview(caster, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, offsetDist);
} else {
beamEntity.setupCharging(anchorData, windUpTicks, offsetDist);
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]);
}
}
}
beamEntity.setEffects(this.effects);
beamEntity.setSourceAbility(this);
spawnAbilityEntity(world, beamEntity);
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
beamEntities = createBeamEntities(caster, target, world);
for (EntityAbilityBeam beam : beamEntities) {
spawnAbilityEntity(world, beam);
}
for (EntityAbilityBeam beam : beamEntities) {
fireBeamEntity(beam, 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) {
// Signal completion when entity dies
if (beamEntity == null || beamEntity.isDead) {
beamEntity = null;
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();
}
}
@@ -142,11 +281,15 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
@Override
public void cleanup() {
// Despawn beam entity if still alive
if (beamEntity != null && !beamEntity.isDead) {
beamEntity.setDead();
if (beamEntities != null) {
for (int i = 0; i < beamEntities.length; i++) {
if (beamEntities[i] != null && !beamEntities[i].isDead) {
beamEntities[i].setDead();
}
beamEntities[i] = null;
}
}
beamEntity = null;
beamEntities = null;
}
@Override
@@ -155,7 +298,6 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
return null;
}
// Create circle telegraph at target position
Telegraph telegraph = Telegraph.circle(headSize * 2.0f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
@@ -163,7 +305,6 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
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());
@@ -180,172 +321,307 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("beamWidth", beamWidth);
nbt.setFloat("headSize", headSize);
anchorData.writeNBT(nbt);
colorData.writeNBT(nbt);
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);
lightningData.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) {
this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f;
this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f;
anchorData.readNBT(nbt);
colorData.readNBT(nbt);
int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1;
initProjectiles(count);
if (nbt.hasKey("fireDelay")) {
this.fireDelay = nbt.getInteger("fireDelay");
} else if (nbt.hasKey("dualFireDelay")) {
this.fireDelay = nbt.getInteger("dualFireDelay");
} else {
this.fireDelay = 0;
}
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lightningData.readNBT(nbt);
lifespanData.readNBT(nbt);
if (nbt.hasKey("Projectile_0")) {
NBTTagCompound proj0 = nbt.getCompoundTag("Projectile_0");
if (proj0.hasKey("colorOverride")) {
// NEW format
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
} else {
// OLD per-projectile format
displayData.readNBT(proj0);
lightningData.readNBT(proj0);
projectiles[0].anchor.readNBT(proj0);
for (int i = 1; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
NBTTagCompound projNbt = nbt.getCompoundTag("Projectile_" + i);
projectiles[i].anchor.readNBT(projNbt);
EnergyDisplayData tempDisplay = new EnergyDisplayData();
tempDisplay.readNBT(projNbt);
if (tempDisplay.innerColor != displayData.innerColor || tempDisplay.outerColor != displayData.outerColor) {
projectiles[i].colorOverride = true;
projectiles[i].innerColor = tempDisplay.innerColor;
projectiles[i].outerColor = tempDisplay.outerColor;
}
}
}
}
} else {
// LEGACY flat format
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
projectiles[0].anchor.readNBT(nbt);
}
}
// Getters & Setters - Standalone fields
// ═══════════════════════════════════════════════════════════════════
// API GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public float getBeamWidth() { return beamWidth; }
public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; }
public float getHeadSize() { return headSize; }
public void setHeadSize(float headSize) { this.headSize = headSize; }
// 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 boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; }
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 float getRotationSpeed() { return colorData.rotationSpeed; }
public void setRotationSpeed(float rotationSpeed) { this.colorData.rotationSpeed = rotationSpeed; }
// 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 - Homing data
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { this.homingData.speed = speed; }
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; }
// 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; }
// 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 - Anchor point
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 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 anchorData.anchorPoint.ordinal(); }
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
@Override
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
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); }
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
// ═══════════════════════════════════════════════════════════════════
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.section("ability.section.beam"),
FieldDef.row(
FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth),
FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
.hover("ability.hover.homing"),
FieldDef.row(
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
.visibleWhen(this::isHoming),
FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange)
.visibleWhen(this::isHoming)
),
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),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.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.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.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")
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)
));
defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback));
defs.add(FieldDef.section("ability.section.beam"));
defs.add(FieldDef.row(
FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth),
FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize)
));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
));
defs.add(FieldDef.section("ability.section.homing"));
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange).visibleWhen(this::isHoming)
));
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));
// 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),
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));
}
}
}
@@ -1,528 +0,0 @@
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.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.EntityAbilityBeam;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.util.ValueUtil;
import java.util.Arrays;
import java.util.List;
public class AbilityBeamDual extends Ability {
private float beamWidth = 0.4f;
private float headSize = 0.6f;
private boolean dualFire= true;
private int dualFireDelay = 0;
// Data classes
private final EnergyDisplayData[] displayData = new EnergyDisplayData[]{
new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f),
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 6.0f),
};
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 EnergyLightningData[] lightningData = new EnergyLightningData[]{
new EnergyLightningData(), new EnergyLightningData()
};
private final EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200);
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND)
};
private final EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{
new EnergyTrajectoryData(), new EnergyTrajectoryData()
};
// Transient state for beam entity (used during windup charging)
private transient EntityAbilityBeam beamEntity = null;
private transient EntityAbilityBeam beamEntity2 = null;
public AbilityBeamDual() {
this.typeId = "ability.cnpc.beam_dual";
this.name = "Beam Dual";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 20.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 80;
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animations
this.windUpAnimationName = "Ability_BeamDual_Windup";
this.activeAnimationName = "Ability_BeamDual_Active";
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
// Start firing the beam that was spawned during windup
if (beamEntity != null && !beamEntity.isDead) {
if (isPreview()) {
beamEntity.startPreviewFiring();
} else {
beamEntity.startFiring(target);
}
}
if (dualFire) {
if (beamEntity2 != null && !beamEntity2.isDead && dualFireDelay <= 0) {
if (isPreview()) {
beamEntity2.startPreviewFiring();
} else {
beamEntity2.startFiring(target);
}
}
} else if (beamEntity2 != null) {
beamEntity2.setDead();
}
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
// Spawn beam in charging mode on first tick of windup
if (tick == 1) {
float offsetDist = 1.0f;
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0], offsetDist);
beamEntity = new EntityAbilityBeam(
world, caster, target,
spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord,
beamWidth, headSize,
displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0],
lockMovement.locksActive());
Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1], offsetDist);
beamEntity2 = new EntityAbilityBeam(
world, caster, target,
spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord,
beamWidth, headSize,
displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1],
lockMovement.locksActive());
beamEntity.setSiblingEntityId(beamEntity2.getEntityId());
beamEntity2.setSiblingEntityId(beamEntity.getEntityId());
if (isPreview()) {
beamEntity.setupPreview(caster, beamWidth, headSize, displayData[0], lightningData[0], anchorData[0], windUpTicks, offsetDist);
beamEntity2.setupPreview(caster, beamWidth, headSize, displayData[1], lightningData[1], anchorData[1], windUpTicks, offsetDist);
} else {
beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist);
beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist);
}
beamEntity.setEffects(this.effects);
if (dualFire) beamEntity2.setEffects(this.effects);
beamEntity.setSourceAbility(this);
beamEntity2.setSourceAbility(this);
spawnAbilityEntity(world, beamEntity);
spawnAbilityEntity(world, beamEntity2);
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (dualFire) {
if ((beamEntity == null || beamEntity.isDead) && (beamEntity2 == null || beamEntity2.isDead)) {
beamEntity = null;
beamEntity2 = null;
signalCompletion();
}
if (tick == dualFireDelay && beamEntity2 != null && !beamEntity2.isDead) {
beamEntity2.startFiring(target);
}
} else {
if (beamEntity == null || beamEntity.isDead) {
beamEntity = null;
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() {
// Despawn beam entity if still alive
if (beamEntity != null && !beamEntity.isDead) {
beamEntity.setDead();
}
if (beamEntity2 != null && !beamEntity2.isDead) {
beamEntity2.setDead();
}
beamEntity = null;
beamEntity2 = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create circle telegraph at target position
Telegraph telegraph = Telegraph.circle(headSize * 2.0f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, 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) {
nbt.setFloat("beamWidth", beamWidth);
nbt.setFloat("headSize", headSize);
nbt.setBoolean("dualFire", dualFire);
nbt.setInteger("dualFireDelay", dualFireDelay);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
NBTTagCompound beam1 = new NBTTagCompound();
NBTTagCompound beam2 = new NBTTagCompound();
for (int i = 0; i < 2; i++) {
NBTTagCompound beam = i == 0 ? beam1 : beam2;
anchorData[i].writeNBT(beam);
displayData[i].writeNBT(beam);
lightningData[i].writeNBT(beam);
trajectoryData[i].writeNBT(beam);
}
nbt.setTag("Beam_1", beam1);
nbt.setTag("Beam_2", beam2);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
NBTTagCompound beam1 = nbt.hasKey("Beam_1") ? nbt.getCompoundTag("Beam_1") : new NBTTagCompound();
NBTTagCompound beam2 = nbt.hasKey("Beam_2") ? nbt.getCompoundTag("Beam_2") : new NBTTagCompound();
this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f;
this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f;
this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire");
this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0;
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
for (int i = 0; i < 2; i++) {
NBTTagCompound beam = i == 0 ? beam1 : beam2;
anchorData[i].readNBT(beam);
displayData[i].readNBT(beam);
lightningData[i].readNBT(beam);
trajectoryData[i].readNBT(beam);
}
}
// Getters & Setters - Standalone fields
public float getBeamWidth() { return beamWidth; }
public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; }
public float getHeadSize() { return headSize; }
public void setHeadSize(float headSize) { this.headSize = headSize; }
public boolean isDualFire() { return dualFire; }
public void setDualFire(boolean dualFire) { this.dualFire = dualFire; }
public int getDualFireDelay() { return dualFireDelay; }
public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; }
private EnergyDisplayData getColorData(int beam) {
beam = ValueUtil.clamp(beam, 0, 1);
return displayData[beam];
}
private EnergyLightningData getLightningData(int beam) {
beam = ValueUtil.clamp(beam, 0, 1);
return lightningData[beam];
}
private EnergyAnchorData getAnchorData(int beam) {
beam = ValueUtil.clamp(beam, 0, 1);
return anchorData[beam];
}
// Getters & Setters - Color data
public int getInnerColor(int beam) { return getColorData(beam).innerColor; }
public void setInnerColor(int beam, int innerColor) { getColorData(beam).innerColor = innerColor; }
public int getOuterColor(int beam) { return getColorData(beam).outerColor; }
public void setOuterColor(int beam, int outerColor) { getColorData(beam).outerColor = outerColor; }
public boolean isOuterColorEnabled(int beam) { return getColorData(beam).outerColorEnabled; }
public void setOuterColorEnabled(int beam, boolean enabled) { getColorData(beam).outerColorEnabled = enabled; }
public float getOuterColorWidth(int beam) { return getColorData(beam).outerColorWidth; }
public void setOuterColorWidth(int beam, float width) { getColorData(beam).outerColorWidth = width; }
public float getOuterColorAlpha(int beam) { return getColorData(beam).outerColorAlpha; }
public void setOuterColorAlpha(int beam, float alpha) { getColorData(beam).outerColorAlpha = alpha; }
public float getRotationSpeed(int beam) { return getColorData(beam).rotationSpeed; }
public void setRotationSpeed(int beam, float speed) { getColorData(beam).rotationSpeed = speed; }
public int getInnerColor() { return getInnerColor(0); }
public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); }
public int getOuterColor() { return getOuterColor(0); }
public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); }
public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); }
public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); }
public float getOuterColorWidth() { return getOuterColorWidth(0); }
public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); }
public float getOuterColorAlpha() { return getOuterColorAlpha(0); }
public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); }
public float getRotationSpeed() { return getRotationSpeed(0); }
public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); }
// 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 - Homing data
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { this.homingData.speed = speed; }
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; }
// Getters & Setters - Lightning data
public boolean hasLightningEffect(int beam) { return getLightningData(beam).lightningEffect; }
public void setLightningEffect(int beam, boolean lightningEffect) { getLightningData(beam).lightningEffect = lightningEffect; }
public float getLightningDensity(int beam) { return getLightningData(beam).lightningDensity; }
public void setLightningDensity(int beam, float lightningDensity) { getLightningData(beam).lightningDensity = lightningDensity; }
public float getLightningRadius(int beam) { return getLightningData(beam).lightningRadius; }
public void setLightningRadius(int beam, float lightningRadius) { getLightningData(beam).lightningRadius = lightningRadius; }
public boolean hasLightningEffect() { return hasLightningEffect(0); }
public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); }
public float getLightningDensity() { return getLightningDensity(0); }
public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); }
public float getLightningRadius() { return getLightningRadius(0); }
public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); }
// 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 - Anchor point
public AnchorPoint getAnchorPointEnum(int beam) { return getAnchorData(beam).anchorPoint; }
public void setAnchorPointEnum(int beam, AnchorPoint anchorPoint) { getAnchorData(beam).anchorPoint = anchorPoint; }
public float getAnchorOffsetX(int beam) { return getAnchorData(beam).anchorOffsetX; }
public void setAnchorOffsetX(int beam, float x) { getAnchorData(beam).anchorOffsetX = x; }
public float getAnchorOffsetY(int beam) { return getAnchorData(beam).anchorOffsetY; }
public void setAnchorOffsetY(int beam, float y) { getAnchorData(beam).anchorOffsetY = y; }
public float getAnchorOffsetZ(int beam) { return getAnchorData(beam).anchorOffsetZ; }
public void setAnchorOffsetZ(int beam, float z) { getAnchorData(beam).anchorOffsetZ = z; }
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
//@Override
public int getAnchorPoint(int beam) { return getAnchorData(beam).anchorPoint.ordinal(); }
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
//@Override
public void setAnchorPoint(int beam, int point) { getAnchorData(beam).anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.section("ability.section.beam"),
FieldDef.row(
FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth),
FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.dual"),
FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire),
FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay)
.visibleWhen(this::isDualFire),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
.hover("ability.hover.homing"),
FieldDef.row(
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
.visibleWhen(this::isHoming),
FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange)
.visibleWhen(this::isHoming)
),
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 - Beam 1
FieldDef.section("ability.section.beam1").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(0), v -> setInnerColor(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(0), v -> setOuterColor(0, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v))
.visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(0))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(0), v -> setRotationSpeed(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(0), v -> setLightningEffect(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(0), v -> setLightningDensity(0, v))
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(0)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(0), v -> setLightningRadius(0, v))
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(0))
).tab("ability.tab.visual"),
// Visual tab - Beam 2
FieldDef.section("ability.section.beam2").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(1), v -> setInnerColor(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(1), v -> setOuterColor(1, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v))
.visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(1))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(1), v -> setRotationSpeed(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(1), v -> setLightningEffect(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(1), v -> setLightningDensity(1, v))
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(1), v -> setLightningRadius(1, v))
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1))
).tab("ability.tab.visual")
));
}
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
}
@@ -22,33 +22,45 @@ import noppes.npcs.api.ability.type.IAbilityDisc;
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 {
// Disc geometry
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
// Boomerang properties (shared)
private boolean boomerang = false;
private int boomerangDelay = 40;
// Energy data classes
private final EnergyDisplayData colorData = new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.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, 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 EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200);
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Transient state for disc entity (used during windup charging)
private transient EntityAbilityDisc discEntity = null;
// Per-projectile data (anchor + optional color override)
private ProjectileData[] projectiles;
// Transient entity state
private transient EntityAbilityDisc[] discEntities;
public AbilityDisc() {
this.typeId = "ability.cnpc.disc";
@@ -61,9 +73,90 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animations
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));
}
}
/**
* 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.LEFT_HAND;
case 1: return AnchorPoint.RIGHT_HAND;
default: return AnchorPoint.FRONT;
}
}
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);
}
// Legacy compat
public int getDualFireDelay() { return fireDelay; }
public void setDualFireDelay(int delay) { setFireDelay(delay); }
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Disc");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilityDisc disc = (AbilityDisc) a;
a.setName("Dual Disc");
disc.setProjectileCount(2);
disc.setFireDelay(10);
// Set color override for second disc
disc.projectiles[1].colorOverride = true;
disc.projectiles[1].innerColor = 0xFFFFFF;
disc.projectiles[1].outerColor = 0x8800FF;
a.setWindUpAnimationName("Ability_DiscDual_Windup");
a.setActiveAnimationName("Ability_DiscDual_Active");
})
);
}
@Override
public boolean allowOverlap() {
return true;
}
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
@@ -76,20 +169,33 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
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;
}
// Start moving the disc that was spawned during windup
if (discEntity != null && !discEntity.isDead) {
if (isPreview()) {
discEntity.startPreviewFiring();
} else {
discEntity.startMoving(target);
}
private void fireDiscEntity(EntityAbilityDisc disc, EntityLivingBase target) {
if (disc == null || disc.isDead) return;
if (isPreview()) {
disc.startPreviewFiring();
} else {
disc.startMoving(target);
}
}
@@ -97,33 +203,76 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
// Spawn disc in charging mode on first tick of windup
if (tick == 1) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
discEntity = new EntityAbilityDisc(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
discRadius, discThickness,
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData,
boomerang, boomerangDelay);
if (isPreview()) {
discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks, vertical);
} else {
discEntity.setupCharging(anchorData, windUpTicks, vertical);
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]);
}
}
}
discEntity.setEffects(this.effects);
discEntity.setSourceAbility(this);
spawnAbilityEntity(world, discEntity);
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
discEntities = createDiscEntities(caster, target, world);
for (EntityAbilityDisc disc : discEntities) {
spawnAbilityEntity(world, disc);
}
for (EntityAbilityDisc disc : discEntities) {
fireDiscEntity(disc, 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) {
// Signal completion when entity dies
if (discEntity == null || discEntity.isDead) {
discEntity = null;
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();
}
}
@@ -139,11 +288,15 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
@Override
public void cleanup() {
// Despawn disc entity if still alive
if (discEntity != null && !discEntity.isDead) {
discEntity.setDead();
if (discEntities != null) {
for (int i = 0; i < discEntities.length; i++) {
if (discEntities[i] != null && !discEntities[i].isDead) {
discEntities[i].setDead();
}
discEntities[i] = null;
}
}
discEntity = null;
discEntities = null;
}
@Override
@@ -180,12 +333,24 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
nbt.setBoolean("vertical", vertical);
nbt.setBoolean("boomerang", boomerang);
nbt.setInteger("boomerangDelay", boomerangDelay);
anchorData.writeNBT(nbt);
colorData.writeNBT(nbt);
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);
lightningData.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
@@ -195,157 +360,303 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical");
this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang");
this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40;
anchorData.readNBT(nbt);
colorData.readNBT(nbt);
int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1;
initProjectiles(count);
// Read fireDelay with fallback to old dualFireDelay key
if (nbt.hasKey("fireDelay")) {
this.fireDelay = nbt.getInteger("fireDelay");
} else if (nbt.hasKey("dualFireDelay")) {
this.fireDelay = nbt.getInteger("dualFireDelay");
} else {
this.fireDelay = 0;
}
// Shared combat/movement data
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lightningData.readNBT(nbt);
lifespanData.readNBT(nbt);
// Determine format and read accordingly
if (nbt.hasKey("Projectile_0")) {
NBTTagCompound proj0 = nbt.getCompoundTag("Projectile_0");
if (proj0.hasKey("colorOverride")) {
// NEW format: shared visuals at root + ProjectileData per projectile
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
} else {
// OLD format: full per-projectile arrays (displayData, lightningData, etc. in each Projectile_i)
// Read primary visuals from Projectile_0
displayData.readNBT(proj0);
lightningData.readNBT(proj0);
projectiles[0].anchor.readNBT(proj0);
// Read remaining projectiles, set color override if colors differ
for (int i = 1; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
NBTTagCompound projNbt = nbt.getCompoundTag("Projectile_" + i);
projectiles[i].anchor.readNBT(projNbt);
// Check if this projectile had different colors
EnergyDisplayData tempDisplay = new EnergyDisplayData();
tempDisplay.readNBT(projNbt);
if (tempDisplay.innerColor != displayData.innerColor || tempDisplay.outerColor != displayData.outerColor) {
projectiles[i].colorOverride = true;
projectiles[i].innerColor = tempDisplay.innerColor;
projectiles[i].outerColor = tempDisplay.outerColor;
}
}
}
}
} else {
// LEGACY flat format: all data at root level
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
projectiles[0].anchor.readNBT(nbt);
}
}
// Getters & Setters
// ═══════════════════════════════════════════════════════════════════
// API GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { homingData.speed = speed; }
public float getDiscRadius() { return discRadius; }
public void setDiscRadius(float discRadius) { this.discRadius = discRadius; }
public float getDiscThickness() { return discThickness; }
public void setDiscThickness(float discThickness) { this.discThickness = discThickness; }
public boolean isVertical() { return vertical; }
public void setVertical(boolean vertical) { this.vertical = vertical; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; }
public float getDamage() { return combatData.damage; }
public void setDamage(float damage) { combatData.damage = damage; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float knockback) { combatData.knockback = knockback; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean homing) { homingData.homing = homing; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; }
public boolean isBoomerang() { return boomerang; }
public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; }
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 explosive) { combatData.explosive = explosive; }
public void setExplosive(boolean e) { combatData.explosive = e; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; }
public void setExplosionRadius(float r) { combatData.explosionRadius = r; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; }
public int getInnerColor() { return colorData.innerColor; }
public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; }
public int getOuterColor() { return colorData.outerColor; }
public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; }
public float getOuterColorWidth() { return colorData.outerColorWidth; }
public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; }
public float getOuterColorAlpha() { return colorData.outerColorAlpha; }
public void setOuterColorAlpha(float outerColorAlpha) { colorData.outerColorAlpha = outerColorAlpha; }
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; }
public float getRotationSpeed() { return colorData.rotationSpeed; }
public void setRotationSpeed(float rotationSpeed) { colorData.rotationSpeed = rotationSpeed; }
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 lightningEffect) { lightningData.lightningEffect = lightningEffect; }
public void setLightningEffect(boolean e) { lightningData.lightningEffect = e; }
public float getLightningDensity() { return lightningData.lightningDensity; }
public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; }
public void setLightningDensity(float d) { lightningData.lightningDensity = d; }
public float getLightningRadius() { return lightningData.lightningRadius; }
public void setLightningRadius(float lightningRadius) { 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 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 anchorData.anchorPoint.ordinal(); }
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
@Override
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
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); }
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
// ═══════════════════════════════════════════════════════════════════
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.section("ability.section.disc"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
),
FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical)
.hover("ability.hover.vertical"),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
.hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
.visibleWhen(this::isHoming),
FieldDef.section("ability.section.boomerang"),
FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang)
.hover("ability.hover.boomerang"),
FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay)
.visibleWhen(this::isBoomerang),
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),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.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.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.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")
// Type tab - projectile count and fire delay
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)
));
defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback));
defs.add(FieldDef.section("ability.section.disc"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
));
defs.add(FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical)
.hover("ability.hover.vertical"));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
));
defs.add(FieldDef.section("ability.section.homing"));
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming));
defs.add(FieldDef.section("ability.section.boomerang"));
defs.add(FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang).hover("ability.hover.boomerang"));
defs.add(FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay).visibleWhen(this::isBoomerang));
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));
// 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),
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));
}
}
}
@@ -1,551 +0,0 @@
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.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.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.util.ValueUtil;
import java.util.Arrays;
import java.util.List;
public class AbilityDiscDual extends Ability {
// Disc geometry
private float discRadius = 1.0f;
private float discThickness = 0.2f;
private boolean vertical = false;
// Boomerang properties
private boolean boomerang = false;
private int boomerangDelay = 40;
// Dual properties
private boolean dualFire = true;
private int dualFireDelay = 10;
// Energy data classes
private final EnergyDisplayData[] displayData = new EnergyDisplayData[]{
new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f),
new EnergyDisplayData(0xFFFFFF, 0x8800FF, true, 0.4f, 0.5f, 5.0f)
};
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 EnergyLightningData[] lightningData = new EnergyLightningData[]{
new EnergyLightningData(), new EnergyLightningData()
};
private final EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200);
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
new EnergyAnchorData(AnchorPoint.LEFT_HAND), new EnergyAnchorData(AnchorPoint.RIGHT_HAND)
};
private final EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{
new EnergyTrajectoryData(), new EnergyTrajectoryData()
};
// Transient state for disc entity (used during windup charging)
private transient EntityAbilityDisc discEntity = null;
private transient EntityAbilityDisc discEntity2 = null;
public AbilityDiscDual() {
this.typeId = "ability.cnpc.disc_dual";
this.name = "Disc Dual";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 30.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 60;
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animations
this.windUpAnimationName = "Ability_DiscDual_Windup";
this.activeAnimationName = "Ability_DiscDual_Active";
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
// Start moving the disc that was spawned during windup
if (discEntity != null && !discEntity.isDead) {
if (isPreview()) {
discEntity.startPreviewFiring();
} else {
discEntity.startMoving(target);
}
}
if (dualFire) {
if (discEntity2 != null && !discEntity2.isDead && dualFireDelay <= 0) {
if (isPreview()) {
discEntity2.startPreviewFiring();
} else {
discEntity2.startMoving(target);
}
}
} else if (discEntity2 != null) {
discEntity2.setDead();
}
// 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 onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
// Spawn disc in charging mode on first tick of windup
if (tick == 1) {
// Create disc in charging mode - follows caster based on anchor point during windup
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]);
discEntity = new EntityAbilityDisc(
world, caster, target,
spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord,
discRadius, discThickness,
displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0],
boomerang, boomerangDelay);
Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]);
discEntity2 = new EntityAbilityDisc(
world, caster, target,
spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord,
discRadius, discThickness,
displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1],
boomerang, boomerangDelay);
discEntity.setSiblingEntityId(discEntity2.getEntityId());
discEntity2.setSiblingEntityId(discEntity.getEntityId());
if (isPreview()) {
discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks, vertical);
discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks, vertical);
} else {
discEntity.setupCharging(anchorData[0], windUpTicks, vertical);
discEntity2.setupCharging(anchorData[1], windUpTicks, vertical);
}
discEntity.setEffects(this.effects);
if (dualFire) {
discEntity2.setEffects(this.effects);
}
discEntity.setSourceAbility(this);
discEntity2.setSourceAbility(this);
spawnAbilityEntity(world, discEntity);
spawnAbilityEntity(world, discEntity2);
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (dualFire) {
if ((discEntity == null || discEntity.isDead) && (discEntity2 == null || discEntity2.isDead)) {
discEntity = null;
discEntity2 = null;
signalCompletion();
}
if (tick == dualFireDelay && discEntity2 != null && !discEntity2.isDead) {
if (isPreview()) {
discEntity2.startPreviewFiring();
} else {
discEntity2.startMoving(target);
}
}
} else {
if (discEntity == null || discEntity.isDead) {
discEntity = null;
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() {
// Despawn disc entity if still alive
if (discEntity != null && !discEntity.isDead) {
discEntity.setDead();
}
if (discEntity2 != null && !discEntity2.isDead) {
discEntity2.setDead();
}
discEntity = null;
discEntity2 = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create circle telegraph at target position
Telegraph telegraph = Telegraph.circle(discRadius * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, 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) {
nbt.setFloat("discRadius", discRadius);
nbt.setFloat("discThickness", discThickness);
nbt.setBoolean("vertical", vertical);
nbt.setBoolean("boomerang", boomerang);
nbt.setInteger("boomerangDelay", boomerangDelay);
nbt.setBoolean("dualFire", dualFire);
nbt.setInteger("dualFireDelay", dualFireDelay);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
NBTTagCompound disc1 = new NBTTagCompound();
NBTTagCompound disc2 = new NBTTagCompound();
for (int i = 0; i < 2; i++) {
NBTTagCompound disc = i == 0 ? disc1 : disc2;
anchorData[i].writeNBT(disc);
displayData[i].writeNBT(disc);
lightningData[i].writeNBT(disc);
trajectoryData[i].writeNBT(disc);
}
nbt.setTag("Disc_1", disc1);
nbt.setTag("Disc_2", disc2);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
NBTTagCompound disc1 = nbt.hasKey("Disc_1") ? nbt.getCompoundTag("Disc_1") : new NBTTagCompound();
NBTTagCompound disc2 = nbt.hasKey("Disc_2") ? nbt.getCompoundTag("Disc_2") : new NBTTagCompound();
this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f;
this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f;
this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical");
this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang");
this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40;
this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire");
this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0;
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
for (int i = 0; i < 2; i++) {
NBTTagCompound disc = i == 0 ? disc1 : disc2;
anchorData[i].readNBT(disc);
displayData[i].readNBT(disc);
lightningData[i].readNBT(disc);
trajectoryData[i].readNBT(disc);
}
}
// Getters & Setters
private EnergyDisplayData getColorData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return displayData[orb];
}
private EnergyLightningData getLightningData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return lightningData[orb];
}
private EnergyAnchorData getAnchorData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return anchorData[orb];
}
public boolean isDualFire() { return dualFire; }
public void setDualFire(boolean dualFire) { this.dualFire = dualFire; }
public int getDualFireDelay() { return dualFireDelay; }
public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; }
public float getSpeed() { return homingData.speed; }
public void setSpeed(float speed) { homingData.speed = speed; }
public float getDiscRadius() { return discRadius; }
public void setDiscRadius(float discRadius) { this.discRadius = discRadius; }
public float getDiscThickness() { return discThickness; }
public void setDiscThickness(float discThickness) { this.discThickness = discThickness; }
public boolean isVertical() { return vertical; }
public void setVertical(boolean vertical) { this.vertical = vertical; }
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; }
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; }
public float getDamage() { return combatData.damage; }
public void setDamage(float damage) { combatData.damage = damage; }
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float knockback) { combatData.knockback = knockback; }
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; }
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean homing) { homingData.homing = homing; }
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; }
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; }
public boolean isBoomerang() { return boomerang; }
public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; }
public int getBoomerangDelay() { return boomerangDelay; }
public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; }
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean explosive) { combatData.explosive = explosive; }
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; }
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; }
public int getInnerColor(int orb) { return getColorData(orb).innerColor; }
public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; }
public int getOuterColor(int orb) { return getColorData(orb).outerColor; }
public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; }
public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; }
public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; }
public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; }
public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; }
public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; }
public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; }
public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; }
public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; }
public int getInnerColor() { return getInnerColor(0); }
public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); }
public int getOuterColor() { return getOuterColor(0); }
public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); }
public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); }
public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); }
public float getOuterColorWidth() { return getOuterColorWidth(0); }
public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); }
public float getOuterColorAlpha() { return getOuterColorAlpha(0); }
public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); }
public float getRotationSpeed() { return getRotationSpeed(0); }
public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); }
public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; }
public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; }
public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; }
public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; }
public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; }
public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; }
public boolean hasLightningEffect() { return hasLightningEffect(0); }
public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); }
public float getLightningDensity() { return getLightningDensity(0); }
public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); }
public float getLightningRadius() { return getLightningRadius(0); }
public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); }
public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; }
public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; }
public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; }
public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; }
public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; }
public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; }
public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; }
public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; }
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
//@Override
public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); }
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
//@Override
public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.section("ability.section.disc"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
),
FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical)
.hover("ability.hover.vertical"),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.dual"),
FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire),
FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay)
.visibleWhen(this::isDualFire),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
.hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
.visibleWhen(this::isHoming),
FieldDef.section("ability.section.boomerang"),
FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang)
.hover("ability.hover.boomerang"),
FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay)
.visibleWhen(this::isBoomerang),
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 - Disc 1
FieldDef.section("ability.section.disc1").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(0), v -> setInnerColor(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(0), v -> setOuterColor(0, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v))
.visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(0))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(0), v -> setRotationSpeed(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(0), v -> setLightningEffect(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(0), v -> setLightningDensity(0, v))
.range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(0), v -> setLightningRadius(0, v))
.range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0))
).tab("ability.tab.visual"),
// Visual tab - Disc 2
FieldDef.section("ability.section.disc2").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(1), v -> setInnerColor(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(1), v -> setOuterColor(1, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v))
.visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(1))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(1), v -> setRotationSpeed(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(1), v -> setLightningEffect(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(1), v -> setLightningDensity(1, v))
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(1), v -> setLightningRadius(1, v))
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1))
).tab("ability.tab.visual")
));
}
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
}
@@ -70,6 +70,11 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
return false;
}
@Override
public boolean allowBurst() {
return false;
}
public enum CounterType {
FLAT,
PERCENT;
@@ -113,6 +113,11 @@ public class AbilityHazard extends Ability implements IAbilityHazard {
this.telegraphType = TelegraphType.CIRCLE;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
@@ -63,6 +63,11 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
return false;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
@@ -63,6 +63,11 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
this.activeAnimationName = "Ability_Laser_Active";
}
@Override
public boolean allowOverlap() {
return true;
}
@Override
public boolean isTargetingModeLocked() {
return false;
@@ -83,32 +88,47 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
return laserWidth * 2.0f; // Make telegraph slightly wider for visibility
}
private EntityAbilityLaser spawnLaserEntity(EntityLivingBase caster, EntityLivingBase target, World world) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
EntityAbilityLaser entity = new EntityAbilityLaser(
world, 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;
}
private void fireLaserEntity(EntityAbilityLaser laser, EntityLivingBase target) {
if (laser == null || laser.isDead) return;
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) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
laserEntity = new EntityAbilityLaser(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
laserWidth,
colorData, combatData, lightningData, lifespanData, trajectoryData,
expansionSpeed, lingerTicks
);
laserEntity = spawnLaserEntity(caster, target, world);
if (isPreview()) {
laserEntity.setupPreview(caster, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance);
} else {
laserEntity.setupCharging(anchorData, windUpTicks);
}
laserEntity.setEffects(this.effects);
laserEntity.setSourceAbility(this);
spawnAbilityEntity(world, laserEntity);
}
}
@Override
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
laserEntity = spawnLaserEntity(caster, target, world);
fireLaserEntity(laserEntity, target);
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
@@ -121,8 +141,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
return;
}
laserEntity.setLockVerticalDirection(true);
laserEntity.startMoving(target);
fireLaserEntity(laserEntity, target);
}
@Override
@@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
@@ -23,29 +22,38 @@ import noppes.npcs.api.ability.type.IAbilityOrb;
import java.util.Arrays;
import java.util.List;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
/**
* Orb ability: Spawns a homing projectile sphere that tracks target.
* The EntityAbilityOrb handles all movement, collision, and damage logic.
* This ability just configures and spawns the orb entity.
* 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 {
// Ability-specific properties
private static final int MAX_PROJECTILES = 8;
// Orb properties
private float orbSize = 1.0f;
// Data classes for energy properties
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
private final EnergyCombatData combatData = new EnergyCombatData();
private final EnergyDisplayData displayData = new EnergyDisplayData();
private final EnergyHomingData homingData = new EnergyHomingData();
private final EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData();
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Projectile count and fire stagger
private int projectileCount = 1;
private int fireDelay = 0;
// Transient state for orb entity (used during windup charging)
private transient EntityAbilityOrb orbEntity = null;
// 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() {
this.typeId = "ability.cnpc.orb";
@@ -58,9 +66,100 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animation
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));
}
}
private AnchorPoint getDefaultAnchor(int index) {
switch (index) {
case 0: return AnchorPoint.RIGHT_HAND;
case 1: return AnchorPoint.LEFT_HAND;
default: return AnchorPoint.FRONT;
}
}
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];
}
}
}
public int getProjectileCount() {
return projectileCount;
}
public int getFireDelay() {
return fireDelay;
}
public void setFireDelay(int delay) {
this.fireDelay = Math.max(0, delay);
}
// Legacy compat
public int getDualFireDelay() { return fireDelay; }
public void setDualFireDelay(int delay) { setFireDelay(delay); }
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Orb");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilityOrb orb = (AbilityOrb) a;
a.setName("Dual Orb");
orb.setProjectileCount(2);
orb.setFireDelay(5);
a.setWindUpAnimationName("Ability_OrbDual_Windup");
a.setActiveAnimationName("Ability_OrbDual_Active");
}),
new AbilityVariant("ability.variant.barrage", a -> {
AbilityOrb orb = (AbilityOrb) a;
a.setName("Orb Barrage");
orb.setOrbSize(0.5f);
orb.setDamage(4.0f);
orb.setKnockback(0.0f);
orb.setKnockbackUp(0.0f);
a.setMaxRange(75.0f);
a.setBurstEnabled(true);
a.setBurstAmount(15);
a.setBurstDelay(10);
a.setBurstReplayAnimations(false);
a.setBurstOverlap(true);
a.setWindUpAnimationName("Ability_BeamDual_Windup");
a.setActiveAnimationName("Ability_BeamDual_Active");
})
);
}
@Override
public boolean allowOverlap() {
return true;
}
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
@@ -73,6 +172,63 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
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);
}
for (EntityAbilityOrb orb : orbEntities) {
fireOrbEntity(orb, target);
}
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
@@ -80,55 +236,50 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
return;
}
// Start moving the orb that was spawned during windup
if (orbEntity != null && !orbEntity.isDead) {
if (isPreview()) {
orbEntity.startPreviewFiring();
} else {
orbEntity.startMoving(target);
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);
}
}
// 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 onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
// Spawn orb in charging mode on first tick of windup
if (tick == 1) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
orbEntity = new EntityAbilityOrb(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
displayData, combatData, homingData, lightningData, lifespanData, trajectoryData);
if (isPreview()) {
orbEntity.setupPreview(caster, orbSize, displayData, lightningData, anchorData, windUpTicks);
} else {
orbEntity.setupCharging(anchorData, windUpTicks);
}
orbEntity.setEffects(this.effects);
orbEntity.setSourceAbility(this);
spawnAbilityEntity(world, orbEntity);
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (orbEntity == null || orbEntity.isDead) {
orbEntity = null;
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) {
// Nothing to clean up - entity manages itself
}
@Override
@@ -138,11 +289,15 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
@Override
public void cleanup() {
// Despawn orb entity if still alive
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.setDead();
if (orbEntities != null) {
for (int i = 0; i < orbEntities.length; i++) {
if (orbEntities[i] != null && !orbEntities[i].isDead) {
orbEntities[i].setDead();
}
orbEntities[i] = null;
}
}
orbEntity = null;
orbEntities = null;
}
@Override
@@ -151,7 +306,6 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
return null;
}
// Create small circle telegraph at target position
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
@@ -159,7 +313,6 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
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());
@@ -175,241 +328,217 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("orbSize", orbSize);
anchorData.writeNBT(nbt);
combatData.writeNBT(nbt);
nbt.setInteger("projectileCount", projectileCount);
nbt.setInteger("fireDelay", fireDelay);
// Shared visual data
displayData.writeNBT(nbt);
homingData.writeNBT(nbt);
lightningData.writeNBT(nbt);
// Shared combat/movement data
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
trajectoryData.writeNBT(nbt);
// Backward compat: old key was "orbSpeed"
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
NBTTagCompound projNbt = new NBTTagCompound();
projectiles[i].writeNBT(projNbt);
nbt.setTag("Projectile_" + i, projNbt);
}
// Backward compat write
nbt.setFloat("orbSpeed", homingData.speed);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f;
anchorData.readNBT(nbt);
int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1;
initProjectiles(count);
// Read fireDelay with fallback to old dualFireDelay key
if (nbt.hasKey("fireDelay")) {
this.fireDelay = nbt.getInteger("fireDelay");
} else if (nbt.hasKey("dualFireDelay")) {
this.fireDelay = nbt.getInteger("dualFireDelay");
} else {
this.fireDelay = 0;
}
// Shared combat/movement data
combatData.readNBT(nbt);
displayData.readNBT(nbt);
homingData.readNBT(nbt);
lightningData.readNBT(nbt);
lifespanData.readNBT(nbt);
trajectoryData.readNBT(nbt);
// Backward compat: old key was "orbSpeed"
// Determine format and read accordingly
if (nbt.hasKey("Projectile_0")) {
NBTTagCompound proj0 = nbt.getCompoundTag("Projectile_0");
if (proj0.hasKey("colorOverride")) {
// NEW format: shared visuals at root + ProjectileData per projectile
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
} else {
// OLD format: full per-projectile arrays (displayData, lightningData, etc. in each Projectile_i)
// Read primary visuals from Projectile_0
displayData.readNBT(proj0);
lightningData.readNBT(proj0);
projectiles[0].anchor.readNBT(proj0);
// Read remaining projectiles, set color override if colors differ
for (int i = 1; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
NBTTagCompound projNbt = nbt.getCompoundTag("Projectile_" + i);
projectiles[i].anchor.readNBT(projNbt);
// Check if this projectile had different colors
EnergyDisplayData tempDisplay = new EnergyDisplayData();
tempDisplay.readNBT(projNbt);
if (tempDisplay.innerColor != displayData.innerColor || tempDisplay.outerColor != displayData.outerColor) {
projectiles[i].colorOverride = true;
projectiles[i].innerColor = tempDisplay.innerColor;
projectiles[i].outerColor = tempDisplay.outerColor;
}
}
}
}
} else {
// LEGACY flat format: all data at root level
displayData.readNBT(nbt);
lightningData.readNBT(nbt);
projectiles[0].anchor.readNBT(nbt);
}
// Backward compat
if (nbt.hasKey("orbSpeed")) {
homingData.speed = nbt.getFloat("orbSpeed");
}
}
// Getters & Setters
public float getOrbSpeed() {
return homingData.speed;
}
// ═══════════════════════════════════════════════════════════════════
// API GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public void setOrbSpeed(float orbSpeed) {
homingData.speed = orbSpeed;
}
public float getOrbSpeed() { return homingData.speed; }
public void setOrbSpeed(float speed) { homingData.speed = speed; }
public float getOrbSize() {
return orbSize;
}
public float getOrbSize() { return orbSize; }
public void setOrbSize(float size) { this.orbSize = size; }
public void setOrbSize(float orbSize) {
this.orbSize = orbSize;
}
public float getMaxDistance() { return lifespanData.maxDistance; }
public void setMaxDistance(float d) { lifespanData.maxDistance = d; }
public float getMaxDistance() {
return lifespanData.maxDistance;
}
public int getMaxLifetime() { return lifespanData.maxLifetime; }
public void setMaxLifetime(int t) { lifespanData.maxLifetime = t; }
public void setMaxDistance(float maxDistance) {
lifespanData.maxDistance = maxDistance;
}
public float getDamage() { return combatData.damage; }
public void setDamage(float d) { combatData.damage = d; }
public int getMaxLifetime() {
return lifespanData.maxLifetime;
}
public float getKnockback() { return combatData.knockback; }
public void setKnockback(float k) { combatData.knockback = k; }
public void setMaxLifetime(int maxLifetime) {
lifespanData.maxLifetime = maxLifetime;
}
public float getKnockbackUp() { return combatData.knockbackUp; }
public void setKnockbackUp(float k) { combatData.knockbackUp = k; }
public float getDamage() {
return combatData.damage;
}
public boolean isHoming() { return homingData.homing; }
public void setHoming(boolean h) { homingData.homing = h; }
public void setDamage(float damage) {
combatData.damage = damage;
}
public float getHomingStrength() { return homingData.homingStrength; }
public void setHomingStrength(float s) { homingData.homingStrength = s; }
public float getKnockback() {
return combatData.knockback;
}
public float getHomingRange() { return homingData.homingRange; }
public void setHomingRange(float r) { homingData.homingRange = r; }
public void setKnockback(float knockback) {
combatData.knockback = knockback;
}
public boolean isExplosive() { return combatData.explosive; }
public void setExplosive(boolean e) { combatData.explosive = e; }
public float getKnockbackUp() {
return combatData.knockbackUp;
}
public float getExplosionRadius() { return combatData.explosionRadius; }
public void setExplosionRadius(float r) { combatData.explosionRadius = r; }
public void setKnockbackUp(float knockbackUp) {
combatData.knockbackUp = knockbackUp;
}
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
public void setExplosionDamageFalloff(float f) { combatData.explosionDamageFalloff = f; }
public boolean isHoming() {
return homingData.homing;
}
// Shared display data (primary colors)
public int getInnerColor() { return displayData.innerColor; }
public void setInnerColor(int c) { displayData.innerColor = c; }
public void setHoming(boolean homing) {
homingData.homing = homing;
}
public int getOuterColor() { return displayData.outerColor; }
public void setOuterColor(int c) { displayData.outerColor = c; }
public float getHomingStrength() {
return homingData.homingStrength;
}
public float getOuterColorWidth() { return displayData.outerColorWidth; }
public void setOuterColorWidth(float w) { displayData.outerColorWidth = w; }
public void setHomingStrength(float homingStrength) {
homingData.homingStrength = homingStrength;
}
public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
public void setOuterColorAlpha(float a) { displayData.outerColorAlpha = a; }
public float getHomingRange() {
return homingData.homingRange;
}
public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
public void setOuterColorEnabled(boolean e) { displayData.outerColorEnabled = e; }
public void setHomingRange(float homingRange) {
homingData.homingRange = homingRange;
}
public float getRotationSpeed() { return displayData.rotationSpeed; }
public void setRotationSpeed(float s) { displayData.rotationSpeed = s; }
public boolean isExplosive() {
return combatData.explosive;
}
// Shared lightning data
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
public void setLightningEffect(boolean e) { lightningData.lightningEffect = e; }
public void setExplosive(boolean explosive) {
combatData.explosive = explosive;
}
public float getLightningDensity() { return lightningData.lightningDensity; }
public void setLightningDensity(float d) { lightningData.lightningDensity = d; }
public float getExplosionRadius() {
return combatData.explosionRadius;
}
public float getLightningRadius() { return lightningData.lightningRadius; }
public void setLightningRadius(float r) { lightningData.lightningRadius = r; }
public void setExplosionRadius(float explosionRadius) {
combatData.explosionRadius = explosionRadius;
}
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
public void setLightningFadeTime(int t) { lightningData.lightningFadeTime = t; }
public float getExplosionDamageFalloff() {
return combatData.explosionDamageFalloff;
}
// Anchor - default to projectile 0
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
public void setAnchorPointEnum(AnchorPoint p) { projectiles[0].anchor.anchorPoint = p; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) {
combatData.explosionDamageFalloff = explosionDamageFalloff;
}
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
public int getInnerColor() {
return displayData.innerColor;
}
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
public void setInnerColor(int innerColor) {
displayData.innerColor = innerColor;
}
public int getOuterColor() {
return displayData.outerColor;
}
public void setOuterColor(int outerColor) {
displayData.outerColor = outerColor;
}
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 boolean isOuterColorEnabled() {
return displayData.outerColorEnabled;
}
public void setOuterColorEnabled(boolean outerColorEnabled) {
displayData.outerColorEnabled = outerColorEnabled;
}
public float getRotationSpeed() {
return displayData.rotationSpeed;
}
public void setRotationSpeed(float rotationSpeed) {
displayData.rotationSpeed = rotationSpeed;
}
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;
}
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 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) {
this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point);
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); }
private int clampIndex(int index) {
return Math.max(0, Math.min(index, projectileCount - 1));
}
@Override
@@ -417,61 +546,95 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
// ═══════════════════════════════════════════════════════════════════
// GUI FIELD DEFINITIONS
// ═══════════════════════════════════════════════════════════════════
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
),
FieldDef.row(
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
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),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.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.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).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)
.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")
// Type tab - projectile count and fire delay
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)
));
defs.add(FieldDef.row(
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
));
defs.add(FieldDef.section("ability.section.homing"));
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming));
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));
// 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),
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));
}
}
}
@@ -1,612 +0,0 @@
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.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.EntityAbilityOrb;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
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 net.minecraftforge.common.util.Constants;
import noppes.npcs.client.gui.builder.FieldDef;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.util.ValueUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class AbilityOrbBarrage extends Ability {
// Ability-specific properties
private float orbSize = 0.5f;
private int orbAmount = 16;
private int firingSpeed = 2;
private boolean dualCharging = true;
// Data classes for energy properties
private final EnergyDisplayData colorData = new EnergyDisplayData();
private final EnergyCombatData combatData = new EnergyCombatData(4.0f, 0.0f, 0.0f, false, 1.5f, 0.5f);
private final EnergyHomingData homingData = new EnergyHomingData();
private final EnergyLightningData lightningData = new EnergyLightningData();
private final EnergyLifespanData lifespanData = new EnergyLifespanData();
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND)
};
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
// Array to store entities
private transient List<EntityAbilityOrb> orbEntities = new ArrayList<>();
private transient EntityAbilityOrb[] chargingEntities = null;
// Field to keep track of the currently firing orb
private int currentOrb = 0;
public AbilityOrbBarrage() {
this.typeId = "ability.cnpc.orb_barrage";
this.name = "Orb Barrage";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 75.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animation
this.windUpAnimationName = "Ability_BeamDual_Windup";
this.activeAnimationName = "Ability_BeamDual_Active";
}
// @Override
// @SideOnly(Side.CLIENT)
// public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
// return new SubGuiAbilityOrbBarrage(this, callback);
// }
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
for (int i = 0; i < chargingEntities.length; i++) {
EntityAbilityOrb orbEntity = chargingEntities[i];
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.setDead();
}
chargingEntities[i] = null;
}
}
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
// Spawn orb in charging mode on first tick of windup
if (tick == 1) {
if (dualCharging) {
chargingEntities = new EntityAbilityOrb[2];
} else {
chargingEntities = new EntityAbilityOrb[1];
}
for (int i = 0; i < chargingEntities.length; i++) {
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[i]);
EntityAbilityOrb orbEntity = new EntityAbilityOrb(
world, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData
);
if (isPreview()) {
orbEntity.setupPreview(caster, orbSize, colorData, lightningData, anchorData[i], windUpTicks);
} else {
orbEntity.setupCharging(anchorData[i], windUpTicks);
}
orbEntity.setSourceAbility(this);
chargingEntities[i] = orbEntity;
spawnAbilityEntity(world, orbEntity);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (!orbEntities.isEmpty()) {
boolean allDead = true;
for (EntityAbilityOrb orbEntity : orbEntities) {
if (orbEntity != null && !orbEntity.isDead) {
allDead = false;
break;
}
}
if (allDead && currentOrb >= orbAmount) {
signalCompletion();
return;
}
}
if (currentOrb >= orbAmount) {
return;
}
int ticksPerOrb = Math.max(1, 20 / firingSpeed);
if (tick % ticksPerOrb != 0) {
return;
}
EnergyAnchorData anchor;
if (dualCharging) {
anchor = anchorData[currentOrb % 2];
} else {
anchor = anchorData[0];
}
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchor);
EntityAbilityOrb orb = new EntityAbilityOrb(
world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
orbSize, colorData, combatData, homingData, lightningData, lifespanData, trajectoryData
);
orb.setSourceAbility(this);
world.spawnEntityInWorld(orb);
orbEntities.add(orb);
currentOrb++;
}
@Override
public boolean canInterrupt(DamageSource source) {
// Can be interrupted if the caster is hit when the beam is moving
if (!interruptible) {
return false;
}
// Only direct physical hits can interrupt, not magic, fire, or other indirect damage
if (source == null) {
return false;
}
// Reject indirect damage types
if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion()) {
return false;
}
// Reject damage without a direct attacker entity
if (source.getEntity() == null) {
return false;
}
// Direct hit from an entity - can interrupt
return true;
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
currentOrb = 0;
if (chargingEntities != null) {
for (int i = 0; i < chargingEntities.length; i++) {
if (chargingEntities[i] != null && !chargingEntities[i].isDead) {
chargingEntities[i].setDead();
}
chargingEntities[i] = null;
}
chargingEntities = null;
}
if (orbEntities != null && !orbEntities.isEmpty()) {
for (EntityAbilityOrb orbEntity : orbEntities) {
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.setDead();
}
}
orbEntities.clear();
}
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create small circle telegraph at target position
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, 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) {
nbt.setFloat("orbSize", orbSize);
nbt.setInteger("orbAmount", orbAmount);
nbt.setInteger("firingSpeed", firingSpeed);
nbt.setBoolean("dualCharging", dualCharging);
colorData.writeNBT(nbt);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lightningData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
trajectoryData.writeNBT(nbt);
for (int i = 0; i < anchorData.length; i++) {
NBTTagCompound comp = new NBTTagCompound();
anchorData[i].writeNBT(comp);
nbt.setTag("Anchor_" + i, comp);
}
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f;
this.orbAmount = nbt.hasKey("orbAmount") ? nbt.getInteger("orbAmount") : 16;
this.firingSpeed = nbt.hasKey("firingSpeed") ? nbt.getInteger("firingSpeed") : 2;
this.dualCharging = !nbt.hasKey("dualCharging") || nbt.getBoolean("dualCharging");
colorData.readNBT(nbt);
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lightningData.readNBT(nbt);
lifespanData.readNBT(nbt);
trajectoryData.readNBT(nbt);
for (int i = 0; i < anchorData.length; i++) {
if (nbt.hasKey("Anchor_" + i, Constants.NBT.TAG_COMPOUND)) {
anchorData[i].readNBT(nbt.getCompoundTag("Anchor_" + i));
} else {
anchorData[i] = new EnergyAnchorData(i == 0 ? AnchorPoint.RIGHT_HAND : AnchorPoint.LEFT_HAND);
}
}
}
// Getters & Setters
public float getOrbSpeed() {
return homingData.speed;
}
public void setOrbSpeed(float orbSpeed) {
homingData.speed = orbSpeed;
}
public float getOrbSize() {
return orbSize;
}
public void setOrbSize(float orbSize) {
this.orbSize = orbSize;
}
public int getOrbAmount() { return orbAmount; }
public void setOrbAmount(int orbAmount) { this.orbAmount = orbAmount; }
public int getFiringSpeed() { return firingSpeed; }
public void setFiringSpeed(int firingSpeed) { this.firingSpeed = firingSpeed; }
public boolean isDualCharging() { return dualCharging; }
public void setDualCharging(boolean dualCharging) { this.dualCharging = dualCharging; }
public float getMaxDistance() {
return lifespanData.maxDistance;
}
public void setMaxDistance(float maxDistance) {
lifespanData.maxDistance = maxDistance;
}
public int getMaxLifetime() {
return lifespanData.maxLifetime;
}
public void setMaxLifetime(int maxLifetime) {
lifespanData.maxLifetime = maxLifetime;
}
public float getDamage() {
return combatData.damage;
}
public void setDamage(float damage) {
combatData.damage = damage;
}
public float getKnockback() {
return combatData.knockback;
}
public void setKnockback(float knockback) {
combatData.knockback = knockback;
}
public float getKnockbackUp() {
return combatData.knockbackUp;
}
public void setKnockbackUp(float knockbackUp) {
combatData.knockbackUp = knockbackUp;
}
public boolean isHoming() {
return homingData.homing;
}
public void setHoming(boolean homing) {
homingData.homing = homing;
}
public float getHomingStrength() {
return homingData.homingStrength;
}
public void setHomingStrength(float homingStrength) {
homingData.homingStrength = homingStrength;
}
public float getHomingRange() {
return homingData.homingRange;
}
public void setHomingRange(float homingRange) {
homingData.homingRange = homingRange;
}
public boolean isExplosive() {
return combatData.explosive;
}
public void setExplosive(boolean explosive) {
combatData.explosive = explosive;
}
public float getExplosionRadius() {
return combatData.explosionRadius;
}
public void setExplosionRadius(float explosionRadius) {
combatData.explosionRadius = explosionRadius;
}
public float getExplosionDamageFalloff() {
return combatData.explosionDamageFalloff;
}
public void setExplosionDamageFalloff(float explosionDamageFalloff) {
combatData.explosionDamageFalloff = explosionDamageFalloff;
}
public int getInnerColor() {
return colorData.innerColor;
}
public void setInnerColor(int innerColor) {
colorData.innerColor = innerColor;
}
public int getOuterColor() {
return colorData.outerColor;
}
public void setOuterColor(int outerColor) {
colorData.outerColor = outerColor;
}
public float getOuterColorWidth() {
return colorData.outerColorWidth;
}
public void setOuterColorWidth(float outerColorWidth) {
colorData.outerColorWidth = outerColorWidth;
}
public float getOuterColorAlpha() {
return colorData.outerColorAlpha;
}
public void setOuterColorAlpha(float outerColorAlpha) {
colorData.outerColorAlpha = outerColorAlpha;
}
public boolean isOuterColorEnabled() {
return colorData.outerColorEnabled;
}
public void setOuterColorEnabled(boolean outerColorEnabled) {
colorData.outerColorEnabled = outerColorEnabled;
}
public float getRotationSpeed() {
return colorData.rotationSpeed;
}
public void setRotationSpeed(float rotationSpeed) {
colorData.rotationSpeed = rotationSpeed;
}
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;
}
private EnergyAnchorData getAnchorData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return anchorData[orb];
}
public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; }
public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; }
public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; }
public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; }
public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; }
public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; }
public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; }
public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; }
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
//@Override
public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); }
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
//@Override
public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
),
FieldDef.row(
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.barrage"),
FieldDef.row(
FieldDef.intField("ability.amount", this::getOrbAmount, this::setOrbAmount),
FieldDef.intField("ability.firingSpeed", this::getFiringSpeed, this::setFiringSpeed)
),
FieldDef.boolField("ability.dualCharge", this::isDualCharging, this::setDualCharging),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
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),
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
.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.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).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")
));
}
}
@@ -1,600 +0,0 @@
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.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.EntityAbilityOrb;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.util.ValueUtil;
import java.util.Arrays;
import java.util.List;
public class AbilityOrbDual extends Ability {
// Ability-specific properties
private float orbSize = 1.0f;
private boolean dualFire= true;
private int dualFireDelay = 5;
// Data classes for energy properties
public final EnergyDisplayData[] displayData = new EnergyDisplayData[]{
new EnergyDisplayData(), new EnergyDisplayData()
};
public final EnergyCombatData combatData = new EnergyCombatData();
public final EnergyHomingData homingData = new EnergyHomingData();
public final EnergyLightningData[] lightningData = new EnergyLightningData[]{
new EnergyLightningData(), new EnergyLightningData()
};
public final EnergyLifespanData lifespanData = new EnergyLifespanData();
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND)
};
private EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{
new EnergyTrajectoryData(), new EnergyTrajectoryData()
};
// Transient state for orb entity (used during windup charging)
private transient EntityAbilityOrb orbEntity = null;
private transient EntityAbilityOrb orbEntity2 = null;
public AbilityOrbDual() {
this.typeId = "ability.cnpc.orb_dual";
this.name = "Orb Dual";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 25.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.lockMovement = LockMovementType.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
// Default built-in animation
this.windUpAnimationName = "Ability_OrbDual_Windup";
this.activeAnimationName = "Ability_OrbDual_Active";
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
if (world.isRemote && !isPreview()) {
signalCompletion();
return;
}
// Start moving the orb that was spawned during windup
if (orbEntity != null && !orbEntity.isDead) {
if (isPreview()) {
orbEntity.startPreviewFiring();
} else {
orbEntity.startMoving(target);
}
}
if (dualFire) {
if (orbEntity2 != null && !orbEntity2.isDead && dualFireDelay <= 0) {
if (isPreview()) {
orbEntity2.startPreviewFiring();
} else {
orbEntity2.startMoving(target);
}
}
} else if (orbEntity2 != null) {
orbEntity2.setDead();
}
// 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 onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (world.isRemote && !isPreview()) return;
// Spawn orb in charging mode on first tick of windup
if (tick == 1) {
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]);
orbEntity = new EntityAbilityOrb(
world, caster, target,
spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, orbSize,
displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0]);
Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]);
orbEntity2 = new EntityAbilityOrb(
world, caster, target,
spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, orbSize,
displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1]);
orbEntity.setSiblingEntityId(orbEntity2.getEntityId());
orbEntity2.setSiblingEntityId(orbEntity.getEntityId());
if (isPreview()) {
orbEntity.setupPreview(caster, orbSize, displayData[0], lightningData[0], anchorData[0], windUpTicks);
orbEntity2.setupPreview(caster, orbSize, displayData[1], lightningData[1], anchorData[1], windUpTicks);
} else {
orbEntity.setupCharging(anchorData[0], windUpTicks);
orbEntity2.setupCharging(anchorData[1], windUpTicks);
}
orbEntity.setEffects(this.effects);
if (dualFire) {
orbEntity2.setEffects(this.effects);
}
orbEntity.setSourceAbility(this);
orbEntity2.setSourceAbility(this);
spawnAbilityEntity(world, orbEntity);
spawnAbilityEntity(world, orbEntity2);
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
// Signal completion when entity dies
if (dualFire) {
if ((orbEntity == null || orbEntity.isDead) && (orbEntity2 == null || orbEntity2.isDead)) {
orbEntity = null;
orbEntity2 = null;
signalCompletion();
}
if (tick == dualFireDelay && orbEntity2 != null && !orbEntity2.isDead) {
orbEntity2.startMoving(target);
}
} else {
if (orbEntity == null || orbEntity.isDead) {
orbEntity = null;
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() {
// Despawn orb entity if still alive
if (orbEntity != null && !orbEntity.isDead) {
orbEntity.setDead();
}
if (orbEntity2 != null && !orbEntity2.isDead) {
orbEntity2.setDead();
}
orbEntity = null;
orbEntity2 = null;
}
@Override
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
return null;
}
// Create small circle telegraph at target position
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at target and follow target during windup
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, 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) {
nbt.setFloat("orbSize", orbSize);
nbt.setBoolean("dualFire", dualFire);
nbt.setInteger("dualFireDelay", dualFireDelay);
combatData.writeNBT(nbt);
homingData.writeNBT(nbt);
lifespanData.writeNBT(nbt);
// Backward compat: old key was "orbSpeed"
nbt.setFloat("orbSpeed", homingData.speed);
NBTTagCompound orb1 = new NBTTagCompound();
NBTTagCompound orb2 = new NBTTagCompound();
for (int i = 0; i < 2; i++) {
NBTTagCompound orb = i == 0 ? orb1 : orb2;
anchorData[i].writeNBT(orb);
displayData[i].writeNBT(orb);
lightningData[i].writeNBT(orb);
trajectoryData[i].writeNBT(orb);
}
nbt.setTag("Orb_1", orb1);
nbt.setTag("Orb_2", orb2);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
NBTTagCompound orb1 = nbt.hasKey("Orb_1") ? nbt.getCompoundTag("Orb_1") : new NBTTagCompound();
NBTTagCompound orb2 = nbt.hasKey("Orb_2") ? nbt.getCompoundTag("Orb_2") : new NBTTagCompound();
this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f;
this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire");
this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0;
combatData.readNBT(nbt);
homingData.readNBT(nbt);
lifespanData.readNBT(nbt);
// Backward compat: old key was "orbSpeed"
if (nbt.hasKey("orbSpeed")) {
homingData.speed = nbt.getFloat("orbSpeed");
}
for (int i = 0; i < 2; i++) {
NBTTagCompound orb = i == 0 ? orb1 : orb2;
anchorData[i].readNBT(orb);
displayData[i].readNBT(orb);
lightningData[i].readNBT(orb);
trajectoryData[i].readNBT(orb);
}
}
// Getters & Setters
private EnergyDisplayData getColorData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return displayData[orb];
}
private EnergyLightningData getLightningData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return lightningData[orb];
}
private EnergyAnchorData getAnchorData(int orb) {
orb = ValueUtil.clamp(orb, 0, 1);
return anchorData[orb];
}
public float getOrbSpeed() {
return homingData.speed;
}
public void setOrbSpeed(float orbSpeed) {
homingData.speed = orbSpeed;
}
public float getOrbSize() {
return orbSize;
}
public void setOrbSize(float orbSize) {
this.orbSize = orbSize;
}
public boolean isDualFire() {
return dualFire;
}
public void setDualFire(boolean dualFire) {
this.dualFire = dualFire;
}
public int getDualFireDelay() {
return dualFireDelay;
}
public void setDualFireDelay(int dualFireDelay) {
this.dualFireDelay = dualFireDelay;
}
public float getMaxDistance() {
return lifespanData.maxDistance;
}
public void setMaxDistance(float maxDistance) {
lifespanData.maxDistance = maxDistance;
}
public int getMaxLifetime() {
return lifespanData.maxLifetime;
}
public void setMaxLifetime(int maxLifetime) {
lifespanData.maxLifetime = maxLifetime;
}
public float getDamage() {
return combatData.damage;
}
public void setDamage(float damage) {
combatData.damage = damage;
}
public float getKnockback() {
return combatData.knockback;
}
public void setKnockback(float knockback) {
combatData.knockback = knockback;
}
public float getKnockbackUp() {
return combatData.knockbackUp;
}
public void setKnockbackUp(float knockbackUp) {
combatData.knockbackUp = knockbackUp;
}
public boolean isHoming() {
return homingData.homing;
}
public void setHoming(boolean homing) {
homingData.homing = homing;
}
public float getHomingStrength() {
return homingData.homingStrength;
}
public void setHomingStrength(float homingStrength) {
homingData.homingStrength = homingStrength;
}
public float getHomingRange() {
return homingData.homingRange;
}
public void setHomingRange(float homingRange) {
homingData.homingRange = homingRange;
}
public boolean isExplosive() {
return combatData.explosive;
}
public void setExplosive(boolean explosive) {
combatData.explosive = explosive;
}
public float getExplosionRadius() {
return combatData.explosionRadius;
}
public void setExplosionRadius(float explosionRadius) {
combatData.explosionRadius = explosionRadius;
}
public float getExplosionDamageFalloff() {
return combatData.explosionDamageFalloff;
}
public void setExplosionDamageFalloff(float explosionDamageFalloff) {
combatData.explosionDamageFalloff = explosionDamageFalloff;
}
public int getInnerColor(int orb) { return getColorData(orb).innerColor; }
public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; }
public int getOuterColor(int orb) { return getColorData(orb).outerColor; }
public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; }
public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; }
public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; }
public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; }
public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; }
public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; }
public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; }
public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; }
public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; }
public int getInnerColor() { return getInnerColor(0); }
public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); }
public int getOuterColor() { return getOuterColor(0); }
public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); }
public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); }
public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); }
public float getOuterColorWidth() { return getOuterColorWidth(0); }
public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); }
public float getOuterColorAlpha() { return getOuterColorAlpha(0); }
public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); }
public float getRotationSpeed() { return getRotationSpeed(0); }
public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); }
public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; }
public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; }
public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; }
public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; }
public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; }
public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; }
public boolean hasLightningEffect() { return hasLightningEffect(0); }
public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); }
public float getLightningDensity() { return getLightningDensity(0); }
public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); }
public float getLightningRadius() { return getLightningRadius(0); }
public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); }
public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; }
public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; }
public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; }
public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; }
public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; }
public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; }
public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; }
public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; }
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
//@Override
public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); }
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
//@Override
public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); }
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
// Type tab
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
),
FieldDef.row(
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
),
FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
),
FieldDef.section("ability.section.dual"),
FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire),
FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay)
.visibleWhen(this::isDualFire),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
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 - Orb 1
FieldDef.section("ability.section.orb1").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(0), v -> setInnerColor(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(0), v -> setOuterColor(0, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v))
.visibleWhen(() -> isOuterColorEnabled(0)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(0))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(0), v -> setRotationSpeed(0, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(0), v -> setLightningEffect(0, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(0), v -> setLightningDensity(0, v))
.range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(0), v -> setLightningRadius(0, v))
.range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0))
).tab("ability.tab.visual"),
// Visual tab - Orb 2
FieldDef.section("ability.section.orb2").tab("ability.tab.visual"),
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)),
FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.innerColor",
() -> getInnerColor(1), v -> setInnerColor(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled",
() -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v))
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor",
() -> getOuterColor(1), v -> setOuterColor(1, v))
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.row(
FieldDef.floatField("ability.outerWidth",
() -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v))
.visibleWhen(() -> isOuterColorEnabled(1)),
FieldDef.floatField("ability.outerAlpha",
() -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v))
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(1))
).tab("ability.tab.visual"),
FieldDef.floatField("ability.rotationSpeed",
() -> getRotationSpeed(1), v -> setRotationSpeed(1, v))
.tab("ability.tab.visual"),
FieldDef.boolField("ability.lightning",
() -> hasLightningEffect(1), v -> setLightningEffect(1, v))
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density",
() -> getLightningDensity(1), v -> setLightningDensity(1, v))
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)),
FieldDef.floatField("gui.radius",
() -> getLightningRadius(1), v -> setLightningRadius(1, v))
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1))
).tab("ability.tab.visual")
));
}
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
}
@@ -50,6 +50,11 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
this.showTelegraph = false;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
@@ -61,6 +61,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
this.showTelegraph = true;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
@@ -85,6 +85,11 @@ public class AbilityTrap extends Ability implements IAbilityTrap {
this.telegraphType = TelegraphType.CIRCLE;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
+43 -1
View File
@@ -174,11 +174,23 @@ public class DataAbilities {
// Handle phase-specific logic
switch (currentAbility.getPhase()) {
case WINDUP:
if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) {
// Burst replay: re-enter windup - set up telegraph, sound, animation, locks
if (currentAbility.isRotationLockedDuringWindup()) {
captureLockedRotation();
}
if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) {
captureLockedPosition();
}
spawnTelegraph(currentAbility, target);
playAbilitySound(currentAbility.getWindUpSound());
playAbilityAnimation(currentAbility.getWindUpAnimation());
}
currentAbility.onWindUpTick(npc, target, npc.worldObj, currentAbility.getCurrentTick());
break;
case ACTIVE:
if (phaseChanged && oldPhase == AbilityPhase.WINDUP) {
if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) {
// Just entered ACTIVE phase - lock telegraph position if it was following
// This commits the ability to its current target position
TelegraphInstance telegraph = currentAbility.getTelegraphInstance();
@@ -207,6 +219,11 @@ 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
playAbilitySound(currentAbility.getActiveSound());
playAbilityAnimation(currentAbility.getActiveAnimation());
@@ -240,6 +257,31 @@ public class DataAbilities {
handleAbilityCompletion(target);
return;
}
// Auto-complete for burst overlap mode (entities fly independently)
// This allows entity-spawning abilities to immediately chain next burst
// without each type needing to check burst state in their onActiveTick()
// Wait until all staggered projectiles have been fired before auto-completing
if (currentAbility.isBurstEnabled() && currentAbility.isBurstOverlap()
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
currentAbility.signalCompletion();
}
// Check if ability entered burst delay during onActiveTick or overlap auto-complete
if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) {
// Release all locks - free movement/rotation during delay
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
if (hitScanActive) releaseRotationControl();
}
break;
case BURST_DELAY:
// Free movement and rotation during burst delay
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
break;
case DAZED:
@@ -3,6 +3,7 @@ package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.controllers.data.ability.AbilitySlot;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket;
import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket;
@@ -61,6 +62,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
// Existing preset names for duplicate checking
private final Set<String> existingPresetNames = new HashSet<>();
// Pending variant selection
private String pendingTypeId = null;
public static int modIndex = 0;
public static ScrollType scrollType = ScrollType.CNPC;
@@ -197,8 +201,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
String displayName = availableTypesScroll.getSelected();
String typeId = displayNameToTypeId.get(displayName);
if (typeId != null) {
java.util.List<AbilityVariant> variants = AbilityController.Instance.getVariantsForType(typeId);
if (variants.size() > 1) {
pendingTypeId = typeId;
setSubGui(new SubGuiAbilityVariantSelect(variants));
return;
}
Ability newAbility = AbilityController.Instance.create(typeId);
if (newAbility != null) {
if (variants.size() == 1) {
variants.get(0).apply(newAbility);
}
newAbility.setId(UUID.randomUUID().toString());
npcSlots.add(AbilitySlot.inline(newAbility));
selectedAbilityIndex = npcSlots.size() - 1;
@@ -633,7 +646,24 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
@Override
public void subGuiClosed(SubGuiInterface subgui) {
if (subgui instanceof SubGuiAbilityEditMode) {
if (subgui instanceof SubGuiAbilityVariantSelect) {
SubGuiAbilityVariantSelect variantGui = (SubGuiAbilityVariantSelect) subgui;
int idx = variantGui.getSelectedIndex();
if (idx >= 0 && pendingTypeId != null) {
Ability newAbility = AbilityController.Instance.create(pendingTypeId);
if (newAbility != null) {
variantGui.getVariants().get(idx).apply(newAbility);
newAbility.setId(UUID.randomUUID().toString());
npcSlots.add(AbilitySlot.inline(newAbility));
selectedAbilityIndex = npcSlots.size() - 1;
updateNpcAbilitiesList();
selectAbilityByIndex(selectedAbilityIndex);
save();
}
}
pendingTypeId = null;
initGui();
} else if (subgui instanceof SubGuiAbilityEditMode) {
int mode = ((SubGuiAbilityEditMode) subgui).getResult();
if (mode < 0) return; // cancelled
@@ -324,7 +324,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
@Override
public void unFocused(GuiNpcTextField textField) {
if (textField.id < DECLARATIVE_ID_START) return;
builder.handleTextFieldEvent(textField.id, textField);
if (builder.handleTextFieldEvent(textField.id, textField)) {
initGui();
}
}
// ═══════════════════════════════════════════════════════════════════════════
@@ -0,0 +1,92 @@
package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.util.StatCollector;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiScrollWindow;
import noppes.npcs.client.gui.util.SubGuiInterface;
import java.util.ArrayList;
import java.util.List;
/**
* SubGui for selecting a variant/template when creating a new ability.
* Uses a scroll window so external mods can register many variants.
*/
public class SubGuiAbilityVariantSelect extends SubGuiInterface {
private static final int VARIANT_BTN_START = 10;
private static final int BTN_H = 20;
private static final int BTN_PAD = 2;
private final List<AbilityVariant> variants;
private final List<String> displayNames = new ArrayList<>();
private int selectedIndex = -1;
public SubGuiAbilityVariantSelect(List<AbilityVariant> variants) {
this.variants = variants;
for (AbilityVariant variant : variants) {
displayNames.add(StatCollector.translateToLocal(variant.getDisplayKey()));
}
setBackground("menubg.png");
xSize = 200;
ySize = 216;
}
@Override
public void initGui() {
super.initGui();
addLabel(new GuiNpcLabel(0, "ability.selectVariant", guiLeft + 10, guiTop + 8));
// Scroll window for variant buttons
int swX = guiLeft + 5;
int swY = guiTop + 22;
int swW = 190;
int swH = 155;
GuiScrollWindow sw = new GuiScrollWindow(this, swX, swY, swW, swH, 0);
addScrollableGui(0, sw); // Must register BEFORE adding components (initGui clears them)
int localY = 2;
int btnW = swW - 14; // Leave room for scrollbar
for (int i = 0; i < displayNames.size(); i++) {
GuiNpcButton btn = new GuiNpcButton(VARIANT_BTN_START + i, 2, localY, btnW, BTN_H, displayNames.get(i));
if (i == selectedIndex) {
btn.packedFGColour = 0x55FF55;
}
sw.addButton(btn);
localY += BTN_H + BTN_PAD;
}
sw.maxScrollY = Math.max(localY - swH, 0);
// Select / Cancel buttons
addButton(new GuiNpcButton(0, guiLeft + 5, guiTop + 188, 90, 20, "gui.select"));
getButton(0).setEnabled(selectedIndex >= 0);
addButton(new GuiNpcButton(1, guiLeft + 105, guiTop + 188, 90, 20, "gui.cancel"));
}
@Override
public void buttonEvent(GuiButton guibutton) {
int id = guibutton.id;
if (id == 0 && selectedIndex >= 0) {
close();
} else if (id == 1) {
selectedIndex = -1;
close();
} else if (id >= VARIANT_BTN_START && id < VARIANT_BTN_START + displayNames.size()) {
selectedIndex = id - VARIANT_BTN_START;
initGui();
}
}
public int getSelectedIndex() {
return selectedIndex;
}
public List<AbilityVariant> getVariants() {
return variants;
}
}
@@ -120,6 +120,9 @@ public class GuiFieldBuilder {
continue;
}
// Skip invisible fields
if (!def.isVisible()) continue;
// Section header
if (def.getType() == FieldType.SECTION_HEADER) {
y += 3;
@@ -391,7 +394,7 @@ public class GuiFieldBuilder {
/**
* Handles text field unfocus events for declarative fields.
* Call from parent GUI's unFocused(). Returns true if the event was handled.
* Call from parent GUI's unFocused(). Returns true if the value actually changed.
*/
public boolean handleTextFieldEvent(int textFieldId, GuiNpcTextField field) {
FieldDef def = textFieldMap.get(textFieldId);
@@ -399,14 +402,36 @@ public class GuiFieldBuilder {
switch (def.getType()) {
case FLOAT:
try { def.setValue(Float.parseFloat(field.getText())); } catch (NumberFormatException ignored) {}
return true;
try {
float newVal = Float.parseFloat(field.getText());
Object old = def.getValue();
float oldVal = old instanceof Number ? ((Number) old).floatValue() : 0f;
if (newVal != oldVal) {
def.setValue(newVal);
return true;
}
} catch (NumberFormatException ignored) {}
return false;
case INT:
try { def.setValue(Integer.parseInt(field.getText())); } catch (NumberFormatException ignored) {}
return true;
try {
int newVal = Integer.parseInt(field.getText());
Object old = def.getValue();
int oldVal = old instanceof Number ? ((Number) old).intValue() : 0;
if (newVal != oldVal) {
def.setValue(newVal);
return true;
}
} catch (NumberFormatException ignored) {}
return false;
case STRING:
def.setValue(field.getText());
return true;
String newVal = field.getText();
Object old = def.getValue();
String oldVal = old != null ? old.toString() : "";
if (!newVal.equals(oldVal)) {
def.setValue(newVal);
return true;
}
return false;
default:
return false;
}
@@ -16,8 +16,10 @@ import net.minecraft.client.gui.GuiYesNoCallback;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.StatCollector;
import noppes.npcs.client.NoppesUtil;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.SubGuiAbilityTypeSelect;
import noppes.npcs.client.gui.advanced.SubGuiAbilityVariantSelect;
import noppes.npcs.client.gui.advanced.SubGuiDuplicateNameConfirm;
import noppes.npcs.client.gui.util.AbilityPreviewExecutor;
import noppes.npcs.client.gui.util.GuiAbilityInterface;
@@ -68,6 +70,9 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
private Ability pendingSaveAbility = null;
private boolean pendingNewCreation = false;
// ==================== PENDING VARIANT ====================
private String pendingTypeId = null;
// ==================== PREVIEW ====================
private AbilityPreviewExecutor previewExecutor;
private long prevTick = 0;
@@ -343,11 +348,41 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
@Override
public void subGuiClosed(SubGuiInterface subgui) {
if (subgui instanceof SubGuiAbilityTypeSelect) {
if (subgui instanceof SubGuiAbilityVariantSelect) {
SubGuiAbilityVariantSelect variantGui = (SubGuiAbilityVariantSelect) subgui;
int idx = variantGui.getSelectedIndex();
if (idx >= 0 && pendingTypeId != null) {
Ability newAbility = AbilityController.Instance.create(pendingTypeId);
if (newAbility != null) {
variantGui.getVariants().get(idx).apply(newAbility);
newAbility.setId(UUID.randomUUID().toString());
if (hasDuplicateName(newAbility)) {
pendingSaveAbility = newAbility;
pendingNewCreation = true;
pendingTypeId = null;
setSubGui(new SubGuiDuplicateNameConfirm());
return;
}
pendingTypeId = null;
saveAndOpenConfig(newAbility);
return;
}
}
pendingTypeId = null;
} else if (subgui instanceof SubGuiAbilityTypeSelect) {
String typeId = ((SubGuiAbilityTypeSelect) subgui).getSelectedTypeId();
if (typeId != null) {
java.util.List<AbilityVariant> variants = AbilityController.Instance.getVariantsForType(typeId);
if (variants.size() > 1) {
pendingTypeId = typeId;
setSubGui(new SubGuiAbilityVariantSelect(variants));
return;
}
Ability newAbility = AbilityController.Instance.create(typeId);
if (newAbility != null) {
if (variants.size() == 1) {
variants.get(0).apply(newAbility);
}
newAbility.setId(UUID.randomUUID().toString());
if (hasDuplicateName(newAbility)) {
pendingSaveAbility = newAbility;