diff --git a/src/api b/src/api index 2b21264ad..b8ba682e9 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 2b21264ada69ec5e800977a000e892bc82d7c61f +Subproject commit b8ba682e9dd4e542a5ccf0de231ab5e5ebdad448 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 a82a8fc6e..08eed148b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -64,8 +64,12 @@ public abstract class Ability implements IAbility { 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 - protected int activeAnimationId = -1; // Animation to play during active phase + 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; @@ -669,6 +673,8 @@ public abstract class Ability implements IAbility { 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); @@ -738,6 +744,8 @@ public abstract class Ability implements IAbility { 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 { @@ -959,23 +967,63 @@ public abstract class Ability implements IAbility { public Animation getWindUpAnimation() { if (AnimationController.Instance == null) return null; - return (Animation) AnimationController.Instance.get(windUpAnimationId); + // 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; - return (Animation) AnimationController.Instance.get(activeAnimationId); + // 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; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java deleted file mode 100644 index 190be70d3..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java +++ /dev/null @@ -1,101 +0,0 @@ -package kamkeel.npcs.controllers.data.ability; - -import noppes.npcs.CustomNpcs; -import noppes.npcs.LogWriter; -import noppes.npcs.controllers.data.Animation; -import noppes.npcs.util.NBTJsonUtil; -import org.apache.commons.io.FileUtils; - -import java.io.File; -import java.io.InputStream; -import java.util.HashMap; - -import static kamkeel.npcs.controllers.data.ability.AbilityPhase.*; - -public enum AbilityAnimation { - ABILITYORB("Ability_Orb_Windup", WINDUP), - ABILITYBEAM("Ability_Beam_Windup", "Ability_Beam_Active"); - private final HashMap animationNames = new HashMap<>(); - - AbilityAnimation(String idle, String windUp, String active, String recovery) { - animationNames.put(IDLE, idle); - animationNames.put(WINDUP, windUp); - animationNames.put(ACTIVE, active); - animationNames.put(DAZED, recovery); - } - - AbilityAnimation(String idle, String windUp, String active) { - animationNames.put(IDLE, idle); - animationNames.put(WINDUP, windUp); - animationNames.put(ACTIVE, active); - } - - AbilityAnimation(String windUp, String active) { - animationNames.put(WINDUP, windUp); - animationNames.put(ACTIVE, active); - } - - AbilityAnimation(String anim, AbilityPhase phase) { - animationNames.put(phase, anim); - } - - public String fileName(AbilityPhase phase) { - return animationNames.get(phase); - } - - public File getFile(AbilityPhase phase) { - if (animationNames.get(phase).isEmpty() || animationNames.get(phase).equals("N/A")) - return null; - - return getFileFromName(animationNames.get(phase)); - } - - public Animation getAnimation(AbilityPhase phase) { - try { - File file = getFile(phase); - - if (file == null) { - return null; - } - - Animation animation = new Animation(); - animation.readFromNBT(NBTJsonUtil.LoadFile(file)); - - return animation; - } catch (Exception e) { - LogWriter.except(e); - } - - return null; - } - - public Animation idle() { - return getAnimation(IDLE); - } - - public Animation windUp() { - return getAnimation(WINDUP); - } - - public Animation active() { - return getAnimation(ACTIVE); - } - - public Animation dazed() { - return getAnimation(DAZED); - } - - private File getFileFromName(String fileName) { - try (InputStream stream = CustomNpcs.class.getResourceAsStream("/internal/animations/" + name() + "/" + fileName + ".json")) { - - File file = new File("temp"); - - FileUtils.copyInputStreamToFile(stream, file); - - return file; - } catch (Exception i) { - LogWriter.except(i); - } - return null; - } -} 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 a68e38cd6..d85720b6e 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 @@ -121,10 +121,13 @@ public class AbilityDisc extends Ability implements IAbilityDisc { if (discEntity != null && !discEntity.isDead) { discEntity.startMoving(target); } - discEntity = null; - // Disc entity manages itself - ability is done - signalCompletion(); + // If movement is locked during active phase, keep ability active until entity dies + // Otherwise signal completion immediately + if (!lockMovement.locksActive()) { + discEntity = null; + signalCompletion(); + } } @Override @@ -154,6 +157,11 @@ public class AbilityDisc extends Ability implements IAbilityDisc { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // If we're tracking the entity for movement lock, check if it's dead + if (discEntity != null && discEntity.isDead) { + discEntity = null; + signalCompletion(); + } } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index b8946306a..7ebd4bb7b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -3,7 +3,6 @@ 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.AbilityAnimation; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -13,13 +12,12 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityBeam; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; -import net.minecraft.nbt.NBTTagCompound;; +import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityEnergyBeam; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.api.ability.type.IAbilityEnergyBeam; -import noppes.npcs.controllers.data.Animation; import noppes.npcs.entity.EntityNPCInterface; /** @@ -71,9 +69,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { private float lightningRadius = 0.5f; // Anchor point for charging position - private AnchorPoint anchorPoint = AnchorPoint.FRONT; - - private final AbilityAnimation animation = AbilityAnimation.ABILITYBEAM; + private AnchorPoint anchorPoint = AnchorPoint.RIGHT_HAND; // Transient state for beam entity (used during windup charging) private transient EntityAbilityBeam beamEntity = null; @@ -89,6 +85,9 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { this.lockMovement = 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"; } @Override @@ -123,10 +122,13 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { if (beamEntity != null && !beamEntity.isDead) { beamEntity.startFiring(target); } - beamEntity = null; - // Beam entity manages itself - ability is done - signalCompletion(); + // If movement is locked during active phase, keep ability active until entity dies + // Otherwise signal completion immediately + if (!lockMovement.locksActive()) { + beamEntity = null; + signalCompletion(); + } } @Override @@ -159,6 +161,11 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // If we're tracking the entity for movement lock, check if it's dead + if (beamEntity != null && beamEntity.isDead) { + beamEntity = null; + signalCompletion(); + } } @Override @@ -313,23 +320,6 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } - - @Override - public Animation getWindUpAnimation() { - if (windUpAnimationId == -1) - return animation.windUp(); - - return super.getWindUpAnimation(); - } - - @Override - public Animation getActiveAnimation() { - if (activeAnimationId == -1) - return animation.active(); - - return super.getActiveAnimation(); - } - public AnchorPoint getAnchorPointEnum() { return anchorPoint; } public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } 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 da9f07780..a9cf74473 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 @@ -56,6 +56,9 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { private float lightningDensity = 0.15f; private float lightningRadius = 0.5f; + // Transient state for laser entity (used for movement locking) + private transient EntityAbilityLaser laserEntity = null; + public AbilityLaserShot() { this.typeId = "ability.cnpc.laser_shot"; this.name = "Laser Shot"; @@ -111,7 +114,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { double spawnY = npc.posY + npc.getEyeHeight() * 0.8; double spawnZ = npc.posZ; - EntityAbilityLaser laser = new EntityAbilityLaser( + laserEntity = new EntityAbilityLaser( world, npc, target, spawnX, spawnY, spawnZ, laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, @@ -123,15 +126,23 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { lightningEffect, lightningDensity, lightningRadius ); - world.spawnEntityInWorld(laser); + world.spawnEntityInWorld(laserEntity); - // Laser entity manages itself - ability is done - signalCompletion(); + // If movement is locked during active phase, keep ability active until entity dies + // Otherwise signal completion immediately + if (!lockMovement.locksActive()) { + laserEntity = null; + signalCompletion(); + } } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // Laser entity manages itself + // If we're tracking the entity for movement lock, check if it's dead + if (laserEntity != null && laserEntity.isDead) { + laserEntity = null; + signalCompletion(); + } } @Override @@ -142,6 +153,15 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { } + @Override + public void cleanup() { + // Despawn laser entity if still alive + if (laserEntity != null && !laserEntity.isDead) { + laserEntity.setDead(); + } + laserEntity = null; + } + @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("laserWidth", laserWidth); 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 336f54e7c..d69a5c944 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 @@ -3,7 +3,6 @@ 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.AbilityAnimation; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -19,7 +18,6 @@ import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.controllers.data.Animation; import noppes.npcs.api.ability.type.IAbilityOrb; import noppes.npcs.entity.EntityNPCInterface; @@ -70,13 +68,11 @@ public class AbilityOrb extends Ability implements IAbilityOrb { private int lightningFadeTime = 6; // Ticks before lightning fades out // Anchor point for charging position - private AnchorPoint anchorPoint = AnchorPoint.FRONT; + private AnchorPoint anchorPoint = AnchorPoint.RIGHT_HAND; // Transient state for orb entity (used during windup charging) private transient EntityAbilityOrb orbEntity = null; - private final AbilityAnimation animation = AbilityAnimation.ABILITYORB; - public AbilityOrb() { this.typeId = "ability.cnpc.orb"; this.name = "Orb"; @@ -88,6 +84,8 @@ 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"; } @Override @@ -122,10 +120,13 @@ public class AbilityOrb extends Ability implements IAbilityOrb { if (orbEntity != null && !orbEntity.isDead) { orbEntity.startMoving(target); } - orbEntity = null; - // Orb entity manages itself - ability is done - signalCompletion(); + // If movement is locked during active phase, keep ability active until entity dies + // Otherwise signal completion immediately + if (!lockMovement.locksActive()) { + orbEntity = null; + signalCompletion(); + } } @Override @@ -154,7 +155,11 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // Nothing to do - entity manages itself + // If we're tracking the entity for movement lock, check if it's dead + if (orbEntity != null && orbEntity.isDead) { + orbEntity = null; + signalCompletion(); + } } @Override @@ -467,14 +472,6 @@ public class AbilityOrb extends Ability implements IAbilityOrb { this.anchorPoint = anchorPoint; } - @Override - public Animation getWindUpAnimation() { - if (windUpAnimationId != -1) - return super.getWindUpAnimation(); - - return animation.windUp(); - } - public int getAnchorPoint() { return anchorPoint.getId(); } 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 2abbbbb48..78dae4fdc 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 @@ -91,8 +91,6 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { return; } - LogWriter.info("[Sweeper] onExecute called at NPC pos " + npc.posX + ", " + npc.posY + ", " + npc.posZ); - // Spawn the entity that handles BOTH visuals AND damage activeEntity = new EntityAbilitySweeper(world, npc, target, beamLength, beamWidth, beamHeight, @@ -102,14 +100,19 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { lockOnTarget); world.spawnEntityInWorld(activeEntity); - LogWriter.info("[Sweeper] Spawned sweeper entity"); - // Entity manages itself - completion signaled from onActiveTick when entity dies + // If movement is locked during active phase, keep ability active until entity dies + // Otherwise signal completion immediately + if (!lockMovement.locksActive()) { + activeEntity = null; + signalCompletion(); + } } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // Entity handles everything - check if entity finished its rotations - if (activeEntity == null || activeEntity.isDead) { + // If we're tracking the entity for movement lock, check if it's dead + if (activeEntity != null && activeEntity.isDead) { + activeEntity = null; signalCompletion(); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java index a2bc51d0a..39b60a40f 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java @@ -44,5 +44,6 @@ public final class AnimationsGetPacket extends AbstractPacket { return; NoppesUtilServer.sendAnimationDataAll((EntityPlayerMP) player); + NoppesUtilServer.sendBuiltInAnimationData((EntityPlayerMP) player); } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 538829cef..526a3bdd7 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -412,7 +412,7 @@ public class DataAbilities { * @param animation The animation to play, or null if none */ public void playAbilityAnimation(Animation animation) { - if (animation == null || animation.id < 0) return; + if (animation == null) return; if (AnimationController.Instance == null) return; npc.display.animationData.setEnabled(true); diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index ffc778eb0..6a33cb2c0 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -163,7 +163,16 @@ public class NoppesUtilServer { for (Animation animation : AnimationController.getInstance().animations.values()) { map.put(animation.name, animation.id); } - sendScrollData(player, map, EnumScrollData.OPTIONAL); + sendScrollData(player, map, EnumScrollData.ANIMATIONS); + } + + public static void sendBuiltInAnimationData(EntityPlayerMP player) { + Map map = new HashMap(); + for (String name : AnimationController.getInstance().builtInAnimations.keySet()) { + // Use -1 as a marker for built-in animations (they have no ID) + map.put(AnimationController.getInstance().builtInAnimations.get(name).getName(), -1); + } + sendScrollData(player, map, EnumScrollData.BUILTIN_ANIMATIONS); } public static void sendTagDataAll(EntityPlayerMP player) { 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 11dbcbc85..a4fef92ef 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -129,8 +129,11 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Effects this.windUpSound = ability.getWindUpSound(); this.activeSound = ability.getActiveSound(); - this.windUpAnimationId = ability.getWindUpAnimation() != null ? ability.getWindUpAnimation().getID() : -1; - this.activeAnimationId = ability.getActiveAnimation() != null ? ability.getActiveAnimation().getID() : -1; + // Animation: check name first (built-in), then ID (user) + this.windUpAnimationName = ability.getWindUpAnimationName(); + this.activeAnimationName = ability.getActiveAnimationName(); + this.windUpAnimationId = ability.getWindUpAnimationId(); + this.activeAnimationId = ability.getActiveAnimationId(); // Telegraph this.showTelegraph = ability.isShowTelegraph(); @@ -286,8 +289,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi windupField.setMinMaxDefault(0, 1000, 20); addTextField(windupField); - // Sync button - only show if windup animation is selected - if (windUpAnimationId >= 0) { + // Sync button - only show if windup animation is selected (either by name or ID) + if (windUpAnimationId >= 0 || (windUpAnimationName != null && !windUpAnimationName.isEmpty())) { GuiNpcButton syncBtn = new GuiNpcButton(17, guiLeft + 115, y, 40, 20, "gui.sync"); syncBtn.setHoverText("ability.hover.sync"); addButton(syncBtn); @@ -591,12 +594,16 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } private String getAnimationName(int animId, boolean isWindUp) { - if (animId < 0) return StatCollector.translateToLocal("gui.none"); - String cachedName = isWindUp ? windUpAnimationName : activeAnimationName; - if (cachedName != null && !cachedName.isEmpty()) { - return truncateString(cachedName, 8); + String animName = isWindUp ? windUpAnimationName : activeAnimationName; + // Check if we have a built-in animation (by name) + if (animName != null && !animName.isEmpty()) { + return truncateString(animName, 8); } - return "ID: " + animId; + // Check if we have a user animation (by ID) + if (animId >= 0) { + return "ID: " + animId; + } + return StatCollector.translateToLocal("gui.none"); } // ═══════════════════════════════════════════════════════════════════════════ @@ -726,19 +733,19 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Animation buttons else if (id == 32) { editingAnimationId = 32; - setSubGui(new GuiAnimationSelection(windUpAnimationId)); + setSubGui(new GuiAnimationSelection(windUpAnimationId, windUpAnimationName)); } else if (id == 33) { editingAnimationId = 33; - setSubGui(new GuiAnimationSelection(activeAnimationId)); + setSubGui(new GuiAnimationSelection(activeAnimationId, activeAnimationName)); } // Animation clear buttons else if (id == 37) { windUpAnimationId = -1; - windUpAnimationName = null; + windUpAnimationName = ""; initGui(); } else if (id == 38) { activeAnimationId = -1; - activeAnimationName = null; + activeAnimationName = ""; initGui(); } @@ -824,11 +831,25 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } else if (subgui instanceof GuiAnimationSelection) { GuiAnimationSelection animSelector = (GuiAnimationSelection) subgui; if (editingAnimationId == 32) { - windUpAnimationId = animSelector.selectedAnimationId; - windUpAnimationName = animSelector.getSelectedName(); + if (animSelector.isBuiltInSelected()) { + // Built-in animation selected - use name, clear ID + windUpAnimationName = animSelector.selectedBuiltInName; + windUpAnimationId = -1; + } else { + // User animation selected - use ID, clear name + windUpAnimationId = animSelector.selectedAnimationId; + windUpAnimationName = ""; + } } else if (editingAnimationId == 33) { - activeAnimationId = animSelector.selectedAnimationId; - activeAnimationName = animSelector.getSelectedName(); + if (animSelector.isBuiltInSelected()) { + // Built-in animation selected - use name, clear ID + activeAnimationName = animSelector.selectedBuiltInName; + activeAnimationId = -1; + } else { + // User animation selected - use ID, clear name + activeAnimationId = animSelector.selectedAnimationId; + activeAnimationName = ""; + } } editingAnimationId = 0; initGui(); @@ -896,6 +917,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi ability.setActiveSound(activeSound); ability.setWindUpAnimationId(windUpAnimationId); ability.setActiveAnimationId(activeAnimationId); + ability.setWindUpAnimationName(windUpAnimationName != null ? windUpAnimationName : ""); + ability.setActiveAnimationName(activeAnimationName != null ? activeAnimationName : ""); // Telegraph ability.setShowTelegraph(showTelegraph); @@ -948,11 +971,20 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi * Syncs windup ticks with the selected windup animation's total duration. */ private void syncWindupWithAnimation() { - if (windUpAnimationId < 0 || AnimationController.Instance == null) { + if (AnimationController.Instance == null) { return; } - Animation animation = (Animation) AnimationController.Instance.get(windUpAnimationId); + Animation animation = null; + // Check for built-in animation (by name) first + if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationName); + } + // Fall back to user animation (by ID) + else if (windUpAnimationId >= 0) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationId); + } + if (animation == null || animation.frames.isEmpty()) { return; } diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java index cc3e06cca..2a1f04344 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java @@ -19,20 +19,42 @@ import java.util.Vector; /** * Animation selection dialog for choosing global animations. - * Requests animation data from the server via packets. + * Toggle between Built-in animations (by name) and Custom animations (by ID). */ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScrollListener, IScrollData { private GuiCustomScroll scrollAnimations; - private HashMap animationData = new HashMap(); + // Built-in animations (name only, no ID) + private HashMap builtInData = new HashMap<>(); + // Custom/user animations (name -> ID) + private HashMap customData = new HashMap<>(); + + // View mode: true = built-in, false = custom + private boolean showingBuiltIn = true; + + // Initial values passed in private int initialAnimationId; - public int selectedAnimationId = -1; - private String selectedName = null; + private String initialAnimationName; + // Selected values to return + public int selectedAnimationId = -1; + public String selectedBuiltInName = ""; + + private String selectedCustomName = null; private String search = ""; + /** + * Constructor for selecting by ID (user animations). + */ public GuiAnimationSelection(int animationId) { + this(animationId, ""); + } + + /** + * Constructor for selecting by name (built-in) or ID (user). + */ + public GuiAnimationSelection(int animationId, String animationName) { drawDefaultBackground = false; title = ""; setBackground("menubg.png"); @@ -40,7 +62,12 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr ySize = 226; this.initialAnimationId = animationId; + this.initialAnimationName = animationName != null ? animationName : ""; this.selectedAnimationId = animationId; + this.selectedBuiltInName = this.initialAnimationName; + + // Start showing built-in if we have a built-in name, otherwise custom + this.showingBuiltIn = !this.initialAnimationName.isEmpty(); // Request animation data from server PacketClient.sendClient(new AnimationsGetPacket()); @@ -50,56 +77,87 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr public void initGui() { super.initGui(); - this.addLabel(new GuiNpcLabel(0, "menu.animations", guiLeft + 8, guiTop + 4)); + int y = guiTop + 4; - this.addButton(new GuiNpcButton(1, guiLeft + 4, guiTop + ySize - 35, 50, 20, "gui.clear")); - this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, guiTop + ySize - 35, 50, 20, "gui.done")); - this.addButton(new GuiNpcButton(3, guiLeft + xSize - 108, guiTop + ySize - 35, 50, 20, "gui.cancel")); + // Toggle button for switching views + String viewLabel = showingBuiltIn ? "gui.builtinAnimations" : "gui.customAnimations"; + this.addButton(new GuiNpcButton(10, guiLeft + 4, y, 212, 20, viewLabel)); + y += 24; + // Animation scroll list if (scrollAnimations == null) { scrollAnimations = new GuiCustomScroll(this, 0, 0); - scrollAnimations.setSize(212, 150); + scrollAnimations.setSize(212, 130); } scrollAnimations.setList(getSearchList()); - if (selectedName != null) { - scrollAnimations.setSelected(selectedName); - } - scrollAnimations.guiLeft = guiLeft + 4; - scrollAnimations.guiTop = guiTop + 14; - this.addScroll(scrollAnimations); - // Search textbox - moved up to avoid overlapping with buttons - addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, guiTop + 166, 212, 20, search)); + // Set selection based on current mode + if (showingBuiltIn && !selectedBuiltInName.isEmpty()) { + scrollAnimations.setSelected(selectedBuiltInName); + } else if (!showingBuiltIn && selectedCustomName != null) { + scrollAnimations.setSelected(selectedCustomName); + } + + scrollAnimations.guiLeft = guiLeft + 4; + scrollAnimations.guiTop = y; + this.addScroll(scrollAnimations); + y += 134; + + // Search textbox + addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, y, 212, 20, search)); + y += 24; + + // Buttons + this.addButton(new GuiNpcButton(1, guiLeft + 4, y, 50, 20, "gui.clear")); + this.addButton(new GuiNpcButton(3, guiLeft + xSize - 108, y, 50, 20, "gui.cancel")); + this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, y, 50, 20, "gui.done")); } @Override public void setData(Vector list, HashMap data, EnumScrollData type) { - this.animationData = data; + if (type == EnumScrollData.BUILTIN_ANIMATIONS) { + this.builtInData = data; - // Find the name for our initial animation ID - if (initialAnimationId >= 0) { - for (String name : data.keySet()) { - if (data.get(name) == initialAnimationId) { - selectedName = name; - break; + // Check if initial name matches a built-in animation + if (!initialAnimationName.isEmpty()) { + for (String name : builtInData.keySet()) { + if (name.equalsIgnoreCase(initialAnimationName)) { + selectedBuiltInName = name; + selectedAnimationId = -1; + selectedCustomName = null; + showingBuiltIn = true; + break; + } + } + } + } else if (type == EnumScrollData.ANIMATIONS) { + this.customData = data; + + // Find the name for our initial animation ID (if not using built-in) + if (selectedBuiltInName.isEmpty() && initialAnimationId >= 0) { + for (String name : data.keySet()) { + if (data.get(name) == initialAnimationId) { + selectedCustomName = name; + showingBuiltIn = false; + break; + } } } } if (scrollAnimations != null) { scrollAnimations.setList(getSearchList()); - if (selectedName != null) { - scrollAnimations.setSelected(selectedName); + if (showingBuiltIn && !selectedBuiltInName.isEmpty()) { + scrollAnimations.setSelected(selectedBuiltInName); + } else if (!showingBuiltIn && selectedCustomName != null) { + scrollAnimations.setSelected(selectedCustomName); } } } @Override public void setSelected(String selected) { - this.selectedName = selected; - if (scrollAnimations != null) { - scrollAnimations.setSelected(selected); - } + // Not used in this implementation } @Override @@ -110,18 +168,22 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr if (search.equals(getTextField(55).getText())) return; search = getTextField(55).getText().toLowerCase(); - scrollAnimations.resetScroll(); - scrollAnimations.setList(getSearchList()); + if (scrollAnimations != null) { + scrollAnimations.resetScroll(); + scrollAnimations.setList(getSearchList()); + } } } } private List getSearchList() { + HashMap sourceData = showingBuiltIn ? builtInData : customData; + if (search.isEmpty()) { - return new ArrayList(animationData.keySet()); + return new ArrayList<>(sourceData.keySet()); } - List list = new ArrayList(); - for (String name : animationData.keySet()) { + List list = new ArrayList<>(); + for (String name : sourceData.keySet()) { if (name.toLowerCase().contains(search)) list.add(name); } @@ -130,11 +192,20 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { - if (guiCustomScroll.id == 0) { - String selected = scrollAnimations.getSelected(); - if (selected != null && animationData.containsKey(selected)) { - selectedName = selected; - selectedAnimationId = animationData.get(selected); + String selected = scrollAnimations.getSelected(); + if (selected == null) return; + + if (showingBuiltIn) { + if (builtInData.containsKey(selected)) { + selectedBuiltInName = selected; + selectedAnimationId = -1; + selectedCustomName = null; + } + } else { + if (customData.containsKey(selected)) { + selectedCustomName = selected; + selectedAnimationId = customData.get(selected); + selectedBuiltInName = ""; } } initGui(); @@ -142,27 +213,40 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (selectedAnimationId < 0) - return; - close(); + // Double-click to confirm selection + if (!selectedBuiltInName.isEmpty() || selectedAnimationId >= 0) { + close(); + } } @Override protected void actionPerformed(GuiButton guibutton) { int id = guibutton.id; - if (id == 1) { - // Clear selection - selectedName = null; + + // Toggle view button + if (id == 10) { + showingBuiltIn = !showingBuiltIn; + search = ""; + if (scrollAnimations != null) { + scrollAnimations.resetScroll(); + } + initGui(); + } + // Clear + else if (id == 1) { + selectedBuiltInName = ""; selectedAnimationId = -1; + selectedCustomName = null; close(); } - if (id == 2) { - // Done - keep selection + // Done + else if (id == 2) { close(); } - if (id == 3) { - // Cancel - restore initial value + // Cancel + else if (id == 3) { selectedAnimationId = initialAnimationId; + selectedBuiltInName = initialAnimationName; close(); } } @@ -171,6 +255,16 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr * Get the name of the selected animation (for display purposes). */ public String getSelectedName() { - return selectedName; + if (!selectedBuiltInName.isEmpty()) { + return selectedBuiltInName; + } + return selectedCustomName; + } + + /** + * Check if a built-in animation was selected. + */ + public boolean isBuiltInSelected() { + return !selectedBuiltInName.isEmpty(); } } diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index abf55a84e..235f2719b 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -6,5 +6,7 @@ public enum EnumScrollData { MAGIC_CYCLES, DIALOG_GROUP, QUEST_GROUP, - ABILITY_TYPES + ABILITY_TYPES, + ANIMATIONS, + BUILTIN_ANIMATIONS } diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index e3caddce5..e5e11d666 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -8,23 +8,43 @@ import noppes.npcs.LogWriter; import noppes.npcs.api.handler.IAnimationHandler; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; import noppes.npcs.util.NBTJsonUtil; import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; +import java.util.stream.Stream; import java.util.zip.GZIPInputStream; public class AnimationController implements IAnimationHandler { public HashMap animations; private HashMap bootOrder; + // Built-in animations loaded from assets folder (read-only, keyed by name) + public HashMap builtInAnimations = new HashMap<>(); + private static final String BUILTIN_ANIMATIONS_PATH = "/assets/customnpcs/animations"; + private static final String BUILTIN_ANIMATIONS_RESOURCE = "assets/customnpcs/animations"; + public static AnimationController Instance; private int lastUsedID = 0; @@ -42,12 +62,94 @@ public class AnimationController implements IAnimationHandler { public void load() { bootOrder = new HashMap<>(); animations = new HashMap<>(); + builtInAnimations = new HashMap<>(); LogWriter.info("Loading animations..."); + loadBuiltInAnimations(); readAnimationMap(); loadAnimations(); LogWriter.info("Done loading animations."); } + /** + * Load built-in animations from assets/customnpcs/animations/ folder. + * These animations are read-only and accessed by name only (no IDs). + * Scans the folder directly instead of using a manifest. + */ + private void loadBuiltInAnimations() { + try { + URL resourceUrl = CustomNpcs.class.getResource(BUILTIN_ANIMATIONS_PATH); + if (resourceUrl == null) { + LogWriter.info("Built-in animations folder not found: " + BUILTIN_ANIMATIONS_PATH); + return; + } + + URI uri = resourceUrl.toURI(); + Path animationsPath; + + if (uri.getScheme().equals("jar")) { + // Running from JAR - need to create a FileSystem to access it + FileSystem fileSystem = null; + try { + fileSystem = FileSystems.getFileSystem(uri); + } catch (Exception e) { + fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap()); + } + animationsPath = fileSystem.getPath(BUILTIN_ANIMATIONS_PATH); + } else { + // Running from file system (dev environment) + animationsPath = Paths.get(uri); + } + + // Scan the directory for .json files + try (Stream paths = Files.walk(animationsPath, 1)) { + paths.filter(path -> path.toString().endsWith(".json")) + .forEach(path -> { + String fileName = path.getFileName().toString(); + String animName = fileName.substring(0, fileName.length() - 5); // Remove .json + try { + loadBuiltInAnimation(animName); + } catch (Exception e) { + LogWriter.error("Error loading built-in animation: " + animName, e); + } + }); + } + + LogWriter.info("Loaded " + builtInAnimations.size() + " built-in animations."); + } catch (Exception e) { + LogWriter.error("Error scanning built-in animations folder", e); + } + } + + /** + * Load a single built-in animation by name from assets folder. + */ + private void loadBuiltInAnimation(String animName) throws Exception { + String resourcePath = BUILTIN_ANIMATIONS_PATH + "/" + animName + ".json"; + try (InputStream stream = CustomNpcs.class.getResourceAsStream(resourcePath)) { + if (stream == null) { + return; + } + + // Read JSON content + StringBuilder content = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String line; + while ((line = reader.readLine()) != null) { + content.append(line); + } + reader.close(); + + // Parse NBT from JSON + NBTTagCompound nbt = NBTJsonUtil.Convert(content.toString()); + + BuiltInAnimation animation = new BuiltInAnimation(animName); + animation.readFromNBT(nbt); + + // Store with lowercase key for case-insensitive lookup + builtInAnimations.put(animName.toLowerCase(), animation); + } + } + private void loadAnimations() { animations.clear(); @@ -110,6 +212,12 @@ public class AnimationController implements IAnimationHandler { } public IAnimation saveAnimation(IAnimation animation) { + // Reject saving built-in animations + if (animation instanceof BuiltInAnimation) { + LogWriter.info("Cannot save built-in animation: " + animation.getName()); + return animation; + } + if (animation.getID() < 0) { animation.setID(getUnusedId()); while (hasName(animation.getName())) @@ -157,6 +265,12 @@ public class AnimationController implements IAnimationHandler { public void delete(String name) { Animation delete = getAnimationFromName(name); if (delete != null) { + // Reject deleting built-in animations + if (delete instanceof BuiltInAnimation) { + LogWriter.info("Cannot delete built-in animation: " + name); + return; + } + Animation foundAnimation = this.animations.remove(delete.getID()); if (foundAnimation != null && foundAnimation.name != null) { File dir = this.getDir(); @@ -175,7 +289,8 @@ public class AnimationController implements IAnimationHandler { } public void delete(int id) { - if (!this.animations.containsKey(id)) + // Only user animations have valid IDs (>= 0) + if (id < 0 || !this.animations.containsKey(id)) return; Animation foundAnimation = this.animations.remove(id); @@ -199,20 +314,83 @@ public class AnimationController implements IAnimationHandler { } public IAnimation get(String name) { + // Check built-in animations first (by name) + BuiltInAnimation builtIn = builtInAnimations.get(name.toLowerCase()); + if (builtIn != null) { + return builtIn; + } + // Then check user animations return getAnimationFromName(name); } + /** + * Get a user animation by ID. + * Built-in animations have no IDs and cannot be retrieved this way. + */ public IAnimation get(int id) { + // Only user animations have valid IDs + if (id < 0) { + return null; + } return this.animations.get(id); } + /** + * Get a built-in animation by name. + * @param name The animation name (case-insensitive) + * @return The built-in animation, or null if not found + */ + public BuiltInAnimation getBuiltInAnimation(String name) { + return builtInAnimations.get(name.toLowerCase()); + } + public IAnimation[] getAnimations() { ArrayList animations = new ArrayList<>(this.animations.values()); return animations.toArray(new IAnimation[0]); } + /** + * Get all built-in animations. + */ + public IAnimation[] getBuiltInAnimations() { + return builtInAnimations.values().toArray(new IAnimation[0]); + } + + /** + * Get all animations (both built-in and user-created). + */ + public IAnimation[] getAllAnimations() { + ArrayList all = new ArrayList<>(); + all.addAll(builtInAnimations.values()); + all.addAll(animations.values()); + return all.toArray(new IAnimation[0]); + } + + /** + * Check if an animation name is a built-in animation. + * @param name The animation name to check + * @return true if this is a built-in animation + */ + public boolean isBuiltIn(String name) { + return builtInAnimations.containsKey(name.toLowerCase()); + } + + /** + * @deprecated Built-in animations no longer use IDs. Use isBuiltIn(String name) instead. + */ + @Deprecated + public boolean isBuiltIn(int id) { + return false; // Built-in animations have no IDs + } + public Animation getAnimationFromName(String animation) { - for (Map.Entry entryAnimation : AnimationController.getInstance().animations.entrySet()) { + // Check built-in animations first (by name) + BuiltInAnimation builtIn = builtInAnimations.get(animation.toLowerCase()); + if (builtIn != null) { + return builtIn; + } + // Then check user animations + for (Map.Entry entryAnimation : animations.entrySet()) { if (entryAnimation.getValue().name.equalsIgnoreCase(animation)) { return entryAnimation.getValue(); } @@ -220,6 +398,13 @@ public class AnimationController implements IAnimationHandler { return null; } + /** + * Get names of all built-in animations. + */ + public String[] getBuiltInAnimationNames() { + return builtInAnimations.keySet().toArray(new String[0]); + } + public String[] getNames() { String[] names = new String[animations.size()]; int i = 0; diff --git a/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java b/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java new file mode 100644 index 000000000..a30923eac --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java @@ -0,0 +1,94 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.data.IAnimation; + +/** + * A read-only animation loaded from mod assets. + * Built-in animations are identified by name only, never by ID. + * They cannot be saved, deleted, or modified. + */ +public class BuiltInAnimation extends Animation { + + public BuiltInAnimation() { + super(); + this.id = -1; // No valid ID for built-in animations + } + + public BuiltInAnimation(String name) { + super(); + this.name = name; + this.id = -1; + } + + /** + * Built-in animations cannot be saved. + * @return this animation unchanged + */ + @Override + public IAnimation save() { + // Do nothing - built-in animations are read-only + return this; + } + + /** + * Built-in animations have no valid ID. + * @return always -1 + */ + @Override + public int getID() { + return -1; + } + + /** + * Built-in animations cannot have their ID changed. + */ + @Override + public void setID(int newID) { + // Ignore - built-in animations don't use IDs + } + + /** + * Check if this is a built-in animation. + * @return always true for BuiltInAnimation + */ + public boolean isBuiltIn() { + return true; + } + + /** + * Read animation data from NBT. + * Preserves the name set in constructor (from filename) rather than reading from NBT. + */ + @Override + public void readFromNBT(NBTTagCompound compound) { + // Preserve the name set in constructor - don't read from NBT + // (filename is the source of truth for built-in animations) + String preservedName = this.name; + + speed = compound.getFloat("Speed"); + smooth = compound.getByte("Smooth"); + loop = compound.getInteger("Loop"); + + frames.clear(); + net.minecraft.nbt.NBTTagList list = compound.getTagList("Frames", 10); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound item = list.getCompoundTagAt(i); + Frame frame = new Frame(); + frame.parent = this; + frame.readFromNBT(item); + frames.add(frame); + } + + this.whileStanding = compound.getBoolean("WhileStanding"); + this.whileMoving = compound.getBoolean("WhileWalking"); + this.whileAttacking = compound.getBoolean("WhileAttacking"); + + this.currentFrame = compound.getInteger("CurrentFrame"); + this.currentFrameTime = compound.getInteger("CurrentFrameTime"); + + // Ensure name and ID are preserved + this.name = preservedName; + this.id = -1; + } +} diff --git a/src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json similarity index 100% rename from src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Active.json rename to src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json diff --git a/src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json similarity index 100% rename from src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Windup.json rename to src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json diff --git a/src/main/resources/internal/animations/ABILITYORB/Ability_Orb_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json similarity index 100% rename from src/main/resources/internal/animations/ABILITYORB/Ability_Orb_Windup.json rename to src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index fd148ea08..8e3f6a606 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -120,6 +120,8 @@ gui.range=Range gui.partly=Partially gui.copy=Copy gui.clear=Clear +gui.builtinAnimations=Built-in Animations +gui.customAnimations=Custom Animations gui.done=Done gui.paste=Paste gui.test=Test