diff --git a/src/api b/src/api index fe1cec898..8917e37a6 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit fe1cec89814290a745ac708b309703125b5a3c15 +Subproject commit 8917e37a6d35230a5c7b75ac8f89428edf1aabd2 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 65d941327..49ffbf587 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -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 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 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 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). */ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 05110f553..294f9a765 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -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 fieldProviders = new ArrayList<>(); + /** External variant definitions registered by addons, keyed by typeId. */ + private final Map> 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 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 getVariantsForType(String typeId) { + List result = new ArrayList<>(); + Ability temp = create(typeId); + if (temp != null) { + result.addAll(temp.getVariants()); + } + List ext = externalVariants.get(typeId); + if (ext != null) { + result.addAll(ext); + } + return result; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java index 0611a0274..af1bd8d30 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java @@ -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) } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java new file mode 100644 index 000000000..91d151a85 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java @@ -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 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 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); + } + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java new file mode 100644 index 000000000..49ee50f7e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java @@ -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; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 356c1c4e9..5f12cc183 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -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 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 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)); + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java deleted file mode 100644 index daf04a446..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java +++ /dev/null @@ -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 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; - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 98fe9eaac..d38d6a0b2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -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 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 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)); + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java deleted file mode 100644 index 87d7fffd5..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java +++ /dev/null @@ -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 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; - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index ec9f157f9..477be2cc4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -70,6 +70,11 @@ public class AbilityGuard extends Ability implements IAbilityGuard { return false; } + @Override + public boolean allowBurst() { + return false; + } + public enum CounterType { FLAT, PERCENT; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 112b2a637..70696eb8f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index b60c46d9d..d913fe586 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index d5b48f1fd..7ddfcc0e7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -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 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 1f1cb5e7d..024fe2912 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -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 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 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)); + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java deleted file mode 100644 index f384c4b71..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java +++ /dev/null @@ -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 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 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") - )); - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java deleted file mode 100644 index 3cabf4892..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java +++ /dev/null @@ -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 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; - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 75c27a507..b6657ac60 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 3155310ef..9be02101f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 1623bacd8..5a540d634 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -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; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 606df7742..5e1ff694f 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -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: diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index fd8461245..7bc5c3846 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -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 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 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 diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index c8802a1e0..bcfce7687 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -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(); + } } // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java new file mode 100644 index 000000000..09d875dbe --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java @@ -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 variants; + private final List displayNames = new ArrayList<>(); + private int selectedIndex = -1; + + public SubGuiAbilityVariantSelect(List 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 getVariants() { + return variants; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java index 42e117ecf..b1f123956 100644 --- a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java +++ b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java @@ -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; } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index f911c6f43..b06f9d097 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -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 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; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 497134b16..3238d2851 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1605,11 +1605,8 @@ ability.cnpc.slam=Slam ability.cnpc.heavy_hit=Heavy Hit ability.cnpc.cutter=Cutter ability.cnpc.beam=Beam -ability.cnpc.beam_dual=Beam Dual ability.cnpc.projectile=Projectile ability.cnpc.orb=Orb -ability.cnpc.orb_dual=Orb Dual -ability.cnpc.orb_barrage=Orb Barrage ability.cnpc.charge=Charge ability.cnpc.dash=Dash ability.cnpc.teleport=Teleport @@ -1620,7 +1617,6 @@ ability.cnpc.heal=Heal ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap ability.cnpc.disc=Disc -ability.cnpc.disc_dual=Disc Dual ability.cnpc.laser_shot=Laser Shot ability.cnpc.sweeper=Sweeper @@ -1674,14 +1670,16 @@ ability.section.boomerang=Boomerang ability.section.explosive=Explosive ability.section.colors=Colors ability.section.effects=Effects -ability.section.dual=Dual -ability.section.beam1=Beam 1 -ability.section.beam2=Beam 2 -ability.section.disc1=Disc 1 -ability.section.disc2=Disc 2 -ability.section.orb1=Orb 1 -ability.section.orb2=Orb 2 -ability.section.barrage=Barrage +ability.section.burst=Burst +ability.section.visual=Visual +ability.section.projectile1=Projectile 1 +ability.section.projectile2=Projectile 2 +ability.section.projectile3=Projectile 3 +ability.section.projectile4=Projectile 4 +ability.section.projectile5=Projectile 5 +ability.section.projectile6=Projectile 6 +ability.section.projectile7=Projectile 7 +ability.section.projectile8=Projectile 8 ability.section.counter=Counter ability.section.area=Area ability.section.damage=Damage @@ -1828,9 +1826,20 @@ ability.lightning=Lightning ability.lightningDensity=Density ability.lightningRadius=Radius -# Dual Energy Projectile Settings -ability.dualFire=Dual Fire +# Projectile Count / Settings +ability.projectileCount=Count ability.dualFireDelay=Fire Delay +ability.fireDelay=Fire Delay +ability.colorOverride=Color Override + +# Burst System +ability.burstEnabled=Burst +ability.burstAmount=Amount +ability.burstDelay=Delay +ability.burstReplayAnimations=Replay Animations +ability.burstOverlap=Overlap +ability.hover.burstOverlap=When enabled, projectiles from previous bursts keep flying while new ones fire +ability.hover.burstReplay=Replay windup and active animations for each burst iteration # Ability Save/Load ability.save.confirm=Save ability preset @@ -1848,6 +1857,12 @@ ability.edit.mode=How should this ability be edited? ability.cloneModify=Clone & Edit ability.modifyParent=Edit Parent +# Ability Variant Selection +ability.selectVariant=Select Variant +ability.variant.single=Single +ability.variant.dual=Dual +ability.variant.barrage=Barrage + # Ability Type Selection ability.selectType=Select Ability Type: