From b6749e306b331d53daddaf971181787617a5a85d Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 21 Feb 2026 20:39:16 -0500 Subject: [PATCH] Add Hit Type to Projectiles --- src/api | 2 +- .../controllers/data/ability/HitType.java | 32 ++++ .../data/ability/data/EnergyCombatData.java | 13 +- .../type/energy/AbilityEnergyProjectile.java | 26 +++ .../ability/type/energy/AbilityLaserShot.java | 17 +- .../ability/type/energy/AbilitySlicer.java | 18 +- .../npcs/entity/EntityAbilityBeam.java | 43 +---- .../npcs/entity/EntityAbilityDisc.java | 52 +----- .../npcs/entity/EntityAbilityLaser.java | 41 +---- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 53 +----- .../npcs/entity/EntityEnergyProjectile.java | 156 ++++++++++++++++-- .../npcs/entity/EntityEnergySlicer.java | 71 +------- .../entity/ScriptEnergyProjectile.java | 16 ++ .../scripted/entity/ScriptEnergySlicer.java | 8 - .../assets/customnpcs/lang/en_US.lang | 9 +- 15 files changed, 265 insertions(+), 292 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/HitType.java diff --git a/src/api b/src/api index a9c77d20e..0d31b56a1 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit a9c77d20eb83be6a3168480c0d62fa78419e9469 +Subproject commit 0d31b56a12dd18f611505047402d2251a6dd0906 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/HitType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/HitType.java new file mode 100644 index 000000000..3892e9362 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/HitType.java @@ -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(); + } + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java index ad3d603e5..e6026e18e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java @@ -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; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java index 50b68554e..40ea82080 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -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 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 @SideOnly(Side.CLIENT) @Override public final void getAbilityDefinitions(List 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); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java index 0ffa52946..30e99fedb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java @@ -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 implements IAbilityLaserShot { @@ -34,7 +34,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile { 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 { 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 { 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 { 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 { 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) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index b80926aab..44589a842 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -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 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; } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index d16fc7709..b5daa8701 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -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 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); } /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index f3a899181..ec851f802 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -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. *

* 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 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); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 82447edd2..efb1db6a9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -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 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 ==================== diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index 4a33e2489..9dfa105d6 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -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 hitOnceEntities = new HashSet(); + protected final Map lastHitTickByEntity = new HashMap(); // ==================== 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 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() { diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java index 29d0ac39a..1c2a868a8 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergySlicer.java @@ -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 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 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); } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java index 38a95db0c..de9c8036e 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java @@ -161,6 +161,22 @@ public class ScriptEnergyProjectile 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() { diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java index f0105cdd9..f120350c4 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergySlicer.java @@ -45,14 +45,6 @@ public class ScriptEnergySlicer extends ScriptEner entity.setSliceThickness(thickness); } - public boolean isPiercing() { - return entity.isPiercing(); - } - - public void setPiercing(boolean piercing) { - entity.setPiercing(piercing); - } - // ==================== FIRE ==================== @Override diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index dbe0442cd..0a7b1ecdc 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1783,6 +1783,11 @@ gui.applyPreset=Apply Preset ability.radius=Radius ability.knockback=Knockback ability.knockbackUp=Knockback Up +ability.hitType=Hit Type +ability.hitType.single=Single +ability.hitType.pierce=Pierce +ability.hitType.multi=Multi +ability.multiHitDelay=Multi-Hit Delay ability.distance=Distance ability.maxDistance=Max Distance ability.lifetime=Lifetime @@ -1985,6 +1990,8 @@ ability.hover.windUpColor=Color of telegraph during charge-up phase ability.hover.activeColor=Color of telegraph when ability is active ability.hover.homing=Projectile tracks toward target ability.hover.homingStr=How aggressively the projectile tracks (higher = tighter turns) +ability.hover.hitType=Single: stops on first hit. Pierce: can hit each entity once. Multi: can re-hit entities using the delay below. +ability.hover.multiHitDelay=Ticks between repeat hits on the same entity when Hit Type is Multi (20 ticks = 1 second) ability.hover.explosive=Creates explosion on impact ability.hover.explosionRad=Size of explosion radius ability.hover.piercing=Hits multiple targets instead of stopping on first hit @@ -2064,8 +2071,6 @@ ability.validFor=Valid For ability.userType.NPC_ONLY=NPCs ability.userType.PLAYER_ONLY=Players ability.userType.BOTH=Both -ability.dieOnImpact=Die on Impact - ability.icon.section.uv=UV Coordinates ability.icon.x=Icon X ability.icon.y=Icon Y