package kamkeel.npcs.controllers.data.ability; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.block.Block; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.DataAbilities; import noppes.npcs.NpcDamageSource; import noppes.npcs.api.INbt; import noppes.npcs.api.ability.IAbility; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import java.util.ArrayList; import java.util.Collections; import java.util.List; import net.minecraft.entity.Entity; import somehussar.gui.annotationHandling.GuiEditable; @GuiEditable public abstract class Ability implements IAbility { // ═══════════════════════════════════════════════════════════════════ // CONFIGURATION (saved to NBT) // ═══════════════════════════════════════════════════════════════════ @GuiEditable.Field("id") protected String id; @GuiEditable.Field("name") protected String name; @GuiEditable.Field("test") protected String typeId; // e.g., "ability.cnpc.slam" (also used as lang key) // Selection protected int weight = 10; protected boolean enabled = true; protected List conditions = new ArrayList<>(); // Targeting protected TargetingMode targetingMode = TargetingMode.AGGRO_TARGET; protected float minRange = 0; protected float maxRange = 20; // Timing (ticks) protected int cooldownTicks = 0; // Added to global cooldown after ability completes protected int windUpTicks = 20; protected int dazedTicks = 80; // Only used when interrupted during WINDUP (if interruptible) // Interruption protected boolean interruptible = true; // Feedback protected LockMovementType lockMovement = LockMovementType.WINDUP; protected int windUpColor = 0x80FF4400; // Telegraph color during wind up protected int activeColor = 0xC0FF0000; // Telegraph warning/active color // Sounds protected String windUpSound = ""; // Sound to play when wind up starts protected String activeSound = ""; // Sound to play when active phase starts // Animations (global animation IDs, -1 = none) protected int windUpAnimationId = -1; // Animation to play during wind up (user animations) protected int activeAnimationId = -1; // Animation to play during active phase (user animations) // Built-in animation names (empty = none, takes priority over IDs if set) protected String windUpAnimationName = ""; // Built-in animation name for wind up protected String activeAnimationName = ""; // Built-in animation name for active phase // Telegraph configuration protected boolean showTelegraph = true; protected TelegraphType telegraphType = TelegraphType.CIRCLE; protected float telegraphHeightOffset = 0.1f; // Custom data for external mods protected NBTTagCompound customData = new NBTTagCompound(); // Configurable potion effects protected List effects = new ArrayList<>(); // ═══════════════════════════════════════════════════════════════════ // EXECUTION STATE (not saved, reset each combat) // ═══════════════════════════════════════════════════════════════════ protected transient AbilityPhase phase = AbilityPhase.IDLE; protected transient int currentTick = 0; protected transient EntityLivingBase currentTarget; protected transient long executionStartTime; protected transient TelegraphInstance telegraphInstance; // ═══════════════════════════════════════════════════════════════════ // ABSTRACT METHODS // ═══════════════════════════════════════════════════════════════════ /** * Called first tick of ACTIVE phase */ public abstract void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world); /** * Called every tick of ACTIVE phase */ public abstract void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick); /** * Write type-specific config to NBT */ public abstract void writeTypeNBT(NBTTagCompound nbt); /** * Read type-specific config from NBT */ public abstract void readTypeNBT(NBTTagCompound nbt); // ═══════════════════════════════════════════════════════════════════ // OPTIONAL OVERRIDES // ═══════════════════════════════════════════════════════════════════ public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { } public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { } public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { } /** * Apply damage to an entity with ability hit event support. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * * @param npc The NPC executing the ability * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback * @return true if damage was applied (not cancelled), false if cancelled */ protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, float damage, float knockback) { double dx = hitEntity.posX - npc.posX; double dz = hitEntity.posZ - npc.posZ; return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, 0.0f, dx, dz); } /** * Apply damage to an entity with ability hit event support and vertical knockback. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * * @param npc The NPC executing the ability * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback * @param knockbackUp The vertical knockback * @return true if damage was applied (not cancelled), false if cancelled */ protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { double dx = hitEntity.posX - npc.posX; double dz = hitEntity.posZ - npc.posZ; return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, knockbackUp, dx, dz); } /** * Apply damage to an entity with ability hit event support and custom knockback direction. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * * @param npc The NPC executing the ability * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback * @param knockbackDirX The X component of knockback direction * @param knockbackDirZ The Z component of knockback direction * @return true if damage was applied (not cancelled), false if cancelled */ protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity, float damage, float knockback, double knockbackDirX, double knockbackDirZ) { return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, 0.0f, knockbackDirX, knockbackDirZ); } /** * Internal unified damage application method. * All public damage methods delegate to this. */ private boolean applyAbilityDamageInternal(EntityNPCInterface npc, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp, double knockbackDirX, double knockbackDirZ) { DataAbilities dataAbilities = npc.abilities; AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( this, currentTarget, hitEntity, damage, knockback, knockbackUp); if (event == null) { return false; // Cancelled } // Apply damage with potentially modified values float finalDamage = event.getDamage(); float finalKnockback = event.getKnockback(); float finalKnockbackUp = event.getKnockbackUp(); // Apply damage if (finalDamage > 0) { hitEntity.attackEntityFrom(new NpcDamageSource("mob", npc), finalDamage); } // Apply knockback if any if (finalKnockback > 0 || finalKnockbackUp > 0) { applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback, finalKnockbackUp); } return true; } private void applyNpcKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback, float knockbackUp) { double len = Math.sqrt(dirX * dirX + dirZ * dirZ); double x = 0; double z = 0; if (len > 0) { x = (dirX / len) * knockback * 0.5; z = (dirZ / len) * knockback * 0.5; } double y = knockbackUp > 0 ? knockbackUp : 0.1D; hitEntity.addVelocity(x, y, z); hitEntity.velocityChanged = true; } /** * Applies all configured effects to the given entity. * Call this after damage is applied to the target. * * @param entity The entity to apply effects to */ protected void applyEffects(EntityLivingBase entity) { if (entity == null || effects.isEmpty()) { return; } for (AbilityEffect effect : effects) { effect.apply(entity); } } public float getTelegraphRadius() { return 5.0f; } public float getTelegraphLength() { return 5.0f; } public float getTelegraphWidth() { return 2.0f; } public float getTelegraphAngle() { return 45.0f; } /** * Returns true if this ability controls NPC movement during ACTIVE phase. * When true, AI pathfinding will be blocked during ability execution. * Override in abilities that move the NPC (Charge, Dash, Slam, etc.) */ public boolean hasAbilityMovement() { return false; } /** * Returns true if the targeting mode for this ability type is locked and cannot be changed. * Most abilities have fixed targeting (e.g., Heal is always SELF, Charge is always AGGRO_TARGET). * Override and return false for abilities with flexible targeting. */ public boolean isTargetingModeLocked() { return true; } /** * Returns the allowed targeting modes for this ability, or null if all modes are allowed. * Override in abilities that only support specific targeting modes. */ public TargetingMode[] getAllowedTargetingModes() { return null; } /** * Returns true if the telegraph type for this ability is locked and cannot be changed. * Telegraph type is inherent to how the ability works (e.g., Charge uses LINE, Slam uses CIRCLE). */ public boolean isTelegraphTypeLocked() { return true; } /** * Returns the declarative field definitions for this ability's GUI. * Override in subclasses to declare type-specific and visual fields. * The GUI rendering engine in SubGuiAbilityConfig uses this list to * automatically build the Type and Visual tabs. */ public List getFieldDefinitions() { return Collections.emptyList(); } /** * Returns field definitions for all base Ability fields (General, Target, Effects tabs). */ @SideOnly(Side.CLIENT) public List getBaseFieldDefinitions() { List defs = new ArrayList<>(); // General tab defs.add(FieldDef.stringField("gui.name", this::getName, this::setName) .tab(TabTarget.GENERAL)); defs.add(FieldDef.intField("ability.weight", this::getWeight, this::setWeight) .tab(TabTarget.GENERAL).range(1, 1000)); defs.add(FieldDef.enumField("ability.lockMove", LockMovementType.class, this::getLockMovement, this::setLockMovement) .tab(TabTarget.GENERAL).hover("ability.hover.lockMove")); defs.add(FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) .tab(TabTarget.GENERAL).hover("ability.hover.interruptible")); defs.add(FieldDef.intField("ability.dazed", this::getDazedTicks, this::setDazedTicks) .tab(TabTarget.GENERAL).range(0, 1000) .visibleWhen(this::isInterruptible)); defs.add(FieldDef.intField("ability.windup", this::getWindUpTicks, this::setWindUpTicks) .tab(TabTarget.GENERAL).range(0, 1000)); defs.add(FieldDef.intField("ability.cooldown", this::getCooldownTicks, this::setCooldownTicks) .tab(TabTarget.GENERAL).range(0, 10000)); defs.add(FieldDef.boolField("gui.enabled", this::isEnabled, this::setEnabled) .tab(TabTarget.GENERAL)); // Target tab defs.add(FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)) .tab(TabTarget.TARGET).range(0, 100)); defs.add(FieldDef.intField("ability.maxRange", () -> (int) getMaxRange(), v -> setMaxRange(v)) .tab(TabTarget.TARGET).range(1, 100)); if (!isTargetingModeLocked()) { defs.add(FieldDef.enumField("ability.targeting", TargetingMode.class, this::getTargetingMode, this::setTargetingMode) .tab(TabTarget.TARGET).hover("ability.hover.targeting")); } // Effects tab defs.add(FieldDef.soundSubGui("ability.windUpSound", this::getWindUpSound, this::setWindUpSound) .tab(TabTarget.EFFECTS)); defs.add(FieldDef.soundSubGui("ability.activeSound", this::getActiveSound, this::setActiveSound) .tab(TabTarget.EFFECTS)); defs.add(FieldDef.animSubGui("ability.windUpAnim", this::getWindUpAnimationId, this::setWindUpAnimationId, this::getWindUpAnimationName, this::setWindUpAnimationName) .tab(TabTarget.EFFECTS)); defs.add(FieldDef.animSubGui("ability.activeAnim", this::getActiveAnimationId, this::setActiveAnimationId, this::getActiveAnimationName, this::setActiveAnimationName) .tab(TabTarget.EFFECTS)); TelegraphType tType = getTelegraphType(); if (tType != null && tType != TelegraphType.NONE) { defs.add(FieldDef.boolField("ability.showTelegraph", this::isShowTelegraph, this::setShowTelegraph) .tab(TabTarget.EFFECTS).hover("ability.hover.showTelegraph")); defs.add(FieldDef.colorSubGui("ability.windUpColor", this::getWindUpColor, this::setWindUpColor) .tab(TabTarget.EFFECTS).visibleWhen(this::isShowTelegraph)); defs.add(FieldDef.colorSubGui("ability.activeColor", this::getActiveColor, this::setActiveColor) .tab(TabTarget.EFFECTS).visibleWhen(this::isShowTelegraph)); } return defs; } /** * Returns ALL field definitions: base (General/Target/Effects) + type-specific (Type/Visual). */ @SideOnly(Side.CLIENT) public final List getAllFieldDefinitions() { List all = new ArrayList<>(getBaseFieldDefinitions()); all.addAll(getFieldDefinitions()); return all; } /** * Returns true if this ability type has custom settings that need a Type-specific tab. * Override to return true if the ability has settings beyond the base Ability fields. */ public boolean hasTypeSettings() { return false; } /** * Get the number of rows needed for type-specific settings in the GUI. * Each row is approximately 24 pixels. Used for GUI layout. */ public int getTypeSettingsRowCount() { return 0; } /** * Creates the GUI for configuring this ability. * Override this method to provide a custom GUI subclass for type-specific settings. *

* Third-party mods can extend SubGuiAbilityConfig and override initTypeTab(), * handleTypeButton(), and handleTypeTextField() to provide custom type settings. * * @param callback The callback to notify when the ability is saved * @return A SubGuiAbilityConfig instance (or subclass) for configuring this ability */ @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { return new SubGuiAbilityConfig(this, callback); } /** * Create a telegraph instance for this ability. * Override for custom telegraph shapes. * * @param npc The caster * @param target The target (for position calculation) * @return The telegraph instance, or null if no telegraph */ public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE) { return null; } Telegraph telegraph; double x, y, z; float yaw = npc.rotationYaw; // Determine position based on targeting mode boolean positionAtNpc = targetingMode == TargetingMode.AOE_SELF || targetingMode == TargetingMode.SELF || telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE; if (positionAtNpc) { x = npc.posX; y = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); z = npc.posZ; } else if (target != null) { x = target.posX; y = findGroundLevel(npc.worldObj, target.posX, target.posY, target.posZ); z = target.posZ; } else { x = npc.posX; y = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); z = npc.posZ; } // Create telegraph based on type switch (telegraphType) { case CIRCLE: telegraph = Telegraph.circle(getTelegraphRadius()); break; case RING: telegraph = Telegraph.ring(getTelegraphRadius(), getTelegraphRadius() * 0.5f); break; case LINE: telegraph = Telegraph.line(getTelegraphLength(), getTelegraphWidth()); break; case CONE: telegraph = Telegraph.cone(getTelegraphLength(), getTelegraphAngle()); break; case POINT: telegraph = new Telegraph("", TelegraphType.POINT); break; default: return null; } telegraph.setDurationTicks(windUpTicks); telegraph.setColor(windUpColor); telegraph.setWarningColor(activeColor); telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); instance.setCasterEntityId(npc.getEntityId()); // Set entity to follow based on targeting mode if (positionAtNpc) { // AOE_SELF abilities: telegraph follows NPC during windup instance.setEntityIdToFollow(npc.getEntityId()); // For LINE/CONE telegraphs: track target direction during windup if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) { instance.setTargetEntityId(target.getEntityId()); } } else if (target != null) { // AOE_TARGET abilities: telegraph follows target during windup instance.setEntityIdToFollow(target.getEntityId()); } return instance; } /** * Find the ground level at a given position. * Searches downward from the given Y to find a solid block. * * @param world The world * @param x X coordinate * @param startY Starting Y coordinate (entity feet position) * @param z Z coordinate * @return The Y coordinate of the ground surface */ public static double findGroundLevel(World world, double x, double startY, double z) { if (world == null) return startY; int blockX = (int) Math.floor(x); int blockZ = (int) Math.floor(z); int startBlockY = (int) Math.floor(startY); // Search downward for solid ground (max 10 blocks down) for (int checkY = startBlockY; checkY >= startBlockY - 10 && checkY >= 0; checkY--) { Block block = world.getBlock(blockX, checkY, blockZ); if (block != null && block.getMaterial().isSolid()) { // Found solid block, telegraph goes on top of it return checkY + 1; } } // No ground found, use original position return startY; } /** * Calculates offset position near the given coordinates. * Used to place effects near a target rather than exactly on them. * * @param baseX Base X coordinate * @param baseY Base Y coordinate * @param baseZ Base Z coordinate * @param minOffset Minimum offset distance * @param maxOffset Maximum offset distance * @param randomOffset Whether to use random offset within range, or fixed at max * @param random Random instance to use * @return Array of [x, y, z] with offset applied */ public static double[] calculateOffsetPosition(double baseX, double baseY, double baseZ, float minOffset, float maxOffset, boolean randomOffset, java.util.Random random) { if (maxOffset <= 0) { return new double[]{baseX, baseY, baseZ}; } double offsetDist; double offsetAngle; if (randomOffset) { offsetDist = minOffset + random.nextDouble() * (maxOffset - minOffset); offsetAngle = random.nextDouble() * Math.PI * 2; } else { offsetDist = maxOffset; offsetAngle = random.nextDouble() * Math.PI * 2; } double offsetX = Math.cos(offsetAngle) * offsetDist; double offsetZ = Math.sin(offsetAngle) * offsetDist; return new double[]{baseX + offsetX, baseY, baseZ + offsetZ}; } /** * Get the current telegraph instance. */ public TelegraphInstance getTelegraphInstance() { return telegraphInstance; } /** * Set the telegraph instance (called by DataAbilities). */ public void setTelegraphInstance(TelegraphInstance instance) { this.telegraphInstance = instance; } // ═══════════════════════════════════════════════════════════════════ // EXECUTION LOGIC (called by CombatHandler) // ═══════════════════════════════════════════════════════════════════ /** * Start executing this ability */ public void start(EntityLivingBase target) { this.phase = AbilityPhase.WINDUP; this.currentTick = 0; this.currentTarget = target; this.executionStartTime = System.currentTimeMillis(); } /** * Tick the ability forward. Called every game tick while executing. *

