diff --git a/src/api b/src/api index 658da549e..2438b2110 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 658da549eec622ed7bcfb331f655938e8841520a +Subproject commit 2438b2110e74bfd49125cf74254d98c8b18e2331 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 4a8558218..8c8f16efe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -465,6 +465,11 @@ public abstract class Ability implements IAbility, IAbilityAction { } } + // Save velocity before damage to cancel vanilla knockback when ability knockback is zero + double prevMotionX = hitEntity.motionX; + double prevMotionY = hitEntity.motionY; + double prevMotionZ = hitEntity.motionZ; + // Apply damage if (damage > 0) { // Check for ability extenders (e.g., DBC Addon damage routing) @@ -483,9 +488,14 @@ public abstract class Ability implements IAbility, IAbilityAction { } } - // Apply knockback if any + // Apply knockback if any, otherwise restore velocity to cancel vanilla knockback if (knockback > 0 || knockbackUp > 0) { applyKnockback(hitEntity, knockbackDirX, knockbackDirZ, knockback, knockbackUp); + } else { + hitEntity.motionX = prevMotionX; + hitEntity.motionY = prevMotionY; + hitEntity.motionZ = prevMotionZ; + hitEntity.velocityChanged = true; } return true; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java index 07810bfe3..266a0d6a4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java @@ -258,10 +258,12 @@ public abstract class AbilityMovement extends Ability { /** * Apply movement velocity with zero vertical motion. * Used for ground-locked movement like Charge. + * motionY is set BEFORE sending velocity packet to prevent client-server desync + * (which causes "moved too fast" kicks when used in mid-air). */ protected void applyVelocityFlat(EntityLivingBase caster, float speed) { - applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed); caster.motionY = 0; + applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed); } /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index b04dd2766..b4173bb1a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -27,7 +27,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { private float pushRadius = 8.0f; private float pushStrength = 1.5f; private float damage = 8.0f; - private int maxTargets = 10; + private boolean aoe = true; private int activeDisplayTicks = 10; public AbilityShockwave() { @@ -70,47 +70,79 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { @SuppressWarnings("unchecked") List entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box); - int count = 0; - for (EntityLivingBase entity : entities) { - if (entity == caster) continue; - if (entity.isDead) continue; - if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue; + if (aoe) { + // ALL: push every valid enemy in range + for (EntityLivingBase entity : entities) { + if (entity == caster) continue; + if (entity.isDead) continue; + if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue; - double dist = caster.getDistanceToEntity(entity); - if (dist > pushRadius) continue; + double dist = caster.getDistanceToEntity(entity); + if (dist > pushRadius) continue; - count++; - if (count > maxTargets) break; - - // Calculate push direction (away from caster) - double dx = entity.posX - caster.posX; - double dz = entity.posZ - caster.posZ; - double len = Math.sqrt(dx * dx + dz * dz); - - if (len > 0) { - dx /= len; - dz /= len; - } else { - // Entity is directly on top of caster, push in random direction - double angle = Math.random() * Math.PI * 2; - dx = Math.cos(angle); - dz = Math.sin(angle); + applyShockwavePush(caster, entity, dist); + } + } else { + // SINGULAR: push one target + // NPC: push aggro target if valid, otherwise nearest enemy + // Player: push nearest enemy in range + if (!isPlayerCaster(caster) && target != null && !target.isDead) { + double dist = caster.getDistanceToEntity(target); + if (dist <= pushRadius && AbilityTargetHelper.shouldAffect(caster, target, TargetFilter.ENEMIES, false)) { + applyShockwavePush(caster, target, dist); + return; + } } - // Scale knockback by distance (closer = stronger) - float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; - float finalPush = pushStrength * distFactor; - // Apply damage with custom knockback direction - boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz); + // Find nearest valid enemy + EntityLivingBase nearest = null; + double nearestDist = Double.MAX_VALUE; + for (EntityLivingBase entity : entities) { + if (entity == caster) continue; + if (entity.isDead) continue; + if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue; - // Apply effects if hit connected - if (wasHit) { - applyEffects(entity); + double dist = caster.getDistanceToEntity(entity); + if (dist <= pushRadius && dist < nearestDist) { + nearest = entity; + nearestDist = dist; + } + } + if (nearest != null) { + applyShockwavePush(caster, nearest, nearestDist); } } } } + private void applyShockwavePush(EntityLivingBase caster, EntityLivingBase entity, double dist) { + // Calculate push direction (away from caster) + double dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; + double len = Math.sqrt(dx * dx + dz * dz); + + if (len > 0) { + dx /= len; + dz /= len; + } else { + // Entity is directly on top of caster, push in random direction + double angle = Math.random() * Math.PI * 2; + dx = Math.cos(angle); + dz = Math.sin(angle); + } + + // Scale knockback by distance (closer = stronger) + float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; + float finalPush = pushStrength * distFactor; + // Apply damage with custom knockback direction + boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz); + + // Apply effects if hit connected + if (wasHit) { + applyEffects(entity); + } + } + @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { if (tick >= activeDisplayTicks) @@ -122,7 +154,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { nbt.setFloat("pushRadius", pushRadius); nbt.setFloat("pushStrength", pushStrength); nbt.setFloat("damage", damage); - nbt.setInteger("maxTargets", maxTargets); + nbt.setBoolean("aoe", aoe); nbt.setInteger("activeDisplayTicks", activeDisplayTicks); } @@ -131,7 +163,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { this.pushRadius = nbt.getFloat("pushRadius"); this.pushStrength = nbt.getFloat("pushStrength"); this.damage = nbt.getFloat("damage"); - this.maxTargets = nbt.getInteger("maxTargets"); + this.aoe = !nbt.hasKey("aoe") || nbt.getBoolean("aoe"); this.activeDisplayTicks = nbt.hasKey("activeDisplayTicks") ? nbt.getInteger("activeDisplayTicks") : 10; } @@ -160,12 +192,12 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { this.damage = damage; } - public int getMaxTargets() { - return maxTargets; + public boolean isAoe() { + return aoe; } - public void setMaxTargets(int maxTargets) { - this.maxTargets = maxTargets; + public void setAoe(boolean aoe) { + this.aoe = aoe; } public int getActiveDisplayTicks() { @@ -186,7 +218,9 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { FieldDef.floatField("gui.radius", this::getPushRadius, this::setPushRadius), FieldDef.floatField("gui.strength", this::getPushStrength, this::setPushStrength) ), - FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets), + FieldDef.section("ability.section.aoe"), + FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe) + .hover("ability.hover.aoe"), FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200), AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) )); 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 84afb11b6..c557c4b89 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 @@ -13,7 +13,9 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.network.play.server.S12PacketEntityVelocity; import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.api.ability.type.IAbilitySlam; @@ -61,6 +63,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam { private transient boolean hasRisen = false; // Whether entity has risen above launch position private transient int airTicks = 0; private transient int maxAirTicks = 60; // Timeout to prevent stuck in air + private transient boolean airSlam = false; // True when slam initiated while already in the air + private transient boolean wasFlying = false; // Saved flight state to restore after air slam public AbilitySlam() { this.typeId = "ability.cnpc.slam"; @@ -165,22 +169,59 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } /** - * Player slam: Launch in look direction using same ballistic arc as NPC slam. - * AOE damage triggers wherever the player lands. + * Player slam: behavior depends on whether the player is on the ground or in the air. + *

