diff --git a/src/api b/src/api index aac5f98be..ae98d12fa 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit aac5f98be85b2004f61733c1c97bdae26329a714 +Subproject commit ae98d12fafa29f48656f8bc9d2ad998a7fd2c3d3 diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index e2d8451f5..5c96d1f56 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -17,6 +17,7 @@ import kamkeel.npcs.controllers.data.ability.enums.UserType; import kamkeel.npcs.controllers.data.ability.extender.IAbilityExtender; import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider; import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; +import kamkeel.npcs.controllers.data.ability.CustomAbility; import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; import kamkeel.npcs.controllers.data.ability.type.AbilityDash; @@ -37,11 +38,15 @@ import kamkeel.npcs.controllers.data.ability.type.energy.AbilityOrb; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityShield; import kamkeel.npcs.controllers.data.ability.type.energy.AbilitySweeper; import kamkeel.npcs.controllers.data.ability.type.energy.AbilityWall; +import net.minecraftforge.common.util.Constants; +import noppes.npcs.controllers.data.AbilityScript; +import noppes.npcs.controllers.data.ChainedAbilityScript; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; +import noppes.npcs.api.ability.IChainedAbility; import noppes.npcs.api.handler.IAbilityHandler; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.data.PlayerData; @@ -52,7 +57,6 @@ import java.io.File; import java.util.*; import java.util.function.Predicate; import java.util.function.Supplier; -import java.util.regex.Pattern; public class AbilityController implements IAbilityHandler { @@ -64,6 +68,10 @@ public class AbilityController implements IAbilityHandler { private final Map customAbilities = new LinkedHashMap<>(); // name → Ability private final Map customAbilitiesById = new LinkedHashMap<>(); // UUID → Ability + // ── Script Handlers ──────────────────────────────────────────────────── + public final HashMap abilityScriptHandlers = new HashMap<>(); + public final HashMap chainedAbilityScriptHandlers = new HashMap<>(); + // ── Extension Points ───────────────────────────────────────────────────── private final Map> externalVariants = new LinkedHashMap<>(); private final List fieldProviders = new ArrayList<>(); @@ -72,11 +80,6 @@ public class AbilityController implements IAbilityHandler { private final Map> conditionTypes = new HashMap<>(); private final List> flightCheckers = new ArrayList<>(); - // ── Legacy Migration ───────────────────────────────────────────────────── - private static final Pattern UUID_PATTERN = - Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", - Pattern.CASE_INSENSITIVE); - // ── Chained Abilities ────────────────────────────────────────────────── private final Map chainedAbilities = new LinkedHashMap<>(); // name → ChainedAbility private final Map chainedAbilitiesById = new LinkedHashMap<>(); // UUID → ChainedAbility @@ -97,7 +100,7 @@ public class AbilityController implements IAbilityHandler { // TYPE REGISTRATION // ═══════════════════════════════════════════════════════════════════════════ - public void registerType(String factoryKey, Supplier factory) { + public void registerType(Supplier factory) { Ability temp = factory.get(); String typeId = temp.getTypeId(); if (abilityTypes.containsKey(typeId)) { @@ -132,36 +135,35 @@ public class AbilityController implements IAbilityHandler { } private void registerBuiltinTypes() { - registerType("cnpc:slam", AbilitySlam::new); - registerType("cnpc:charge", AbilityCharge::new); + registerType(CustomAbility::new); - registerType("cnpc:heavy_hit", AbilityHeavyHit::new); - registerType("cnpc:cutter", AbilityCutter::new); + registerType(AbilitySlam::new); + registerType(AbilityCharge::new); - // registerType("cnpc:projectile", AbilityProjectile::new); + registerType(AbilityHeavyHit::new); + registerType(AbilityCutter::new); - registerType("cnpc:sweeper", AbilitySweeper::new); - registerType("cnpc:orb", AbilityOrb::new); - registerType("cnpc:disc", AbilityDisc::new); - registerType("cnpc:laser_shot", AbilityLaser::new); - registerType("cnpc:beam", AbilityBeam::new); - // registerType("cnpc:slicer", AbilitySlicer::new); + registerType(AbilitySweeper::new); + registerType(AbilityOrb::new); + registerType(AbilityDisc::new); + registerType(AbilityLaser::new); + registerType(AbilityBeam::new); - registerType("cnpc:dash", AbilityDash::new); - registerType("cnpc:teleport", AbilityTeleport::new); + registerType(AbilityDash::new); + registerType(AbilityTeleport::new); - registerType("cnpc:vortex", AbilityVortex::new); - registerType("cnpc:shockwave", AbilityShockwave::new); + registerType(AbilityVortex::new); + registerType(AbilityShockwave::new); - registerType("cnpc:guard", AbilityGuard::new); - registerType("cnpc:effect", AbilityEffect::new); + registerType(AbilityGuard::new); + registerType(AbilityEffect::new); - registerType("cnpc:hazard", AbilityHazard::new); - registerType("cnpc:trap", AbilityTrap::new); + registerType(AbilityHazard::new); + registerType(AbilityTrap::new); - registerType("cnpc:dome", AbilityDome::new); - registerType("cnpc:wall", AbilityWall::new); - registerType("cnpc:shield", AbilityShield::new); + registerType(AbilityDome::new); + registerType(AbilityWall::new); + registerType(AbilityShield::new); } private void registerBuiltinConditionTypes() { @@ -193,7 +195,7 @@ public class AbilityController implements IAbilityHandler { public Ability getAbilityByDisplayName(String displayName) { for (Ability ability : builtAbilities.values()) { - if (ability.getName().equals(displayName)) { + if (ability.getDisplayName().equals(displayName)) { return ability; } } @@ -214,8 +216,6 @@ public class AbilityController implements IAbilityHandler { public void load() { customAbilities.clear(); customAbilitiesById.clear(); - int migrated = 0; - Map migratedAbilityNames = new HashMap(); File dir = getDir(); File[] files = dir.exists() ? dir.listFiles() : null; @@ -230,59 +230,29 @@ public class AbilityController implements IAbilityHandler { Ability ability = fromNBT(nbt); if (ability == null) continue; - // Skip built-in abilities that were erroneously saved as custom files - if (ability.isBuiltIn()) { - file.delete(); - LogWriter.info("Removed stale built-in ability file: " + filename); - continue; - } - - String storedName = ability.getName(); - String preferredName; - if (UUID_PATTERN.matcher(fileKey).matches()) { - preferredName = (storedName != null && !storedName.isEmpty()) ? storedName : fileKey; - } else { - preferredName = fileKey; - } - String name = FileNameHelper.sanitizeName(preferredName, "Ability"); + String name = FileNameHelper.sanitizeName(fileKey, "Ability"); name = makeUniqueNameForLoad(customAbilities, dir, file, name, fileKey); boolean dirty = false; if (!name.equals(fileKey)) { File renamedFile = new File(dir, name + ".json"); if (file.renameTo(renamedFile)) { - LogWriter.info("Migrated ability file: " + fileKey + ".json -> " + name + ".json"); file = renamedFile; - migrated++; dirty = true; - } else { - LogWriter.error("Failed to rename ability file: " + fileKey + ".json -> " + name + ".json"); } } ability.setName(name); - if (storedName != null && !storedName.equals(name)) { - dirty = true; - if (!storedName.isEmpty()) { - migratedAbilityNames.put(storedName, name); - } - } - if (!fileKey.equals(name)) { - migratedAbilityNames.put(fileKey, name); - } - // Ensure ability has a UUID; generate one for legacy abilities String uuid = ability.getId(); - if (uuid == null || uuid.isEmpty() || uuid.equals(fileKey) || uuid.equals(name)) { - // Legacy: id was the name or empty — generate a real UUID + if (uuid == null || uuid.isEmpty()) { uuid = UUID.randomUUID().toString(); ability.setId(uuid); - migrated++; dirty = true; } if (dirty) { - NBTJsonUtil.SaveFile(file, ability.writeNBT()); + NBTJsonUtil.SaveFile(file, ability.writeNBT(true)); } customAbilities.put(name, ability); @@ -294,12 +264,9 @@ public class AbilityController implements IAbilityHandler { } customAbilityRevision++; - if (migrated > 0) { - LogWriter.info("Migrated " + migrated + " abilities (filename normalization / UUID repair)"); - } LogWriter.info("Loaded " + customAbilities.size() + " custom abilities"); - loadChainedAbilities(migratedAbilityNames); + loadChainedAbilities(); } private boolean nameFileExists(File dir, String name, File ignoredFile) { @@ -325,32 +292,6 @@ public class AbilityController implements IAbilityHandler { return candidate; } - private boolean remapChainEntryReferences(ChainedAbility chain, Map migratedAbilityNames) { - if (chain == null || migratedAbilityNames == null || migratedAbilityNames.isEmpty()) return false; - boolean changed = false; - for (ChainedAbilityEntry entry : chain.getEntries()) { - if (entry == null || !entry.isReference()) continue; - String ref = entry.getAbilityReference(); - if (ref == null || ref.isEmpty()) continue; - - String mapped = migratedAbilityNames.get(ref); - if (mapped == null) { - for (Map.Entry e : migratedAbilityNames.entrySet()) { - if (e.getKey().equalsIgnoreCase(ref)) { - mapped = e.getValue(); - break; - } - } - } - - if (mapped != null && !mapped.equals(ref)) { - entry.setAbilityReference(mapped); - changed = true; - } - } - return changed; - } - public boolean saveCustomAbility(Ability ability) { if (ability == null) return false; if (ability.isBuiltIn()) return false; @@ -396,7 +337,8 @@ public class AbilityController implements IAbilityHandler { File fileCurrent = new File(dir, name + ".json"); try { - NBTTagCompound nbt = ability.writeNBT(); + NBTTagCompound nbt = ability.writeNBT(true); + NBTJsonUtil.SaveFile(fileNew, nbt); if (fileCurrent.exists()) { @@ -434,10 +376,11 @@ public class AbilityController implements IAbilityHandler { } if (removed == null) return false; - // Also remove from UUID index + // Also remove from UUID index and script handler String uuid = removed.getId(); if (uuid != null && !uuid.isEmpty()) { customAbilitiesById.remove(uuid); + abilityScriptHandlers.remove(uuid); } File dir = getDir(); @@ -459,6 +402,9 @@ public class AbilityController implements IAbilityHandler { pData.abilityData.lockAbility(name); changed = true; } + if (changed) { + pData.save(); + } } } } @@ -516,10 +462,9 @@ public class AbilityController implements IAbilityHandler { // CHAINED ABILITIES // ═══════════════════════════════════════════════════════════════════════════ - private void loadChainedAbilities(Map migratedAbilityNames) { + private void loadChainedAbilities() { chainedAbilities.clear(); chainedAbilitiesById.clear(); - int migrated = 0; File dir = getChainedDir(); File[] files = dir.exists() ? dir.listFiles() : null; @@ -534,7 +479,6 @@ public class AbilityController implements IAbilityHandler { ChainedAbility chain = new ChainedAbility(); chain.readNBT(nbt); - String storedName = chain.getName(); String name = FileNameHelper.sanitizeName(fileKey, "Chain"); name = makeUniqueNameForLoad(chainedAbilities, dir, file, name, fileKey); @@ -542,33 +486,22 @@ public class AbilityController implements IAbilityHandler { if (!name.equals(fileKey)) { File renamedFile = new File(dir, name + ".json"); if (file.renameTo(renamedFile)) { - LogWriter.info("Migrated chained ability file: " + fileKey + ".json -> " + name + ".json"); file = renamedFile; dirty = true; - } else { - LogWriter.error("Failed to rename chained ability file: " + fileKey + ".json -> " + name + ".json"); } } chain.setName(name); - if (storedName != null && !storedName.equals(name)) { - dirty = true; - } - - if (remapChainEntryReferences(chain, migratedAbilityNames)) { - dirty = true; - } String uuid = chain.getId(); - if (uuid == null || uuid.isEmpty() || uuid.equals(fileKey) || uuid.equals(name)) { + if (uuid == null || uuid.isEmpty()) { uuid = UUID.randomUUID().toString(); chain.setId(uuid); dirty = true; } if (dirty) { - NBTJsonUtil.SaveFile(file, chain.writeNBT()); - migrated++; + NBTJsonUtil.SaveFile(file, chain.writeNBT(true)); } chainedAbilities.put(name, chain); @@ -580,9 +513,6 @@ public class AbilityController implements IAbilityHandler { } chainedAbilityRevision++; - if (migrated > 0) { - LogWriter.info("Migrated " + migrated + " chained abilities"); - } LogWriter.info("Loaded " + chainedAbilities.size() + " chained abilities"); } @@ -626,7 +556,8 @@ public class AbilityController implements IAbilityHandler { File fileCurrent = new File(dir, name + ".json"); try { - NBTTagCompound nbt = chain.writeNBT(); + NBTTagCompound nbt = chain.writeNBT(true); + NBTJsonUtil.SaveFile(fileNew, nbt); if (fileCurrent.exists()) { @@ -667,6 +598,7 @@ public class AbilityController implements IAbilityHandler { String uuid = removed.getId(); if (uuid != null && !uuid.isEmpty()) { chainedAbilitiesById.remove(uuid); + chainedAbilityScriptHandlers.remove(uuid); } File dir = getChainedDir(); @@ -775,7 +707,7 @@ public class AbilityController implements IAbilityHandler { } @Override - public boolean saveChainedAbility(noppes.npcs.api.ability.IChainedAbility chain) { + public boolean saveChainedAbility(IChainedAbility chain) { if (chain instanceof ChainedAbility) { return saveChainedAbility((ChainedAbility) chain); } @@ -818,48 +750,36 @@ public class AbilityController implements IAbilityHandler { Ability byUuid = customAbilitiesById.get(key); if (byUuid != null) return byUuid.deepCopy(); - // Built-in: exact name + // Built-in: exact key Ability builtIn = builtAbilities.get(key); if (builtIn != null) return builtIn.deepCopy(); - // Built-in: case-insensitive map key + // Built-in: case-insensitive key + String lowerKey = key.toLowerCase(); for (Map.Entry entry : builtAbilities.entrySet()) { - if (entry.getKey().equalsIgnoreCase(key)) { + if (entry.getKey().equalsIgnoreCase(lowerKey)) { return entry.getValue().deepCopy(); } } - // Built-in: by ability name (name may differ from map key, e.g. "Ki Fist" vs "ki_fist") + // Built-in: by ID (case-insensitive) for (Ability ability : builtAbilities.values()) { - if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) { + if (key.equalsIgnoreCase(ability.getId())) { return ability.deepCopy(); } } - // Built-in: registry key / ID - for (Ability ability : builtAbilities.values()) { - if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) { - return ability.deepCopy(); - } - } - - // Custom: exact name (fallback for legacy references) + // Custom: exact name Ability custom = customAbilities.get(key); if (custom != null) return custom.deepCopy(); // Custom: case-insensitive name - for (Ability ability : customAbilities.values()) { - if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) { - return ability.deepCopy(); + for (Map.Entry entry : customAbilities.entrySet()) { + if (entry.getKey().equalsIgnoreCase(key)) { + return entry.getValue().deepCopy(); } } - String sanitizedKey = FileNameHelper.sanitizeTextInput(key); - if (!sanitizedKey.equals(key)) { - Ability sanitized = customAbilities.get(sanitizedKey); - if (sanitized != null) return sanitized.deepCopy(); - } - return null; } @@ -904,14 +824,8 @@ public class AbilityController implements IAbilityHandler { } public boolean hasAbility(String key) { - if (builtAbilities.containsKey(key) || customAbilities.containsKey(key) - || customAbilitiesById.containsKey(key)) { - return true; - } - String sanitized = FileNameHelper.sanitizeTextInput(key); - return !sanitized.equals(key) - && (builtAbilities.containsKey(sanitized) || customAbilities.containsKey(sanitized) - || customAbilitiesById.containsKey(sanitized)); + return builtAbilities.containsKey(key) || customAbilities.containsKey(key) + || customAbilitiesById.containsKey(key); } public Ability getCustomAbilityByUUID(String uuid) { @@ -919,12 +833,30 @@ public class AbilityController implements IAbilityHandler { } public Ability getCustomAbilityByName(String name) { - Ability ability = customAbilities.get(name); - if (ability != null) return ability; - String sanitized = FileNameHelper.sanitizeTextInput(name); - if (!sanitized.equals(name)) { - return customAbilities.get(sanitized); + return customAbilities.get(name); + } + + /** + * Resolve an ability by key and return the live (non-copied) reference. + * For validation/permission checks only — do NOT expose or modify the returned ability. + * Uses the same lookup chain as {@link #resolveAbility(String)}. + */ + public Ability peekAbility(String key) { + if (key == null || key.isEmpty()) return null; + + Ability byUuid = customAbilitiesById.get(key); + if (byUuid != null) return byUuid; + + Ability builtIn = builtAbilities.get(key); + if (builtIn != null) return builtIn; + + for (Ability ability : builtAbilities.values()) { + if (key.equals(ability.getId())) return ability; } + + Ability custom = customAbilities.get(key); + if (custom != null) return custom; + return null; } @@ -935,33 +867,20 @@ public class AbilityController implements IAbilityHandler { public boolean canResolveAbility(String key) { if (key == null || key.isEmpty()) return false; - // Custom: UUID lookup (most common for persistent references) + // Custom: UUID lookup if (customAbilitiesById.containsKey(key)) return true; - // Built-in: exact name + // Built-in: exact key if (builtAbilities.containsKey(key)) return true; - // Built-in: case-insensitive name - for (String name : builtAbilities.keySet()) { - if (name.equalsIgnoreCase(key)) return true; - } - - // Built-in: registry key / ID + // Built-in: by ID for (Ability ability : builtAbilities.values()) { - if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) return true; + if (key.equals(ability.getId())) return true; } - // Custom: exact name (fallback for legacy references) + // Custom: exact name if (customAbilities.containsKey(key)) return true; - // Custom: case-insensitive name - for (Ability ability : customAbilities.values()) { - if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) return true; - } - - String sanitizedKey = FileNameHelper.sanitizeTextInput(key); - if (!sanitizedKey.equals(key) && customAbilities.containsKey(sanitizedKey)) return true; - return false; } diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 2d6384c27..6403a2c1b 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -337,7 +337,7 @@ public class SyncController { NBTTagList list = new NBTTagList(); NBTTagCompound compound = new NBTTagCompound(); for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { - NBTTagCompound abilityNBT = entry.getValue().writeNBT(); + NBTTagCompound abilityNBT = entry.getValue().writeNBT(false); abilityNBT.setString("CustomAbilityId", entry.getKey()); list.appendTag(abilityNBT); } @@ -358,7 +358,7 @@ public class SyncController { NBTTagList list = new NBTTagList(); NBTTagCompound compound = new NBTTagCompound(); for (Map.Entry entry : AbilityController.Instance.getChainedAbilities().entrySet()) { - NBTTagCompound chainNBT = entry.getValue().writeNBT(); + NBTTagCompound chainNBT = entry.getValue().writeNBT(false); chainNBT.setString("ChainedAbilityId", entry.getKey()); list.appendTag(chainNBT); } 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 1b3b290fe..f650450af 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -36,23 +36,20 @@ import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.StatCollector; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.DataAbilities; import noppes.npcs.NpcDamageSource; import noppes.npcs.api.INbt; import noppes.npcs.api.ability.IAbility; -import noppes.npcs.api.entity.IPlayer; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.builder.FieldDef; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.controllers.AnimationController; -import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.Animation; import noppes.npcs.controllers.data.Frame; -import noppes.npcs.controllers.data.PlayerDataScript; +import noppes.npcs.controllers.data.AbilityScript; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.EventHooks; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; -import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import java.util.ArrayList; import java.util.Collections; @@ -139,6 +136,13 @@ public abstract class Ability implements IAbility, IAbilityAction { protected transient boolean builtIn = false; protected transient String registryKey; + // Default icon (set in subclass constructors, NOT persisted) + protected transient String defaultIconTexture = ""; + protected transient java.util.function.IntSupplier defaultIconColorSource = null; + protected transient int defaultIconWidth = 48; + protected transient int defaultIconHeight = 48; + protected transient String[] defaultIconStateTextures = null; + // ═══════════════════════════════════════════════════════════════════ // TOGGLE CONFIGURATION (saved to NBT for custom abilities) // ═══════════════════════════════════════════════════════════════════ @@ -295,28 +299,14 @@ public abstract class Ability implements IAbility, IAbilityAction { // ═══════════════════════════════════════════════════════════════════ /** - * Called when this toggle ability is first activated (state goes from 0 to any active state). - * Override to apply effects (e.g., set a DBC flag, apply a buff). - */ - public void onToggleOn(EntityLivingBase caster) { - } - - /** - * Called when this toggle ability is fully deactivated (state goes to 0). - * Override to remove effects (e.g., clear a DBC flag, remove a buff). - */ - public void onToggleOff(EntityLivingBase caster) { - } - - /** - * Called whenever the toggle state changes (including on/off transitions and mid-cycling). - * For multi-state toggles, this fires on every state transition. + * Called whenever the toggle state changes (activation, deactivation, or mid-cycling). + * Override to apply/remove effects based on state transitions. * * @param caster The entity * @param oldState Previous state (0 = was off) * @param newState New state (0 = now off, 1+ = active state) */ - public void onToggleStateChanged(EntityLivingBase caster, int oldState, int newState) { + public void onToggle(EntityLivingBase caster, int oldState, int newState) { } /** @@ -432,39 +422,15 @@ public abstract class Ability implements IAbility, IAbilityAction { private boolean applyAbilityDamageInternal(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp, double knockbackDirX, double knockbackDirZ) { - // Fire hit event for NPC casters (NPC event system) - if (caster instanceof EntityNPCInterface) { - EntityNPCInterface npc = (EntityNPCInterface) caster; - DataAbilities dataAbilities = npc.abilities; - AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( - this, currentTarget, hitEntity, damage, knockback, knockbackUp); - - if (event == null) { - return false; // Cancelled - } - - damage = event.getDamage(); - knockback = event.getKnockback(); - knockbackUp = event.getKnockbackUp(); - } - // Fire hit event for Player casters (Player event system) - else if (caster instanceof EntityPlayer && ScriptController.Instance != null) { - EntityPlayer player = (EntityPlayer) caster; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler != null) { - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.HitEvent event = new PlayerAbilityEvent.HitEvent( - iPlayer, this, currentTarget, hitEntity, damage, knockback, knockbackUp); - - if (noppes.npcs.EventHooks.onPlayerAbilityHit(handler, event)) { - return false; // Cancelled - } - - damage = event.getDamage(); - knockback = event.getKnockback(); - knockbackUp = event.getKnockbackUp(); - } + // Fire hit event (unified — works for both NPC and Player casters) + AbilityEvent.HitEvent hitEvent = new AbilityEvent.HitEvent( + caster, this, currentTarget, hitEntity, damage, knockback, knockbackUp); + if (EventHooks.onAbilityHit(this, hitEvent)) { + return false; // Cancelled } + damage = hitEvent.getDamage(); + knockback = hitEvent.getKnockback(); + knockbackUp = hitEvent.getKnockbackUp(); // Save velocity before damage to cancel vanilla knockback when ability knockback is zero double prevMotionX = hitEntity.motionX; @@ -809,23 +775,30 @@ public abstract class Ability implements IAbility, IAbilityAction { } } - // ── Icon tab (native) ───────────────────────────────────── + // ── Icon section (Effects tab) ─────────────────────────── if (!isNpcInlineEdit()) { AbilityIconData icon = AbilityIconData.fromAbility(this); + defs.add(FieldDef.section("ability.section.icon").tab("Effects")); + defs.add(FieldDef.boolField("ability.hasIcon", icon::isEnabled, icon::setEnabled) + .tab("Effects")); defs.add(FieldDef.stringField("gui.texture", icon::getTexture, icon::setTexture) - .tab("Icon")); - defs.add(FieldDef.section("ability.icon.section.uv").tab("Icon")); + .tab("Effects").visibleWhen(icon::isEnabled)); + defs.add(FieldDef.section("ability.icon.section.uv").tab("Effects") + .visibleWhen(icon::isEnabled)); defs.add(FieldDef.intField("ability.icon.x", icon::getIconX, icon::setIconX) - .tab("Icon").range(0, 4096)); + .tab("Effects").range(0, 4096).visibleWhen(icon::isEnabled)); defs.add(FieldDef.intField("ability.icon.y", icon::getIconY, icon::setIconY) - .tab("Icon").range(0, 4096)); - defs.add(FieldDef.section("gui.size").tab("Icon")); + .tab("Effects").range(0, 4096).visibleWhen(icon::isEnabled)); + defs.add(FieldDef.section("gui.size").tab("Effects") + .visibleWhen(icon::isEnabled)); defs.add(FieldDef.intField("gui.width", icon::getWidth, icon::setWidth) - .tab("Icon").range(1, 256)); + .tab("Effects").range(1, 256).visibleWhen(icon::isEnabled)); defs.add(FieldDef.intField("gui.height", icon::getHeight, icon::setHeight) - .tab("Icon").range(1, 256)); + .tab("Effects").range(1, 256).visibleWhen(icon::isEnabled)); defs.add(FieldDef.floatField("gui.scale", icon::getScale, icon::setScale) - .tab("Icon").range(0.1f, 10.0f)); + .tab("Effects").range(0.1f, 10.0f).visibleWhen(icon::isEnabled)); + defs.add(FieldDef.colorSubGui("ability.icon.tint", icon::getTintColor, icon::setTintColor) + .tab("Effects").visibleWhen(icon::isEnabled)); } // External field providers (e.g., DBC Addon injecting a "DBC" tab) @@ -1378,7 +1351,7 @@ public abstract class Ability implements IAbility, IAbilityAction { // NBT (config only, execution state is transient) // ═══════════════════════════════════════════════════════════════════ - public NBTTagCompound writeNBT() { + public NBTTagCompound writeNBT(boolean saveScripts) { NBTTagCompound nbt = new NBTTagCompound(); nbt.setString("id", id); nbt.setString("name", name); @@ -1452,6 +1425,16 @@ public abstract class Ability implements IAbility, IAbilityAction { writeTypeNBT(typeNBT); nbt.setTag("typeData", typeNBT); + // Script handler data (matching CustomEffect.writeToNBT pattern) + if (saveScripts) { + AbilityScript handler = getScriptHandler(); + if (handler != null) { + NBTTagCompound scriptData = new NBTTagCompound(); + handler.writeToNBT(scriptData); + nbt.setTag("ScriptData", scriptData); + } + } + return nbt; } @@ -1548,6 +1531,13 @@ public abstract class Ability implements IAbility, IAbilityAction { if (nbt.hasKey("typeData")) { readTypeNBT(nbt.getCompoundTag("typeData")); } + + // Script handler data (matching CustomEffect.readFromNBT pattern) + if (nbt.hasKey("ScriptData", 10)) { + AbilityScript handler = new AbilityScript(this.id); + handler.readFromNBT(nbt.getCompoundTag("ScriptData")); + setScriptHandler(handler); + } } // ═══════════════════════════════════════════════════════════════════ @@ -1627,8 +1617,48 @@ public abstract class Ability implements IAbility, IAbilityAction { this.typeId = "ability." + registryKey.replace(':', '.'); } + // ═══════════════════════════════════════════════════════════════════ + // DEFAULT ICON + // ═══════════════════════════════════════════════════════════════════ + + public String getDefaultIconTexture() { + return defaultIconTexture; + } + + /** + * Returns the default icon texture for a specific toggle state. + * Falls back to the base defaultIconTexture if no state-specific texture exists. + * @param state 1-indexed toggle state (0 = off/default) + */ + public String getDefaultIconTextureForState(int state) { + if (state > 0 && defaultIconStateTextures != null && state - 1 < defaultIconStateTextures.length) { + return defaultIconStateTextures[state - 1]; + } + return defaultIconTexture; + } + + public int getDefaultIconColor() { + return defaultIconColorSource != null ? defaultIconColorSource.getAsInt() : 0xFFFFFF; + } + + public int getDefaultIconWidth() { + return defaultIconWidth; + } + + public int getDefaultIconHeight() { + return defaultIconHeight; + } + + public String[] getDefaultIconStateTextures() { + return defaultIconStateTextures; + } + + public boolean hasDefaultIcon() { + return defaultIconTexture != null && !defaultIconTexture.isEmpty(); + } + public Ability deepCopy() { - return AbilityController.Instance.fromNBT(this.writeNBT()); + return AbilityController.Instance.fromNBT(this.writeNBT(true)); } @Override @@ -2497,11 +2527,35 @@ public abstract class Ability implements IAbility, IAbilityAction { @Override public INbt getNbt() { - return NpcAPI.Instance().getINbt(writeNBT()); + return NpcAPI.Instance().getINbt(writeNBT(false)); } @Override public void setNbt(INbt nbt) { readNBT(nbt.getMCNBT()); } + + // ═══════════════════════════════════════════════════════════════════ + // SCRIPT HANDLER + // ═══════════════════════════════════════════════════════════════════ + + public AbilityScript getScriptHandler() { + if (this.id == null || this.id.isEmpty()) return null; + return AbilityController.Instance.abilityScriptHandlers.get(this.id); + } + + public void setScriptHandler(AbilityScript handler) { + if (this.id == null || this.id.isEmpty()) return; + AbilityController.Instance.abilityScriptHandlers.put(this.id, handler); + } + + public AbilityScript getOrCreateScriptHandler() { + if (this.id == null || this.id.isEmpty()) return null; + AbilityScript handler = getScriptHandler(); + if (handler == null) { + handler = new AbilityScript(this.id); + AbilityController.Instance.abilityScriptHandlers.put(this.id, handler); + } + return handler; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java index aadca44a3..571b2fd35 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java @@ -218,7 +218,7 @@ public class AbilityAction { Ability resolved = getAbility(); if (resolved == null) return false; - inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT()); + inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT(true)); slotType = SlotType.INLINE_ABILITY; referenceId = null; cachedAction = null; @@ -245,12 +245,12 @@ public class AbilityAction { // NBT // ═══════════════════════════════════════════════════════════════════ - public NBTTagCompound writeNBT() { + public NBTTagCompound writeNBT(boolean saveScripts) { switch (slotType) { case INLINE_CHAIN: { NBTTagCompound nbt = new NBTTagCompound(); if (inlineChain != null) { - nbt.setTag("InlineChain", inlineChain.writeNBT()); + nbt.setTag("InlineChain", inlineChain.writeNBT(saveScripts)); } return nbt; } @@ -275,7 +275,7 @@ public class AbilityAction { case INLINE_ABILITY: default: - return inlineAbility != null ? inlineAbility.writeNBT() : new NBTTagCompound(); + return inlineAbility != null ? inlineAbility.writeNBT(saveScripts) : new NBTTagCompound(); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java new file mode 100644 index 000000000..c59eb9765 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java @@ -0,0 +1,165 @@ +package kamkeel.npcs.controllers.data.ability; + +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.ability.ICustomAbility; +import noppes.npcs.client.gui.builder.FieldDef; + +import java.util.List; + +/** + * A blank-slate ability that does nothing by itself. + * All behavior is driven by script event hooks (abilityStart, abilityTick, abilityComplete, + * abilityToggle, abilityToggleTick). + *

+ * Supports two modes based on toggle configuration: + *

    + *
  • Instant mode (toggleStates = 0): Uses standard execution lifecycle with + * configurable duration. Script hooks: abilityStart, abilityTick, abilityComplete.
  • + *
  • Toggle mode (toggleStates >= 1): Uses toggle lifecycle. + * Script hooks: abilityToggle, abilityToggleTick.
  • + *
+ */ +public class CustomAbility extends Ability implements ICustomAbility { + + private int durationTicks = 20; + + public CustomAbility() { + this.typeId = "ability.cnpc.custom"; + this.name = "Custom"; + this.targetingMode = TargetingMode.SELF; + this.lockMovement = LockMode.NO; + this.cooldownTicks = 0; + this.windUpTicks = 0; + this.telegraphType = TelegraphType.NONE; + this.showTelegraph = false; + } + + // ==================== EXECUTION ==================== + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target) { + if (durationTicks <= 0) { + signalCompletion(); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { + if (durationTicks > 0 && tick >= durationTicks) { + signalCompletion(); + } + } + + // ==================== PROPERTIES ==================== + + @Override + public boolean hasDamage() { + return false; + } + + @Override + public boolean isConcurrentCapable() { + return true; + } + + @Override + public boolean isTargetingModeLocked() { + return false; + } + + @Override + public boolean allowFreeOnCast() { + return true; + } + + // ==================== FIELDS ==================== + + @Override + public int getDurationTicks() { + return durationTicks; + } + + @Override + public void setDurationTicks(int ticks) { + this.durationTicks = Math.max(0, ticks); + } + + // ==================== ICustomAbility ==================== + + @Override + public int getTelegraphShapeType() { + return getTelegraphType().ordinal(); + } + + @Override + public void setTelegraphShapeType(int type) { + TelegraphType[] values = TelegraphType.values(); + if (type >= 0 && type < values.length) { + setTelegraphType(values[type]); + } + } + + @Override + public int getTargetingModeType() { + return getTargetingMode().ordinal(); + } + + @Override + public void setTargetingModeType(int type) { + TargetingMode[] values = TargetingMode.values(); + if (type >= 0 && type < values.length) { + setTargetingMode(values[type]); + } + } + + // ==================== NBT ==================== + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.getInteger("durationTicks"); + } + + // ==================== GUI ==================== + + @Override + public void getAbilityDefinitions(List defs) { + // User Type (NPC / Player / Both) + defs.add(FieldDef.enumField("ability.allowedBy", UserType.class, + this::getAllowedBy, this::setAllowedBy)); + + // Targeting Mode + defs.add(FieldDef.enumField("ability.targetingMode", TargetingMode.class, + this::getTargetingMode, this::setTargetingMode)); + + // Telegraph Shape + defs.add(FieldDef.enumField("ability.telegraphShape", TelegraphType.class, + this::getTelegraphType, this::setTelegraphType)); + + // Toggle section + defs.add(FieldDef.section("ability.section.toggle")); + defs.add(FieldDef.boolField("ability.toggleEnabled", + this::isToggleable, enabled -> setToggleStates(enabled ? Math.max(1, getToggleStates()) : 0))); + defs.add(FieldDef.intField("ability.toggleStates", this::getToggleStates, this::setToggleStates) + .range(1, 10) + .visibleWhen(this::isToggleable)); + defs.add(FieldDef.boolField("ability.hasActiveToggle", this::hasActiveToggle, this::setHasActiveToggle) + .visibleWhen(this::isToggleable)); + + // Duration (instant mode only) + defs.add(FieldDef.section("ability.section.execution") + .visibleWhen(() -> !this.isToggleable())); + defs.add(FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(0, 6000) + .visibleWhen(() -> !this.isToggleable())); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/AbilityIconData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/AbilityIconData.java index 77dd746ea..9380e2116 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/AbilityIconData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/AbilityIconData.java @@ -21,6 +21,12 @@ public class AbilityIconData { public int height = 32; public float scale = 1.0f; + /** Whether a custom icon override is active. Default false = use type's default icon. */ + private boolean enabled = false; + + /** Manual tint color for the custom icon (0xRRGGBB). Default white = no tint. */ + private int tintColor = 0xFFFFFF; + /** Per-state icon UV overrides (0-indexed, maps to toggle states 1, 2, ...). */ private int[][] stateIcons = null; @@ -64,12 +70,14 @@ public class AbilityIconData { } public void writeToNBT(NBTTagCompound nbt) { + nbt.setBoolean("Enabled", enabled); nbt.setString("Texture", texture); nbt.setInteger("IconX", iconX); nbt.setInteger("IconY", iconY); nbt.setInteger("Width", width); nbt.setInteger("Height", height); nbt.setFloat("Scale", scale); + nbt.setInteger("TintColor", tintColor); if (stateIcons != null && stateIcons.length > 0) { NBTTagList list = new NBTTagList(); for (int[] pair : stateIcons) { @@ -83,7 +91,9 @@ public class AbilityIconData { } public void readFromNBT(NBTTagCompound nbt) { + enabled = nbt.getBoolean("Enabled"); texture = nbt.getString("Texture"); + tintColor = nbt.hasKey("TintColor") ? nbt.getInteger("TintColor") : 0xFFFFFF; iconX = nbt.getInteger("IconX"); iconY = nbt.getInteger("IconY"); width = nbt.getInteger("Width"); @@ -114,6 +124,24 @@ public class AbilityIconData { // GETTERS AND SETTERS (with auto-save) // ═══════════════════════════════════════════════════════════════════ + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + save(); + } + + public int getTintColor() { + return tintColor; + } + + public void setTintColor(int tintColor) { + this.tintColor = tintColor & 0xFFFFFF; + save(); + } + public String getTexture() { return texture; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java index 5390e0c1a..e3aa63346 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java @@ -2,6 +2,7 @@ package kamkeel.npcs.controllers.data.ability.data; import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.data.ability.Ability; +import noppes.npcs.controllers.data.ChainedAbilityScript; import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry; import kamkeel.npcs.controllers.data.ability.enums.UserType; import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition; @@ -358,7 +359,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { */ public ChainedAbility deepCopy() { ChainedAbility copy = new ChainedAbility(); - copy.readNBT(this.writeNBT()); + copy.readNBT(this.writeNBT(true)); return copy; } @@ -376,7 +377,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ - public NBTTagCompound writeNBT() { + public NBTTagCompound writeNBT(boolean saveScripts) { NBTTagCompound nbt = new NBTTagCompound(); nbt.setString("Id", id); nbt.setString("Name", name); @@ -398,13 +399,23 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { // Entries NBTTagList entryList = new NBTTagList(); for (ChainedAbilityEntry entry : entries) { - entryList.appendTag(entry.writeNBT()); + entryList.appendTag(entry.writeNBT(saveScripts)); } nbt.setTag("Entries", entryList); // Custom data (for external mods) nbt.setTag("customData", (NBTTagCompound) customData.copy()); + // Script handler data (matching CustomEffect.writeToNBT pattern) + if (saveScripts) { + ChainedAbilityScript handler = getScriptHandler(); + if (handler != null) { + NBTTagCompound scriptData = new NBTTagCompound(); + handler.writeToNBT(scriptData); + nbt.setTag("ScriptData", scriptData); + } + } + return nbt; } @@ -418,7 +429,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { cooldownTicks = nbt.hasKey("CooldownTicks") ? nbt.getInteger("CooldownTicks") : 100; minRange = nbt.getFloat("MinRange"); maxRange = nbt.hasKey("MaxRange") ? nbt.getFloat("MaxRange") : 20; - // AllowedBy is computed from child abilities — old NBT data silently ignored + // AllowedBy is computed from child abilities // Conditions conditions.clear(); @@ -442,5 +453,33 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { // Custom data (for external mods) customData = (NBTTagCompound) nbt.getCompoundTag("customData").copy(); + + // Script handler data (matching CustomEffect.readFromNBT pattern) + if (nbt.hasKey("ScriptData", 10)) { + ChainedAbilityScript handler = new ChainedAbilityScript(this.id); + handler.readFromNBT(nbt.getCompoundTag("ScriptData")); + setScriptHandler(handler); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // SCRIPT HANDLER + // ═══════════════════════════════════════════════════════════════════ + + public ChainedAbilityScript getScriptHandler() { + return AbilityController.Instance.chainedAbilityScriptHandlers.get(this.id); + } + + public void setScriptHandler(ChainedAbilityScript handler) { + AbilityController.Instance.chainedAbilityScriptHandlers.put(this.id, handler); + } + + public ChainedAbilityScript getOrCreateScriptHandler() { + ChainedAbilityScript handler = getScriptHandler(); + if (handler == null) { + handler = new ChainedAbilityScript(this.id); + AbilityController.Instance.chainedAbilityScriptHandlers.put(this.id, handler); + } + return handler; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/IAbilityAction.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/IAbilityAction.java index 59a296f7b..341fdd061 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/IAbilityAction.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/IAbilityAction.java @@ -50,5 +50,5 @@ public interface IAbilityAction { */ IAbilityAction deepCopyAction(); - NBTTagCompound writeNBT(); + NBTTagCompound writeNBT(boolean saveScripts); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyDisplayData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyDisplayData.java index f15f31b6d..72bcffbb8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyDisplayData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyDisplayData.java @@ -16,10 +16,6 @@ public class EnergyDisplayData implements IEnergyDisplayData { public float innerAlpha = 1.0f; public float rotationSpeed = 4.0f; - // TODO: Skybox Feature - // public boolean skyboxEnabled = false; - // public String skyboxTexture = ""; - public EnergyDisplayData() { } @@ -122,9 +118,6 @@ public class EnergyDisplayData implements IEnergyDisplayData { nbt.setFloat("outerColorAlpha", outerColorAlpha); nbt.setFloat("innerAlpha", innerAlpha); nbt.setFloat("rotationSpeed", rotationSpeed); - // TODO: Skybox Feature - // nbt.setBoolean("skyboxEnabled", skyboxEnabled); - // nbt.setString("skyboxTexture", skyboxTexture); } public void readNBT(NBTTagCompound nbt) { @@ -135,16 +128,10 @@ public class EnergyDisplayData implements IEnergyDisplayData { outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; innerAlpha = nbt.hasKey("innerAlpha") ? nbt.getFloat("innerAlpha") : 1.0f; rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; - // TODO: Skybox Feature - // skyboxEnabled = nbt.hasKey("skyboxEnabled") && nbt.getBoolean("skyboxEnabled"); - // skyboxTexture = nbt.hasKey("skyboxTexture") ? nbt.getString("skyboxTexture") : ""; } public EnergyDisplayData copy() { EnergyDisplayData copy = new EnergyDisplayData(innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, innerAlpha, rotationSpeed); - // TODO: Skybox Feature - // copy.skyboxEnabled = skyboxEnabled; - // copy.skyboxTexture = skyboxTexture; return copy; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/ChainedAbilityEntry.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/ChainedAbilityEntry.java index 85fa819ff..0e7b4bf97 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/ChainedAbilityEntry.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/entry/ChainedAbilityEntry.java @@ -155,7 +155,7 @@ public class ChainedAbilityEntry { Ability resolved = ctrl.resolveAbility(abilityReference); if (resolved == null) return false; - inlineAbility = ctrl.fromNBT(resolved.writeNBT()); + inlineAbility = ctrl.fromNBT(resolved.writeNBT(true)); entryType = EntryType.INLINE; abilityReference = ""; return true; @@ -175,22 +175,22 @@ public class ChainedAbilityEntry { copy.delayTicks = this.delayTicks; copy.concurrentEnabled = this.concurrentEnabled; if (this.inlineAbility != null) { - copy.inlineAbility = AbilityController.Instance.fromNBT(this.inlineAbility.writeNBT()); + copy.inlineAbility = AbilityController.Instance.fromNBT(this.inlineAbility.writeNBT(true)); } return copy; } // ═══════════════════════════════════════════════════════════════════ - // NBT (backwards compatible) + // NBT // ═══════════════════════════════════════════════════════════════════ - public NBTTagCompound writeNBT() { + public NBTTagCompound writeNBT(boolean saveScripts) { NBTTagCompound nbt = new NBTTagCompound(); nbt.setInteger("Delay", delayTicks); nbt.setBoolean("Concurrent", concurrentEnabled); if (entryType == EntryType.INLINE && inlineAbility != null) { - nbt.setTag("InlineAbility", inlineAbility.writeNBT()); + nbt.setTag("InlineAbility", inlineAbility.writeNBT(saveScripts)); } else { nbt.setString("Reference", abilityReference); } @@ -204,14 +204,12 @@ public class ChainedAbilityEntry { entry.delayTicks = Math.max(0, nbt.getInteger("Delay")); entry.concurrentEnabled = nbt.hasKey("Concurrent") ? nbt.getBoolean("Concurrent") : true; - // Inline entry (new format) if (nbt.hasKey("InlineAbility")) { NBTTagCompound abilityNBT = nbt.getCompoundTag("InlineAbility"); entry.entryType = EntryType.INLINE; entry.inlineAbility = AbilityController.Instance != null ? AbilityController.Instance.fromNBT(abilityNBT) : null; } else { - // Reference entry (old + new format) entry.entryType = EntryType.REFERENCE; entry.abilityReference = nbt.getString("Reference"); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/preview/AbilityPreviewExecutor.java b/src/main/java/kamkeel/npcs/controllers/data/ability/preview/AbilityPreviewExecutor.java index 93c8f1b73..aa2f5492f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/preview/AbilityPreviewExecutor.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/preview/AbilityPreviewExecutor.java @@ -137,7 +137,7 @@ public class AbilityPreviewExecutor implements PreviewEntityHandler { for (ChainedAbilityEntry entry : chain.getEntries()) { Ability resolved = entry.resolve(); if (resolved != null && AbilityController.Instance != null) { - Ability copy = AbilityController.Instance.fromNBT(resolved.writeNBT()); + Ability copy = AbilityController.Instance.fromNBT(resolved.writeNBT(true)); if (copy != null) { chainAbilities.add(copy); chainDelays.add(entry.getDelayTicks()); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java index 1d9a0374d..e4015f7e3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java @@ -55,6 +55,9 @@ public class AbilityBeam extends AbilityEnergyProjectile impl this.showTelegraph = true; this.windUpAnimationName = "Ability_Beam_Windup"; this.activeAnimationName = "Ability_Beam_Active"; + + this.defaultIconTexture = "customnpcs:textures/gui/ability/beam.png"; + this.defaultIconColorSource = () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor(); } // ==================== ABSTRACT IMPLEMENTATIONS ==================== diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java index afb6d0400..20a861786 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java @@ -55,6 +55,9 @@ public class AbilityDisc extends AbilityEnergyProjectile impl this.showTelegraph = true; this.windUpAnimationName = "Ability_Disc_Windup"; this.activeAnimationName = "Ability_Disc_Active"; + + this.defaultIconTexture = "customnpcs:textures/gui/ability/disc.png"; + this.defaultIconColorSource = () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor(); } // ==================== ABSTRACT IMPLEMENTATIONS ==================== 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 e0e096b3c..8d8949f07 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 @@ -192,20 +192,6 @@ public abstract class AbilityEnergyProjectile this.fireDelay = Math.max(0, delay); } - /** - * @deprecated Use {@link #getFireDelay()} - */ - public int getDualFireDelay() { - return fireDelay; - } - - /** - * @deprecated Use {@link #setFireDelay(int)} - */ - public void setDualFireDelay(int delay) { - setFireDelay(delay); - } - protected int clampIndex(int index) { return Math.max(0, Math.min(index, projectileCount - 1)); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java index 75480b622..9dc4557c6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java @@ -53,6 +53,9 @@ public class AbilityLaser extends AbilityEnergyProjectile im this.showTelegraph = true; this.windUpAnimationName = "Ability_Laser_Windup"; this.activeAnimationName = "Ability_Laser_Active"; + + this.defaultIconTexture = "customnpcs:textures/gui/ability/laser.png"; + this.defaultIconColorSource = () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor(); } // ==================== OVERRIDES ==================== diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java index b28813262..fed7c0755 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java @@ -49,6 +49,9 @@ public class AbilityOrb extends AbilityEnergyProjectile implem this.showTelegraph = true; this.windUpAnimationName = "Ability_Orb_Windup"; this.activeAnimationName = "Ability_Orb_Active"; + + this.defaultIconTexture = "customnpcs:textures/gui/ability/orb.png"; + this.defaultIconColorSource = () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor(); } // ==================== ABSTRACT IMPLEMENTATIONS ==================== diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index a66f56fc4..7c73a3edb 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -226,6 +226,8 @@ import kamkeel.npcs.network.packets.request.role.RoleCompanionUpdatePacket; import kamkeel.npcs.network.packets.request.role.RoleGetPacket; import kamkeel.npcs.network.packets.request.role.RoleSavePacket; import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; +import kamkeel.npcs.network.packets.request.script.AbilityScriptPacket; +import kamkeel.npcs.network.packets.request.script.ChainedAbilityScriptPacket; import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import kamkeel.npcs.network.packets.request.script.EventScriptPacket; import kamkeel.npcs.network.packets.request.script.ForgeScriptPacket; @@ -316,6 +318,8 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new ScriptInfoPacket()); REQUEST_PACKET.registerPacket(new ScriptFilesPacket()); REQUEST_PACKET.registerPacket(new EffectScriptPacket()); + REQUEST_PACKET.registerPacket(new AbilityScriptPacket()); + REQUEST_PACKET.registerPacket(new ChainedAbilityScriptPacket()); // Cloner Packets REQUEST_PACKET.registerPacket(new CloneListPacket()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 8592163be..03e605af7 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -214,4 +214,6 @@ public enum EnumRequestPacket { RecipeScript, ManageAuctionAction, + AbilityScript, + ChainedAbilityScript, } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java index 67a8f8241..42a63a79e 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/BuiltInAbilityGetPacket.java @@ -62,7 +62,7 @@ public final class BuiltInAbilityGetPacket extends AbstractPacket { String name = ByteBufUtils.readString(in); Ability ability = AbilityController.Instance.getAbilityByDisplayName(name); if (ability != null) { - NBTTagCompound compound = ability.writeNBT(); + NBTTagCompound compound = ability.writeNBT(false); compound.setBoolean("BuiltIn", true); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java index 2bcc48601..cecc8c9e5 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilityGetPacket.java @@ -61,7 +61,7 @@ public final class ChainedAbilityGetPacket extends AbstractPacket { String chainName = ByteBufUtils.readString(in); ChainedAbility chain = AbilityController.Instance.getChainedAbility(chainName); if (chain != null) { - NBTTagCompound compound = chain.writeNBT(); + NBTTagCompound compound = chain.writeNBT(false); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java index e4564e8f0..f270a8785 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/ChainedAbilitySavePacket.java @@ -73,7 +73,7 @@ public final class ChainedAbilitySavePacket extends AbstractPacket { if (chain.getName() != null && !chain.getName().isEmpty()) { AbilityController.Instance.saveChainedAbility(chain); NoppesUtilServer.sendChainedAbilitiesData((EntityPlayerMP) player); - GuiDataPacket.sendGuiData((EntityPlayerMP) player, chain.writeNBT()); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, chain.writeNBT(false)); } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java index b4588935b..b0556926a 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java @@ -61,7 +61,7 @@ public final class CustomAbilityGetPacket extends AbstractPacket { String abilityName = ByteBufUtils.readString(in); Ability ability = AbilityController.Instance.getCustomAbility(abilityName); if (ability != null) { - NBTTagCompound compound = ability.writeNBT(); + NBTTagCompound compound = ability.writeNBT(false); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java index f360b0eff..3d91f5a86 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java @@ -70,7 +70,7 @@ public final class CustomAbilitySavePacket extends AbstractPacket { if (ability != null) { AbilityController.Instance.saveCustomAbility(ability); NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); - NBTTagCompound responseNbt = ability.writeNBT(); + NBTTagCompound responseNbt = ability.writeNBT(false); GuiDataPacket.sendGuiData((EntityPlayerMP) player, responseNbt); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java new file mode 100644 index 000000000..411d98a00 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java @@ -0,0 +1,117 @@ +package kamkeel.npcs.network.packets.request.script; + +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.LogWriter; +import noppes.npcs.config.ConfigDebug; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.data.AbilityScript; +import noppes.npcs.controllers.data.IScriptHandler; + +import java.io.IOException; + +public final class AbilityScriptPacket extends AbstractPacket { + public static String packetName = "Request|AbilityScript"; + + private Action type; + private String abilityId; + private int page; + private int maxSize; + private NBTTagCompound compound; + + public AbilityScriptPacket() { + } + + public AbilityScriptPacket(Action type, String abilityId, int page, int maxSize, NBTTagCompound compound) { + this.type = type; + this.abilityId = abilityId; + this.page = page; + this.maxSize = maxSize; + this.compound = compound; + } + + @Override + public Enum getType() { + return EnumRequestPacket.AbilityScript; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.SCRIPT_PLAYER; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeInt(type.ordinal()); + ByteBufUtils.writeUTF8String(out, abilityId); + + if (type == Action.SAVE) { + out.writeInt(this.page); + out.writeInt(this.maxSize); + ByteBufUtils.writeNBT(out, this.compound); + } + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!ConfigScript.canScript(player, CustomNpcsPermissions.SCRIPT)) + return; + + Action requestedAction = Action.values()[in.readInt()]; + String id = ByteBufUtils.readUTF8String(in); + + Ability ability = AbilityController.Instance.resolveAbility(id); + if (ability == null) + return; + + AbilityScript data = ability.getOrCreateScriptHandler(); + if (requestedAction == Action.GET) { + PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); + } else { + data.saveScript(in); + // Persist to disk + AbilityController.Instance.saveCustomAbility(ability); + if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { + LogWriter.script(String.format("[%s] (Player) %s SAVED ABILITY SCRIPT %s [%s]", + "ABILITY SCRIPTS", player.getCommandSenderName(), ability.getName(), ability.getId())); + } + } + } + + public static void Save(String abilityId, int id, int maxSize, NBTTagCompound compound) { + PacketClient.sendClient(new AbilityScriptPacket(Action.SAVE, abilityId, id, maxSize, compound)); + } + + public static void Get(String abilityId) { + PacketClient.sendClient(new AbilityScriptPacket(Action.GET, abilityId, -1, -1, new NBTTagCompound())); + } + + private enum Action { + GET, + SAVE + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java new file mode 100644 index 000000000..2589b0e13 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java @@ -0,0 +1,117 @@ +package kamkeel.npcs.network.packets.request.script; + +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.AbilityController; +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.LogWriter; +import noppes.npcs.config.ConfigDebug; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.data.ChainedAbilityScript; +import noppes.npcs.controllers.data.IScriptHandler; + +import java.io.IOException; + +public final class ChainedAbilityScriptPacket extends AbstractPacket { + public static String packetName = "Request|ChainedAbilityScript"; + + private Action type; + private String chainId; + private int page; + private int maxSize; + private NBTTagCompound compound; + + public ChainedAbilityScriptPacket() { + } + + public ChainedAbilityScriptPacket(Action type, String chainId, int page, int maxSize, NBTTagCompound compound) { + this.type = type; + this.chainId = chainId; + this.page = page; + this.maxSize = maxSize; + this.compound = compound; + } + + @Override + public Enum getType() { + return EnumRequestPacket.ChainedAbilityScript; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.SCRIPT_PLAYER; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + out.writeInt(type.ordinal()); + ByteBufUtils.writeUTF8String(out, chainId); + + if (type == Action.SAVE) { + out.writeInt(this.page); + out.writeInt(this.maxSize); + ByteBufUtils.writeNBT(out, this.compound); + } + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!ConfigScript.canScript(player, CustomNpcsPermissions.SCRIPT)) + return; + + Action requestedAction = Action.values()[in.readInt()]; + String id = ByteBufUtils.readUTF8String(in); + + ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(id); + if (chain == null) + return; + + ChainedAbilityScript data = chain.getOrCreateScriptHandler(); + if (requestedAction == Action.GET) { + PacketUtil.getScripts((IScriptHandler) data, (EntityPlayerMP) player); + } else { + data.saveScript(in); + // Persist to disk + AbilityController.Instance.saveChainedAbility(chain); + if (ConfigDebug.PlayerLogging && FMLCommonHandler.instance().getEffectiveSide() == Side.SERVER) { + LogWriter.script(String.format("[%s] (Player) %s SAVED CHAIN SCRIPT %s [%s]", + "CHAIN SCRIPTS", player.getCommandSenderName(), chain.getName(), chain.getId())); + } + } + } + + public static void Save(String chainId, int id, int maxSize, NBTTagCompound compound) { + PacketClient.sendClient(new ChainedAbilityScriptPacket(Action.SAVE, chainId, id, maxSize, compound)); + } + + public static void Get(String chainId) { + PacketClient.sendClient(new ChainedAbilityScriptPacket(Action.GET, chainId, -1, -1, new NBTTagCompound())); + } + + private enum Action { + GET, + SAVE + } +} diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index a01ef84c1..53f4f092b 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -93,7 +93,7 @@ public class Register { public void register() { for (Map.Entry> entry : entries.entrySet()) { - AbilityController.Instance.registerType(entry.getKey(), entry.getValue()); + AbilityController.Instance.registerType(entry.getValue()); if (!typeOnly.contains(entry.getKey())) { AbilityController.Instance.registerAbility(uniqueNames.get(entry.getKey()), entry.getValue().get()); } diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java index 58a8c2fb3..80334cbe2 100644 --- a/src/main/java/noppes/npcs/AbstractDataAbilities.java +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -11,6 +11,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import net.minecraft.entity.EntityLivingBase; import net.minecraft.util.DamageSource; import noppes.npcs.controllers.AnimationController; +import noppes.npcs.scripted.event.AbilityEvent; import noppes.npcs.controllers.data.Animation; import java.util.ArrayList; @@ -124,28 +125,53 @@ public abstract class AbstractDataAbilities { */ protected abstract long getWorldTime(); - // -- Event Firing -- + // -- Event Firing (unified via EventHooks) -- - /** - * Fire tick event. Implementation differs between NPC (NpcAPI) and Player (ScriptController). - */ - protected abstract void fireTickEvent(Ability ability, EntityLivingBase target); + protected void fireTickEvent(Ability ability, EntityLivingBase target) { + EventHooks.onAbilityTick(ability, getEntity(), target, ability.getPhase().ordinal(), ability.getCurrentTick()); + } - /** - * Fire execute event. Returns true if cancelled. - */ - protected abstract boolean fireExecuteEvent(Ability ability, EntityLivingBase target); + protected boolean fireExecuteEvent(Ability ability, EntityLivingBase target) { + return EventHooks.onAbilityExecute(ability, getEntity(), target); + } - /** - * Fire complete event. - */ - protected abstract void fireCompleteEvent(Ability ability, EntityLivingBase target); + protected void fireCompleteEvent(Ability ability, EntityLivingBase target) { + EventHooks.onAbilityComplete(ability, getEntity(), target); + } - /** - * Fire interrupt event. - */ - protected abstract void fireInterruptEvent(Ability ability, EntityLivingBase target, - DamageSource source, float damage); + protected void fireInterruptEvent(Ability ability, EntityLivingBase target, + DamageSource source, float damage) { + EventHooks.onAbilityInterrupt(ability, getEntity(), target, source, damage); + } + + protected boolean fireToggleEvent(Ability ability, int oldState, int newState) { + return EventHooks.onAbilityToggle(ability, getEntity(), oldState, newState); + } + + protected boolean fireToggleUpdateEvent(Ability ability, int tick, int state) { + AbilityEvent.ToggleUpdateEvent event = new AbilityEvent.ToggleUpdateEvent(getEntity(), ability, tick, state); + EventHooks.onAbilityToggleUpdate(ability, event); + return event.isEnabled(); + } + + // -- Chain Events (unified via EventHooks) -- + + protected void fireChainStartEvent(ChainedAbility chain, int entryIndex, EntityLivingBase target) { + EventHooks.onChainStart(chain, getEntity(), entryIndex, target); + } + + protected void fireChainNextEvent(ChainedAbility chain, int entryIndex, EntityLivingBase target) { + EventHooks.onChainNext(chain, getEntity(), entryIndex, target); + } + + protected void fireChainCompleteEvent(ChainedAbility chain, int entryIndex, EntityLivingBase target) { + EventHooks.onChainComplete(chain, getEntity(), entryIndex, target); + } + + protected void fireChainInterruptEvent(ChainedAbility chain, int entryIndex, EntityLivingBase target, + DamageSource source, float damage) { + EventHooks.onChainInterrupt(chain, getEntity(), entryIndex, target, source, damage); + } // -- Telegraph -- @@ -391,8 +417,7 @@ public abstract class AbstractDataAbilities { AbilityToggleEntry entry = new AbilityToggleEntry(ability, state); activeToggles.put(key, entry); - ability.onToggleOn(getEntity()); - ability.onToggleStateChanged(getEntity(), 0, state); + ability.onToggle(getEntity(), 0, state); onToggleStateChanged(key, true, state); return true; } @@ -404,7 +429,7 @@ public abstract class AbstractDataAbilities { if (fireToggleEvent(ability, oldState, newState)) return oldState; entry.setState(newState); - ability.onToggleStateChanged(getEntity(), oldState, newState); + ability.onToggle(getEntity(), oldState, newState); onToggleStateChanged(key, true, newState); return newState; } @@ -417,8 +442,7 @@ public abstract class AbstractDataAbilities { if (fireToggleEvent(entry.getAbility(), oldState, 0)) return; activeToggles.remove(key); - entry.getAbility().onToggleOff(getEntity()); - entry.getAbility().onToggleStateChanged(getEntity(), oldState, 0); + entry.getAbility().onToggle(getEntity(), oldState, 0); onToggleStateChanged(key, false, 0); } @@ -460,8 +484,7 @@ public abstract class AbstractDataAbilities { AbilityToggleEntry entry = activeToggles.remove(key); if (entry != null) { int oldState = entry.getState(); - entry.getAbility().onToggleOff(entity); - entry.getAbility().onToggleStateChanged(entity, oldState, 0); + entry.getAbility().onToggle(entity, oldState, 0); onToggleStateChanged(key, false, 0); } } @@ -474,41 +497,23 @@ public abstract class AbstractDataAbilities { protected void onToggleStateChanged(String key, boolean active, int state) { } - /** - * Fire a toggle event before state change. - * @param oldState Previous state (0 = off) - * @param newState Target state (0 = off) - * @return true if the event was canceled - */ - protected boolean fireToggleEvent(Ability ability, int oldState, int newState) { - return false; - } - - /** - * Fire a toggle update event every 10 ticks. - * @return true if the toggle should remain active, false to force-deactivate - */ - protected boolean fireToggleUpdateEvent(Ability ability, int tick, int state) { - return true; - } /** * Clear all active toggles (e.g., on death/reset). - * Calls onToggleOff and onToggleStateChanged for each active toggle. + * Calls onToggle for each active toggle to signal deactivation. */ protected void clearActiveToggles() { if (activeToggles.isEmpty()) return; EntityLivingBase entity = getEntity(); for (Map.Entry mapEntry : activeToggles.entrySet()) { AbilityToggleEntry entry = mapEntry.getValue(); - entry.getAbility().onToggleOff(entity); - entry.getAbility().onToggleStateChanged(entity, entry.getState(), 0); + entry.getAbility().onToggle(entity, entry.getState(), 0); } activeToggles.clear(); } /** - * Add a toggle entry directly without calling onToggleOn. + * Add a toggle entry directly without calling onToggle. * Used for client-side sync and NBT restoration. * @param key The ability key * @param state The toggle state (0 = remove, 1+ = set at state) @@ -747,6 +752,7 @@ public abstract class AbstractDataAbilities { } } if (startChainEntry(chainTarget)) { + fireChainNextEvent(currentChain, chainEntryIndex, chainTarget); launchConsecutiveConcurrentEntries(chainTarget); } } @@ -1036,10 +1042,11 @@ public abstract class AbstractDataAbilities { releaseRotationControl(); releaseLockedPosition(); - // If chain was active, roll chain cooldown and clear chain state + // If chain was active, fire interrupt event, roll cooldown and clear chain state if (currentChain != null) { rollChainCooldown(currentChain); interruptCooldownRolled = true; + fireChainInterruptEvent(currentChain, chainEntryIndex, getTarget(), source, damage); currentChain = null; chainEntryIndex = -1; chainDelayRemaining = -1; @@ -1116,6 +1123,25 @@ public abstract class AbstractDataAbilities { onAbilityComplete(); } + /** + * Cleanly complete the currently executing ability. + * Unlike cancel/interrupt, this triggers the normal completion path + * (fires abilityComplete event, rolls cooldown, advances chains). + * Only works during the ACTIVE phase. + */ + public void completeCurrentAbility() { + if (currentAbility == null || !currentAbility.isExecuting()) return; + if (currentAbility.getPhase() != AbilityPhase.ACTIVE) return; + + // Signal completion on the ability (handles cleanup, detach/freeOnCast, burst logic) + if (!currentAbility.signalCompletion()) return; + + // If signalCompletion moved to IDLE, handle the clean completion path + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(getTarget()); + } + } + // ═══════════════════════════════════════════════════════════════════ // CHAINED ABILITY EXECUTION // ═══════════════════════════════════════════════════════════════════ @@ -1137,6 +1163,7 @@ public abstract class AbstractDataAbilities { // AFTER semantics: no delay before first entry (delay applies after each entry completes) boolean started = startChainEntry(target); if (started) { + fireChainStartEvent(currentChain, 0, target); launchConsecutiveConcurrentEntries(target); } return started; @@ -1253,7 +1280,7 @@ public abstract class AbstractDataAbilities { chainEntryIndex++; // Deep copy the ability so it has its own state - Ability concurrentCopy = AbilityController.Instance.fromNBT(resolved.writeNBT()); + Ability concurrentCopy = AbilityController.Instance.fromNBT(resolved.writeNBT(true)); if (concurrentCopy == null) continue; ConcurrentSlot slot = new ConcurrentSlot(concurrentCopy); @@ -1279,6 +1306,7 @@ public abstract class AbstractDataAbilities { protected void completeChain() { if (currentChain != null) { rollChainCooldown(currentChain); + fireChainCompleteEvent(currentChain, chainEntryIndex, getTarget()); } currentChain = null; chainEntryIndex = -1; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 31b6b02c6..012b66ec6 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -17,8 +17,8 @@ import net.minecraft.nbt.NBTTagList; import net.minecraft.util.DamageSource; import noppes.npcs.controllers.data.Animation; import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; +import noppes.npcs.EventHooks; import java.util.ArrayList; import java.util.List; @@ -124,51 +124,6 @@ public class DataAbilities extends AbstractDataAbilities { return npc.worldObj.getTotalWorldTime(); } - @Override - protected void fireTickEvent(Ability ability, EntityLivingBase target) { - AbilityEvent.TickEvent event = new AbilityEvent.TickEvent( - npc.wrappedNPC, ability, target, - ability.getPhase().ordinal(), ability.getCurrentTick()); - NpcAPI.EVENT_BUS.post(event); - } - - @Override - protected boolean fireExecuteEvent(Ability ability, EntityLivingBase target) { - AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent( - npc.wrappedNPC, ability, target); - return NpcAPI.EVENT_BUS.post(executeEvent); - } - - @Override - protected void fireCompleteEvent(Ability ability, EntityLivingBase target) { - AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent( - npc.wrappedNPC, ability, target); - NpcAPI.EVENT_BUS.post(completeEvent); - } - - @Override - protected void fireInterruptEvent(Ability ability, EntityLivingBase target, - DamageSource source, float damage) { - AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent( - npc.wrappedNPC, ability, target, source, damage); - NpcAPI.EVENT_BUS.post(interruptEvent); - } - - @Override - protected boolean fireToggleEvent(Ability ability, int oldState, int newState) { - AbilityEvent.ToggleEvent event = new AbilityEvent.ToggleEvent( - npc.wrappedNPC, ability, oldState, newState); - return NpcAPI.EVENT_BUS.post(event); - } - - @Override - protected boolean fireToggleUpdateEvent(Ability ability, int tick, int state) { - AbilityEvent.ToggleUpdateEvent event = new AbilityEvent.ToggleUpdateEvent( - npc.wrappedNPC, ability, tick, state); - NpcAPI.EVENT_BUS.post(event); - return event.isEnabled(); - } - @Override protected void spawnTelegraph(Ability ability, EntityLivingBase target) { List telegraphs = ability.createTelegraphs(npc, target); @@ -502,10 +457,7 @@ public class DataAbilities extends AbstractDataAbilities { */ private boolean startAbility(Ability ability, EntityLivingBase target) { // Fire start event (cancelable) - AbilityEvent.StartEvent startEvent = new AbilityEvent.StartEvent( - npc.wrappedNPC, ability, target); - if (NpcAPI.EVENT_BUS.post(startEvent)) { - // Event was cancelled - don't start the ability + if (EventHooks.onAbilityStart(ability, npc, target)) { return false; } @@ -705,20 +657,11 @@ public class DataAbilities extends AbstractDataAbilities { * @param ability The ability doing the hit * @param target The original target of the ability * @param hitEntity The entity being hit - * @param damage The damage amount - * @param knockback The horizontal knockback - * @param knockbackUp The vertical knockback - * @return The event (with possibly modified values), or null if cancelled + * @param event The mutable hit event to fire + * @return true if the event was cancelled */ - public AbilityEvent.HitEvent fireHitEvent(Ability ability, EntityLivingBase target, - EntityLivingBase hitEntity, float damage, - float knockback, float knockbackUp) { - AbilityEvent.HitEvent event = new AbilityEvent.HitEvent( - npc.wrappedNPC, ability, target, hitEntity, damage, knockback, knockbackUp); - if (NpcAPI.EVENT_BUS.post(event)) { - return null; // Cancelled - } - return event; + public boolean fireHitEvent(Ability ability, AbilityEvent.HitEvent event) { + return EventHooks.onAbilityHit(ability, event); } /** @@ -1091,7 +1034,7 @@ public class DataAbilities extends AbstractDataAbilities { NBTTagList actionList = new NBTTagList(); for (AbilityAction slot : actionSlots) { - actionList.appendTag(slot.writeNBT()); + actionList.appendTag(slot.writeNBT(true)); } compound.setTag("AbilityActions", actionList); @@ -1146,7 +1089,7 @@ public class DataAbilities extends AbstractDataAbilities { } } - // Active toggles - restore state directly (no onToggleOn callback during load) + // Active toggles - restore state directly (no onToggle callback during load) activeToggles.clear(); if (compound.hasKey("ActiveToggles")) { NBTTagList toggleNbt = compound.getTagList("ActiveToggles", 10); // 10 = TAG_COMPOUND diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 76029adab..7063b7653 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -53,7 +53,16 @@ import noppes.npcs.controllers.data.RecipeScript; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.entity.EntityProjectile; import noppes.npcs.scripted.NpcAPI; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.event.IAbilityEvent; +import noppes.npcs.api.event.IChainEvent; +import noppes.npcs.controllers.data.AbilityScript; +import noppes.npcs.controllers.data.ChainedAbilityScript; +import noppes.npcs.scripted.event.AbilityEvent; import noppes.npcs.scripted.event.AnimationEvent; +import noppes.npcs.scripted.event.ChainEvent; import noppes.npcs.scripted.event.BlockEvent; import noppes.npcs.scripted.event.CustomNPCsEvent; import noppes.npcs.scripted.event.EnergyProjectileEvent; @@ -68,7 +77,6 @@ import noppes.npcs.scripted.event.player.AuctionEvent; import noppes.npcs.scripted.event.player.CustomGuiEvent; import noppes.npcs.scripted.event.player.DialogEvent; import noppes.npcs.scripted.event.player.FactionEvent; -import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import noppes.npcs.scripted.event.player.PlayerEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ChatEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ContainerOpen; @@ -851,44 +859,146 @@ public class EventHooks { return NpcAPI.EVENT_BUS.post(event); } - public static boolean onPlayerAbilityStart(PlayerDataScript handler, PlayerAbilityEvent.StartEvent event) { - handler.callScript(EnumScriptType.ABILITY_START, event); + // ==================== ABILITY EVENTS (Unified NPC/Player) ==================== + + private static IScriptHandler getEntityScriptHandler(IEntityLivingBase entity) { + if (entity instanceof ICustomNpc) { + return ((EntityNPCInterface) ((ICustomNpc) entity).getMCEntity()).script; + } else if (entity instanceof IPlayer && ScriptController.Instance != null) { + return ScriptController.Instance.getPlayerScripts( + (EntityPlayer) ((IPlayer) entity).getMCEntity()); + } + return null; + } + + /** + * Dispatches an ability event through the three-tier routing chain: + *
    + *
  1. AbilityScript — fires for BOTH NPC and Player casters
  2. + *
  3. Entity script handler — routes to the caster's own handler: + * NPC casters fire on the NPC script handler only, + * Player casters fire on the Player script handler only
  4. + *
  5. EVENT_BUS — global event bus, fires last
  6. + *
+ * + * @return true if the event was cancelled (for @Cancelable events) + */ + private static boolean postAbilityEvent(Ability ability, Event event) { + IAbilityEvent abilityEvent = (IAbilityEvent) event; + + // 1. Internal ability script (fires for ALL — both NPC and Player) + AbilityScript abScript = ability.getScriptHandler(); + if (abScript != null) { + abScript.callScript(abilityEvent.getHookName(), event); + } + + // Short-circuit if cancelled by ability script + if (event.isCancelable() && event.isCanceled()) { + return true; + } + + // 2. Entity script handler (NPC → NPC handler, Player → Player handler, never cross-fires) + IScriptHandler handler = getEntityScriptHandler(abilityEvent.getEntity()); + if (handler != null && !handler.isClient()) { + handler.callScript(abilityEvent.getHookName(), event); + } + + // Short-circuit if cancelled by entity script + if (event.isCancelable() && event.isCanceled()) { + return true; + } + + // 3. EVENT_BUS (global — fires last) return NpcAPI.EVENT_BUS.post(event); } - public static boolean onPlayerAbilityExecute(PlayerDataScript handler, PlayerAbilityEvent.ExecuteEvent event) { - handler.callScript(EnumScriptType.ABILITY_EXECUTE, event); + private static boolean postChainEvent(ChainedAbility chain, Event event) { + IChainEvent chainEvent = (IChainEvent) event; + + // 1. Internal chain script (fires first) + ChainedAbilityScript chainScript = chain.getScriptHandler(); + if (chainScript != null) { + chainScript.callScript(chainEvent.getHookName(), event); + } + + // Short-circuit if cancelled by chain script + if (event.isCancelable() && event.isCanceled()) { + return true; + } + + // 2. Entity script handler (NPC or Player) + IScriptHandler handler = getEntityScriptHandler(chainEvent.getEntity()); + if (handler != null && !handler.isClient()) { + handler.callScript(chainEvent.getHookName(), event); + } + + // Short-circuit if cancelled by entity script + if (event.isCancelable() && event.isCanceled()) { + return true; + } + + // 3. EVENT_BUS (global — fires last) return NpcAPI.EVENT_BUS.post(event); } - public static void onPlayerAbilityTick(PlayerDataScript handler, PlayerAbilityEvent.TickEvent event) { - handler.callScript(EnumScriptType.ABILITY_TICK, event); - NpcAPI.EVENT_BUS.post(event); + public static boolean onAbilityStart(Ability ability, EntityLivingBase entity, EntityLivingBase target) { + if (ability == null || entity == null) return false; + return postAbilityEvent(ability, new AbilityEvent.StartEvent(entity, ability, target)); } - public static void onPlayerAbilityInterrupt(PlayerDataScript handler, PlayerAbilityEvent.InterruptEvent event) { - handler.callScript(EnumScriptType.ABILITY_INTERRUPT, event); - NpcAPI.EVENT_BUS.post(event); + public static boolean onAbilityExecute(Ability ability, EntityLivingBase entity, EntityLivingBase target) { + if (ability == null || entity == null) return false; + return postAbilityEvent(ability, new AbilityEvent.ExecuteEvent(entity, ability, target)); } - public static void onPlayerAbilityComplete(PlayerDataScript handler, PlayerAbilityEvent.CompleteEvent event) { - handler.callScript(EnumScriptType.ABILITY_COMPLETE, event); - NpcAPI.EVENT_BUS.post(event); + public static boolean onAbilityTick(Ability ability, EntityLivingBase entity, EntityLivingBase target, int phase, int tick) { + if (ability == null || entity == null) return false; + return postAbilityEvent(ability, new AbilityEvent.TickEvent(entity, ability, target, phase, tick)); } - public static boolean onPlayerAbilityHit(PlayerDataScript handler, PlayerAbilityEvent.HitEvent event) { - handler.callScript(EnumScriptType.ABILITY_HIT, event); - return NpcAPI.EVENT_BUS.post(event); + public static boolean onAbilityComplete(Ability ability, EntityLivingBase entity, EntityLivingBase target) { + if (ability == null || entity == null) return false; + return postAbilityEvent(ability, new AbilityEvent.CompleteEvent(entity, ability, target)); } - public static boolean onPlayerAbilityToggle(PlayerDataScript handler, PlayerAbilityEvent.ToggleEvent event) { - handler.callScript(EnumScriptType.ABILITY_TOGGLE, event); - return NpcAPI.EVENT_BUS.post(event); + public static boolean onAbilityInterrupt(Ability ability, EntityLivingBase entity, EntityLivingBase target, + DamageSource source, float damage) { + if (ability == null || entity == null) return false; + return postAbilityEvent(ability, new AbilityEvent.InterruptEvent(entity, ability, target, source, damage)); } - public static void onPlayerAbilityToggleUpdate(PlayerDataScript handler, PlayerAbilityEvent.ToggleUpdateEvent event) { - handler.callScript(EnumScriptType.ABILITY_TOGGLE_UPDATE, event); - NpcAPI.EVENT_BUS.post(event); + public static boolean onAbilityHit(Ability ability, AbilityEvent.HitEvent event) { + if (ability == null || event == null) return false; + return postAbilityEvent(ability, event); + } + + public static boolean onAbilityToggle(Ability ability, EntityLivingBase entity, int oldState, int newState) { + if (ability == null || entity == null) return false; + return postAbilityEvent(ability, new AbilityEvent.ToggleEvent(entity, ability, oldState, newState)); + } + + public static boolean onAbilityToggleUpdate(Ability ability, AbilityEvent.ToggleUpdateEvent event) { + if (ability == null || event == null) return false; + return postAbilityEvent(ability, event); + } + + // Chain events + + public static void onChainStart(ChainedAbility chain, EntityLivingBase entity, int entryIndex, EntityLivingBase target) { + postChainEvent(chain, new ChainEvent.StartEvent(entity, chain, entryIndex, target)); + } + + public static void onChainNext(ChainedAbility chain, EntityLivingBase entity, int entryIndex, EntityLivingBase target) { + postChainEvent(chain, new ChainEvent.NextEvent(entity, chain, entryIndex, target)); + } + + public static void onChainComplete(ChainedAbility chain, EntityLivingBase entity, int entryIndex, EntityLivingBase target) { + postChainEvent(chain, new ChainEvent.CompleteEvent(entity, chain, entryIndex, target)); + } + + public static void onChainInterrupt(ChainedAbility chain, EntityLivingBase entity, int entryIndex, + EntityLivingBase target, DamageSource source, float damage) { + postChainEvent(chain, new ChainEvent.InterruptEvent(entity, chain, entryIndex, target, source, damage)); } public static void onPlayerChangeDim(PlayerDataScript handler, IPlayer player, int fromDim, int toDim) { 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 23d18ad20..b1e58d94a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -649,9 +649,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Search matches either the display name or the typeId (strip color codes for matching) String stripped = displayName.replaceAll("\u00A7.", ""); if (search.isEmpty() || stripped.toLowerCase().contains(search) || typeId.toLowerCase().contains(search)) { - // Yellow for concurrent-capable types, gray for other built-in types + // Lime green for concurrent-capable types, gray for other built-in types if (AbilityController.Instance.isConcurrentCapableType(typeId)) { - displayName = "\u00A7e" + displayName; + displayName = "\u00A7a" + displayName; } else if (AbilityController.Instance.isBuiltInType(typeId)) { displayName = "\u00A77" + displayName; } @@ -787,7 +787,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC NBTTagList actionList = new NBTTagList(); for (AbilityAction slot : npcSlots) { - actionList.appendTag(slot.writeNBT()); + actionList.appendTag(slot.writeNBT(false)); } compound.setTag("AbilityActions", actionList); @@ -1105,7 +1105,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC public void onAbilitySaved(Ability ability) { // Editing a global parent ability via "Modify Parent" on a chain entry if (editingChainEntryParent) { - PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT(false))); return; } @@ -1119,7 +1119,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC AbilityAction slot = npcSlots.get(selectedAbilityIndex); if (slot.isReference()) { // Modify Parent on standalone reference — save to global preset - PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT(false))); } else { // Inline ability — update slot data npcSlots.set(selectedAbilityIndex, AbilityAction.inline(ability)); 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 544a7fefd..46447712b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -3,6 +3,8 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition; import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldBuilder; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.controllers.data.AbilityScript; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; @@ -143,6 +145,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi GuiMenuTopButton closeBtn = new GuiMenuTopButton(-1000, guiLeft + xSize - 22, guiTop - 17, "X"); addTopButton(closeBtn); + GuiMenuTopButton scriptBtn = new GuiMenuTopButton(-999, "script.scripts", closeBtn); + addTopButton(scriptBtn); + // Build scroll window dimensions int swX = guiLeft + 4; int swY = guiTop + 5; @@ -152,9 +157,12 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi int y = 5; int labelCounter = LABEL_ID_START; - // General tab: type label + optional concurrent label at top — reserve space + // General tab: type label + optional info labels at top — reserve space if (activeTab == TAB_GENERAL) { y += ROW_H; + if ("ability.cnpc.custom".equals(ability.getTypeId())) { + y += 15; + } if (ability.isConcurrentCapable()) { y += 15; } @@ -168,14 +176,20 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi GuiScrollWindow sw = builder.buildScrollWindow(tabFields, swX, swY, swW, swH); - // General tab: add type label + optional concurrent label at top of scroll window + // General tab: add type label + optional info labels at top of scroll window if (activeTab == TAB_GENERAL) { int baseId = builder.getNextLabelId(); + int infoY = 25; sw.addLabel(new GuiNpcLabel(baseId, "gui.type", 5, 10, 0xFFFFFF)); sw.addLabel(new GuiNpcLabel(baseId + 1, ability.getTypeId(), 55, 10, 0xFFFFFF)); - if (ability.isConcurrentCapable()) { + if ("ability.cnpc.custom".equals(ability.getTypeId())) { sw.addLabel(new GuiNpcLabel(baseId + 2, - StatCollector.translateToLocal("ability.concurrentInfo"), 5, 25, 0x55FF55)); + StatCollector.translateToLocal("ability.customScriptInfo"), 5, infoY, 0xFF55FF)); + infoY += 15; + } + if (ability.isConcurrentCapable()) { + sw.addLabel(new GuiNpcLabel(baseId + 3, + StatCollector.translateToLocal("ability.concurrentInfo"), 5, infoY, 0x55FF55)); } } @@ -302,6 +316,13 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi initGui(); return; } + if (id == -999) { + GuiNpcTextField.unfocus(); + applyToAbility(); + callback.onAbilitySaved(ability); + GuiScriptInterface.open(parent, new AbilityScript(ability.getId())); + return; + } if (id == -1000) { close(); return; @@ -405,7 +426,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi public void loadAbility(Ability loadedAbility) { if (loadedAbility == null) return; - NBTTagCompound nbt = loadedAbility.writeNBT(); + NBTTagCompound nbt = loadedAbility.writeNBT(false); nbt.setString("typeId", ability.getTypeId()); ability.readNBT(nbt); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java index a70b0318d..7f51b15e0 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java @@ -70,7 +70,7 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGui private void doSave() { saved = true; - PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT(false))); if (callback != null) { callback.onAbilitySaved(ability); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java index b0c03a811..7d9c6cc32 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java @@ -83,8 +83,10 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS continue; String displayName = I18n.format(typeId); - if (AbilityController.Instance.isConcurrentCapableType(typeId)) { - displayName = "\u00A7e" + displayName; + if (typeId.equals("ability.cnpc.custom")) { + displayName = "\u00A7d" + displayName; + } else if (AbilityController.Instance.isConcurrentCapableType(typeId)) { + displayName = "\u00A7a" + displayName; } allDisplayNameToTypeId.put(displayName, typeId); } @@ -122,6 +124,21 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS } Collections.sort(list, (a, b) -> String.CASE_INSENSITIVE_ORDER.compare( a.replaceAll("\u00A7.", ""), b.replaceAll("\u00A7.", ""))); + + // Pin Custom Ability to top of list + String customEntry = null; + for (String name : list) { + String tid = displayNameToTypeId.get(name); + if (tid != null && tid.equals("ability.cnpc.custom")) { + customEntry = name; + break; + } + } + if (customEntry != null) { + list.remove(customEntry); + list.add(0, customEntry); + } + return list; } 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 400f38853..5c6deb37f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java @@ -5,6 +5,8 @@ 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.data.ChainedAbility; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.controllers.data.ChainedAbilityScript; import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry; import kamkeel.npcs.controllers.data.ability.data.AbilityIconData; import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider; @@ -57,6 +59,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText private static final int BTN_TAB_ENTRIES = 91; private static final int BTN_TAB_TARGET = 92; private static final int BTN_TAB_EXTRA_BASE = 93; + private static final int BTN_SCRIPT = -999; private static final int BTN_CLOSE = -1000; // ── Entries tab: per-entry ID encoding ───────────────────────────────────── @@ -258,6 +261,9 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText GuiMenuTopButton closeBtn = new GuiMenuTopButton(BTN_CLOSE, guiLeft + xSize - 22, guiTop - 17, "X"); addTopButton(closeBtn); + GuiMenuTopButton scriptBtn = new GuiMenuTopButton(BTN_SCRIPT, "script.scripts", closeBtn); + addTopButton(scriptBtn); + // Scroll window dimensions int swX = guiLeft + 4; int swY = guiTop + 5; @@ -487,6 +493,10 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText initGui(); return; } + if (id == BTN_SCRIPT) { + GuiScriptInterface.open(parent, new ChainedAbilityScript(chain.getId())); + return; + } if (id == BTN_CLOSE) { saveOnClose = true; close(); @@ -853,7 +863,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText public void onAbilitySaved(Ability ability) { if (editingParentAbility) { // Persist the global parent ability to the server - PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT(false))); editingParentAbility = false; return; } 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 64be9e499..a1be3bfb1 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -495,7 +495,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private void handleChainConfigClosed() { if (pendingSaveChain != null) { - PacketClient.sendClient(new ChainedAbilitySavePacket(pendingSaveChain.writeNBT())); + PacketClient.sendClient(new ChainedAbilitySavePacket(pendingSaveChain.writeNBT(false))); selected = pendingSaveChain.getName(); pendingSaveChain = null; } @@ -550,7 +550,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface if (pendingSaveAbility == null) return false; if (gui.isConfirmed()) { - PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT())); + PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT(false))); pendingSaveAbility = null; return false; } @@ -574,7 +574,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface setSubGui(new SubGuiDuplicateNameConfirm()); return true; } - PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT())); + PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT(false))); pendingSaveAbility = null; return false; } diff --git a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityIcon.java b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityIcon.java index 09fc2e2da..cb1a8d445 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityIcon.java +++ b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityIcon.java @@ -25,6 +25,13 @@ public class AbilityIcon extends Gui { public int width; public int height; + // Default icon info (extracted from ability) + private String defaultTexture = ""; + private String[] defaultStateTextures = null; + private int defaultIconWidth = 48; + private int defaultIconHeight = 48; + private Ability sourceAbility = null; + private AbilityIcon(AbilityIconData data) { this.data = data; this.width = data.width; @@ -33,7 +40,13 @@ public class AbilityIcon extends Gui { public static AbilityIcon fromAbility(Ability ability) { if (ability != null) { - return new AbilityIcon(AbilityIconData.fromAbility(ability)); + AbilityIcon icon = new AbilityIcon(AbilityIconData.fromAbility(ability)); + icon.defaultTexture = ability.getDefaultIconTexture(); + icon.defaultStateTextures = ability.getDefaultIconStateTextures(); + icon.defaultIconWidth = ability.getDefaultIconWidth(); + icon.defaultIconHeight = ability.getDefaultIconHeight(); + icon.sourceAbility = ability; + return icon; } return fromDefaults(); } @@ -66,30 +79,85 @@ public class AbilityIcon extends Gui { public void draw(int state, float alpha) { TextureManager renderEngine = Minecraft.getMinecraft().renderEngine; - ImageData imageData = null; - if (data.hasTexture()) { - imageData = ClientCacheHandler.getImageData(data.texture); - } - GL11.glPushMatrix(); - GL11.glColor4f(1, 1, 1, alpha); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); Tessellator t; - if (imageData == null || !imageData.imageLoaded()) { - GL11.glScalef(1.0f, 1.0f, 1); + if (data.isEnabled() && data.hasTexture()) { + // Path 1: Custom icon enabled with texture — draw with manual tintColor + ImageData imageData = ClientCacheHandler.getImageData(data.texture); + if (imageData != null && imageData.imageLoaded()) { + int tint = data.getTintColor(); + float r = ((tint >> 16) & 0xFF) / 255f; + float g = ((tint >> 8) & 0xFF) / 255f; + float b = (tint & 0xFF) / 255f; + GL11.glColor4f(r, g, b, alpha); + GL11.glScalef(data.scale, data.scale, 1); + renderEngine.bindTexture(imageData.getLocation()); + t = getTessellator(imageData, state); + t.draw(); + } else { + GL11.glColor4f(1, 1, 1, alpha); + renderEngine.bindTexture(FALLBACK_TEXTURE); + t = getFallbackTessellator(); + t.draw(); + } + } else if (hasDefaultTexture()) { + // Path 2: No custom icon, but type has a default icon — draw with auto-color tint + String texturePath = resolveDefaultTexture(state); + ImageData defaultImageData = ClientCacheHandler.getImageData(texturePath); + if (defaultImageData != null && defaultImageData.imageLoaded()) { + int autoColor = sourceAbility != null ? sourceAbility.getDefaultIconColor() : 0xFFFFFF; + float r = ((autoColor >> 16) & 0xFF) / 255f; + float g = ((autoColor >> 8) & 0xFF) / 255f; + float b = (autoColor & 0xFF) / 255f; + GL11.glColor4f(r, g, b, alpha); + renderEngine.bindTexture(defaultImageData.getLocation()); + t = getDefaultIconTessellator(); + t.draw(); + } else { + // Default texture not found — use fallback at default icon dimensions with auto-color tint + int autoColor = sourceAbility != null ? sourceAbility.getDefaultIconColor() : 0xFFFFFF; + float r = ((autoColor >> 16) & 0xFF) / 255f; + float g = ((autoColor >> 8) & 0xFF) / 255f; + float b = (autoColor & 0xFF) / 255f; + GL11.glColor4f(r, g, b, alpha); + renderEngine.bindTexture(FALLBACK_TEXTURE); + t = getDefaultIconTessellator(); + t.draw(); + } + } else { + // Path 3: No custom icon, no default icon — generic fallback with auto-color tint if available + int autoColor = sourceAbility != null ? sourceAbility.getDefaultIconColor() : 0xFFFFFF; + float r = ((autoColor >> 16) & 0xFF) / 255f; + float g = ((autoColor >> 8) & 0xFF) / 255f; + float b = (autoColor & 0xFF) / 255f; + GL11.glColor4f(r, g, b, alpha); renderEngine.bindTexture(FALLBACK_TEXTURE); t = getFallbackTessellator(); - } else { - GL11.glScalef(data.scale, data.scale, 1); - renderEngine.bindTexture(imageData.getLocation()); - t = getTessellator(imageData, state); + t.draw(); } - t.draw(); GL11.glPopMatrix(); } + /** + * Resolve the default texture path for a given toggle state. + * Falls back to the base defaultTexture if no state-specific texture exists. + */ + private String resolveDefaultTexture(int state) { + if (state > 0 && defaultStateTextures != null && state - 1 < defaultStateTextures.length) { + return defaultStateTextures[state - 1]; + } + return defaultTexture; + } + + private boolean hasDefaultTexture() { + return defaultTexture != null && !defaultTexture.isEmpty(); + } + private Tessellator getTessellator(ImageData imageData, int state) { float hw = data.width / 2f; float hh = data.height / 2f; @@ -114,6 +182,19 @@ public class AbilityIcon extends Gui { return t; } + private Tessellator getDefaultIconTessellator() { + float hw = defaultIconWidth / 2f; + float hh = defaultIconHeight / 2f; + + Tessellator t = Tessellator.instance; + t.startDrawingQuads(); + t.addVertexWithUV(-hw, hh, zLevel, 0, 1); + t.addVertexWithUV(hw, hh, zLevel, 1, 1); + t.addVertexWithUV(hw, -hh, zLevel, 1, 0); + t.addVertexWithUV(-hw, -hh, zLevel, 0, 0); + return t; + } + private Tessellator getFallbackTessellator() { float hw = 16f; float hh = 16f; @@ -128,17 +209,20 @@ public class AbilityIcon extends Gui { } public boolean hasTexture() { - return data.hasTexture(); + return (data.isEnabled() && data.hasTexture()) || hasDefaultTexture(); } /** * Returns the approximate visual size (max dimension) that draw() produces. - * Textured: max(width,height) * scale. Fallback: 32. + * Custom icon: max(width,height) * scale. Default icon: max(defaultW,defaultH). Fallback: 32. */ public float getDrawSize() { - if (data.hasTexture()) { + if (data.isEnabled() && data.hasTexture()) { return Math.max(width, height) * data.scale; } + if (hasDefaultTexture()) { + return Math.max(defaultIconWidth, defaultIconHeight); + } return 32f; } } diff --git a/src/main/java/noppes/npcs/constants/EnumScriptType.java b/src/main/java/noppes/npcs/constants/EnumScriptType.java index 5b5a318b3..01fa36ac2 100644 --- a/src/main/java/noppes/npcs/constants/EnumScriptType.java +++ b/src/main/java/noppes/npcs/constants/EnumScriptType.java @@ -127,7 +127,12 @@ public enum EnumScriptType { ABILITY_HIT("abilityHit"), ABILITY_TICK("abilityTick"), ABILITY_TOGGLE("abilityToggle"), - ABILITY_TOGGLE_UPDATE("abilityToggleUpdate"), + ABILITY_TOGGLE_TICK("abilityToggleTick"), + + CHAIN_START("chainStart"), + CHAIN_NEXT("chainNext"), + CHAIN_COMPLETE("chainComplete"), + CHAIN_INTERRUPT("chainInterrupt"), ENERGY_PROJECTILE_FIRED("energyProjectileFired"), ENERGY_PROJECTILE_TICK("energyProjectileTick"), diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index 9b06001ff..e35a361d7 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -13,7 +13,7 @@ import noppes.npcs.api.event.IForgeEvent; import noppes.npcs.api.event.IItemEvent; import noppes.npcs.api.event.INpcEvent; import noppes.npcs.api.event.IPartyEvent; -import noppes.npcs.api.event.IPlayerAbilityEvent; +import noppes.npcs.api.event.IChainEvent; import noppes.npcs.api.event.IPlayerEvent; import noppes.npcs.api.event.IProjectileEvent; import noppes.npcs.api.event.IQuestEvent; @@ -61,6 +61,7 @@ public class ScriptContext { IEnergyProjectileEvent.class, IEnergyBarrierEvent.class, IAbilityEvent.class, + IChainEvent.class, IAnimationEvent.class ); @@ -74,7 +75,8 @@ public class ScriptContext { ICustomGuiEvent.class, IEnergyProjectileEvent.class, IEnergyBarrierEvent.class, - IPlayerAbilityEvent.class, + IAbilityEvent.class, + IChainEvent.class, IAuctionEvent.class ); @@ -102,6 +104,14 @@ public class ScriptContext { "Effect" ); + public static final ScriptContext ABILITY = register("ABILITY", "ability", + "Ability" + ); + + public static final ScriptContext CHAINED_ABILITY = register("CHAINED_ABILITY", "chained_ability", + "ChainedAbility" + ); + public static final ScriptContext GLOBAL = register("GLOBAL", "", "Global" // Special case: Global namespace doesn't have a corresponding event class ); diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index dd8c18550..5547fb203 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -1,6 +1,7 @@ package noppes.npcs.controllers; import noppes.npcs.api.event.IAbilityEvent; +import noppes.npcs.api.event.IChainEvent; import noppes.npcs.api.event.IAnimationEvent; import noppes.npcs.api.event.IAuctionEvent; import noppes.npcs.api.event.IBlockEvent; @@ -15,7 +16,6 @@ import noppes.npcs.api.event.IItemEvent; import noppes.npcs.api.event.ILinkedItemEvent; import noppes.npcs.api.event.INpcEvent; import noppes.npcs.api.event.IPartyEvent; -import noppes.npcs.api.event.IPlayerAbilityEvent; import noppes.npcs.api.event.IPlayerEvent; import noppes.npcs.api.event.IProjectileEvent; import noppes.npcs.api.event.IQuestEvent; @@ -41,6 +41,8 @@ import static noppes.npcs.constants.ScriptContext.ITEM; import static noppes.npcs.constants.ScriptContext.LINKED_ITEM; import static noppes.npcs.constants.ScriptContext.NPC; import static noppes.npcs.constants.ScriptContext.PLAYER; +import static noppes.npcs.constants.ScriptContext.ABILITY; +import static noppes.npcs.constants.ScriptContext.CHAINED_ABILITY; import static noppes.npcs.constants.ScriptContext.RECIPE; /** @@ -81,6 +83,7 @@ public class ScriptHookController implements IScriptHookHandler { initializeLinkedItemHooks(); initializeRecipeHooks(); initializeEffectHooks(); + initializeAbilityHooks(); initializeForgeHooks(); } @@ -124,6 +127,14 @@ public class ScriptHookController implements IScriptHookHandler { hook(NPC, ABILITY_TICK, IAbilityEvent.TickEvent.class); hook(NPC, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); hook(NPC, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); + hook(NPC, ABILITY_TOGGLE, IAbilityEvent.ToggleEvent.class); + hook(NPC, ABILITY_TOGGLE_TICK, IAbilityEvent.ToggleUpdateEvent.class); + + // Chain + hook(NPC, CHAIN_START, IChainEvent.StartEvent.class); + hook(NPC, CHAIN_NEXT, IChainEvent.NextEvent.class); + hook(NPC, CHAIN_COMPLETE, IChainEvent.CompleteEvent.class); + hook(NPC, CHAIN_INTERRUPT, IChainEvent.InterruptEvent.class); // Energy Barrier hook(NPC, ENERGY_BARRIER_SPAWNED, IEnergyBarrierEvent.SpawnedEvent.class); @@ -245,12 +256,20 @@ public class ScriptHookController implements IScriptHookHandler { hook(PLAYER, ENERGY_BARRIER_DESTROYED, IEnergyBarrierEvent.DestroyedEvent.class); // Ability - hook(PLAYER, ABILITY_START, IPlayerAbilityEvent.StartEvent.class); - hook(PLAYER, ABILITY_EXECUTE, IPlayerAbilityEvent.ExecuteEvent.class); - hook(PLAYER, ABILITY_HIT, IPlayerAbilityEvent.HitEvent.class); - hook(PLAYER, ABILITY_TICK, IPlayerAbilityEvent.TickEvent.class); - hook(PLAYER, ABILITY_INTERRUPT, IPlayerAbilityEvent.InterruptEvent.class); - hook(PLAYER, ABILITY_COMPLETE, IPlayerAbilityEvent.CompleteEvent.class); + hook(PLAYER, ABILITY_START, IAbilityEvent.StartEvent.class); + hook(PLAYER, ABILITY_EXECUTE, IAbilityEvent.ExecuteEvent.class); + hook(PLAYER, ABILITY_HIT, IAbilityEvent.HitEvent.class); + hook(PLAYER, ABILITY_TICK, IAbilityEvent.TickEvent.class); + hook(PLAYER, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); + hook(PLAYER, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); + hook(PLAYER, ABILITY_TOGGLE, IAbilityEvent.ToggleEvent.class); + hook(PLAYER, ABILITY_TOGGLE_TICK, IAbilityEvent.ToggleUpdateEvent.class); + + // Chain + hook(PLAYER, CHAIN_START, IChainEvent.StartEvent.class); + hook(PLAYER, CHAIN_NEXT, IChainEvent.NextEvent.class); + hook(PLAYER, CHAIN_COMPLETE, IChainEvent.CompleteEvent.class); + hook(PLAYER, CHAIN_INTERRUPT, IChainEvent.InterruptEvent.class); // Auction hook(PLAYER, AUCTION_CREATE, IAuctionEvent.CreateEvent.class); @@ -333,6 +352,24 @@ public class ScriptHookController implements IScriptHookHandler { hook(EFFECT, ON_EFFECT_REMOVE, IPlayerEvent.EffectEvent.Removed.class); } + private void initializeAbilityHooks() { + // Ability context (internal per-ability scripts) + hook(ABILITY, ABILITY_START, IAbilityEvent.StartEvent.class); + hook(ABILITY, ABILITY_EXECUTE, IAbilityEvent.ExecuteEvent.class); + hook(ABILITY, ABILITY_HIT, IAbilityEvent.HitEvent.class); + hook(ABILITY, ABILITY_TICK, IAbilityEvent.TickEvent.class); + hook(ABILITY, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); + hook(ABILITY, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); + hook(ABILITY, ABILITY_TOGGLE, IAbilityEvent.ToggleEvent.class); + hook(ABILITY, ABILITY_TOGGLE_TICK, IAbilityEvent.ToggleUpdateEvent.class); + + // Chained ability context (chain-level lifecycle hooks) + hook(CHAINED_ABILITY, CHAIN_START, IChainEvent.StartEvent.class); + hook(CHAINED_ABILITY, CHAIN_NEXT, IChainEvent.NextEvent.class); + hook(CHAINED_ABILITY, CHAIN_COMPLETE, IChainEvent.CompleteEvent.class); + hook(CHAINED_ABILITY, CHAIN_INTERRUPT, IChainEvent.InterruptEvent.class); + } + private void initializeForgeHooks() { hook(FORGE, INIT, IForgeEvent.InitEvent.class); hook(FORGE, FORGE_WORLD, IForgeEvent.WorldEvent.class); diff --git a/src/main/java/noppes/npcs/controllers/data/AbilityScript.java b/src/main/java/noppes/npcs/controllers/data/AbilityScript.java new file mode 100644 index 000000000..82af01b52 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AbilityScript.java @@ -0,0 +1,54 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.network.packets.request.script.AbilityScriptPacket; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.ScriptContext; + +/** + * Script handler for ability scripts. + * Manages a single script container for ability hooks (abilityStart, abilityTick, abilityComplete, etc.). + */ +public class AbilityScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** + * The ability ID (UUID) for packet communication. Empty if not bound. + */ + private String abilityId = ""; + + /** + * Create an unbound AbilityScript (for server-side use). + */ + public AbilityScript() { + } + + /** + * Create an AbilityScript bound to a specific ability (for GUI use). + * + * @param abilityId The UUID of the ability + */ + public AbilityScript(String abilityId) { + this.abilityId = abilityId != null ? abilityId : ""; + } + + @Override + public ScriptContext getContext() { + return ScriptContext.ABILITY; + } + + @Override + public String noticeString() { + return !abilityId.isEmpty() ? "Ability[" + abilityId + "]" : "Ability"; + } + + @Override + public void requestData() { + if (!abilityId.isEmpty()) + AbilityScriptPacket.Get(abilityId); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (!abilityId.isEmpty()) + AbilityScriptPacket.Save(abilityId, index, totalCount, nbt); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/ChainedAbilityScript.java b/src/main/java/noppes/npcs/controllers/data/ChainedAbilityScript.java new file mode 100644 index 000000000..e15b05d10 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/ChainedAbilityScript.java @@ -0,0 +1,54 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.network.packets.request.script.ChainedAbilityScriptPacket; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.ScriptContext; + +/** + * Script handler for chained ability scripts. + * Manages a single script container for chained ability hooks. + */ +public class ChainedAbilityScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** + * The chained ability ID (UUID) for packet communication. Empty if not bound. + */ + private String chainId = ""; + + /** + * Create an unbound ChainedAbilityScript (for server-side use). + */ + public ChainedAbilityScript() { + } + + /** + * Create a ChainedAbilityScript bound to a specific chained ability (for GUI use). + * + * @param chainId The UUID of the chained ability + */ + public ChainedAbilityScript(String chainId) { + this.chainId = chainId != null ? chainId : ""; + } + + @Override + public ScriptContext getContext() { + return ScriptContext.CHAINED_ABILITY; + } + + @Override + public String noticeString() { + return !chainId.isEmpty() ? "ChainedAbility[" + chainId + "]" : "ChainedAbility"; + } + + @Override + public void requestData() { + if (!chainId.isEmpty()) + ChainedAbilityScriptPacket.Get(chainId); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (!chainId.isEmpty()) + ChainedAbilityScriptPacket.Save(chainId, index, totalCount, nbt); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java index 7a5db74de..c088204e5 100644 --- a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java +++ b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java @@ -26,12 +26,6 @@ public class CustomEffect implements ICustomEffect { public String icon = ""; public int iconX = 0, iconY = 0; - /** - * Experimental script stuff. - */ - public Consumer onAddedConsumer; - public Consumer onTickConsumer; - public Consumer onRemovedConsumer; public String menuName = "§aNEW EFFECT"; public int width = 16, height = 16; @@ -169,26 +163,10 @@ public class CustomEffect implements ICustomEffect { } - public void onAdded(Consumer function) { - onAddedConsumer = function; - } - - public void onTick(Consumer function) { - onTickConsumer = function; - } - - public void onRemoved(Consumer function) { - onRemovedConsumer = function; - } - public void onAdded(EntityPlayer player, PlayerEffect playerEffect) { IPlayer iPlayer = NoppesUtilServer.getIPlayer(player); if (playerEffect.index == 0) { PlayerEvent.EffectEvent.Added event = new PlayerEvent.EffectEvent.Added(iPlayer, playerEffect); - - if (onAddedConsumer != null) - onAddedConsumer.accept(event); - EffectScript script = getScriptHandler(); if (script == null) { return; @@ -204,11 +182,6 @@ public class CustomEffect implements ICustomEffect { IPlayer iPlayer = NoppesUtilServer.getIPlayer(player); if (playerEffect.index == 0) { PlayerEvent.EffectEvent.Ticked event = new PlayerEvent.EffectEvent.Ticked(iPlayer, playerEffect); - - if (onTickConsumer != null) { - onTickConsumer.accept(event); - } - EffectScript script = getScriptHandler(); if (script == null) { return; @@ -225,11 +198,6 @@ public class CustomEffect implements ICustomEffect { if (playerEffect.index == 0) { PlayerEvent.EffectEvent.Removed event = new PlayerEvent.EffectEvent.Removed(iPlayer, playerEffect, type); - - if (onRemovedConsumer != null) { - onRemovedConsumer.accept(event); - } - EffectScript script = getScriptHandler(); if (script == null) { return; diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index f5a36e836..dc894dec6 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -25,10 +25,6 @@ import noppes.npcs.AbstractDataAbilities; import noppes.npcs.EventHooks; import noppes.npcs.LogWriter; import noppes.npcs.api.ability.IPlayerAbilityData; -import noppes.npcs.api.entity.IPlayer; -import noppes.npcs.controllers.ScriptController; -import noppes.npcs.scripted.NpcAPI; -import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import java.util.ArrayList; import java.util.Iterator; @@ -129,53 +125,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA return player != null ? player.worldObj.getTotalWorldTime() : 0; } - @Override - protected void fireTickEvent(Ability ability, EntityLivingBase target) { - EntityPlayer player = playerData.player; - if (ScriptController.Instance == null || player == null) return; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.TickEvent event = new PlayerAbilityEvent.TickEvent( - iPlayer, ability, target, ability.getPhase().ordinal(), ability.getCurrentTick()); - EventHooks.onPlayerAbilityTick(handler, event); - } - - @Override - protected boolean fireExecuteEvent(Ability ability, EntityLivingBase target) { - EntityPlayer player = playerData.player; - if (ScriptController.Instance == null || player == null) return false; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return false; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.ExecuteEvent event = new PlayerAbilityEvent.ExecuteEvent(iPlayer, ability, target); - return EventHooks.onPlayerAbilityExecute(handler, event); - } - - @Override - protected void fireCompleteEvent(Ability ability, EntityLivingBase target) { - EntityPlayer player = playerData.player; - if (ScriptController.Instance == null || player == null) return; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.CompleteEvent event = new PlayerAbilityEvent.CompleteEvent(iPlayer, ability, target); - EventHooks.onPlayerAbilityComplete(handler, event); - } - - @Override - protected void fireInterruptEvent(Ability ability, EntityLivingBase target, - DamageSource source, float damage) { - EntityPlayer player = playerData.player; - if (ScriptController.Instance == null || player == null) return; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.InterruptEvent event = new PlayerAbilityEvent.InterruptEvent( - iPlayer, ability, target, source, damage); - EventHooks.onPlayerAbilityInterrupt(handler, event); - } - @Override protected void spawnTelegraph(Ability ability, EntityLivingBase target) { EntityPlayer player = playerData.player; @@ -423,7 +372,7 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } // Fire start event (cancelable) - if (firePlayerStartEvent(player, ability)) { + if (EventHooks.onAbilityStart(ability, player, null)) { return false; // Cancelled } @@ -491,29 +440,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA protected void onToggleStateChanged(String key, boolean active, int state) { } - @Override - protected boolean fireToggleEvent(Ability ability, int oldState, int newState) { - EntityPlayer player = playerData.player; - if (ScriptController.Instance == null || player == null) return false; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return false; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.ToggleEvent event = new PlayerAbilityEvent.ToggleEvent(iPlayer, ability, oldState, newState); - return EventHooks.onPlayerAbilityToggle(handler, event); - } - - @Override - protected boolean fireToggleUpdateEvent(Ability ability, int tick, int state) { - EntityPlayer player = playerData.player; - if (ScriptController.Instance == null || player == null) return true; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return true; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.ToggleUpdateEvent event = new PlayerAbilityEvent.ToggleUpdateEvent(iPlayer, ability, tick, state); - EventHooks.onPlayerAbilityToggleUpdate(handler, event); - return event.isEnabled(); - } - // ═══════════════════════════════════════════════════════════════════ // SELECTION // ═══════════════════════════════════════════════════════════════════ @@ -1035,22 +961,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } } - // ═══════════════════════════════════════════════════════════════════ - // SCRIPT EVENTS (Player-specific: start event only, others via abstract) - // ═══════════════════════════════════════════════════════════════════ - - /** - * Fire the ability start event. Returns true if cancelled. - */ - private boolean firePlayerStartEvent(EntityPlayer player, Ability ability) { - if (ScriptController.Instance == null) return false; - PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); - if (handler == null) return false; - IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); - PlayerAbilityEvent.StartEvent event = new PlayerAbilityEvent.StartEvent(iPlayer, ability, null); - return EventHooks.onPlayerAbilityStart(handler, event); - } - // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ @@ -1096,7 +1006,7 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA } playingAbilityAnimation = compound.getBoolean("AbilityAnimating"); - // Active toggles - restore state directly (no onToggleOn callback during load) + // Active toggles - restore state directly (no onToggle callback during load) activeToggles.clear(); if (compound.hasKey("ActiveToggles")) { NBTTagList toggleNbt = compound.getTagList("ActiveToggles", 10); // 10 = TAG_COMPOUND diff --git a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java index 769add405..a5d5c5bf4 100644 --- a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java @@ -8,22 +8,48 @@ import noppes.npcs.api.IDamageSource; import noppes.npcs.api.ability.IAbility; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.event.IAbilityEvent; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.scripted.NpcAPI; /** - * Events fired during NPC ability execution lifecycle. + * Unified ability events for both NPCs and Players. */ -public class AbilityEvent extends NpcEvent implements IAbilityEvent { +public class AbilityEvent extends CustomNPCsEvent implements IAbilityEvent { + protected final IEntityLivingBase entity; + public final IPlayer player; + public final ICustomNpc npc; protected final Ability ability; protected final IEntityLivingBase target; private transient IAbility abilityCopy; - public AbilityEvent(ICustomNpc npc, Ability ability, EntityLivingBase target) { - super(npc); + public AbilityEvent(EntityLivingBase entity, Ability ability, EntityLivingBase target) { + this.entity = entity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(entity) : null; this.ability = ability; this.target = target != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(target) : null; + this.player = (this.entity instanceof IPlayer) ? (IPlayer) this.entity : null; + this.npc = (this.entity instanceof ICustomNpc) ? (ICustomNpc) this.entity : null; + } + + @Override + public IEntityLivingBase getEntity() { + return entity; + } + + @Override + public IPlayer getPlayer() { + return player; + } + + @Override + public ICustomNpc getNpc() { + return npc; + } + + @Override + public boolean isNPC() { + return entity instanceof ICustomNpc; } @Override @@ -44,13 +70,10 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { return "abilityEvent"; } - /** - * Fired when an ability starts executing (enters WINDUP phase). - */ @Cancelable public static class StartEvent extends AbilityEvent implements IAbilityEvent.StartEvent { - public StartEvent(ICustomNpc npc, Ability ability, EntityLivingBase target) { - super(npc, ability, target); + public StartEvent(EntityLivingBase entity, Ability ability, EntityLivingBase target) { + super(entity, ability, target); } @Override @@ -59,13 +82,10 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { } } - /** - * Fired when an ability enters the ACTIVE phase and performs its effect. - */ @Cancelable public static class ExecuteEvent extends AbilityEvent implements IAbilityEvent.ExecuteEvent { - public ExecuteEvent(ICustomNpc npc, Ability ability, EntityLivingBase target) { - super(npc, ability, target); + public ExecuteEvent(EntityLivingBase entity, Ability ability, EntityLivingBase target) { + super(entity, ability, target); } @Override @@ -74,16 +94,13 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { } } - /** - * Fired when an ability is interrupted by damage. - */ public static class InterruptEvent extends AbilityEvent implements IAbilityEvent.InterruptEvent { private final IDamageSource damageSource; private final float damage; - public InterruptEvent(ICustomNpc npc, Ability ability, EntityLivingBase target, DamageSource source, float damage) { - super(npc, ability, target); - this.damageSource = NpcAPI.Instance().getIDamageSource(source); + public InterruptEvent(EntityLivingBase entity, Ability ability, EntityLivingBase target, DamageSource source, float damage) { + super(entity, ability, target); + this.damageSource = source != null ? NpcAPI.Instance().getIDamageSource(source) : null; this.damage = damage; } @@ -103,12 +120,9 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { } } - /** - * Fired when an ability completes its full execution cycle. - */ public static class CompleteEvent extends AbilityEvent implements IAbilityEvent.CompleteEvent { - public CompleteEvent(ICustomNpc npc, Ability ability, EntityLivingBase target) { - super(npc, ability, target); + public CompleteEvent(EntityLivingBase entity, Ability ability, EntityLivingBase target) { + super(entity, ability, target); } @Override @@ -117,10 +131,6 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { } } - /** - * Fired when an ability hits an entity with damage. - * This is a cancelable event - canceling prevents the damage from being applied. - */ @Cancelable public static class HitEvent extends AbilityEvent implements IAbilityEvent.HitEvent { private final IEntityLivingBase hitEntity; @@ -128,9 +138,9 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { private float knockback; private float knockbackUp; - public HitEvent(ICustomNpc npc, Ability ability, EntityLivingBase target, + public HitEvent(EntityLivingBase entity, Ability ability, EntityLivingBase target, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { - super(npc, ability, target); + super(entity, ability, target); this.hitEntity = hitEntity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(hitEntity) : null; this.damage = damage; this.knockback = knockback; @@ -178,16 +188,12 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { } } - /** - * Fired every tick while an ability is executing. - * Provides information about the current phase and tick. - */ public static class TickEvent extends AbilityEvent implements IAbilityEvent.TickEvent { private final int abilityPhase; private final int tick; - public TickEvent(ICustomNpc npc, Ability ability, EntityLivingBase target, int phase, int tick) { - super(npc, ability, target); + public TickEvent(EntityLivingBase entity, Ability ability, EntityLivingBase target, int phase, int tick) { + super(entity, ability, target); this.abilityPhase = phase; this.tick = tick; } @@ -208,17 +214,13 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { } } - /** - * Fired when a toggle ability changes state. - * Canceling prevents the toggle from changing state. - */ @Cancelable public static class ToggleEvent extends AbilityEvent implements IAbilityEvent.ToggleEvent { private final int oldState; private final int newState; - public ToggleEvent(ICustomNpc npc, Ability ability, int oldState, int newState) { - super(npc, ability, null); + public ToggleEvent(EntityLivingBase entity, Ability ability, int oldState, int newState) { + super(entity, ability, null); this.oldState = oldState; this.newState = newState; } @@ -244,17 +246,13 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { } } - /** - * Fired every 10 ticks for each active toggle ability. - * Scripts can set enabled to false to force-deactivate. - */ public static class ToggleUpdateEvent extends AbilityEvent implements IAbilityEvent.ToggleUpdateEvent { private final int tick; private final int state; private boolean enabled = true; - public ToggleUpdateEvent(ICustomNpc npc, Ability ability, int tick, int state) { - super(npc, ability, null); + public ToggleUpdateEvent(EntityLivingBase entity, Ability ability, int tick, int state) { + super(entity, ability, null); this.tick = tick; this.state = state; } @@ -281,7 +279,7 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { @Override public String getHookName() { - return EnumScriptType.ABILITY_TOGGLE_UPDATE.function; + return EnumScriptType.ABILITY_TOGGLE_TICK.function; } } } diff --git a/src/main/java/noppes/npcs/scripted/event/ChainEvent.java b/src/main/java/noppes/npcs/scripted/event/ChainEvent.java new file mode 100644 index 000000000..2c0852e6c --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/event/ChainEvent.java @@ -0,0 +1,134 @@ +package noppes.npcs.scripted.event; + +import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.util.DamageSource; +import noppes.npcs.api.IDamageSource; +import noppes.npcs.api.ability.IChainedAbility; +import noppes.npcs.api.entity.ICustomNpc; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.event.IChainEvent; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.scripted.NpcAPI; + +/** + * Unified chain events for both NPCs and Players. + */ +public class ChainEvent extends CustomNPCsEvent implements IChainEvent { + protected final IEntityLivingBase entity; + public final IPlayer player; + public final ICustomNpc npc; + protected final ChainedAbility chain; + protected final IEntityLivingBase target; + protected final int entryIndex; + + public ChainEvent(EntityLivingBase entity, ChainedAbility chain, int entryIndex, EntityLivingBase target) { + this.entity = entity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(entity) : null; + this.chain = chain; + this.entryIndex = entryIndex; + this.target = target != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(target) : null; + this.player = (this.entity instanceof IPlayer) ? (IPlayer) this.entity : null; + this.npc = (this.entity instanceof ICustomNpc) ? (ICustomNpc) this.entity : null; + } + + @Override + public IEntityLivingBase getEntity() { + return entity; + } + + @Override + public IPlayer getPlayer() { + return player; + } + + @Override + public ICustomNpc getNpc() { + return npc; + } + + @Override + public boolean isNPC() { + return entity instanceof ICustomNpc; + } + + @Override + public IChainedAbility getChain() { + return chain; + } + + @Override + public IEntityLivingBase getTarget() { + return target; + } + + @Override + public int getEntryIndex() { + return entryIndex; + } + + @Override + public String getHookName() { + return "chainEvent"; + } + + public static class StartEvent extends ChainEvent implements IChainEvent.StartEvent { + public StartEvent(EntityLivingBase entity, ChainedAbility chain, int entryIndex, EntityLivingBase target) { + super(entity, chain, entryIndex, target); + } + + @Override + public String getHookName() { + return EnumScriptType.CHAIN_START.function; + } + } + + public static class NextEvent extends ChainEvent implements IChainEvent.NextEvent { + public NextEvent(EntityLivingBase entity, ChainedAbility chain, int entryIndex, EntityLivingBase target) { + super(entity, chain, entryIndex, target); + } + + @Override + public String getHookName() { + return EnumScriptType.CHAIN_NEXT.function; + } + } + + public static class CompleteEvent extends ChainEvent implements IChainEvent.CompleteEvent { + public CompleteEvent(EntityLivingBase entity, ChainedAbility chain, int entryIndex, EntityLivingBase target) { + super(entity, chain, entryIndex, target); + } + + @Override + public String getHookName() { + return EnumScriptType.CHAIN_COMPLETE.function; + } + } + + public static class InterruptEvent extends ChainEvent implements IChainEvent.InterruptEvent { + private final IDamageSource damageSource; + private final float damage; + + public InterruptEvent(EntityLivingBase entity, ChainedAbility chain, int entryIndex, EntityLivingBase target, + DamageSource source, float damage) { + super(entity, chain, entryIndex, target); + this.damageSource = source != null ? NpcAPI.Instance().getIDamageSource(source) : null; + this.damage = damage; + } + + @Override + public IDamageSource getDamageSource() { + return damageSource; + } + + @Override + public float getDamage() { + return damage; + } + + @Override + public String getHookName() { + return EnumScriptType.CHAIN_INTERRUPT.function; + } + } +} diff --git a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java deleted file mode 100644 index 95bda90e6..000000000 --- a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java +++ /dev/null @@ -1,286 +0,0 @@ -package noppes.npcs.scripted.event.player; - -import cpw.mods.fml.common.eventhandler.Cancelable; -import kamkeel.npcs.controllers.data.ability.Ability; -import net.minecraft.entity.EntityLivingBase; -import net.minecraft.util.DamageSource; -import noppes.npcs.api.IDamageSource; -import noppes.npcs.api.ability.IAbility; -import noppes.npcs.api.entity.IEntityLivingBase; -import noppes.npcs.api.entity.IPlayer; -import noppes.npcs.api.event.IPlayerAbilityEvent; -import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.scripted.NpcAPI; - -/** - * Events fired during player ability execution lifecycle. - */ -public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEvent { - protected final Ability ability; - protected final IEntityLivingBase target; - private transient IAbility abilityCopy; - - public PlayerAbilityEvent(IPlayer player, Ability ability, EntityLivingBase target) { - super(player); - this.ability = ability; - this.target = target != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(target) : null; - } - - @Override - public IAbility getAbility() { - if (abilityCopy == null && ability != null) { - abilityCopy = ability.deepCopy(); - } - return abilityCopy; - } - - @Override - public IEntityLivingBase getTarget() { - return target; - } - - @Override - public String getHookName() { - return "playerAbilityEvent"; - } - - /** - * Fired when a player's ability starts executing (enters WINDUP phase). - */ - @Cancelable - public static class StartEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.StartEvent { - public StartEvent(IPlayer player, Ability ability, EntityLivingBase target) { - super(player, ability, target); - } - - @Override - public String getHookName() { - return EnumScriptType.ABILITY_START.function; - } - } - - /** - * Fired when a player's ability enters the ACTIVE phase and performs its effect. - */ - @Cancelable - public static class ExecuteEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.ExecuteEvent { - public ExecuteEvent(IPlayer player, Ability ability, EntityLivingBase target) { - super(player, ability, target); - } - - @Override - public String getHookName() { - return EnumScriptType.ABILITY_EXECUTE.function; - } - } - - /** - * Fired when a player's ability is interrupted by damage. - */ - public static class InterruptEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.InterruptEvent { - private final IDamageSource damageSource; - private final float damage; - - public InterruptEvent(IPlayer player, Ability ability, EntityLivingBase target, DamageSource source, float damage) { - super(player, ability, target); - this.damageSource = source != null ? NpcAPI.Instance().getIDamageSource(source) : null; - this.damage = damage; - } - - @Override - public IDamageSource getDamageSource() { - return damageSource; - } - - @Override - public float getDamage() { - return damage; - } - - @Override - public String getHookName() { - return EnumScriptType.ABILITY_INTERRUPT.function; - } - } - - /** - * Fired when a player's ability completes its full execution cycle. - */ - public static class CompleteEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.CompleteEvent { - public CompleteEvent(IPlayer player, Ability ability, EntityLivingBase target) { - super(player, ability, target); - } - - @Override - public String getHookName() { - return EnumScriptType.ABILITY_COMPLETE.function; - } - } - - /** - * Fired every tick while a player's ability is executing. - */ - public static class TickEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.TickEvent { - private final int abilityPhase; - private final int tick; - - public TickEvent(IPlayer player, Ability ability, EntityLivingBase target, int phase, int tick) { - super(player, ability, target); - this.abilityPhase = phase; - this.tick = tick; - } - - @Override - public int getAbilityPhase() { - return abilityPhase; - } - - @Override - public int getTick() { - return tick; - } - - @Override - public String getHookName() { - return EnumScriptType.ABILITY_TICK.function; - } - } - - /** - * Fired when a player's ability hits an entity with damage. - * Canceling prevents the damage from being applied. - */ - @Cancelable - public static class HitEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.HitEvent { - private final IEntityLivingBase hitEntity; - private float damage; - private float knockback; - private float knockbackUp; - - public HitEvent(IPlayer player, Ability ability, EntityLivingBase target, - EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { - super(player, ability, target); - this.hitEntity = hitEntity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(hitEntity) : null; - this.damage = damage; - this.knockback = knockback; - this.knockbackUp = knockbackUp; - } - - @Override - public IEntityLivingBase getHitEntity() { - return hitEntity; - } - - @Override - public float getDamage() { - return damage; - } - - @Override - public void setDamage(float damage) { - this.damage = damage; - } - - @Override - public float getKnockback() { - return knockback; - } - - @Override - public void setKnockback(float knockback) { - this.knockback = knockback; - } - - @Override - public float getKnockbackUp() { - return knockbackUp; - } - - @Override - public void setKnockbackUp(float knockbackUp) { - this.knockbackUp = knockbackUp; - } - - @Override - public String getHookName() { - return EnumScriptType.ABILITY_HIT.function; - } - } - - /** - * Fired when a toggle ability changes state. - * Canceling prevents the toggle from changing state. - */ - @Cancelable - public static class ToggleEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.ToggleEvent { - private final int oldState; - private final int newState; - - public ToggleEvent(IPlayer player, Ability ability, int oldState, int newState) { - super(player, ability, null); - this.oldState = oldState; - this.newState = newState; - } - - @Override - public boolean isTogglingOn() { - return newState > 0; - } - - @Override - public int getOldState() { - return oldState; - } - - @Override - public int getNewState() { - return newState; - } - - @Override - public String getHookName() { - return EnumScriptType.ABILITY_TOGGLE.function; - } - } - - /** - * Fired every 10 ticks for each active toggle ability. - * Scripts can set enabled to false to force-deactivate. - */ - public static class ToggleUpdateEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.ToggleUpdateEvent { - private final int tick; - private final int state; - private boolean enabled = true; - - public ToggleUpdateEvent(IPlayer player, Ability ability, int tick, int state) { - super(player, ability, null); - this.tick = tick; - this.state = state; - } - - @Override - public int getTick() { - return tick; - } - - @Override - public int getState() { - return state; - } - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @Override - public String getHookName() { - return EnumScriptType.ABILITY_TOGGLE_UPDATE.function; - } - } -} diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java index d475c40ef..9fb509761 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -58,8 +58,8 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public void addAbilityReference(String key) { - // Validate ability exists and allows NPCs - Ability ability = AbilityController.Instance.resolveAbility(key); + // Validate ability exists and allows NPCs (peek avoids deep copy) + Ability ability = AbilityController.Instance.peekAbility(key); if (ability == null) { return; // Ability doesn't exist } @@ -145,6 +145,11 @@ public class ScriptDataAbilities implements IDataAbilities { data.interruptCurrentAbility(null, 0); } + @Override + public void completeCurrentAbility() { + data.completeCurrentAbility(); + } + @Override public int getGlobalCooldown() { return (int) data.getRemainingCooldown(); @@ -191,8 +196,8 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public boolean executeAbility(String key, Object target) { - // Validate ability exists and allows NPCs - Ability ability = AbilityController.Instance.resolveAbility(key); + // Validate ability exists and allows NPCs (peek avoids deep copy) + Ability ability = AbilityController.Instance.peekAbility(key); if (ability == null) { return false; // Ability doesn't exist } diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java index 1860aa259..bb6b1b930 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -28,8 +28,8 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { @Override public void unlockAbility(String key) { - // Validate ability exists and allows players - Ability ability = AbilityController.Instance.resolveAbility(key); + // Validate ability exists and allows players (peek avoids deep copy) + Ability ability = AbilityController.Instance.peekAbility(key); if (ability == null) { return; // Ability doesn't exist } @@ -94,6 +94,11 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { data.interruptCurrentAbility(); } + @Override + public void completeCurrentAbility() { + data.completeCurrentAbility(); + } + @Override public boolean isOnCooldown() { return data.isOnCooldown(); @@ -162,7 +167,7 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { * Uses the same resolution as unlockAbility: registry key for built-in, UUID for custom. */ private String resolveCanonicalKey(String key) { - Ability ability = AbilityController.Instance.resolveAbility(key); + Ability ability = AbilityController.Instance.peekAbility(key); if (ability != null && ability.getId() != null) { return ability.getId(); } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index c407a411b..587629322 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1678,6 +1678,8 @@ telegraph.ring=Ring telegraph.line=Line telegraph.cone=Cone telegraph.point=Point +telegraph.square=Square +telegraph.none=None ability.tab.timing=Timing ability.tab.effects=Effects @@ -1688,6 +1690,13 @@ ability.telegraphSizeNote=Size is based on ability parameters # Ability Effects Tab +ability.cnpc.custom=Custom +ability.telegraphShape=Telegraph Shape +ability.toggleEnabled=Toggle +ability.toggleStates=Toggle States +ability.hasActiveToggle=Active Toggle Tick +ability.section.toggle=Toggle +ability.section.execution=Execution ability.cnpc.slam=Slam ability.cnpc.heavy_hit=Heavy Hit ability.cnpc.cutter=Cutter @@ -1915,6 +1924,7 @@ ability.targetFilter.all=All ability.includeSelf=Include Self ability.section.targeting=Targeting ability.concurrent=Concurrent +ability.customScriptInfo=This ability is for scripting purposes ability.concurrentInfo=This ability allows Concurrency in Chains ability.hover.concurrent=When enabled, this ability fires at the same time as the ability above it in the chain ability.customEffects=Custom Effects @@ -1961,7 +1971,6 @@ ability.lightningRadius=Radius # Projectile Count / Settings ability.projectileCount=Count -ability.dualFireDelay=Fire Delay ability.fireDelay=Fire Delay ability.colorOverride=Color Override @@ -2085,6 +2094,9 @@ ability.section.properties=Properties ability.section.panel=Panel ability.section.launch=Launch ability.section.slicer=Slicer +ability.section.icon=Icon +ability.hasIcon=Custom Icon +ability.icon.tint=Tint Color ability.domeRadius=Dome Radius ability.useHealth=Use Health @@ -2197,6 +2209,9 @@ condition.hover.hitCount=Activates after receiving X hits within the time window condition.hover.hits=Number of hits required to trigger condition condition.hover.within=Time window in ticks (20 ticks = 1 second) condition.hover.threshold=Health percentage threshold (1-99%) +condition.hover.armor_slots=Equipment in specific armor slots +condition.hover.item_name=Filter by item name or identifier +condition.hover.required_count=Number of matching items required # Effect Hover Tooltips effect.hover.type=Potion effect type to apply to targets