* Normal flow: WINDUP -> ACTIVE -> IDLE * Interrupted flow: WINDUP -> DAZED -> IDLE (dazed only on interrupt during WINDUP) * * @return true if phase changed this tick */ public boolean tick() { if (phase == AbilityPhase.IDLE) return false; currentTick++; switch (phase) { case WINDUP: if (currentTick >= windUpTicks) { phase = AbilityPhase.ACTIVE; currentTick = 0; return true; } break; case ACTIVE: // Abilities control their own completion by calling signalCompletion() // No auto-complete - each ability determines when it's done break; case DAZED: // Dazed phase is only entered on interrupt during WINDUP // NPC cannot attack during this time if (currentTick >= dazedTicks) { phase = AbilityPhase.IDLE; currentTick = 0; return true; } break; } return false; } /** * Signal that the ability has completed its active phase. * Called by abilities when they are done. * For example, AbilitySlam calls this when the NPC lands. * * @return true if phase changed to IDLE */ public boolean signalCompletion() { if (phase == AbilityPhase.ACTIVE) { cleanup(); phase = AbilityPhase.IDLE; currentTick = 0; return true; } return false; } /** * Interrupt this ability. Transitions to DAZED phase if interruptible during WINDUP. * Called when NPC takes direct damage during WINDUP. */ public void interrupt() { cleanup(); if (interruptible && phase == AbilityPhase.WINDUP) { // Interrupted during windup - go to dazed state phase = AbilityPhase.DAZED; currentTick = 0; } else { // Force stop - go directly to IDLE phase = AbilityPhase.IDLE; currentTick = 0; } currentTarget = null; telegraphInstance = null; } /** * Check if executing */ public boolean isExecuting() { return phase != AbilityPhase.IDLE; } /** * Check if can be interrupted by this damage source. * Only direct physical hits (not magic, fire, or other indirect damage) can interrupt. * Interruption only occurs during WINDUP phase - if ability is already active, it completes. * * @param source The damage source * @return true if the ability should be interrupted */ public boolean canInterrupt(DamageSource source) { if (!interruptible || phase != AbilityPhase.WINDUP) { 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; } /** * Reset all execution state (called on combat end, NPC death, target lost, etc.) */ public void reset() { cleanup(); phase = AbilityPhase.IDLE; currentTick = 0; currentTarget = null; telegraphInstance = null; } /** * Cleanup any resources created by this ability. * Override this to kill spawned entities, cancel telegraphs, etc. * Called when ability ends for any reason (completion, interrupt, reset, NPC death). */ public void cleanup() { // Override in subclasses to clean up spawned entities, etc. } // ═══════════════════════════════════════════════════════════════════ // CLIENT-SIDE PREVIEW METHODS // ═══════════════════════════════════════════════════════════════════ /** * Get total active phase duration for preview (in ticks). * Override in subclasses to provide accurate preview duration. * * @return Duration of active phase in ticks (default: 40 = 2 seconds) */ public int getPreviewActiveDuration() { return 40; } /** * Called during preview windup tick (client-side only). * Override for visual updates during windup preview. * Do NOT apply damage or world effects here. * * @param npc The preview NPC * @param tick Current tick within windup phase */ @SideOnly(Side.CLIENT) public void onPreviewWindUpTick(EntityNPCInterface npc, int tick) { // Default: no-op. Override in subclasses for visual effects. } /** * Called during preview active tick (client-side only). * Override for visual updates during active preview. * Do NOT apply damage or world effects here. * * @param npc The preview NPC * @param tick Current tick within active phase */ @SideOnly(Side.CLIENT) public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { // Default: no-op. Override in subclasses for visual effects. } /** * Create a preview entity for GUI display (client-side only). * The entity should be in preview mode and not apply damage. * Override in abilities that spawn entities (Beam, Orb, Disc, etc.) * * @param npc The preview NPC * @return Preview entity, or null if ability doesn't spawn entities */ @SideOnly(Side.CLIENT) public Entity createPreviewEntity(EntityNPCInterface npc) { return null; // Override in entity-spawning abilities } /** * Whether to spawn preview entity during WINDUP phase (true) or ACTIVE phase (false). * Most abilities (Orb, Beam, Disc) spawn during windup for charging effect. * Laser spawns at active phase since it has no charging state. */ public boolean spawnPreviewDuringWindup() { return true; // Default: spawn during windup for charging } // ── Preview target (set by executor for abilities that need targeting) ── protected transient EntityLivingBase previewTarget; /** * Set the fake target entity for preview mode. * Called by AbilityPreviewExecutor before starting preview. */ public void setPreviewTarget(EntityLivingBase target) { this.previewTarget = target; } public EntityLivingBase getPreviewTarget() { return previewTarget; } /** * Called once when transitioning from WINDUP to ACTIVE in preview mode. * Override in movement-based abilities to initiate movement. */ @SideOnly(Side.CLIENT) public void onPreviewExecute(EntityNPCInterface npc) { // Default: no-op. Override in movement abilities. } // ═══════════════════════════════════════════════════════════════════ // CONDITION CHECKING // ═══════════════════════════════════════════════════════════════════ public boolean checkConditions(EntityNPCInterface npc, EntityLivingBase target) { for (Condition c : conditions) { if (!c.check(npc, target)) return false; } return true; } // ═══════════════════════════════════════════════════════════════════ // NBT (config only, execution state is transient) // ═══════════════════════════════════════════════════════════════════ public NBTTagCompound writeNBT() { NBTTagCompound nbt = new NBTTagCompound(); nbt.setString("id", id); nbt.setString("name", name); nbt.setString("typeId", typeId); nbt.setInteger("weight", weight); nbt.setBoolean("enabled", enabled); nbt.setString("targetingMode", targetingMode.name()); nbt.setFloat("minRange", minRange); nbt.setFloat("maxRange", maxRange); nbt.setInteger("cooldown", cooldownTicks); nbt.setInteger("windUp", windUpTicks); nbt.setInteger("recovery", dazedTicks); nbt.setBoolean("interruptible", interruptible); nbt.setInteger("lockMovement", lockMovement.ordinal()); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); nbt.setString("windUpSound", windUpSound); nbt.setString("activeSound", activeSound); nbt.setInteger("windUpAnimationId", windUpAnimationId); nbt.setInteger("activeAnimationId", activeAnimationId); nbt.setString("windUpAnimationName", windUpAnimationName); nbt.setString("activeAnimationName", activeAnimationName); nbt.setBoolean("showTelegraph", showTelegraph); nbt.setString("telegraphType", telegraphType.name()); nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset); nbt.setTag("customData", customData); // Conditions NBTTagList condList = new NBTTagList(); for (Condition c : conditions) condList.appendTag(c.writeNBT()); nbt.setTag("conditions", condList); // Effects NBTTagList effectList = new NBTTagList(); for (AbilityEffect effect : effects) { effectList.appendTag(effect.writeNBT()); } nbt.setTag("effects", effectList); // Type-specific NBTTagCompound typeNBT = new NBTTagCompound(); writeTypeNBT(typeNBT); nbt.setTag("typeData", typeNBT); return nbt; } public void readNBT(NBTTagCompound nbt) { id = nbt.getString("id"); name = nbt.getString("name"); typeId = nbt.getString("typeId"); weight = nbt.getInteger("weight"); enabled = nbt.getBoolean("enabled"); targetingMode = TargetingMode.valueOf(nbt.getString("targetingMode")); minRange = nbt.getFloat("minRange"); maxRange = nbt.getFloat("maxRange"); cooldownTicks = nbt.getInteger("cooldown"); windUpTicks = nbt.getInteger("windUp"); dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); // Support old telegraphColor key for backwards compatibility if (nbt.hasKey("windUpColor")) { windUpColor = nbt.getInteger("windUpColor"); } else if (nbt.hasKey("telegraphColor")) { windUpColor = nbt.getInteger("telegraphColor"); } if (nbt.hasKey("activeColor")) { activeColor = nbt.getInteger("activeColor"); } else { // Default active color to windUpColor with higher alpha activeColor = (windUpColor & 0x00FFFFFF) | 0xC0000000; } // Sound fields (with backwards compatibility for old castSound) if (nbt.hasKey("windUpSound")) { windUpSound = nbt.getString("windUpSound"); } else if (nbt.hasKey("castSound")) { windUpSound = nbt.getString("castSound"); // Migrate old field } else { windUpSound = ""; } activeSound = nbt.hasKey("activeSound") ? nbt.getString("activeSound") : ""; // Animation fields (with backwards compatibility for old animationId) if (nbt.hasKey("windUpAnimationId")) { windUpAnimationId = nbt.getInteger("windUpAnimationId"); } else if (nbt.hasKey("animationId")) { windUpAnimationId = nbt.getInteger("animationId"); // Migrate old field } else { windUpAnimationId = -1; } activeAnimationId = nbt.hasKey("activeAnimationId") ? nbt.getInteger("activeAnimationId") : -1; windUpAnimationName = nbt.hasKey("windUpAnimationName") ? nbt.getString("windUpAnimationName") : ""; activeAnimationName = nbt.hasKey("activeAnimationName") ? nbt.getString("activeAnimationName") : ""; showTelegraph = !nbt.hasKey("showTelegraph") || nbt.getBoolean("showTelegraph"); if (nbt.hasKey("telegraphType")) { try { telegraphType = TelegraphType.valueOf(nbt.getString("telegraphType")); } catch (Exception e) { telegraphType = TelegraphType.CIRCLE; } } telegraphHeightOffset = nbt.hasKey("telegraphHeightOffset") ? nbt.getFloat("telegraphHeightOffset") : 0.1f; customData = nbt.getCompoundTag("customData"); // Conditions conditions.clear(); NBTTagList condList = nbt.getTagList("conditions", 10); for (int i = 0; i < condList.tagCount(); i++) { Condition c = Condition.fromNBT(condList.getCompoundTagAt(i)); if (c != null) conditions.add(c); } // Effects effects.clear(); if (nbt.hasKey("effects")) { NBTTagList effectList = nbt.getTagList("effects", 10); for (int i = 0; i < effectList.tagCount(); i++) { AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i)); if (effect != null && effect.isValid()) { effects.add(effect); } } } readTypeNBT(nbt.getCompoundTag("typeData")); } // ═══════════════════════════════════════════════════════════════════ // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getTypeId() { return typeId; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public TargetingMode getTargetingMode() { return targetingMode; } public void setTargetingMode(TargetingMode targetingMode) { this.targetingMode = targetingMode; } public float getMinRange() { return minRange; } public void setMinRange(float minRange) { this.minRange = minRange; } public float getMaxRange() { return maxRange; } public void setMaxRange(float maxRange) { this.maxRange = maxRange; } public int getCooldownTicks() { return cooldownTicks; } public void setCooldownTicks(int cooldownTicks) { this.cooldownTicks = Math.max(0, cooldownTicks); } public int getWindUpTicks() { return windUpTicks; } public void setWindUpTicks(int windUpTicks) { this.windUpTicks = Math.max(0, windUpTicks); } public int getDazedTicks() { return dazedTicks; } public void setDazedTicks(int dazedTicks) { this.dazedTicks = Math.max(0, dazedTicks); } public boolean isInterruptible() { return interruptible; } public void setInterruptible(boolean interruptible) { this.interruptible = interruptible; } public LockMovementType getLockMovement() { return lockMovement; } public void setLockMovement(LockMovementType lockMovement) { this.lockMovement = lockMovement; } /** * API method: Get lock movement type as integer. * @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE */ @Override public int getLockMovementType() { return lockMovement.ordinal(); } /** * API method: Set lock movement type from integer. * @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE */ @Override public void setLockMovementType(int type) { this.lockMovement = LockMovementType.fromOrdinal(type); } /** * Check if movement should be locked during WINDUP phase. */ @Override public boolean isMovementLockedDuringWindup() { return lockMovement.locksWindup(); } /** * Check if movement should be locked during ACTIVE phase. */ @Override public boolean isMovementLockedDuringActive() { return lockMovement.locksActive(); } /** * Check if movement should be locked during the current phase. */ public boolean isMovementLockedForCurrentPhase() { switch (phase) { case WINDUP: return lockMovement.locksWindup(); case ACTIVE: return lockMovement.locksActive(); default: return false; } } public int getWindUpColor() { return windUpColor; } public void setWindUpColor(int windUpColor) { this.windUpColor = windUpColor; } public int getActiveColor() { return activeColor; } public void setActiveColor(int activeColor) { this.activeColor = activeColor; } public String getWindUpSound() { return windUpSound; } public void setWindUpSound(String windUpSound) { this.windUpSound = windUpSound; } public String getActiveSound() { return activeSound; } public void setActiveSound(String activeSound) { this.activeSound = activeSound; } public Animation getWindUpAnimation() { if (AnimationController.Instance == null) return null; // Built-in animation by name takes priority if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { return (Animation) AnimationController.Instance.get(windUpAnimationName); } // Fall back to user animation by ID if (windUpAnimationId >= 0) { return (Animation) AnimationController.Instance.get(windUpAnimationId); } return null; } public int getWindUpAnimationId() { return windUpAnimationId; } public void setWindUpAnimationId(int windUpAnimationId) { this.windUpAnimationId = windUpAnimationId; } public String getWindUpAnimationName() { return windUpAnimationName; } public void setWindUpAnimationName(String windUpAnimationName) { this.windUpAnimationName = windUpAnimationName != null ? windUpAnimationName : ""; } public Animation getActiveAnimation() { if (AnimationController.Instance == null) return null; // Built-in animation by name takes priority if (activeAnimationName != null && !activeAnimationName.isEmpty()) { return (Animation) AnimationController.Instance.get(activeAnimationName); } // Fall back to user animation by ID if (activeAnimationId >= 0) { return (Animation) AnimationController.Instance.get(activeAnimationId); } return null; } public int getActiveAnimationId() { return activeAnimationId; } public void setActiveAnimationId(int activeAnimationId) { this.activeAnimationId = activeAnimationId; } public String getActiveAnimationName() { return activeAnimationName; } public void setActiveAnimationName(String activeAnimationName) { this.activeAnimationName = activeAnimationName != null ? activeAnimationName : ""; } public boolean isShowTelegraph() { return showTelegraph; } public void setShowTelegraph(boolean showTelegraph) { this.showTelegraph = showTelegraph; } public TelegraphType getTelegraphType() { return telegraphType; } public void setTelegraphType(TelegraphType telegraphType) { this.telegraphType = telegraphType; } public float getTelegraphHeightOffset() { return telegraphHeightOffset; } public void setTelegraphHeightOffset(float telegraphHeightOffset) { this.telegraphHeightOffset = telegraphHeightOffset; } public AbilityPhase getPhase() { return phase; } /** * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=DAZED). * Required for IAbility interface. */ @Override public int getPhaseInt() { return phase.ordinal(); } public int getCurrentTick() { return currentTick; } public EntityLivingBase getCurrentTarget() { return currentTarget; } public NBTTagCompound getCustomData() { return customData; } public List getConditions() { return conditions; } public void addCondition(Condition c) { conditions.add(c); } public List getEffects() { return effects; } public void setEffects(List effects) { this.effects = effects != null ? effects : new ArrayList<>(); } public void addEffect(AbilityEffect effect) { if (effect != null && effect.isValid()) { effects.add(effect); } } public void clearEffects() { effects.clear(); } // ═══════════════════════════════════════════════════════════════════ // IAbility API METHODS // ═══════════════════════════════════════════════════════════════════ @Override public INbt getNbt() { return NpcAPI.Instance().getINbt(writeNBT()); } @Override public void setNbt(INbt nbt) { readNBT(nbt.getMCNBT()); } }