mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-11 23:07:37 +00:00
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:
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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user