package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.Condition; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.select.GuiAnimationSelection; import noppes.npcs.client.gui.select.GuiSoundSelection; import noppes.npcs.client.gui.util.*; import java.util.ArrayList; import java.util.List; /** * Base SubGui for editing ability configuration with tabbed interface. * Uses GuiMenuTopButton for navigation tabs. * * Tabs: * - General: Name, Weight, Lock Movement, Interruptible, Timing * - Type: Type-specific settings per Ability (override in subclasses) * - Target: Min/Max Range, Targeting Mode, Conditions (up to 3) * - Effects: Sounds, Animations, Telegraph settings, Colors * * Subclasses should override initTypeTab() and handleTypeButton()/handleTypeTextField() * to provide type-specific settings UI. */ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldListener, ISubGuiListener { // Tab constants protected static final int TAB_GENERAL = 0; protected static final int TAB_TYPE = 1; protected static final int TAB_TARGET = 2; protected static final int TAB_EFFECTS = 3; // Core references protected final Ability ability; protected final IAbilityConfigCallback callback; protected int activeTab = TAB_GENERAL; // ═══════════════════════════════════════════════════════════════════════════ // CACHED VALUES - General Tab // ═══════════════════════════════════════════════════════════════════════════ protected String name; protected boolean enabled; protected int weight; protected boolean lockMovement; protected boolean interruptible; // Timing protected int cooldownTicks; protected int windUpTicks; protected int activeTicks; protected int recoveryTicks; // ═══════════════════════════════════════════════════════════════════════════ // CACHED VALUES - Target Tab // ═══════════════════════════════════════════════════════════════════════════ protected float minRange; protected float maxRange; protected TargetingMode targetingMode; protected List conditions; // ═══════════════════════════════════════════════════════════════════════════ // CACHED VALUES - Effects Tab // ═══════════════════════════════════════════════════════════════════════════ protected String windUpSound; protected String activeSound; protected int windUpAnimationId; protected int activeAnimationId; protected String windUpAnimationName = null; protected String activeAnimationName = null; // Telegraph protected boolean showTelegraph; protected int windUpColor; protected int activeColor; // Editing state protected int editingColorId = 0; protected int editingSoundId = 0; protected int editingAnimationId = 0; protected int editingConditionIndex = -1; // ═══════════════════════════════════════════════════════════════════════════ // ABILITY TYPE FEATURES // ═══════════════════════════════════════════════════════════════════════════ protected final boolean supportsTelegraph; protected final boolean targetingModeLocked; protected final boolean hasTypeSettings; public SubGuiAbilityConfig(Ability ability, IAbilityConfigCallback callback) { this.ability = ability; this.callback = callback; // Cache values from ability - General this.name = ability.getName() != null ? ability.getName() : ""; this.enabled = ability.isEnabled(); this.weight = ability.getWeight(); this.lockMovement = ability.isLockMovement(); this.interruptible = ability.isInterruptible(); // Timing this.cooldownTicks = ability.getCooldownTicks(); this.windUpTicks = ability.getWindUpTicks(); this.activeTicks = ability.getActiveTicks(); this.recoveryTicks = ability.getRecoveryTicks(); // Target this.minRange = ability.getMinRange(); this.maxRange = ability.getMaxRange(); this.targetingMode = ability.getTargetingMode(); this.conditions = new ArrayList<>(ability.getConditions()); // Effects this.windUpSound = ability.getWindUpSound(); this.activeSound = ability.getActiveSound(); this.windUpAnimationId = ability.getWindUpAnimationId(); this.activeAnimationId = ability.getActiveAnimationId(); // Telegraph this.showTelegraph = ability.isShowTelegraph(); this.windUpColor = ability.getWindUpColor(); this.activeColor = ability.getActiveColor(); // Determine ability type features TelegraphType defaultType = ability.getTelegraphType(); this.supportsTelegraph = defaultType != null && defaultType != TelegraphType.NONE; this.targetingModeLocked = ability.isTargetingModeLocked(); this.hasTypeSettings = ability.hasTypeSettings(); setBackground("menubg.png"); xSize = 356; ySize = 200; } @Override public void initGui() { super.initGui(); // ═══════════════════════════════════════════════════════════════════════ // TOP TABS using GuiMenuTopButton // ═══════════════════════════════════════════════════════════════════════ GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "menu.general"); generalTab.active = (activeTab == TAB_GENERAL); addTopButton(generalTab); GuiMenuTopButton lastTab = generalTab; if (hasTypeSettings) { GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type"); typeTab.active = (activeTab == TAB_TYPE); addTopButton(typeTab); lastTab = typeTab; } GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "script.target"); targetTab.active = (activeTab == TAB_TARGET); addTopButton(targetTab); lastTab = targetTab; GuiMenuTopButton effectsTab = new GuiMenuTopButton(93, lastTab, "ability.tab.effects"); effectsTab.active = (activeTab == TAB_EFFECTS); addTopButton(effectsTab); // Close button (X) in top menu bar - use -1000 to avoid any ID conflicts GuiMenuTopButton closeBtn = new GuiMenuTopButton(-1000, guiLeft + xSize - 22, guiTop - 17, "X"); addTopButton(closeBtn); int contentY = guiTop + 5; // ═══════════════════════════════════════════════════════════════════════ // TAB CONTENT // ═══════════════════════════════════════════════════════════════════════ switch (activeTab) { case TAB_GENERAL: initGeneralTab(contentY); break; case TAB_TYPE: initTypeTab(contentY); break; case TAB_TARGET: initTargetTab(contentY); break; case TAB_EFFECTS: initEffectsTab(contentY); break; } // ═══════════════════════════════════════════════════════════════════════ // BOTTOM ROW - Enabled toggle // ═══════════════════════════════════════════════════════════════════════ addLabel(new GuiNpcLabel(99, "gui.enabled", guiLeft + 8, guiTop + ySize - 18)); GuiNpcButton enabledBtn = new GuiNpcButton(2, guiLeft + 55, guiTop + ySize - 24, 40, 20, new String[]{"gui.no", "gui.yes"}, enabled ? 1 : 0); enabledBtn.setTextColor(enabled ? 0x00FF00 : 0xFF0000); addButton(enabledBtn); } // ═══════════════════════════════════════════════════════════════════════════ // GENERAL TAB // ═══════════════════════════════════════════════════════════════════════════ private void initGeneralTab(int startY) { int y = startY; int col1LabelX = guiLeft + 8; int col1FieldX = guiLeft + 70; int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 248; // Row 1: Type (read-only) + Name addLabel(new GuiNpcLabel(1, "gui.type", col1LabelX, y + 5)); addLabel(new GuiNpcLabel(2, ability.getTypeId(), col1FieldX, y + 5)); addLabel(new GuiNpcLabel(3, "gui.name", col2LabelX, y + 5)); GuiNpcTextField nameField = new GuiNpcTextField(1, this, fontRendererObj, col2FieldX, y, 100, 20, name); addTextField(nameField); y += 24; // Row 2: Weight + Lock Movement addLabel(new GuiNpcLabel(4, "ability.weight", col1LabelX, y + 5)); GuiNpcTextField weightField = new GuiNpcTextField(3, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(weight)); weightField.setIntegersOnly(); weightField.setMinMaxDefault(1, 1000, 10); addTextField(weightField); addLabel(new GuiNpcLabel(5, "ability.lockMove", col2LabelX, y + 5)); addButton(new GuiNpcButton(16, col2FieldX, y, 40, 20, new String[]{"gui.no", "gui.yes"}, lockMovement ? 1 : 0)); y += 24; // Row 3: Interruptible addLabel(new GuiNpcLabel(6, "ability.interruptible", col1LabelX, y + 5)); addButton(new GuiNpcButton(14, guiLeft + 85, y, 40, 20, new String[]{"gui.no", "gui.yes"}, interruptible ? 1 : 0)); y += 28; // Separator - Timing section header addLabel(new GuiNpcLabel(7, "ability.timing", col1LabelX, y)); y += 14; // Row 4: Windup Ticks + Active Ticks addLabel(new GuiNpcLabel(10, "ability.windup", col1LabelX, y + 5)); GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks)); windupField.setIntegersOnly(); windupField.setMinMaxDefault(0, 1000, 20); addTextField(windupField); addLabel(new GuiNpcLabel(11, "ability.active", col2LabelX, y + 5)); GuiNpcTextField activeField = new GuiNpcTextField(12, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(activeTicks)); activeField.setIntegersOnly(); activeField.setMinMaxDefault(1, 1000, 10); addTextField(activeField); y += 24; // Row 5: Recovery Ticks + Cooldown Ticks addLabel(new GuiNpcLabel(12, "ability.recovery", col1LabelX, y + 5)); GuiNpcTextField recoveryField = new GuiNpcTextField(13, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(recoveryTicks)); recoveryField.setIntegersOnly(); recoveryField.setMinMaxDefault(0, 1000, 20); addTextField(recoveryField); addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5)); GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks)); cooldownField.setIntegersOnly(); cooldownField.setMinMaxDefault(0, 10000, 100); addTextField(cooldownField); } // ═══════════════════════════════════════════════════════════════════════════ // TYPE TAB - Override in subclasses for type-specific settings // ═══════════════════════════════════════════════════════════════════════════ /** * Override this method to render type-specific settings in the Type tab. * Use the helper methods createFloatField(), createIntField(), etc. * Button/TextField IDs should start at 100. * * @param startY The Y position to start rendering from */ protected void initTypeTab(int startY) { // Default: show message that no type settings are available addLabel(new GuiNpcLabel(100, "ability.noTypeSettings", guiLeft + 8, startY + 5)); } /** * Override this method to handle button clicks for type-specific buttons. * Button IDs 100+ are reserved for type-specific use. */ protected void handleTypeButton(int id, GuiNpcButton button) { // Default: no-op } /** * Override this method to handle text field changes for type-specific fields. * Field IDs 100+ are reserved for type-specific use. */ protected void handleTypeTextField(int id, GuiNpcTextField field) { // Default: no-op } // ═══════════════════════════════════════════════════════════════════════════ // TARGET TAB // ═══════════════════════════════════════════════════════════════════════════ private void initTargetTab(int startY) { int y = startY; int col1LabelX = guiLeft + 8; int col1FieldX = guiLeft + 70; int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 248; // Row 1: Min Range + Max Range addLabel(new GuiNpcLabel(20, "ability.minRange", col1LabelX, y + 5)); GuiNpcTextField minRangeField = new GuiNpcTextField(5, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf((int) minRange)); minRangeField.setIntegersOnly(); minRangeField.setMinMaxDefault(0, 100, 0); addTextField(minRangeField); addLabel(new GuiNpcLabel(21, "ability.maxRange", col2LabelX, y + 5)); GuiNpcTextField maxRangeField = new GuiNpcTextField(6, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf((int) maxRange)); maxRangeField.setIntegersOnly(); maxRangeField.setMinMaxDefault(1, 100, 20); addTextField(maxRangeField); y += 24; // Row 2: Targeting Mode (if not locked) addLabel(new GuiNpcLabel(22, "ability.targeting", col1LabelX, y + 5)); if (!targetingModeLocked) { String[] targetingModes = getAvailableTargetingModes(); int selectedIndex = getTargetingModeIndex(targetingMode); addButton(new GuiNpcButton(4, col1FieldX, y, 90, 20, targetingModes, selectedIndex)); } else { String modeName = "ability.target." + targetingMode.name().toLowerCase(); addLabel(new GuiNpcLabel(23, modeName, col1FieldX, y + 5)); } y += 28; // ───────────────────────────────────────────────────────────────────── // CONDITIONS SECTION (up to 3) // ───────────────────────────────────────────────────────────────────── addLabel(new GuiNpcLabel(24, "ability.conditions", col1LabelX, y)); y += 14; // Display existing conditions (similar to Marks pattern) for (int i = 0; i < conditions.size() && i < 3; i++) { Condition cond = conditions.get(i); String condName = getConditionDisplayName(cond); // Condition type button addButton(new GuiNpcButton(50 + i * 10, col1LabelX, y, 140, 20, condName)); // Edit button addButton(new GuiNpcButton(51 + i * 10, col1LabelX + 145, y, 40, 20, "gui.edit")); // Delete button addButton(new GuiNpcButton(52 + i * 10, col1LabelX + 190, y, 20, 20, "X")); y += 22; } // Add button (if less than 3 conditions) if (conditions.size() < 3) { addButton(new GuiNpcButton(80, col1LabelX, y, 50, 20, "gui.add")); } } private String getConditionDisplayName(Condition cond) { if (cond == null) return "None"; String typeId = cond.getTypeId(); switch (typeId) { case "hp_above": return StatCollector.translateToLocal("condition.hp_above") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; case "hp_below": return StatCollector.translateToLocal("condition.hp_below") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; case "target_hp_above": return StatCollector.translateToLocal("condition.target_hp_above") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; case "target_hp_below": return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; case "hit_count": Condition.ConditionHitCount hitCount = (Condition.ConditionHitCount) cond; return StatCollector.translateToLocal("condition.hit_count") + ": " + hitCount.getRequiredHits() + "/" + hitCount.getWithinTicks() + "t"; default: return typeId; } } private float getConditionThreshold(Condition cond) { // Read threshold from NBT since inner classes don't expose it NBTTagCompound nbt = cond.writeNBT(); return nbt.hasKey("threshold") ? nbt.getFloat("threshold") : 0.5f; } private String[] getAvailableTargetingModes() { TargetingMode[] modes = ability.getAllowedTargetingModes(); if (modes == null) { modes = TargetingMode.values(); } String[] result = new String[modes.length]; for (int i = 0; i < modes.length; i++) { result[i] = "ability.target." + modes[i].name().toLowerCase(); } return result; } private int getTargetingModeIndex(TargetingMode mode) { TargetingMode[] modes = ability.getAllowedTargetingModes(); if (modes == null) { return mode.ordinal(); } for (int i = 0; i < modes.length; i++) { if (modes[i] == mode) return i; } return 0; } // ═══════════════════════════════════════════════════════════════════════════ // EFFECTS TAB // ═══════════════════════════════════════════════════════════════════════════ private void initEffectsTab(int startY) { int y = startY; int col1LabelX = guiLeft + 8; int col1FieldX = guiLeft + 85; int col1ClearX = guiLeft + 160; int col2LabelX = guiLeft + 185; int col2FieldX = guiLeft + 255; // ───────────────────────────────────────────────────────────────────── // SOUNDS SECTION // ───────────────────────────────────────────────────────────────────── addLabel(new GuiNpcLabel(30, "advanced.sounds", col1LabelX, y)); y += 14; // Wind Up Sound addLabel(new GuiNpcLabel(31, "ability.windUpSound", col1LabelX, y + 5)); String windUpSoundDisplay = windUpSound != null && !windUpSound.isEmpty() ? truncateString(windUpSound, 10) : StatCollector.translateToLocal("gui.none"); addButton(new GuiNpcButton(30, col1FieldX, y, 70, 20, windUpSoundDisplay)); addButton(new GuiNpcButton(35, col1ClearX, y, 20, 20, "X")); // Active Sound addLabel(new GuiNpcLabel(32, "ability.activeSound", col2LabelX, y + 5)); String activeSoundDisplay = activeSound != null && !activeSound.isEmpty() ? truncateString(activeSound, 10) : StatCollector.translateToLocal("gui.none"); addButton(new GuiNpcButton(31, col2FieldX, y, 70, 20, activeSoundDisplay)); addButton(new GuiNpcButton(36, col2FieldX + 75, y, 20, 20, "X")); y += 26; // ───────────────────────────────────────────────────────────────────── // ANIMATIONS SECTION // ───────────────────────────────────────────────────────────────────── addLabel(new GuiNpcLabel(33, "menu.animations", col1LabelX, y)); y += 14; // Wind Up Animation addLabel(new GuiNpcLabel(34, "ability.windUpAnim", col1LabelX, y + 5)); String windUpAnimName = getAnimationName(windUpAnimationId, true); addButton(new GuiNpcButton(32, col1FieldX, y, 70, 20, windUpAnimName)); addButton(new GuiNpcButton(37, col1ClearX, y, 20, 20, "X")); // Active Animation addLabel(new GuiNpcLabel(35, "ability.activeAnim", col2LabelX, y + 5)); String activeAnimName = getAnimationName(activeAnimationId, false); addButton(new GuiNpcButton(33, col2FieldX, y, 70, 20, activeAnimName)); addButton(new GuiNpcButton(38, col2FieldX + 75, y, 20, 20, "X")); y += 26; // ───────────────────────────────────────────────────────────────────── // TELEGRAPH SECTION (only if supported) // ───────────────────────────────────────────────────────────────────── if (supportsTelegraph) { addLabel(new GuiNpcLabel(40, "ability.telegraph", col1LabelX, y)); y += 14; // Show Telegraph addLabel(new GuiNpcLabel(41, "ability.showTelegraph", col1LabelX, y + 5)); addButton(new GuiNpcButton(20, guiLeft + 95, y, 40, 20, new String[]{"gui.no", "gui.yes"}, showTelegraph ? 1 : 0)); // Telegraph Type (separate column) String typeKey = ability.getTelegraphType().name().toLowerCase(); addLabel(new GuiNpcLabel(42, "ability.telegraphType", guiLeft + 200, y + 5)); String telegraphLangKey = typeKey.equals("none") ? "gui.none" : "telegraph." + typeKey; addLabel(new GuiNpcLabel(43, telegraphLangKey, guiLeft + 280, y + 5)); y += 24; // Colors addLabel(new GuiNpcLabel(44, "ability.windUpColor", col1LabelX, y + 5)); String windUpHex = String.format("%06X", windUpColor & 0xFFFFFF); GuiNpcButton windUpColorBtn = new GuiNpcButton(22, guiLeft + 95, y, 55, 20, windUpHex); windUpColorBtn.setEnabled(showTelegraph); windUpColorBtn.setTextColor(windUpColor & 0xFFFFFF); addButton(windUpColorBtn); addLabel(new GuiNpcLabel(45, "ability.activeColor", guiLeft + 200, y + 5)); String activeHex = String.format("%06X", activeColor & 0xFFFFFF); GuiNpcButton activeColorBtn = new GuiNpcButton(24, guiLeft + 280, y, 55, 20, activeHex); activeColorBtn.setEnabled(showTelegraph); activeColorBtn.setTextColor(activeColor & 0xFFFFFF); addButton(activeColorBtn); } } private String truncateString(String str, int maxLen) { if (str == null) return ""; if (str.length() <= maxLen) return str; int lastSlash = str.lastIndexOf('/'); if (lastSlash >= 0 && str.length() - lastSlash <= maxLen) { return "..." + str.substring(lastSlash); } return "..." + str.substring(str.length() - maxLen + 3); } private String getAnimationName(int animId, boolean isWindUp) { if (animId < 0) return StatCollector.translateToLocal("gui.none"); String cachedName = isWindUp ? windUpAnimationName : activeAnimationName; if (cachedName != null && !cachedName.isEmpty()) { return truncateString(cachedName, 8); } return "ID: " + animId; } // ═══════════════════════════════════════════════════════════════════════════ // BUTTON EVENTS // ═══════════════════════════════════════════════════════════════════════════ @Override public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; // Tab switching if (id == 90) { activeTab = TAB_GENERAL; initGui(); return; } if (id == 91 && hasTypeSettings) { activeTab = TAB_TYPE; initGui(); return; } if (id == 92) { activeTab = TAB_TARGET; initGui(); return; } if (id == 93) { activeTab = TAB_EFFECTS; initGui(); return; } // Enabled button (bottom left) if (id == 2) { enabled = ((GuiNpcButton) guibutton).getValue() == 1; initGui(); } // General tab buttons else if (id == 14) { interruptible = ((GuiNpcButton) guibutton).getValue() == 1; } else if (id == 16) { lockMovement = ((GuiNpcButton) guibutton).getValue() == 1; } // Target tab - Targeting mode else if (id == 4) { TargetingMode[] modes = ability.getAllowedTargetingModes(); int idx = ((GuiNpcButton) guibutton).getValue(); if (modes != null && idx < modes.length) { targetingMode = modes[idx]; } else { targetingMode = TargetingMode.values()[idx]; } } // Close button (X in top menu) - use -1000 to avoid any ID conflicts else if (id == -1000) { close(); return; } // Target tab - Condition buttons (50-79) else if (id >= 50 && id < 80) { int condIndex = (id - 50) / 10; int action = (id - 50) % 10; if (action == 0 || action == 1) { // Click on condition name or Edit button - open edit subgui if (condIndex < conditions.size()) { editingConditionIndex = condIndex; setSubGui(new SubGuiConditionEdit(conditions.get(condIndex))); } } else if (action == 2) { // Delete button if (condIndex < conditions.size()) { conditions.remove(condIndex); initGui(); } } } // Add condition button else if (id == 80) { if (conditions.size() < 3) { editingConditionIndex = conditions.size(); setSubGui(new SubGuiConditionEdit(null)); } } // Effects tab - Telegraph else if (id == 20) { showTelegraph = ((GuiNpcButton) guibutton).getValue() == 1; initGui(); } else if (id == 22) { editingColorId = 22; setSubGui(new SubGuiColorSelector(windUpColor)); } else if (id == 24) { editingColorId = 24; setSubGui(new SubGuiColorSelector(activeColor)); } // Effects tab - Sound buttons else if (id == 30) { editingSoundId = 30; setSubGui(new GuiSoundSelection(windUpSound)); } else if (id == 31) { editingSoundId = 31; setSubGui(new GuiSoundSelection(activeSound)); } // Sound clear buttons else if (id == 35) { windUpSound = ""; initGui(); } else if (id == 36) { activeSound = ""; initGui(); } // Animation buttons else if (id == 32) { editingAnimationId = 32; setSubGui(new GuiAnimationSelection(windUpAnimationId)); } else if (id == 33) { editingAnimationId = 33; setSubGui(new GuiAnimationSelection(activeAnimationId)); } // Animation clear buttons else if (id == 37) { windUpAnimationId = -1; windUpAnimationName = null; initGui(); } else if (id == 38) { activeAnimationId = -1; activeAnimationName = null; initGui(); } // Type-specific buttons (100+) - delegate to subclass else if (id >= 100) { handleTypeButton(id, (GuiNpcButton) guibutton); } } // ═══════════════════════════════════════════════════════════════════════════ // TEXT FIELD EVENTS // ═══════════════════════════════════════════════════════════════════════════ @Override public void unFocused(GuiNpcTextField textField) { int id = textField.id; // General tab if (id == 1) { name = textField.getText(); } else if (id == 3) { weight = textField.getInteger(); } // Target tab else if (id == 5) { minRange = textField.getInteger(); } else if (id == 6) { maxRange = textField.getInteger(); } // Timing (General tab) else if (id == 10) { cooldownTicks = textField.getInteger(); } else if (id == 11) { windUpTicks = textField.getInteger(); } else if (id == 12) { activeTicks = textField.getInteger(); } else if (id == 13) { recoveryTicks = textField.getInteger(); } // Type-specific fields (100+) - delegate to subclass else if (id >= 100) { handleTypeTextField(id, textField); } } // ═══════════════════════════════════════════════════════════════════════════ // SUBGUI CLOSED HANDLER // ═══════════════════════════════════════════════════════════════════════════ @Override public void subGuiClosed(SubGuiInterface subgui) { if (subgui instanceof SubGuiColorSelector) { SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; int rgb = colorSelector.color & 0x00FFFFFF; if (editingColorId == 22) { windUpColor = 0x80000000 | rgb; } else if (editingColorId == 24) { activeColor = 0xC0000000 | rgb; } editingColorId = 0; initGui(); } else if (subgui instanceof GuiSoundSelection) { GuiSoundSelection soundSelector = (GuiSoundSelection) subgui; if (soundSelector.selectedResource != null) { String sound = soundSelector.selectedResource.toString(); if (editingSoundId == 30) { windUpSound = sound; } else if (editingSoundId == 31) { activeSound = sound; } } editingSoundId = 0; initGui(); } else if (subgui instanceof GuiAnimationSelection) { GuiAnimationSelection animSelector = (GuiAnimationSelection) subgui; if (editingAnimationId == 32) { windUpAnimationId = animSelector.selectedAnimationId; windUpAnimationName = animSelector.getSelectedName(); } else if (editingAnimationId == 33) { activeAnimationId = animSelector.selectedAnimationId; activeAnimationName = animSelector.getSelectedName(); } editingAnimationId = 0; initGui(); } else if (subgui instanceof SubGuiConditionEdit) { SubGuiConditionEdit condEdit = (SubGuiConditionEdit) subgui; Condition result = condEdit.getResult(); if (result != null && editingConditionIndex >= 0) { if (editingConditionIndex < conditions.size()) { conditions.set(editingConditionIndex, result); } else { conditions.add(result); } } editingConditionIndex = -1; initGui(); } } // ═══════════════════════════════════════════════════════════════════════════ // CLOSE - Save before closing // ═══════════════════════════════════════════════════════════════════════════ @Override public void close() { // Apply all cached values to the ability before closing applyToAbility(); // Notify callback that ability was saved callback.onAbilitySaved(ability); // Close the GUI super.close(); } // ═══════════════════════════════════════════════════════════════════════════ // APPLY TO ABILITY // ═══════════════════════════════════════════════════════════════════════════ protected void applyToAbility() { // General ability.setName(name); ability.setEnabled(enabled); ability.setWeight(weight); ability.setLockMovement(lockMovement); ability.setInterruptible(interruptible); // Timing ability.setCooldownTicks(cooldownTicks); ability.setWindUpTicks(windUpTicks); ability.setActiveTicks(activeTicks); ability.setRecoveryTicks(recoveryTicks); // Target ability.setMinRange(minRange); ability.setMaxRange(maxRange); ability.setTargetingMode(targetingMode); // Conditions ability.getConditions().clear(); for (Condition c : conditions) { ability.addCondition(c); } // Effects ability.setWindUpSound(windUpSound); ability.setActiveSound(activeSound); ability.setWindUpAnimationId(windUpAnimationId); ability.setActiveAnimationId(activeAnimationId); // Telegraph ability.setShowTelegraph(showTelegraph); ability.setWindUpColor(windUpColor); ability.setActiveColor(activeColor); // Type-specific values are applied directly in handleTypeTextField and handleTypeButton } /** * Called when an ability is loaded from saved presets. */ public void loadAbility(Ability loadedAbility) { if (loadedAbility == null) return; // Copy all values from loaded ability to current ability NBTTagCompound nbt = loadedAbility.writeNBT(); // Keep the current typeId since we can't change types mid-edit nbt.setString("typeId", ability.getTypeId()); ability.readNBT(nbt); // Update cached values this.name = ability.getName() != null ? ability.getName() : ""; this.enabled = ability.isEnabled(); this.weight = ability.getWeight(); this.lockMovement = ability.isLockMovement(); this.interruptible = ability.isInterruptible(); this.cooldownTicks = ability.getCooldownTicks(); this.windUpTicks = ability.getWindUpTicks(); this.activeTicks = ability.getActiveTicks(); this.recoveryTicks = ability.getRecoveryTicks(); this.minRange = ability.getMinRange(); this.maxRange = ability.getMaxRange(); this.targetingMode = ability.getTargetingMode(); this.conditions = new ArrayList<>(ability.getConditions()); this.showTelegraph = ability.isShowTelegraph(); this.windUpColor = ability.getWindUpColor(); this.activeColor = ability.getActiveColor(); initGui(); } // ═══════════════════════════════════════════════════════════════════════════ // HELPER METHODS FOR SUBCLASSES // ═══════════════════════════════════════════════════════════════════════════ /** * Creates a float text field for type-specific settings. */ protected GuiNpcTextField createFloatField(int id, int x, int y, int width, float value) { GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.format("%.1f", value)); return field; } /** * Creates an integer text field for type-specific settings. */ protected GuiNpcTextField createIntField(int id, int x, int y, int width, int value) { GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.valueOf(value)); field.setIntegersOnly(); return field; } /** * Parses a float from a text field with a fallback default value. */ protected float parseFloat(GuiNpcTextField field, float defaultValue) { try { return Float.parseFloat(field.getText()); } catch (NumberFormatException e) { return defaultValue; } } /** * Gets the ability being edited. */ protected Ability getAbility() { return ability; } }