feat: implement player ability state management with rotation and position locking

This commit is contained in:
Kam committed 2026-02-14 03:50:28 -05:00
1 parent ab1f0d7f92
commit 0b0dcdc58d
8 files changed
+427 -21

No files matched your search

@@ -86,10 +86,20 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
@Override
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
if (isPlayerCaster(caster)) {
// Player: slam will launch from current position, no target tracking needed
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
// Player: update target along look direction during windup
if (targetingMode == TargetingMode.AOE_SELF) {
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
} else {
float yawRad = (float) Math.toRadians(caster.rotationYaw);
double dirX = -Math.sin(yawRad);
double dirZ = Math.cos(yawRad);
double launchDist = Math.max(4.0, maxRange * 0.5);
targetX = caster.posX + dirX * launchDist;
targetY = caster.posY;
targetZ = caster.posZ + dirZ * launchDist;
}
} else {
// NPC: update target position during windup for telegraph tracking
if (targetingMode == TargetingMode.AOE_SELF) {
@@ -139,18 +149,22 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
}
/**
* Player slam: Launch straight up. Player controls horizontal movement via WASD.
* Player slam: Launch in look direction using same ballistic arc as NPC slam.
* AOE damage triggers wherever the player lands.
*/
private void executePlayerSlam(EntityLivingBase caster) {
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
// Calculate target position along player's look direction (horizontal only)
float yawRad = (float) Math.toRadians(caster.rotationYaw);
double dirX = -Math.sin(yawRad);
double dirZ = Math.cos(yawRad);
double vy = calculateLaunchVelocity(Math.max(1.0, leapHeight));
caster.motionY = vy;
hasLaunched = true;
caster.velocityChanged = true;
double launchDist = Math.max(4.0, maxRange * 0.5);
targetX = caster.posX + dirX * launchDist;
targetY = caster.posY;
targetZ = caster.posZ + dirZ * launchDist;
launchTowardTarget(caster);
}
/**
@@ -297,14 +311,18 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
return;
}
// NPC: face toward target destination while in air
// Player: free look — player controls their own camera
if (!isPreview() && !isPlayerCaster(caster)) {
// Face toward target destination while in air (both NPC and Player)
if (!isPreview()) {
double dx = targetX - caster.posX;
double dz = targetZ - caster.posZ;
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
caster.rotationYaw = targetYaw;
caster.rotationYawHead = targetYaw;
if (dx * dx + dz * dz > 0.25) {
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
caster.rotationYaw = targetYaw;
caster.rotationYawHead = targetYaw;
if (isPlayerCaster(caster)) {
caster.velocityChanged = true;
}
}
}
}
@@ -26,6 +26,7 @@ import kamkeel.npcs.network.packets.data.SoundManagementPacket;
import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket;
import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket;
import kamkeel.npcs.network.packets.data.VillagerListPacket;
import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket;
import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket;
import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket;
@@ -588,6 +589,7 @@ public class PacketHandler {
DATA_PACKET.registerPacket(new TelegraphSpawnPacket());
DATA_PACKET.registerPacket(new TelegraphRemovePacket());
DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket());
DATA_PACKET.registerPacket(new PlayerAbilityStatePacket());
}
public void registerPlayerPackets() {
@@ -63,6 +63,7 @@ public enum EnumDataPacket {
TELEGRAPH_SPAWN,
TELEGRAPH_REMOVE,
PLAYER_ABILITY_SYNC,
PLAYER_ABILITY_STATE,
// Auction System
AUCTION_DATA,
@@ -0,0 +1,84 @@
package kamkeel.npcs.network.packets.data.ability;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumDataPacket;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import noppes.npcs.client.ClientAbilityState;
import java.io.IOException;
/**
* Lightweight packet syncing ability lock state from server to client.
* Sent every tick during ability execution when flags change, and once when ability ends.
*
* Flags byte layout:
* bit 0: movement locked (suppress WASD, zero motion)
* bit 1: rotation locked (freeze yaw/pitch)
* bit 2: has ability movement (ability controls motion — suppress WASD but don't zero)
* bit 3: position locked (freeze position)
*/
public final class PlayerAbilityStatePacket extends AbstractPacket {
public static final String packetName = "Data|PlayerAbilityState";
public static final byte FLAG_MOVEMENT_LOCKED = 1;
public static final byte FLAG_ROTATION_LOCKED = 2;
public static final byte FLAG_HAS_ABILITY_MOVEMENT = 4;
public static final byte FLAG_POSITION_LOCKED = 8;
private byte flags;
private float lockedYaw;
private float lockedPitch;
public PlayerAbilityStatePacket() {
}
public PlayerAbilityStatePacket(byte flags, float lockedYaw, float lockedPitch) {
this.flags = flags;
this.lockedYaw = lockedYaw;
this.lockedPitch = lockedPitch;
}
@Override
public Enum getType() {
return EnumDataPacket.PLAYER_ABILITY_STATE;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.DATA_PACKET;
}
@Override
public void sendData(ByteBuf out) throws IOException {
out.writeByte(flags);
if ((flags & FLAG_ROTATION_LOCKED) != 0) {
out.writeFloat(lockedYaw);
out.writeFloat(lockedPitch);
}
}
@SideOnly(Side.CLIENT)
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
byte flags = in.readByte();
float yaw = 0, pitch = 0;
if ((flags & FLAG_ROTATION_LOCKED) != 0) {
yaw = in.readFloat();
pitch = in.readFloat();
}
ClientAbilityState.update(flags, yaw, pitch);
}
/**
* Send the current ability state to the player's client.
*/
public static void sendToPlayer(EntityPlayerMP player, byte flags, float lockedYaw, float lockedPitch) {
PacketHandler.Instance.sendToPlayer(new PlayerAbilityStatePacket(flags, lockedYaw, lockedPitch), player);
}
}
@@ -8,6 +8,7 @@ import cpw.mods.fml.relauncher.Side;
import kamkeel.npcs.addon.DBCAddon;
import kamkeel.npcs.controllers.AttributeController;
import kamkeel.npcs.controllers.SyncController;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.util.AttributeAttackUtil;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
@@ -100,9 +101,14 @@ public class ScriptPlayerEventHandler {
playerData.abilityData.tick(player);
// Lock player movement during ability phases that require it
// Skip zeroing if the ability has its own movement (Charge/Dash/Slam)
if (playerData.abilityData.isMovementLocked()) {
player.motionX = 0;
player.motionZ = 0;
Ability current = playerData.abilityData.getCurrentAbility();
if (current == null || !current.hasAbilityMovement()) {
player.motionX = 0;
player.motionZ = 0;
player.velocityChanged = true;
}
}
if (playerData.updateClient) {
@@ -0,0 +1,59 @@
package noppes.npcs.client;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket;
/**
* Client-side state holder for player ability lock flags.
* Updated by {@link PlayerAbilityStatePacket} from the server.
* Read by {@link ClientTickHandler} to suppress WASD input and enforce rotation.
*/
@SideOnly(Side.CLIENT)
public class ClientAbilityState {
public static boolean movementLocked = false;
public static boolean rotationLocked = false;
public static boolean hasAbilityMovement = false;
public static boolean positionLocked = false;
public static float lockedYaw = 0;
public static float lockedPitch = 0;
/**
* Update state from a received packet.
*/
public static void update(byte flags, float yaw, float pitch) {
movementLocked = (flags & PlayerAbilityStatePacket.FLAG_MOVEMENT_LOCKED) != 0;
rotationLocked = (flags & PlayerAbilityStatePacket.FLAG_ROTATION_LOCKED) != 0;
hasAbilityMovement = (flags & PlayerAbilityStatePacket.FLAG_HAS_ABILITY_MOVEMENT) != 0;
positionLocked = (flags & PlayerAbilityStatePacket.FLAG_POSITION_LOCKED) != 0;
lockedYaw = yaw;
lockedPitch = pitch;
}
/**
* Whether player WASD movement input should be suppressed.
* True when movement/position is locked or ability has its own movement.
*/
public static boolean shouldSuppressMovementInput() {
return movementLocked || hasAbilityMovement || positionLocked;
}
/**
* Whether player camera rotation should be locked.
*/
public static boolean shouldLockRotation() {
return rotationLocked;
}
/**
* Reset all state. Called on world change / disconnect.
*/
public static void reset() {
movementLocked = false;
rotationLocked = false;
hasAbilityMovement = false;
positionLocked = false;
lockedYaw = 0;
lockedPitch = 0;
}
}
@@ -56,6 +56,7 @@ public class ClientTickHandler {
if ((this.prevWorld == null || mc.theWorld == null) && this.prevWorld != mc.theWorld) {
if (mc.theWorld == null) {
ClientCacheHandler.clearCache();
ClientAbilityState.reset();
}
this.prevWorld = mc.theWorld;
}
@@ -72,6 +73,19 @@ public class ClientTickHandler {
lastSpecialKeyDown = specialKeyDown;
}
// Suppress player input during ability-controlled phases
if (ClientAbilityState.shouldSuppressMovementInput()) {
mc.thePlayer.movementInput.moveForward = 0;
mc.thePlayer.movementInput.moveStrafe = 0;
mc.thePlayer.movementInput.jump = false;
}
if (ClientAbilityState.shouldLockRotation()) {
mc.thePlayer.rotationYaw = ClientAbilityState.lockedYaw;
mc.thePlayer.rotationPitch = ClientAbilityState.lockedPitch;
mc.thePlayer.prevRotationYaw = ClientAbilityState.lockedYaw;
mc.thePlayer.prevRotationPitch = ClientAbilityState.lockedPitch;
}
if (player.ridingEntity instanceof EntityNPCInterface) {
EntityNPCInterface mount = (EntityNPCInterface) player.ridingEntity;
if (mount.advanced.role == EnumRoleType.Mount && mount.roleInterface instanceof RoleMount) {
@@ -19,6 +19,7 @@ import noppes.npcs.api.ability.IPlayerAbilityData;
import noppes.npcs.controllers.AnimationController;
import noppes.npcs.controllers.data.Animation;
import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket;
import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket;
import java.util.ArrayList;
@@ -77,6 +78,26 @@ public class PlayerAbilityData implements IPlayerAbilityData {
*/
private transient EntityLivingBase currentTarget;
/**
* Rotation lock state - freezes player yaw/pitch during ability phases.
*/
private transient boolean rotationLocked = false;
private transient float lockedYaw = 0;
private transient float lockedPitch = 0;
/**
* Position lock state - freezes player position during ability phases.
*/
private transient boolean positionLocked = false;
private transient double lockedPosX = 0;
private transient double lockedPosY = 0;
private transient double lockedPosZ = 0;
/**
* Last synced state flags byte for change detection (avoids spamming packets).
*/
private transient byte lastSyncedFlags = 0;
// ═══════════════════════════════════════════════════════════════════
// CONSTRUCTOR
// ═══════════════════════════════════════════════════════════════════
@@ -100,6 +121,20 @@ public class PlayerAbilityData implements IPlayerAbilityData {
if (currentAbility != null && currentAbility.isExecuting()) {
tickCurrentAbility(player);
// Apply rotation and position locks after ability tick
applyRotationControl(player);
applyPositionLock(player);
// 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);
}
}
}
@@ -114,17 +149,47 @@ public class PlayerAbilityData implements IPlayerAbilityData {
switch (currentAbility.getPhase()) {
case WINDUP:
if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) {
// Burst replay: re-enter windup - set up locks
if (currentAbility.isRotationLockedDuringWindup()) {
captureLockedRotation(player);
}
if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) {
captureLockedPosition(player);
}
spawnTelegraph(currentAbility, player, null);
playAbilitySound(player, currentAbility.getWindUpSound());
playAbilityAnimation(currentAbility.getWindUpAnimation());
}
currentAbility.onWindUpTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick());
break;
case ACTIVE:
if (phaseChanged && oldPhase == AbilityPhase.WINDUP) {
if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) {
// Just entered ACTIVE phase - lock all telegraph positions
for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) {
telegraph.lockPosition();
}
removeTelegraph(currentAbility, player);
// Handle rotation control transition from WINDUP to ACTIVE
if (currentAbility.isRotationLockedDuringActive()) {
if (!rotationLocked) {
captureLockedRotation(player);
}
} else if (rotationLocked) {
releaseRotationControl();
}
// Handle position lock transition from WINDUP to ACTIVE
if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) {
if (!positionLocked) {
captureLockedPosition(player);
}
} else if (positionLocked) {
releaseLockedPosition();
}
// Play active sound and animation
playAbilitySound(player, currentAbility.getActiveSound());
playAbilityAnimation(currentAbility.getActiveAnimation());
@@ -143,6 +208,18 @@ public class PlayerAbilityData implements IPlayerAbilityData {
handleAbilityCompletion(player);
return;
}
// Release locks during burst delay
if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) {
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
}
break;
case BURST_DELAY:
// Free movement and rotation during burst delay
if (rotationLocked) releaseRotationControl();
if (positionLocked) releaseLockedPosition();
break;
case DAZED:
@@ -163,6 +240,10 @@ public class PlayerAbilityData implements IPlayerAbilityData {
currentAbility.onComplete(player, currentTarget);
// Release all locks
releaseRotationControl();
releaseLockedPosition();
// Apply universal cooldown (using base cooldownTicks only, not min/max random)
if (!currentAbility.isIgnoreCooldown()) {
cooldownEndTime = player.worldObj.getTotalWorldTime() + currentAbility.getCooldownTicks();
@@ -174,6 +255,9 @@ public class PlayerAbilityData implements IPlayerAbilityData {
currentAbility = null;
currentAbilityKey = null;
currentTarget = null;
// Send cleared state to client
syncAbilityStateClear(player);
}
// ═══════════════════════════════════════════════════════════════════
@@ -231,9 +315,21 @@ public class PlayerAbilityData implements IPlayerAbilityData {
if (ability.getPhase() == AbilityPhase.ACTIVE) {
// Windup was 0 — skip telegraph/windup and go straight to active
if (ability.isRotationLockedDuringActive()) {
captureLockedRotation(player);
}
if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) {
captureLockedPosition(player);
}
executeImmediate(ability, player);
} else {
// Normal windup flow
if (ability.isRotationLockedDuringWindup()) {
captureLockedRotation(player);
}
if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) {
captureLockedPosition(player);
}
spawnTelegraph(ability, player, null);
playAbilitySound(player, ability.getWindUpSound());
playAbilityAnimation(ability.getWindUpAnimation());
@@ -412,9 +508,12 @@ public class PlayerAbilityData implements IPlayerAbilityData {
if (currentAbility != null && currentAbility.isExecuting()) {
currentAbility.interrupt();
stopAbilityAnimation();
releaseRotationControl();
releaseLockedPosition();
currentAbility = null;
currentAbilityKey = null;
currentTarget = null;
syncAbilityStateClear(playerData.player);
}
}
@@ -493,6 +592,129 @@ public class PlayerAbilityData implements IPlayerAbilityData {
&& currentAbility.isMovementLockedForCurrentPhase();
}
// ═══════════════════════════════════════════════════════════════════
// ROTATION CONTROL
// ═══════════════════════════════════════════════════════════════════
/**
* Capture current rotation values to lock player's look direction.
*/
private void captureLockedRotation(EntityPlayer player) {
lockedYaw = player.rotationYaw;
lockedPitch = player.rotationPitch;
rotationLocked = true;
}
/**
* Release rotation lock.
*/
private void releaseRotationControl() {
rotationLocked = false;
}
/**
* 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;
}
}
}
// ═══════════════════════════════════════════════════════════════════
// POSITION LOCKING
// ═══════════════════════════════════════════════════════════════════
/**
* Capture current position to lock player in place.
*/
private void captureLockedPosition(EntityPlayer player) {
lockedPosX = player.posX;
lockedPosY = player.posY;
lockedPosZ = player.posZ;
positionLocked = true;
}
/**
* Release position lock.
*/
private void releaseLockedPosition() {
positionLocked = false;
}
/**
* Apply position lock on the server side.
* Snaps player back to locked position and zeroes motion.
*/
private void applyPositionLock(EntityPlayer player) {
if (!positionLocked) return;
player.setPosition(lockedPosX, lockedPosY, lockedPosZ);
player.prevPosX = lockedPosX;
player.prevPosY = lockedPosY;
player.prevPosZ = lockedPosZ;
player.motionX = 0;
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;
}
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);
}
}
// ═══════════════════════════════════════════════════════════════════
// TELEGRAPH
// ═══════════════════════════════════════════════════════════════════