Built In Animation System + Fixes to Defaults for Energy Projectile Abilities

This commit is contained in:
Kam committed 2026-01-28 16:31:26 -05:00
1 parent 7d2bba7b95
commit e73054a529
20 files changed
+622 -238

No files matched your search

@@ -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;
}
@@ -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<AbilityPhase, String> 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;
}
}
@@ -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
@@ -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; }
@@ -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);
@@ -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();
}
@@ -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();
}
}
@@ -44,5 +44,6 @@ public final class AnimationsGetPacket extends AbstractPacket {
return;
NoppesUtilServer.sendAnimationDataAll((EntityPlayerMP) player);
NoppesUtilServer.sendBuiltInAnimationData((EntityPlayerMP) player);
}
}
+1 -1
View File
@@ -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);
@@ -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<String, Integer> map = new HashMap<String, Integer>();
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) {
@@ -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;
}
@@ -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<String, Integer> animationData = new HashMap<String, Integer>();
// Built-in animations (name only, no ID)
private HashMap<String, Integer> builtInData = new HashMap<>();
// Custom/user animations (name -> ID)
private HashMap<String, Integer> 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<String> list, HashMap<String, Integer> 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<String> getSearchList() {
HashMap<String, Integer> sourceData = showingBuiltIn ? builtInData : customData;
if (search.isEmpty()) {
return new ArrayList<String>(animationData.keySet());
return new ArrayList<>(sourceData.keySet());
}
List<String> list = new ArrayList<String>();
for (String name : animationData.keySet()) {
List<String> 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();
}
}
@@ -6,5 +6,7 @@ public enum EnumScrollData {
MAGIC_CYCLES,
DIALOG_GROUP,
QUEST_GROUP,
ABILITY_TYPES
ABILITY_TYPES,
ANIMATIONS,
BUILTIN_ANIMATIONS
}
@@ -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<Integer, Animation> animations;
private HashMap<Integer, String> bootOrder;
// Built-in animations loaded from assets folder (read-only, keyed by name)
public HashMap<String, BuiltInAnimation> 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<Path> 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<IAnimation> 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<IAnimation> 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<Integer, Animation> 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<Integer, Animation> 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;
@@ -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;
}
}