diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java index 91467556a..4639901d4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java @@ -34,6 +34,7 @@ public class AbilityLaserShot extends AbilityEnergyProjectile hitEntities = new HashSet<>(); @@ -298,6 +301,20 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { hitEntities.add(entity.getEntityId()); applyDamage(entity); + if (dieOnImpact) { + // Stop at impact point and begin linger/fade + double dx = entity.posX - startX; + double dy = (entity.posY + entity.height * 0.5) - startY; + double dz = entity.posZ - startZ; + float impactDist = (float) Math.sqrt(dx * dx + dy * dy + dz * dz); + currentLength = Math.min(impactDist, currentLength); + endX = startX + dirX * currentLength; + endY = startY + dirY * currentLength; + endZ = startZ + dirZ * currentLength; + fullyExtended = true; + return; // Stop checking further entities + } + // Piercing - don't stop, continue to next entity } } @@ -386,6 +403,14 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { this.lingerTicks = ticks; } + public boolean isDieOnImpact() { + return dieOnImpact; + } + + public void setDieOnImpact(boolean dieOnImpact) { + this.dieOnImpact = dieOnImpact; + } + public void setDirection(double x, double y, double z) { this.dirX = x; this.dirY = y; @@ -451,6 +476,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { this.laserWidth = nbt.hasKey("LaserWidth") ? nbt.getFloat("LaserWidth") : 0.2f; this.expansionSpeed = nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f; this.lingerTicks = nbt.hasKey("LingerTicks") ? nbt.getInteger("LingerTicks") : 10; + this.dieOnImpact = nbt.getBoolean("DieOnImpact"); this.dirX = nbt.getDouble("DirX"); this.dirY = nbt.getDouble("DirY"); this.dirZ = nbt.getDouble("DirZ"); @@ -468,6 +494,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { nbt.setFloat("LaserWidth", laserWidth); nbt.setFloat("ExpansionSpeed", expansionSpeed); nbt.setInteger("LingerTicks", lingerTicks); + nbt.setBoolean("DieOnImpact", dieOnImpact); nbt.setDouble("DirX", dirX); nbt.setDouble("DirY", dirY); nbt.setDouble("DirZ", dirZ); diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index d793dcea9..81ff9ed6b 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -332,10 +332,10 @@ public class ScriptPlayerEventHandler { return; if (event.player.worldObj instanceof WorldServer && event.player instanceof EntityPlayerMP) { - // Cancel any executing ability on dimension change + // Fully reset ability state on dimension change (no cooldown rollover) PlayerData playerData = PlayerData.get(event.player); if (playerData != null) { - playerData.abilityData.interruptCurrentAbility(); + playerData.abilityData.resetOnDimensionChange(); } PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(event.player); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index f0b610d19..f662da835 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -92,6 +92,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi @Override public void initGui() { + // Commit any focused text field before rebuilding + GuiNpcTextField.unfocus(); + // Save scroll position before super clears scroll windows GuiScrollWindow oldSw = getScrollableGui(0); if (oldSw != null && activeTab < tabScrollY.length) { diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java index 090d86dbb..aa2df26f9 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java @@ -45,13 +45,13 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText // ── Tab constants (core tabs) ──────────────────────────────────────────── private static final int TAB_GENERAL = 0; private static final int TAB_ENTRIES = 1; - private static final int TAB_CONDITIONS = 2; + private static final int TAB_TARGET = 2; private static final int CORE_TAB_COUNT = 3; // ── Tab button IDs ──────────────────────────────────────────────────────── private static final int BTN_TAB_GENERAL = 90; private static final int BTN_TAB_ENTRIES = 91; - private static final int BTN_TAB_CONDITIONS = 92; + private static final int BTN_TAB_TARGET = 92; private static final int BTN_TAB_EXTRA_BASE = 93; private static final int BTN_CLOSE = -1000; @@ -133,16 +133,18 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText fieldDefs.add(FieldDef.intField("ability.weight", chain::getWeight, chain::setWeight).range(1, 100).tab("General")); fieldDefs.add(FieldDef.boolField("ability.windUpAll", chain::isWindUpAll, chain::setWindUpAll).tab("General")); fieldDefs.add(FieldDef.intField("ability.cooldown", chain::getCooldownTicks, chain::setCooldownTicks).range(0, 12000).tab("General")); + + // ── Target tab ─────────────────────────────────────────────── fieldDefs.add(FieldDef.row( FieldDef.floatField("ability.minRange", chain::getMinRange, chain::setMinRange).range(0, 64), FieldDef.floatField("ability.maxRange", chain::getMaxRange, chain::setMaxRange).range(0, 64) - ).tab("General")); + ).tab("Target")); - // Computed compatibility label (derived from child abilities) - fieldDefs.add(FieldDef.labelField("ability.compatible", () -> { + // Computed "Valid For" label (derived from child abilities) + fieldDefs.add(FieldDef.labelField("ability.validFor", () -> { UserType ut = computeAllowedBy(); - return StatCollector.translateToLocal("ability.compatible") + ": \u00A7e" + ut.name(); - }).tab("General")); + return StatCollector.translateToLocal("ability.validFor") + ": \u00A7e" + StatCollector.translateToLocal("ability.userType." + ut.name()); + }).tab("Target")); // External field providers (e.g., DBC Addon injecting an "Icon" tab) if (AbilityController.Instance != null) { @@ -155,7 +157,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText Set tabNames = new LinkedHashSet<>(); for (FieldDef def : fieldDefs) { String tab = def.getTab(); - if (tab != null && !tab.isEmpty() && !"General".equals(tab)) { + if (tab != null && !tab.isEmpty() && !"General".equals(tab) && !"Target".equals(tab)) { tabNames.add(tab); } } @@ -196,6 +198,9 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText @Override public void initGui() { + // Commit any focused text field before rebuilding + GuiNpcTextField.unfocus(); + // Save scroll position GuiScrollWindow oldSw = getScrollableGui(0); if (oldSw != null && activeTab < tabScrollY.length) { @@ -213,12 +218,12 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText entriesTab.active = (activeTab == TAB_ENTRIES); addTopButton(entriesTab); - GuiMenuTopButton conditionsTab = new GuiMenuTopButton(BTN_TAB_CONDITIONS, entriesTab, "ability.conditions"); - conditionsTab.active = (activeTab == TAB_CONDITIONS); - addTopButton(conditionsTab); + GuiMenuTopButton targetTab = new GuiMenuTopButton(BTN_TAB_TARGET, entriesTab, "script.target"); + targetTab.active = (activeTab == TAB_TARGET); + addTopButton(targetTab); // Dynamic extra tabs from field providers - GuiMenuTopButton prevTab = conditionsTab; + GuiMenuTopButton prevTab = targetTab; for (int i = 0; i < extraTabs.size(); i++) { int tabIndex = CORE_TAB_COUNT + i; GuiMenuTopButton extraTab = new GuiMenuTopButton(BTN_TAB_EXTRA_BASE + i, prevTab, extraTabs.get(i)); @@ -240,8 +245,8 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText buildFieldDefTab("General", swX, swY, swW, swH); } else if (activeTab == TAB_ENTRIES) { buildEntriesTab(swX, swY, swW, swH); - } else if (activeTab == TAB_CONDITIONS) { - buildConditionsTab(swX, swY, swW, swH); + } else if (activeTab == TAB_TARGET) { + buildTargetTab(swX, swY, swW, swH); } else if (activeTab >= CORE_TAB_COUNT) { int extraIndex = activeTab - CORE_TAB_COUNT; if (extraIndex < extraTabs.size()) { @@ -339,18 +344,35 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText } // ═══════════════════════════════════════════════════════════════════════════ - // CONDITIONS TAB + // TARGET TAB (FieldDef fields + conditions list) // ═══════════════════════════════════════════════════════════════════════════ - private void buildConditionsTab(int swX, int swY, int swW, int swH) { - GuiScrollWindow sw = new GuiScrollWindow(this, swX, swY, swW, swH, 0); - sw.backgroundColor = 0x88000000; - addScrollableGui(0, sw); + private void buildTargetTab(int swX, int swY, int swW, int swH) { + // Build FieldDef fields (ranges, Valid For) first + List tabFields = new ArrayList<>(); + for (FieldDef def : fieldDefs) { + if ("Target".equals(def.getTab()) && def.isVisible()) { + tabFields.add(def); + } + } - int y = 5; - int labelCounter = 0; + builder = new GuiFieldBuilder(this, fontRendererObj); + builder.startIds(DECLARATIVE_ID_START, CLEAR_ID_START, LABEL_ID_START); + builder.startY(5); - sw.addLabel(new GuiNpcLabel(labelCounter++, "ability.conditions", L_LABEL_X, y + 2, 0xFFFF55)); + GuiScrollWindow sw = builder.buildScrollWindow(tabFields, swX, swY, swW, swH); + + // Append conditions list below the FieldDef fields + int y = builder.getLastBuildY(); + int condY = renderConditions(sw, y, builder.getNextLabelId()); + sw.maxScrollY = Math.max(condY - swH, 0); + + restoreScroll(sw); + } + + private int renderConditions(GuiScrollWindow sw, int y, int labelCounter) { + y += 3; + sw.addLabel(new GuiNpcLabel(labelCounter, "ability.conditions", L_LABEL_X, y + 2, 0xFFFF55)); y += 15; for (int i = 0; i < conditions.size() && i < 3; i++) { @@ -367,8 +389,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText y += ROW_H; } - sw.maxScrollY = Math.max(y - swH, 0); - restoreScroll(sw); + return y; } private String getConditionDisplayName(Condition cond) { @@ -427,8 +448,8 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText initGui(); return; } - if (id == BTN_TAB_CONDITIONS) { - activeTab = TAB_CONDITIONS; + if (id == BTN_TAB_TARGET) { + activeTab = TAB_TARGET; initGui(); return; } @@ -450,8 +471,8 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText if (handleEntryButton(id)) return; } - // Conditions tab - if (activeTab == TAB_CONDITIONS) { + // Target tab (includes conditions) + if (activeTab == TAB_TARGET) { if (handleConditionButton(id)) return; } @@ -470,7 +491,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText * Whether the current active tab uses the declarative FieldDef system. */ private boolean isFieldDefTab() { - return activeTab == TAB_GENERAL || activeTab >= CORE_TAB_COUNT; + return activeTab == TAB_GENERAL || activeTab == TAB_TARGET || activeTab >= CORE_TAB_COUNT; } private boolean handleEntryButton(int id) { @@ -568,7 +589,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText return; } - // Entries tab — delay fields (ENTRY_BASE + i * ENTRY_STRIDE + 1) + // Entries tab - delay fields (ENTRY_BASE + i * ENTRY_STRIDE + 1) if (activeTab == TAB_ENTRIES && id >= ENTRY_BASE + 1 && id < ENTRY_END) { int entryIndex = (id - ENTRY_BASE - 1) / ENTRY_STRIDE; if (entryIndex >= 0 && entryIndex < entries.size()) { 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 52ddbba9d..63e78dd11 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -175,13 +175,12 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // Search bar addTextField(new GuiNpcTextField(TF_SEARCH, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); - // Preview only for ability views (not chained) + // Check if we have something to preview if (showingChained) { - return; - } - - // Show ability info only if one is selected - if (selectedAbility == null || selected == null) { + if (selectedChain == null || selected == null || selectedChain.getEntries().isEmpty()) { + return; + } + } else if (selectedAbility == null || selected == null) { return; } @@ -311,6 +310,27 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface return; } + // Preview controls (shared across all views) + if (id == BTN_PREVIEW_PLAY) { + if (previewExecutor.isPaused()) { + previewExecutor.play(); + } else if (showingChained && selectedChain != null) { + previewExecutor.startChainPreview(selectedChain, npc); + } else if (selectedAbility != null) { + previewExecutor.startPreview(selectedAbility, npc); + } + initGui(); + return; + } else if (id == BTN_PREVIEW_PAUSE) { + previewExecutor.pause(); + initGui(); + return; + } else if (id == BTN_PREVIEW_STOP) { + previewExecutor.stop(); + initGui(); + return; + } + if (showingChained) { if (id == BTN_ADD) { // New chained ability @@ -320,6 +340,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), CONFIRM_REMOVE_CHAIN); displayGuiScreen(guiyesno); } else if (id == BTN_EDIT && selectedChain != null) { + previewExecutor.stop(); setSubGui(new SubGuiChainedAbilityConfig(selectedChain, this)); } return; @@ -335,24 +356,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // Edit previewExecutor.stop(); setSubGui(selectedAbility.createConfigGui(this)); - } else if (id == BTN_PREVIEW_PLAY) { - // Play button - if (selectedAbility != null) { - if (previewExecutor.isPaused()) { - previewExecutor.play(); - } else { - previewExecutor.startPreview(selectedAbility, npc); - } - initGui(); - } - } else if (id == BTN_PREVIEW_PAUSE) { - // Pause button - previewExecutor.pause(); - initGui(); - } else if (id == BTN_PREVIEW_STOP) { - // Stop button - previewExecutor.stop(); - initGui(); } } 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 6d2534633..bde8ae194 100644 --- a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java +++ b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java @@ -2,8 +2,11 @@ package noppes.npcs.client.gui.util; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.ChainedAbility; +import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry; import kamkeel.npcs.controllers.data.ability.PreviewEntityHandler; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import net.minecraft.entity.Entity; @@ -39,7 +42,7 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { /** * Executor's own phase tracking (separate from ability.getPhase()) */ - private enum ExecutorPhase {IDLE, WINDUP, ACTIVE, EXTENDED} + private enum ExecutorPhase {IDLE, WINDUP, ACTIVE, EXTENDED, CHAIN_DELAY} private ExecutorPhase executorPhase = ExecutorPhase.IDLE; @@ -62,6 +65,14 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { private TelegraphInstance previewTelegraph; private List previewEntities = new ArrayList<>(); + // ==================== CHAIN PREVIEW ==================== + private ChainedAbility previewChain; + private List chainAbilities; + private List chainDelays; + private int chainIndex = -1; + private int chainDelayRemaining = 0; + private boolean chainWindUpAll = true; + // ==================== CALLBACKS ==================== private GuiAbilityInterface parentGui; @@ -73,28 +84,21 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { } /** - * Start previewing an ability using unified execution. + * Start previewing a single ability using unified execution. * Sets previewMode=true on the ability, then calls its real methods. */ public void startPreview(Ability ability, EntityNPCInterface npc) { stop(); - this.previewAbility = ability; this.previewNpc = npc; - this.currentTick = 0; this.playing = true; this.paused = false; - 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); @@ -103,10 +107,86 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { parentGui.startTrackingMovement(); } + beginAbilityPreview(ability); + } + + /** + * Start previewing a chained ability — plays all entries in sequence with delays. + * Each entry is resolved and deep-copied to avoid modifying originals. + */ + public void startChainPreview(ChainedAbility chain, EntityNPCInterface npc) { + stop(); + + this.previewNpc = npc; + this.playing = true; + this.paused = false; + this.previewChain = chain; + this.chainWindUpAll = chain.isWindUpAll(); + + // Save NPC start position for restoring later + this.npcStartX = npc.posX; + this.npcStartY = npc.posY; + this.npcStartZ = npc.posZ; + + // Resolve all entries to deep-copied abilities + chainAbilities = new ArrayList<>(); + chainDelays = new ArrayList<>(); + for (ChainedAbilityEntry entry : chain.getEntries()) { + Ability resolved = entry.resolve(); + if (resolved != null && AbilityController.Instance != null) { + Ability copy = AbilityController.Instance.fromNBT(resolved.writeNBT()); + if (copy != null) { + chainAbilities.add(copy); + chainDelays.add(entry.getDelayTicks()); + } + } + } + + if (chainAbilities.isEmpty()) { + playing = false; + return; + } + + // Create invisible fake target NPC for targeting + createFakeTarget(npc); + + // Start tracking NPC movement + if (parentGui != null) { + parentGui.startTrackingMovement(); + } + + chainIndex = 0; + beginAbilityPreview(chainAbilities.get(0)); + } + + /** + * Begin previewing a single ability (shared by single and chain preview). + * Cleans up the previous ability if any (for chain advancement). + */ + private void beginAbilityPreview(Ability ability) { + // Clean up previous ability if any (for chain advancement) + if (previewAbility != null) { + previewAbility.cleanup(); + previewAbility.setPreviewMode(false); + previewAbility.setPreviewEntityHandler(null); + previewAbility.reset(); + } + + this.previewAbility = ability; + this.currentTick = 0; + this.maxPreviewDuration = ability.getMaxPreviewDuration(); + + // Enable preview mode on the ability + ability.setPreviewMode(true); + ability.setPreviewEntityHandler(this); + // Use the ability's real start() method with the fake target ability.start(fakeTarget); - if (ability.getWindUpTicks() <= 0) { + // Skip windup for non-first chain entries when windUpAll is false + boolean skipWindup = previewChain != null && !chainWindUpAll && chainIndex > 0; + + if (ability.getWindUpTicks() <= 0 || skipWindup) { // No windup — go straight to active this.executorPhase = ExecutorPhase.ACTIVE; transitionToActive(); @@ -115,12 +195,12 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { this.executorPhase = ExecutorPhase.WINDUP; // Create telegraph for preview - previewTelegraph = ability.createTelegraph(npc, fakeTarget); + previewTelegraph = ability.createTelegraph(previewNpc, fakeTarget); // Start windup animation Animation windUpAnim = ability.getWindUpAnimation(); if (windUpAnim != null) { - AnimationData data = npc.display.animationData; + AnimationData data = previewNpc.display.animationData; data.setEnabled(true); data.setAnimation(windUpAnim); data.animation.paused = false; @@ -216,6 +296,13 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { executorPhase = ExecutorPhase.IDLE; currentTick = 0; + // Clear chain state + previewChain = null; + chainAbilities = null; + chainDelays = null; + chainIndex = -1; + chainDelayRemaining = 0; + previewTelegraph = null; if (parentGui != null) { @@ -286,6 +373,9 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { case EXTENDED: tickExtended(); break; + case CHAIN_DELAY: + tickChainDelay(); + break; default: break; } @@ -309,13 +399,21 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { // Check if the ability signaled completion (phase went to IDLE) if (previewAbility.getPhase() == AbilityPhase.IDLE) { - transitionToExtended(); + if (hasNextChainEntry()) { + transitionToChainDelay(); + } else { + transitionToExtended(); + } return; } // Safety timeout if (currentTick >= maxPreviewDuration) { - transitionToExtended(); + if (hasNextChainEntry()) { + transitionToChainDelay(); + } else { + transitionToExtended(); + } } } @@ -326,6 +424,44 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { } } + // ==================== CHAIN ADVANCEMENT ==================== + + private boolean hasNextChainEntry() { + return previewChain != null && chainAbilities != null && chainIndex < chainAbilities.size() - 1; + } + + private void transitionToChainDelay() { + executorPhase = ExecutorPhase.CHAIN_DELAY; + currentTick = 0; + + // Use the NEXT entry's delay + int nextIndex = chainIndex + 1; + chainDelayRemaining = (nextIndex < chainDelays.size()) ? chainDelays.get(nextIndex) : 0; + + // Clear current animation + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + data.setAnimation(new Animation()); + } + } + + private void tickChainDelay() { + if (chainDelayRemaining > 0) { + chainDelayRemaining--; + return; + } + advanceChainEntry(); + } + + private void advanceChainEntry() { + chainIndex++; + if (chainIndex >= chainAbilities.size()) { + transitionToExtended(); + return; + } + beginAbilityPreview(chainAbilities.get(chainIndex)); + } + private void transitionToActive() { executorPhase = ExecutorPhase.ACTIVE; currentTick = 0; @@ -439,6 +575,10 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { return playing || paused; } + public boolean isChainPreview() { + return previewChain != null; + } + public AbilityPhase getPhase() { switch (executorPhase) { case WINDUP: @@ -473,14 +613,23 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { public String getStatusString() { if (!playing && !paused) { return "Stopped"; - } else if (paused) { - return "Paused"; + } + + String chainPrefix = ""; + if (previewChain != null && chainAbilities != null) { + chainPrefix = "Chain " + (chainIndex + 1) + "/" + chainAbilities.size() + " - "; + } + + if (paused) { + return chainPrefix + "Paused"; } else if (executorPhase == ExecutorPhase.WINDUP) { - return "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0); + return chainPrefix + "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0); } else if (executorPhase == ExecutorPhase.ACTIVE) { - return "Active: " + currentTick + "/" + maxPreviewDuration; + return chainPrefix + "Active: " + currentTick + "/" + maxPreviewDuration; } else if (executorPhase == ExecutorPhase.EXTENDED) { - return "Extended: " + currentTick + "/" + EXTENDED_DURATION; + return chainPrefix + "Extended: " + currentTick + "/" + EXTENDED_DURATION; + } else if (executorPhase == ExecutorPhase.CHAIN_DELAY) { + return chainPrefix + "Delay: " + chainDelayRemaining; } return ""; } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index a11483bc0..8562053c2 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -581,6 +581,51 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA interruptCurrentAbility(null, 0); } + // ═══════════════════════════════════════════════════════════════════ + // DIMENSION CHANGE RESET + // ═══════════════════════════════════════════════════════════════════ + + /** + * Fully reset ability state when changing dimensions. + * Unlike interruptCurrentAbility(), this does NOT roll any cooldowns + * and ensures the player can immediately use abilities in the new dimension. + * Also re-syncs to client since the entity may be re-created. + */ + public void resetOnDimensionChange() { + // Clean up any executing ability without rolling cooldowns + if (currentAbility != null && currentAbility.isExecuting()) { + removeTelegraph(currentAbility); + currentAbility.cleanup(); + currentAbility.interrupt(); + stopAbilityAnimation(); + releaseRotationControl(); + releaseLockedPosition(); + } + + // Clear chain state + currentChain = null; + chainEntryIndex = -1; + chainDelayRemaining = -1; + + // Clear all transient state + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + + // Reset cooldowns completely + cooldownEndTime = 0; + interruptCooldownRolled = false; + + // Sync cleared state to client + EntityPlayer player = playerData.player; + if (player != null) { + syncAbilityStateClear(player); + } + + // Re-sync ability data (unlocked list, selection) to client + syncToClient(); + } + // ═══════════════════════════════════════════════════════════════════ // DAMAGE HANDLING // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 2778a1ec7..6574f71bc 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -2037,6 +2037,11 @@ ability.selectSlot=Select NPC Ability ability.loadType=Load Type ability.loadAbility=Load Ability ability.loadChain=Load Chain +ability.validFor=Valid For +ability.userType.NPC_ONLY=NPCs +ability.userType.PLAYER_ONLY=Players +ability.userType.BOTH=Both +ability.dieOnImpact=Die on Impact gui.both=Both gui.players=Players