Various Fixes to CNPC Abilities

- super slow speed orb has weird properites, after awhile it flies weirdly
- Binding HUD scroll key to middle mouse button crashes client
- Zero Knockback still does knockback to CNPCs
- Charge: the telegraph is where you are looking but you actuually charge where your current body rotation is instead
- using slam mid air makes it not work correctly
- Beam disappears after going out of the render distance but still fires
- Player vortex can only pull one target

Slam now takes current height when being used to calculate damage
This commit is contained in:
Kam committed 2026-02-22 21:19:44 -05:00
1 parent ec60a9bb21
commit d2d5bcc59b
15 files changed
+317 -95

No files matched your search

+1 -1
Submodule src/api updated: 658da549ee...2438b2110e.
@@ -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;
@@ -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);
}
/**
@@ -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<EntityLivingBase> 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)
));
@@ -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.
* <p>
* 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<Entity> 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
@@ -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<EntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> 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"),
@@ -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));
@@ -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;
}
@@ -264,11 +264,13 @@ public class EntityAbilityDome extends EntityAbilityBarrier {
List<EntityLivingBase> 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;
@@ -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;
@@ -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) {
+1 -1
View File
@@ -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);
@@ -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;
@@ -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()) {
@@ -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