+ * On ground: Launch in look direction using ballistic arc, full damage on landing. + * In air: Slam straight down from current height, damage scales with fall distance. + * Works regardless of flying state — just checks height above ground. */ private void executePlayerSlam(EntityLivingBase caster) { - // 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); + if (!caster.onGround) { + // Air slam: player is already airborne (flying, falling, jumping) + airSlam = true; + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; - double launchDist = Math.max(4.0, maxRange * 0.5); + // Temporarily disable flight so gravity works during descent + if (caster instanceof EntityPlayerMP) { + EntityPlayerMP mp = (EntityPlayerMP) caster; + wasFlying = mp.capabilities.isFlying; + if (wasFlying) { + mp.capabilities.isFlying = false; + mp.sendPlayerAbilities(); + } + } - targetX = caster.posX + dirX * launchDist; - targetY = caster.posY; - targetZ = caster.posZ + dirZ * launchDist; + // Push downward — no horizontal movement, no upward arc + caster.motionX = 0; + caster.motionZ = 0; + caster.motionY = -0.5; + hasLaunched = true; + hasRisen = true; // Skip rise detection — we're already above ground - launchTowardTarget(caster); + if (!isPreview() && caster instanceof EntityPlayerMP) { + ((EntityPlayerMP) caster).playerNetServerHandler.sendPacket( + new S12PacketEntityVelocity(caster)); + caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); + } + } else { + // Ground slam: normal ballistic arc + airSlam = false; + + // 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 launchDist = Math.max(4.0, maxRange * 0.5); + + targetX = caster.posX + dirX * launchDist; + targetY = caster.posY; + targetZ = caster.posZ + dirZ * launchDist; + + launchTowardTarget(caster); + } } /** @@ -244,7 +285,12 @@ public class AbilitySlam extends Ability implements IAbilitySlam { ((EntityNPCInterface) caster).setNpcJumpingState(true); } if (!isPreview()) { - caster.velocityChanged = true; + if (caster instanceof EntityPlayerMP) { + ((EntityPlayerMP) caster).playerNetServerHandler.sendPacket( + new S12PacketEntityVelocity(caster)); + } else { + caster.velocityChanged = true; + } caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); } return; @@ -292,7 +338,14 @@ public class AbilitySlam extends Ability implements IAbilitySlam { ((EntityNPCInterface) caster).setNpcJumpingState(true); } if (!isPreview()) { - caster.velocityChanged = true; + // Use direct S12 packet for players to ensure velocity reaches client + // before flight code can override it + if (caster instanceof EntityPlayerMP) { + ((EntityPlayerMP) caster).playerNetServerHandler.sendPacket( + new S12PacketEntityVelocity(caster)); + } else { + caster.velocityChanged = true; + } } // Face the target @@ -313,6 +366,15 @@ public class AbilitySlam extends Ability implements IAbilitySlam { caster.fallDistance = 0; } + // Air slam: accelerate downward, prevent horizontal drift + if (airSlam && !isPreview()) { + caster.motionX = 0; + caster.motionZ = 0; + if (caster.motionY > -1.5) { + caster.motionY -= 0.15; + } + } + // Prevent NPC navigator from interfering with ballistic arc. // lockMovement=WINDUP means applyMovementControl() doesn't clear // the navigator during ACTIVE, so AI pathfinding could add motion. @@ -375,6 +437,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam { ((EntityNPCInterface) caster).setNpcJumpingState(false); } + // Restore flight if it was active before an air slam + if (airSlam && wasFlying && caster instanceof EntityPlayerMP) { + EntityPlayerMP mp = (EntityPlayerMP) caster; + mp.capabilities.isFlying = true; + mp.sendPlayerAbilities(); + } + // Signal that the ability has completed its active phase signalCompletion(); @@ -394,6 +463,16 @@ public class AbilitySlam extends Ability implements IAbilitySlam { // Play slam impact sound on landing world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f); + // Scale damage based on fall distance for air slams + float effectiveDamage = damage; + if (airSlam) { + double fallDistance = startY - caster.posY; + if (fallDistance < 0) fallDistance = 0; + // Scale linearly: full damage at leapHeight distance, minimum 25% for tiny falls + float heightFactor = (float) Math.min(1.0, fallDistance / Math.max(1.0, leapHeight)); + effectiveDamage = damage * Math.max(0.25f, heightFactor); + } + // Find all entities in radius @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABBExcludingEntity( @@ -409,7 +488,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { double dz = livingTarget.posZ - caster.posZ; if (dx * dx + dz * dz <= radius * radius) { // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockbackStrength); + boolean wasHit = applyAbilityDamage(caster, livingTarget, effectiveDamage, knockbackStrength); if (wasHit) { applyEffects(livingTarget); } @@ -462,10 +541,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam { if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(false); } + restoreFlightIfNeeded(caster); hasLaunched = false; hasLanded = false; hasRisen = false; airTicks = 0; + airSlam = false; + wasFlying = false; } @Override @@ -473,10 +555,26 @@ public class AbilitySlam extends Ability implements IAbilitySlam { if (!isPreview() && caster instanceof EntityNPCInterface) { ((EntityNPCInterface) caster).setNpcJumpingState(false); } + restoreFlightIfNeeded(caster); hasLaunched = false; hasLanded = false; hasRisen = false; airTicks = 0; + airSlam = false; + wasFlying = false; + } + + /** + * Restore flight state if it was temporarily disabled for an air slam. + */ + private void restoreFlightIfNeeded(EntityLivingBase caster) { + if (airSlam && wasFlying && caster instanceof EntityPlayerMP) { + EntityPlayerMP mp = (EntityPlayerMP) caster; + if (!mp.capabilities.isFlying) { + mp.capabilities.isFlying = true; + mp.sendPlayerAbilities(); + } + } } @Override @@ -485,6 +583,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam { hasLanded = false; hasRisen = false; airTicks = 0; + airSlam = false; + wasFlying = false; } @Override @@ -494,6 +594,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam { hasLanded = false; hasRisen = false; airTicks = 0; + airSlam = false; + wasFlying = false; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index c20871dd6..890f51511 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -32,8 +32,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { private float pullStrength = 0.8f; private float damage = 0.0f; private float knockback = 0.0f; - private boolean aoe = false; - private int maxTargets = 5; + private boolean aoe = true; private boolean damageOnPull = false; private float pullDamage = 0.0f; @@ -84,12 +83,12 @@ public class AbilityVortex extends Ability implements IAbilityVortex { ticksSincePullDamage = 0; if (!isPreview()) { - if (aoe) { - AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); - @SuppressWarnings("unchecked") - List entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box); + AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + @SuppressWarnings("unchecked") + List entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box); - int count = 0; + if (aoe) { + // ALL: pull every valid enemy in range for (EntityLivingBase entity : entities) { if (entity == caster) continue; if (entity.isDead) continue; @@ -98,19 +97,37 @@ public class AbilityVortex extends Ability implements IAbilityVortex { double dist = caster.getDistanceToEntity(entity); if (dist <= pullRadius) { getPulledEntities().add(entity.getUniqueID()); - count++; - if (count >= maxTargets) break; } } } else { - // NPC single-target mode: pull the aggro target - // Player: single-target mode has no effect (no target to pull — use AOE mode instead) + // SINGULAR: pull one target + // NPC: pull aggro target if valid, otherwise nearest enemy + // Player: pull nearest enemy in range if (!isPlayerCaster(caster) && target != null && !target.isDead) { double dist = caster.getDistanceToEntity(target); if (dist <= pullRadius) { getPulledEntities().add(target.getUniqueID()); + return; } } + + // Find nearest valid enemy + EntityLivingBase nearest = null; + double nearestDist = Double.MAX_VALUE; + for (EntityLivingBase entity : entities) { + if (entity == caster) continue; + if (entity.isDead) continue; + if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue; + + double dist = caster.getDistanceToEntity(entity); + if (dist <= pullRadius && dist < nearestDist) { + nearest = entity; + nearestDist = dist; + } + } + if (nearest != null) { + getPulledEntities().add(nearest.getUniqueID()); + } } } } @@ -229,7 +246,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex { nbt.setFloat("damage", damage); nbt.setFloat("knockback", knockback); nbt.setBoolean("aoe", aoe); - nbt.setInteger("maxTargets", maxTargets); nbt.setBoolean("damageOnPull", damageOnPull); nbt.setFloat("pullDamage", pullDamage); } @@ -240,8 +256,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { this.pullStrength = nbt.getFloat("pullStrength"); this.damage = nbt.getFloat("damage"); this.knockback = nbt.getFloat("knockback"); - this.aoe = nbt.getBoolean("aoe"); - this.maxTargets = nbt.getInteger("maxTargets"); + this.aoe = !nbt.hasKey("aoe") || nbt.getBoolean("aoe"); this.damageOnPull = nbt.getBoolean("damageOnPull"); this.pullDamage = nbt.getFloat("pullDamage"); } @@ -287,14 +302,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex { this.aoe = aoe; } - public int getMaxTargets() { - return maxTargets; - } - - public void setMaxTargets(int maxTargets) { - this.maxTargets = maxTargets; - } - public boolean isDamageOnPull() { return damageOnPull; } @@ -327,8 +334,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex { FieldDef.section("ability.section.aoe"), FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe) .hover("ability.hover.aoe"), - FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets) - .visibleWhen(this::isAoe), FieldDef.section("ability.section.pullDamage"), FieldDef.boolField("gui.enabled", this::isDamageOnPull, this::setDamageOnPull) .hover("ability.hover.dmgOnPull"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java index 980a1dfeb..21a2a4390 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java @@ -291,12 +291,16 @@ public abstract class AbilityBarrier extends AbilityEnergy { // Properties section defs.add(FieldDef.section("ability.section.properties")); - defs.add(FieldDef.boolField("ability.solid", this::isSolid, this::setSolid)); - defs.add(FieldDef.boolField("ability.knockbackEnabled", this::isKnockbackEnabled, this::setKnockbackEnabled)); + defs.add(FieldDef.boolField("ability.solid", this::isSolid, this::setSolid) + .hover("ability.hover.solid")); + defs.add(FieldDef.boolField("ability.knockbackEnabled", this::isKnockbackEnabled, this::setKnockbackEnabled) + .hover("ability.hover.knockbackEnabled")); defs.add(FieldDef.floatField("ability.knockbackStrength", this::getKnockbackStrength, this::setKnockbackStrength) .range(0, 10).visibleWhen(this::isKnockbackEnabled)); - defs.add(FieldDef.boolField("ability.absorbing", this::isAbsorbing, this::setAbsorbing)); - defs.add(FieldDef.boolField("ability.meleeEnabled", this::isMeleeEnabled, this::setMeleeEnabled)); + defs.add(FieldDef.boolField("ability.absorbing", this::isAbsorbing, this::setAbsorbing) + .hover("ability.hover.absorbing")); + defs.add(FieldDef.boolField("ability.meleeEnabled", this::isMeleeEnabled, this::setMeleeEnabled) + .hover("ability.hover.meleeEnabled")); defs.add(FieldDef.floatField("ability.meleeDamageMultiplier", this::getMeleeDamageMultiplier, this::setMeleeDamageMultiplier) .range(0, 10).visibleWhen(this::isMeleeEnabled)); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index ab374bf41..979ee4b55 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -1,5 +1,7 @@ package kamkeel.npcs.entity; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData; @@ -67,6 +69,16 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { super(world); } + @Override + @SideOnly(Side.CLIENT) + public boolean isInRangeToRenderDist(double distance) { + // Beam can extend far from entity position (head); use a generous render distance + // that accounts for the full beam length (origin to head) + double headDist = Math.sqrt(headOffsetX * headOffsetX + headOffsetY * headOffsetY + headOffsetZ * headOffsetZ); + double range = Math.max(128.0D, headDist * 2.0D + 64.0D); + return distance < range * range; + } + private static BeamMode modeFromAnchored(boolean anchored) { return anchored ? BeamMode.ANCHORED : BeamMode.FREE_TRAIL; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java index 8c0fc8fae..cf20be91e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java @@ -264,11 +264,13 @@ public class EntityAbilityDome extends EntityAbilityBarrier { List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase ent : entities) { if (ent.getEntityId() == ownerEntityId) continue; - if (isAllyOfOwner(ent)) continue; // Client-side: only process local player for solid prediction + // Must be checked before isAllyOfOwner which uses server-only PlayerData if (worldObj.isRemote && (localPlayer == null || ent != localPlayer)) continue; + if (isAllyOfOwner(ent)) continue; + // Current position relative to dome center double dx = ent.posX - posX; double dy = (ent.posY + ent.height * 0.5) - posY; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java index 653d336a2..a0092a2d7 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java @@ -421,11 +421,13 @@ public class EntityAbilityPanel extends EntityAbilityBarrier { for (EntityLivingBase ent : entities) { if (ent.getEntityId() == ownerEntityId) continue; - if (isAllyOfOwner(ent)) continue; // Client-side: only process local player for solid prediction + // Must be checked before isAllyOfOwner which uses server-only PlayerData if (worldObj.isRemote && (localPlayer == null || ent != localPlayer)) continue; + if (isAllyOfOwner(ent)) continue; + // Current position relative to panel center double dx = ent.posX - posX; double dy = (ent.posY + ent.height * 0.5) - posY; diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index 278fb8001..e866db2a0 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -432,9 +432,23 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { */ protected void handleClientInterpolation() { if (this.interpSteps > 0) { - double newX = this.posX + (this.interpTargetX - this.posX) / this.interpSteps; - double newY = this.posY + (this.interpTargetY - this.posY) / this.interpSteps; - double newZ = this.posZ + (this.interpTargetZ - this.posZ) / this.interpSteps; + // For very slow / stationary projectiles, snap directly to avoid jitter + double dx = this.interpTargetX - this.posX; + double dy = this.interpTargetY - this.posY; + double dz = this.interpTargetZ - this.posZ; + double distSq = dx * dx + dy * dy + dz * dz; + if (distSq < 0.04) { + this.setPosition(this.interpTargetX, this.interpTargetY, this.interpTargetZ); + this.motionX = this.interpTargetMotionX; + this.motionY = this.interpTargetMotionY; + this.motionZ = this.interpTargetMotionZ; + this.interpSteps = 0; + return; + } + + double newX = this.posX + dx / this.interpSteps; + double newY = this.posY + dy / this.interpSteps; + double newZ = this.posZ + dz / this.interpSteps; this.motionX = this.motionX + (this.interpTargetMotionX - this.motionX) / this.interpSteps; this.motionY = this.motionY + (this.interpTargetMotionY - this.motionY) / this.interpSteps; @@ -443,6 +457,11 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { this.setPosition(newX, newY, newZ); this.interpSteps--; } else { + // Skip motion application for nearly-stationary projectiles to prevent drift + double speedSq = this.motionX * this.motionX + this.motionY * this.motionY + this.motionZ * this.motionZ; + if (speedSq < 0.0001) { + return; + } this.posX += this.motionX; this.posY += this.motionY; this.posZ += this.motionZ; @@ -549,10 +568,17 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (target == owner) continue; if (shouldIgnoreExplosionTarget(target)) continue; + // Measure distance to nearest point on entity's bounding box, not feet position. + // Using feet position (target.posY) causes explosions at impact height to miss + // entities standing below — e.g. a projectile hitting at eye height (~1.62 above feet) + // would measure dist=1.62, failing a radius<=1.6 check despite a direct hit. + double closestX = Math.max(target.boundingBox.minX, Math.min(posX, target.boundingBox.maxX)); + double closestY = Math.max(target.boundingBox.minY, Math.min(posY, target.boundingBox.maxY)); + double closestZ = Math.max(target.boundingBox.minZ, Math.min(posZ, target.boundingBox.maxZ)); double dist = Math.sqrt( - Math.pow(target.posX - posX, 2) + - Math.pow(target.posY - posY, 2) + - Math.pow(target.posZ - posZ, 2) + (closestX - posX) * (closestX - posX) + + (closestY - posY) * (closestY - posY) + + (closestZ - posZ) * (closestZ - posZ) ); if (dist <= explosionRad) { diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 718793ddd..93ec65bb6 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -295,7 +295,7 @@ public class CustomNpcs { registerNewEntity(EntityAbilityOrb.class, "abilityorb", 64, 1, true); registerNewEntity(EntityAbilityDisc.class, "abilitydisc", 64, 1, true); registerNewEntity(EntityAbilityLaser.class, "abilitylaser", 64, 1, true); - registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 64, 1, true); + registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 160, 1, true); registerNewEntity(EntityAbilitySweeper.class, "abilitysweeper", 64, 3, true); registerNewEntity(EntityAbilityZone.class, "abilityzone", 64, 3, true); registerNewEntity(EntityAbilityDome.class, "energydome", 64, 3, true); diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 5b5efccbe..31a15551a 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -10,6 +10,7 @@ import kamkeel.npcs.controllers.AbilityController; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.SyncController; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; import kamkeel.npcs.util.AttributeAttackUtil; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; @@ -105,7 +106,8 @@ public class ScriptPlayerEventHandler { // Skip zeroing if the ability has its own movement (Charge/Dash/Slam) if (playerData.abilityData.isMovementLocked()) { Ability current = playerData.abilityData.getCurrentAbility(); - if (current == null || !current.hasAbilityMovement()) { + if (current == null || !current.hasAbilityMovement() + || current.getPhase() != AbilityPhase.ACTIVE) { boolean sFlying = playerData.abilityData.wasFlyingAtLock() || AbilityController.Instance.isPlayerFlying(player); @@ -120,6 +122,17 @@ public class ScriptPlayerEventHandler { } } + // Zero WASD input during ability-controlled movement to prevent + // moveEntityWithHeading from interfering with ability velocity + { + Ability current = playerData.abilityData.getCurrentAbility(); + if (current != null && current.hasAbilityMovement() + && current.getPhase() == AbilityPhase.ACTIVE) { + player.moveForward = 0; + player.moveStrafing = 0; + } + } + if (playerData.updateClient) { SyncController.syncPlayerData((EntityPlayerMP) player, true); playerData.updateClient = false; diff --git a/src/main/java/noppes/npcs/client/ClientEventHandler.java b/src/main/java/noppes/npcs/client/ClientEventHandler.java index 6eebfd5d5..b5e37503a 100644 --- a/src/main/java/noppes/npcs/client/ClientEventHandler.java +++ b/src/main/java/noppes/npcs/client/ClientEventHandler.java @@ -102,7 +102,13 @@ public class ClientEventHandler { // Ability Hotbar: Hold HUD key + scroll wheel to cycle if (event.dwheel != 0 && ClientProxy.AbilityHudKey != null) { int hudKeyCode = ClientProxy.AbilityHudKey.getKeyCode(); - if (hudKeyCode != 0 && org.lwjgl.input.Keyboard.isKeyDown(hudKeyCode)) { + boolean hudKeyHeld = false; + if (hudKeyCode > 0) { + hudKeyHeld = org.lwjgl.input.Keyboard.isKeyDown(hudKeyCode); + } else if (hudKeyCode < 0) { + hudKeyHeld = org.lwjgl.input.Mouse.isButtonDown(hudKeyCode + 100); + } + if (hudKeyHeld) { AbilityHotbarComponent comp = (AbilityHotbarComponent) ClientHudManager.getInstance() .getHudComponents().get(EnumHudComponent.AbilityHotbar); if (comp != null && comp.hasAnyAbilities()) { diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 90d877011..61553093e 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -2058,6 +2058,10 @@ ability.meleeDamageMultiplier=Melee Multiplier ability.followCaster=Follow Caster ability.hover.followCaster=Dome stays centered on the caster as they move +ability.hover.solid=Entities cannot pass through the barrier boundary +ability.hover.knockbackEnabled=Repulsion field pushing entities away from barrier surface +ability.hover.absorbing=Damage dealt to the caster is redirected to the barrier's health +ability.hover.meleeEnabled=Barrier can be hit and damaged by melee attacks ability.variant.small=Small