Add Hit Type to Projectiles

This commit is contained in:
Kam committed 2026-02-21 20:39:16 -05:00
1 parent 26da3ae2f9
commit b6749e306b
15 files changed
+265 -292

No files matched your search

@@ -0,0 +1,32 @@
package kamkeel.npcs.controllers.data.ability;
/**
* Shared hit behavior for energy projectiles.
*/
public enum HitType {
SINGLE,
PIERCE,
MULTI;
public static HitType fromOrdinal(int ordinal) {
HitType[] values = values();
if (ordinal >= 0 && ordinal < values.length) {
return values[ordinal];
}
return SINGLE;
}
@Override
public String toString() {
switch (this) {
case SINGLE:
return "ability.hitType.single";
case PIERCE:
return "ability.hitType.pierce";
case MULTI:
return "ability.hitType.multi";
default:
return name();
}
}
}
@@ -1,5 +1,6 @@
package kamkeel.npcs.controllers.data.ability.data;
import kamkeel.npcs.controllers.data.ability.HitType;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.api.ability.data.IEnergyCombatData;
@@ -14,6 +15,8 @@ public class EnergyCombatData implements IEnergyCombatData {
public boolean explosive = false;
public float explosionRadius = 3.0f;
public float explosionDamageFalloff = 0.5f;
public HitType hitType = HitType.SINGLE;
public int multiHitDelayTicks = 5;
public EnergyCombatData() {
}
@@ -95,6 +98,8 @@ public class EnergyCombatData implements IEnergyCombatData {
nbt.setBoolean("explosive", explosive);
nbt.setFloat("explosionRadius", explosionRadius);
nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff);
nbt.setInteger("hitType", hitType.ordinal());
nbt.setInteger("multiHitDelayTicks", multiHitDelayTicks);
}
public void readNBT(NBTTagCompound nbt) {
@@ -104,15 +109,21 @@ public class EnergyCombatData implements IEnergyCombatData {
explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive");
explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f;
explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f;
hitType = HitType.fromOrdinal(nbt.hasKey("hitType") ? nbt.getInteger("hitType") : 0);
multiHitDelayTicks = nbt.hasKey("multiHitDelayTicks") ? nbt.getInteger("multiHitDelayTicks") : 5;
// Sanitize
if (Float.isNaN(damage) || Float.isInfinite(damage)) damage = 7.0f;
if (Float.isNaN(knockback) || Float.isInfinite(knockback) || knockback < 0) knockback = 1.0f;
if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius) || explosionRadius < 0) explosionRadius = 3.0f;
if (multiHitDelayTicks < 1) multiHitDelayTicks = 1;
}
public EnergyCombatData copy() {
return new EnergyCombatData(damage, knockback, knockbackUp,
EnergyCombatData copy = new EnergyCombatData(damage, knockback, knockbackUp,
explosive, explosionRadius, explosionDamageFalloff);
copy.hitType = hitType;
copy.multiHitDelayTicks = multiHitDelayTicks;
return copy;
}
}
@@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.HitType;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
@@ -557,6 +558,22 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
combatData.explosionDamageFalloff = falloff;
}
public int getHitType() {
return combatData.hitType.ordinal();
}
public void setHitType(int hitType) {
this.combatData.hitType = HitType.fromOrdinal(hitType);
}
public int getMultiHitDelayTicks() {
return combatData.multiHitDelayTicks;
}
public void setMultiHitDelayTicks(int delay) {
this.combatData.multiHitDelayTicks = Math.max(1, delay);
}
// Lifespan data
@Override
public float getMaxDistance() {
@@ -696,6 +713,15 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
@SideOnly(Side.CLIENT)
@Override
public final void getAbilityDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.enumField("ability.hitType", HitType.class,
() -> HitType.fromOrdinal(getHitType()),
(v) -> setHitType(v.ordinal()))
.hover("ability.hover.hitType"));
defs.add(FieldDef.intField("ability.multiHitDelay", this::getMultiHitDelayTicks, this::setMultiHitDelayTicks)
.range(1, 200)
.visibleWhen(() -> getHitType() == HitType.MULTI.ordinal())
.hover("ability.hover.multiHitDelay"));
// Type-specific fields first
addTypeDefinitions(defs);
@@ -25,8 +25,8 @@ import noppes.npcs.client.gui.builder.FieldDef;
import java.util.List;
/**
* Laser Shot ability: Fast expanding thin line that pierces through targets.
* Travels in a straight line from origin, damaging all entities it passes through.
* Laser Shot ability: Fast expanding thin line.
* Travels in a straight line from origin.
* Single-projectile only (always projectileCount=1).
*/
public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser> implements IAbilityLaserShot {
@@ -34,7 +34,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
private float laserWidth = 0.3f;
private float expansionSpeed = 3.0f;
private int lingerTicks = 8;
private boolean dieOnImpact = false;
public AbilityLaserShot() {
super(
@@ -123,7 +122,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
laserWidth,
resolved, combatData, lightningData, lifespanData, trajectoryData,
expansionSpeed, lingerTicks);
laser.setDieOnImpact(dieOnImpact);
return laser;
}
@@ -161,7 +159,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
nbt.setFloat("laserWidth", laserWidth);
nbt.setFloat("expansionSpeed", expansionSpeed);
nbt.setInteger("lingerTicks", lingerTicks);
nbt.setBoolean("dieOnImpact", dieOnImpact);
}
@Override
@@ -169,7 +166,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
this.laserWidth = nbt.getFloat("laserWidth");
this.expansionSpeed = nbt.getFloat("expansionSpeed");
this.lingerTicks = nbt.getInteger("lingerTicks");
this.dieOnImpact = nbt.getBoolean("dieOnImpact");
}
// ==================== TYPE-SPECIFIC GETTERS ====================
@@ -198,14 +194,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
this.lingerTicks = lingerTicks;
}
public boolean isDieOnImpact() {
return dieOnImpact;
}
public void setDieOnImpact(boolean dieOnImpact) {
this.dieOnImpact = dieOnImpact;
}
// ==================== TYPE-SPECIFIC GUI ====================
@SideOnly(Side.CLIENT)
@@ -226,7 +214,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200)
));
defs.add(FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f));
defs.add(FieldDef.boolField("ability.dieOnImpact", this::isDieOnImpact, this::setDieOnImpact));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
@@ -23,14 +23,13 @@ import java.util.List;
/**
* Energy Slicer ability: Fires a thin, wide blade projectile in a straight line.
* JJK Cleave/Dismantle inspired. Piercing by default.
* JJK Cleave/Dismantle inspired.
* Extends AbilityEnergyProjectile to leverage the existing projectile infrastructure.
*/
public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
private float sliceWidth = 3.0f;
private float sliceThickness = 0.15f;
private boolean piercing = true;
public AbilitySlicer() {
super(
@@ -62,8 +61,7 @@ public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
sliceWidth, sliceThickness,
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
piercing
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData
);
}
@@ -122,14 +120,12 @@ public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
protected void writeTypeSpecificNBT(NBTTagCompound nbt) {
nbt.setFloat("sliceWidth", sliceWidth);
nbt.setFloat("sliceThickness", sliceThickness);
nbt.setBoolean("piercing", piercing);
}
@Override
protected void readTypeSpecificNBT(NBTTagCompound nbt) {
this.sliceWidth = nbt.hasKey("sliceWidth") ? nbt.getFloat("sliceWidth") : 3.0f;
this.sliceThickness = nbt.hasKey("sliceThickness") ? nbt.getFloat("sliceThickness") : 0.15f;
this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing");
}
// ==================== TYPE-SPECIFIC GETTERS ====================
@@ -158,14 +154,6 @@ public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
this.sliceThickness = Math.max(0.05f, thickness);
}
public boolean isPiercing() {
return piercing;
}
public void setPiercing(boolean piercing) {
this.piercing = piercing;
}
// ==================== TYPE-SPECIFIC GUI ====================
@SideOnly(Side.CLIENT)
@@ -186,8 +174,6 @@ public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
FieldDef.floatField("gui.width", this::getSliceWidth, this::setSliceWidth).range(0.5f, 100.0f),
FieldDef.floatField("gui.thickness", this::getSliceThickness, this::setSliceThickness).range(0.05f, 100.0f)
));
defs.add(FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing)
.hover("ability.hover.piercing"));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200)
@@ -13,10 +13,8 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.EventHooks;
import noppes.npcs.LogWriter;
import java.util.ArrayList;
@@ -546,24 +544,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
double prevHeadWorldY = startY + prevHeadOffsetY;
double prevHeadWorldZ = startZ + prevHeadOffsetZ;
Vec3 currentPos = Vec3.createVectorHelper(prevHeadWorldX, prevHeadWorldY, prevHeadWorldZ);
Vec3 nextPos = Vec3.createVectorHelper(headX, headY, headZ);
// Use full raytrace that doesn't stop at liquids and checks all blocks
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
if (!worldObj.isRemote) {
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
}
hasHit = true;
if (isExplosive()) {
posX = blockHit.hitVec.xCoord;
posY = blockHit.hitVec.yCoord;
posZ = blockHit.hitVec.zCoord;
doExplosion();
}
this.setDead();
}
handleBlockImpact(rayTraceBlocks(prevHeadWorldX, prevHeadWorldY, prevHeadWorldZ, headX, headY, headZ), true);
}
private void checkEntityCollision(double headX, double headY, double headZ) {
@@ -577,24 +558,12 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
for (EntityLivingBase entity : entities) {
if (shouldIgnoreEntity(entity)) continue;
if (DEBUG_LOGGING) {
LogWriter.info("[Beam] DEAD: Entity collision with " + entity.getClass().getSimpleName() + " at tick " + ticksExisted);
if (processEntityHit(entity, headX, headY, headZ)) {
if (DEBUG_LOGGING) {
LogWriter.info("[Beam] DEAD: Entity collision with " + entity.getClass().getSimpleName() + " at tick " + ticksExisted);
}
return;
}
hasHit = true;
if (isExplosive()) {
posX = headX;
posY = headY;
posZ = headZ;
doExplosion();
} else {
applyDamage(entity);
}
this.setDead();
return;
}
}
@@ -12,12 +12,8 @@ import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.EventHooks;
import java.util.List;
/**
* Disc projectile - flat spinning disc with optional boomerang behavior.
@@ -185,7 +181,6 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
// Calculate velocity toward target
Entity owner = getOwnerEntity();
if (target != null) {
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
@@ -248,15 +243,7 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
}
// Check wall collision
if (this.isCollidedHorizontally || this.isCollidedVertically) {
if (!worldObj.isRemote) {
hasHit = true;
if (isExplosive()) {
doExplosion();
}
}
this.setDead();
}
handleSolidCollisionTermination();
}
@Override
@@ -352,24 +339,7 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
}
private void checkBlockCollision() {
Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ);
Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ);
// Use full raytrace that doesn't stop at liquids and checks all blocks
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
if (!worldObj.isRemote) {
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
}
hasHit = true;
if (isExplosive()) {
posX = blockHit.hitVec.xCoord;
posY = blockHit.hitVec.yCoord;
posZ = blockHit.hitVec.zCoord;
doExplosion();
}
this.setDead();
}
handleBlockImpact(rayTraceBlocks(posX, posY, posZ, posX + motionX, posY + motionY, posZ + motionZ), true);
}
private void checkEntityCollision() {
@@ -381,23 +351,7 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
posX + halfWidth, posY + halfHeight, posZ + halfWidth
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
for (EntityLivingBase entity : entities) {
if (shouldIgnoreEntity(entity)) continue;
hasHit = true;
if (isExplosive()) {
doExplosion();
} else {
applyDamage(entity);
}
this.setDead();
return;
}
processEntitiesInHitBox(hitBox, posX, posY, posZ);
}
/**
@@ -17,12 +17,10 @@ import net.minecraft.world.World;
import noppes.npcs.EventHooks;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Laser projectile - fast expanding thin line that pierces through multiple targets.
* Laser projectile - fast expanding thin line.
* No homing, travels in a straight line from origin to max distance.
* <p>
* Design inspired by LouisXIV's energy attack system.
@@ -45,12 +43,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
// Lock vertical direction after firing (only update yaw, keep pitch fixed)
private boolean lockVerticalDirection = false;
// Whether to die on first entity impact (hit-scan mode)
private boolean dieOnImpact = false;
// Track hit entities to avoid double-damage
private Set<Integer> hitEntities = new HashSet<>();
// End point for rendering
private double endX, endY, endZ;
@@ -217,8 +209,8 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
public void startMoving(EntityLivingBase target) {
setCharging(false);
// Set initial direction from owner's rotation
Entity owner = getOwnerEntity();
// Set initial direction from owner's rotation
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
@@ -262,10 +254,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
}
private void checkBlockCollision() {
Vec3 start = Vec3.createVectorHelper(startX, startY, startZ);
Vec3 end = Vec3.createVectorHelper(endX, endY, endZ);
// Use full raytrace that doesn't stop at liquids and checks all blocks
MovingObjectPosition blockHit = worldObj.func_147447_a(start, end, false, true, false);
MovingObjectPosition blockHit = rayTraceBlocks(startX, startY, startZ, endX, endY, endZ);
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
if (!worldObj.isRemote) {
@@ -315,15 +304,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
for (EntityLivingBase entity : entities) {
if (shouldIgnoreEntity(entity)) continue;
if (hitEntities.contains(entity.getEntityId())) continue;
if (!canHitEntityNow(entity)) continue;
// Check if entity's bounding box intersects with the laser line
if (isEntityOnLine(entity)) {
hitEntities.add(entity.getEntityId());
applyDamage(entity);
if (dieOnImpact) {
// Stop at impact point and begin linger/fade
if (processEntityHit(entity, entity.posX, entity.posY + entity.height * 0.5, entity.posZ)) {
// Stop at impact point and terminate
double dx = entity.posX - startX;
double dy = (entity.posY + entity.height * 0.5) - startY;
double dz = entity.posZ - startZ;
@@ -332,11 +318,10 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
fullyExtended = true;
return; // Stop checking further entities
return;
}
// Piercing - don't stop, continue to next entity
// PIERCE/MULTI continue through remaining entities.
}
}
}
@@ -517,14 +502,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
this.lingerTicks = ticks;
}
public boolean isDieOnImpact() {
return dieOnImpact;
}
public void setDieOnImpact(boolean dieOnImpact) {
this.dieOnImpact = dieOnImpact;
}
public void setDirection(double x, double y, double z) {
this.dirX = x;
this.dirY = y;
@@ -593,7 +570,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
if (Float.isNaN(expansionSpeed) || Float.isInfinite(expansionSpeed) || expansionSpeed <= 0) expansionSpeed = 2.0f;
this.lingerTicks = nbt.hasKey("LingerTicks") ? nbt.getInteger("LingerTicks") : 10;
if (lingerTicks <= 0) lingerTicks = 1;
this.dieOnImpact = nbt.getBoolean("DieOnImpact");
this.lockVerticalDirection = nbt.getBoolean("LockVerticalDir");
this.ticksSinceFullExtension = nbt.getInteger("TicksSinceExtended");
this.dirX = nbt.getDouble("DirX");
@@ -613,7 +589,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
nbt.setFloat("LaserWidth", laserWidth);
nbt.setFloat("ExpansionSpeed", expansionSpeed);
nbt.setInteger("LingerTicks", lingerTicks);
nbt.setBoolean("DieOnImpact", dieOnImpact);
nbt.setBoolean("LockVerticalDir", lockVerticalDirection);
nbt.setInteger("TicksSinceExtended", ticksSinceFullExtension);
nbt.setDouble("DirX", dirX);
@@ -12,12 +12,8 @@ import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.EventHooks;
import java.util.List;
/**
* Orb projectile - spherical homing energy ball.
@@ -140,7 +136,6 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
// Calculate velocity toward target
Entity owner = getOwnerEntity();
if (target != null) {
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
@@ -185,15 +180,7 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
}
// Check wall collision
if (this.isCollidedHorizontally || this.isCollidedVertically) {
if (!worldObj.isRemote) {
hasHit = true;
if (isExplosive()) {
doExplosion();
}
}
this.setDead();
}
handleSolidCollisionTermination();
}
/**
@@ -233,24 +220,7 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
}
private void checkBlockCollision() {
Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ);
Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ);
// Use full raytrace that doesn't stop at liquids and checks all blocks
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
if (!worldObj.isRemote) {
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
}
hasHit = true;
if (isExplosive()) {
posX = blockHit.hitVec.xCoord;
posY = blockHit.hitVec.yCoord;
posZ = blockHit.hitVec.zCoord;
doExplosion();
}
this.setDead();
}
handleBlockImpact(rayTraceBlocks(posX, posY, posZ, posX + motionX, posY + motionY, posZ + motionZ), true);
}
private void checkEntityCollision() {
@@ -259,24 +229,7 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
posX - hitSize, posY - hitSize, posZ - hitSize,
posX + hitSize, posY + hitSize, posZ + hitSize
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
for (EntityLivingBase entity : entities) {
if (shouldIgnoreEntity(entity)) continue;
hasHit = true;
if (isExplosive()) {
doExplosion();
} else {
applyDamage(entity);
}
this.setDead();
return;
}
processEntitiesInHitBox(hitBox, posX, posY, posZ);
}
// ==================== GETTERS ====================
@@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.HitType;
import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData;
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
@@ -20,6 +21,7 @@ import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.EventHooks;
@@ -35,9 +37,11 @@ import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Base class for all ability projectiles (Orb, Disc, Beam, Laser, Slicer).
@@ -112,6 +116,8 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
// ==================== STATE ====================
protected boolean hasHit = false;
protected final Set<Integer> hitOnceEntities = new HashSet<Integer>();
protected final Map<Integer, Integer> lastHitTickByEntity = new HashMap<Integer, Integer>();
// ==================== CHARGING STATE (projectile-specific) ====================
protected float targetSize = 1.0f;
@@ -364,20 +370,6 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
@Override
public void setPositionAndRotation2(double x, double y, double z, float yaw, float pitch, int posRotationIncrements) {
// If position changed significantly (e.g. snap from anchor to look vector),
// skip interpolation and jump directly to prevent visual offset from crosshair
double dx = x - this.posX;
double dy = y - this.posY;
double dz = z - this.posZ;
if (dx * dx + dy * dy + dz * dz > 1.0) {
this.setPosition(x, y, z);
this.prevPosX = x;
this.prevPosY = y;
this.prevPosZ = z;
this.interpSteps = 0;
return;
}
this.interpTargetX = x;
this.interpTargetY = y;
this.interpTargetZ = z;
@@ -861,6 +853,142 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
combatData.setExplosionDamageFalloff(falloff);
}
public int getHitType() {
return combatData.hitType.ordinal();
}
public void setHitType(int hitType) {
combatData.hitType = HitType.fromOrdinal(hitType);
}
public int getMultiHitDelayTicks() {
return combatData.multiHitDelayTicks;
}
public void setMultiHitDelayTicks(int delayTicks) {
combatData.multiHitDelayTicks = Math.max(1, delayTicks);
}
protected boolean canHitEntityNow(EntityLivingBase entity) {
if (entity == null) return false;
int entityId = entity.getEntityId();
switch (combatData.hitType) {
case PIERCE:
return !hitOnceEntities.contains(entityId);
case MULTI:
Integer lastHitTick = lastHitTickByEntity.get(entityId);
if (lastHitTick == null) return true;
int delay = Math.max(1, combatData.multiHitDelayTicks);
return ticksExisted - lastHitTick >= delay;
case SINGLE:
default:
return !hasHit;
}
}
protected void recordEntityHit(EntityLivingBase entity) {
if (entity == null) return;
int entityId = entity.getEntityId();
hitOnceEntities.add(entityId);
lastHitTickByEntity.put(entityId, ticksExisted);
}
protected boolean shouldTerminateAfterHit() {
return combatData.hitType == HitType.SINGLE;
}
/**
* Shared block raytrace helper used by most projectile types.
*/
protected MovingObjectPosition rayTraceBlocks(double fromX, double fromY, double fromZ, double toX, double toY, double toZ) {
Vec3 currentPos = Vec3.createVectorHelper(fromX, fromY, fromZ);
Vec3 nextPos = Vec3.createVectorHelper(toX, toY, toZ);
return worldObj.func_147447_a(currentPos, nextPos, false, true, false);
}
/**
* Shared block-hit handling: fires block impact hook, optional explosion, and kills projectile.
*/
protected boolean handleBlockImpact(MovingObjectPosition blockHit, boolean explodeAtHitPoint) {
if (blockHit == null || blockHit.typeOfHit != MovingObjectPosition.MovingObjectType.BLOCK) return false;
if (!worldObj.isRemote) {
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
}
hasHit = true;
if (isExplosive()) {
if (explodeAtHitPoint && blockHit.hitVec != null) {
posX = blockHit.hitVec.xCoord;
posY = blockHit.hitVec.yCoord;
posZ = blockHit.hitVec.zCoord;
}
doExplosion();
}
this.setDead();
return true;
}
/**
* Apply projectile hit behavior to a target and return true if the projectile terminated.
*/
protected boolean processEntityHit(EntityLivingBase entity, double impactX, double impactY, double impactZ) {
if (entity == null || shouldIgnoreEntity(entity) || !canHitEntityNow(entity)) return false;
recordEntityHit(entity);
if (isExplosive()) {
double oldX = posX;
double oldY = posY;
double oldZ = posZ;
posX = impactX;
posY = impactY;
posZ = impactZ;
doExplosion();
posX = oldX;
posY = oldY;
posZ = oldZ;
} else {
applyDamage(entity);
}
if (shouldTerminateAfterHit()) {
hasHit = true;
this.setDead();
return true;
}
return false;
}
/**
* Scan entities in a hitbox and process the first/each valid hit based on hit mode.
*/
protected boolean processEntitiesInHitBox(AxisAlignedBB hitBox, double impactX, double impactY, double impactZ) {
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
for (EntityLivingBase entity : entities) {
if (processEntityHit(entity, impactX, impactY, impactZ)) {
return true;
}
}
return false;
}
protected boolean handleSolidCollisionTermination() {
if (!this.isCollidedHorizontally && !this.isCollidedVertically) return false;
if (!worldObj.isRemote) {
hasHit = true;
if (isExplosive()) {
doExplosion();
}
}
this.setDead();
return true;
}
// ==================== HOMING GETTERS & SETTERS ====================
public boolean isHoming() {
@@ -11,25 +11,18 @@ import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.MovingObjectPosition;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.EventHooks;
import java.util.ArrayList;
import java.util.List;
/**
* Energy Slicer entity - a thin, wide blade projectile that flies in a straight line.
* JJK Cleave/Dismantle inspired. Piercing by default (passes through entities).
* JJK Cleave/Dismantle inspired.
* Extends EntityEnergyAbility to leverage the existing projectile infrastructure.
*/
public class EntityEnergySlicer extends EntityEnergyProjectile {
private float sliceWidth = 3.0f;
private float sliceThickness = 0.15f;
private boolean piercing = true;
private List<Integer> hitEntities = new ArrayList<>();
public EntityEnergySlicer(World world) {
super(world);
@@ -43,15 +36,13 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
float sliceWidth, float sliceThickness,
EnergyDisplayData display, EnergyCombatData combat,
EnergyHomingData homing, EnergyLightningData lightning,
EnergyLifespanData lifespan, EnergyTrajectoryData trajectory,
boolean piercing) {
EnergyLifespanData lifespan, EnergyTrajectoryData trajectory) {
super(world);
initProjectile(owner, target, x, y, z, sliceWidth, display, combat, lightning, lifespan, trajectory);
this.homingData = homing;
this.sliceWidth = sliceWidth;
this.sliceThickness = sliceThickness;
this.piercing = piercing;
calculateInitialVelocity(owner, target, x, y, z);
}
@@ -196,15 +187,7 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
}
// Wall collision
if (this.isCollidedHorizontally || this.isCollidedVertically) {
if (!worldObj.isRemote) {
hasHit = true;
if (isExplosive()) {
doExplosion();
}
}
this.setDead();
}
handleSolidCollisionTermination();
}
@Override
@@ -223,19 +206,7 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
}
private void checkBlockCollision() {
if (piercing) return; // Piercing slicers pass through blocks
Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ);
Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ);
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
if (!worldObj.isRemote) {
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
}
hasHit = true;
this.setDead();
}
handleBlockImpact(rayTraceBlocks(posX, posY, posZ, posX + motionX, posY + motionY, posZ + motionZ), false);
}
private void checkEntityCollision() {
@@ -246,29 +217,7 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
posX + halfW, posY + halfH, posZ + halfW
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
for (EntityLivingBase entity : entities) {
if (shouldIgnoreEntity(entity)) continue;
if (hitEntities.contains(entity.getEntityId())) continue; // Already hit this entity
if (piercing) {
// Pierce: damage but keep flying
applyDamage(entity);
hitEntities.add(entity.getEntityId());
} else {
// Non-piercing: damage and die
hasHit = true;
if (isExplosive()) {
doExplosion();
} else {
applyDamage(entity);
}
this.setDead();
return;
}
}
processEntitiesInHitBox(hitBox, posX, posY, posZ);
}
/**
@@ -315,14 +264,6 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
this.sliceThickness = thickness;
}
public boolean isPiercing() {
return piercing;
}
public void setPiercing(boolean piercing) {
this.piercing = piercing;
}
// ==================== NBT ====================
@Override
@@ -330,7 +271,6 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
readChargingNBT(nbt);
this.sliceWidth = sanitize(nbt.hasKey("SliceWidth") ? nbt.getFloat("SliceWidth") : 3.0f, 3.0f, MAX_ENTITY_SIZE);
this.sliceThickness = sanitize(nbt.hasKey("SliceThickness") ? nbt.getFloat("SliceThickness") : 0.15f, 0.15f, MAX_ENTITY_SIZE);
this.piercing = !nbt.hasKey("Piercing") || nbt.getBoolean("Piercing");
this.targetSliceWidth = sanitize(nbt.hasKey("TargetSliceWidth") ? nbt.getFloat("TargetSliceWidth") : sliceWidth, sliceWidth, MAX_ENTITY_SIZE);
this.targetSliceThickness = sanitize(nbt.hasKey("TargetSliceThickness") ? nbt.getFloat("TargetSliceThickness") : sliceThickness, sliceThickness, MAX_ENTITY_SIZE);
}
@@ -340,7 +280,6 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
writeChargingNBT(nbt);
nbt.setFloat("SliceWidth", sliceWidth);
nbt.setFloat("SliceThickness", sliceThickness);
nbt.setBoolean("Piercing", piercing);
nbt.setFloat("TargetSliceWidth", targetSliceWidth);
nbt.setFloat("TargetSliceThickness", targetSliceThickness);
}
@@ -161,6 +161,22 @@ public class ScriptEnergyProjectile<T extends EntityEnergyProjectile> extends Sc
entity.setExplosionDamageFalloff(falloff);
}
public int getHitType() {
return entity.getHitType();
}
public void setHitType(int hitType) {
entity.setHitType(hitType);
}
public int getMultiHitDelayTicks() {
return entity.getMultiHitDelayTicks();
}
public void setMultiHitDelayTicks(int delayTicks) {
entity.setMultiHitDelayTicks(delayTicks);
}
// ==================== MOVEMENT ====================
public float getSpeed() {
@@ -45,14 +45,6 @@ public class ScriptEnergySlicer<T extends EntityEnergySlicer> extends ScriptEner
entity.setSliceThickness(thickness);
}
public boolean isPiercing() {
return entity.isPiercing();
}
public void setPiercing(boolean piercing) {
entity.setPiercing(piercing);
}
// ==================== FIRE ====================
@Override