From 45a4313b70c344097763ac6d3c8d16c0b8e6a429 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 4 Feb 2026 21:50:39 -0500 Subject: [PATCH 01/34] Enhance ability system with position locking and duplicate name confirmation for presets --- src/main/java/kamkeel/npcs/util/Register.java | 4 +- src/main/java/noppes/npcs/DataAbilities.java | 113 +++++++++++++++++- .../client/gui/advanced/GuiNPCAbilities.java | 62 +++++++--- .../advanced/SubGuiAbilitySaveConfirm.java | 39 ++++-- .../gui/advanced/SubGuiAbilityTypeSelect.java | 54 +++++++-- .../advanced/SubGuiDuplicateNameConfirm.java | 47 ++++++++ .../gui/global/GuiNpcManageAbilities.java | 80 +++++++++++-- .../client/gui/util/GuiAbilityInterface.java | 3 + .../npcs/entity/EntityNPCInterface.java | 3 +- .../assets/customnpcs/lang/en_US.lang | 2 + 10 files changed, 351 insertions(+), 56 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiDuplicateNameConfirm.java 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 From 095709b9d2c5ae7beed84de5f0f3d173823a7e86 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 5 Feb 2026 22:49:40 -0500 Subject: [PATCH 02/34] Add ability wheel and toggle abilities for enhanced gameplay --- .../kamkeel/npcs/command/AbilityCommand.java | 192 ++++++++++++++++++ .../kamkeel/npcs/command/CommandKamkeel.java | 4 + .../data/ability/AbilityController.java | 77 ++++++- .../packets/player/SpecialKeyStatePacket.java | 10 + src/main/java/noppes/npcs/DataAbilities.java | 10 + .../java/noppes/npcs/NoppesUtilServer.java | 4 + .../client/gui/advanced/GuiNPCAbilities.java | 6 +- .../scripted/wrapper/ScriptDataAbilities.java | 23 ++- .../wrapper/ScriptPlayerAbilityData.java | 14 +- 9 files changed, 330 insertions(+), 10 deletions(-) diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index d5b9420cc..004274fc3 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -2,7 +2,12 @@ package kamkeel.npcs.command; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.UserType; import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; import java.util.ArrayList; import java.util.Collections; @@ -122,5 +127,192 @@ public class AbilityCommand extends CommandKamkeelBase { } sendResult(sender, " Range: \u00A7e" + ability.getMinRange() + " - " + ability.getMaxRange()); sendResult(sender, " Telegraph: \u00A7e" + (ability.isShowTelegraph() ? "Yes" : "No")); + sendResult(sender, " Allowed By: \u00A7e" + ability.getAllowedBy().name()); + } + + @SubCommand( + desc = "List all built-in abilities available for players" + ) + public void prebuilts(ICommandSender sender, String[] args) { + Set names = AbilityController.Instance.getAbilityNames(); + if (names.isEmpty()) { + sendResult(sender, "No built-in abilities found."); + return; + } + + List playerAbilities = new ArrayList<>(); + for (String name : names) { + Ability ability = AbilityController.Instance.getAbility(name); + if (ability != null && ability.getAllowedBy().allowsPlayer()) { + playerAbilities.add(name); + } + } + + if (playerAbilities.isEmpty()) { + sendResult(sender, "No player-usable built-in abilities found."); + return; + } + + Collections.sort(playerAbilities); + sendResult(sender, "Built-in Player Abilities (" + playerAbilities.size() + "):"); + for (String name : playerAbilities) { + Ability ability = AbilityController.Instance.getAbility(name); + String userType = ability.getAllowedBy() == UserType.PLAYER_ONLY ? "\u00A7d[PLAYER]" : "\u00A7a[BOTH]"; + sendResult(sender, " - \u00A7b" + name + " " + userType); + } + } + + @SubCommand( + desc = "Unlock an ability for a player", + usage = " " + ) + public void unlock(ICommandSender sender, String[] args) { + if (args.length < 2) { + sendError(sender, "Usage: /kam ability unlock "); + return; + } + + String playerName = args[0]; + String abilityKey = joinArgs(args, 1); + + // Find player + EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName); + if (player == null) { + sendError(sender, "Player not found: " + playerName); + return; + } + + // Resolve ability + Ability ability = AbilityController.Instance.resolveAbility(abilityKey); + if (ability == null) { + sendError(sender, "Ability not found: " + abilityKey); + return; + } + + // Use the ability's canonical ID for storage (registry key for built-in, UUID for custom) + String canonicalKey = ability.getId(); + if (canonicalKey == null || canonicalKey.isEmpty()) { + canonicalKey = abilityKey; // Fallback to user-provided key + } + String displayName = ability.getName() != null ? ability.getName() : canonicalKey; + + // Check if ability allows players + if (!ability.getAllowedBy().allowsPlayer()) { + sendError(sender, "Ability '\u00A7b" + displayName + "\u00A7c' is NPC-only and cannot be given to players."); + return; + } + + // Get player data and unlock + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (data.abilityData.hasUnlockedAbility(canonicalKey)) { + sendError(sender, "Player already has ability: " + displayName); + return; + } + + data.abilityData.unlockAbility(canonicalKey); + sendResult(sender, "Unlocked ability '\u00A7b" + displayName + "\u00A77' for player \u00A7a" + playerName); + } + + @SubCommand( + desc = "Lock (revoke) an ability from a player", + usage = " " + ) + public void lock(ICommandSender sender, String[] args) { + if (args.length < 2) { + sendError(sender, "Usage: /kam ability lock "); + return; + } + + String playerName = args[0]; + String abilityKey = joinArgs(args, 1); + + // Find player + EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName); + if (player == null) { + sendError(sender, "Player not found: " + playerName); + return; + } + + // Resolve ability to get canonical key + Ability ability = AbilityController.Instance.resolveAbility(abilityKey); + String canonicalKey = abilityKey; + String displayName = abilityKey; + if (ability != null) { + canonicalKey = ability.getId() != null ? ability.getId() : abilityKey; + displayName = ability.getName() != null ? ability.getName() : canonicalKey; + } + + // Get player data and lock + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (!data.abilityData.hasUnlockedAbility(canonicalKey)) { + sendError(sender, "Player doesn't have ability: " + displayName); + return; + } + + data.abilityData.lockAbility(canonicalKey); + sendResult(sender, "Locked ability '\u00A7b" + displayName + "\u00A77' from player \u00A7a" + playerName); + } + + @SubCommand( + desc = "List abilities unlocked for a player", + usage = "" + ) + public void player(ICommandSender sender, String[] args) { + if (args.length < 1) { + sendError(sender, "Usage: /kam ability player "); + return; + } + + String playerName = args[0]; + + // Find player + EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName); + if (player == null) { + sendError(sender, "Player not found: " + playerName); + return; + } + + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + String[] abilityKeys = data.abilityData.getUnlockedAbilities(); + + if (abilityKeys.length == 0) { + sendResult(sender, "Player \u00A7a" + playerName + "\u00A77 has no unlocked abilities."); + return; + } + + sendResult(sender, "Abilities for \u00A7a" + playerName + "\u00A77 (" + abilityKeys.length + "):"); + int selected = data.abilityData.getSelectedIndex(); + for (int i = 0; i < abilityKeys.length; i++) { + String key = abilityKeys[i]; + Ability ability = AbilityController.Instance.resolveAbility(key); + String displayName = ability != null && ability.getName() != null ? ability.getName() : key; + String prefix = (i == selected) ? "\u00A7e> " : " "; + // Show display name and key if different + if (!displayName.equals(key)) { + sendResult(sender, prefix + "\u00A7b" + displayName + " \u00A78[" + key + "]"); + } else { + sendResult(sender, prefix + "\u00A7b" + displayName); + } + } + } + + private String joinArgs(String[] args, int startIndex) { + StringBuilder sb = new StringBuilder(); + for (int i = startIndex; i < args.length; i++) { + if (i > startIndex) sb.append(" "); + sb.append(args[i]); + } + return sb.toString(); + } + + /** + * Get list of player-usable ability keys (for tab completion). + * Uses registry keys for built-in abilities (no spaces) and UUIDs for custom. + * Used by CommandKamkeel for usage token. + */ + public static List getPlayerAbilityNames() { + List keys = new ArrayList<>(AbilityController.Instance.getPlayerAbilityKeys()); + Collections.sort(keys); + return keys; } } diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java index d0341f211..91356c825 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import static kamkeel.npcs.command.AbilityCommand.getPlayerAbilityNames; import static kamkeel.npcs.command.EffectCommand.getSortedEffectNames; public class CommandKamkeel extends CommandBase { @@ -135,6 +136,9 @@ public class CommandKamkeel extends CommandBase { } else if (usage.equals("")) { List keys = getSortedEffectNames(); return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()])); + } else if (usage.equals("")) { + List keys = getPlayerAbilityNames(); + return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()])); } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 213c1a214..05110f553 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -180,30 +180,93 @@ public class AbilityController implements IAbilityHandler { // ═══════════════════════════════════════════════════════════════════════════ /** - * Resolve an ability by key. Checks built-in abilities first (by name), - * then custom abilities (by UUID). Returns a copy safe to modify. + * Resolve an ability by key. Checks built-in abilities first (by name or registry key), + * then custom abilities (by UUID or name). Returns a copy safe to modify for + * custom abilities, or the singleton instance for built-in abilities. * - * @param key The ability name (built-in) or UUID (custom) - * @return A copy of the resolved ability, or null if not found + * @param key The ability name, registry key (built-in) or UUID (custom) + * @return The resolved ability, or null if not found */ public Ability resolveAbility(String key) { if (key == null || key.isEmpty()) return null; - // Check built-in abilities first + // Check built-in abilities first (exact match by name) Ability builtIn = abilities.get(key); if (builtIn != null) { - return fromNBT(builtIn.writeNBT()); + return builtIn; } - // Check custom abilities + // Check built-in abilities by name (case-insensitive) + for (Map.Entry entry : abilities.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue(); + } + } + + // Check built-in abilities by registry key / ID + for (Ability ability : abilities.values()) { + if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) { + return ability; + } + } + + // Check custom abilities (exact UUID match) Ability custom = customAbilities.get(key); if (custom != null) { return fromNBT(custom.writeNBT()); } + // Check custom abilities by name (case-insensitive) + for (Ability ability : customAbilities.values()) { + if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) { + return fromNBT(ability.writeNBT()); + } + } + return null; } + /** + * Get all keys that can be used to look up abilities (for tab completion). + * Returns registry keys for built-in abilities (no spaces) and UUIDs for custom. + */ + public Set getAbilityKeys() { + Set keys = new LinkedHashSet<>(); + // Built-in abilities - use the ID (registry key for BuiltInAbility) + for (Ability ability : abilities.values()) { + String id = ability.getId(); + if (id != null && !id.isEmpty()) { + keys.add(id); + } + } + // Custom abilities - use UUID + keys.addAll(customAbilities.keySet()); + return keys; + } + + /** + * Get keys for player-usable abilities only (for tab completion). + */ + public Set getPlayerAbilityKeys() { + Set keys = new LinkedHashSet<>(); + // Built-in abilities - use the ID (registry key for BuiltInAbility) + for (Ability ability : abilities.values()) { + if (ability.getAllowedBy().allowsPlayer()) { + String id = ability.getId(); + if (id != null && !id.isEmpty()) { + keys.add(id); + } + } + } + // Custom abilities - use UUID (if player-usable) + for (Map.Entry entry : customAbilities.entrySet()) { + if (entry.getValue().getAllowedBy().allowsPlayer()) { + keys.add(entry.getKey()); + } + } + return keys; + } + /** * Check if an ability key exists (either built-in or custom). */ diff --git a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java index 48ac3391b..a58b896f4 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java @@ -59,6 +59,16 @@ public final class SpecialKeyStatePacket extends AbstractPacket { // Trigger ability activation on key press (rising edge) if (keyDown && !wasDown) { + // Don't activate if player has a GUI open (not the default inventory) + if (player.openContainer != null && !(player.openContainer instanceof net.minecraft.inventory.ContainerPlayer)) { + return; + } + + // Don't activate if player is riding an entity (mount) + if (player.ridingEntity != null) { + return; + } + data.abilityData.activateAbility(player); } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index cb83caae2..36dc6f34a 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -368,6 +368,11 @@ public class DataAbilities { * Check if a specific ability is eligible for use. */ private boolean isAbilityEligible(Ability ability, EntityLivingBase target) { + // Check UserType allows NPCs + if (!ability.getAllowedBy().allowsNpc()) { + return false; + } + // Check enabled if (!ability.isEnabled()) { return false; @@ -695,6 +700,11 @@ public class DataAbilities { return false; } + // Check UserType allows NPCs + if (!ability.getAllowedBy().allowsNpc()) { + return false; + } + // Stop any currently executing ability stopCurrentAbility(); diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 4b93ad2b5..7faa7b2b5 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -2,6 +2,7 @@ package noppes.npcs; import io.netty.buffer.ByteBuf; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.packets.data.ParticlePacket; @@ -792,6 +793,9 @@ public class NoppesUtilServer { if (controller != null) { int index = 0; for (String uuid : controller.getCustomAbilityIds()) { + Ability ability = controller.getCustomAbility(uuid); + if (ability == null || !ability.getAllowedBy().allowsNpc()) continue; + String name = controller.getCustomAbilityName(uuid); if (name == null || name.isEmpty()) name = uuid; map.put(name + "\t" + uuid, index++); 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 998473623..fd8461245 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -334,8 +334,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC Map dummyMap = new HashMap<>(allAbilityTypes); filteredAbilityTypes.clear(); filteredAbilityTypes.putAll(getFilteredData(dummyMap)); - } + // Reset scroll position when changing category + if (availableTypesScroll != null) { + availableTypesScroll.resetScroll(); + } + } initGui(); } diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java index d0b5259ea..e27f6e075 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -42,12 +42,25 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public void addAbility(IAbility ability) { if (ability instanceof Ability) { - data.addAbility((Ability) ability); + Ability a = (Ability) ability; + // Validate ability allows NPCs + if (!a.getAllowedBy().allowsNpc()) { + return; // Ability is PLAYER_ONLY + } + data.addAbility(a); } } @Override public void addAbilityReference(String key) { + // Validate ability exists and allows NPCs + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null) { + return; // Ability doesn't exist + } + if (!ability.getAllowedBy().allowsNpc()) { + return; // Ability is PLAYER_ONLY + } data.addAbilityReference(key); } @@ -158,6 +171,14 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public boolean executeAbility(String key, Object target) { + // Validate ability exists and allows NPCs + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null) { + return false; // Ability doesn't exist + } + if (!ability.getAllowedBy().allowsNpc()) { + return false; // Ability is PLAYER_ONLY + } EntityLivingBase targetEntity = resolveTarget(target); return data.executeAbility(key, targetEntity); } diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java index a36c65d51..42ed3372e 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -1,5 +1,7 @@ package noppes.npcs.scripted.wrapper; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.entity.player.EntityPlayer; import noppes.npcs.api.ability.IAbility; import noppes.npcs.api.ability.IPlayerAbilityData; @@ -26,7 +28,17 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { @Override public void unlockAbility(String key) { - data.unlockAbility(key); + // Validate ability exists and allows players + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null) { + return; // Ability doesn't exist + } + if (!ability.getAllowedBy().allowsPlayer()) { + return; // Ability is NPC-only + } + // Use canonical key (registry key for built-in, UUID for custom) + String canonicalKey = ability.getId() != null ? ability.getId() : key; + data.unlockAbility(canonicalKey); } @Override From 4f7773eb3f34c9635ef7590249adf74d760145b9 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Feb 2026 03:34:29 -0500 Subject: [PATCH 03/34] Fix Preview Entity --- .../client/gui/util/GuiAbilityInterface.java | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) 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 278903bb7..3d0573bf2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java @@ -11,10 +11,7 @@ import net.minecraft.client.renderer.entity.RenderManager; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import noppes.npcs.client.ClientEventHandler; -import noppes.npcs.client.EntityUtil; -import noppes.npcs.entity.EntityCustomNpc; import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.entity.data.ModelData; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; @@ -50,7 +47,6 @@ public class GuiAbilityInterface extends GuiNPCInterface2 { private GuiNpcButton btnLeft, btnRight; // ==================== NPC STATE ==================== - public ModelData playerdata; public EntityNPCInterface npc; /** NPC starting position (saved when preview starts, used to track movement) */ @@ -85,7 +81,6 @@ public class GuiAbilityInterface extends GuiNPCInterface2 { public GuiAbilityInterface(EntityNPCInterface npc, boolean hasMenuNpc) { super(hasMenuNpc ? npc : null); this.npc = npc; - this.playerdata = ((EntityCustomNpc) npc).modelData; this.drawDefaultBackground = false; CAMERA_ZOOM = 30f; @@ -144,14 +139,8 @@ public class GuiAbilityInterface extends GuiNPCInterface2 { GL11.glEnable(GL11.GL_SCISSOR_TEST); setScissorClip(previewX, previewY, previewWidth, previewHeight); - // Get the entity to render (could be transformed player model) - EntityLivingBase renderEntity = playerdata.getEntity(npc); - if (renderEntity == null) { - renderEntity = this.npc; - } - - // Sync NPC properties to render entity - EntityUtil.Copy(npc, renderEntity); + // Always use the standard NPC animation renderer for ability preview + EntityLivingBase renderEntity = this.npc; // Set NPC facing direction (affects anchor points and visual facing) npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW; @@ -207,11 +196,7 @@ public class GuiAbilityInterface extends GuiNPCInterface2 { ClientEventHandler.renderingEntityInGUI = true; // Render the NPC at its position delta (for movement like slam jumps) - try { - RenderManager.instance.renderEntityWithPosYaw(renderEntity, npcDeltaX, npcDeltaY, npcDeltaZ, 0.0F, partialTicks); - } catch (Exception e) { - playerdata.setEntityClass(null); - } + RenderManager.instance.renderEntityWithPosYaw(renderEntity, npcDeltaX, npcDeltaY, npcDeltaZ, 0.0F, partialTicks); // Render preview entities at their positions relative to NPC start position renderPreviewEntities(partialTicks); From 55cd8cc5b3cf556b82d58b6a66a2ef4fb804c7be Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Feb 2026 03:34:45 -0500 Subject: [PATCH 04/34] Fix Rotation Locking --- src/main/java/noppes/npcs/DataAbilities.java | 25 ++++++++++++++++++- .../npcs/entity/EntityNPCInterface.java | 7 ++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 36dc6f34a..19dc59ecd 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -117,10 +117,33 @@ public class DataAbilities { * Should be called every tick from onLivingUpdate(). */ public void tick() { - if (!enabled || npc.worldObj.isRemote || npc.isKilled()) { + if (npc.worldObj.isRemote) { return; } + // Force-stop any running ability when NPC is killed or abilities are disabled + if (!enabled || npc.isKilled()) { + if (currentAbility != null) { + removeTelegraph(currentAbility); + stopAbilityAnimation(); + releaseLockedRotation(); + releaseLockedPosition(); + currentAbility = null; + lastTarget = null; + } else { + // No ability but locks could be orphaned + if (rotationLocked) releaseLockedRotation(); + if (positionLocked) releaseLockedPosition(); + } + return; + } + + // Safety: release orphaned locks if no ability is actively executing + if (currentAbility == null || !currentAbility.isExecuting()) { + if (rotationLocked) releaseLockedRotation(); + if (positionLocked) releaseLockedPosition(); + } + // Tick current ability if executing if (currentAbility != null && currentAbility.isExecuting()) { tickCurrentAbility(); diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 6b344f9b9..00de6d405 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -308,6 +308,13 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti @Override public void onUpdate() { super.onUpdate(); + + // Re-apply locks after super.onUpdate() to override body rotation smoothing + // in EntityLivingBase.onUpdate() (func_110146_f) which runs AFTER onLivingUpdate() + // and overwrites renderYawOffset, causing a visible twitch each frame. + abilities.applyLockedRotation(); + abilities.applyLockedPosition(); + if (!isRemote()) { if (this.ticksExisted % 10 == 0) { EventHooks.onNPCUpdate(this); From 70583743ce67d6e591d122773dfdc019eb950a7a Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Feb 2026 03:36:32 -0500 Subject: [PATCH 05/34] Fix Ability and Custom Button --- .../client/gui/global/GuiNPCManageAnimations.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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 ac4675243..3ac3923b4 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java @@ -65,16 +65,17 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void initGui() { super.initGui(); + // Toggle button - added FIRST to maintain stable buttonList index + // (prevents double-toggle when initGui() is called from actionPerformed) + String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom"; + this.addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 56, 45, 20, toggleLabel)); + // 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); scrollAnimations.setSize(143, 185); @@ -200,11 +201,15 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol // Toggle button if (button.id == 10) { showingBuiltIn = !showingBuiltIn; - // Clear selection when switching views + // Clear selection and search when switching views selected = null; + search = ""; animation = new Animation(); currentIsBuiltIn = false; + playingAnimation = false; scrollAnimations.clear(); + // Re-request data to ensure both lists are populated + PacketClient.sendClient(new AnimationsGetPacket()); initGui(); return; } From 2e5c4c3bf41eab22c1bd9bb2e3ec59bef74c886a Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Feb 2026 05:04:19 -0500 Subject: [PATCH 06/34] Fix Preview Handling for Abilities, Fix Disc Positioning in Real World --- .../controllers/data/ability/Ability.java | 112 ++++------ .../data/ability/PreviewEntityHandler.java | 13 ++ .../data/ability/type/AbilityBeam.java | 35 ++- .../data/ability/type/AbilityBeamDual.java | 45 ++-- .../data/ability/type/AbilityCharge.java | 83 ++----- .../data/ability/type/AbilityCutter.java | 20 +- .../data/ability/type/AbilityDash.java | 85 ++------ .../data/ability/type/AbilityDisc.java | 37 ++-- .../data/ability/type/AbilityDiscDual.java | 51 +++-- .../data/ability/type/AbilityHazard.java | 92 ++++---- .../data/ability/type/AbilityHeal.java | 38 ++-- .../data/ability/type/AbilityHeavyHit.java | 68 +++--- .../data/ability/type/AbilityLaserShot.java | 31 +-- .../data/ability/type/AbilityOrb.java | 32 ++- .../data/ability/type/AbilityOrbBarrage.java | 26 +-- .../data/ability/type/AbilityOrbDual.java | 42 ++-- .../data/ability/type/AbilityProjectile.java | 89 ++++---- .../data/ability/type/AbilityShockwave.java | 70 +++--- .../data/ability/type/AbilitySlam.java | 140 +++--------- .../data/ability/type/AbilitySweeper.java | 14 +- .../data/ability/type/AbilityTeleport.java | 63 ++++-- .../data/ability/type/AbilityTrap.java | 34 +-- .../data/ability/type/AbilityVortex.java | 48 +++-- .../npcs/entity/EntityAbilityBeam.java | 25 ++- .../npcs/entity/EntityAbilityDisc.java | 1 - .../npcs/entity/EntityAbilityLaser.java | 4 - .../npcs/entity/EntityAbilitySweeper.java | 13 ++ .../java/noppes/npcs/client/ClientProxy.java | 2 + .../gui/util/AbilityPreviewExecutor.java | 202 +++++++++++------- 29 files changed, 714 insertions(+), 801 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 03faf42d5..e6404a0cb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -110,6 +110,13 @@ public abstract class Ability implements IAbility { protected transient long executionStartTime; protected transient TelegraphInstance telegraphInstance; + // ═══════════════════════════════════════════════════════════════════ + // PREVIEW STATE (client-side only, not saved) + // ═══════════════════════════════════════════════════════════════════ + + protected transient boolean previewMode = false; + protected transient PreviewEntityHandler previewEntityHandler; + // ═══════════════════════════════════════════════════════════════════ // ABSTRACT METHODS // ═══════════════════════════════════════════════════════════════════ @@ -837,6 +844,8 @@ public abstract class Ability implements IAbility { currentTick = 0; currentTarget = null; telegraphInstance = null; + previewMode = false; + previewEntityHandler = null; } /** @@ -849,90 +858,57 @@ public abstract class Ability implements IAbility { } // ═══════════════════════════════════════════════════════════════════ - // CLIENT-SIDE PREVIEW METHODS + // PREVIEW MODE SUPPORT // ═══════════════════════════════════════════════════════════════════ /** - * Get total active phase duration for preview (in ticks). - * Override in subclasses to provide accurate preview duration. - * - * @return Duration of active phase in ticks (default: 40 = 2 seconds) + * Whether this ability is running in preview mode (GUI preview). + * When true, abilities should skip damage, effects, sounds, and particles + * but still run their core logic (entity spawning, movement, timing). */ - public int getPreviewActiveDuration() { - return 40; + public boolean isPreview() { + return previewMode; + } + + public void setPreviewMode(boolean preview) { + this.previewMode = preview; + } + + public void setPreviewEntityHandler(PreviewEntityHandler handler) { + this.previewEntityHandler = handler; } /** - * Called during preview windup tick (client-side only). - * Override for visual updates during windup preview. - * Do NOT apply damage or world effects here. - * - * @param npc The preview NPC - * @param tick Current tick within windup phase + * Spawn an entity during ability execution. + * In preview mode, routes to PreviewEntityHandler instead of world.spawnEntityInWorld(). */ - @SideOnly(Side.CLIENT) - public void onPreviewWindUpTick(EntityNPCInterface npc, int tick) { - // Default: no-op. Override in subclasses for visual effects. + protected void spawnAbilityEntity(World world, Entity entity) { + if (previewMode && previewEntityHandler != null) { + previewEntityHandler.onEntitySpawned(entity); + } else { + world.spawnEntityInWorld(entity); + } } /** - * Called during preview active tick (client-side only). - * Override for visual updates during active preview. - * Do NOT apply damage or world effects here. - * - * @param npc The preview NPC - * @param tick Current tick within active phase + * Kill an entity spawned by this ability. + * In preview mode, notifies PreviewEntityHandler of removal. */ - @SideOnly(Side.CLIENT) - public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { - // Default: no-op. Override in subclasses for visual effects. + protected void killAbilityEntity(Entity entity) { + if (entity == null) return; + entity.setDead(); + if (previewMode && previewEntityHandler != null) { + previewEntityHandler.onEntityRemoved(entity); + } } /** - * Create a preview entity for GUI display (client-side only). - * The entity should be in preview mode and not apply damage. - * Override in abilities that spawn entities (Beam, Orb, Disc, etc.) - * - * @param npc The preview NPC - * @return Preview entity, or null if ability doesn't spawn entities + * Maximum duration (in ticks) for preview before auto-stopping. + * Override in movement abilities that have variable duration. + * Default: 200 ticks (10 seconds). */ - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - return null; // Override in entity-spawning abilities - } - - /** - * Whether to spawn preview entity during WINDUP phase (true) or ACTIVE phase (false). - * Most abilities (Orb, Beam, Disc) spawn during windup for charging effect. - * Laser spawns at active phase since it has no charging state. - */ - public boolean spawnPreviewDuringWindup() { - return true; // Default: spawn during windup for charging - } - - // ── Preview target (set by executor for abilities that need targeting) ── - - protected transient EntityLivingBase previewTarget; - - /** - * Set the fake target entity for preview mode. - * Called by AbilityPreviewExecutor before starting preview. - */ - public void setPreviewTarget(EntityLivingBase target) { - this.previewTarget = target; - } - - public EntityLivingBase getPreviewTarget() { - return previewTarget; - } - - /** - * Called once when transitioning from WINDUP to ACTIVE in preview mode. - * Override in movement-based abilities to initiate movement. - */ - @SideOnly(Side.CLIENT) - public void onPreviewExecute(EntityNPCInterface npc) { - // Default: no-op. Override in movement abilities. + public int getMaxPreviewDuration() { + return 200; } // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java b/src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java new file mode 100644 index 000000000..77fae9774 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/PreviewEntityHandler.java @@ -0,0 +1,13 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.entity.Entity; + +/** + * Interface for routing entity spawn/kill calls during preview mode. + * In preview mode, entities are not added to the world but instead + * tracked by the GUI for rendering in the 3D viewport. + */ +public interface PreviewEntityHandler { + void onEntitySpawned(Entity entity); + void onEntityRemoved(Entity entity); +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 5157eea01..8e3bfc575 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -76,29 +76,29 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } // Start firing the beam that was spawned during windup if (beamEntity != null && !beamEntity.isDead) { - beamEntity.startFiring(target); + if (isPreview()) { + beamEntity.startPreviewFiring(); + } else { + beamEntity.startFiring(target); + } } - - // 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 onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn beam in charging mode on first tick of windup if (tick == 1) { float offsetDist = 1.0f; - // Create beam in charging mode - follows caster based on anchor point during windup Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist); beamEntity = new EntityAbilityBeam( world, caster, target, @@ -106,11 +106,16 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { beamWidth, headSize, colorData, combatData, homingData, lightningData, lifespanData, trajectoryData, lockMovement.locksActive()); - beamEntity.setupCharging(anchorData, windUpTicks, offsetDist); + + if (isPreview()) { + beamEntity.setupPreview(caster, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, offsetDist); + } else { + beamEntity.setupCharging(anchorData, windUpTicks, offsetDist); + } beamEntity.setEffects(this.effects); beamEntity.setSourceAbility(this); - world.spawnEntityInWorld(beamEntity); + spawnAbilityEntity(world, beamEntity); } } @@ -267,17 +272,7 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); - beam.setupPreview(npc, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, 1.0f); - return beam; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java index 762145fc4..89c5b126f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java @@ -81,37 +81,41 @@ public class AbilityBeamDual extends Ability { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } // Start firing the beam that was spawned during windup if (beamEntity != null && !beamEntity.isDead) { - beamEntity.startFiring(target); + if (isPreview()) { + beamEntity.startPreviewFiring(); + } else { + beamEntity.startFiring(target); + } } if (dualFire) { if (beamEntity2 != null && !beamEntity2.isDead && dualFireDelay <= 0) { - beamEntity2.startFiring(target); + if (isPreview()) { + beamEntity2.startPreviewFiring(); + } else { + beamEntity2.startFiring(target); + } } } else if (beamEntity2 != null) { beamEntity2.setDead(); } - - // 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 onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn beam in charging mode on first tick of windup if (tick == 1) { float offsetDist = 1.0f; - // Create beam in charging mode - follows caster based on anchor point during windup Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0], offsetDist); beamEntity = new EntityAbilityBeam( world, caster, target, @@ -131,16 +135,21 @@ public class AbilityBeamDual extends Ability { beamEntity.setSiblingEntityId(beamEntity2.getEntityId()); beamEntity2.setSiblingEntityId(beamEntity.getEntityId()); - beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist); - beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist); + if (isPreview()) { + beamEntity.setupPreview(caster, beamWidth, headSize, displayData[0], lightningData[0], anchorData[0], windUpTicks, offsetDist); + beamEntity2.setupPreview(caster, beamWidth, headSize, displayData[1], lightningData[1], anchorData[1], windUpTicks, offsetDist); + } else { + beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist); + beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist); + } beamEntity.setEffects(this.effects); if (dualFire) beamEntity2.setEffects(this.effects); beamEntity.setSourceAbility(this); beamEntity2.setSourceAbility(this); - world.spawnEntityInWorld(beamEntity); - world.spawnEntityInWorld(beamEntity2); + spawnAbilityEntity(world, beamEntity); + spawnAbilityEntity(world, beamEntity2); } } @@ -501,17 +510,7 @@ public class AbilityBeamDual extends Ability { } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); - beam.setupPreview(npc, beamWidth, headSize, displayData[0], lightningData[0], anchorData[0], windUpTicks, 1.0f); - return beam; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index e09ff7e11..8e179dc73 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -138,7 +138,9 @@ public class AbilityCharge extends Ability implements IAbilityCharge { if (chargeDirection == null) return; // Enforce rotation every tick - enforceLockedRotation(caster); + if (!isPreview()) { + enforceLockedRotation(caster); + } // Calculate distance traveled double distanceTraveled = Math.sqrt( @@ -150,12 +152,15 @@ public class AbilityCharge extends Ability implements IAbilityCharge { if (distanceTraveled >= maxRange) { caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } signalCompletion(); return; } - if (isChargeBlocked(caster)) { + // Block detection (skip in preview - no real world collision) + if (!isPreview() && isChargeBlocked(caster)) { stopMomentum(caster); signalCompletion(); return; @@ -165,10 +170,12 @@ public class AbilityCharge extends Ability implements IAbilityCharge { caster.motionX = chargeDirection.xCoord * chargeSpeed; caster.motionY = 0; caster.motionZ = chargeDirection.zCoord * chargeSpeed; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } - // Server-side collision damage - if (!world.isRemote) { + // Server-side collision damage (skip in preview) + if (!world.isRemote && !isPreview()) { AxisAlignedBB hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth); @SuppressWarnings("unchecked") @@ -211,7 +218,9 @@ public class AbilityCharge extends Ability implements IAbilityCharge { private void stopMomentum(EntityLivingBase caster) { caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } } private boolean isChargeBlocked(EntityLivingBase caster) { @@ -277,66 +286,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge { return instance; } - // ==================== PREVIEW MODE ==================== - - private transient double previewDirX, previewDirZ; - private transient double previewStartX, previewStartZ; - private transient boolean previewCharging = false; - @Override - @SideOnly(Side.CLIENT) - public void onPreviewExecute(EntityNPCInterface npc) { - previewStartX = npc.posX; - previewStartZ = npc.posZ; - previewCharging = false; - - // Calculate direction toward fake target - if (previewTarget != null) { - double dx = previewTarget.posX - npc.posX; - double dz = previewTarget.posZ - npc.posZ; - double len = Math.sqrt(dx * dx + dz * dz); - if (len > 0) { - previewDirX = dx / len; - previewDirZ = dz / len; - } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); - previewDirX = -Math.sin(yaw); - previewDirZ = Math.cos(yaw); - } - } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); - previewDirX = -Math.sin(yaw); - previewDirZ = Math.cos(yaw); - } - previewCharging = true; - } - - @Override - @SideOnly(Side.CLIENT) - public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { - if (!previewCharging) return; - - // Check distance traveled - double distTraveled = Math.sqrt( - Math.pow(npc.posX - previewStartX, 2) + - Math.pow(npc.posZ - previewStartZ, 2) - ); - - if (distTraveled >= maxRange) { - previewCharging = false; - return; - } - - npc.prevPosX = npc.posX; - npc.prevPosY = npc.posY; - npc.prevPosZ = npc.posZ; - - npc.posX += previewDirX * chargeSpeed; - npc.posZ += previewDirZ * chargeSpeed; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return (int) Math.ceil(maxRange / chargeSpeed) + 5; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 405dac612..22831f4ff 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -109,31 +109,35 @@ public class AbilityCutter extends Ability implements IAbilityCutter { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; switch (sweepMode) { case SWIPE: float prevRotation = currentRotation; currentRotation += sweepSpeed; if (currentRotation > arcAngle / 2.0f) { - // Final sweep — check from prevRotation to end of arc - performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f); + if (!isPreview()) { + performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f); + } signalCompletion(); return; } - // Check entities between previous and current sweep position - performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation); + if (!isPreview()) { + performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation); + } break; case SPIN: if (tick >= spinDurationTicks) { - signalCompletion(); // Spin duration complete + signalCompletion(); return; } float prevSpin = currentRotation; currentRotation = (currentRotation + sweepSpeed) % 360.0f; - hitEntities.clear(); - performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation); + if (!isPreview()) { + hitEntities.clear(); + performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation); + } break; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 1a1e76aa3..f80f336aa 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -172,7 +172,7 @@ public class AbilityDash extends Ability implements IAbilityDash { if (dashDirection == null) return; // Stall detection: if entity hasn't moved since last tick, it's stuck against a wall - if (tick > 1) { + if (!isPreview() && tick > 1) { double movedThisTick = Math.sqrt( Math.pow(caster.posX - prevTickX, 2) + Math.pow(caster.posZ - prevTickZ, 2)); @@ -197,12 +197,15 @@ public class AbilityDash extends Ability implements IAbilityDash { if (distanceTraveled >= dashDistance) { caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } signalCompletion(); return; } - if (isDashBlocked(caster)) { + // Block detection (skip in preview - no real world collision) + if (!isPreview() && isDashBlocked(caster)) { caster.motionX = 0; caster.motionZ = 0; caster.velocityChanged = true; @@ -214,24 +217,30 @@ public class AbilityDash extends Ability implements IAbilityDash { caster.motionX = dashDirection.xCoord * dashSpeed; caster.motionY = 0; caster.motionZ = dashDirection.zCoord * dashSpeed; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; - // Trail particles - world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0); + // Trail particles + world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0); + } } @Override public void onComplete(EntityLivingBase caster, EntityLivingBase target) { caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } } @Override public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } } @Override @@ -247,66 +256,8 @@ public class AbilityDash extends Ability implements IAbilityDash { return !caster.worldObj.getCollidingBoundingBoxes(caster, caster.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); } - // ==================== PREVIEW MODE ==================== - - private transient double previewDirX, previewDirZ; - private transient double previewStartX, previewStartZ; - private transient boolean previewDashing = false; - @Override - @SideOnly(Side.CLIENT) - public void onPreviewExecute(EntityNPCInterface npc) { - previewStartX = npc.posX; - previewStartZ = npc.posZ; - previewDashing = false; - - // Pick a random direction based on mode - DashDirection[] directions = dashMode == DashMode.AGGRESSIVE - ? AGGRESSIVE_DIRECTIONS - : DEFENSIVE_DIRECTIONS; - DashDirection dir = directions[RANDOM.nextInt(directions.length)]; - - float baseYaw; - if (previewTarget != null) { - double dx = previewTarget.posX - npc.posX; - double dz = previewTarget.posZ - npc.posZ; - baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - } else { - baseYaw = npc.rotationYaw; - } - - float dashYaw = baseYaw + dir.getAngleOffset(); - float yawRad = (float) Math.toRadians(dashYaw); - previewDirX = -Math.sin(yawRad); - previewDirZ = Math.cos(yawRad); - previewDashing = true; - } - - @Override - @SideOnly(Side.CLIENT) - public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { - if (!previewDashing) return; - - double distTraveled = Math.sqrt( - Math.pow(npc.posX - previewStartX, 2) + - Math.pow(npc.posZ - previewStartZ, 2) - ); - - if (distTraveled >= dashDistance) { - previewDashing = false; - return; - } - - npc.prevPosX = npc.posX; - npc.prevPosY = npc.posY; - npc.prevPosZ = npc.posZ; - - npc.posX += previewDirX * dashSpeed; - npc.posZ += previewDirZ * dashSpeed; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return (int) Math.ceil(dashDistance / dashSpeed) + 5; } 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 0f91869fa..77d137eb9 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 @@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityDisc; import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; @@ -23,7 +22,6 @@ import noppes.npcs.api.ability.type.IAbilityDisc; import java.util.Arrays; import java.util.List; -import noppes.npcs.entity.EntityNPCInterface; /** * Disc ability: Spawns a flat spinning disc projectile. @@ -79,27 +77,27 @@ public class AbilityDisc extends Ability implements IAbilityDisc { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } // Start moving the disc that was spawned during windup if (discEntity != null && !discEntity.isDead) { - discEntity.startMoving(target); + if (isPreview()) { + discEntity.startPreviewFiring(); + } else { + discEntity.startMoving(target); + } } - - // 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 onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn disc in charging mode on first tick of windup if (tick == 1) { - // Create disc in charging mode - follows caster based on anchor point during windup Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); discEntity = new EntityAbilityDisc( world, caster, target, @@ -107,11 +105,16 @@ public class AbilityDisc extends Ability implements IAbilityDisc { discRadius, discThickness, colorData, combatData, homingData, lightningData, lifespanData, trajectoryData, boomerang, boomerangDelay); - discEntity.setupCharging(anchorData, windUpTicks); + + if (isPreview()) { + discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks); + } else { + discEntity.setupCharging(anchorData, windUpTicks); + } discEntity.setEffects(this.effects); discEntity.setSourceAbility(this); - world.spawnEntityInWorld(discEntity); + spawnAbilityEntity(world, discEntity); } } @@ -264,17 +267,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); - disc.setupPreview(npc, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks); - return disc; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java index 421861fcd..2fbd6cbda 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java @@ -14,12 +14,10 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityDisc; import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.util.ValueUtil; import java.util.Arrays; @@ -89,19 +87,27 @@ public class AbilityDiscDual extends Ability { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } - // Start moving the orb that was spawned during windup + // Start moving the disc that was spawned during windup if (discEntity != null && !discEntity.isDead) { - discEntity.startMoving(target); + if (isPreview()) { + discEntity.startPreviewFiring(); + } else { + discEntity.startMoving(target); + } } if (dualFire) { if (discEntity2 != null && !discEntity2.isDead && dualFireDelay <= 0) { - discEntity2.startMoving(target); + if (isPreview()) { + discEntity2.startPreviewFiring(); + } else { + discEntity2.startMoving(target); + } } } else if (discEntity2 != null) { discEntity2.setDead(); @@ -113,7 +119,7 @@ public class AbilityDiscDual extends Ability { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn disc in charging mode on first tick of windup if (tick == 1) { @@ -137,8 +143,13 @@ public class AbilityDiscDual extends Ability { discEntity.setSiblingEntityId(discEntity2.getEntityId()); discEntity2.setSiblingEntityId(discEntity.getEntityId()); - discEntity.setupCharging(anchorData[0], windUpTicks); - discEntity2.setupCharging(anchorData[1], windUpTicks); + if (isPreview()) { + discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks); + discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks); + } else { + discEntity.setupCharging(anchorData[0], windUpTicks); + discEntity2.setupCharging(anchorData[1], windUpTicks); + } discEntity.setEffects(this.effects); @@ -148,8 +159,8 @@ public class AbilityDiscDual extends Ability { discEntity.setSourceAbility(this); discEntity2.setSourceAbility(this); - world.spawnEntityInWorld(discEntity); - world.spawnEntityInWorld(discEntity2); + spawnAbilityEntity(world, discEntity); + spawnAbilityEntity(world, discEntity2); } } @@ -164,7 +175,11 @@ public class AbilityDiscDual extends Ability { } if (tick == dualFireDelay && discEntity2 != null && !discEntity2.isDead) { - discEntity2.startMoving(target); + if (isPreview()) { + discEntity2.startPreviewFiring(); + } else { + discEntity2.startMoving(target); + } } } else { if (discEntity == null || discEntity.isDead) { @@ -511,17 +526,7 @@ public class AbilityDiscDual extends Ability { } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); - disc.setupPreview(npc, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks); - return disc; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 99ea47172..112b2a637 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -243,7 +243,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Check if hazard duration has ended if (tick >= durationTicks) { @@ -251,53 +251,55 @@ public class AbilityHazard extends Ability implements IAbilityHazard { return; } - damagedThisTick.clear(); - ticksSinceDamage++; + if (!isPreview()) { + damagedThisTick.clear(); + ticksSinceDamage++; - switch (placement) { - case FOLLOW_CASTER: - zoneX = caster.posX; - zoneY = caster.posY; - zoneZ = caster.posZ; - break; - case FOLLOW_TARGET: - if (target != null) { - zoneX = target.posX; - zoneY = target.posY; - zoneZ = target.posZ; - } - break; - default: - break; - } - - if (ticksSinceDamage >= damageInterval) { - ticksSinceDamage = 0; - - AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( - zoneX - radius, zoneY - heightBelow, zoneZ - radius, - zoneX + radius, zoneY + heightAbove, zoneZ + radius - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); - - for (EntityLivingBase entity : entities) { - if (entity == caster && !affectsCaster) continue; - if (damagedThisTick.contains(entity.getEntityId())) continue; - if (!isInZone(entity, caster)) continue; - - if (damagePerSecond > 0) { - if (ignoreInvulnFrames) { - entity.hurtResistantTime = 0; + switch (placement) { + case FOLLOW_CASTER: + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; + break; + case FOLLOW_TARGET: + if (target != null) { + zoneX = target.posX; + zoneY = target.posY; + zoneZ = target.posZ; } - // Apply damage with scripted event support (no knockback for hazard) - boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0); - if (!wasHit) continue; // Skip debuffs if hit was cancelled - } + break; + default: + break; + } - applyDebuffs(entity); - damagedThisTick.add(entity.getEntityId()); + if (ticksSinceDamage >= damageInterval) { + ticksSinceDamage = 0; + + AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( + zoneX - radius, zoneY - heightBelow, zoneZ - radius, + zoneX + radius, zoneY + heightAbove, zoneZ + radius + ); + + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); + + for (EntityLivingBase entity : entities) { + if (entity == caster && !affectsCaster) continue; + if (damagedThisTick.contains(entity.getEntityId())) continue; + if (!isInZone(entity, caster)) continue; + + if (damagePerSecond > 0) { + if (ignoreInvulnFrames) { + entity.hurtResistantTime = 0; + } + // Apply damage with scripted event support (no knockback for hazard) + boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0); + if (!wasHit) continue; // Skip debuffs if hit was cancelled + } + + applyDebuffs(entity); + damagedThisTick.add(entity.getEntityId()); + } } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index c1faf000b..b60c46d9d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -75,27 +75,31 @@ public class AbilityHeal extends Ability implements IAbilityHeal { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; - getHealedAllies().clear(); + if (!isPreview()) { + getHealedAllies().clear(); - // Always find allies if we're healing them (needed for both instant and HoT) - if (healAllies && healRadius > 0) { - findAlliesInRadius(caster, world); + // Always find allies if we're healing them (needed for both instant and HoT) + if (healAllies && healRadius > 0) { + findAlliesInRadius(caster, world); + } + + if (instantHeal) { + // Instant heal - apply all healing now + if (healSelf) { + healEntity(caster); + spawnHealParticles(world, caster); + } + + for (EntityLivingBase ally : getHealedAllies()) { + healEntity(ally); + spawnHealParticles(world, ally); + } + } } if (instantHeal) { - // Instant heal - apply all healing now - if (healSelf) { - healEntity(caster); - spawnHealParticles(world, caster); - } - - for (EntityLivingBase ally : getHealedAllies()) { - healEntity(ally); - spawnHealParticles(world, ally); - } - // Instant heal completes immediately signalCompletion(); } @@ -104,7 +108,7 @@ public class AbilityHeal extends Ability implements IAbilityHeal { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote || instantHeal) return; + if ((world.isRemote && !isPreview()) || instantHeal) return; // Heal over time - distribute heal across duration ticks if (tick % 10 == 0) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 0e236de32..dd5d5fc09 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -68,51 +68,53 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } - // Calculate forward and right vectors from caster yaw - float yawRad = (float) Math.toRadians(caster.rotationYaw); - double forwardX = -Math.sin(yawRad); - double forwardZ = Math.cos(yawRad); - double rightX = forwardZ; // perpendicular right - double rightZ = -forwardX; + if (!isPreview()) { + // Calculate forward and right vectors from caster yaw + float yawRad = (float) Math.toRadians(caster.rotationYaw); + double forwardX = -Math.sin(yawRad); + double forwardZ = Math.cos(yawRad); + double rightX = forwardZ; // perpendicular right + double rightZ = -forwardX; - // Search area: AABB that encompasses the rectangle - float searchDist = Math.max(hitLength, hitWidth) + 1.0f; - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABBExcludingEntity( - caster, caster.boundingBox.expand(searchDist, 2, searchDist)); + // Search area: AABB that encompasses the rectangle + float searchDist = Math.max(hitLength, hitWidth) + 1.0f; + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABBExcludingEntity( + caster, caster.boundingBox.expand(searchDist, 2, searchDist)); - boolean anyHit = false; - for (Entity entity : entities) { - if (!(entity instanceof EntityLivingBase) || entity == caster) continue; - EntityLivingBase livingTarget = (EntityLivingBase) entity; + boolean anyHit = false; + for (Entity entity : entities) { + if (!(entity instanceof EntityLivingBase) || entity == caster) continue; + EntityLivingBase livingTarget = (EntityLivingBase) entity; - double dx = livingTarget.posX - caster.posX; - double dz = livingTarget.posZ - caster.posZ; + double dx = livingTarget.posX - caster.posX; + double dz = livingTarget.posZ - caster.posZ; - // Project onto forward direction (must be in front, within hitLength) - double forwardDist = dx * forwardX + dz * forwardZ; - if (forwardDist < 0 || forwardDist > hitLength) continue; + // Project onto forward direction (must be in front, within hitLength) + double forwardDist = dx * forwardX + dz * forwardZ; + if (forwardDist < 0 || forwardDist > hitLength) continue; - // Project onto right direction (must be within hitWidth to each side) - double sideDist = dx * rightX + dz * rightZ; - if (Math.abs(sideDist) > hitWidth) continue; + // Project onto right direction (must be within hitWidth to each side) + double sideDist = dx * rightX + dz * rightZ; + if (Math.abs(sideDist) > hitWidth) continue; - // Entity is within the rectangle - apply damage - boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback); - if (wasHit) { - applyEffects(livingTarget); - anyHit = true; + // Entity is within the rectangle - apply damage + boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback); + if (wasHit) { + applyEffects(livingTarget); + anyHit = true; + } } - } - // Play hit sound even if nothing was hit (the attack still happens) - if (!anyHit) { - world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f); + // Play hit sound even if nothing was hit (the attack still happens) + if (!anyHit) { + world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f); + } } } 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 4be2315d5..d211de6d2 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 @@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityLaser; import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -22,7 +21,6 @@ import noppes.npcs.api.ability.type.IAbilityLaserShot; import java.util.Arrays; import java.util.List; -import noppes.npcs.entity.EntityNPCInterface; /** * Laser Shot ability: Fast expanding thin line that pierces through targets. @@ -84,7 +82,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; if (tick == 1) { Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); @@ -96,17 +94,21 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { expansionSpeed, lingerTicks ); - laserEntity.setupCharging(anchorData, windUpTicks); + if (isPreview()) { + laserEntity.setupPreview(caster, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance); + } else { + laserEntity.setupCharging(anchorData, windUpTicks); + } laserEntity.setEffects(this.effects); laserEntity.setSourceAbility(this); - world.spawnEntityInWorld(laserEntity); + spawnAbilityEntity(world, laserEntity); } } @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } @@ -235,25 +237,10 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityLaser laser = new EntityAbilityLaser(npc.worldObj); - laser.setupPreview(npc, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance); - return laser; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60; } - @Override - public boolean spawnPreviewDuringWindup() { - return false; // Laser has no charging, spawns at active phase - } - @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { 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 96881cf4e..6a87b38b4 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 @@ -75,14 +75,18 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } // Start moving the orb that was spawned during windup if (orbEntity != null && !orbEntity.isDead) { - orbEntity.startMoving(target); + if (isPreview()) { + orbEntity.startPreviewFiring(); + } else { + orbEntity.startMoving(target); + } } // Ability stays active until entity dies (prevents firing another while projectile is alive) @@ -91,21 +95,25 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn orb in charging mode on first tick of windup if (tick == 1) { - // Create orb in charging mode - follows caster based on anchor point during windup Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); orbEntity = new EntityAbilityOrb( world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, displayData, combatData, homingData, lightningData, lifespanData, trajectoryData); - orbEntity.setupCharging(anchorData, windUpTicks); + + if (isPreview()) { + orbEntity.setupPreview(caster, orbSize, displayData, lightningData, anchorData, windUpTicks); + } else { + orbEntity.setupCharging(anchorData, windUpTicks); + } orbEntity.setEffects(this.effects); orbEntity.setSourceAbility(this); - world.spawnEntityInWorld(orbEntity); + spawnAbilityEntity(world, orbEntity); } } @@ -405,17 +413,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); - orb.setupPreview(npc, orbSize, displayData, lightningData, anchorData, windUpTicks); - return orb; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java index 71be07a34..dda01f68a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java @@ -89,7 +89,7 @@ public class AbilityOrbBarrage extends Ability { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } @@ -107,12 +107,10 @@ public class AbilityOrbBarrage extends Ability { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn orb in charging mode on first tick of windup if (tick == 1) { - // Create orb in charging mode - follows caster based on anchor point during windup - if (dualCharging) { chargingEntities = new EntityAbilityOrb[2]; } else { @@ -128,11 +126,15 @@ public class AbilityOrbBarrage extends Ability { colorData, combatData, homingData, lightningData, lifespanData, trajectoryData ); - orbEntity.setupCharging(anchorData[i], windUpTicks); + if (isPreview()) { + orbEntity.setupPreview(caster, orbSize, colorData, lightningData, anchorData[i], windUpTicks); + } else { + orbEntity.setupCharging(anchorData[i], windUpTicks); + } orbEntity.setSourceAbility(this); chargingEntities[i] = orbEntity; - world.spawnEntityInWorld(orbEntity); + spawnAbilityEntity(world, orbEntity); } } } @@ -540,17 +542,7 @@ public class AbilityOrbBarrage extends Ability { public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); - orb.setupPreview(npc, orbSize, colorData, lightningData, anchorData[0], windUpTicks); - return orb; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java index bd2870438..7b8362464 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java @@ -80,19 +80,27 @@ public class AbilityOrbDual extends Ability { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } // Start moving the orb that was spawned during windup if (orbEntity != null && !orbEntity.isDead) { - orbEntity.startMoving(target); + if (isPreview()) { + orbEntity.startPreviewFiring(); + } else { + orbEntity.startMoving(target); + } } if (dualFire) { if (orbEntity2 != null && !orbEntity2.isDead && dualFireDelay <= 0) { - orbEntity2.startMoving(target); + if (isPreview()) { + orbEntity2.startPreviewFiring(); + } else { + orbEntity2.startMoving(target); + } } } else if (orbEntity2 != null) { orbEntity2.setDead(); @@ -104,11 +112,10 @@ public class AbilityOrbDual extends Ability { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; // Spawn orb in charging mode on first tick of windup if (tick == 1) { - // Create orb in charging mode - follows caster based on anchor point during windup Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]); orbEntity = new EntityAbilityOrb( world, caster, target, @@ -124,8 +131,13 @@ public class AbilityOrbDual extends Ability { orbEntity.setSiblingEntityId(orbEntity2.getEntityId()); orbEntity2.setSiblingEntityId(orbEntity.getEntityId()); - orbEntity.setupCharging(anchorData[0], windUpTicks); - orbEntity2.setupCharging(anchorData[1], windUpTicks); + if (isPreview()) { + orbEntity.setupPreview(caster, orbSize, displayData[0], lightningData[0], anchorData[0], windUpTicks); + orbEntity2.setupPreview(caster, orbSize, displayData[1], lightningData[1], anchorData[1], windUpTicks); + } else { + orbEntity.setupCharging(anchorData[0], windUpTicks); + orbEntity2.setupCharging(anchorData[1], windUpTicks); + } orbEntity.setEffects(this.effects); @@ -135,8 +147,8 @@ public class AbilityOrbDual extends Ability { orbEntity.setSourceAbility(this); orbEntity2.setSourceAbility(this); - world.spawnEntityInWorld(orbEntity); - world.spawnEntityInWorld(orbEntity2); + spawnAbilityEntity(world, orbEntity); + spawnAbilityEntity(world, orbEntity2); } } @@ -570,17 +582,7 @@ public class AbilityOrbDual extends Ability { } @Override - @SideOnly(Side.CLIENT) - public Entity createPreviewEntity(EntityNPCInterface npc) { - if (npc == null || npc.worldObj == null) return null; - - EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); - orb.setupPreview(npc, orbSize, displayData[0], lightningData[0], anchorData[0], windUpTicks); - return orb; - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index e5f6f5df6..75c27a507 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -62,59 +62,64 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote || target == null) return; - - // Calculate direction to target - double dx = target.posX - caster.posX; - double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); - double dz = target.posZ - caster.posZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - - if (len > 0) { - dx /= len; - dy /= len; - dz /= len; + if ((world.isRemote && !isPreview()) || target == null) { + signalCompletion(); + return; } - // Deal instant damage with scripted event support - // TODO: Use custom EntityAbilityProjectile for actual tracking - applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); + if (!isPreview()) { + // Calculate direction to target + double dx = target.posX - caster.posX; + double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); + double dz = target.posZ - caster.posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - // Play sound - world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); + if (len > 0) { + dx /= len; + dy /= len; + dz /= len; + } - // Handle splash damage for explosive projectiles - if (explosive && explosionRadius > 0) { - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABBExcludingEntity(target, - target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); + // Deal instant damage with scripted event support + // TODO: Use custom EntityAbilityProjectile for actual tracking + applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); - for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != caster) { - EntityLivingBase living = (EntityLivingBase) entity; - float dist = target.getDistanceToEntity(living); - if (dist < explosionRadius) { - float falloff = 1.0f - (dist / explosionRadius); - // Apply splash damage with scripted event support (no knockback) - applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); + // Play sound + world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); + + // Handle splash damage for explosive projectiles + if (explosive && explosionRadius > 0) { + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABBExcludingEntity(target, + target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); + + for (Entity entity : entities) { + if (entity instanceof EntityLivingBase && entity != caster) { + EntityLivingBase living = (EntityLivingBase) entity; + float dist = target.getDistanceToEntity(living); + if (dist < explosionRadius) { + float falloff = 1.0f - (dist / explosionRadius); + // Apply splash damage with scripted event support (no knockback) + applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); + } } } + + // Explosion particles + world.playSoundAtEntity(target, "random.explode", 0.5f, 1.0f); + for (int i = 0; i < 10; i++) { + world.spawnParticle("explode", + target.posX + (world.rand.nextDouble() - 0.5) * explosionRadius, + target.posY + world.rand.nextDouble() * target.height, + target.posZ + (world.rand.nextDouble() - 0.5) * explosionRadius, + 0, 0.1, 0); + } } - // Explosion particles - world.playSoundAtEntity(target, "random.explode", 0.5f, 1.0f); - for (int i = 0; i < 10; i++) { - world.spawnParticle("explode", - target.posX + (world.rand.nextDouble() - 0.5) * explosionRadius, - target.posY + world.rand.nextDouble() * target.height, - target.posZ + (world.rand.nextDouble() - 0.5) * explosionRadius, - 0, 0.1, 0); - } + // Spawn projectile particles (visual trail) + spawnProjectileParticles(world, caster, target); } - // Spawn projectile particles (visual trail) - spawnProjectileParticles(world, caster, target); - // Projectile is instant signalCompletion(); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 775c15515..6788a3ea3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -63,48 +63,50 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - // Shockwave is instant - apply effect immediately after windup + if (!isPreview()) { + // Shockwave is instant - apply effect immediately after windup - // Get all entities in radius - AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); + // Get all entities in radius + AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); - int count = 0; - for (EntityLivingBase entity : entities) { - if (entity == caster) continue; - if (entity.isDead) continue; + int count = 0; + for (EntityLivingBase entity : entities) { + if (entity == caster) continue; + if (entity.isDead) continue; - double dist = caster.getDistanceToEntity(entity); - if (dist > pushRadius) continue; + double dist = caster.getDistanceToEntity(entity); + if (dist > pushRadius) continue; - count++; - if (count > maxTargets) break; + count++; + if (count > maxTargets) break; - // Calculate push direction (away from caster) - double dx = entity.posX - caster.posX; - double dz = entity.posZ - caster.posZ; - double len = Math.sqrt(dx * dx + dz * dz); + // Calculate push direction (away from caster) + double dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; + double len = Math.sqrt(dx * dx + dz * dz); - if (len > 0) { - dx /= len; - dz /= len; - } else { - // Entity is directly on top of caster, push in random direction - double angle = Math.random() * Math.PI * 2; - dx = Math.cos(angle); - dz = Math.sin(angle); - } + if (len > 0) { + dx /= len; + dz /= len; + } else { + // Entity is directly on top of caster, push in random direction + double angle = Math.random() * Math.PI * 2; + dx = Math.cos(angle); + dz = Math.sin(angle); + } - // Scale knockback by distance (closer = stronger) - float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; - float finalPush = pushStrength * distFactor; - // Apply damage with custom knockback direction - boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz); + // Scale knockback by distance (closer = stronger) + float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; + float finalPush = pushStrength * distFactor; + // Apply damage with custom knockback direction + boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz); - // Apply effects if hit connected - if (wasHit) { - applyEffects(entity); + // Apply effects if hit connected + if (wasHit) { + applyEffects(entity); + } } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 1be741d35..15b4060b8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -187,11 +187,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam { caster.motionZ = 0; caster.motionY = vy; hasLaunched = true; - if (caster instanceof EntityNPCInterface) { + if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(true); } - caster.velocityChanged = true; - caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); + if (!isPreview()) { + caster.velocityChanged = true; + caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); + } return; } @@ -233,10 +235,12 @@ public class AbilitySlam extends Ability implements IAbilitySlam { caster.motionY = vy; hasLaunched = true; - if (caster instanceof EntityNPCInterface) { + if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(true); } - caster.velocityChanged = true; + if (!isPreview()) { + caster.velocityChanged = true; + } // Face the target float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); @@ -252,7 +256,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam { airTicks++; // Continuously reset fall distance during slam to prevent fall damage - caster.fallDistance = 0; + if (!isPreview()) { + caster.fallDistance = 0; + } // Check for landing if (caster.onGround && airTicks > 3) { @@ -268,11 +274,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } // While in air, face toward target - double dx = targetX - caster.posX; - double dz = targetZ - caster.posZ; - float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - caster.rotationYaw = targetYaw; - caster.rotationYawHead = targetYaw; + if (!isPreview()) { + double dx = targetX - caster.posX; + double dz = targetZ - caster.posZ; + float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; + } } /** @@ -280,22 +288,25 @@ public class AbilitySlam extends Ability implements IAbilitySlam { */ private void onLanding(EntityLivingBase caster, World world) { hasLanded = true; - if (caster instanceof EntityNPCInterface) { + if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(false); } // Signal that the ability has completed its active phase signalCompletion(); - // Reset fall distance to prevent fall damage on landing - caster.fallDistance = 0; - // Stop horizontal momentum caster.motionX = 0; caster.motionZ = 0; - caster.velocityChanged = true; - if (world.isRemote) return; + if (!isPreview()) { + // Reset fall distance to prevent fall damage on landing + caster.fallDistance = 0; + caster.velocityChanged = true; + } + + if (world.isRemote && !isPreview()) return; + if (isPreview()) return; // Play slam impact sound on landing world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f); @@ -361,7 +372,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { @Override public void onComplete(EntityLivingBase caster, EntityLivingBase target) { - if (caster instanceof EntityNPCInterface) { + if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(false); } hasLaunched = false; @@ -371,7 +382,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { @Override public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { - if (caster instanceof EntityNPCInterface) { + if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(false); } hasLaunched = false; @@ -387,97 +398,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam { airTicks = 0; } - // ==================== PREVIEW MODE ==================== - - private transient double previewVelX, previewVelY, previewVelZ; - private transient boolean previewLaunched = false; - private transient double previewGroundY; - @Override - @SideOnly(Side.CLIENT) - public void onPreviewExecute(EntityNPCInterface npc) { - previewLaunched = false; - - double tx, ty, tz; - if (previewTarget != null && targetingMode == TargetingMode.AOE_TARGET) { - tx = previewTarget.posX; - ty = previewTarget.posY; - tz = previewTarget.posZ; - } else { - tx = npc.posX; - ty = npc.posY; - tz = npc.posZ; - } - - previewGroundY = npc.posY; - - double dx = tx - npc.posX; - double dz = tz - npc.posZ; - double horizontalDist = Math.sqrt(dx * dx + dz * dz); - - // Calculate vertical velocity for exact leapHeight - double arcHeight = Math.max(1.0, leapHeight); - double vy = calculateLaunchVelocity(arcHeight); - - if (horizontalDist < 0.5) { - previewVelX = 0; - previewVelZ = 0; - previewVelY = vy; - } else { - if (horizontalDist > maxRange) { - double scale = maxRange / horizontalDist; - dx *= scale; - dz *= scale; - horizontalDist = maxRange; - } - - double speedFactor = Math.max(0.1, leapSpeed); - int flightTicks = (int) Math.max(10, Math.min(horizontalDist * 1.5 / speedFactor, 40)); - double drag = 0.91; - double dragPowN = Math.pow(drag, flightTicks); - double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN); - - double dirX = dx / horizontalDist; - double dirZ = dz / horizontalDist; - previewVelX = dirX * vHorizontal; - previewVelZ = dirZ * vHorizontal; - previewVelY = vy; - } - previewLaunched = true; - } - - @Override - @SideOnly(Side.CLIENT) - public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { - if (!previewLaunched) return; - - // Apply velocity - npc.prevPosX = npc.posX; - npc.prevPosY = npc.posY; - npc.prevPosZ = npc.posZ; - - npc.posX += previewVelX; - npc.posY += previewVelY; - npc.posZ += previewVelZ; - - // Gravity and drag - previewVelY -= 0.08; - previewVelY *= 0.98; - previewVelX *= 0.91; - previewVelZ *= 0.91; - - // Ground clamp - stop falling below starting Y - if (npc.posY < previewGroundY && previewVelY < 0) { - npc.posY = previewGroundY; - previewVelY = 0; - previewVelX = 0; - previewVelZ = 0; - previewLaunched = false; - } - } - - @Override - public int getPreviewActiveDuration() { + public int getMaxPreviewDuration() { return 60; } 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 2fcc62956..3155310ef 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 @@ -73,7 +73,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } @@ -85,7 +85,12 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { sweepSpeed, numberOfRotations, damage, damageInterval, piercing, lockOnTarget); - world.spawnEntityInWorld(activeEntity); + + if (isPreview()) { + activeEntity.setupPreview(caster); + } + + spawnAbilityEntity(world, activeEntity); // Ability stays active until entity dies (prevents firing another while projectile is alive) // Movement locking is handled separately by the base class @@ -200,6 +205,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; } + @Override + public int getMaxPreviewDuration() { + return (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10; + } + @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index cb06b7b6b..fe847bf3a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -128,7 +128,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { } private void performBlink(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote) return; + if (world.isRemote && !isPreview()) return; double oldX = caster.posX; double oldY = caster.posY; @@ -137,36 +137,47 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { Vec3 destination = calculateDestination(caster, target, world); if (destination == null) return; - boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight; - if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ, - destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) { - destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ, + if (!isPreview()) { + boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight; + if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ, + destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) { + destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ, + destination.xCoord, destination.yCoord, destination.zCoord); + if (destination == null) return; + } + + destination = findSafeDestination(world, oldX, oldY, oldZ, destination.xCoord, destination.yCoord, destination.zCoord); if (destination == null) return; + + // Damage at origin + if (damageAtStart) { + dealDamageAt(caster, world, oldX, oldY, oldZ); + } + + // Spawn particles at origin + spawnTeleportParticles(world, oldX, oldY, oldZ); } - destination = findSafeDestination(world, oldX, oldY, oldZ, - destination.xCoord, destination.yCoord, destination.zCoord); - if (destination == null) return; - - // Damage at origin - if (damageAtStart) { - dealDamageAt(caster, world, oldX, oldY, oldZ); - } - - // Spawn particles at origin - spawnTeleportParticles(world, oldX, oldY, oldZ); - // Teleport - caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); + if (isPreview()) { + caster.setPosition(destination.xCoord, destination.yCoord, destination.zCoord); + caster.prevPosX = destination.xCoord; + caster.prevPosY = destination.yCoord; + caster.prevPosZ = destination.zCoord; + } else { + caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); + } caster.fallDistance = 0; - // Spawn particles at destination - spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord); + if (!isPreview()) { + // Spawn particles at destination + spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord); - // Damage at destination - if (damageAtEnd) { - dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord); + // Damage at destination + if (damageAtEnd) { + dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord); + } } // Face target after teleport @@ -561,6 +572,12 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { this.damageRadius = damageRadius; } + @Override + public int getMaxPreviewDuration() { + int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1; + return blinkLimit * blinkDelayTicks + 10; + } + @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 102c4395f..1623bacd8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -207,26 +207,28 @@ public class AbilityTrap extends Ability implements IAbilityTrap { return; } - AxisAlignedBB box = AxisAlignedBB.getBoundingBox( - trapX - triggerRadius, trapY - 1, trapZ - triggerRadius, - trapX + triggerRadius, trapY + 2, trapZ + triggerRadius - ); + if (!isPreview()) { + AxisAlignedBB box = AxisAlignedBB.getBoundingBox( + trapX - triggerRadius, trapY - 1, trapZ - triggerRadius, + trapX + triggerRadius, trapY + 2, trapZ + triggerRadius + ); - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); - for (EntityLivingBase entity : entities) { - if (entity == caster) continue; - if (entity.isDead) continue; - if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; + for (EntityLivingBase entity : entities) { + if (entity == caster) continue; + if (entity.isDead) continue; + if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; - double dx = entity.posX - trapX; - double dz = entity.posZ - trapZ; - double dist = Math.sqrt(dx * dx + dz * dz); + double dx = entity.posX - trapX; + double dz = entity.posZ - trapZ; + double dist = Math.sqrt(dx * dx + dz * dz); - if (dist <= triggerRadius) { - triggerTrap(caster, entity, world); - return; + if (dist <= triggerRadius) { + triggerTrap(caster, entity, world); + return; + } } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index e31a1e6ce..73f9816f7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -84,29 +84,31 @@ public class AbilityVortex extends Ability implements IAbilityVortex { pullComplete = false; ticksSincePullDamage = 0; - if (aoe) { - AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); + if (!isPreview()) { + if (aoe) { + AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); - int count = 0; - for (EntityLivingBase entity : entities) { - if (entity == caster) continue; - if (entity.isDead) continue; + int count = 0; + for (EntityLivingBase entity : entities) { + if (entity == caster) continue; + if (entity.isDead) continue; - double dist = caster.getDistanceToEntity(entity); - if (dist <= pullRadius) { - getPulledEntities().add(entity.getUniqueID()); - count++; - if (count >= maxTargets) break; + double dist = caster.getDistanceToEntity(entity); + if (dist <= pullRadius) { + getPulledEntities().add(entity.getUniqueID()); + count++; + if (count >= maxTargets) break; + } } - } - } else { - // Single target mode - still check pullRadius - if (target != null && !target.isDead) { - double dist = caster.getDistanceToEntity(target); - if (dist <= pullRadius) { - getPulledEntities().add(target.getUniqueID()); + } else { + // Single target mode - still check pullRadius + if (target != null && !target.isDead) { + double dist = caster.getDistanceToEntity(target); + if (dist <= pullRadius) { + getPulledEntities().add(target.getUniqueID()); + } } } } @@ -114,6 +116,12 @@ public class AbilityVortex extends Ability implements IAbilityVortex { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (isPreview()) { + // No entities to pull in preview, just run animation for a duration + if (tick >= 60) signalCompletion(); + return; + } + if (pullComplete || getPulledEntities().isEmpty()) { signalCompletion(); return; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 60175169a..1427b298f 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -222,24 +222,39 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { setCharging(false); - // Update start position to current position + // Origin (tail) stays at the charged position startX = posX; startY = posY; startZ = posZ; + // Reset head offset to origin + headOffsetX = 0; + headOffsetY = 0; + headOffsetZ = 0; + prevHeadOffsetX = 0; + prevHeadOffsetY = 0; + prevHeadOffsetZ = 0; + + // Origin no longer follows owner after firing + attachedToOwner = false; + // Sync prev position to prevent visual jump on first frame prevPosX = posX; prevPosY = posY; prevPosZ = posZ; + // Reset trail + trailPoints.clear(); + trailPointAges.clear(); + trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); + // Fire forward based on owner facing direction Entity owner = getOwnerEntity(); if (owner != null) { float yaw = (float) Math.toRadians(owner.rotationYaw); - float pitch = 0; // Fire horizontally - motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed(); - motionY = -Math.sin(pitch) * getSpeed(); - motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed(); + motionX = -Math.sin(yaw) * getSpeed(); + motionY = 0; + motionZ = Math.cos(yaw) * getSpeed(); } else { motionX = getSpeed(); motionY = 0; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index bbd77abd0..3578ac9bf 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -38,7 +38,6 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { private int chargeTick = 0; private float targetDiscRadius = 1.0f; // Full radius to grow to during charging private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging - private EnergyAnchorData anchorData = new EnergyAnchorData(); // Data watcher index for charging state (synced to clients) private static final int DW_CHARGING = 20; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 1a036573e..9c09bd467 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -1,6 +1,5 @@ package kamkeel.npcs.entity; -import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; @@ -46,7 +45,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { private boolean charging = false; private int chargeDuration = 40; private int chargeTick = 0; - private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); private float targetSize = 1.0f; // Full size to grow to during charging // Data watcher index for charging state (synced to clients) @@ -454,7 +452,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size; - anchorData.readNBT(nbt); } @Override @@ -476,7 +473,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { nbt.setInteger("ChargeDuration", chargeDuration); nbt.setInteger("ChargeTick", chargeTick); nbt.setFloat("TargetSize", targetSize); - anchorData.writeNBT(nbt); } /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index 8eceb11de..4d072090e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -60,6 +60,10 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa private int maxTicks = 400; private long deathWorldTime = -1; + // Preview mode + private boolean previewMode = false; + private EntityLivingBase previewOwner = null; + // Damage state private transient int ticksSinceDamage = 0; private transient Set hitThisTick = new HashSet<>(); @@ -281,7 +285,16 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa target.velocityChanged = true; } + /** + * Set up preview mode. Owner is stored directly since world entity lookup won't work. + */ + public void setupPreview(EntityLivingBase owner) { + this.previewMode = true; + this.previewOwner = owner; + } + private Entity getOwner() { + if (previewMode && previewOwner != null) return previewOwner; if (ownerEntityId == -1) return null; return worldObj.getEntityByID(ownerEntityId); } diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 3255452c9..4331ab0ea 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -509,6 +509,8 @@ public class ClientProxy extends CommonProxy { } else { abilityNpc = new EntityCustomNpc(Minecraft.getMinecraft().theWorld); abilityNpc.display.texture = "customnpcs:textures/entity/humanmale/AnimationBody.png"; + abilityNpc.height = 1.8f; + abilityNpc.width = 0.6f; } return new GuiNpcManageAbilities(abilityNpc, npc != null); } diff --git a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java index 90204fdc4..2da4a1a7b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java +++ b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java @@ -4,15 +4,10 @@ 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.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.PreviewEntityHandler; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; -import kamkeel.npcs.entity.EntityAbilityBeam; -import kamkeel.npcs.entity.EntityAbilityDisc; -import kamkeel.npcs.entity.EntityAbilityLaser; -import kamkeel.npcs.entity.EntityAbilityOrb; -import kamkeel.npcs.entity.EntityAbilityProjectile; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; -import net.minecraft.world.World; import noppes.npcs.controllers.data.Animation; import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.entity.EntityCustomNpc; @@ -25,38 +20,39 @@ import java.util.List; /** * Client-side executor for previewing abilities in the GUI. * - * Lifecycle: - * 1. WINDUP phase - plays windup animation, entities spawn and charge - * 2. ACTIVE phase - plays active animation, entities fire and move - * 3. EXTENDED phase - continues for +100 ticks after animations to see results - * 4. IDLE - preview complete + * Uses unified execution: calls the REAL ability methods (onWindUpTick, onExecute, onActiveTick) + * with previewMode=true. Abilities skip damage/effects/sounds when isPreview() returns true. * - * Features: - * - Invisible fake target NPC placed in front of the preview NPC for targeting - * - NPC movement tracking for abilities that move the NPC (e.g. slam) + * Lifecycle: + * 1. WINDUP phase - real onWindUpTick() called, telegraph shown, entities may spawn + * 2. ACTIVE phase - real onExecute() then onActiveTick() called, physics simulated + * 3. EXTENDED phase - 100 extra ticks to observe results (entities continue updating) + * 4. IDLE - preview complete */ @SideOnly(Side.CLIENT) -public class AbilityPreviewExecutor { +public class AbilityPreviewExecutor implements PreviewEntityHandler { // ==================== PREVIEW STATE ==================== private Ability previewAbility; private EntityNPCInterface previewNpc; - private AbilityPhase phase = AbilityPhase.IDLE; + + /** Executor's own phase tracking (separate from ability.getPhase()) */ + private enum ExecutorPhase { IDLE, WINDUP, ACTIVE, EXTENDED } + private ExecutorPhase executorPhase = ExecutorPhase.IDLE; + private int currentTick = 0; private boolean playing = false; private boolean paused = false; // ==================== TIMING ==================== private static final int EXTENDED_DURATION = 100; - private int activeDuration = 40; + private int maxPreviewDuration = 200; // ==================== FAKE TARGET ==================== - /** Invisible fake target NPC for ability targeting (placed 5 blocks in front of preview NPC) */ private EntityNPCInterface fakeTarget; private static final double TARGET_DISTANCE = 5.0; // ==================== NPC START POSITION ==================== - /** Saved NPC position for restoring after movement abilities */ private double npcStartX, npcStartY, npcStartZ; // ==================== PREVIEW CONTENT ==================== @@ -74,7 +70,8 @@ public class AbilityPreviewExecutor { } /** - * Start previewing an ability. + * Start previewing an ability using unified execution. + * Sets previewMode=true on the ability, then calls its real methods. */ public void startPreview(Ability ability, EntityNPCInterface npc) { stop(); @@ -84,31 +81,35 @@ public class AbilityPreviewExecutor { this.currentTick = 0; this.playing = true; this.paused = false; - this.activeDuration = ability.getPreviewActiveDuration(); + this.maxPreviewDuration = ability.getMaxPreviewDuration(); // Save NPC start position for restoring later this.npcStartX = npc.posX; this.npcStartY = npc.posY; this.npcStartZ = npc.posZ; + // Enable preview mode on the ability + ability.setPreviewMode(true); + ability.setPreviewEntityHandler(this); + // Create invisible fake target NPC for targeting createFakeTarget(npc); - // Set preview target on the ability - ability.setPreviewTarget(fakeTarget); - // Start tracking NPC movement if (parentGui != null) { parentGui.startTrackingMovement(); } + // Use the ability's real start() method with the fake target + ability.start(fakeTarget); + if (ability.getWindUpTicks() <= 0) { // No windup — go straight to active - this.phase = AbilityPhase.ACTIVE; + this.executorPhase = ExecutorPhase.ACTIVE; transitionToActive(); } else { // Normal windup flow - this.phase = AbilityPhase.WINDUP; + this.executorPhase = ExecutorPhase.WINDUP; // Create telegraph for preview previewTelegraph = ability.createTelegraph(npc, fakeTarget); @@ -121,24 +122,17 @@ public class AbilityPreviewExecutor { data.setAnimation(windUpAnim); data.animation.paused = false; } - - // Spawn preview entity on first tick if ability spawns during windup - if (ability.spawnPreviewDuringWindup()) { - spawnPreviewEntity(); - } } } /** * Create an invisible fake target NPC placed in front of the preview NPC. - * This entity is NOT rendered - it only exists for ability targeting logic. */ private void createFakeTarget(EntityNPCInterface npc) { if (npc.worldObj == null) return; try { fakeTarget = new EntityCustomNpc(npc.worldObj); - // Position target in front of NPC based on its facing direction float facingYaw = GuiAbilityInterface.NPC_FACING_YAW; double yawRad = Math.toRadians(facingYaw); double targetX = npc.posX - Math.sin(yawRad) * TARGET_DISTANCE; @@ -153,15 +147,12 @@ public class AbilityPreviewExecutor { } } - /** - * Get the fake target entity for abilities that need a target. - */ public EntityLivingBase getFakeTarget() { return fakeTarget; } public void play() { - if (previewAbility == null || phase == AbilityPhase.IDLE) { + if (previewAbility == null || executorPhase == ExecutorPhase.IDLE) { return; } playing = true; @@ -187,9 +178,17 @@ public class AbilityPreviewExecutor { } public void stop() { + // Clean up the ability + if (previewAbility != null) { + previewAbility.cleanup(); + previewAbility.setPreviewMode(false); + previewAbility.setPreviewEntityHandler(null); + previewAbility.reset(); + } + playing = false; paused = false; - phase = AbilityPhase.IDLE; + executorPhase = ExecutorPhase.IDLE; currentTick = 0; previewTelegraph = null; @@ -211,9 +210,11 @@ public class AbilityPreviewExecutor { previewNpc.prevPosX = npcStartX; previewNpc.prevPosY = npcStartY; previewNpc.prevPosZ = npcStartZ; + previewNpc.motionX = 0; + previewNpc.motionY = 0; + previewNpc.motionZ = 0; } - // Remove fake target fakeTarget = null; if (previewNpc != null) { @@ -224,7 +225,7 @@ public class AbilityPreviewExecutor { } /** - * Tick the preview forward. + * Tick the preview forward. Calls real ability methods. */ public void tick() { if (!playing || paused || previewAbility == null || previewNpc == null) { @@ -239,14 +240,14 @@ public class AbilityPreviewExecutor { tickEntities(); - switch (phase) { + switch (executorPhase) { case WINDUP: tickWindup(); break; case ACTIVE: tickActive(); break; - case IDLE: + case EXTENDED: tickExtended(); break; default: @@ -255,7 +256,8 @@ public class AbilityPreviewExecutor { } private void tickWindup() { - previewAbility.onPreviewWindUpTick(previewNpc, currentTick); + // Call the REAL windup tick method + previewAbility.onWindUpTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick); if (currentTick >= previewAbility.getWindUpTicks()) { transitionToActive(); @@ -263,35 +265,41 @@ public class AbilityPreviewExecutor { } private void tickActive() { - previewAbility.onPreviewActiveTick(previewNpc, currentTick); + // Call the REAL active tick method + previewAbility.onActiveTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick); - if (currentTick == 1 && !previewAbility.spawnPreviewDuringWindup()) { - spawnPreviewEntity(); - fireEntities(); + // Simulate physics for movement abilities + simulatePhysics(); + + // Check if the ability signaled completion (phase went to IDLE) + if (previewAbility.getPhase() == AbilityPhase.IDLE) { + transitionToExtended(); + return; } - if (currentTick >= activeDuration) { + // Safety timeout + if (currentTick >= maxPreviewDuration) { transitionToExtended(); } } private void tickExtended() { + // Continue ticking entities so they can finish their animations if (currentTick >= EXTENDED_DURATION) { playing = false; } } private void transitionToActive() { - phase = AbilityPhase.ACTIVE; + executorPhase = ExecutorPhase.ACTIVE; currentTick = 0; previewTelegraph = null; - // Notify ability of execution (for movement abilities like Slam/Charge/Dash) - previewAbility.onPreviewExecute(previewNpc); - - fireEntities(); + // Call the REAL onExecute method (fires entities, initiates movement, etc.) + previewAbility.onExecute(previewNpc, fakeTarget, previewNpc.worldObj); + // Start active animation Animation activeAnim = previewAbility.getActiveAnimation(); if (activeAnim != null && previewNpc != null) { AnimationData data = previewNpc.display.animationData; @@ -301,7 +309,7 @@ public class AbilityPreviewExecutor { } private void transitionToExtended() { - phase = AbilityPhase.IDLE; + executorPhase = ExecutorPhase.EXTENDED; currentTick = 0; if (previewNpc != null) { @@ -310,26 +318,40 @@ public class AbilityPreviewExecutor { } } - private void spawnPreviewEntity() { - Entity entity = previewAbility.createPreviewEntity(previewNpc); - if (entity != null) { - previewEntities.add(entity); - if (parentGui != null) { - parentGui.addPreviewEntity(entity); - } - } - } + /** + * Simulate basic physics for movement abilities (Slam, Charge, Dash). + * Applies gravity, drag, and ground clamping to NPC position. + */ + private void simulatePhysics() { + if (!previewAbility.hasAbilityMovement()) return; + if (previewNpc == null) return; - private void fireEntities() { - for (Entity entity : previewEntities) { - if (entity instanceof EntityAbilityOrb) { - ((EntityAbilityOrb) entity).startPreviewFiring(); - } else if (entity instanceof EntityAbilityBeam) { - ((EntityAbilityBeam) entity).startPreviewFiring(); - } else if (entity instanceof EntityAbilityDisc) { - ((EntityAbilityDisc) entity).startPreviewFiring(); - } + // Save previous position for interpolation + previewNpc.prevPosX = previewNpc.posX; + previewNpc.prevPosY = previewNpc.posY; + previewNpc.prevPosZ = previewNpc.posZ; + + // Apply velocity + previewNpc.posX += previewNpc.motionX; + previewNpc.posY += previewNpc.motionY; + previewNpc.posZ += previewNpc.motionZ; + + // Apply gravity + previewNpc.motionY -= 0.08; + previewNpc.motionY *= 0.98; + + // Apply horizontal drag + previewNpc.motionX *= 0.91; + previewNpc.motionZ *= 0.91; + + // Ground clamp: if below start position and falling, stop + if (previewNpc.posY < npcStartY && previewNpc.motionY < 0) { + previewNpc.posY = npcStartY; + previewNpc.motionY = 0; + previewNpc.onGround = true; } + + previewNpc.fallDistance = 0; } private void tickEntities() { @@ -347,12 +369,38 @@ public class AbilityPreviewExecutor { } } + // ==================== PreviewEntityHandler ==================== + + @Override + public void onEntitySpawned(Entity entity) { + if (entity == null) return; + previewEntities.add(entity); + if (parentGui != null) { + parentGui.addPreviewEntity(entity); + } + } + + @Override + public void onEntityRemoved(Entity entity) { + if (entity == null) return; + previewEntities.remove(entity); + if (parentGui != null) { + parentGui.removePreviewEntity(entity); + } + } + // ==================== GETTERS ==================== public boolean isPlaying() { return playing; } public boolean isPaused() { return paused; } public boolean isActive() { return playing || paused; } - public AbilityPhase getPhase() { return phase; } + public AbilityPhase getPhase() { + switch (executorPhase) { + case WINDUP: return AbilityPhase.WINDUP; + case ACTIVE: return AbilityPhase.ACTIVE; + default: return AbilityPhase.IDLE; + } + } public int getCurrentTick() { return currentTick; } public TelegraphInstance getTelegraph() { return previewTelegraph; } public List getPreviewEntities() { return previewEntities; } @@ -364,11 +412,11 @@ public class AbilityPreviewExecutor { return "Stopped"; } else if (paused) { return "Paused"; - } else if (phase == AbilityPhase.WINDUP) { + } else if (executorPhase == ExecutorPhase.WINDUP) { return "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0); - } else if (phase == AbilityPhase.ACTIVE) { - return "Active: " + currentTick + "/" + activeDuration; - } else if (phase == AbilityPhase.IDLE && playing) { + } else if (executorPhase == ExecutorPhase.ACTIVE) { + return "Active: " + currentTick + "/" + maxPreviewDuration; + } else if (executorPhase == ExecutorPhase.EXTENDED) { return "Extended: " + currentTick + "/" + EXTENDED_DURATION; } return ""; From af02b0ab1d6dbd82daff76b7b43a10aea547064d Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Feb 2026 16:28:38 -0500 Subject: [PATCH 07/34] Various Fixes --- src/api | 2 +- .../client/renderer/RenderAbilityDisc.java | 11 +- .../controllers/data/ability/Ability.java | 154 +++++++++++++---- .../data/ability/RotationMode.java | 53 ++++++ .../data/ability/gui/AbilityFieldDefs.java | 1 + .../data/ability/type/AbilityBeam.java | 9 + .../data/ability/type/AbilityBeamDual.java | 12 ++ .../data/ability/type/AbilityDisc.java | 17 +- .../data/ability/type/AbilityDiscDual.java | 27 ++- .../data/ability/type/AbilityLaserShot.java | 15 +- .../data/ability/type/AbilityOrb.java | 6 + .../data/ability/type/AbilityOrbBarrage.java | 6 + .../data/ability/type/AbilityOrbDual.java | 12 ++ .../npcs/entity/EntityAbilityDisc.java | 31 +++- .../npcs/entity/EntityAbilityLaser.java | 153 ++++++++++++----- .../kamkeel/npcs/util/AnchorPointHelper.java | 38 ++++- src/main/java/noppes/npcs/DataAbilities.java | 155 +++++++++++------- .../gui/util/AbilityPreviewExecutor.java | 35 +++- .../npcs/entity/EntityNPCInterface.java | 4 +- .../assets/customnpcs/lang/en_US.lang | 21 ++- 20 files changed, 599 insertions(+), 163 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/RotationMode.java diff --git a/src/api b/src/api index f383fc303..fe1cec898 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit f383fc3037221791e2f22a272580df9be013810f +Subproject commit fe1cec89814290a745ac708b309703125b5a3c15 diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java index 20486d70e..8cb8cdaad 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -43,7 +43,16 @@ public class RenderAbilityDisc extends RenderAbilityProjectile { GL11.glPushMatrix(); GL11.glScalef(scale, scale, scale); - // Apply ONLY Y-axis rotation for flat spin + if (disc.isVertical()) { + // Vertical mode: thin edge faces direction of travel (like a saw blade) + // 1. Spin in local space (Y-axis) + // 2. Tilt 90° to stand up (X-axis) + // 3. Orient to face travel direction (Y-axis world yaw) + GL11.glRotatef(disc.getTravelYaw(), 0.0f, 1.0f, 0.0f); + GL11.glRotatef(90.0f, 1.0f, 0.0f, 0.0f); + } + + // Apply Y-axis rotation for spin GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); // Inner scale defines the core size diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index e6404a0cb..65d941327 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -67,7 +67,8 @@ public abstract class Ability implements IAbility { // Feedback protected LockMovementType lockMovement = LockMovementType.WINDUP; - protected LockType lockType = LockType.BOTH; + protected RotationMode rotationMode = RotationMode.FREE; + protected LockMovementType rotationPhase = LockMovementType.WINDUP_AND_ACTIVE; protected int windUpColor = 0x80FF4400; // Telegraph color during wind up protected int activeColor = 0xC0FF0000; // Telegraph warning/active color @@ -418,26 +419,33 @@ public abstract class Ability implements IAbility { defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks) .tab("General").range(0, 10000)); defs.add(FieldDef.section("ability.section.movement").tab("General")); - defs.add(FieldDef.row( - FieldDef.stringEnumField("ability.lockPosition", LockMovementType.getDisplayKeys(), + defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(), () -> this.getLockMovement().getDisplayKey(), v -> { - String[] keys = LockMovementType.getDisplayKeys(); - for (int i = 0; i < keys.length; i++) { - if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; } + for (LockMovementType t : LockMovementType.values()) { + if (t.getDisplayKey().equals(v)) { this.setLockMovement(t); break; } } }) - .hover("ability.hover.lockPosition"), - FieldDef.stringEnumField("ability.lockType", LockType.getDisplayKeys(), - () -> this.getLockType().getDisplayKey(), + .hover("ability.hover.lockMovement") + .tab("General")); + defs.add(FieldDef.row( + FieldDef.stringEnumField("ability.rotationMode", RotationMode.getDisplayKeys(), + () -> this.getRotationMode().getDisplayKey(), v -> { - String[] keys = LockType.getDisplayKeys(); - for (int i = 0; i < keys.length; i++) { - if (keys[i].equals(v)) { this.setLockType(LockType.fromOrdinal(i)); break; } + for (RotationMode m : RotationMode.values()) { + if (m.getDisplayKey().equals(v)) { this.setRotationMode(m); break; } } }) - .hover("ability.hover.lockType") - .visibleWhen(() -> this.lockMovement != LockMovementType.NO) + .hover("ability.hover.rotationMode"), + FieldDef.stringEnumField("ability.rotationPhase", getRotationPhaseKeys(), + () -> this.getRotationPhase().getDisplayKey(), + v -> { + for (LockMovementType t : LockMovementType.values()) { + if (t.getDisplayKey().equals(v)) { this.setRotationPhase(t); break; } + } + }) + .hover("ability.hover.rotationPhase") + .visibleWhen(() -> this.rotationMode != RotationMode.FREE) ).tab("General")); defs.add(FieldDef.row( FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) @@ -606,12 +614,12 @@ public abstract class Ability implements IAbility { instance.setEntityIdToFollow(caster.getEntityId()); // For LINE/CONE telegraphs: track target direction during windup - // Only track if movement is NOT locked during windup (locked = NPC can't rotate) + // Only track if rotation is NOT locked during windup (NPC can still turn) if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) { - if (!lockMovement.locksWindup()) { + if (!isRotationLockedDuringWindup()) { instance.setTargetEntityId(target.getEntityId()); } - // If windup is locked, yaw stays fixed at creation time + // If rotation is locked, yaw stays fixed at creation time } } else if (target != null) { // AOE_TARGET abilities: telegraph follows target during windup @@ -954,7 +962,8 @@ public abstract class Ability implements IAbility { nbt.setInteger("recovery", dazedTicks); nbt.setBoolean("interruptible", interruptible); nbt.setInteger("lockMovement", lockMovement.ordinal()); - nbt.setInteger("lockType", lockType.ordinal()); + nbt.setInteger("rotationMode", rotationMode.ordinal()); + nbt.setInteger("rotationPhase", rotationPhase.ordinal()); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); nbt.setString("windUpSound", windUpSound); @@ -1005,7 +1014,30 @@ public abstract class Ability implements IAbility { dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); - lockType = nbt.hasKey("lockType") ? LockType.fromOrdinal(nbt.getInteger("lockType")) : LockType.BOTH; + if (nbt.hasKey("rotationMode")) { + // Current format + rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode")); + rotationPhase = LockMovementType.fromOrdinal(nbt.getInteger("rotationPhase")); + } else if (nbt.hasKey("lockType")) { + // Legacy format migration: lockType was LockType enum (0=MOVEMENT, 1=ROTATION, 2=BOTH) + int oldLockType = nbt.getInteger("lockType"); + if (oldLockType == 0) { + // MOVEMENT only: movement locked, rotation was free + rotationMode = RotationMode.FREE; + } else if (oldLockType == 1) { + // ROTATION only: rotation locked same timing as old lockMovement, movement was free + rotationMode = RotationMode.LOCKED; + rotationPhase = lockMovement; + lockMovement = LockMovementType.NO; + } else { + // BOTH: both locked same timing + rotationMode = RotationMode.LOCKED; + rotationPhase = lockMovement; + } + } else { + rotationMode = RotationMode.FREE; + rotationPhase = LockMovementType.WINDUP_AND_ACTIVE; + } windUpColor = nbt.getInteger("windUpColor"); activeColor = nbt.getInteger("activeColor"); windUpSound = nbt.getString("windUpSound"); @@ -1170,12 +1202,20 @@ public abstract class Ability implements IAbility { this.lockMovement = lockMovement; } - public LockType getLockType() { - return lockType; + public RotationMode getRotationMode() { + return rotationMode; } - public void setLockType(LockType lockType) { - this.lockType = lockType; + public void setRotationMode(RotationMode rotationMode) { + this.rotationMode = rotationMode; + } + + public LockMovementType getRotationPhase() { + return rotationPhase; + } + + public void setRotationPhase(LockMovementType rotationPhase) { + this.rotationPhase = rotationPhase; } /** @@ -1197,21 +1237,39 @@ public abstract class Ability implements IAbility { } /** - * API method: Get lock type as integer. - * @return 0=MOVEMENT, 1=ROTATION, 2=BOTH + * API method: Get rotation mode as integer. + * @return 0=FREE, 1=LOCKED, 2=TRACK */ @Override - public int getLockTypeOrdinal() { - return lockType.ordinal(); + public int getRotationModeType() { + return rotationMode.ordinal(); } /** - * API method: Set lock type from integer. - * @param type 0=MOVEMENT, 1=ROTATION, 2=BOTH + * API method: Set rotation mode from integer. + * @param type 0=FREE, 1=LOCKED, 2=TRACK */ @Override - public void setLockTypeOrdinal(int type) { - this.lockType = LockType.fromOrdinal(type); + public void setRotationModeType(int type) { + this.rotationMode = RotationMode.fromOrdinal(type); + } + + /** + * API method: Get rotation phase as integer. + * @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public int getRotationPhaseType() { + return rotationPhase.ordinal(); + } + + /** + * API method: Set rotation phase from integer. + * @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public void setRotationPhaseType(int type) { + this.rotationPhase = LockMovementType.fromOrdinal(type); } /** @@ -1219,7 +1277,7 @@ public abstract class Ability implements IAbility { */ @Override public boolean isMovementLockedDuringWindup() { - return lockMovement.locksWindup() && lockType.locksMovement(); + return lockMovement.locksWindup(); } /** @@ -1227,7 +1285,7 @@ public abstract class Ability implements IAbility { */ @Override public boolean isMovementLockedDuringActive() { - return lockMovement.locksActive() && lockType.locksMovement(); + return lockMovement.locksActive(); } /** @@ -1235,7 +1293,7 @@ public abstract class Ability implements IAbility { */ @Override public boolean isRotationLockedDuringWindup() { - return lockMovement.locksWindup() && lockType.locksRotation(); + return rotationMode == RotationMode.LOCKED && rotationPhase.locksWindup(); } /** @@ -1243,7 +1301,7 @@ public abstract class Ability implements IAbility { */ @Override public boolean isRotationLockedDuringActive() { - return lockMovement.locksActive() && lockType.locksRotation(); + return rotationMode == RotationMode.LOCKED && rotationPhase.locksActive(); } /** @@ -1274,6 +1332,21 @@ public abstract class Ability implements IAbility { } } + /** + * Check if hit scan (force face target) is active during the current phase. + */ + public boolean isHitScanForCurrentPhase() { + if (rotationMode != RotationMode.TRACK) return false; + switch (phase) { + case WINDUP: + return rotationPhase.locksWindup(); + case ACTIVE: + return rotationPhase.locksActive(); + default: + return false; + } + } + public int getWindUpColor() { return windUpColor; } @@ -1457,6 +1530,17 @@ public abstract class Ability implements IAbility { effects.clear(); } + /** + * Get display keys for rotation phase dropdown (excludes "None" since mode=FREE handles that). + */ + private static String[] getRotationPhaseKeys() { + return new String[]{ + "ability.lockMove.windup", + "ability.lockMove.active", + "ability.lockMove.both" + }; + } + // ═══════════════════════════════════════════════════════════════════ // SHIT FROM THE ASS // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/RotationMode.java b/src/main/java/kamkeel/npcs/controllers/data/ability/RotationMode.java new file mode 100644 index 000000000..e1142b571 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/RotationMode.java @@ -0,0 +1,53 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines what happens to NPC rotation during ability phases. + * Used with a LockMovementType to control WHEN it applies. + */ +public enum RotationMode { + /** + * NPC rotates normally via AI look helper. + */ + FREE, + + /** + * Freeze rotation at the moment the phase starts. + * NPC stays facing the direction it was looking when the phase began. + */ + LOCKED, + + /** + * Force NPC to continuously face its target (hit scan). + * Overrides AI look helper every tick for precise aiming. + */ + TRACK; + + public String getDisplayKey() { + switch (this) { + case FREE: + return "ability.rotation.free"; + case LOCKED: + return "ability.rotation.locked"; + case TRACK: + return "ability.rotation.track"; + default: + return "ability.rotation.free"; + } + } + + public static String[] getDisplayKeys() { + return new String[]{ + "ability.rotation.free", + "ability.rotation.locked", + "ability.rotation.track" + }; + } + + public static RotationMode fromOrdinal(int ordinal) { + RotationMode[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return FREE; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java index bc1dc9ab8..6e419b943 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/AbilityFieldDefs.java @@ -23,4 +23,5 @@ public class AbilityFieldDefs { () -> getter.get(), v -> setter.accept((List) v)); } + } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 8e3bfc575..356c1c4e9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.RotationMode; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.controllers.data.telegraph.Telegraph; @@ -57,6 +58,8 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { this.cooldownTicks = 0; this.windUpTicks = 40; this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.rotationMode = RotationMode.TRACK; + this.rotationPhase = LockMovementType.WINDUP_AND_ACTIVE; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; // Default built-in animations @@ -313,6 +316,12 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), + FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) + .tab("ability.tab.visual"), FieldDef.section("ability.section.colors").tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) .tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java index 89c5b126f..daf04a446 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java @@ -439,6 +439,12 @@ public class AbilityBeamDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(0), v -> setInnerColor(0, v)) .tab("ability.tab.visual"), @@ -475,6 +481,12 @@ public class AbilityBeamDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(1), v -> setInnerColor(1, v)) .tab("ability.tab.visual"), 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 77d137eb9..98fe9eaac 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 @@ -32,6 +32,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { // Disc geometry private float discRadius = 1.0f; private float discThickness = 0.2f; + private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward) // Boomerang properties private boolean boomerang = false; @@ -107,9 +108,9 @@ public class AbilityDisc extends Ability implements IAbilityDisc { boomerang, boomerangDelay); if (isPreview()) { - discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks); + discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks, vertical); } else { - discEntity.setupCharging(anchorData, windUpTicks); + discEntity.setupCharging(anchorData, windUpTicks, vertical); } discEntity.setEffects(this.effects); @@ -176,6 +177,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("discRadius", discRadius); nbt.setFloat("discThickness", discThickness); + nbt.setBoolean("vertical", vertical); nbt.setBoolean("boomerang", boomerang); nbt.setInteger("boomerangDelay", boomerangDelay); anchorData.writeNBT(nbt); @@ -190,6 +192,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void readTypeNBT(NBTTagCompound nbt) { this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical"); this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; anchorData.readNBT(nbt); @@ -207,6 +210,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } public float getDiscThickness() { return discThickness; } public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public boolean isVertical() { return vertical; } + public void setVertical(boolean vertical) { this.vertical = vertical; } public float getMaxDistance() { return lifespanData.maxDistance; } public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } public int getMaxLifetime() { return lifespanData.maxLifetime; } @@ -286,6 +291,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius), FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness) ), + FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical) + .hover("ability.hover.vertical"), FieldDef.row( FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) @@ -309,6 +316,12 @@ public class AbilityDisc extends Ability implements IAbilityDisc { // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), + FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) + .tab("ability.tab.visual"), FieldDef.section("ability.section.colors").tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) .tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java index 2fbd6cbda..87d7fffd5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java @@ -28,6 +28,7 @@ public class AbilityDiscDual extends Ability { // Disc geometry private float discRadius = 1.0f; private float discThickness = 0.2f; + private boolean vertical = false; // Boomerang properties private boolean boomerang = false; @@ -144,11 +145,11 @@ public class AbilityDiscDual extends Ability { discEntity2.setSiblingEntityId(discEntity.getEntityId()); if (isPreview()) { - discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks); - discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks); + discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks, vertical); + discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks, vertical); } else { - discEntity.setupCharging(anchorData[0], windUpTicks); - discEntity2.setupCharging(anchorData[1], windUpTicks); + discEntity.setupCharging(anchorData[0], windUpTicks, vertical); + discEntity2.setupCharging(anchorData[1], windUpTicks, vertical); } discEntity.setEffects(this.effects); @@ -244,6 +245,7 @@ public class AbilityDiscDual extends Ability { public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("discRadius", discRadius); nbt.setFloat("discThickness", discThickness); + nbt.setBoolean("vertical", vertical); nbt.setBoolean("boomerang", boomerang); nbt.setInteger("boomerangDelay", boomerangDelay); nbt.setBoolean("dualFire", dualFire); @@ -274,6 +276,7 @@ public class AbilityDiscDual extends Ability { this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical"); this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); @@ -315,6 +318,8 @@ public class AbilityDiscDual extends Ability { public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } public float getDiscThickness() { return discThickness; } public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public boolean isVertical() { return vertical; } + public void setVertical(boolean vertical) { this.vertical = vertical; } public float getMaxDistance() { return lifespanData.maxDistance; } public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } public int getMaxLifetime() { return lifespanData.maxLifetime; } @@ -426,6 +431,8 @@ public class AbilityDiscDual extends Ability { FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius), FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness) ), + FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical) + .hover("ability.hover.vertical"), FieldDef.row( FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) @@ -455,6 +462,12 @@ public class AbilityDiscDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(0), v -> setInnerColor(0, v)) .tab("ability.tab.visual"), @@ -491,6 +504,12 @@ public class AbilityDiscDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(1), v -> setInnerColor(1, v)) .tab("ability.tab.visual"), 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 d211de6d2..d5b48f1fd 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 @@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.RotationMode; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.data.*; import noppes.npcs.client.gui.builder.FieldDef; @@ -38,7 +39,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { private final EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f); private final EnergyLightningData lightningData = new EnergyLightningData(); private final EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100); - private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); + private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData(); // Transient state for laser entity (used for movement locking) @@ -53,6 +54,8 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { this.cooldownTicks = 0; this.windUpTicks = 15; this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.rotationMode = RotationMode.TRACK; + this.rotationPhase = LockMovementType.WINDUP; this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; // Default built-in animations @@ -118,10 +121,8 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { return; } + laserEntity.setLockVerticalDirection(true); laserEntity.startMoving(target); - - // Ability stays active until entity dies (prevents firing another while projectile is alive) - // Movement locking is handled separately by the base class } @Override @@ -269,6 +270,12 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), + FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) + .tab("ability.tab.visual"), FieldDef.section("ability.section.colors").tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"), FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"), 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 6a87b38b4..1f1cb5e7d 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 @@ -446,6 +446,12 @@ public class AbilityOrb extends Ability implements IAbilityOrb { // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), + FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) + .tab("ability.tab.visual"), FieldDef.section("ability.section.colors").tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"), FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java index dda01f68a..f384c4b71 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java @@ -581,6 +581,12 @@ public class AbilityOrbBarrage extends Ability { // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), + FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) + .tab("ability.tab.visual"), FieldDef.section("ability.section.colors").tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"), FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java index 7b8362464..3cabf4892 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java @@ -511,6 +511,12 @@ public class AbilityOrbDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(0), v -> setInnerColor(0, v)) .tab("ability.tab.visual"), @@ -547,6 +553,12 @@ public class AbilityOrbDual extends Ability { FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)), + FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v)) + ).tab("ability.tab.visual"), + FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v)) + .tab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", () -> getInnerColor(1), v -> setInnerColor(1, v)) .tab("ability.tab.visual"), diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 3578ac9bf..edd7b3dff 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -31,6 +31,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { // Disc shape properties private float discRadius = 1.0f; // Width of disc private float discThickness = 0.2f; // Height of disc + private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward) // Charging state (during windup) private boolean charging = false; @@ -122,11 +123,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { * The disc will grow from 0 to discRadius over chargeDuration ticks. * Position follows the owner based on anchor point. */ - public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { + public void setupCharging(EnergyAnchorData anchor, int chargeDuration, boolean vertical) { setCharging(true); this.chargeDuration = chargeDuration; this.chargeTick = 0; this.anchorData = anchor; + this.vertical = vertical; this.targetDiscRadius = this.discRadius; this.targetDiscThickness = this.discThickness; this.discRadius = 0.01f; @@ -144,7 +146,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { * Follows anchor point and animations like in the real game. * Can be fired when transitioning to active phase. */ - public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { + public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, boolean vertical) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -157,6 +159,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.chargeDuration = chargeDuration; this.chargeTick = 0; this.anchorData = anchor; + this.vertical = vertical; // Store target size and start at 0 for grow effect this.targetDiscRadius = discRadius; @@ -541,6 +544,28 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return returning; } + public boolean isVertical() { + return vertical; + } + + /** + * Get the yaw angle (degrees) from motion vector for vertical disc orientation. + * During charging, uses owner's facing direction. + */ + public float getTravelYaw() { + if (isCharging()) { + Entity owner = previewMode ? previewOwner : getOwnerEntity(); + if (owner != null) { + return owner.rotationYaw; + } + } + double speed = Math.sqrt(motionX * motionX + motionZ * motionZ); + if (speed > 0.001) { + return (float) (Math.atan2(-motionX, motionZ) * 180.0 / Math.PI); + } + return 0.0f; + } + // ==================== NBT ==================== @Override @@ -549,6 +574,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.boomerangDelay = nbt.hasKey("BoomerangDelay") ? nbt.getInteger("BoomerangDelay") : 40; this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f; this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f; + this.vertical = nbt.hasKey("Vertical") ? nbt.getBoolean("Vertical") : false; this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning"); // Charging state boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); @@ -566,6 +592,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { nbt.setInteger("BoomerangDelay", boomerangDelay); nbt.setFloat("DiscRadius", discRadius); nbt.setFloat("DiscThickness", discThickness); + nbt.setBoolean("Vertical", vertical); nbt.setBoolean("Returning", returning); // Charging state nbt.setBoolean("Charging", isCharging()); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 9c09bd467..8a1325a27 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -35,6 +35,9 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { // Direction (normalized) private double dirX, dirY, dirZ; + // Lock vertical direction after firing (only update yaw, keep pitch fixed) + private boolean lockVerticalDirection = false; + // Track hit entities to avoid double-damage private Set hitEntities = new HashSet<>(); @@ -165,6 +168,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { return; } + // Track owner's anchor position and rotation each tick + // so the laser follows the NPC's facing direction + updateLaserOriginAndDirection(); + if (!fullyExtended) { // Expand the laser currentLength += expansionSpeed; @@ -187,27 +194,76 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } else { // Laser is fully extended, count down linger time ticksSinceFullExtension++; + + // Update end point during linger (origin/direction may still change) + endX = startX + dirX * currentLength; + endY = startY + dirY * currentLength; + endZ = startZ + dirZ * currentLength; + if (ticksSinceFullExtension >= lingerTicks) { this.setDead(); } } } + /** + * Update laser origin and direction from the owner's current anchor position and rotation. + * Called each tick so the laser follows the NPC when tracking a target. + */ + private void updateLaserOriginAndDirection() { + Entity owner = getOwnerEntity(); + if (owner == null || owner.isDead) return; + if (!(owner instanceof EntityLivingBase)) return; + + EntityLivingBase livingOwner = (EntityLivingBase) owner; + + // Update origin from anchor point + if (anchorData != null) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData); + startX = pos.xCoord; + startY = pos.yCoord; + startZ = pos.zCoord; + } else { + startX = owner.posX; + startY = owner.posY + owner.height * 0.7; + startZ = owner.posZ; + } + + // Update direction from owner's current rotation + float yaw = (float) Math.toRadians(owner.rotationYaw); + if (lockVerticalDirection) { + // Only update horizontal direction (yaw), keep vertical (pitch) fixed + double horizontalLen = Math.sqrt(dirX * dirX + dirZ * dirZ); + if (horizontalLen < 0.001) horizontalLen = 1.0; + dirX = -Math.sin(yaw) * horizontalLen; + dirZ = Math.cos(yaw) * horizontalLen; + } else { + float pitch = (float) Math.toRadians(owner.rotationPitch); + dirX = -Math.sin(yaw) * Math.cos(pitch); + dirY = -Math.sin(pitch); + dirZ = Math.cos(yaw) * Math.cos(pitch); + } + + // Keep entity positioned at origin + prevPosX = startX; + prevPosY = startY; + prevPosZ = startZ; + setPosition(startX, startY, startZ); + } + public void startMoving(EntityLivingBase target) { if (!isCharging()) return; setCharging(false); - // Update start position to current position + // Initialize from current anchor position + // updateLaserOriginAndDirection() will keep these updated each tick startX = posX; startY = posY; startZ = posZ; - // Always derive direction from the owner's current yaw/pitch. - // This ensures the laser fires in the direction the NPC is visually facing, - // which matches the telegraph line direction regardless of Lock Movement mode. + // Set initial direction from owner's rotation Entity owner = getOwnerEntity(); - if (owner != null) { float yaw = (float) Math.toRadians(owner.rotationYaw); float pitch = (float) Math.toRadians(owner.rotationPitch); @@ -215,7 +271,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { this.dirY = -Math.sin(pitch); this.dirZ = Math.cos(yaw) * Math.cos(pitch); } else if (target != null) { - // Fallback: aim at target if owner is somehow unavailable double dx = target.posX - startX; double dy = (target.posY + target.getEyeHeight() - 0.4) - startY; double dz = target.posZ - startZ; @@ -227,12 +282,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } } - setPosition(endX, endY, endZ); - - // Initialize end point - this.endX = posX; - this.endY = posY; - this.endZ = posZ; + // Initialize end point at start (will expand from here) + this.endX = startX; + this.endY = startY; + this.endZ = startZ; } private void checkBlockCollision() { @@ -308,14 +361,14 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } private void checkEntityCollisionAlongLine() { - // Check collision along the entire laser line - // Use a slightly expanded AABB for the full line - double minX = Math.min(startX, endX) - laserWidth; - double minY = Math.min(startY, endY) - laserWidth; - double minZ = Math.min(startZ, endZ) - laserWidth; - double maxX = Math.max(startX, endX) + laserWidth; - double maxY = Math.max(startY, endY) + laserWidth; - double maxZ = Math.max(startZ, endZ) + laserWidth; + // Expand search AABB by entity sizes to ensure we find all potential targets + double expand = Math.max(laserWidth, 1.0); + double minX = Math.min(startX, endX) - expand; + double minY = Math.min(startY, endY) - expand; + double minZ = Math.min(startZ, endZ) - expand; + double maxX = Math.max(startX, endX) + expand; + double maxY = Math.max(startY, endY) + expand; + double maxZ = Math.max(startZ, endZ) + expand; AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ); @@ -326,7 +379,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { if (shouldIgnoreEntity(entity)) continue; if (hitEntities.contains(entity.getEntityId())) continue; - // Check if entity is close to the laser line + // Check if entity's bounding box intersects with the laser line if (isEntityOnLine(entity)) { hitEntities.add(entity.getEntityId()); applyDamage(entity); @@ -337,42 +390,64 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } /** - * Check if an entity intersects with the laser line. + * Check if an entity's bounding box intersects with the laser line. + * Uses separate XZ (horizontal) and Y (vertical) checks against + * the entity's full bounding box rather than just its center point. */ private boolean isEntityOnLine(EntityLivingBase entity) { - // Get entity center double ex = entity.posX; - double ey = entity.posY + entity.height * 0.5; double ez = entity.posZ; - // Vector from start to entity + // Project entity XZ position onto the laser line direction double vx = ex - startX; - double vy = ey - startY; double vz = ez - startZ; - - // Project onto line direction - double dot = vx * dirX + vy * dirY + vz * dirZ; - - // Clamp to line segment + // Project using horizontal direction components only + double dirLenXZ = Math.sqrt(dirX * dirX + dirZ * dirZ); + double dot; + if (dirLenXZ > 0.001) { + dot = (vx * dirX + vz * dirZ) / (dirLenXZ * dirLenXZ) * dirLenXZ; + // Remap to full 3D parameter + dot = (vx * dirX + vz * dirZ + (entity.posY + entity.height * 0.5 - startY) * dirY); + } else { + // Laser fires nearly straight up/down - use full 3D projection + dot = vx * dirX + (entity.posY + entity.height * 0.5 - startY) * dirY + vz * dirZ; + } dot = Math.max(0, Math.min(dot, currentLength)); - // Closest point on line + // Closest point on laser line at this parameter double closestX = startX + dirX * dot; double closestY = startY + dirY * dot; double closestZ = startZ + dirZ * dot; - // Distance from entity to closest point - double distSq = (ex - closestX) * (ex - closestX) + - (ey - closestY) * (ey - closestY) + - (ez - closestZ) * (ez - closestZ); + // XZ distance check: entity center vs laser line point + // Use minimum collision width so visually thin lasers still hit reliably + double xzDistSq = (ex - closestX) * (ex - closestX) + (ez - closestZ) * (ez - closestZ); + double effectiveWidth = Math.max(laserWidth, 0.5); + double hitRadiusXZ = effectiveWidth * 0.5 + entity.width * 0.5; + if (xzDistSq > hitRadiusXZ * hitRadiusXZ) return false; - // Hit if within laser width + entity radius - double hitRadius = laserWidth * 0.5 + entity.width * 0.5; - return distSq <= hitRadius * hitRadius; + // Y overlap check: laser point vs entity's full height range + double entityMinY = entity.boundingBox.minY; + double entityMaxY = entity.boundingBox.maxY; + double laserHalfWidth = effectiveWidth * 0.5; + + // Check if laser Y is within entity's height range (expanded by laser half-width) + if (closestY < entityMinY - laserHalfWidth) return false; + if (closestY > entityMaxY + laserHalfWidth) return false; + + return true; } // ==================== GETTERS FOR RENDERER ==================== + public void setLockVerticalDirection(boolean lock) { + this.lockVerticalDirection = lock; + } + + public boolean isLockVerticalDirection() { + return lockVerticalDirection; + } + public float getLaserWidth() { return laserWidth; } diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 3ec5560ba..5ee5936b1 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -100,9 +100,12 @@ public class AnchorPointHelper { break; } - x += anchorX * scale; - y += anchorY * scale; - z += anchorZ * scale; + // Rotate offsets relative to entity's body yaw + // +X = entity's right, +Y = up, +Z = entity's forward + Vec3 rotatedOffset = rotateOffsetByYaw(anchorX * scale, anchorY * scale, anchorZ * scale, bodyYaw); + x += rotatedOffset.xCoord; + y += rotatedOffset.yCoord; + z += rotatedOffset.zCoord; return Vec3.createVectorHelper(x, y, z); } @@ -246,9 +249,11 @@ public class AnchorPointHelper { double worldY = entity.posY + shoulderHeight - blockY; double worldZ = entity.posZ + worldOffsetZ; - worldX += anchor.anchorOffsetX * scale; - worldY += anchor.anchorOffsetY * scale; - worldZ += anchor.anchorOffsetZ * scale; + // Rotate offsets relative to entity's body yaw + Vec3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw); + worldX += rotatedOffset.xCoord; + worldY += rotatedOffset.yCoord; + worldZ += rotatedOffset.zCoord; return Vec3.createVectorHelper(worldX, worldY, worldZ); } @@ -317,9 +322,11 @@ public class AnchorPointHelper { double y = entity.posY + entity.height * FALLBACK_ARM_HEIGHT + armLengthOffset; double z = entity.posZ + lateralZ + forwardZ; - x += anchor.anchorOffsetX * scale; - y += anchor.anchorOffsetY * scale; - z += anchor.anchorOffsetZ * scale; + // Rotate offsets relative to entity's body yaw + Vec3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw); + x += rotatedOffset.xCoord; + y += rotatedOffset.yCoord; + z += rotatedOffset.zCoord; return Vec3.createVectorHelper(x, y, z); } @@ -344,4 +351,17 @@ public class AnchorPointHelper { return FRONT_HEIGHT; } } + + /** + * Rotate an offset vector by the entity's body yaw so offsets are entity-relative. + * +X = entity's right, +Y = up, +Z = entity's forward. + * @param bodyYawRad body yaw in radians + */ + private static Vec3 rotateOffsetByYaw(double offsetX, double offsetY, double offsetZ, float bodyYawRad) { + double cos = Math.cos(bodyYawRad); + double sin = Math.sin(bodyYawRad); + double worldX = -offsetX * cos - offsetZ * sin; + double worldZ = -offsetX * sin + offsetZ * cos; + return Vec3.createVectorHelper(worldX, offsetY, worldZ); + } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 19dc59ecd..606df7742 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -100,6 +100,14 @@ public class DataAbilities { private transient double lockedPosY = 0; private transient double lockedPosZ = 0; + /** + * Hit scan state - forces NPC to face target. + * Set during tickCurrentAbility(), applied after super.onLivingUpdate() + * to override AI look helper rotation. + */ + private transient boolean hitScanActive = false; + private transient EntityLivingBase hitScanTarget = null; + // ═══════════════════════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════════════════════ @@ -126,13 +134,13 @@ public class DataAbilities { if (currentAbility != null) { removeTelegraph(currentAbility); stopAbilityAnimation(); - releaseLockedRotation(); + releaseRotationControl(); releaseLockedPosition(); currentAbility = null; lastTarget = null; } else { // No ability but locks could be orphaned - if (rotationLocked) releaseLockedRotation(); + if (rotationLocked || hitScanActive) releaseRotationControl(); if (positionLocked) releaseLockedPosition(); } return; @@ -140,7 +148,7 @@ public class DataAbilities { // Safety: release orphaned locks if no ability is actively executing if (currentAbility == null || !currentAbility.isExecuting()) { - if (rotationLocked) releaseLockedRotation(); + if (rotationLocked || hitScanActive) releaseRotationControl(); if (positionLocked) releaseLockedPosition(); } @@ -181,13 +189,13 @@ public class DataAbilities { // Remove telegraph - it has served its purpose removeTelegraph(currentAbility); - // Handle rotation lock transition from WINDUP to ACTIVE + // Handle rotation control transition from WINDUP to ACTIVE if (currentAbility.isRotationLockedDuringActive()) { if (!rotationLocked) { captureLockedRotation(); } } else if (rotationLocked) { - releaseLockedRotation(); + releaseRotationControl(); } // Handle position lock transition from WINDUP to ACTIVE @@ -203,6 +211,11 @@ public class DataAbilities { playAbilitySound(currentAbility.getActiveSound()); playAbilityAnimation(currentAbility.getActiveAnimation()); + // Snap NPC to face target before execute so startMoving() reads correct rotation + if (currentAbility.isHitScanForCurrentPhase() && target != null) { + faceTarget(target); + } + // Fire execute event (cancelable) AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent( npc.wrappedNPC, currentAbility, target); @@ -239,6 +252,15 @@ public class DataAbilities { break; } + // Update hit scan state - actual facing is deferred to applyRotationControl() + // which runs AFTER super.onLivingUpdate() to override AI look helper + if (currentAbility != null && currentAbility.isExecuting() + && currentAbility.isHitScanForCurrentPhase() && target != null) { + enableHitScan(target); + } else if (hitScanActive) { + releaseRotationControl(); + } + // Apply movement control if ability is still executing if (currentAbility != null && currentAbility.isExecuting()) { applyMovementControl(); @@ -284,7 +306,7 @@ public class DataAbilities { NpcAPI.EVENT_BUS.post(completeEvent); // Release locks before cleanup - releaseLockedRotation(); + releaseRotationControl(); releaseLockedPosition(); // Clean up and roll cooldown (this sets currentAbility to null) @@ -301,7 +323,7 @@ public class DataAbilities { stopAbilityAnimation(); // Release locks - releaseLockedRotation(); + releaseRotationControl(); releaseLockedPosition(); // Calculate cooldown: random(min, max) + ability offset @@ -679,7 +701,7 @@ public class DataAbilities { stopAbilityAnimation(); // Release locks - releaseLockedRotation(); + releaseRotationControl(); releaseLockedPosition(); // Fire interrupt event @@ -702,7 +724,7 @@ public class DataAbilities { if (currentAbility != null) { removeTelegraph(currentAbility); stopAbilityAnimation(); - releaseLockedRotation(); + releaseRotationControl(); releaseLockedPosition(); currentAbility.interrupt(); currentAbility = null; @@ -1000,15 +1022,15 @@ public class DataAbilities { } // ═══════════════════════════════════════════════════════════════════ - // ROTATION LOCKING + // ROTATION CONTROL // ═══════════════════════════════════════════════════════════════════ - /** Bit flag for rotation lock in the existing data watcher slot 15 bitfield */ - private static final int ROTATION_LOCKED_FLAG = 16; + /** Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 */ + private static final int ROTATION_CONTROLLED_FLAG = 16; /** * Capture current rotation values to lock NPC's look direction. - * Called when entering a locked phase (WINDUP or ACTIVE depending on LockMovementType). + * Called when entering a LOCKED rotation phase. */ private void captureLockedRotation() { lockedYaw = npc.rotationYaw; @@ -1016,41 +1038,79 @@ public class DataAbilities { lockedRenderYawOffset = npc.renderYawOffset; lockedPitch = npc.rotationPitch; rotationLocked = true; - npc.setBoolFlag(true, ROTATION_LOCKED_FLAG); + npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG); } /** - * Release the rotation lock. - * Called when leaving the locked phase or ability completes. + * Release rotation control (both locked and hit scan). */ - private void releaseLockedRotation() { + private void releaseRotationControl() { rotationLocked = false; - npc.setBoolFlag(false, ROTATION_LOCKED_FLAG); + hitScanActive = false; + hitScanTarget = null; + npc.setBoolFlag(false, ROTATION_CONTROLLED_FLAG); } /** - * Apply locked rotation values to the NPC. - * Called AFTER super.onLivingUpdate() in EntityNPCInterface to override - * any rotation changes made by the look helper, AI tasks, or body smoothing. - * Runs on both client and server. + * Enable hit scan tracking for the given target. */ - public void applyLockedRotation() { + private void enableHitScan(EntityLivingBase target) { + if (!hitScanActive) { + hitScanActive = true; + npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG); + } + hitScanTarget = target; + } + + /** + * Snap the NPC to face the target instantly. + */ + private void faceTarget(EntityLivingBase target) { + double dx = target.posX - npc.posX; + double dz = target.posZ - npc.posZ; + double dy = (target.posY + target.getEyeHeight() * 0.5) - (npc.posY + npc.getEyeHeight()); + double distXZ = Math.sqrt(dx * dx + dz * dz); + + float yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); + float pitch = (float) (-(Math.atan2(dy, distXZ)) * 180.0 / Math.PI); + + npc.rotationYaw = yaw; + npc.rotationYawHead = yaw; + npc.renderYawOffset = yaw; + npc.rotationPitch = pitch; + npc.prevRotationYaw = yaw; + npc.prevRotationYawHead = yaw; + npc.prevRenderYawOffset = yaw; + npc.prevRotationPitch = pitch; + } + + /** + * Apply rotation control after super.onLivingUpdate() and super.onUpdate(). + * Handles both LOCKED (freeze at captured values) and TRACK (face target) modes. + * + * Server: computes the correct rotation (locked values or target facing). + * Client: trusts the server-synced rotation values and prevents body smoothing override. + */ + public void applyRotationControl() { if (npc.worldObj.isRemote) { - // Client-side: check bit 16 in the existing flags data watcher (slot 15) - boolean flagActive = npc.getBoolFlag(ROTATION_LOCKED_FLAG); - if (!flagActive) { + // Client: check single flag for any rotation control + if (!npc.getBoolFlag(ROTATION_CONTROLLED_FLAG)) { rotationLocked = false; return; } - // First tick the flag is active: capture current rotation values - if (!rotationLocked) { - lockedYaw = npc.rotationYaw; - lockedYawHead = npc.rotationYawHead; - lockedRenderYawOffset = npc.renderYawOffset; - lockedPitch = npc.rotationPitch; - rotationLocked = true; - } - // Force all rotation values back to locked state + // Trust server rotation, prevent body smoothing from overriding + npc.prevRotationYaw = npc.rotationYaw; + npc.prevRotationYawHead = npc.rotationYawHead; + npc.renderYawOffset = npc.rotationYawHead; + npc.prevRenderYawOffset = npc.rotationYawHead; + npc.prevRotationPitch = npc.rotationPitch; + return; + } + + // Server: apply the appropriate rotation + if (hitScanActive && hitScanTarget != null && !hitScanTarget.isDead) { + faceTarget(hitScanTarget); + } else if (rotationLocked) { npc.rotationYaw = lockedYaw; npc.rotationYawHead = lockedYawHead; npc.renderYawOffset = lockedRenderYawOffset; @@ -1059,39 +1119,22 @@ public class DataAbilities { npc.prevRotationYawHead = lockedYawHead; npc.prevRenderYawOffset = lockedRenderYawOffset; npc.prevRotationPitch = lockedPitch; - return; } - - // Server-side: use stored locked values - if (!rotationLocked) { - return; - } - - // Force rotation back to locked values - npc.rotationYaw = lockedYaw; - npc.rotationYawHead = lockedYawHead; - npc.renderYawOffset = lockedRenderYawOffset; - npc.rotationPitch = lockedPitch; - - // Also set prev values to prevent interpolation jitter - npc.prevRotationYaw = lockedYaw; - npc.prevRotationYawHead = lockedYawHead; - npc.prevRenderYawOffset = lockedRenderYawOffset; - npc.prevRotationPitch = lockedPitch; } /** - * Check if rotation is currently locked. + * Check if rotation is currently controlled (locked or tracking target). + * Used by AI tasks to determine if they should try to change NPC look direction. */ public boolean isRotationLocked() { - return rotationLocked; + return rotationLocked || hitScanActive; } // ═══════════════════════════════════════════════════════════════════ // POSITION LOCKING // ═══════════════════════════════════════════════════════════════════ - /** Bit flag for position lock in the existing data watcher slot 15 bitfield */ + /** Bit flag for position lock in data watcher slot 15 */ private static final int POSITION_LOCKED_FLAG = 32; /** diff --git a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java index 2da4a1a7b..7d856d916 100644 --- a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java +++ b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java @@ -169,6 +169,9 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { public void pause() { paused = true; + // Immediately sync prevPos to pos for NPC and all entities to prevent interpolation jitter + syncPrevPositions(); + if (previewNpc != null) { AnimationData data = previewNpc.display.animationData; if (data.animation != null) { @@ -177,6 +180,25 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { } } + /** + * Sync prevPos to pos for the NPC and all preview entities. + * Prevents rendering interpolation jitter when standing still or paused. + */ + private void syncPrevPositions() { + if (previewNpc != null) { + previewNpc.prevPosX = previewNpc.posX; + previewNpc.prevPosY = previewNpc.posY; + previewNpc.prevPosZ = previewNpc.posZ; + } + for (Entity entity : previewEntities) { + if (entity != null && !entity.isDead) { + entity.prevPosX = entity.posX; + entity.prevPosY = entity.posY; + entity.prevPosZ = entity.posZ; + } + } + } + public void stop() { // Clean up the ability if (previewAbility != null) { @@ -228,10 +250,21 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { * Tick the preview forward. Calls real ability methods. */ public void tick() { - if (!playing || paused || previewAbility == null || previewNpc == null) { + if (!playing || previewAbility == null || previewNpc == null) { return; } + if (paused) { + // Sync prevPos to pos for NPC and all entities to prevent interpolation jitter while paused + syncPrevPositions(); + return; + } + + // Sync NPC's prevPos to current pos BEFORE any updates to prevent jitter when standing still + previewNpc.prevPosX = previewNpc.posX; + previewNpc.prevPosY = previewNpc.posY; + previewNpc.prevPosZ = previewNpc.posZ; + currentTick++; if (previewTelegraph != null) { diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 00de6d405..faaa2c9f5 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -312,7 +312,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti // Re-apply locks after super.onUpdate() to override body rotation smoothing // in EntityLivingBase.onUpdate() (func_110146_f) which runs AFTER onLivingUpdate() // and overwrites renderYawOffset, causing a visible twitch each frame. - abilities.applyLockedRotation(); + abilities.applyRotationControl(); abilities.applyLockedPosition(); if (!isRemote()) { @@ -489,7 +489,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti super.onLivingUpdate(); // Apply locks after super.onLivingUpdate() to override AI/physics changes - abilities.applyLockedRotation(); + abilities.applyRotationControl(); 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 7317856c9..497134b16 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1632,11 +1632,12 @@ ability.cnpc.sweeper=Sweeper ability.weight=Weight ability.windUpTicks=Wind Up ability.cooldownTicks=Cooldown -ability.lockPosition=Lock Position -ability.lockType=Lock Type -ability.lockType.movement=Movement -ability.lockType.rotation=Rotation -ability.lockType.both=Both +ability.lockMovement=Lock Movement +ability.rotationMode=Rotation +ability.rotationPhase=Phase +ability.rotation.free=Free +ability.rotation.locked=Locked +ability.rotation.track=Track ability.interruptible=Interruptible ability.dazedTicks=Dazed Ticks ability.targetingMode=Targeting @@ -1724,6 +1725,10 @@ ability.effects=Effects ability.mode=Mode ability.duration=Duration ability.anchorPoint=Anchor Point +ability.anchor.offsetX=Offset X +ability.anchor.offsetY=Offset Y +ability.anchor.offsetZ=Offset Z +ability.vertical=Vertical ability.rotationSpeed=Rotation Speed # Slam @@ -1864,8 +1869,9 @@ ability.noTypeSettings=No type-specific settings # ═══════════════════════════════════════════════════════════════ ability.hover.weight=Higher weight = more likely to be chosen when multiple abilities available -ability.hover.lockPosition=Prevents NPC from moving or rotating while ability is charging or active -ability.hover.lockType=What to lock: Movement (pathfinding), Rotation (facing direction), or Both +ability.hover.lockMovement=Prevents NPC pathfinding during the selected phases (Windup, Active, or Both) +ability.hover.rotationMode=Free: normal AI rotation. Locked: freeze facing direction. Track: force face target for precise aiming. +ability.hover.rotationPhase=When the rotation mode applies (Windup, Active, or Both) ability.hover.interruptible=If enabled, ability can be interrupted by taking damage ability.hover.dazed=Stun duration in ticks if ability is interrupted (20 ticks = 1 second) ability.hover.windup=Charge-up time before ability activates (20 ticks = 1 second) @@ -1886,6 +1892,7 @@ ability.hover.piercing=Hits multiple targets instead of stopping on first hit ability.hover.lightning=Add electrical effect to visuals ability.hover.outerGlow=Enable outer glow layer around projectile ability.hover.anchorPoint=Where on the NPC the ability spawns from +ability.hover.vertical=Disc flies standing up with thin edge facing target (like a saw blade) ability.hover.dualCharge=Whether or not should create two anchor points ability.hover.damage=Base damage dealt to targets ability.hover.speed=Movement speed of projectile From 74cae17ebd57bfdd9c4f9b1033c168ee979f8def Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Feb 2026 20:25:52 -0500 Subject: [PATCH 08/34] Burst and Dual Mode + Variant Ability Support --- src/api | 2 +- .../controllers/data/ability/Ability.java | 194 ++++- .../data/ability/AbilityController.java | 46 +- .../data/ability/AbilityPhase.java | 3 +- .../data/ability/AbilityVariant.java | 39 + .../data/ability/data/ProjectileData.java | 60 ++ .../data/ability/type/AbilityBeam.java | 630 +++++++++++---- .../data/ability/type/AbilityBeamDual.java | 528 ------------ .../data/ability/type/AbilityDisc.java | 637 +++++++++++---- .../data/ability/type/AbilityDiscDual.java | 551 ------------- .../data/ability/type/AbilityGuard.java | 5 + .../data/ability/type/AbilityHazard.java | 5 + .../data/ability/type/AbilityHeal.java | 5 + .../data/ability/type/AbilityLaserShot.java | 49 +- .../data/ability/type/AbilityOrb.java | 765 +++++++++++------- .../data/ability/type/AbilityOrbBarrage.java | 612 -------------- .../data/ability/type/AbilityOrbDual.java | 600 -------------- .../data/ability/type/AbilityProjectile.java | 5 + .../data/ability/type/AbilitySweeper.java | 5 + .../data/ability/type/AbilityTrap.java | 5 + src/main/java/noppes/npcs/DataAbilities.java | 44 +- .../client/gui/advanced/GuiNPCAbilities.java | 32 +- .../gui/advanced/SubGuiAbilityConfig.java | 4 +- .../advanced/SubGuiAbilityVariantSelect.java | 92 +++ .../client/gui/builder/GuiFieldBuilder.java | 39 +- .../gui/global/GuiNpcManageAbilities.java | 37 +- .../assets/customnpcs/lang/en_US.lang | 43 +- 27 files changed, 2055 insertions(+), 2982 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java diff --git a/src/api b/src/api index fe1cec898..8917e37a6 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit fe1cec89814290a745ac708b309703125b5a3c15 +Subproject commit 8917e37a6d35230a5c7b75ac8f89428edf1aabd2 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 65d941327..49ffbf587 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -32,6 +32,7 @@ import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import noppes.npcs.client.gui.builder.FieldDef; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import net.minecraft.entity.Entity; @@ -101,6 +102,13 @@ public abstract class Ability implements IAbility { // Configurable potion effects protected List effects = new ArrayList<>(); + // Burst + protected boolean burstEnabled = false; + protected int burstAmount = 0; // number of EXTRA repetitions + protected int burstDelay = 0; // ticks between bursts + protected boolean burstReplayAnimations = true; + protected boolean burstOverlap = false; // if true, don't wait for entity death between bursts + // ═══════════════════════════════════════════════════════════════════ // EXECUTION STATE (not saved, reset each combat) // ═══════════════════════════════════════════════════════════════════ @@ -111,6 +119,10 @@ public abstract class Ability implements IAbility { protected transient long executionStartTime; protected transient TelegraphInstance telegraphInstance; + // Burst execution state + protected transient int burstIndex = 0; + protected transient List burstEntities = new ArrayList<>(); + // ═══════════════════════════════════════════════════════════════════ // PREVIEW STATE (client-side only, not saved) // ═══════════════════════════════════════════════════════════════════ @@ -156,6 +168,50 @@ public abstract class Ability implements IAbility { return true; } + /** + * Override to provide variant templates for this ability type. + * When a user creates this ability type, they will be shown a selection dialog + * with the returned variants. If empty or single-entry, the dialog is skipped. + */ + public List getVariants() { + return Collections.emptyList(); + } + + /** + * Whether this ability type supports the burst system. + * Override to return false for abilities that shouldn't repeat (Guard, Heal, Sweeper, etc.). + */ + public boolean allowBurst() { + return true; + } + + /** + * Whether this ability supports overlap mode for burst (entities from previous bursts keep flying). + * Override to return true for entity-spawning projectile abilities (Orb, Beam, Disc, LaserShot). + */ + public boolean allowOverlap() { + return false; + } + + /** + * Whether this ability is ready for auto-completion during burst overlap mode. + * Override to delay auto-completion until all staggered projectiles have been fired. + * @param activeTick the current tick within the ACTIVE phase + */ + public boolean isReadyForBurstCompletion(int activeTick) { + return true; + } + + /** + * Called when burst re-fires without replay animations (burstReplayAnimations=false). + * Entity-spawning abilities should override to spawn and fire entities in one step, + * since onWindUpTick was not called (windup was skipped). + * Default: does nothing (ability goes directly to onExecute with existing state). + */ + public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) { + // Override in entity-spawning abilities (Orb, Beam, Disc, LaserShot) + } + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { } @@ -454,6 +510,25 @@ public abstract class Ability implements IAbility { .range(0, 1000).visibleWhen(this::isInterruptible) ).tab("General")); + // ── Burst section ──────────────────────────────────────────── + if (allowBurst()) { + defs.add(FieldDef.section("ability.section.burst").tab("General")); + defs.add(FieldDef.boolField("ability.burstEnabled", this::isBurstEnabled, this::setBurstEnabled) + .tab("General")); + defs.add(FieldDef.row( + FieldDef.intField("ability.burstAmount", this::getBurstAmount, this::setBurstAmount).range(1, 100), + FieldDef.intField("ability.burstDelay", this::getBurstDelay, this::setBurstDelay).range(0, 1000) + ).tab("General").visibleWhen(this::isBurstEnabled)); + defs.add(FieldDef.boolField("ability.burstReplayAnimations", this::isBurstReplayAnimations, this::setBurstReplayAnimations) + .tab("General").visibleWhen(this::isBurstEnabled) + .hover("ability.hover.burstReplay")); + if (allowOverlap()) { + defs.add(FieldDef.boolField("ability.burstOverlap", this::isBurstOverlap, this::setBurstOverlap) + .tab("General").visibleWhen(this::isBurstEnabled) + .hover("ability.hover.burstOverlap")); + } + } + // ── Target tab ─────────────────────────────────────────────── defs.add(FieldDef.row( FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)).range(0, 100), @@ -722,6 +797,8 @@ public abstract class Ability implements IAbility { this.currentTick = 0; this.currentTarget = target; this.executionStartTime = System.currentTimeMillis(); + this.burstIndex = 0; + this.burstEntities.clear(); if (windUpTicks <= 0) { this.phase = AbilityPhase.ACTIVE; @@ -764,6 +841,19 @@ public abstract class Ability implements IAbility { return true; } break; + + case BURST_DELAY: + // Waiting between burst repetitions - free movement/rotation + if (currentTick >= burstDelay) { + if (burstReplayAnimations && getWindUpTicks() > 0) { + phase = AbilityPhase.WINDUP; + } else { + phase = AbilityPhase.ACTIVE; + } + currentTick = 0; + return true; + } + break; } return false; } @@ -777,6 +867,19 @@ public abstract class Ability implements IAbility { */ public boolean signalCompletion() { if (phase == AbilityPhase.ACTIVE) { + // Check if more burst iterations remain + if (burstEnabled && burstAmount > 0 && burstIndex < burstAmount) { + if (!burstOverlap) { + cleanup(); + } + burstIndex++; + phase = AbilityPhase.BURST_DELAY; + currentTick = 0; + return true; + } + + // Final completion - let burst entities die naturally (don't force-kill) + burstEntities.clear(); cleanup(); phase = AbilityPhase.IDLE; currentTick = 0; @@ -790,9 +893,10 @@ public abstract class Ability implements IAbility { * Called when NPC takes direct damage during WINDUP. */ public void interrupt() { + cleanupBurstEntities(); cleanup(); - if (interruptible && phase == AbilityPhase.WINDUP) { - // Interrupted during windup - go to dazed state + if (interruptible && (phase == AbilityPhase.WINDUP || phase == AbilityPhase.BURST_DELAY)) { + // Interrupted during windup or burst delay - go to dazed state phase = AbilityPhase.DAZED; currentTick = 0; } else { @@ -800,6 +904,7 @@ public abstract class Ability implements IAbility { phase = AbilityPhase.IDLE; currentTick = 0; } + burstIndex = 0; currentTarget = null; telegraphInstance = null; } @@ -820,7 +925,7 @@ public abstract class Ability implements IAbility { * @return true if the ability should be interrupted */ public boolean canInterrupt(DamageSource source) { - if (!interruptible || phase != AbilityPhase.WINDUP) { + if (!interruptible || (phase != AbilityPhase.WINDUP && phase != AbilityPhase.BURST_DELAY)) { return false; } @@ -847,9 +952,11 @@ public abstract class Ability implements IAbility { * Reset all execution state (called on combat end, NPC death, target lost, etc.) */ public void reset() { + cleanupBurstEntities(); cleanup(); phase = AbilityPhase.IDLE; currentTick = 0; + burstIndex = 0; currentTarget = null; telegraphInstance = null; previewMode = false; @@ -865,6 +972,18 @@ public abstract class Ability implements IAbility { // Override in subclasses to clean up spawned entities, etc. } + /** + * Kill all entities tracked during burst overlap and clear the list. + */ + protected void cleanupBurstEntities() { + for (Entity e : burstEntities) { + if (e != null && !e.isDead) { + killAbilityEntity(e); + } + } + burstEntities.clear(); + } + // ═══════════════════════════════════════════════════════════════════ // PREVIEW MODE SUPPORT // ═══════════════════════════════════════════════════════════════════ @@ -896,6 +1015,9 @@ public abstract class Ability implements IAbility { } else { world.spawnEntityInWorld(entity); } + if (burstEnabled && burstOverlap) { + burstEntities.add(entity); + } } /** @@ -979,6 +1101,13 @@ public abstract class Ability implements IAbility { nbt.setInteger("allowedBy", allowedBy.ordinal()); nbt.setBoolean("ignoreCooldown", ignoreCooldown); + // Burst + nbt.setBoolean("burstEnabled", burstEnabled); + nbt.setInteger("burstAmount", burstAmount); + nbt.setInteger("burstDelay", burstDelay); + nbt.setBoolean("burstReplayAnimations", burstReplayAnimations); + nbt.setBoolean("burstOverlap", burstOverlap); + // Conditions NBTTagList condList = new NBTTagList(); for (Condition c : conditions) condList.appendTag(c.writeNBT()); @@ -1059,6 +1188,13 @@ public abstract class Ability implements IAbility { allowedBy = nbt.hasKey("allowedBy") ? UserType.fromOrdinal(nbt.getInteger("allowedBy")) : UserType.BOTH; ignoreCooldown = nbt.hasKey("ignoreCooldown") && nbt.getBoolean("ignoreCooldown"); + // Burst + burstEnabled = nbt.hasKey("burstEnabled") && nbt.getBoolean("burstEnabled"); + burstAmount = nbt.hasKey("burstAmount") ? nbt.getInteger("burstAmount") : 0; + burstDelay = nbt.hasKey("burstDelay") ? nbt.getInteger("burstDelay") : 0; + burstReplayAnimations = !nbt.hasKey("burstReplayAnimations") || nbt.getBoolean("burstReplayAnimations"); + burstOverlap = nbt.hasKey("burstOverlap") && nbt.getBoolean("burstOverlap"); + // Conditions conditions.clear(); NBTTagList condList = nbt.getTagList("conditions", 10); @@ -1313,6 +1449,8 @@ public abstract class Ability implements IAbility { return isMovementLockedDuringWindup(); case ACTIVE: return isMovementLockedDuringActive(); + case BURST_DELAY: + return false; // Free movement during burst delay default: return false; } @@ -1327,6 +1465,8 @@ public abstract class Ability implements IAbility { return isRotationLockedDuringWindup(); case ACTIVE: return isRotationLockedDuringActive(); + case BURST_DELAY: + return false; // Free rotation during burst delay default: return false; } @@ -1342,6 +1482,8 @@ public abstract class Ability implements IAbility { return rotationPhase.locksWindup(); case ACTIVE: return rotationPhase.locksActive(); + case BURST_DELAY: + return false; // Free rotation during burst delay default: return false; } @@ -1530,6 +1672,52 @@ public abstract class Ability implements IAbility { effects.clear(); } + // Burst getters/setters + + public boolean isBurstEnabled() { + return burstEnabled; + } + + public void setBurstEnabled(boolean burstEnabled) { + this.burstEnabled = burstEnabled; + } + + public int getBurstAmount() { + return burstAmount; + } + + public void setBurstAmount(int burstAmount) { + this.burstAmount = Math.max(0, burstAmount); + } + + public int getBurstDelay() { + return burstDelay; + } + + public void setBurstDelay(int burstDelay) { + this.burstDelay = Math.max(0, burstDelay); + } + + public boolean isBurstReplayAnimations() { + return burstReplayAnimations; + } + + public void setBurstReplayAnimations(boolean burstReplayAnimations) { + this.burstReplayAnimations = burstReplayAnimations; + } + + public boolean isBurstOverlap() { + return burstOverlap; + } + + public void setBurstOverlap(boolean burstOverlap) { + this.burstOverlap = burstOverlap; + } + + public int getBurstIndex() { + return burstIndex; + } + /** * Get display keys for rotation phase dropdown (excludes "None" since mode=FREE handles that). */ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 05110f553..294f9a765 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -45,6 +45,9 @@ public class AbilityController implements IAbilityHandler { /** External field providers for ability GUI (e.g., DBC Addon). Client-side only. */ private final List fieldProviders = new ArrayList<>(); + /** External variant definitions registered by addons, keyed by typeId. */ + private final Map> externalVariants = new LinkedHashMap<>(); + public AbilityController() { registerBuiltinTypes(); } @@ -432,13 +435,9 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:sweeper", AbilitySweeper::new); registerType("cnpc:projectile", AbilityProjectile::new); registerType("cnpc:orb", AbilityOrb::new); - registerType("cnpc:dual_orb", AbilityOrbDual::new); - registerType("cnpc:orb_barrage", AbilityOrbBarrage::new); - registerType("cnpc:dual_disc", AbilityDisc::new); - registerType("cnpc:dual_disc", AbilityDiscDual::new); + registerType("cnpc:disc", AbilityDisc::new); registerType("cnpc:laser_shot", AbilityLaserShot::new); registerType("cnpc:beam", AbilityBeam::new); - registerType("cnpc:dual_beam", AbilityBeamDual::new); // Movement abilities registerType("cnpc:charge", AbilityCharge::new); @@ -490,4 +489,41 @@ public class AbilityController implements IAbilityHandler { public List getFieldProviders() { return fieldProviders; } + + // ═══════════════════════════════════════════════════════════════════════════ + // EXTERNAL VARIANT REGISTRY + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Register an external variant for an ability type. + * External mods can call this to add variant presets that appear + * alongside built-in variants in the variant selection SubGui. + * + * @param typeId The ability typeId (e.g., "ability.cnpc.beam") + * @param variant The variant to register + */ + public void registerVariant(String typeId, AbilityVariant variant) { + externalVariants.computeIfAbsent(typeId, k -> new ArrayList<>()).add(variant); + } + + /** + * Get all variants for a given ability type (built-in + external). + * Returns the combined list of variants from the ability's getVariants() + * and any externally registered variants. + * + * @param typeId The ability typeId + * @return Combined list of variants, empty if none + */ + public List getVariantsForType(String typeId) { + List result = new ArrayList<>(); + Ability temp = create(typeId); + if (temp != null) { + result.addAll(temp.getVariants()); + } + List ext = externalVariants.get(typeId); + if (ext != null) { + result.addAll(ext); + } + return result; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java index 0611a0274..af1bd8d30 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java @@ -4,5 +4,6 @@ public enum AbilityPhase { IDLE, // Not executing WINDUP, // Charging up (telegraph visible) ACTIVE, // Executing (damage happens) - DAZED // Stunned after interrupt during WINDUP (cannot attack) + DAZED, // Stunned after interrupt during WINDUP (cannot attack) + BURST_DELAY // Waiting between burst repetitions (free movement/rotation) } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java new file mode 100644 index 000000000..91d151a85 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java @@ -0,0 +1,39 @@ +package kamkeel.npcs.controllers.data.ability; + +import java.util.function.Consumer; + +/** + * Represents a variant/template for an ability type. + * When a user creates an ability that has registered variants, + * they are presented with a selection dialog before the ability is created. + * + * Each variant has a display name (lang key) and a configurator that + * applies preset values to the newly created ability. + */ +public class AbilityVariant { + + private final String displayKey; + private final Consumer configurator; + + /** + * @param displayKey Lang key for the variant display name (e.g., "ability.variant.dual") + * @param configurator Lambda that configures the ability with preset values + */ + public AbilityVariant(String displayKey, Consumer configurator) { + this.displayKey = displayKey; + this.configurator = configurator; + } + + public String getDisplayKey() { + return displayKey; + } + + /** + * Apply this variant's preset configuration to the given ability. + */ + public void apply(Ability ability) { + if (configurator != null) { + configurator.accept(ability); + } + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java new file mode 100644 index 000000000..49ee50f7e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ProjectileData.java @@ -0,0 +1,60 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import net.minecraft.nbt.NBTTagCompound; + +/** + * Per-projectile data: anchor point + optional color override. + * All projectiles share the primary EnergyDisplayData; this class + * allows individual projectiles to override just the inner/outer colors. + */ +public class ProjectileData { + public EnergyAnchorData anchor; + public boolean colorOverride = false; + public int innerColor = 0xFFFFFF; + public int outerColor = 0x8888FF; + + public ProjectileData(AnchorPoint defaultAnchor) { + this.anchor = new EnergyAnchorData(defaultAnchor); + } + + /** + * Returns the display data to use for this projectile. + * If colorOverride is false, returns the primary display data directly. + * If colorOverride is true, returns a copy with overridden inner/outer colors. + */ + public EnergyDisplayData resolveDisplay(EnergyDisplayData primary) { + if (!colorOverride) return primary; + EnergyDisplayData resolved = primary.copy(); + resolved.innerColor = this.innerColor; + resolved.outerColor = this.outerColor; + return resolved; + } + + public void writeNBT(NBTTagCompound nbt) { + anchor.writeNBT(nbt); + nbt.setBoolean("colorOverride", colorOverride); + if (colorOverride) { + nbt.setInteger("overrideInnerColor", innerColor); + nbt.setInteger("overrideOuterColor", outerColor); + } + } + + public void readNBT(NBTTagCompound nbt) { + anchor.readNBT(nbt); + colorOverride = nbt.getBoolean("colorOverride"); + if (colorOverride) { + innerColor = nbt.hasKey("overrideInnerColor") ? nbt.getInteger("overrideInnerColor") : 0xFFFFFF; + outerColor = nbt.hasKey("overrideOuterColor") ? nbt.getInteger("overrideOuterColor") : 0x8888FF; + } + } + + public ProjectileData copy() { + ProjectileData copy = new ProjectileData(anchor.anchorPoint); + copy.anchor = anchor.copy(); + copy.colorOverride = colorOverride; + copy.innerColor = innerColor; + copy.outerColor = outerColor; + return copy; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 356c1c4e9..5f12cc183 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -13,7 +13,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityBeam; import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; @@ -24,30 +23,39 @@ import noppes.npcs.api.ability.type.IAbilityEnergyBeam; import java.util.Arrays; import java.util.List; -import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; /** * Energy Beam ability: A homing head with a trailing path. - * The beam stays attached to the origin and curves as it homes in on target. - * Cannot cross over itself (self-intersection prevention). + * Supports 1-8 projectiles with shared visuals and per-projectile anchor points. */ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { - // Shape properties (standalone) + private static final int MAX_PROJECTILES = 8; + + // Beam shape properties (shared) private float beamWidth = 0.4f; private float headSize = 0.6f; - // Data classes - private final EnergyDisplayData colorData = new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f); + // Projectile count and fire stagger + private int projectileCount = 1; + private int fireDelay = 0; + + // Shared visual data (all projectiles use these) + private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f); + private EnergyLightningData lightningData = new EnergyLightningData(); + private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData(); + + // Shared combat/movement data private final EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f); private final EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f); - private final EnergyLightningData lightningData = new EnergyLightningData(); private final EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200); - private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); - private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData(); - // Transient state for beam entity (used during windup charging) - private transient EntityAbilityBeam beamEntity = null; + // Per-projectile data (anchor + optional color override) + private ProjectileData[] projectiles; + + // Transient entity state + private transient EntityAbilityBeam[] beamEntities; public AbilityBeam() { this.typeId = "ability.cnpc.beam"; @@ -62,9 +70,88 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { this.rotationPhase = 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"; + initProjectiles(1); + } + + private void initProjectiles(int count) { + this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES)); + this.projectiles = new ProjectileData[projectileCount]; + for (int i = 0; i < projectileCount; i++) { + projectiles[i] = new ProjectileData(getDefaultAnchor(i)); + } + } + + private AnchorPoint getDefaultAnchor(int index) { + switch (index) { + case 0: return AnchorPoint.RIGHT_HAND; + case 1: return AnchorPoint.LEFT_HAND; + default: return AnchorPoint.FRONT; + } + } + + public void setProjectileCount(int count) { + int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES)); + if (newCount == projectileCount) return; + + ProjectileData[] oldProjectiles = projectiles; + initProjectiles(newCount); + + if (oldProjectiles != null) { + int copyCount = Math.min(oldProjectiles.length, newCount); + for (int i = 0; i < copyCount; i++) { + if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i]; + } + } + } + + public int getProjectileCount() { + return projectileCount; + } + + public int getFireDelay() { + return fireDelay; + } + + public void setFireDelay(int delay) { + this.fireDelay = Math.max(0, delay); + } + + // Legacy compat + public int getDualFireDelay() { return fireDelay; } + public void setDualFireDelay(int delay) { setFireDelay(delay); } + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.single", a -> { + a.setName("Beam"); + }), + new AbilityVariant("ability.variant.dual", a -> { + AbilityBeam beam = (AbilityBeam) a; + a.setName("Dual Beam"); + beam.setProjectileCount(2); + beam.setFireDelay(0); + // Set color override for second beam + beam.projectiles[1].colorOverride = true; + beam.projectiles[1].innerColor = 0xFFFFFF; + beam.projectiles[1].outerColor = 0xFF0000; + a.setWindUpTicks(80); + a.setWindUpAnimationName("Ability_BeamDual_Windup"); + a.setActiveAnimationName("Ability_BeamDual_Active"); + }) + ); + } + + @Override + public boolean allowOverlap() { + return true; + } + + @Override + public boolean isReadyForBurstCompletion(int activeTick) { + return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1); } @Override @@ -77,20 +164,34 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } - @Override - public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote && !isPreview()) { - signalCompletion(); - return; + private EntityAbilityBeam[] createBeamEntities(EntityLivingBase caster, EntityLivingBase target, World world) { + EntityAbilityBeam[] entities = new EntityAbilityBeam[projectileCount]; + float offsetDist = 1.0f; + for (int i = 0; i < projectileCount; i++) { + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor, offsetDist); + EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); + entities[i] = new EntityAbilityBeam( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + beamWidth, headSize, + resolved, combatData, homingData, lightningData, lifespanData, trajectoryData, + lockMovement.locksActive()); + entities[i].setEffects(this.effects); + entities[i].setSourceAbility(this); } + if (projectileCount == 2 && entities[0] != null && entities[1] != null) { + entities[0].setSiblingEntityId(entities[1].getEntityId()); + entities[1].setSiblingEntityId(entities[0].getEntityId()); + } + return entities; + } - // Start firing the beam that was spawned during windup - if (beamEntity != null && !beamEntity.isDead) { - if (isPreview()) { - beamEntity.startPreviewFiring(); - } else { - beamEntity.startFiring(target); - } + private void fireBeamEntity(EntityAbilityBeam beam, EntityLivingBase target) { + if (beam == null || beam.isDead) return; + if (isPreview()) { + beam.startPreviewFiring(); + } else { + beam.startFiring(target); } } @@ -98,35 +199,73 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote && !isPreview()) return; - // Spawn beam in charging mode on first tick of windup if (tick == 1) { + beamEntities = createBeamEntities(caster, target, world); float offsetDist = 1.0f; - - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist); - beamEntity = new EntityAbilityBeam( - world, caster, target, - spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, - beamWidth, headSize, - colorData, combatData, homingData, lightningData, lifespanData, trajectoryData, - lockMovement.locksActive()); - - if (isPreview()) { - beamEntity.setupPreview(caster, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, offsetDist); - } else { - beamEntity.setupCharging(anchorData, windUpTicks, offsetDist); + for (int i = 0; i < projectileCount; i++) { + EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); + if (isPreview()) { + beamEntities[i].setupPreview(caster, beamWidth, headSize, resolved, lightningData, projectiles[i].anchor, windUpTicks, offsetDist); + } else { + beamEntities[i].setupCharging(projectiles[i].anchor, windUpTicks, offsetDist); + } + spawnAbilityEntity(world, beamEntities[i]); } + } + } - beamEntity.setEffects(this.effects); - beamEntity.setSourceAbility(this); - spawnAbilityEntity(world, beamEntity); + @Override + public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) { + beamEntities = createBeamEntities(caster, target, world); + for (EntityAbilityBeam beam : beamEntities) { + spawnAbilityEntity(world, beam); + } + for (EntityAbilityBeam beam : beamEntities) { + fireBeamEntity(beam, target); + } + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote && !isPreview()) { + signalCompletion(); + return; + } + + if (beamEntities == null) return; + + fireBeamEntity(beamEntities[0], target); + + if (fireDelay <= 0) { + for (int i = 1; i < projectileCount; i++) { + fireBeamEntity(beamEntities[i], target); + } } } @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - // Signal completion when entity dies - if (beamEntity == null || beamEntity.isDead) { - beamEntity = null; + if (beamEntities == null) { + signalCompletion(); + return; + } + + if (fireDelay > 0) { + for (int i = 1; i < projectileCount; i++) { + if (tick == fireDelay * i) { + fireBeamEntity(beamEntities[i], target); + } + } + } + + boolean allDead = true; + for (EntityAbilityBeam beam : beamEntities) { + if (beam != null && !beam.isDead) { + allDead = false; + break; + } + } + if (allDead) { signalCompletion(); } } @@ -142,11 +281,15 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { @Override public void cleanup() { - // Despawn beam entity if still alive - if (beamEntity != null && !beamEntity.isDead) { - beamEntity.setDead(); + if (beamEntities != null) { + for (int i = 0; i < beamEntities.length; i++) { + if (beamEntities[i] != null && !beamEntities[i].isDead) { + beamEntities[i].setDead(); + } + beamEntities[i] = null; + } } - beamEntity = null; + beamEntities = null; } @Override @@ -155,7 +298,6 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { return null; } - // Create circle telegraph at target position Telegraph telegraph = Telegraph.circle(headSize * 2.0f); telegraph.setDurationTicks(windUpTicks); telegraph.setColor(windUpColor); @@ -163,7 +305,6 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - // Position at target and follow target during windup TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); instance.setCasterEntityId(caster.getEntityId()); instance.setEntityIdToFollow(target.getEntityId()); @@ -180,172 +321,307 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("beamWidth", beamWidth); nbt.setFloat("headSize", headSize); - anchorData.writeNBT(nbt); - colorData.writeNBT(nbt); + nbt.setInteger("projectileCount", projectileCount); + nbt.setInteger("fireDelay", fireDelay); + + // Shared visual data + displayData.writeNBT(nbt); + lightningData.writeNBT(nbt); + + // Shared combat/movement data combatData.writeNBT(nbt); homingData.writeNBT(nbt); - lightningData.writeNBT(nbt); lifespanData.writeNBT(nbt); + + // Per-projectile data + for (int i = 0; i < projectileCount; i++) { + NBTTagCompound projNbt = new NBTTagCompound(); + projectiles[i].writeNBT(projNbt); + nbt.setTag("Projectile_" + i, projNbt); + } } @Override public void readTypeNBT(NBTTagCompound nbt) { this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; - anchorData.readNBT(nbt); - colorData.readNBT(nbt); + + int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1; + initProjectiles(count); + + if (nbt.hasKey("fireDelay")) { + this.fireDelay = nbt.getInteger("fireDelay"); + } else if (nbt.hasKey("dualFireDelay")) { + this.fireDelay = nbt.getInteger("dualFireDelay"); + } else { + this.fireDelay = 0; + } + combatData.readNBT(nbt); homingData.readNBT(nbt); - lightningData.readNBT(nbt); lifespanData.readNBT(nbt); + + if (nbt.hasKey("Projectile_0")) { + NBTTagCompound proj0 = nbt.getCompoundTag("Projectile_0"); + if (proj0.hasKey("colorOverride")) { + // NEW format + displayData.readNBT(nbt); + lightningData.readNBT(nbt); + for (int i = 0; i < projectileCount; i++) { + if (nbt.hasKey("Projectile_" + i)) { + projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i)); + } + } + } else { + // OLD per-projectile format + displayData.readNBT(proj0); + lightningData.readNBT(proj0); + projectiles[0].anchor.readNBT(proj0); + + for (int i = 1; i < projectileCount; i++) { + if (nbt.hasKey("Projectile_" + i)) { + NBTTagCompound projNbt = nbt.getCompoundTag("Projectile_" + i); + projectiles[i].anchor.readNBT(projNbt); + + EnergyDisplayData tempDisplay = new EnergyDisplayData(); + tempDisplay.readNBT(projNbt); + if (tempDisplay.innerColor != displayData.innerColor || tempDisplay.outerColor != displayData.outerColor) { + projectiles[i].colorOverride = true; + projectiles[i].innerColor = tempDisplay.innerColor; + projectiles[i].outerColor = tempDisplay.outerColor; + } + } + } + } + } else { + // LEGACY flat format + displayData.readNBT(nbt); + lightningData.readNBT(nbt); + projectiles[0].anchor.readNBT(nbt); + } } - // Getters & Setters - Standalone fields + // ═══════════════════════════════════════════════════════════════════ + // API GETTERS & SETTERS + // ═══════════════════════════════════════════════════════════════════ + public float getBeamWidth() { return beamWidth; } public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public float getHeadSize() { return headSize; } public void setHeadSize(float headSize) { this.headSize = headSize; } - // Getters & Setters - Color data - public int getInnerColor() { return colorData.innerColor; } - public void setInnerColor(int innerColor) { this.colorData.innerColor = innerColor; } - public int getOuterColor() { return colorData.outerColor; } - public void setOuterColor(int outerColor) { this.colorData.outerColor = outerColor; } - public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } - public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; } - public float getOuterColorWidth() { return colorData.outerColorWidth; } - public void setOuterColorWidth(float outerColorWidth) { this.colorData.outerColorWidth = outerColorWidth; } - public float getOuterColorAlpha() { return colorData.outerColorAlpha; } - public void setOuterColorAlpha(float outerColorAlpha) { this.colorData.outerColorAlpha = outerColorAlpha; } - public float getRotationSpeed() { return colorData.rotationSpeed; } - public void setRotationSpeed(float rotationSpeed) { this.colorData.rotationSpeed = rotationSpeed; } - - // Getters & Setters - Combat data - public float getDamage() { return combatData.damage; } - public void setDamage(float damage) { this.combatData.damage = damage; } - public float getKnockback() { return combatData.knockback; } - public void setKnockback(float knockback) { this.combatData.knockback = knockback; } - public float getKnockbackUp() { return combatData.knockbackUp; } - public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } - public boolean isExplosive() { return combatData.explosive; } - public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } - public float getExplosionRadius() { return combatData.explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } - public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } - - // Getters & Setters - Homing data public float getSpeed() { return homingData.speed; } public void setSpeed(float speed) { this.homingData.speed = speed; } - public boolean isHoming() { return homingData.homing; } - public void setHoming(boolean homing) { this.homingData.homing = homing; } - public float getHomingStrength() { return homingData.homingStrength; } - public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; } - public float getHomingRange() { return homingData.homingRange; } - public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; } - // Getters & Setters - Lightning data - public boolean hasLightningEffect() { return lightningData.lightningEffect; } - public void setLightningEffect(boolean lightningEffect) { this.lightningData.lightningEffect = lightningEffect; } - public float getLightningDensity() { return lightningData.lightningDensity; } - public void setLightningDensity(float lightningDensity) { this.lightningData.lightningDensity = lightningDensity; } - public float getLightningRadius() { return lightningData.lightningRadius; } - public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; } - - // Getters & Setters - Lifespan data public float getMaxDistance() { return lifespanData.maxDistance; } public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } - // Getters & Setters - Anchor point - public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } - public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } - public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } - public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } - public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } - public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } - public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } - public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { this.combatData.damage = damage; } + + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { this.combatData.knockback = knockback; } + + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } + + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { this.homingData.homing = homing; } + + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; } + + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; } + + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } + + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } + + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } + + // Shared display data (primary colors) + public int getInnerColor() { return displayData.innerColor; } + public void setInnerColor(int innerColor) { displayData.innerColor = innerColor; } + + public int getOuterColor() { return displayData.outerColor; } + public void setOuterColor(int outerColor) { displayData.outerColor = outerColor; } + + public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { displayData.outerColorEnabled = outerColorEnabled; } + + public float getOuterColorWidth() { return displayData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { displayData.outerColorWidth = outerColorWidth; } + + public float getOuterColorAlpha() { return displayData.outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { displayData.outerColorAlpha = outerColorAlpha; } + + public float getRotationSpeed() { return displayData.rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { displayData.rotationSpeed = rotationSpeed; } + + // Shared lightning data + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { lightningData.lightningEffect = lightningEffect; } + + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; } + + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; } + + public int getLightningFadeTime() { return lightningData.lightningFadeTime; } + public void setLightningFadeTime(int lightningFadeTime) { lightningData.lightningFadeTime = lightningFadeTime; } + + // Anchor - default to projectile 0 + public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { projectiles[0].anchor.anchorPoint = anchorPoint; } + + public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; } + public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; } + + public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; } + public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; } + + public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; } + public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; } @Override - public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); } + public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); } @Override - public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); } + + // Indexed API methods + public int getInnerColor(int index) { + ProjectileData p = projectiles[clampIndex(index)]; + return p.colorOverride ? p.innerColor : displayData.innerColor; + } + public void setInnerColor(int index, int color) { + ProjectileData p = projectiles[clampIndex(index)]; + p.colorOverride = true; + p.innerColor = color; + } + + public int getOuterColor(int index) { + ProjectileData p = projectiles[clampIndex(index)]; + return p.colorOverride ? p.outerColor : displayData.outerColor; + } + public void setOuterColor(int index, int color) { + ProjectileData p = projectiles[clampIndex(index)]; + p.colorOverride = true; + p.outerColor = color; + } + + public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); } + public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); } + + private int clampIndex(int index) { + return Math.max(0, Math.min(index, projectileCount - 1)); + } @Override public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } + // ═══════════════════════════════════════════════════════════════════ + // GUI FIELD DEFINITIONS + // ═══════════════════════════════════════════════════════════════════ + @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { - defs.addAll(Arrays.asList( - // Type tab - FieldDef.row( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed) - ), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.section("ability.section.beam"), - FieldDef.row( - FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth), - FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize) - ), - FieldDef.row( - FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) - ), - FieldDef.section("ability.section.homing"), - FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) - .hover("ability.hover.homing"), - FieldDef.row( - FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) - .visibleWhen(this::isHoming), - FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange) - .visibleWhen(this::isHoming) - ), - FieldDef.section("ability.section.explosive"), - FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) - .hover("ability.hover.explosive"), - FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) - .visibleWhen(this::isExplosive), - AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), - // Visual tab - FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), - FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) - .tab("ability.tab.visual"), - FieldDef.section("ability.section.colors").tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) - .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), - FieldDef.row( - FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) - .visibleWhen(this::isOuterColorEnabled), - FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) - .range(0, 1).visibleWhen(this::isOuterColorEnabled) - ).tab("ability.tab.visual"), - FieldDef.section("ability.section.effects").tab("ability.tab.visual"), - FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) - .range(0.01f, 100f).visibleWhen(this::hasLightningEffect), - FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) - .range(0.1f, 100f).visibleWhen(this::hasLightningEffect) - ).tab("ability.tab.visual") + defs.add(FieldDef.row( + FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES), + FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay) + .range(0, 200).visibleWhen(() -> projectileCount > 1) )); + + defs.add(FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed) + )); + defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)); + defs.add(FieldDef.section("ability.section.beam")); + defs.add(FieldDef.row( + FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth), + FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize) + )); + defs.add(FieldDef.row( + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) + )); + defs.add(FieldDef.section("ability.section.homing")); + defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange).visibleWhen(this::isHoming) + )); + defs.add(FieldDef.section("ability.section.explosive")); + defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive")); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive)); + defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); + + // Visual tab - shared primary colors + defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual")); + defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab("ability.tab.visual")); + defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab("ability.tab.visual")); + defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .range(0, 1).visibleWhen(this::isOuterColorEnabled) + ).tab("ability.tab.visual")); + + // Visual tab - shared effects + defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual")); + defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .tab("ability.tab.visual")); + defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab("ability.tab.visual")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .visibleWhen(this::hasLightningEffect).range(0.01f, 100f), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .range(0.1f, 100f).visibleWhen(this::hasLightningEffect) + ).tab("ability.tab.visual")); + + // Visual tab - per-projectile sections + for (int i = 0; i < MAX_PROJECTILES; i++) { + final int idx = i; + String sectionKey = "ability.section.projectile" + (i + 1); + + defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual") + .visibleWhen(() -> idx < projectileCount)); + + defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v), + FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v) + ).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + + // Color override (only when multiple projectiles) + defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1)); + defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride)); + defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride)); + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java deleted file mode 100644 index daf04a446..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java +++ /dev/null @@ -1,528 +0,0 @@ -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.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.LockMovementType; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.data.*; -import noppes.npcs.client.gui.builder.FieldDef; -import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; -import kamkeel.npcs.controllers.data.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; -import kamkeel.npcs.entity.EntityAbilityBeam; -import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityLivingBase; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.Vec3; -import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.util.ValueUtil; - -import java.util.Arrays; -import java.util.List; - -public class AbilityBeamDual extends Ability { - private float beamWidth = 0.4f; - private float headSize = 0.6f; - private boolean dualFire= true; - private int dualFireDelay = 0; - - // Data classes - private final EnergyDisplayData[] displayData = new EnergyDisplayData[]{ - new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f), - new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 6.0f), - }; - private final EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f); - private final EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f); - private final EnergyLightningData[] lightningData = new EnergyLightningData[]{ - new EnergyLightningData(), new EnergyLightningData() - }; - private final EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200); - private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ - new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) - }; - private final EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{ - new EnergyTrajectoryData(), new EnergyTrajectoryData() - }; - - // Transient state for beam entity (used during windup charging) - private transient EntityAbilityBeam beamEntity = null; - private transient EntityAbilityBeam beamEntity2 = null; - - public AbilityBeamDual() { - this.typeId = "ability.cnpc.beam_dual"; - this.name = "Beam Dual"; - this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 20.0f; - this.minRange = 5.0f; - this.cooldownTicks = 0; - this.windUpTicks = 80; - this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; - this.telegraphType = TelegraphType.CIRCLE; - this.showTelegraph = true; - // Default built-in animations - this.windUpAnimationName = "Ability_BeamDual_Windup"; - this.activeAnimationName = "Ability_BeamDual_Active"; - } - - @Override - public boolean isTargetingModeLocked() { - return true; - } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; - } - - @Override - public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote && !isPreview()) { - signalCompletion(); - return; - } - - // Start firing the beam that was spawned during windup - if (beamEntity != null && !beamEntity.isDead) { - if (isPreview()) { - beamEntity.startPreviewFiring(); - } else { - beamEntity.startFiring(target); - } - } - - if (dualFire) { - if (beamEntity2 != null && !beamEntity2.isDead && dualFireDelay <= 0) { - if (isPreview()) { - beamEntity2.startPreviewFiring(); - } else { - beamEntity2.startFiring(target); - } - } - } else if (beamEntity2 != null) { - beamEntity2.setDead(); - } - } - - @Override - public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote && !isPreview()) return; - - // Spawn beam in charging mode on first tick of windup - if (tick == 1) { - float offsetDist = 1.0f; - - Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0], offsetDist); - beamEntity = new EntityAbilityBeam( - world, caster, target, - spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, - beamWidth, headSize, - displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0], - lockMovement.locksActive()); - - Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1], offsetDist); - beamEntity2 = new EntityAbilityBeam( - world, caster, target, - spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, - beamWidth, headSize, - displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1], - lockMovement.locksActive()); - - beamEntity.setSiblingEntityId(beamEntity2.getEntityId()); - beamEntity2.setSiblingEntityId(beamEntity.getEntityId()); - - if (isPreview()) { - beamEntity.setupPreview(caster, beamWidth, headSize, displayData[0], lightningData[0], anchorData[0], windUpTicks, offsetDist); - beamEntity2.setupPreview(caster, beamWidth, headSize, displayData[1], lightningData[1], anchorData[1], windUpTicks, offsetDist); - } else { - beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist); - beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist); - } - - beamEntity.setEffects(this.effects); - if (dualFire) beamEntity2.setEffects(this.effects); - - beamEntity.setSourceAbility(this); - beamEntity2.setSourceAbility(this); - spawnAbilityEntity(world, beamEntity); - spawnAbilityEntity(world, beamEntity2); - } - } - - @Override - public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - // Signal completion when entity dies - if (dualFire) { - if ((beamEntity == null || beamEntity.isDead) && (beamEntity2 == null || beamEntity2.isDead)) { - beamEntity = null; - beamEntity2 = null; - signalCompletion(); - } - - if (tick == dualFireDelay && beamEntity2 != null && !beamEntity2.isDead) { - beamEntity2.startFiring(target); - } - } else { - if (beamEntity == null || beamEntity.isDead) { - beamEntity = null; - signalCompletion(); - } - } - } - - @Override - public void onComplete(EntityLivingBase caster, EntityLivingBase target) { - } - - @Override - public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { - cleanup(); - } - - @Override - public void cleanup() { - // Despawn beam entity if still alive - if (beamEntity != null && !beamEntity.isDead) { - beamEntity.setDead(); - } - - if (beamEntity2 != null && !beamEntity2.isDead) { - beamEntity2.setDead(); - } - - beamEntity = null; - beamEntity2 = null; - } - - @Override - public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { - return null; - } - - // Create circle telegraph at target position - Telegraph telegraph = Telegraph.circle(headSize * 2.0f); - telegraph.setDurationTicks(windUpTicks); - telegraph.setColor(windUpColor); - telegraph.setWarningColor(activeColor); - telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); - telegraph.setHeightOffset(telegraphHeightOffset); - - // Position at target and follow target during windup - TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); - instance.setCasterEntityId(caster.getEntityId()); - instance.setEntityIdToFollow(target.getEntityId()); - - return instance; - } - - @Override - public float getTelegraphRadius() { - return headSize * 2.0f; - } - - @Override - public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("beamWidth", beamWidth); - nbt.setFloat("headSize", headSize); - nbt.setBoolean("dualFire", dualFire); - nbt.setInteger("dualFireDelay", dualFireDelay); - combatData.writeNBT(nbt); - homingData.writeNBT(nbt); - lifespanData.writeNBT(nbt); - - NBTTagCompound beam1 = new NBTTagCompound(); - NBTTagCompound beam2 = new NBTTagCompound(); - - for (int i = 0; i < 2; i++) { - NBTTagCompound beam = i == 0 ? beam1 : beam2; - anchorData[i].writeNBT(beam); - displayData[i].writeNBT(beam); - lightningData[i].writeNBT(beam); - trajectoryData[i].writeNBT(beam); - } - - nbt.setTag("Beam_1", beam1); - nbt.setTag("Beam_2", beam2); - } - - @Override - public void readTypeNBT(NBTTagCompound nbt) { - NBTTagCompound beam1 = nbt.hasKey("Beam_1") ? nbt.getCompoundTag("Beam_1") : new NBTTagCompound(); - NBTTagCompound beam2 = nbt.hasKey("Beam_2") ? nbt.getCompoundTag("Beam_2") : new NBTTagCompound(); - - this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; - this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; - this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); - this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; - combatData.readNBT(nbt); - homingData.readNBT(nbt); - lifespanData.readNBT(nbt); - - for (int i = 0; i < 2; i++) { - NBTTagCompound beam = i == 0 ? beam1 : beam2; - anchorData[i].readNBT(beam); - displayData[i].readNBT(beam); - lightningData[i].readNBT(beam); - trajectoryData[i].readNBT(beam); - } - } - - // Getters & Setters - Standalone fields - public float getBeamWidth() { return beamWidth; } - public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } - public float getHeadSize() { return headSize; } - public void setHeadSize(float headSize) { this.headSize = headSize; } - public boolean isDualFire() { return dualFire; } - public void setDualFire(boolean dualFire) { this.dualFire = dualFire; } - public int getDualFireDelay() { return dualFireDelay; } - public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; } - - private EnergyDisplayData getColorData(int beam) { - beam = ValueUtil.clamp(beam, 0, 1); - return displayData[beam]; - } - private EnergyLightningData getLightningData(int beam) { - beam = ValueUtil.clamp(beam, 0, 1); - return lightningData[beam]; - } - private EnergyAnchorData getAnchorData(int beam) { - beam = ValueUtil.clamp(beam, 0, 1); - return anchorData[beam]; - } - - // Getters & Setters - Color data - public int getInnerColor(int beam) { return getColorData(beam).innerColor; } - public void setInnerColor(int beam, int innerColor) { getColorData(beam).innerColor = innerColor; } - public int getOuterColor(int beam) { return getColorData(beam).outerColor; } - public void setOuterColor(int beam, int outerColor) { getColorData(beam).outerColor = outerColor; } - public boolean isOuterColorEnabled(int beam) { return getColorData(beam).outerColorEnabled; } - public void setOuterColorEnabled(int beam, boolean enabled) { getColorData(beam).outerColorEnabled = enabled; } - public float getOuterColorWidth(int beam) { return getColorData(beam).outerColorWidth; } - public void setOuterColorWidth(int beam, float width) { getColorData(beam).outerColorWidth = width; } - public float getOuterColorAlpha(int beam) { return getColorData(beam).outerColorAlpha; } - public void setOuterColorAlpha(int beam, float alpha) { getColorData(beam).outerColorAlpha = alpha; } - public float getRotationSpeed(int beam) { return getColorData(beam).rotationSpeed; } - public void setRotationSpeed(int beam, float speed) { getColorData(beam).rotationSpeed = speed; } - - - public int getInnerColor() { return getInnerColor(0); } - public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } - public int getOuterColor() { return getOuterColor(0); } - public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } - public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } - public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } - public float getOuterColorWidth() { return getOuterColorWidth(0); } - public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } - public float getOuterColorAlpha() { return getOuterColorAlpha(0); } - public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } - public float getRotationSpeed() { return getRotationSpeed(0); } - public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } - - // Getters & Setters - Combat data - public float getDamage() { return combatData.damage; } - public void setDamage(float damage) { this.combatData.damage = damage; } - public float getKnockback() { return combatData.knockback; } - public void setKnockback(float knockback) { this.combatData.knockback = knockback; } - public float getKnockbackUp() { return combatData.knockbackUp; } - public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } - public boolean isExplosive() { return combatData.explosive; } - public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } - public float getExplosionRadius() { return combatData.explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } - public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } - - // Getters & Setters - Homing data - public float getSpeed() { return homingData.speed; } - public void setSpeed(float speed) { this.homingData.speed = speed; } - public boolean isHoming() { return homingData.homing; } - public void setHoming(boolean homing) { this.homingData.homing = homing; } - public float getHomingStrength() { return homingData.homingStrength; } - public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; } - public float getHomingRange() { return homingData.homingRange; } - public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; } - - // Getters & Setters - Lightning data - public boolean hasLightningEffect(int beam) { return getLightningData(beam).lightningEffect; } - public void setLightningEffect(int beam, boolean lightningEffect) { getLightningData(beam).lightningEffect = lightningEffect; } - public float getLightningDensity(int beam) { return getLightningData(beam).lightningDensity; } - public void setLightningDensity(int beam, float lightningDensity) { getLightningData(beam).lightningDensity = lightningDensity; } - public float getLightningRadius(int beam) { return getLightningData(beam).lightningRadius; } - public void setLightningRadius(int beam, float lightningRadius) { getLightningData(beam).lightningRadius = lightningRadius; } - - public boolean hasLightningEffect() { return hasLightningEffect(0); } - public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } - public float getLightningDensity() { return getLightningDensity(0); } - public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } - public float getLightningRadius() { return getLightningRadius(0); } - public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } - - - // Getters & Setters - Lifespan data - public float getMaxDistance() { return lifespanData.maxDistance; } - public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } - public int getMaxLifetime() { return lifespanData.maxLifetime; } - public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } - - // Getters & Setters - Anchor point - public AnchorPoint getAnchorPointEnum(int beam) { return getAnchorData(beam).anchorPoint; } - public void setAnchorPointEnum(int beam, AnchorPoint anchorPoint) { getAnchorData(beam).anchorPoint = anchorPoint; } - public float getAnchorOffsetX(int beam) { return getAnchorData(beam).anchorOffsetX; } - public void setAnchorOffsetX(int beam, float x) { getAnchorData(beam).anchorOffsetX = x; } - public float getAnchorOffsetY(int beam) { return getAnchorData(beam).anchorOffsetY; } - public void setAnchorOffsetY(int beam, float y) { getAnchorData(beam).anchorOffsetY = y; } - public float getAnchorOffsetZ(int beam) { return getAnchorData(beam).anchorOffsetZ; } - public void setAnchorOffsetZ(int beam, float z) { getAnchorData(beam).anchorOffsetZ = z; } - - public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } - public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } - public float getAnchorOffsetX() { return getAnchorOffsetX(0); } - public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } - public float getAnchorOffsetY() { return getAnchorOffsetY(0); } - public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } - public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } - public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } - - //@Override - public int getAnchorPoint(int beam) { return getAnchorData(beam).anchorPoint.ordinal(); } - public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } - - //@Override - public void setAnchorPoint(int beam, int point) { getAnchorData(beam).anchorPoint = AnchorPoint.fromOrdinal(point); } - public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } - - @SideOnly(Side.CLIENT) - @Override - public void getAbilityDefinitions(List defs) { - defs.addAll(Arrays.asList( - // Type tab - FieldDef.row( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed) - ), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.section("ability.section.beam"), - FieldDef.row( - FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth), - FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize) - ), - FieldDef.row( - FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) - ), - FieldDef.section("ability.section.dual"), - FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire), - FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay) - .visibleWhen(this::isDualFire), - FieldDef.section("ability.section.homing"), - FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) - .hover("ability.hover.homing"), - FieldDef.row( - FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) - .visibleWhen(this::isHoming), - FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange) - .visibleWhen(this::isHoming) - ), - FieldDef.section("ability.section.explosive"), - FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) - .hover("ability.hover.explosive"), - FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) - .visibleWhen(this::isExplosive), - AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), - // Visual tab - Beam 1 - FieldDef.section("ability.section.beam1").tab("ability.tab.visual"), - FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, - () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)), - FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.innerColor", - () -> getInnerColor(0), v -> setInnerColor(0, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.outerEnabled", - () -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.outerColor", - () -> getOuterColor(0), v -> setOuterColor(0, v)) - .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)), - FieldDef.row( - FieldDef.floatField("ability.outerWidth", - () -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v)) - .visibleWhen(() -> isOuterColorEnabled(0)), - FieldDef.floatField("ability.outerAlpha", - () -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v)) - .range(0, 1).visibleWhen(() -> isOuterColorEnabled(0)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.rotationSpeed", - () -> getRotationSpeed(0), v -> setRotationSpeed(0, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.lightning", - () -> hasLightningEffect(0), v -> setLightningEffect(0, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.density", - () -> getLightningDensity(0), v -> setLightningDensity(0, v)) - .range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(0)), - FieldDef.floatField("gui.radius", - () -> getLightningRadius(0), v -> setLightningRadius(0, v)) - .range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(0)) - ).tab("ability.tab.visual"), - // Visual tab - Beam 2 - FieldDef.section("ability.section.beam2").tab("ability.tab.visual"), - FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, - () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)), - FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.innerColor", - () -> getInnerColor(1), v -> setInnerColor(1, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.outerEnabled", - () -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.outerColor", - () -> getOuterColor(1), v -> setOuterColor(1, v)) - .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)), - FieldDef.row( - FieldDef.floatField("ability.outerWidth", - () -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v)) - .visibleWhen(() -> isOuterColorEnabled(1)), - FieldDef.floatField("ability.outerAlpha", - () -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v)) - .range(0, 1).visibleWhen(() -> isOuterColorEnabled(1)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.rotationSpeed", - () -> getRotationSpeed(1), v -> setRotationSpeed(1, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.lightning", - () -> hasLightningEffect(1), v -> setLightningEffect(1, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.density", - () -> getLightningDensity(1), v -> setLightningDensity(1, v)) - .range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)), - FieldDef.floatField("gui.radius", - () -> getLightningRadius(1), v -> setLightningRadius(1, v)) - .range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1)) - ).tab("ability.tab.visual") - )); - } - - @Override - public int getMaxPreviewDuration() { - return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; - } -} 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 98fe9eaac..d38d6a0b2 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 @@ -22,33 +22,45 @@ import noppes.npcs.api.ability.type.IAbilityDisc; import java.util.Arrays; import java.util.List; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; /** * Disc ability: Spawns a flat spinning disc projectile. + * Supports 1-8 projectiles with shared visuals and per-projectile anchor points. * Has optional boomerang behavior to return to owner. */ public class AbilityDisc extends Ability implements IAbilityDisc { - // Disc geometry + private static final int MAX_PROJECTILES = 8; + + // Disc geometry (shared) private float discRadius = 1.0f; private float discThickness = 0.2f; private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward) - // Boomerang properties + // Boomerang properties (shared) private boolean boomerang = false; private int boomerangDelay = 40; - // Energy data classes - private final EnergyDisplayData colorData = new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f); + // Projectile count and fire stagger + private int projectileCount = 1; + private int fireDelay = 0; + + // Shared visual data (all projectiles use these) + private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f); + private EnergyLightningData lightningData = new EnergyLightningData(); + private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData(); + + // Shared combat/movement data private final EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f); private final EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f); - private final EnergyLightningData lightningData = new EnergyLightningData(); private final EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200); - private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); - private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData(); - // Transient state for disc entity (used during windup charging) - private transient EntityAbilityDisc discEntity = null; + // Per-projectile data (anchor + optional color override) + private ProjectileData[] projectiles; + + // Transient entity state + private transient EntityAbilityDisc[] discEntities; public AbilityDisc() { this.typeId = "ability.cnpc.disc"; @@ -61,9 +73,90 @@ public class AbilityDisc extends Ability implements IAbilityDisc { this.lockMovement = LockMovementType.WINDUP; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; - // Default built-in animations this.windUpAnimationName = "Ability_Disc_Windup"; this.activeAnimationName = "Ability_Disc_Active"; + initProjectiles(1); + } + + private void initProjectiles(int count) { + this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES)); + this.projectiles = new ProjectileData[projectileCount]; + for (int i = 0; i < projectileCount; i++) { + projectiles[i] = new ProjectileData(getDefaultAnchor(i)); + } + } + + /** + * Disc anchor order is REVERSED from Orb/Beam: [0]=LEFT_HAND, [1]=RIGHT_HAND + */ + private AnchorPoint getDefaultAnchor(int index) { + switch (index) { + case 0: return AnchorPoint.LEFT_HAND; + case 1: return AnchorPoint.RIGHT_HAND; + default: return AnchorPoint.FRONT; + } + } + + public void setProjectileCount(int count) { + int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES)); + if (newCount == projectileCount) return; + + ProjectileData[] oldProjectiles = projectiles; + initProjectiles(newCount); + + if (oldProjectiles != null) { + int copyCount = Math.min(oldProjectiles.length, newCount); + for (int i = 0; i < copyCount; i++) { + if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i]; + } + } + } + + public int getProjectileCount() { + return projectileCount; + } + + public int getFireDelay() { + return fireDelay; + } + + public void setFireDelay(int delay) { + this.fireDelay = Math.max(0, delay); + } + + // Legacy compat + public int getDualFireDelay() { return fireDelay; } + public void setDualFireDelay(int delay) { setFireDelay(delay); } + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.single", a -> { + a.setName("Disc"); + }), + new AbilityVariant("ability.variant.dual", a -> { + AbilityDisc disc = (AbilityDisc) a; + a.setName("Dual Disc"); + disc.setProjectileCount(2); + disc.setFireDelay(10); + // Set color override for second disc + disc.projectiles[1].colorOverride = true; + disc.projectiles[1].innerColor = 0xFFFFFF; + disc.projectiles[1].outerColor = 0x8800FF; + a.setWindUpAnimationName("Ability_DiscDual_Windup"); + a.setActiveAnimationName("Ability_DiscDual_Active"); + }) + ); + } + + @Override + public boolean allowOverlap() { + return true; + } + + @Override + public boolean isReadyForBurstCompletion(int activeTick) { + return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1); } @Override @@ -76,20 +169,33 @@ public class AbilityDisc extends Ability implements IAbilityDisc { return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } - @Override - public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote && !isPreview()) { - signalCompletion(); - return; + private EntityAbilityDisc[] createDiscEntities(EntityLivingBase caster, EntityLivingBase target, World world) { + EntityAbilityDisc[] entities = new EntityAbilityDisc[projectileCount]; + for (int i = 0; i < projectileCount; i++) { + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor); + EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); + entities[i] = new EntityAbilityDisc( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + discRadius, discThickness, + resolved, combatData, homingData, lightningData, lifespanData, trajectoryData, + boomerang, boomerangDelay); + entities[i].setEffects(this.effects); + entities[i].setSourceAbility(this); } + if (projectileCount == 2 && entities[0] != null && entities[1] != null) { + entities[0].setSiblingEntityId(entities[1].getEntityId()); + entities[1].setSiblingEntityId(entities[0].getEntityId()); + } + return entities; + } - // Start moving the disc that was spawned during windup - if (discEntity != null && !discEntity.isDead) { - if (isPreview()) { - discEntity.startPreviewFiring(); - } else { - discEntity.startMoving(target); - } + private void fireDiscEntity(EntityAbilityDisc disc, EntityLivingBase target) { + if (disc == null || disc.isDead) return; + if (isPreview()) { + disc.startPreviewFiring(); + } else { + disc.startMoving(target); } } @@ -97,33 +203,76 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote && !isPreview()) return; - // Spawn disc in charging mode on first tick of windup if (tick == 1) { - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); - discEntity = new EntityAbilityDisc( - world, caster, target, - spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, - discRadius, discThickness, - colorData, combatData, homingData, lightningData, lifespanData, trajectoryData, - boomerang, boomerangDelay); - - if (isPreview()) { - discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks, vertical); - } else { - discEntity.setupCharging(anchorData, windUpTicks, vertical); + discEntities = createDiscEntities(caster, target, world); + for (int i = 0; i < projectileCount; i++) { + EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); + if (isPreview()) { + discEntities[i].setupPreview(caster, discRadius, discThickness, resolved, lightningData, projectiles[i].anchor, windUpTicks, vertical); + } else { + discEntities[i].setupCharging(projectiles[i].anchor, windUpTicks, vertical); + } + spawnAbilityEntity(world, discEntities[i]); } + } + } - discEntity.setEffects(this.effects); - discEntity.setSourceAbility(this); - spawnAbilityEntity(world, discEntity); + @Override + public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) { + discEntities = createDiscEntities(caster, target, world); + for (EntityAbilityDisc disc : discEntities) { + spawnAbilityEntity(world, disc); + } + for (EntityAbilityDisc disc : discEntities) { + fireDiscEntity(disc, target); + } + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote && !isPreview()) { + signalCompletion(); + return; + } + + if (discEntities == null) return; + + // Fire first projectile immediately + fireDiscEntity(discEntities[0], target); + + // Fire remaining projectiles if no delay + if (fireDelay <= 0) { + for (int i = 1; i < projectileCount; i++) { + fireDiscEntity(discEntities[i], target); + } } } @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - // Signal completion when entity dies - if (discEntity == null || discEntity.isDead) { - discEntity = null; + if (discEntities == null) { + signalCompletion(); + return; + } + + // Fire staggered projectiles + if (fireDelay > 0) { + for (int i = 1; i < projectileCount; i++) { + if (tick == fireDelay * i) { + fireDiscEntity(discEntities[i], target); + } + } + } + + // Check if all entities are dead + boolean allDead = true; + for (EntityAbilityDisc disc : discEntities) { + if (disc != null && !disc.isDead) { + allDead = false; + break; + } + } + if (allDead) { signalCompletion(); } } @@ -139,11 +288,15 @@ public class AbilityDisc extends Ability implements IAbilityDisc { @Override public void cleanup() { - // Despawn disc entity if still alive - if (discEntity != null && !discEntity.isDead) { - discEntity.setDead(); + if (discEntities != null) { + for (int i = 0; i < discEntities.length; i++) { + if (discEntities[i] != null && !discEntities[i].isDead) { + discEntities[i].setDead(); + } + discEntities[i] = null; + } } - discEntity = null; + discEntities = null; } @Override @@ -180,12 +333,24 @@ public class AbilityDisc extends Ability implements IAbilityDisc { nbt.setBoolean("vertical", vertical); nbt.setBoolean("boomerang", boomerang); nbt.setInteger("boomerangDelay", boomerangDelay); - anchorData.writeNBT(nbt); - colorData.writeNBT(nbt); + nbt.setInteger("projectileCount", projectileCount); + nbt.setInteger("fireDelay", fireDelay); + + // Shared visual data + displayData.writeNBT(nbt); + lightningData.writeNBT(nbt); + + // Shared combat/movement data combatData.writeNBT(nbt); homingData.writeNBT(nbt); - lightningData.writeNBT(nbt); lifespanData.writeNBT(nbt); + + // Per-projectile data + for (int i = 0; i < projectileCount; i++) { + NBTTagCompound projNbt = new NBTTagCompound(); + projectiles[i].writeNBT(projNbt); + nbt.setTag("Projectile_" + i, projNbt); + } } @Override @@ -195,157 +360,303 @@ public class AbilityDisc extends Ability implements IAbilityDisc { this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical"); this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; - anchorData.readNBT(nbt); - colorData.readNBT(nbt); + + int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1; + initProjectiles(count); + + // Read fireDelay with fallback to old dualFireDelay key + if (nbt.hasKey("fireDelay")) { + this.fireDelay = nbt.getInteger("fireDelay"); + } else if (nbt.hasKey("dualFireDelay")) { + this.fireDelay = nbt.getInteger("dualFireDelay"); + } else { + this.fireDelay = 0; + } + + // Shared combat/movement data combatData.readNBT(nbt); homingData.readNBT(nbt); - lightningData.readNBT(nbt); lifespanData.readNBT(nbt); + + // Determine format and read accordingly + if (nbt.hasKey("Projectile_0")) { + NBTTagCompound proj0 = nbt.getCompoundTag("Projectile_0"); + if (proj0.hasKey("colorOverride")) { + // NEW format: shared visuals at root + ProjectileData per projectile + displayData.readNBT(nbt); + lightningData.readNBT(nbt); + for (int i = 0; i < projectileCount; i++) { + if (nbt.hasKey("Projectile_" + i)) { + projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i)); + } + } + } else { + // OLD format: full per-projectile arrays (displayData, lightningData, etc. in each Projectile_i) + // Read primary visuals from Projectile_0 + displayData.readNBT(proj0); + lightningData.readNBT(proj0); + projectiles[0].anchor.readNBT(proj0); + + // Read remaining projectiles, set color override if colors differ + for (int i = 1; i < projectileCount; i++) { + if (nbt.hasKey("Projectile_" + i)) { + NBTTagCompound projNbt = nbt.getCompoundTag("Projectile_" + i); + projectiles[i].anchor.readNBT(projNbt); + + // Check if this projectile had different colors + EnergyDisplayData tempDisplay = new EnergyDisplayData(); + tempDisplay.readNBT(projNbt); + if (tempDisplay.innerColor != displayData.innerColor || tempDisplay.outerColor != displayData.outerColor) { + projectiles[i].colorOverride = true; + projectiles[i].innerColor = tempDisplay.innerColor; + projectiles[i].outerColor = tempDisplay.outerColor; + } + } + } + } + } else { + // LEGACY flat format: all data at root level + displayData.readNBT(nbt); + lightningData.readNBT(nbt); + projectiles[0].anchor.readNBT(nbt); + } } - // Getters & Setters + // ═══════════════════════════════════════════════════════════════════ + // API GETTERS & SETTERS + // ═══════════════════════════════════════════════════════════════════ + public float getSpeed() { return homingData.speed; } public void setSpeed(float speed) { homingData.speed = speed; } + public float getDiscRadius() { return discRadius; } public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } + public float getDiscThickness() { return discThickness; } public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public boolean isVertical() { return vertical; } public void setVertical(boolean vertical) { this.vertical = vertical; } - public float getMaxDistance() { return lifespanData.maxDistance; } - public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } - public int getMaxLifetime() { return lifespanData.maxLifetime; } - public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; } - public float getDamage() { return combatData.damage; } - public void setDamage(float damage) { combatData.damage = damage; } - public float getKnockback() { return combatData.knockback; } - public void setKnockback(float knockback) { combatData.knockback = knockback; } - public float getKnockbackUp() { return combatData.knockbackUp; } - public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; } - public boolean isHoming() { return homingData.homing; } - public void setHoming(boolean homing) { homingData.homing = homing; } - public float getHomingStrength() { return homingData.homingStrength; } - public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; } - public float getHomingRange() { return homingData.homingRange; } - public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; } + public boolean isBoomerang() { return boomerang; } public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; } + public int getBoomerangDelay() { return boomerangDelay; } public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; } + + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float d) { lifespanData.maxDistance = d; } + + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int t) { lifespanData.maxLifetime = t; } + + public float getDamage() { return combatData.damage; } + public void setDamage(float d) { combatData.damage = d; } + + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float k) { combatData.knockback = k; } + + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float k) { combatData.knockbackUp = k; } + + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean h) { homingData.homing = h; } + + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float s) { homingData.homingStrength = s; } + + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float r) { homingData.homingRange = r; } + public boolean isExplosive() { return combatData.explosive; } - public void setExplosive(boolean explosive) { combatData.explosive = explosive; } + public void setExplosive(boolean e) { combatData.explosive = e; } + public float getExplosionRadius() { return combatData.explosionRadius; } - public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; } + public void setExplosionRadius(float r) { combatData.explosionRadius = r; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; } - public int getInnerColor() { return colorData.innerColor; } - public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; } - public int getOuterColor() { return colorData.outerColor; } - public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; } - public float getOuterColorWidth() { return colorData.outerColorWidth; } - public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; } - public float getOuterColorAlpha() { return colorData.outerColorAlpha; } - public void setOuterColorAlpha(float outerColorAlpha) { colorData.outerColorAlpha = outerColorAlpha; } - public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } - public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; } - public float getRotationSpeed() { return colorData.rotationSpeed; } - public void setRotationSpeed(float rotationSpeed) { colorData.rotationSpeed = rotationSpeed; } + public void setExplosionDamageFalloff(float f) { combatData.explosionDamageFalloff = f; } + + // Shared display data (primary colors) + public int getInnerColor() { return displayData.innerColor; } + public void setInnerColor(int c) { displayData.innerColor = c; } + + public int getOuterColor() { return displayData.outerColor; } + public void setOuterColor(int c) { displayData.outerColor = c; } + + public float getOuterColorWidth() { return displayData.outerColorWidth; } + public void setOuterColorWidth(float w) { displayData.outerColorWidth = w; } + + public float getOuterColorAlpha() { return displayData.outerColorAlpha; } + public void setOuterColorAlpha(float a) { displayData.outerColorAlpha = a; } + + public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; } + public void setOuterColorEnabled(boolean e) { displayData.outerColorEnabled = e; } + + public float getRotationSpeed() { return displayData.rotationSpeed; } + public void setRotationSpeed(float s) { displayData.rotationSpeed = s; } + + // Shared lightning data public boolean hasLightningEffect() { return lightningData.lightningEffect; } - public void setLightningEffect(boolean lightningEffect) { lightningData.lightningEffect = lightningEffect; } + public void setLightningEffect(boolean e) { lightningData.lightningEffect = e; } + public float getLightningDensity() { return lightningData.lightningDensity; } - public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; } + public void setLightningDensity(float d) { lightningData.lightningDensity = d; } + public float getLightningRadius() { return lightningData.lightningRadius; } - public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; } - public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } - public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } - public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } - public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } - public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } - public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } - public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } - public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + public void setLightningRadius(float r) { lightningData.lightningRadius = r; } + + public int getLightningFadeTime() { return lightningData.lightningFadeTime; } + public void setLightningFadeTime(int t) { lightningData.lightningFadeTime = t; } + + // Anchor - default to projectile 0 + public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; } + public void setAnchorPointEnum(AnchorPoint p) { projectiles[0].anchor.anchorPoint = p; } + + public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; } + public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; } + + public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; } + public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; } + + public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; } + public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; } @Override - public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); } + public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); } @Override - public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); } + + // Indexed API methods + public int getInnerColor(int index) { + ProjectileData p = projectiles[clampIndex(index)]; + return p.colorOverride ? p.innerColor : displayData.innerColor; + } + public void setInnerColor(int index, int color) { + ProjectileData p = projectiles[clampIndex(index)]; + p.colorOverride = true; + p.innerColor = color; + } + + public int getOuterColor(int index) { + ProjectileData p = projectiles[clampIndex(index)]; + return p.colorOverride ? p.outerColor : displayData.outerColor; + } + public void setOuterColor(int index, int color) { + ProjectileData p = projectiles[clampIndex(index)]; + p.colorOverride = true; + p.outerColor = color; + } + + public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); } + public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); } + + private int clampIndex(int index) { + return Math.max(0, Math.min(index, projectileCount - 1)); + } @Override public int getMaxPreviewDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } + // ═══════════════════════════════════════════════════════════════════ + // GUI FIELD DEFINITIONS + // ═══════════════════════════════════════════════════════════════════ + @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { - defs.addAll(Arrays.asList( - // Type tab - FieldDef.row( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed) - ), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.section("ability.section.disc"), - FieldDef.row( - FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius), - FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness) - ), - FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical) - .hover("ability.hover.vertical"), - FieldDef.row( - FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) - ), - FieldDef.section("ability.section.homing"), - FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) - .hover("ability.hover.homing"), - FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) - .visibleWhen(this::isHoming), - FieldDef.section("ability.section.boomerang"), - FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang) - .hover("ability.hover.boomerang"), - FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay) - .visibleWhen(this::isBoomerang), - FieldDef.section("ability.section.explosive"), - FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) - .hover("ability.hover.explosive"), - FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) - .visibleWhen(this::isExplosive), - AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), - // Visual tab - FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), - FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) - .tab("ability.tab.visual"), - FieldDef.section("ability.section.colors").tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) - .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), - FieldDef.row( - FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) - .visibleWhen(this::isOuterColorEnabled), - FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) - .range(0, 1).visibleWhen(this::isOuterColorEnabled) - ).tab("ability.tab.visual"), - FieldDef.section("ability.section.effects").tab("ability.tab.visual"), - FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) - .range(0.01f, 100f).visibleWhen(this::hasLightningEffect), - FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) - .range(0.1f, 100f).visibleWhen(this::hasLightningEffect) - ).tab("ability.tab.visual") + // Type tab - projectile count and fire delay + defs.add(FieldDef.row( + FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES), + FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay) + .range(0, 200).visibleWhen(() -> projectileCount > 1) )); + + // Type tab - disc properties + defs.add(FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed) + )); + defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)); + defs.add(FieldDef.section("ability.section.disc")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius), + FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness) + )); + defs.add(FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical) + .hover("ability.hover.vertical")); + defs.add(FieldDef.row( + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) + )); + defs.add(FieldDef.section("ability.section.homing")); + defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing")); + defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming)); + defs.add(FieldDef.section("ability.section.boomerang")); + defs.add(FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang).hover("ability.hover.boomerang")); + defs.add(FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay).visibleWhen(this::isBoomerang)); + defs.add(FieldDef.section("ability.section.explosive")); + defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive")); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive)); + defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); + + // Visual tab - shared primary colors + defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual")); + defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab("ability.tab.visual")); + defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab("ability.tab.visual")); + defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .range(0, 1).visibleWhen(this::isOuterColorEnabled) + ).tab("ability.tab.visual")); + + // Visual tab - shared effects + defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual")); + defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .tab("ability.tab.visual")); + defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab("ability.tab.visual")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .visibleWhen(this::hasLightningEffect).range(0.01f, 100f), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .range(0.1f, 100f).visibleWhen(this::hasLightningEffect) + ).tab("ability.tab.visual")); + + // Visual tab - per-projectile sections + for (int i = 0; i < MAX_PROJECTILES; i++) { + final int idx = i; + String sectionKey = "ability.section.projectile" + (i + 1); + + defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual") + .visibleWhen(() -> idx < projectileCount)); + + // Anchor point + defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v), + FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v) + ).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + + // Color override (only when multiple projectiles) + defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1)); + defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride)); + defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride)); + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java deleted file mode 100644 index 87d7fffd5..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java +++ /dev/null @@ -1,551 +0,0 @@ -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.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.LockMovementType; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.data.*; -import noppes.npcs.client.gui.builder.FieldDef; -import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; -import kamkeel.npcs.controllers.data.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; -import kamkeel.npcs.entity.EntityAbilityDisc; -import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.EntityLivingBase; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.Vec3; -import net.minecraft.world.World; -import noppes.npcs.util.ValueUtil; - -import java.util.Arrays; -import java.util.List; - -public class AbilityDiscDual extends Ability { - - // Disc geometry - private float discRadius = 1.0f; - private float discThickness = 0.2f; - private boolean vertical = false; - - // Boomerang properties - private boolean boomerang = false; - private int boomerangDelay = 40; - - // Dual properties - private boolean dualFire = true; - private int dualFireDelay = 10; - - // Energy data classes - private final EnergyDisplayData[] displayData = new EnergyDisplayData[]{ - new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f), - new EnergyDisplayData(0xFFFFFF, 0x8800FF, true, 0.4f, 0.5f, 5.0f) - }; - private final EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f); - private final EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f); - private final EnergyLightningData[] lightningData = new EnergyLightningData[]{ - new EnergyLightningData(), new EnergyLightningData() - }; - private final EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200); - private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ - new EnergyAnchorData(AnchorPoint.LEFT_HAND), new EnergyAnchorData(AnchorPoint.RIGHT_HAND) - }; - private final EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{ - new EnergyTrajectoryData(), new EnergyTrajectoryData() - }; - - // Transient state for disc entity (used during windup charging) - private transient EntityAbilityDisc discEntity = null; - private transient EntityAbilityDisc discEntity2 = null; - - public AbilityDiscDual() { - this.typeId = "ability.cnpc.disc_dual"; - this.name = "Disc Dual"; - this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 30.0f; - this.minRange = 5.0f; - this.cooldownTicks = 0; - this.windUpTicks = 60; - this.lockMovement = LockMovementType.WINDUP; - this.telegraphType = TelegraphType.CIRCLE; - this.showTelegraph = true; - // Default built-in animations - this.windUpAnimationName = "Ability_DiscDual_Windup"; - this.activeAnimationName = "Ability_DiscDual_Active"; - } - - @Override - public boolean isTargetingModeLocked() { - return true; - } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; - } - - @Override - public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote && !isPreview()) { - signalCompletion(); - return; - } - - // Start moving the disc that was spawned during windup - if (discEntity != null && !discEntity.isDead) { - if (isPreview()) { - discEntity.startPreviewFiring(); - } else { - discEntity.startMoving(target); - } - } - - if (dualFire) { - if (discEntity2 != null && !discEntity2.isDead && dualFireDelay <= 0) { - if (isPreview()) { - discEntity2.startPreviewFiring(); - } else { - discEntity2.startMoving(target); - } - } - } else if (discEntity2 != null) { - discEntity2.setDead(); - } - - // 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 onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote && !isPreview()) return; - - // Spawn disc in charging mode on first tick of windup - if (tick == 1) { - // Create disc in charging mode - follows caster based on anchor point during windup - Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]); - discEntity = new EntityAbilityDisc( - world, caster, target, - spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, - discRadius, discThickness, - displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0], - boomerang, boomerangDelay); - - Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]); - discEntity2 = new EntityAbilityDisc( - world, caster, target, - spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, - discRadius, discThickness, - displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1], - boomerang, boomerangDelay); - - discEntity.setSiblingEntityId(discEntity2.getEntityId()); - discEntity2.setSiblingEntityId(discEntity.getEntityId()); - - if (isPreview()) { - discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks, vertical); - discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks, vertical); - } else { - discEntity.setupCharging(anchorData[0], windUpTicks, vertical); - discEntity2.setupCharging(anchorData[1], windUpTicks, vertical); - } - - discEntity.setEffects(this.effects); - - if (dualFire) { - discEntity2.setEffects(this.effects); - } - - discEntity.setSourceAbility(this); - discEntity2.setSourceAbility(this); - spawnAbilityEntity(world, discEntity); - spawnAbilityEntity(world, discEntity2); - } - } - - @Override - public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - // Signal completion when entity dies - if (dualFire) { - if ((discEntity == null || discEntity.isDead) && (discEntity2 == null || discEntity2.isDead)) { - discEntity = null; - discEntity2 = null; - signalCompletion(); - } - - if (tick == dualFireDelay && discEntity2 != null && !discEntity2.isDead) { - if (isPreview()) { - discEntity2.startPreviewFiring(); - } else { - discEntity2.startMoving(target); - } - } - } else { - if (discEntity == null || discEntity.isDead) { - discEntity = null; - signalCompletion(); - } - } - } - - @Override - public void onComplete(EntityLivingBase caster, EntityLivingBase target) { - } - - @Override - public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { - cleanup(); - } - - @Override - public void cleanup() { - // Despawn disc entity if still alive - if (discEntity != null && !discEntity.isDead) { - discEntity.setDead(); - } - - if (discEntity2 != null && !discEntity2.isDead) { - discEntity2.setDead(); - } - - discEntity = null; - discEntity2 = null; - } - - @Override - public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { - return null; - } - - // Create circle telegraph at target position - Telegraph telegraph = Telegraph.circle(discRadius * 1.5f); - telegraph.setDurationTicks(windUpTicks); - telegraph.setColor(windUpColor); - telegraph.setWarningColor(activeColor); - telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); - telegraph.setHeightOffset(telegraphHeightOffset); - - // Position at target and follow target during windup - TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); - instance.setCasterEntityId(caster.getEntityId()); - instance.setEntityIdToFollow(target.getEntityId()); - - return instance; - } - - @Override - public float getTelegraphRadius() { - return discRadius * 1.5f; - } - - @Override - public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("discRadius", discRadius); - nbt.setFloat("discThickness", discThickness); - nbt.setBoolean("vertical", vertical); - nbt.setBoolean("boomerang", boomerang); - nbt.setInteger("boomerangDelay", boomerangDelay); - nbt.setBoolean("dualFire", dualFire); - nbt.setInteger("dualFireDelay", dualFireDelay); - combatData.writeNBT(nbt); - homingData.writeNBT(nbt); - lifespanData.writeNBT(nbt); - - NBTTagCompound disc1 = new NBTTagCompound(); - NBTTagCompound disc2 = new NBTTagCompound(); - - for (int i = 0; i < 2; i++) { - NBTTagCompound disc = i == 0 ? disc1 : disc2; - anchorData[i].writeNBT(disc); - displayData[i].writeNBT(disc); - lightningData[i].writeNBT(disc); - trajectoryData[i].writeNBT(disc); - } - - nbt.setTag("Disc_1", disc1); - nbt.setTag("Disc_2", disc2); - } - - @Override - public void readTypeNBT(NBTTagCompound nbt) { - NBTTagCompound disc1 = nbt.hasKey("Disc_1") ? nbt.getCompoundTag("Disc_1") : new NBTTagCompound(); - NBTTagCompound disc2 = nbt.hasKey("Disc_2") ? nbt.getCompoundTag("Disc_2") : new NBTTagCompound(); - - this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; - this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; - this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical"); - this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); - this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; - this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); - this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; - combatData.readNBT(nbt); - homingData.readNBT(nbt); - lifespanData.readNBT(nbt); - - for (int i = 0; i < 2; i++) { - NBTTagCompound disc = i == 0 ? disc1 : disc2; - anchorData[i].readNBT(disc); - displayData[i].readNBT(disc); - lightningData[i].readNBT(disc); - trajectoryData[i].readNBT(disc); - } - } - - // Getters & Setters - private EnergyDisplayData getColorData(int orb) { - orb = ValueUtil.clamp(orb, 0, 1); - return displayData[orb]; - } - private EnergyLightningData getLightningData(int orb) { - orb = ValueUtil.clamp(orb, 0, 1); - return lightningData[orb]; - } - private EnergyAnchorData getAnchorData(int orb) { - orb = ValueUtil.clamp(orb, 0, 1); - return anchorData[orb]; - } - - public boolean isDualFire() { return dualFire; } - public void setDualFire(boolean dualFire) { this.dualFire = dualFire; } - public int getDualFireDelay() { return dualFireDelay; } - public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; } - public float getSpeed() { return homingData.speed; } - public void setSpeed(float speed) { homingData.speed = speed; } - public float getDiscRadius() { return discRadius; } - public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } - public float getDiscThickness() { return discThickness; } - public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } - public boolean isVertical() { return vertical; } - public void setVertical(boolean vertical) { this.vertical = vertical; } - public float getMaxDistance() { return lifespanData.maxDistance; } - public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } - public int getMaxLifetime() { return lifespanData.maxLifetime; } - public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; } - public float getDamage() { return combatData.damage; } - public void setDamage(float damage) { combatData.damage = damage; } - public float getKnockback() { return combatData.knockback; } - public void setKnockback(float knockback) { combatData.knockback = knockback; } - public float getKnockbackUp() { return combatData.knockbackUp; } - public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; } - public boolean isHoming() { return homingData.homing; } - public void setHoming(boolean homing) { homingData.homing = homing; } - public float getHomingStrength() { return homingData.homingStrength; } - public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; } - public float getHomingRange() { return homingData.homingRange; } - public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; } - public boolean isBoomerang() { return boomerang; } - public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; } - public int getBoomerangDelay() { return boomerangDelay; } - public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; } - public boolean isExplosive() { return combatData.explosive; } - public void setExplosive(boolean explosive) { combatData.explosive = explosive; } - public float getExplosionRadius() { return combatData.explosionRadius; } - public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; } - public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; } - - - public int getInnerColor(int orb) { return getColorData(orb).innerColor; } - public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; } - public int getOuterColor(int orb) { return getColorData(orb).outerColor; } - public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; } - public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; } - public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; } - public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; } - public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; } - public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; } - public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; } - public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; } - public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; } - - public int getInnerColor() { return getInnerColor(0); } - public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } - public int getOuterColor() { return getOuterColor(0); } - public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } - public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } - public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } - public float getOuterColorWidth() { return getOuterColorWidth(0); } - public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } - public float getOuterColorAlpha() { return getOuterColorAlpha(0); } - public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } - public float getRotationSpeed() { return getRotationSpeed(0); } - public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } - - - public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; } - public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; } - public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; } - public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; } - public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; } - public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; } - - public boolean hasLightningEffect() { return hasLightningEffect(0); } - public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } - public float getLightningDensity() { return getLightningDensity(0); } - public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } - public float getLightningRadius() { return getLightningRadius(0); } - public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } - - - public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; } - public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; } - public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; } - public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; } - public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; } - public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; } - public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; } - public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; } - - public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } - public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } - public float getAnchorOffsetX() { return getAnchorOffsetX(0); } - public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } - public float getAnchorOffsetY() { return getAnchorOffsetY(0); } - public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } - public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } - public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } - - //@Override - public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); } - public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } - - //@Override - public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); } - public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } - - @SideOnly(Side.CLIENT) - @Override - public void getAbilityDefinitions(List defs) { - defs.addAll(Arrays.asList( - // Type tab - FieldDef.row( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed) - ), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.section("ability.section.disc"), - FieldDef.row( - FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius), - FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness) - ), - FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical) - .hover("ability.hover.vertical"), - FieldDef.row( - FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) - ), - FieldDef.section("ability.section.dual"), - FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire), - FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay) - .visibleWhen(this::isDualFire), - FieldDef.section("ability.section.homing"), - FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) - .hover("ability.hover.homing"), - FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) - .visibleWhen(this::isHoming), - FieldDef.section("ability.section.boomerang"), - FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang) - .hover("ability.hover.boomerang"), - FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay) - .visibleWhen(this::isBoomerang), - FieldDef.section("ability.section.explosive"), - FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) - .hover("ability.hover.explosive"), - FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) - .visibleWhen(this::isExplosive), - AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), - // Visual tab - Disc 1 - FieldDef.section("ability.section.disc1").tab("ability.tab.visual"), - FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, - () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)), - FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.innerColor", - () -> getInnerColor(0), v -> setInnerColor(0, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.outerEnabled", - () -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.outerColor", - () -> getOuterColor(0), v -> setOuterColor(0, v)) - .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)), - FieldDef.row( - FieldDef.floatField("ability.outerWidth", - () -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v)) - .visibleWhen(() -> isOuterColorEnabled(0)), - FieldDef.floatField("ability.outerAlpha", - () -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v)) - .range(0, 1).visibleWhen(() -> isOuterColorEnabled(0)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.rotationSpeed", - () -> getRotationSpeed(0), v -> setRotationSpeed(0, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.lightning", - () -> hasLightningEffect(0), v -> setLightningEffect(0, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.density", - () -> getLightningDensity(0), v -> setLightningDensity(0, v)) - .range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)), - FieldDef.floatField("gui.radius", - () -> getLightningRadius(0), v -> setLightningRadius(0, v)) - .range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0)) - ).tab("ability.tab.visual"), - // Visual tab - Disc 2 - FieldDef.section("ability.section.disc2").tab("ability.tab.visual"), - FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, - () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)), - FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.innerColor", - () -> getInnerColor(1), v -> setInnerColor(1, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.outerEnabled", - () -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.outerColor", - () -> getOuterColor(1), v -> setOuterColor(1, v)) - .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)), - FieldDef.row( - FieldDef.floatField("ability.outerWidth", - () -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v)) - .visibleWhen(() -> isOuterColorEnabled(1)), - FieldDef.floatField("ability.outerAlpha", - () -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v)) - .range(0, 1).visibleWhen(() -> isOuterColorEnabled(1)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.rotationSpeed", - () -> getRotationSpeed(1), v -> setRotationSpeed(1, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.lightning", - () -> hasLightningEffect(1), v -> setLightningEffect(1, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.density", - () -> getLightningDensity(1), v -> setLightningDensity(1, v)) - .range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)), - FieldDef.floatField("gui.radius", - () -> getLightningRadius(1), v -> setLightningRadius(1, v)) - .range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1)) - ).tab("ability.tab.visual") - )); - } - - @Override - public int getMaxPreviewDuration() { - return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index ec9f157f9..477be2cc4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -70,6 +70,11 @@ public class AbilityGuard extends Ability implements IAbilityGuard { return false; } + @Override + public boolean allowBurst() { + return false; + } + public enum CounterType { FLAT, PERCENT; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 112b2a637..70696eb8f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -113,6 +113,11 @@ public class AbilityHazard extends Ability implements IAbilityHazard { this.telegraphType = TelegraphType.CIRCLE; } + @Override + public boolean allowBurst() { + return false; + } + @Override public boolean isTargetingModeLocked() { return true; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index b60c46d9d..d913fe586 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -63,6 +63,11 @@ public class AbilityHeal extends Ability implements IAbilityHeal { return false; } + @Override + public boolean allowBurst() { + return false; + } + @Override public boolean isTargetingModeLocked() { return true; 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 d5b48f1fd..7ddfcc0e7 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 @@ -63,6 +63,11 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { this.activeAnimationName = "Ability_Laser_Active"; } + @Override + public boolean allowOverlap() { + return true; + } + @Override public boolean isTargetingModeLocked() { return false; @@ -83,32 +88,47 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { return laserWidth * 2.0f; // Make telegraph slightly wider for visibility } + private EntityAbilityLaser spawnLaserEntity(EntityLivingBase caster, EntityLivingBase target, World world) { + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); + EntityAbilityLaser entity = new EntityAbilityLaser( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + laserWidth, + colorData, combatData, lightningData, lifespanData, trajectoryData, + expansionSpeed, lingerTicks + ); + entity.setEffects(this.effects); + entity.setSourceAbility(this); + spawnAbilityEntity(world, entity); + return entity; + } + + private void fireLaserEntity(EntityAbilityLaser laser, EntityLivingBase target) { + if (laser == null || laser.isDead) return; + laser.setLockVerticalDirection(true); + laser.startMoving(target); + } + @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote && !isPreview()) return; if (tick == 1) { - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); - laserEntity = new EntityAbilityLaser( - world, caster, target, - spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, - laserWidth, - colorData, combatData, lightningData, lifespanData, trajectoryData, - expansionSpeed, lingerTicks - ); - + laserEntity = spawnLaserEntity(caster, target, world); if (isPreview()) { laserEntity.setupPreview(caster, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance); } else { laserEntity.setupCharging(anchorData, windUpTicks); } - - laserEntity.setEffects(this.effects); - laserEntity.setSourceAbility(this); - spawnAbilityEntity(world, laserEntity); } } + @Override + public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) { + laserEntity = spawnLaserEntity(caster, target, world); + fireLaserEntity(laserEntity, target); + } + @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote && !isPreview()) { @@ -121,8 +141,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { return; } - laserEntity.setLockVerticalDirection(true); - laserEntity.startMoving(target); + fireLaserEntity(laserEntity, target); } @Override 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 1f1cb5e7d..024fe2912 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 @@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityOrb; import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; @@ -23,29 +22,38 @@ import noppes.npcs.api.ability.type.IAbilityOrb; import java.util.Arrays; import java.util.List; -import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; /** - * Orb ability: Spawns a homing projectile sphere that tracks target. - * The EntityAbilityOrb handles all movement, collision, and damage logic. - * This ability just configures and spawns the orb entity. + * Orb ability: Spawns homing projectile sphere(s) that track target. + * Supports 1-8 projectiles with shared visuals and per-projectile anchor points. */ public class AbilityOrb extends Ability implements IAbilityOrb { - // Ability-specific properties + private static final int MAX_PROJECTILES = 8; + + // Orb properties private float orbSize = 1.0f; - // Data classes for energy properties - private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); - private final EnergyCombatData combatData = new EnergyCombatData(); - private final EnergyDisplayData displayData = new EnergyDisplayData(); - private final EnergyHomingData homingData = new EnergyHomingData(); - private final EnergyLightningData lightningData = new EnergyLightningData(); - private final EnergyLifespanData lifespanData = new EnergyLifespanData(); - private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData(); + // Projectile count and fire stagger + private int projectileCount = 1; + private int fireDelay = 0; - // Transient state for orb entity (used during windup charging) - private transient EntityAbilityOrb orbEntity = null; + // Shared visual data (all projectiles use these) + private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f); + private EnergyLightningData lightningData = new EnergyLightningData(); + private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData(); + + // Shared combat/movement data + private final EnergyCombatData combatData = new EnergyCombatData(); + private final EnergyHomingData homingData = new EnergyHomingData(); + private final EnergyLifespanData lifespanData = new EnergyLifespanData(); + + // Per-projectile data (anchor + optional color override) + private ProjectileData[] projectiles; + + // Transient entity state + private transient EntityAbilityOrb[] orbEntities; public AbilityOrb() { this.typeId = "ability.cnpc.orb"; @@ -58,9 +66,100 @@ 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"; this.activeAnimationName = "Ability_Orb_Active"; + initProjectiles(1); + } + + private void initProjectiles(int count) { + this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES)); + this.projectiles = new ProjectileData[projectileCount]; + for (int i = 0; i < projectileCount; i++) { + projectiles[i] = new ProjectileData(getDefaultAnchor(i)); + } + } + + private AnchorPoint getDefaultAnchor(int index) { + switch (index) { + case 0: return AnchorPoint.RIGHT_HAND; + case 1: return AnchorPoint.LEFT_HAND; + default: return AnchorPoint.FRONT; + } + } + + public void setProjectileCount(int count) { + int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES)); + if (newCount == projectileCount) return; + + ProjectileData[] oldProjectiles = projectiles; + initProjectiles(newCount); + + // Preserve existing projectile data + if (oldProjectiles != null) { + int copyCount = Math.min(oldProjectiles.length, newCount); + for (int i = 0; i < copyCount; i++) { + if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i]; + } + } + } + + public int getProjectileCount() { + return projectileCount; + } + + public int getFireDelay() { + return fireDelay; + } + + public void setFireDelay(int delay) { + this.fireDelay = Math.max(0, delay); + } + + // Legacy compat + public int getDualFireDelay() { return fireDelay; } + public void setDualFireDelay(int delay) { setFireDelay(delay); } + + @Override + public List getVariants() { + return Arrays.asList( + new AbilityVariant("ability.variant.single", a -> { + a.setName("Orb"); + }), + new AbilityVariant("ability.variant.dual", a -> { + AbilityOrb orb = (AbilityOrb) a; + a.setName("Dual Orb"); + orb.setProjectileCount(2); + orb.setFireDelay(5); + a.setWindUpAnimationName("Ability_OrbDual_Windup"); + a.setActiveAnimationName("Ability_OrbDual_Active"); + }), + new AbilityVariant("ability.variant.barrage", a -> { + AbilityOrb orb = (AbilityOrb) a; + a.setName("Orb Barrage"); + orb.setOrbSize(0.5f); + orb.setDamage(4.0f); + orb.setKnockback(0.0f); + orb.setKnockbackUp(0.0f); + a.setMaxRange(75.0f); + a.setBurstEnabled(true); + a.setBurstAmount(15); + a.setBurstDelay(10); + a.setBurstReplayAnimations(false); + a.setBurstOverlap(true); + a.setWindUpAnimationName("Ability_BeamDual_Windup"); + a.setActiveAnimationName("Ability_BeamDual_Active"); + }) + ); + } + + @Override + public boolean allowOverlap() { + return true; + } + + @Override + public boolean isReadyForBurstCompletion(int activeTick) { + return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1); } @Override @@ -73,6 +172,63 @@ public class AbilityOrb extends Ability implements IAbilityOrb { return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } + private EntityAbilityOrb[] createOrbEntities(EntityLivingBase caster, EntityLivingBase target, World world) { + EntityAbilityOrb[] entities = new EntityAbilityOrb[projectileCount]; + for (int i = 0; i < projectileCount; i++) { + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor); + EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); + entities[i] = new EntityAbilityOrb( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, + resolved, combatData, homingData, lightningData, lifespanData, trajectoryData); + entities[i].setEffects(this.effects); + entities[i].setSourceAbility(this); + } + if (projectileCount == 2 && entities[0] != null && entities[1] != null) { + entities[0].setSiblingEntityId(entities[1].getEntityId()); + entities[1].setSiblingEntityId(entities[0].getEntityId()); + } + return entities; + } + + private void fireOrbEntity(EntityAbilityOrb orb, EntityLivingBase target) { + if (orb == null || orb.isDead) return; + if (isPreview()) { + orb.startPreviewFiring(); + } else { + orb.startMoving(target); + } + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote && !isPreview()) return; + + if (tick == 1) { + orbEntities = createOrbEntities(caster, target, world); + for (int i = 0; i < projectileCount; i++) { + EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData); + if (isPreview()) { + orbEntities[i].setupPreview(caster, orbSize, resolved, lightningData, projectiles[i].anchor, windUpTicks); + } else { + orbEntities[i].setupCharging(projectiles[i].anchor, windUpTicks); + } + spawnAbilityEntity(world, orbEntities[i]); + } + } + } + + @Override + public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) { + orbEntities = createOrbEntities(caster, target, world); + for (EntityAbilityOrb orb : orbEntities) { + spawnAbilityEntity(world, orb); + } + for (EntityAbilityOrb orb : orbEntities) { + fireOrbEntity(orb, target); + } + } + @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote && !isPreview()) { @@ -80,55 +236,50 @@ public class AbilityOrb extends Ability implements IAbilityOrb { return; } - // Start moving the orb that was spawned during windup - if (orbEntity != null && !orbEntity.isDead) { - if (isPreview()) { - orbEntity.startPreviewFiring(); - } else { - orbEntity.startMoving(target); + if (orbEntities == null) return; + + // Fire first projectile immediately + fireOrbEntity(orbEntities[0], target); + + // Fire remaining projectiles if no delay + if (fireDelay <= 0) { + for (int i = 1; i < projectileCount; i++) { + fireOrbEntity(orbEntities[i], target); } } - - // 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 onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote && !isPreview()) return; - - // Spawn orb in charging mode on first tick of windup - if (tick == 1) { - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); - orbEntity = new EntityAbilityOrb( - world, caster, target, - spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, - displayData, combatData, homingData, lightningData, lifespanData, trajectoryData); - - if (isPreview()) { - orbEntity.setupPreview(caster, orbSize, displayData, lightningData, anchorData, windUpTicks); - } else { - orbEntity.setupCharging(anchorData, windUpTicks); - } - - orbEntity.setEffects(this.effects); - orbEntity.setSourceAbility(this); - spawnAbilityEntity(world, orbEntity); - } } @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - // Signal completion when entity dies - if (orbEntity == null || orbEntity.isDead) { - orbEntity = null; + if (orbEntities == null) { + signalCompletion(); + return; + } + + // Fire staggered projectiles + if (fireDelay > 0) { + for (int i = 1; i < projectileCount; i++) { + if (tick == fireDelay * i) { + fireOrbEntity(orbEntities[i], target); + } + } + } + + // Check if all entities are dead + boolean allDead = true; + for (EntityAbilityOrb orb : orbEntities) { + if (orb != null && !orb.isDead) { + allDead = false; + break; + } + } + if (allDead) { signalCompletion(); } } @Override public void onComplete(EntityLivingBase caster, EntityLivingBase target) { - // Nothing to clean up - entity manages itself } @Override @@ -138,11 +289,15 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void cleanup() { - // Despawn orb entity if still alive - if (orbEntity != null && !orbEntity.isDead) { - orbEntity.setDead(); + if (orbEntities != null) { + for (int i = 0; i < orbEntities.length; i++) { + if (orbEntities[i] != null && !orbEntities[i].isDead) { + orbEntities[i].setDead(); + } + orbEntities[i] = null; + } } - orbEntity = null; + orbEntities = null; } @Override @@ -151,7 +306,6 @@ public class AbilityOrb extends Ability implements IAbilityOrb { return null; } - // Create small circle telegraph at target position Telegraph telegraph = Telegraph.circle(orbSize * 1.5f); telegraph.setDurationTicks(windUpTicks); telegraph.setColor(windUpColor); @@ -159,7 +313,6 @@ public class AbilityOrb extends Ability implements IAbilityOrb { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - // Position at target and follow target during windup TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); instance.setCasterEntityId(caster.getEntityId()); instance.setEntityIdToFollow(target.getEntityId()); @@ -175,241 +328,217 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("orbSize", orbSize); - anchorData.writeNBT(nbt); - combatData.writeNBT(nbt); + nbt.setInteger("projectileCount", projectileCount); + nbt.setInteger("fireDelay", fireDelay); + + // Shared visual data displayData.writeNBT(nbt); - homingData.writeNBT(nbt); lightningData.writeNBT(nbt); + + // Shared combat/movement data + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); lifespanData.writeNBT(nbt); - trajectoryData.writeNBT(nbt); - // Backward compat: old key was "orbSpeed" + + // Per-projectile data + for (int i = 0; i < projectileCount; i++) { + NBTTagCompound projNbt = new NBTTagCompound(); + projectiles[i].writeNBT(projNbt); + nbt.setTag("Projectile_" + i, projNbt); + } + + // Backward compat write nbt.setFloat("orbSpeed", homingData.speed); } @Override public void readTypeNBT(NBTTagCompound nbt) { this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; - anchorData.readNBT(nbt); + + int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1; + initProjectiles(count); + + // Read fireDelay with fallback to old dualFireDelay key + if (nbt.hasKey("fireDelay")) { + this.fireDelay = nbt.getInteger("fireDelay"); + } else if (nbt.hasKey("dualFireDelay")) { + this.fireDelay = nbt.getInteger("dualFireDelay"); + } else { + this.fireDelay = 0; + } + + // Shared combat/movement data combatData.readNBT(nbt); - displayData.readNBT(nbt); homingData.readNBT(nbt); - lightningData.readNBT(nbt); lifespanData.readNBT(nbt); - trajectoryData.readNBT(nbt); - // Backward compat: old key was "orbSpeed" + + // Determine format and read accordingly + if (nbt.hasKey("Projectile_0")) { + NBTTagCompound proj0 = nbt.getCompoundTag("Projectile_0"); + if (proj0.hasKey("colorOverride")) { + // NEW format: shared visuals at root + ProjectileData per projectile + displayData.readNBT(nbt); + lightningData.readNBT(nbt); + for (int i = 0; i < projectileCount; i++) { + if (nbt.hasKey("Projectile_" + i)) { + projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i)); + } + } + } else { + // OLD format: full per-projectile arrays (displayData, lightningData, etc. in each Projectile_i) + // Read primary visuals from Projectile_0 + displayData.readNBT(proj0); + lightningData.readNBT(proj0); + projectiles[0].anchor.readNBT(proj0); + + // Read remaining projectiles, set color override if colors differ + for (int i = 1; i < projectileCount; i++) { + if (nbt.hasKey("Projectile_" + i)) { + NBTTagCompound projNbt = nbt.getCompoundTag("Projectile_" + i); + projectiles[i].anchor.readNBT(projNbt); + + // Check if this projectile had different colors + EnergyDisplayData tempDisplay = new EnergyDisplayData(); + tempDisplay.readNBT(projNbt); + if (tempDisplay.innerColor != displayData.innerColor || tempDisplay.outerColor != displayData.outerColor) { + projectiles[i].colorOverride = true; + projectiles[i].innerColor = tempDisplay.innerColor; + projectiles[i].outerColor = tempDisplay.outerColor; + } + } + } + } + } else { + // LEGACY flat format: all data at root level + displayData.readNBT(nbt); + lightningData.readNBT(nbt); + projectiles[0].anchor.readNBT(nbt); + } + + // Backward compat if (nbt.hasKey("orbSpeed")) { homingData.speed = nbt.getFloat("orbSpeed"); } } - // Getters & Setters - public float getOrbSpeed() { - return homingData.speed; - } + // ═══════════════════════════════════════════════════════════════════ + // API GETTERS & SETTERS + // ═══════════════════════════════════════════════════════════════════ - public void setOrbSpeed(float orbSpeed) { - homingData.speed = orbSpeed; - } + public float getOrbSpeed() { return homingData.speed; } + public void setOrbSpeed(float speed) { homingData.speed = speed; } - public float getOrbSize() { - return orbSize; - } + public float getOrbSize() { return orbSize; } + public void setOrbSize(float size) { this.orbSize = size; } - public void setOrbSize(float orbSize) { - this.orbSize = orbSize; - } + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float d) { lifespanData.maxDistance = d; } - public float getMaxDistance() { - return lifespanData.maxDistance; - } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int t) { lifespanData.maxLifetime = t; } - public void setMaxDistance(float maxDistance) { - lifespanData.maxDistance = maxDistance; - } + public float getDamage() { return combatData.damage; } + public void setDamage(float d) { combatData.damage = d; } - public int getMaxLifetime() { - return lifespanData.maxLifetime; - } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float k) { combatData.knockback = k; } - public void setMaxLifetime(int maxLifetime) { - lifespanData.maxLifetime = maxLifetime; - } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float k) { combatData.knockbackUp = k; } - public float getDamage() { - return combatData.damage; - } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean h) { homingData.homing = h; } - public void setDamage(float damage) { - combatData.damage = damage; - } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float s) { homingData.homingStrength = s; } - public float getKnockback() { - return combatData.knockback; - } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float r) { homingData.homingRange = r; } - public void setKnockback(float knockback) { - combatData.knockback = knockback; - } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean e) { combatData.explosive = e; } - public float getKnockbackUp() { - return combatData.knockbackUp; - } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float r) { combatData.explosionRadius = r; } - public void setKnockbackUp(float knockbackUp) { - combatData.knockbackUp = knockbackUp; - } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float f) { combatData.explosionDamageFalloff = f; } - public boolean isHoming() { - return homingData.homing; - } + // Shared display data (primary colors) + public int getInnerColor() { return displayData.innerColor; } + public void setInnerColor(int c) { displayData.innerColor = c; } - public void setHoming(boolean homing) { - homingData.homing = homing; - } + public int getOuterColor() { return displayData.outerColor; } + public void setOuterColor(int c) { displayData.outerColor = c; } - public float getHomingStrength() { - return homingData.homingStrength; - } + public float getOuterColorWidth() { return displayData.outerColorWidth; } + public void setOuterColorWidth(float w) { displayData.outerColorWidth = w; } - public void setHomingStrength(float homingStrength) { - homingData.homingStrength = homingStrength; - } + public float getOuterColorAlpha() { return displayData.outerColorAlpha; } + public void setOuterColorAlpha(float a) { displayData.outerColorAlpha = a; } - public float getHomingRange() { - return homingData.homingRange; - } + public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; } + public void setOuterColorEnabled(boolean e) { displayData.outerColorEnabled = e; } - public void setHomingRange(float homingRange) { - homingData.homingRange = homingRange; - } + public float getRotationSpeed() { return displayData.rotationSpeed; } + public void setRotationSpeed(float s) { displayData.rotationSpeed = s; } - public boolean isExplosive() { - return combatData.explosive; - } + // Shared lightning data + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean e) { lightningData.lightningEffect = e; } - public void setExplosive(boolean explosive) { - combatData.explosive = explosive; - } + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float d) { lightningData.lightningDensity = d; } - public float getExplosionRadius() { - return combatData.explosionRadius; - } + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float r) { lightningData.lightningRadius = r; } - public void setExplosionRadius(float explosionRadius) { - combatData.explosionRadius = explosionRadius; - } + public int getLightningFadeTime() { return lightningData.lightningFadeTime; } + public void setLightningFadeTime(int t) { lightningData.lightningFadeTime = t; } - public float getExplosionDamageFalloff() { - return combatData.explosionDamageFalloff; - } + // Anchor - default to projectile 0 + public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; } + public void setAnchorPointEnum(AnchorPoint p) { projectiles[0].anchor.anchorPoint = p; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { - combatData.explosionDamageFalloff = explosionDamageFalloff; - } + public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; } + public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; } - public int getInnerColor() { - return displayData.innerColor; - } + public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; } + public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; } - public void setInnerColor(int innerColor) { - displayData.innerColor = innerColor; - } - - public int getOuterColor() { - return displayData.outerColor; - } - - public void setOuterColor(int outerColor) { - displayData.outerColor = outerColor; - } - - public float getOuterColorWidth() { - return displayData.outerColorWidth; - } - - public void setOuterColorWidth(float outerColorWidth) { - displayData.outerColorWidth = outerColorWidth; - } - - public float getOuterColorAlpha() { - return displayData.outerColorAlpha; - } - - public void setOuterColorAlpha(float outerColorAlpha) { - displayData.outerColorAlpha = outerColorAlpha; - } - - public boolean isOuterColorEnabled() { - return displayData.outerColorEnabled; - } - - public void setOuterColorEnabled(boolean outerColorEnabled) { - displayData.outerColorEnabled = outerColorEnabled; - } - - public float getRotationSpeed() { - return displayData.rotationSpeed; - } - - public void setRotationSpeed(float rotationSpeed) { - displayData.rotationSpeed = rotationSpeed; - } - - public boolean hasLightningEffect() { - return lightningData.lightningEffect; - } - - public void setLightningEffect(boolean lightningEffect) { - lightningData.lightningEffect = lightningEffect; - } - - public float getLightningDensity() { - return lightningData.lightningDensity; - } - - public void setLightningDensity(float lightningDensity) { - lightningData.lightningDensity = lightningDensity; - } - - public float getLightningRadius() { - return lightningData.lightningRadius; - } - - public void setLightningRadius(float lightningRadius) { - lightningData.lightningRadius = lightningRadius; - } - - public int getLightningFadeTime() { - return lightningData.lightningFadeTime; - } - - public void setLightningFadeTime(int lightningFadeTime) { - lightningData.lightningFadeTime = lightningFadeTime; - } - - public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } - - public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } - - public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } - - public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } - - public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } - - public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } - - public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } - - public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } - - public int getAnchorPoint() { - return anchorData.anchorPoint.ordinal(); - } + public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; } + public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; } + public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); } @Override - public void setAnchorPoint(int point) { - this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); + public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); } + + // Indexed API methods + public int getInnerColor(int index) { + ProjectileData p = projectiles[clampIndex(index)]; + return p.colorOverride ? p.innerColor : displayData.innerColor; + } + public void setInnerColor(int index, int color) { + ProjectileData p = projectiles[clampIndex(index)]; + p.colorOverride = true; + p.innerColor = color; + } + + public int getOuterColor(int index) { + ProjectileData p = projectiles[clampIndex(index)]; + return p.colorOverride ? p.outerColor : displayData.outerColor; + } + public void setOuterColor(int index, int color) { + ProjectileData p = projectiles[clampIndex(index)]; + p.colorOverride = true; + p.outerColor = color; + } + + public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); } + public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); } + + private int clampIndex(int index) { + return Math.max(0, Math.min(index, projectileCount - 1)); } @Override @@ -417,61 +546,95 @@ public class AbilityOrb extends Ability implements IAbilityOrb { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } + // ═══════════════════════════════════════════════════════════════════ + // GUI FIELD DEFINITIONS + // ═══════════════════════════════════════════════════════════════════ + @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { - defs.addAll(Arrays.asList( - // Type tab - FieldDef.row( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed) - ), - FieldDef.row( - FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback) - ), - FieldDef.row( - FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) - ), - FieldDef.section("ability.section.homing"), - FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), - FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), - FieldDef.section("ability.section.explosive"), - FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), - FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), - AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), - - - // Visual tab - FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), - FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) - .tab("ability.tab.visual"), - FieldDef.section("ability.section.colors").tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"), - FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) - .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), - FieldDef.row( - FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) - .visibleWhen(this::isOuterColorEnabled), - FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) - .range(0, 1).visibleWhen(this::isOuterColorEnabled) - ).tab("ability.tab.visual"), - FieldDef.section("ability.section.effects").tab("ability.tab.visual"), - FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).tab("ability.tab.visual"), - FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) - .visibleWhen(this::hasLightningEffect).range(0.01f, 100f), - FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) - .range(0.1f, 100f).visibleWhen(this::hasLightningEffect) - ).tab("ability.tab.visual") + // Type tab - projectile count and fire delay + defs.add(FieldDef.row( + FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES), + FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay) + .range(0, 200).visibleWhen(() -> projectileCount > 1) )); + + // Type tab - orb properties + defs.add(FieldDef.row( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed) + )); + defs.add(FieldDef.row( + FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback) + )); + defs.add(FieldDef.row( + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) + )); + defs.add(FieldDef.section("ability.section.homing")); + defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing")); + defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming)); + defs.add(FieldDef.section("ability.section.explosive")); + defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive")); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive)); + defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); + + // Visual tab - shared primary colors + defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual")); + defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab("ability.tab.visual")); + defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab("ability.tab.visual")); + defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .range(0, 1).visibleWhen(this::isOuterColorEnabled) + ).tab("ability.tab.visual")); + + // Visual tab - shared effects + defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual")); + defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .tab("ability.tab.visual")); + defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab("ability.tab.visual")); + defs.add(FieldDef.row( + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .visibleWhen(this::hasLightningEffect).range(0.01f, 100f), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .range(0.1f, 100f).visibleWhen(this::hasLightningEffect) + ).tab("ability.tab.visual")); + + // Visual tab - per-projectile sections + for (int i = 0; i < MAX_PROJECTILES; i++) { + final int idx = i; + String sectionKey = "ability.section.projectile" + (i + 1); + + defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual") + .visibleWhen(() -> idx < projectileCount)); + + // Anchor point + defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, + () -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v), + FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v) + ).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); + + // Color override (only when multiple projectiles) + defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1)); + defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride)); + defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride)); + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java deleted file mode 100644 index f384c4b71..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbBarrage.java +++ /dev/null @@ -1,612 +0,0 @@ -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.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.LockMovementType; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.data.*; -import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; -import kamkeel.npcs.controllers.data.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; -import kamkeel.npcs.entity.EntityAbilityOrb; -import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityLivingBase; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.DamageSource; -import net.minecraft.util.Vec3; -import net.minecraft.world.World; -import net.minecraftforge.common.util.Constants; -import noppes.npcs.client.gui.builder.FieldDef; -import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.util.ValueUtil; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -public class AbilityOrbBarrage extends Ability { - - // Ability-specific properties - private float orbSize = 0.5f; - private int orbAmount = 16; - private int firingSpeed = 2; - private boolean dualCharging = true; - - // Data classes for energy properties - private final EnergyDisplayData colorData = new EnergyDisplayData(); - private final EnergyCombatData combatData = new EnergyCombatData(4.0f, 0.0f, 0.0f, false, 1.5f, 0.5f); - private final EnergyHomingData homingData = new EnergyHomingData(); - private final EnergyLightningData lightningData = new EnergyLightningData(); - private final EnergyLifespanData lifespanData = new EnergyLifespanData(); - private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ - new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) - }; - private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData(); - - - // Array to store entities - private transient List orbEntities = new ArrayList<>(); - private transient EntityAbilityOrb[] chargingEntities = null; - - // Field to keep track of the currently firing orb - private int currentOrb = 0; - - public AbilityOrbBarrage() { - this.typeId = "ability.cnpc.orb_barrage"; - this.name = "Orb Barrage"; - this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 75.0f; - this.minRange = 5.0f; - this.cooldownTicks = 0; - this.windUpTicks = 30; - this.lockMovement = LockMovementType.WINDUP; - this.telegraphType = TelegraphType.CIRCLE; - this.showTelegraph = true; - // Default built-in animation - this.windUpAnimationName = "Ability_BeamDual_Windup"; - this.activeAnimationName = "Ability_BeamDual_Active"; - } - -// @Override -// @SideOnly(Side.CLIENT) -// public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { -// return new SubGuiAbilityOrbBarrage(this, callback); -// } - - @Override - public boolean isTargetingModeLocked() { - return true; - } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; - } - - @Override - public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote && !isPreview()) { - signalCompletion(); - return; - } - - for (int i = 0; i < chargingEntities.length; i++) { - EntityAbilityOrb orbEntity = chargingEntities[i]; - - if (orbEntity != null && !orbEntity.isDead) { - orbEntity.setDead(); - } - - chargingEntities[i] = null; - } - } - - @Override - public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote && !isPreview()) return; - - // Spawn orb in charging mode on first tick of windup - if (tick == 1) { - if (dualCharging) { - chargingEntities = new EntityAbilityOrb[2]; - } else { - chargingEntities = new EntityAbilityOrb[1]; - } - - for (int i = 0; i < chargingEntities.length; i++) { - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[i]); - - EntityAbilityOrb orbEntity = new EntityAbilityOrb( - world, caster, target, - spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, - colorData, combatData, homingData, lightningData, lifespanData, trajectoryData - ); - - if (isPreview()) { - orbEntity.setupPreview(caster, orbSize, colorData, lightningData, anchorData[i], windUpTicks); - } else { - orbEntity.setupCharging(anchorData[i], windUpTicks); - } - orbEntity.setSourceAbility(this); - - chargingEntities[i] = orbEntity; - spawnAbilityEntity(world, orbEntity); - } - } - } - - @Override - public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (!orbEntities.isEmpty()) { - boolean allDead = true; - - for (EntityAbilityOrb orbEntity : orbEntities) { - if (orbEntity != null && !orbEntity.isDead) { - allDead = false; - break; - } - } - - if (allDead && currentOrb >= orbAmount) { - signalCompletion(); - return; - } - } - - if (currentOrb >= orbAmount) { - return; - } - - int ticksPerOrb = Math.max(1, 20 / firingSpeed); - - if (tick % ticksPerOrb != 0) { - return; - } - - EnergyAnchorData anchor; - - if (dualCharging) { - anchor = anchorData[currentOrb % 2]; - } else { - anchor = anchorData[0]; - } - - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchor); - - EntityAbilityOrb orb = new EntityAbilityOrb( - world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, - orbSize, colorData, combatData, homingData, lightningData, lifespanData, trajectoryData - ); - - orb.setSourceAbility(this); - world.spawnEntityInWorld(orb); - - orbEntities.add(orb); - currentOrb++; - } - - @Override - public boolean canInterrupt(DamageSource source) { - // Can be interrupted if the caster is hit when the beam is moving - if (!interruptible) { - return false; - } - - // Only direct physical hits can interrupt, not magic, fire, or other indirect damage - if (source == null) { - return false; - } - - // Reject indirect damage types - if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion()) { - return false; - } - - // Reject damage without a direct attacker entity - if (source.getEntity() == null) { - return false; - } - - // Direct hit from an entity - can interrupt - return true; - } - - @Override - public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { - cleanup(); - } - - @Override - public void cleanup() { - currentOrb = 0; - - if (chargingEntities != null) { - for (int i = 0; i < chargingEntities.length; i++) { - if (chargingEntities[i] != null && !chargingEntities[i].isDead) { - chargingEntities[i].setDead(); - } - chargingEntities[i] = null; - } - chargingEntities = null; - } - - if (orbEntities != null && !orbEntities.isEmpty()) { - for (EntityAbilityOrb orbEntity : orbEntities) { - if (orbEntity != null && !orbEntity.isDead) { - orbEntity.setDead(); - } - } - orbEntities.clear(); - } - } - - @Override - public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { - return null; - } - - // Create small circle telegraph at target position - Telegraph telegraph = Telegraph.circle(orbSize * 1.5f); - telegraph.setDurationTicks(windUpTicks); - telegraph.setColor(windUpColor); - telegraph.setWarningColor(activeColor); - telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); - telegraph.setHeightOffset(telegraphHeightOffset); - - // Position at target and follow target during windup - TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); - instance.setCasterEntityId(caster.getEntityId()); - instance.setEntityIdToFollow(target.getEntityId()); - - return instance; - } - - @Override - public float getTelegraphRadius() { - return orbSize * 1.5f; - } - - @Override - public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("orbSize", orbSize); - nbt.setInteger("orbAmount", orbAmount); - nbt.setInteger("firingSpeed", firingSpeed); - nbt.setBoolean("dualCharging", dualCharging); - colorData.writeNBT(nbt); - combatData.writeNBT(nbt); - homingData.writeNBT(nbt); - lightningData.writeNBT(nbt); - lifespanData.writeNBT(nbt); - trajectoryData.writeNBT(nbt); - - for (int i = 0; i < anchorData.length; i++) { - NBTTagCompound comp = new NBTTagCompound(); - anchorData[i].writeNBT(comp); - nbt.setTag("Anchor_" + i, comp); - } - } - - @Override - public void readTypeNBT(NBTTagCompound nbt) { - this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; - this.orbAmount = nbt.hasKey("orbAmount") ? nbt.getInteger("orbAmount") : 16; - this.firingSpeed = nbt.hasKey("firingSpeed") ? nbt.getInteger("firingSpeed") : 2; - this.dualCharging = !nbt.hasKey("dualCharging") || nbt.getBoolean("dualCharging"); - colorData.readNBT(nbt); - combatData.readNBT(nbt); - homingData.readNBT(nbt); - lightningData.readNBT(nbt); - lifespanData.readNBT(nbt); - trajectoryData.readNBT(nbt); - - for (int i = 0; i < anchorData.length; i++) { - if (nbt.hasKey("Anchor_" + i, Constants.NBT.TAG_COMPOUND)) { - anchorData[i].readNBT(nbt.getCompoundTag("Anchor_" + i)); - } else { - anchorData[i] = new EnergyAnchorData(i == 0 ? AnchorPoint.RIGHT_HAND : AnchorPoint.LEFT_HAND); - } - } - } - - // Getters & Setters - public float getOrbSpeed() { - return homingData.speed; - } - - public void setOrbSpeed(float orbSpeed) { - homingData.speed = orbSpeed; - } - - public float getOrbSize() { - return orbSize; - } - - public void setOrbSize(float orbSize) { - this.orbSize = orbSize; - } - - public int getOrbAmount() { return orbAmount; } - - public void setOrbAmount(int orbAmount) { this.orbAmount = orbAmount; } - - public int getFiringSpeed() { return firingSpeed; } - - public void setFiringSpeed(int firingSpeed) { this.firingSpeed = firingSpeed; } - - public boolean isDualCharging() { return dualCharging; } - - public void setDualCharging(boolean dualCharging) { this.dualCharging = dualCharging; } - - public float getMaxDistance() { - return lifespanData.maxDistance; - } - - public void setMaxDistance(float maxDistance) { - lifespanData.maxDistance = maxDistance; - } - - public int getMaxLifetime() { - return lifespanData.maxLifetime; - } - - public void setMaxLifetime(int maxLifetime) { - lifespanData.maxLifetime = maxLifetime; - } - - public float getDamage() { - return combatData.damage; - } - - public void setDamage(float damage) { - combatData.damage = damage; - } - - public float getKnockback() { - return combatData.knockback; - } - - public void setKnockback(float knockback) { - combatData.knockback = knockback; - } - - public float getKnockbackUp() { - return combatData.knockbackUp; - } - - public void setKnockbackUp(float knockbackUp) { - combatData.knockbackUp = knockbackUp; - } - - public boolean isHoming() { - return homingData.homing; - } - - public void setHoming(boolean homing) { - homingData.homing = homing; - } - - public float getHomingStrength() { - return homingData.homingStrength; - } - - public void setHomingStrength(float homingStrength) { - homingData.homingStrength = homingStrength; - } - - public float getHomingRange() { - return homingData.homingRange; - } - - public void setHomingRange(float homingRange) { - homingData.homingRange = homingRange; - } - - public boolean isExplosive() { - return combatData.explosive; - } - - public void setExplosive(boolean explosive) { - combatData.explosive = explosive; - } - - public float getExplosionRadius() { - return combatData.explosionRadius; - } - - public void setExplosionRadius(float explosionRadius) { - combatData.explosionRadius = explosionRadius; - } - - public float getExplosionDamageFalloff() { - return combatData.explosionDamageFalloff; - } - - public void setExplosionDamageFalloff(float explosionDamageFalloff) { - combatData.explosionDamageFalloff = explosionDamageFalloff; - } - - public int getInnerColor() { - return colorData.innerColor; - } - - public void setInnerColor(int innerColor) { - colorData.innerColor = innerColor; - } - - public int getOuterColor() { - return colorData.outerColor; - } - - public void setOuterColor(int outerColor) { - colorData.outerColor = outerColor; - } - - public float getOuterColorWidth() { - return colorData.outerColorWidth; - } - - public void setOuterColorWidth(float outerColorWidth) { - colorData.outerColorWidth = outerColorWidth; - } - - public float getOuterColorAlpha() { - return colorData.outerColorAlpha; - } - - public void setOuterColorAlpha(float outerColorAlpha) { - colorData.outerColorAlpha = outerColorAlpha; - } - - public boolean isOuterColorEnabled() { - return colorData.outerColorEnabled; - } - - public void setOuterColorEnabled(boolean outerColorEnabled) { - colorData.outerColorEnabled = outerColorEnabled; - } - - public float getRotationSpeed() { - return colorData.rotationSpeed; - } - - public void setRotationSpeed(float rotationSpeed) { - colorData.rotationSpeed = rotationSpeed; - } - - public boolean hasLightningEffect() { - return lightningData.lightningEffect; - } - - public void setLightningEffect(boolean lightningEffect) { - lightningData.lightningEffect = lightningEffect; - } - - public float getLightningDensity() { - return lightningData.lightningDensity; - } - - public void setLightningDensity(float lightningDensity) { - lightningData.lightningDensity = lightningDensity; - } - - public float getLightningRadius() { - return lightningData.lightningRadius; - } - - public void setLightningRadius(float lightningRadius) { - lightningData.lightningRadius = lightningRadius; - } - - public int getLightningFadeTime() { - return lightningData.lightningFadeTime; - } - - public void setLightningFadeTime(int lightningFadeTime) { - lightningData.lightningFadeTime = lightningFadeTime; - } - - private EnergyAnchorData getAnchorData(int orb) { - orb = ValueUtil.clamp(orb, 0, 1); - return anchorData[orb]; - } - - public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; } - public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; } - public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; } - public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; } - public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; } - public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; } - public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; } - public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; } - - public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } - public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } - public float getAnchorOffsetX() { return getAnchorOffsetX(0); } - public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } - public float getAnchorOffsetY() { return getAnchorOffsetY(0); } - public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } - public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } - public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } - - //@Override - public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); } - public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } - - //@Override - public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); } - public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } - - @Override - public int getMaxPreviewDuration() { - return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; - } - - @SideOnly(Side.CLIENT) - @Override - public void getAbilityDefinitions(List defs) { - defs.addAll(Arrays.asList( - // Type tab - FieldDef.row( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed) - ), - FieldDef.row( - FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback) - ), - FieldDef.row( - FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) - ), - FieldDef.section("ability.section.barrage"), - FieldDef.row( - FieldDef.intField("ability.amount", this::getOrbAmount, this::setOrbAmount), - FieldDef.intField("ability.firingSpeed", this::getFiringSpeed, this::setFiringSpeed) - ), - FieldDef.boolField("ability.dualCharge", this::isDualCharging, this::setDualCharging), - FieldDef.section("ability.section.homing"), - FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), - FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), - FieldDef.section("ability.section.explosive"), - FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), - FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), - AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), - - - // Visual tab - FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX), - FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ) - .tab("ability.tab.visual"), - FieldDef.section("ability.section.colors").tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"), - FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) - .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), - FieldDef.row( - FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) - .visibleWhen(this::isOuterColorEnabled), - FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) - .range(0, 1).visibleWhen(this::isOuterColorEnabled) - ).tab("ability.tab.visual"), - FieldDef.section("ability.section.effects").tab("ability.tab.visual"), - FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).tab("ability.tab.visual"), - FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) - .range(0.01f, 100f).visibleWhen(this::hasLightningEffect), - FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) - .range(0.1f, 100f).visibleWhen(this::hasLightningEffect) - ).tab("ability.tab.visual") - )); - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java deleted file mode 100644 index 3cabf4892..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java +++ /dev/null @@ -1,600 +0,0 @@ -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.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.LockMovementType; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.data.*; -import noppes.npcs.client.gui.builder.FieldDef; -import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; -import kamkeel.npcs.controllers.data.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; -import kamkeel.npcs.entity.EntityAbilityOrb; -import kamkeel.npcs.util.AnchorPointHelper; -import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityLivingBase; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.Vec3; -import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.util.ValueUtil; - -import java.util.Arrays; -import java.util.List; - -public class AbilityOrbDual extends Ability { - // Ability-specific properties - private float orbSize = 1.0f; - private boolean dualFire= true; - private int dualFireDelay = 5; - - // Data classes for energy properties - public final EnergyDisplayData[] displayData = new EnergyDisplayData[]{ - new EnergyDisplayData(), new EnergyDisplayData() - }; - public final EnergyCombatData combatData = new EnergyCombatData(); - public final EnergyHomingData homingData = new EnergyHomingData(); - public final EnergyLightningData[] lightningData = new EnergyLightningData[]{ - new EnergyLightningData(), new EnergyLightningData() - }; - public final EnergyLifespanData lifespanData = new EnergyLifespanData(); - private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ - new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) - }; - private EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{ - new EnergyTrajectoryData(), new EnergyTrajectoryData() - }; - - // Transient state for orb entity (used during windup charging) - private transient EntityAbilityOrb orbEntity = null; - private transient EntityAbilityOrb orbEntity2 = null; - - public AbilityOrbDual() { - this.typeId = "ability.cnpc.orb_dual"; - this.name = "Orb Dual"; - this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 25.0f; - this.minRange = 5.0f; - this.cooldownTicks = 0; - this.windUpTicks = 30; - this.lockMovement = LockMovementType.WINDUP; - this.telegraphType = TelegraphType.CIRCLE; - this.showTelegraph = true; - // Default built-in animation - this.windUpAnimationName = "Ability_OrbDual_Windup"; - this.activeAnimationName = "Ability_OrbDual_Active"; - } - - @Override - public boolean isTargetingModeLocked() { - return true; - } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; - } - - @Override - public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if (world.isRemote && !isPreview()) { - signalCompletion(); - return; - } - - // Start moving the orb that was spawned during windup - if (orbEntity != null && !orbEntity.isDead) { - if (isPreview()) { - orbEntity.startPreviewFiring(); - } else { - orbEntity.startMoving(target); - } - } - - if (dualFire) { - if (orbEntity2 != null && !orbEntity2.isDead && dualFireDelay <= 0) { - if (isPreview()) { - orbEntity2.startPreviewFiring(); - } else { - orbEntity2.startMoving(target); - } - } - } else if (orbEntity2 != null) { - orbEntity2.setDead(); - } - - // 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 onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote && !isPreview()) return; - - // Spawn orb in charging mode on first tick of windup - if (tick == 1) { - Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]); - orbEntity = new EntityAbilityOrb( - world, caster, target, - spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, orbSize, - displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0]); - - Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]); - orbEntity2 = new EntityAbilityOrb( - world, caster, target, - spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, orbSize, - displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1]); - - orbEntity.setSiblingEntityId(orbEntity2.getEntityId()); - orbEntity2.setSiblingEntityId(orbEntity.getEntityId()); - - if (isPreview()) { - orbEntity.setupPreview(caster, orbSize, displayData[0], lightningData[0], anchorData[0], windUpTicks); - orbEntity2.setupPreview(caster, orbSize, displayData[1], lightningData[1], anchorData[1], windUpTicks); - } else { - orbEntity.setupCharging(anchorData[0], windUpTicks); - orbEntity2.setupCharging(anchorData[1], windUpTicks); - } - - orbEntity.setEffects(this.effects); - - if (dualFire) { - orbEntity2.setEffects(this.effects); - } - - orbEntity.setSourceAbility(this); - orbEntity2.setSourceAbility(this); - spawnAbilityEntity(world, orbEntity); - spawnAbilityEntity(world, orbEntity2); - } - } - - @Override - public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - // Signal completion when entity dies - if (dualFire) { - if ((orbEntity == null || orbEntity.isDead) && (orbEntity2 == null || orbEntity2.isDead)) { - orbEntity = null; - orbEntity2 = null; - signalCompletion(); - } - - if (tick == dualFireDelay && orbEntity2 != null && !orbEntity2.isDead) { - orbEntity2.startMoving(target); - } - } else { - if (orbEntity == null || orbEntity.isDead) { - orbEntity = null; - signalCompletion(); - } - } - } - - @Override - public void onComplete(EntityLivingBase caster, EntityLivingBase target) { - } - - @Override - public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { - cleanup(); - } - - @Override - public void cleanup() { - // Despawn orb entity if still alive - if (orbEntity != null && !orbEntity.isDead) { - orbEntity.setDead(); - } - - if (orbEntity2 != null && !orbEntity2.isDead) { - orbEntity2.setDead(); - } - - orbEntity = null; - orbEntity2 = null; - } - - @Override - public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { - return null; - } - - // Create small circle telegraph at target position - Telegraph telegraph = Telegraph.circle(orbSize * 1.5f); - telegraph.setDurationTicks(windUpTicks); - telegraph.setColor(windUpColor); - telegraph.setWarningColor(activeColor); - telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); - telegraph.setHeightOffset(telegraphHeightOffset); - - // Position at target and follow target during windup - TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); - instance.setCasterEntityId(caster.getEntityId()); - instance.setEntityIdToFollow(target.getEntityId()); - - return instance; - } - - @Override - public float getTelegraphRadius() { - return orbSize * 1.5f; - } - - @Override - public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("orbSize", orbSize); - nbt.setBoolean("dualFire", dualFire); - nbt.setInteger("dualFireDelay", dualFireDelay); - combatData.writeNBT(nbt); - homingData.writeNBT(nbt); - lifespanData.writeNBT(nbt); - // Backward compat: old key was "orbSpeed" - nbt.setFloat("orbSpeed", homingData.speed); - - NBTTagCompound orb1 = new NBTTagCompound(); - NBTTagCompound orb2 = new NBTTagCompound(); - - for (int i = 0; i < 2; i++) { - NBTTagCompound orb = i == 0 ? orb1 : orb2; - anchorData[i].writeNBT(orb); - displayData[i].writeNBT(orb); - lightningData[i].writeNBT(orb); - trajectoryData[i].writeNBT(orb); - } - - nbt.setTag("Orb_1", orb1); - nbt.setTag("Orb_2", orb2); - } - - @Override - public void readTypeNBT(NBTTagCompound nbt) { - NBTTagCompound orb1 = nbt.hasKey("Orb_1") ? nbt.getCompoundTag("Orb_1") : new NBTTagCompound(); - NBTTagCompound orb2 = nbt.hasKey("Orb_2") ? nbt.getCompoundTag("Orb_2") : new NBTTagCompound(); - - this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; - this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); - this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; - combatData.readNBT(nbt); - homingData.readNBT(nbt); - lifespanData.readNBT(nbt); - // Backward compat: old key was "orbSpeed" - if (nbt.hasKey("orbSpeed")) { - homingData.speed = nbt.getFloat("orbSpeed"); - } - - for (int i = 0; i < 2; i++) { - NBTTagCompound orb = i == 0 ? orb1 : orb2; - anchorData[i].readNBT(orb); - displayData[i].readNBT(orb); - lightningData[i].readNBT(orb); - trajectoryData[i].readNBT(orb); - } - } - - // Getters & Setters - private EnergyDisplayData getColorData(int orb) { - orb = ValueUtil.clamp(orb, 0, 1); - return displayData[orb]; - } - private EnergyLightningData getLightningData(int orb) { - orb = ValueUtil.clamp(orb, 0, 1); - return lightningData[orb]; - } - private EnergyAnchorData getAnchorData(int orb) { - orb = ValueUtil.clamp(orb, 0, 1); - return anchorData[orb]; - } - - public float getOrbSpeed() { - return homingData.speed; - } - - public void setOrbSpeed(float orbSpeed) { - homingData.speed = orbSpeed; - } - - public float getOrbSize() { - return orbSize; - } - - public void setOrbSize(float orbSize) { - this.orbSize = orbSize; - } - - public boolean isDualFire() { - return dualFire; - } - - public void setDualFire(boolean dualFire) { - this.dualFire = dualFire; - } - - public int getDualFireDelay() { - return dualFireDelay; - } - - public void setDualFireDelay(int dualFireDelay) { - this.dualFireDelay = dualFireDelay; - } - - public float getMaxDistance() { - return lifespanData.maxDistance; - } - - public void setMaxDistance(float maxDistance) { - lifespanData.maxDistance = maxDistance; - } - - public int getMaxLifetime() { - return lifespanData.maxLifetime; - } - - public void setMaxLifetime(int maxLifetime) { - lifespanData.maxLifetime = maxLifetime; - } - - public float getDamage() { - return combatData.damage; - } - - public void setDamage(float damage) { - combatData.damage = damage; - } - - public float getKnockback() { - return combatData.knockback; - } - - public void setKnockback(float knockback) { - combatData.knockback = knockback; - } - - public float getKnockbackUp() { - return combatData.knockbackUp; - } - - public void setKnockbackUp(float knockbackUp) { - combatData.knockbackUp = knockbackUp; - } - - public boolean isHoming() { - return homingData.homing; - } - - public void setHoming(boolean homing) { - homingData.homing = homing; - } - - public float getHomingStrength() { - return homingData.homingStrength; - } - - public void setHomingStrength(float homingStrength) { - homingData.homingStrength = homingStrength; - } - - public float getHomingRange() { - return homingData.homingRange; - } - - public void setHomingRange(float homingRange) { - homingData.homingRange = homingRange; - } - - public boolean isExplosive() { - return combatData.explosive; - } - - public void setExplosive(boolean explosive) { - combatData.explosive = explosive; - } - - public float getExplosionRadius() { - return combatData.explosionRadius; - } - - public void setExplosionRadius(float explosionRadius) { - combatData.explosionRadius = explosionRadius; - } - - public float getExplosionDamageFalloff() { - return combatData.explosionDamageFalloff; - } - - public void setExplosionDamageFalloff(float explosionDamageFalloff) { - combatData.explosionDamageFalloff = explosionDamageFalloff; - } - - - public int getInnerColor(int orb) { return getColorData(orb).innerColor; } - public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; } - public int getOuterColor(int orb) { return getColorData(orb).outerColor; } - public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; } - public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; } - public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; } - public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; } - public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; } - public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; } - public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; } - public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; } - public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; } - - public int getInnerColor() { return getInnerColor(0); } - public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } - public int getOuterColor() { return getOuterColor(0); } - public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } - public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } - public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } - public float getOuterColorWidth() { return getOuterColorWidth(0); } - public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } - public float getOuterColorAlpha() { return getOuterColorAlpha(0); } - public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } - public float getRotationSpeed() { return getRotationSpeed(0); } - public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } - - - public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; } - public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; } - public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; } - public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; } - public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; } - public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; } - - public boolean hasLightningEffect() { return hasLightningEffect(0); } - public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } - public float getLightningDensity() { return getLightningDensity(0); } - public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } - public float getLightningRadius() { return getLightningRadius(0); } - public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } - - - public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; } - public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; } - public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; } - public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; } - public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; } - public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; } - public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; } - public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; } - - public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } - public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } - public float getAnchorOffsetX() { return getAnchorOffsetX(0); } - public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } - public float getAnchorOffsetY() { return getAnchorOffsetY(0); } - public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } - public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } - public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } - - //@Override - public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); } - public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } - - //@Override - public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); } - public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } - - @SideOnly(Side.CLIENT) - @Override - public void getAbilityDefinitions(List defs) { - defs.addAll(Arrays.asList( - // Type tab - FieldDef.row( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed) - ), - FieldDef.row( - FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback) - ), - FieldDef.row( - FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime) - ), - FieldDef.section("ability.section.dual"), - FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire), - FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay) - .visibleWhen(this::isDualFire), - FieldDef.section("ability.section.homing"), - FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), - FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), - FieldDef.section("ability.section.explosive"), - FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), - FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), - AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects), - // Visual tab - Orb 1 - FieldDef.section("ability.section.orb1").tab("ability.tab.visual"), - FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, - () -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(0), v -> setAnchorOffsetX(0, v)), - FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(0), v -> setAnchorOffsetY(0, v)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(0), v -> setAnchorOffsetZ(0, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.innerColor", - () -> getInnerColor(0), v -> setInnerColor(0, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.outerEnabled", - () -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.outerColor", - () -> getOuterColor(0), v -> setOuterColor(0, v)) - .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)), - FieldDef.row( - FieldDef.floatField("ability.outerWidth", - () -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v)) - .visibleWhen(() -> isOuterColorEnabled(0)), - FieldDef.floatField("ability.outerAlpha", - () -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v)) - .range(0, 1).visibleWhen(() -> isOuterColorEnabled(0)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.rotationSpeed", - () -> getRotationSpeed(0), v -> setRotationSpeed(0, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.lightning", - () -> hasLightningEffect(0), v -> setLightningEffect(0, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.density", - () -> getLightningDensity(0), v -> setLightningDensity(0, v)) - .range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)), - FieldDef.floatField("gui.radius", - () -> getLightningRadius(0), v -> setLightningRadius(0, v)) - .range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0)) - ).tab("ability.tab.visual"), - // Visual tab - Orb 2 - FieldDef.section("ability.section.orb2").tab("ability.tab.visual"), - FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, - () -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("ability.anchor.offsetX", () -> getAnchorOffsetX(1), v -> setAnchorOffsetX(1, v)), - FieldDef.floatField("ability.anchor.offsetY", () -> getAnchorOffsetY(1), v -> setAnchorOffsetY(1, v)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.anchor.offsetZ", () -> getAnchorOffsetZ(1), v -> setAnchorOffsetZ(1, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.innerColor", - () -> getInnerColor(1), v -> setInnerColor(1, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.outerEnabled", - () -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v)) - .tab("ability.tab.visual"), - FieldDef.colorSubGui("ability.outerColor", - () -> getOuterColor(1), v -> setOuterColor(1, v)) - .tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)), - FieldDef.row( - FieldDef.floatField("ability.outerWidth", - () -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v)) - .visibleWhen(() -> isOuterColorEnabled(1)), - FieldDef.floatField("ability.outerAlpha", - () -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v)) - .range(0, 1).visibleWhen(() -> isOuterColorEnabled(1)) - ).tab("ability.tab.visual"), - FieldDef.floatField("ability.rotationSpeed", - () -> getRotationSpeed(1), v -> setRotationSpeed(1, v)) - .tab("ability.tab.visual"), - FieldDef.boolField("ability.lightning", - () -> hasLightningEffect(1), v -> setLightningEffect(1, v)) - .tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.density", - () -> getLightningDensity(1), v -> setLightningDensity(1, v)) - .range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)), - FieldDef.floatField("gui.radius", - () -> getLightningRadius(1), v -> setLightningRadius(1, v)) - .range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1)) - ).tab("ability.tab.visual") - )); - } - - @Override - public int getMaxPreviewDuration() { - return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 75c27a507..b6657ac60 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -50,6 +50,11 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { this.showTelegraph = false; } + @Override + public boolean allowBurst() { + return false; + } + @Override public boolean isTargetingModeLocked() { return true; 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 3155310ef..9be02101f 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 @@ -61,6 +61,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { this.showTelegraph = true; } + @Override + public boolean allowBurst() { + return false; + } + @Override public boolean isTargetingModeLocked() { return true; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 1623bacd8..5a540d634 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -85,6 +85,11 @@ public class AbilityTrap extends Ability implements IAbilityTrap { this.telegraphType = TelegraphType.CIRCLE; } + @Override + public boolean allowBurst() { + return false; + } + @Override public boolean isTargetingModeLocked() { return true; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 606df7742..5e1ff694f 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -174,11 +174,23 @@ public class DataAbilities { // Handle phase-specific logic switch (currentAbility.getPhase()) { case WINDUP: + if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) { + // Burst replay: re-enter windup - set up telegraph, sound, animation, locks + if (currentAbility.isRotationLockedDuringWindup()) { + captureLockedRotation(); + } + if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) { + captureLockedPosition(); + } + spawnTelegraph(currentAbility, target); + playAbilitySound(currentAbility.getWindUpSound()); + playAbilityAnimation(currentAbility.getWindUpAnimation()); + } currentAbility.onWindUpTick(npc, target, npc.worldObj, currentAbility.getCurrentTick()); break; case ACTIVE: - if (phaseChanged && oldPhase == AbilityPhase.WINDUP) { + if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) { // Just entered ACTIVE phase - lock telegraph position if it was following // This commits the ability to its current target position TelegraphInstance telegraph = currentAbility.getTelegraphInstance(); @@ -207,6 +219,11 @@ public class DataAbilities { releaseLockedPosition(); } + // Burst re-fire without replay: let entity-spawning abilities spawn+fire in one step + if (oldPhase == AbilityPhase.BURST_DELAY && !currentAbility.isBurstReplayAnimations()) { + currentAbility.onBurstRefire(npc, target, npc.worldObj); + } + // Play active sound and animation playAbilitySound(currentAbility.getActiveSound()); playAbilityAnimation(currentAbility.getActiveAnimation()); @@ -240,6 +257,31 @@ public class DataAbilities { handleAbilityCompletion(target); return; } + + // Auto-complete for burst overlap mode (entities fly independently) + // This allows entity-spawning abilities to immediately chain next burst + // without each type needing to check burst state in their onActiveTick() + // Wait until all staggered projectiles have been fired before auto-completing + if (currentAbility.isBurstEnabled() && currentAbility.isBurstOverlap() + && currentAbility.getBurstIndex() < currentAbility.getBurstAmount() + && currentAbility.getPhase() == AbilityPhase.ACTIVE + && currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) { + currentAbility.signalCompletion(); + } + + // Check if ability entered burst delay during onActiveTick or overlap auto-complete + if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) { + // Release all locks - free movement/rotation during delay + if (rotationLocked) releaseRotationControl(); + if (positionLocked) releaseLockedPosition(); + if (hitScanActive) releaseRotationControl(); + } + break; + + case BURST_DELAY: + // Free movement and rotation during burst delay + if (rotationLocked) releaseRotationControl(); + if (positionLocked) releaseLockedPosition(); break; case DAZED: 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 fd8461245..7bc5c3846 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -3,6 +3,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilitySlot; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; @@ -61,6 +62,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Existing preset names for duplicate checking private final Set existingPresetNames = new HashSet<>(); + // Pending variant selection + private String pendingTypeId = null; + public static int modIndex = 0; public static ScrollType scrollType = ScrollType.CNPC; @@ -197,8 +201,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC String displayName = availableTypesScroll.getSelected(); String typeId = displayNameToTypeId.get(displayName); if (typeId != null) { + java.util.List variants = AbilityController.Instance.getVariantsForType(typeId); + if (variants.size() > 1) { + pendingTypeId = typeId; + setSubGui(new SubGuiAbilityVariantSelect(variants)); + return; + } Ability newAbility = AbilityController.Instance.create(typeId); if (newAbility != null) { + if (variants.size() == 1) { + variants.get(0).apply(newAbility); + } newAbility.setId(UUID.randomUUID().toString()); npcSlots.add(AbilitySlot.inline(newAbility)); selectedAbilityIndex = npcSlots.size() - 1; @@ -633,7 +646,24 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiAbilityEditMode) { + if (subgui instanceof SubGuiAbilityVariantSelect) { + SubGuiAbilityVariantSelect variantGui = (SubGuiAbilityVariantSelect) subgui; + int idx = variantGui.getSelectedIndex(); + if (idx >= 0 && pendingTypeId != null) { + Ability newAbility = AbilityController.Instance.create(pendingTypeId); + if (newAbility != null) { + variantGui.getVariants().get(idx).apply(newAbility); + newAbility.setId(UUID.randomUUID().toString()); + npcSlots.add(AbilitySlot.inline(newAbility)); + selectedAbilityIndex = npcSlots.size() - 1; + updateNpcAbilitiesList(); + selectAbilityByIndex(selectedAbilityIndex); + save(); + } + } + pendingTypeId = null; + initGui(); + } else if (subgui instanceof SubGuiAbilityEditMode) { int mode = ((SubGuiAbilityEditMode) subgui).getResult(); if (mode < 0) return; // cancelled diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index c8802a1e0..bcfce7687 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -324,7 +324,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi @Override public void unFocused(GuiNpcTextField textField) { if (textField.id < DECLARATIVE_ID_START) return; - builder.handleTextFieldEvent(textField.id, textField); + if (builder.handleTextFieldEvent(textField.id, textField)) { + initGui(); + } } // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java new file mode 100644 index 000000000..09d875dbe --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java @@ -0,0 +1,92 @@ +package noppes.npcs.client.gui.advanced; + +import kamkeel.npcs.controllers.data.ability.AbilityVariant; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiScrollWindow; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * SubGui for selecting a variant/template when creating a new ability. + * Uses a scroll window so external mods can register many variants. + */ +public class SubGuiAbilityVariantSelect extends SubGuiInterface { + + private static final int VARIANT_BTN_START = 10; + private static final int BTN_H = 20; + private static final int BTN_PAD = 2; + + private final List variants; + private final List displayNames = new ArrayList<>(); + private int selectedIndex = -1; + + public SubGuiAbilityVariantSelect(List variants) { + this.variants = variants; + for (AbilityVariant variant : variants) { + displayNames.add(StatCollector.translateToLocal(variant.getDisplayKey())); + } + setBackground("menubg.png"); + xSize = 200; + ySize = 216; + } + + @Override + public void initGui() { + super.initGui(); + + addLabel(new GuiNpcLabel(0, "ability.selectVariant", guiLeft + 10, guiTop + 8)); + + // Scroll window for variant buttons + int swX = guiLeft + 5; + int swY = guiTop + 22; + int swW = 190; + int swH = 155; + + GuiScrollWindow sw = new GuiScrollWindow(this, swX, swY, swW, swH, 0); + addScrollableGui(0, sw); // Must register BEFORE adding components (initGui clears them) + + int localY = 2; + int btnW = swW - 14; // Leave room for scrollbar + for (int i = 0; i < displayNames.size(); i++) { + GuiNpcButton btn = new GuiNpcButton(VARIANT_BTN_START + i, 2, localY, btnW, BTN_H, displayNames.get(i)); + if (i == selectedIndex) { + btn.packedFGColour = 0x55FF55; + } + sw.addButton(btn); + localY += BTN_H + BTN_PAD; + } + sw.maxScrollY = Math.max(localY - swH, 0); + + // Select / Cancel buttons + addButton(new GuiNpcButton(0, guiLeft + 5, guiTop + 188, 90, 20, "gui.select")); + getButton(0).setEnabled(selectedIndex >= 0); + addButton(new GuiNpcButton(1, guiLeft + 105, guiTop + 188, 90, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + int id = guibutton.id; + if (id == 0 && selectedIndex >= 0) { + close(); + } else if (id == 1) { + selectedIndex = -1; + close(); + } else if (id >= VARIANT_BTN_START && id < VARIANT_BTN_START + displayNames.size()) { + selectedIndex = id - VARIANT_BTN_START; + initGui(); + } + } + + public int getSelectedIndex() { + return selectedIndex; + } + + public List getVariants() { + return variants; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java index 42e117ecf..b1f123956 100644 --- a/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java +++ b/src/main/java/noppes/npcs/client/gui/builder/GuiFieldBuilder.java @@ -120,6 +120,9 @@ public class GuiFieldBuilder { continue; } + // Skip invisible fields + if (!def.isVisible()) continue; + // Section header if (def.getType() == FieldType.SECTION_HEADER) { y += 3; @@ -391,7 +394,7 @@ public class GuiFieldBuilder { /** * Handles text field unfocus events for declarative fields. - * Call from parent GUI's unFocused(). Returns true if the event was handled. + * Call from parent GUI's unFocused(). Returns true if the value actually changed. */ public boolean handleTextFieldEvent(int textFieldId, GuiNpcTextField field) { FieldDef def = textFieldMap.get(textFieldId); @@ -399,14 +402,36 @@ public class GuiFieldBuilder { switch (def.getType()) { case FLOAT: - try { def.setValue(Float.parseFloat(field.getText())); } catch (NumberFormatException ignored) {} - return true; + try { + float newVal = Float.parseFloat(field.getText()); + Object old = def.getValue(); + float oldVal = old instanceof Number ? ((Number) old).floatValue() : 0f; + if (newVal != oldVal) { + def.setValue(newVal); + return true; + } + } catch (NumberFormatException ignored) {} + return false; case INT: - try { def.setValue(Integer.parseInt(field.getText())); } catch (NumberFormatException ignored) {} - return true; + try { + int newVal = Integer.parseInt(field.getText()); + Object old = def.getValue(); + int oldVal = old instanceof Number ? ((Number) old).intValue() : 0; + if (newVal != oldVal) { + def.setValue(newVal); + return true; + } + } catch (NumberFormatException ignored) {} + return false; case STRING: - def.setValue(field.getText()); - return true; + String newVal = field.getText(); + Object old = def.getValue(); + String oldVal = old != null ? old.toString() : ""; + if (!newVal.equals(oldVal)) { + def.setValue(newVal); + return true; + } + return false; default: return false; } 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 f911c6f43..b06f9d097 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -16,8 +16,10 @@ import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; import noppes.npcs.client.NoppesUtil; +import kamkeel.npcs.controllers.data.ability.AbilityVariant; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.SubGuiAbilityTypeSelect; +import noppes.npcs.client.gui.advanced.SubGuiAbilityVariantSelect; import noppes.npcs.client.gui.advanced.SubGuiDuplicateNameConfirm; import noppes.npcs.client.gui.util.AbilityPreviewExecutor; import noppes.npcs.client.gui.util.GuiAbilityInterface; @@ -68,6 +70,9 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private Ability pendingSaveAbility = null; private boolean pendingNewCreation = false; + // ==================== PENDING VARIANT ==================== + private String pendingTypeId = null; + // ==================== PREVIEW ==================== private AbilityPreviewExecutor previewExecutor; private long prevTick = 0; @@ -343,11 +348,41 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiAbilityTypeSelect) { + if (subgui instanceof SubGuiAbilityVariantSelect) { + SubGuiAbilityVariantSelect variantGui = (SubGuiAbilityVariantSelect) subgui; + int idx = variantGui.getSelectedIndex(); + if (idx >= 0 && pendingTypeId != null) { + Ability newAbility = AbilityController.Instance.create(pendingTypeId); + if (newAbility != null) { + variantGui.getVariants().get(idx).apply(newAbility); + newAbility.setId(UUID.randomUUID().toString()); + if (hasDuplicateName(newAbility)) { + pendingSaveAbility = newAbility; + pendingNewCreation = true; + pendingTypeId = null; + setSubGui(new SubGuiDuplicateNameConfirm()); + return; + } + pendingTypeId = null; + saveAndOpenConfig(newAbility); + return; + } + } + pendingTypeId = null; + } else if (subgui instanceof SubGuiAbilityTypeSelect) { String typeId = ((SubGuiAbilityTypeSelect) subgui).getSelectedTypeId(); if (typeId != null) { + java.util.List variants = AbilityController.Instance.getVariantsForType(typeId); + if (variants.size() > 1) { + pendingTypeId = typeId; + setSubGui(new SubGuiAbilityVariantSelect(variants)); + return; + } Ability newAbility = AbilityController.Instance.create(typeId); if (newAbility != null) { + if (variants.size() == 1) { + variants.get(0).apply(newAbility); + } newAbility.setId(UUID.randomUUID().toString()); if (hasDuplicateName(newAbility)) { pendingSaveAbility = newAbility; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 497134b16..3238d2851 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1605,11 +1605,8 @@ ability.cnpc.slam=Slam ability.cnpc.heavy_hit=Heavy Hit ability.cnpc.cutter=Cutter ability.cnpc.beam=Beam -ability.cnpc.beam_dual=Beam Dual ability.cnpc.projectile=Projectile ability.cnpc.orb=Orb -ability.cnpc.orb_dual=Orb Dual -ability.cnpc.orb_barrage=Orb Barrage ability.cnpc.charge=Charge ability.cnpc.dash=Dash ability.cnpc.teleport=Teleport @@ -1620,7 +1617,6 @@ ability.cnpc.heal=Heal ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap ability.cnpc.disc=Disc -ability.cnpc.disc_dual=Disc Dual ability.cnpc.laser_shot=Laser Shot ability.cnpc.sweeper=Sweeper @@ -1674,14 +1670,16 @@ ability.section.boomerang=Boomerang ability.section.explosive=Explosive ability.section.colors=Colors ability.section.effects=Effects -ability.section.dual=Dual -ability.section.beam1=Beam 1 -ability.section.beam2=Beam 2 -ability.section.disc1=Disc 1 -ability.section.disc2=Disc 2 -ability.section.orb1=Orb 1 -ability.section.orb2=Orb 2 -ability.section.barrage=Barrage +ability.section.burst=Burst +ability.section.visual=Visual +ability.section.projectile1=Projectile 1 +ability.section.projectile2=Projectile 2 +ability.section.projectile3=Projectile 3 +ability.section.projectile4=Projectile 4 +ability.section.projectile5=Projectile 5 +ability.section.projectile6=Projectile 6 +ability.section.projectile7=Projectile 7 +ability.section.projectile8=Projectile 8 ability.section.counter=Counter ability.section.area=Area ability.section.damage=Damage @@ -1828,9 +1826,20 @@ ability.lightning=Lightning ability.lightningDensity=Density ability.lightningRadius=Radius -# Dual Energy Projectile Settings -ability.dualFire=Dual Fire +# Projectile Count / Settings +ability.projectileCount=Count ability.dualFireDelay=Fire Delay +ability.fireDelay=Fire Delay +ability.colorOverride=Color Override + +# Burst System +ability.burstEnabled=Burst +ability.burstAmount=Amount +ability.burstDelay=Delay +ability.burstReplayAnimations=Replay Animations +ability.burstOverlap=Overlap +ability.hover.burstOverlap=When enabled, projectiles from previous bursts keep flying while new ones fire +ability.hover.burstReplay=Replay windup and active animations for each burst iteration # Ability Save/Load ability.save.confirm=Save ability preset @@ -1848,6 +1857,12 @@ ability.edit.mode=How should this ability be edited? ability.cloneModify=Clone & Edit ability.modifyParent=Edit Parent +# Ability Variant Selection +ability.selectVariant=Select Variant +ability.variant.single=Single +ability.variant.dual=Dual +ability.variant.barrage=Barrage + # Ability Type Selection ability.selectType=Select Ability Type: From 12380d838ba69f7968e1d397562ca166e30c9051 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Feb 2026 22:39:47 -0500 Subject: [PATCH 09/34] lighning --- .../renderer/RenderAbilityProjectile.java | 2 +- .../lightning/AttachedLightningRenderer.java | 21 ++++++------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java index 2ba840aec..96e8e7b08 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java @@ -31,7 +31,7 @@ public abstract class RenderAbilityProjectile extends Render { */ protected void setupRenderState() { GL11.glPushMatrix(); - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_LIGHTING_BIT); GL11.glDisable(GL11.GL_CULL_FACE); GL11.glEnable(GL12.GL_RESCALE_NORMAL); GL11.glEnable(GL11.GL_ALPHA_TEST); diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java index 4fa28cda8..d68c4037b 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java @@ -88,17 +88,14 @@ public class AttachedLightningRenderer { /** * Render all active arcs in local space. * Call this after glTranslated to entity position. + * Note: Expects the caller to have already set up GL state (disabled textures, + * disabled lighting, disabled culling, enabled blending). This avoids nesting + * glPushAttrib calls which can overflow the GL attrib stack (depth 16) when + * other mods also push attribs during entity rendering. */ public void render() { if (arcs.isEmpty()) return; - // Setup GL state - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); - GL11.glDisable(GL11.GL_TEXTURE_2D); - GL11.glDisable(GL11.GL_LIGHTING); - GL11.glDisable(GL11.GL_CULL_FACE); - GL11.glEnable(GL11.GL_BLEND); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); GL11.glDepthMask(false); Tessellator tess = Tessellator.instance; @@ -114,7 +111,6 @@ public class AttachedLightningRenderer { } GL11.glDepthMask(true); - GL11.glPopAttrib(); } public int getArcCount() { @@ -239,6 +235,8 @@ public class AttachedLightningRenderer { /** * Render a single instant lightning arc (no fading, drawn this frame only). + * Expects the caller to have already set up GL state (disabled textures, + * disabled lighting, disabled culling, enabled blending). */ public static void renderInstantArc(float radius, int outerColor, int innerColor) { double theta = rand.nextDouble() * Math.PI * 2; @@ -254,12 +252,6 @@ public class AttachedLightningRenderer { List points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement); - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); - GL11.glDisable(GL11.GL_TEXTURE_2D); - GL11.glDisable(GL11.GL_LIGHTING); - GL11.glDisable(GL11.GL_CULL_FACE); - GL11.glEnable(GL11.GL_BLEND); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); GL11.glDepthMask(false); Tessellator tess = Tessellator.instance; @@ -267,7 +259,6 @@ public class AttachedLightningRenderer { renderBoltPath(tess, points, innerColor, 0.012f, 0.7f); GL11.glDepthMask(true); - GL11.glPopAttrib(); } /** From 4c5e44fb2249eee82515da6c9fc88f0b37e16b5f Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 6 Feb 2026 22:43:07 -0500 Subject: [PATCH 10/34] Add anchor offset methods and update ability variant selection UI --- .../data/ability/AbilityVariant.java | 15 +++ .../data/ability/type/AbilityBeam.java | 3 + .../data/ability/type/AbilityDisc.java | 3 + .../data/ability/type/AbilityOrb.java | 3 + src/main/java/kamkeel/npcs/util/Register.java | 3 +- .../advanced/SubGuiAbilityVariantSelect.java | 126 +++++++++++++----- .../npcs/client/gui/util/GuiCustomScroll.java | 17 ++- 7 files changed, 134 insertions(+), 36 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java index 91d151a85..797e6b20b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityVariant.java @@ -13,6 +13,7 @@ import java.util.function.Consumer; public class AbilityVariant { private final String displayKey; + private final String group; private final Consumer configurator; /** @@ -20,7 +21,17 @@ public class AbilityVariant { * @param configurator Lambda that configures the ability with preset values */ public AbilityVariant(String displayKey, Consumer configurator) { + this(displayKey, null, configurator); + } + + /** + * @param displayKey Lang key for the variant display name + * @param group Optional group header shown in the variant selection GUI (e.g., "DBC Addon") + * @param configurator Lambda that configures the ability with preset values + */ + public AbilityVariant(String displayKey, String group, Consumer configurator) { this.displayKey = displayKey; + this.group = group; this.configurator = configurator; } @@ -28,6 +39,10 @@ public class AbilityVariant { return displayKey; } + public String getGroup() { + return group; + } + /** * Apply this variant's preset configuration to the given ability. */ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 5f12cc183..91b0287eb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -521,6 +521,9 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); } public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; } + public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; } + public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; } private int clampIndex(int index) { return Math.max(0, Math.min(index, projectileCount - 1)); 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 d38d6a0b2..e0ed1b43c 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 @@ -551,6 +551,9 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); } public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; } + public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; } + public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; } private int clampIndex(int index) { return Math.max(0, Math.min(index, projectileCount - 1)); 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 024fe2912..e0f6bc19f 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 @@ -536,6 +536,9 @@ public class AbilityOrb extends Ability implements IAbilityOrb { public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); } public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; } + public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; } + public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; } private int clampIndex(int index) { return Math.max(0, Math.min(index, projectileCount - 1)); diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index c97188316..ff6b0f24e 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -29,7 +29,8 @@ public class Register { public static boolean isEmpty(String registryKey) { if (REGISTERED_NAMESPACES.isEmpty()) return true; - if (REGISTERED_NAMESPACES.get(registryKey).isEmpty()) return true; + List list = REGISTERED_NAMESPACES.get(registryKey); + if (list == null || list.isEmpty()) return true; return false; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java index 09d875dbe..1c65d0dc6 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityVariantSelect.java @@ -2,10 +2,10 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.AbilityVariant; import net.minecraft.client.gui.GuiButton; -import net.minecraft.util.StatCollector; +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.GuiScrollWindow; +import noppes.npcs.client.gui.util.ICustomScrollListener; import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; @@ -13,23 +13,18 @@ import java.util.List; /** * SubGui for selecting a variant/template when creating a new ability. - * Uses a scroll window so external mods can register many variants. + * Uses a scroll list with group headers for external addon variants. */ -public class SubGuiAbilityVariantSelect extends SubGuiInterface { - - private static final int VARIANT_BTN_START = 10; - private static final int BTN_H = 20; - private static final int BTN_PAD = 2; +public class SubGuiAbilityVariantSelect extends SubGuiInterface implements ICustomScrollListener { private final List variants; - private final List displayNames = new ArrayList<>(); private int selectedIndex = -1; + // Maps each scroll list index to a variant list index (-1 for non-selectable entries) + private final List scrollToVariantIndex = new ArrayList<>(); + public SubGuiAbilityVariantSelect(List variants) { this.variants = variants; - for (AbilityVariant variant : variants) { - displayNames.add(StatCollector.translateToLocal(variant.getDisplayKey())); - } setBackground("menubg.png"); xSize = 200; ySize = 216; @@ -41,26 +36,78 @@ public class SubGuiAbilityVariantSelect extends SubGuiInterface { addLabel(new GuiNpcLabel(0, "ability.selectVariant", guiLeft + 10, guiTop + 8)); - // Scroll window for variant buttons - int swX = guiLeft + 5; - int swY = guiTop + 22; - int swW = 190; - int swH = 155; + // Build the unsorted scroll list with group headers + List scrollList = new ArrayList<>(); + scrollToVariantIndex.clear(); - GuiScrollWindow sw = new GuiScrollWindow(this, swX, swY, swW, swH, 0); - addScrollableGui(0, sw); // Must register BEFORE adding components (initGui clears them) + boolean hasBaseVariants = false; + boolean hasGroupedVariants = false; - int localY = 2; - int btnW = swW - 14; // Leave room for scrollbar - for (int i = 0; i < displayNames.size(); i++) { - GuiNpcButton btn = new GuiNpcButton(VARIANT_BTN_START + i, 2, localY, btnW, BTN_H, displayNames.get(i)); - if (i == selectedIndex) { - btn.packedFGColour = 0x55FF55; - } - sw.addButton(btn); - localY += BTN_H + BTN_PAD; + for (AbilityVariant v : variants) { + if (v.getGroup() == null) + hasBaseVariants = true; + else + hasGroupedVariants = true; } - sw.maxScrollY = Math.max(localY - swH, 0); + + // Add base (non-grouped) variants first + for (int i = 0; i < variants.size(); i++) { + if (variants.get(i).getGroup() == null) { + scrollList.add(variants.get(i).getDisplayKey()); + scrollToVariantIndex.add(i); + } + } + + // Blank separator between base and grouped variants + if (hasBaseVariants && hasGroupedVariants) { + scrollList.add(""); + scrollToVariantIndex.add(-1); + } + + // Add grouped variants with group headers + String lastGroup = null; + for (int i = 0; i < variants.size(); i++) { + AbilityVariant v = variants.get(i); + if (v.getGroup() == null) + continue; + + if (!v.getGroup().equals(lastGroup)) { + scrollList.add(v.getGroup()); + scrollToVariantIndex.add(-1); + lastGroup = v.getGroup(); + } + + scrollList.add(v.getDisplayKey()); + scrollToVariantIndex.add(i); + } + + GuiCustomScroll scroll = new GuiCustomScroll(this, 0); + scroll.guiLeft = guiLeft + 5; + scroll.guiTop = guiTop + 22; + scroll.setUnsortedList(scrollList); + scroll.setSize(190, 155); + + // Mark blank separators and group headers as non-interactive colored entries + scroll.colors.put("", 0x000000); + if (lastGroup != null) { + for (int i = 0; i < variants.size(); i++) { + String group = variants.get(i).getGroup(); + if (group != null) + scroll.colors.put(group, 0xFFFF00); // Yellow + } + } + + // Restore selection in scroll + if (selectedIndex >= 0) { + for (int s = 0; s < scrollToVariantIndex.size(); s++) { + if (scrollToVariantIndex.get(s) == selectedIndex) { + scroll.selected = s; + break; + } + } + } + + addScroll(scroll); // Select / Cancel buttons addButton(new GuiNpcButton(0, guiLeft + 5, guiTop + 188, 90, 20, "gui.select")); @@ -68,6 +115,24 @@ public class SubGuiAbilityVariantSelect extends SubGuiInterface { addButton(new GuiNpcButton(1, guiLeft + 105, guiTop + 188, 90, 20, "gui.cancel")); } + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { + if (scroll.id == 0 && scroll.selected >= 0 && scroll.selected < scrollToVariantIndex.size()) { + int variantIdx = scrollToVariantIndex.get(scroll.selected); + if (variantIdx >= 0) { + selectedIndex = variantIdx; + getButton(0).setEnabled(true); + } + } + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { + if (selectedIndex >= 0) { + close(); + } + } + @Override public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; @@ -76,9 +141,6 @@ public class SubGuiAbilityVariantSelect extends SubGuiInterface { } else if (id == 1) { selectedIndex = -1; close(); - } else if (id >= VARIANT_BTN_START && id < VARIANT_BTN_START + displayNames.size()) { - selectedIndex = id - VARIANT_BTN_START; - initGui(); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java b/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java index e726239b1..f9b3df568 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiCustomScroll.java @@ -200,7 +200,8 @@ public class GuiCustomScroll extends GuiScreen { int k = (14 * i + 4) - scrollY; if (k >= 4 && k + 12 < ySize) { int xOffset = scrollHeight < ySize - 8 ? 0 : 10; - String displayString = StatCollector.translateToLocal(list.get(i)); + String rawEntry = list.get(i); + String displayString = StatCollector.translateToLocal(rawEntry); String text = ""; float maxWidth = (xSize + xOffset - 8) * 0.8f; @@ -215,7 +216,13 @@ public class GuiCustomScroll extends GuiScreen { text += "..."; } else text = displayString; - if ((multipleSelection && selectedList.contains(text)) || (!multipleSelection && selected == i)) { + + // Entries with a custom color are non-interactive (headers/separators) + Integer customColor = colors.get(rawEntry); + if (customColor != null) { + if (!text.isEmpty()) + fontRendererObj.drawString(text, j, k, customColor); + } else if ((multipleSelection && selectedList.contains(text)) || (!multipleSelection && selected == i)) { drawVerticalLine(j - 2, k - 4, k + 10, 0xffffffff); drawVerticalLine(j + xSize - 18 + xOffset, k - 4, k + 10, 0xffffffff); drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k - 3, 0xffffffff); @@ -239,13 +246,17 @@ public class GuiCustomScroll extends GuiScreen { private int getMouseOver(int i, int j) { i -= guiLeft; j -= guiTop; - //fontRenderer.drawString(".", i , j, 0xffffff); if (i >= 4 && i < xSize - 4 && j >= 4 && j < ySize) { for (int j1 = 0; j1 < list.size(); j1++) { if (!mouseInOption(i, j, j1)) { continue; } + // Skip non-interactive entries (colored headers/separators) + if (colors.containsKey(list.get(j1))) { + continue; + } + return j1; } From 9054abadeb0b1fd39ba0a1c330cb99adfd9ca9be Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 7 Feb 2026 01:54:16 -0500 Subject: [PATCH 11/34] Fix Ability --- src/api | 2 +- .../npcs/controllers/data/ability/type/AbilityDisc.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api b/src/api index 8917e37a6..fd4c77d01 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 8917e37a6d35230a5c7b75ac8f89428edf1aabd2 +Subproject commit fd4c77d015add1f6f3012a1681561b2774f2f997 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 e0ed1b43c..22832a104 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 @@ -91,8 +91,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { */ private AnchorPoint getDefaultAnchor(int index) { switch (index) { - case 0: return AnchorPoint.LEFT_HAND; - case 1: return AnchorPoint.RIGHT_HAND; + case 0: return AnchorPoint.RIGHT_HAND; + case 1: return AnchorPoint.LEFT_HAND; default: return AnchorPoint.FRONT; } } From 0fcf37542b244c95627ef729a671421fd1aab2fa Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 7 Feb 2026 15:31:49 -0500 Subject: [PATCH 12/34] Update AbilityDash.java --- .../data/ability/type/AbilityDash.java | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index f80f336aa..79db9dd42 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -87,13 +87,14 @@ public class AbilityDash extends Ability implements IAbilityDash { // Type-specific parameters private DashMode dashMode = DashMode.DEFENSIVE; private float dashDistance = 4.0f; - private float dashSpeed = 1.5f; + private float dashSpeed = 0.5f; // Runtime state private transient Vec3 dashDirection; private transient double startX, startY, startZ; private transient double prevTickX, prevTickZ; private transient DashDirection chosenDirection; + private transient int maxActiveTicks; public AbilityDash() { this.typeId = "ability.cnpc.dash"; @@ -139,6 +140,9 @@ public class AbilityDash extends Ability implements IAbilityDash { prevTickX = caster.posX; prevTickZ = caster.posZ; + // Safety timeout: expected ticks + generous buffer to prevent infinite dash + maxActiveTicks = dashSpeed > 0 ? (int)(dashDistance / dashSpeed) + 10 : 10; + // Choose random direction based on mode DashDirection[] directions = dashMode == DashMode.AGGRESSIVE ? AGGRESSIVE_DIRECTIONS @@ -164,22 +168,28 @@ public class AbilityDash extends Ability implements IAbilityDash { 0, Math.cos(yawRad) ); - } @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (dashDirection == null) return; + // Safety timeout or missing state: force-complete to prevent stuck NPC + if (!isPreview() && (dashDirection == null || tick > maxActiveTicks)) { + stopDash(caster); + signalCompletion(); + return; + } + + if (dashDirection == null) { + signalCompletion(); + return; + } // Stall detection: if entity hasn't moved since last tick, it's stuck against a wall if (!isPreview() && tick > 1) { - double movedThisTick = Math.sqrt( - Math.pow(caster.posX - prevTickX, 2) + - Math.pow(caster.posZ - prevTickZ, 2)); - if (movedThisTick < 0.01) { - caster.motionX = 0; - caster.motionZ = 0; - caster.velocityChanged = true; + double dx = caster.posX - prevTickX; + double dz = caster.posZ - prevTickZ; + if (dx * dx + dz * dz < 0.0001) { + stopDash(caster); signalCompletion(); return; } @@ -187,28 +197,18 @@ public class AbilityDash extends Ability implements IAbilityDash { prevTickX = caster.posX; prevTickZ = caster.posZ; - // Calculate distance traveled - double distanceTraveled = Math.sqrt( - Math.pow(caster.posX - startX, 2) + - Math.pow(caster.posZ - startZ, 2) - ); - // Check if reached max distance - if (distanceTraveled >= dashDistance) { - caster.motionX = 0; - caster.motionZ = 0; - if (!isPreview()) { - caster.velocityChanged = true; - } + double travelDx = caster.posX - startX; + double travelDz = caster.posZ - startZ; + if (travelDx * travelDx + travelDz * travelDz >= (double) dashDistance * dashDistance) { + stopDash(caster); signalCompletion(); return; } // Block detection (skip in preview - no real world collision) if (!isPreview() && isDashBlocked(caster)) { - caster.motionX = 0; - caster.motionZ = 0; - caster.velocityChanged = true; + stopDash(caster); signalCompletion(); return; } @@ -225,8 +225,7 @@ public class AbilityDash extends Ability implements IAbilityDash { } } - @Override - public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + private void stopDash(EntityLivingBase caster) { caster.motionX = 0; caster.motionZ = 0; if (!isPreview()) { @@ -234,19 +233,21 @@ public class AbilityDash extends Ability implements IAbilityDash { } } + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + stopDash(caster); + } + @Override public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { - caster.motionX = 0; - caster.motionZ = 0; - if (!isPreview()) { - caster.velocityChanged = true; - } + stopDash(caster); } @Override public void cleanup() { dashDirection = null; chosenDirection = null; + maxActiveTicks = 0; } private boolean isDashBlocked(EntityLivingBase caster) { @@ -281,7 +282,7 @@ public class AbilityDash extends Ability implements IAbilityDash { this.dashMode = DashMode.DEFENSIVE; } this.dashDistance = nbt.hasKey("dashDistance") ? nbt.getFloat("dashDistance") : 4.0f; - this.dashSpeed = nbt.hasKey("dashSpeed") ? nbt.getFloat("dashSpeed") : 1.5f; + this.dashSpeed = nbt.hasKey("dashSpeed") ? nbt.getFloat("dashSpeed") : 0.5f; } // Getters & Setters From c05b6e60b8b1ef7906dadfed7a3f4a697eee4dbe Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 7 Feb 2026 17:07:05 -0500 Subject: [PATCH 13/34] Update AbilityDash.java --- .../npcs/controllers/data/ability/type/AbilityDash.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 79db9dd42..2ea0095eb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -168,6 +168,9 @@ public class AbilityDash extends Ability implements IAbilityDash { 0, Math.cos(yawRad) ); + + // Small upward impulse for a skip/hop arc (gravity handles the descent) + caster.motionY = 0.2; } @Override @@ -213,9 +216,8 @@ public class AbilityDash extends Ability implements IAbilityDash { return; } - // Move caster + // Move caster (motionY left to gravity for skip arc) caster.motionX = dashDirection.xCoord * dashSpeed; - caster.motionY = 0; caster.motionZ = dashDirection.zCoord * dashSpeed; if (!isPreview()) { caster.velocityChanged = true; From 2387ece1ff1856e9f049c78b75021f7517dc0d4f Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 8 Feb 2026 01:32:18 -0500 Subject: [PATCH 14/34] Update Abilities to Support Player Casting (Part 1) --- .../controllers/data/ability/Ability.java | 12 ++ .../data/ability/type/AbilityBeam.java | 9 +- .../data/ability/type/AbilityCharge.java | 14 +- .../data/ability/type/AbilityDash.java | 5 +- .../data/ability/type/AbilityDisc.java | 9 +- .../data/ability/type/AbilityHazard.java | 4 +- .../data/ability/type/AbilityLaserShot.java | 5 + .../data/ability/type/AbilityOrb.java | 9 +- .../data/ability/type/AbilityProjectile.java | 195 +++++++++++++----- .../data/ability/type/AbilitySlam.java | 76 ++++--- .../data/ability/type/AbilitySweeper.java | 4 +- .../data/ability/type/AbilityTeleport.java | 24 +-- .../data/ability/type/AbilityTrap.java | 5 +- .../data/ability/type/AbilityVortex.java | 5 +- 14 files changed, 259 insertions(+), 117 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 49ffbf587..b95879c3b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -392,6 +392,18 @@ public abstract class Ability implements IAbility { return 45.0f; } + /** + * Returns true if the caster is a player (not an NPC). + * Use this instead of checking {@code target != null} to make NPC/Player + * code paths explicit and readable. + *

+ * NPCs always have an aggro target passed to ability methods. + * Players never do — they use look direction for aiming instead. + */ + protected final boolean isPlayerCaster(EntityLivingBase caster) { + return caster instanceof EntityPlayer; + } + /** * Returns true if this ability controls NPC movement during ACTIVE phase. * When true, AI pathfinding will be blocked during ability execution. diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index 91b0287eb..f99a03528 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -164,6 +164,11 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } + /** + * Create beam entities. + * NPC: target is the aggro target — beams will home toward it. + * Player: target is null — beams fire in caster's look direction. + */ private EntityAbilityBeam[] createBeamEntities(EntityLivingBase caster, EntityLivingBase target, World world) { EntityAbilityBeam[] entities = new EntityAbilityBeam[projectileCount]; float offsetDist = 1.0f; @@ -294,7 +299,9 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { @Override public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + // NPC: telegraph on aggro target position + // Player: no telegraph (beam fires in look direction, no fixed position to mark) + if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) { return null; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 8e179dc73..4ae81bfcb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -87,11 +87,13 @@ public class AbilityCharge extends Ability implements IAbilityCharge { } /** - * Locks the charge direction based on current target position. - * Called once at windup start - direction won't change even if target moves. + * Locks the charge direction. Called once at windup start — direction won't change even if target moves. + * NPC: charges toward aggro target. + * Player: charges in look direction. */ private void lockChargeDirection(EntityLivingBase caster, EntityLivingBase target) { - if (target != null) { + if (!isPlayerCaster(caster) && target != null) { + // NPC: charge toward aggro target double dx = target.posX - caster.posX; double dz = target.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dz * dz); @@ -102,6 +104,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge { chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); } } else { + // Player: charge in look direction float yaw = (float) Math.toRadians(caster.rotationYaw); chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); } @@ -258,9 +261,10 @@ public class AbilityCharge extends Ability implements IAbilityCharge { return null; } - // Calculate direction to target at this moment (locked) + // NPC: telegraph points toward aggro target + // Player: telegraph points in look direction float yaw; - if (target != null) { + if (!isPlayerCaster(caster) && target != null) { double dx = target.posX - caster.posX; double dz = target.posZ - caster.posZ; yaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 2ea0095eb..0f8cbe374 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -149,9 +149,10 @@ public class AbilityDash extends Ability implements IAbilityDash { : DEFENSIVE_DIRECTIONS; chosenDirection = directions[RANDOM.nextInt(directions.length)]; - // Calculate dash direction relative to target + // NPC: dash direction is relative to aggro target facing + // Player: dash direction is relative to look direction float baseYaw; - if (target != null) { + if (!isPlayerCaster(caster) && target != null) { double dx = target.posX - caster.posX; double dz = target.posZ - caster.posZ; baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); 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 22832a104..21d4073ed 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 @@ -169,6 +169,11 @@ public class AbilityDisc extends Ability implements IAbilityDisc { return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } + /** + * Create disc entities. + * NPC: target is the aggro target — discs will home toward it. + * Player: target is null — discs fire in caster's look direction. + */ private EntityAbilityDisc[] createDiscEntities(EntityLivingBase caster, EntityLivingBase target, World world) { EntityAbilityDisc[] entities = new EntityAbilityDisc[projectileCount]; for (int i = 0; i < projectileCount; i++) { @@ -301,7 +306,9 @@ public class AbilityDisc extends Ability implements IAbilityDisc { @Override public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + // NPC: telegraph on aggro target position + // Player: no telegraph (discs fire in look direction, no fixed position to mark) + if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) { return null; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 70696eb8f..58c2560a4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -111,6 +112,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { this.cooldownTicks = 0; this.windUpTicks = 30; this.telegraphType = TelegraphType.CIRCLE; + this.allowedBy = UserType.NPC_ONLY; } @Override @@ -202,14 +204,12 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } break; case AT_TARGET: - // Use telegraph position if available, apply offset if (telegraph != null) { double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(), minOffset, maxOffset, randomOffset, RANDOM); zoneX = pos[0]; zoneY = pos[1]; zoneZ = pos[2]; - // Update telegraph to show actual hazard position telegraph.setX(zoneX); telegraph.setY(zoneY); telegraph.setZ(zoneZ); 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 7ddfcc0e7..afffa9c13 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 @@ -88,6 +88,11 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { return laserWidth * 2.0f; // Make telegraph slightly wider for visibility } + /** + * Spawn laser entity. + * NPC: target is the aggro target — laser expands toward it. + * Player: target is null — laser expands in caster's look direction. + */ private EntityAbilityLaser spawnLaserEntity(EntityLivingBase caster, EntityLivingBase target, World world) { Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); EntityAbilityLaser entity = new EntityAbilityLaser( 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 e0f6bc19f..17b878305 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 @@ -172,6 +172,11 @@ public class AbilityOrb extends Ability implements IAbilityOrb { return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } + /** + * Create orb entities. + * NPC: target is the aggro target — orbs will home toward it. + * Player: target is null — orbs fire in caster's look direction. + */ private EntityAbilityOrb[] createOrbEntities(EntityLivingBase caster, EntityLivingBase target, World world) { EntityAbilityOrb[] entities = new EntityAbilityOrb[projectileCount]; for (int i = 0; i < projectileCount; i++) { @@ -302,7 +307,9 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + // NPC: telegraph on aggro target position + // Player: no telegraph (orbs fire in look direction, no fixed position to mark) + if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) { return null; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index b6657ac60..8abd5d01e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -9,6 +9,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; @@ -67,68 +68,131 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - if ((world.isRemote && !isPreview()) || target == null) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } - if (!isPreview()) { - // Calculate direction to target - double dx = target.posX - caster.posX; - double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); - double dz = target.posZ - caster.posZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - - if (len > 0) { - dx /= len; - dy /= len; - dz /= len; - } - - // Deal instant damage with scripted event support - // TODO: Use custom EntityAbilityProjectile for actual tracking - applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); - - // Play sound - world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); - - // Handle splash damage for explosive projectiles - if (explosive && explosionRadius > 0) { - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABBExcludingEntity(target, - target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); - - for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != caster) { - EntityLivingBase living = (EntityLivingBase) entity; - float dist = target.getDistanceToEntity(living); - if (dist < explosionRadius) { - float falloff = 1.0f - (dist / explosionRadius); - // Apply splash damage with scripted event support (no knockback) - applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); - } - } - } - - // Explosion particles - world.playSoundAtEntity(target, "random.explode", 0.5f, 1.0f); - for (int i = 0; i < 10; i++) { - world.spawnParticle("explode", - target.posX + (world.rand.nextDouble() - 0.5) * explosionRadius, - target.posY + world.rand.nextDouble() * target.height, - target.posZ + (world.rand.nextDouble() - 0.5) * explosionRadius, - 0, 0.1, 0); - } - } - - // Spawn projectile particles (visual trail) - spawnProjectileParticles(world, caster, target); + if (isPlayerCaster(caster)) { + // Player: instant hit-scan in look direction + executePlayerProjectile(caster, world); + } else if (target != null) { + // NPC: instant hit-scan on aggro target + executeNpcProjectile(caster, target, world); } - // Projectile is instant signalCompletion(); } + /** + * NPC projectile: instant hit-scan damage on the aggro target. + */ + private void executeNpcProjectile(EntityLivingBase caster, EntityLivingBase target, World world) { + if (isPreview()) return; + + double dx = target.posX - caster.posX; + double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); + double dz = target.posZ - caster.posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { dx /= len; dy /= len; dz /= len; } + + applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); + world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); + + if (explosive && explosionRadius > 0) { + applyExplosionDamage(caster, target, world, target.posX, target.posY, target.posZ); + } + + spawnProjectileParticles(world, caster, target); + } + + /** + * Player projectile: fires instant hit-scan in look direction. + * Raycasts for the first entity in range and applies damage. + */ + private void executePlayerProjectile(EntityLivingBase caster, World world) { + if (isPreview()) return; + + Vec3 look = caster.getLookVec(); + double eyeX = caster.posX; + double eyeY = caster.posY + caster.getEyeHeight(); + double eyeZ = caster.posZ; + + // Raycast: find the closest entity along look direction + EntityLivingBase hitTarget = null; + double closestDist = maxRange; + + @SuppressWarnings("unchecked") + List candidates = world.getEntitiesWithinAABBExcludingEntity(caster, + caster.boundingBox.expand(maxRange, maxRange, maxRange)); + + for (Entity entity : candidates) { + if (!(entity instanceof EntityLivingBase)) continue; + if (entity.isDead) continue; + + // Check if entity is roughly along look direction + double dx = entity.posX - eyeX; + double dy = (entity.posY + entity.height / 2) - eyeY; + double dz = entity.posZ - eyeZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (dist > maxRange || dist < 0.5) continue; + + // Dot product to check alignment with look vector + double dot = (dx * look.xCoord + dy * look.yCoord + dz * look.zCoord) / dist; + if (dot < 0.95) continue; // Must be within ~18 degree cone + + if (dist < closestDist) { + closestDist = dist; + hitTarget = (EntityLivingBase) entity; + } + } + + world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); + + if (hitTarget != null) { + applyAbilityDamageWithDirection(caster, hitTarget, damage, knockback, look.xCoord, look.zCoord); + + if (explosive && explosionRadius > 0) { + applyExplosionDamage(caster, hitTarget, world, hitTarget.posX, hitTarget.posY, hitTarget.posZ); + } + + spawnProjectileParticles(world, caster, hitTarget); + } else { + // No hit — spawn particles along look direction anyway + spawnProjectileParticlesInDirection(world, caster, look); + } + } + + /** + * Apply explosion splash damage around a point. + */ + private void applyExplosionDamage(EntityLivingBase caster, EntityLivingBase primaryTarget, World world, + double x, double y, double z) { + @SuppressWarnings("unchecked") + List entities = world.getEntitiesWithinAABBExcludingEntity(primaryTarget, + primaryTarget.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); + + for (Entity entity : entities) { + if (entity instanceof EntityLivingBase && entity != caster) { + EntityLivingBase living = (EntityLivingBase) entity; + float dist = primaryTarget.getDistanceToEntity(living); + if (dist < explosionRadius) { + float falloff = 1.0f - (dist / explosionRadius); + applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); + } + } + } + + world.playSoundAtEntity(primaryTarget, "random.explode", 0.5f, 1.0f); + for (int i = 0; i < 10; i++) { + world.spawnParticle("explode", + x + (world.rand.nextDouble() - 0.5) * explosionRadius, + y + world.rand.nextDouble() * 2, + z + (world.rand.nextDouble() - 0.5) * explosionRadius, + 0, 0.1, 0); + } + } + private void spawnProjectileParticles(World world, EntityLivingBase caster, EntityLivingBase target) { double startX = caster.posX; double startY = caster.posY + caster.getEyeHeight(); @@ -160,6 +224,31 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { } } + /** + * Spawn projectile particles along a direction (player miss — no entity hit). + */ + private void spawnProjectileParticlesInDirection(World world, EntityLivingBase caster, Vec3 look) { + double startX = caster.posX; + double startY = caster.posY + caster.getEyeHeight(); + double startZ = caster.posZ; + + String particle = "flame"; + if (projectileType.equals("arrow")) { + particle = "crit"; + } else if (projectileType.equals("magic")) { + particle = "witchMagic"; + } + + int steps = (int) Math.max(5, maxRange * 2); + for (int i = 0; i < steps; i++) { + double progress = (double) i / steps * maxRange; + double px = startX + look.xCoord * progress; + double py = startY + look.yCoord * progress; + double pz = startZ + look.zCoord * progress; + world.spawnParticle(particle, px, py, pz, 0, 0, 0); + } + } + @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Projectile is instant, nothing to do per-tick diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 15b4060b8..a03ecdf6a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -85,51 +85,74 @@ public class AbilitySlam extends Ability implements IAbilitySlam { @Override public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - // Update target position during windup - if (targetingMode == TargetingMode.AOE_SELF) { - // AOE_SELF: slam at caster's current position + if (isPlayerCaster(caster)) { + // Player: slam will launch from current position, no target tracking needed targetX = caster.posX; targetY = caster.posY; targetZ = caster.posZ; - } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { - // AOE_TARGET: telegraph follows target via setEntityIdToFollow - targetX = target.posX; - targetY = target.posY; - targetZ = target.posZ; + } else { + // NPC: update target position during windup for telegraph tracking + if (targetingMode == TargetingMode.AOE_SELF) { + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; + } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { + targetX = target.posX; + targetY = target.posY; + targetZ = target.posZ; + } } } @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - // Lock in the destination at moment of launch + hasLaunched = false; + hasLanded = false; + airTicks = 0; + caster.fallDistance = 0; + + if (isPlayerCaster(caster)) { + executePlayerSlam(caster); + } else { + executeNpcSlam(caster, target); + } + } + + /** + * NPC slam: Lock destination based on targeting mode, then launch in a ballistic arc toward it. + */ + private void executeNpcSlam(EntityLivingBase caster, EntityLivingBase target) { if (targetingMode == TargetingMode.AOE_SELF) { - // AOE_SELF: slam in place (just jump up) targetX = caster.posX; targetY = caster.posY; targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { - // AOE_TARGET: leap to target's current position targetX = target.posX; targetY = target.posY; targetZ = target.posZ; } else { - // Fallback: slam in place targetX = caster.posX; targetY = caster.posY; targetZ = caster.posZ; } - - hasLaunched = false; - hasLanded = false; - airTicks = 0; - - // Reset fall distance to prevent fall damage during slam - caster.fallDistance = 0; - - // Calculate and apply leap velocity launchTowardTarget(caster); } + /** + * Player slam: Launch straight up. Player controls horizontal movement via WASD. + * AOE damage triggers wherever the player lands. + */ + private void executePlayerSlam(EntityLivingBase caster) { + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; + + double vy = calculateLaunchVelocity(Math.max(1.0, leapHeight)); + caster.motionY = vy; + hasLaunched = true; + caster.velocityChanged = true; + } + /** * Calculate the initial vertical velocity needed to reach a target height * in Minecraft's physics (gravity=0.08, vertical drag=0.98). @@ -273,8 +296,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam { return; } - // While in air, face toward target - if (!isPreview()) { + // NPC: face toward target destination while in air + // Player: free look — player controls their own camera + if (!isPreview() && !isPlayerCaster(caster)) { double dx = targetX - caster.posX; double dz = targetZ - caster.posZ; float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); @@ -441,11 +465,11 @@ public class AbilitySlam extends Ability implements IAbilitySlam { TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, caster.rotationYaw); instance.setCasterEntityId(caster.getEntityId()); - // AOE_TARGET: telegraph follows target during windup - if (targetingMode == TargetingMode.AOE_TARGET && target != null) { + // NPC AOE_TARGET: telegraph follows target during windup + // Player: telegraph stays at caster position (no target to follow) + if (!isPlayerCaster(caster) && targetingMode == TargetingMode.AOE_TARGET && target != null) { instance.setEntityIdToFollow(target.getEntityId()); } - // AOE_SELF: telegraph stays at caster position, no follow return instance; } 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 9be02101f..8638dd2a4 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 @@ -83,7 +83,9 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { return; } - // Spawn the entity that handles BOTH visuals AND damage + // Spawn the sweeper entity that handles BOTH visuals AND damage. + // NPC: target is the aggro target — lockOnTarget tracks it during sweep. + // Player: target is null — sweep rotates around caster's facing direction, lockOnTarget has no effect. activeEntity = new EntityAbilitySweeper(world, caster, target, beamLength, beamWidth, beamHeight, colorData, diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index fe847bf3a..8e09c43e7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.block.Block; import net.minecraft.entity.Entity; @@ -72,6 +73,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { this.typeId = "ability.cnpc.teleport"; this.name = "Teleport"; this.targetingMode = TargetingMode.AGGRO_TARGET; + this.allowedBy = UserType.NPC_ONLY; this.maxRange = 30.0f; this.minRange = 5.0f; this.cooldownTicks = 0; @@ -228,26 +230,8 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { } } - // No target (player use) - teleport in caster's look direction - Vec3 look = caster.getLookVec(); - switch (mode) { - case BEHIND: - // Teleport forward in look direction by behindDistance - newX = caster.posX + look.xCoord * behindDistance; - newZ = caster.posZ + look.zCoord * behindDistance; - newY = caster.posY; - return Vec3.createVectorHelper(newX, newY, newZ); - case SINGLE: - case BLINK: - default: - // Teleport forward in look direction within blinkRadius - double dist = Math.min(blinkRadius, maxRange); - double offset = (RANDOM.nextDouble() * 0.5 + 0.5) * dist; // 50-100% of max distance - newX = caster.posX + look.xCoord * offset; - newZ = caster.posZ + look.zCoord * offset; - newY = caster.posY; - return Vec3.createVectorHelper(newX, newY, newZ); - } + // NPC-only ability — should not reach here + return null; } private Vec3 findSafeDestination(World world, double oldX, double oldY, double oldZ, diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 5a540d634..494192e04 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -83,6 +84,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap { this.cooldownTicks = 0; this.windUpTicks = 20; this.telegraphType = TelegraphType.CIRCLE; + this.allowedBy = UserType.NPC_ONLY; } @Override @@ -128,7 +130,6 @@ public class AbilityTrap extends Ability implements IAbilityTrap { } break; case AT_TARGET: - // Telegraph follows target during windup, locks on execute if (target != null) { instance.setEntityIdToFollow(target.getEntityId()); } @@ -154,14 +155,12 @@ public class AbilityTrap extends Ability implements IAbilityTrap { trapZ = caster.posZ; break; case AT_TARGET: - // Use telegraph position with offset if (telegraph != null) { double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(), minOffset, maxOffset, randomOffset, RANDOM); trapX = pos[0]; trapY = pos[1]; trapZ = pos[2]; - // Update telegraph to show actual trap position telegraph.setX(trapX); telegraph.setY(trapY); telegraph.setZ(trapZ); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index 73f9816f7..90aaf054b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -103,8 +103,9 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } } } else { - // Single target mode - still check pullRadius - if (target != null && !target.isDead) { + // NPC single-target mode: pull the aggro target + // Player: single-target mode has no effect (no target to pull — use AOE mode instead) + if (!isPlayerCaster(caster) && target != null && !target.isDead) { double dist = caster.getDistanceToEntity(target); if (dist <= pullRadius) { getPulledEntities().add(target.getUniqueID()); From d76f5d40a4eb8e30c0961086572e1aed90c24df5 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 8 Feb 2026 01:46:13 -0500 Subject: [PATCH 15/34] Prevent Passive Factions from taking Damage --- .../noppes/npcs/client/gui/global/GuiNPCManageFactions.java | 2 ++ src/main/java/noppes/npcs/entity/EntityNPCInterface.java | 3 +++ src/main/resources/assets/customnpcs/lang/en_US.lang | 4 +++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageFactions.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageFactions.java index 246e528f3..59adbdd91 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageFactions.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageFactions.java @@ -85,11 +85,13 @@ public class GuiNPCManageFactions extends GuiNPCInterface2 implements IScrollDat addLabel(new GuiNpcLabel(3, "faction.hidden", guiLeft + 8, y + 5)); this.addButton(new GuiNpcButton(3, guiLeft + 100, y, 45, 20, new String[]{"gui.no", "gui.yes"}, faction.hideFaction ? 1 : 0)); + getButton(3).setHoverText("faction.hidden.hover"); y += 23; addLabel(new GuiNpcLabel(4, "faction.attacked", guiLeft + 8, y + 5)); this.addButton(new GuiNpcButton(4, guiLeft + 100, y, 45, 20, new String[]{"gui.no", "gui.yes"}, faction.getsAttacked ? 1 : 0)); + getButton(4).setHoverText("faction.attacked.hover"); y += 23; diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index faaa2c9f5..6e4b40884 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -678,6 +678,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti if (entity instanceof EntityLivingBase) attackingEntity = (EntityLivingBase) entity; + if (faction.isPassive) + return false; + if (attackingEntity != null && attackingEntity == getOwner()) return false; else if (attackingEntity instanceof EntityNPCInterface) { diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 3238d2851..3bc0fa119 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -639,6 +639,7 @@ faction.friendly=Friendly faction.points=Points faction.ondeath=On death faction.hidden=Hidden +faction.hidden.hover=Hides this faction from the player's faction standings UI faction.default=Default faction.attackHostile=Attack hostile factions faction.defend=Defence faction members @@ -647,8 +648,9 @@ faction.options=Faction Options faction.increasepoints=Faction %s increased by %s points faction.decreasepoints=Faction %s decreased by %s points faction.attacked=Attacked by mobs +faction.attacked.hover=Whether vanilla mobs will target and attack NPCs in this faction faction.passive=Is Passive -faction.passive.hover=Passive factions will not queue any attacking tasks [Good for Performance] +faction.passive.hover=Passive faction NPCs cannot be damaged and will not queue any attacking tasks [Good for Performance] menu.inventory=Inventory menu.display=Display From 974cfc8c568ccfe685931a6b3291ea2871ffb9e1 Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 10:59:42 -0300 Subject: [PATCH 16/34] Changed fire delay for dual disc --- .../data/ability/type/AbilityDisc.java | 2 +- .../animations/Ability_DiscDual_Active.json | 256 +++++------------ .../animations/Ability_DiscDual_Windup.json | 264 +++++++++--------- 3 files changed, 198 insertions(+), 324 deletions(-) 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 21d4073ed..f890b7d7b 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 @@ -138,7 +138,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { AbilityDisc disc = (AbilityDisc) a; a.setName("Dual Disc"); disc.setProjectileCount(2); - disc.setFireDelay(10); + disc.setFireDelay(5); // Set color override for second disc disc.projectiles[1].colorOverride = true; disc.projectiles[1].innerColor = 0xFFFFFF; diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json index 8fbaa73da..98a3299a2 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json @@ -1,17 +1,17 @@ { - "Loop": 5, + "Loop": -1, "Speed": 1.0f, "Frames": [ { "ColorMarker": 16777215, "Comment": "", - "Speed": 2.0f, + "Speed": 3.0f, "FrameParts": [ { - "Pivot2": -5.0f, - "Part": "BODY", - "Rotation1": -37.0f, - "Rotation0": 37.0f, + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, "Pivot0": 3.0f, "Pivot1": 2.0f, "Rotation2": 0.0f @@ -25,15 +25,6 @@ "Pivot1": 2.0f, "Rotation2": 56.0f }, - { - "Pivot2": 0.0f, - "Part": "LEFT_LEG", - "Rotation1": -37.0f, - "Rotation0": 31.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": -25.0f - }, { "Pivot2": 0.0f, "Part": "LEFT_ARM", @@ -43,33 +34,6 @@ "Pivot1": 1.0f, "Rotation2": 0.0f }, - { - "Pivot2": -1.0f, - "Part": "RIGHT_LEG", - "Rotation1": -18.0f, - "Rotation0": 12.0f, - "Pivot0": -1.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -6.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 18.0f, - "Pivot0": 3.0f, - "Pivot1": 2.0f, - "Rotation2": 0.0f - } - ], - "Smooth": 0b, - "Duration": 2 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 1.5f, - "FrameParts": [ { "Pivot2": -5.0f, "Part": "BODY", @@ -79,33 +43,6 @@ "Pivot1": 2.0f, "Rotation2": 0.0f }, - { - "Pivot2": -7.0f, - "Part": "RIGHT_ARM", - "Rotation1": 0.0f, - "Rotation0": 25.0f, - "Pivot0": 3.0f, - "Pivot1": 2.0f, - "Rotation2": 56.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_LEG", - "Rotation1": -37.0f, - "Rotation0": 31.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": -25.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": 0.0f, - "Rotation0": 120.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, { "Pivot2": -1.0f, "Part": "RIGHT_LEG", @@ -115,42 +52,6 @@ "Pivot1": 0.0f, "Rotation2": 0.0f }, - { - "Pivot2": -6.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 18.0f, - "Pivot0": 3.0f, - "Pivot1": 2.0f, - "Rotation2": 0.0f - } - ], - "Smooth": 0b, - "Duration": 4 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.5f, - "FrameParts": [ - { - "Pivot2": -5.0f, - "Part": "BODY", - "Rotation1": -37.0f, - "Rotation0": 37.0f, - "Pivot0": 3.0f, - "Pivot1": 2.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -7.0f, - "Part": "RIGHT_ARM", - "Rotation1": 0.0f, - "Rotation0": 25.0f, - "Pivot0": 3.0f, - "Pivot1": 2.0f, - "Rotation2": 56.0f - }, { "Pivot2": 0.0f, "Part": "LEFT_LEG", @@ -159,37 +60,10 @@ "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": -25.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": 0.0f, - "Rotation0": 120.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "RIGHT_LEG", - "Rotation1": -18.0f, - "Rotation0": 12.0f, - "Pivot0": -1.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -6.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 18.0f, - "Pivot0": 3.0f, - "Pivot1": 2.0f, - "Rotation2": 0.0f } ], "Smooth": 0b, - "Duration": 4 + "Duration": 3 }, { "ColorMarker": 16777215, @@ -198,8 +72,8 @@ "FrameParts": [ { "Pivot2": 2.0f, - "Part": "BODY", - "Rotation1": -25.0f, + "Part": "HEAD", + "Rotation1": 0.0f, "Rotation0": -12.0f, "Pivot0": -1.0f, "Pivot1": 1.0f, @@ -214,15 +88,6 @@ "Pivot1": 0.0f, "Rotation2": 75.0f }, - { - "Pivot2": 0.0f, - "Part": "LEFT_LEG", - "Rotation1": -25.0f, - "Rotation0": 12.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": -12.0f - }, { "Pivot2": 2.0f, "Part": "LEFT_ARM", @@ -232,6 +97,15 @@ "Pivot1": 1.0f, "Rotation2": 0.0f }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -242,17 +116,17 @@ "Rotation2": 6.0f }, { - "Pivot2": 2.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f } ], "Smooth": 0b, - "Duration": 2 + "Duration": 3 }, { "ColorMarker": 16777215, @@ -261,8 +135,8 @@ "FrameParts": [ { "Pivot2": 2.0f, - "Part": "BODY", - "Rotation1": -25.0f, + "Part": "HEAD", + "Rotation1": 0.0f, "Rotation0": -12.0f, "Pivot0": -1.0f, "Pivot1": 1.0f, @@ -277,15 +151,6 @@ "Pivot1": 0.0f, "Rotation2": 75.0f }, - { - "Pivot2": 0.0f, - "Part": "LEFT_LEG", - "Rotation1": -25.0f, - "Rotation0": 12.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": -12.0f - }, { "Pivot2": 2.0f, "Part": "LEFT_ARM", @@ -295,6 +160,15 @@ "Pivot1": 1.0f, "Rotation2": 0.0f }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -305,27 +179,27 @@ "Rotation2": 6.0f }, { - "Pivot2": 2.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f } ], "Smooth": 0b, - "Duration": 4 + "Duration": 3 }, { "ColorMarker": 16777215, "Comment": "", - "Speed": 0.5f, + "Speed": 0.25f, "FrameParts": [ { "Pivot2": 2.0f, - "Part": "BODY", - "Rotation1": -25.0f, + "Part": "HEAD", + "Rotation1": 0.0f, "Rotation0": -12.0f, "Pivot0": -1.0f, "Pivot1": 1.0f, @@ -340,15 +214,6 @@ "Pivot1": 0.0f, "Rotation2": 75.0f }, - { - "Pivot2": 0.0f, - "Part": "LEFT_LEG", - "Rotation1": -25.0f, - "Rotation0": 12.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": -12.0f - }, { "Pivot2": 2.0f, "Part": "LEFT_ARM", @@ -358,6 +223,15 @@ "Pivot1": 1.0f, "Rotation2": 0.0f }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -368,17 +242,17 @@ "Rotation2": 6.0f }, { - "Pivot2": 2.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f } ], "Smooth": 0b, - "Duration": 4 + "Duration": 3 } ], "CurrentFrameTime": 0, diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json index 080306c95..0dc050b19 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json @@ -7,51 +7,6 @@ "Comment": "", "Speed": 0.5f, "FrameParts": [ - { - "Pivot2": -8.0f, - "Part": "BODY", - "Rotation1": 25.0f, - "Rotation0": 31.0f, - "Pivot0": -2.0f, - "Pivot1": 2.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -5.0f, - "Part": "RIGHT_ARM", - "Rotation1": 0.0f, - "Rotation0": 50.0f, - "Pivot0": 0.0f, - "Pivot1": 2.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -2.0f, - "Part": "LEFT_LEG", - "Rotation1": 0.0f, - "Rotation0": 25.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": -31.0f - }, - { - "Pivot2": -8.0f, - "Part": "LEFT_ARM", - "Rotation1": 31.0f, - "Rotation0": -56.0f, - "Pivot0": -2.0f, - "Pivot1": 2.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -3.0f, - "Part": "RIGHT_LEG", - "Rotation1": 25.0f, - "Rotation0": 18.0f, - "Pivot0": -1.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, { "Pivot2": 0.0f, "Part": "FULL_MODEL", @@ -69,6 +24,51 @@ "Pivot0": -2.0f, "Pivot1": 2.0f, "Rotation2": -12.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -8.0f, + "Part": "LEFT_ARM", + "Rotation1": 31.0f, + "Rotation0": -56.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -8.0f, + "Part": "BODY", + "Rotation1": 25.0f, + "Rotation0": 31.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": 25.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -31.0f } ], "Smooth": 0b, @@ -79,51 +79,6 @@ "Comment": "", "Speed": 0.35f, "FrameParts": [ - { - "Pivot2": 0.0f, - "Part": "BODY", - "Rotation1": 50.0f, - "Rotation0": -6.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": 5.0f, - "Part": "RIGHT_ARM", - "Rotation1": 0.0f, - "Rotation0": 50.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": 170.0f - }, - { - "Pivot2": -3.0f, - "Part": "LEFT_ARM", - "Rotation1": 75.0f, - "Rotation0": -107.0f, - "Pivot0": -6.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -6.0f, - "Rotation0": 12.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": 0.0f, - "Part": "RIGHT_LEG", - "Rotation1": 50.0f, - "Rotation0": 18.0f, - "Pivot0": -3.0f, - "Pivot1": -4.0f, - "Rotation2": 0.0f - }, { "Pivot2": 0.0f, "Part": "FULL_MODEL", @@ -141,6 +96,51 @@ "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 170.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 75.0f, + "Rotation0": -107.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 50.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 50.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": -4.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -6.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f } ], "Smooth": 0b, @@ -153,9 +153,18 @@ "FrameParts": [ { "Pivot2": 0.0f, - "Part": "BODY", - "Rotation1": 50.0f, - "Rotation0": -6.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f @@ -179,10 +188,10 @@ "Rotation2": 0.0f }, { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -6.0f, - "Rotation0": 12.0f, + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 50.0f, + "Rotation0": -6.0f, "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f @@ -197,19 +206,10 @@ "Rotation2": 0.0f }, { - "Pivot2": 0.0f, - "Part": "FULL_MODEL", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": 0.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -6.0f, + "Rotation0": 12.0f, "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f @@ -221,13 +221,13 @@ { "ColorMarker": 16777215, "Comment": "", - "Speed": 2.0f, + "Speed": 3.0f, "FrameParts": [ { - "Pivot2": -5.0f, - "Part": "BODY", - "Rotation1": -37.0f, - "Rotation0": 37.0f, + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, "Pivot0": 3.0f, "Pivot1": 2.0f, "Rotation2": 0.0f @@ -241,15 +241,6 @@ "Pivot1": 2.0f, "Rotation2": 56.0f }, - { - "Pivot2": 0.0f, - "Part": "LEFT_LEG", - "Rotation1": -37.0f, - "Rotation0": 31.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": -25.0f - }, { "Pivot2": 0.0f, "Part": "LEFT_ARM", @@ -259,6 +250,15 @@ "Pivot1": 1.0f, "Rotation2": 0.0f }, + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, { "Pivot2": -1.0f, "Part": "RIGHT_LEG", @@ -269,13 +269,13 @@ "Rotation2": 0.0f }, { - "Pivot2": -6.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 18.0f, - "Pivot0": 3.0f, - "Pivot1": 2.0f, - "Rotation2": 0.0f + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f } ], "Smooth": 0b, @@ -283,7 +283,7 @@ } ], "CurrentFrameTime": 0, - "CurrentFrame": 1, + "CurrentFrame": 3, "Smooth": 0b, "ID": 17, "WhileAttacking": 1b, From 91762bb1bacc13b7a7ee390cff9ab9b28d5eeb84 Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 11:28:00 -0300 Subject: [PATCH 17/34] Hazard animation --- .../data/ability/type/AbilityHazard.java | 2 + .../animations/Ability_Hazard_Active.json | 266 ++++++++++++ .../animations/Ability_Hazard_Windup.json | 392 ++++++++++++++++++ 3 files changed, 660 insertions(+) create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Hazard_Active.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Hazard_Windup.json diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 58c2560a4..f1ae7e719 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -113,6 +113,8 @@ public class AbilityHazard extends Ability implements IAbilityHazard { this.windUpTicks = 30; this.telegraphType = TelegraphType.CIRCLE; this.allowedBy = UserType.NPC_ONLY; + this.windUpAnimationName = "Ability_Hazard_Windup"; + this.activeAnimationName = "Ability_Hazard_Active"; } @Override diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Hazard_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Hazard_Active.json new file mode 100644 index 000000000..11cb0ef7a --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Hazard_Active.json @@ -0,0 +1,266 @@ +{ + "Loop": 3, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": -12.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 82.0f, + "Pivot0": 0.0f, + "Pivot1": 12.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 25.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": -10.0f, + "Part": "LEFT_ARM", + "Rotation1": 6.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 12.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -11.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 101.0f, + "Pivot0": 0.0f, + "Pivot1": 12.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -25.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": -10.0f, + "Part": "RIGHT_ARM", + "Rotation1": -6.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": 12.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 10 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -56.0f, + "Rotation0": -132.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 56.0f, + "Rotation0": -132.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -56.0f, + "Rotation0": -132.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 56.0f, + "Rotation0": -132.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -56.0f, + "Rotation0": -37.0f, + "Pivot0": -2.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 56.0f, + "Rotation0": -37.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 5 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 24, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Hazard_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Hazard_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Hazard_Windup.json new file mode 100644 index 000000000..8a16135a4 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Hazard_Windup.json @@ -0,0 +1,392 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 12.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 37.0f, + "Rotation0": -37.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -12.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -37.0f, + "Rotation0": -37.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.25f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -56.0f, + "Rotation0": -132.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 56.0f, + "Rotation0": -132.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -56.0f, + "Rotation0": -132.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 56.0f, + "Rotation0": -132.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.75f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -56.0f, + "Rotation0": -132.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 56.0f, + "Rotation0": -132.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -56.0f, + "Rotation0": -132.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 56.0f, + "Rotation0": -132.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -12.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 82.0f, + "Pivot0": 0.0f, + "Pivot1": 12.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 25.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": -10.0f, + "Part": "LEFT_ARM", + "Rotation1": 6.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 12.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -11.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 101.0f, + "Pivot0": 0.0f, + "Pivot1": 12.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -25.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": -10.0f, + "Part": "RIGHT_ARM", + "Rotation1": -6.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": 12.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 5, + "Smooth": 0b, + "ID": 23, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Hazard_Windup", + "WhileStanding": 1b +} From eee9b32a5b2a5c32ad44a54a20586b072118c8ce Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 12:14:21 -0300 Subject: [PATCH 18/34] Added eye anchor point using the entity's eye height --- .../kamkeel/npcs/controllers/data/ability/AnchorPoint.java | 4 +++- src/main/java/kamkeel/npcs/util/AnchorPointHelper.java | 7 +++++++ src/main/resources/assets/customnpcs/lang/en_US.lang | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java index 1c308493c..58e54b5aa 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java @@ -10,7 +10,8 @@ public enum AnchorPoint { RIGHT_HAND("Right Hand"), // Right arm/hand position LEFT_HAND("Left Hand"), // Left arm/hand position ABOVE_HEAD("Above Head"), // Above the entity - CHEST("Chest"); // Chest level, centered + CHEST("Chest"), // Chest level, centered + EYE("Eye"); // Eye level, centered private final String displayName; @@ -55,6 +56,7 @@ public enum AnchorPoint { case LEFT_HAND: return "ability.anchor.leftHand"; case ABOVE_HEAD: return "ability.anchor.aboveHead"; case CHEST: return "ability.anchor.chest"; + case EYE: return "ability.anchor.eye"; default: return name(); } } diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 5ee5936b1..20d557bef 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -37,6 +37,7 @@ public class AnchorPointHelper { private static final float ARM_HEIGHT = 0.75f; private static final float ABOVE_HEAD_HEIGHT = 1.2f; private static final float CHEST_HEIGHT = 0.65f; + private static final float EYE_HEIGHT = 0.85f; // Distance in front for FRONT anchor private static final float DEFAULT_FRONT_DISTANCE = 1.0f; @@ -98,6 +99,10 @@ public class AnchorPointHelper { case CHEST: y += height * CHEST_HEIGHT; break; + + case EYE: + y += entity.getEyeHeight(); + break; } // Rotate offsets relative to entity's body yaw @@ -347,6 +352,8 @@ public class AnchorPointHelper { return ABOVE_HEAD_HEIGHT; case CHEST: return CHEST_HEIGHT; + case EYE: + return EYE_HEIGHT; default: return FRONT_HEIGHT; } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 3bc0fa119..9319f1b36 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1549,6 +1549,7 @@ ability.anchor.rightHand=Right Hand ability.anchor.leftHand=Left Hand ability.anchor.aboveHead=Above Head ability.anchor.chest=Chest +ability.anchor.eye=Eye # Sweep Mode ability.sweep.swipe=Swipe From 0dc6f9aab30fe3b2a4e53f8346cfb64387ece2fc Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 12:38:13 -0300 Subject: [PATCH 19/34] Orb Barrage animation --- .../data/ability/type/AbilityOrb.java | 6 +- .../animations/Ability_OrbBarrage_Active.json | 203 ++++++++++++++++++ .../animations/Ability_OrbBarrage_Windup.json | 201 +++++++++++++++++ 3 files changed, 407 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_OrbBarrage_Active.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_OrbBarrage_Windup.json 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 17b878305..c4a4a1ebb 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 @@ -143,11 +143,11 @@ public class AbilityOrb extends Ability implements IAbilityOrb { a.setMaxRange(75.0f); a.setBurstEnabled(true); a.setBurstAmount(15); - a.setBurstDelay(10); + a.setBurstDelay(5); a.setBurstReplayAnimations(false); a.setBurstOverlap(true); - a.setWindUpAnimationName("Ability_BeamDual_Windup"); - a.setActiveAnimationName("Ability_BeamDual_Active"); + a.setWindUpAnimationName("Ability_OrbBarrage_Windup"); + a.setActiveAnimationName("Ability_OrbBarrage_Active"); }) ); } diff --git a/src/main/resources/assets/customnpcs/animations/Ability_OrbBarrage_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_OrbBarrage_Active.json new file mode 100644 index 000000000..0a0ba828c --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_OrbBarrage_Active.json @@ -0,0 +1,203 @@ +{ + "Loop": 1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -7.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -6.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_ARM", + "Rotation1": -25.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -25.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -7.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 25.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": 25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -6.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 5 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -7.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -6.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_ARM", + "Rotation1": -25.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -25.0f + } + ], + "Smooth": 0b, + "Duration": 5 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 26, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_OrbBarrage_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_OrbBarrage_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_OrbBarrage_Windup.json new file mode 100644 index 000000000..dfe8f601b --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_OrbBarrage_Windup.json @@ -0,0 +1,201 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 56.0f, + "Rotation0": -126.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -56.0f, + "Rotation0": -126.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 12.0f + } + ], + "Duration": 8 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.25f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 56.0f, + "Rotation0": -126.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -56.0f, + "Rotation0": -126.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 12.0f + } + ], + "Smooth": 0b, + "Duration": 19 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -7.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 31.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -6.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_ARM", + "Rotation1": -25.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -25.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 2, + "Smooth": 0b, + "ID": 25, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_OrbBarrage_Windup", + "WhileStanding": 1b +} From 171578ad9b3b8c8f974424d7555ab2c0a8505074 Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 12:52:58 -0300 Subject: [PATCH 20/34] Fixed heavy hit animation --- .../animations/Ability_HeavyHit_Windup.json | 1181 +---------------- 1 file changed, 55 insertions(+), 1126 deletions(-) diff --git a/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json index 15d49bce6..a276742ba 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json @@ -7,24 +7,6 @@ "Comment": "", "Speed": 0.8f, "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 31.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -12.0f - }, { "Pivot2": -1.0f, "Part": "HEAD", @@ -34,15 +16,6 @@ "Pivot1": 1.0f, "Rotation2": 0.0f }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 6.0f - }, { "Pivot2": -1.0f, "Part": "BODY", @@ -60,24 +33,6 @@ "Pivot0": 0.0f, "Pivot1": 1.0f, "Rotation2": 6.0f - } - ], - "Smooth": 0b, - "Duration": 10 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f }, { "Pivot2": -4.0f, @@ -86,16 +41,16 @@ "Rotation0": 31.0f, "Pivot0": 2.0f, "Pivot1": 0.0f, - "Rotation2": -18.0f + "Rotation2": -12.0f }, { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 31.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, "Pivot1": 1.0f, - "Rotation2": -6.0f + "Rotation2": -37.0f }, { "Pivot2": -2.0f, @@ -104,7 +59,25 @@ "Rotation0": 25.0f, "Pivot0": -2.0f, "Pivot1": -1.0f, - "Rotation2": 0.0f + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 13 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f }, { "Pivot2": -1.0f, @@ -123,24 +96,6 @@ "Pivot0": 0.0f, "Pivot1": 1.0f, "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f }, { "Pivot2": -4.0f, @@ -151,51 +106,6 @@ "Pivot1": 0.0f, "Rotation2": -18.0f }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ { "Pivot2": 3.0f, "Part": "RIGHT_ARM", @@ -205,24 +115,6 @@ "Pivot1": 1.0f, "Rotation2": -37.0f }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, { "Pivot2": -2.0f, "Part": "RIGHT_LEG", @@ -231,997 +123,16 @@ "Pivot0": -2.0f, "Pivot1": -1.0f, "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f } ], "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.1f, - "FrameParts": [ - { - "Pivot2": 3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 40.0f, - "Rotation0": -107.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -37.0f - }, - { - "Pivot2": -4.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -1.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 37.0f, - "Rotation0": 25.0f, - "Pivot0": -2.0f, - "Pivot1": -1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -1.0f, - "Part": "BODY", - "Rotation1": 18.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": -40.0f, - "Rotation0": -40.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - } - ], - "Smooth": 0b, - "Duration": 1 + "Duration": 15 }, { "ColorMarker": 16777215, "Comment": "", "Speed": 2.0f, "FrameParts": [ - { - "Pivot2": -5.0f, - "Part": "RIGHT_ARM", - "Rotation1": 0.0f, - "Rotation0": -90.0f, - "Pivot0": 0.0f, - "Pivot1": 2.0f, - "Rotation2": 90.0f - }, - { - "Pivot2": 2.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 31.0f, - "Pivot0": 1.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, { "Pivot2": 0.0f, "Part": "HEAD", @@ -1231,15 +142,6 @@ "Pivot1": 1.0f, "Rotation2": 0.0f }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_LEG", - "Rotation1": 6.0f, - "Rotation0": 12.0f, - "Pivot0": -1.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, { "Pivot2": 0.0f, "Part": "BODY", @@ -1257,6 +159,33 @@ "Pivot0": -2.0f, "Pivot1": 1.0f, "Rotation2": -6.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 6.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f } ], "Smooth": 0b, @@ -1264,7 +193,7 @@ } ], "CurrentFrameTime": 0, - "CurrentFrame": 19, + "CurrentFrame": 1, "Smooth": 0b, "ID": 15, "WhileAttacking": 1b, From cac27535f877d1fd2540419766ab103181e12353 Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 13:03:57 -0300 Subject: [PATCH 21/34] Charge animation --- .../data/ability/type/AbilityCharge.java | 2 + .../animations/Ability_Charge_Active.json | 59 +++++++++ .../animations/Ability_Charge_Windup.json | 122 ++++++++++++++++++ 3 files changed, 183 insertions(+) create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Charge_Active.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Charge_Windup.json diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 4ae81bfcb..2a0a5ff8c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -56,6 +56,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge { this.showTelegraph = true; this.windUpSound = "mob.zombie.wood"; this.activeSound = "mob.zombie.attack"; + this.windUpAnimationName = "Ability_Charge_Windup"; + this.activeAnimationName = "Ability_Active_Windup"; } @Override diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Charge_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Charge_Active.json new file mode 100644 index 000000000..82b2294ca --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Charge_Active.json @@ -0,0 +1,59 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -6.0f, + "Part": "RIGHT_ARM", + "Rotation1": -50.0f, + "Rotation0": -75.0f, + "Pivot0": 0.0f, + "Pivot1": 3.0f, + "Rotation2": -45.0f + }, + { + "Pivot2": -6.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -75.0f, + "Pivot0": 0.0f, + "Pivot1": 3.0f, + "Rotation2": 45.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 28, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Charge_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Charge_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Charge_Windup.json new file mode 100644 index 000000000..c627007e3 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Charge_Windup.json @@ -0,0 +1,122 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.8f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 69.0f, + "Pivot0": 0.0f, + "Pivot1": 5.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -8.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 12.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": -6.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 44.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -8.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 12.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -10.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 0.0f, + "Pivot1": 12.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -10.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 75.0f, + "Pivot0": 0.0f, + "Pivot1": 12.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 18 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -6.0f, + "Part": "RIGHT_ARM", + "Rotation1": -50.0f, + "Rotation0": -75.0f, + "Pivot0": 0.0f, + "Pivot1": 3.0f, + "Rotation2": -45.0f + }, + { + "Pivot2": -6.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -75.0f, + "Pivot0": 0.0f, + "Pivot1": 3.0f, + "Rotation2": 45.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 3.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 27, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Charge_Windup", + "WhileStanding": 1b +} From 540903d71fb3a071896e5c8546c43a95c13bb155 Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 13:24:28 -0300 Subject: [PATCH 22/34] Changed Abilities register method call --- src/main/java/kamkeel/npcs/util/Register.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index ff6b0f24e..c343a9f03 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -69,7 +69,7 @@ public class Register { public void register() { for (Map.Entry> entry : entries.entrySet()) { - AbilityController.Instance.registerType(entry.getValue().get().getTypeId(), entry.getValue()); + AbilityController.Instance.registerAbility(entry.getKey(), entry.getValue().get()); } } From 8a3b0b98662ecfa25a4d99f1b6c498642abdfbe8 Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 13:48:15 -0300 Subject: [PATCH 23/34] why... --- src/main/java/kamkeel/npcs/util/Register.java | 43 +++++-------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index c343a9f03..af944ed59 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -12,7 +12,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 registryKey; protected final String namespace; protected final Map> entries = new LinkedHashMap<>(); @@ -35,45 +35,24 @@ public class Register { return false; } - public List catalogue() { - List list = new ArrayList<>(); - for (String regKey : REGISTERED_NAMESPACES.keySet()) { - List nameSp = REGISTERED_NAMESPACES.get(regKey); - for (String name : nameSp) { - List nameSpContent = catalogue(regKey, name); - list.addAll(nameSpContent); - } - } - - return list; - } - - public List catalogue(String namespace) { - return catalogue(registryKey, namespace); - } - - public List catalogue(String registryKey, String namespace) { - List list = new ArrayList<>(); - for (Map.Entry> entry : entries.entrySet()) { - if (entry.getKey().startsWith(registryKey + "." + namespace + ":")) - list.add(entry.getValue().get()); - } - - return list; - } - - public static class Abilities extends Register { + public static class Abilities extends Register { private Abilities(String namespace) { super("ability", namespace); } + @Override + public T register(String factoryName, Supplier factory) { + entries.put(registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_"), factory); + return factory.get(); + } + public void register() { - for (Map.Entry> entry : entries.entrySet()) { + for (Map.Entry> entry : entries.entrySet()) { AbilityController.Instance.registerAbility(entry.getKey(), entry.getValue().get()); } } - public static Register.Abilities create(String namespace, String displayName) { + public static Register.Abilities create(String namespace, String displayName) { if (!REGISTERED_NAMESPACES.containsKey("ability")) REGISTERED_NAMESPACES.put("ability", new ArrayList<>()); @@ -84,7 +63,7 @@ public class Register { REGISTERED_NAMESPACES.get("ability").add(namespace); NAMESPACE_DISPLAY_NAMES.put(namespace, displayName); - return new Register.Abilities(namespace); + return new Register.Abilities<>(namespace); } } From c2c571ae7ea9b4d393da62cbed264c474b28ad53 Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 14:02:00 -0300 Subject: [PATCH 24/34] Hotfix --- src/main/java/kamkeel/npcs/util/Register.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index af944ed59..8c27ba57a 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -35,24 +35,24 @@ public class Register { return false; } - public static class Abilities extends Register { + public static class Abilities extends Register { private Abilities(String namespace) { super("ability", namespace); } @Override - public T register(String factoryName, Supplier factory) { + public Ability register(String factoryName, Supplier factory) { entries.put(registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_"), factory); return factory.get(); } public void register() { - for (Map.Entry> entry : entries.entrySet()) { + for (Map.Entry> entry : entries.entrySet()) { AbilityController.Instance.registerAbility(entry.getKey(), entry.getValue().get()); } } - public static Register.Abilities create(String namespace, String displayName) { + public static Register.Abilities create(String namespace, String displayName) { if (!REGISTERED_NAMESPACES.containsKey("ability")) REGISTERED_NAMESPACES.put("ability", new ArrayList<>()); @@ -63,7 +63,7 @@ public class Register { REGISTERED_NAMESPACES.get("ability").add(namespace); NAMESPACE_DISPLAY_NAMES.put(namespace, displayName); - return new Register.Abilities<>(namespace); + return new Register.Abilities(namespace); } } From 70ed5a07174bd277f939c807fd9900253c4f08eb Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 14:13:37 -0300 Subject: [PATCH 25/34] =?UTF-8?q?Hotfix=C2=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/kamkeel/npcs/util/Register.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index 8c27ba57a..db6802f34 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -36,19 +36,23 @@ public class Register { } public static class Abilities extends Register { + protected final Map uniqueNames = new LinkedHashMap<>(); + private Abilities(String namespace) { super("ability", namespace); } @Override public Ability register(String factoryName, Supplier factory) { - entries.put(registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_"), factory); + String name = registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_"); + entries.put(name, factory); + uniqueNames.put(name, factoryName); return factory.get(); } public void register() { for (Map.Entry> entry : entries.entrySet()) { - AbilityController.Instance.registerAbility(entry.getKey(), entry.getValue().get()); + AbilityController.Instance.registerAbility(uniqueNames.get(entry.getKey()), entry.getValue().get()); } } From 3500b0f55f1a1792c6aa298ea61cdbd5a8269099 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 12 Feb 2026 13:55:56 -0500 Subject: [PATCH 26/34] Ability Trap & Hazard Update --- src/api | 2 +- .../client/renderer/RenderAbilityZone.java | 754 ++++++++++++++ .../lightning/AttachedLightningRenderer.java | 33 + .../data/ability/type/AbilityHazard.java | 599 ++--------- .../data/ability/type/AbilityTrap.java | 487 ++------- .../data/ability/type/AbilityZone.java | 554 +++++++++++ .../npcs/entity/EntityAbilityZone.java | 928 ++++++++++++++++++ src/main/java/noppes/npcs/CustomNpcs.java | 2 + .../java/noppes/npcs/client/ClientProxy.java | 3 + .../java/noppes/npcs/client/fx/CustomFX.java | 4 + .../noppes/npcs/client/fx/ZoneParticleFX.java | 65 ++ .../npcs/client/renderer/ImageData.java | 2 +- .../assets/customnpcs/lang/en_US.lang | 57 +- 13 files changed, 2561 insertions(+), 929 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java create mode 100644 src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java create mode 100644 src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java diff --git a/src/api b/src/api index fd4c77d01..092b18fe9 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit fd4c77d015add1f6f3012a1681561b2774f2f997 +Subproject commit 092b18fe95d57353d16960411e5289ac3bbfa6ef diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java new file mode 100644 index 000000000..6c555aca8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java @@ -0,0 +1,754 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; +import kamkeel.npcs.entity.EntityAbilityZone; +import kamkeel.npcs.entity.EntityAbilityZone.AccentStyle; +import kamkeel.npcs.entity.EntityAbilityZone.ParticleMotion; +import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape; +import kamkeel.npcs.entity.EntityAbilityZone.ZoneType; +import noppes.npcs.client.fx.CustomFX; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.fx.ZoneParticleFX; +import org.lwjgl.opengl.GL11; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +/** + * Renders zone effects for Trap and Hazard abilities. + * Supports CIRCLE and SQUARE shapes with composable visual layers. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityZone extends Render { + + private static final int CIRCLE_SEGMENTS = 32; + private static final int ACCENT_LINE_COUNT = 12; + private static final Random RANDOM = new Random(); + + // Track last particle spawn tick per entity to avoid duplicate spawns + private final Map lastParticleTick = new HashMap<>(); + + // Persistent lightning state + private final Map lightningStates = new HashMap<>(); + + // ═══════════════════════════════════════════════════════════════════ + // RENDER CONTEXT + // ═══════════════════════════════════════════════════════════════════ + + private static class Ctx { + EntityAbilityZone zone; + ZoneShape shape; + float radius, height; + float ir, ig, ib; // inner color + float outerR, outerG, outerB; + boolean outerEnabled; + float time, pulse, rotation, brightness; + boolean flash, newTick; + float particleDensity, particleScale, animSpeed, lightningDensity; + // Visual layer fields (always populated) + boolean groundFill, rings, border, accents, lightning, particles; + float groundAlpha, borderSpeed; + int ringCount, accentStyle, particleMotion, particleSize; + boolean particleGlow; + String particleDir; + } + + // ═══════════════════════════════════════════════════════════════════ + // MAIN RENDER + // ═══════════════════════════════════════════════════════════════════ + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityZone zone = (EntityAbilityZone) entity; + + // Build context + Ctx c = new Ctx(); + c.zone = zone; + c.shape = zone.getShape(); + c.radius = zone.getRadius(); + c.height = zone.getZoneHeight(); + + int inner = zone.getInnerColor(); + c.ir = ((inner >> 16) & 0xFF) / 255.0f; + c.ig = ((inner >> 8) & 0xFF) / 255.0f; + c.ib = (inner & 0xFF) / 255.0f; + + int outer = zone.getOuterColor(); + c.outerR = ((outer >> 16) & 0xFF) / 255.0f; + c.outerG = ((outer >> 8) & 0xFF) / 255.0f; + c.outerB = (outer & 0xFF) / 255.0f; + c.outerEnabled = zone.isOuterColorEnabled(); + + c.particleDensity = zone.getParticleDensity(); + c.particleScale = zone.getParticleScale(); + c.animSpeed = zone.getAnimSpeed(); + c.lightningDensity = zone.getLightningDensity(); + + // Visual layer fields — always populated + c.groundFill = zone.isGroundFill(); + c.groundAlpha = zone.getGroundAlpha(); + c.rings = zone.isRings(); + c.ringCount = zone.getRingCount(); + c.border = zone.isBorder(); + c.borderSpeed = zone.getBorderSpeed(); + c.accents = zone.isAccents(); + c.accentStyle = zone.getAccentStyle(); + c.lightning = zone.isLightning(); + c.particles = zone.isParticles(); + c.particleMotion = zone.getParticleMotion(); + c.particleDir = zone.getParticleDir(); + c.particleSize = zone.getParticleSize(); + c.particleGlow = zone.isParticleGlow(); + + c.time = zone.ticksExisted + partialTicks; + c.pulse = c.time * 0.1f * c.animSpeed; + c.rotation = c.time * 1.5f * c.animSpeed; + + c.brightness = 1.0f; + if (zone.getZoneType() == ZoneType.TRAP) { + if (!zone.isArmed()) { + c.brightness = 0.4f; + } else { + c.pulse = c.time * 0.2f; + } + } + + c.flash = zone.getTriggerFlashTick() >= 0 + && zone.ticksExisted - zone.getTriggerFlashTick() < 4; + + c.newTick = isNewTick(zone); + + // Render + setupRenderState(); + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + GL11.glDepthMask(false); + + renderZone(c); + + GL11.glDepthMask(true); + GL11.glPopMatrix(); + restoreRenderState(); + } + + private boolean isNewTick(EntityAbilityZone zone) { + int id = zone.getEntityId(); + int tick = zone.ticksExisted; + Integer last = lastParticleTick.get(id); + if (last == null || last != tick) { + lastParticleTick.put(id, tick); + return true; + } + return false; + } + + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityZone zone) { + int id = zone.getEntityId(); + AttachedLightningRenderer.LightningState state = lightningStates.get(id); + if (state == null) { + state = new AttachedLightningRenderer.LightningState(); + lightningStates.put(id, state); + } + return state; + } + + // ═══════════════════════════════════════════════════════════════════ + // UNIFIED RENDER — Composable visual layers + // ═══════════════════════════════════════════════════════════════════ + + private void renderZone(Ctx c) { + float rR = ringR(c), rG = ringG(c), rB = ringB(c); + + // 1. Ground fill + if (c.groundFill) { + if (c.flash) { + renderShapeFill(c.shape, c.radius, 1, 1, 1, 0.5f); + } else { + renderShapeFill(c.shape, c.radius, + c.ir, c.ig, c.ib, c.groundAlpha * c.brightness); + } + } + + // 2. Pulse rings + if (c.rings) { + int count = Math.max(1, Math.min(5, c.ringCount)); + for (int i = 0; i < count; i++) { + float frac = (i + 1.0f) / (count + 1); + float ra = (0.4f + 0.3f * sin(c.pulse + i * Math.PI / 3)) * c.brightness; + float yOff = 0.02f + i * 0.01f; + renderRingLine(c.shape, c.radius * frac, + rR, rG, rB, ra, yOff); + } + // Always include outer edge ring + float edgeA = (0.5f + 0.4f * sin(c.pulse + count * Math.PI / 3)) * c.brightness; + renderRingLine(c.shape, c.radius, rR, rG, rB, edgeA, 0.02f + count * 0.01f); + } + + // 3. Rotating border + if (c.border) { + renderRotatingBorder(c.shape, c.radius, rR, rG, rB, + 0.7f * c.brightness, c.rotation * c.borderSpeed, 0.05f); + } + + // 4. Vertical accents + if (c.accents) { + float aa = 0.5f * c.brightness * (0.3f + 0.2f * sin(c.pulse * 0.7f)); + AccentStyle style = AccentStyle.values()[Math.min(c.accentStyle, AccentStyle.values().length - 1)]; + switch (style) { + case SWAYING: + renderSwayingAccents(c, c.ir, c.ig, c.ib, aa, c.height); + break; + case FLICKERING: + renderFlickeringAccents(c, c.ir, c.ig, c.ib, aa, c.height); + break; + default: + renderVerticalAccents(c.shape, c.radius, c.ir, c.ig, c.ib, aa, c.height, c.rotation * 0.3f); + break; + } + } + + // 5. Lightning + if (c.lightning) { + int innerColor = c.zone.getInnerColor(); + int outerColor = c.outerEnabled ? c.zone.getOuterColor() : innerColor; + AttachedLightningRenderer.LightningState state = getLightningState(c.zone); + if (c.newTick) { + state.tick(); + float density = 0.4f * c.brightness * c.lightningDensity; + if (RANDOM.nextFloat() < density) { + double[] p1 = randomPosInZone(c); + double[] p2 = randomPosInZone(c); + double y1 = RANDOM.nextDouble() * c.height * 0.4; + double y2 = RANDOM.nextDouble() * c.height * 0.4; + float disp = c.radius * 0.15f; + state.addArc(AttachedLightningRenderer.createArcBetween( + p1[0], y1, p1[2], p2[0], y2, p2[2], disp, outerColor, innerColor, 6)); + } + if (RANDOM.nextFloat() < density * 0.5f * c.lightningDensity) { + double[] edgePos = randomEdgePos(c); + double ex = edgePos[0]; + double ez = edgePos[1]; + float disp = c.radius * 0.12f; + state.addArc(AttachedLightningRenderer.createArcBetween( + ex, 0, ez, ex + (RANDOM.nextDouble() - 0.5) * 0.5, + c.height * (0.3 + RANDOM.nextDouble() * 0.4), + ez + (RANDOM.nextDouble() - 0.5) * 0.5, + disp, outerColor, innerColor, 5)); + } + } + state.render(); + GL11.glDepthMask(false); // Reset after lightning + } + + // 6. Particles + if (c.particles && c.newTick && c.particleDensity > 0) { + if (c.particleDir != null && !c.particleDir.isEmpty()) { + spawnCustomFXParticle(c); + } else { + spawnDefaultParticles(c); + } + } + } + + /** Spawn CustomFX particles using a texture directory. */ + private void spawnCustomFXParticle(Ctx c) { + int count = Math.round(3 * c.particleDensity); + for (int i = 0; i < count; i++) { + double[] pos = randomPosInZone(c); + double[] motion = getMotionForStyle(c.particleMotion); + CustomFX fx = new CustomFX(c.zone.worldObj, null, c.particleDir, + c.zone.posX + pos[0], c.zone.posY + pos[1], c.zone.posZ + pos[2], + motion[0], motion[1], motion[2]); + fx.setMaxAge(20 + RANDOM.nextInt(20)); + fx.scaleX1 = c.particleScale * 10.0f; + fx.scaleY1 = c.particleScale * 10.0f; + fx.width = c.particleSize; + fx.height = c.particleSize; + fx.HEXColor = c.zone.getInnerColor(); + fx.facePlayer = true; + fx.glows = c.particleGlow; + fx.alpha1 = 0.8f; + fx.alpha2 = 0.0f; + fx.alphaRate = 0.04f; + Minecraft.getMinecraft().effectRenderer.addEffect(fx); + } + } + + /** Spawn default ZoneParticleFX with motion style from ParticleMotion enum. */ + private void spawnDefaultParticles(Ctx c) { + int count = Math.round(3 * c.particleDensity); + ParticleMotion motion = ParticleMotion.values()[Math.min(c.particleMotion, ParticleMotion.values().length - 1)]; + for (int i = 0; i < count; i++) { + double[] pos = randomPosInZone(c); + double mx, my, mz; + float gravity; + int age; + switch (motion) { + case DRIFTING: + mx = (RANDOM.nextDouble() - 0.5) * 0.005; + my = 0.005 + RANDOM.nextDouble() * 0.01; + mz = (RANDOM.nextDouble() - 0.5) * 0.005; + gravity = 0; + age = 40 + RANDOM.nextInt(20); + break; + case SPARKS: + mx = (RANDOM.nextDouble() - 0.5) * 0.08; + my = RANDOM.nextDouble() * 0.06; + mz = (RANDOM.nextDouble() - 0.5) * 0.08; + gravity = 0; + age = 4 + RANDOM.nextInt(6); + break; + default: // RISING + mx = (RANDOM.nextDouble() - 0.5) * 0.01; + my = 0.01 + RANDOM.nextDouble() * 0.02; + mz = (RANDOM.nextDouble() - 0.5) * 0.01; + gravity = -0.01f; + age = 30 + RANDOM.nextInt(20); + break; + } + float scale = (1.5f + RANDOM.nextFloat() * 1.0f) * c.particleScale; + spawnParticle(c.zone, pos[0], pos[1], pos[2], mx, my, mz, + c.ir, c.ig, c.ib, 0.7f, scale, age, gravity, c.particleGlow); + } + } + + /** Get motion vector for a CustomFX particle based on ParticleMotion ordinal. */ + private double[] getMotionForStyle(int motionOrdinal) { + ParticleMotion motion = ParticleMotion.values()[Math.min(motionOrdinal, ParticleMotion.values().length - 1)]; + switch (motion) { + case DRIFTING: + return new double[]{ + (RANDOM.nextDouble() - 0.5) * 0.005, + 0.005 + RANDOM.nextDouble() * 0.01, + (RANDOM.nextDouble() - 0.5) * 0.005 + }; + case SPARKS: + return new double[]{ + (RANDOM.nextDouble() - 0.5) * 0.08, + RANDOM.nextDouble() * 0.06, + (RANDOM.nextDouble() - 0.5) * 0.08 + }; + default: // RISING + return new double[]{ + (RANDOM.nextDouble() - 0.5) * 0.01, + 0.01 + RANDOM.nextDouble() * 0.02, + (RANDOM.nextDouble() - 0.5) * 0.01 + }; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // STYLE HELPERS + // ═══════════════════════════════════════════════════════════════════ + + /** Get the ring/outer color (falls back to inner if outer not enabled). */ + private float ringR(Ctx c) { return c.outerEnabled ? c.outerR : c.ir; } + private float ringG(Ctx c) { return c.outerEnabled ? c.outerG : c.ig; } + private float ringB(Ctx c) { return c.outerEnabled ? c.outerB : c.ib; } + + private static float sin(double v) { return (float) Math.sin(v); } + + /** Vertical accents with sinusoidal sway per line. */ + private void renderSwayingAccents(Ctx c, float r, float g, float b, float a, float height) { + if (height <= 0.1f) return; + float rotRad = (float) Math.toRadians(c.rotation * 0.2f); + Tessellator tess = Tessellator.instance; + + if (c.shape == ZoneShape.SQUARE) { + renderSquareSwayingAccents(c, r, g, b, a, height, rotRad, tess); + } else { + for (int i = 0; i < ACCENT_LINE_COUNT; i++) { + float baseAngle = (float) (Math.PI * 2 * i) / ACCENT_LINE_COUNT + rotRad; + double lx = Math.cos(baseAngle) * c.radius; + double lz = Math.sin(baseAngle) * c.radius; + + float swayX = 0.15f * sin(c.time * 0.08f + i * 1.7f); + float swayZ = 0.15f * sin(c.time * 0.06f + i * 2.3f); + + tess.startDrawing(GL11.GL_LINES); + tess.setColorRGBA_F(r, g, b, a); + tess.addVertex(lx, 0.01, lz); + tess.setColorRGBA_F(r, g, b, 0.0f); + tess.addVertex(lx + swayX, height, lz + swayZ); + tess.draw(); + } + } + } + + private void renderSquareSwayingAccents(Ctx c, float r, float g, float b, float a, float height, float rotRad, Tessellator tess) { + int perSide = Math.max(1, ACCENT_LINE_COUNT / 4); + int idx = 0; + for (int side = 0; side < 4; side++) { + for (int j = 0; j < perSide; j++) { + float frac = (j + 0.5f) / perSide; + double lx, lz; + switch (side) { + case 0: lx = -c.radius + frac * 2 * c.radius; lz = -c.radius; break; + case 1: lx = c.radius; lz = -c.radius + frac * 2 * c.radius; break; + case 2: lx = c.radius - frac * 2 * c.radius; lz = c.radius; break; + default: lx = -c.radius; lz = c.radius - frac * 2 * c.radius; break; + } + float swayX = 0.15f * sin(c.time * 0.08f + idx * 1.7f); + float swayZ = 0.15f * sin(c.time * 0.06f + idx * 2.3f); + + tess.startDrawing(GL11.GL_LINES); + tess.setColorRGBA_F(r, g, b, a); + tess.addVertex(lx, 0.01, lz); + tess.setColorRGBA_F(r, g, b, 0.0f); + tess.addVertex(lx + swayX, height, lz + swayZ); + tess.draw(); + idx++; + } + } + } + + /** Vertical accents with per-line random alpha flicker. */ + private void renderFlickeringAccents(Ctx c, float r, float g, float b, float baseAlpha, float height) { + if (height <= 0.1f) return; + float rotRad = (float) Math.toRadians(c.rotation * 0.4f); + Tessellator tess = Tessellator.instance; + + if (c.shape == ZoneShape.SQUARE) { + renderSquareFlickeringAccents(c, r, g, b, baseAlpha, height, tess); + } else { + for (int i = 0; i < ACCENT_LINE_COUNT; i++) { + float baseAngle = (float) (Math.PI * 2 * i) / ACCENT_LINE_COUNT + rotRad; + double lx = Math.cos(baseAngle) * c.radius; + double lz = Math.sin(baseAngle) * c.radius; + + float flicker = 0.3f + 0.7f * Math.abs(sin(c.time * 0.7f + i * 3.14f)); + float lineAlpha = baseAlpha * flicker; + float lineHeight = height * (0.6f + 0.4f * flicker); + + tess.startDrawing(GL11.GL_LINES); + tess.setColorRGBA_F(r, g, b, lineAlpha); + tess.addVertex(lx, 0.01, lz); + tess.setColorRGBA_F(r, g, b, 0.0f); + tess.addVertex(lx, lineHeight, lz); + tess.draw(); + } + } + } + + private void renderSquareFlickeringAccents(Ctx c, float r, float g, float b, float baseAlpha, float height, Tessellator tess) { + int perSide = Math.max(1, ACCENT_LINE_COUNT / 4); + int idx = 0; + for (int side = 0; side < 4; side++) { + for (int j = 0; j < perSide; j++) { + float frac = (j + 0.5f) / perSide; + double lx, lz; + switch (side) { + case 0: lx = -c.radius + frac * 2 * c.radius; lz = -c.radius; break; + case 1: lx = c.radius; lz = -c.radius + frac * 2 * c.radius; break; + case 2: lx = c.radius - frac * 2 * c.radius; lz = c.radius; break; + default: lx = -c.radius; lz = c.radius - frac * 2 * c.radius; break; + } + float flicker = 0.3f + 0.7f * Math.abs(sin(c.time * 0.7f + idx * 3.14f)); + float lineAlpha = baseAlpha * flicker; + float lineHeight = height * (0.6f + 0.4f * flicker); + + tess.startDrawing(GL11.GL_LINES); + tess.setColorRGBA_F(r, g, b, lineAlpha); + tess.addVertex(lx, 0.01, lz); + tess.setColorRGBA_F(r, g, b, 0.0f); + tess.addVertex(lx, lineHeight, lz); + tess.draw(); + idx++; + } + } + } + + /** Pick a random position within the zone's area. Returns {x, y, z} offsets from zone center. */ + private double[] randomPosInZone(Ctx c) { + if (c.shape == ZoneShape.SQUARE) { + double px = (RANDOM.nextDouble() * 2 - 1) * c.radius; + double pz = (RANDOM.nextDouble() * 2 - 1) * c.radius; + return new double[]{px, 0, pz}; + } else { + double angle = RANDOM.nextDouble() * Math.PI * 2; + double dist = Math.sqrt(RANDOM.nextDouble()) * c.radius; + double px = Math.cos(angle) * dist; + double pz = Math.sin(angle) * dist; + return new double[]{px, 0, pz}; + } + } + + /** Pick a random position on the edge of the zone. Returns {x, z}. */ + private double[] randomEdgePos(Ctx c) { + if (c.shape == ZoneShape.SQUARE) { + int side = RANDOM.nextInt(4); + double frac = RANDOM.nextDouble() * 2 - 1; + switch (side) { + case 0: return new double[]{frac * c.radius, -c.radius}; + case 1: return new double[]{frac * c.radius, c.radius}; + case 2: return new double[]{-c.radius, frac * c.radius}; + default: return new double[]{c.radius, frac * c.radius}; + } + } else { + double angle = RANDOM.nextDouble() * Math.PI * 2; + double dist = c.radius * (0.5 + RANDOM.nextDouble() * 0.5); + return new double[]{Math.cos(angle) * dist, Math.sin(angle) * dist}; + } + } + + /** Spawn a zone particle at world position. */ + private void spawnParticle(EntityAbilityZone zone, double offsetX, double offsetY, double offsetZ, + double mx, double my, double mz, + float r, float g, float b, float alpha, + float scale, int maxAge, float gravity, boolean glow) { + ZoneParticleFX particle = new ZoneParticleFX( + zone.worldObj, + zone.posX + offsetX, zone.posY + offsetY, zone.posZ + offsetZ, + mx, my, mz, r, g, b, alpha, scale, maxAge, gravity, glow); + Minecraft.getMinecraft().effectRenderer.addEffect(particle); + } + + // ═══════════════════════════════════════════════════════════════════ + // GL STATE + // ═══════════════════════════════════════════════════════════════════ + + private void setupRenderState() { + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glShadeModel(GL11.GL_SMOOTH); + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0f, 240.0f); + } + + private void restoreRenderState() { + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glPopAttrib(); + } + + // ═══════════════════════════════════════════════════════════════════ + // SHAPE FILL (Ground plane) + // ═══════════════════════════════════════════════════════════════════ + + private void renderShapeFill(ZoneShape shape, float radius, float r, float g, float b, float a) { + GL11.glEnable(GL11.GL_POLYGON_OFFSET_FILL); + GL11.glPolygonOffset(-1.0f, -1.0f); + switch (shape) { + case CIRCLE: + renderCircleFill(radius, r, g, b, a); + break; + case SQUARE: + renderSquareFill(radius, r, g, b, a); + break; + } + GL11.glDisable(GL11.GL_POLYGON_OFFSET_FILL); + } + + private void renderCircleFill(float radius, float r, float g, float b, float a) { + Tessellator tess = Tessellator.instance; + tess.startDrawing(GL11.GL_TRIANGLE_FAN); + tess.setColorRGBA_F(r, g, b, a); + tess.addVertex(0, 0, 0); + for (int i = 0; i <= CIRCLE_SEGMENTS; i++) { + double angle = (Math.PI * 2 * i) / CIRCLE_SEGMENTS; + tess.addVertex(Math.cos(angle) * radius, 0, Math.sin(angle) * radius); + } + tess.draw(); + } + + private void renderSquareFill(float radius, float r, float g, float b, float a) { + Tessellator tess = Tessellator.instance; + tess.startDrawing(GL11.GL_QUADS); + tess.setColorRGBA_F(r, g, b, a); + tess.addVertex(-radius, 0, -radius); + tess.addVertex(radius, 0, -radius); + tess.addVertex(radius, 0, radius); + tess.addVertex(-radius, 0, radius); + tess.draw(); + } + + // ═══════════════════════════════════════════════════════════════════ + // RING LINES (Concentric pulse rings) + // ═══════════════════════════════════════════════════════════════════ + + private void renderRingLine(ZoneShape shape, float radius, + float r, float g, float b, float a, float yOffset) { + switch (shape) { + case CIRCLE: + renderCircleRingLine(radius, r, g, b, a, yOffset); + break; + case SQUARE: + renderSquareRingLine(radius, r, g, b, a, yOffset); + break; + } + } + + private void renderCircleRingLine(float radius, float r, float g, float b, float a, float yOffset) { + Tessellator tess = Tessellator.instance; + tess.startDrawing(GL11.GL_LINE_LOOP); + tess.setColorRGBA_F(r, g, b, a); + for (int i = 0; i < CIRCLE_SEGMENTS; i++) { + double angle = (Math.PI * 2 * i) / CIRCLE_SEGMENTS; + tess.addVertex(Math.cos(angle) * radius, yOffset, Math.sin(angle) * radius); + } + tess.draw(); + } + + private void renderSquareRingLine(float radius, float r, float g, float b, float a, float yOffset) { + Tessellator tess = Tessellator.instance; + tess.startDrawing(GL11.GL_LINE_LOOP); + tess.setColorRGBA_F(r, g, b, a); + tess.addVertex(-radius, yOffset, -radius); + tess.addVertex(radius, yOffset, -radius); + tess.addVertex(radius, yOffset, radius); + tess.addVertex(-radius, yOffset, radius); + tess.draw(); + } + + // ═══════════════════════════════════════════════════════════════════ + // ROTATING BORDER + // ═══════════════════════════════════════════════════════════════════ + + private void renderRotatingBorder(ZoneShape shape, float radius, + float r, float g, float b, float a, + float rotationDeg, float yOffset) { + int dashCount = 16; + + if (shape == ZoneShape.SQUARE) { + // Dashes along 4 rectangle edges with offset + float perimeter = radius * 8; // total perimeter = 4 * (2*radius) + float dashLen = perimeter / dashCount; + float offset = (rotationDeg % 360.0f) / 360.0f * perimeter; + + Tessellator tess = Tessellator.instance; + for (int i = 0; i < dashCount; i += 2) { + float startDist = (i * dashLen + offset) % perimeter; + float endDist = startDist + dashLen; + + tess.startDrawing(GL11.GL_LINE_STRIP); + tess.setColorRGBA_F(r, g, b, a); + int steps = 4; + for (int s = 0; s <= steps; s++) { + float d = startDist + (endDist - startDist) * s / steps; + double[] pos = perimeterPos(d % perimeter, radius); + tess.addVertex(pos[0], yOffset, pos[1]); + } + tess.draw(); + } + } else { + float dashAngle = (float) (Math.PI * 2) / dashCount; + float rotRad = (float) Math.toRadians(rotationDeg); + + Tessellator tess = Tessellator.instance; + for (int i = 0; i < dashCount; i += 2) { + float startAngle = i * dashAngle + rotRad; + float endAngle = startAngle + dashAngle; + + tess.startDrawing(GL11.GL_LINE_STRIP); + tess.setColorRGBA_F(r, g, b, a); + int steps = 4; + for (int s = 0; s <= steps; s++) { + float segAngle = startAngle + (endAngle - startAngle) * s / steps; + tess.addVertex(Math.cos(segAngle) * radius, yOffset, Math.sin(segAngle) * radius); + } + tess.draw(); + } + } + } + + /** Convert a distance along the square perimeter to an x,z position. */ + private double[] perimeterPos(float dist, float radius) { + float sideLen = radius * 2; + if (dist < sideLen) { + // Bottom edge: (-radius, -radius) to (radius, -radius) + return new double[]{-radius + dist, -radius}; + } + dist -= sideLen; + if (dist < sideLen) { + // Right edge: (radius, -radius) to (radius, radius) + return new double[]{radius, -radius + dist}; + } + dist -= sideLen; + if (dist < sideLen) { + // Top edge: (radius, radius) to (-radius, radius) + return new double[]{radius - dist, radius}; + } + dist -= sideLen; + // Left edge: (-radius, radius) to (-radius, -radius) + return new double[]{-radius, radius - dist}; + } + + // ═══════════════════════════════════════════════════════════════════ + // VERTICAL ACCENT LINES + // ═══════════════════════════════════════════════════════════════════ + + private void renderVerticalAccents(ZoneShape shape, float radius, + float r, float g, float b, float a, + float height, float rotationDeg) { + if (height <= 0.1f) return; + + Tessellator tess = Tessellator.instance; + + if (shape == ZoneShape.SQUARE) { + // Lines at corners + distributed along edges + int perSide = Math.max(1, ACCENT_LINE_COUNT / 4); + for (int side = 0; side < 4; side++) { + for (int j = 0; j < perSide; j++) { + float frac = (j + 0.5f) / perSide; + double lx, lz; + switch (side) { + case 0: lx = -radius + frac * 2 * radius; lz = -radius; break; + case 1: lx = radius; lz = -radius + frac * 2 * radius; break; + case 2: lx = radius - frac * 2 * radius; lz = radius; break; + default: lx = -radius; lz = radius - frac * 2 * radius; break; + } + tess.startDrawing(GL11.GL_LINES); + tess.setColorRGBA_F(r, g, b, a); + tess.addVertex(lx, 0.01, lz); + tess.setColorRGBA_F(r, g, b, 0.0f); + tess.addVertex(lx, height, lz); + tess.draw(); + } + } + // Corner accent lines + float[][] corners = {{-radius, -radius}, {radius, -radius}, {radius, radius}, {-radius, radius}}; + for (float[] corner : corners) { + tess.startDrawing(GL11.GL_LINES); + tess.setColorRGBA_F(r, g, b, a); + tess.addVertex(corner[0], 0.01, corner[1]); + tess.setColorRGBA_F(r, g, b, 0.0f); + tess.addVertex(corner[0], height, corner[1]); + tess.draw(); + } + } else { + float rotRad = (float) Math.toRadians(rotationDeg); + for (int i = 0; i < ACCENT_LINE_COUNT; i++) { + float angle = (float) (Math.PI * 2 * i) / ACCENT_LINE_COUNT + rotRad; + double lx = Math.cos(angle) * radius; + double lz = Math.sin(angle) * radius; + + tess.startDrawing(GL11.GL_LINES); + tess.setColorRGBA_F(r, g, b, a); + tess.addVertex(lx, 0.01, lz); + tess.setColorRGBA_F(r, g, b, 0.0f); + tess.addVertex(lx, height, lz); + tess.draw(); + } + } + } + + @Override + protected ResourceLocation getEntityTexture(Entity entity) { + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java index d68c4037b..2910a9566 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java @@ -85,6 +85,28 @@ public class AttachedLightningRenderer { } } + /** + * Age existing arcs and remove dead ones without spawning new arcs. + * Use with addArc() for custom arc generation. + */ + public void tick() { + Iterator iter = arcs.iterator(); + while (iter.hasNext()) { + LightningArc arc = iter.next(); + arc.tick(); + if (arc.isDead()) { + iter.remove(); + } + } + } + + /** + * Add a custom arc to the state. + */ + public void addArc(LightningArc arc) { + arcs.add(arc); + } + /** * Render all active arcs in local space. * Call this after glTranslated to entity position. @@ -231,6 +253,17 @@ public class AttachedLightningRenderer { } } + /** + * Create a lightning arc between two arbitrary points. + */ + public static LightningArc createArcBetween(double x1, double y1, double z1, + double x2, double y2, double z2, + float displacement, int outerColor, int innerColor, int maxAge) { + int segments = 4 + rand.nextInt(3); + List points = generateLightningPath(x1, y1, z1, x2, y2, z2, segments, displacement); + return new LightningArc(points, maxAge, outerColor, innerColor); + } + // ==================== INSTANT RENDERING (no persistence) ==================== /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index f1ae7e719..89dd8b00d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -1,19 +1,11 @@ package kamkeel.npcs.controllers.data.ability.type; -import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.LockMovementType; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.UserType; -import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData; +import kamkeel.npcs.entity.EntityAbilityZone; +import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; -import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityHazard; @@ -23,93 +15,28 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Random; -import java.util.Set; /** - * Hazard ability: Creates persistent ground effect zones. - * Deals damage and/or applies debuffs to entities in the zone over time. + * Hazard ability: Creates persistent ground effect zones around the caster. + * Spawns one or more EntityAbilityZone entities in random positions + * within spawnRadius. Damage and zone logic handled by the entity. */ -public class AbilityHazard extends Ability implements IAbilityHazard { +public class AbilityHazard extends AbilityZone implements IAbilityHazard { - public enum HazardShape { - CIRCLE, - RING, - CONE; - - @Override - public String toString() { - switch (this) { - case CIRCLE: return "ability.shape.circle"; - case RING: return "ability.shape.ring"; - case CONE: return "ability.shape.cone"; - default: return name(); - } - } - } - - public enum PlacementMode { - AT_CASTER, - AT_TARGET, - FOLLOW_CASTER, - FOLLOW_TARGET; - - @Override - public String toString() { - switch (this) { - case AT_CASTER: return "ability.placement.atCaster"; - case AT_TARGET: return "ability.placement.atTarget"; - case FOLLOW_CASTER: return "ability.placement.followCaster"; - case FOLLOW_TARGET: return "ability.placement.followTarget"; - default: return name(); - } - } - } - - private int durationTicks = 100; + // Hazard-specific fields private float radius = 4.0f; - private float innerRadius = 0.0f; - private float coneAngle = 45.0f; - private HazardShape shape = HazardShape.CIRCLE; - private PlacementMode placement = PlacementMode.AT_TARGET; - private float damagePerSecond = 1.0f; private int damageInterval = 20; private boolean ignoreInvulnFrames = false; - - private int slownessLevel = -1; - private int weaknessLevel = -1; - private int poisonLevel = -1; - private int witherLevel = -1; - private int blindnessLevel = -1; - private int debuffDuration = 40; - private boolean affectsCaster = false; - private float heightAbove = 2.0f; - private float heightBelow = 1.0f; - - // Offset parameters - hazard spawns near target, not exactly on them - private float minOffset = 0.0f; // Minimum offset from target - private float maxOffset = 2.0f; // Maximum offset from target - private boolean randomOffset = true; // Random or fixed offset - - // Runtime state - private static final Random RANDOM = new Random(); - private transient double zoneX, zoneY, zoneZ; - private transient boolean positionLocked = false; - private transient int ticksSinceDamage = 0; - private transient Set damagedThisTick = new HashSet<>(); public AbilityHazard() { + super(100, new EnergyDisplayData(0x00CC00, 0x006600, true, 1.0f, 0.5f, 1.5f)); this.typeId = "ability.cnpc.hazard"; this.name = "Hazard"; - this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 15.0f; - this.lockMovement = LockMovementType.WINDUP; - this.cooldownTicks = 0; this.windUpTicks = 30; this.telegraphType = TelegraphType.CIRCLE; this.allowedBy = UserType.NPC_ONLY; @@ -117,21 +44,6 @@ public class AbilityHazard extends Ability implements IAbilityHazard { this.activeAnimationName = "Ability_Hazard_Active"; } - @Override - public boolean allowBurst() { - return false; - } - - @Override - public boolean isTargetingModeLocked() { - return true; - } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; - } - @Override public float getTelegraphRadius() { return radius; @@ -193,222 +105,71 @@ public class AbilityHazard extends Ability implements IAbilityHazard { @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - // Lock position from telegraph (which was following target) or calculate it now - TelegraphInstance telegraph = getTelegraphInstance(); - - switch (placement) { - case AT_CASTER: - // Already locked during windup - if (!positionLocked) { - zoneX = caster.posX; - zoneY = caster.posY; - zoneZ = caster.posZ; - } - break; - case AT_TARGET: - if (telegraph != null) { - double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(), - minOffset, maxOffset, randomOffset, RANDOM); - zoneX = pos[0]; - zoneY = pos[1]; - zoneZ = pos[2]; - telegraph.setX(zoneX); - telegraph.setY(zoneY); - telegraph.setZ(zoneZ); - } else if (target != null) { - double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ, - minOffset, maxOffset, randomOffset, RANDOM); - zoneX = pos[0]; - zoneY = pos[1]; - zoneZ = pos[2]; - } else { - zoneX = caster.posX; - zoneY = caster.posY; - zoneZ = caster.posZ; - } - break; - case FOLLOW_CASTER: - zoneX = caster.posX; - zoneY = caster.posY; - zoneZ = caster.posZ; - break; - case FOLLOW_TARGET: - if (target != null) { - zoneX = target.posX; - zoneY = target.posY; - zoneZ = target.posZ; - } else { - zoneX = caster.posX; - zoneY = caster.posY; - zoneZ = caster.posZ; - } - break; - } - positionLocked = true; - ticksSinceDamage = damageInterval; - } - - @Override - public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (world.isRemote && !isPreview()) return; - - // Check if hazard duration has ended - if (tick >= durationTicks) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } - if (!isPreview()) { - damagedThisTick.clear(); - ticksSinceDamage++; + activeEntities.clear(); - switch (placement) { - case FOLLOW_CASTER: - zoneX = caster.posX; - zoneY = caster.posY; - zoneZ = caster.posZ; - break; - case FOLLOW_TARGET: - if (target != null) { - zoneX = target.posX; - zoneY = target.posY; - zoneZ = target.posZ; - } - break; - default: - break; + List placedPositions = new ArrayList<>(); + float minSeparation = radius * 2.0f; + + for (int i = 0; i < zoneCount; i++) { + double[] pos = findSpawnPosition(caster, placedPositions, minSeparation); + placedPositions.add(pos); + + EntityAbilityZone entity = EntityAbilityZone.createHazard(world, caster, + pos[0], caster.posY, pos[1], + zoneShape, + radius, + damagePerSecond, damageInterval, + ignoreInvulnFrames, affectsCaster, + durationTicks, + colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled, + zoneHeight, + particleDensity, particleScale, animSpeed, lightningDensity, + getEffects()); + + applyVisualToEntity(entity); + + if (isPreview()) { + entity.setupPreview(caster); } - if (ticksSinceDamage >= damageInterval) { - ticksSinceDamage = 0; - - AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( - zoneX - radius, zoneY - heightBelow, zoneZ - radius, - zoneX + radius, zoneY + heightAbove, zoneZ + radius - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); - - for (EntityLivingBase entity : entities) { - if (entity == caster && !affectsCaster) continue; - if (damagedThisTick.contains(entity.getEntityId())) continue; - if (!isInZone(entity, caster)) continue; - - if (damagePerSecond > 0) { - if (ignoreInvulnFrames) { - entity.hurtResistantTime = 0; - } - // Apply damage with scripted event support (no knockback for hazard) - boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0); - if (!wasHit) continue; // Skip debuffs if hit was cancelled - } - - applyDebuffs(entity); - damagedThisTick.add(entity.getEntityId()); - } - } + spawnAbilityEntity(world, entity); + activeEntities.add(entity); } } - private boolean isInZone(EntityLivingBase entity, EntityLivingBase caster) { - double dx = entity.posX - zoneX; - double dz = entity.posZ - zoneZ; - double dist = Math.sqrt(dx * dx + dz * dz); - - switch (shape) { - case CIRCLE: - return dist <= radius; - case RING: - return dist >= innerRadius && dist <= radius; - case CONE: - if (dist > radius) return false; - float casterYaw = caster.rotationYaw; - double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz)); - double angleDiff = Math.abs(normalizeAngle(angleToEntity - casterYaw)); - return angleDiff <= coneAngle / 2; - default: - return dist <= radius; - } - } - - private double normalizeAngle(double angle) { - while (angle > 180) angle -= 360; - while (angle < -180) angle += 360; - return angle; - } - - private void applyDebuffs(EntityLivingBase target) { - if (slownessLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, debuffDuration, slownessLevel)); - } - if (weaknessLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.weakness.id, debuffDuration, weaknessLevel)); - } - if (poisonLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.poison.id, debuffDuration, poisonLevel)); - } - if (witherLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.wither.id, debuffDuration, witherLevel)); - } - if (blindnessLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.blindness.id, debuffDuration, blindnessLevel)); - } - } - - @Override - public void onComplete(EntityLivingBase caster, EntityLivingBase target) { - damagedThisTick.clear(); - positionLocked = false; - } - - @Override - public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { - damagedThisTick.clear(); - positionLocked = false; - } + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ @Override public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setInteger("durationTicks", durationTicks); + writeZoneNBT(nbt); nbt.setFloat("radius", radius); - nbt.setFloat("innerRadius", innerRadius); - nbt.setFloat("coneAngle", coneAngle); - nbt.setString("shape", shape.name()); - nbt.setString("placement", placement.name()); nbt.setFloat("damagePerSecond", damagePerSecond); nbt.setInteger("damageInterval", damageInterval); nbt.setBoolean("ignoreInvulnFrames", ignoreInvulnFrames); - nbt.setInteger("slownessLevel", slownessLevel); - nbt.setInteger("weaknessLevel", weaknessLevel); - nbt.setInteger("poisonLevel", poisonLevel); - nbt.setInteger("witherLevel", witherLevel); - nbt.setInteger("blindnessLevel", blindnessLevel); - nbt.setInteger("debuffDuration", debuffDuration); nbt.setBoolean("affectsCaster", affectsCaster); - nbt.setFloat("heightAbove", heightAbove); - nbt.setFloat("heightBelow", heightBelow); - nbt.setFloat("minOffset", minOffset); - nbt.setFloat("maxOffset", maxOffset); - nbt.setBoolean("randomOffset", randomOffset); } @Override public void readTypeNBT(NBTTagCompound nbt) { - this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 100; + readZoneNBT(nbt, 100); + + // Backward compat: old "shape" key with RING/CONE → ignore (base defaults to CIRCLE) + // The old HazardShape enum had CIRCLE/RING/CONE — only CIRCLE maps to new ZoneShape + if (nbt.hasKey("shape")) { + String oldShape = nbt.getString("shape"); + if ("RING".equals(oldShape) || "CONE".equals(oldShape)) { + this.zoneShape = ZoneShape.CIRCLE; + } + } + this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 4.0f; - this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; - this.coneAngle = nbt.hasKey("coneAngle") ? nbt.getFloat("coneAngle") : 45.0f; - try { - this.shape = HazardShape.valueOf(nbt.getString("shape")); - } catch (Exception e) { - this.shape = HazardShape.CIRCLE; - } - try { - this.placement = PlacementMode.valueOf(nbt.getString("placement")); - } catch (Exception e) { - this.placement = PlacementMode.AT_TARGET; - } // Support legacy "damagePerTick" key for backwards compatibility if (nbt.hasKey("damagePerSecond")) { this.damagePerSecond = nbt.getFloat("damagePerSecond"); @@ -419,252 +180,60 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 20; this.ignoreInvulnFrames = nbt.hasKey("ignoreInvulnFrames") && nbt.getBoolean("ignoreInvulnFrames"); - this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : -1; - this.weaknessLevel = nbt.hasKey("weaknessLevel") ? nbt.getInteger("weaknessLevel") : -1; - this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : -1; - this.witherLevel = nbt.hasKey("witherLevel") ? nbt.getInteger("witherLevel") : -1; - this.blindnessLevel = nbt.hasKey("blindnessLevel") ? nbt.getInteger("blindnessLevel") : -1; - this.debuffDuration = nbt.hasKey("debuffDuration") ? nbt.getInteger("debuffDuration") : 40; this.affectsCaster = nbt.hasKey("affectsCaster") && nbt.getBoolean("affectsCaster"); - this.heightAbove = nbt.hasKey("heightAbove") ? nbt.getFloat("heightAbove") : 2.0f; - this.heightBelow = nbt.hasKey("heightBelow") ? nbt.getFloat("heightBelow") : 1.0f; - this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f; - this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 2.0f; - this.randomOffset = !nbt.hasKey("randomOffset") || nbt.getBoolean("randomOffset"); } - // Getters & Setters - public int getDurationTicks() { - return durationTicks; - } + // ═══════════════════════════════════════════════════════════════════ + // HAZARD-SPECIFIC GETTERS & SETTERS + // ═══════════════════════════════════════════════════════════════════ - public void setDurationTicks(int durationTicks) { - this.durationTicks = Math.max(1, durationTicks); - } + public float getRadius() { return radius; } + public void setRadius(float radius) { this.radius = radius; } - public float getRadius() { - return radius; - } + public float getDamagePerSecond() { return damagePerSecond; } + public void setDamagePerSecond(float damagePerSecond) { this.damagePerSecond = damagePerSecond; } - public void setRadius(float radius) { - this.radius = radius; - } + public int getDamageInterval() { return damageInterval; } + public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; } - public float getInnerRadius() { - return innerRadius; - } + public boolean isIgnoreInvulnFrames() { return ignoreInvulnFrames; } + public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { this.ignoreInvulnFrames = ignoreInvulnFrames; } - public void setInnerRadius(float innerRadius) { - this.innerRadius = innerRadius; - } + public boolean isAffectsCaster() { return affectsCaster; } + public void setAffectsCaster(boolean affectsCaster) { this.affectsCaster = affectsCaster; } - public float getConeAngle() { - return coneAngle; - } - - public void setConeAngle(float coneAngle) { - this.coneAngle = coneAngle; - } - - public HazardShape getShapeEnum() { - return shape; - } - - public void setShapeEnum(HazardShape shape) { - this.shape = shape; - } - - @Override - public int getShape() { - return shape.ordinal(); - } - - @Override - public void setShape(int shape) { - HazardShape[] values = HazardShape.values(); - this.shape = shape >= 0 && shape < values.length ? values[shape] : HazardShape.CIRCLE; - } - - public PlacementMode getPlacementEnum() { - return placement; - } - - public void setPlacementEnum(PlacementMode placement) { - this.placement = placement; - } - - @Override - public int getPlacement() { - return placement.ordinal(); - } - - @Override - public void setPlacement(int placement) { - PlacementMode[] values = PlacementMode.values(); - this.placement = placement >= 0 && placement < values.length ? values[placement] : PlacementMode.AT_CASTER; - } - - public float getDamagePerSecond() { - return damagePerSecond; - } - - public void setDamagePerSecond(float damagePerSecond) { - this.damagePerSecond = damagePerSecond; - } - - public int getDamageInterval() { - return damageInterval; - } - - public void setDamageInterval(int damageInterval) { - this.damageInterval = damageInterval; - } - - public boolean isIgnoreInvulnFrames() { - return ignoreInvulnFrames; - } - - public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { - this.ignoreInvulnFrames = ignoreInvulnFrames; - } - - public int getSlownessLevel() { - return slownessLevel; - } - - public void setSlownessLevel(int slownessLevel) { - this.slownessLevel = slownessLevel; - } - - public int getWeaknessLevel() { - return weaknessLevel; - } - - public void setWeaknessLevel(int weaknessLevel) { - this.weaknessLevel = weaknessLevel; - } - - public int getPoisonLevel() { - return poisonLevel; - } - - public void setPoisonLevel(int poisonLevel) { - this.poisonLevel = poisonLevel; - } - - public int getWitherLevel() { - return witherLevel; - } - - public void setWitherLevel(int witherLevel) { - this.witherLevel = witherLevel; - } - - public int getBlindnessLevel() { - return blindnessLevel; - } - - public void setBlindnessLevel(int blindnessLevel) { - this.blindnessLevel = blindnessLevel; - } - - public int getDebuffDuration() { - return debuffDuration; - } - - public void setDebuffDuration(int debuffDuration) { - this.debuffDuration = debuffDuration; - } - - public boolean isAffectsCaster() { - return affectsCaster; - } - - public void setAffectsCaster(boolean affectsCaster) { - this.affectsCaster = affectsCaster; - } - - public float getHeightAbove() { - return heightAbove; - } - - public void setHeightAbove(float heightAbove) { - this.heightAbove = heightAbove; - } - - public float getHeightBelow() { - return heightBelow; - } - - public void setHeightBelow(float heightBelow) { - this.heightBelow = heightBelow; - } - - public float getMinOffset() { - return minOffset; - } - - public void setMinOffset(float minOffset) { - this.minOffset = minOffset; - } - - public float getMaxOffset() { - return maxOffset; - } - - public void setMaxOffset(float maxOffset) { - this.maxOffset = maxOffset; - } - - public boolean isRandomOffset() { - return randomOffset; - } - - public void setRandomOffset(boolean randomOffset) { - this.randomOffset = randomOffset; - } - - // Runtime getters - public double getZoneX() { - return zoneX; - } - - public double getZoneY() { - return zoneY; - } - - public double getZoneZ() { - return zoneZ; - } + // ═══════════════════════════════════════════════════════════════════ + // GUI FIELD DEFINITIONS + // ═══════════════════════════════════════════════════════════════════ @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { defs.addAll(Arrays.asList( - FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000), - FieldDef.enumField("ability.shape", HazardShape.class, this::getShapeEnum, this::setShapeEnum) - .hover("ability.hover.shape"), - FieldDef.enumField("ability.placement", PlacementMode.class, this::getPlacementEnum, this::setPlacementEnum) - .hover("ability.hover.placement"), - FieldDef.section("ability.section.area"), FieldDef.row( - FieldDef.floatField("gui.radius", this::getRadius, this::setRadius), - FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius) + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000), + FieldDef.enumField("ability.zoneShape", ZoneShape.class, this::getZoneShapeEnum, this::setZoneShapeEnum) ), + FieldDef.section("ability.section.zone"), + FieldDef.row( + FieldDef.floatField("gui.radius", this::getSpawnRadius, this::setSpawnRadius), + FieldDef.intField("gui.count", this::getZoneCount, this::setZoneCount).range(1, 20) + ), + FieldDef.floatField("gui.height", this::getZoneHeight, this::setZoneHeight), + FieldDef.section("ability.section.area"), + FieldDef.floatField("gui.radius", this::getRadius, this::setRadius), FieldDef.section("ability.section.damage"), FieldDef.row( - FieldDef.floatField("ability.damagePerSecond", this::getDamagePerSecond, this::setDamagePerSecond), - FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval) + FieldDef.floatField("gui.dps", this::getDamagePerSecond, this::setDamagePerSecond), + FieldDef.intField("gui.interval", this::getDamageInterval, this::setDamageInterval) ), - FieldDef.section("ability.section.debuffs"), - FieldDef.row( - FieldDef.intField("ability.slownessLevel", this::getSlownessLevel, this::setSlownessLevel), - FieldDef.intField("ability.debuffDuration", this::getDebuffDuration, this::setDebuffDuration) - ), - FieldDef.intField("ability.poisonLevel", this::getPoisonLevel, this::setPoisonLevel), + FieldDef.boolField("ability.ignoreInvulnFrames", this::isIgnoreInvulnFrames, this::setIgnoreInvulnFrames), FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster) .hover("ability.hover.affectsCaster"), AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) )); + + addVisualFieldDefs(defs); + addTelegraphSizeField(defs); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 494192e04..173efa0de 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -1,17 +1,11 @@ package kamkeel.npcs.controllers.data.ability.type; -import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.LockMovementType; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.UserType; -import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData; +import kamkeel.npcs.entity.EntityAbilityZone; +import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityTrap; @@ -21,38 +15,18 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; import java.util.List; -import java.util.Random; -import java.util.Set; -import java.util.UUID; /** - * Trap ability: Places a proximity-triggered trap. - * Features arm time, trigger radius, and various effects on trigger. + * Trap ability: Places proximity-triggered traps around the caster. + * Spawns one or more EntityAbilityZone entities in random positions + * within spawnRadius of the caster. Damage and trigger logic handled by the entity. */ -public class AbilityTrap extends Ability implements IAbilityTrap { +public class AbilityTrap extends AbilityZone implements IAbilityTrap { - public enum TrapPlacement { - AT_CASTER, - AT_TARGET, - AHEAD_OF_CASTER; - - @Override - public String toString() { - switch (this) { - case AT_CASTER: return "ability.trapPlace.atCaster"; - case AT_TARGET: return "ability.trapPlace.atTarget"; - case AHEAD_OF_CASTER: return "ability.trapPlace.aheadOfCaster"; - default: return name(); - } - } - } - - private int durationTicks = 200; - private TrapPlacement placement = TrapPlacement.AT_TARGET; - private float placementDistance = 5.0f; + // Trap-specific fields private float triggerRadius = 2.0f; private int armTime = 20; private int maxTriggers = 1; @@ -62,240 +36,57 @@ public class AbilityTrap extends Ability implements IAbilityTrap { private float knockback = 0.5f; private boolean visible = true; - // Offset parameters - trap spawns near target, not exactly on them - private float minOffset = 0.0f; - private float maxOffset = 1.5f; - private boolean randomOffset = true; - - // Runtime state - private static final Random RANDOM = new Random(); - private transient double trapX, trapY, trapZ; - private transient boolean armed = false; - private transient int triggerCount = 0; - private transient int ticksSinceLastTrigger = 0; - private transient Set triggeredEntities = new HashSet<>(); - public AbilityTrap() { + super(200, new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.0f, 0.5f, 1.5f)); this.typeId = "ability.cnpc.trap"; this.name = "Trap"; - this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 15.0f; - this.lockMovement = LockMovementType.WINDUP; - this.cooldownTicks = 0; this.windUpTicks = 20; - this.telegraphType = TelegraphType.CIRCLE; - this.allowedBy = UserType.NPC_ONLY; - } - - @Override - public boolean allowBurst() { - return false; - } - - @Override - public boolean isTargetingModeLocked() { - return true; - } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; - } - - @Override - public float getTelegraphRadius() { - return triggerRadius; - } - - @Override - public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(caster, target); - if (instance == null) return null; - - // Control telegraph following based on placement mode - switch (placement) { - case AT_CASTER: - case AHEAD_OF_CASTER: - // Telegraph at caster or ahead, no following - instance.setEntityIdToFollow(-1); - if (placement == TrapPlacement.AHEAD_OF_CASTER) { - double yaw = Math.toRadians(caster.rotationYaw); - instance.setX(caster.posX - Math.sin(yaw) * placementDistance); - instance.setY(caster.posY); - instance.setZ(caster.posZ + Math.cos(yaw) * placementDistance); - } else { - instance.setX(caster.posX); - instance.setY(caster.posY); - instance.setZ(caster.posZ); - } - break; - case AT_TARGET: - if (target != null) { - instance.setEntityIdToFollow(target.getEntityId()); - } - break; - } - return instance; } @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { - armed = false; - triggerCount = 0; - ticksSinceLastTrigger = armTime; - triggeredEntities.clear(); - - // Use telegraph position if available (for AT_TARGET, it was following target) - TelegraphInstance telegraph = getTelegraphInstance(); - - switch (placement) { - case AT_CASTER: - trapX = caster.posX; - trapY = caster.posY; - trapZ = caster.posZ; - break; - case AT_TARGET: - if (telegraph != null) { - double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(), - minOffset, maxOffset, randomOffset, RANDOM); - trapX = pos[0]; - trapY = pos[1]; - trapZ = pos[2]; - telegraph.setX(trapX); - telegraph.setY(trapY); - telegraph.setZ(trapZ); - } else if (target != null) { - double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ, - minOffset, maxOffset, randomOffset, RANDOM); - trapX = pos[0]; - trapY = pos[1]; - trapZ = pos[2]; - } else { - trapX = caster.posX; - trapY = caster.posY; - trapZ = caster.posZ; - } - break; - case AHEAD_OF_CASTER: - double yaw = Math.toRadians(caster.rotationYaw); - trapX = caster.posX - Math.sin(yaw) * placementDistance; - trapY = caster.posY; - trapZ = caster.posZ + Math.cos(yaw) * placementDistance; - break; - } - } - - @Override - public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - // Check if trap duration has ended - if (tick >= durationTicks) { + if (world.isRemote && !isPreview()) { signalCompletion(); return; } - if (!armed) { - if (tick >= armTime) { - armed = true; + activeEntities.clear(); + + List placedPositions = new ArrayList<>(); + float minSeparation = triggerRadius * 2.0f; + + for (int i = 0; i < zoneCount; i++) { + double[] pos = findSpawnPosition(caster, placedPositions, minSeparation); + placedPositions.add(pos); + + EntityAbilityZone entity = EntityAbilityZone.createTrap(world, caster, + pos[0], caster.posY, pos[1], + zoneShape, + triggerRadius, armTime, maxTriggers, triggerCooldown, + damage, damageRadius, knockback, durationTicks, + colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled, + zoneHeight, + particleDensity, particleScale, animSpeed, lightningDensity, + getEffects()); + + applyVisualToEntity(entity); + + if (isPreview()) { + entity.setupPreview(caster); } - return; - } - ticksSinceLastTrigger++; - - if (maxTriggers > 0 && triggerCount >= maxTriggers) { - signalCompletion(); // All triggers used, trap is done - return; - } - - if (ticksSinceLastTrigger < triggerCooldown) { - return; - } - - if (!isPreview()) { - AxisAlignedBB box = AxisAlignedBB.getBoundingBox( - trapX - triggerRadius, trapY - 1, trapZ - triggerRadius, - trapX + triggerRadius, trapY + 2, trapZ + triggerRadius - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); - - for (EntityLivingBase entity : entities) { - if (entity == caster) continue; - if (entity.isDead) continue; - if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; - - double dx = entity.posX - trapX; - double dz = entity.posZ - trapZ; - double dist = Math.sqrt(dx * dx + dz * dz); - - if (dist <= triggerRadius) { - triggerTrap(caster, entity, world); - return; - } - } + spawnAbilityEntity(world, entity); + activeEntities.add(entity); } } - private void triggerTrap(EntityLivingBase caster, EntityLivingBase triggerer, World world) { - triggerCount++; - ticksSinceLastTrigger = 0; - triggeredEntities.add(triggerer.getUniqueID()); - - Set affected = new HashSet<>(); - - if (damageRadius > 0) { - AxisAlignedBB box = AxisAlignedBB.getBoundingBox( - trapX - damageRadius, trapY - 1, trapZ - damageRadius, - trapX + damageRadius, trapY + 3, trapZ + damageRadius - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); - - for (EntityLivingBase entity : entities) { - if (entity == caster) continue; - double dx = entity.posX - trapX; - double dz = entity.posZ - trapZ; - double dist = Math.sqrt(dx * dx + dz * dz); - if (dist <= damageRadius) { - affected.add(entity); - } - } - } else { - affected.add(triggerer); - } - - for (EntityLivingBase entity : affected) { - // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback); - - // Apply effects if the hit wasn't cancelled - if (wasHit) { - applyEffects(entity); - } - } - } - - @Override - public void onComplete(EntityLivingBase caster, EntityLivingBase target) { - armed = false; - triggerCount = 0; - triggeredEntities.clear(); - } - - @Override - public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { - armed = false; - triggerCount = 0; - triggeredEntities.clear(); - } + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ @Override public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setInteger("durationTicks", durationTicks); - nbt.setString("placement", placement.name()); - nbt.setFloat("placementDistance", placementDistance); + writeZoneNBT(nbt); nbt.setFloat("triggerRadius", triggerRadius); nbt.setInteger("armTime", armTime); nbt.setInteger("maxTriggers", maxTriggers); @@ -304,20 +95,11 @@ public class AbilityTrap extends Ability implements IAbilityTrap { nbt.setFloat("damageRadius", damageRadius); nbt.setFloat("knockback", knockback); nbt.setBoolean("visible", visible); - nbt.setFloat("minOffset", minOffset); - nbt.setFloat("maxOffset", maxOffset); - nbt.setBoolean("randomOffset", randomOffset); } @Override public void readTypeNBT(NBTTagCompound nbt) { - this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 200; - try { - this.placement = TrapPlacement.valueOf(nbt.getString("placement")); - } catch (Exception e) { - this.placement = TrapPlacement.AT_TARGET; - } - this.placementDistance = nbt.hasKey("placementDistance") ? nbt.getFloat("placementDistance") : 5.0f; + readZoneNBT(nbt, 200); this.triggerRadius = nbt.hasKey("triggerRadius") ? nbt.getFloat("triggerRadius") : 2.0f; this.armTime = nbt.hasKey("armTime") ? nbt.getInteger("armTime") : 20; this.maxTriggers = nbt.hasKey("maxTriggers") ? nbt.getInteger("maxTriggers") : 1; @@ -326,168 +108,58 @@ public class AbilityTrap extends Ability implements IAbilityTrap { this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 0.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; this.visible = !nbt.hasKey("visible") || nbt.getBoolean("visible"); - this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f; - this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 1.5f; - this.randomOffset = !nbt.hasKey("randomOffset") || nbt.getBoolean("randomOffset"); } - // Getters & Setters - public int getDurationTicks() { - return durationTicks; - } + // ═══════════════════════════════════════════════════════════════════ + // TRAP-SPECIFIC GETTERS & SETTERS + // ═══════════════════════════════════════════════════════════════════ - public void setDurationTicks(int durationTicks) { - this.durationTicks = Math.max(1, durationTicks); - } + public float getTriggerRadius() { return triggerRadius; } + public void setTriggerRadius(float triggerRadius) { this.triggerRadius = triggerRadius; } + public int getArmTime() { return armTime; } + public void setArmTime(int armTime) { this.armTime = armTime; } + public int getMaxTriggers() { return maxTriggers; } + public void setMaxTriggers(int maxTriggers) { this.maxTriggers = maxTriggers; } + public int getTriggerCooldown() { return triggerCooldown; } + public void setTriggerCooldown(int triggerCooldown) { this.triggerCooldown = triggerCooldown; } + public float getDamage() { return damage; } + public void setDamage(float damage) { this.damage = damage; } + public float getDamageRadius() { return damageRadius; } + public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; } + public float getKnockback() { return knockback; } + public void setKnockback(float knockback) { this.knockback = knockback; } + public boolean isVisible() { return visible; } + public void setVisible(boolean visible) { this.visible = visible; } - public TrapPlacement getPlacementEnum() { - return placement; - } - - public void setPlacementEnum(TrapPlacement placement) { - this.placement = placement; - } - - @Override - public int getPlacement() { - return placement.ordinal(); - } - - @Override - public void setPlacement(int placement) { - TrapPlacement[] values = TrapPlacement.values(); - this.placement = placement >= 0 && placement < values.length ? values[placement] : TrapPlacement.AT_TARGET; - } - - public float getPlacementDistance() { - return placementDistance; - } - - public void setPlacementDistance(float placementDistance) { - this.placementDistance = placementDistance; - } - - public float getTriggerRadius() { - return triggerRadius; - } - - public void setTriggerRadius(float triggerRadius) { - this.triggerRadius = triggerRadius; - } - - public int getArmTime() { - return armTime; - } - - public void setArmTime(int armTime) { - this.armTime = armTime; - } - - public int getMaxTriggers() { - return maxTriggers; - } - - public void setMaxTriggers(int maxTriggers) { - this.maxTriggers = maxTriggers; - } - - public int getTriggerCooldown() { - return triggerCooldown; - } - - public void setTriggerCooldown(int triggerCooldown) { - this.triggerCooldown = triggerCooldown; - } - - public float getDamage() { - return damage; - } - - public void setDamage(float damage) { - this.damage = damage; - } - - public float getDamageRadius() { - return damageRadius; - } - - public void setDamageRadius(float damageRadius) { - this.damageRadius = damageRadius; - } - - public float getKnockback() { - return knockback; - } - - public void setKnockback(float knockback) { - this.knockback = knockback; - } - - public boolean isVisible() { - return visible; - } - - public void setVisible(boolean visible) { - this.visible = visible; - } - - public float getMinOffset() { - return minOffset; - } - - public void setMinOffset(float minOffset) { - this.minOffset = minOffset; - } - - public float getMaxOffset() { - return maxOffset; - } - - public void setMaxOffset(float maxOffset) { - this.maxOffset = maxOffset; - } - - public boolean isRandomOffset() { - return randomOffset; - } - - public void setRandomOffset(boolean randomOffset) { - this.randomOffset = randomOffset; - } - - // Runtime getters - public double getTrapX() { - return trapX; - } - - public double getTrapY() { - return trapY; - } - - public double getTrapZ() { - return trapZ; - } - - public boolean isArmed() { - return armed; - } + // ═══════════════════════════════════════════════════════════════════ + // GUI FIELD DEFINITIONS + // ═══════════════════════════════════════════════════════════════════ @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { defs.addAll(Arrays.asList( - FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) - .range(1, 2000), - FieldDef.enumField("ability.placement", TrapPlacement.class, this::getPlacementEnum, this::setPlacementEnum) - .hover("ability.hover.placement"), + FieldDef.row( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000), + FieldDef.enumField("ability.zoneShape", ZoneShape.class, this::getZoneShapeEnum, this::setZoneShapeEnum) + ), + FieldDef.section("ability.section.zone"), + FieldDef.row( + FieldDef.floatField("gui.radius", this::getSpawnRadius, this::setSpawnRadius), + FieldDef.intField("gui.count", this::getZoneCount, this::setZoneCount).range(1, 20) + ), + FieldDef.row( + FieldDef.floatField("gui.height", this::getZoneHeight, this::setZoneHeight), + FieldDef.boolField("ability.visible", this::isVisible, this::setVisible) + ), FieldDef.section("ability.section.trigger"), FieldDef.row( FieldDef.floatField("gui.radius", this::getTriggerRadius, this::setTriggerRadius), FieldDef.intField("ability.armTime", this::getArmTime, this::setArmTime) ), FieldDef.row( - FieldDef.intField("ability.maxTriggers", this::getMaxTriggers, this::setMaxTriggers), - FieldDef.intField("ability.triggerCooldown", this::getTriggerCooldown, this::setTriggerCooldown) + FieldDef.intField("gui.max", this::getMaxTriggers, this::setMaxTriggers), + FieldDef.intField("gui.cooldown", this::getTriggerCooldown, this::setTriggerCooldown) ), FieldDef.section("ability.section.damage"), FieldDef.row( @@ -497,5 +169,8 @@ public class AbilityTrap extends Ability implements IAbilityTrap { FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) )); + + addVisualFieldDefs(defs); + addTelegraphSizeField(defs); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java new file mode 100644 index 000000000..94ceea56d --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -0,0 +1,554 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.UserType; +import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityZone; +import kamkeel.npcs.entity.EntityAbilityZone.AccentStyle; +import kamkeel.npcs.entity.EntityAbilityZone.ParticleMotion; +import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.DamageSource; +import net.minecraft.world.World; + +import noppes.npcs.client.gui.builder.FieldDef; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +/** + * Abstract base class for zone-based abilities (Trap, Hazard). + * Extracts shared fields: duration, shape, spawn, visual, lifecycle, and FieldDefs. + */ +public abstract class AbilityZone extends Ability { + + // Zone properties + protected int durationTicks; + protected ZoneShape zoneShape = ZoneShape.CIRCLE; + protected float spawnRadius = 5.0f; + protected float telegraphSize = 5.0f; + protected int zoneCount = 1; + protected float zoneHeight = 2.0f; + + // Visual parameters + protected float particleDensity = 1.0f; + protected float particleScale = 1.0f; + protected float animSpeed = 1.0f; + protected float lightningDensity = 1.0f; + protected EnergyDisplayData colorData; + + // Visual layer fields (always active) + protected boolean groundFill = true; + protected float groundAlpha = 0.25f; + protected boolean rings = true; + protected int ringCount = 3; + protected boolean border = true; + protected float borderSpeed = 1.0f; + protected boolean accents = true; + protected int accentStyle = 0; + protected boolean lightning = false; + protected boolean particles = true; + protected int particleMotion = 0; + protected String particleDir = ""; + protected int particleSize = 32; + protected boolean particleGlow = true; + + // Runtime state + protected static final Random RANDOM = new Random(); + protected transient List activeEntities = new ArrayList<>(); + + protected AbilityZone(int defaultDuration, EnergyDisplayData defaultColors) { + this.durationTicks = defaultDuration; + this.colorData = defaultColors; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 15.0f; + this.lockMovement = LockMovementType.WINDUP; + this.cooldownTicks = 0; + this.telegraphType = TelegraphType.CIRCLE; + this.allowedBy = UserType.NPC_ONLY; + } + + // ═════════════════════════════════════════════════════════════════ + // SHARED OVERRIDES + // ═════════════════════════════════════════════════════════════════ + + @Override + public boolean allowBurst() { + return false; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public float getTelegraphRadius() { + return telegraphSize; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + TelegraphInstance instance = super.createTelegraph(caster, target); + if (instance == null) return null; + + // Always show telegraph at caster position + instance.setEntityIdToFollow(-1); + instance.setX(caster.posX); + instance.setY(caster.posY); + instance.setZ(caster.posZ); + return instance; + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Remove dead entities + Iterator it = activeEntities.iterator(); + while (it.hasNext()) { + EntityAbilityZone entity = it.next(); + if (entity == null || entity.isDead) { + it.remove(); + } + } + + if (activeEntities.isEmpty()) { + signalCompletion(); + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + cleanup(); + } + + @Override + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + cleanup(); + } + + @Override + public void cleanup() { + for (EntityAbilityZone entity : activeEntities) { + if (entity != null && !entity.isDead) { + killAbilityEntity(entity); + } + } + activeEntities.clear(); + } + + @Override + public int getMaxPreviewDuration() { + return durationTicks + 10; + } + + // ═════════════════════════════════════════════════════════════════ + // SHARED SPAWN HELPER + // ═════════════════════════════════════════════════════════════════ + + /** + * Find a non-overlapping spawn position within spawnRadius of the caster. + */ + protected double[] findSpawnPosition(EntityLivingBase caster, List placedPositions, float minSeparation) { + double spawnX = caster.posX; + double spawnZ = caster.posZ; + + for (int attempt = 0; attempt < 15; attempt++) { + double angle = RANDOM.nextDouble() * Math.PI * 2; + double dist = Math.sqrt(RANDOM.nextDouble()) * spawnRadius; + double candidateX = caster.posX + Math.cos(angle) * dist; + double candidateZ = caster.posZ + Math.sin(angle) * dist; + + boolean overlaps = false; + for (double[] placed : placedPositions) { + double dx = candidateX - placed[0]; + double dz = candidateZ - placed[1]; + if (Math.sqrt(dx * dx + dz * dz) < minSeparation) { + overlaps = true; + break; + } + } + + spawnX = candidateX; + spawnZ = candidateZ; + if (!overlaps) break; + } + + return new double[]{spawnX, spawnZ}; + } + + /** + * Apply visual settings to a zone entity. + */ + protected void applyVisualToEntity(EntityAbilityZone entity) { + entity.applyVisual(groundFill, groundAlpha, + rings, ringCount, border, borderSpeed, + accents, accentStyle, lightning, + particles, particleMotion, particleDir, + particleSize, particleGlow); + } + + // ═════════════════════════════════════════════════════════════════ + // PRESET SUPPORT + // ═════════════════════════════════════════════════════════════════ + + /** + * Apply preset defaults by old style name (for backward compatibility). + */ + private void applyPresetDefaults(String styleName) { + switch (styleName) { + case "TOXIC": + groundFill = true; groundAlpha = 0.30f; rings = true; ringCount = 3; + border = false; borderSpeed = 1.0f; accents = true; accentStyle = 1; + lightning = false; particles = true; particleMotion = 0; particleGlow = true; + break; + case "INFERNO": + groundFill = true; groundAlpha = 0.35f; rings = true; ringCount = 3; + border = true; borderSpeed = 2.0f; accents = true; accentStyle = 2; + lightning = false; particles = true; particleMotion = 0; particleGlow = true; + break; + case "ARCANE": + groundFill = true; groundAlpha = 0.20f; rings = true; ringCount = 1; + border = true; borderSpeed = 0.8f; accents = true; accentStyle = 0; + lightning = false; particles = true; particleMotion = 1; particleGlow = true; + break; + case "ELECTRIC": + groundFill = true; groundAlpha = 0.15f; rings = false; ringCount = 1; + border = false; borderSpeed = 1.0f; accents = false; accentStyle = 0; + lightning = true; particles = true; particleMotion = 2; particleGlow = true; + break; + case "FROST": + groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3; + border = true; borderSpeed = 0.3f; accents = true; accentStyle = 0; + lightning = false; particles = true; particleMotion = 1; particleGlow = true; + break; + case "DEFAULT": + groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3; + border = true; borderSpeed = 1.0f; accents = true; accentStyle = 0; + lightning = false; particles = false; particleMotion = 0; particleGlow = true; + break; + default: + break; + } + } + + /** + * Client-only preset enum for the GUI dropdown. + * Not persisted — presets simply set field values. + */ + @SideOnly(Side.CLIENT) + public enum ZonePreset { + CUSTOM, DEFAULT, TOXIC, INFERNO, ARCANE, ELECTRIC, FROST; + + @Override + public String toString() { + return "ability.preset." + name().toLowerCase(); + } + } + + /** + * Check current field values against each preset, return matching preset or CUSTOM. + */ + @SideOnly(Side.CLIENT) + public ZonePreset getCurrentPreset() { + if (matchesPreset(true, 0.25f, true, 3, true, 1.0f, true, 0, false, false, 0, true)) + return ZonePreset.DEFAULT; + if (matchesPreset(true, 0.30f, true, 3, false, 1.0f, true, 1, false, true, 0, true)) + return ZonePreset.TOXIC; + if (matchesPreset(true, 0.35f, true, 3, true, 2.0f, true, 2, false, true, 0, true)) + return ZonePreset.INFERNO; + if (matchesPreset(true, 0.20f, true, 1, true, 0.8f, true, 0, false, true, 1, true)) + return ZonePreset.ARCANE; + if (matchesPreset(true, 0.15f, false, 1, false, 1.0f, false, 0, true, true, 2, true)) + return ZonePreset.ELECTRIC; + if (matchesPreset(true, 0.25f, true, 3, true, 0.3f, true, 0, false, true, 1, true)) + return ZonePreset.FROST; + return ZonePreset.CUSTOM; + } + + @SideOnly(Side.CLIENT) + private boolean matchesPreset(boolean gf, float ga, boolean r, int rc, boolean b, float bs, + boolean a, int as, boolean l, boolean p, int pm, boolean pg) { + return groundFill == gf && Math.abs(groundAlpha - ga) < 0.001f + && rings == r && ringCount == rc && border == b && Math.abs(borderSpeed - bs) < 0.001f + && accents == a && accentStyle == as && lightning == l + && particles == p && particleMotion == pm && particleGlow == pg; + } + + /** + * Apply a preset's field values. CUSTOM does nothing (user configures manually). + */ + @SideOnly(Side.CLIENT) + public void applyPreset(ZonePreset preset) { + if (preset == ZonePreset.CUSTOM) return; + applyPresetDefaults(preset.name()); + } + + // ═════════════════════════════════════════════════════════════════ + // SHARED NBT + // ═════════════════════════════════════════════════════════════════ + + protected void writeZoneNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); + nbt.setString("zoneShape", zoneShape.name()); + nbt.setFloat("spawnRadius", spawnRadius); + nbt.setFloat("telegraphSize", telegraphSize); + nbt.setInteger("zoneCount", zoneCount); + nbt.setFloat("zoneHeight", zoneHeight); + nbt.setFloat("particleDensity", particleDensity); + nbt.setFloat("particleScale", particleScale); + nbt.setFloat("animSpeed", animSpeed); + nbt.setFloat("lightningDensity", lightningDensity); + colorData.writeNBT(nbt); + + // Visual layer fields — always written as top-level keys + nbt.setBoolean("groundFill", groundFill); + nbt.setFloat("groundAlpha", groundAlpha); + nbt.setBoolean("rings", rings); + nbt.setInteger("ringCount", ringCount); + nbt.setBoolean("border", border); + nbt.setFloat("borderSpeed", borderSpeed); + nbt.setBoolean("accents", accents); + nbt.setInteger("accentStyle", accentStyle); + nbt.setBoolean("lightning", lightning); + nbt.setBoolean("particles", particles); + nbt.setInteger("particleMotion", particleMotion); + nbt.setString("particleDir", particleDir); + nbt.setInteger("particleSize", particleSize); + nbt.setBoolean("particleGlow", particleGlow); + } + + protected void readZoneNBT(NBTTagCompound nbt, int defaultDuration) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : defaultDuration; + if (nbt.hasKey("zoneShape")) { + try { this.zoneShape = ZoneShape.valueOf(nbt.getString("zoneShape")); } + catch (Exception e) { this.zoneShape = ZoneShape.CIRCLE; } + } else { + this.zoneShape = ZoneShape.CIRCLE; + } + this.spawnRadius = nbt.hasKey("spawnRadius") ? nbt.getFloat("spawnRadius") : 5.0f; + this.telegraphSize = nbt.hasKey("telegraphSize") ? nbt.getFloat("telegraphSize") : spawnRadius; + this.zoneCount = nbt.hasKey("zoneCount") ? nbt.getInteger("zoneCount") : 1; + this.zoneHeight = nbt.hasKey("zoneHeight") ? nbt.getFloat("zoneHeight") : 2.0f; + this.particleDensity = nbt.hasKey("particleDensity") ? nbt.getFloat("particleDensity") : 1.0f; + this.particleScale = nbt.hasKey("particleScale") ? nbt.getFloat("particleScale") : 1.0f; + this.animSpeed = nbt.hasKey("animSpeed") ? nbt.getFloat("animSpeed") : 1.0f; + this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 1.0f; + colorData.readNBT(nbt); + + // Visual fields — backward compat with old zoneStyle format + if (nbt.hasKey("zoneStyle")) { + // OLD FORMAT: apply preset defaults, then overlay CustomVisual + applyPresetDefaults(nbt.getString("zoneStyle")); + if (nbt.hasKey("CustomVisual")) { + NBTTagCompound custom = nbt.getCompoundTag("CustomVisual"); + this.groundFill = !custom.hasKey("groundFill") || custom.getBoolean("groundFill"); + this.groundAlpha = custom.hasKey("groundAlpha") ? custom.getFloat("groundAlpha") : 0.25f; + this.rings = !custom.hasKey("rings") || custom.getBoolean("rings"); + this.ringCount = custom.hasKey("ringCount") ? custom.getInteger("ringCount") : 3; + this.border = !custom.hasKey("border") || custom.getBoolean("border"); + this.borderSpeed = custom.hasKey("borderSpeed") ? custom.getFloat("borderSpeed") : 1.0f; + this.accents = !custom.hasKey("accents") || custom.getBoolean("accents"); + this.accentStyle = custom.hasKey("accentStyle") ? custom.getInteger("accentStyle") : 0; + this.lightning = custom.hasKey("lightning") && custom.getBoolean("lightning"); + this.particles = !custom.hasKey("particles") || custom.getBoolean("particles"); + this.particleMotion = custom.hasKey("particleMotion") ? custom.getInteger("particleMotion") : 0; + this.particleDir = custom.hasKey("particleDir") ? custom.getString("particleDir") : ""; + this.particleSize = custom.hasKey("particleSize") ? custom.getInteger("particleSize") : 32; + this.particleGlow = !custom.hasKey("particleGlow") || custom.getBoolean("particleGlow"); + } + } else { + // NEW FORMAT: read individual top-level keys + this.groundFill = !nbt.hasKey("groundFill") || nbt.getBoolean("groundFill"); + this.groundAlpha = nbt.hasKey("groundAlpha") ? nbt.getFloat("groundAlpha") : 0.25f; + this.rings = !nbt.hasKey("rings") || nbt.getBoolean("rings"); + this.ringCount = nbt.hasKey("ringCount") ? nbt.getInteger("ringCount") : 3; + this.border = !nbt.hasKey("border") || nbt.getBoolean("border"); + this.borderSpeed = nbt.hasKey("borderSpeed") ? nbt.getFloat("borderSpeed") : 1.0f; + this.accents = !nbt.hasKey("accents") || nbt.getBoolean("accents"); + this.accentStyle = nbt.hasKey("accentStyle") ? nbt.getInteger("accentStyle") : 0; + this.lightning = nbt.hasKey("lightning") && nbt.getBoolean("lightning"); + this.particles = !nbt.hasKey("particles") || nbt.getBoolean("particles"); + this.particleMotion = nbt.hasKey("particleMotion") ? nbt.getInteger("particleMotion") : 0; + this.particleDir = nbt.hasKey("particleDir") ? nbt.getString("particleDir") : ""; + this.particleSize = nbt.hasKey("particleSize") ? nbt.getInteger("particleSize") : 32; + this.particleGlow = !nbt.hasKey("particleGlow") || nbt.getBoolean("particleGlow"); + } + } + + // ═════════════════════════════════════════════════════════════════ + // SHARED GETTERS & SETTERS + // ═════════════════════════════════════════════════════════════════ + + public int getDurationTicks() { return durationTicks; } + public void setDurationTicks(int durationTicks) { this.durationTicks = Math.max(1, durationTicks); } + + public ZoneShape getZoneShapeEnum() { return zoneShape; } + public void setZoneShapeEnum(ZoneShape shape) { this.zoneShape = shape; } + + public int getZoneShapeOrdinal() { return zoneShape.ordinal(); } + public void setZoneShapeOrdinal(int shape) { + ZoneShape[] values = ZoneShape.values(); + this.zoneShape = shape >= 0 && shape < values.length ? values[shape] : ZoneShape.CIRCLE; + } + + public float getSpawnRadius() { return spawnRadius; } + public void setSpawnRadius(float spawnRadius) { this.spawnRadius = Math.max(0, spawnRadius); } + public float getTelegraphSize() { return telegraphSize; } + public void setTelegraphSize(float telegraphSize) { this.telegraphSize = Math.max(0, telegraphSize); } + public int getZoneCount() { return zoneCount; } + public void setZoneCount(int zoneCount) { this.zoneCount = Math.max(1, zoneCount); } + public float getZoneHeight() { return zoneHeight; } + public void setZoneHeight(float zoneHeight) { this.zoneHeight = Math.max(0.5f, zoneHeight); } + + public float getParticleDensity() { return particleDensity; } + public void setParticleDensity(float v) { this.particleDensity = Math.max(0, Math.min(5, v)); } + public float getParticleScale() { return particleScale; } + public void setParticleScale(float v) { this.particleScale = Math.max(0.1f, Math.min(5, v)); } + public float getAnimSpeed() { return animSpeed; } + public void setAnimSpeed(float v) { this.animSpeed = Math.max(0.1f, Math.min(5, v)); } + public float getLightningDensity() { return lightningDensity; } + public void setLightningDensity(float v) { this.lightningDensity = Math.max(0, Math.min(3, v)); } + + // Color getters/setters + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int color) { colorData.innerColor = color; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int color) { colorData.outerColor = color; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean enabled) { colorData.outerColorEnabled = enabled; } + + // Visual layer getters/setters + public boolean isGroundFill() { return groundFill; } + public void setGroundFill(boolean v) { this.groundFill = v; } + public float getGroundAlpha() { return groundAlpha; } + public void setGroundAlpha(float v) { this.groundAlpha = Math.max(0, Math.min(1, v)); } + public boolean isRings() { return rings; } + public void setRings(boolean v) { this.rings = v; } + public int getRingCount() { return ringCount; } + public void setRingCount(int v) { this.ringCount = Math.max(1, Math.min(5, v)); } + public boolean isBorder() { return border; } + public void setBorder(boolean v) { this.border = v; } + public float getBorderSpeed() { return borderSpeed; } + public void setBorderSpeed(float v) { this.borderSpeed = Math.max(0.1f, Math.min(5, v)); } + public boolean isAccents() { return accents; } + public void setAccents(boolean v) { this.accents = v; } + public int getAccentStyle() { return accentStyle; } + public void setAccentStyle(int v) { this.accentStyle = Math.max(0, Math.min(AccentStyle.values().length - 1, v)); } + public AccentStyle getAccentStyleEnum() { return AccentStyle.values()[Math.max(0, Math.min(accentStyle, AccentStyle.values().length - 1))]; } + public void setAccentStyleEnum(AccentStyle s) { this.accentStyle = s.ordinal(); } + public boolean isLightning() { return lightning; } + public void setLightning(boolean v) { this.lightning = v; } + public boolean isParticles() { return particles; } + public void setParticles(boolean v) { this.particles = v; } + public int getParticleMotion() { return particleMotion; } + public void setParticleMotion(int v) { this.particleMotion = Math.max(0, Math.min(ParticleMotion.values().length - 1, v)); } + public ParticleMotion getParticleMotionEnum() { return ParticleMotion.values()[Math.max(0, Math.min(particleMotion, ParticleMotion.values().length - 1))]; } + public void setParticleMotionEnum(ParticleMotion m) { this.particleMotion = m.ordinal(); } + public String getParticleDir() { return particleDir; } + public void setParticleDir(String v) { this.particleDir = v != null ? v : ""; } + public int getParticleSize() { return particleSize; } + public void setParticleSize(int v) { this.particleSize = Math.max(1, Math.min(256, v)); } + public boolean isParticleGlow() { return particleGlow; } + public void setParticleGlow(boolean v) { this.particleGlow = v; } + + // ═════════════════════════════════════════════════════════════════ + // SHARED FIELD DEFINITIONS (client-only) + // ═════════════════════════════════════════════════════════════════ + + @SideOnly(Side.CLIENT) + protected void addVisualFieldDefs(List defs) { + defs.addAll(Arrays.asList( + // Preset dropdown + FieldDef.enumField("gui.preset", ZonePreset.class, this::getCurrentPreset, this::applyPreset) + .tab("ability.tab.visual"), + + // Ground Fill + FieldDef.section("ability.section.ground").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isGroundFill, this::setGroundFill), + FieldDef.floatField("gui.alpha", this::getGroundAlpha, this::setGroundAlpha) + ).tab("ability.tab.visual"), + + // Rings + FieldDef.section("ability.section.rings").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isRings, this::setRings), + FieldDef.intField("gui.count", this::getRingCount, this::setRingCount).range(1, 5) + ).tab("ability.tab.visual"), + + // Border + FieldDef.section("ability.section.border").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isBorder, this::setBorder), + FieldDef.floatField("gui.speed", this::getBorderSpeed, this::setBorderSpeed) + ).tab("ability.tab.visual"), + + // Accents + FieldDef.section("ability.section.accents").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isAccents, this::setAccents), + FieldDef.enumField("gui.style", AccentStyle.class, this::getAccentStyleEnum, this::setAccentStyleEnum) + ).tab("ability.tab.visual"), + + // Lightning + FieldDef.section("ability.section.lightning").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isLightning, this::setLightning), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + ).tab("ability.tab.visual"), + + // Particles + FieldDef.section("ability.section.particles").tab("ability.tab.visual"), + FieldDef.row( + FieldDef.boolField("gui.enabled", this::isParticles, this::setParticles), + FieldDef.floatField("gui.density", this::getParticleDensity, this::setParticleDensity) + ).tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("gui.scale", this::getParticleScale, this::setParticleScale), + FieldDef.enumField("gui.motion", ParticleMotion.class, this::getParticleMotionEnum, this::setParticleMotionEnum) + ).tab("ability.tab.visual"), + FieldDef.boolField("gui.glow", this::isParticleGlow, this::setParticleGlow) + .tab("ability.tab.visual"), + FieldDef.stringField("gui.texture", this::getParticleDir, this::setParticleDir) + .tab("ability.tab.visual").visibleWhen(this::isParticles), + FieldDef.intField("gui.size", this::getParticleSize, this::setParticleSize).range(1, 256) + .tab("ability.tab.visual").visibleWhen(this::isParticles), + + // Animation + FieldDef.section("ability.section.animation").tab("ability.tab.visual"), + FieldDef.floatField("gui.speed", this::getAnimSpeed, this::setAnimSpeed) + .tab("ability.tab.visual"), + + // Colors + FieldDef.section("ability.section.colors").tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled) + )); + } + + @SideOnly(Side.CLIENT) + protected void addTelegraphSizeField(List defs) { + defs.add(FieldDef.floatField("gui.size", this::getTelegraphSize, this::setTelegraphSize) + .tab("ability.tab.effects")); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java new file mode 100644 index 000000000..2f2bc00e9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java @@ -0,0 +1,928 @@ +package kamkeel.npcs.entity; + +import cpw.mods.fml.common.network.ByteBufUtils; +import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; +import net.minecraft.world.World; +import noppes.npcs.NpcDamageSource; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +/** + * Shared zone entity for Trap and Hazard abilities. + * Handles rendering, positioning, and damage logic. + */ +public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnData { + + // ═══════════════════════════════════════════════════════════════════ + // ENUMS + // ═══════════════════════════════════════════════════════════════════ + + public enum ZoneType { TRAP, HAZARD } + public enum ZoneShape { + CIRCLE, SQUARE; + + @Override + public String toString() { + switch (this) { + case CIRCLE: return "ability.shape.circle"; + case SQUARE: return "ability.shape.square"; + default: return name(); + } + } + } + + public enum AccentStyle { + STATIC, SWAYING, FLICKERING; + + @Override + public String toString() { + switch (this) { + case STATIC: return "ability.accentStyle.static"; + case SWAYING: return "ability.accentStyle.swaying"; + case FLICKERING: return "ability.accentStyle.flickering"; + default: return name(); + } + } + } + + public enum ParticleMotion { + RISING, DRIFTING, SPARKS; + + @Override + public String toString() { + switch (this) { + case RISING: return "ability.particleMotion.rising"; + case DRIFTING: return "ability.particleMotion.drifting"; + case SPARKS: return "ability.particleMotion.sparks"; + default: return name(); + } + } + } + + /** Tiny Y offset above ground to prevent z-clipping */ + private static final double GROUND_OFFSET = 0.005; + + // ═══════════════════════════════════════════════════════════════════ + // COMMON PROPERTIES (synced via ByteBuf) + // ═══════════════════════════════════════════════════════════════════ + + private ZoneType zoneType = ZoneType.TRAP; + private ZoneShape shape = ZoneShape.CIRCLE; + + private int ownerEntityId = -1; + + private float radius = 4.0f; + + private int durationTicks = 200; + private int maxTicks = 200; + + private int innerColor = 0xFF6600; + private int outerColor = 0xFF0000; + private boolean outerColorEnabled = true; + + private float zoneHeight = 2.0f; + + // Visual parameters (synced for renderer) + private float particleDensity = 1.0f; + private float particleScale = 1.0f; + private float animSpeed = 1.0f; + private float lightningDensity = 1.0f; + + // Visual layer parameters (always active) + private boolean groundFill = true; + private float groundAlpha = 0.25f; + private boolean rings = true; + private int ringCount = 3; + private boolean border = true; + private float borderSpeed = 1.0f; + private boolean accents = true; + private int accentStyle = 0; // AccentStyle ordinal + private boolean lightning = false; + private boolean particles = true; + private int particleMotion = 0; // ParticleMotion ordinal + private String particleDir = ""; + private int particleSize = 32; + private boolean particleGlow = true; + + // Lifetime + private long deathWorldTime = -1; + + // Effects + private List effects = new ArrayList<>(); + + // ═══════════════════════════════════════════════════════════════════ + // TRAP-SPECIFIC PROPERTIES + // ═══════════════════════════════════════════════════════════════════ + + private int armTime = 20; + private float triggerRadius = 2.0f; + private int maxTriggers = 1; + private int triggerCooldown = 20; + private float damage = 6.0f; + private float damageRadius = 0.0f; + private float knockback = 0.5f; + + // ═══════════════════════════════════════════════════════════════════ + // HAZARD-SPECIFIC PROPERTIES + // ═══════════════════════════════════════════════════════════════════ + + private float damagePerSecond = 1.0f; + private int damageInterval = 20; + private boolean ignoreInvulnFrames = false; + private boolean affectsCaster = false; + + // ═══════════════════════════════════════════════════════════════════ + // RUNTIME STATE (transient) + // ═══════════════════════════════════════════════════════════════════ + + // Trap runtime + private transient boolean armed = false; + private transient int triggerCount = 0; + private transient int ticksSinceLastTrigger = 0; + private transient Set triggeredEntities = new HashSet<>(); + private transient int triggerFlashTick = -1; // For visual flash on trigger + + // Hazard runtime + private transient int ticksSinceDamage = 0; + private transient Set damagedThisTick = new HashSet<>(); + + // Preview mode + private boolean previewMode = false; + private EntityLivingBase previewOwner = null; + + // ═══════════════════════════════════════════════════════════════════ + // CONSTRUCTORS + // ═══════════════════════════════════════════════════════════════════ + + public EntityAbilityZone(World world) { + super(world); + this.setSize(0.1f, 0.1f); + this.noClip = true; + this.isImmuneToFire = true; + this.ignoreFrustumCheck = true; + } + + /** + * Full constructor for creating a zone entity. + * Use the static factory methods for cleaner creation. + * Position is snapped to ground level with a tiny offset to avoid z-clipping. + */ + private EntityAbilityZone(World world, ZoneType type, EntityLivingBase owner, + double x, double y, double z) { + this(world); + this.zoneType = type; + this.ownerEntityId = owner.getEntityId(); + // Snap to ground with tiny offset to prevent z-clipping + double groundY = Ability.findGroundLevel(world, x, y, z) + GROUND_OFFSET; + this.setPosition(x, groundY, z); + } + + /** + * Create a Trap zone entity. + */ + public static EntityAbilityZone createTrap(World world, EntityLivingBase owner, + double x, double y, double z, + ZoneShape shape, + float triggerRadius, int armTime, int maxTriggers, + int triggerCooldown, float damage, float damageRadius, + float knockback, int durationTicks, + int innerColor, int outerColor, boolean outerColorEnabled, + float zoneHeight, + float particleDensity, float particleScale, + float animSpeed, float lightningDensity, + List effects) { + EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.TRAP, owner, x, y, z); + zone.shape = shape; + zone.radius = triggerRadius; + zone.triggerRadius = triggerRadius; + zone.armTime = armTime; + zone.maxTriggers = maxTriggers; + zone.triggerCooldown = triggerCooldown; + zone.damage = damage; + zone.damageRadius = damageRadius; + zone.knockback = knockback; + zone.durationTicks = durationTicks; + zone.maxTicks = durationTicks; + zone.innerColor = innerColor; + zone.outerColor = outerColor; + zone.outerColorEnabled = outerColorEnabled; + zone.zoneHeight = zoneHeight; + zone.particleDensity = particleDensity; + zone.particleScale = particleScale; + zone.animSpeed = animSpeed; + zone.lightningDensity = lightningDensity; + if (effects != null) { + for (AbilityEffect e : effects) { + zone.effects.add(e.copy()); + } + } + // Ready to trigger immediately after arm time + zone.ticksSinceLastTrigger = triggerCooldown; + return zone; + } + + /** + * Create a Hazard zone entity. + */ + public static EntityAbilityZone createHazard(World world, EntityLivingBase owner, + double x, double y, double z, + ZoneShape shape, + float radius, + float damagePerSecond, int damageInterval, + boolean ignoreInvulnFrames, boolean affectsCaster, + int durationTicks, + int innerColor, int outerColor, boolean outerColorEnabled, + float zoneHeight, + float particleDensity, float particleScale, + float animSpeed, float lightningDensity, + List effects) { + EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.HAZARD, owner, x, y, z); + zone.shape = shape; + zone.radius = radius; + zone.damagePerSecond = damagePerSecond; + zone.damageInterval = damageInterval; + zone.ignoreInvulnFrames = ignoreInvulnFrames; + zone.affectsCaster = affectsCaster; + zone.durationTicks = durationTicks; + zone.maxTicks = durationTicks; + zone.innerColor = innerColor; + zone.outerColor = outerColor; + zone.outerColorEnabled = outerColorEnabled; + zone.zoneHeight = zoneHeight; + zone.particleDensity = particleDensity; + zone.particleScale = particleScale; + zone.animSpeed = animSpeed; + zone.lightningDensity = lightningDensity; + if (effects != null) { + for (AbilityEffect e : effects) { + zone.effects.add(e.copy()); + } + } + // Ready to deal damage immediately + zone.ticksSinceDamage = damageInterval; + return zone; + } + + // ═══════════════════════════════════════════════════════════════════ + // ENTITY LIFECYCLE + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void entityInit() { + // No DataWatcher needed + } + + @Override + public void onUpdate() { + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + + super.onUpdate(); + + // Set death time on first tick + if (deathWorldTime < 0 && worldObj != null) { + deathWorldTime = worldObj.getTotalWorldTime() + maxTicks; + } + + // Check lifetime + if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { + this.setDead(); + return; + } + + // Server handles damage + if (!worldObj.isRemote && !previewMode) { + switch (zoneType) { + case TRAP: + tickTrap(); + break; + case HAZARD: + tickHazard(); + break; + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // TRAP LOGIC (server-only) + // ═══════════════════════════════════════════════════════════════════ + + private void tickTrap() { + // Arm time countdown + if (!armed) { + if (ticksExisted >= armTime) { + armed = true; + } + return; + } + + ticksSinceLastTrigger++; + + // All triggers used + if (maxTriggers > 0 && triggerCount >= maxTriggers) { + this.setDead(); + return; + } + + // Cooldown between triggers + if (ticksSinceLastTrigger < triggerCooldown) { + return; + } + + // Proximity detection + AxisAlignedBB box = AxisAlignedBB.getBoundingBox( + posX - triggerRadius, posY - 0.5, posZ - triggerRadius, + posX + triggerRadius, posY + zoneHeight, posZ + triggerRadius + ); + + Entity owner = getOwner(); + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box); + + for (EntityLivingBase entity : entities) { + if (owner != null && entity == owner) continue; + if (entity.isDead) continue; + if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; + + double dx = entity.posX - posX; + double dz = entity.posZ - posZ; + + boolean inRange; + if (shape == ZoneShape.SQUARE) { + inRange = Math.abs(dx) <= triggerRadius && Math.abs(dz) <= triggerRadius; + } else { + double dist = Math.sqrt(dx * dx + dz * dz); + inRange = dist <= triggerRadius; + } + + if (inRange) { + triggerTrap(entity); + return; + } + } + } + + private void triggerTrap(EntityLivingBase triggerer) { + Entity owner = getOwner(); + Set affected = new HashSet<>(); + + if (damageRadius > 0) { + AxisAlignedBB box = AxisAlignedBB.getBoundingBox( + posX - damageRadius, posY - 0.5, posZ - damageRadius, + posX + damageRadius, posY + zoneHeight, posZ + damageRadius + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box); + + for (EntityLivingBase entity : entities) { + if (owner != null && entity == owner) continue; + double dx = entity.posX - posX; + double dz = entity.posZ - posZ; + if (shape == ZoneShape.SQUARE) { + if (Math.abs(dx) <= damageRadius && Math.abs(dz) <= damageRadius) { + affected.add(entity); + } + } else { + double dist = Math.sqrt(dx * dx + dz * dz); + if (dist <= damageRadius) { + affected.add(entity); + } + } + } + } else { + affected.add(triggerer); + } + + boolean anyDamaged = false; + for (EntityLivingBase entity : affected) { + boolean hit = applyDamage(entity, owner, damage); + if (hit) { + anyDamaged = true; + } + + // Apply knockback only if damage landed + if (hit && knockback > 0) { + double dx = entity.posX - posX; + double dz = entity.posZ - posZ; + double len = Math.sqrt(dx * dx + dz * dz); + if (len > 0) { + entity.addVelocity((dx / len) * knockback * 0.5, 0.1, (dz / len) * knockback * 0.5); + entity.velocityChanged = true; + } + } + + // Apply effects only if damage landed + if (hit) { + applyEffects(entity); + } + } + + // Only count the trigger if at least one entity was actually damaged + if (anyDamaged) { + triggerCount++; + ticksSinceLastTrigger = 0; + triggeredEntities.add(triggerer.getUniqueID()); + triggerFlashTick = ticksExisted; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // HAZARD LOGIC (server-only) + // ═══════════════════════════════════════════════════════════════════ + + private void tickHazard() { + damagedThisTick.clear(); + ticksSinceDamage++; + + if (ticksSinceDamage < damageInterval) { + return; + } + ticksSinceDamage = 0; + + AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( + posX - radius, posY - 0.5, posZ - radius, + posX + radius, posY + zoneHeight, posZ + radius + ); + + Entity owner = getOwner(); + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); + + for (EntityLivingBase entity : entities) { + if (owner != null && entity == owner && !affectsCaster) continue; + if (damagedThisTick.contains(entity.getEntityId())) continue; + if (!isInZone(entity)) continue; + + if (damagePerSecond > 0) { + if (ignoreInvulnFrames) { + entity.hurtResistantTime = 0; + } + applyDamage(entity, owner, damagePerSecond); + } + + applyEffects(entity); + damagedThisTick.add(entity.getEntityId()); + } + } + + private boolean isInZone(EntityLivingBase entity) { + double dx = entity.posX - posX; + double dz = entity.posZ - posZ; + + switch (shape) { + case CIRCLE: + return Math.sqrt(dx * dx + dz * dz) <= radius; + case SQUARE: + return Math.abs(dx) <= radius && Math.abs(dz) <= radius; + default: + return Math.sqrt(dx * dx + dz * dz) <= radius; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // DAMAGE + // ═══════════════════════════════════════════════════════════════════ + + private boolean applyDamage(EntityLivingBase target, Entity owner, float dmg) { + if (dmg <= 0) return false; + + if (owner instanceof EntityNPCInterface) { + return target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg); + } else if (owner instanceof EntityPlayer) { + return target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg); + } else if (owner instanceof EntityLivingBase) { + return target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg); + } else { + return target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); + } + } + + private void applyEffects(EntityLivingBase entity) { + if (entity == null || effects.isEmpty()) return; + for (AbilityEffect effect : effects) { + effect.apply(entity); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // ENTITY HELPERS + // ═══════════════════════════════════════════════════════════════════ + + private Entity getOwner() { + if (previewMode && previewOwner != null) return previewOwner; + if (ownerEntityId == -1) return null; + return worldObj.getEntityByID(ownerEntityId); + } + + public void setupPreview(EntityLivingBase owner) { + this.previewMode = true; + this.previewOwner = owner; + } + + // ═══════════════════════════════════════════════════════════════════ + // RENDERING SUPPORT + // ═══════════════════════════════════════════════════════════════════ + + @Override + @SideOnly(Side.CLIENT) + public boolean isInRangeToRenderDist(double distance) { + return distance < 16384.0D; // 128 blocks + } + + @Override + public boolean shouldRenderInPass(int pass) { + return pass == 1; // Translucent pass + } + + @Override + public float getBrightness(float partialTicks) { + return 1.0f; + } + + @Override + @SideOnly(Side.CLIENT) + public int getBrightnessForRender(float partialTicks) { + return 0xF000F0; + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS FOR RENDERER + // ═══════════════════════════════════════════════════════════════════ + + public ZoneType getZoneType() { return zoneType; } + public ZoneShape getShape() { return shape; } + public float getRadius() { return radius; } + public int getInnerColor() { return innerColor; } + public int getOuterColor() { return outerColor; } + public boolean isOuterColorEnabled() { return outerColorEnabled; } + public float getZoneHeight() { return zoneHeight; } + public boolean isArmed() { return armed || ticksExisted >= armTime; } + public int getTriggerFlashTick() { return triggerFlashTick; } + public int getDurationTicks() { return durationTicks; } + public int getMaxTicks() { return maxTicks; } + public float getParticleDensity() { return particleDensity; } + public float getParticleScale() { return particleScale; } + public float getAnimSpeed() { return animSpeed; } + public float getLightningDensity() { return lightningDensity; } + + // Visual layer getters + public boolean isGroundFill() { return groundFill; } + public float getGroundAlpha() { return groundAlpha; } + public boolean isRings() { return rings; } + public int getRingCount() { return ringCount; } + public boolean isBorder() { return border; } + public float getBorderSpeed() { return borderSpeed; } + public boolean isAccents() { return accents; } + public int getAccentStyle() { return accentStyle; } + public boolean isLightning() { return lightning; } + public boolean isParticles() { return particles; } + public int getParticleMotion() { return particleMotion; } + public String getParticleDir() { return particleDir; } + public int getParticleSize() { return particleSize; } + public boolean isParticleGlow() { return particleGlow; } + + /** + * Apply visual settings from ability data. + */ + public void applyVisual(boolean groundFill, float groundAlpha, + boolean rings, int ringCount, + boolean border, float borderSpeed, + boolean accents, int accentStyle, + boolean lightning, + boolean particles, int particleMotion, + String particleDir, int particleSize, boolean particleGlow) { + this.groundFill = groundFill; + this.groundAlpha = groundAlpha; + this.rings = rings; + this.ringCount = ringCount; + this.border = border; + this.borderSpeed = borderSpeed; + this.accents = accents; + this.accentStyle = accentStyle; + this.lightning = lightning; + this.particles = particles; + this.particleMotion = particleMotion; + this.particleDir = particleDir != null ? particleDir : ""; + this.particleSize = particleSize; + this.particleGlow = particleGlow; + } + + /** + * Apply preset defaults by old style name (for backward compatibility). + */ + private void applyPresetDefaults(String styleName) { + switch (styleName) { + case "TOXIC": + groundFill = true; groundAlpha = 0.30f; rings = true; ringCount = 3; + border = false; borderSpeed = 1.0f; accents = true; accentStyle = 1; + lightning = false; particles = true; particleMotion = 0; particleGlow = true; + break; + case "INFERNO": + groundFill = true; groundAlpha = 0.35f; rings = true; ringCount = 3; + border = true; borderSpeed = 2.0f; accents = true; accentStyle = 2; + lightning = false; particles = true; particleMotion = 0; particleGlow = true; + break; + case "ARCANE": + groundFill = true; groundAlpha = 0.20f; rings = true; ringCount = 1; + border = true; borderSpeed = 0.8f; accents = true; accentStyle = 0; + lightning = false; particles = true; particleMotion = 1; particleGlow = true; + break; + case "ELECTRIC": + groundFill = true; groundAlpha = 0.15f; rings = false; ringCount = 1; + border = false; borderSpeed = 1.0f; accents = false; accentStyle = 0; + lightning = true; particles = true; particleMotion = 2; particleGlow = true; + break; + case "FROST": + groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3; + border = true; borderSpeed = 0.3f; accents = true; accentStyle = 0; + lightning = false; particles = true; particleMotion = 1; particleGlow = true; + break; + default: // DEFAULT + groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3; + border = true; borderSpeed = 1.0f; accents = true; accentStyle = 0; + lightning = false; particles = false; particleMotion = 0; particleGlow = true; + break; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT SERIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void readEntityFromNBT(NBTTagCompound nbt) { + this.zoneType = ZoneType.values()[nbt.getInteger("ZoneType")]; + + // Shape: read new string key first, fall back to old ordinal (always CIRCLE for old RING/CONE) + if (nbt.hasKey("ShapeName")) { + try { + this.shape = ZoneShape.valueOf(nbt.getString("ShapeName")); + } catch (Exception e) { + this.shape = ZoneShape.CIRCLE; + } + } else { + // Legacy: old ordinal data — RING(1) and CONE(2) map to CIRCLE + this.shape = ZoneShape.CIRCLE; + } + + this.ownerEntityId = nbt.getInteger("OwnerId"); + this.radius = nbt.getFloat("Radius"); + this.durationTicks = nbt.getInteger("Duration"); + this.maxTicks = nbt.getInteger("MaxTicks"); + this.innerColor = nbt.getInteger("InnerColor"); + this.outerColor = nbt.getInteger("OuterColor"); + this.outerColorEnabled = nbt.getBoolean("OuterColorEnabled"); + this.zoneHeight = nbt.hasKey("ZoneHeight") ? nbt.getFloat("ZoneHeight") : 2.0f; + this.particleDensity = nbt.hasKey("ParticleDensity") ? nbt.getFloat("ParticleDensity") : 1.0f; + this.particleScale = nbt.hasKey("ParticleScale") ? nbt.getFloat("ParticleScale") : 1.0f; + this.animSpeed = nbt.hasKey("AnimSpeed") ? nbt.getFloat("AnimSpeed") : 1.0f; + this.lightningDensity = nbt.hasKey("LightningDensity") ? nbt.getFloat("LightningDensity") : 1.0f; + this.deathWorldTime = nbt.hasKey("DeathWorldTime") ? nbt.getLong("DeathWorldTime") : -1; + + // Visual fields — backward compat with old ZoneStyle format + if (nbt.hasKey("ZoneStyle")) { + // OLD FORMAT: apply preset defaults, then overlay CustomVisual + applyPresetDefaults(nbt.getString("ZoneStyle")); + if (nbt.hasKey("CustomVisual")) { + NBTTagCompound cv = nbt.getCompoundTag("CustomVisual"); + this.groundFill = !cv.hasKey("GroundFill") || cv.getBoolean("GroundFill"); + this.groundAlpha = cv.hasKey("GroundAlpha") ? cv.getFloat("GroundAlpha") : 0.25f; + this.rings = !cv.hasKey("Rings") || cv.getBoolean("Rings"); + this.ringCount = cv.hasKey("RingCount") ? cv.getInteger("RingCount") : 3; + this.border = !cv.hasKey("Border") || cv.getBoolean("Border"); + this.borderSpeed = cv.hasKey("BorderSpeed") ? cv.getFloat("BorderSpeed") : 1.0f; + this.accents = !cv.hasKey("Accents") || cv.getBoolean("Accents"); + this.accentStyle = cv.hasKey("AccentStyle") ? cv.getInteger("AccentStyle") : 0; + this.lightning = cv.hasKey("Lightning") && cv.getBoolean("Lightning"); + this.particles = !cv.hasKey("Particles") || cv.getBoolean("Particles"); + this.particleMotion = cv.hasKey("ParticleMotion") ? cv.getInteger("ParticleMotion") : 0; + this.particleDir = cv.hasKey("ParticleDir") ? cv.getString("ParticleDir") : ""; + this.particleSize = cv.hasKey("ParticleSize") ? cv.getInteger("ParticleSize") : 32; + this.particleGlow = !cv.hasKey("ParticleGlow") || cv.getBoolean("ParticleGlow"); + } + } else { + // NEW FORMAT: read individual top-level keys + this.groundFill = !nbt.hasKey("GroundFill") || nbt.getBoolean("GroundFill"); + this.groundAlpha = nbt.hasKey("GroundAlpha") ? nbt.getFloat("GroundAlpha") : 0.25f; + this.rings = !nbt.hasKey("Rings") || nbt.getBoolean("Rings"); + this.ringCount = nbt.hasKey("RingCount") ? nbt.getInteger("RingCount") : 3; + this.border = !nbt.hasKey("Border") || nbt.getBoolean("Border"); + this.borderSpeed = nbt.hasKey("BorderSpeed") ? nbt.getFloat("BorderSpeed") : 1.0f; + this.accents = !nbt.hasKey("Accents") || nbt.getBoolean("Accents"); + this.accentStyle = nbt.hasKey("AccentStyle") ? nbt.getInteger("AccentStyle") : 0; + this.lightning = nbt.hasKey("Lightning") && nbt.getBoolean("Lightning"); + this.particles = !nbt.hasKey("Particles") || nbt.getBoolean("Particles"); + this.particleMotion = nbt.hasKey("ParticleMotion") ? nbt.getInteger("ParticleMotion") : 0; + this.particleDir = nbt.hasKey("ParticleDir") ? nbt.getString("ParticleDir") : ""; + this.particleSize = nbt.hasKey("ParticleSize") ? nbt.getInteger("ParticleSize") : 32; + this.particleGlow = !nbt.hasKey("ParticleGlow") || nbt.getBoolean("ParticleGlow"); + } + + // Trap + this.armTime = nbt.getInteger("ArmTime"); + this.triggerRadius = nbt.getFloat("TriggerRadius"); + this.maxTriggers = nbt.getInteger("MaxTriggers"); + this.triggerCooldown = nbt.getInteger("TriggerCooldown"); + this.damage = nbt.getFloat("Damage"); + this.damageRadius = nbt.getFloat("DamageRadius"); + this.knockback = nbt.getFloat("Knockback"); + + // Hazard + this.damagePerSecond = nbt.getFloat("DamagePerSecond"); + this.damageInterval = nbt.getInteger("DamageInterval"); + this.ignoreInvulnFrames = nbt.getBoolean("IgnoreInvuln"); + this.affectsCaster = nbt.getBoolean("AffectsCaster"); + + // Effects + effects.clear(); + if (nbt.hasKey("Effects")) { + NBTTagList effectList = nbt.getTagList("Effects", 10); + for (int i = 0; i < effectList.tagCount(); i++) { + AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i)); + if (effect != null && effect.isValid()) { + effects.add(effect); + } + } + } + } + + @Override + protected void writeEntityToNBT(NBTTagCompound nbt) { + nbt.setInteger("ZoneType", zoneType.ordinal()); + nbt.setString("ShapeName", shape.name()); + nbt.setInteger("OwnerId", ownerEntityId); + nbt.setFloat("Radius", radius); + nbt.setInteger("Duration", durationTicks); + nbt.setInteger("MaxTicks", maxTicks); + nbt.setInteger("InnerColor", innerColor); + nbt.setInteger("OuterColor", outerColor); + nbt.setBoolean("OuterColorEnabled", outerColorEnabled); + nbt.setFloat("ZoneHeight", zoneHeight); + nbt.setFloat("ParticleDensity", particleDensity); + nbt.setFloat("ParticleScale", particleScale); + nbt.setFloat("AnimSpeed", animSpeed); + nbt.setFloat("LightningDensity", lightningDensity); + nbt.setLong("DeathWorldTime", deathWorldTime); + + // Visual fields — always written as top-level keys + nbt.setBoolean("GroundFill", groundFill); + nbt.setFloat("GroundAlpha", groundAlpha); + nbt.setBoolean("Rings", rings); + nbt.setInteger("RingCount", ringCount); + nbt.setBoolean("Border", border); + nbt.setFloat("BorderSpeed", borderSpeed); + nbt.setBoolean("Accents", accents); + nbt.setInteger("AccentStyle", accentStyle); + nbt.setBoolean("Lightning", lightning); + nbt.setBoolean("Particles", particles); + nbt.setInteger("ParticleMotion", particleMotion); + nbt.setString("ParticleDir", particleDir); + nbt.setInteger("ParticleSize", particleSize); + nbt.setBoolean("ParticleGlow", particleGlow); + + // Trap + nbt.setInteger("ArmTime", armTime); + nbt.setFloat("TriggerRadius", triggerRadius); + nbt.setInteger("MaxTriggers", maxTriggers); + nbt.setInteger("TriggerCooldown", triggerCooldown); + nbt.setFloat("Damage", damage); + nbt.setFloat("DamageRadius", damageRadius); + nbt.setFloat("Knockback", knockback); + + // Hazard + nbt.setFloat("DamagePerSecond", damagePerSecond); + nbt.setInteger("DamageInterval", damageInterval); + nbt.setBoolean("IgnoreInvuln", ignoreInvulnFrames); + nbt.setBoolean("AffectsCaster", affectsCaster); + + // Effects + NBTTagList effectList = new NBTTagList(); + for (AbilityEffect effect : effects) { + effectList.appendTag(effect.writeNBT()); + } + nbt.setTag("Effects", effectList); + } + + // ═══════════════════════════════════════════════════════════════════ + // SPAWN DATA (ByteBuf for client sync) + // ═══════════════════════════════════════════════════════════════════ + + @Override + public void writeSpawnData(ByteBuf buffer) { + buffer.writeInt(zoneType.ordinal()); + ByteBufUtils.writeUTF8String(buffer, shape.name()); + buffer.writeInt(ownerEntityId); + buffer.writeFloat(radius); + buffer.writeInt(durationTicks); + buffer.writeInt(maxTicks); + buffer.writeInt(innerColor); + buffer.writeInt(outerColor); + buffer.writeBoolean(outerColorEnabled); + buffer.writeFloat(zoneHeight); + buffer.writeFloat(particleDensity); + buffer.writeFloat(particleScale); + buffer.writeFloat(animSpeed); + buffer.writeFloat(lightningDensity); + + // Visual fields — always written individually + buffer.writeBoolean(groundFill); + buffer.writeFloat(groundAlpha); + buffer.writeBoolean(rings); + buffer.writeInt(ringCount); + buffer.writeBoolean(border); + buffer.writeFloat(borderSpeed); + buffer.writeBoolean(accents); + buffer.writeInt(accentStyle); + buffer.writeBoolean(lightning); + buffer.writeBoolean(particles); + buffer.writeInt(particleMotion); + ByteBufUtils.writeUTF8String(buffer, particleDir); + buffer.writeInt(particleSize); + buffer.writeBoolean(particleGlow); + + // Trap-specific (client needs armTime for visual state) + buffer.writeInt(armTime); + + // Effects via NBT in buffer + NBTTagCompound effectsNbt = new NBTTagCompound(); + NBTTagList effectList = new NBTTagList(); + for (AbilityEffect effect : effects) { + effectList.appendTag(effect.writeNBT()); + } + effectsNbt.setTag("Effects", effectList); + ByteBufUtils.writeTag(buffer, effectsNbt); + } + + @Override + public void readSpawnData(ByteBuf buffer) { + this.zoneType = ZoneType.values()[buffer.readInt()]; + String shapeName = ByteBufUtils.readUTF8String(buffer); + try { + this.shape = ZoneShape.valueOf(shapeName); + } catch (Exception e) { + this.shape = ZoneShape.CIRCLE; + } + this.ownerEntityId = buffer.readInt(); + this.radius = buffer.readFloat(); + this.durationTicks = buffer.readInt(); + this.maxTicks = buffer.readInt(); + this.innerColor = buffer.readInt(); + this.outerColor = buffer.readInt(); + this.outerColorEnabled = buffer.readBoolean(); + this.zoneHeight = buffer.readFloat(); + this.particleDensity = buffer.readFloat(); + this.particleScale = buffer.readFloat(); + this.animSpeed = buffer.readFloat(); + this.lightningDensity = buffer.readFloat(); + + // Visual fields — always read individually + this.groundFill = buffer.readBoolean(); + this.groundAlpha = buffer.readFloat(); + this.rings = buffer.readBoolean(); + this.ringCount = buffer.readInt(); + this.border = buffer.readBoolean(); + this.borderSpeed = buffer.readFloat(); + this.accents = buffer.readBoolean(); + this.accentStyle = buffer.readInt(); + this.lightning = buffer.readBoolean(); + this.particles = buffer.readBoolean(); + this.particleMotion = buffer.readInt(); + this.particleDir = ByteBufUtils.readUTF8String(buffer); + this.particleSize = buffer.readInt(); + this.particleGlow = buffer.readBoolean(); + + this.armTime = buffer.readInt(); + + // Effects via NBT + NBTTagCompound effectsNbt = ByteBufUtils.readTag(buffer); + effects.clear(); + if (effectsNbt != null && effectsNbt.hasKey("Effects")) { + NBTTagList effectList = effectsNbt.getTagList("Effects", 10); + for (int i = 0; i < effectList.tagCount(); i++) { + AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i)); + if (effect != null && effect.isValid()) { + effects.add(effect); + } + } + } + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 0bc081429..933450557 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -91,6 +91,7 @@ import kamkeel.npcs.entity.EntityAbilityDisc; import kamkeel.npcs.entity.EntityAbilityLaser; import kamkeel.npcs.entity.EntityAbilityBeam; import kamkeel.npcs.entity.EntityAbilitySweeper; +import kamkeel.npcs.entity.EntityAbilityZone; import noppes.npcs.entity.old.EntityNPCDwarfFemale; import noppes.npcs.entity.old.EntityNPCDwarfMale; import noppes.npcs.entity.old.EntityNPCElfFemale; @@ -292,6 +293,7 @@ public class CustomNpcs { registerNewEntity(EntityAbilityLaser.class, "abilitylaser", 64, 3, true); registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 64, 3, true); registerNewEntity(EntityAbilitySweeper.class, "abilitysweeper", 64, 3, true); + registerNewEntity(EntityAbilityZone.class, "abilityzone", 64, 3, true); new RecipeController(); diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 4331ab0ea..2970d835a 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -151,12 +151,14 @@ import kamkeel.npcs.client.renderer.RenderAbilityDisc; import kamkeel.npcs.client.renderer.RenderAbilityLaser; import kamkeel.npcs.client.renderer.RenderAbilityBeam; import kamkeel.npcs.client.renderer.RenderAbilitySweeper; +import kamkeel.npcs.client.renderer.RenderAbilityZone; import kamkeel.npcs.client.renderer.lightning.LightningHandler; import kamkeel.npcs.entity.EntityAbilityOrb; import kamkeel.npcs.entity.EntityAbilityDisc; import kamkeel.npcs.entity.EntityAbilityLaser; import kamkeel.npcs.entity.EntityAbilityBeam; import kamkeel.npcs.entity.EntityAbilitySweeper; +import kamkeel.npcs.entity.EntityAbilityZone; import noppes.npcs.client.renderer.blocks.BlockBannerRenderer; import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer; import noppes.npcs.client.renderer.blocks.BlockBeamRenderer; @@ -270,6 +272,7 @@ public class ClientProxy extends CommonProxy { RenderingRegistry.registerEntityRenderingHandler(EntityAbilityLaser.class, new RenderAbilityLaser()); RenderingRegistry.registerEntityRenderingHandler(EntityAbilityBeam.class, new RenderAbilityBeam()); RenderingRegistry.registerEntityRenderingHandler(EntityAbilitySweeper.class, new RenderAbilitySweeper()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityZone.class, new RenderAbilityZone()); RenderingRegistry.registerEntityRenderingHandler(EntityCustomNpc.class, new RenderCustomNpc()); diff --git a/src/main/java/noppes/npcs/client/fx/CustomFX.java b/src/main/java/noppes/npcs/client/fx/CustomFX.java index 7a296d8e8..79d5f452c 100644 --- a/src/main/java/noppes/npcs/client/fx/CustomFX.java +++ b/src/main/java/noppes/npcs/client/fx/CustomFX.java @@ -84,6 +84,10 @@ public class CustomFX extends EntityFX { this.imageData = ClientCacheHandler.getImageData(this.directory); } + public void setMaxAge(int age) { + this.particleMaxAge = age; + } + public static CustomFX fromScriptedParticle(ScriptParticle particle, World worldObj, Entity entity) { EntityClientPlayerMP player = Minecraft.getMinecraft().thePlayer; if (entity == player) { diff --git a/src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java b/src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java new file mode 100644 index 000000000..80f6f687b --- /dev/null +++ b/src/main/java/noppes/npcs/client/fx/ZoneParticleFX.java @@ -0,0 +1,65 @@ +package noppes.npcs.client.fx; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.particle.EntityFX; +import net.minecraft.world.World; + +/** + * Simple particle for zone visual effects (Trap/Hazard styles). + * Uses vanilla particle atlas with color tinting. Fades out over lifetime. + */ +@SideOnly(Side.CLIENT) +public class ZoneParticleFX extends EntityFX { + + private final float startAlpha; + private final boolean glow; + + public ZoneParticleFX(World world, double x, double y, double z, + double motionX, double motionY, double motionZ, + float r, float g, float b, float alpha, + float scale, int maxAge, float gravity, boolean glow) { + super(world, x, y, z, 0, 0, 0); + this.motionX = motionX; + this.motionY = motionY; + this.motionZ = motionZ; + this.particleRed = r; + this.particleGreen = g; + this.particleBlue = b; + this.particleAlpha = alpha; + this.startAlpha = alpha; + this.particleScale = scale; + this.particleMaxAge = maxAge; + this.particleGravity = gravity; + this.glow = glow; + this.noClip = true; + this.setParticleTextureIndex(0); + } + + @Override + public int getBrightnessForRender(float partialTick) { + return glow ? 0xF000F0 : super.getBrightnessForRender(partialTick); + } + + @Override + public void onUpdate() { + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + + if (this.particleAge++ >= this.particleMaxAge) { + this.setDead(); + return; + } + + // Linear fade out + float lifeRatio = (float) this.particleAge / (float) this.particleMaxAge; + this.particleAlpha = this.startAlpha * (1.0f - lifeRatio); + + // Movement + this.motionY -= 0.04D * (double) this.particleGravity; + this.posX += this.motionX; + this.posY += this.motionY; + this.posZ += this.motionZ; + } +} diff --git a/src/main/java/noppes/npcs/client/renderer/ImageData.java b/src/main/java/noppes/npcs/client/renderer/ImageData.java index 15c383e40..8e6f651c5 100644 --- a/src/main/java/noppes/npcs/client/renderer/ImageData.java +++ b/src/main/java/noppes/npcs/client/renderer/ImageData.java @@ -25,7 +25,7 @@ public class ImageData { public ImageData(String directory) { this.location = new ResourceLocation(directory); - if (directory.startsWith("https://")) { + if (directory.startsWith("https://") || directory.startsWith("http://")) { this.isUrl = true; TextureManager texturemanager = Minecraft.getMinecraft().getTextureManager(); this.imageDownloadAlt = new ImageDownloadAlt(null, directory, new ResourceLocation("customnpcs:textures/gui/invisible.png"), new ImageBufferDownloadAlt(true, false)); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 9319f1b36..408136c34 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1563,10 +1563,46 @@ ability.dash.defensive=Defensive ability.counter.flat=Flat ability.counter.percent=Percent -# Hazard Shape +# Zone Shape ability.shape.circle=Circle +ability.shape.square=Square ability.shape.ring=Ring ability.shape.cone=Cone +ability.zoneShape=Shape + +# Zone Presets +ability.preset.custom=Custom +ability.preset.default=Default +ability.preset.toxic=Toxic +ability.preset.inferno=Inferno +ability.preset.arcane=Arcane +ability.preset.electric=Electric +ability.preset.frost=Frost + +# Accent Styles +ability.accentStyle.static=Static +ability.accentStyle.swaying=Swaying +ability.accentStyle.flickering=Flickering + +# Particle Motion Styles +ability.particleMotion.rising=Rising +ability.particleMotion.drifting=Drifting +ability.particleMotion.sparks=Sparks + +# Zone Settings +ability.section.zone=Zone Placement +ability.telegraphSize=Telegraph Size +ability.coneAngle=Cone Angle +ability.ignoreInvulnFrames=Ignore Invuln Frames +ability.visible=Visible + +# Visual Layer Section Headers +ability.section.ground=Ground Fill +ability.section.rings=Rings +ability.section.border=Border +ability.section.accents=Accents +ability.section.lightning=Lightning +ability.section.particles=Particles # Placement Mode ability.placement.atCaster=At Caster @@ -1662,6 +1698,7 @@ ability.section.timing=Timing ability.section.movement=Movement ability.section.sounds=Sounds ability.section.animations=Animations +ability.section.animation=Animation ability.section.telegraph=Telegraph ability.section.beam=Beam ability.section.sweep=Sweep @@ -1711,6 +1748,19 @@ gui.animation=Animation gui.duration=Duration gui.amp=Amp gui.speed=Speed +gui.size=Size +gui.alpha=Alpha +gui.count=Count +gui.style=Style +gui.scale=Scale +gui.motion=Motion +gui.glow=Glow +gui.texture=Texture +gui.dps=DPS +gui.interval=Interval +gui.cooldown=Cooldown +gui.max=Max +gui.preset=Preset # Common Type Settings ability.radius=Radius @@ -1793,7 +1843,6 @@ ability.healAllies=Heal Allies # Hazard ability.shape=Shape ability.placement=Placement -ability.damagePerSecond=Damage/Sec ability.damageInterval=Damage Interval ability.slownessLevel=Slowness Level ability.debuffDuration=Debuff Duration @@ -1802,8 +1851,6 @@ ability.affectsCaster=Affects Caster # Trap ability.armTime=Arm Time -ability.maxTriggers=Max Triggers -ability.triggerCooldown=Trigger Cooldown # Sweeper ability.sweepSpeed=Sweep Speed @@ -1826,7 +1873,6 @@ ability.outerEnabled=Outer Glow ability.outerWidth=Outer Width ability.outerAlpha=Outer Alpha ability.lightning=Lightning -ability.lightningDensity=Density ability.lightningRadius=Radius # Projectile Count / Settings @@ -1930,7 +1976,6 @@ ability.hover.placement=Where the ability effect is placed (Caster, Target, etc. ability.hover.lockTarget=Sweeper beam tracks the target's position ability.hover.canCounter=Enable counterattack when blocking damage ability.hover.counterType=Flat = fixed damage, Percent = % of blocked damage -ability.hover.shape=Shape of the hazard zone (Circle, Ring, Cone) ability.hover.affectsCaster=Hazard can damage the NPC that created it ability.hover.sweepMode=Swipe = single arc attack, Spin = continuous rotation From 64ba087cc5d2d9281e5228b6fc0d17c5d927adf1 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 12 Feb 2026 14:07:03 -0500 Subject: [PATCH 27/34] Merge Conflict Compile Issues --- .../data/ability/type/AbilityHazard.java | 56 ------------------- 1 file changed, 56 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 89dd8b00d..35397371b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -38,8 +38,6 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { this.typeId = "ability.cnpc.hazard"; this.name = "Hazard"; this.windUpTicks = 30; - this.telegraphType = TelegraphType.CIRCLE; - this.allowedBy = UserType.NPC_ONLY; this.windUpAnimationName = "Ability_Hazard_Windup"; this.activeAnimationName = "Ability_Hazard_Active"; } @@ -49,60 +47,6 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { return radius; } - @Override - public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(caster, target); - if (instance == null) return null; - - // Control telegraph following based on placement mode - switch (placement) { - case AT_CASTER: - case FOLLOW_CASTER: - // Telegraph stays at caster position, no following - instance.setEntityIdToFollow(-1); - instance.setX(caster.posX); - instance.setY(caster.posY); - instance.setZ(caster.posZ); - break; - case AT_TARGET: - // Telegraph follows target during windup, locks on execute - if (target != null) { - instance.setEntityIdToFollow(target.getEntityId()); - } - break; - case FOLLOW_TARGET: - // Telegraph follows target during windup AND active phase - if (target != null) { - instance.setEntityIdToFollow(target.getEntityId()); - } - break; - } - return instance; - } - - @Override - public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - // AT_CASTER locks immediately, AT_TARGET follows during windup and locks on execute - if (tick == 0) { - switch (placement) { - case AT_CASTER: - zoneX = caster.posX; - zoneY = caster.posY; - zoneZ = caster.posZ; - positionLocked = true; - break; - case AT_TARGET: - // Position will be set from telegraph on execute - // Telegraph follows target during windup - positionLocked = false; - break; - default: - positionLocked = false; - break; - } - } - } - @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote && !isPreview()) { From 2d3106a2562b2e389499915abb342236433a1dab Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 12 Feb 2026 15:01:19 -0500 Subject: [PATCH 28/34] Fix Zone Defaults --- src/api | 2 +- .../client/renderer/TelegraphRenderer.java | 30 ++++ .../controllers/data/ability/Ability.java | 32 +++-- .../data/ability/type/AbilityBeam.java | 62 +++----- .../data/ability/type/AbilityDisc.java | 67 +++------ .../data/ability/type/AbilityHazard.java | 50 +++---- .../data/ability/type/AbilityOrb.java | 74 +++------- .../data/ability/type/AbilityTeleport.java | 9 -- .../data/ability/type/AbilityTrap.java | 30 +++- .../data/ability/type/AbilityZone.java | 136 +++++++++--------- .../controllers/data/telegraph/Telegraph.java | 6 + .../data/telegraph/TelegraphType.java | 5 + src/main/java/noppes/npcs/DataAbilities.java | 26 ++-- .../controllers/data/PlayerAbilityData.java | 21 +-- 14 files changed, 253 insertions(+), 297 deletions(-) diff --git a/src/api b/src/api index 092b18fe9..456bb2cbf 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 092b18fe95d57353d16960411e5289ac3bbfa6ef +Subproject commit 456bb2cbfa7456d114bfc22caf49020353282e07 diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java index 3b5e49e2d..78ba58aed 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java @@ -110,6 +110,9 @@ public class TelegraphRenderer { case POINT: renderPoint(red, green, blue, alpha); break; + case SQUARE: + renderSquare(telegraph.getRadius(), red, green, blue, alpha); + break; default: break; } @@ -258,6 +261,30 @@ public class TelegraphRenderer { tessellator.draw(); } + private void renderSquare(float radius, float r, float g, float b, float a) { + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(r, g, b, a); + + tessellator.addVertex(-radius, 0, -radius); + tessellator.addVertex(radius, 0, -radius); + tessellator.addVertex(radius, 0, radius); + tessellator.addVertex(-radius, 0, radius); + + tessellator.draw(); + + // Draw border + tessellator.startDrawing(GL11.GL_LINE_LOOP); + tessellator.setColorRGBA_F(r, g, b, Math.min(1.0f, a * 2)); + + tessellator.addVertex(-radius, 0.01, -radius); + tessellator.addVertex(radius, 0.01, -radius); + tessellator.addVertex(radius, 0.01, radius); + tessellator.addVertex(-radius, 0.01, radius); + + tessellator.draw(); + } + // ==================== GUI PREVIEW RENDERING ==================== /** @@ -322,6 +349,9 @@ public class TelegraphRenderer { case POINT: renderPoint(red, green, blue, alpha); break; + case SQUARE: + renderSquare(telegraph.getRadius(), red, green, blue, alpha); + break; default: break; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index b95879c3b..2dd86a4b4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -117,7 +117,7 @@ public abstract class Ability implements IAbility { protected transient int currentTick = 0; protected transient EntityLivingBase currentTarget; protected transient long executionStartTime; - protected transient TelegraphInstance telegraphInstance; + protected transient List telegraphInstances = new ArrayList<>(); // Burst execution state protected transient int burstIndex = 0; @@ -682,6 +682,9 @@ public abstract class Ability implements IAbility { case POINT: telegraph = new Telegraph("", TelegraphType.POINT); break; + case SQUARE: + telegraph = Telegraph.square(getTelegraphRadius()); + break; default: return null; } @@ -716,6 +719,17 @@ public abstract class Ability implements IAbility { return instance; } + /** + * Create all telegraph instances for this ability. + * Override for abilities that need multiple telegraphs (e.g., zone abilities). + * Default wraps createTelegraph() in a singleton list. + */ + public List createTelegraphs(EntityLivingBase caster, EntityLivingBase target) { + TelegraphInstance instance = createTelegraph(caster, target); + if (instance == null) return new ArrayList<>(); + return new ArrayList<>(Collections.singletonList(instance)); + } + /** * Find the ground level at a given position. * Searches downward from the given Y to find a solid block. @@ -784,17 +798,17 @@ public abstract class Ability implements IAbility { } /** - * Get the current telegraph instance. + * Get the current telegraph instances. */ - public TelegraphInstance getTelegraphInstance() { - return telegraphInstance; + public List getTelegraphInstances() { + return telegraphInstances; } /** - * Set the telegraph instance (called by DataAbilities). + * Set the telegraph instances (called by DataAbilities). */ - public void setTelegraphInstance(TelegraphInstance instance) { - this.telegraphInstance = instance; + public void setTelegraphInstances(List instances) { + this.telegraphInstances = instances != null ? instances : new ArrayList<>(); } // ═══════════════════════════════════════════════════════════════════ @@ -918,7 +932,7 @@ public abstract class Ability implements IAbility { } burstIndex = 0; currentTarget = null; - telegraphInstance = null; + telegraphInstances.clear(); } /** @@ -970,7 +984,7 @@ public abstract class Ability implements IAbility { currentTick = 0; burstIndex = 0; currentTarget = null; - telegraphInstance = null; + telegraphInstances.clear(); previewMode = false; previewEntityHandler = null; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index f99a03528..8bf2341f5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -118,8 +118,9 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { this.fireDelay = Math.max(0, delay); } - // Legacy compat + /** @deprecated Use {@link #getFireDelay()} */ public int getDualFireDelay() { return fireDelay; } + /** @deprecated Use {@link #setFireDelay(int)} */ public void setDualFireDelay(int delay) { setFireDelay(delay); } @Override @@ -225,8 +226,13 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { for (EntityAbilityBeam beam : beamEntities) { spawnAbilityEntity(world, beam); } - for (EntityAbilityBeam beam : beamEntities) { - fireBeamEntity(beam, target); + + // Fire first projectile immediately, let onActiveTick handle the rest with fireDelay + fireBeamEntity(beamEntities[0], target); + if (fireDelay <= 0) { + for (int i = 1; i < projectileCount; i++) { + fireBeamEntity(beamEntities[i], target); + } } } @@ -356,55 +362,21 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam { int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1; initProjectiles(count); - if (nbt.hasKey("fireDelay")) { - this.fireDelay = nbt.getInteger("fireDelay"); - } else if (nbt.hasKey("dualFireDelay")) { - this.fireDelay = nbt.getInteger("dualFireDelay"); - } else { - this.fireDelay = 0; - } + this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0; combatData.readNBT(nbt); homingData.readNBT(nbt); lifespanData.readNBT(nbt); - if (nbt.hasKey("Projectile_0")) { - NBTTagCompound proj0 = nbt.getCompoundTag("Projectile_0"); - if (proj0.hasKey("colorOverride")) { - // NEW format - displayData.readNBT(nbt); - lightningData.readNBT(nbt); - for (int i = 0; i < projectileCount; i++) { - if (nbt.hasKey("Projectile_" + i)) { - projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i)); - } - } - } else { - // OLD per-projectile format - displayData.readNBT(proj0); - lightningData.readNBT(proj0); - projectiles[0].anchor.readNBT(proj0); + // Shared visual data + displayData.readNBT(nbt); + lightningData.readNBT(nbt); - for (int i = 1; i < projectileCount; i++) { - if (nbt.hasKey("Projectile_" + i)) { - NBTTagCompound projNbt = nbt.getCompoundTag("Projectile_" + i); - projectiles[i].anchor.readNBT(projNbt); - - EnergyDisplayData tempDisplay = new EnergyDisplayData(); - tempDisplay.readNBT(projNbt); - if (tempDisplay.innerColor != displayData.innerColor || tempDisplay.outerColor != displayData.outerColor) { - projectiles[i].colorOverride = true; - projectiles[i].innerColor = tempDisplay.innerColor; - projectiles[i].outerColor = tempDisplay.outerColor; - } - } - } + // Per-projectile data + for (int i = 0; i < projectileCount; i++) { + if (nbt.hasKey("Projectile_" + i)) { + projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i)); } - } else { - // LEGACY flat format - displayData.readNBT(nbt); - lightningData.readNBT(nbt); - projectiles[0].anchor.readNBT(nbt); } } 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 f890b7d7b..886a269e2 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 @@ -124,8 +124,9 @@ public class AbilityDisc extends Ability implements IAbilityDisc { this.fireDelay = Math.max(0, delay); } - // Legacy compat + /** @deprecated Use {@link #getFireDelay()} */ public int getDualFireDelay() { return fireDelay; } + /** @deprecated Use {@link #setFireDelay(int)} */ public void setDualFireDelay(int delay) { setFireDelay(delay); } @Override @@ -228,8 +229,13 @@ public class AbilityDisc extends Ability implements IAbilityDisc { for (EntityAbilityDisc disc : discEntities) { spawnAbilityEntity(world, disc); } - for (EntityAbilityDisc disc : discEntities) { - fireDiscEntity(disc, target); + + // Fire first projectile immediately, let onActiveTick handle the rest with fireDelay + fireDiscEntity(discEntities[0], target); + if (fireDelay <= 0) { + for (int i = 1; i < projectileCount; i++) { + fireDiscEntity(discEntities[i], target); + } } } @@ -371,61 +377,22 @@ public class AbilityDisc extends Ability implements IAbilityDisc { int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1; initProjectiles(count); - // Read fireDelay with fallback to old dualFireDelay key - if (nbt.hasKey("fireDelay")) { - this.fireDelay = nbt.getInteger("fireDelay"); - } else if (nbt.hasKey("dualFireDelay")) { - this.fireDelay = nbt.getInteger("dualFireDelay"); - } else { - this.fireDelay = 0; - } + this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0; // Shared combat/movement data combatData.readNBT(nbt); homingData.readNBT(nbt); lifespanData.readNBT(nbt); - // Determine format and read accordingly - if (nbt.hasKey("Projectile_0")) { - NBTTagCompound proj0 = nbt.getCompoundTag("Projectile_0"); - if (proj0.hasKey("colorOverride")) { - // NEW format: shared visuals at root + ProjectileData per projectile - displayData.readNBT(nbt); - lightningData.readNBT(nbt); - for (int i = 0; i < projectileCount; i++) { - if (nbt.hasKey("Projectile_" + i)) { - projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i)); - } - } - } else { - // OLD format: full per-projectile arrays (displayData, lightningData, etc. in each Projectile_i) - // Read primary visuals from Projectile_0 - displayData.readNBT(proj0); - lightningData.readNBT(proj0); - projectiles[0].anchor.readNBT(proj0); + // Shared visual data + displayData.readNBT(nbt); + lightningData.readNBT(nbt); - // Read remaining projectiles, set color override if colors differ - for (int i = 1; i < projectileCount; i++) { - if (nbt.hasKey("Projectile_" + i)) { - NBTTagCompound projNbt = nbt.getCompoundTag("Projectile_" + i); - projectiles[i].anchor.readNBT(projNbt); - - // Check if this projectile had different colors - EnergyDisplayData tempDisplay = new EnergyDisplayData(); - tempDisplay.readNBT(projNbt); - if (tempDisplay.innerColor != displayData.innerColor || tempDisplay.outerColor != displayData.outerColor) { - projectiles[i].colorOverride = true; - projectiles[i].innerColor = tempDisplay.innerColor; - projectiles[i].outerColor = tempDisplay.outerColor; - } - } - } + // Per-projectile data + for (int i = 0; i < projectileCount; i++) { + if (nbt.hasKey("Projectile_" + i)) { + projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i)); } - } else { - // LEGACY flat format: all data at root level - displayData.readNBT(nbt); - lightningData.readNBT(nbt); - projectiles[0].anchor.readNBT(nbt); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 35397371b..551fbe9cc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -27,14 +27,14 @@ import java.util.List; public class AbilityHazard extends AbilityZone implements IAbilityHazard { // Hazard-specific fields - private float radius = 4.0f; + private float radius = 2.0f; private float damagePerSecond = 1.0f; private int damageInterval = 20; private boolean ignoreInvulnFrames = false; private boolean affectsCaster = false; public AbilityHazard() { - super(100, new EnergyDisplayData(0x00CC00, 0x006600, true, 1.0f, 0.5f, 1.5f)); + super(300, new EnergyDisplayData(0x00CC00, 0x006600, true, 1.0f, 0.5f, 1.5f)); this.typeId = "ability.cnpc.hazard"; this.name = "Hazard"; this.windUpTicks = 30; @@ -43,7 +43,7 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { } @Override - public float getTelegraphRadius() { + public float getZoneRadius() { return radius; } @@ -56,12 +56,24 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { activeEntities.clear(); - List placedPositions = new ArrayList<>(); - float minSeparation = radius * 2.0f; + // Use pre-calculated positions from telegraph phase, or generate new ones + List positions; + if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) { + positions = new ArrayList<>(preCalculatedPositions); + preCalculatedPositions.clear(); + } else { + positions = new ArrayList<>(); + List placedPositions = new ArrayList<>(); + float minSeparation = radius * 2.0f; + for (int i = 0; i < zoneCount; i++) { + double[] pos = findSpawnPosition(caster, placedPositions, minSeparation); + placedPositions.add(pos); + positions.add(pos); + } + } for (int i = 0; i < zoneCount; i++) { - double[] pos = findSpawnPosition(caster, placedPositions, minSeparation); - placedPositions.add(pos); + double[] pos = positions.get(i); EntityAbilityZone entity = EntityAbilityZone.createHazard(world, caster, pos[0], caster.posY, pos[1], @@ -102,26 +114,9 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { @Override public void readTypeNBT(NBTTagCompound nbt) { - readZoneNBT(nbt, 100); - - // Backward compat: old "shape" key with RING/CONE → ignore (base defaults to CIRCLE) - // The old HazardShape enum had CIRCLE/RING/CONE — only CIRCLE maps to new ZoneShape - if (nbt.hasKey("shape")) { - String oldShape = nbt.getString("shape"); - if ("RING".equals(oldShape) || "CONE".equals(oldShape)) { - this.zoneShape = ZoneShape.CIRCLE; - } - } - - this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 4.0f; - // Support legacy "damagePerTick" key for backwards compatibility - if (nbt.hasKey("damagePerSecond")) { - this.damagePerSecond = nbt.getFloat("damagePerSecond"); - } else if (nbt.hasKey("damagePerTick")) { - this.damagePerSecond = nbt.getFloat("damagePerTick"); - } else { - this.damagePerSecond = 1.0f; - } + readZoneNBT(nbt, 300); + this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 2.0f; + this.damagePerSecond = nbt.hasKey("damagePerSecond") ? nbt.getFloat("damagePerSecond") : 1.0f; this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 20; this.ignoreInvulnFrames = nbt.hasKey("ignoreInvulnFrames") && nbt.getBoolean("ignoreInvulnFrames"); this.affectsCaster = nbt.hasKey("affectsCaster") && nbt.getBoolean("affectsCaster"); @@ -178,6 +173,5 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { )); addVisualFieldDefs(defs); - addTelegraphSizeField(defs); } } 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 c4a4a1ebb..8b4783f7c 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 @@ -115,8 +115,9 @@ public class AbilityOrb extends Ability implements IAbilityOrb { this.fireDelay = Math.max(0, delay); } - // Legacy compat + /** @deprecated Use {@link #getFireDelay()} */ public int getDualFireDelay() { return fireDelay; } + /** @deprecated Use {@link #setFireDelay(int)} */ public void setDualFireDelay(int delay) { setFireDelay(delay); } @Override @@ -229,8 +230,13 @@ public class AbilityOrb extends Ability implements IAbilityOrb { for (EntityAbilityOrb orb : orbEntities) { spawnAbilityEntity(world, orb); } - for (EntityAbilityOrb orb : orbEntities) { - fireOrbEntity(orb, target); + + // Fire first projectile immediately, let onActiveTick handle the rest with fireDelay + fireOrbEntity(orbEntities[0], target); + if (fireDelay <= 0) { + for (int i = 1; i < projectileCount; i++) { + fireOrbEntity(orbEntities[i], target); + } } } @@ -354,8 +360,6 @@ public class AbilityOrb extends Ability implements IAbilityOrb { nbt.setTag("Projectile_" + i, projNbt); } - // Backward compat write - nbt.setFloat("orbSpeed", homingData.speed); } @Override @@ -365,66 +369,22 @@ public class AbilityOrb extends Ability implements IAbilityOrb { int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1; initProjectiles(count); - // Read fireDelay with fallback to old dualFireDelay key - if (nbt.hasKey("fireDelay")) { - this.fireDelay = nbt.getInteger("fireDelay"); - } else if (nbt.hasKey("dualFireDelay")) { - this.fireDelay = nbt.getInteger("dualFireDelay"); - } else { - this.fireDelay = 0; - } + this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0; // Shared combat/movement data combatData.readNBT(nbt); homingData.readNBT(nbt); lifespanData.readNBT(nbt); - // Determine format and read accordingly - if (nbt.hasKey("Projectile_0")) { - NBTTagCompound proj0 = nbt.getCompoundTag("Projectile_0"); - if (proj0.hasKey("colorOverride")) { - // NEW format: shared visuals at root + ProjectileData per projectile - displayData.readNBT(nbt); - lightningData.readNBT(nbt); - for (int i = 0; i < projectileCount; i++) { - if (nbt.hasKey("Projectile_" + i)) { - projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i)); - } - } - } else { - // OLD format: full per-projectile arrays (displayData, lightningData, etc. in each Projectile_i) - // Read primary visuals from Projectile_0 - displayData.readNBT(proj0); - lightningData.readNBT(proj0); - projectiles[0].anchor.readNBT(proj0); + // Shared visual data + displayData.readNBT(nbt); + lightningData.readNBT(nbt); - // Read remaining projectiles, set color override if colors differ - for (int i = 1; i < projectileCount; i++) { - if (nbt.hasKey("Projectile_" + i)) { - NBTTagCompound projNbt = nbt.getCompoundTag("Projectile_" + i); - projectiles[i].anchor.readNBT(projNbt); - - // Check if this projectile had different colors - EnergyDisplayData tempDisplay = new EnergyDisplayData(); - tempDisplay.readNBT(projNbt); - if (tempDisplay.innerColor != displayData.innerColor || tempDisplay.outerColor != displayData.outerColor) { - projectiles[i].colorOverride = true; - projectiles[i].innerColor = tempDisplay.innerColor; - projectiles[i].outerColor = tempDisplay.outerColor; - } - } - } + // Per-projectile data + for (int i = 0; i < projectileCount; i++) { + if (nbt.hasKey("Projectile_" + i)) { + projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i)); } - } else { - // LEGACY flat format: all data at root level - displayData.readNBT(nbt); - lightningData.readNBT(nbt); - projectiles[0].anchor.readNBT(nbt); - } - - // Backward compat - if (nbt.hasKey("orbSpeed")) { - homingData.speed = nbt.getFloat("orbSpeed"); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 8e09c43e7..4b19bc91d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -441,15 +441,6 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { } catch (Exception e) { this.mode = TeleportMode.BLINK; } - } else if (nbt.hasKey("pattern")) { - String legacy = nbt.getString("pattern"); - if ("BEHIND_TARGET".equals(legacy)) { - this.mode = TeleportMode.BEHIND; - } else if ("RANDOM".equals(legacy)) { - this.mode = TeleportMode.BLINK; - } else { - this.mode = TeleportMode.SINGLE; - } } else { this.mode = TeleportMode.BLINK; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 173efa0de..b296fa8bc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -37,12 +37,17 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { private boolean visible = true; public AbilityTrap() { - super(200, new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.0f, 0.5f, 1.5f)); + super(300, new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.0f, 0.5f, 1.5f)); this.typeId = "ability.cnpc.trap"; this.name = "Trap"; this.windUpTicks = 20; } + @Override + public float getZoneRadius() { + return triggerRadius; + } + @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote && !isPreview()) { @@ -52,12 +57,24 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { activeEntities.clear(); - List placedPositions = new ArrayList<>(); - float minSeparation = triggerRadius * 2.0f; + // Use pre-calculated positions from telegraph phase, or generate new ones + List positions; + if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) { + positions = new ArrayList<>(preCalculatedPositions); + preCalculatedPositions.clear(); + } else { + positions = new ArrayList<>(); + List placedPositions = new ArrayList<>(); + float minSeparation = triggerRadius * 2.0f; + for (int i = 0; i < zoneCount; i++) { + double[] pos = findSpawnPosition(caster, placedPositions, minSeparation); + placedPositions.add(pos); + positions.add(pos); + } + } for (int i = 0; i < zoneCount; i++) { - double[] pos = findSpawnPosition(caster, placedPositions, minSeparation); - placedPositions.add(pos); + double[] pos = positions.get(i); EntityAbilityZone entity = EntityAbilityZone.createTrap(world, caster, pos[0], caster.posY, pos[1], @@ -99,7 +116,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { @Override public void readTypeNBT(NBTTagCompound nbt) { - readZoneNBT(nbt, 200); + readZoneNBT(nbt, 300); this.triggerRadius = nbt.hasKey("triggerRadius") ? nbt.getFloat("triggerRadius") : 2.0f; this.armTime = nbt.hasKey("armTime") ? nbt.getInteger("armTime") : 20; this.maxTriggers = nbt.hasKey("maxTriggers") ? nbt.getInteger("maxTriggers") : 1; @@ -171,6 +188,5 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { )); addVisualFieldDefs(defs); - addTelegraphSizeField(defs); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java index 94ceea56d..5f8c33499 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -5,6 +5,7 @@ import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityZone; @@ -36,9 +37,8 @@ public abstract class AbilityZone extends Ability { // Zone properties protected int durationTicks; protected ZoneShape zoneShape = ZoneShape.CIRCLE; - protected float spawnRadius = 5.0f; - protected float telegraphSize = 5.0f; - protected int zoneCount = 1; + protected float spawnRadius = 10.0f; + protected int zoneCount = 3; protected float zoneHeight = 2.0f; // Visual parameters @@ -67,15 +67,15 @@ public abstract class AbilityZone extends Ability { // Runtime state protected static final Random RANDOM = new Random(); protected transient List activeEntities = new ArrayList<>(); + protected transient List preCalculatedPositions = new ArrayList<>(); protected AbilityZone(int defaultDuration, EnergyDisplayData defaultColors) { this.durationTicks = defaultDuration; this.colorData = defaultColors; this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 15.0f; + this.maxRange = 20.0f; this.lockMovement = LockMovementType.WINDUP; this.cooldownTicks = 0; - this.telegraphType = TelegraphType.CIRCLE; this.allowedBy = UserType.NPC_ONLY; } @@ -98,22 +98,53 @@ public abstract class AbilityZone extends Ability { return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } - @Override - public float getTelegraphRadius() { - return telegraphSize; - } + /** + * Returns the actual zone effect radius for telegraph sizing. + * AbilityHazard returns its damage radius, AbilityTrap returns triggerRadius. + */ + public abstract float getZoneRadius(); @Override - public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(caster, target); - if (instance == null) return null; + public List createTelegraphs(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || windUpTicks <= 0) return new ArrayList<>(); - // Always show telegraph at caster position - instance.setEntityIdToFollow(-1); - instance.setX(caster.posX); - instance.setY(caster.posY); - instance.setZ(caster.posZ); - return instance; + // Pre-calculate zone positions + preCalculatedPositions.clear(); + List placedPositions = new ArrayList<>(); + float minSeparation = getZoneRadius() * 2.0f; + + for (int i = 0; i < zoneCount; i++) { + double[] pos = findSpawnPosition(caster, placedPositions, minSeparation); + placedPositions.add(pos); + preCalculatedPositions.add(pos); + } + + // Create a telegraph for each zone position + List telegraphs = new ArrayList<>(); + float zoneRadius = getZoneRadius(); + + for (double[] pos : preCalculatedPositions) { + Telegraph telegraph; + if (zoneShape == ZoneShape.SQUARE) { + telegraph = Telegraph.square(zoneRadius); + } else { + telegraph = Telegraph.circle(zoneRadius); + } + + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + double groundY = findGroundLevel(caster.worldObj, pos[0], caster.posY, pos[1]); + TelegraphInstance instance = new TelegraphInstance(telegraph, pos[0], groundY, pos[1], 0); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(-1); + telegraphs.add(instance); + } + + return telegraphs; } @Override @@ -150,6 +181,7 @@ public abstract class AbilityZone extends Ability { } } activeEntities.clear(); + preCalculatedPositions.clear(); } @Override @@ -208,7 +240,7 @@ public abstract class AbilityZone extends Ability { // ═════════════════════════════════════════════════════════════════ /** - * Apply preset defaults by old style name (for backward compatibility). + * Apply preset defaults by style name. */ private void applyPresetDefaults(String styleName) { switch (styleName) { @@ -307,7 +339,6 @@ public abstract class AbilityZone extends Ability { nbt.setInteger("durationTicks", durationTicks); nbt.setString("zoneShape", zoneShape.name()); nbt.setFloat("spawnRadius", spawnRadius); - nbt.setFloat("telegraphSize", telegraphSize); nbt.setInteger("zoneCount", zoneCount); nbt.setFloat("zoneHeight", zoneHeight); nbt.setFloat("particleDensity", particleDensity); @@ -341,9 +372,8 @@ public abstract class AbilityZone extends Ability { } else { this.zoneShape = ZoneShape.CIRCLE; } - this.spawnRadius = nbt.hasKey("spawnRadius") ? nbt.getFloat("spawnRadius") : 5.0f; - this.telegraphSize = nbt.hasKey("telegraphSize") ? nbt.getFloat("telegraphSize") : spawnRadius; - this.zoneCount = nbt.hasKey("zoneCount") ? nbt.getInteger("zoneCount") : 1; + this.spawnRadius = nbt.hasKey("spawnRadius") ? nbt.getFloat("spawnRadius") : 10.0f; + this.zoneCount = nbt.hasKey("zoneCount") ? nbt.getInteger("zoneCount") : 3; this.zoneHeight = nbt.hasKey("zoneHeight") ? nbt.getFloat("zoneHeight") : 2.0f; this.particleDensity = nbt.hasKey("particleDensity") ? nbt.getFloat("particleDensity") : 1.0f; this.particleScale = nbt.hasKey("particleScale") ? nbt.getFloat("particleScale") : 1.0f; @@ -351,44 +381,21 @@ public abstract class AbilityZone extends Ability { this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 1.0f; colorData.readNBT(nbt); - // Visual fields — backward compat with old zoneStyle format - if (nbt.hasKey("zoneStyle")) { - // OLD FORMAT: apply preset defaults, then overlay CustomVisual - applyPresetDefaults(nbt.getString("zoneStyle")); - if (nbt.hasKey("CustomVisual")) { - NBTTagCompound custom = nbt.getCompoundTag("CustomVisual"); - this.groundFill = !custom.hasKey("groundFill") || custom.getBoolean("groundFill"); - this.groundAlpha = custom.hasKey("groundAlpha") ? custom.getFloat("groundAlpha") : 0.25f; - this.rings = !custom.hasKey("rings") || custom.getBoolean("rings"); - this.ringCount = custom.hasKey("ringCount") ? custom.getInteger("ringCount") : 3; - this.border = !custom.hasKey("border") || custom.getBoolean("border"); - this.borderSpeed = custom.hasKey("borderSpeed") ? custom.getFloat("borderSpeed") : 1.0f; - this.accents = !custom.hasKey("accents") || custom.getBoolean("accents"); - this.accentStyle = custom.hasKey("accentStyle") ? custom.getInteger("accentStyle") : 0; - this.lightning = custom.hasKey("lightning") && custom.getBoolean("lightning"); - this.particles = !custom.hasKey("particles") || custom.getBoolean("particles"); - this.particleMotion = custom.hasKey("particleMotion") ? custom.getInteger("particleMotion") : 0; - this.particleDir = custom.hasKey("particleDir") ? custom.getString("particleDir") : ""; - this.particleSize = custom.hasKey("particleSize") ? custom.getInteger("particleSize") : 32; - this.particleGlow = !custom.hasKey("particleGlow") || custom.getBoolean("particleGlow"); - } - } else { - // NEW FORMAT: read individual top-level keys - this.groundFill = !nbt.hasKey("groundFill") || nbt.getBoolean("groundFill"); - this.groundAlpha = nbt.hasKey("groundAlpha") ? nbt.getFloat("groundAlpha") : 0.25f; - this.rings = !nbt.hasKey("rings") || nbt.getBoolean("rings"); - this.ringCount = nbt.hasKey("ringCount") ? nbt.getInteger("ringCount") : 3; - this.border = !nbt.hasKey("border") || nbt.getBoolean("border"); - this.borderSpeed = nbt.hasKey("borderSpeed") ? nbt.getFloat("borderSpeed") : 1.0f; - this.accents = !nbt.hasKey("accents") || nbt.getBoolean("accents"); - this.accentStyle = nbt.hasKey("accentStyle") ? nbt.getInteger("accentStyle") : 0; - this.lightning = nbt.hasKey("lightning") && nbt.getBoolean("lightning"); - this.particles = !nbt.hasKey("particles") || nbt.getBoolean("particles"); - this.particleMotion = nbt.hasKey("particleMotion") ? nbt.getInteger("particleMotion") : 0; - this.particleDir = nbt.hasKey("particleDir") ? nbt.getString("particleDir") : ""; - this.particleSize = nbt.hasKey("particleSize") ? nbt.getInteger("particleSize") : 32; - this.particleGlow = !nbt.hasKey("particleGlow") || nbt.getBoolean("particleGlow"); - } + // Visual layer fields + this.groundFill = !nbt.hasKey("groundFill") || nbt.getBoolean("groundFill"); + this.groundAlpha = nbt.hasKey("groundAlpha") ? nbt.getFloat("groundAlpha") : 0.25f; + this.rings = !nbt.hasKey("rings") || nbt.getBoolean("rings"); + this.ringCount = nbt.hasKey("ringCount") ? nbt.getInteger("ringCount") : 3; + this.border = !nbt.hasKey("border") || nbt.getBoolean("border"); + this.borderSpeed = nbt.hasKey("borderSpeed") ? nbt.getFloat("borderSpeed") : 1.0f; + this.accents = !nbt.hasKey("accents") || nbt.getBoolean("accents"); + this.accentStyle = nbt.hasKey("accentStyle") ? nbt.getInteger("accentStyle") : 0; + this.lightning = nbt.hasKey("lightning") && nbt.getBoolean("lightning"); + this.particles = !nbt.hasKey("particles") || nbt.getBoolean("particles"); + this.particleMotion = nbt.hasKey("particleMotion") ? nbt.getInteger("particleMotion") : 0; + this.particleDir = nbt.hasKey("particleDir") ? nbt.getString("particleDir") : ""; + this.particleSize = nbt.hasKey("particleSize") ? nbt.getInteger("particleSize") : 32; + this.particleGlow = !nbt.hasKey("particleGlow") || nbt.getBoolean("particleGlow"); } // ═════════════════════════════════════════════════════════════════ @@ -409,8 +416,6 @@ public abstract class AbilityZone extends Ability { public float getSpawnRadius() { return spawnRadius; } public void setSpawnRadius(float spawnRadius) { this.spawnRadius = Math.max(0, spawnRadius); } - public float getTelegraphSize() { return telegraphSize; } - public void setTelegraphSize(float telegraphSize) { this.telegraphSize = Math.max(0, telegraphSize); } public int getZoneCount() { return zoneCount; } public void setZoneCount(int zoneCount) { this.zoneCount = Math.max(1, zoneCount); } public float getZoneHeight() { return zoneHeight; } @@ -546,9 +551,4 @@ public abstract class AbilityZone extends Ability { )); } - @SideOnly(Side.CLIENT) - protected void addTelegraphSizeField(List defs) { - defs.add(FieldDef.floatField("gui.size", this::getTelegraphSize, this::setTelegraphSize) - .tab("ability.tab.effects")); - } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java index 3ba858acc..bcb7a7958 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java @@ -150,6 +150,12 @@ public class Telegraph { return new Telegraph("", TelegraphType.POINT); } + public static Telegraph square(float radius) { + Telegraph t = new Telegraph("", TelegraphType.SQUARE); + t.radius = radius; + return t; + } + // ═══════════════════════════════════════════════════════════════════ // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java index 4a2fe4b12..dd080f093 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java @@ -29,6 +29,11 @@ public enum TelegraphType { */ POINT, + /** + * Filled square (uses radius as half-size) + */ + SQUARE, + /** * No telegraph */ diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 5e1ff694f..b5c23c756 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -191,10 +191,9 @@ public class DataAbilities { case ACTIVE: if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) { - // Just entered ACTIVE phase - lock telegraph position if it was following + // Just entered ACTIVE phase - lock telegraph positions if they were following // This commits the ability to its current target position - TelegraphInstance telegraph = currentAbility.getTelegraphInstance(); - if (telegraph != null) { + for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) { telegraph.lockPosition(); } @@ -603,26 +602,27 @@ public class DataAbilities { } /** - * Spawn and send telegraph for an ability. + * Spawn and send telegraphs for an ability. */ private void spawnTelegraph(Ability ability, EntityLivingBase target) { - TelegraphInstance telegraph = ability.createTelegraph(npc, target); - if (telegraph != null) { - ability.setTelegraphInstance(telegraph); - // Send to all nearby players - TelegraphSpawnPacket.sendToTracking(telegraph, npc); + List telegraphs = ability.createTelegraphs(npc, target); + if (!telegraphs.isEmpty()) { + ability.setTelegraphInstances(telegraphs); + for (TelegraphInstance telegraph : telegraphs) { + TelegraphSpawnPacket.sendToTracking(telegraph, npc); + } } } /** - * Remove telegraph when ability ends or is interrupted. + * Remove telegraphs when ability ends or is interrupted. */ private void removeTelegraph(Ability ability) { - TelegraphInstance telegraph = ability.getTelegraphInstance(); - if (telegraph != null) { + List telegraphs = ability.getTelegraphInstances(); + for (TelegraphInstance telegraph : telegraphs) { TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), npc); - ability.setTelegraphInstance(null); } + ability.setTelegraphInstances(null); } // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 6d265de27..a653e2a94 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -118,9 +118,8 @@ public class PlayerAbilityData implements IPlayerAbilityData { case ACTIVE: if (phaseChanged && oldPhase == AbilityPhase.WINDUP) { - // Just entered ACTIVE phase - TelegraphInstance telegraph = currentAbility.getTelegraphInstance(); - if (telegraph != null) { + // Just entered ACTIVE phase - lock all telegraph positions + for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) { telegraph.lockPosition(); } removeTelegraph(currentAbility, player); @@ -451,23 +450,25 @@ public class PlayerAbilityData implements IPlayerAbilityData { // ═══════════════════════════════════════════════════════════════════ private void spawnTelegraph(Ability ability, EntityPlayer player, EntityLivingBase target) { - TelegraphInstance telegraph = ability.createTelegraph(player, target); - if (telegraph != null) { - ability.setTelegraphInstance(telegraph); + List telegraphs = ability.createTelegraphs(player, target); + if (!telegraphs.isEmpty()) { + ability.setTelegraphInstances(telegraphs); if (player instanceof EntityPlayerMP) { - TelegraphSpawnPacket.sendToTracking(telegraph, player); + for (TelegraphInstance telegraph : telegraphs) { + TelegraphSpawnPacket.sendToTracking(telegraph, player); + } } } } private void removeTelegraph(Ability ability, EntityPlayer player) { - TelegraphInstance telegraph = ability.getTelegraphInstance(); - if (telegraph != null) { + List telegraphs = ability.getTelegraphInstances(); + for (TelegraphInstance telegraph : telegraphs) { if (player instanceof EntityPlayerMP) { TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player); } - ability.setTelegraphInstance(null); } + ability.setTelegraphInstances(null); } // ═══════════════════════════════════════════════════════════════════ From bf3391a1a8e6984e3ec6b14b91910e5d35c2114e Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 12 Feb 2026 18:46:11 -0300 Subject: [PATCH 29/34] Made trap windup a bit slower to match the animation Made the previously only hazard animation play on both hazard and trap --- .../npcs/controllers/data/ability/type/AbilityHazard.java | 4 ++-- .../npcs/controllers/data/ability/type/AbilityTrap.java | 4 +++- .../{Ability_Hazard_Active.json => Ability_Zone_Active.json} | 2 +- .../{Ability_Hazard_Windup.json => Ability_Zone_Windup.json} | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) rename src/main/resources/assets/customnpcs/animations/{Ability_Hazard_Active.json => Ability_Zone_Active.json} (99%) rename src/main/resources/assets/customnpcs/animations/{Ability_Hazard_Windup.json => Ability_Zone_Windup.json} (99%) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 551fbe9cc..51538da55 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -38,8 +38,8 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { this.typeId = "ability.cnpc.hazard"; this.name = "Hazard"; this.windUpTicks = 30; - this.windUpAnimationName = "Ability_Hazard_Windup"; - this.activeAnimationName = "Ability_Hazard_Active"; + this.windUpAnimationName = "Ability_Zone_Windup"; + this.activeAnimationName = "Ability_Zone_Active"; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index b296fa8bc..d55a4060c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -40,7 +40,9 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { super(300, new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.0f, 0.5f, 1.5f)); this.typeId = "ability.cnpc.trap"; this.name = "Trap"; - this.windUpTicks = 20; + this.windUpTicks = 30; + this.windUpAnimationName = "Ability_Zone_Windup"; + this.activeAnimationName = "Ability_Zone_Active"; } @Override diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Hazard_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Zone_Active.json similarity index 99% rename from src/main/resources/assets/customnpcs/animations/Ability_Hazard_Active.json rename to src/main/resources/assets/customnpcs/animations/Ability_Zone_Active.json index 11cb0ef7a..747f73860 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Hazard_Active.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Zone_Active.json @@ -261,6 +261,6 @@ "ID": 24, "WhileAttacking": 1b, "WhileWalking": 1b, - "Name": "Ability_Hazard_Active", + "Name": "Ability_Zone_Active", "WhileStanding": 1b } diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Hazard_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Zone_Windup.json similarity index 99% rename from src/main/resources/assets/customnpcs/animations/Ability_Hazard_Windup.json rename to src/main/resources/assets/customnpcs/animations/Ability_Zone_Windup.json index 8a16135a4..40a6abef7 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Hazard_Windup.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Zone_Windup.json @@ -387,6 +387,6 @@ "ID": 23, "WhileAttacking": 1b, "WhileWalking": 1b, - "Name": "Ability_Hazard_Windup", + "Name": "Ability_Zone_Windup", "WhileStanding": 1b } From 6f9cafa1414accff7d56947c676685c15f75c78f Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 12 Feb 2026 18:15:41 -0500 Subject: [PATCH 30/34] Add Custom Particle Handling and Preset Handling to Trap and Hazards --- .../client/renderer/RenderAbilityZone.java | 35 +++++- .../controllers/data/ability/Ability.java | 10 ++ .../data/ability/AbilityEffect.java | 8 +- .../data/ability/type/AbilityCharge.java | 39 +++++- .../data/ability/type/AbilityCutter.java | 91 +++++++++++++- .../data/ability/type/AbilityHazard.java | 2 + .../data/ability/type/AbilityShockwave.java | 2 +- .../data/ability/type/AbilityTrap.java | 2 + .../data/ability/type/AbilityZone.java | 115 +++++++++--------- src/main/java/noppes/npcs/DataAbilities.java | 15 ++- .../client/gui/SubGuiZonePresetSelector.java | 72 +++++++++++ .../npcs/client/gui/builder/FieldDef.java | 4 +- .../assets/customnpcs/lang/en_US.lang | 2 + 13 files changed, 318 insertions(+), 79 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/SubGuiZonePresetSelector.java diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java index 6c555aca8..9fae0e982 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityZone.java @@ -252,13 +252,30 @@ public class RenderAbilityZone extends Render { // 6. Particles if (c.particles && c.newTick && c.particleDensity > 0) { if (c.particleDir != null && !c.particleDir.isEmpty()) { - spawnCustomFXParticle(c); + if (c.particleDir.startsWith("mc:")) { + spawnVanillaParticle(c); + } else { + spawnCustomFXParticle(c); + } } else { spawnDefaultParticles(c); } } } + /** Spawn vanilla Minecraft particles using mc: prefix (e.g. "mc:flame"). */ + private void spawnVanillaParticle(Ctx c) { + String particleName = c.particleDir.substring(3); + int count = Math.round(3 * c.particleDensity); + for (int i = 0; i < count; i++) { + double[] pos = randomPosInZone(c); + double[] motion = getMotionForStyle(c.particleMotion); + c.zone.worldObj.spawnParticle(particleName, + c.zone.posX + pos[0], c.zone.posY + pos[1], c.zone.posZ + pos[2], + motion[0], motion[1], motion[2]); + } + } + /** Spawn CustomFX particles using a texture directory. */ private void spawnCustomFXParticle(Ctx c) { int count = Math.round(3 * c.particleDensity); @@ -267,18 +284,26 @@ public class RenderAbilityZone extends Render { double[] motion = getMotionForStyle(c.particleMotion); CustomFX fx = new CustomFX(c.zone.worldObj, null, c.particleDir, c.zone.posX + pos[0], c.zone.posY + pos[1], c.zone.posZ + pos[2], - motion[0], motion[1], motion[2]); + 0, 0, 0); + fx.motionX = motion[0]; + fx.motionY = motion[1]; + fx.motionZ = motion[2]; fx.setMaxAge(20 + RANDOM.nextInt(20)); + fx.width = -1; + fx.height = -1; fx.scaleX1 = c.particleScale * 10.0f; fx.scaleY1 = c.particleScale * 10.0f; - fx.width = c.particleSize; - fx.height = c.particleSize; + fx.scaleX2 = fx.scaleX1; + fx.scaleY2 = fx.scaleY1; + fx.scaleXRate = 0; + fx.scaleYRate = 0; fx.HEXColor = c.zone.getInnerColor(); fx.facePlayer = true; fx.glows = c.particleGlow; + fx.noClip = true; fx.alpha1 = 0.8f; fx.alpha2 = 0.0f; - fx.alphaRate = 0.04f; + fx.alphaRate = -0.04f; Minecraft.getMinecraft().effectRenderer.addEffect(fx); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 2dd86a4b4..44bb6beb2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -404,6 +404,16 @@ public abstract class Ability implements IAbility { return caster instanceof EntityPlayer; } + /** + * Whether the telegraph should remain visible during the ACTIVE phase. + * Default: false (telegraph removed at WINDUP→ACTIVE transition). + * Override for abilities where the active phase IS the attack (e.g., sweeping/spinning attacks) + * so the telegraph stays visible while damage is being dealt. + */ + public boolean keepTelegraphDuringActive() { + return false; + } + /** * Returns true if this ability controls NPC movement during ACTIVE phase. * When true, AI pathfinding will be blocked during ability execution. diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java index 57f0cbf17..73f465049 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java @@ -20,7 +20,8 @@ public class AbilityEffect { BLINDNESS("potion.blindness", Potion.blindness.id), NAUSEA("potion.confusion", Potion.confusion.id), HUNGER("potion.hunger", Potion.hunger.id), - MINING_FATIGUE("potion.digSlowDown", Potion.digSlowdown.id); + MINING_FATIGUE("potion.digSlowDown", Potion.digSlowdown.id), + BURN("ability.effect.burn", -1); private final String langKey; private final int potionId; @@ -83,6 +84,11 @@ public class AbilityEffect { return; } + if (type == EffectType.BURN) { + entity.setFire(Math.max(1, durationTicks / 20)); + return; + } + int potionId = type.getPotionId(); if (potionId >= 0) { entity.addPotionEffect(new PotionEffect(potionId, durationTicks, amplifier)); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 2a0a5ff8c..a7430ce20 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -38,9 +38,11 @@ public class AbilityCharge extends Ability implements IAbilityCharge { // Runtime state (transient) private transient double startX, startY, startZ; + private transient double prevTickX, prevTickZ; private transient Vec3 chargeDirection; private transient Set hitEntities = new HashSet<>(); private transient float lockedYaw; + private transient int maxActiveTicks; public AbilityCharge() { this.typeId = "ability.cnpc.charge"; @@ -119,8 +121,13 @@ public class AbilityCharge extends Ability implements IAbilityCharge { startX = caster.posX; startY = caster.posY; startZ = caster.posZ; + prevTickX = caster.posX; + prevTickZ = caster.posZ; hitEntities.clear(); + // Safety timeout: expected ticks + buffer to prevent infinite charge + maxActiveTicks = chargeSpeed > 0 ? (int)(maxRange / chargeSpeed) + 10 : 10; + // If direction wasn't set during windup (shouldn't happen), set it now if (chargeDirection == null) { lockChargeDirection(caster, target); @@ -140,13 +147,36 @@ public class AbilityCharge extends Ability implements IAbilityCharge { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - if (chargeDirection == null) return; + // Safety timeout or missing state: force-complete to prevent stuck NPC + if (!isPreview() && (chargeDirection == null || tick > maxActiveTicks)) { + stopMomentum(caster); + signalCompletion(); + return; + } + + if (chargeDirection == null) { + signalCompletion(); + return; + } // Enforce rotation every tick if (!isPreview()) { enforceLockedRotation(caster); } + // Stall detection: if entity hasn't moved since last tick, it's stuck against a wall + if (!isPreview() && tick > 1) { + double dx = caster.posX - prevTickX; + double dz = caster.posZ - prevTickZ; + if (dx * dx + dz * dz < 0.0001) { + stopMomentum(caster); + signalCompletion(); + return; + } + } + prevTickX = caster.posX; + prevTickZ = caster.posZ; + // Calculate distance traveled double distanceTraveled = Math.sqrt( Math.pow(caster.posX - startX, 2) + @@ -155,11 +185,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge { // Check if reached max distance if (distanceTraveled >= maxRange) { - caster.motionX = 0; - caster.motionZ = 0; - if (!isPreview()) { - caster.velocityChanged = true; - } + stopMomentum(caster); signalCompletion(); return; } @@ -240,6 +266,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge { super.reset(); chargeDirection = null; hitEntities.clear(); + maxActiveTicks = 0; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 22831f4ff..900ae6080 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -3,6 +3,8 @@ package kamkeel.npcs.controllers.data.ability.type; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -60,6 +62,8 @@ public class AbilityCutter extends Ability implements IAbilityCutter { // Runtime state private transient Set hitEntities = new HashSet<>(); private transient float currentRotation = 0.0f; + private transient boolean activeSoundPlayed = false; + private transient float sweepBaseYaw = 0.0f; public AbilityCutter() { this.typeId = "ability.cnpc.cutter"; @@ -86,6 +90,54 @@ public class AbilityCutter extends Ability implements IAbilityCutter { return new TargetingMode[]{TargetingMode.AOE_SELF}; } + /** + * Calculate how many ticks the active sweep phase takes. + * SWIPE: arcAngle / sweepSpeed ticks + * SPIN: spinDurationTicks + */ + private int getActiveDurationTicks() { + if (sweepMode == SweepMode.SPIN) { + return spinDurationTicks; + } + return sweepSpeed > 0 ? (int) Math.ceil(arcAngle / sweepSpeed) : 1; + } + + @Override + public boolean keepTelegraphDuringActive() { + return true; + } + + /** + * Creates a CONE telegraph that lasts through both windup and active sweep phases. + * Warning color transitions when the sweep actually begins (at windUpTicks). + */ + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { + return null; + } + + Telegraph telegraph = Telegraph.cone(getTelegraphLength(), getTelegraphAngle()); + int totalDuration = windUpTicks + getActiveDurationTicks(); + telegraph.setDurationTicks(totalDuration); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + // Warning transition starts when the sweep begins (active phase) + telegraph.setWarningStartTick(windUpTicks); + telegraph.setHeightOffset(telegraphHeightOffset); + + double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(caster.getEntityId()); + + if (target != null && !isRotationLockedDuringWindup()) { + instance.setTargetEntityId(target.getEntityId()); + } + + return instance; + } + @Override public float getTelegraphRadius() { return range; @@ -105,6 +157,21 @@ public class AbilityCutter extends Ability implements IAbilityCutter { public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { hitEntities.clear(); currentRotation = -arcAngle / 2.0f; + activeSoundPlayed = false; + + // Lock sweep direction toward target at execute time to prevent + // AI look-at jitter from misdirecting the sweep at close range + if (target != null) { + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; + if (dx * dx + dz * dz > 0.0001) { + sweepBaseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + } else { + sweepBaseYaw = caster.rotationYaw; + } + } else { + sweepBaseYaw = caster.rotationYaw; + } } @Override @@ -115,6 +182,12 @@ public class AbilityCutter extends Ability implements IAbilityCutter { case SWIPE: float prevRotation = currentRotation; currentRotation += sweepSpeed; + + // Play active sound when sweep reaches center (where target typically stands) + if (!activeSoundPlayed && currentRotation >= 0) { + playActiveSound(caster, world); + } + if (currentRotation > arcAngle / 2.0f) { if (!isPreview()) { performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f); @@ -128,6 +201,11 @@ public class AbilityCutter extends Ability implements IAbilityCutter { break; case SPIN: + // Play active sound at spin start + if (!activeSoundPlayed) { + playActiveSound(caster, world); + } + if (tick >= spinDurationTicks) { signalCompletion(); return; @@ -142,6 +220,13 @@ public class AbilityCutter extends Ability implements IAbilityCutter { } } + private void playActiveSound(EntityLivingBase caster, World world) { + activeSoundPlayed = true; + if (activeSound != null && !activeSound.isEmpty()) { + world.playSoundAtEntity(caster, activeSound, 1.0f, 1.0f); + } + } + /** * Check for entities between two sweep angles, ensuring no gaps between ticks. * For SWIPE mode, this covers the continuous arc from startAngle to endAngle. @@ -171,7 +256,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter { double dist = Math.sqrt(dx * dx + dz * dz); if (dist < minDist || dist > maxDist) continue; - if (!isInSweepRange(dx, dz, caster.rotationYaw, minAngle, maxAngle)) continue; + if (!isInSweepRange(dx, dz, sweepBaseYaw, minAngle, maxAngle)) continue; hitEntities.add(entity.getEntityId()); @@ -214,12 +299,16 @@ public class AbilityCutter extends Ability implements IAbilityCutter { public void onComplete(EntityLivingBase caster, EntityLivingBase target) { hitEntities.clear(); currentRotation = 0.0f; + activeSoundPlayed = false; + sweepBaseYaw = 0.0f; } @Override public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { hitEntities.clear(); currentRotation = 0.0f; + activeSoundPlayed = false; + sweepBaseYaw = 0.0f; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 51538da55..a8bbafc1a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -148,6 +148,8 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard { @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { + addPresetFieldDef(defs); + defs.addAll(Arrays.asList( FieldDef.row( FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 6788a3ea3..0ef0dc50f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -35,7 +35,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { this.typeId = "ability.cnpc.shockwave"; this.name = "Shockwave"; this.targetingMode = TargetingMode.AOE_SELF; - this.maxRange = 15.0f; + this.maxRange = 8.0f; this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; this.cooldownTicks = 0; this.windUpTicks = 25; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index d55a4060c..b35891d9a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -157,6 +157,8 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap { @SideOnly(Side.CLIENT) @Override public void getAbilityDefinitions(List defs) { + addPresetFieldDef(defs); + defs.addAll(Arrays.asList( FieldDef.row( FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java index 5f8c33499..8a15c2ce5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityZone.java @@ -1,6 +1,7 @@ package kamkeel.npcs.controllers.data.ability.type; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.UserType; @@ -17,6 +18,7 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.SubGuiZonePresetSelector; import noppes.npcs.client.gui.builder.FieldDef; import cpw.mods.fml.relauncher.Side; @@ -243,36 +245,61 @@ public abstract class AbilityZone extends Ability { * Apply preset defaults by style name. */ private void applyPresetDefaults(String styleName) { + effects.clear(); + particleDir = ""; switch (styleName) { case "TOXIC": groundFill = true; groundAlpha = 0.30f; rings = true; ringCount = 3; border = false; borderSpeed = 1.0f; accents = true; accentStyle = 1; - lightning = false; particles = true; particleMotion = 0; particleGlow = true; + lightning = false; particles = true; particleDensity = 1.5f; particleScale = 0.8f; + particleMotion = 1; particleGlow = true; particleDir = "mc:mobSpell"; + colorData.innerColor = 0x44DD44; colorData.outerColor = 0x116611; + windUpColor = 0x6044DD44; activeColor = 0xC044FF44; + effects.add(new AbilityEffect(AbilityEffect.EffectType.POISON, 100, 0)); break; case "INFERNO": groundFill = true; groundAlpha = 0.35f; rings = true; ringCount = 3; border = true; borderSpeed = 2.0f; accents = true; accentStyle = 2; - lightning = false; particles = true; particleMotion = 0; particleGlow = true; + lightning = false; particles = true; particleDensity = 2.0f; particleScale = 1.2f; + particleMotion = 0; particleGlow = true; particleDir = "mc:flame"; + colorData.innerColor = 0xFF6611; colorData.outerColor = 0xCC2200; + windUpColor = 0x60FF6611; activeColor = 0xC0FF4400; + effects.add(new AbilityEffect(AbilityEffect.EffectType.BURN, 60, 0)); break; case "ARCANE": groundFill = true; groundAlpha = 0.20f; rings = true; ringCount = 1; border = true; borderSpeed = 0.8f; accents = true; accentStyle = 0; - lightning = false; particles = true; particleMotion = 1; particleGlow = true; + lightning = false; particles = true; particleDensity = 1.0f; particleScale = 1.0f; + particleMotion = 1; particleGlow = true; particleDir = "mc:portal"; + colorData.innerColor = 0xAA44FF; colorData.outerColor = 0x6622BB; + windUpColor = 0x60AA44FF; activeColor = 0xC0CC66FF; + effects.add(new AbilityEffect(AbilityEffect.EffectType.WEAKNESS, 100, 0)); break; case "ELECTRIC": groundFill = true; groundAlpha = 0.15f; rings = false; ringCount = 1; border = false; borderSpeed = 1.0f; accents = false; accentStyle = 0; - lightning = true; particles = true; particleMotion = 2; particleGlow = true; + lightning = true; particles = true; particleDensity = 0.8f; particleScale = 0.6f; + particleMotion = 2; particleGlow = true; particleDir = "mc:enchantmenttable"; + colorData.innerColor = 0x4488FF; colorData.outerColor = 0x2244BB; + windUpColor = 0x604488FF; activeColor = 0xC066AAFF; + effects.add(new AbilityEffect(AbilityEffect.EffectType.MINING_FATIGUE, 80, 1)); break; case "FROST": groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3; border = true; borderSpeed = 0.3f; accents = true; accentStyle = 0; - lightning = false; particles = true; particleMotion = 1; particleGlow = true; + lightning = false; particles = true; particleDensity = 1.5f; particleScale = 1.0f; + particleMotion = 1; particleGlow = true; particleDir = "mc:snowshovel"; + colorData.innerColor = 0x88CCFF; colorData.outerColor = 0x4488CC; + windUpColor = 0x6088CCFF; activeColor = 0xC0AADDFF; + effects.add(new AbilityEffect(AbilityEffect.EffectType.SLOWNESS, 100, 1)); break; case "DEFAULT": groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3; border = true; borderSpeed = 1.0f; accents = true; accentStyle = 0; - lightning = false; particles = false; particleMotion = 0; particleGlow = true; + lightning = false; particles = false; particleDensity = 1.0f; particleScale = 1.0f; + particleMotion = 0; particleGlow = true; + colorData.innerColor = 0xCCCCCC; colorData.outerColor = 0x666666; + windUpColor = 0x60CCCCCC; activeColor = 0xC0FFFFFF; break; default: break; @@ -285,7 +312,7 @@ public abstract class AbilityZone extends Ability { */ @SideOnly(Side.CLIENT) public enum ZonePreset { - CUSTOM, DEFAULT, TOXIC, INFERNO, ARCANE, ELECTRIC, FROST; + DEFAULT, TOXIC, INFERNO, ARCANE, ELECTRIC, FROST; @Override public String toString() { @@ -293,44 +320,6 @@ public abstract class AbilityZone extends Ability { } } - /** - * Check current field values against each preset, return matching preset or CUSTOM. - */ - @SideOnly(Side.CLIENT) - public ZonePreset getCurrentPreset() { - if (matchesPreset(true, 0.25f, true, 3, true, 1.0f, true, 0, false, false, 0, true)) - return ZonePreset.DEFAULT; - if (matchesPreset(true, 0.30f, true, 3, false, 1.0f, true, 1, false, true, 0, true)) - return ZonePreset.TOXIC; - if (matchesPreset(true, 0.35f, true, 3, true, 2.0f, true, 2, false, true, 0, true)) - return ZonePreset.INFERNO; - if (matchesPreset(true, 0.20f, true, 1, true, 0.8f, true, 0, false, true, 1, true)) - return ZonePreset.ARCANE; - if (matchesPreset(true, 0.15f, false, 1, false, 1.0f, false, 0, true, true, 2, true)) - return ZonePreset.ELECTRIC; - if (matchesPreset(true, 0.25f, true, 3, true, 0.3f, true, 0, false, true, 1, true)) - return ZonePreset.FROST; - return ZonePreset.CUSTOM; - } - - @SideOnly(Side.CLIENT) - private boolean matchesPreset(boolean gf, float ga, boolean r, int rc, boolean b, float bs, - boolean a, int as, boolean l, boolean p, int pm, boolean pg) { - return groundFill == gf && Math.abs(groundAlpha - ga) < 0.001f - && rings == r && ringCount == rc && border == b && Math.abs(borderSpeed - bs) < 0.001f - && accents == a && accentStyle == as && lightning == l - && particles == p && particleMotion == pm && particleGlow == pg; - } - - /** - * Apply a preset's field values. CUSTOM does nothing (user configures manually). - */ - @SideOnly(Side.CLIENT) - public void applyPreset(ZonePreset preset) { - if (preset == ZonePreset.CUSTOM) return; - applyPresetDefaults(preset.name()); - } - // ═════════════════════════════════════════════════════════════════ // SHARED NBT // ═════════════════════════════════════════════════════════════════ @@ -476,13 +465,21 @@ public abstract class AbilityZone extends Ability { // SHARED FIELD DEFINITIONS (client-only) // ═════════════════════════════════════════════════════════════════ + @SideOnly(Side.CLIENT) + protected void addPresetFieldDef(List defs) { + defs.add(FieldDef.subGuiField("gui.applyPreset", + SubGuiZonePresetSelector::new, + gui -> { + SubGuiZonePresetSelector selector = (SubGuiZonePresetSelector) gui; + if (selector.selectedPreset != null) { + applyPresetDefaults(selector.selectedPreset); + } + })); + } + @SideOnly(Side.CLIENT) protected void addVisualFieldDefs(List defs) { defs.addAll(Arrays.asList( - // Preset dropdown - FieldDef.enumField("gui.preset", ZonePreset.class, this::getCurrentPreset, this::applyPreset) - .tab("ability.tab.visual"), - // Ground Fill FieldDef.section("ability.section.ground").tab("ability.tab.visual"), FieldDef.row( @@ -520,20 +517,18 @@ public abstract class AbilityZone extends Ability { // Particles FieldDef.section("ability.section.particles").tab("ability.tab.visual"), - FieldDef.row( - FieldDef.boolField("gui.enabled", this::isParticles, this::setParticles), - FieldDef.floatField("gui.density", this::getParticleDensity, this::setParticleDensity) - ).tab("ability.tab.visual"), - FieldDef.row( - FieldDef.floatField("gui.scale", this::getParticleScale, this::setParticleScale), - FieldDef.enumField("gui.motion", ParticleMotion.class, this::getParticleMotionEnum, this::setParticleMotionEnum) - ).tab("ability.tab.visual"), - FieldDef.boolField("gui.glow", this::isParticleGlow, this::setParticleGlow) + FieldDef.boolField("gui.enabled", this::isParticles, this::setParticles) .tab("ability.tab.visual"), + FieldDef.row( + FieldDef.floatField("gui.density", this::getParticleDensity, this::setParticleDensity), + FieldDef.floatField("gui.scale", this::getParticleScale, this::setParticleScale) + ).tab("ability.tab.visual").visibleWhen(this::isParticles), + FieldDef.row( + FieldDef.enumField("gui.motion", ParticleMotion.class, this::getParticleMotionEnum, this::setParticleMotionEnum), + FieldDef.boolField("gui.glow", this::isParticleGlow, this::setParticleGlow) + ).tab("ability.tab.visual").visibleWhen(this::isParticles), FieldDef.stringField("gui.texture", this::getParticleDir, this::setParticleDir) .tab("ability.tab.visual").visibleWhen(this::isParticles), - FieldDef.intField("gui.size", this::getParticleSize, this::setParticleSize).range(1, 256) - .tab("ability.tab.visual").visibleWhen(this::isParticles), // Animation FieldDef.section("ability.section.animation").tab("ability.tab.visual"), diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index b5c23c756..9020b0ba8 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -197,8 +197,11 @@ public class DataAbilities { telegraph.lockPosition(); } - // Remove telegraph - it has served its purpose - removeTelegraph(currentAbility); + // Remove telegraph unless the ability keeps it during active phase + // (e.g., sweeping attacks where the telegraph shows the ongoing danger zone) + if (!currentAbility.keepTelegraphDuringActive()) { + removeTelegraph(currentAbility); + } // Handle rotation control transition from WINDUP to ACTIVE if (currentAbility.isRotationLockedDuringActive()) { @@ -224,7 +227,10 @@ public class DataAbilities { } // Play active sound and animation - playAbilitySound(currentAbility.getActiveSound()); + // Abilities that keep telegraph during active manage their own sound timing + if (!currentAbility.keepTelegraphDuringActive()) { + playAbilitySound(currentAbility.getActiveSound()); + } playAbilityAnimation(currentAbility.getActiveAnimation()); // Snap NPC to face target before execute so startMoving() reads correct rotation @@ -338,6 +344,9 @@ public class DataAbilities { private void handleAbilityCompletion(EntityLivingBase target) { if (currentAbility == null) return; + // Remove any remaining telegraphs (for abilities that keep telegraph during active) + removeTelegraph(currentAbility); + // Call onComplete callback currentAbility.onComplete(npc, target); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiZonePresetSelector.java b/src/main/java/noppes/npcs/client/gui/SubGuiZonePresetSelector.java new file mode 100644 index 000000000..f67ef3db1 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/SubGuiZonePresetSelector.java @@ -0,0 +1,72 @@ +package noppes.npcs.client.gui; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.SubGuiInterface; + +@SideOnly(Side.CLIENT) +public class SubGuiZonePresetSelector extends SubGuiInterface { + + public String selectedPreset = null; + + private static final String[] PRESET_NAMES = {"DEFAULT", "TOXIC", "INFERNO", "ARCANE", "ELECTRIC", "FROST"}; + private static final String[] PRESET_LABELS = { + "ability.preset.default", "ability.preset.toxic", "ability.preset.inferno", + "ability.preset.arcane", "ability.preset.electric", "ability.preset.frost" + }; + private static final int[] PRESET_COLORS = {0xCCCCCC, 0x44DD44, 0xFF6611, 0xAA44FF, 0x4488FF, 0x88CCFF}; + + public SubGuiZonePresetSelector() { + xSize = 170; + ySize = 135; + setBackground("menubg.png"); + } + + @Override + public void initGui() { + super.initGui(); + int btnW = 72; + int btnH = 20; + int gapY = 4; + int col1 = guiLeft + 7; + int col2 = col1 + btnW + 6; + int startY = guiTop + 22; + + for (int i = 0; i < 6; i++) { + int x = (i % 2 == 0) ? col1 : col2; + int y = startY + (i / 2) * (btnH + gapY); + addButton(new GuiNpcButton(i, x, y, btnW, btnH, PRESET_LABELS[i])); + } + + int cancelY = startY + 3 * (btnH + gapY) + 4; + addButton(new GuiNpcButton(10, guiLeft + xSize / 2 - 36, cancelY, 72, btnH, "gui.cancel")); + } + + @Override + protected void actionPerformed(GuiButton btn) { + super.actionPerformed(btn); + if (btn.id >= 0 && btn.id < 6) { + selectedPreset = PRESET_NAMES[btn.id]; + close(); + } else if (btn.id == 10) { + close(); + } + } + + @Override + public void drawScreen(int mouseX, int mouseY, float f) { + super.drawScreen(mouseX, mouseY, f); + drawCenteredString(fontRendererObj, StatCollector.translateToLocal("gui.applyPreset"), guiLeft + xSize / 2, guiTop + 8, 0xFFFFFF); + for (int i = 0; i < 6; i++) { + GuiNpcButton btn = getButton(i); + if (btn != null && btn.visible) { + int x = btn.xPosition + 4; + int y = btn.yPosition + 5; + drawRect(x, y, x + 10, y + 10, 0xFF000000 | PRESET_COLORS[i]); + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java b/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java index 9263cc880..5319d16e4 100644 --- a/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java +++ b/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java @@ -154,8 +154,8 @@ public class FieldDef { public static FieldDef colorSubGui(String label, Supplier getter, Consumer setter) { return subGuiField(label, - () -> new SubGuiColorSelector(getter.get()), - gui -> setter.accept(((SubGuiColorSelector) gui).color & 0x00FFFFFF)) + () -> new SubGuiColorSelector(getter.get() & 0xFFFFFF), + gui -> setter.accept((((SubGuiColorSelector) gui).color & 0x00FFFFFF) | (getter.get() & 0xFF000000))) .buttonLabel(() -> String.format("%06X", getter.get() & 0xFFFFFF)) .buttonTextColor(() -> getter.get() & 0xFFFFFF); } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 408136c34..3e8fa323a 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1761,6 +1761,7 @@ gui.interval=Interval gui.cooldown=Cooldown gui.max=Max gui.preset=Preset +gui.applyPreset=Apply Preset # Common Type Settings ability.radius=Radius @@ -1773,6 +1774,7 @@ ability.stunTicks=Stun Ticks ability.hitWidth=Hit Width ability.hitLength=Hit Length ability.effects=Effects +ability.effect.burn=Burn ability.mode=Mode ability.duration=Duration ability.anchorPoint=Anchor Point From 9dae4bae55f846912561530827a3e0b09560f204 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 12 Feb 2026 18:53:40 -0500 Subject: [PATCH 31/34] feat: enhance damage handling for abilities and implement guard counter mechanics --- .../data/ability/type/AbilityGuard.java | 33 +++++++------ .../data/ability/type/AbilityTeleport.java | 5 -- .../noppes/npcs/ScriptPlayerEventHandler.java | 6 +++ .../controllers/data/PlayerAbilityData.java | 48 +++++++++++++++++++ 4 files changed, 73 insertions(+), 19 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 477be2cc4..461fef2bd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -65,11 +65,6 @@ public class AbilityGuard extends Ability implements IAbilityGuard { this.counterAnimationName = "Ability_Guard_Counter"; } - @Override - public boolean hasDamage() { - return false; - } - @Override public boolean allowBurst() { return false; @@ -133,16 +128,22 @@ public class AbilityGuard extends Ability implements IAbilityGuard { lastAttacker = attacker; lastDamageTaken = damage; - float reduction = ValueUtil.clamp(getDamageReductionFactor(), 0, damage); - float newHealth = ValueUtil.clamp(caster.getHealth() + reduction, 0, caster.getMaxHealth()); - - caster.setHealth(newHealth); - - if (!canCounter || !counterEligible || counterTriggered || !isDirectHit(source)) { - caster.setHealth(newHealth); + // Players: damage reduction handled via LivingHurtEvent (event.ammount modification) + // NPCs: called AFTER damageEntity() so health was already reduced — heal back here + if (caster instanceof EntityPlayer) { + if (canCounter && counterEligible && !counterTriggered && isDirectHit(source)) { + counterTriggered = true; + } } else { - caster.setHealth(ValueUtil.clamp(caster.getHealth() + damage, 0, caster.getMaxHealth())); - counterTriggered = true; + float reduction = ValueUtil.clamp(getDamageReductionFactor(), 0, damage); + float newHealth = ValueUtil.clamp(caster.getHealth() + reduction, 0, caster.getMaxHealth()); + + if (canCounter && counterEligible && !counterTriggered && isDirectHit(source)) { + caster.setHealth(ValueUtil.clamp(caster.getHealth() + damage, 0, caster.getMaxHealth())); + counterTriggered = true; + } else { + caster.setHealth(newHealth); + } } } @@ -342,6 +343,10 @@ public class AbilityGuard extends Ability implements IAbilityGuard { this.counterAnimationId = counterAnimationId; } + public boolean isCounterTriggered() { + return counterTriggered; + } + public String getCounterAnimationName() { return counterAnimationName; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 4b19bc91d..29eb73832 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -86,11 +86,6 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { this.activeSound = "mob.endermen.portal"; } - @Override - public boolean hasDamage() { - return false; - } - @Override public boolean isTargetingModeLocked() { return true; diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 0541f64c4..c77083fa1 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -596,6 +596,12 @@ public class ScriptPlayerEventHandler { } if (event.entityLiving instanceof EntityPlayerMP) { + // Handle ability damage interactions (Guard counter, damage reduction) + PlayerData pData = PlayerData.get((EntityPlayer) event.entityLiving); + if (pData != null && pData.abilityData != null && pData.abilityData.isExecutingAbility()) { + event.ammount = pData.abilityData.onDamage(event.source, event.ammount); + } + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.entityLiving); noppes.npcs.scripted.event.player.PlayerEvent.DamagedEvent pevent = new noppes.npcs.scripted.event.player.PlayerEvent.DamagedEvent((IPlayer) NpcAPI.Instance().getIEntity((EntityPlayer) event.entityLiving), source, event.ammount, event.source); cancel = EventHooks.onPlayerDamaged(handler, pevent); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index a653e2a94..3e6489b1c 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -4,6 +4,7 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.UserType; +import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; @@ -417,6 +418,53 @@ public class PlayerAbilityData implements IPlayerAbilityData { } } + /** + * Handle damage taken while an ability is executing. + * Called from LivingHurtEvent BEFORE damage is applied to health. + * Triggers Guard counter detection and handles ability interruption. + * + * @param source The damage source + * @param amount The damage amount (post-armor) + * @return The modified damage amount (reduced by guard, or 0 if counter absorbs) + */ + public float onDamage(DamageSource source, float amount) { + if (currentAbility == null || !currentAbility.isExecuting()) { + return amount; + } + + net.minecraft.entity.Entity sourceEntity = source.getEntity(); + EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null; + + // Track counter state before calling onDamageTaken + boolean wasCounterTriggered = false; + if (currentAbility instanceof AbilityGuard) { + wasCounterTriggered = ((AbilityGuard) currentAbility).isCounterTriggered(); + } + + currentAbility.onDamageTaken(playerData.player, attacker, source, amount); + + // Guard: handle damage reduction and counter absorption + if (currentAbility instanceof AbilityGuard) { + AbilityGuard guard = (AbilityGuard) currentAbility; + if (guard.isGuarding()) { + // Counter just triggered — absorb full damage + if (!wasCounterTriggered && guard.isCounterTriggered()) { + return 0; + } + // Normal guard — apply flat damage reduction + float reduction = guard.getDamageReductionFactor(); + return Math.max(0, amount - reduction); + } + } + + // Check for ability interruption + if (currentAbility != null && currentAbility.canInterrupt(source)) { + interruptCurrentAbility(); + } + + return amount; + } + @Override public boolean isOnCooldown() { EntityPlayer player = playerData.player; From 356ae65b69f4b84e274f2cd4167479ee98451791 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 12 Feb 2026 20:28:50 -0500 Subject: [PATCH 32/34] Fix Dialog and Quest Category Bug --- src/main/java/noppes/npcs/client/gui/SubGuiNpcDialog.java | 1 - src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java | 2 -- src/main/java/noppes/npcs/client/gui/SubGuiNpcQuest.java | 1 - 3 files changed, 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialog.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialog.java index 3e194f85f..38777d9ab 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialog.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialog.java @@ -198,6 +198,5 @@ public class SubGuiNpcDialog extends SubGuiInterface implements ISubGuiListener, } public void save() { - PacketClient.sendClient(new DialogSavePacket(this.dialogCategoryID, dialog.writeToNBT(new NBTTagCompound()), false)); } } diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java index 807a3e719..86f174bc4 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java @@ -521,8 +521,6 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis public void save() { GuiNpcTextField.unfocus(); - if (dialog.id >= 0) - PacketClient.sendClient(new DialogSavePacket(((SubGuiNpcDialog) parent).dialogCategoryID, dialog.writeToNBT(new NBTTagCompound()), false)); } private DialogImage getSelectedImage() { diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcQuest.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcQuest.java index 6406733be..9d693db81 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcQuest.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcQuest.java @@ -239,6 +239,5 @@ public class SubGuiNpcQuest extends SubGuiInterface implements ISubGuiListener, } public void save() { - PacketClient.sendClient(new QuestSavePacket(this.questCategoryID, quest.writeToNBT(new NBTTagCompound()), false)); } } From 2c00ecae9efb4fb57dbebd6cb3fe03465183b199 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 12 Feb 2026 20:30:12 -0500 Subject: [PATCH 33/34] feat: add built-in ability handling and UI updates for ability management --- .../controllers/data/ability/Ability.java | 4 + .../data/ability/AbilityController.java | 28 +++- .../data/ability/BuiltInAbility.java | 8 +- .../kamkeel/npcs/network/PacketHandler.java | 2 + .../npcs/network/enums/EnumRequestPacket.java | 1 + .../ability/CustomAbilitiesGetPacket.java | 1 + src/main/java/kamkeel/npcs/util/Register.java | 19 ++- .../java/noppes/npcs/NoppesUtilServer.java | 17 +++ .../client/gui/advanced/GuiNPCAbilities.java | 21 ++- .../gui/advanced/SubGuiAbilityTypeSelect.java | 2 + .../gui/global/GuiNpcManageAbilities.java | 144 +++++++++++++----- .../noppes/npcs/constants/EnumScrollData.java | 1 + 12 files changed, 201 insertions(+), 47 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 44bb6beb2..b2743f629 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -1278,6 +1278,10 @@ public abstract class Ability implements IAbility { return typeId; } + public boolean isBuiltIn() { + return false; + } + public int getWeight() { return weight; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 294f9a765..01ddb7b58 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -48,6 +48,9 @@ public class AbilityController implements IAbilityHandler { /** External variant definitions registered by addons, keyed by typeId. */ private final Map> externalVariants = new LinkedHashMap<>(); + /** TypeIds that produce BuiltInAbility instances (non-editable in GUI). */ + private final Set builtInTypeIds = new HashSet<>(); + public AbilityController() { registerBuiltinTypes(); } @@ -173,7 +176,9 @@ public class AbilityController implements IAbilityHandler { if (abilities.containsKey(name)) { LogWriter.info("AbilityController: Overwriting built-in ability: " + name); } - ability.setName(name); + if (!ability.isBuiltIn()) { + ability.setName(name); + } abilities.put(name, ability); LogWriter.info("Registered ability: " + name); } @@ -281,12 +286,22 @@ public class AbilityController implements IAbilityHandler { // ACCESSORS // ═══════════════════════════════════════════════════════════════════════════ - /** Get a built-in ability by name. Returns the stored instance (not a copy). */ + /** Get a built-in ability by map key. Returns the stored instance (not a copy). */ public Ability getAbility(String name) { return abilities.get(name); } - /** Get all built-in ability names. */ + /** Get a built-in ability by its display name (ability.getName()). */ + public Ability getAbilityByDisplayName(String displayName) { + for (Ability ability : abilities.values()) { + if (ability.getName().equals(displayName)) { + return ability; + } + } + return null; + } + + /** Get all built-in ability map keys. */ public Set getAbilityNames() { return new LinkedHashSet<>(abilities.keySet()); } @@ -338,6 +353,10 @@ public class AbilityController implements IAbilityHandler { return typeFactories.keySet().toArray(new String[0]); } + public boolean isBuiltInType(String typeId) { + return builtInTypeIds.contains(typeId); + } + @Override public boolean hasType(String typeId) { return typeFactories.containsKey(typeId); @@ -386,6 +405,9 @@ public class AbilityController implements IAbilityHandler { LogWriter.info("AbilityController: Overwriting type: " + typeId); } typeFactories.put(typeId, factory); + if (temp.isBuiltIn()) { + builtInTypeIds.add(typeId); + } } /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java index 904fad0b9..36344e9e5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java @@ -21,7 +21,13 @@ public abstract class BuiltInAbility extends Ability { this.registryKey = registryKey; this.id = registryKey; this.name = registryKey; - this.typeId = registryKey; + // Convert "namespace:name" to "ability.namespace.name" for GUI filter compatibility + this.typeId = "ability." + registryKey.replace(':', '.'); + } + + @Override + public boolean isBuiltIn() { + return true; } /** diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index bcfd18abd..6db07c841 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.ability.AbilitiesNpcSavePacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.BuiltInAbilityGetPacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; @@ -516,6 +517,7 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new CustomAbilityGetPacket()); REQUEST_PACKET.registerPacket(new CustomAbilityRemovePacket()); REQUEST_PACKET.registerPacket(new CustomAbilitySavePacket()); + REQUEST_PACKET.registerPacket(new BuiltInAbilityGetPacket()); // Other Packets REQUEST_PACKET.registerPacket(new IsGuiOpenInform()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 2f3e6ea94..25ba2bba6 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -205,6 +205,7 @@ public enum EnumRequestPacket { CustomAbilityGet, CustomAbilityRemove, CustomAbilitySave, + BuiltInAbilityGet, Hammer, RecipeScript, diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java index 8febd9fc0..6097c7483 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java @@ -47,5 +47,6 @@ public final class CustomAbilitiesGetPacket extends AbstractPacket { return; NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); + NoppesUtilServer.sendBuiltInAbilitiesData((EntityPlayerMP) player); } } diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index db6802f34..5a64fca6a 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -37,6 +37,7 @@ public class Register { public static class Abilities extends Register { protected final Map uniqueNames = new LinkedHashMap<>(); + protected final Set typeOnly = new HashSet<>(); private Abilities(String namespace) { super("ability", namespace); @@ -50,9 +51,25 @@ public class Register { return factory.get(); } + /** + * Register a type only (shell template, no built-in preset). + * The type will appear in the ability type picker but won't be + * available as a pre-configured ability by name. + */ + public Ability registerType(String factoryName, Supplier factory) { + String name = registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_"); + entries.put(name, factory); + uniqueNames.put(name, factoryName); + typeOnly.add(name); + return factory.get(); + } + public void register() { for (Map.Entry> entry : entries.entrySet()) { - AbilityController.Instance.registerAbility(uniqueNames.get(entry.getKey()), entry.getValue().get()); + AbilityController.Instance.registerType(entry.getKey(), entry.getValue()); + if (!typeOnly.contains(entry.getKey())) { + AbilityController.Instance.registerAbility(uniqueNames.get(entry.getKey()), entry.getValue().get()); + } } } diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 7faa7b2b5..b54b4b245 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -781,12 +781,29 @@ public class NoppesUtilServer { if (controller != null) { int index = 0; for (String typeId : controller.getTypes()) { + // Exclude built-in types from the NPC type list + if (controller.isBuiltInType(typeId)) continue; map.put(typeId, index++); } } sendScrollData(player, map, EnumScrollData.ABILITY_TYPES); } + public static void sendBuiltInAbilitiesData(EntityPlayerMP player) { + HashMap map = new HashMap(); + AbilityController controller = AbilityController.Instance; + if (controller != null) { + int index = 0; + for (String name : controller.getAbilityNames()) { + Ability ability = controller.getAbility(name); + if (ability != null) { + map.put(ability.getName(), index++); + } + } + } + sendScrollData(player, map, EnumScrollData.BUILTIN_ABILITIES); + } + public static void sendCustomAbilitiesData(EntityPlayerMP player) { HashMap map = new HashMap(); kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance; 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 7bc5c3846..4b9e99a13 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -168,9 +168,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addButton(toggleBtn); } + // Check if selected ability is built-in (non-editable) + boolean selectedIsBuiltIn = false; + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability sel = npcSlots.get(selectedAbilityIndex).getAbility(); + if (sel != null && sel.isBuiltIn()) selectedIsBuiltIn = true; + } + // Right side buttons: Edit and Up/Down carrots with Save button addButton(new GuiNpcButton(72, guiLeft + 210, y + 145, 55, 20, "gui.edit")); - getButton(72).setEnabled(selectedAbilityIndex >= 0); + getButton(72).setEnabled(selectedAbilityIndex >= 0 && !selectedIsBuiltIn); // Up/Down carrot buttons addButton(new GuiNpcButton(73, guiLeft + 270, y + 145, 20, 20, "<")); @@ -180,7 +187,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Save button (to the right of carrots, only enabled when ability is selected) addButton(new GuiNpcButton(76, centerX, y + 82, 40, 20, "gui.save")); - getButton(76).setEnabled(selectedAbilityIndex >= 0); + getButton(76).setEnabled(selectedAbilityIndex >= 0 && !selectedIsBuiltIn); } @Override @@ -464,6 +471,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC String displayName = I18n.format(typeId); // Search matches either the display name or the typeId if (search.isEmpty() || displayName.toLowerCase().contains(search) || typeId.toLowerCase().contains(search)) { + // Gray out built-in types to distinguish them from configurable types + if (AbilityController.Instance.isBuiltInType(typeId)) { + displayName = "\u00A77" + displayName; + } list.add(displayName); displayNameToTypeId.put(displayName, typeId); } @@ -512,6 +523,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC if (slot.isReference()) { return colorPrefix + (index + 1) + ". \u00A7e> " + nameDisplay + "\u00A7r"; } + if (ability.isBuiltIn()) { + return colorPrefix + (index + 1) + ". \u00A77" + nameDisplay + "\u00A7r"; + } return colorPrefix + (index + 1) + ". " + nameDisplay; } @@ -612,10 +626,11 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + Ability ability = slot.getAbility(); + if (ability != null && ability.isBuiltIn()) return; if (slot.isReference()) { setSubGui(new SubGuiAbilityEditMode()); } else { - Ability ability = slot.getAbility(); if (ability != null) { setSubGui(ability.createConfigGui(this)); } 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 5d810862f..cce48bd32 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java @@ -58,6 +58,8 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS allDisplayNameToTypeId.clear(); String[] types = AbilityController.Instance.getTypes(); for (String typeId : types) { + // Exclude built-in types — they cannot be created as custom presets + if (AbilityController.Instance.isBuiltInType(typeId)) continue; String displayName = I18n.format(typeId); allDisplayNameToTypeId.put(displayName, typeId); } 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 b06f9d097..66e8ed46c 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -6,6 +6,7 @@ import kamkeel.npcs.controllers.data.ability.AbilityController; import java.util.UUID; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.BuiltInAbilityGetPacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; @@ -51,6 +52,7 @@ import java.util.Vector; * - Left side: 3D preview of NPC with ability effects * - Right side: Ability list with search * - Play/Pause/Stop buttons to preview abilities + * - Toggle between Custom and Built-in ability views */ public class GuiNpcManageAbilities extends GuiAbilityInterface implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback, @@ -58,7 +60,8 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // ==================== DATA ==================== private GuiCustomScroll scroll; - private HashMap abilityData = new HashMap<>(); + private HashMap customData = new HashMap<>(); + private HashMap builtInData = new HashMap<>(); /** Maps display name -> UUID for custom abilities */ private final HashMap displayToUuid = new HashMap<>(); private String selected = null; @@ -66,6 +69,10 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private String search = ""; private Ability selectedAbility = null; + // ==================== BUILT-IN TOGGLE ==================== + private boolean showingBuiltIn = false; + private boolean currentIsBuiltIn = false; + // ==================== PENDING SAVE ==================== private Ability pendingSaveAbility = null; private boolean pendingNewCreation = false; @@ -88,7 +95,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface previewExecutor = new AbilityPreviewExecutor(); previewExecutor.setParentGui(this); - // Request ability data from server + // Request ability data from server (sends both custom and built-in) PacketClient.sendClient(new CustomAbilitiesGetPacket()); // Enable animation data @@ -100,18 +107,26 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface public void initGui() { super.initGui(); - // Add button - addButton(new GuiNpcButton(2, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); + // Toggle button — added FIRST to maintain stable buttonList index + String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom"; + addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 52, 45, 20, toggleLabel)); - // Remove button - addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove")); - getButton(1).setEnabled(selected != null && !selected.isEmpty() && displayToUuid.containsKey(selected)); + if (!showingBuiltIn) { + // Add button — only for custom view + addButton(new GuiNpcButton(2, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); - // Edit button - addButton(new GuiNpcButton(100, guiLeft + 368, guiTop + 52, 45, 20, "gui.edit")); - getButton(100).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null); + // Remove button — only for custom view + addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove")); + getButton(1).setEnabled(selected != null && !selected.isEmpty() && displayToUuid.containsKey(selected)); + } - // Scroll list of saved abilities (right side) + // Edit button — hidden when viewing built-in + if (!showingBuiltIn && !currentIsBuiltIn) { + addButton(new GuiNpcButton(100, guiLeft + 368, guiTop + 74, 45, 20, "gui.edit")); + getButton(100).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null); + } + + // Scroll list of abilities (right side) if (scroll == null) { scroll = new GuiCustomScroll(this, 0); scroll.setSize(143, 185); @@ -122,7 +137,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // Update scroll list scroll.setList(getSearchList()); - if (selected != null && displayToUuid.containsKey(selected)) { + if (selected != null) { scroll.setSelected(selected); } @@ -134,9 +149,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface return; } - // ID/Name label (below search bar) - addLabel(new GuiNpcLabel(10, "ID: " + selectedAbility.getId(), guiLeft + 220, guiTop + 215)); - // Play/Pause/Stop buttons (below model preview area) String animTexture = "customnpcs:textures/gui/animation.png"; int playButtonOffsetX = 60; @@ -161,17 +173,34 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface } } + private HashMap getCurrentData() { + return showingBuiltIn ? builtInData : customData; + } + private List getSearchList() { - if (search.isEmpty()) { - return new ArrayList<>(displayToUuid.keySet()); - } - List list = new ArrayList<>(); - for (String name : displayToUuid.keySet()) { - if (name.toLowerCase().contains(search.toLowerCase())) { - list.add(name); + if (showingBuiltIn) { + if (search.isEmpty()) { + return new ArrayList<>(builtInData.keySet()); } + List list = new ArrayList<>(); + for (String name : builtInData.keySet()) { + if (name.toLowerCase().contains(search.toLowerCase())) { + list.add(name); + } + } + return list; + } else { + if (search.isEmpty()) { + return new ArrayList<>(displayToUuid.keySet()); + } + List list = new ArrayList<>(); + for (String name : displayToUuid.keySet()) { + if (name.toLowerCase().contains(search.toLowerCase())) { + list.add(name); + } + } + return list; } - return list; } @Override @@ -227,13 +256,30 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface int id = guibutton.id; - if (id == 2) { + // Toggle between custom and built-in views + if (id == 10) { + showingBuiltIn = !showingBuiltIn; + selected = null; + selectedUuid = null; + selectedAbility = null; + currentIsBuiltIn = false; + search = ""; + previewExecutor.stop(); + if (scroll != null) { + scroll.clear(); + } + PacketClient.sendClient(new CustomAbilitiesGetPacket()); + initGui(); + return; + } + + if (id == 2 && !showingBuiltIn) { // Add — open type selection setSubGui(new SubGuiAbilityTypeSelect()); - } else if (id == 1 && selected != null && selectedUuid != null) { + } else if (id == 1 && !showingBuiltIn && selected != null && selectedUuid != null) { GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1); displayGuiScreen(guiyesno); - } else if (id == 100 && selectedAbility != null) { + } else if (id == 100 && !showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) { // Edit previewExecutor.stop(); setSubGui(selectedAbility.createConfigGui(this)); @@ -276,11 +322,20 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface String newSelection = scroll.getSelected(); if (newSelection != null && !newSelection.equals(selected)) { previewExecutor.stop(); - selected = newSelection; - selectedUuid = displayToUuid.get(selected); - if (selectedUuid != null) { - PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + + if (showingBuiltIn) { + // Fetch built-in ability by display name + selectedUuid = null; + currentIsBuiltIn = true; + PacketClient.sendClient(new BuiltInAbilityGetPacket(selected)); + } else { + // Fetch custom ability by UUID + selectedUuid = displayToUuid.get(selected); + currentIsBuiltIn = false; + if (selectedUuid != null) { + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } } } } @@ -289,8 +344,8 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { if (scroll.id == 0 && selection != null && !selection.isEmpty()) { - // Double-click to edit - if (selectedAbility != null) { + // Double-click to edit — only for custom, non-built-in abilities + if (!showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) { previewExecutor.stop(); setSubGui(selectedAbility.createConfigGui(this)); } @@ -301,7 +356,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface public void setData(Vector list, HashMap data, EnumScrollData type) { if (type == EnumScrollData.CUSTOM_ABILITIES) { String prevSelected = scroll != null ? scroll.getSelected() : null; - this.abilityData = data; + this.customData = data; displayToUuid.clear(); for (String key : data.keySet()) { // key format: "displayName\tUUID" @@ -314,18 +369,30 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface displayToUuid.put(key, key); } } - if (scroll != null) { + if (!showingBuiltIn && scroll != null) { scroll.setList(getSearchList()); if (prevSelected != null && displayToUuid.containsKey(prevSelected)) { scroll.setSelected(prevSelected); } } initGui(); + } else if (type == EnumScrollData.BUILTIN_ABILITIES) { + this.builtInData = data; + if (showingBuiltIn && scroll != null) { + scroll.setList(getSearchList()); + if (selected != null && builtInData.containsKey(selected)) { + scroll.setSelected(selected); + } + } + initGui(); } } @Override public void setGuiData(NBTTagCompound compound) { + // Check if this is a built-in ability response + currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn"); + // Received ability data from server selectedAbility = AbilityController.Instance.fromNBT(compound); if (selectedAbility != null) { @@ -364,7 +431,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface return; } pendingTypeId = null; - saveAndOpenConfig(newAbility); + openConfig(newAbility); return; } } @@ -390,7 +457,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface setSubGui(new SubGuiDuplicateNameConfirm()); return; } - saveAndOpenConfig(newAbility); + openConfig(newAbility); return; } } @@ -398,7 +465,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface SubGuiDuplicateNameConfirm confirm = (SubGuiDuplicateNameConfirm) subgui; if (confirm.isConfirmed() && pendingSaveAbility != null) { if (pendingNewCreation) { - saveAndOpenConfig(pendingSaveAbility); + openConfig(pendingSaveAbility); pendingSaveAbility = null; pendingNewCreation = false; return; @@ -434,8 +501,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface initGui(); } - private void saveAndOpenConfig(Ability ability) { - PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + private void openConfig(Ability ability) { selectedAbility = ability; selected = ability.getName(); selectedUuid = ability.getId(); diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index 06d5a6ec3..cbec6ad34 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -8,6 +8,7 @@ public enum EnumScrollData { QUEST_GROUP, ABILITY_TYPES, CUSTOM_ABILITIES, + BUILTIN_ABILITIES, ANIMATIONS, BUILTIN_ANIMATIONS } From 5d682962be60d21ec09fb07b73a11fadb625c811 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 12 Feb 2026 20:30:45 -0500 Subject: [PATCH 34/34] feat: add BuiltInAbilityGetPacket for requesting built-in abilities by name --- .../ability/BuiltInAbilityGetPacket.java | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java new file mode 100644 index 000000000..6e565f3de --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java @@ -0,0 +1,70 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +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 java.io.IOException; + +/** + * Request packet to get a specific built-in ability by name. + * Built-in abilities are registered presets with fixed configurations. + */ +public final class BuiltInAbilityGetPacket extends AbstractPacket { + public static String packetName = "Request|BuiltInAbilityGet"; + + private String abilityName; + + public BuiltInAbilityGetPacket() { + } + + public BuiltInAbilityGetPacket(String abilityName) { + this.abilityName = abilityName; + } + + @Override + public Enum getType() { + return EnumRequestPacket.BuiltInAbilityGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, abilityName); + } + + @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); + Ability ability = AbilityController.Instance.getAbilityByDisplayName(name); + if (ability != null) { + NBTTagCompound compound = ability.writeNBT(); + compound.setBoolean("BuiltIn", true); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + } +}