From 1a0ce41e008be6f0e07ec78c9bc990b4ad4766fb Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 28 Feb 2026 19:25:13 -0500 Subject: [PATCH] Add Free Aim Functionality for Beams --- .../data/ability/type/energy/AbilityBeam.java | 30 +++++++- .../ability/type/energy/AbilityLaser.java | 5 +- .../npcs/entity/EntityAbilityBeam.java | 75 ++++++++++++++++++- .../assets/customnpcs/lang/en_US.lang | 2 + 4 files changed, 109 insertions(+), 3 deletions(-) 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 ef75e242f..f811cef9e 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 @@ -33,6 +33,7 @@ public class AbilityBeam extends AbilityEnergyProjectile impl private float beamWidth = 0.4f; private float headSize = 0.6f; + private boolean freeAim = false; public AbilityBeam() { super( @@ -67,12 +68,14 @@ public class AbilityBeam extends AbilityEnergyProjectile impl @Override protected EntityAbilityBeam createEntity(EntityLivingBase caster, EntityLivingBase target, Vec3 spawnPos, EnergyDisplayData resolved, int index) { - return new EntityAbilityBeam( + EntityAbilityBeam beam = new EntityAbilityBeam( caster.worldObj, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, beamWidth, headSize, resolved, combatData, homingData, lightningData, lifespanData, lockMovement.locksActive()); + beam.setFreeAim(freeAim); + return beam; } @Override @@ -100,6 +103,18 @@ public class AbilityBeam extends AbilityEnergyProjectile impl return new EntityAbilityBeam[size]; } + @Override + public void detach() { + if (entities != null) { + for (EntityAbilityBeam beam : entities) { + if (beam != null && !beam.isDead) { + beam.setAttachedToOwner(false); + } + } + } + super.detach(); + } + @Override protected float getProjectileTelegraphRadius() { return headSize * 2.0f; @@ -139,12 +154,14 @@ public class AbilityBeam extends AbilityEnergyProjectile impl protected void writeTypeSpecificNBT(NBTTagCompound nbt) { nbt.setFloat("beamWidth", beamWidth); nbt.setFloat("headSize", headSize); + nbt.setBoolean("freeAim", freeAim); } @Override protected void readTypeSpecificNBT(NBTTagCompound nbt) { this.beamWidth = nbt.getFloat("beamWidth"); this.headSize = nbt.getFloat("headSize"); + this.freeAim = nbt.getBoolean("freeAim"); } // ==================== TYPE-SPECIFIC GETTERS ==================== @@ -173,6 +190,14 @@ public class AbilityBeam extends AbilityEnergyProjectile impl this.headSize = headSize; } + public boolean isFreeAim() { + return freeAim; + } + + public void setFreeAim(boolean freeAim) { + this.freeAim = freeAim; + } + // ==================== TYPE-SPECIFIC GUI ==================== @SideOnly(Side.CLIENT) @@ -189,6 +214,9 @@ public class AbilityBeam extends AbilityEnergyProjectile impl )); defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)); defs.add(FieldDef.section("ability.section.beam")); + defs.add(FieldDef.boolField("ability.freeAim", this::isFreeAim, this::setFreeAim) + .hover("ability.hover.freeAim") + .enabledWhen(() -> lockMovement.locksActive() && rotationMode == RotationMode.FREE && !freeOnCast)); defs.add(FieldDef.row( FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth).range(0.1f, 100.0f), FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize).range(0.1f, 100.0f) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java index 3ce1280de..918f493ba 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaser.java @@ -213,7 +213,10 @@ public class AbilityLaser extends AbilityEnergyProjectile im FieldDef.floatField("ability.laserWidth", this::getLaserWidth, this::setLaserWidth).range(0.1f, 100.0f), FieldDef.floatField("ability.expansionSpeed", this::getExpansionSpeed, this::setExpansionSpeed).range(0.1f, 50.0f) )); - defs.add(FieldDef.floatField("ability.maxLength", this::getMaxLength, this::setMaxLength).range(1.0f, 500.0f)); + defs.add(FieldDef.row( + FieldDef.floatField("ability.maxLength", this::getMaxLength, this::setMaxLength).range(1.0f, 500.0f), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200) + )); 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) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 47e2c7d8d..3ae05c655 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -11,6 +11,7 @@ import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.AxisAlignedBB; @@ -55,6 +56,10 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { // Beam behavior mode (single source of truth). private BeamMode beamMode = BeamMode.ANCHORED; + // Free Aim (players only) - steers beam toward player's look direction + private boolean freeAim = false; + private static final float FREE_AIM_STRENGTH = 0.15f; + // Charging state (beam-specific) private float chargeOffsetDistance = 1.0f; private int trailFadeTime = 20; // Ticks for trail to fully fade @@ -433,8 +438,64 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { addTrailPoint(); } + /** + * Steer beam toward player's look direction (Free Aim mode). + * Target point is projected along look vector at the current head distance from origin. + */ + private void updateFreeAim() { + Entity owner = getOwnerEntity(); + if (!(owner instanceof EntityPlayer)) return; + + Vec3 look = getOwnerLookVector(); + if (look == null) return; + + double headDist = Math.sqrt(headOffsetX * headOffsetX + headOffsetY * headOffsetY + headOffsetZ * headOffsetZ); + if (headDist < 0.1) return; + + // Target point along look vector at current head distance + double targetX = startX + look.xCoord * headDist; + double targetY = startY + look.yCoord * headDist; + double targetZ = startZ + look.zCoord * headDist; + + // Current head world position + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + + double dx = targetX - headWorldX; + double dy = targetY - headWorldY; + double dz = targetZ - headWorldZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist < 0.01) return; + + double desiredVX = (dx / dist) * getSpeed(); + double desiredVY = (dy / dist) * getSpeed(); + double desiredVZ = (dz / dist) * getSpeed(); + + motionX += (desiredVX - motionX) * FREE_AIM_STRENGTH; + motionY += (desiredVY - motionY) * FREE_AIM_STRENGTH; + motionZ += (desiredVZ - motionZ) * FREE_AIM_STRENGTH; + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * getSpeed(); + motionY = (motionY / vLen) * getSpeed(); + motionZ = (motionZ / vLen) * getSpeed(); + } + } + @Override protected void updateHoming() { + if (freeAim) { + Entity owner = getOwnerEntity(); + if (owner instanceof EntityPlayer) { + updateFreeAim(); + return; + } + // NPCs fall through to normal homing + } + if (!isHoming()) return; Entity target = getTargetEntity(); @@ -545,6 +606,14 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { setBeamMode(modeFromAnchored(attached)); } + public boolean isFreeAim() { + return freeAim; + } + + public void setFreeAim(boolean freeAim) { + this.freeAim = freeAim; + } + /** * Get interpolated head offset for smooth rendering. * Returns offset RELATIVE to origin. @@ -643,8 +712,9 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { resetHeadOffsets(); resetTrailStorage(); - // Disable homing — reflected beams fly straight. + // Disable homing and free aim — reflected beams fly straight. homingData.setHoming(false); + freeAim = false; setTargetEntityId(-1); } return reflected; @@ -668,6 +738,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { resetHeadOffsets(); resetTrailStorage(); homingData.setHoming(false); + freeAim = false; setTargetEntityId(-1); } @@ -686,6 +757,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { readChargingNBT(nbt); this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f; this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20; + this.freeAim = nbt.getBoolean("FreeAim"); boolean attachedLegacy = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner"); BeamMode loadedMode = modeFromAnchored(attachedLegacy); @@ -737,6 +809,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { nbt.setString("BeamMode", beamMode.name()); writeChargingNBT(nbt); nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); + nbt.setBoolean("FreeAim", freeAim); nbt.setBoolean("FadeTrail", isFadingMode()); nbt.setInteger("TrailFadeTime", trailFadeTime); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 86c7db40a..1e757a1c1 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1731,6 +1731,8 @@ ability.cooldownTicks=Cooldown ability.perAbilityCooldown=Per-Ability ability.freeOnCast=Free on Cast ability.hover.freeOnCast=Ability enters cooldown immediately after entities are spawned, allowing the caster to use other abilities while summoned entities remain active +ability.freeAim=Free Aim +ability.hover.freeAim=Players Only. Allows the player to steer the beam direction with their mouse during the active phase. Requires: Lock Movement set to Active or Both, Rotation Mode set to Free, Free on Cast disabled. ability.lockMovement=Lock Movement ability.rotationMode=Rotation ability.rotationPhase=Phase