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 c44f1bd47..f93a5c772 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java
@@ -14,6 +14,7 @@ import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
+import net.minecraft.util.StatCollector;
import net.minecraft.world.World;
import noppes.npcs.DataAbilities;
import noppes.npcs.NpcDamageSource;
@@ -281,6 +282,17 @@ public abstract class Ability implements IAbility, IAbilityAction {
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
}
+ /**
+ * Called when the ability enters BURST_DELAY between burst iterations.
+ * Override in subclasses to reset transient state that should not carry
+ * across burst repetitions (e.g., movement direction, hit entity lists).
+ *
+ * Unlike {@link #cleanup()}, this does NOT kill spawned entities (they may
+ * still be in flight for overlap mode).
+ */
+ public void resetForBurst() {
+ }
+
public void onDamageTaken(EntityLivingBase caster, EntityLivingBase attacker, DamageSource source, float damage) {
}
@@ -535,6 +547,10 @@ public abstract class Ability implements IAbility, IAbilityAction {
.tab("General"));
defs.add(FieldDef.stringField("gui.displayName", this::getRawDisplayName, this::setDisplayName)
.tab("General"));
+ defs.add(FieldDef.labelField("ability.validFor", () ->
+ StatCollector.translateToLocal("ability.validFor") + ": \u00A7e"
+ + StatCollector.translateToLocal("ability.userType." + getAllowedBy().name()))
+ .tab("General"));
defs.add(FieldDef.row(
FieldDef.intField("ability.weight", this::getWeight, this::setWeight).range(1, 1000),
FieldDef.boolField("gui.enabled", this::isEnabled, this::setEnabled)
@@ -979,9 +995,9 @@ public abstract class Ability implements IAbility, IAbilityAction {
if (phase == AbilityPhase.ACTIVE) {
// Check if more burst iterations remain
if (burstEnabled && burstAmount > 0 && burstIndex < burstAmount) {
- // Don't cleanup here - entities may still be in flight
- // For non-overlap: cleaned up when next burst starts (in onExecute)
- // For overlap: entities continue flying indefinitely
+ // Reset transient state for next burst iteration (e.g., movement direction, hit lists)
+ // Does NOT kill entities - they may still be in flight (overlap mode)
+ resetForBurst();
burstIndex++;
phase = AbilityPhase.BURST_DELAY;
currentTick = 0;
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 9f063ff72..120f6190d 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
@@ -171,6 +171,12 @@ public class AbilityCharge extends AbilityMovement implements IAbilityCharge {
hitEntities.clear();
}
+ @Override
+ public void resetForBurst() {
+ super.resetForBurst();
+ hitEntities.clear();
+ }
+
@Override
public float getTelegraphLength() {
return maxRange;
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 84df55642..fffbf6ca5 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
@@ -196,6 +196,12 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
chosenDirection = null;
}
+ @Override
+ public void resetForBurst() {
+ super.resetForBurst();
+ chosenDirection = null;
+ }
+
@Override
public int getMaxPreviewDuration() {
return (int) Math.ceil(dashDistance / dashSpeed) + 5;
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java
index de2090cc3..d5b8d8c52 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java
@@ -53,6 +53,12 @@ public abstract class AbilityMovement extends Ability {
maxActiveTicks = 0;
}
+ @Override
+ public void resetForBurst() {
+ movementDirection = null;
+ maxActiveTicks = 0;
+ }
+
// ═══════════════════════════════════════════════════════════════════
// DIRECTION HELPERS
// ═══════════════════════════════════════════════════════════════════
diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java
index 6575b29c0..2286919d6 100644
--- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java
+++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java
@@ -68,6 +68,7 @@ public abstract class AbilityEnergyProjectile
this.homingData = homingData;
this.lifespanData = lifespanData;
this.trajectoryData = new EnergyTrajectoryData();
+ this.burstOverlap = true;
initProjectiles(1);
}
diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java
index c31154bc3..13d3095c9 100644
--- a/src/main/java/noppes/npcs/AbstractDataAbilities.java
+++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java
@@ -616,8 +616,10 @@ public abstract class AbstractDataAbilities {
return;
}
- // Auto-complete for burst overlap mode
+ // Auto-complete for burst overlap mode only.
+ // Non-overlap waits for all entities to die naturally (via allDead check in onActiveTick).
if (currentAbility.isBurstEnabled()
+ && currentAbility.isBurstOverlap()
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
@@ -682,10 +684,12 @@ public abstract class AbstractDataAbilities {
// Chain mode: advance to next entry instead of completing
if (currentChain != null) {
+ // AFTER semantics: delay from the CURRENT (just-completed) entry
+ ChainedAbilityEntry completedEntry = currentChain.getEntries().get(chainEntryIndex);
+ int delay = completedEntry.getDelayTicks();
+
chainEntryIndex++;
if (chainEntryIndex < currentChain.getEntries().size()) {
- ChainedAbilityEntry nextEntry = currentChain.getEntries().get(chainEntryIndex);
-
// Check if target died and retarget
if (target != null && target.isDead) {
target = retargetForChain();
@@ -695,13 +699,9 @@ public abstract class AbstractDataAbilities {
}
}
- int delay = nextEntry.getDelayTicks();
- if (delay > 0) {
- chainDelayRemaining = delay;
- currentAbility = null;
- } else {
- startChainEntry(target);
- }
+ // Enforce minimum 1-tick delay between chain entries
+ chainDelayRemaining = Math.max(1, delay);
+ currentAbility = null;
return;
}
// All entries complete
@@ -819,14 +819,7 @@ public abstract class AbstractDataAbilities {
chainEntryIndex = 0;
chainDelayRemaining = -1;
- // Check if first entry has a delay
- ChainedAbilityEntry firstEntry = chain.getEntries().get(0);
- int delay = firstEntry.getDelayTicks();
- if (delay > 0) {
- chainDelayRemaining = delay;
- return true;
- }
-
+ // AFTER semantics: no delay before first entry (delay applies after each entry completes)
return startChainEntry(target);
}
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 e95b2c97b..0a83895ed 100644
--- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java
+++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java
@@ -100,6 +100,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
// ── Pending: chain editing ────────────────────────────────────────────────
private ChainedAbility pendingChain = null;
+ private int pendingChainSlotIdx = -1;
// ── Pending: inline chain-entry editing ──────────────────────────────────
private boolean editingChainEntry = false;
@@ -233,17 +234,22 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
// Check if selected slot is editable
boolean selectedIsBuiltIn = false;
boolean selectedIsChainEntry = selectedEntryIndex >= 0;
- if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size() && !selectedIsChainEntry) {
+ boolean selectedIsRefChainEntry = false;
+ if (selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) {
AbilityAction sel = npcSlots.get(selectedSlotIndex);
- if (!sel.isChain()) {
+ if (!selectedIsChainEntry && !sel.isChain()) {
Ability a = sel.getAbility();
if (a != null && a.isBuiltIn()) selectedIsBuiltIn = true;
}
+ // Sub-entries of reference chains are not directly editable from the NPC list
+ if (selectedIsChainEntry && sel.isChainReference()) {
+ selectedIsRefChainEntry = true;
+ }
}
// Right side buttons: Edit and Up/Down carrots with Save button
addButton(new GuiNpcButton(BTN_EDIT, guiLeft + 210, y + 145, 55, 20, "gui.edit"));
- getButton(BTN_EDIT).setEnabled(selectedSlotIndex >= 0 && !selectedIsBuiltIn);
+ getButton(BTN_EDIT).setEnabled(selectedSlotIndex >= 0 && !selectedIsBuiltIn && !selectedIsRefChainEntry);
// Up/Down carrot buttons
addButton(new GuiNpcButton(BTN_MOVE_UP, guiLeft + 270, y + 145, 20, 20, "<"));
@@ -354,6 +360,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
ChainedAbility chain = slot.getInlineChain();
if (chain != null) {
pendingChain = chain;
+ pendingChainSlotIdx = selectedSlotIndex;
setSubGui(new SubGuiChainedAbilityConfig(chain, this, true, npcSlots));
}
}
@@ -484,6 +491,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
newChain.setId(UUID.randomUUID().toString());
newChain.setName("NewChain");
pendingChain = newChain;
+ pendingChainSlotIdx = -1;
setSubGui(new SubGuiChainedAbilityConfig(newChain, this, true, npcSlots));
return;
}
@@ -837,8 +845,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
@Override
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
if (scroll == npcAbilitiesScroll && selectedSlotIndex >= 0 && selectedSlotIndex < npcSlots.size()) {
- // Double-click mirrors Edit button logic
- buttonEvent(new GuiNpcButton(BTN_EDIT, 0, 0, 0, 0, ""));
+ GuiNpcButton editBtn = getButton(BTN_EDIT);
+ if (editBtn != null && editBtn.enabled) {
+ buttonEvent(new GuiNpcButton(BTN_EDIT, 0, 0, 0, 0, ""));
+ }
}
}
@@ -918,22 +928,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
private void handleChainConfigClosed() {
if (pendingChain != null) {
- boolean found = false;
- for (int i = 0; i < npcSlots.size(); i++) {
- AbilityAction slot = npcSlots.get(i);
- if (slot.isInlineChain() && slot.getInlineChain() == pendingChain) {
- found = true;
- break;
- }
- }
- if (!found) {
+ if (pendingChainSlotIdx < 0) {
+ // New chain — add to NPC slots
npcSlots.add(AbilityAction.inlineChain(pendingChain));
selectedSlotIndex = npcSlots.size() - 1;
selectedEntryIndex = -1;
selectedAbilityIndex = selectedSlotIndex;
if (npcAbilitiesScroll != null) npcAbilitiesScroll.resetScroll();
}
+ // Existing slot (pendingChainSlotIdx >= 0): already updated in-place, nothing to add
pendingChain = null;
+ pendingChainSlotIdx = -1;
}
updateNpcAbilitiesList();
save();
@@ -953,6 +958,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
ChainedAbility chain = slot.getInlineChain();
if (chain != null) {
pendingChain = chain;
+ pendingChainSlotIdx = selectedSlotIndex;
setSubGui(new SubGuiChainedAbilityConfig(chain, this, true, npcSlots));
}
updateNpcAbilitiesList();
@@ -962,6 +968,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
ChainedAbility chain = slot.getChainedAbility();
if (chain != null) {
pendingChain = chain;
+ pendingChainSlotIdx = selectedSlotIndex;
setSubGui(new SubGuiChainedAbilityConfig(chain, this, true, npcSlots));
}
}
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 aa2df26f9..ba3bfb955 100644
--- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java
+++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java
@@ -3,11 +3,14 @@ package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityAction;
+import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.ChainedAbility;
import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry;
import kamkeel.npcs.controllers.data.ability.Condition;
import kamkeel.npcs.controllers.data.ability.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.UserType;
+import kamkeel.npcs.network.PacketClient;
+import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.StatCollector;
@@ -79,6 +82,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
private final ChainedAbility chain;
private final IChainedAbilityConfigCallback callback;
private final boolean npcContext;
+ private final boolean readOnlyEntries;
private final List npcSlots;
private int activeTab = TAB_GENERAL;
@@ -94,6 +98,8 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
// Entry editing
private int editingEntryIndex = -1;
+ private String pendingTypeId;
+ private boolean editingParentAbility = false;
// Condition editing
private int editingConditionIndex = -1;
@@ -105,14 +111,20 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
private final List consumedSlotIndices = new ArrayList<>();
public SubGuiChainedAbilityConfig(ChainedAbility chain, IChainedAbilityConfigCallback callback) {
- this(chain, callback, false, null);
+ this(chain, callback, false, null, false);
}
public SubGuiChainedAbilityConfig(ChainedAbility chain, IChainedAbilityConfigCallback callback,
boolean npcContext, List npcSlots) {
+ this(chain, callback, npcContext, npcSlots, false);
+ }
+
+ public SubGuiChainedAbilityConfig(ChainedAbility chain, IChainedAbilityConfigCallback callback,
+ boolean npcContext, List npcSlots, boolean readOnlyEntries) {
this.chain = chain;
this.callback = callback;
this.npcContext = npcContext;
+ this.readOnlyEntries = readOnlyEntries;
this.npcSlots = npcSlots;
this.entries = new ArrayList<>(chain.getEntries());
this.conditions = new ArrayList<>(chain.getConditions());
@@ -129,6 +141,11 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
fieldDefs.add(FieldDef.stringField("gui.name", chain::getName, chain::setName).tab("General"));
fieldDefs.add(FieldDef.stringField("gui.displayName", chain::getRawDisplayName, chain::setDisplayName).tab("General"));
+ // Computed "Valid For" label (derived from child abilities)
+ fieldDefs.add(FieldDef.labelField("ability.validFor", () -> {
+ UserType ut = computeAllowedBy();
+ return StatCollector.translateToLocal("ability.validFor") + ": \u00A7e" + StatCollector.translateToLocal("ability.userType." + ut.name());
+ }).tab("General"));
fieldDefs.add(FieldDef.boolField("gui.enabled", chain::isEnabled, chain::setEnabled).tab("General"));
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"));
@@ -140,12 +157,6 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
FieldDef.floatField("ability.maxRange", chain::getMaxRange, chain::setMaxRange).range(0, 64)
).tab("Target"));
- // Computed "Valid For" label (derived from child abilities)
- fieldDefs.add(FieldDef.labelField("ability.validFor", () -> {
- UserType ut = computeAllowedBy();
- 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) {
for (IChainedAbilityFieldProvider provider : AbilityController.Instance.getChainedFieldProviders()) {
@@ -307,34 +318,46 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
btnLabel = "\u00A7e> " + (resolved != null ? resolved.getDisplayName() : refName);
}
}
- sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE, L_LABEL_X + 15, y, 140, 20, btnLabel));
-
- // Delay label + field
- sw.addLabel(new GuiNpcLabel(labelCounter++, "ability.delay", L_LABEL_X + 160, y + 5, 0xAAAAAA));
- GuiNpcTextField delayField = new GuiNpcTextField(ENTRY_BASE + i * ENTRY_STRIDE + 1, this, fontRendererObj,
- L_LABEL_X + 195, y, 40, 20, String.valueOf(entry.getDelayTicks()));
- delayField.setIntegersOnly();
- delayField.setMinMaxDefault(0, 6000, 0);
- sw.addTextField(delayField);
-
- // Up button
- if (i > 0) {
- sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 2, L_LABEL_X + 240, y, 20, 20, "\u2191"));
+ GuiNpcButton entryBtn = new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE, L_LABEL_X + 15, y, 140, 20, btnLabel);
+ if (readOnlyEntries) {
+ entryBtn.setEnabled(false);
}
+ sw.addButton(entryBtn);
- // Down button
- if (i < entries.size() - 1) {
- sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 3, L_LABEL_X + 263, y, 20, 20, "\u2193"));
+ if (!readOnlyEntries) {
+ // Delay label + field
+ sw.addLabel(new GuiNpcLabel(labelCounter++, "ability.delay", L_LABEL_X + 160, y + 5, 0xAAAAAA));
+ GuiNpcTextField delayField = new GuiNpcTextField(ENTRY_BASE + i * ENTRY_STRIDE + 1, this, fontRendererObj,
+ L_LABEL_X + 195, y, 40, 20, String.valueOf(entry.getDelayTicks()));
+ delayField.setIntegersOnly();
+ delayField.setMinMaxDefault(0, 6000, 0);
+ sw.addTextField(delayField);
+
+ // Up button
+ if (i > 0) {
+ sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 2, L_LABEL_X + 240, y, 20, 20, "\u2191"));
+ }
+
+ // Down button
+ if (i < entries.size() - 1) {
+ sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 3, L_LABEL_X + 263, y, 20, 20, "\u2193"));
+ }
+
+ // Delete button
+ sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 4, L_LABEL_X + 288, y, 20, 20, "X"));
+ } else {
+ // Read-only: show delay as label only
+ int delay = entry.getDelayTicks();
+ if (delay > 0) {
+ sw.addLabel(new GuiNpcLabel(labelCounter++, delay + "t", L_LABEL_X + 165, y + 5, 0x888888));
+ }
}
- // Delete button
- sw.addButton(new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 4, L_LABEL_X + 288, y, 20, 20, "X"));
-
y += ROW_H;
}
- // Add entry button
- if (entries.size() < MAX_ENTRIES) {
+ // Add entry button (hidden in read-only mode)
+ if (!readOnlyEntries && entries.size() < MAX_ENTRIES) {
sw.addButton(new GuiNpcButton(BTN_ADD_ENTRY, L_LABEL_X, y, 80, 20, "gui.add"));
y += ROW_H;
}
@@ -512,11 +535,11 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
if (entryIndex < 0 || entryIndex >= entries.size()) return false;
switch (action) {
- case 0: // Entry name button — reference: select ability; inline: edit config
+ case 0: // Entry name button — reference: clone/modify dialog; inline: edit config
ChainedAbilityEntry clickedEntry = entries.get(entryIndex);
if (clickedEntry.isReference()) {
editingEntryIndex = entryIndex;
- setSubGui(new SubGuiAbilitySelect());
+ setSubGui(new SubGuiAbilityEditMode());
} else if (clickedEntry.isInline() && clickedEntry.getInlineAbility() != null) {
editingEntryIndex = entryIndex;
setSubGui(new SubGuiAbilityConfig(clickedEntry.getInlineAbility(), this));
@@ -626,13 +649,24 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
initGui();
return;
}
+ if (subgui instanceof SubGuiAbilityVariantSelect) {
+ handleVariantSelectClosed((SubGuiAbilityVariantSelect) subgui);
+ if (!hasSubGui()) initGui();
+ return;
+ }
if (subgui instanceof SubGuiAbilityTypeSelect) {
handleTypeSelectClosed((SubGuiAbilityTypeSelect) subgui);
if (!hasSubGui()) initGui();
return;
}
+ if (subgui instanceof SubGuiAbilityEditMode) {
+ handleEditModeClosed((SubGuiAbilityEditMode) subgui);
+ if (!hasSubGui()) initGui();
+ return;
+ }
if (subgui instanceof SubGuiAbilityConfig) {
editingEntryIndex = -1;
+ editingParentAbility = false;
initGui();
return;
}
@@ -687,8 +721,17 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
private void handleTypeSelectClosed(SubGuiAbilityTypeSelect gui) {
String typeId = gui.getSelectedTypeId();
if (typeId != null) {
+ 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(java.util.UUID.randomUUID().toString());
entries.add(ChainedAbilityEntry.inline(newAbility, 0));
editingEntryIndex = entries.size() - 1;
@@ -697,6 +740,53 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
}
}
+ private void handleVariantSelectClosed(SubGuiAbilityVariantSelect gui) {
+ int idx = gui.getSelectedIndex();
+ if (idx >= 0 && pendingTypeId != null) {
+ Ability newAbility = AbilityController.Instance.create(pendingTypeId);
+ if (newAbility != null) {
+ gui.getVariants().get(idx).apply(newAbility);
+ newAbility.setId(java.util.UUID.randomUUID().toString());
+ entries.add(ChainedAbilityEntry.inline(newAbility, 0));
+ editingEntryIndex = entries.size() - 1;
+ pendingTypeId = null;
+ setSubGui(new SubGuiAbilityConfig(newAbility, this));
+ return;
+ }
+ }
+ pendingTypeId = null;
+ }
+
+ private void handleEditModeClosed(SubGuiAbilityEditMode gui) {
+ int mode = gui.getResult();
+ if (mode < 0 || editingEntryIndex < 0 || editingEntryIndex >= entries.size()) {
+ editingEntryIndex = -1;
+ return;
+ }
+
+ ChainedAbilityEntry entry = entries.get(editingEntryIndex);
+
+ if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) {
+ if (entry.convertToInline()) {
+ Ability a = entry.getInlineAbility();
+ if (a != null) {
+ a.setId(java.util.UUID.randomUUID().toString());
+ setSubGui(new SubGuiAbilityConfig(a, this));
+ return;
+ }
+ }
+ editingEntryIndex = -1;
+ } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) {
+ Ability resolved = entry.resolve();
+ if (resolved != null) {
+ editingParentAbility = true;
+ setSubGui(resolved.createConfigGui(this));
+ return;
+ }
+ editingEntryIndex = -1;
+ }
+ }
+
private void handleConditionEditClosed(SubGuiConditionEdit gui) {
Condition result = gui.getResult();
if (result != null && editingConditionIndex >= 0) {
@@ -715,6 +805,12 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText
@Override
public void onAbilitySaved(Ability ability) {
+ if (editingParentAbility) {
+ // Persist the global parent ability to the server
+ PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
+ editingParentAbility = false;
+ return;
+ }
// Inline ability mutated in-place; entry already holds the reference
}