mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-11 23:07:37 +00:00
IAbilityAction interface, AbilityAction unified slot, and bug fixes
- Added IAbilityAction interface (internal + API) for unified Ability/ChainedAbility handling - Created AbilityAction (replaces AbilitySlot) with 3 modes: INLINE_ABILITY, ABILITY_REFERENCE, CHAIN_REFERENCE - Unified DataAbilities storage from dual lists to single List<AbilityAction> - Added ChainedAbility, ChainedAbilityEntry, ChainedAbilityController - Updated PlayerAbilityData with resolveActionKey() for chain: prefix resolution - Added resolveAction/canResolveAction to AbilityController - Fixed unsafe cast in DataAbilities.onDamage() - Fixed NPE in GuiNPCAbilities for chain reference slots - Fixed NPC chain interrupt double cooldown - Fixed removeAbility() incorrectly removing chain references Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f39d843c70
commit
2f814c1903
17 files changed
+1482
-144
No files matched your search
@@ -2,6 +2,7 @@ package kamkeel.npcs.command;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
import net.minecraft.command.ICommandSender;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
@@ -77,7 +78,8 @@ public class AbilityCommand extends CommandKamkeelBase {
|
||||
)
|
||||
public void reload(ICommandSender sender, String[] args) {
|
||||
AbilityController.Instance.load();
|
||||
sendResult(sender, "Abilities reloaded from disk.");
|
||||
ChainedAbilityController.Instance.load();
|
||||
sendResult(sender, "Abilities and chained abilities reloaded from disk.");
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
|
||||
@@ -41,6 +41,8 @@ import noppes.npcs.controllers.data.RecipeAnvil;
|
||||
import noppes.npcs.controllers.data.RecipeCarpentry;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbilityController;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.EnumMap;
|
||||
@@ -76,7 +78,8 @@ public class SyncController {
|
||||
EnumSyncType.CUSTOM_EFFECTS,
|
||||
EnumSyncType.MAGIC,
|
||||
EnumSyncType.MAGIC_CYCLE,
|
||||
EnumSyncType.CUSTOM_ABILITY
|
||||
EnumSyncType.CUSTOM_ABILITY,
|
||||
EnumSyncType.CHAINED_ABILITY
|
||||
};
|
||||
|
||||
public static void load() {
|
||||
@@ -93,6 +96,7 @@ public class SyncController {
|
||||
registerCache(EnumSyncType.MAGIC, SyncController::magicsNBT);
|
||||
registerCache(EnumSyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT);
|
||||
registerCache(EnumSyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT);
|
||||
registerCache(EnumSyncType.CHAINED_ABILITY, SyncController::chainedAbilitiesNBT);
|
||||
}
|
||||
|
||||
public static void syncPlayer(EntityPlayerMP player) {
|
||||
@@ -349,6 +353,27 @@ public class SyncController {
|
||||
updateAllPlayerRevisions(EnumSyncType.CUSTOM_ABILITY, payload.getRevision());
|
||||
}
|
||||
|
||||
public static NBTTagCompound chainedAbilitiesNBT() {
|
||||
NBTTagList list = new NBTTagList();
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
for (Map.Entry<String, ChainedAbility> entry : ChainedAbilityController.Instance.getChainedAbilities().entrySet()) {
|
||||
NBTTagCompound chainNBT = entry.getValue().writeNBT();
|
||||
chainNBT.setString("ChainedAbilityId", entry.getKey());
|
||||
list.appendTag(chainNBT);
|
||||
}
|
||||
compound.setTag("Data", list);
|
||||
return compound;
|
||||
}
|
||||
|
||||
public static void syncAllChainedAbilities() {
|
||||
CachedSyncPayload payload = rebuildNow(EnumSyncType.CHAINED_ABILITY);
|
||||
if (payload == null) {
|
||||
return;
|
||||
}
|
||||
PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CHAINED_ABILITY, payload));
|
||||
updateAllPlayerRevisions(EnumSyncType.CHAINED_ABILITY, payload.getRevision());
|
||||
}
|
||||
|
||||
public static void syncPlayerData(EntityPlayerMP player, boolean update) {
|
||||
PlayerData data = PlayerData.get(player);
|
||||
if (data != null) {
|
||||
@@ -636,6 +661,20 @@ public class SyncController {
|
||||
AbilityController.Instance.setCustomAbilities(sync);
|
||||
break;
|
||||
}
|
||||
case CHAINED_ABILITY: {
|
||||
NBTTagList list = fullCompound.getTagList("Data", 10);
|
||||
LinkedHashMap<String, ChainedAbility> sync = new LinkedHashMap<>();
|
||||
for (int i = 0; i < list.tagCount(); i++) {
|
||||
NBTTagCompound nbt = list.getCompoundTagAt(i);
|
||||
String id = nbt.getString("ChainedAbilityId");
|
||||
ChainedAbility chain = new ChainedAbility();
|
||||
chain.readNBT(nbt);
|
||||
chain.setName(id);
|
||||
sync.put(id, chain);
|
||||
}
|
||||
ChainedAbilityController.Instance.setChainedAbilities(sync);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ClientCacheHandler.updateClientRevision(enumSyncType, revision);
|
||||
@@ -886,6 +925,8 @@ public class SyncController {
|
||||
return EnumSet.of(EnumSyncType.CUSTOM_EFFECTS);
|
||||
case CUSTOM_ABILITY:
|
||||
return EnumSet.of(EnumSyncType.CUSTOM_ABILITY);
|
||||
case CHAINED_ABILITY:
|
||||
return EnumSet.of(EnumSyncType.CHAINED_ABILITY);
|
||||
default:
|
||||
return EnumSet.noneOf(EnumSyncType.class);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ import java.util.List;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
|
||||
public abstract class Ability implements IAbility {
|
||||
public abstract class Ability implements IAbility, IAbilityAction {
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// CONFIGURATION (saved to NBT)
|
||||
@@ -1277,6 +1277,16 @@ public abstract class Ability implements IAbility {
|
||||
return AbilityController.Instance.fromNBT(this.writeNBT());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChain() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IAbilityAction deepCopyAction() {
|
||||
return deepCopy();
|
||||
}
|
||||
|
||||
public boolean isNpcInlineEdit() {
|
||||
return npcInlineEdit;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
/**
|
||||
* Unified slot for storing any combat action: inline ability, ability reference, or chain reference.
|
||||
* Replaces {@link AbilitySlot} with support for all {@link IAbilityAction} types.
|
||||
* <p>
|
||||
* References are lazy-resolved via their respective controllers and cached until
|
||||
* the controller revision changes.
|
||||
*/
|
||||
public class AbilityAction {
|
||||
|
||||
public enum SlotType {
|
||||
INLINE_ABILITY,
|
||||
ABILITY_REFERENCE,
|
||||
CHAIN_REFERENCE
|
||||
}
|
||||
|
||||
private SlotType slotType;
|
||||
private Ability inlineAbility;
|
||||
private String referenceId;
|
||||
|
||||
/** Per-slot enabled override for reference slots (null = use master's default). */
|
||||
private Boolean enabledOverride;
|
||||
|
||||
// Cache
|
||||
private transient IAbilityAction cachedAction;
|
||||
private transient int cachedRevision = -1;
|
||||
|
||||
private AbilityAction() {}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// FACTORY METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public static AbilityAction inline(Ability ability) {
|
||||
AbilityAction slot = new AbilityAction();
|
||||
slot.slotType = SlotType.INLINE_ABILITY;
|
||||
slot.inlineAbility = ability;
|
||||
return slot;
|
||||
}
|
||||
|
||||
public static AbilityAction abilityReference(String referenceId) {
|
||||
AbilityAction slot = new AbilityAction();
|
||||
slot.slotType = SlotType.ABILITY_REFERENCE;
|
||||
slot.referenceId = referenceId;
|
||||
return slot;
|
||||
}
|
||||
|
||||
public static AbilityAction chainReference(String referenceId) {
|
||||
AbilityAction slot = new AbilityAction();
|
||||
slot.slotType = SlotType.CHAIN_REFERENCE;
|
||||
slot.referenceId = referenceId;
|
||||
return slot;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// QUERIES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public SlotType getSlotType() {
|
||||
return slotType;
|
||||
}
|
||||
|
||||
public boolean isReference() {
|
||||
return slotType != SlotType.INLINE_ABILITY;
|
||||
}
|
||||
|
||||
public boolean isChainReference() {
|
||||
return slotType == SlotType.CHAIN_REFERENCE;
|
||||
}
|
||||
|
||||
public boolean isAbilityReference() {
|
||||
return slotType == SlotType.ABILITY_REFERENCE;
|
||||
}
|
||||
|
||||
public String getReferenceId() {
|
||||
return referenceId;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// RESOLUTION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Resolve the action. Inline returns directly; references resolve
|
||||
* from the appropriate controller with version-based caching.
|
||||
*/
|
||||
public IAbilityAction getAction() {
|
||||
switch (slotType) {
|
||||
case INLINE_ABILITY:
|
||||
return inlineAbility;
|
||||
|
||||
case ABILITY_REFERENCE: {
|
||||
AbilityController ctrl = AbilityController.Instance;
|
||||
if (ctrl == null) return null;
|
||||
int rev = ctrl.getCustomAbilityRevision();
|
||||
if (cachedAction != null && cachedRevision == rev) {
|
||||
return cachedAction;
|
||||
}
|
||||
Ability resolved = ctrl.resolveAbility(referenceId);
|
||||
if (resolved != null && enabledOverride != null) {
|
||||
resolved.setEnabled(enabledOverride);
|
||||
}
|
||||
cachedAction = resolved;
|
||||
cachedRevision = rev;
|
||||
return cachedAction;
|
||||
}
|
||||
|
||||
case CHAIN_REFERENCE: {
|
||||
ChainedAbilityController ctrl = ChainedAbilityController.Instance;
|
||||
if (ctrl == null) return null;
|
||||
int rev = ctrl.getRevision();
|
||||
if (cachedAction != null && cachedRevision == rev) {
|
||||
return cachedAction;
|
||||
}
|
||||
ChainedAbility resolved = ctrl.resolve(referenceId);
|
||||
if (resolved != null && enabledOverride != null) {
|
||||
resolved.setEnabled(enabledOverride);
|
||||
}
|
||||
cachedAction = resolved;
|
||||
cachedRevision = rev;
|
||||
return cachedAction;
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get as Ability (returns null if this is a chain slot or resolution fails).
|
||||
*/
|
||||
public Ability getAbility() {
|
||||
IAbilityAction action = getAction();
|
||||
return (action instanceof Ability) ? (Ability) action : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get as ChainedAbility (returns null if this is an ability slot or resolution fails).
|
||||
*/
|
||||
public ChainedAbility getChainedAbility() {
|
||||
IAbilityAction action = getAction();
|
||||
return (action instanceof ChainedAbility) ? (ChainedAbility) action : null;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ENABLED OVERRIDE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
if (slotType == SlotType.INLINE_ABILITY) {
|
||||
if (inlineAbility != null) {
|
||||
inlineAbility.setEnabled(enabled);
|
||||
}
|
||||
} else {
|
||||
this.enabledOverride = enabled;
|
||||
if (cachedAction != null) {
|
||||
cachedAction.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// INLINE CONVERSION (ability references only)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public Ability getInlineAbility() {
|
||||
return inlineAbility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this reference slot to an inline slot by copying the resolved ability.
|
||||
* Only works for ability references (chains cannot be inlined).
|
||||
*/
|
||||
public boolean convertToInline() {
|
||||
if (slotType != SlotType.ABILITY_REFERENCE) return false;
|
||||
|
||||
Ability resolved = getAbility();
|
||||
if (resolved == null) return false;
|
||||
|
||||
inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT());
|
||||
slotType = SlotType.INLINE_ABILITY;
|
||||
referenceId = null;
|
||||
cachedAction = null;
|
||||
cachedRevision = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// NBT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public NBTTagCompound writeNBT() {
|
||||
switch (slotType) {
|
||||
case CHAIN_REFERENCE: {
|
||||
NBTTagCompound nbt = new NBTTagCompound();
|
||||
nbt.setString("ChainReference", referenceId);
|
||||
if (enabledOverride != null) {
|
||||
nbt.setBoolean("RefEnabled", enabledOverride);
|
||||
}
|
||||
return nbt;
|
||||
}
|
||||
|
||||
case ABILITY_REFERENCE: {
|
||||
NBTTagCompound nbt = new NBTTagCompound();
|
||||
nbt.setString("Reference", referenceId);
|
||||
if (enabledOverride != null) {
|
||||
nbt.setBoolean("RefEnabled", enabledOverride);
|
||||
}
|
||||
return nbt;
|
||||
}
|
||||
|
||||
case INLINE_ABILITY:
|
||||
default:
|
||||
return inlineAbility != null ? inlineAbility.writeNBT() : new NBTTagCompound();
|
||||
}
|
||||
}
|
||||
|
||||
public static AbilityAction fromNBT(NBTTagCompound nbt) {
|
||||
if (nbt == null) return null;
|
||||
|
||||
// Chain reference (new format)
|
||||
if (nbt.hasKey("ChainReference")) {
|
||||
AbilityAction slot = chainReference(nbt.getString("ChainReference"));
|
||||
if (nbt.hasKey("RefEnabled")) {
|
||||
slot.enabledOverride = nbt.getBoolean("RefEnabled");
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Ability reference (existing format)
|
||||
if (nbt.hasKey("Reference")) {
|
||||
AbilityAction slot = abilityReference(nbt.getString("Reference"));
|
||||
if (nbt.hasKey("RefEnabled")) {
|
||||
slot.enabledOverride = nbt.getBoolean("RefEnabled");
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Inline ability (existing format)
|
||||
Ability ability = AbilityController.Instance.fromNBT(nbt);
|
||||
if (ability == null) return null;
|
||||
return inline(ability);
|
||||
}
|
||||
}
|
||||
@@ -448,6 +448,36 @@ public class AbilityController implements IAbilityHandler {
|
||||
return false;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// UNIFIED ACTION RESOLUTION
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Resolve any action key to an IAbilityAction.
|
||||
* Checks abilities first, then chained abilities.
|
||||
*/
|
||||
public IAbilityAction resolveAction(String key) {
|
||||
if (key == null || key.isEmpty()) return null;
|
||||
|
||||
Ability ability = resolveAbility(key);
|
||||
if (ability != null) return ability;
|
||||
|
||||
if (ChainedAbilityController.Instance != null) {
|
||||
return ChainedAbilityController.Instance.resolve(key);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key can be resolved as any action (ability or chain).
|
||||
*/
|
||||
public boolean canResolveAction(String key) {
|
||||
if (canResolveAbility(key)) return true;
|
||||
return ChainedAbilityController.Instance != null
|
||||
&& ChainedAbilityController.Instance.canResolve(key);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// EXTENSION POINTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -621,4 +651,19 @@ public class AbilityController implements IAbilityHandler {
|
||||
public boolean deleteCustomAbilityByName(String name) {
|
||||
return deleteCustomAbility(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getChainedAbilityNames() {
|
||||
return ChainedAbilityController.Instance.getNames().toArray(new String[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasChainedAbilityName(String name) {
|
||||
return ChainedAbilityController.Instance.hasName(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteChainedAbilityByName(String name) {
|
||||
return ChainedAbilityController.Instance.delete(name);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,10 @@ import net.minecraft.nbt.NBTTagCompound;
|
||||
* <p>
|
||||
* References are lazy-resolved via {@link AbilityController#resolveAbility(String)}
|
||||
* and cached until the controller version changes.
|
||||
*
|
||||
* @deprecated Use {@link AbilityAction} instead, which supports abilities and chained abilities.
|
||||
*/
|
||||
@Deprecated
|
||||
public class AbilitySlot {
|
||||
|
||||
private Ability inlineAbility;
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import noppes.npcs.api.ability.IChainedAbility;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A chained ability is an ordered sequence of ability references that execute
|
||||
* one after another. It has its own conditions, cooldown, weight, and per-entry
|
||||
* delay configuration. Individual ability conditions are skipped during chain execution.
|
||||
* <p>
|
||||
* All entries must be ability references (by name/key). Inline abilities are not supported.
|
||||
* When saving, all references are validated against {@link AbilityController}.
|
||||
*/
|
||||
public class ChainedAbility implements IChainedAbility, IAbilityAction {
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// CONFIGURATION (saved to NBT)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private String name = "";
|
||||
|
||||
private boolean enabled = true;
|
||||
|
||||
/** Weight for NPC weighted random selection. */
|
||||
private int weight = 10;
|
||||
|
||||
/** Whether each ability in the chain uses its own windup phase. */
|
||||
private boolean windUpAll = true;
|
||||
|
||||
/** Chain-level cooldown in ticks (applied after entire chain completes). */
|
||||
private int cooldownTicks = 100;
|
||||
|
||||
/** Minimum range for NPC eligibility check. */
|
||||
private float minRange = 0;
|
||||
|
||||
/** Maximum range for NPC eligibility check. */
|
||||
private float maxRange = 20;
|
||||
|
||||
/** Which entity types can use this chained ability. */
|
||||
private UserType allowedBy = UserType.BOTH;
|
||||
|
||||
/** Chain-level conditions (individual ability conditions are ignored). */
|
||||
private List<Condition> conditions = new ArrayList<>();
|
||||
|
||||
/** Ordered list of ability entries to execute. */
|
||||
private List<ChainedAbilityEntry> entries = new ArrayList<>();
|
||||
|
||||
public ChainedAbility() {}
|
||||
|
||||
public ChainedAbility(String name) {
|
||||
this.name = name != null ? name : "";
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GETTERS / SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name != null ? name : "";
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public int getWeight() {
|
||||
return weight;
|
||||
}
|
||||
|
||||
public void setWeight(int weight) {
|
||||
this.weight = Math.max(1, weight);
|
||||
}
|
||||
|
||||
public boolean isWindUpAll() {
|
||||
return windUpAll;
|
||||
}
|
||||
|
||||
public void setWindUpAll(boolean windUpAll) {
|
||||
this.windUpAll = windUpAll;
|
||||
}
|
||||
|
||||
public int getCooldownTicks() {
|
||||
return cooldownTicks;
|
||||
}
|
||||
|
||||
public void setCooldownTicks(int cooldownTicks) {
|
||||
this.cooldownTicks = Math.max(0, cooldownTicks);
|
||||
}
|
||||
|
||||
public float getMinRange() {
|
||||
return minRange;
|
||||
}
|
||||
|
||||
public void setMinRange(float minRange) {
|
||||
this.minRange = Math.max(0, minRange);
|
||||
}
|
||||
|
||||
public float getMaxRange() {
|
||||
return maxRange;
|
||||
}
|
||||
|
||||
public void setMaxRange(float maxRange) {
|
||||
this.maxRange = Math.max(0, maxRange);
|
||||
}
|
||||
|
||||
public UserType getAllowedBy() {
|
||||
return allowedBy;
|
||||
}
|
||||
|
||||
public void setAllowedBy(UserType allowedBy) {
|
||||
this.allowedBy = allowedBy != null ? allowedBy : UserType.BOTH;
|
||||
}
|
||||
|
||||
public List<Condition> getConditions() {
|
||||
return conditions;
|
||||
}
|
||||
|
||||
public void setConditions(List<Condition> conditions) {
|
||||
this.conditions = conditions != null ? conditions : new ArrayList<>();
|
||||
}
|
||||
|
||||
public List<ChainedAbilityEntry> getEntries() {
|
||||
return entries;
|
||||
}
|
||||
|
||||
public void setEntries(List<ChainedAbilityEntry> entries) {
|
||||
this.entries = entries != null ? entries : new ArrayList<>();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// IChainedAbility INTERFACE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
public int getEntryCount() {
|
||||
return entries.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEntryReference(int index) {
|
||||
if (index < 0 || index >= entries.size()) return null;
|
||||
return entries.get(index).getAbilityReference();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEntryDelay(int index) {
|
||||
if (index < 0 || index >= entries.size()) return 0;
|
||||
return entries.get(index).getDelayTicks();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ENTRY MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public void addEntry(ChainedAbilityEntry entry) {
|
||||
if (entry != null) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
public void addEntry(String abilityReference, int delayTicks) {
|
||||
entries.add(new ChainedAbilityEntry(abilityReference, delayTicks));
|
||||
}
|
||||
|
||||
public void removeEntry(int index) {
|
||||
if (index >= 0 && index < entries.size()) {
|
||||
entries.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
public void moveEntryUp(int index) {
|
||||
if (index > 0 && index < entries.size()) {
|
||||
ChainedAbilityEntry entry = entries.remove(index);
|
||||
entries.add(index - 1, entry);
|
||||
}
|
||||
}
|
||||
|
||||
public void moveEntryDown(int index) {
|
||||
if (index >= 0 && index < entries.size() - 1) {
|
||||
ChainedAbilityEntry entry = entries.remove(index);
|
||||
entries.add(index + 1, entry);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// CONDITIONS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Check all chain-level conditions against the caster and target.
|
||||
*/
|
||||
public boolean checkConditions(EntityLivingBase caster, EntityLivingBase target) {
|
||||
for (Condition c : conditions) {
|
||||
if (!c.check(caster, target)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check conditions for player use (skip target-requiring conditions).
|
||||
*/
|
||||
public boolean checkConditionsForPlayer(EntityLivingBase caster) {
|
||||
for (Condition c : conditions) {
|
||||
if (c.requiresTarget()) continue;
|
||||
if (!c.check(caster, null)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// VALIDATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Validate this chained ability. Returns a list of error messages.
|
||||
* An empty list means valid.
|
||||
*/
|
||||
public List<String> validate() {
|
||||
List<String> errors = new ArrayList<>();
|
||||
|
||||
if (name == null || name.isEmpty()) {
|
||||
errors.add("Name is required");
|
||||
}
|
||||
|
||||
if (entries.isEmpty()) {
|
||||
errors.add("At least one ability entry is required");
|
||||
}
|
||||
|
||||
AbilityController controller = AbilityController.Instance;
|
||||
if (controller != null) {
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
ChainedAbilityEntry entry = entries.get(i);
|
||||
String ref = entry.getAbilityReference();
|
||||
if (ref == null || ref.isEmpty()) {
|
||||
errors.add("Entry " + (i + 1) + ": ability reference is empty");
|
||||
} else if (!controller.canResolveAbility(ref)) {
|
||||
errors.add("Entry " + (i + 1) + ": cannot resolve ability '" + ref + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// DEEP COPY
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Create a deep copy via NBT round-trip.
|
||||
*/
|
||||
public ChainedAbility deepCopy() {
|
||||
ChainedAbility copy = new ChainedAbility();
|
||||
copy.readNBT(this.writeNBT());
|
||||
return copy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChain() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IAbilityAction deepCopyAction() {
|
||||
return deepCopy();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// NBT SERIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public NBTTagCompound writeNBT() {
|
||||
NBTTagCompound nbt = new NBTTagCompound();
|
||||
nbt.setString("Name", name);
|
||||
nbt.setBoolean("Enabled", enabled);
|
||||
nbt.setInteger("Weight", weight);
|
||||
nbt.setBoolean("WindUpAll", windUpAll);
|
||||
nbt.setInteger("CooldownTicks", cooldownTicks);
|
||||
nbt.setFloat("MinRange", minRange);
|
||||
nbt.setFloat("MaxRange", maxRange);
|
||||
nbt.setInteger("AllowedBy", allowedBy.ordinal());
|
||||
|
||||
// Conditions
|
||||
NBTTagList condList = new NBTTagList();
|
||||
for (Condition c : conditions) {
|
||||
condList.appendTag(c.writeNBT());
|
||||
}
|
||||
nbt.setTag("Conditions", condList);
|
||||
|
||||
// Entries
|
||||
NBTTagList entryList = new NBTTagList();
|
||||
for (ChainedAbilityEntry entry : entries) {
|
||||
entryList.appendTag(entry.writeNBT());
|
||||
}
|
||||
nbt.setTag("Entries", entryList);
|
||||
|
||||
return nbt;
|
||||
}
|
||||
|
||||
public void readNBT(NBTTagCompound nbt) {
|
||||
name = nbt.getString("Name");
|
||||
enabled = !nbt.hasKey("Enabled") || nbt.getBoolean("Enabled");
|
||||
weight = nbt.hasKey("Weight") ? nbt.getInteger("Weight") : 10;
|
||||
windUpAll = !nbt.hasKey("WindUpAll") || nbt.getBoolean("WindUpAll");
|
||||
cooldownTicks = nbt.hasKey("CooldownTicks") ? nbt.getInteger("CooldownTicks") : 100;
|
||||
minRange = nbt.getFloat("MinRange");
|
||||
maxRange = nbt.hasKey("MaxRange") ? nbt.getFloat("MaxRange") : 20;
|
||||
allowedBy = UserType.fromOrdinal(nbt.getInteger("AllowedBy"));
|
||||
|
||||
// Conditions
|
||||
conditions.clear();
|
||||
NBTTagList condList = nbt.getTagList("Conditions", 10);
|
||||
for (int i = 0; i < condList.tagCount(); i++) {
|
||||
Condition c = Condition.fromNBT(condList.getCompoundTagAt(i));
|
||||
if (c != null) {
|
||||
conditions.add(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Entries
|
||||
entries.clear();
|
||||
NBTTagList entryList = nbt.getTagList("Entries", 10);
|
||||
for (int i = 0; i < entryList.tagCount(); i++) {
|
||||
ChainedAbilityEntry entry = ChainedAbilityEntry.fromNBT(entryList.getCompoundTagAt(i));
|
||||
if (entry != null) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import kamkeel.npcs.controllers.SyncController;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.LogWriter;
|
||||
import noppes.npcs.util.NBTJsonUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Global registry and storage for {@link ChainedAbility} definitions.
|
||||
* Follows the same patterns as {@link AbilityController} for custom abilities:
|
||||
* JSON file storage, revision tracking for cache invalidation, and client sync.
|
||||
*/
|
||||
public class ChainedAbilityController {
|
||||
|
||||
public static ChainedAbilityController Instance = new ChainedAbilityController();
|
||||
|
||||
private final Map<String, ChainedAbility> chainedAbilities = new LinkedHashMap<>();
|
||||
|
||||
/** Incremented on any change; used for cache invalidation. */
|
||||
private int revision = 0;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// LOAD / SAVE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Load all chained abilities from disk.
|
||||
*/
|
||||
public void load() {
|
||||
chainedAbilities.clear();
|
||||
|
||||
File dir = getDir();
|
||||
File[] files = dir.exists() ? dir.listFiles() : null;
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
|
||||
try {
|
||||
String filename = file.getName();
|
||||
String key = filename.substring(0, filename.length() - 5);
|
||||
|
||||
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
|
||||
ChainedAbility chain = new ChainedAbility();
|
||||
chain.readNBT(nbt);
|
||||
|
||||
// Use filename as authoritative key
|
||||
chain.setName(key);
|
||||
chainedAbilities.put(key, chain);
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
revision++;
|
||||
LogWriter.info("Loaded " + chainedAbilities.size() + " chained abilities");
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a chained ability to disk. Handles renames and uniqueness.
|
||||
*/
|
||||
public boolean save(ChainedAbility chain) {
|
||||
if (chain == null) return false;
|
||||
|
||||
String name = chain.getName();
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
|
||||
File dir = getDir();
|
||||
File fileNew = new File(dir, name + ".json_new");
|
||||
File fileCurrent = new File(dir, name + ".json");
|
||||
|
||||
try {
|
||||
NBTTagCompound nbt = chain.writeNBT();
|
||||
NBTJsonUtil.SaveFile(fileNew, nbt);
|
||||
|
||||
if (fileCurrent.exists()) {
|
||||
fileCurrent.delete();
|
||||
}
|
||||
fileNew.renameTo(fileCurrent);
|
||||
if (fileNew.exists()) {
|
||||
fileNew.delete();
|
||||
}
|
||||
|
||||
chainedAbilities.put(name, chain);
|
||||
revision++;
|
||||
LogWriter.info("Saved chained ability: " + name);
|
||||
SyncController.syncAllChainedAbilities();
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("Error saving chained ability: " + name, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a chained ability by name.
|
||||
*/
|
||||
public boolean delete(String name) {
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
|
||||
ChainedAbility removed = chainedAbilities.remove(name);
|
||||
if (removed == null) return false;
|
||||
|
||||
File dir = getDir();
|
||||
File file = new File(dir, name + ".json");
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
revision++;
|
||||
LogWriter.info("Deleted chained ability: " + name);
|
||||
SyncController.syncAllChainedAbilities();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// RESOLUTION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Resolve a chained ability by name, returning a deep copy.
|
||||
* Tries exact match first, then case-insensitive.
|
||||
*/
|
||||
public ChainedAbility resolve(String name) {
|
||||
if (name == null || name.isEmpty()) return null;
|
||||
|
||||
// Exact match
|
||||
ChainedAbility chain = chainedAbilities.get(name);
|
||||
if (chain != null) return chain.deepCopy();
|
||||
|
||||
// Case-insensitive match
|
||||
for (Map.Entry<String, ChainedAbility> entry : chainedAbilities.entrySet()) {
|
||||
if (entry.getKey().equalsIgnoreCase(name)) {
|
||||
return entry.getValue().deepCopy();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a name can be resolved without creating a deep copy.
|
||||
*/
|
||||
public boolean canResolve(String name) {
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
|
||||
if (chainedAbilities.containsKey(name)) return true;
|
||||
|
||||
for (String key : chainedAbilities.keySet()) {
|
||||
if (key.equalsIgnoreCase(name)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// QUERIES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public ChainedAbility get(String name) {
|
||||
return chainedAbilities.get(name);
|
||||
}
|
||||
|
||||
public Set<String> getNames() {
|
||||
return new LinkedHashSet<>(chainedAbilities.keySet());
|
||||
}
|
||||
|
||||
public Map<String, ChainedAbility> getChainedAbilities() {
|
||||
return chainedAbilities;
|
||||
}
|
||||
|
||||
public boolean hasName(String name) {
|
||||
return chainedAbilities.containsKey(name);
|
||||
}
|
||||
|
||||
public int getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// CLIENT SYNC
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Set chained abilities from sync payload (client-side).
|
||||
*/
|
||||
public void setChainedAbilities(Map<String, ChainedAbility> synced) {
|
||||
chainedAbilities.clear();
|
||||
chainedAbilities.putAll(synced);
|
||||
revision++;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// STORAGE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private File getDir() {
|
||||
File dir = new File(CustomNpcs.getWorldSaveDirectory(), "chained_abilities");
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
/**
|
||||
* A single entry in a {@link ChainedAbility}, representing one ability
|
||||
* to execute in sequence along with its delay configuration.
|
||||
*/
|
||||
public class ChainedAbilityEntry {
|
||||
|
||||
/** Reference key (built-in name or custom ability name). */
|
||||
private String abilityReference = "";
|
||||
|
||||
/** Delay in ticks BEFORE this ability starts (after previous entry completes). */
|
||||
private int delayTicks = 0;
|
||||
|
||||
public ChainedAbilityEntry() {}
|
||||
|
||||
public ChainedAbilityEntry(String abilityReference, int delayTicks) {
|
||||
this.abilityReference = abilityReference != null ? abilityReference : "";
|
||||
this.delayTicks = Math.max(0, delayTicks);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GETTERS / SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public String getAbilityReference() {
|
||||
return abilityReference;
|
||||
}
|
||||
|
||||
public void setAbilityReference(String abilityReference) {
|
||||
this.abilityReference = abilityReference != null ? abilityReference : "";
|
||||
}
|
||||
|
||||
public int getDelayTicks() {
|
||||
return delayTicks;
|
||||
}
|
||||
|
||||
public void setDelayTicks(int delayTicks) {
|
||||
this.delayTicks = Math.max(0, delayTicks);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// NBT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public NBTTagCompound writeNBT() {
|
||||
NBTTagCompound nbt = new NBTTagCompound();
|
||||
nbt.setString("Reference", abilityReference);
|
||||
nbt.setInteger("Delay", delayTicks);
|
||||
return nbt;
|
||||
}
|
||||
|
||||
public static ChainedAbilityEntry fromNBT(NBTTagCompound nbt) {
|
||||
if (nbt == null) return null;
|
||||
|
||||
ChainedAbilityEntry entry = new ChainedAbilityEntry();
|
||||
entry.abilityReference = nbt.getString("Reference");
|
||||
entry.delayTicks = Math.max(0, nbt.getInteger("Delay"));
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Common interface for all combat actions that can be assigned to NPCs or players.
|
||||
* Implemented by {@link Ability} (individual ability) and {@link ChainedAbility} (sequence).
|
||||
* <p>
|
||||
* This enables unified storage via {@link AbilityAction}, unified weighted random selection,
|
||||
* and unified eligibility checking without instanceof or separate codepaths.
|
||||
*/
|
||||
public interface IAbilityAction {
|
||||
|
||||
String getName();
|
||||
|
||||
boolean isEnabled();
|
||||
|
||||
void setEnabled(boolean enabled);
|
||||
|
||||
int getWeight();
|
||||
|
||||
int getCooldownTicks();
|
||||
|
||||
float getMinRange();
|
||||
|
||||
float getMaxRange();
|
||||
|
||||
UserType getAllowedBy();
|
||||
|
||||
List<Condition> getConditions();
|
||||
|
||||
boolean checkConditions(EntityLivingBase caster, EntityLivingBase target);
|
||||
|
||||
boolean checkConditionsForPlayer(EntityLivingBase caster);
|
||||
|
||||
/**
|
||||
* Whether this action is a chained ability (sequence of abilities) vs an individual ability.
|
||||
*/
|
||||
boolean isChain();
|
||||
|
||||
/**
|
||||
* Create a deep copy of this action.
|
||||
*/
|
||||
IAbilityAction deepCopyAction();
|
||||
|
||||
NBTTagCompound writeNBT();
|
||||
}
|
||||
@@ -13,5 +13,6 @@ public enum EnumSyncType {
|
||||
CARPENTRY_RECIPES,
|
||||
ANVIL_RECIPES,
|
||||
CUSTOM_EFFECTS,
|
||||
CUSTOM_ABILITY
|
||||
CUSTOM_ABILITY,
|
||||
CHAINED_ABILITY
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package noppes.npcs;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.util.DamageSource;
|
||||
@@ -45,6 +47,14 @@ public abstract class AbstractDataAbilities {
|
||||
protected double lockedPosY = 0;
|
||||
protected double lockedPosZ = 0;
|
||||
|
||||
/** Chained ability execution state */
|
||||
protected ChainedAbility currentChain;
|
||||
protected int chainEntryIndex = -1;
|
||||
protected int chainDelayRemaining = -1;
|
||||
|
||||
/** Set when interrupt already rolled cooldown (prevents double cooldown on NPC dazed completion). */
|
||||
protected boolean interruptCooldownRolled = false;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ABSTRACT METHODS - Subclasses must implement
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -108,6 +118,12 @@ public abstract class AbstractDataAbilities {
|
||||
/** Additional completion logic (NPC clears lastTarget, Player clears key/target and syncs). */
|
||||
protected abstract void onAbilityComplete();
|
||||
|
||||
/**
|
||||
* Roll cooldown after a chained ability completes.
|
||||
* NPC: random(min, max) + chain.cooldownTicks. Player: chain.cooldownTicks only.
|
||||
*/
|
||||
protected abstract void rollChainCooldown(ChainedAbility chain);
|
||||
|
||||
|
||||
// -- Hooks (optional overrides) --
|
||||
|
||||
@@ -120,6 +136,9 @@ public abstract class AbstractDataAbilities {
|
||||
/** Additional lock releases in BURST_DELAY. NPC releases hitScan. */
|
||||
protected void onBurstDelayReleaseLocks() {}
|
||||
|
||||
/** Hook for retargeting during chain execution when target dies. NPC overrides, Player returns null. */
|
||||
protected EntityLivingBase retargetForChain() { return null; }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SHARED ANIMATION METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -168,6 +187,16 @@ public abstract class AbstractDataAbilities {
|
||||
return currentAbility;
|
||||
}
|
||||
|
||||
/** Check if a chained ability is currently executing. */
|
||||
public boolean isExecutingChain() {
|
||||
return currentChain != null;
|
||||
}
|
||||
|
||||
/** Get the currently executing chained ability. */
|
||||
public ChainedAbility getCurrentChain() {
|
||||
return currentChain;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// COOLDOWN MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -242,6 +271,24 @@ public abstract class AbstractDataAbilities {
|
||||
* Subclass-specific behavior is injected via abstract methods and hooks.
|
||||
*/
|
||||
protected void tickCurrentAbility() {
|
||||
// Handle chain delay between entries
|
||||
if (chainDelayRemaining > 0) {
|
||||
chainDelayRemaining--;
|
||||
if (chainDelayRemaining <= 0) {
|
||||
EntityLivingBase chainTarget = getTarget();
|
||||
// Check if target is dead and retarget
|
||||
if (currentChain != null && chainTarget != null && chainTarget.isDead) {
|
||||
chainTarget = retargetForChain();
|
||||
if (chainTarget == null) {
|
||||
completeChain();
|
||||
return;
|
||||
}
|
||||
}
|
||||
startChainEntry(chainTarget);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
EntityLivingBase entity = getEntity();
|
||||
EntityLivingBase target = getTarget();
|
||||
AbilityPhase oldPhase = currentAbility.getPhase();
|
||||
@@ -381,6 +428,7 @@ public abstract class AbstractDataAbilities {
|
||||
|
||||
/**
|
||||
* Handle ability completion. Called when ability phase becomes IDLE.
|
||||
* If a chain is active, advances to the next entry instead of completing.
|
||||
*/
|
||||
protected void handleAbilityCompletion(EntityLivingBase target) {
|
||||
if (currentAbility == null) return;
|
||||
@@ -400,10 +448,44 @@ public abstract class AbstractDataAbilities {
|
||||
// Release locks
|
||||
releaseRotationControl();
|
||||
releaseLockedPosition();
|
||||
|
||||
// Subclass-specific completion (cooldown, cleanup, sync)
|
||||
rollCooldown(currentAbility);
|
||||
stopAbilityAnimation();
|
||||
|
||||
// Chain mode: advance to next entry instead of completing
|
||||
if (currentChain != null) {
|
||||
chainEntryIndex++;
|
||||
if (chainEntryIndex < currentChain.getEntries().size()) {
|
||||
ChainedAbilityEntry nextEntry = currentChain.getEntries().get(chainEntryIndex);
|
||||
|
||||
// Check if target died and retarget
|
||||
if (target != null && target.isDead) {
|
||||
target = retargetForChain();
|
||||
if (target == null) {
|
||||
completeChain();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int delay = nextEntry.getDelayTicks();
|
||||
if (delay > 0) {
|
||||
chainDelayRemaining = delay;
|
||||
currentAbility = null;
|
||||
} else {
|
||||
startChainEntry(target);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// All entries complete
|
||||
completeChain();
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal (non-chain) completion
|
||||
if (interruptCooldownRolled) {
|
||||
// Cooldown was already rolled during interrupt (e.g., chain interrupted, NPC ticked through DAZED)
|
||||
interruptCooldownRolled = false;
|
||||
} else {
|
||||
rollCooldown(currentAbility);
|
||||
}
|
||||
onAbilityComplete();
|
||||
}
|
||||
|
||||
@@ -465,6 +547,15 @@ public abstract class AbstractDataAbilities {
|
||||
releaseRotationControl();
|
||||
releaseLockedPosition();
|
||||
|
||||
// If chain was active, roll chain cooldown and clear chain state
|
||||
if (currentChain != null) {
|
||||
rollChainCooldown(currentChain);
|
||||
interruptCooldownRolled = true;
|
||||
currentChain = null;
|
||||
chainEntryIndex = -1;
|
||||
chainDelayRemaining = -1;
|
||||
}
|
||||
|
||||
// Let subclass handle cleanup
|
||||
onInterruptComplete();
|
||||
}
|
||||
@@ -475,4 +566,112 @@ public abstract class AbstractDataAbilities {
|
||||
* NPC: keeps currentAbility (ticks through DAZED). Player: clears immediately.
|
||||
*/
|
||||
protected void onInterruptComplete() {}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// CHAINED ABILITY EXECUTION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Start executing a chained ability. Sets up chain state and starts the first entry.
|
||||
*
|
||||
* @param chain The chained ability to execute (should be a deep copy)
|
||||
* @param target The initial target
|
||||
* @return true if the chain was started
|
||||
*/
|
||||
protected boolean startChain(ChainedAbility chain, EntityLivingBase target) {
|
||||
if (chain == null || chain.getEntries().isEmpty()) return false;
|
||||
|
||||
currentChain = chain;
|
||||
chainEntryIndex = 0;
|
||||
chainDelayRemaining = -1;
|
||||
|
||||
// Check if first entry has a delay
|
||||
ChainedAbilityEntry firstEntry = chain.getEntries().get(0);
|
||||
int delay = firstEntry.getDelayTicks();
|
||||
if (delay > 0) {
|
||||
chainDelayRemaining = delay;
|
||||
return true;
|
||||
}
|
||||
|
||||
return startChainEntry(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the current chain entry's ability.
|
||||
*/
|
||||
protected boolean startChainEntry(EntityLivingBase target) {
|
||||
if (currentChain == null || chainEntryIndex < 0 || chainEntryIndex >= currentChain.getEntries().size()) {
|
||||
completeChain();
|
||||
return false;
|
||||
}
|
||||
|
||||
ChainedAbilityEntry entry = currentChain.getEntries().get(chainEntryIndex);
|
||||
Ability ability = AbilityController.Instance.resolveAbility(entry.getAbilityReference());
|
||||
if (ability == null) {
|
||||
// Broken reference - complete chain
|
||||
completeChain();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate the resolved ability's UserType matches the chain's allowed context.
|
||||
// The chain's own UserType was already checked at selection time, but individual
|
||||
// abilities may have stricter restrictions (e.g., PLAYER_ONLY in an NPC chain).
|
||||
if (!ability.getAllowedBy().allowsNpc() && getEntity() instanceof noppes.npcs.entity.EntityNPCInterface) {
|
||||
// Skip this entry - ability doesn't allow NPCs
|
||||
completeChain();
|
||||
return false;
|
||||
}
|
||||
if (!ability.getAllowedBy().allowsPlayer() && getEntity() instanceof net.minecraft.entity.player.EntityPlayer) {
|
||||
// Skip this entry - ability doesn't allow players
|
||||
completeChain();
|
||||
return false;
|
||||
}
|
||||
|
||||
// If windUpAll=false, force windup to 0 (skip individual windups)
|
||||
if (!currentChain.isWindUpAll()) {
|
||||
ability.setWindUpTicks(0);
|
||||
}
|
||||
|
||||
// Start the ability (skip its own conditions - chain conditions were already checked)
|
||||
currentAbility = ability;
|
||||
ability.start(target);
|
||||
|
||||
if (ability.getPhase() == AbilityPhase.ACTIVE) {
|
||||
// Windup was 0 - capture locks for immediate active phase, then execute
|
||||
if (ability.isRotationLockedDuringActive()) {
|
||||
captureLockedRotation();
|
||||
}
|
||||
if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) {
|
||||
captureLockedPosition();
|
||||
}
|
||||
executeImmediate(ability, target);
|
||||
} else {
|
||||
// Normal windup flow
|
||||
if (ability.isRotationLockedDuringWindup()) {
|
||||
captureLockedRotation();
|
||||
}
|
||||
if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) {
|
||||
captureLockedPosition();
|
||||
}
|
||||
spawnTelegraph(ability, target);
|
||||
playAbilitySound(ability.getWindUpSound());
|
||||
playAbilityAnimation(ability.getWindUpAnimation());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete the chain execution. Rolls chain cooldown and clears all chain state.
|
||||
*/
|
||||
protected void completeChain() {
|
||||
if (currentChain != null) {
|
||||
rollChainCooldown(currentChain);
|
||||
}
|
||||
currentChain = null;
|
||||
chainEntryIndex = -1;
|
||||
chainDelayRemaining = -1;
|
||||
currentAbility = null;
|
||||
onAbilityComplete();
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import kamkeel.npcs.controllers.ProfileController;
|
||||
import kamkeel.npcs.controllers.SyncController;
|
||||
import kamkeel.npcs.controllers.TelegraphController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbilityController;
|
||||
import kamkeel.npcs.controllers.data.profile.CNPCData;
|
||||
import kamkeel.npcs.developer.Developer;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
@@ -355,6 +356,7 @@ public class CustomNpcs {
|
||||
new SpawnController();
|
||||
new LinkedNpcController();
|
||||
AbilityController.Instance.load();
|
||||
ChainedAbilityController.Instance.load();
|
||||
TelegraphController.init();
|
||||
|
||||
LinkedItemController.getInstance().load();
|
||||
|
||||
@@ -3,7 +3,10 @@ package noppes.npcs;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilitySlot;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityAction;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.IAbilityAction;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
|
||||
@@ -35,9 +38,9 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* List of ability slots this NPC can use (inline or reference).
|
||||
* Unified list of action slots (abilities and chained abilities).
|
||||
*/
|
||||
private List<AbilitySlot> abilitySlots = new ArrayList<>();
|
||||
private List<AbilityAction> actionSlots = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Whether the ability system is enabled for this NPC
|
||||
@@ -207,6 +210,20 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
cooldownEndTime += ability.getCooldownTicks();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void rollChainCooldown(ChainedAbility chain) {
|
||||
int baseCooldown = minCooldown;
|
||||
if (maxCooldown > minCooldown) {
|
||||
baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1);
|
||||
}
|
||||
cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown + chain.getCooldownTicks();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected EntityLivingBase retargetForChain() {
|
||||
return npc.getAttackTarget();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAbilityComplete() {
|
||||
currentAbility = null;
|
||||
@@ -288,17 +305,21 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
if (rotationLocked || hitScanActive) releaseRotationControl();
|
||||
if (positionLocked) releaseLockedPosition();
|
||||
}
|
||||
// Clear chain state
|
||||
currentChain = null;
|
||||
chainEntryIndex = -1;
|
||||
chainDelayRemaining = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety: release orphaned locks if no ability is actively executing
|
||||
if (currentAbility == null || !currentAbility.isExecuting()) {
|
||||
if ((currentAbility == null || !currentAbility.isExecuting()) && chainDelayRemaining <= 0) {
|
||||
if (rotationLocked || hitScanActive) releaseRotationControl();
|
||||
if (positionLocked) releaseLockedPosition();
|
||||
}
|
||||
|
||||
// Tick current ability if executing (shared logic from base class)
|
||||
if (currentAbility != null && currentAbility.isExecuting()) {
|
||||
// Tick current ability if executing, or tick chain delay between entries
|
||||
if (chainDelayRemaining > 0 || (currentAbility != null && currentAbility.isExecuting())) {
|
||||
tickCurrentAbility();
|
||||
}
|
||||
}
|
||||
@@ -329,7 +350,7 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Try to select and start an ability for the given target.
|
||||
* Try to select and start an ability or chained ability for the given target.
|
||||
* Called by CombatHandler when NPC is in combat and ready for an ability.
|
||||
*
|
||||
* @param target The combat target
|
||||
@@ -340,24 +361,61 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
return false;
|
||||
}
|
||||
|
||||
Ability selected = selectAbility(target);
|
||||
if (selected == null) {
|
||||
// Build eligible pool from unified action slots
|
||||
List<IAbilityAction> eligible = new ArrayList<>();
|
||||
List<Integer> weights = new ArrayList<>();
|
||||
int totalWeight = 0;
|
||||
|
||||
for (AbilityAction slot : actionSlots) {
|
||||
IAbilityAction action = slot.getAction();
|
||||
if (action != null && isActionEligible(action, target)) {
|
||||
eligible.add(action);
|
||||
weights.add(action.getWeight());
|
||||
totalWeight += action.getWeight();
|
||||
}
|
||||
}
|
||||
|
||||
if (eligible.isEmpty() || totalWeight <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return startAbility(selected, target);
|
||||
// Weighted random selection
|
||||
int roll = random.nextInt(totalWeight);
|
||||
int cumulative = 0;
|
||||
IAbilityAction selected = null;
|
||||
for (int i = 0; i < eligible.size(); i++) {
|
||||
cumulative += weights.get(i);
|
||||
if (roll < cumulative) {
|
||||
selected = eligible.get(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selected == null) {
|
||||
selected = eligible.get(eligible.size() - 1);
|
||||
}
|
||||
|
||||
// Dispatch
|
||||
lastTarget = target;
|
||||
if (selected.isChain()) {
|
||||
return startChain((ChainedAbility) selected, target);
|
||||
} else {
|
||||
return startAbility((Ability) selected, target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an ability can be selected right now.
|
||||
*/
|
||||
public boolean canSelectAbility() {
|
||||
if (!enabled || abilitySlots.isEmpty()) {
|
||||
if (!enabled || actionSlots.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (currentAbility != null && currentAbility.isExecuting()) {
|
||||
return false;
|
||||
}
|
||||
if (isExecutingChain()) {
|
||||
return false;
|
||||
}
|
||||
// Check if still on cooldown
|
||||
if (npc.worldObj.getTotalWorldTime() < cooldownEndTime) {
|
||||
return false;
|
||||
@@ -366,65 +424,36 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an ability using weighted random from eligible abilities.
|
||||
* Check if an action (ability or chain) is eligible for use.
|
||||
*/
|
||||
private Ability selectAbility(EntityLivingBase target) {
|
||||
List<Ability> eligible = new ArrayList<>();
|
||||
int totalWeight = 0;
|
||||
|
||||
for (Ability ability : getAbilities()) {
|
||||
if (isAbilityEligible(ability, target)) {
|
||||
eligible.add(ability);
|
||||
totalWeight += ability.getWeight();
|
||||
}
|
||||
private boolean isActionEligible(IAbilityAction action, EntityLivingBase target) {
|
||||
if (!action.getAllowedBy().allowsNpc()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (eligible.isEmpty() || totalWeight <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Weighted random selection
|
||||
int roll = random.nextInt(totalWeight);
|
||||
int cumulative = 0;
|
||||
for (Ability ability : eligible) {
|
||||
cumulative += ability.getWeight();
|
||||
if (roll < cumulative) {
|
||||
return ability;
|
||||
}
|
||||
}
|
||||
|
||||
return eligible.get(eligible.size() - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific ability is eligible for use.
|
||||
*/
|
||||
private boolean isAbilityEligible(Ability ability, EntityLivingBase target) {
|
||||
// Check UserType allows NPCs
|
||||
if (!ability.getAllowedBy().allowsNpc()) {
|
||||
if (!action.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check enabled
|
||||
if (!ability.isEnabled()) {
|
||||
// Ability-specific: check if already executing
|
||||
if (!action.isChain() && ((Ability) action).isExecuting()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if already executing
|
||||
if (ability.isExecuting()) {
|
||||
// Chain-specific: must have entries
|
||||
if (action.isChain() && ((ChainedAbility) action).getEntries().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check range
|
||||
// Range check
|
||||
if (target != null) {
|
||||
float distance = npc.getDistanceToEntity(target);
|
||||
if (distance < ability.getMinRange() || distance > ability.getMaxRange()) {
|
||||
if (distance < action.getMinRange() || distance > action.getMaxRange()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check conditions
|
||||
if (!ability.checkConditions(npc, target)) {
|
||||
// Conditions
|
||||
if (!action.checkConditions(npc, target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -496,7 +525,9 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentAbility.onDamageTaken(npc, (EntityLivingBase) source.getEntity(), source, amount);
|
||||
net.minecraft.entity.Entity sourceEntity = source.getEntity();
|
||||
EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null;
|
||||
currentAbility.onDamageTaken(npc, attacker, source, amount);
|
||||
|
||||
if (currentAbility.canInterrupt(source)) {
|
||||
interruptCurrentAbility(source, amount);
|
||||
@@ -568,6 +599,10 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
currentAbility = null;
|
||||
lastTarget = null;
|
||||
}
|
||||
// Also clear chain state
|
||||
currentChain = null;
|
||||
chainEntryIndex = -1;
|
||||
chainDelayRemaining = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -687,15 +722,20 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ABILITY LIST MANAGEMENT
|
||||
// ACTION SLOT MANAGEMENT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Get the unified action slot list. */
|
||||
public List<AbilityAction> getAbilityActions() {
|
||||
return actionSlots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resolved abilities from all slots, filtering out broken references.
|
||||
* Get resolved abilities from all ability slots, filtering out broken references and chains.
|
||||
*/
|
||||
public List<Ability> getAbilities() {
|
||||
List<Ability> resolved = new ArrayList<>();
|
||||
for (AbilitySlot slot : abilitySlots) {
|
||||
for (AbilityAction slot : actionSlots) {
|
||||
Ability a = slot.getAbility();
|
||||
if (a != null) {
|
||||
resolved.add(a);
|
||||
@@ -704,39 +744,54 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/** Get the raw slot list (for GUI access). */
|
||||
public List<AbilitySlot> getAbilitySlots() {
|
||||
return abilitySlots;
|
||||
}
|
||||
|
||||
/** Add an inline ability. */
|
||||
public void addAbility(Ability ability) {
|
||||
abilitySlots.add(AbilitySlot.inline(ability));
|
||||
actionSlots.add(AbilityAction.inline(ability));
|
||||
}
|
||||
|
||||
/** Add a reference ability by key (built-in name or custom ability name). */
|
||||
public void addAbilityReference(String key) {
|
||||
abilitySlots.add(AbilitySlot.reference(key));
|
||||
actionSlots.add(AbilityAction.abilityReference(key));
|
||||
}
|
||||
|
||||
public void removeAbility(int index) {
|
||||
if (index >= 0 && index < abilitySlots.size()) {
|
||||
abilitySlots.remove(index);
|
||||
/** Add a chained ability reference by name. */
|
||||
public void addChainReference(String name) {
|
||||
if (name != null && !name.isEmpty()) {
|
||||
for (AbilityAction slot : actionSlots) {
|
||||
if (slot.isChainReference() && name.equals(slot.getReferenceId())) {
|
||||
return; // Prevent duplicates
|
||||
}
|
||||
}
|
||||
actionSlots.add(AbilityAction.chainReference(name));
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove an action slot by index. */
|
||||
public void removeAction(int index) {
|
||||
if (index >= 0 && index < actionSlots.size()) {
|
||||
actionSlots.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeAbility(int index) {
|
||||
removeAction(index);
|
||||
}
|
||||
|
||||
public void removeAbility(String id) {
|
||||
abilitySlots.removeIf(slot -> {
|
||||
if (slot.isReference()) {
|
||||
actionSlots.removeIf(slot -> {
|
||||
if (slot.isAbilityReference()) {
|
||||
return slot.getReferenceId().equals(id);
|
||||
}
|
||||
if (slot.isChainReference()) {
|
||||
return false; // Don't remove chain references via removeAbility
|
||||
}
|
||||
Ability a = slot.getAbility();
|
||||
return a != null && a.getId().equals(id);
|
||||
});
|
||||
}
|
||||
|
||||
public Ability getAbility(String id) {
|
||||
for (AbilitySlot slot : abilitySlots) {
|
||||
for (AbilityAction slot : actionSlots) {
|
||||
Ability a = slot.getAbility();
|
||||
if (a != null && a.getId().equals(id)) {
|
||||
return a;
|
||||
@@ -747,22 +802,22 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
|
||||
/** Check if a slot at a given index is a reference. */
|
||||
public boolean isSlotReference(int index) {
|
||||
if (index < 0 || index >= abilitySlots.size()) return false;
|
||||
return abilitySlots.get(index).isReference();
|
||||
if (index < 0 || index >= actionSlots.size()) return false;
|
||||
return actionSlots.get(index).isReference();
|
||||
}
|
||||
|
||||
/** Convert a reference slot to inline. Returns false if resolution fails. */
|
||||
/** Convert a reference slot to inline. Returns false if resolution fails or is a chain. */
|
||||
public boolean convertToInline(int index) {
|
||||
if (index < 0 || index >= abilitySlots.size()) return false;
|
||||
return abilitySlots.get(index).convertToInline();
|
||||
if (index < 0 || index >= actionSlots.size()) return false;
|
||||
return actionSlots.get(index).convertToInline();
|
||||
}
|
||||
|
||||
public void clearAbilities() {
|
||||
abilitySlots.clear();
|
||||
actionSlots.clear();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return abilitySlots.isEmpty();
|
||||
return actionSlots.isEmpty();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -977,11 +1032,11 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
compound.setInteger("AbilityMinCooldown", minCooldown);
|
||||
compound.setInteger("AbilityMaxCooldown", maxCooldown);
|
||||
|
||||
NBTTagList abilityList = new NBTTagList();
|
||||
for (AbilitySlot slot : abilitySlots) {
|
||||
abilityList.appendTag(slot.writeNBT());
|
||||
NBTTagList actionList = new NBTTagList();
|
||||
for (AbilityAction slot : actionSlots) {
|
||||
actionList.appendTag(slot.writeNBT());
|
||||
}
|
||||
compound.setTag("Abilities", abilityList);
|
||||
compound.setTag("AbilityActions", actionList);
|
||||
|
||||
return compound;
|
||||
}
|
||||
@@ -991,13 +1046,36 @@ public class DataAbilities extends AbstractDataAbilities {
|
||||
minCooldown = compound.getInteger("AbilityMinCooldown");
|
||||
maxCooldown = compound.getInteger("AbilityMaxCooldown");
|
||||
|
||||
abilitySlots.clear();
|
||||
NBTTagList abilityList = compound.getTagList("Abilities", 10);
|
||||
for (int i = 0; i < abilityList.tagCount(); i++) {
|
||||
NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i);
|
||||
AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT);
|
||||
if (slot != null) {
|
||||
abilitySlots.add(slot);
|
||||
actionSlots.clear();
|
||||
|
||||
if (compound.hasKey("AbilityActions")) {
|
||||
// New unified format
|
||||
NBTTagList actionList = compound.getTagList("AbilityActions", 10);
|
||||
for (int i = 0; i < actionList.tagCount(); i++) {
|
||||
AbilityAction slot = AbilityAction.fromNBT(actionList.getCompoundTagAt(i));
|
||||
if (slot != null) {
|
||||
actionSlots.add(slot);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Legacy migration: read old separate lists
|
||||
if (compound.hasKey("Abilities")) {
|
||||
NBTTagList abilityList = compound.getTagList("Abilities", 10);
|
||||
for (int i = 0; i < abilityList.tagCount(); i++) {
|
||||
AbilityAction slot = AbilityAction.fromNBT(abilityList.getCompoundTagAt(i));
|
||||
if (slot != null) {
|
||||
actionSlots.add(slot);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (compound.hasKey("ChainedAbilities")) {
|
||||
NBTTagList chainList = compound.getTagList("ChainedAbilities", 8);
|
||||
for (int i = 0; i < chainList.tagCount(); i++) {
|
||||
String ref = chainList.getStringTagAt(i);
|
||||
if (ref != null && !ref.isEmpty()) {
|
||||
actionSlots.add(AbilityAction.chainReference(ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package noppes.npcs.client.gui.advanced;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilitySlot;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityAction;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.network.PacketClient;
|
||||
import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket;
|
||||
@@ -48,8 +48,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
// Display name to typeId mapping for the available types scroll
|
||||
private final HashMap<String, String> displayNameToTypeId = new HashMap<>();
|
||||
|
||||
// NPC's current ability slots
|
||||
private final List<AbilitySlot> npcSlots = new ArrayList<>();
|
||||
// NPC's current action slots
|
||||
private final List<AbilityAction> npcSlots = new ArrayList<>();
|
||||
|
||||
// Settings
|
||||
private boolean abilitiesEnabled = false;
|
||||
@@ -163,9 +163,11 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
// On/Off toggle for selected ability (under Load button)
|
||||
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
|
||||
Ability selectedAbility = npcSlots.get(selectedAbilityIndex).getAbility();
|
||||
GuiNpcButton toggleBtn = new GuiNpcButton(77, centerX, y + 112, 40, 20,
|
||||
new String[]{"gui.off", "gui.on"}, selectedAbility.isEnabled() ? 1 : 0);
|
||||
addButton(toggleBtn);
|
||||
if (selectedAbility != null) {
|
||||
GuiNpcButton toggleBtn = new GuiNpcButton(77, centerX, y + 112, 40, 20,
|
||||
new String[]{"gui.off", "gui.on"}, selectedAbility.isEnabled() ? 1 : 0);
|
||||
addButton(toggleBtn);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if selected ability is built-in (non-editable)
|
||||
@@ -220,7 +222,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
variants.get(0).apply(newAbility);
|
||||
}
|
||||
newAbility.setId(UUID.randomUUID().toString());
|
||||
npcSlots.add(AbilitySlot.inline(newAbility));
|
||||
npcSlots.add(AbilityAction.inline(newAbility));
|
||||
selectedAbilityIndex = npcSlots.size() - 1;
|
||||
updateNpcAbilitiesList();
|
||||
selectAbilityByIndex(selectedAbilityIndex);
|
||||
@@ -247,7 +249,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
// Edit ability
|
||||
if (id == 72) {
|
||||
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
|
||||
AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
|
||||
AbilityAction slot = npcSlots.get(selectedAbilityIndex);
|
||||
if (slot.isReference()) {
|
||||
// Reference slot - ask clone-and-modify vs modify-parent
|
||||
setSubGui(new SubGuiAbilityEditMode());
|
||||
@@ -265,7 +267,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
// Move up (carrot left = earlier in list)
|
||||
if (id == 73) {
|
||||
if (selectedAbilityIndex > 0) {
|
||||
AbilitySlot temp = npcSlots.get(selectedAbilityIndex);
|
||||
AbilityAction temp = npcSlots.get(selectedAbilityIndex);
|
||||
npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex - 1));
|
||||
npcSlots.set(selectedAbilityIndex - 1, temp);
|
||||
selectedAbilityIndex--;
|
||||
@@ -280,7 +282,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
// Move down (carrot right = later in list)
|
||||
if (id == 74) {
|
||||
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1) {
|
||||
AbilitySlot temp = npcSlots.get(selectedAbilityIndex);
|
||||
AbilityAction temp = npcSlots.get(selectedAbilityIndex);
|
||||
npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex + 1));
|
||||
npcSlots.set(selectedAbilityIndex + 1, temp);
|
||||
selectedAbilityIndex++;
|
||||
@@ -317,7 +319,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
// Toggle selected ability on/off
|
||||
if (id == 77) {
|
||||
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
|
||||
AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
|
||||
AbilityAction slot = npcSlots.get(selectedAbilityIndex);
|
||||
Ability ability = slot.getAbility();
|
||||
if (ability != null) {
|
||||
boolean enabled = ((GuiNpcButton) guibutton).getValue() == 1;
|
||||
@@ -397,7 +399,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
// Only process NPC ability data, not saved ability preset data
|
||||
// SubGuiAbilityLoad also implements IGuiData and may receive responses
|
||||
// that get forwarded here - ignore those by checking for expected keys
|
||||
if (!compound.hasKey("Abilities")) {
|
||||
if (!compound.hasKey("AbilityActions") && !compound.hasKey("Abilities")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -411,10 +413,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
}
|
||||
|
||||
npcSlots.clear();
|
||||
NBTTagList abilityList = compound.getTagList("Abilities", 10);
|
||||
for (int i = 0; i < abilityList.tagCount(); i++) {
|
||||
NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i);
|
||||
AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT);
|
||||
// Support both new unified format and legacy format
|
||||
String tagName = compound.hasKey("AbilityActions") ? "AbilityActions" : "Abilities";
|
||||
NBTTagList actionList = compound.getTagList(tagName, 10);
|
||||
for (int i = 0; i < actionList.tagCount(); i++) {
|
||||
NBTTagCompound slotNBT = actionList.getCompoundTagAt(i);
|
||||
AbilityAction slot = AbilityAction.fromNBT(slotNBT);
|
||||
if (slot != null) {
|
||||
npcSlots.add(slot);
|
||||
}
|
||||
@@ -526,7 +530,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
* Broken: "[●] 1. > [Missing: id...]" in red
|
||||
*/
|
||||
private String getAbilityListEntry(int index) {
|
||||
AbilitySlot slot = npcSlots.get(index);
|
||||
AbilityAction slot = npcSlots.get(index);
|
||||
Ability ability = slot.getAbility();
|
||||
|
||||
if (ability == null) {
|
||||
@@ -579,11 +583,11 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
compound.setInteger("AbilityMinCooldown", minCooldown);
|
||||
compound.setInteger("AbilityMaxCooldown", maxCooldown);
|
||||
|
||||
NBTTagList abilityList = new NBTTagList();
|
||||
for (AbilitySlot slot : npcSlots) {
|
||||
abilityList.appendTag(slot.writeNBT());
|
||||
NBTTagList actionList = new NBTTagList();
|
||||
for (AbilityAction slot : npcSlots) {
|
||||
actionList.appendTag(slot.writeNBT());
|
||||
}
|
||||
compound.setTag("Abilities", abilityList);
|
||||
compound.setTag("AbilityActions", actionList);
|
||||
|
||||
PacketClient.sendClient(new AbilitiesNpcSavePacket(compound));
|
||||
}
|
||||
@@ -595,7 +599,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
public void loadAbility(Ability loadedAbility) {
|
||||
if (loadedAbility != null) {
|
||||
loadedAbility.setId(UUID.randomUUID().toString());
|
||||
npcSlots.add(AbilitySlot.inline(loadedAbility));
|
||||
npcSlots.add(AbilityAction.inline(loadedAbility));
|
||||
selectedAbilityIndex = npcSlots.size() - 1;
|
||||
updateNpcAbilitiesList();
|
||||
selectAbilityByIndex(selectedAbilityIndex);
|
||||
@@ -610,7 +614,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
*/
|
||||
public void loadAbilityReference(String referenceId) {
|
||||
if (referenceId != null && !referenceId.isEmpty()) {
|
||||
npcSlots.add(AbilitySlot.reference(referenceId));
|
||||
npcSlots.add(AbilityAction.abilityReference(referenceId));
|
||||
selectedAbilityIndex = npcSlots.size() - 1;
|
||||
updateNpcAbilitiesList();
|
||||
selectAbilityByIndex(selectedAbilityIndex);
|
||||
@@ -652,7 +656,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
@Override
|
||||
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
|
||||
if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
|
||||
AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
|
||||
AbilityAction slot = npcSlots.get(selectedAbilityIndex);
|
||||
Ability ability = slot.getAbility();
|
||||
if (ability != null && ability.isBuiltIn()) return;
|
||||
if (slot.isReference()) {
|
||||
@@ -697,7 +701,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
if (newAbility != null) {
|
||||
variantGui.getVariants().get(idx).apply(newAbility);
|
||||
newAbility.setId(UUID.randomUUID().toString());
|
||||
npcSlots.add(AbilitySlot.inline(newAbility));
|
||||
npcSlots.add(AbilityAction.inline(newAbility));
|
||||
selectedAbilityIndex = npcSlots.size() - 1;
|
||||
updateNpcAbilitiesList();
|
||||
selectAbilityByIndex(selectedAbilityIndex);
|
||||
@@ -711,7 +715,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
if (mode < 0) return; // cancelled
|
||||
|
||||
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
|
||||
AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
|
||||
AbilityAction slot = npcSlots.get(selectedAbilityIndex);
|
||||
if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) {
|
||||
// Convert reference to inline, then edit
|
||||
if (slot.convertToInline()) {
|
||||
@@ -743,13 +747,13 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
*/
|
||||
public void onAbilitySaved(Ability ability) {
|
||||
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
|
||||
AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
|
||||
AbilityAction slot = npcSlots.get(selectedAbilityIndex);
|
||||
if (slot.isReference()) {
|
||||
// For reference slots, save back to controller
|
||||
PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
|
||||
} else {
|
||||
// For inline slots, replace with the updated ability
|
||||
npcSlots.set(selectedAbilityIndex, AbilitySlot.inline(ability));
|
||||
npcSlots.set(selectedAbilityIndex, AbilityAction.inline(ability));
|
||||
}
|
||||
updateNpcAbilitiesList();
|
||||
save();
|
||||
|
||||
@@ -3,6 +3,9 @@ package noppes.npcs.controllers.data;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.IAbilityAction;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
|
||||
@@ -229,6 +232,11 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void rollChainCooldown(ChainedAbility chain) {
|
||||
cooldownEndTime = getWorldTime() + chain.getCooldownTicks();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAbilityComplete() {
|
||||
currentAbility = null;
|
||||
@@ -263,7 +271,7 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentAbility != null && currentAbility.isExecuting()) {
|
||||
if (chainDelayRemaining > 0 || (currentAbility != null && currentAbility.isExecuting())) {
|
||||
tickCurrentAbility();
|
||||
|
||||
// Apply rotation and position locks after ability tick
|
||||
@@ -302,11 +310,28 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
|
||||
return activateAbility(player, key);
|
||||
}
|
||||
|
||||
/** Prefix used to identify chained ability keys in the unlocked list. */
|
||||
public static final String CHAIN_PREFIX = "chain:";
|
||||
|
||||
/**
|
||||
* Activate a specific ability by key.
|
||||
* Resolve an action key to an IAbilityAction.
|
||||
* Handles "chain:" prefix for chained abilities, otherwise resolves from AbilityController.
|
||||
*/
|
||||
private IAbilityAction resolveActionKey(String key) {
|
||||
if (key.startsWith(CHAIN_PREFIX)) {
|
||||
if (ChainedAbilityController.Instance == null) return null;
|
||||
return ChainedAbilityController.Instance.resolve(key.substring(CHAIN_PREFIX.length()));
|
||||
}
|
||||
if (AbilityController.Instance == null) return null;
|
||||
return AbilityController.Instance.resolveAbility(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a specific ability by key. Keys prefixed with "chain:" are
|
||||
* treated as chained abilities and resolved from {@link ChainedAbilityController}.
|
||||
*
|
||||
* @param player The player
|
||||
* @param key The ability key (built-in or preset name)
|
||||
* @param key The ability key (built-in or preset name), or "chain:name" for chains
|
||||
* @return true if the ability was started
|
||||
*/
|
||||
public boolean activateAbility(EntityPlayer player, String key) {
|
||||
@@ -314,20 +339,32 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
|
||||
|
||||
// Can't activate if already executing
|
||||
if (currentAbility != null && currentAbility.isExecuting()) return false;
|
||||
if (isExecutingChain()) return false;
|
||||
|
||||
// Resolve ability from controller
|
||||
if (AbilityController.Instance == null) return false;
|
||||
Ability ability = AbilityController.Instance.resolveAbility(key);
|
||||
if (ability == null) return false;
|
||||
// Resolve via unified resolver
|
||||
IAbilityAction action = resolveActionKey(key);
|
||||
if (action == null) return false;
|
||||
|
||||
// Check user type allows player
|
||||
if (!ability.getAllowedBy().allowsPlayer()) return false;
|
||||
|
||||
// Check universal cooldown
|
||||
if (!ability.isIgnoreCooldown() && isOnCooldown()) return false;
|
||||
if (!action.getAllowedBy().allowsPlayer()) return false;
|
||||
|
||||
// Check conditions (skip target-requiring ones)
|
||||
if (!ability.checkConditionsForPlayer(player)) return false;
|
||||
if (!action.checkConditionsForPlayer(player)) return false;
|
||||
|
||||
// Dispatch: chain vs ability
|
||||
if (action.isChain()) {
|
||||
if (isOnCooldown()) return false;
|
||||
|
||||
currentAbilityKey = key;
|
||||
currentTarget = null;
|
||||
return startChain((ChainedAbility) action, null);
|
||||
}
|
||||
|
||||
// Regular ability activation
|
||||
Ability ability = (Ability) action;
|
||||
|
||||
// Check universal cooldown (abilities can optionally ignore it)
|
||||
if (!ability.isIgnoreCooldown() && isOnCooldown()) return false;
|
||||
|
||||
// Fire extender start hook (e.g., resource cost checks)
|
||||
if (!AbilityController.Instance.fireOnAbilityStart(ability, player, null)) {
|
||||
@@ -777,6 +814,7 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
|
||||
/**
|
||||
* Remove any unlocked ability keys that can no longer be resolved.
|
||||
* Called during load to clean up references to deleted abilities.
|
||||
* Supports both regular ability keys and "chain:" prefixed chained ability keys.
|
||||
*/
|
||||
private void validateUnlockedAbilities() {
|
||||
if (AbilityController.Instance == null) return;
|
||||
@@ -784,7 +822,8 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
|
||||
Iterator<String> it = unlockedAbilities.iterator();
|
||||
while (it.hasNext()) {
|
||||
String key = it.next();
|
||||
if (!AbilityController.Instance.canResolveAbility(key)) {
|
||||
IAbilityAction resolved = resolveActionKey(key);
|
||||
if (resolved == null) {
|
||||
it.remove();
|
||||
LogWriter.info("Removed invalid ability reference from player data: " + key);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package noppes.npcs.scripted.wrapper;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilitySlot;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityAction;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import noppes.npcs.DataAbilities;
|
||||
import noppes.npcs.api.ability.IAbility;
|
||||
@@ -81,12 +81,12 @@ public class ScriptDataAbilities implements IDataAbilities {
|
||||
|
||||
@Override
|
||||
public boolean isAbilityReference(String abilityId) {
|
||||
List<AbilitySlot> slots = data.getAbilitySlots();
|
||||
List<AbilityAction> slots = data.getAbilityActions();
|
||||
for (int i = 0; i < slots.size(); i++) {
|
||||
kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i);
|
||||
if (slot.isReference()) {
|
||||
AbilityAction slot = slots.get(i);
|
||||
if (slot.isAbilityReference()) {
|
||||
if (slot.getReferenceId().equals(abilityId)) return true;
|
||||
} else {
|
||||
} else if (!slot.isReference()) {
|
||||
Ability a = slot.getAbility();
|
||||
if (a != null && a.getId().equals(abilityId)) return false;
|
||||
}
|
||||
@@ -96,10 +96,10 @@ public class ScriptDataAbilities implements IDataAbilities {
|
||||
|
||||
@Override
|
||||
public boolean convertToInline(String abilityId) {
|
||||
List<kamkeel.npcs.controllers.data.ability.AbilitySlot> slots = data.getAbilitySlots();
|
||||
List<AbilityAction> slots = data.getAbilityActions();
|
||||
for (int i = 0; i < slots.size(); i++) {
|
||||
kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i);
|
||||
if (slot.isReference() && slot.getReferenceId().equals(abilityId)) {
|
||||
AbilityAction slot = slots.get(i);
|
||||
if (slot.isAbilityReference() && slot.getReferenceId().equals(abilityId)) {
|
||||
return data.convertToInline(i);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user