2026-01-30 05:01:54 -05:00
|
|
|
package noppes.npcs.controllers.data;
|
|
|
|
|
|
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-01-30 05:01: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.IAbilityAction;
|
2026-02-18 22:19:18 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.ToggleEntry;
|
2026-02-12 18:53:40 -05:00
|
|
|
import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
|
2026-01-30 05:01:54 -05:00
|
|
|
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
2026-02-20 03:47:12 -05:00
|
|
|
import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket;
|
2026-02-18 05:13:51 -05:00
|
|
|
import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket;
|
|
|
|
|
import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket;
|
2026-01-30 05:01:54 -05:00
|
|
|
import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
|
|
|
|
|
import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket;
|
|
|
|
|
import net.minecraft.entity.EntityLivingBase;
|
|
|
|
|
import net.minecraft.entity.player.EntityPlayer;
|
|
|
|
|
import net.minecraft.entity.player.EntityPlayerMP;
|
|
|
|
|
import net.minecraft.nbt.NBTTagCompound;
|
|
|
|
|
import net.minecraft.nbt.NBTTagList;
|
|
|
|
|
import net.minecraft.nbt.NBTTagString;
|
|
|
|
|
import net.minecraft.util.DamageSource;
|
2026-02-17 21:03:54 -05:00
|
|
|
import noppes.npcs.AbstractDataAbilities;
|
2026-02-16 01:03:05 -05:00
|
|
|
import noppes.npcs.EventHooks;
|
2026-02-18 05:13:51 -05:00
|
|
|
import noppes.npcs.LogWriter;
|
2026-01-30 05:01:54 -05:00
|
|
|
import noppes.npcs.api.ability.IPlayerAbilityData;
|
2026-02-16 01:03:05 -05:00
|
|
|
import noppes.npcs.api.entity.IPlayer;
|
|
|
|
|
import noppes.npcs.controllers.ScriptController;
|
|
|
|
|
import noppes.npcs.scripted.NpcAPI;
|
|
|
|
|
import noppes.npcs.scripted.event.player.PlayerAbilityEvent;
|
2026-01-30 05:01:54 -05:00
|
|
|
|
|
|
|
|
import java.util.ArrayList;
|
2026-02-16 01:39:14 -05:00
|
|
|
import java.util.Iterator;
|
2026-01-30 05:01:54 -05:00
|
|
|
import java.util.List;
|
2026-02-18 22:19:18 -05:00
|
|
|
import java.util.Map;
|
2026-01-30 05:01:54 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Manages player ability data - unlocked abilities, selection, cooldowns, and execution.
|
|
|
|
|
* Players reference abilities by key (built-in keys or preset names), never customized copies.
|
2026-02-17 21:03:54 -05:00
|
|
|
* Extends AbstractDataAbilities for shared ability lifecycle logic.
|
2026-01-30 05:01:54 -05:00
|
|
|
* <p>
|
|
|
|
|
* Key differences from NPC DataAbilities:
|
|
|
|
|
* - Universal cooldown (shared across all abilities)
|
|
|
|
|
* - Player manually selects ability (no weighted random)
|
|
|
|
|
* - Conditions requiring a target are skipped
|
|
|
|
|
* - Uses ability's base cooldownTicks only
|
|
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerAbilityData {
|
2026-01-30 05:01:54 -05:00
|
|
|
|
|
|
|
|
private final PlayerData playerData;
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// CONFIGURATION (saved to NBT)
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* List of unlocked ability keys (preset names or built-in keys).
|
|
|
|
|
* Order matters - determines selection index.
|
|
|
|
|
*/
|
|
|
|
|
private List<String> unlockedAbilities = new ArrayList<>();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Currently selected ability index in the unlocked list.
|
|
|
|
|
*/
|
|
|
|
|
private int selectedIndex = 0;
|
|
|
|
|
|
2026-02-15 22:03:32 -05:00
|
|
|
/**
|
|
|
|
|
* Tracks whether the current animation was started by an ability.
|
|
|
|
|
* Persisted to NBT so orphaned ability animations can be detected and
|
|
|
|
|
* cleared on login (ability state is transient, but animation state is not).
|
|
|
|
|
*/
|
|
|
|
|
private boolean playingAbilityAnimation = false;
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-02-17 21:03:54 -05:00
|
|
|
// PLAYER-SPECIFIC RUNTIME STATE (not saved)
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Current ability's key (for cooldown tracking after completion).
|
|
|
|
|
*/
|
|
|
|
|
private transient String currentAbilityKey;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Target for the current ability (can be null for self-targeting).
|
|
|
|
|
*/
|
|
|
|
|
private transient EntityLivingBase currentTarget;
|
|
|
|
|
|
2026-02-14 03:50:28 -05:00
|
|
|
/**
|
|
|
|
|
* Last synced state flags byte for change detection (avoids spamming packets).
|
|
|
|
|
*/
|
|
|
|
|
private transient byte lastSyncedFlags = 0;
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// CONSTRUCTOR
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
public PlayerAbilityData(PlayerData playerData) {
|
|
|
|
|
this.playerData = playerData;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-02-17 21:03:54 -05:00
|
|
|
// ABSTRACT METHOD IMPLEMENTATIONS
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected EntityLivingBase getEntity() {
|
|
|
|
|
return playerData.player;
|
|
|
|
|
}
|
2026-02-14 03:50:28 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected EntityLivingBase getTarget() {
|
|
|
|
|
return currentTarget;
|
|
|
|
|
}
|
2026-02-14 03:50:28 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected long getWorldTime() {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
return player != null ? player.worldObj.getTotalWorldTime() : 0;
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void fireTickEvent(Ability ability, EntityLivingBase target) {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (ScriptController.Instance == null || player == null) return;
|
|
|
|
|
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
if (handler == null) return;
|
|
|
|
|
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
PlayerAbilityEvent.TickEvent event = new PlayerAbilityEvent.TickEvent(
|
|
|
|
|
iPlayer, ability, target, ability.getPhase().ordinal(), ability.getCurrentTick());
|
|
|
|
|
EventHooks.onPlayerAbilityTick(handler, event);
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected boolean fireExecuteEvent(Ability ability, EntityLivingBase target) {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (ScriptController.Instance == null || player == null) return false;
|
|
|
|
|
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
if (handler == null) return false;
|
|
|
|
|
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
PlayerAbilityEvent.ExecuteEvent event = new PlayerAbilityEvent.ExecuteEvent(iPlayer, ability, target);
|
|
|
|
|
return EventHooks.onPlayerAbilityExecute(handler, event);
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void fireCompleteEvent(Ability ability, EntityLivingBase target) {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (ScriptController.Instance == null || player == null) return;
|
|
|
|
|
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
if (handler == null) return;
|
|
|
|
|
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
PlayerAbilityEvent.CompleteEvent event = new PlayerAbilityEvent.CompleteEvent(iPlayer, ability, target);
|
|
|
|
|
EventHooks.onPlayerAbilityComplete(handler, event);
|
|
|
|
|
}
|
2026-02-16 01:03:05 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void fireInterruptEvent(Ability ability, EntityLivingBase target,
|
2026-02-18 05:13:51 -05:00
|
|
|
DamageSource source, float damage) {
|
2026-02-17 21:03:54 -05:00
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (ScriptController.Instance == null || player == null) return;
|
|
|
|
|
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
if (handler == null) return;
|
|
|
|
|
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
PlayerAbilityEvent.InterruptEvent event = new PlayerAbilityEvent.InterruptEvent(
|
|
|
|
|
iPlayer, ability, target, source, damage);
|
|
|
|
|
EventHooks.onPlayerAbilityInterrupt(handler, event);
|
|
|
|
|
}
|
2026-02-14 21:06:12 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void spawnTelegraph(Ability ability, EntityLivingBase target) {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
List<TelegraphInstance> telegraphs = ability.createTelegraphs(player, target);
|
|
|
|
|
if (!telegraphs.isEmpty()) {
|
|
|
|
|
ability.setTelegraphInstances(telegraphs);
|
|
|
|
|
if (player instanceof EntityPlayerMP) {
|
|
|
|
|
for (TelegraphInstance telegraph : telegraphs) {
|
|
|
|
|
TelegraphSpawnPacket.sendToTracking(telegraph, player);
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void removeTelegraph(Ability ability) {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
List<TelegraphInstance> telegraphs = ability.getTelegraphInstances();
|
|
|
|
|
for (TelegraphInstance telegraph : telegraphs) {
|
|
|
|
|
if (player instanceof EntityPlayerMP) {
|
|
|
|
|
TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ability.setTelegraphInstances(null);
|
|
|
|
|
}
|
2026-02-14 03:50:28 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void setAnimationData(Animation animation) {
|
|
|
|
|
playerData.animationData.setEnabled(true);
|
|
|
|
|
playerData.animationData.setAnimation(animation);
|
|
|
|
|
playerData.animationData.updateClient();
|
|
|
|
|
playingAbilityAnimation = true;
|
|
|
|
|
}
|
2026-02-14 15:21:56 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void clearAnimationData() {
|
|
|
|
|
playerData.animationData.setAnimation(null);
|
|
|
|
|
playerData.animationData.updateClient();
|
|
|
|
|
playingAbilityAnimation = false;
|
|
|
|
|
}
|
2026-02-14 03:50:28 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void playAbilitySound(String sound) {
|
|
|
|
|
if (sound != null && !sound.isEmpty()) {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (player != null) {
|
|
|
|
|
player.worldObj.playSoundAtEntity(player, sound, 1.0f, 1.0f);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void captureLockedRotation() {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
lockedYaw = player.rotationYaw;
|
|
|
|
|
lockedPitch = player.rotationPitch;
|
|
|
|
|
rotationLocked = true;
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void rollCooldown(Ability ability) {
|
2026-02-20 03:47:12 -05:00
|
|
|
if (ability.isIgnoreCooldown()) return;
|
|
|
|
|
|
|
|
|
|
int duration = ability.getCooldownTicks();
|
|
|
|
|
long endTime = getWorldTime() + duration;
|
|
|
|
|
|
|
|
|
|
if (ability.isPerAbilityCooldown()) {
|
|
|
|
|
// Per-ability: only this ability goes on cooldown
|
|
|
|
|
if (currentAbilityKey != null) {
|
|
|
|
|
setPerAbilityCooldown(currentAbilityKey, endTime, duration);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Global: all global-cooldown abilities share this cooldown
|
|
|
|
|
cooldownEndTime = endTime;
|
|
|
|
|
globalCooldownDuration = duration;
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
2026-02-20 03:47:12 -05:00
|
|
|
syncCooldownToClient();
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 23:22:03 -05:00
|
|
|
@Override
|
|
|
|
|
protected void rollChainCooldown(ChainedAbility chain) {
|
2026-02-20 03:47:12 -05:00
|
|
|
int duration = chain.getCooldownTicks();
|
|
|
|
|
cooldownEndTime = getWorldTime() + duration;
|
|
|
|
|
globalCooldownDuration = duration;
|
|
|
|
|
syncCooldownToClient();
|
2026-02-17 23:22:03 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void onAbilityComplete() {
|
|
|
|
|
currentAbility = null;
|
|
|
|
|
currentAbilityKey = null;
|
|
|
|
|
currentTarget = null;
|
|
|
|
|
syncAbilityStateClear(playerData.player);
|
|
|
|
|
}
|
2026-02-14 21:06:12 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// HOOK OVERRIDES
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
protected void onInterruptComplete() {
|
|
|
|
|
// Player clears state immediately (unlike NPC which ticks through DAZED)
|
2026-02-20 02:16:26 -05:00
|
|
|
// Stop the dazed animation that was started in interruptCurrentAbility()
|
|
|
|
|
stopAbilityAnimation();
|
2026-02-17 21:03:54 -05:00
|
|
|
currentAbility = null;
|
|
|
|
|
currentAbilityKey = null;
|
|
|
|
|
currentTarget = null;
|
|
|
|
|
syncAbilityStateClear(playerData.player);
|
|
|
|
|
}
|
2026-02-16 01:03:05 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// TICK - Called from player tick handler
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-02-14 03:50:28 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
/**
|
|
|
|
|
* Main tick method - handles ability execution.
|
|
|
|
|
* Should be called every server tick.
|
|
|
|
|
*/
|
|
|
|
|
public void tick(EntityPlayer player) {
|
|
|
|
|
if (player.worldObj.isRemote || player.isDead) {
|
|
|
|
|
return;
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
// Tick active toggles (independent of currentAbility)
|
|
|
|
|
tickActiveToggles();
|
|
|
|
|
|
2026-02-17 23:22:03 -05:00
|
|
|
if (chainDelayRemaining > 0 || (currentAbility != null && currentAbility.isExecuting())) {
|
2026-02-17 21:03:54 -05:00
|
|
|
tickCurrentAbility();
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// Apply rotation and position locks after ability tick
|
|
|
|
|
applyRotationControl(player);
|
|
|
|
|
applyPositionLock(player);
|
2026-02-14 03:50:28 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// Sync lock state to client (only sends when flags change)
|
|
|
|
|
syncAbilityStateIfNeeded(player);
|
|
|
|
|
} else {
|
|
|
|
|
// Safety: release orphaned locks if no ability is executing
|
|
|
|
|
if (rotationLocked || positionLocked) {
|
|
|
|
|
releaseRotationControl();
|
|
|
|
|
releaseLockedPosition();
|
|
|
|
|
syncAbilityStateClear(player);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// ABILITY ACTIVATION - Called on Special Key press
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Activate the currently selected ability.
|
|
|
|
|
* Called when the player presses the Special Key.
|
|
|
|
|
*
|
|
|
|
|
* @param player The player
|
|
|
|
|
* @return true if the ability was started
|
|
|
|
|
*/
|
|
|
|
|
public boolean activateAbility(EntityPlayer player) {
|
|
|
|
|
if (player.worldObj.isRemote) return false;
|
|
|
|
|
if (unlockedAbilities.isEmpty()) return false;
|
|
|
|
|
if (selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) return false;
|
|
|
|
|
|
|
|
|
|
String key = unlockedAbilities.get(selectedIndex);
|
|
|
|
|
return activateAbility(player, key);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 05:13:51 -05:00
|
|
|
/**
|
|
|
|
|
* Prefix used to identify chained ability keys in the unlocked list.
|
|
|
|
|
*/
|
2026-02-17 23:22:03 -05:00
|
|
|
public static final String CHAIN_PREFIX = "chain:";
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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)) {
|
2026-02-18 04:02:51 -05:00
|
|
|
return AbilityController.Instance.resolveChainedAbility(key.substring(CHAIN_PREFIX.length()));
|
2026-02-17 23:22:03 -05:00
|
|
|
}
|
|
|
|
|
return AbilityController.Instance.resolveAbility(key);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
/**
|
2026-02-17 23:22:03 -05:00
|
|
|
* Activate a specific ability by key. Keys prefixed with "chain:" are
|
2026-02-18 04:02:51 -05:00
|
|
|
* treated as chained abilities and resolved from {@link AbilityController}.
|
2026-01-30 05:01:54 -05:00
|
|
|
*
|
|
|
|
|
* @param player The player
|
2026-02-17 23:22:03 -05:00
|
|
|
* @param key The ability key (built-in or preset name), or "chain:name" for chains
|
2026-01-30 05:01:54 -05:00
|
|
|
* @return true if the ability was started
|
|
|
|
|
*/
|
|
|
|
|
public boolean activateAbility(EntityPlayer player, String key) {
|
|
|
|
|
if (player.worldObj.isRemote || key == null || key.isEmpty()) return false;
|
|
|
|
|
|
|
|
|
|
// Can't activate if already executing
|
|
|
|
|
if (currentAbility != null && currentAbility.isExecuting()) return false;
|
2026-02-17 23:22:03 -05:00
|
|
|
if (isExecutingChain()) return false;
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 23:22:03 -05:00
|
|
|
// Resolve via unified resolver
|
|
|
|
|
IAbilityAction action = resolveActionKey(key);
|
|
|
|
|
if (action == null) return false;
|
2026-01-30 05:01:54 -05:00
|
|
|
|
|
|
|
|
// Check user type allows player
|
2026-02-17 23:22:03 -05:00
|
|
|
if (!action.getAllowedBy().allowsPlayer()) return false;
|
2026-01-30 05:01:54 -05:00
|
|
|
|
|
|
|
|
// Check conditions (skip target-requiring ones)
|
2026-02-17 23:22:03 -05:00
|
|
|
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;
|
|
|
|
|
|
2026-02-20 01:59:01 -05:00
|
|
|
// Toggle abilities cycle state instead of executing
|
|
|
|
|
if (ability.isToggleable()) {
|
|
|
|
|
toggleAbility(key);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 03:47:12 -05:00
|
|
|
// Check cooldown (per-ability cooldowns are independent from global)
|
|
|
|
|
if (!ability.isIgnoreCooldown()) {
|
|
|
|
|
if (ability.isPerAbilityCooldown()) {
|
|
|
|
|
if (isOnPerAbilityCooldown(key)) return false;
|
|
|
|
|
} else {
|
|
|
|
|
if (isOnCooldown()) return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-14 21:06:12 -05:00
|
|
|
// Fire extender start hook (e.g., resource cost checks)
|
|
|
|
|
if (!AbilityController.Instance.fireOnAbilityStart(ability, player, null)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 01:03:05 -05:00
|
|
|
// Fire start event (cancelable)
|
|
|
|
|
if (firePlayerStartEvent(player, ability)) {
|
|
|
|
|
return false; // Cancelled
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// Start the ability
|
|
|
|
|
currentAbility = ability;
|
|
|
|
|
currentAbilityKey = key;
|
|
|
|
|
currentTarget = null; // Players don't have auto-targets
|
|
|
|
|
ability.start(null);
|
|
|
|
|
|
2026-02-03 15:52:46 -05:00
|
|
|
if (ability.getPhase() == AbilityPhase.ACTIVE) {
|
2026-02-17 21:03:54 -05:00
|
|
|
// Windup was 0 — capture locks for immediate active phase, then execute
|
2026-02-14 03:50:28 -05:00
|
|
|
if (ability.isRotationLockedDuringActive()) {
|
2026-02-17 21:03:54 -05:00
|
|
|
captureLockedRotation();
|
2026-02-14 03:50:28 -05:00
|
|
|
}
|
|
|
|
|
if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) {
|
2026-02-17 21:03:54 -05:00
|
|
|
captureLockedPosition();
|
2026-02-14 03:50:28 -05:00
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
executeImmediate(ability, currentTarget);
|
2026-02-03 15:52:46 -05:00
|
|
|
} else {
|
|
|
|
|
// Normal windup flow
|
2026-02-14 03:50:28 -05:00
|
|
|
if (ability.isRotationLockedDuringWindup()) {
|
2026-02-17 21:03:54 -05:00
|
|
|
captureLockedRotation();
|
2026-02-14 03:50:28 -05:00
|
|
|
}
|
|
|
|
|
if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) {
|
2026-02-17 21:03:54 -05:00
|
|
|
captureLockedPosition();
|
2026-02-14 03:50:28 -05:00
|
|
|
}
|
2026-02-17 21:03:54 -05:00
|
|
|
spawnTelegraph(ability, null);
|
|
|
|
|
playAbilitySound(ability.getWindUpSound());
|
2026-02-03 15:52:46 -05:00
|
|
|
playAbilityAnimation(ability.getWindUpAnimation());
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// CLIENT SYNC
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sync ability data to the client.
|
|
|
|
|
* Called when abilities change (unlock/lock/select) and on login.
|
|
|
|
|
*/
|
|
|
|
|
public void syncToClient() {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (player instanceof EntityPlayerMP) {
|
|
|
|
|
PlayerAbilitySyncPacket.sendToPlayer((EntityPlayerMP) player);
|
2026-02-20 03:47:12 -05:00
|
|
|
syncCooldownToClient();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Sync cooldown state (global + per-ability) to the client.
|
|
|
|
|
* Sent as a lightweight packet separate from full ability sync.
|
|
|
|
|
*/
|
|
|
|
|
public void syncCooldownToClient() {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (player instanceof EntityPlayerMP) {
|
|
|
|
|
AbilityCooldownSyncPacket.sendToPlayer((EntityPlayerMP) player);
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 23:08:12 -05:00
|
|
|
@Override
|
2026-02-18 22:19:18 -05:00
|
|
|
protected void onToggleStateChanged(String key, boolean active, int state) {
|
2026-02-17 23:08:12 -05:00
|
|
|
syncToClient();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 22:19:18 -05:00
|
|
|
@Override
|
|
|
|
|
protected boolean fireToggleEvent(Ability ability, int oldState, int newState) {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (ScriptController.Instance == null || player == null) return false;
|
|
|
|
|
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
if (handler == null) return false;
|
|
|
|
|
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
PlayerAbilityEvent.ToggleEvent event = new PlayerAbilityEvent.ToggleEvent(iPlayer, ability, oldState, newState);
|
|
|
|
|
return EventHooks.onPlayerAbilityToggle(handler, event);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
protected boolean fireToggleUpdateEvent(Ability ability, int tick, int state) {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (ScriptController.Instance == null || player == null) return true;
|
|
|
|
|
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
if (handler == null) return true;
|
|
|
|
|
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
PlayerAbilityEvent.ToggleUpdateEvent event = new PlayerAbilityEvent.ToggleUpdateEvent(iPlayer, ability, tick, state);
|
|
|
|
|
EventHooks.onPlayerAbilityToggleUpdate(handler, event);
|
|
|
|
|
return event.isEnabled();
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// SELECTION
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
public int getSelectedIndex() {
|
|
|
|
|
return selectedIndex;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setSelectedIndex(int index) {
|
2026-02-20 01:59:01 -05:00
|
|
|
if (index == -1 || (index >= 0 && index < unlockedAbilities.size())) {
|
2026-01-30 05:01:54 -05:00
|
|
|
this.selectedIndex = index;
|
|
|
|
|
syncToClient();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void selectNext() {
|
|
|
|
|
if (unlockedAbilities.isEmpty()) return;
|
|
|
|
|
selectedIndex = (selectedIndex + 1) % unlockedAbilities.size();
|
|
|
|
|
syncToClient();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void selectPrevious() {
|
|
|
|
|
if (unlockedAbilities.isEmpty()) return;
|
|
|
|
|
selectedIndex = (selectedIndex - 1 + unlockedAbilities.size()) % unlockedAbilities.size();
|
|
|
|
|
syncToClient();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public String getSelectedAbilityKey() {
|
|
|
|
|
if (unlockedAbilities.isEmpty() || selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return unlockedAbilities.get(selectedIndex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// UNLOCKED ABILITIES
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
public List<String> getUnlockedAbilityList() {
|
|
|
|
|
return unlockedAbilities;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public String[] getUnlockedAbilities() {
|
|
|
|
|
return unlockedAbilities.toArray(new String[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void setUnlockedAbilities(List<String> abilities) {
|
|
|
|
|
this.unlockedAbilities = abilities != null ? new ArrayList<>(abilities) : new ArrayList<>();
|
|
|
|
|
// Clamp selected index
|
|
|
|
|
if (selectedIndex >= unlockedAbilities.size()) {
|
|
|
|
|
selectedIndex = Math.max(0, unlockedAbilities.size() - 1);
|
|
|
|
|
}
|
|
|
|
|
syncToClient();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void unlockAbility(String key) {
|
|
|
|
|
if (key != null && !key.isEmpty() && !unlockedAbilities.contains(key)) {
|
|
|
|
|
unlockedAbilities.add(key);
|
|
|
|
|
syncToClient();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void lockAbility(String key) {
|
|
|
|
|
unlockedAbilities.remove(key);
|
|
|
|
|
if (selectedIndex >= unlockedAbilities.size()) {
|
|
|
|
|
selectedIndex = Math.max(0, unlockedAbilities.size() - 1);
|
|
|
|
|
}
|
|
|
|
|
syncToClient();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public boolean hasUnlockedAbility(String key) {
|
|
|
|
|
return unlockedAbilities.contains(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-02-17 21:03:54 -05:00
|
|
|
// COOLDOWNS (Player-specific overloads)
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
/**
|
|
|
|
|
* Check if on cooldown using player reference.
|
|
|
|
|
*/
|
2026-01-30 05:01:54 -05:00
|
|
|
public boolean isOnCooldown(EntityPlayer player) {
|
|
|
|
|
return player.worldObj.getTotalWorldTime() < cooldownEndTime;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if a specific ability key is on cooldown.
|
2026-02-20 03:47:12 -05:00
|
|
|
* Respects per-ability cooldown if the ability has it enabled.
|
2026-01-30 05:01:54 -05:00
|
|
|
*/
|
|
|
|
|
public boolean isOnCooldown(String key, EntityPlayer player) {
|
2026-02-20 03:47:12 -05:00
|
|
|
if (AbilityController.Instance != null) {
|
|
|
|
|
Ability ability = AbilityController.Instance.resolveAbility(key);
|
|
|
|
|
if (ability != null && ability.isPerAbilityCooldown()) {
|
|
|
|
|
return isOnPerAbilityCooldown(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
return isOnCooldown(player);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public long getRemainingCooldown(EntityPlayer player) {
|
|
|
|
|
long remaining = cooldownEndTime - player.worldObj.getTotalWorldTime();
|
|
|
|
|
return remaining > 0 ? remaining : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reset cooldown for a specific ability key.
|
2026-02-20 03:47:12 -05:00
|
|
|
* Resets per-ability cooldown if applicable, and global cooldown.
|
2026-01-30 05:01:54 -05:00
|
|
|
*/
|
|
|
|
|
public void resetCooldown(String key) {
|
2026-02-20 03:47:12 -05:00
|
|
|
resetPerAbilityCooldown(key);
|
2026-01-30 05:01:54 -05:00
|
|
|
cooldownEndTime = 0;
|
2026-02-20 03:47:12 -05:00
|
|
|
syncCooldownToClient();
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-20 03:47:12 -05:00
|
|
|
* Reset all cooldowns (global + all per-ability).
|
2026-01-30 05:01:54 -05:00
|
|
|
*/
|
|
|
|
|
public void resetAllCooldowns() {
|
|
|
|
|
cooldownEndTime = 0;
|
2026-02-20 03:47:12 -05:00
|
|
|
resetAllPerAbilityCooldowns();
|
|
|
|
|
syncCooldownToClient();
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// IPlayerAbilityData INTERFACE
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
@Override
|
|
|
|
|
public boolean activateAbility() {
|
|
|
|
|
return activateAbility(playerData.player);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public boolean activateAbility(String key) {
|
|
|
|
|
return activateAbility(playerData.player, key);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
public boolean isOnCooldown() {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (player == null) return false;
|
|
|
|
|
return isOnCooldown(player);
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
public boolean isOnCooldown(String key) {
|
|
|
|
|
EntityPlayer player = playerData.player;
|
|
|
|
|
if (player == null) return false;
|
|
|
|
|
return isOnCooldown(key, player);
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
@Override
|
|
|
|
|
public void resetCooldown() {
|
|
|
|
|
cooldownEndTime = 0;
|
2026-02-20 03:47:12 -05:00
|
|
|
resetAllPerAbilityCooldowns();
|
|
|
|
|
syncCooldownToClient();
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// INTERRUPTION (convenience overload)
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
public void interruptCurrentAbility() {
|
2026-02-16 01:03:05 -05:00
|
|
|
interruptCurrentAbility(null, 0);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 14:10:55 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-02-18 23:16:01 -05:00
|
|
|
// ENTITY RECONSTRUCTION RESET
|
2026-02-18 14:10:55 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-18 23:16:01 -05:00
|
|
|
* Handle entity reconstruction events (dimension change, death/respawn, etc.).
|
|
|
|
|
* Cleans up any executing ability state without firing script events (those were
|
|
|
|
|
* already fired on the original entity), clears transient runtime state, and
|
|
|
|
|
* re-syncs ability data to the client on the new entity.
|
|
|
|
|
* <p>
|
|
|
|
|
* This should be called whenever the player's entity object is recreated by Forge,
|
|
|
|
|
* since the client-side entity loses all synced ability state.
|
|
|
|
|
*
|
|
|
|
|
* @param clearCooldowns true to fully reset cooldowns (dimension change),
|
|
|
|
|
* false to preserve cooldowns (death/respawn — interrupt already rolled them)
|
|
|
|
|
* @param clearToggles true to clear active toggles (death/respawn),
|
|
|
|
|
* false to keep them (dimension change)
|
2026-02-18 14:10:55 -05:00
|
|
|
*/
|
2026-02-18 23:16:01 -05:00
|
|
|
public void onEntityReconstructed(boolean clearCooldowns, boolean clearToggles) {
|
|
|
|
|
// Clean up any executing ability silently (no script events — already fired on old entity)
|
2026-02-18 14:10:55 -05:00
|
|
|
if (currentAbility != null && currentAbility.isExecuting()) {
|
|
|
|
|
removeTelegraph(currentAbility);
|
|
|
|
|
currentAbility.cleanup();
|
|
|
|
|
currentAbility.interrupt();
|
|
|
|
|
stopAbilityAnimation();
|
|
|
|
|
releaseRotationControl();
|
|
|
|
|
releaseLockedPosition();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-19 12:55:37 -05:00
|
|
|
// Clear chain and concurrent state
|
2026-02-18 14:10:55 -05:00
|
|
|
currentChain = null;
|
|
|
|
|
chainEntryIndex = -1;
|
|
|
|
|
chainDelayRemaining = -1;
|
2026-02-19 12:55:37 -05:00
|
|
|
interruptConcurrentSlots();
|
2026-02-18 14:10:55 -05:00
|
|
|
|
|
|
|
|
// Clear all transient state
|
|
|
|
|
currentAbility = null;
|
|
|
|
|
currentAbilityKey = null;
|
|
|
|
|
currentTarget = null;
|
|
|
|
|
|
2026-02-18 23:16:01 -05:00
|
|
|
if (clearCooldowns) {
|
|
|
|
|
cooldownEndTime = 0;
|
2026-02-20 03:47:12 -05:00
|
|
|
resetAllPerAbilityCooldowns();
|
2026-02-18 23:16:01 -05:00
|
|
|
}
|
2026-02-18 14:10:55 -05:00
|
|
|
interruptCooldownRolled = false;
|
|
|
|
|
|
2026-02-18 23:16:01 -05:00
|
|
|
if (clearToggles) {
|
|
|
|
|
clearActiveToggles();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Force-send cleared lock state to client. We must always send this
|
|
|
|
|
// (not gated by lastSyncedFlags != 0) because the NEW entity's client
|
|
|
|
|
// may have stale lock state from before the reconstruction.
|
|
|
|
|
lastSyncedFlags = 0;
|
2026-02-18 14:10:55 -05:00
|
|
|
EntityPlayer player = playerData.player;
|
2026-02-18 23:16:01 -05:00
|
|
|
if (player instanceof EntityPlayerMP) {
|
|
|
|
|
PlayerAbilityStatePacket.sendToPlayer((EntityPlayerMP) player, (byte) 0, 0, 0);
|
2026-02-18 14:10:55 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-18 23:16:01 -05:00
|
|
|
// Re-sync full ability data (unlocked list, selection, toggles) to client
|
2026-02-18 14:10:55 -05:00
|
|
|
syncToClient();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-18 23:16:01 -05:00
|
|
|
/**
|
|
|
|
|
* Fully reset ability state when changing dimensions.
|
|
|
|
|
* Clears cooldowns so the player can immediately use abilities in the new dimension.
|
|
|
|
|
* Keeps active toggles (dimension change shouldn't reset ongoing effects).
|
|
|
|
|
*/
|
|
|
|
|
public void resetOnDimensionChange() {
|
|
|
|
|
onEntityReconstructed(true, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reset ability state after death/respawn.
|
|
|
|
|
* Preserves cooldowns (interrupt on death already rolled them) but clears
|
|
|
|
|
* active toggles (death should end ongoing effects).
|
|
|
|
|
*/
|
|
|
|
|
public void resetOnRespawn() {
|
|
|
|
|
onEntityReconstructed(false, true);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// DAMAGE HANDLING
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-01-30 05:01:54 -05:00
|
|
|
|
2026-02-12 18:53:40 -05:00
|
|
|
/**
|
|
|
|
|
* Handle damage taken while an ability is executing.
|
|
|
|
|
* Called from LivingHurtEvent BEFORE damage is applied to health.
|
|
|
|
|
* Triggers Guard counter detection and handles ability interruption.
|
|
|
|
|
*
|
|
|
|
|
* @param source The damage source
|
|
|
|
|
* @param amount The damage amount (post-armor)
|
|
|
|
|
* @return The modified damage amount (reduced by guard, or 0 if counter absorbs)
|
|
|
|
|
*/
|
|
|
|
|
public float onDamage(DamageSource source, float amount) {
|
|
|
|
|
if (currentAbility == null || !currentAbility.isExecuting()) {
|
|
|
|
|
return amount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
net.minecraft.entity.Entity sourceEntity = source.getEntity();
|
|
|
|
|
EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null;
|
|
|
|
|
|
|
|
|
|
// Track counter state before calling onDamageTaken
|
|
|
|
|
boolean wasCounterTriggered = false;
|
|
|
|
|
if (currentAbility instanceof AbilityGuard) {
|
|
|
|
|
wasCounterTriggered = ((AbilityGuard) currentAbility).isCounterTriggered();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
currentAbility.onDamageTaken(playerData.player, attacker, source, amount);
|
|
|
|
|
|
|
|
|
|
// Guard: handle damage reduction and counter absorption
|
|
|
|
|
if (currentAbility instanceof AbilityGuard) {
|
|
|
|
|
AbilityGuard guard = (AbilityGuard) currentAbility;
|
|
|
|
|
if (guard.isGuarding()) {
|
|
|
|
|
// Counter just triggered — absorb full damage
|
|
|
|
|
if (!wasCounterTriggered && guard.isCounterTriggered()) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
// Normal guard — apply flat damage reduction
|
|
|
|
|
float reduction = guard.getDamageReductionFactor();
|
|
|
|
|
return Math.max(0, amount - reduction);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check for ability interruption
|
|
|
|
|
if (currentAbility != null && currentAbility.canInterrupt(source)) {
|
2026-02-16 01:03:05 -05:00
|
|
|
interruptCurrentAbility(source, amount);
|
2026-02-12 18:53:40 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return amount;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// STATE QUERIES (Player-specific)
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-01-30 05:01:54 -05:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check if the player's movement should be locked due to an executing ability.
|
|
|
|
|
* Used by the tick handler to restrict player movement during ability phases.
|
|
|
|
|
*/
|
|
|
|
|
public boolean isMovementLocked() {
|
|
|
|
|
return currentAbility != null && currentAbility.isExecuting()
|
|
|
|
|
&& currentAbility.isMovementLockedForCurrentPhase();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 03:50:28 -05:00
|
|
|
/**
|
2026-02-17 21:03:54 -05:00
|
|
|
* Returns true if an ability-driven animation is currently playing.
|
|
|
|
|
* Used by PlayerData.onLogin() to detect orphaned ability animations.
|
2026-02-14 03:50:28 -05:00
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
public boolean isPlayingAbilityAnimation() {
|
|
|
|
|
return playingAbilityAnimation;
|
2026-02-14 03:50:28 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-17 21:03:54 -05:00
|
|
|
* Clears an orphaned ability animation (e.g. after relog when the ability is gone).
|
|
|
|
|
* Stops the animation and resets the tracking flag.
|
2026-02-14 03:50:28 -05:00
|
|
|
*/
|
2026-02-17 21:03:54 -05:00
|
|
|
public void clearOrphanedAbilityAnimation() {
|
|
|
|
|
stopAbilityAnimation();
|
2026-02-14 03:50:28 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 21:03:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// ROTATION CONTROL (Player-specific application)
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
2026-02-14 03:50:28 -05:00
|
|
|
/**
|
|
|
|
|
* Apply rotation control on the server side.
|
|
|
|
|
* Enforces locked rotation values on the player entity.
|
|
|
|
|
*/
|
|
|
|
|
private void applyRotationControl(EntityPlayer player) {
|
|
|
|
|
if (!rotationLocked || currentAbility == null) return;
|
|
|
|
|
|
|
|
|
|
if (currentAbility.isRotationLockedForCurrentPhase()) {
|
|
|
|
|
player.rotationYaw = lockedYaw;
|
|
|
|
|
player.rotationPitch = lockedPitch;
|
|
|
|
|
player.prevRotationYaw = lockedYaw;
|
|
|
|
|
player.prevRotationPitch = lockedPitch;
|
|
|
|
|
if (player instanceof EntityPlayerMP) {
|
|
|
|
|
player.rotationYawHead = lockedYaw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-02-17 21:03:54 -05:00
|
|
|
// POSITION LOCKING (Player-specific application)
|
2026-02-14 03:50:28 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Apply position lock on the server side.
|
|
|
|
|
* Snaps player back to locked position and zeroes motion.
|
|
|
|
|
*/
|
|
|
|
|
private void applyPositionLock(EntityPlayer player) {
|
|
|
|
|
if (!positionLocked) return;
|
|
|
|
|
|
2026-02-19 03:12:59 -05:00
|
|
|
player.setPosition(lockedPosX, lockedPosY, lockedPosZ);
|
|
|
|
|
player.prevPosX = lockedPosX;
|
|
|
|
|
player.prevPosY = lockedPosY;
|
|
|
|
|
player.prevPosZ = lockedPosZ;
|
2026-02-14 03:50:28 -05:00
|
|
|
player.motionX = 0;
|
2026-02-19 03:12:59 -05:00
|
|
|
player.motionY = 0;
|
2026-02-14 03:50:28 -05:00
|
|
|
player.motionZ = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// ABILITY STATE SYNC
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Build the current ability state flags byte.
|
|
|
|
|
*/
|
|
|
|
|
private byte getAbilityStateFlags() {
|
|
|
|
|
byte flags = 0;
|
|
|
|
|
if (currentAbility == null || !currentAbility.isExecuting()) return flags;
|
|
|
|
|
|
|
|
|
|
if (currentAbility.isMovementLockedForCurrentPhase()) {
|
|
|
|
|
flags |= PlayerAbilityStatePacket.FLAG_MOVEMENT_LOCKED;
|
|
|
|
|
}
|
|
|
|
|
if (rotationLocked && currentAbility.isRotationLockedForCurrentPhase()) {
|
|
|
|
|
flags |= PlayerAbilityStatePacket.FLAG_ROTATION_LOCKED;
|
|
|
|
|
}
|
|
|
|
|
if (currentAbility.hasAbilityMovement() && currentAbility.getPhase() == AbilityPhase.ACTIVE) {
|
|
|
|
|
flags |= PlayerAbilityStatePacket.FLAG_HAS_ABILITY_MOVEMENT;
|
|
|
|
|
}
|
|
|
|
|
if (positionLocked) {
|
|
|
|
|
flags |= PlayerAbilityStatePacket.FLAG_POSITION_LOCKED;
|
|
|
|
|
}
|
2026-02-19 03:12:59 -05:00
|
|
|
if (wasFlyingAtLock) {
|
|
|
|
|
flags |= PlayerAbilityStatePacket.FLAG_WAS_FLYING_AT_LOCK;
|
|
|
|
|
}
|
2026-02-14 03:50:28 -05:00
|
|
|
return flags;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send ability state to client if flags changed since last sync.
|
|
|
|
|
*/
|
|
|
|
|
private void syncAbilityStateIfNeeded(EntityPlayer player) {
|
|
|
|
|
if (!(player instanceof EntityPlayerMP)) return;
|
|
|
|
|
byte flags = getAbilityStateFlags();
|
|
|
|
|
if (flags != lastSyncedFlags) {
|
|
|
|
|
lastSyncedFlags = flags;
|
|
|
|
|
PlayerAbilityStatePacket.sendToPlayer((EntityPlayerMP) player, flags, lockedYaw, lockedPitch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Send cleared (all-zero) state to client. Called on ability completion/interrupt.
|
|
|
|
|
*/
|
|
|
|
|
private void syncAbilityStateClear(EntityPlayer player) {
|
|
|
|
|
if (!(player instanceof EntityPlayerMP)) return;
|
|
|
|
|
if (lastSyncedFlags != 0) {
|
|
|
|
|
lastSyncedFlags = 0;
|
|
|
|
|
PlayerAbilityStatePacket.sendToPlayer((EntityPlayerMP) player, (byte) 0, 0, 0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
2026-02-17 21:03:54 -05:00
|
|
|
// SCRIPT EVENTS (Player-specific: start event only, others via abstract)
|
2026-02-16 01:03:05 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Fire the ability start event. Returns true if cancelled.
|
|
|
|
|
*/
|
|
|
|
|
private boolean firePlayerStartEvent(EntityPlayer player, Ability ability) {
|
|
|
|
|
if (ScriptController.Instance == null) return false;
|
|
|
|
|
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
|
|
|
|
if (handler == null) return false;
|
|
|
|
|
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
|
|
|
|
PlayerAbilityEvent.StartEvent event = new PlayerAbilityEvent.StartEvent(iPlayer, ability, null);
|
|
|
|
|
return EventHooks.onPlayerAbilityStart(handler, event);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
// NBT SERIALIZATION
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
public void writeToNBT(NBTTagCompound compound) {
|
|
|
|
|
NBTTagList list = new NBTTagList();
|
|
|
|
|
for (String key : unlockedAbilities) {
|
|
|
|
|
list.appendTag(new NBTTagString(key));
|
|
|
|
|
}
|
|
|
|
|
compound.setTag("PlayerAbilities", list);
|
|
|
|
|
compound.setInteger("PlayerAbilitySelected", selectedIndex);
|
2026-02-15 22:03:32 -05:00
|
|
|
compound.setBoolean("AbilityAnimating", playingAbilityAnimation);
|
2026-02-17 23:08:12 -05:00
|
|
|
|
2026-02-18 22:19:18 -05:00
|
|
|
// Active toggles (compound format with state)
|
2026-02-17 23:08:12 -05:00
|
|
|
NBTTagList toggleList = new NBTTagList();
|
2026-02-18 22:19:18 -05:00
|
|
|
for (Map.Entry<String, ToggleEntry> entry : activeToggles.entrySet()) {
|
|
|
|
|
NBTTagCompound toggleNbt = new NBTTagCompound();
|
|
|
|
|
toggleNbt.setString("Key", entry.getKey());
|
|
|
|
|
toggleNbt.setInteger("State", entry.getValue().getState());
|
|
|
|
|
toggleList.appendTag(toggleNbt);
|
2026-02-17 23:08:12 -05:00
|
|
|
}
|
|
|
|
|
compound.setTag("ActiveToggles", toggleList);
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void readFromNBT(NBTTagCompound compound) {
|
|
|
|
|
unlockedAbilities.clear();
|
|
|
|
|
if (compound.hasKey("PlayerAbilities")) {
|
|
|
|
|
NBTTagList list = compound.getTagList("PlayerAbilities", 8); // 8 = string tag
|
|
|
|
|
for (int i = 0; i < list.tagCount(); i++) {
|
|
|
|
|
String key = list.getStringTagAt(i);
|
|
|
|
|
if (key != null && !key.isEmpty()) {
|
|
|
|
|
unlockedAbilities.add(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
selectedIndex = compound.getInteger("PlayerAbilitySelected");
|
2026-02-16 01:39:14 -05:00
|
|
|
|
|
|
|
|
// Prune abilities that no longer exist (deleted custom abilities or removed built-ins)
|
|
|
|
|
validateUnlockedAbilities();
|
|
|
|
|
|
2026-01-30 05:01:54 -05:00
|
|
|
if (selectedIndex >= unlockedAbilities.size()) {
|
|
|
|
|
selectedIndex = Math.max(0, unlockedAbilities.size() - 1);
|
|
|
|
|
}
|
2026-02-15 22:03:32 -05:00
|
|
|
playingAbilityAnimation = compound.getBoolean("AbilityAnimating");
|
2026-02-17 23:08:12 -05:00
|
|
|
|
|
|
|
|
// Active toggles - restore state directly (no onToggleOn callback during load)
|
|
|
|
|
activeToggles.clear();
|
|
|
|
|
if (compound.hasKey("ActiveToggles")) {
|
2026-02-18 22:19:18 -05:00
|
|
|
NBTTagList toggleNbt = compound.getTagList("ActiveToggles", 10); // 10 = TAG_COMPOUND
|
2026-02-17 23:08:12 -05:00
|
|
|
for (int i = 0; i < toggleNbt.tagCount(); i++) {
|
2026-02-18 22:19:18 -05:00
|
|
|
NBTTagCompound entry = toggleNbt.getCompoundTagAt(i);
|
|
|
|
|
String key = entry.getString("Key");
|
|
|
|
|
int state = entry.hasKey("State") ? entry.getInteger("State") : 1;
|
|
|
|
|
setToggleEntryDirect(key, state);
|
2026-02-17 23:08:12 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|
2026-02-16 01:39:14 -05:00
|
|
|
|
|
|
|
|
/**
|
2026-02-20 01:59:01 -05:00
|
|
|
* Remove any unlocked ability keys that can no longer be resolved or
|
|
|
|
|
* are no longer allowed for players (e.g. NPC-only abilities).
|
|
|
|
|
* Called during load to clean up references to deleted/restricted abilities.
|
2026-02-17 23:22:03 -05:00
|
|
|
* Supports both regular ability keys and "chain:" prefixed chained ability keys.
|
2026-02-16 01:39:14 -05:00
|
|
|
*/
|
|
|
|
|
private void validateUnlockedAbilities() {
|
|
|
|
|
if (AbilityController.Instance == null) return;
|
|
|
|
|
|
|
|
|
|
Iterator<String> it = unlockedAbilities.iterator();
|
|
|
|
|
while (it.hasNext()) {
|
|
|
|
|
String key = it.next();
|
2026-02-17 23:22:03 -05:00
|
|
|
IAbilityAction resolved = resolveActionKey(key);
|
|
|
|
|
if (resolved == null) {
|
2026-02-16 01:39:14 -05:00
|
|
|
it.remove();
|
|
|
|
|
LogWriter.info("Removed invalid ability reference from player data: " + key);
|
2026-02-20 01:59:01 -05:00
|
|
|
} else if (!resolved.getAllowedBy().allowsPlayer()) {
|
|
|
|
|
it.remove();
|
|
|
|
|
LogWriter.info("Removed non-player ability from player data: " + key);
|
2026-02-16 01:39:14 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-30 05:01:54 -05:00
|
|
|
}
|