From adf9cd7012f11f23388974717fb3dc847eac1339 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 23 Feb 2026 00:17:34 -0500 Subject: [PATCH] Add Barrier Reflection! --- .../client/renderer/RenderEnergyAbility.java | 13 + .../data/energy/EnergyBarrierData.java | 26 ++ .../ability/data/energy/EnergyCombatData.java | 14 +- .../ability/type/energy/AbilityBarrier.java | 21 + .../data/ability/type/energy/AbilityBeam.java | 4 +- .../data/ability/type/energy/AbilityDisc.java | 4 +- .../type/energy/AbilityEnergyProjectile.java | 4 +- .../ability/type/energy/AbilityLaserShot.java | 4 +- .../data/ability/type/energy/AbilityOrb.java | 4 +- .../ability/type/energy/AbilitySlicer.java | 4 +- .../npcs/entity/EntityAbilityBarrier.java | 111 ++++- .../npcs/entity/EntityAbilityBeam.java | 26 +- .../npcs/entity/EntityAbilityDisc.java | 40 +- .../npcs/entity/EntityAbilityLaser.java | 141 ++++++- .../npcs/entity/EntityEnergyProjectile.java | 395 +++++++++++++++++- .../npcs/client/ClientEventHandler.java | 8 +- .../hud/ability/AbilityHotbarComponent.java | 15 +- .../scripted/entity/ScriptEnergyBarrier.java | 16 + .../assets/customnpcs/lang/en_US.lang | 3 + 19 files changed, 792 insertions(+), 61 deletions(-) diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java index ac88c47e8..e87e24d51 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java @@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; import kamkeel.npcs.entity.EntityEnergyAbility; +import kamkeel.npcs.entity.EntityEnergyProjectile; import net.minecraft.client.renderer.OpenGlHelper; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.entity.Render; @@ -296,6 +297,18 @@ public abstract class RenderEnergyAbility extends Render { int innerColor = entity.getInnerColor(); int fadeTime = entity.getLightningFadeTime(); + // Barrier impact spark: briefly force brighter white crackles at the contact point. + if (entity instanceof EntityEnergyProjectile) { + int sparkTicks = ((EntityEnergyProjectile) entity).getBarrierSparkTicks(); + if (sparkTicks > 0) { + density = Math.max(density, 4.0f + sparkTicks * 0.6f); + radius = Math.max(radius, innerRadius + Math.max(0.35f, baseSize * 0.65f)); + outerColor = 0xF0F6FF; + innerColor = 0xFFFFFF; + fadeTime = Math.max(fadeTime, 6); + } + } + state.update(density, radius, outerColor, innerColor, fadeTime); state.render(); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java index 130b7d2ba..1217c85d5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java @@ -37,6 +37,10 @@ public class EnergyBarrierData { public boolean meleeEnabled = false; public float meleeDamageMultiplier = 1.0f; + // Reflection: rebound projectiles that fail to break the barrier + public boolean reflect = false; + public float reflectStrengthPct = 25.0f; + /** * Per-projectile-type damage multipliers. * Key is the ability typeId (e.g., "ability.cnpc.orb", "ability.cnpc.beam"). @@ -119,6 +123,22 @@ public class EnergyBarrierData { this.defaultMultiplier = defaultMultiplier; } + public boolean isReflect() { + return reflect; + } + + public void setReflect(boolean reflect) { + this.reflect = reflect; + } + + public float getReflectStrengthPct() { + return reflectStrengthPct; + } + + public void setReflectStrengthPct(float reflectStrengthPct) { + this.reflectStrengthPct = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct)); + } + // ==================== NBT ==================== public void writeNBT(NBTTagCompound nbt) { @@ -133,6 +153,8 @@ public class EnergyBarrierData { nbt.setBoolean("barrierAbsorbing", absorbing); nbt.setBoolean("barrierMelee", meleeEnabled); nbt.setFloat("barrierMeleeMult", meleeDamageMultiplier); + nbt.setBoolean("barrierReflect", reflect); + nbt.setFloat("barrierReflectStrPct", reflectStrengthPct); NBTTagCompound multNbt = new NBTTagCompound(); for (Map.Entry entry : damageMultipliers.entrySet()) { @@ -153,6 +175,8 @@ public class EnergyBarrierData { absorbing = nbt.hasKey("barrierAbsorbing") && nbt.getBoolean("barrierAbsorbing"); meleeEnabled = nbt.hasKey("barrierMelee") && nbt.getBoolean("barrierMelee"); meleeDamageMultiplier = nbt.hasKey("barrierMeleeMult") ? nbt.getFloat("barrierMeleeMult") : 1.0f; + reflect = nbt.hasKey("barrierReflect") && nbt.getBoolean("barrierReflect"); + setReflectStrengthPct(nbt.hasKey("barrierReflectStrPct") ? nbt.getFloat("barrierReflectStrPct") : 25.0f); damageMultipliers.clear(); if (nbt.hasKey("barrierMultipliers")) { @@ -174,6 +198,8 @@ public class EnergyBarrierData { copy.absorbing = absorbing; copy.meleeEnabled = meleeEnabled; copy.meleeDamageMultiplier = meleeDamageMultiplier; + copy.reflect = reflect; + copy.reflectStrengthPct = reflectStrengthPct; copy.damageMultipliers = new HashMap<>(damageMultipliers); return copy; } 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 0825fcfef..1c8418b96 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 @@ -9,6 +9,8 @@ import noppes.npcs.api.ability.data.IEnergyCombatData; * Used as a parameter object for entity constructors and ability configuration. */ public class EnergyCombatData implements IEnergyCombatData { + public static final float MAX_EXPLOSION_RADIUS = 15.0f; + public float damage = 7.0f; public float knockback = 1.0f; public float knockbackUp = 0.1f; @@ -28,7 +30,7 @@ public class EnergyCombatData implements IEnergyCombatData { this.knockback = knockback; this.knockbackUp = knockbackUp; this.explosive = explosive; - this.explosionRadius = explosionRadius; + this.explosionRadius = clampExplosionRadius(explosionRadius); this.explosionDamageFalloff = explosionDamageFalloff; this.hitType = hitType; this.multiHitDelayTicks = multiHitDelay; @@ -40,7 +42,7 @@ public class EnergyCombatData implements IEnergyCombatData { this.knockback = knockback; this.knockbackUp = knockbackUp; this.explosive = explosive; - this.explosionRadius = explosionRadius; + this.explosionRadius = clampExplosionRadius(explosionRadius); this.explosionDamageFalloff = explosionDamageFalloff; } @@ -91,7 +93,7 @@ public class EnergyCombatData implements IEnergyCombatData { @Override public void setExplosionRadius(float explosionRadius) { - this.explosionRadius = Math.max(0, explosionRadius); + this.explosionRadius = clampExplosionRadius(explosionRadius); } @Override @@ -129,9 +131,15 @@ public class EnergyCombatData implements IEnergyCombatData { 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; + explosionRadius = clampExplosionRadius(explosionRadius); if (multiHitDelayTicks < 1) multiHitDelayTicks = 1; } + private static float clampExplosionRadius(float explosionRadius) { + if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius)) return 0.0f; + return Math.max(0.0f, Math.min(MAX_EXPLOSION_RADIUS, explosionRadius)); + } + public EnergyCombatData copy() { EnergyCombatData copy = new EnergyCombatData(damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java index 21a2a4390..d1054e858 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java @@ -254,6 +254,23 @@ public abstract class AbilityBarrier extends AbilityEnergy { barrierData.absorbing = absorbing; } + // Reflection data + public boolean isReflect() { + return barrierData.reflect; + } + + public void setReflect(boolean reflect) { + barrierData.reflect = reflect; + } + + public float getReflectStrengthPct() { + return barrierData.reflectStrengthPct; + } + + public void setReflectStrengthPct(float strengthPct) { + barrierData.setReflectStrengthPct(strengthPct); + } + // Melee data public boolean isMeleeEnabled() { return barrierData.meleeEnabled; @@ -299,6 +316,10 @@ public abstract class AbilityBarrier extends AbilityEnergy { .range(0, 10).visibleWhen(this::isKnockbackEnabled)); defs.add(FieldDef.boolField("ability.absorbing", this::isAbsorbing, this::setAbsorbing) .hover("ability.hover.absorbing")); + defs.add(FieldDef.boolField("ability.reflect", this::isReflect, this::setReflect) + .hover("ability.hover.reflect")); + defs.add(FieldDef.floatField("ability.reflectStrength", this::getReflectStrengthPct, this::setReflectStrengthPct) + .range(0, 100).visibleWhen(this::isReflect)); defs.add(FieldDef.boolField("ability.meleeEnabled", this::isMeleeEnabled, this::setMeleeEnabled) .hover("ability.hover.meleeEnabled")); defs.add(FieldDef.floatField("ability.meleeDamageMultiplier", this::getMeleeDamageMultiplier, this::setMeleeDamageMultiplier) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java index b8cf779be..6e1305ed8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java @@ -200,7 +200,9 @@ public class AbilityBeam extends AbilityEnergyProjectile impl )); 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)); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS) + .visibleWhen(this::isExplosive)); defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java index d8cb38e4f..f264f2802 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java @@ -226,7 +226,9 @@ public class AbilityDisc extends AbilityEnergyProjectile impl defs.add(FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay).visibleWhen(this::isBoomerang)); 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)); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS) + .visibleWhen(this::isExplosive)); defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); } } 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 92e53f10b..4318c88eb 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 @@ -651,12 +651,12 @@ public abstract class AbilityEnergyProjectile @Override public float getExplosionRadius() { - return combatData.explosionRadius; + return combatData.getExplosionRadius(); } @Override public void setExplosionRadius(float explosionRadius) { - combatData.explosionRadius = explosionRadius; + combatData.setExplosionRadius(explosionRadius); } @Override 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 522890d47..49efb5ad1 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 @@ -213,7 +213,9 @@ public class AbilityLaserShot extends AbilityEnergyProjectile implem defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming)); 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)); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS) + .visibleWhen(this::isExplosive)); defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java index ca9cbc7eb..c38131e9f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java @@ -180,7 +180,9 @@ public class AbilitySlicer extends AbilityEnergyProjectile { )); 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)); + defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS) + .visibleWhen(this::isExplosive)); defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)); } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java index f1309bb97..6c5fcc76c 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java @@ -24,6 +24,55 @@ import java.util.List; */ public abstract class EntityAbilityBarrier extends EntityEnergyAbility { + /** + * Outcome of a projectile vs barrier interaction. + */ + public enum ProjectileHitResult { + PASS, + BLOCKED, + BROKEN; + + public boolean isAbsorbed() { + return this == BLOCKED || this == BROKEN; + } + } + + /** + * Full result of a projectile vs barrier hit after multiplier/event processing. + */ + public static class ProjectileHitOutcome { + public final ProjectileHitResult result; + public final float barrierHealthBefore; + public final float barrierHealthAfter; + public final float appliedBarrierDamage; + public final float remainingProjectileDamage; + public final boolean reflectEnabled; + public final float reflectStrengthPct; + public final boolean useHealth; + + public ProjectileHitOutcome(ProjectileHitResult result, + float barrierHealthBefore, + float barrierHealthAfter, + float appliedBarrierDamage, + float remainingProjectileDamage, + boolean reflectEnabled, + float reflectStrengthPct, + boolean useHealth) { + this.result = result; + this.barrierHealthBefore = barrierHealthBefore; + this.barrierHealthAfter = barrierHealthAfter; + this.appliedBarrierDamage = appliedBarrierDamage; + this.remainingProjectileDamage = remainingProjectileDamage; + this.reflectEnabled = reflectEnabled; + this.reflectStrengthPct = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct)); + this.useHealth = useHealth; + } + + public boolean shouldReflect() { + return reflectEnabled && useHealth && result == ProjectileHitResult.BLOCKED; + } + } + // ==================== ACTIVE BARRIER REGISTRY ==================== private static final List> activeBarriers = new ArrayList<>(); private boolean tracked = false; @@ -164,6 +213,20 @@ public abstract class EntityAbilityBarrier extends EntityEnergyAbility { * Returns true if the barrier absorbed the hit (projectile should be destroyed). */ public boolean onProjectileHit(EntityEnergyProjectile projectile, float baseDamage) { + return onProjectileHitDetailed(projectile, baseDamage).isAbsorbed(); + } + + /** + * Apply damage to this barrier from a projectile and return detailed outcome. + */ + public ProjectileHitResult onProjectileHitDetailed(EntityEnergyProjectile projectile, float baseDamage) { + return onProjectileHitResolved(projectile, baseDamage).result; + } + + /** + * Apply damage to this barrier from a projectile and return full hit context. + */ + public ProjectileHitOutcome onProjectileHitResolved(EntityEnergyProjectile projectile, float baseDamage) { // Get damage multiplier for this projectile type String typeId = ""; if (projectile.getSourceAbility() != null) { @@ -175,25 +238,67 @@ public abstract class EntityAbilityBarrier extends EntityEnergyAbility { // Fire hit event (may cancel or modify damage) if (!worldObj.isRemote) { float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, projectile, damage); - if (eventDamage < 0) return false; // Event cancelled — don't block + if (eventDamage < 0) { + return new ProjectileHitOutcome( + ProjectileHitResult.PASS, + currentHealth, + currentHealth, + 0.0f, + 0.0f, + barrierData.reflect, + barrierData.reflectStrengthPct, + barrierData.useHealth + ); + } damage = eventDamage; } triggerHitFlash(); + float healthBefore = currentHealth; + if (!barrierData.useHealth) { - return true; // Duration-only mode: block but don't take damage + return new ProjectileHitOutcome( + ProjectileHitResult.BLOCKED, + healthBefore, + healthBefore, + 0.0f, + 0.0f, + false, + barrierData.reflectStrengthPct, + false + ); } currentHealth -= damage; syncHealthPercent(); if (currentHealth <= 0) { + float remaining = Math.max(0.0f, damage - healthBefore); onBarrierDestroyed(); this.setDead(); + return new ProjectileHitOutcome( + ProjectileHitResult.BROKEN, + healthBefore, + 0.0f, + damage, + remaining, + barrierData.reflect, + barrierData.reflectStrengthPct, + true + ); } - return true; + return new ProjectileHitOutcome( + ProjectileHitResult.BLOCKED, + healthBefore, + currentHealth, + damage, + 0.0f, + barrierData.reflect, + barrierData.reflectStrengthPct, + true + ); } // ==================== MELEE DAMAGE ==================== diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 979ee4b55..1822e0c99 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -515,22 +515,22 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { } private void checkEntityCollision(double headX, double headY, double headZ) { - double hitSize = headSize * 0.5; + // Swept head hitbox prevents misses when beam head moves quickly between ticks. + double prevHeadWorldX = startX + prevHeadOffsetX; + double prevHeadWorldY = startY + prevHeadOffsetY; + double prevHeadWorldZ = startZ + prevHeadOffsetZ; + double hitSize = Math.max(0.05, headSize * 0.5); AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( - headX - hitSize, headY - hitSize, headZ - hitSize, - headX + hitSize, headY + hitSize, headZ + hitSize + Math.min(prevHeadWorldX, headX) - hitSize, + Math.min(prevHeadWorldY, headY) - hitSize, + Math.min(prevHeadWorldZ, headZ) - hitSize, + Math.max(prevHeadWorldX, headX) + hitSize, + Math.max(prevHeadWorldY, headY) + hitSize, + Math.max(prevHeadWorldZ, headZ) + hitSize ); - @SuppressWarnings("unchecked") - List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); - - for (EntityLivingBase entity : entities) { - if (processEntityHit(entity, headX, headY, headZ)) { - if (DEBUG_LOGGING) { - LogWriter.info("[Beam] DEAD: Entity collision with " + entity.getClass().getSimpleName() + " at tick " + ticksExisted); - } - return; - } + if (processEntitiesInHitBox(hitBox, headX, headY, headZ) && DEBUG_LOGGING) { + LogWriter.info("[Beam] DEAD: Entity collision at tick " + ticksExisted); } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index e8b8acc58..503d64f3d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -265,15 +265,43 @@ public class EntityAbilityDisc extends EntityEnergyProjectile { } private void checkEntityCollision() { - // Disc hitbox: wider but thinner - double halfWidth = discRadius * 0.5; - double halfHeight = discThickness * 0.5; + // Swept disc hitbox to avoid miss-through at high speed. + double nextX = posX + motionX; + double nextY = posY + motionY; + double nextZ = posZ + motionZ; + + double halfRadius = Math.max(0.05, discRadius * 0.5); + double halfThickness = Math.max(0.03, discThickness * 0.5); + double halfX; + double halfY; + double halfZ; + + if (vertical) { + // Vertical disc: thin axis follows horizontal travel direction, tall on Y. + double horizLen = Math.sqrt(motionX * motionX + motionZ * motionZ); + if (horizLen > 1.0e-5) { + double nx = motionX / horizLen; + double nz = motionZ / horizLen; + halfX = Math.abs(nx) * halfThickness + Math.abs(nz) * halfRadius; + halfZ = Math.abs(nz) * halfThickness + Math.abs(nx) * halfRadius; + } else { + halfX = halfRadius; + halfZ = halfRadius; + } + halfY = halfRadius; + } else { + // Horizontal disc: wide on XZ, thin on Y. + halfX = halfRadius; + halfY = halfThickness; + halfZ = halfRadius; + } + AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( - posX - halfWidth, posY - halfHeight, posZ - halfWidth, - posX + halfWidth, posY + halfHeight, posZ + halfWidth + Math.min(posX, nextX) - halfX, Math.min(posY, nextY) - halfY, Math.min(posZ, nextZ) - halfZ, + Math.max(posX, nextX) + halfX, Math.max(posY, nextY) + halfY, Math.max(posZ, nextZ) + halfZ ); - processEntitiesInHitBox(hitBox, posX, posY, posZ); + processEntitiesInHitBox(hitBox, nextX, nextY, nextZ); } /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 1ff3ece70..a632b16ee 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -41,6 +41,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { // Lock vertical direction after firing (only update yaw, keep pitch fixed) private boolean lockVerticalDirection = false; + private int reflectedLockTicks = 0; // End point for rendering private double endX, endY, endZ; @@ -120,9 +121,14 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { return; } - // Track owner's anchor position and rotation each tick - // so the laser follows the NPC's facing direction - updateLaserOriginAndDirection(); + // After reflection, hold trajectory briefly so the rebound is visible. + if (reflectedLockTicks > 0) { + reflectedLockTicks--; + } else { + // Track owner's anchor position and rotation each tick + // so the laser follows the NPC's facing direction + updateLaserOriginAndDirection(); + } if (!fullyExtended) { // Expand the laser @@ -404,18 +410,23 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { EntityAbilityDome dome = (EntityAbilityDome) barrier; float intersectDist = getLineSphereIntersection(dome); if (intersectDist >= 0) { - // Truncate laser at dome surface - currentLength = intersectDist; - endX = startX + dirX * currentLength; - endY = startY + dirY * currentLength; - endZ = startZ + dirZ * currentLength; - fullyExtended = true; - float damage = getModifiedDamage(); - dome.onProjectileHit(this, damage); - // Block this tick so entity collision doesn't continue through the dome. - // Laser remains alive and lingers at truncated length. - return true; + EntityAbilityBarrier.ProjectileHitOutcome hitOutcome = dome.onProjectileHitResolved(this, damage); + if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) { + continue; + } + + if (hitOutcome.result != EntityAbilityBarrier.ProjectileHitResult.BROKEN) { + currentLength = Math.max(0.0f, intersectDist); + endX = startX + dirX * currentLength; + endY = startY + dirY * currentLength; + endZ = startZ + dirZ * currentLength; + } + + if (handleBarrierHitOutcome(dome, hitOutcome)) { + return true; + } + return false; } } else { // For line-based lasers, use a swept segment against generic barrier checks. @@ -433,17 +444,113 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { if (incoming) { float damage = getModifiedDamage(); - if (barrier.onProjectileHit(this, damage)) { - hasHit = true; - this.setDead(); + EntityAbilityBarrier.ProjectileHitOutcome hitOutcome = barrier.onProjectileHitResolved(this, damage); + if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) { + continue; + } + + if (hitOutcome.result != EntityAbilityBarrier.ProjectileHitResult.BROKEN) { + // Keep current segment length at the exact collision tick position. + endX = startX + dirX * currentLength; + endY = startY + dirY * currentLength; + endZ = startZ + dirZ * currentLength; + } + + if (handleBarrierHitOutcome(barrier, hitOutcome)) { return true; } + return false; } } } return false; } + @Override + protected boolean reflectFromBarrier(EntityAbilityBarrier barrier, float reflectStrengthPct) { + if (barrier == null) { + return false; + } + + double vx = dirX; + double vy = dirY; + double vz = dirZ; + double vLenSq = vx * vx + vy * vy + vz * vz; + if (vLenSq < 1.0e-8) { + return false; + } + + double[] normal = getBarrierImpactNormal(barrier, vx, vy, vz); + if (normal == null) { + return false; + } + + double dot = vx * normal[0] + vy * normal[1] + vz * normal[2]; + if (dot > 0.0) { + normal[0] = -normal[0]; + normal[1] = -normal[1]; + normal[2] = -normal[2]; + dot = vx * normal[0] + vy * normal[1] + vz * normal[2]; + } + + double rx = vx - 2.0 * dot * normal[0]; + double ry = vy - 2.0 * dot * normal[1]; + double rz = vz - 2.0 * dot * normal[2]; + if (ry < -0.06) { + ry = Math.max(0.08, Math.abs(ry) * 0.35); + } + double rLen = Math.sqrt(rx * rx + ry * ry + rz * rz); + if (rLen < 1.0e-8) { + rx = -vx; + ry = -vy; + rz = -vz; + rLen = Math.sqrt(rx * rx + ry * ry + rz * rz); + if (rLen < 1.0e-8) { + return false; + } + } + + dirX = rx / rLen; + dirY = ry / rLen; + dirZ = rz / rLen; + + // Start a fresh segment at the impact edge to make the rebound visible. + startX = endX; + startY = endY; + startZ = endZ; + setPosition(startX, startY, startZ); + syncPositionStateToCurrent(true); + + currentLength = 0.0f; + fullyExtended = false; + ticksSinceFullExtension = 0; + endX = startX; + endY = startY; + endZ = startZ; + + reflectedLockTicks = Math.max(reflectedLockTicks, 10); + barrierImpactPauseTicks = 0; + barrierImpactDestroyOnResume = false; + + Entity barrierOwner = barrier.getOwnerEntity(); + if (barrierOwner != null) { + setOwnerEntityId(barrierOwner.getEntityId()); + trackProjectile(this); + } + setTargetEntityId(-1); + + setInnerColor(barrier.getInnerColor()); + setOuterColor(barrier.getOuterColor()); + + float clampedStrength = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct)); + float reducedDamage = getDamage() * (1.0f - clampedStrength / 100.0f); + setCombatDamage(Math.max(0.0f, reducedDamage)); + + hitOnceEntities.clear(); + lastHitTickByEntity.clear(); + return true; + } + /** * Line-sphere intersection between the laser line segment and a dome. * Returns the distance along the laser direction to the first entry point, diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index aa50745ea..aab79a444 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -126,6 +126,14 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { protected boolean hasHit = false; protected final Set hitOnceEntities = new HashSet(); protected final Map lastHitTickByEntity = new HashMap(); + protected static final int BARRIER_IMPACT_PAUSE_TICKS = 10; + protected static final int BARRIER_BREAK_SPARK_TICKS = 10; + protected static final int DW_BARRIER_SPARK_TICKS = 21; + protected static final int DW_SYNC_INNER_COLOR = 22; + protected static final int DW_SYNC_OUTER_COLOR = 23; + protected int barrierImpactPauseTicks = 0; + protected boolean barrierImpactDestroyOnResume = false; + protected double pausedMotionX, pausedMotionY, pausedMotionZ; // ==================== CHARGING STATE (projectile-specific) ==================== protected float targetSize = 1.0f; @@ -150,6 +158,15 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { this.noClip = false; } + @Override + protected void entityInit() { + super.entityInit(); // DW_CHARGING = 20 + this.dataWatcher.addObject(DW_BARRIER_SPARK_TICKS, 0); + // displayData is not guaranteed to be initialized yet during Entity construction. + this.dataWatcher.addObject(DW_SYNC_INNER_COLOR, 0xFFFFFF); + this.dataWatcher.addObject(DW_SYNC_OUTER_COLOR, 0xFFFFFF); + } + /** * Initialize common properties using data classes. Call from subclass constructors. */ @@ -174,6 +191,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { this.lifespanData = lifespan; // deathWorldTime will be set on first tick when world is available this.lightningData = lightning; this.trajectoryData = trajectory; + syncProjectileColorWatchers(); // Initialize render size this.renderCurrentSize = size; @@ -209,12 +227,21 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { } } + if (!previewMode) { + syncDisplayColorsFromWatchers(); + } + // Update rotation updateRotation(); // Lerp render size toward actual size this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; + // Server-synced visual timer used for brief barrier impact lightning. + if (!previewMode && !worldObj.isRemote) { + tickBarrierSparkTimer(); + } + // Skip lifetime/distance checks in preview mode if (!previewMode) { // Only self-destruct if the owner is confirmed dead/killed. @@ -276,10 +303,16 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { } } + if (!previewMode && !worldObj.isRemote && !isCharging()) { + if (tickBarrierImpactPause()) { + return; + } + } + // Check barrier collisions (server-side, non-preview, non-charging) if (!previewMode && !worldObj.isRemote && !isCharging()) { if (checkBarrierCollision()) { - return; // Projectile was absorbed by a barrier + return; // Barrier interaction handled (pause + destroy/continue) } } @@ -330,7 +363,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { * Check for collision with energy barrier entities. * Barriers only block incoming projectiles from enemies. * - * @return true if projectile was absorbed (caller should stop processing) + * @return true if barrier interaction was handled (caller should stop processing this tick) */ protected boolean checkBarrierCollision() { List barriers = EntityAbilityBarrier.getActiveBarriers(worldObj); @@ -347,17 +380,355 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (barrier.isIncomingProjectile(this)) { float damage = getModifiedDamage(); - if (barrier.onProjectileHit(this, damage)) { - hasHit = true; - this.setDead(); + EntityAbilityBarrier.ProjectileHitOutcome hitOutcome = barrier.onProjectileHitResolved(this, damage); + if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) { + continue; + } + if (handleBarrierHitOutcome(barrier, hitOutcome)) { return true; } + return false; } } return false; } + /** + * Handle the result of a projectile/barrier interaction. + * + * @return true if caller should stop processing this tick. + */ + protected boolean handleBarrierHitOutcome(EntityAbilityBarrier barrier, EntityAbilityBarrier.ProjectileHitOutcome hitOutcome) { + if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) { + return false; + } + + if (hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.BROKEN) { + if (hitOutcome.remainingProjectileDamage <= 0.0f) { + setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS)); + hasHit = true; + return true; + } + + setCombatDamage(hitOutcome.remainingProjectileDamage); + setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS)); + return false; + } + + if (hitOutcome.shouldReflect()) { + if (!reflectFromBarrier(barrier, hitOutcome.reflectStrengthPct)) { + beginBarrierImpactPause(true, barrier); + } + return true; + } + + beginBarrierImpactPause(true, barrier); + return true; + } + + protected boolean reflectFromBarrier(EntityAbilityBarrier barrier, float reflectStrengthPct) { + if (barrier == null) { + return false; + } + + double vx = motionX; + double vy = motionY; + double vz = motionZ; + double vLenSq = vx * vx + vy * vy + vz * vz; + if (vLenSq < 1.0e-8) { + return false; + } + + double[] normal = getBarrierImpactNormal(barrier, vx, vy, vz); + if (normal == null) { + return false; + } + + double dot = vx * normal[0] + vy * normal[1] + vz * normal[2]; + if (dot > 0.0) { + normal[0] = -normal[0]; + normal[1] = -normal[1]; + normal[2] = -normal[2]; + dot = vx * normal[0] + vy * normal[1] + vz * normal[2]; + } + + double rx = vx - 2.0 * dot * normal[0]; + double ry = vy - 2.0 * dot * normal[1]; + double rz = vz - 2.0 * dot * normal[2]; + double rLenSq = rx * rx + ry * ry + rz * rz; + if (rLenSq < 1.0e-8) { + rx = -vx; + ry = -vy; + rz = -vz; + rLenSq = rx * rx + ry * ry + rz * rz; + } + + // Prefer rebounding out and away from ground instead of immediately diving downward. + if (ry < -0.06) { + double speed = Math.sqrt(Math.max(rLenSq, 1.0e-8)); + ry = Math.max(0.08, Math.abs(ry) * 0.35); + double newLen = Math.sqrt(rx * rx + ry * ry + rz * rz); + if (newLen > 1.0e-8) { + double scale = speed / newLen; + rx *= scale; + ry *= scale; + rz *= scale; + } + } + + // Push slightly outside the collision boundary so the reflected motion + // starts cleanly from the barrier surface instead of re-colliding in place. + snapOutsideBarrierForPause(barrier); + + motionX = rx; + motionY = ry; + motionZ = rz; + pausedMotionX = rx; + pausedMotionY = ry; + pausedMotionZ = rz; + barrierImpactPauseTicks = 0; + barrierImpactDestroyOnResume = false; + + Entity barrierOwner = barrier.getOwnerEntity(); + if (barrierOwner != null) { + setOwnerEntityId(barrierOwner.getEntityId()); + // Ensure ownership-sensitive systems can query this projectile under the new owner. + trackProjectile(this); + } + // Old homing target may now be the new owner or ally; clear it after reflection. + setTargetEntityId(-1); + + setInnerColor(barrier.getInnerColor()); + setOuterColor(barrier.getOuterColor()); + + float clampedStrength = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct)); + float reducedDamage = getDamage() * (1.0f - clampedStrength / 100.0f); + setCombatDamage(Math.max(0.0f, reducedDamage)); + + hitOnceEntities.clear(); + lastHitTickByEntity.clear(); + return true; + } + + protected double[] getBarrierImpactNormal(EntityAbilityBarrier barrier, double velocityX, double velocityY, double velocityZ) { + if (barrier instanceof EntityAbilityDome) { + EntityAbilityDome dome = (EntityAbilityDome) barrier; + return normalizeVector(posX - dome.posX, posY - dome.posY, posZ - dome.posZ, velocityX, velocityY, velocityZ); + } + + if (barrier instanceof EntityAbilityPanel) { + EntityAbilityPanel panel = (EntityAbilityPanel) barrier; + float yawRad = (float) Math.toRadians(panel.getPanelYaw()); + double nx = -Math.sin(yawRad); + double nz = Math.cos(yawRad); + double relX = posX - panel.posX; + double relZ = posZ - panel.posZ; + double signedDist = relX * nx + relZ * nz; + double side = signedDist; + if (Math.abs(side) < 1.0e-5) { + side = velocityX * nx + velocityZ * nz; + } + if (side < 0.0) { + nx = -nx; + nz = -nz; + } + return normalizeVector(nx, 0.0, nz, velocityX, velocityY, velocityZ); + } + + return normalizeVector(posX - barrier.posX, posY - barrier.posY, posZ - barrier.posZ, velocityX, velocityY, velocityZ); + } + + protected double[] normalizeVector(double x, double y, double z, double fallbackX, double fallbackY, double fallbackZ) { + double lenSq = x * x + y * y + z * z; + if (lenSq > 1.0e-8) { + double invLen = 1.0 / Math.sqrt(lenSq); + return new double[]{x * invLen, y * invLen, z * invLen}; + } + + double fallbackLenSq = fallbackX * fallbackX + fallbackY * fallbackY + fallbackZ * fallbackZ; + if (fallbackLenSq > 1.0e-8) { + double invFallbackLen = 1.0 / Math.sqrt(fallbackLenSq); + return new double[]{-fallbackX * invFallbackLen, -fallbackY * invFallbackLen, -fallbackZ * invFallbackLen}; + } + + return new double[]{0.0, 1.0, 0.0}; + } + + protected void beginBarrierImpactPause(boolean destroyOnResume, EntityAbilityBarrier barrier) { + if (barrierImpactPauseTicks <= 0) { + pausedMotionX = motionX; + pausedMotionY = motionY; + pausedMotionZ = motionZ; + } + + barrierImpactDestroyOnResume = destroyOnResume; + barrierImpactPauseTicks = BARRIER_IMPACT_PAUSE_TICKS; + motionX = 0; + motionY = 0; + motionZ = 0; + + int sparkTicks = getBarrierSparkTicks(); + if (sparkTicks < BARRIER_IMPACT_PAUSE_TICKS) { + setBarrierSparkTicks(BARRIER_IMPACT_PAUSE_TICKS); + } + } + + protected float getBarrierPauseOutsideDistance() { + // Keep the paused projectile just outside the surface without a visible "fling". + return Math.max(0.06f, Math.min(0.22f, size * 0.18f + 0.03f)); + } + + protected void snapOutsideBarrierForPause(EntityAbilityBarrier barrier) { + if (barrier == null) return; + + float bias = getBarrierPauseOutsideDistance(); + if (barrier instanceof EntityAbilityDome) { + EntityAbilityDome dome = (EntityAbilityDome) barrier; + double nx = posX - dome.posX; + double ny = posY - dome.posY; + double nz = posZ - dome.posZ; + double len = Math.sqrt(nx * nx + ny * ny + nz * nz); + + if (len < 1.0e-5) { + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 1.0e-5) { + nx = -motionX / vLen; + ny = -motionY / vLen; + nz = -motionZ / vLen; + len = 1.0; + } else { + nx = 0.0; + ny = 1.0; + nz = 0.0; + len = 1.0; + } + } + + double invLen = 1.0 / len; + double target = dome.getDomeRadius() + bias; + setPosition( + dome.posX + nx * invLen * target, + dome.posY + ny * invLen * target, + dome.posZ + nz * invLen * target + ); + return; + } + + if (barrier instanceof EntityAbilityPanel) { + EntityAbilityPanel panel = (EntityAbilityPanel) barrier; + float yawRad = (float) Math.toRadians(panel.getPanelYaw()); + double normalX = -Math.sin(yawRad); + double normalZ = Math.cos(yawRad); + double relX = posX - panel.posX; + double relZ = posZ - panel.posZ; + double signedDist = relX * normalX + relZ * normalZ; + double halfThickness = 0.25; + double side; + if (Math.abs(signedDist) > 0.035) { + side = signedDist >= 0 ? 1.0 : -1.0; + } else { + // Near the plane, use incoming direction so we don't snap through and "fling". + double vDot = motionX * normalX + motionZ * normalZ; + if (Math.abs(vDot) > 1.0e-5) { + side = vDot < 0 ? 1.0 : -1.0; + } else { + side = signedDist >= 0 ? 1.0 : -1.0; + } + } + double targetDist = side * (halfThickness + bias); + double delta = targetDist - signedDist; + setPosition(posX + normalX * delta, posY, posZ + normalZ * delta); + return; + } + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 1.0e-5) { + setPosition( + posX - (motionX / vLen) * bias, + posY - (motionY / vLen) * bias, + posZ - (motionZ / vLen) * bias + ); + } + } + + protected boolean tickBarrierImpactPause() { + if (barrierImpactPauseTicks <= 0) { + return false; + } + + motionX = 0; + motionY = 0; + motionZ = 0; + barrierImpactPauseTicks--; + + if (barrierImpactPauseTicks <= 0) { + if (barrierImpactDestroyOnResume) { + hasHit = true; + this.setDead(); + } else { + motionX = pausedMotionX; + motionY = pausedMotionY; + motionZ = pausedMotionZ; + } + barrierImpactDestroyOnResume = false; + } + + return true; + } + + protected void tickBarrierSparkTimer() { + int sparkTicks = getBarrierSparkTicks(); + if (sparkTicks > 0) { + setBarrierSparkTicks(sparkTicks - 1); + } + } + + protected void setBarrierSparkTicks(int ticks) { + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_BARRIER_SPARK_TICKS, Math.max(0, ticks)); + } + } + + public int getBarrierSparkTicks() { + if (this.dataWatcher == null) { + return 0; + } + return Math.max(0, this.dataWatcher.getWatchableObjectInt(DW_BARRIER_SPARK_TICKS)); + } + + protected void syncProjectileColorWatchers() { + if (this.dataWatcher == null) { + return; + } + this.dataWatcher.updateObject(DW_SYNC_INNER_COLOR, this.displayData.getInnerColor()); + this.dataWatcher.updateObject(DW_SYNC_OUTER_COLOR, this.displayData.getOuterColor()); + } + + protected void syncDisplayColorsFromWatchers() { + if (this.dataWatcher == null) { + return; + } + this.displayData.setInnerColor(this.dataWatcher.getWatchableObjectInt(DW_SYNC_INNER_COLOR)); + this.displayData.setOuterColor(this.dataWatcher.getWatchableObjectInt(DW_SYNC_OUTER_COLOR)); + } + + @Override + public void setInnerColor(int color) { + super.setInnerColor(color); + if (!previewMode) { + syncProjectileColorWatchers(); + } + } + + @Override + public void setOuterColor(int color) { + super.setOuterColor(color); + if (!previewMode) { + syncProjectileColorWatchers(); + } + } + /** * Get the projectile damage modified by extenders (e.g. DBC damage scaling). * Falls back to base getDamage() when no sourceAbility or no extenders modify it. @@ -605,7 +976,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { */ protected void spawnExplosionRenderEntity(float explosionRad) { if (worldObj == null || worldObj.isRemote) return; - float renderRad = Math.max(0.75f, Math.min(explosionRad, 12.0f)); + float renderRad = Math.max(0.75f, Math.min(explosionRad, EnergyCombatData.MAX_EXPLOSION_RADIUS)); EntityEnergyExplosion fx = new EntityEnergyExplosion(worldObj, this, renderRad); EnergyExplosionSpawnPacket.sendToTracking(getExplosionVisualInstanceId(), fx, this); } @@ -620,7 +991,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { protected void spawnExplosionVisuals(float explosionRad) { if (worldObj == null) return; - float visualRad = Math.max(0.75f, Math.min(explosionRad, 12.0f)); + float visualRad = Math.max(0.75f, Math.min(explosionRad, EnergyCombatData.MAX_EXPLOSION_RADIUS)); int coreBursts = Math.max(1, Math.min(4, Math.round(visualRad * 0.35f))); int shellCount = Math.max(1, Math.min(3, Math.round(visualRad / 3.5f) + 1)); @@ -771,7 +1142,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (!ConfigEnergy.EnableEnergyExplosionBlockDamage) return; if (!worldObj.getGameRules().getGameRuleBooleanValue("mobGriefing")) return; - float terrainRad = Math.max(0.5f, Math.min(explosionRad, 12.0f)); + float terrainRad = Math.max(0.5f, Math.min(explosionRad, EnergyCombatData.MAX_EXPLOSION_RADIUS)); int minX = MathHelper.floor_double(posX - terrainRad); int maxX = MathHelper.floor_double(posX + terrainRad); int minY = MathHelper.floor_double(posY - terrainRad); @@ -1459,7 +1830,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { } public float getExplosionRadius() { - return combatData.explosionRadius; + return combatData.getExplosionRadius(); } public void setExplosionRadius(float radius) { @@ -1686,6 +2057,11 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { return hasHit; } + @Override + public boolean hasLightningEffect() { + return super.hasLightningEffect() || getBarrierSparkTicks() > 0; + } + // ==================== NBT ==================== @Override @@ -1766,6 +2142,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { homingData.readNBT(nbt); lifespanData.readNBT(nbt); trajectoryData.readNBT(nbt); + syncProjectileColorWatchers(); } /** diff --git a/src/main/java/noppes/npcs/client/ClientEventHandler.java b/src/main/java/noppes/npcs/client/ClientEventHandler.java index b5e37503a..1e8d7a56d 100644 --- a/src/main/java/noppes/npcs/client/ClientEventHandler.java +++ b/src/main/java/noppes/npcs/client/ClientEventHandler.java @@ -104,9 +104,13 @@ public class ClientEventHandler { int hudKeyCode = ClientProxy.AbilityHudKey.getKeyCode(); boolean hudKeyHeld = false; if (hudKeyCode > 0) { - hudKeyHeld = org.lwjgl.input.Keyboard.isKeyDown(hudKeyCode); + hudKeyHeld = hudKeyCode < org.lwjgl.input.Keyboard.getKeyCount() + && org.lwjgl.input.Keyboard.isKeyDown(hudKeyCode); } else if (hudKeyCode < 0) { - hudKeyHeld = org.lwjgl.input.Mouse.isButtonDown(hudKeyCode + 100); + int mouseButton = hudKeyCode + 100; + hudKeyHeld = mouseButton >= 0 + && mouseButton < org.lwjgl.input.Mouse.getButtonCount() + && org.lwjgl.input.Mouse.isButtonDown(mouseButton); } if (hudKeyHeld) { AbilityHotbarComponent comp = (AbilityHotbarComponent) ClientHudManager.getInstance() diff --git a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java index 90ca61ea6..fda6547c5 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java +++ b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java @@ -604,7 +604,20 @@ public class AbilityHotbarComponent extends HudComponent { private boolean isHudKeyHeld() { if (ClientProxy.AbilityHudKey == null) return false; int keyCode = ClientProxy.AbilityHudKey.getKeyCode(); - return keyCode != 0 && org.lwjgl.input.Keyboard.isKeyDown(keyCode); + if (keyCode == 0 || keyCode == -1) return false; + + // Mouse bindings are encoded as negative key codes (offset by 100). + if (keyCode < -1) { + int mouseButton = keyCode + 100; + return mouseButton >= 0 + && mouseButton < org.lwjgl.input.Mouse.getButtonCount() + && org.lwjgl.input.Mouse.isButtonDown(mouseButton); + } + + // Guard against invalid/out-of-range keyboard codes to avoid Buffer.checkIndex crashes. + return keyCode > 0 + && keyCode < org.lwjgl.input.Keyboard.getKeyCount() + && org.lwjgl.input.Keyboard.isKeyDown(keyCode); } public boolean hasAnyAbilities() { diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java index a015a06ad..fe02608e1 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java @@ -121,6 +121,22 @@ public class ScriptEnergyBarrier extends ScriptE entity.getBarrierData().absorbing = absorbing; } + public boolean isReflect() { + return entity.getBarrierData().reflect; + } + + public void setReflect(boolean reflect) { + entity.getBarrierData().reflect = reflect; + } + + public float getReflectStrengthPct() { + return entity.getBarrierData().reflectStrengthPct; + } + + public void setReflectStrengthPct(float strengthPct) { + entity.getBarrierData().setReflectStrengthPct(strengthPct); + } + public int getBarrierType() { return entity instanceof EntityAbilityDome ? 0 : 1; } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 61553093e..35d0ac83d 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -2053,6 +2053,8 @@ ability.solid=Solid ability.knockbackEnabled=Knockback ability.knockbackStrength=Knockback Strength ability.absorbing=Absorbing +ability.reflect=Reflect +ability.reflectStrength=Strength (%) ability.meleeEnabled=Allows Melee ability.meleeDamageMultiplier=Melee Multiplier @@ -2061,6 +2063,7 @@ ability.hover.followCaster=Dome stays centered on the caster as they move ability.hover.solid=Entities cannot pass through the barrier boundary ability.hover.knockbackEnabled=Repulsion field pushing entities away from barrier surface ability.hover.absorbing=Damage dealt to the caster is redirected to the barrier's health +ability.hover.reflect=Rebounds non-breaking projectiles and reduces their damage by Strength ability.hover.meleeEnabled=Barrier can be hit and damaged by melee attacks