Remove all Legacy Ability Content, cleanup API.

- Add Script Tab to Abilities
- Add Custom Ability
- Add Icon Support for Abilities / Default Icon Support
- Remove Icon Tab and throw it into Effects with allowed Override
- Removed Deprecated API Sources
This commit is contained in:
Kam committed 2026-02-27 16:43:35 -05:00
1 parent 936dc7f0cc
commit 675c4ef4f9
50 files changed
+1488 -938

No files matched your search

+1 -1
Submodule src/api updated: aac5f98be8...ae98d12faf.
@@ -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<String, Ability> customAbilities = new LinkedHashMap<>(); // name → Ability
private final Map<String, Ability> customAbilitiesById = new LinkedHashMap<>(); // UUID → Ability
// ── Script Handlers ────────────────────────────────────────────────────
public final HashMap<String, AbilityScript> abilityScriptHandlers = new HashMap<>();
public final HashMap<String, ChainedAbilityScript> chainedAbilityScriptHandlers = new HashMap<>();
// ── Extension Points ─────────────────────────────────────────────────────
private final Map<String, List<AbilityVariant>> externalVariants = new LinkedHashMap<>();
private final List<IAbilityFieldProvider> fieldProviders = new ArrayList<>();
@@ -72,11 +80,6 @@ public class AbilityController implements IAbilityHandler {
private final Map<String, Supplier<AbilityCondition>> conditionTypes = new HashMap<>();
private final List<Predicate<EntityPlayer>> 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<String, ChainedAbility> chainedAbilities = new LinkedHashMap<>(); // name → ChainedAbility
private final Map<String, ChainedAbility> chainedAbilitiesById = new LinkedHashMap<>(); // UUID → ChainedAbility
@@ -97,7 +100,7 @@ public class AbilityController implements IAbilityHandler {
// TYPE REGISTRATION
// ═══════════════════════════════════════════════════════════════════════════
public void registerType(String factoryKey, Supplier<Ability> factory) {
public void registerType(Supplier<Ability> 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<String, String> migratedAbilityNames = new HashMap<String, String>();
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<String, String> 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<String, String> 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<String, String> 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<String, Ability> 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<String, Ability> 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;
}
@@ -337,7 +337,7 @@ public class SyncController {
NBTTagList list = new NBTTagList();
NBTTagCompound compound = new NBTTagCompound();
for (Map.Entry<String, Ability> 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<String, ChainedAbility> entry : AbilityController.Instance.getChainedAbilities().entrySet()) {
NBTTagCompound chainNBT = entry.getValue().writeNBT();
NBTTagCompound chainNBT = entry.getValue().writeNBT(false);
chainNBT.setString("ChainedAbilityId", entry.getKey());
list.appendTag(chainNBT);
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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).
* <p>
* Supports two modes based on toggle configuration:
* <ul>
* <li><b>Instant mode</b> (toggleStates = 0): Uses standard execution lifecycle with
* configurable duration. Script hooks: abilityStart, abilityTick, abilityComplete.</li>
* <li><b>Toggle mode</b> (toggleStates >= 1): Uses toggle lifecycle.
* Script hooks: abilityToggle, abilityToggleTick.</li>
* </ul>
*/
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<FieldDef> 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()));
}
}
@@ -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;
}
@@ -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;
}
}
@@ -50,5 +50,5 @@ public interface IAbilityAction {
*/
IAbilityAction deepCopyAction();
NBTTagCompound writeNBT();
NBTTagCompound writeNBT(boolean saveScripts);
}
@@ -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;
}
}
@@ -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");
}
@@ -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());
@@ -55,6 +55,9 @@ public class AbilityBeam extends AbilityEnergyProjectile<EntityAbilityBeam> 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 ====================
@@ -55,6 +55,9 @@ public class AbilityDisc extends AbilityEnergyProjectile<EntityAbilityDisc> 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 ====================
@@ -192,20 +192,6 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
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));
}
@@ -53,6 +53,9 @@ public class AbilityLaser extends AbilityEnergyProjectile<EntityAbilityLaser> 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 ====================
@@ -49,6 +49,9 @@ public class AbilityOrb extends AbilityEnergyProjectile<EntityAbilityOrb> 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 ====================
@@ -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());
@@ -214,4 +214,6 @@ public enum EnumRequestPacket {
RecipeScript,
ManageAuctionAction,
AbilityScript,
ChainedAbilityScript,
}
@@ -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);
}
@@ -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);
}
}
@@ -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));
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
}
}
@@ -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
}
}
@@ -93,7 +93,7 @@ public class Register<T> {
public void register() {
for (Map.Entry<String, Supplier<Ability>> 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());
}
@@ -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<String, AbilityToggleEntry> 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;
+8 -65
View File
@@ -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<TelegraphInstance> 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
+133 -23
View File
@@ -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:
* <ol>
* <li><b>AbilityScript</b> — fires for BOTH NPC and Player casters</li>
* <li><b>Entity script handler</b> — 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</li>
* <li><b>EVENT_BUS</b> — global event bus, fires last</li>
* </ol>
*
* @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) {
@@ -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));
@@ -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);
@@ -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);
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
}
@@ -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"),
@@ -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
);
@@ -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);
@@ -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);
}
}
@@ -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);
}
}
@@ -26,12 +26,6 @@ public class CustomEffect implements ICustomEffect {
public String icon = "";
public int iconX = 0, iconY = 0;
/**
* Experimental script stuff.
*/
public Consumer<PlayerEvent.EffectEvent.Added> onAddedConsumer;
public Consumer<PlayerEvent.EffectEvent.Ticked> onTickConsumer;
public Consumer<PlayerEvent.EffectEvent.Removed> 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<PlayerEvent.EffectEvent.Added> function) {
onAddedConsumer = function;
}
public void onTick(Consumer<PlayerEvent.EffectEvent.Ticked> function) {
onTickConsumer = function;
}
public void onRemoved(Consumer<PlayerEvent.EffectEvent.Removed> 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;
@@ -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
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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
}
@@ -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();
}
@@ -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