Fixes to some GUI and Chained Abiltiy Logic for Clearing States

This commit is contained in:
Kam committed 2026-02-18 15:31:33 -05:00
1 parent 48f6c162d5
commit 40a02b73e2
8 files changed
+196 -65

No files matched your search

@@ -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).
* <p>
* 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;
@@ -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;
@@ -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;
@@ -53,6 +53,12 @@ public abstract class AbilityMovement extends Ability {
maxActiveTicks = 0;
}
@Override
public void resetForBurst() {
movementDirection = null;
maxActiveTicks = 0;
}
// ═══════════════════════════════════════════════════════════════════
// DIRECTION HELPERS
// ═══════════════════════════════════════════════════════════════════
@@ -68,6 +68,7 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
this.homingData = homingData;
this.lifespanData = lifespanData;
this.trajectoryData = new EnergyTrajectoryData();
this.burstOverlap = true;
initProjectiles(1);
}
@@ -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);
}
@@ -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));
}
}
@@ -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<AbilityAction> 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<Integer> 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<AbilityAction> npcSlots) {
this(chain, callback, npcContext, npcSlots, false);
}
public SubGuiChainedAbilityConfig(ChainedAbility chain, IChainedAbilityConfigCallback callback,
boolean npcContext, List<AbilityAction> 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<AbilityVariant> 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
}