diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index 69dbd1991..c97188316 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -11,6 +11,7 @@ import java.util.function.Supplier; public class Register { public static final Map> REGISTERED_NAMESPACES = new LinkedHashMap<>(); + public static final Map NAMESPACE_DISPLAY_NAMES = new LinkedHashMap<>(); private final String registryKey; protected final String namespace; protected final Map> entries = new LinkedHashMap<>(); @@ -71,7 +72,7 @@ public class Register { } } - public static Register.Abilities create(String namespace) { + public static Register.Abilities create(String namespace, String displayName) { if (!REGISTERED_NAMESPACES.containsKey("ability")) REGISTERED_NAMESPACES.put("ability", new ArrayList<>()); @@ -80,6 +81,7 @@ public class Register { } REGISTERED_NAMESPACES.get("ability").add(namespace); + NAMESPACE_DISPLAY_NAMES.put(namespace, displayName); return new Register.Abilities(namespace); } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 3f8cb5357..cb83caae2 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -44,7 +44,7 @@ public class DataAbilities { /** * Whether the ability system is enabled for this NPC */ - public boolean enabled = false; + public boolean enabled = true; /** * Minimum cooldown ticks between abilities (global minimum) @@ -90,6 +90,16 @@ public class DataAbilities { private transient float lockedRenderYawOffset = 0; private transient float lockedPitch = 0; + /** + * Locked position values for movement lock. + * Stored when entering a locked phase, applied after super.onLivingUpdate() + * to override any position changes from AI/physics. + */ + private transient boolean positionLocked = false; + private transient double lockedPosX = 0; + private transient double lockedPosY = 0; + private transient double lockedPosZ = 0; + // ═══════════════════════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════════════════════ @@ -150,15 +160,22 @@ public class DataAbilities { // Handle rotation lock transition from WINDUP to ACTIVE if (currentAbility.isRotationLockedDuringActive()) { - // ACTIVE or WINDUP_AND_ACTIVE: capture (or keep) rotation lock if (!rotationLocked) { captureLockedRotation(); } } else if (rotationLocked) { - // WINDUP only: release the lock now that windup is over releaseLockedRotation(); } + // Handle position lock transition from WINDUP to ACTIVE + if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) { + if (!positionLocked) { + captureLockedPosition(); + } + } else if (positionLocked) { + releaseLockedPosition(); + } + // Play active sound and animation playAbilitySound(currentAbility.getActiveSound()); playAbilityAnimation(currentAbility.getActiveAnimation()); @@ -243,8 +260,9 @@ public class DataAbilities { npc.wrappedNPC, currentAbility, target); NpcAPI.EVENT_BUS.post(completeEvent); - // Release rotation lock before cleanup + // Release locks before cleanup releaseLockedRotation(); + releaseLockedPosition(); // Clean up and roll cooldown (this sets currentAbility to null) onAbilityComplete(); @@ -259,8 +277,9 @@ public class DataAbilities { // Stop any ability animation stopAbilityAnimation(); - // Release rotation lock + // Release locks releaseLockedRotation(); + releaseLockedPosition(); // Calculate cooldown: random(min, max) + ability offset int abilityCooldownOffset = currentAbility.getCooldownTicks(); @@ -399,6 +418,9 @@ public class DataAbilities { if (ability.isRotationLockedDuringWindup()) { captureLockedRotation(); } + if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) { + captureLockedPosition(); + } spawnTelegraph(ability, target); playAbilitySound(ability.getWindUpSound()); playAbilityAnimation(ability.getWindUpAnimation()); @@ -416,6 +438,9 @@ public class DataAbilities { if (ability.isRotationLockedDuringActive()) { captureLockedRotation(); } + if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) { + captureLockedPosition(); + } // Play active sound and animation playAbilitySound(ability.getActiveSound()); @@ -625,8 +650,9 @@ public class DataAbilities { // Stop any ability animation stopAbilityAnimation(); - // Release rotation lock + // Release locks releaseLockedRotation(); + releaseLockedPosition(); // Fire interrupt event AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent( @@ -649,6 +675,7 @@ public class DataAbilities { removeTelegraph(currentAbility); stopAbilityAnimation(); releaseLockedRotation(); + releaseLockedPosition(); currentAbility.interrupt(); currentAbility = null; lastTarget = null; @@ -1027,6 +1054,80 @@ public class DataAbilities { return rotationLocked; } + // ═══════════════════════════════════════════════════════════════════ + // POSITION LOCKING + // ═══════════════════════════════════════════════════════════════════ + + /** Bit flag for position lock in the existing data watcher slot 15 bitfield */ + private static final int POSITION_LOCKED_FLAG = 32; + + /** + * Capture current position to lock NPC in place. + * Called when entering a phase with movement lock. + */ + private void captureLockedPosition() { + lockedPosX = npc.posX; + lockedPosY = npc.posY; + lockedPosZ = npc.posZ; + positionLocked = true; + npc.setBoolFlag(true, POSITION_LOCKED_FLAG); + } + + /** + * Release the position lock. + * Called when leaving the locked phase or ability completes. + */ + private void releaseLockedPosition() { + positionLocked = false; + npc.setBoolFlag(false, POSITION_LOCKED_FLAG); + } + + /** + * Apply locked position values to the NPC. + * Called AFTER super.onLivingUpdate() in EntityNPCInterface to override + * any position changes from AI pathfinding, physics, or entity collisions. + * Runs on both client and server. + */ + public void applyLockedPosition() { + if (npc.worldObj.isRemote) { + // Client-side: check flag via data watcher + boolean flagActive = npc.getBoolFlag(POSITION_LOCKED_FLAG); + if (!flagActive) { + positionLocked = false; + return; + } + // First tick the flag is active: capture current position + if (!positionLocked) { + lockedPosX = npc.posX; + lockedPosY = npc.posY; + lockedPosZ = npc.posZ; + positionLocked = true; + } + // Snap position back and set prev to prevent interpolation jitter + npc.setPosition(lockedPosX, lockedPosY, lockedPosZ); + npc.prevPosX = lockedPosX; + npc.prevPosY = lockedPosY; + npc.prevPosZ = lockedPosZ; + npc.motionX = 0; + npc.motionY = 0; + npc.motionZ = 0; + return; + } + + // Server-side: use stored locked values + if (!positionLocked) { + return; + } + + npc.setPosition(lockedPosX, lockedPosY, lockedPosZ); + npc.prevPosX = lockedPosX; + npc.prevPosY = lockedPosY; + npc.prevPosZ = lockedPosZ; + npc.motionX = 0; + npc.motionY = 0; + npc.motionZ = 0; + } + // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index e86370492..998473623 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -7,6 +7,7 @@ import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import kamkeel.npcs.util.Register; import net.minecraft.client.gui.GuiButton; @@ -57,6 +58,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC private String search = ""; private int selectedAbilityIndex = -1; + // Existing preset names for duplicate checking + private final Set existingPresetNames = new HashSet<>(); + public static int modIndex = 0; public static ScrollType scrollType = ScrollType.CNPC; @@ -64,6 +68,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC super(npc); PacketClient.sendClient(new AbilitiesGetAllPacket()); PacketClient.sendClient(new AbilitiesNpcGetPacket()); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); } public enum ScrollType { @@ -79,7 +84,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC if (Register.isEmpty("ability")) return "modded"; - return Register.REGISTERED_NAMESPACES.get("ability").get(modIndex); + String namespace = Register.REGISTERED_NAMESPACES.get("ability").get(modIndex); + String displayName = Register.NAMESPACE_DISPLAY_NAMES.get(namespace); + return displayName != null ? displayName : namespace; case ALL: return "filter.all"; default: return name(); } @@ -92,38 +99,38 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC int y = guiTop + 10; - // Enabled checkbox - addLabel(new GuiNpcLabel(100, "gui.enabled", guiLeft + 5, y + 5)); - addButton(new GuiNpcButton(100, guiLeft + 55, y, 50, 20, new String[]{"gui.no", "gui.yes"}, abilitiesEnabled ? 1 : 0)); + addButton(new GuiNpcButton(50, guiLeft + 5, y, 140, 20, scrollType.toString())); // Cooldown range: Min Cooldown and Max Cooldown (spaced apart more) - addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 115, y + 5)); - GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 165, y, 40, 20, "" + minCooldown); + addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 210, y + 5)); + GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 260, y, 40, 20, "" + minCooldown); minField.setIntegersOnly(); minField.setMinMaxDefault(0, 10000, 20); addTextField(minField); - addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 260, y + 5)); - GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 310, y, 40, 20, "" + maxCooldown); + addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 320, y + 5)); + GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 370, y, 40, 20, "" + maxCooldown); maxField.setIntegersOnly(); maxField.setMinMaxDefault(0, 10000, 60); addTextField(maxField); y += 28; + // Enabled checkbox + addButton(new GuiNpcButton(100, guiLeft + 334, y + 145, 76, 20, new String[]{"gui.disabled", "gui.enabled"}, abilitiesEnabled ? 1 : 0)); + getButton(100).packedFGColour = abilitiesEnabled ? 0x00FF00 : 0xFF0000; + // Left scroll: available ability types addLabel(new GuiNpcLabel(1, "ability.availableTypes", guiLeft + 5, y)); if (availableTypesScroll == null) { availableTypesScroll = new GuiCustomScroll(this, 0); - availableTypesScroll.setSize(140, 103); + availableTypesScroll.setSize(140, 130); } availableTypesScroll.guiLeft = guiLeft + 5; availableTypesScroll.guiTop = y + 12; availableTypesScroll.setList(getFilteredTypeList()); addScroll(availableTypesScroll); - addButton(new GuiNpcButton(50, guiLeft + 5, y + 120, 140, 20, scrollType.toString())); - // Search bar for types addTextField(new GuiNpcTextField(4, this, fontRendererObj, guiLeft + 5, y + 145, 140, 18, search)); @@ -131,9 +138,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addLabel(new GuiNpcLabel(2, "ability.npcAbilities", guiLeft + 210, y)); if (npcAbilitiesScroll == null) { npcAbilitiesScroll = new GuiCustomScroll(this, 1); - npcAbilitiesScroll.setSize(160, 130); + npcAbilitiesScroll.setSize(200, 130); } else { - npcAbilitiesScroll.setSize(160, 130); + npcAbilitiesScroll.setSize(200, 130); } npcAbilitiesScroll.guiLeft = guiLeft + 210; npcAbilitiesScroll.guiTop = y + 12; @@ -142,11 +149,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Center buttons: Add/Remove int centerX = guiLeft + 158; - addButton(new GuiNpcButton(70, centerX, y + 40, 40, 20, ">>>")); // Add - addButton(new GuiNpcButton(71, centerX, y + 62, 40, 20, "<<<")); // Remove + addButton(new GuiNpcButton(70, centerX, y + 15, 40, 20, ">>>")); // Add + addButton(new GuiNpcButton(71, centerX, y + 37, 40, 20, "<<<")); // Remove + getButton(71).setEnabled(selectedAbilityIndex >= 0); // Load button (under the add/remove arrows) - addButton(new GuiNpcButton(75, centerX, y + 90, 40, 20, "gui.load")); + addButton(new GuiNpcButton(75, centerX, y + 60, 40, 20, "gui.load")); // On/Off toggle for selected ability (under Load button) if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { @@ -167,7 +175,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1); // Save button (to the right of carrots, only enabled when ability is selected) - addButton(new GuiNpcButton(76, guiLeft + 320, y + 145, 50, 20, "gui.save")); + addButton(new GuiNpcButton(76, centerX, y + 82, 40, 20, "gui.save")); getButton(76).setEnabled(selectedAbilityIndex >= 0); } @@ -178,6 +186,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Enabled toggle if (id == 100) { abilitiesEnabled = ((GuiNpcButton) guibutton).getValue() == 1; + initGui(); save(); return; } @@ -276,7 +285,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) { setSubGui(abilityToSave.createConfigGui(this)); } else { - setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave)); + setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave, null, existingPresetNames)); } } } @@ -301,7 +310,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC if (id == 50) { if (scrollType != ScrollType.MODDED) { ScrollType[] values = ScrollType.values(); - scrollType = values[(scrollType.ordinal() + 1) % values.length]; + ScrollType next = values[(scrollType.ordinal() + 1) % values.length]; + // Skip MODDED if no namespaces are registered + if (next == ScrollType.MODDED && Register.isEmpty("ability")) { + next = ScrollType.ALL; + } + scrollType = next; } else { List list = Register.REGISTERED_NAMESPACES.get("ability"); @@ -336,6 +350,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC if (availableTypesScroll != null) { availableTypesScroll.setList(getFilteredTypeList()); } + } else if (type == EnumScrollData.CUSTOM_ABILITIES) { + existingPresetNames.clear(); + for (String key : data.keySet()) { + int tabIndex = key.indexOf('\t'); + if (tabIndex > 0) { + existingPresetNames.add(key.substring(0, tabIndex)); + } else { + existingPresetNames.add(key); + } + } } initGui(); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java index 9407d20c5..b8a4e0e66 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java @@ -7,24 +7,29 @@ import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.ISubGuiListener; import noppes.npcs.client.gui.util.SubGuiInterface; +import java.util.Set; + /** * Confirmation dialog for saving an ability preset. * Uses packet-based communication for proper client-server architecture. */ -public class SubGuiAbilitySaveConfirm extends SubGuiInterface { +public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGuiListener { private final Ability ability; private final IAbilityConfigCallback callback; + private final Set existingNames; public SubGuiAbilitySaveConfirm(Ability ability) { - this(ability, null); + this(ability, null, null); } - public SubGuiAbilitySaveConfirm(Ability ability, IAbilityConfigCallback callback) { + public SubGuiAbilitySaveConfirm(Ability ability, IAbilityConfigCallback callback, Set existingNames) { this.ability = ability; this.callback = callback; + this.existingNames = existingNames; setBackground("menubg.png"); xSize = 200; @@ -52,14 +57,32 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { int id = guibutton.id; if (id == 0) { - // Save the ability via packet - PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); - if (callback != null) { - callback.onAbilitySaved(ability); + if (existingNames != null && existingNames.contains(ability.getName())) { + setSubGui(new SubGuiDuplicateNameConfirm()); + return; } - close(); + doSave(); } else if (id == 1) { close(); } } + + private void doSave() { + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + if (callback != null) { + callback.onAbilitySaved(ability); + } + close(); + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiDuplicateNameConfirm) { + if (((SubGuiDuplicateNameConfirm) subgui).isConfirmed()) { + doSave(); + } else { + close(); + } + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java index 2bf724684..5d810862f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java @@ -5,22 +5,26 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.resources.I18n; import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Map; /** * SubGui for selecting an ability type when creating a new ability. */ -public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomScrollListener { +public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomScrollListener, ITextfieldListener { private GuiCustomScroll scroll; private final HashMap displayNameToTypeId = new HashMap<>(); + private final HashMap allDisplayNameToTypeId = new HashMap<>(); private String selectedTypeId = null; + private String search = ""; public SubGuiAbilityTypeSelect() { setBackground("menubg.png"); @@ -32,32 +36,42 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS public void initGui() { super.initGui(); - addLabel(new GuiNpcLabel(0, "ability.selectType", guiLeft + 5, guiTop + 5)); + addTextField(new GuiNpcTextField(10, this, fontRendererObj, guiLeft + 5, guiTop + 5, 190, 18, search)); if (scroll == null) { scroll = new GuiCustomScroll(this, 0); - scroll.setSize(190, 165); + scroll.setSize(190, 145); } scroll.guiLeft = guiLeft + 5; - scroll.guiTop = guiTop + 16; + scroll.guiTop = guiTop + 26; addScroll(scroll); - List list = buildTypeList(); - scroll.setList(list); + buildAllTypes(); + scroll.setList(getFilteredTypeList()); addButton(new GuiNpcButton(0, guiLeft + 5, guiTop + 188, 90, 20, "gui.add")); getButton(0).setEnabled(scroll.hasSelected()); addButton(new GuiNpcButton(1, guiLeft + 105, guiTop + 188, 90, 20, "gui.cancel")); } - private List buildTypeList() { - List list = new ArrayList<>(); - displayNameToTypeId.clear(); + private void buildAllTypes() { + allDisplayNameToTypeId.clear(); String[] types = AbilityController.Instance.getTypes(); for (String typeId : types) { String displayName = I18n.format(typeId); - list.add(displayName); - displayNameToTypeId.put(displayName, typeId); + allDisplayNameToTypeId.put(displayName, typeId); + } + } + + private List getFilteredTypeList() { + List list = new ArrayList<>(); + displayNameToTypeId.clear(); + for (Map.Entry entry : allDisplayNameToTypeId.entrySet()) { + String displayName = entry.getKey(); + if (search.isEmpty() || displayName.toLowerCase().contains(search) || entry.getValue().toLowerCase().contains(search)) { + list.add(displayName); + displayNameToTypeId.put(displayName, entry.getValue()); + } } return list; } @@ -89,6 +103,22 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS } } + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + if (getTextField(10) != null && getTextField(10).isFocused()) { + if (!search.equals(getTextField(10).getText())) { + search = getTextField(10).getText().toLowerCase(); + scroll.setList(getFilteredTypeList()); + scroll.resetScroll(); + } + } + } + + @Override + public void unFocused(GuiNpcTextField textField) { + } + public String getSelectedTypeId() { return selectedTypeId; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiDuplicateNameConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiDuplicateNameConfirm.java new file mode 100644 index 000000000..b16386c32 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiDuplicateNameConfirm.java @@ -0,0 +1,47 @@ +package noppes.npcs.client.gui.advanced; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * Confirmation dialog shown when saving an ability preset with a name that already exists. + */ +public class SubGuiDuplicateNameConfirm extends SubGuiInterface { + + private boolean confirmed = false; + + public SubGuiDuplicateNameConfirm() { + setBackground("menubg.png"); + xSize = 220; + ySize = 80; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.duplicate.name", guiLeft + 10, y)); + + y += 30; + addButton(new GuiNpcButton(0, guiLeft + 20, y, 80, 20, "gui.continue")); + addButton(new GuiNpcButton(1, guiLeft + 120, y, 80, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + confirmed = true; + close(); + } else if (guibutton.id == 1) { + confirmed = false; + close(); + } + } + + public boolean isConfirmed() { + return confirmed; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index 5296bb615..f911c6f43 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -16,7 +16,9 @@ import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.SubGuiAbilityTypeSelect; +import noppes.npcs.client.gui.advanced.SubGuiDuplicateNameConfirm; import noppes.npcs.client.gui.util.AbilityPreviewExecutor; import noppes.npcs.client.gui.util.GuiAbilityInterface; import noppes.npcs.client.gui.util.GuiCustomScroll; @@ -62,6 +64,10 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private String search = ""; private Ability selectedAbility = null; + // ==================== PENDING SAVE ==================== + private Ability pendingSaveAbility = null; + private boolean pendingNewCreation = false; + // ==================== PREVIEW ==================== private AbilityPreviewExecutor previewExecutor; private long prevTick = 0; @@ -343,21 +349,76 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface Ability newAbility = AbilityController.Instance.create(typeId); if (newAbility != null) { newAbility.setId(UUID.randomUUID().toString()); - newAbility.setName(newAbility.getTypeId()); - // Save to server, then open config GUI - PacketClient.sendClient(new CustomAbilitySavePacket(newAbility.writeNBT())); - selectedAbility = newAbility; - selected = newAbility.getName(); - selectedUuid = newAbility.getId(); - previewExecutor.stop(); - setSubGui(selectedAbility.createConfigGui(this)); + if (hasDuplicateName(newAbility)) { + pendingSaveAbility = newAbility; + pendingNewCreation = true; + setSubGui(new SubGuiDuplicateNameConfirm()); + return; + } + saveAndOpenConfig(newAbility); return; } } + } else if (subgui instanceof SubGuiDuplicateNameConfirm) { + SubGuiDuplicateNameConfirm confirm = (SubGuiDuplicateNameConfirm) subgui; + if (confirm.isConfirmed() && pendingSaveAbility != null) { + if (pendingNewCreation) { + saveAndOpenConfig(pendingSaveAbility); + pendingSaveAbility = null; + pendingNewCreation = false; + return; + } else { + PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT())); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); + pendingSaveAbility = null; + } + } else if (pendingSaveAbility != null) { + if (pendingNewCreation) { + // Cancel new creation - discard + pendingSaveAbility = null; + pendingNewCreation = false; + } else { + // Cancel edit save - re-open config GUI to change name + Ability ability = pendingSaveAbility; + pendingSaveAbility = null; + setSubGui(ability.createConfigGui(this)); + return; + } + } + } else if (pendingSaveAbility != null) { + // Config GUI closed with a pending save (set by onAbilitySaved) + if (hasDuplicateName(pendingSaveAbility)) { + pendingNewCreation = false; + setSubGui(new SubGuiDuplicateNameConfirm()); + return; + } + PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT())); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); + pendingSaveAbility = null; } initGui(); } + private void saveAndOpenConfig(Ability ability) { + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + selectedAbility = ability; + selected = ability.getName(); + selectedUuid = ability.getId(); + previewExecutor.stop(); + setSubGui(ability.createConfigGui(this)); + } + + private boolean hasDuplicateName(Ability ability) { + String name = ability.getName(); + if (name == null || name.isEmpty()) return false; + for (java.util.Map.Entry entry : displayToUuid.entrySet()) { + if (entry.getKey().equals(name) && !entry.getValue().equals(ability.getId())) { + return true; + } + } + return false; + } + @Override public void confirmClicked(boolean result, int id) { NoppesUtil.openGUI(player, this); @@ -375,7 +436,8 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void onAbilitySaved(Ability ability) { - PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + // Defer save until subGuiClosed where we can check for duplicate names + pendingSaveAbility = ability; } @Override diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java index 31a093b8d..278903bb7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java @@ -135,6 +135,9 @@ public class GuiAbilityInterface extends GuiNPCInterface2 { * Draw the 3D preview with NPC and entities. */ protected void drawPreview(int mouseX, int mouseY, float partialTicks) { + if (hasSubGui()) + return; + GL11.glColor4f(1, 1, 1, 1); // Enable scissor to clip rendering to preview area diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 9e6366fdf..6b344f9b9 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -481,8 +481,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti super.onLivingUpdate(); - // Apply locked rotation after super.onLivingUpdate() to override look helper changes + // Apply locks after super.onLivingUpdate() to override AI/physics changes abilities.applyLockedRotation(); + abilities.applyLockedPosition(); handleMountRiderState(); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 2158e9ea4..7317856c9 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -164,6 +164,7 @@ gui.playerMap=Regen PlayerMap gui.unused=Unused gui.apply=Apply gui.cancel=Cancel +gui.continue=Continue gui.yes=Yes gui.lineColor1=Upper line color @@ -1829,6 +1830,7 @@ ability.dualFireDelay=Fire Delay # Ability Save/Load ability.save.confirm=Save ability preset ability.save.overwrite=Overwrite existing preset +ability.duplicate.name=A preset with this name already exists ability.load.select=Select ability to load: ability.load.mode=How should this ability be loaded? ability.clone=Clone