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-17 21:03:54 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
|
2026-02-17 23:22:03 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.ChainedAbility;
|
|
|
|
|
import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry;
|
2026-02-17 23:08:12 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.ToggleEntry;
|
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;
|
|
|
|
|
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
|
|
|
/**
|
|
|
|
|
* World time when cooldown ends
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
protected long cooldownEndTime = 0;
|
|
|
|
|
|
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;
|
|
|
|
|
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-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-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
|
|
|
/**
|
|
|
|
|
* Currently active toggles. Key = ability key, Value = toggle entry with tick counter.
|
|
|
|
|
*/
|
2026-02-17 23:08:12 -05:00
|
|
|
protected Map<String, ToggleEntry> activeToggles = new LinkedHashMap<>();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Toggle an ability ON or OFF. If currently on, turns off; if off, turns on.
|
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")
|
|
|
|
|
* @return true if the toggle is now ON, false if OFF
|
|
|
|
|
*/
|
|
|
|
|
public boolean toggleAbility(String key) {
|
|
|
|
|
if (activeToggles.containsKey(key)) {
|
|
|
|
|
deactivateToggle(key);
|
|
|
|
|
return false;
|
|
|
|
|
} else {
|
|
|
|
|
return activateToggle(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set a toggle to a specific state.
|
2026-02-18 05:13:51 -05:00
|
|
|
*
|
2026-02-17 23:08:12 -05:00
|
|
|
* @param key The ability key
|
2026-02-18 05:13:51 -05:00
|
|
|
* @param on true to activate, false to deactivate
|
2026-02-17 23:08:12 -05:00
|
|
|
*/
|
|
|
|
|
public void setAbilityToggled(String key, boolean on) {
|
|
|
|
|
if (on && !activeToggles.containsKey(key)) {
|
|
|
|
|
activateToggle(key);
|
|
|
|
|
} else if (!on && activeToggles.containsKey(key)) {
|
|
|
|
|
deactivateToggle(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a toggle ability is currently active.
|
|
|
|
|
*/
|
|
|
|
|
public boolean isAbilityToggled(String key) {
|
|
|
|
|
return activeToggles.containsKey(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all active toggle keys.
|
|
|
|
|
*/
|
|
|
|
|
public Set<String> getActiveToggleKeys() {
|
|
|
|
|
return new LinkedHashSet<>(activeToggles.keySet());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean activateToggle(String key) {
|
|
|
|
|
Ability ability = AbilityController.Instance != null
|
|
|
|
|
? AbilityController.Instance.resolveAbility(key) : null;
|
|
|
|
|
if (ability == null || !ability.isToggleable()) return false;
|
|
|
|
|
|
|
|
|
|
ToggleEntry entry = new ToggleEntry(ability);
|
|
|
|
|
activeToggles.put(key, entry);
|
|
|
|
|
ability.onToggleOn(getEntity());
|
|
|
|
|
onToggleStateChanged(key, true);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void deactivateToggle(String key) {
|
|
|
|
|
ToggleEntry entry = activeToggles.remove(key);
|
|
|
|
|
if (entry != null) {
|
|
|
|
|
entry.getAbility().onToggleOff(getEntity());
|
|
|
|
|
onToggleStateChanged(key, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
|
|
|
|
protected void tickActiveToggles() {
|
|
|
|
|
if (activeToggles.isEmpty()) return;
|
|
|
|
|
|
|
|
|
|
EntityLivingBase entity = getEntity();
|
|
|
|
|
List<String> toRemove = null;
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, ToggleEntry> mapEntry : activeToggles.entrySet()) {
|
|
|
|
|
ToggleEntry entry = mapEntry.getValue();
|
|
|
|
|
entry.incrementTick();
|
|
|
|
|
|
|
|
|
|
if (entry.getAbility().hasActiveToggle()) {
|
|
|
|
|
if (!entry.getAbility().onToggleTick(entity, entry.getTickCount())) {
|
|
|
|
|
if (toRemove == null) toRemove = new ArrayList<>();
|
|
|
|
|
toRemove.add(mapEntry.getKey());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (toRemove != null) {
|
|
|
|
|
for (String key : toRemove) {
|
|
|
|
|
ToggleEntry entry = activeToggles.remove(key);
|
|
|
|
|
if (entry != null) {
|
|
|
|
|
entry.getAbility().onToggleOff(entity);
|
|
|
|
|
onToggleStateChanged(key, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Hook for subclass to react to toggle state changes (sync packets, script events).
|
|
|
|
|
*/
|
|
|
|
|
protected void onToggleStateChanged(String key, boolean active) {
|
|
|
|
|
}
|
2026-02-17 23:08:12 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Clear all active toggles (e.g., on death/reset).
|
|
|
|
|
* Calls onToggleOff for each active toggle.
|
|
|
|
|
*/
|
|
|
|
|
protected void clearActiveToggles() {
|
|
|
|
|
if (activeToggles.isEmpty()) return;
|
|
|
|
|
EntityLivingBase entity = getEntity();
|
|
|
|
|
for (ToggleEntry entry : activeToggles.values()) {
|
|
|
|
|
entry.getAbility().onToggleOff(entity);
|
|
|
|
|
}
|
|
|
|
|
activeToggles.clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a toggle entry directly without calling onToggleOn.
|
|
|
|
|
* Used for client-side sync and NBT restoration.
|
|
|
|
|
*/
|
|
|
|
|
public void setToggleEntryDirect(String key, boolean active) {
|
|
|
|
|
if (active) {
|
|
|
|
|
Ability ability = AbilityController.Instance != null
|
|
|
|
|
? AbilityController.Instance.resolveAbility(key) : null;
|
|
|
|
|
if (ability != null && ability.isToggleable()) {
|
|
|
|
|
activeToggles.put(key, new ToggleEntry(ability));
|
|
|
|
|
}
|
|
|
|
|
} 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-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;
|
|
|
|
|
onPositionLockChanged(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Release position lock.
|
|
|
|
|
*/
|
|
|
|
|
protected void releaseLockedPosition() {
|
|
|
|
|
positionLocked = false;
|
|
|
|
|
onPositionLockChanged(false);
|
|
|
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
startChainEntry(chainTarget);
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// 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++;
|
|
|
|
|
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-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-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-17 23:22:03 -05:00
|
|
|
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);
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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();
|
|
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
}
|