diff --git a/src/api b/src/api index db7173c4e..788cd0826 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit db7173c4ec6b01ffb4ab23fad130164c5815f7dc +Subproject commit 788cd08264f226953571b0aa737b98a4e2d6d19c diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java index 1c8418b96..88103197f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java @@ -10,6 +10,8 @@ import noppes.npcs.api.ability.data.IEnergyCombatData; */ public class EnergyCombatData implements IEnergyCombatData { public static final float MAX_EXPLOSION_RADIUS = 15.0f; + public static final int DEFAULT_MAX_HITS = 5; + public static final int MAX_HITS = 200; public float damage = 7.0f; public float knockback = 1.0f; @@ -19,6 +21,7 @@ public class EnergyCombatData implements IEnergyCombatData { public float explosionDamageFalloff = 0.5f; public HitType hitType = HitType.SINGLE; public int multiHitDelayTicks = 5; + public int maxHits = DEFAULT_MAX_HITS; public EnergyCombatData() { } @@ -26,6 +29,12 @@ public class EnergyCombatData implements IEnergyCombatData { public EnergyCombatData(float damage, float knockback, float knockbackUp, boolean explosive, float explosionRadius, float explosionDamageFalloff, HitType hitType, int multiHitDelay) { + this(damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, hitType, multiHitDelay, DEFAULT_MAX_HITS); + } + + public EnergyCombatData(float damage, float knockback, float knockbackUp, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + HitType hitType, int multiHitDelay, int maxHits) { this.damage = damage; this.knockback = knockback; this.knockbackUp = knockbackUp; @@ -34,6 +43,7 @@ public class EnergyCombatData implements IEnergyCombatData { this.explosionDamageFalloff = explosionDamageFalloff; this.hitType = hitType; this.multiHitDelayTicks = multiHitDelay; + this.maxHits = clampMaxHits(maxHits); } public EnergyCombatData(float damage, float knockback, float knockbackUp, @@ -106,6 +116,14 @@ public class EnergyCombatData implements IEnergyCombatData { this.explosionDamageFalloff = explosionDamageFalloff; } + public int getMaxHits() { + return maxHits; + } + + public void setMaxHits(int maxHits) { + this.maxHits = clampMaxHits(maxHits); + } + public void writeNBT(NBTTagCompound nbt) { nbt.setFloat("damage", damage); nbt.setFloat("knockback", knockback); @@ -115,6 +133,7 @@ public class EnergyCombatData implements IEnergyCombatData { nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); nbt.setInteger("hitType", hitType.ordinal()); nbt.setInteger("multiHitDelayTicks", multiHitDelayTicks); + nbt.setInteger("maxHits", maxHits); } public void readNBT(NBTTagCompound nbt) { @@ -126,6 +145,7 @@ public class EnergyCombatData implements IEnergyCombatData { 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; + maxHits = nbt.hasKey("maxHits") ? nbt.getInteger("maxHits") : DEFAULT_MAX_HITS; // Sanitize if (Float.isNaN(damage) || Float.isInfinite(damage)) damage = 7.0f; @@ -133,6 +153,7 @@ public class EnergyCombatData implements IEnergyCombatData { if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius) || explosionRadius < 0) explosionRadius = 3.0f; explosionRadius = clampExplosionRadius(explosionRadius); if (multiHitDelayTicks < 1) multiHitDelayTicks = 1; + maxHits = clampMaxHits(maxHits); } private static float clampExplosionRadius(float explosionRadius) { @@ -140,11 +161,16 @@ public class EnergyCombatData implements IEnergyCombatData { return Math.max(0.0f, Math.min(MAX_EXPLOSION_RADIUS, explosionRadius)); } + private static int clampMaxHits(int maxHits) { + return Math.max(1, Math.min(MAX_HITS, maxHits)); + } + public EnergyCombatData copy() { EnergyCombatData copy = new EnergyCombatData(damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff); copy.hitType = hitType; copy.multiHitDelayTicks = multiHitDelayTicks; + copy.maxHits = maxHits; 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 4318c88eb..d65c6e7e6 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 @@ -685,6 +685,14 @@ public abstract class AbilityEnergyProjectile this.combatData.multiHitDelayTicks = Math.max(1, delay); } + public int getMaxHits() { + return combatData.getMaxHits(); + } + + public void setMaxHits(int maxHits) { + combatData.setMaxHits(maxHits); + } + // Lifespan data @Override public float getMaxDistance() { @@ -832,6 +840,10 @@ public abstract class AbilityEnergyProjectile .range(1, 200) .visibleWhen(() -> getHitType() == HitType.MULTI.ordinal()) .hover("ability.hover.multiHitDelay")); + defs.add(FieldDef.intField("ability.maxHits", this::getMaxHits, this::setMaxHits) + .range(1, EnergyCombatData.MAX_HITS) + .visibleWhen(() -> getHitType() != HitType.SINGLE.ordinal()) + .hover("ability.hover.maxHits")); // Type-specific fields first addTypeDefinitions(defs); diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index aab79a444..26d3b9d08 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -124,6 +124,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { // ==================== STATE ==================== protected boolean hasHit = false; + protected int hitCount = 0; protected final Set hitOnceEntities = new HashSet(); protected final Map lastHitTickByEntity = new HashMap(); protected static final int BARRIER_IMPACT_PAUSE_TICKS = 10; @@ -508,6 +509,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { hitOnceEntities.clear(); lastHitTickByEntity.clear(); + hitCount = 0; return true; } @@ -943,13 +945,12 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { // 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)); + Vec3 closest = closestPointOnBoundingBox(target.boundingBox, posX, posY, posZ); + if (closest == null) continue; double dist = Math.sqrt( - (closestX - posX) * (closestX - posX) + - (closestY - posY) * (closestY - posY) + - (closestZ - posZ) * (closestZ - posZ) + (closest.xCoord - posX) * (closest.xCoord - posX) + + (closest.yCoord - posY) * (closest.yCoord - posY) + + (closest.zCoord - posZ) * (closest.zCoord - posZ) ); if (dist <= explosionRad) { @@ -970,6 +971,17 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { return shouldIgnoreEntity(target); } + /** + * Clamp a point to the nearest point inside the provided AABB. + */ + protected Vec3 closestPointOnBoundingBox(AxisAlignedBB box, double x, double y, double z) { + if (box == null) return null; + double closestX = Math.max(box.minX, Math.min(x, box.maxX)); + double closestY = Math.max(box.minY, Math.min(y, box.maxY)); + double closestZ = Math.max(box.minZ, Math.min(z, box.maxZ)); + return Vec3.createVectorHelper(closestX, closestY, closestZ); + } + /** * Spawn a packet-driven client preview so explosions are visible as geometry, * not only as particles. @@ -1861,8 +1873,17 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { combatData.multiHitDelayTicks = Math.max(1, delayTicks); } + public int getMaxHits() { + return combatData.getMaxHits(); + } + + public void setMaxHits(int maxHits) { + combatData.setMaxHits(maxHits); + } + protected boolean canHitEntityNow(EntityLivingBase entity) { if (entity == null) return false; + if (combatData.hitType != HitType.SINGLE && hasReachedMaxHits()) return false; int entityId = entity.getEntityId(); switch (combatData.hitType) { @@ -1884,10 +1905,17 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { int entityId = entity.getEntityId(); hitOnceEntities.add(entityId); lastHitTickByEntity.put(entityId, ticksExisted); + hitCount++; + } + + protected boolean hasReachedMaxHits() { + if (combatData.hitType == HitType.SINGLE) return false; + return hitCount >= combatData.getMaxHits(); } protected boolean shouldTerminateAfterHit() { - return combatData.hitType == HitType.SINGLE; + if (combatData.hitType == HitType.SINGLE) return true; + return hasReachedMaxHits(); } /** @@ -1931,12 +1959,13 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { recordEntityHit(entity); if (isExplosive()) { + Vec3 impactPoint = resolveEntityImpactPoint(entity, impactX, impactY, impactZ); double oldX = posX; double oldY = posY; double oldZ = posZ; - posX = impactX; - posY = impactY; - posZ = impactZ; + posX = impactPoint.xCoord; + posY = impactPoint.yCoord; + posZ = impactPoint.zCoord; doExplosion(); posX = oldX; posY = oldY; @@ -1954,6 +1983,26 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { return false; } + /** + * Resolve a stable impact point on the target so explosive collisions use actual contact. + */ + protected Vec3 resolveEntityImpactPoint(EntityLivingBase entity, double impactX, double impactY, double impactZ) { + if (entity == null || entity.boundingBox == null) { + return Vec3.createVectorHelper(impactX, impactY, impactZ); + } + + Vec3 segmentStart = Vec3.createVectorHelper(posX, posY, posZ); + Vec3 segmentEnd = Vec3.createVectorHelper(impactX, impactY, impactZ); + AxisAlignedBB expandedBox = entity.boundingBox.expand(1.0e-4, 1.0e-4, 1.0e-4); + MovingObjectPosition intercept = expandedBox.calculateIntercept(segmentStart, segmentEnd); + if (intercept != null && intercept.hitVec != null) { + return intercept.hitVec; + } + + Vec3 closest = closestPointOnBoundingBox(entity.boundingBox, impactX, impactY, impactZ); + return closest != null ? closest : Vec3.createVectorHelper(impactX, impactY, impactZ); + } + /** * Scan entities in a hitbox and process the first/each valid hit based on hit mode. */ diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java index de9c8036e..db810848e 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java @@ -177,6 +177,14 @@ public class ScriptEnergyProjectile extends Sc entity.setMultiHitDelayTicks(delayTicks); } + public int getMaxHits() { + return entity.getMaxHits(); + } + + public void setMaxHits(int maxHits) { + entity.setMaxHits(maxHits); + } + // ==================== MOVEMENT ==================== public float getSpeed() { diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 3f8053a06..a4c8103dc 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1819,6 +1819,7 @@ ability.hitType.single=Single ability.hitType.pierce=Pierce ability.hitType.multi=Multi ability.multiHitDelay=Multi-Hit Delay +ability.maxHits=Max Hits ability.distance=Distance ability.maxDistance=Max Distance ability.lifetime=Lifetime @@ -2027,8 +2028,9 @@ 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.hitType=Single: stops on first hit. Pierce: can hit each entity once. Multi: can re-hit entities using the delay below. Pierce/Multi both respect Max Hits. ability.hover.multiHitDelay=Ticks between repeat hits on the same entity when Hit Type is Multi (20 ticks = 1 second) +ability.hover.maxHits=Maximum total hits before projectile ends in Pierce/Multi modes. Lower values reduce repeated hit spam and improve performance. 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