diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index f1281d7be..fc9a6b5be 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -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; + } + } } } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 6db07c841..dee4a3a94 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -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() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index 18321d0a4..8362cb3c5 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -63,6 +63,7 @@ public enum EnumDataPacket { TELEGRAPH_SPAWN, TELEGRAPH_REMOVE, PLAYER_ABILITY_SYNC, + PLAYER_ABILITY_STATE, // Auction System AUCTION_DATA, diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java new file mode 100644 index 000000000..03c0b82b4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilityStatePacket.java @@ -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); + } +} diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index c77083fa1..909634a09 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -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) { diff --git a/src/main/java/noppes/npcs/client/ClientAbilityState.java b/src/main/java/noppes/npcs/client/ClientAbilityState.java new file mode 100644 index 000000000..8092a69a3 --- /dev/null +++ b/src/main/java/noppes/npcs/client/ClientAbilityState.java @@ -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; + } +} diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 203cef3e2..b4e8738c8 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -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) { diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 3e6489b1c..7807074f3 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -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 // ═══════════════════════════════════════════════════════════════════