From c339e5edbcfe57c80a85704cc94b2bd62d10b3c8 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 28 Jan 2026 16:57:49 -0500 Subject: [PATCH] Fix Built In Animation --- .../data/ability/type/AbilityDisc.java | 12 +- .../data/ability/type/AbilityEnergyBeam.java | 12 +- .../data/ability/type/AbilityLaserShot.java | 12 +- .../data/ability/type/AbilityOrb.java | 12 +- .../data/ability/type/AbilitySweeper.java | 12 +- .../kamkeel/npcs/network/PacketHandler.java | 2 + .../npcs/network/enums/EnumRequestPacket.java | 1 + .../animation/BuiltInAnimationGetPacket.java | 72 +++++++++++ .../gui/global/GuiNPCManageAnimations.java | 118 ++++++++++++++---- .../npcs/controllers/AnimationController.java | 23 ++-- .../assets/customnpcs/lang/en_US.lang | 2 + 11 files changed, 205 insertions(+), 73 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java 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 d85720b6e..72b97839d 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 @@ -122,12 +122,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { discEntity.startMoving(target); } - // If movement is locked during active phase, keep ability active until entity dies - // Otherwise signal completion immediately - if (!lockMovement.locksActive()) { - discEntity = null; - signalCompletion(); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } @Override @@ -157,8 +153,8 @@ 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) { + // Signal completion when entity dies + if (discEntity == null || discEntity.isDead) { discEntity = null; signalCompletion(); } 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 7ebd4bb7b..d4620c2bb 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 @@ -123,12 +123,8 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { beamEntity.startFiring(target); } - // If movement is locked during active phase, keep ability active until entity dies - // Otherwise signal completion immediately - if (!lockMovement.locksActive()) { - beamEntity = null; - signalCompletion(); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } @Override @@ -161,8 +157,8 @@ 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) { + // Signal completion when entity dies + if (beamEntity == null || beamEntity.isDead) { beamEntity = null; signalCompletion(); } 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 a9cf74473..eeeebfa74 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 @@ -128,18 +128,14 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { world.spawnEntityInWorld(laserEntity); - // If movement is locked during active phase, keep ability active until entity dies - // Otherwise signal completion immediately - if (!lockMovement.locksActive()) { - laserEntity = null; - signalCompletion(); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // If we're tracking the entity for movement lock, check if it's dead - if (laserEntity != null && laserEntity.isDead) { + // Signal completion when entity dies + if (laserEntity == null || laserEntity.isDead) { laserEntity = null; signalCompletion(); } 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 d69a5c944..badd4aec8 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 @@ -121,12 +121,8 @@ public class AbilityOrb extends Ability implements IAbilityOrb { orbEntity.startMoving(target); } - // If movement is locked during active phase, keep ability active until entity dies - // Otherwise signal completion immediately - if (!lockMovement.locksActive()) { - orbEntity = null; - signalCompletion(); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } @Override @@ -155,8 +151,8 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @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 (orbEntity != null && orbEntity.isDead) { + // Signal completion when entity dies + if (orbEntity == null || orbEntity.isDead) { orbEntity = null; signalCompletion(); } 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 78dae4fdc..d2dced201 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 @@ -100,18 +100,14 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { lockOnTarget); world.spawnEntityInWorld(activeEntity); - // If movement is locked during active phase, keep ability active until entity dies - // Otherwise signal completion immediately - if (!lockMovement.locksActive()) { - activeEntity = null; - signalCompletion(); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // If we're tracking the entity for movement lock, check if it's dead - if (activeEntity != null && activeEntity.isDead) { + // Signal completion when entity dies + if (activeEntity == null || activeEntity.isDead) { activeEntity = null; signalCompletion(); } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index d09882cfc..e659fef74 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -96,6 +96,7 @@ import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket; +import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket; import kamkeel.npcs.network.packets.request.bank.BankGetPacket; import kamkeel.npcs.network.packets.request.bank.BankRemovePacket; import kamkeel.npcs.network.packets.request.bank.BankSavePacket; @@ -469,6 +470,7 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new AnimationGetPacket()); REQUEST_PACKET.registerPacket(new AnimationRemovePacket()); REQUEST_PACKET.registerPacket(new AnimationSavePacket()); + REQUEST_PACKET.registerPacket(new BuiltInAnimationGetPacket()); // Moving Path REQUEST_PACKET.registerPacket(new MovingPathSavePacket()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 005da40a7..bdd566785 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -93,6 +93,7 @@ public enum EnumRequestPacket { AnimationGet, AnimationRemove, AnimationSave, + BuiltInAnimationGet, RemoteNpcsGet, RemoteTpToNpc, diff --git a/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java new file mode 100644 index 000000000..e214689d8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java @@ -0,0 +1,72 @@ +package kamkeel.npcs.network.packets.request.animation; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; + +import java.io.IOException; + +/** + * Packet to request a built-in animation by name. + * Built-in animations don't have IDs, so we fetch by name. + */ +public final class BuiltInAnimationGetPacket extends AbstractPacket { + public static String packetName = "Request|BuiltInAnimationGet"; + + private String animationName; + + public BuiltInAnimationGetPacket() { + } + + public BuiltInAnimationGetPacket(String animationName) { + this.animationName = animationName; + } + + @Override + public Enum getType() { + return EnumRequestPacket.BuiltInAnimationGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, animationName); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String name = ByteBufUtils.readString(in); + BuiltInAnimation animation = AnimationController.getInstance().getBuiltInAnimation(name); + if (animation != null) { + NBTTagCompound compound = animation.writeToNBT(); + // Mark as built-in so client knows + compound.setBoolean("BuiltIn", true); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java index 7d1fa84be..0141e931a 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java @@ -5,6 +5,7 @@ import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket; +import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; @@ -37,13 +38,19 @@ import java.util.Vector; public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrollData, ICustomScrollListener, ITextfieldListener, IGuiData, ISubGuiListener, GuiYesNoCallback { private GuiCustomScroll scrollAnimations; - private HashMap data = new HashMap(); + private HashMap customData = new HashMap(); + private HashMap builtInData = new HashMap(); private Animation animation = new Animation(); public boolean playingAnimation = false; private long prevTick; private String selected = null; private String search = ""; + // Toggle between Custom (false) and Built-in (true) animations + private boolean showingBuiltIn = false; + // Track if current animation is built-in + private boolean currentIsBuiltIn = false; + public GuiNPCManageAnimations(EntityNPCInterface npc, boolean save) { super(npc); this.setSave(save); @@ -58,8 +65,15 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void initGui() { super.initGui(); - this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); - this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove")); + // Add/Remove buttons - only show for custom animations + if (!showingBuiltIn) { + this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); + this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove")); + } + + // Toggle button for Custom/Built-in - under Remove button + String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom"; + this.addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 56, 45, 20, toggleLabel)); if (scrollAnimations == null) { scrollAnimations = new GuiCustomScroll(this, 0, 0); @@ -68,13 +82,23 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol scrollAnimations.guiLeft = guiLeft + 220; scrollAnimations.guiTop = guiTop + 4; addScroll(scrollAnimations); + + // Search field addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); - if (animation.id == -1) + // Update scroll list based on current view + scrollAnimations.setList(getSearchList()); + + if (animation.id == -1 && !currentIsBuiltIn) return; - addLabel(new GuiNpcLabel(10, "ID", guiLeft + 368, guiTop + 192)); - addLabel(new GuiNpcLabel(11, animation.id + "", guiLeft + 368, guiTop + 192 + 10)); + // ID label - only for custom animations + if (!currentIsBuiltIn) { + addLabel(new GuiNpcLabel(10, "ID", guiLeft + 368, guiTop + 192)); + addLabel(new GuiNpcLabel(11, animation.id + "", guiLeft + 368, guiTop + 192 + 10)); + } else { + addLabel(new GuiNpcLabel(10, "gui.builtin", guiLeft + 368, guiTop + 192)); + } AnimationData data = npc.display.animationData; if (!playingAnimation) { @@ -90,12 +114,13 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol } } - if (animation != null) { + // Edit button - only for custom animations + if (animation != null && !currentIsBuiltIn) { this.addButton(new GuiNpcButton(100, guiLeft + 10, guiTop + 192, 45, 20, "gui.edit")); } String animTexture = "customnpcs:textures/gui/animation.png"; - int playButtonOffsetX = 140; + int playButtonOffsetX = currentIsBuiltIn ? 80 : 140; if (animation != null && !animation.frames.isEmpty()) { if (!this.playingAnimation || data.animation.paused) {//Play this.addLabel(new GuiNpcLabel(90, data.animation.paused ? "animation.paused" : "animation.stopped", guiLeft + playButtonOffsetX - 15, guiTop + 198)); @@ -151,12 +176,17 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol } } + private HashMap getCurrentData() { + return showingBuiltIn ? builtInData : customData; + } + private List getSearchList() { + HashMap data = getCurrentData(); if (search.isEmpty()) { - return new ArrayList(this.data.keySet()); + return new ArrayList(data.keySet()); } List list = new ArrayList(); - for (String name : this.data.keySet()) { + for (String name : data.keySet()) { if (name.toLowerCase().contains(search)) list.add(name); } @@ -167,16 +197,31 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol protected void actionPerformed(GuiButton guibutton) { GuiNpcButton button = (GuiNpcButton) guibutton; - if (button.id == 0) { + // Toggle button + if (button.id == 10) { + showingBuiltIn = !showingBuiltIn; + // Clear selection when switching views + selected = null; + animation = new Animation(); + currentIsBuiltIn = false; + scrollAnimations.clear(); + initGui(); + return; + } + + // Add button - only for custom + if (button.id == 0 && !showingBuiltIn) { save(); String name = "New"; - while (data.containsKey(name)) + while (customData.containsKey(name)) name += "_"; Animation animation = new Animation(-1, name); PacketClient.sendClient(new AnimationSavePacket(animation.writeToNBT())); } - if (button.id == 1) { - if (data.containsKey(scrollAnimations.getSelected())) { + + // Remove button - only for custom + if (button.id == 1 && !showingBuiltIn) { + if (customData.containsKey(scrollAnimations.getSelected())) { GuiYesNo guiyesno = new GuiYesNo(this, scrollAnimations.getSelected(), StatCollector.translateToLocal("gui.delete"), 1); displayGuiScreen(guiyesno); } @@ -204,7 +249,8 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol data.animation.paused = false; } - if (guibutton.id == 100) { + // Edit button - only for custom + if (guibutton.id == 100 && !currentIsBuiltIn) { NoppesUtil.openGUI(player, new GuiNPCEditAnimation(this, this.animation, npc)); } @@ -215,6 +261,10 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void setGuiData(NBTTagCompound compound) { this.animation = new Animation(); animation.readFromNBT(compound); + + // Check if this is a built-in animation + currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn"); + setSelected(animation.name); npc.display.animationData.setAnimation(this.animation); this.playingAnimation = false; @@ -225,7 +275,13 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol @Override public void setData(Vector list, HashMap data, EnumScrollData type) { String name = scrollAnimations.getSelected(); - this.data = data; + + if (type == EnumScrollData.ANIMATIONS) { + this.customData = data; + } else if (type == EnumScrollData.BUILTIN_ANIMATIONS) { + this.builtInData = data; + } + scrollAnimations.setList(getSearchList()); if (name != null) @@ -242,28 +298,40 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { if (guiCustomScroll.id == 0) { selected = scrollAnimations.getSelected(); - PacketClient.sendClient(new AnimationGetPacket(data.get(selected))); + HashMap data = getCurrentData(); + + if (showingBuiltIn) { + // Fetch built-in animation by name + PacketClient.sendClient(new BuiltInAnimationGetPacket(selected)); + } else { + // Fetch custom animation by ID + if (data.containsKey(selected)) { + PacketClient.sendClient(new AnimationGetPacket(data.get(selected))); + } + } } } public void save() { - if (selected != null && data.containsKey(selected) && animation != null) { + // Only save custom animations + if (selected != null && customData.containsKey(selected) && animation != null && !currentIsBuiltIn) { PacketClient.sendClient(new AnimationSavePacket(animation.writeToNBT())); } } @Override public void unFocused(GuiNpcTextField guiNpcTextField) { - if (animation.id == -1) + // Don't allow renaming built-in animations + if (animation.id == -1 || currentIsBuiltIn) return; if (guiNpcTextField.id == 0) { String name = guiNpcTextField.getText(); - if (!name.isEmpty() && !data.containsKey(name)) { + if (!name.isEmpty() && !customData.containsKey(name)) { String old = animation.name; - data.remove(animation.name); + customData.remove(animation.name); animation.name = name; - data.put(animation.name, animation.id); + customData.put(animation.name, animation.id); selected = name; scrollAnimations.replace(old, animation.name); } @@ -280,10 +348,12 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol if (!result) return; if (id == 1) { - if (data.containsKey(scrollAnimations.getSelected())) { - PacketClient.sendClient(new AnimationRemovePacket(data.get(selected))); + // Only delete custom animations + if (customData.containsKey(scrollAnimations.getSelected()) && !showingBuiltIn) { + PacketClient.sendClient(new AnimationRemovePacket(customData.get(selected))); scrollAnimations.clear(); animation = new Animation(); + currentIsBuiltIn = false; initGui(); } } diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index e5e11d666..c6837b6c7 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -314,13 +314,18 @@ public class AnimationController implements IAnimationHandler { } public IAnimation get(String name) { - // Check built-in animations first (by name) + // Check user/custom animations first (allows overriding built-in) + for (Map.Entry entry : animations.entrySet()) { + if (entry.getValue().name.equalsIgnoreCase(name)) { + return entry.getValue(); + } + } + // Then check built-in animations BuiltInAnimation builtIn = builtInAnimations.get(name.toLowerCase()); if (builtIn != null) { return builtIn; } - // Then check user animations - return getAnimationFromName(name); + return null; } /** @@ -384,17 +389,17 @@ public class AnimationController implements IAnimationHandler { } public Animation getAnimationFromName(String animation) { - // Check built-in animations first (by name) - BuiltInAnimation builtIn = builtInAnimations.get(animation.toLowerCase()); - if (builtIn != null) { - return builtIn; - } - // Then check user animations + // Check user/custom animations first (allows overriding built-in) for (Map.Entry entryAnimation : animations.entrySet()) { if (entryAnimation.getValue().name.equalsIgnoreCase(animation)) { return entryAnimation.getValue(); } } + // Then check built-in animations + BuiltInAnimation builtIn = builtInAnimations.get(animation.toLowerCase()); + if (builtIn != null) { + return builtIn; + } return null; } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 8e3f6a606..de60eda25 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.builtin=Built-in +gui.custom=Custom gui.builtinAnimations=Built-in Animations gui.customAnimations=Custom Animations gui.done=Done