Fix barrier solid/knockback behavior and dome melee targeting

- Rewrite solid system to use reactive crossing detection (prevPos comparison)
  instead of prediction-based teleportation that caused client rubber-banding
- Reduce knockback force (0.06 multiplier) so it pushes without fully blocking
- Add client-side solid prediction for smooth movement blocking (local player only)
- Fix dome melee from inside: canBeCollidedWith returns false when player is inside
- Fix dome melee from outside: getCollisionBorderSize set to 0 to prevent raycast
  from returning far exit point beyond reach distance
- Clean up dome bounding box: use setPosition/updateBoundingBox instead of custom setSize

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
KamandClaude Opus 4.6 committed 2026-02-20 23:55:32 -05:00
1 parent 0a85bb5a7b
commit e711a9012b
2 files changed
+214 -132

No files matched your search

@@ -7,10 +7,12 @@ import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.CustomNpcs;
import java.util.List;
@@ -53,15 +55,7 @@ public class EntityEnergyDome extends EntityEnergyBarrier {
@Override
protected void setSize(float width, float height) {
if (width != this.width || height != this.height) {
this.width = width;
this.height = height;
this.boundingBox.maxX = this.boundingBox.minX + (double) this.width;
this.boundingBox.maxZ = this.boundingBox.minZ + (double) this.width;
this.boundingBox.maxY = this.boundingBox.minY + (double) this.height;
}
this.ySize = 0.0F;
this.yOffset = -this.height * 0.5F;
super.setSize(width, height);
}
@Override
@@ -102,9 +96,11 @@ public class EntityEnergyDome extends EntityEnergyBarrier {
if (updateBarrierTick()) return;
// Knockback velocity push every tick
if (!worldObj.isRemote && barrierData.knockbackEnabled) {
knockbackEntities();
// Process entity physics every tick
// Server: solid + knockback for all entities
// Client: solid prediction for local player only (smooth movement blocking)
if (barrierData.solid || barrierData.knockbackEnabled) {
processEntityPhysics();
}
}
@@ -210,49 +206,50 @@ public class EntityEnergyDome extends EntityEnergyBarrier {
setCharging(false);
}
// ==================== KNOCKBACK (DBO-style motion prediction) ====================
// ==================== ENTITY PHYSICS (Solid + Knockback) ====================
/**
* DBO Ki Shield style knockback — unified algorithm with geometric jail:
* Processes entity physics for the dome barrier. Two independent systems:
* <p>
* When jailEnabled = false (default):
* - OUTSIDE entities: predict future position, if it would cross into dome push outward.
* If close and approaching, apply soft outward push. Full motion replacement.
* - INSIDE entities: NOT affected (free movement). Dome only blocks entry.
* SOLID: Hard wall — entities cannot cross the dome boundary in either direction.
* Uses reactive crossing detection: compares current position to previous tick
* to detect boundary crossings, then teleports back and applies velocity correction.
* Also preemptively cancels inward velocity when near the surface.
* <p>
* When jailEnabled = true:
* - OUTSIDE entities: pushed outward (same as above, prevents entry).
* - INSIDE entities: pushed inward when near edge (geometric containment).
* Any entity inside stays inside, any entity outside stays outside.
* KNOCKBACK: Repulsion force — entities near the dome surface get pushed away.
* This is a push effect only; entities can still pass through with enough effort.
* Does NOT prevent passthrough — use solid for that.
* <p>
* For moving domes (followCaster), dome velocity is accounted for by using
* relative velocity (entity motion minus dome motion) for predictions.
* the dome's previous position for previous-tick distance calculations.
*/
@Override
@SuppressWarnings("unchecked")
protected void knockbackEntities() {
float margin = 2.0f;
protected void processEntityPhysics() {
float r = domeRadius;
boolean solid = barrierData.solid;
boolean knockback = barrierData.knockbackEnabled;
float strength = barrierData.knockbackStrength;
boolean jail = barrierData.jailEnabled;
float jailThresh = barrierData.jailThreshold;
// Dome velocity for moving domes (followCaster)
double domeVelX = posX - prevPosX;
double domeVelY = posY - prevPosY;
double domeVelZ = posZ - prevPosZ;
float solidMargin = 1.5f; // Zone around surface for preemptive velocity correction
float knockbackMargin = 2.0f; // Zone around surface for knockback push
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
posX - r - margin, posY - r - margin, posZ - r - margin,
posX + r + margin, posY + r + margin, posZ + r + margin
posX - r - knockbackMargin, posY - r - knockbackMargin, posZ - r - knockbackMargin,
posX + r + knockbackMargin, posY + r + knockbackMargin, posZ + r + knockbackMargin
);
// On client, identify local player for client-side solid prediction
EntityPlayer localPlayer = worldObj.isRemote ? CustomNpcs.proxy.getPlayer() : null;
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
for (EntityLivingBase ent : entities) {
if (ent.getEntityId() == ownerEntityId) continue;
if (!isKnockbackTarget(ent)) continue;
if (isAllyOfOwner(ent)) continue;
// Client-side: only process local player for solid prediction
if (worldObj.isRemote && (localPlayer == null || ent != localPlayer)) continue;
// Current position relative to dome center
double dx = ent.posX - posX;
double dy = (ent.posY + ent.height * 0.5) - posY;
@@ -265,71 +262,76 @@ public class EntityEnergyDome extends EntityEnergyBarrier {
double ny = dy / dist;
double nz = dz / dist;
// Entity velocity relative to dome (accounts for moving domes)
double relVelX = ent.motionX - domeVelX;
double relVelY = ent.motionY - domeVelY;
double relVelZ = ent.motionZ - domeVelZ;
boolean isInside = dist < r;
if (isInside && jail) {
// --- INSIDE + JAIL ENABLED: push toward center when near dome edge ---
if (dist > r * jailThresh) {
double overshoot = dist - r * jailThresh;
double force = (overshoot + 0.1) * strength * 0.2;
// Set motion to dome velocity + inward push (keeps entity moving with dome)
ent.motionX = domeVelX - nx * force;
ent.motionY = domeVelY - ny * force;
ent.motionZ = domeVelZ - nz * force;
// --- SOLID: hard wall preventing boundary crossing ---
if (solid) {
// Previous tick: entity position relative to dome's previous position
double prevDx = ent.prevPosX - prevPosX;
double prevDy = (ent.prevPosY + ent.height * 0.5) - prevPosY;
double prevDz = ent.prevPosZ - prevPosZ;
double prevDist = Math.sqrt(prevDx * prevDx + prevDy * prevDy + prevDz * prevDz);
boolean wasInside = prevDist < r;
// Radial velocity (positive = moving outward from dome center)
double radialVel = ent.motionX * nx + ent.motionY * ny + ent.motionZ * nz;
if (wasInside != isInside) {
// Entity crossed the boundary this tick — push back to original side
double pushDist = wasInside ? (r - 0.3) : (r + 0.3);
double surfaceX = posX + nx * pushDist;
double surfaceY = posY + ny * pushDist - ent.height * 0.5;
double surfaceZ = posZ + nz * pushDist;
teleportEntity(ent, surfaceX, surfaceY, surfaceZ);
// Cancel radial velocity and add corrective push
ent.motionX -= radialVel * nx;
ent.motionY -= radialVel * ny;
ent.motionZ -= radialVel * nz;
double pushForce = wasInside ? -0.15 : 0.15;
ent.motionX += nx * pushForce;
ent.motionZ += nz * pushForce;
ent.velocityChanged = true;
} else {
// Predict if entity would escape next tick (relative to dome)
double futDx = dx + relVelX;
double futDy = dy + relVelY;
double futDz = dz + relVelZ;
double futDist = Math.sqrt(futDx * futDx + futDy * futDy + futDz * futDz);
if (futDist >= r) {
// Would escape: cancel outward motion, match dome velocity
double radialVel = relVelX * nx + relVelY * ny + relVelZ * nz;
if (radialVel > 0) {
// Preemptive: near surface and moving toward it — cancel radial velocity
double surfaceDist = Math.abs(dist - r);
if (surfaceDist < solidMargin) {
boolean movingTowardSurface = (!isInside && radialVel < -0.01)
|| (isInside && radialVel > 0.01);
if (movingTowardSurface) {
ent.motionX -= radialVel * nx;
ent.motionY -= radialVel * ny;
ent.motionZ -= radialVel * nz;
double pushForce = isInside ? -0.05 : 0.05;
ent.motionX += nx * pushForce;
ent.motionZ += nz * pushForce;
ent.velocityChanged = true;
}
}
}
} else if (!isInside) {
// --- OUTSIDE: push outward (prevent entry) ---
}
// Predicted future distance from center (relative to dome movement)
double futDx = dx + relVelX;
double futDy = dy + relVelY;
double futDz = dz + relVelZ;
double futDist = Math.sqrt(futDx * futDx + futDy * futDy + futDz * futDz);
// --- KNOCKBACK: repulsion push (server only, synced via velocityChanged) ---
if (!worldObj.isRemote && knockback) {
double surfaceDist = Math.abs(dist - r);
if (surfaceDist < knockbackMargin) {
double proximity = 1.0 - (surfaceDist / knockbackMargin);
double force = proximity * strength * 0.06;
// Radial velocity relative to dome (positive = moving away)
double radialVel = relVelX * nx + relVelY * ny + relVelZ * nz;
if (futDist < r) {
// Would cross INTO dome: hard push outward proportional to overshoot
double overshoot = r - futDist;
double force = (overshoot * 2.0 + 0.05) * strength * 0.1;
ent.motionX = domeVelX + nx * force;
ent.motionY = domeVelY + ny * force * 0.5;
ent.motionZ = domeVelZ + nz * force;
ent.velocityChanged = true;
} else if (radialVel < 0 && dist < r + margin) {
// Close and approaching: soft push outward proportional to proximity
double proximity = 1.0 - ((dist - r) / margin);
double force = proximity * strength * 0.1;
ent.motionX = domeVelX + nx * force;
ent.motionY = domeVelY + ny * force * 0.5;
ent.motionZ = domeVelZ + nz * force;
if (isInside) {
// Push toward center (away from surface)
ent.motionX += -nx * force;
ent.motionY += -ny * force * 0.5;
ent.motionZ += -nz * force;
} else {
// Push outward (away from surface)
ent.motionX += nx * force;
ent.motionY += ny * force * 0.5;
ent.motionZ += nz * force;
}
ent.velocityChanged = true;
}
}
// else: INSIDE + no jail → do nothing (free movement)
}
}
@@ -346,6 +348,24 @@ public class EntityEnergyDome extends EntityEnergyBarrier {
// ==================== MELEE (spherical check) ====================
/**
* On the client side, the dome's large bounding box intercepts all raycasts from inside,
* preventing the player from targeting mobs within the dome. To fix this, return false
* when the local player is inside the dome so the raycast passes through to other entities.
* Players outside the dome can still target it for melee normally.
*/
@Override
public boolean canBeCollidedWith() {
if (!barrierData.meleeEnabled) return false;
if (worldObj.isRemote) {
EntityPlayer player = CustomNpcs.proxy.getPlayer();
if (player != null && isEntityInside(player)) {
return false;
}
}
return true;
}
/**
* Reject melee hits that land on the cubic bounding box but are outside the actual sphere.
* Uses distance from the sphere surface (not center) for accurate spherical rejection.
@@ -368,13 +388,14 @@ public class EntityEnergyDome extends EntityEnergyBarrier {
// ==================== BOUNDING BOX ====================
/**
* Small expansion on top of the dome-sized bounding box for comfortable
* melee targeting. The actual BB is already dome-sized (set in onUpdate),
* so this only adds a small reach margin for crosshair ray-testing.
* No expansion needed — the bounding box is already dome-sized.
* Returning 0 ensures players outside the dome sphere are also outside the
* ray-testing BB, so MC's raycast returns the near-face hit (within reach)
* rather than the far exit point (beyond reach).
*/
@Override
public float getCollisionBorderSize() {
return 1.0f;
return 0.0f;
}
@Override
@@ -414,7 +435,7 @@ public class EntityEnergyDome extends EntityEnergyBarrier {
public void setDomeRadius(float radius) {
this.domeRadius = Math.max(0.1f, radius);
this.setSize(this.domeRadius * 2, this.domeRadius * 2);
this.setPosition(this.posX, this.posY, this.posZ);
}
public boolean isFollowCaster() {
@@ -425,6 +446,30 @@ public class EntityEnergyDome extends EntityEnergyBarrier {
this.followCaster = follow;
}
@Override
@SideOnly(Side.CLIENT)
public void setPositionAndRotation2(double x, double y, double z, float yaw, float pitch, int increments) {
// Don't do the vanilla "push up out of blocks" logic.
this.setPosition(x, y, z);
this.setRotation(yaw, pitch);
}
@Override
public void setPosition(double x, double y, double z) {
this.posX = x;
this.posY = y;
this.posZ = z;
updateBoundingBox();
}
protected void updateBoundingBox() {
float r = this.domeRadius;
this.boundingBox.setBounds(
this.posX - r, this.posY - r, this.posZ - r,
this.posX + r, this.posY + r, this.posZ + r
);
}
// ==================== NBT ====================
@Override
@@ -13,6 +13,7 @@ import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.CustomNpcs;
import noppes.npcs.NpcDamageSource;
import noppes.npcs.entity.EntityNPCInterface;
@@ -119,9 +120,11 @@ public class EntityEnergyPanel extends EntityEnergyBarrier {
if (updateBarrierTick()) return;
// Knockback velocity push every tick (not during launched mode)
if (!worldObj.isRemote && barrierData.knockbackEnabled && mode != PanelMode.LAUNCHED) {
knockbackEntities();
// Process entity physics every tick (not during launched mode)
// Server: solid + knockback for all entities
// Client: solid prediction for local player only (smooth movement blocking)
if ((barrierData.solid || barrierData.knockbackEnabled) && mode != PanelMode.LAUNCHED) {
processEntityPhysics();
}
// Mode-specific updates
@@ -365,21 +368,28 @@ public class EntityEnergyPanel extends EntityEnergyBarrier {
setCharging(false);
}
// ==================== KNOCKBACK (DBO-style motion prediction) ====================
// ==================== ENTITY PHYSICS (Solid + Knockback) ====================
/**
* DBO-inspired planar knockback:
* - Predict future position from current motion
* - Detect crossing or proximity to panel surface
* - Apply counter-force via full motion replacement
* - No separate phases — single unified algorithm
* Processes entity physics for the panel barrier. Two independent systems:
* <p>
* SOLID: Hard wall — entities cannot cross the panel plane within its bounds.
* Uses reactive crossing detection: compares current side to previous tick
* to detect plane crossings, then teleports back and applies velocity correction.
* Also preemptively cancels normal velocity when near the plane.
* <p>
* KNOCKBACK: Repulsion force — entities near the panel surface get pushed away.
* This is a push effect only; entities can still pass through with enough effort.
* Does NOT prevent passthrough — use solid for that.
*/
@Override
@SuppressWarnings("unchecked")
protected void knockbackEntities() {
protected void processEntityPhysics() {
float halfW = panelData.panelWidth * 0.5f;
float halfH = panelData.panelHeight * 0.5f;
float searchExtension = 5.0f;
float searchExtension = 3.0f;
boolean solid = barrierData.solid;
boolean knockback = barrierData.knockbackEnabled;
float strength = barrierData.knockbackStrength;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
@@ -387,6 +397,9 @@ public class EntityEnergyPanel extends EntityEnergyBarrier {
posX + halfW + searchExtension, posY + halfH + searchExtension, posZ + halfW + searchExtension
);
// On client, identify local player for client-side solid prediction
EntityPlayer localPlayer = worldObj.isRemote ? CustomNpcs.proxy.getPlayer() : null;
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
float yawRad = (float) Math.toRadians(panelYaw);
float normalX = -(float) Math.sin(yawRad);
@@ -394,9 +407,11 @@ public class EntityEnergyPanel extends EntityEnergyBarrier {
for (EntityLivingBase ent : entities) {
if (ent.getEntityId() == ownerEntityId) continue;
if (!isKnockbackTarget(ent)) continue;
if (isAllyOfOwner(ent)) continue;
// Client-side: only process local player for solid prediction
if (worldObj.isRemote && (localPlayer == null || ent != localPlayer)) continue;
// Current position relative to panel center
double dx = ent.posX - posX;
double dy = (ent.posY + ent.height * 0.5) - posY;
@@ -407,45 +422,67 @@ public class EntityEnergyPanel extends EntityEnergyBarrier {
float localRight = (float) (dx * (-normalZ) + dz * normalX);
// Skip if outside panel bounds (with margin)
if (Math.abs(localRight) > halfW + searchExtension) continue;
if (Math.abs(dy) > halfH + searchExtension) continue;
if (Math.abs(localForward) > searchExtension) continue;
// Skip if outside panel width/height bounds (no wall to push against)
if (Math.abs(localRight) > halfW + 1.0f) continue;
if (Math.abs(dy) > halfH + 1.0f) continue;
// Predicted future position
double futDx = (ent.posX + ent.motionX) - posX;
double futDz = (ent.posZ + ent.motionZ) - posZ;
float futLocalForward = (float) (futDx * normalX + futDz * normalZ);
// Normal velocity component (positive = moving in normal direction)
double normalVel = ent.motionX * normalX + ent.motionZ * normalZ;
if (Math.abs(localForward) > searchExtension) continue;
// Determine which side the entity is on
float side = localForward >= 0 ? 1.0f : -1.0f;
boolean movingTowardPanel = (localForward >= 0 && normalVel < 0) || (localForward < 0 && normalVel > 0);
// Check if predicted position would cross the plane (sign change)
boolean wouldCross = (localForward > 0 && futLocalForward <= 0) || (localForward < 0 && futLocalForward >= 0);
// Check if within panel width/height bounds
boolean inBounds = Math.abs(localRight) <= halfW && Math.abs(dy) <= halfH;
if (wouldCross && Math.abs(localRight) <= halfW && Math.abs(dy) <= halfH) {
// Hard block: full motion replacement to prevent crossing
// Push back along normal, proportional to how far past the surface they'd go
double overshoot = Math.abs(futLocalForward);
double pushForce = (overshoot + 0.05) * strength * 0.1;
ent.motionX = normalX * side * pushForce;
ent.motionZ = normalZ * side * pushForce;
ent.velocityChanged = true;
} else if (Math.abs(localForward) < 1.0 && movingTowardPanel
&& Math.abs(localRight) <= halfW && Math.abs(dy) <= halfH) {
// Soft push: entity is close and approaching, apply proportional force
double proximity = 1.0 - Math.abs(localForward);
double pushForce = proximity * strength * 0.1;
ent.motionX = normalX * side * pushForce;
ent.motionZ = normalZ * side * pushForce;
ent.velocityChanged = true;
// --- SOLID: hard wall preventing plane crossing ---
if (solid && inBounds) {
// Previous tick: entity position relative to panel
double prevDx = ent.prevPosX - prevPosX;
double prevDz = ent.prevPosZ - prevPosZ;
float prevLocalForward = (float) (prevDx * normalX + prevDz * normalZ);
float prevSide = prevLocalForward >= 0 ? 1.0f : -1.0f;
// Normal velocity component (positive = moving in normal direction)
double normalVel = ent.motionX * normalX + ent.motionZ * normalZ;
if (prevSide != side) {
// Entity crossed the panel plane this tick — push back to original side
double pushDist = 0.3;
double newX = posX + normalX * prevSide * pushDist + (localRight * (-normalZ));
double newZ = posZ + normalZ * prevSide * pushDist + (localRight * normalX);
teleportEntity(ent, newX, ent.posY, newZ);
// Cancel normal velocity and add corrective push
ent.motionX -= normalVel * normalX;
ent.motionZ -= normalVel * normalZ;
ent.motionX += normalX * prevSide * 0.15;
ent.motionZ += normalZ * prevSide * 0.15;
ent.velocityChanged = true;
} else {
// Preemptive: near the plane and moving toward it — cancel normal velocity
double absDist = Math.abs(localForward);
if (absDist < 1.5) {
boolean movingToward = (side > 0 && normalVel < -0.01)
|| (side < 0 && normalVel > 0.01);
if (movingToward) {
ent.motionX -= normalVel * normalX;
ent.motionZ -= normalVel * normalZ;
ent.motionX += normalX * side * 0.05;
ent.motionZ += normalZ * side * 0.05;
ent.velocityChanged = true;
}
}
}
}
// --- KNOCKBACK: repulsion push (server only, synced via velocityChanged) ---
if (!worldObj.isRemote && knockback && inBounds) {
double absDist = Math.abs(localForward);
if (absDist < searchExtension) {
double proximity = 1.0 - (absDist / searchExtension);
double force = proximity * strength * 0.06;
ent.motionX += normalX * side * force;
ent.motionZ += normalZ * side * force;
ent.velocityChanged = true;
}
}
}
}