2026-02-17 21:03:54 -05:00
|
|
|
package noppes.npcs;
|
|
|
|
|
|
2026-02-18 04:02:51 -05:00
|
|
|
import kamkeel.npcs.controllers.AbilityController;
|
2026-02-18 05:13:51 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.Ability;
|
2026-02-22 03:28:56 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry;
|
2026-02-19 12:55:37 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.ConcurrentSlot;
|
2026-02-22 03:28:56 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.data.entry.AbilityToggleEntry;
|
2026-02-17 21:03:54 -05:00
|
|
|
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.controllers.data.Animation;
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
import java.util.ArrayList;
|
2026-02-20 03:47:12 -05:00
|
|
|
import java.util.HashMap;
|
2026-02-19 12:55:37 -05:00
|
|
|
import java.util.Iterator;
|
2026-02-18 05:13:51 -05:00
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
import java.util.LinkedHashSet;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Set;
|
2026-02-17 21:03:54 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Shared base class for NPC and Player ability data management.
|
|
|
|
|
* Contains the common ability lifecycle logic: tick, phase transitions,
|
|
|
|
|
* animation/telegraph/sound management, cooldown state, and position/rotation locking.
|
|
|
|
|
* <p>
|
|
|
|
|
* Subclasses must implement abstract methods for entity-specific behavior:
|
|
|
|
|
* event firing, rotation/position application, telegraph packets, and cooldown rolling.
|
|
|
|
|
*
|
|
|
|
|
* @see DataAbilities - NPC implementation
|
|
|
|
|
* @see noppes.npcs.controllers.data.PlayerAbilityData - Player implementation
|
|
|
|
|
*/
|
|
|
|
|
public abstract class AbstractDataAbilities {
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// SHARED RUNTIME STATE (not saved to NBT)
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Currently executing ability (null if none)
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected Ability currentAbility;
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
2026-02-20 03:47:12 -05:00
|
|
|
* World time when global cooldown ends
|
2026-02-18 05:13:51 -05:00
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected long cooldownEndTime = 0;
|
|
|
|
|
|
2026-02-20 03:47:12 -05:00
|
|
|
/**
|
|
|
|
|
* Duration of the current global cooldown in ticks (for progress calculation)
|
|
|
|
|
*/
|
|
|
|
|
protected int globalCooldownDuration = 0;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Per-ability cooldown end times. Key = ability key, Value = world time when cooldown ends.
|
|
|
|
|
*/
|
|
|
|
|
protected HashMap<String, Long> perAbilityCooldownEndTimes = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Per-ability cooldown durations in ticks (for progress calculation).
|
|
|
|
|
*/
|
|
|
|
|
protected HashMap<String, Integer> perAbilityCooldownDurations = new HashMap<>();
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Rotation lock state
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected boolean rotationLocked = false;
|
|
|
|
|
protected float lockedYaw = 0;
|
|
|
|
|
protected float lockedPitch = 0;
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Position lock state
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected boolean positionLocked = false;
|
2026-02-19 03:12:59 -05:00
|
|
|
protected boolean wasFlyingAtLock = false;
|
2026-02-17 21:03:54 -05:00
|
|
|
protected double lockedPosX = 0;
|
|
|
|
|
protected double lockedPosY = 0;
|
|
|
|
|
protected double lockedPosZ = 0;
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Chained ability execution state
|
|
|
|
|
*/
|
2026-02-17 23:22:03 -05:00
|
|
|
protected ChainedAbility currentChain;
|
|
|
|
|
protected int chainEntryIndex = -1;
|
|
|
|
|
protected int chainDelayRemaining = -1;
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Set when interrupt already rolled cooldown (prevents double cooldown on NPC dazed completion).
|
|
|
|
|
*/
|
2026-02-17 23:22:03 -05:00
|
|
|
protected boolean interruptCooldownRolled = false;
|
|
|
|
|
|
2026-02-19 12:55:37 -05:00
|
|
|
/**
|
|
|
|
|
* Concurrent ability execution slots. Ticked independently alongside the primary ability.
|
|
|
|
|
* Concurrent abilities skip animations, sounds, and movement/rotation locks.
|
|
|
|
|
*/
|
|
|
|
|
protected List<ConcurrentSlot> concurrentSlots = new ArrayList<>();
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// ABSTRACT METHODS - Subclasses must implement
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Get the entity this ability data belongs to (NPC or Player).
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract EntityLivingBase getEntity();
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Get the current target for ability execution.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract EntityLivingBase getTarget();
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Get the current world time.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract long getWorldTime();
|
|
|
|
|
|
|
|
|
|
// -- Event Firing --
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Fire tick event. Implementation differs between NPC (NpcAPI) and Player (ScriptController).
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract void fireTickEvent(Ability ability, EntityLivingBase target);
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Fire execute event. Returns true if cancelled.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract boolean fireExecuteEvent(Ability ability, EntityLivingBase target);
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Fire complete event.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract void fireCompleteEvent(Ability ability, EntityLivingBase target);
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Fire interrupt event.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract void fireInterruptEvent(Ability ability, EntityLivingBase target,
|
|
|
|
|
DamageSource source, float damage);
|
|
|
|
|
|
|
|
|
|
// -- Telegraph --
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Spawn telegraphs for an ability.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract void spawnTelegraph(Ability ability, EntityLivingBase target);
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Remove telegraphs for an ability.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract void removeTelegraph(Ability ability);
|
|
|
|
|
|
|
|
|
|
// -- Animation/Sound Data Access --
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Set animation data on the entity.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract void setAnimationData(Animation animation);
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Clear animation data on the entity.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract void clearAnimationData();
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Play a sound at the entity's location.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract void playAbilitySound(String sound);
|
|
|
|
|
|
|
|
|
|
// -- Rotation (subclass handles extra fields and application) --
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Capture rotation values for locking. NPC captures 4 fields, Player captures 2.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract void captureLockedRotation();
|
|
|
|
|
|
|
|
|
|
// -- Completion --
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Roll cooldown after ability completes.
|
|
|
|
|
* NPC: random(min, max) + ability offset. Player: ability's base cooldownTicks.
|
|
|
|
|
*/
|
|
|
|
|
protected abstract void rollCooldown(Ability ability);
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Additional completion logic (NPC clears lastTarget, Player clears key/target and syncs).
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected abstract void onAbilityComplete();
|
|
|
|
|
|
2026-02-17 23:22:03 -05:00
|
|
|
/**
|
|
|
|
|
* Roll cooldown after a chained ability completes.
|
|
|
|
|
* NPC: random(min, max) + chain.cooldownTicks. Player: chain.cooldownTicks only.
|
|
|
|
|
*/
|
|
|
|
|
protected abstract void rollChainCooldown(ChainedAbility chain);
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
|
|
|
|
|
// -- Hooks (optional overrides) --
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Called before onExecute in ACTIVE phase. NPC uses this for faceTarget/hitScan.
|
|
|
|
|
*/
|
|
|
|
|
protected void onPreExecute(Ability ability, EntityLivingBase target) {
|
|
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Called after the phase switch. NPC uses this for hit scan updates and movement control.
|
|
|
|
|
*/
|
|
|
|
|
protected void onPostPhaseTick(Ability ability, EntityLivingBase target) {
|
|
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Additional lock releases in BURST_DELAY. NPC releases hitScan.
|
|
|
|
|
*/
|
|
|
|
|
protected void onBurstDelayReleaseLocks() {
|
|
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Hook for retargeting during chain execution when target dies. NPC overrides, Player returns null.
|
|
|
|
|
*/
|
|
|
|
|
protected EntityLivingBase retargetForChain() {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2026-02-17 23:22:03 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// SHARED ANIMATION METHODS
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Play an animation on the entity.
|
|
|
|
|
*/
|
|
|
|
|
public void playAbilityAnimation(Animation animation) {
|
|
|
|
|
if (animation == null) return;
|
|
|
|
|
if (AnimationController.Instance == null) return;
|
|
|
|
|
setAnimationData(animation);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void playAbilityAnimation(int animation) {
|
|
|
|
|
if (animation < 0) return;
|
|
|
|
|
if (AnimationController.Instance == null) return;
|
|
|
|
|
if (AnimationController.Instance.get(animation) == null) return;
|
|
|
|
|
playAbilityAnimation((Animation) AnimationController.Instance.get(animation));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void playAbilityAnimation(String animation) {
|
|
|
|
|
if (animation.isEmpty()) return;
|
|
|
|
|
if (AnimationController.Instance == null) return;
|
|
|
|
|
if (AnimationController.Instance.get(animation, true) == null) return;
|
|
|
|
|
playAbilityAnimation((Animation) AnimationController.Instance.get(animation, true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Stop any currently playing ability animation.
|
|
|
|
|
*/
|
|
|
|
|
protected void stopAbilityAnimation() {
|
|
|
|
|
clearAnimationData();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// STATE QUERIES
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Check if an ability is currently executing.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
public boolean isExecutingAbility() {
|
|
|
|
|
return currentAbility != null && currentAbility.isExecuting();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 16:33:01 -05:00
|
|
|
/**
|
|
|
|
|
* True when an executing ability grants invulnerability for its current phase.
|
|
|
|
|
*/
|
|
|
|
|
public boolean isCurrentAbilityInvulnerable() {
|
|
|
|
|
return currentAbility != null
|
|
|
|
|
&& currentAbility.isExecuting()
|
|
|
|
|
&& currentAbility.isInvulnerableForCurrentPhase();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Get the currently executing ability.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
public Ability getCurrentAbility() {
|
|
|
|
|
return currentAbility;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Check if a chained ability is currently executing.
|
|
|
|
|
*/
|
2026-02-17 23:22:03 -05:00
|
|
|
public boolean isExecutingChain() {
|
|
|
|
|
return currentChain != null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Get the currently executing chained ability.
|
|
|
|
|
*/
|
2026-02-17 23:22:03 -05:00
|
|
|
public ChainedAbility getCurrentChain() {
|
|
|
|
|
return currentChain;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-02-17 23:08:12 -05:00
|
|
|
// TOGGLE STATE (active toggles, independent of currentAbility)
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
2026-02-18 22:19:18 -05:00
|
|
|
* Currently active toggles. Key = ability key, Value = toggle entry with state and tick counter.
|
2026-02-18 05:13:51 -05:00
|
|
|
*/
|
2026-02-22 03:28:56 -05:00
|
|
|
protected Map<String, AbilityToggleEntry> activeToggles = new LinkedHashMap<>();
|
2026-02-17 23:08:12 -05:00
|
|
|
|
|
|
|
|
/**
|
2026-02-18 22:19:18 -05:00
|
|
|
* Cycle a toggle ability to its next state.
|
|
|
|
|
* Off(0) -> State 1 -> State 2 -> ... -> State N -> Off(0)
|
2026-02-18 05:13:51 -05:00
|
|
|
*
|
2026-02-17 23:08:12 -05:00
|
|
|
* @param key The ability key (e.g., "npcdbc:ki_fist")
|
2026-02-18 22:19:18 -05:00
|
|
|
* @return The new state (0 = off, 1+ = active state number)
|
|
|
|
|
*/
|
|
|
|
|
public int toggleAbility(String key) {
|
2026-02-22 03:28:56 -05:00
|
|
|
AbilityToggleEntry existing = activeToggles.get(key);
|
2026-02-18 22:19:18 -05:00
|
|
|
if (existing != null) {
|
|
|
|
|
int currentState = existing.getState();
|
|
|
|
|
int maxStates = existing.getAbility().getToggleStates();
|
|
|
|
|
if (currentState < maxStates) {
|
|
|
|
|
return cycleToggleState(key, existing, currentState, currentState + 1);
|
|
|
|
|
} else {
|
|
|
|
|
deactivateToggle(key);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
2026-02-17 23:08:12 -05:00
|
|
|
} else {
|
2026-02-18 22:19:18 -05:00
|
|
|
return activateToggle(key, 1) ? 1 : 0;
|
2026-02-17 23:08:12 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-18 22:19:18 -05:00
|
|
|
* Get the current toggle state for an ability.
|
|
|
|
|
* @return 0 if not active, 1+ for active state
|
|
|
|
|
*/
|
|
|
|
|
public int getToggleState(String key) {
|
2026-02-22 03:28:56 -05:00
|
|
|
AbilityToggleEntry entry = activeToggles.get(key);
|
2026-02-18 22:19:18 -05:00
|
|
|
return entry != null ? entry.getState() : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set a toggle to a specific state. 0 = deactivate, 1+ = specific state.
|
|
|
|
|
*/
|
|
|
|
|
public void setToggleState(String key, int state) {
|
|
|
|
|
if (state <= 0) {
|
|
|
|
|
if (activeToggles.containsKey(key)) {
|
|
|
|
|
deactivateToggle(key);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-22 03:28:56 -05:00
|
|
|
AbilityToggleEntry existing = activeToggles.get(key);
|
2026-02-18 22:19:18 -05:00
|
|
|
if (existing != null) {
|
|
|
|
|
int currentState = existing.getState();
|
|
|
|
|
if (currentState != state) {
|
|
|
|
|
cycleToggleState(key, existing, currentState, state);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
activateToggle(key, state);
|
2026-02-17 23:08:12 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-18 22:19:18 -05:00
|
|
|
* Check if a toggle ability is currently active (any state > 0).
|
2026-02-17 23:08:12 -05:00
|
|
|
*/
|
|
|
|
|
public boolean isAbilityToggled(String key) {
|
|
|
|
|
return activeToggles.containsKey(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all active toggle keys.
|
|
|
|
|
*/
|
|
|
|
|
public Set<String> getActiveToggleKeys() {
|
|
|
|
|
return new LinkedHashSet<>(activeToggles.keySet());
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 22:19:18 -05:00
|
|
|
private boolean activateToggle(String key, int state) {
|
2026-02-17 23:08:12 -05:00
|
|
|
Ability ability = AbilityController.Instance != null
|
|
|
|
|
? AbilityController.Instance.resolveAbility(key) : null;
|
|
|
|
|
if (ability == null || !ability.isToggleable()) return false;
|
2026-02-18 22:19:18 -05:00
|
|
|
if (state < 1 || state > ability.getToggleStates()) state = 1;
|
2026-02-17 23:08:12 -05:00
|
|
|
|
2026-02-18 22:19:18 -05:00
|
|
|
if (fireToggleEvent(ability, 0, state)) return false;
|
|
|
|
|
|
2026-02-22 03:28:56 -05:00
|
|
|
AbilityToggleEntry entry = new AbilityToggleEntry(ability, state);
|
2026-02-17 23:08:12 -05:00
|
|
|
activeToggles.put(key, entry);
|
|
|
|
|
ability.onToggleOn(getEntity());
|
2026-02-18 22:19:18 -05:00
|
|
|
ability.onToggleStateChanged(getEntity(), 0, state);
|
|
|
|
|
onToggleStateChanged(key, true, state);
|
2026-02-17 23:08:12 -05:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 03:28:56 -05:00
|
|
|
private int cycleToggleState(String key, AbilityToggleEntry entry, int oldState, int newState) {
|
2026-02-18 22:19:18 -05:00
|
|
|
Ability ability = entry.getAbility();
|
|
|
|
|
if (newState < 1 || newState > ability.getToggleStates()) return oldState;
|
|
|
|
|
|
|
|
|
|
if (fireToggleEvent(ability, oldState, newState)) return oldState;
|
|
|
|
|
|
|
|
|
|
entry.setState(newState);
|
|
|
|
|
ability.onToggleStateChanged(getEntity(), oldState, newState);
|
|
|
|
|
onToggleStateChanged(key, true, newState);
|
|
|
|
|
return newState;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
private void deactivateToggle(String key) {
|
2026-02-22 03:28:56 -05:00
|
|
|
AbilityToggleEntry entry = activeToggles.get(key);
|
2026-02-18 22:19:18 -05:00
|
|
|
if (entry == null) return;
|
|
|
|
|
int oldState = entry.getState();
|
|
|
|
|
|
|
|
|
|
if (fireToggleEvent(entry.getAbility(), oldState, 0)) return;
|
|
|
|
|
|
|
|
|
|
activeToggles.remove(key);
|
|
|
|
|
entry.getAbility().onToggleOff(getEntity());
|
|
|
|
|
entry.getAbility().onToggleStateChanged(getEntity(), oldState, 0);
|
|
|
|
|
onToggleStateChanged(key, false, 0);
|
2026-02-17 23:08:12 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tick all active toggles. Call from tick() each game tick.
|
|
|
|
|
* Toggles with hasActiveToggle=true get their onToggleTick() called.
|
|
|
|
|
* If onToggleTick returns false, the toggle is auto-deactivated.
|
2026-02-18 22:19:18 -05:00
|
|
|
* Every 10 ticks, fires a toggle update event for ALL active toggles.
|
2026-02-17 23:08:12 -05:00
|
|
|
*/
|
|
|
|
|
protected void tickActiveToggles() {
|
|
|
|
|
if (activeToggles.isEmpty()) return;
|
|
|
|
|
|
|
|
|
|
EntityLivingBase entity = getEntity();
|
|
|
|
|
List<String> toRemove = null;
|
|
|
|
|
|
2026-02-22 03:28:56 -05:00
|
|
|
for (Map.Entry<String, AbilityToggleEntry> mapEntry : activeToggles.entrySet()) {
|
|
|
|
|
AbilityToggleEntry entry = mapEntry.getValue();
|
2026-02-17 23:08:12 -05:00
|
|
|
entry.incrementTick();
|
|
|
|
|
|
|
|
|
|
if (entry.getAbility().hasActiveToggle()) {
|
2026-02-18 22:19:18 -05:00
|
|
|
if (!entry.getAbility().onToggleTick(entity, entry.getTickCount(), entry.getState())) {
|
|
|
|
|
if (toRemove == null) toRemove = new ArrayList<>();
|
|
|
|
|
toRemove.add(mapEntry.getKey());
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (entry.getTickCount() % 10 == 0) {
|
|
|
|
|
boolean enabled = fireToggleUpdateEvent(entry.getAbility(), entry.getTickCount(), entry.getState());
|
|
|
|
|
if (!enabled) {
|
2026-02-17 23:08:12 -05:00
|
|
|
if (toRemove == null) toRemove = new ArrayList<>();
|
|
|
|
|
toRemove.add(mapEntry.getKey());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (toRemove != null) {
|
|
|
|
|
for (String key : toRemove) {
|
2026-02-22 03:28:56 -05:00
|
|
|
AbilityToggleEntry entry = activeToggles.remove(key);
|
2026-02-17 23:08:12 -05:00
|
|
|
if (entry != null) {
|
2026-02-18 22:19:18 -05:00
|
|
|
int oldState = entry.getState();
|
2026-02-17 23:08:12 -05:00
|
|
|
entry.getAbility().onToggleOff(entity);
|
2026-02-18 22:19:18 -05:00
|
|
|
entry.getAbility().onToggleStateChanged(entity, oldState, 0);
|
|
|
|
|
onToggleStateChanged(key, false, 0);
|
2026-02-17 23:08:12 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Hook for subclass to react to toggle state changes (sync packets, script events).
|
|
|
|
|
*/
|
2026-02-18 22:19:18 -05:00
|
|
|
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;
|
2026-02-18 05:13:51 -05:00
|
|
|
}
|
2026-02-17 23:08:12 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear all active toggles (e.g., on death/reset).
|
2026-02-18 22:19:18 -05:00
|
|
|
* Calls onToggleOff and onToggleStateChanged for each active toggle.
|
2026-02-17 23:08:12 -05:00
|
|
|
*/
|
|
|
|
|
protected void clearActiveToggles() {
|
|
|
|
|
if (activeToggles.isEmpty()) return;
|
|
|
|
|
EntityLivingBase entity = getEntity();
|
2026-02-22 03:28:56 -05:00
|
|
|
for (Map.Entry<String, AbilityToggleEntry> mapEntry : activeToggles.entrySet()) {
|
|
|
|
|
AbilityToggleEntry entry = mapEntry.getValue();
|
2026-02-17 23:08:12 -05:00
|
|
|
entry.getAbility().onToggleOff(entity);
|
2026-02-18 22:19:18 -05:00
|
|
|
entry.getAbility().onToggleStateChanged(entity, entry.getState(), 0);
|
2026-02-17 23:08:12 -05:00
|
|
|
}
|
|
|
|
|
activeToggles.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a toggle entry directly without calling onToggleOn.
|
|
|
|
|
* Used for client-side sync and NBT restoration.
|
2026-02-18 22:19:18 -05:00
|
|
|
* @param key The ability key
|
|
|
|
|
* @param state The toggle state (0 = remove, 1+ = set at state)
|
2026-02-17 23:08:12 -05:00
|
|
|
*/
|
2026-02-18 22:19:18 -05:00
|
|
|
public void setToggleEntryDirect(String key, int state) {
|
|
|
|
|
if (state > 0) {
|
2026-02-17 23:08:12 -05:00
|
|
|
Ability ability = AbilityController.Instance != null
|
|
|
|
|
? AbilityController.Instance.resolveAbility(key) : null;
|
|
|
|
|
if (ability != null && ability.isToggleable()) {
|
2026-02-22 03:28:56 -05:00
|
|
|
activeToggles.put(key, new AbilityToggleEntry(ability, state));
|
2026-02-17 23:08:12 -05:00
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
activeToggles.remove(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// COOLDOWN MANAGEMENT
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Check if on cooldown.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
public boolean isOnCooldown() {
|
|
|
|
|
return getWorldTime() < cooldownEndTime;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Get remaining cooldown ticks.
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
public long getRemainingCooldown() {
|
|
|
|
|
long remaining = cooldownEndTime - getWorldTime();
|
|
|
|
|
return remaining > 0 ? remaining : 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Reset cooldown (allow immediate ability use).
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
public void resetCooldown() {
|
|
|
|
|
cooldownEndTime = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Set the cooldown end time directly.
|
|
|
|
|
*/
|
2026-02-18 04:02:51 -05:00
|
|
|
public void setCooldownEndTime(long endTime) {
|
|
|
|
|
cooldownEndTime = endTime;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 03:47:12 -05:00
|
|
|
/**
|
|
|
|
|
* Get the global cooldown end time.
|
|
|
|
|
*/
|
|
|
|
|
public long getCooldownEndTime() {
|
|
|
|
|
return cooldownEndTime;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the global cooldown duration value.
|
|
|
|
|
*/
|
|
|
|
|
public int getGlobalCooldownDurationValue() {
|
|
|
|
|
return globalCooldownDuration;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the per-ability cooldown end times map.
|
|
|
|
|
*/
|
|
|
|
|
public HashMap<String, Long> getPerAbilityCooldownEndTimes() {
|
|
|
|
|
return perAbilityCooldownEndTimes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get the per-ability cooldown durations map.
|
|
|
|
|
*/
|
|
|
|
|
public HashMap<String, Integer> getPerAbilityCooldownDurations() {
|
|
|
|
|
return perAbilityCooldownDurations;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Apply cooldown sync data from server. Called on client side.
|
|
|
|
|
*/
|
|
|
|
|
public void applyCooldownSync(long globalEndTime, int globalDuration,
|
|
|
|
|
HashMap<String, Long> perEndTimes,
|
|
|
|
|
HashMap<String, Integer> perDurations) {
|
|
|
|
|
this.cooldownEndTime = globalEndTime;
|
|
|
|
|
this.globalCooldownDuration = globalDuration;
|
|
|
|
|
this.perAbilityCooldownEndTimes = perEndTimes;
|
|
|
|
|
this.perAbilityCooldownDurations = perDurations;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// PER-ABILITY COOLDOWN MANAGEMENT
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a specific ability is on its per-ability cooldown.
|
|
|
|
|
*/
|
|
|
|
|
public boolean isOnPerAbilityCooldown(String key) {
|
|
|
|
|
Long endTime = perAbilityCooldownEndTimes.get(key);
|
|
|
|
|
return endTime != null && getWorldTime() < endTime;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set a per-ability cooldown.
|
|
|
|
|
*/
|
|
|
|
|
public void setPerAbilityCooldown(String key, long endTime, int duration) {
|
|
|
|
|
perAbilityCooldownEndTimes.put(key, endTime);
|
|
|
|
|
perAbilityCooldownDurations.put(key, duration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reset a specific per-ability cooldown.
|
|
|
|
|
*/
|
|
|
|
|
public void resetPerAbilityCooldown(String key) {
|
|
|
|
|
perAbilityCooldownEndTimes.remove(key);
|
|
|
|
|
perAbilityCooldownDurations.remove(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reset all per-ability cooldowns.
|
|
|
|
|
*/
|
|
|
|
|
public void resetAllPerAbilityCooldowns() {
|
|
|
|
|
perAbilityCooldownEndTimes.clear();
|
|
|
|
|
perAbilityCooldownDurations.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get global cooldown progress for HUD rendering.
|
|
|
|
|
* @return 1.0 when cooldown just started (fully covered), 0.0 when done
|
|
|
|
|
*/
|
|
|
|
|
public float getGlobalCooldownProgress() {
|
|
|
|
|
if (globalCooldownDuration <= 0) return 0f;
|
|
|
|
|
long remaining = cooldownEndTime - getWorldTime();
|
|
|
|
|
if (remaining <= 0) return 0f;
|
|
|
|
|
return Math.min(1f, (float) remaining / globalCooldownDuration);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get per-ability cooldown progress for HUD rendering.
|
|
|
|
|
* @return 1.0 when cooldown just started (fully covered), 0.0 when done
|
|
|
|
|
*/
|
|
|
|
|
public float getPerAbilityCooldownProgress(String key) {
|
|
|
|
|
Long endTime = perAbilityCooldownEndTimes.get(key);
|
|
|
|
|
Integer duration = perAbilityCooldownDurations.get(key);
|
|
|
|
|
if (endTime == null || duration == null || duration <= 0) return 0f;
|
|
|
|
|
long remaining = endTime - getWorldTime();
|
|
|
|
|
if (remaining <= 0) return 0f;
|
|
|
|
|
return Math.min(1f, (float) remaining / duration);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// POSITION LOCKING
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Capture current position to lock entity in place.
|
|
|
|
|
*/
|
|
|
|
|
protected void captureLockedPosition() {
|
|
|
|
|
EntityLivingBase entity = getEntity();
|
|
|
|
|
lockedPosX = entity.posX;
|
|
|
|
|
lockedPosY = entity.posY;
|
|
|
|
|
lockedPosZ = entity.posZ;
|
|
|
|
|
positionLocked = true;
|
2026-02-19 03:12:59 -05:00
|
|
|
|
|
|
|
|
// Record flight state at lock time so airborne players stay at their height
|
|
|
|
|
// even if their flight system deactivates during input suppression
|
|
|
|
|
wasFlyingAtLock = entity instanceof net.minecraft.entity.player.EntityPlayer
|
|
|
|
|
&& AbilityController.Instance != null
|
|
|
|
|
&& AbilityController.Instance.isPlayerFlying((net.minecraft.entity.player.EntityPlayer) entity);
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
onPositionLockChanged(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Release position lock.
|
|
|
|
|
*/
|
|
|
|
|
protected void releaseLockedPosition() {
|
|
|
|
|
positionLocked = false;
|
2026-02-19 03:12:59 -05:00
|
|
|
wasFlyingAtLock = false;
|
2026-02-17 21:03:54 -05:00
|
|
|
onPositionLockChanged(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 03:12:59 -05:00
|
|
|
/**
|
|
|
|
|
* Whether the entity was flying when the position lock was captured.
|
|
|
|
|
*/
|
|
|
|
|
public boolean wasFlyingAtLock() {
|
|
|
|
|
return wasFlyingAtLock;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Hook for subclass to react to position lock state changes (e.g., NPC sets data watcher flag).
|
|
|
|
|
*/
|
|
|
|
|
protected void onPositionLockChanged(boolean locked) {
|
|
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// ROTATION LOCKING
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Release rotation control.
|
|
|
|
|
*/
|
|
|
|
|
protected void releaseRotationControl() {
|
|
|
|
|
rotationLocked = false;
|
|
|
|
|
onRotationLockChanged(false);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Hook for subclass to react to rotation lock state changes (e.g., NPC sets flag, clears hitScan).
|
|
|
|
|
*/
|
|
|
|
|
protected void onRotationLockChanged(boolean locked) {
|
|
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// CORE TICK - Phase transition logic shared between NPC and Player
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tick the currently executing ability. Handles all phase transitions,
|
|
|
|
|
* telegraph/animation/sound lifecycle, and burst management.
|
|
|
|
|
* <p>
|
|
|
|
|
* This is the core shared logic between DataAbilities and PlayerAbilityData.
|
|
|
|
|
* Subclass-specific behavior is injected via abstract methods and hooks.
|
|
|
|
|
*/
|
|
|
|
|
protected void tickCurrentAbility() {
|
2026-02-17 23:22:03 -05:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-19 12:55:37 -05:00
|
|
|
if (startChainEntry(chainTarget)) {
|
|
|
|
|
launchConsecutiveConcurrentEntries(chainTarget);
|
|
|
|
|
}
|
2026-02-17 23:22:03 -05:00
|
|
|
}
|
2026-02-19 12:55:37 -05:00
|
|
|
// Tick concurrent slots even during chain delay
|
|
|
|
|
tickConcurrentSlots();
|
2026-02-17 23:22:03 -05:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
EntityLivingBase entity = getEntity();
|
|
|
|
|
EntityLivingBase target = getTarget();
|
|
|
|
|
AbilityPhase oldPhase = currentAbility.getPhase();
|
|
|
|
|
|
|
|
|
|
// Tick advances time and possibly changes phase
|
|
|
|
|
boolean phaseChanged = currentAbility.tick();
|
|
|
|
|
|
|
|
|
|
// Fire tick event
|
|
|
|
|
fireTickEvent(currentAbility, target);
|
|
|
|
|
|
|
|
|
|
// Fire extender tick hook (e.g., per-tick resource drain)
|
|
|
|
|
if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, entity, target,
|
2026-02-18 05:13:51 -05:00
|
|
|
currentAbility.getPhase(), currentAbility.getCurrentTick())) {
|
2026-02-17 21:03:54 -05:00
|
|
|
interruptCurrentAbility(null, 0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle phase-specific logic
|
|
|
|
|
switch (currentAbility.getPhase()) {
|
|
|
|
|
case WINDUP:
|
|
|
|
|
if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) {
|
|
|
|
|
// Burst replay: re-enter windup - set up locks, telegraph, sound, animation
|
|
|
|
|
if (currentAbility.isRotationLockedDuringWindup()) {
|
|
|
|
|
captureLockedRotation();
|
|
|
|
|
}
|
|
|
|
|
if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) {
|
|
|
|
|
captureLockedPosition();
|
|
|
|
|
}
|
|
|
|
|
spawnTelegraph(currentAbility, target);
|
|
|
|
|
playAbilitySound(currentAbility.getWindUpSound());
|
|
|
|
|
playAbilityAnimation(currentAbility.getWindUpAnimation());
|
|
|
|
|
}
|
|
|
|
|
currentAbility.onWindUpTick(entity, target, currentAbility.getCurrentTick());
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ACTIVE:
|
|
|
|
|
if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) {
|
|
|
|
|
// Just entered ACTIVE phase - lock telegraph positions
|
|
|
|
|
for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) {
|
|
|
|
|
telegraph.lockPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove telegraph unless the ability keeps it during active phase
|
|
|
|
|
if (!currentAbility.keepTelegraphDuringActive()) {
|
|
|
|
|
removeTelegraph(currentAbility);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle rotation control transition from WINDUP to ACTIVE
|
|
|
|
|
if (currentAbility.isRotationLockedDuringActive()) {
|
|
|
|
|
if (!rotationLocked) {
|
|
|
|
|
captureLockedRotation();
|
|
|
|
|
}
|
|
|
|
|
} else if (rotationLocked) {
|
|
|
|
|
releaseRotationControl();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle position lock transition from WINDUP to ACTIVE
|
|
|
|
|
if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) {
|
|
|
|
|
if (!positionLocked) {
|
|
|
|
|
captureLockedPosition();
|
|
|
|
|
}
|
|
|
|
|
} else if (positionLocked) {
|
|
|
|
|
releaseLockedPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Play active sound and animation
|
|
|
|
|
if (!currentAbility.keepTelegraphDuringActive()) {
|
|
|
|
|
playAbilitySound(currentAbility.getActiveSound());
|
|
|
|
|
}
|
|
|
|
|
playAbilityAnimation(currentAbility.getActiveAnimation());
|
|
|
|
|
|
|
|
|
|
// Pre-execute hook (NPC: snap to face target for hit scan)
|
|
|
|
|
onPreExecute(currentAbility, target);
|
|
|
|
|
|
|
|
|
|
// Fire execute event (cancelable)
|
|
|
|
|
if (fireExecuteEvent(currentAbility, target)) {
|
2026-02-18 04:02:51 -05:00
|
|
|
// Script cancelled — abort cleanly instead of leaving stuck in ACTIVE
|
|
|
|
|
currentAbility.interrupt();
|
|
|
|
|
handleAbilityCompletion(target);
|
|
|
|
|
return;
|
2026-02-17 21:03:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call onExecute
|
|
|
|
|
currentAbility.onExecute(entity, target);
|
|
|
|
|
|
|
|
|
|
// Check if ability completed during onExecute
|
|
|
|
|
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
|
|
|
|
|
handleAbilityCompletion(target);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentAbility.onActiveTick(entity, target, currentAbility.getCurrentTick());
|
|
|
|
|
|
|
|
|
|
// Check if ability completed during onActiveTick
|
|
|
|
|
if (currentAbility.getPhase() == AbilityPhase.IDLE) {
|
|
|
|
|
handleAbilityCompletion(target);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 15:31:33 -05:00
|
|
|
// Auto-complete for burst overlap mode only.
|
|
|
|
|
// Non-overlap waits for all entities to die naturally (via allDead check in onActiveTick).
|
2026-02-17 21:03:54 -05:00
|
|
|
if (currentAbility.isBurstEnabled()
|
2026-02-18 15:31:33 -05:00
|
|
|
&& currentAbility.isBurstOverlap()
|
2026-02-17 21:03:54 -05:00
|
|
|
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
|
|
|
|
|
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
|
|
|
|
|
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
|
|
|
|
|
currentAbility.signalCompletion();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if ability entered burst delay
|
|
|
|
|
if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) {
|
|
|
|
|
if (rotationLocked) releaseRotationControl();
|
|
|
|
|
if (positionLocked) releaseLockedPosition();
|
|
|
|
|
onBurstDelayReleaseLocks();
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case BURST_DELAY:
|
|
|
|
|
// Free movement and rotation during burst delay
|
|
|
|
|
if (rotationLocked) releaseRotationControl();
|
|
|
|
|
if (positionLocked) releaseLockedPosition();
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case DAZED:
|
|
|
|
|
// Dazed phase - just wait for phase transition
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case IDLE:
|
|
|
|
|
// Ability completed (reached via tick() from DAZED)
|
|
|
|
|
handleAbilityCompletion(target);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Post-phase hook (NPC: hit scan + movement control)
|
|
|
|
|
onPostPhaseTick(currentAbility, target);
|
2026-02-19 12:55:37 -05:00
|
|
|
|
|
|
|
|
// Tick concurrent slots independently
|
|
|
|
|
tickConcurrentSlots();
|
2026-02-17 21:03:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// ABILITY COMPLETION
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle ability completion. Called when ability phase becomes IDLE.
|
2026-02-17 23:22:03 -05:00
|
|
|
* If a chain is active, advances to the next entry instead of completing.
|
2026-02-17 21:03:54 -05:00
|
|
|
*/
|
|
|
|
|
protected void handleAbilityCompletion(EntityLivingBase target) {
|
|
|
|
|
if (currentAbility == null) return;
|
|
|
|
|
|
|
|
|
|
// Remove any remaining telegraphs
|
|
|
|
|
removeTelegraph(currentAbility);
|
|
|
|
|
|
|
|
|
|
// Fire extender complete hook
|
|
|
|
|
AbilityController.Instance.fireOnAbilityComplete(currentAbility, getEntity(), target, false);
|
|
|
|
|
|
|
|
|
|
// Call onComplete callback
|
|
|
|
|
currentAbility.onComplete(getEntity(), target);
|
|
|
|
|
|
|
|
|
|
// Fire complete event
|
|
|
|
|
fireCompleteEvent(currentAbility, target);
|
|
|
|
|
|
|
|
|
|
// Release locks
|
|
|
|
|
releaseRotationControl();
|
|
|
|
|
releaseLockedPosition();
|
|
|
|
|
stopAbilityAnimation();
|
2026-02-17 23:22:03 -05:00
|
|
|
|
|
|
|
|
// Chain mode: advance to next entry instead of completing
|
|
|
|
|
if (currentChain != null) {
|
2026-02-18 15:31:33 -05:00
|
|
|
// AFTER semantics: delay from the CURRENT (just-completed) entry
|
|
|
|
|
ChainedAbilityEntry completedEntry = currentChain.getEntries().get(chainEntryIndex);
|
|
|
|
|
int delay = completedEntry.getDelayTicks();
|
|
|
|
|
|
2026-02-17 23:22:03 -05:00
|
|
|
chainEntryIndex++;
|
2026-02-19 12:55:37 -05:00
|
|
|
|
|
|
|
|
// Skip any consecutive concurrent entries — launch them to concurrent slots
|
|
|
|
|
launchConsecutiveConcurrentEntries(target);
|
|
|
|
|
|
2026-02-17 23:22:03 -05:00
|
|
|
if (chainEntryIndex < currentChain.getEntries().size()) {
|
|
|
|
|
// Check if target died and retarget
|
|
|
|
|
if (target != null && target.isDead) {
|
|
|
|
|
target = retargetForChain();
|
|
|
|
|
if (target == null) {
|
|
|
|
|
completeChain();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 15:31:33 -05:00
|
|
|
// Enforce minimum 1-tick delay between chain entries
|
|
|
|
|
chainDelayRemaining = Math.max(1, delay);
|
|
|
|
|
currentAbility = null;
|
2026-02-17 23:22:03 -05:00
|
|
|
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);
|
|
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
onAbilityComplete();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// EXECUTE IMMEDIATE
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Execute an ability immediately (no windup). Called when windUpTicks is 0.
|
|
|
|
|
*/
|
|
|
|
|
protected void executeImmediate(Ability ability, EntityLivingBase target) {
|
|
|
|
|
// Play active sound and animation
|
|
|
|
|
playAbilitySound(ability.getActiveSound());
|
|
|
|
|
playAbilityAnimation(ability.getActiveAnimation());
|
|
|
|
|
|
|
|
|
|
// Fire execute event (cancelable)
|
|
|
|
|
if (fireExecuteEvent(ability, target)) {
|
2026-02-18 04:02:51 -05:00
|
|
|
// Script cancelled — abort cleanly instead of leaving stuck in ACTIVE
|
|
|
|
|
ability.interrupt();
|
|
|
|
|
handleAbilityCompletion(target);
|
|
|
|
|
return;
|
2026-02-17 21:03:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call onExecute
|
|
|
|
|
ability.onExecute(getEntity(), target);
|
|
|
|
|
|
|
|
|
|
// Check if ability completed during onExecute
|
|
|
|
|
if (ability.getPhase() == AbilityPhase.IDLE) {
|
|
|
|
|
handleAbilityCompletion(target);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// INTERRUPTION
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Interrupt the currently executing ability.
|
|
|
|
|
* Shared interrupt logic: remove telegraph, play dazed animation,
|
|
|
|
|
* release locks, fire events, call ability.interrupt().
|
|
|
|
|
*/
|
|
|
|
|
public void interruptCurrentAbility(DamageSource source, float damage) {
|
|
|
|
|
if (currentAbility != null && currentAbility.isExecuting()) {
|
|
|
|
|
// Remove telegraph
|
|
|
|
|
removeTelegraph(currentAbility);
|
|
|
|
|
|
|
|
|
|
// Fire extender complete hook (interrupted)
|
|
|
|
|
AbilityController.Instance.fireOnAbilityComplete(currentAbility, getEntity(), getTarget(), true);
|
|
|
|
|
|
|
|
|
|
// Fire interrupt event
|
|
|
|
|
fireInterruptEvent(currentAbility, getTarget(), source, damage);
|
|
|
|
|
|
|
|
|
|
// Call ability callbacks
|
|
|
|
|
currentAbility.onInterrupt(getEntity(), source, damage);
|
|
|
|
|
currentAbility.interrupt();
|
|
|
|
|
|
|
|
|
|
// Play dazed animation
|
|
|
|
|
stopAbilityAnimation();
|
|
|
|
|
playAbilityAnimation(currentAbility.getDazedAnimation());
|
|
|
|
|
|
|
|
|
|
// Release locks
|
|
|
|
|
releaseRotationControl();
|
|
|
|
|
releaseLockedPosition();
|
|
|
|
|
|
2026-02-17 23:22:03 -05:00
|
|
|
// If chain was active, roll chain cooldown and clear chain state
|
|
|
|
|
if (currentChain != null) {
|
|
|
|
|
rollChainCooldown(currentChain);
|
|
|
|
|
interruptCooldownRolled = true;
|
|
|
|
|
currentChain = null;
|
|
|
|
|
chainEntryIndex = -1;
|
|
|
|
|
chainDelayRemaining = -1;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 12:55:37 -05:00
|
|
|
// Interrupt all concurrent slots
|
|
|
|
|
interruptConcurrentSlots();
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// Let subclass handle cleanup
|
|
|
|
|
onInterruptComplete();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Hook called after interrupt completes. Subclass handles clearing state and syncing.
|
|
|
|
|
* NPC: keeps currentAbility (ticks through DAZED). Player: clears immediately.
|
|
|
|
|
*/
|
2026-02-18 05:13:51 -05:00
|
|
|
protected void onInterruptComplete() {
|
2026-02-21 05:09:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cancel the currently executing ability (voluntary player action).
|
|
|
|
|
* Unlike interrupt, this always goes directly to IDLE (never DAZED),
|
|
|
|
|
* immediately rolls cooldown, and clears state.
|
|
|
|
|
* Also handles cancellation during chain delay (between chain entries).
|
|
|
|
|
*/
|
|
|
|
|
public void cancelCurrentAbility() {
|
|
|
|
|
boolean hasExecutingAbility = currentAbility != null && currentAbility.isExecuting();
|
|
|
|
|
boolean isInChainDelay = currentChain != null && chainDelayRemaining > 0;
|
|
|
|
|
|
|
|
|
|
if (!hasExecutingAbility && !isInChainDelay) return;
|
|
|
|
|
|
|
|
|
|
if (hasExecutingAbility) {
|
|
|
|
|
AbilityPhase phase = currentAbility.getPhase();
|
|
|
|
|
// Cannot cancel during DAZED (already interrupted)
|
|
|
|
|
if (phase == AbilityPhase.DAZED) return;
|
|
|
|
|
// Only allow cancel during active phases
|
|
|
|
|
if (phase != AbilityPhase.WINDUP && phase != AbilityPhase.ACTIVE
|
|
|
|
|
&& phase != AbilityPhase.BURST_DELAY) return;
|
|
|
|
|
|
|
|
|
|
// Remove telegraph
|
|
|
|
|
removeTelegraph(currentAbility);
|
|
|
|
|
|
|
|
|
|
// Fire extender complete hook (cancelled = interrupted)
|
|
|
|
|
AbilityController.Instance.fireOnAbilityComplete(currentAbility, getEntity(), getTarget(), true);
|
|
|
|
|
|
|
|
|
|
// Fire interrupt event (source=null, damage=0 for voluntary cancel)
|
|
|
|
|
fireInterruptEvent(currentAbility, getTarget(), null, 0);
|
|
|
|
|
|
|
|
|
|
// Cancel the ability (cleanup + go directly to IDLE, no DAZED)
|
|
|
|
|
currentAbility.onInterrupt(getEntity(), null, 0);
|
|
|
|
|
currentAbility.cancel();
|
|
|
|
|
|
|
|
|
|
// Release locks and stop animation
|
|
|
|
|
stopAbilityAnimation();
|
|
|
|
|
releaseRotationControl();
|
|
|
|
|
releaseLockedPosition();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Roll cooldown
|
|
|
|
|
if (currentChain != null) {
|
|
|
|
|
rollChainCooldown(currentChain);
|
|
|
|
|
currentChain = null;
|
|
|
|
|
chainEntryIndex = -1;
|
|
|
|
|
chainDelayRemaining = -1;
|
|
|
|
|
} else if (currentAbility != null) {
|
|
|
|
|
rollCooldown(currentAbility);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Interrupt all concurrent slots
|
|
|
|
|
interruptConcurrentSlots();
|
|
|
|
|
|
|
|
|
|
// Clear state via the normal completion path
|
|
|
|
|
onAbilityComplete();
|
2026-02-18 05:13:51 -05:00
|
|
|
}
|
2026-02-17 23:22:03 -05:00
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// 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;
|
|
|
|
|
|
2026-02-18 15:31:33 -05:00
|
|
|
// AFTER semantics: no delay before first entry (delay applies after each entry completes)
|
2026-02-19 12:55:37 -05:00
|
|
|
boolean started = startChainEntry(target);
|
|
|
|
|
if (started) {
|
|
|
|
|
launchConsecutiveConcurrentEntries(target);
|
|
|
|
|
}
|
|
|
|
|
return started;
|
2026-02-17 23:22:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
2026-02-18 04:02:51 -05:00
|
|
|
Ability ability = entry.resolve();
|
2026-02-17 23:22:03 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 14:07:05 -05:00
|
|
|
// If windUpAll=false, only the first ability windups; subsequent abilities skip windup
|
|
|
|
|
if (!currentChain.isWindUpAll() && chainEntryIndex > 0) {
|
2026-02-17 23:22:03 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 12:55:37 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// CONCURRENT SLOT MANAGEMENT
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tick all active concurrent slots. Remove completed ones.
|
|
|
|
|
* Called from tickCurrentAbility() and during chain delay countdown.
|
|
|
|
|
*/
|
|
|
|
|
protected void tickConcurrentSlots() {
|
|
|
|
|
if (concurrentSlots.isEmpty()) return;
|
|
|
|
|
|
|
|
|
|
EntityLivingBase entity = getEntity();
|
|
|
|
|
EntityLivingBase target = getTarget();
|
|
|
|
|
|
|
|
|
|
Iterator<ConcurrentSlot> it = concurrentSlots.iterator();
|
|
|
|
|
while (it.hasNext()) {
|
|
|
|
|
ConcurrentSlot slot = it.next();
|
|
|
|
|
slot.tick(entity, target);
|
|
|
|
|
if (slot.isCompleted()) {
|
|
|
|
|
it.remove();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* After starting a primary chain entry, scan forward and launch any consecutive
|
|
|
|
|
* concurrent entries (with delay=0) to concurrent slots immediately.
|
|
|
|
|
* This enables simultaneous execution of Effect abilities alongside the primary.
|
|
|
|
|
*/
|
|
|
|
|
protected void launchConsecutiveConcurrentEntries(EntityLivingBase target) {
|
|
|
|
|
if (currentChain == null) return;
|
|
|
|
|
|
|
|
|
|
List<ChainedAbilityEntry> entries = currentChain.getEntries();
|
|
|
|
|
while (chainEntryIndex + 1 < entries.size()) {
|
|
|
|
|
ChainedAbilityEntry nextEntry = entries.get(chainEntryIndex + 1);
|
|
|
|
|
Ability resolved = nextEntry.resolve();
|
|
|
|
|
if (resolved == null || !resolved.isConcurrentCapable()
|
|
|
|
|
|| !nextEntry.isConcurrentEnabled()) break;
|
|
|
|
|
|
|
|
|
|
// Only auto-launch concurrent entries with 0 delay
|
|
|
|
|
if (nextEntry.getDelayTicks() > 0) break;
|
|
|
|
|
|
|
|
|
|
// Advance past this entry in the chain (it runs concurrently, not sequentially)
|
|
|
|
|
chainEntryIndex++;
|
|
|
|
|
|
|
|
|
|
// Deep copy the ability so it has its own state
|
|
|
|
|
Ability concurrentCopy = AbilityController.Instance.fromNBT(resolved.writeNBT());
|
|
|
|
|
if (concurrentCopy == null) continue;
|
|
|
|
|
|
|
|
|
|
ConcurrentSlot slot = new ConcurrentSlot(concurrentCopy);
|
|
|
|
|
concurrentSlots.add(slot);
|
|
|
|
|
slot.start(getEntity(), target);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Interrupt all active concurrent slots. Called on ability interrupt or combat reset.
|
|
|
|
|
*/
|
|
|
|
|
protected void interruptConcurrentSlots() {
|
|
|
|
|
for (ConcurrentSlot slot : concurrentSlots) {
|
|
|
|
|
slot.interrupt();
|
|
|
|
|
}
|
|
|
|
|
concurrentSlots.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 23:22:03 -05:00
|
|
|
/**
|
|
|
|
|
* Complete the chain execution. Rolls chain cooldown and clears all chain state.
|
2026-02-19 12:55:37 -05:00
|
|
|
* NOTE: Does NOT clear concurrent slots — they outlive the chain and tick independently.
|
2026-02-17 23:22:03 -05:00
|
|
|
*/
|
|
|
|
|
protected void completeChain() {
|
|
|
|
|
if (currentChain != null) {
|
|
|
|
|
rollChainCooldown(currentChain);
|
|
|
|
|
}
|
|
|
|
|
currentChain = null;
|
|
|
|
|
chainEntryIndex = -1;
|
|
|
|
|
chainDelayRemaining = -1;
|
|
|
|
|
currentAbility = null;
|
|
|
|
|
onAbilityComplete();
|
|
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
}
|