Fix Firing Energy Projectiles and Make Charging State purely visual

This commit is contained in:
Kam committed 2026-02-22 03:07:37 -05:00
1 parent 3a8f829de9
commit 18968bf7c7
18 files changed
+1086 -439

No files matched your search

@@ -0,0 +1,38 @@
package kamkeel.npcs.client.renderer;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.entity.RenderManager;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraftforge.client.event.RenderWorldLastEvent;
/**
* Renders packet-driven charging previews through RenderManager.
*/
public class EnergyChargePreviewRenderer {
@SubscribeEvent
public void onRenderWorldLast(RenderWorldLastEvent event) {
if (EnergyChargePreviewManager.ClientInstance == null || !EnergyChargePreviewManager.ClientInstance.hasPreviews()) {
return;
}
Minecraft mc = Minecraft.getMinecraft();
EntityPlayer player = mc.thePlayer;
if (player == null) return;
float partialTicks = event.partialTicks;
for (EntityEnergyProjectile entity : EnergyChargePreviewManager.ClientInstance.getPreviews()) {
if (entity == null || entity.isDead) continue;
double x = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks - RenderManager.renderPosX;
double y = entity.prevPosY + (entity.posY - entity.prevPosY) * partialTicks - RenderManager.renderPosY;
double z = entity.prevPosZ + (entity.posZ - entity.prevPosZ) * partialTicks - RenderManager.renderPosZ;
RenderManager.instance.renderEntityWithPosYaw(entity, x, y, z, entity.rotationYaw, partialTicks);
}
}
}
@@ -15,6 +15,8 @@ import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeRemovePacket;
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeSpawnPacket;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
@@ -55,6 +57,10 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
// Transient entity state
protected transient E[] entities;
protected transient boolean[] projectileSpawned;
protected transient int spawnedCount;
protected transient String[] chargeVisualIds;
protected transient EntityLivingBase chargeVisualCaster;
// ==================== CONSTRUCTOR ====================
@@ -77,7 +83,7 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
/**
* Create a single entity for the given projectile index.
* Called during windup (tick 1) and burst refire (onExecute when entities are null).
* Called when a projectile actually fires.
*/
protected abstract E createEntity(EntityLivingBase caster, EntityLivingBase target,
Vec3 spawnPos, EnergyDisplayData resolved, int index);
@@ -209,32 +215,99 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
// ==================== ENTITY CREATION ====================
/**
* Create all entities for this ability.
* Sets effects, source ability, and sibling links.
*/
protected E[] createAllEntities(EntityLivingBase caster, EntityLivingBase target) {
E[] newEntities = createEntityArray(projectileCount);
for (int i = 0; i < projectileCount; i++) {
Vec3 spawnPos = getSpawnPosition(caster, i);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
newEntities[i] = createEntity(caster, target, spawnPos, resolved, i);
newEntities[i].setEffects(this.effects);
newEntities[i].setSourceAbility(this);
protected void initRuntimeState(EntityLivingBase caster) {
entities = createEntityArray(projectileCount);
projectileSpawned = new boolean[projectileCount];
spawnedCount = 0;
chargeVisualIds = new String[projectileCount];
chargeVisualCaster = caster;
}
protected E createProjectileEntity(EntityLivingBase caster, EntityLivingBase target, int index) {
Vec3 spawnPos = getSpawnPosition(caster, index);
EnergyDisplayData resolved = projectiles[index].resolveDisplay(displayData);
E entity = createEntity(caster, target, spawnPos, resolved, index);
entity.setEffects(this.effects);
entity.setSourceAbility(this);
return entity;
}
protected void spawnProjectileEntity(E entity, int index) {
if (entity == null) return;
spawnAbilityEntity(entity);
entities[index] = entity;
if (!projectileSpawned[index]) {
projectileSpawned[index] = true;
spawnedCount++;
}
return newEntities;
}
protected void spawnAndFireProjectile(EntityLivingBase caster, EntityLivingBase target, int index) {
if (entities != null && entities[index] != null && !entities[index].isDead) return;
E entity = createProjectileEntity(caster, target, index);
// Initialize launch position/motion BEFORE spawn so the
// initial spawn packet already has centered look-vector data on all clients.
fireEntitySafe(entity, caster, target);
spawnProjectileEntity(entity, index);
}
/**
* Hook for launch target selection.
* Default returns null so projectiles launch along owner look vector.
*/
protected EntityLivingBase getLaunchTarget(EntityLivingBase caster, EntityLivingBase target) {
return null;
}
/**
* Fire an entity if it's valid.
* Clears charging state first so the entity can move and check collisions.
*/
protected void fireEntitySafe(E entity, EntityLivingBase target) {
protected void fireEntitySafe(E entity, EntityLivingBase caster, EntityLivingBase target) {
if (entity != null && !entity.isDead) {
if (entity.isCharging()) {
entity.setCharging(false);
}
fireEntity(entity, target);
fireEntity(entity, getLaunchTarget(caster, target));
}
}
protected String getChargeVisualId(EntityLivingBase caster, int index) {
return "energy_charge:" + caster.getEntityId() + ":" + executionStartTime + ":" + burstIndex + ":" + index;
}
protected void spawnChargeVisual(EntityLivingBase caster, EntityLivingBase target, int index, int chargeDuration) {
if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return;
if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return;
E previewEntity = createProjectileEntity(caster, target, index);
previewEntity.setPreviewMode(true);
previewEntity.setPreviewOwner(caster);
setupEntityCharging(previewEntity, projectiles[index], index);
previewEntity.setChargeDuration(Math.max(1, chargeDuration));
String id = getChargeVisualId(caster, index);
chargeVisualIds[index] = id;
chargeVisualCaster = caster;
EnergyChargeSpawnPacket.sendToTracking(id, previewEntity, caster);
}
protected void removeChargeVisual(EntityLivingBase caster, int index) {
if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return;
if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return;
String id = chargeVisualIds[index];
if (id != null && !id.isEmpty()) {
EnergyChargeRemovePacket.sendToTracking(id, caster);
chargeVisualIds[index] = null;
}
}
protected void removeAllChargeVisuals(EntityLivingBase caster) {
if (chargeVisualIds == null || caster == null) return;
for (int i = 0; i < chargeVisualIds.length; i++) {
removeChargeVisual(caster, i);
}
}
@@ -253,6 +326,8 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
@Override
public void detach() {
entities = null;
projectileSpawned = null;
spawnedCount = 0;
}
@Override
@@ -275,21 +350,31 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
if (caster.worldObj.isRemote && !isPreview()) return;
if (tick == 1) {
entities = createAllEntities(caster, target);
for (int i = 0; i < projectileCount; i++) {
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
if (isPreview()) {
setupEntityPreview(entities[i], caster, resolved, projectiles[i], i);
} else {
setupEntityCharging(entities[i], projectiles[i], i);
initRuntimeState(caster);
if (isPreview()) {
for (int i = 0; i < projectileCount; i++) {
E previewEntity = createProjectileEntity(caster, target, i);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
setupEntityPreview(previewEntity, caster, resolved, projectiles[i], i);
spawnAbilityEntity(previewEntity);
entities[i] = previewEntity;
projectileSpawned[i] = true;
spawnedCount++;
}
spawnAbilityEntity(entities[i]);
return;
}
for (int i = 0; i < projectileCount; i++) {
spawnChargeVisual(caster, target, i, windUpTicks);
}
}
}
@Override
public void resetForBurst() {
removeAllChargeVisuals(chargeVisualCaster);
// Non-overlap: kill old entities before next burst creates new ones
if (!burstOverlap) {
cleanup();
@@ -297,54 +382,71 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
// Clear reference so the next iteration creates fresh entities.
// For overlap mode, old entities stay alive (tracked in burstEntities).
entities = null;
projectileSpawned = null;
spawnedCount = 0;
}
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
// Create entities if they don't exist yet.
// They'll already exist if onWindUpTick ran (windup replay), otherwise we create here.
if (entities == null) {
entities = createAllEntities(caster, target);
// If there's a fire delay, put unfired entities in charging state
// to prevent collision checks before they're actually fired.
// Entity[0] will be fired immediately below, so skip it.
if (fireDelay > 0 && projectileCount > 1) {
for (int i = 1; i < projectileCount; i++) {
if (entities[i] != null) {
entities[i].setCharging(true);
}
// GUI preview keeps the original non-world preview entity flow.
if (isPreview()) {
if (entities == null) {
initRuntimeState(caster);
for (int i = 0; i < projectileCount; i++) {
E previewEntity = createProjectileEntity(caster, target, i);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
setupEntityPreview(previewEntity, caster, resolved, projectiles[i], i);
spawnAbilityEntity(previewEntity);
entities[i] = previewEntity;
projectileSpawned[i] = true;
spawnedCount++;
}
}
for (E entity : entities) {
spawnAbilityEntity(entity);
fireEntitySafe(entities[0], caster, target);
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireEntitySafe(entities[i], caster, target);
}
}
return;
}
// Fire first projectile immediately
fireEntitySafe(entities[0], target);
if (entities == null || projectileSpawned == null || entities.length != projectileCount) {
initRuntimeState(caster);
}
// Fire remaining projectiles if no delay
removeAllChargeVisuals(caster);
// Fire first projectile immediately.
spawnAndFireProjectile(caster, target, 0);
// Fire remaining projectiles immediately if there is no stagger delay.
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireEntitySafe(entities[i], target);
spawnAndFireProjectile(caster, target, i);
}
} else {
// During active stagger, keep client-only charging visuals for unfired projectiles.
for (int i = 1; i < projectileCount; i++) {
spawnChargeVisual(caster, target, i, fireDelay * i);
}
}
}
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (entities == null) {
if (entities == null || projectileSpawned == null) {
signalCompletion();
return;
}
// Fire staggered projectiles
// Fire staggered projectiles: spawn real world entity only when it is time to fire.
if (fireDelay > 0) {
for (int i = 1; i < projectileCount; i++) {
if (tick == fireDelay * i) {
fireEntitySafe(entities[i], target);
removeChargeVisual(caster, i);
spawnAndFireProjectile(caster, target, i);
}
}
}
@@ -358,21 +460,21 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
}
}
// Check if all entities are dead or unreachable
boolean allDead = true;
for (E entity : entities) {
if (entity == null || entity.isDead) continue;
// Before all projectiles have fired, the ability is still active by definition.
boolean allDead = spawnedCount >= projectileCount;
if (allDead) {
for (E entity : entities) {
if (entity == null || entity.isDead) continue;
// Check if entity was removed from world (chunk unload, etc.)
// Skip this check for preview entities - they're not registered in the world
if (!isPreview() && tick > 5 && entity.worldObj != null
&& entity.worldObj.getEntityByID(entity.getEntityId()) != entity) {
entity.setDead();
continue;
if (!isPreview() && tick > 5 && entity.worldObj != null
&& entity.worldObj.getEntityByID(entity.getEntityId()) != entity) {
entity.setDead();
continue;
}
allDead = false;
break;
}
allDead = false;
break;
}
// Failsafe timeout: force completion if active phase exceeded max possible flight time
@@ -392,15 +494,19 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
removeAllChargeVisuals(caster);
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
removeAllChargeVisuals(caster);
cleanup();
}
@Override
public void cleanup() {
removeAllChargeVisuals(chargeVisualCaster);
if (entities != null) {
for (int i = 0; i < entities.length; i++) {
if (entities[i] != null && !entities[i].isDead) {
@@ -410,6 +516,10 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
}
}
entities = null;
projectileSpawned = null;
spawnedCount = 0;
chargeVisualIds = null;
chargeVisualCaster = null;
}
// ==================== TELEGRAPH ====================
@@ -139,7 +139,7 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
@Override
protected void setupEntityPreview(EntityAbilityLaser laser, EntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
laser.setupPreview(caster, laserWidth, resolved, lightningData, expansionSpeed, lifespanData.maxDistance);
laser.setupPreview(caster, laserWidth, resolved, lightningData, projData.anchor, windUpTicks, expansionSpeed, lifespanData.maxDistance);
}
@Override
@@ -0,0 +1,68 @@
package kamkeel.npcs.controllers.data.energycharge;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import net.minecraft.world.World;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
/**
* Client-side manager for packet-driven charging preview entities.
* These entities are never spawned into the world; they are ticked and rendered manually.
*/
public class EnergyChargePreviewManager {
public static EnergyChargePreviewManager ClientInstance;
private final ConcurrentHashMap<String, EntityEnergyProjectile> previews = new ConcurrentHashMap<String, EntityEnergyProjectile>();
public static void initClient() {
ClientInstance = new EnergyChargePreviewManager();
}
public void addPreview(String instanceId, EntityEnergyProjectile entity) {
if (instanceId == null || entity == null) return;
previews.put(instanceId, entity);
}
public void removePreview(String instanceId) {
if (instanceId == null) return;
previews.remove(instanceId);
}
public Collection<EntityEnergyProjectile> getPreviews() {
return previews.values();
}
public boolean hasPreviews() {
return !previews.isEmpty();
}
public void clear() {
previews.clear();
}
/**
* Tick all preview entities and remove invalid/expired entries.
*/
public void tick(World world) {
if (world == null) {
clear();
return;
}
Iterator<EntityEnergyProjectile> iterator = previews.values().iterator();
while (iterator.hasNext()) {
EntityEnergyProjectile entity = iterator.next();
if (entity == null || entity.isDead || entity.worldObj != world) {
iterator.remove();
continue;
}
entity.onUpdate();
if (entity.isDead) {
iterator.remove();
}
}
}
}
@@ -18,6 +18,7 @@ import net.minecraft.world.World;
import noppes.npcs.LogWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
@@ -31,6 +32,11 @@ import java.util.List;
*/
public class EntityAbilityBeam extends EntityEnergyProjectile {
private enum BeamMode {
ANCHORED,
FREE_TRAIL
}
// Beam shape properties
private float beamWidth = 0.3f;
private float headSize = 0.5f;
@@ -39,22 +45,18 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
private List<Vec3> trailPoints = new ArrayList<>();
private static final int MAX_TRAIL_POINTS = 200;
private static final double MIN_POINT_DISTANCE = 0.2;
private static final int TRAIL_COMPACT_THRESHOLD = 128;
private int trailStartIndex = 0;
// Head position relative to origin
private double headOffsetX, headOffsetY, headOffsetZ;
private double prevHeadOffsetX, prevHeadOffsetY, prevHeadOffsetZ;
// Origin stays fixed (or follows owner)
private boolean attachedToOwner = true;
// Whether to render tail orb (only when anchored)
private boolean renderTailOrb = true;
// Beam behavior mode (single source of truth).
private BeamMode beamMode = BeamMode.ANCHORED;
// Charging state (beam-specific)
private float chargeOffsetDistance = 1.0f;
// Trail fading for non-anchored beams (comet effect)
private boolean fadeTrail = false;
private int trailFadeTime = 20; // Ticks for trail to fully fade
private List<Integer> trailPointAges = new ArrayList<>();
@@ -65,6 +67,46 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
super(world);
}
private static BeamMode modeFromAnchored(boolean anchored) {
return anchored ? BeamMode.ANCHORED : BeamMode.FREE_TRAIL;
}
private boolean isAnchoredMode() {
return beamMode == BeamMode.ANCHORED;
}
private boolean isFadingMode() {
return beamMode == BeamMode.FREE_TRAIL;
}
private int getActiveTrailSize() {
return Math.max(0, trailPoints.size() - trailStartIndex);
}
private void resetTrailStorage() {
trailPoints.clear();
trailPointAges.clear();
trailStartIndex = 0;
}
private void setBeamMode(BeamMode mode) {
beamMode = mode != null ? mode : BeamMode.ANCHORED;
if (isFadingMode()) {
while (trailPointAges.size() < trailPoints.size()) {
trailPointAges.add(0);
}
while (trailPointAges.size() > trailPoints.size()) {
trailPointAges.remove(trailPointAges.size() - 1);
}
} else {
if (trailStartIndex > 0 && trailStartIndex < trailPoints.size()) {
trailPoints = new ArrayList<Vec3>(trailPoints.subList(trailStartIndex, trailPoints.size()));
}
trailStartIndex = 0;
trailPointAges.clear();
}
}
/**
* Full constructor with all parameters using data classes.
*
@@ -89,9 +131,8 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
this.beamWidth = beamWidth;
this.headSize = headSize;
// Anchored mode controls whether origin follows owner and tail orb is rendered
this.attachedToOwner = anchoredMode;
this.renderTailOrb = anchoredMode;
// Beam mode controls tail attachment/orb/fading behavior.
setBeamMode(modeFromAnchored(anchoredMode));
// Initialize head offset at origin (0,0,0 relative)
this.headOffsetX = 0;
@@ -118,7 +159,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
this.chargeTick = 0;
this.chargeOffsetDistance = chargeOffsetDistance;
this.anchorData = anchor;
this.fadeTrail = !attachedToOwner;
this.motionX = 0;
this.motionY = 0;
this.motionZ = 0;
@@ -160,10 +200,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
this.prevHeadOffsetY = 0;
this.prevHeadOffsetZ = 0;
// Attach to owner for anchor following
this.attachedToOwner = true;
this.renderTailOrb = true;
// Clear motion
this.motionX = 0;
this.motionY = 0;
@@ -174,12 +210,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
* Start preview firing (simulates firing toward a point in front of NPC).
*/
public void startPreviewFiring() {
setCharging(false);
// Origin (tail) stays at the charged position
startX = posX;
startY = posY;
startZ = posZ;
beginLookVectorLaunch(true);
// Reset head offset to origin
headOffsetX = 0;
@@ -189,51 +220,21 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
prevHeadOffsetY = 0;
prevHeadOffsetZ = 0;
// Origin no longer follows owner after firing
attachedToOwner = false;
// Sync prev position to prevent visual jump on first frame
prevPosX = posX;
prevPosY = posY;
prevPosZ = posZ;
// Reset trail
trailPoints.clear();
trailPointAges.clear();
resetTrailStorage();
trailPoints.add(Vec3.createVectorHelper(0, 0, 0));
if (isFadingMode()) trailPointAges.add(0);
// Fire forward based on owner facing direction
Entity owner = getOwnerEntity();
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
motionX = -Math.sin(yaw) * getSpeed();
motionY = 0;
motionZ = Math.cos(yaw) * getSpeed();
} else {
motionX = getSpeed();
motionY = 0;
motionZ = 0;
}
// Fire along look vector for parity with real launch behavior.
setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0);
}
/**
* Start the beam firing (exit charging mode).
* Called by ability when windup ends.
* <p>
* For anchored beams: origin follows owner, head starts at charged position
* For non-anchored beams: origin fixed at charged position, head starts there
*/
public void startFiring(EntityLivingBase target) {
setCharging(false);
// For player casters, snap to look vector for crosshair-aligned launch
snapToPlayerLookVector();
// Origin (tail) stays at the charged position - where the orb was
// This is the same for both anchored and non-anchored modes
startX = posX;
startY = posY;
startZ = posZ;
beginLookVectorLaunch(false);
// Head starts at the origin (tail position)
headOffsetX = 0;
@@ -243,36 +244,13 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
prevHeadOffsetY = 0;
prevHeadOffsetZ = 0;
// Origin no longer follows owner after firing starts
// (for anchored mode, tail is fixed in space; for non-anchored, there's no tail)
attachedToOwner = false;
// Initialize trail with just the origin point
trailPoints.clear();
trailPointAges.clear();
resetTrailStorage();
trailPoints.add(Vec3.createVectorHelper(0, 0, 0));
if (fadeTrail) trailPointAges.add(0);
if (isFadingMode()) trailPointAges.add(0);
// Calculate velocity toward target (head starts at origin = startX/Y/Z)
Entity owner = getOwnerEntity();
if (target != null) {
double dx = target.posX - startX;
double dy = (target.posY + target.getEyeHeight()) - startY;
double dz = target.posZ - startZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
motionX = (dx / len) * getSpeed();
motionY = (dy / len) * getSpeed();
motionZ = (dz / len) * getSpeed();
}
} else if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
motionY = -Math.sin(pitch) * getSpeed();
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
}
// Calculate velocity toward target when provided; otherwise along look vector.
setMotionTowardTargetOrLookVector(target, startX, startY, startZ, getSpeed(), getSpeed(), 0, 0);
if (DEBUG_LOGGING && !worldObj.isRemote) {
LogWriter.info("[Beam] startFiring: origin=(" + startX + "," + startY + "," + startZ +
@@ -310,7 +288,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
prevHeadOffsetZ = headOffsetZ;
// Age trail points for fading effect
if (fadeTrail) {
if (isFadingMode()) {
ageTrailPoints();
}
@@ -344,7 +322,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
headOffsetZ += motionZ;
// Update origin if attached to owner
if (attachedToOwner) {
if (isAnchoredMode()) {
Entity owner = getOwnerEntity();
if (owner != null) {
startX = owner.posX;
@@ -372,7 +350,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
String.format("%.2f", headOffsetZ) + ") origin=(" +
String.format("%.2f", startX) + "," +
String.format("%.2f", startY) + "," +
String.format("%.2f", startZ) + ") trail=" + trailPoints.size());
String.format("%.2f", startZ) + ") trail=" + getActiveTrailSize());
}
// Skip collision checks on first few ticks
@@ -418,25 +396,43 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
* Age trail points and remove old ones (for comet effect).
*/
private void ageTrailPoints() {
// Age all trail points
for (int i = 0; i < trailPointAges.size(); i++) {
if (trailStartIndex >= trailPoints.size()) {
resetTrailStorage();
return;
}
// Age all active trail points.
for (int i = trailStartIndex; i < trailPointAges.size(); i++) {
trailPointAges.set(i, trailPointAges.get(i) + 1);
}
// Remove trail points that have fully faded
while (!trailPointAges.isEmpty() && trailPointAges.get(0) >= trailFadeTime) {
trailPointAges.remove(0);
if (!trailPoints.isEmpty()) {
trailPoints.remove(0);
}
// Move logical start index forward for fully faded points.
while (trailStartIndex < trailPointAges.size() && trailPointAges.get(trailStartIndex) >= trailFadeTime) {
trailStartIndex++;
}
if (trailStartIndex >= trailPoints.size()) {
resetTrailStorage();
return;
}
// Compact stale prefix to avoid unbounded growth of dead entries.
if (trailStartIndex >= TRAIL_COMPACT_THRESHOLD && trailStartIndex * 2 >= trailPoints.size()) {
trailPoints = new ArrayList<Vec3>(trailPoints.subList(trailStartIndex, trailPoints.size()));
trailPointAges = new ArrayList<Integer>(trailPointAges.subList(trailStartIndex, trailPointAges.size()));
trailStartIndex = 0;
}
}
private void addTrailPoint() {
if (trailStartIndex >= trailPoints.size()) {
resetTrailStorage();
}
// Trail points are RELATIVE to origin
if (trailPoints.isEmpty()) {
trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ));
if (fadeTrail) {
if (isFadingMode()) {
trailPointAges.add(0);
}
return;
@@ -451,12 +447,12 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
if (dist >= MIN_POINT_DISTANCE) {
trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ));
if (fadeTrail) {
if (isFadingMode()) {
trailPointAges.add(0);
}
// Limit trail length (only if not using fading - fading handles its own cleanup)
if (!fadeTrail) {
if (!isFadingMode()) {
while (trailPoints.size() > MAX_TRAIL_POINTS) {
trailPoints.remove(0);
}
@@ -474,7 +470,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
headOffsetZ += motionZ;
// Update origin if attached to owner
if (attachedToOwner) {
if (isAnchoredMode()) {
Entity owner = getOwnerEntity();
if (owner != null) {
startX = owner.posX;
@@ -586,7 +582,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
}
public void setAttachedToOwner(boolean attached) {
this.attachedToOwner = attached;
setBeamMode(modeFromAnchored(attached));
}
/**
@@ -609,7 +605,13 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
* Get trail points. These are RELATIVE to origin (startX/Y/Z).
*/
public List<Vec3> getTrailPoints() {
return trailPoints;
if (trailStartIndex <= 0) {
return trailPoints;
}
if (trailStartIndex >= trailPoints.size()) {
return Collections.emptyList();
}
return trailPoints.subList(trailStartIndex, trailPoints.size());
}
public double getOriginX() {
@@ -628,28 +630,34 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
* Whether the tail orb should be rendered (only true in anchored mode).
*/
public boolean shouldRenderTailOrb() {
return renderTailOrb;
return isAnchoredMode();
}
/**
* Whether the beam is attached to its owner (anchored mode).
*/
public boolean isAttachedToOwner() {
return attachedToOwner;
return isAnchoredMode();
}
/**
* Whether trail should fade (comet effect for non-anchored beams).
*/
public boolean hasFadingTrail() {
return fadeTrail;
return isFadingMode();
}
/**
* Get trail point ages for fading calculation.
*/
public List<Integer> getTrailPointAges() {
return trailPointAges;
if (trailStartIndex <= 0) {
return trailPointAges;
}
if (trailStartIndex >= trailPointAges.size()) {
return Collections.emptyList();
}
return trailPointAges.subList(trailStartIndex, trailPointAges.size());
}
/**
@@ -671,17 +679,26 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
this.prevHeadOffsetX = this.headOffsetX;
this.prevHeadOffsetY = this.headOffsetY;
this.prevHeadOffsetZ = this.headOffsetZ;
this.attachedToOwner = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner");
this.renderTailOrb = !nbt.hasKey("RenderTailOrb") || nbt.getBoolean("RenderTailOrb");
readChargingNBT(nbt);
this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f;
this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail");
this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20;
boolean attachedLegacy = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner");
BeamMode loadedMode = modeFromAnchored(attachedLegacy);
if (nbt.hasKey("BeamMode")) {
try {
loadedMode = BeamMode.valueOf(nbt.getString("BeamMode"));
} catch (IllegalArgumentException ignored) {
loadedMode = modeFromAnchored(attachedLegacy);
}
} else if (nbt.hasKey("FadeTrail")) {
loadedMode = nbt.getBoolean("FadeTrail") ? BeamMode.FREE_TRAIL : modeFromAnchored(attachedLegacy);
}
// Read trail points (relative to origin)
trailPoints.clear();
trailPointAges.clear();
trailStartIndex = 0;
if (nbt.hasKey("Trail")) {
NBTTagList trailList = nbt.getTagList("Trail", 10);
for (int i = 0; i < trailList.tagCount(); i++) {
@@ -701,10 +718,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
trailPointAges.add(age);
}
}
// If trail points exist but ages don't (legacy data), initialize ages to 0
while (trailPointAges.size() < trailPoints.size()) {
trailPointAges.add(0);
}
setBeamMode(loadedMode);
}
@Override
@@ -714,16 +728,18 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
nbt.setDouble("HeadOffsetX", headOffsetX);
nbt.setDouble("HeadOffsetY", headOffsetY);
nbt.setDouble("HeadOffsetZ", headOffsetZ);
nbt.setBoolean("AttachedToOwner", attachedToOwner);
nbt.setBoolean("RenderTailOrb", renderTailOrb);
nbt.setBoolean("AttachedToOwner", isAnchoredMode());
nbt.setBoolean("RenderTailOrb", isAnchoredMode());
nbt.setString("BeamMode", beamMode.name());
writeChargingNBT(nbt);
nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance);
nbt.setBoolean("FadeTrail", fadeTrail);
nbt.setBoolean("FadeTrail", isFadingMode());
nbt.setInteger("TrailFadeTime", trailFadeTime);
// Write trail points
NBTTagList trailList = new NBTTagList();
for (Vec3 point : trailPoints) {
for (int i = trailStartIndex; i < trailPoints.size(); i++) {
Vec3 point = trailPoints.get(i);
NBTTagCompound pointNbt = new NBTTagCompound();
pointNbt.setDouble("X", point.xCoord);
pointNbt.setDouble("Y", point.yCoord);
@@ -733,9 +749,11 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
nbt.setTag("Trail", trailList);
// Write trail point ages for fading trail sync
int[] ages = new int[trailPointAges.size()];
for (int i = 0; i < trailPointAges.size(); i++) {
ages[i] = trailPointAges.get(i);
int activeSize = isFadingMode() ? Math.max(0, trailPoints.size() - trailStartIndex) : 0;
int[] ages = new int[activeSize];
for (int i = 0; i < activeSize; i++) {
int idx = trailStartIndex + i;
ages[i] = idx < trailPointAges.size() ? trailPointAges.get(idx) : 0;
}
nbt.setIntArray("TrailAges", ages);
}
@@ -137,31 +137,8 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
* Start preview firing (simulates firing toward a point in front of NPC).
*/
public void startPreviewFiring() {
setCharging(false);
// Update start position to current position
startX = posX;
startY = posY;
startZ = posZ;
// Sync prev position to prevent visual jump on first frame
prevPosX = posX;
prevPosY = posY;
prevPosZ = posZ;
// Fire forward based on owner facing direction
Entity owner = getOwnerEntity();
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = 0; // Fire horizontally
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
motionY = -Math.sin(pitch) * getSpeed();
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
} else {
motionX = getSpeed();
motionY = 0;
motionZ = 0;
}
beginLookVectorLaunch(true);
setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0);
}
/**
@@ -169,35 +146,8 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
* Called by ability when windup ends.
*/
public void startMoving(EntityLivingBase target) {
setCharging(false);
// For player casters, snap to look vector for crosshair-aligned launch
snapToPlayerLookVector();
// Update start position to current position
startX = posX;
startY = posY;
startZ = posZ;
// Calculate velocity toward target
Entity owner = getOwnerEntity();
if (target != null) {
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
double dz = target.posZ - posZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
motionX = (dx / len) * getSpeed();
motionY = (dy / len) * getSpeed();
motionZ = (dz / len) * getSpeed();
}
} else if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
motionY = -Math.sin(pitch) * getSpeed();
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
}
beginLookVectorLaunch(false);
setMotionTowardTargetOrLookVector(target, posX, posY, posZ, getSpeed(), getSpeed(), 0, 0);
}
@Override
@@ -1,5 +1,6 @@
package kamkeel.npcs.entity;
import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData;
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
@@ -8,7 +9,6 @@ import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
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.util.AxisAlignedBB;
import net.minecraft.util.MovingObjectPosition;
@@ -156,13 +156,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
}
/**
* Update laser origin and direction from the owner's current anchor position and rotation.
* Called each tick so the laser follows the NPC when tracking a target.
* Update laser origin and direction from the owner's current look rotation.
* Called each tick so the laser origin stays centered on look vector.
*/
private void updateLaserOriginAndDirection() {
Entity owner = getOwnerEntity();
if (owner == null || !(owner instanceof EntityLivingBase)) return;
EntityLivingBase livingOwner = (EntityLivingBase) owner;
// Update direction from owner's current rotation
@@ -180,71 +179,51 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
dirZ = Math.cos(yaw) * Math.cos(pitch);
}
// Update origin: for player casters, snap to look vector so laser aligns with crosshair.
// For NPCs, use the configured anchor point.
if (owner instanceof EntityPlayer) {
double eyeY = owner.posY + owner.getEyeHeight();
float frontDist = Math.max(0.5f, laserWidth * 0.5f);
startX = owner.posX + dirX * frontDist;
startY = eyeY + dirY * frontDist;
startZ = owner.posZ + dirZ * frontDist;
} else if (anchorData != null) {
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData);
startX = pos.xCoord;
startY = pos.yCoord;
startZ = pos.zCoord;
// Keep direction normalized for consistent expansion and collision math.
Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ);
double dirLen = Math.sqrt(direction.xCoord * direction.xCoord + direction.yCoord * direction.yCoord + direction.zCoord * direction.zCoord);
if (dirLen > 0.0001) {
direction = Vec3.createVectorHelper(
direction.xCoord / dirLen,
direction.yCoord / dirLen,
direction.zCoord / dirLen
);
} else {
startX = owner.posX;
startY = owner.posY + owner.height * 0.7;
startZ = owner.posZ;
}
// Keep entity positioned at origin
prevPosX = startX;
prevPosY = startY;
prevPosZ = startZ;
setPosition(startX, startY, startZ);
}
public void startMoving(EntityLivingBase target) {
setCharging(false);
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);
this.dirX = -Math.sin(yaw) * Math.cos(pitch);
this.dirY = -Math.sin(pitch);
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
} else if (target != null) {
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight() - 0.4) - posY;
double dz = target.posZ - posZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
this.dirX = dx / len;
this.dirY = dy / len;
this.dirZ = dz / len;
Vec3 look = livingOwner.getLookVec();
if (look != null) {
direction = look;
}
}
// For player casters, snap origin to look vector so laser aligns with crosshair.
// For NPCs, use current anchor position.
if (owner instanceof EntityPlayer) {
double eyeY = owner.posY + owner.getEyeHeight();
float frontDist = Math.max(0.5f, laserWidth * 0.5f);
startX = owner.posX + dirX * frontDist;
startY = eyeY + dirY * frontDist;
startZ = owner.posZ + dirZ * frontDist;
setPosition(startX, startY, startZ);
prevPosX = startX;
prevPosY = startY;
prevPosZ = startZ;
} else {
startX = posX;
startY = posY;
startZ = posZ;
dirX = direction.xCoord;
dirY = direction.yCoord;
dirZ = direction.zCoord;
// Keep origin centered on look vector while active and clear owner bbox.
setLookVectorLaunchPosition(livingOwner, direction, false);
syncStartPositionToCurrent();
syncPositionStateToCurrent(false);
}
public void startMoving(EntityLivingBase target) {
beginLookVectorLaunch(false);
currentLength = 0.0f;
fullyExtended = false;
ticksSinceFullExtension = 0;
// Set initial direction: target-based when available, otherwise owner look vector.
Vec3 look = getOwnerLookVector();
if (!setDirectionTowardTarget(target, startX, startY, startZ)) {
if (look != null) {
dirX = look.xCoord;
dirY = look.yCoord;
dirZ = look.zCoord;
} else {
dirX = 1.0;
dirY = 0.0;
dirZ = 0.0;
}
}
// Initialize end point at start (will expand from here)
@@ -253,6 +232,20 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
this.endZ = startZ;
}
private boolean setDirectionTowardTarget(EntityLivingBase target, double sourceX, double sourceY, double sourceZ) {
if (target == null) return false;
double dx = target.posX - sourceX;
double dy = (target.posY + target.getEyeHeight() - 0.4) - sourceY;
double dz = target.posZ - sourceZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len <= 0.0001) return false;
dirX = dx / len;
dirY = dy / len;
dirZ = dz / len;
return true;
}
private void checkBlockCollision() {
MovingObjectPosition blockHit = rayTraceBlocks(startX, startY, startZ, endX, endY, endZ);
@@ -342,8 +335,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
double dirLenXZ = Math.sqrt(dirX * dirX + dirZ * dirZ);
double dot;
if (dirLenXZ > 0.001) {
dot = (vx * dirX + vz * dirZ) / (dirLenXZ * dirLenXZ) * dirLenXZ;
// Remap to full 3D parameter
dot = (vx * dirX + vz * dirZ + (entity.posY + entity.height * 0.5 - startY) * dirY);
} else {
// Laser fires nearly straight up/down - use full 3D projection
@@ -561,6 +552,24 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
return Math.max(0.0f, 1.0f - ((float) ticksSinceFullExtension / lingerTicks));
}
@Override
protected float getLaunchClearanceRadius() {
return Math.max(0.1f, laserWidth * 0.5f);
}
@Override
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
// Ensure charging visuals grow from a small orb based on laser width.
this.size = laserWidth;
this.currentLength = 0.0f;
this.fullyExtended = false;
this.ticksSinceFullExtension = 0;
super.setupCharging(anchor, chargeDuration);
this.endX = posX;
this.endY = posY;
this.endZ = posZ;
}
// ==================== NBT ====================
@Override
@@ -605,10 +614,11 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
/**
* Setup this laser in preview mode for GUI display.
* Laser doesn't have charging state - spawns at active phase and fires immediately.
* Uses charging-orb visuals during windup before firing.
*/
public void setupPreview(EntityLivingBase owner, float laserWidth, EnergyDisplayData display,
EnergyLightningData lightning, float expansionSpeed, float maxDistance) {
EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration,
float expansionSpeed, float maxDistance) {
this.setPreviewMode(true);
this.setPreviewOwner(owner);
@@ -619,28 +629,37 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
this.expansionSpeed = expansionSpeed;
this.lifespanData.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview
this.lightningData = lightning;
this.currentLength = 0.0f;
this.fullyExtended = false;
this.ticksSinceFullExtension = 0;
// Position at chest height
double x = owner.posX;
double y = owner.posY + owner.height * 0.7;
double z = owner.posZ;
this.setPosition(x, y, z);
this.prevPosX = x;
this.prevPosY = y;
this.prevPosZ = z;
this.startX = x;
this.startY = y;
this.startZ = z;
// Charge as an orb at the configured anchor, matching other energy projectiles.
setupCharging(anchor, chargeDuration);
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData);
this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
this.prevPosX = pos.xCoord;
this.prevPosY = pos.yCoord;
this.prevPosZ = pos.zCoord;
this.startX = pos.xCoord;
this.startY = pos.yCoord;
this.startZ = pos.zCoord;
this.endX = pos.xCoord;
this.endY = pos.yCoord;
this.endZ = pos.zCoord;
// Fire in owner's facing direction
float yaw = (float) Math.toRadians(owner.rotationYaw);
this.dirX = -Math.sin(yaw);
this.dirY = 0;
this.dirZ = Math.cos(yaw);
// Initialize end point (same as start, will expand)
this.endX = x;
this.endY = y;
this.endZ = z;
// Precompute initial direction from owner look for launch.
Vec3 look = owner.getLookVec();
if (look == null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
look = Vec3.createVectorHelper(
-Math.sin(yaw) * Math.cos(pitch),
-Math.sin(pitch),
Math.cos(yaw) * Math.cos(pitch)
);
}
this.dirX = look.xCoord;
this.dirY = look.yCoord;
this.dirZ = look.zCoord;
}
}
@@ -14,6 +14,9 @@ import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.LogWriter;
import java.util.Locale;
/**
* Orb projectile - spherical homing energy ball.
@@ -22,6 +25,10 @@ import net.minecraft.world.World;
* Design inspired by LouisXIV's energy attack system.
*/
public class EntityAbilityOrb extends EntityEnergyProjectile {
private static final int HOMING_STARTUP_TICKS = 4;
private static final int HOMING_RAMP_TICKS = 8;
private static final boolean DEBUG_ORB_FIRE_TRACE = false;
private static final int DEBUG_TRACE_TICKS = 14;
public EntityAbilityOrb(World world) {
super(world);
@@ -71,7 +78,7 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
this.renderCurrentSize = 0.01f;
this.prevRenderSize = 0.01f;
// Initial position at anchor point
// Initial position follows anchor helper during charging.
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData);
this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
this.prevPosX = pos.xCoord;
@@ -91,32 +98,8 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
* Start preview firing (simulates firing toward a point in front of NPC).
*/
public void startPreviewFiring() {
setCharging(false);
// Update start position to current position
startX = posX;
startY = posY;
startZ = posZ;
// Sync prev position to prevent visual jump on first frame
prevPosX = posX;
prevPosY = posY;
prevPosZ = posZ;
// Fire forward based on owner facing direction
Entity owner = getOwnerEntity();
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(0); // Fire horizontally
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
motionY = -Math.sin(pitch) * getSpeed();
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
} else {
// Default: fire forward (positive X in model space)
motionX = getSpeed();
motionY = 0;
motionZ = 0;
}
beginLookVectorLaunch(true);
setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0);
}
/**
@@ -124,35 +107,28 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
* Called by ability when windup ends.
*/
public void startMoving(EntityLivingBase target) {
setCharging(false);
// For player casters, snap to look vector for crosshair-aligned launch
snapToPlayerLookVector();
// Update start position to current position
startX = posX;
startY = posY;
startZ = posZ;
// Calculate velocity toward target
// Launch strictly along owner's look vector using shared launch origin.
Entity owner = getOwnerEntity();
if (target != null) {
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
double dz = target.posZ - posZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
motionX = (dx / len) * getSpeed();
motionY = (dy / len) * getSpeed();
motionZ = (dz / len) * getSpeed();
if (owner instanceof EntityLivingBase) {
Vec3 look = getOwnerLookVector();
if (look != null) {
debugTrace("startMoving:before", look, -1.0f);
if (beginLookVectorLaunch((EntityLivingBase) owner, look, false)) {
debugTrace("setLaunchPosition", look, -1.0f);
}
motionX = look.xCoord * getSpeed();
motionY = look.yCoord * getSpeed();
motionZ = look.zCoord * getSpeed();
debugTrace("startMoving:after", look, -1.0f);
return;
}
} else if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
motionY = -Math.sin(pitch) * getSpeed();
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
}
// Fallback if owner is unavailable.
setCharging(false);
syncStartPositionToCurrent();
setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0);
debugTrace("startMoving:fallback", null, -1.0f);
}
@Override
@@ -170,13 +146,17 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
}
if (worldObj.isRemote) {
if (shouldDebugTick()) debugTrace("client:beforeInterpolation", null, -1.0f);
handleClientInterpolation();
if (shouldDebugTick()) debugTrace("client:afterInterpolation", null, -1.0f);
} else {
// Server-side logic
if (shouldDebugTick()) debugTrace("server:beforeMovement", null, -1.0f);
updateMovement();
checkBlockCollision();
checkEntityCollision();
this.moveEntity(motionX, motionY, motionZ);
if (shouldDebugTick()) debugTrace("server:afterMove", null, -1.0f);
}
// Check wall collision
@@ -197,10 +177,28 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
if (!isTrajectoryConcluded()) {
updateTrajectory();
} else {
updateHoming();
if (ticksExisted > HOMING_STARTUP_TICKS) {
int homingTicks = ticksExisted - HOMING_STARTUP_TICKS;
if (homingTicks >= HOMING_RAMP_TICKS) {
if (shouldDebugTick()) debugTrace("updateMovement:homingFull", null, 1.0f);
updateHoming();
} else {
float ramp = homingTicks / (float) HOMING_RAMP_TICKS;
if (shouldDebugTick()) debugTrace("updateMovement:homingRamp", null, ramp);
updateHomingWithRamp(ramp);
}
}
}
}
private void updateHomingWithRamp(float ramp) {
if (ramp <= 0.0f) return;
float originalStrength = homingData.homingStrength;
homingData.homingStrength = originalStrength * ramp;
updateHoming();
homingData.homingStrength = originalStrength;
}
private void updateTrajectory() {
if (isTrajectoryConcluded()) return;
// i really fucking hate math
@@ -224,12 +222,54 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
}
private void checkEntityCollision() {
double hitSize = size * 0.5;
// Swept collision to avoid fast orbs skipping targets between ticks.
double nextX = posX + motionX;
double nextY = posY + motionY;
double nextZ = posZ + motionZ;
// Keep collision radius proportional to rendered orb size.
double hitSize = Math.max(0.05, size * 0.5);
AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox(
posX - hitSize, posY - hitSize, posZ - hitSize,
posX + hitSize, posY + hitSize, posZ + hitSize
Math.min(posX, nextX) - hitSize, Math.min(posY, nextY) - hitSize, Math.min(posZ, nextZ) - hitSize,
Math.max(posX, nextX) + hitSize, Math.max(posY, nextY) + hitSize, Math.max(posZ, nextZ) + hitSize
);
processEntitiesInHitBox(hitBox, posX, posY, posZ);
processEntitiesInHitBox(hitBox, nextX, nextY, nextZ);
}
private boolean shouldDebugTick() {
return DEBUG_ORB_FIRE_TRACE && !isCharging() && !previewMode && ticksExisted <= DEBUG_TRACE_TICKS;
}
private void debugTrace(String stage, Vec3 look, float ramp) {
if (!DEBUG_ORB_FIRE_TRACE || worldObj == null) return;
String side = worldObj.isRemote ? "CLIENT" : "SERVER";
Entity owner = getOwnerEntity();
String ownerInfo = owner == null
? "null"
: owner.getEntityId() + "@(" + fmt(owner.posX) + "," + fmt(owner.posY) + "," + fmt(owner.posZ) + ")";
String lookInfo = look == null
? "null"
: "(" + fmt(look.xCoord) + "," + fmt(look.yCoord) + "," + fmt(look.zCoord) + ")";
LogWriter.info(
"[OrbFireTrace][" + side + "][" + stage + "] "
+ "id=" + getEntityId()
+ " tick=" + ticksExisted
+ " charging=" + isCharging()
+ " start=(" + fmt(startX) + "," + fmt(startY) + "," + fmt(startZ) + ")"
+ " pos=(" + fmt(posX) + "," + fmt(posY) + "," + fmt(posZ) + ")"
+ " prev=(" + fmt(prevPosX) + "," + fmt(prevPosY) + "," + fmt(prevPosZ) + ")"
+ " motion=(" + fmt(motionX) + "," + fmt(motionY) + "," + fmt(motionZ) + ")"
+ " size=" + fmt(size)
+ " speed=" + fmt(getSpeed())
+ " ramp=" + (ramp < 0 ? "n/a" : fmt(ramp))
+ " owner=" + ownerInfo
+ " look=" + lookInfo
);
}
private static String fmt(double v) {
return String.format(Locale.ROOT, "%.4f", v);
}
// ==================== GETTERS ====================
@@ -83,6 +83,10 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit
}
}
public void setChargeDuration(int duration) {
this.chargeDuration = duration;
}
public float getChargeProgress() {
if (chargeDuration <= 0) return 1.0f;
return Math.min(1.0f, (float) chargeTick / chargeDuration);
@@ -333,6 +337,24 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit
readEnergyBaseNBT(nbt);
}
/**
* Export spawn NBT for non-world preview rendering sync.
*/
public final NBTTagCompound exportSpawnNBT() {
NBTTagCompound nbt = new NBTTagCompound();
writeSpawnNBT(nbt);
return nbt;
}
/**
* Import spawn NBT for non-world preview rendering sync.
*/
public final void importSpawnNBT(NBTTagCompound nbt) {
if (nbt != null) {
readSpawnNBT(nbt);
}
}
@Override
public void writeSpawnData(ByteBuf buffer) {
try {
@@ -516,27 +516,237 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
// ==================== LAUNCH HELPERS ====================
/**
* For player casters, snap the projectile to a position along the player's look vector.
* This ensures projectiles launch aligned with the crosshair rather than from the hand anchor.
* Only affects player casters; NPC projectiles launch from their anchor position as configured.
* Snaps the projectile position to the owner's look vector on launch.
* This ensures projectiles launch aligned with the owner's facing direction
* rather than from the last anchor point position.
* Should be called in startMoving/startFiring BEFORE setting startX/Y/Z.
*/
protected void snapToPlayerLookVector() {
protected void snapToLookVector() {
setLookVectorLaunchPosition(true);
}
/**
* Shared launch transition: exit charging, snap to look-vector launch origin,
* sync start position, and optionally reset interpolation state.
*
* @param resetInterpolationState true to reset prev/lastTick/interp state to launch origin.
* @return true when look-vector launch origin was resolved from owner/look data.
*/
protected boolean beginLookVectorLaunch(boolean resetInterpolationState) {
setCharging(false);
boolean positioned = setLookVectorLaunchPosition(true);
syncStartPositionToCurrent();
if (resetInterpolationState) {
syncPositionStateToCurrent(true);
}
return positioned;
}
/**
* Shared launch transition with explicit owner/look vector data.
*/
protected boolean beginLookVectorLaunch(EntityLivingBase owner, Vec3 look, boolean resetInterpolationState) {
setCharging(false);
boolean positioned = setLookVectorLaunchPosition(owner, look, true);
syncStartPositionToCurrent();
if (resetInterpolationState) {
syncPositionStateToCurrent(true);
}
return positioned;
}
/**
* Resolve the owner's look vector.
* Falls back to yaw/pitch math if look vector is unavailable.
*/
protected Vec3 getOwnerLookVector() {
Entity owner = getOwnerEntity();
if (!(owner instanceof EntityPlayer)) return;
if (!(owner instanceof EntityLivingBase)) return null;
Vec3 look = owner.getLookVec();
double eyeY = owner.posY + owner.getEyeHeight();
float frontDist = Math.max(0.5f, size * 0.5f);
if (look != null) return look;
double newX = owner.posX + look.xCoord * frontDist;
double newY = eyeY + look.yCoord * frontDist;
double newZ = owner.posZ + look.zCoord * frontDist;
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
return Vec3.createVectorHelper(
-Math.sin(yaw) * Math.cos(pitch),
-Math.sin(pitch),
Math.cos(yaw) * Math.cos(pitch)
);
}
/**
* Shared launch origin: owner position + chest height + look-vector offset.
*
* @param syncPositionState When true, sync prev/lastTick/interp targets to this position.
*/
protected boolean setLookVectorLaunchPosition(boolean syncPositionState) {
Entity owner = getOwnerEntity();
if (!(owner instanceof EntityLivingBase)) return false;
Vec3 look = getOwnerLookVector();
if (look == null) return false;
return setLookVectorLaunchPosition((EntityLivingBase) owner, look, syncPositionState);
}
/**
* Shared launch origin using provided owner/look data.
*/
protected boolean setLookVectorLaunchPosition(EntityLivingBase owner, Vec3 look, boolean syncPositionState) {
if (owner == null || look == null) return false;
double originX = owner.posX;
double originY = owner.posY + owner.height * 0.7;
double originZ = owner.posZ;
float frontDist = computeLaunchFrontDistance(owner, look, originX, originY, originZ);
double newX = originX + look.xCoord * frontDist;
double newY = originY + look.yCoord * frontDist;
double newZ = originZ + look.zCoord * frontDist;
setPosition(newX, newY, newZ);
prevPosX = newX;
prevPosY = newY;
prevPosZ = newZ;
if (syncPositionState) {
syncPositionState(newX, newY, newZ, true);
}
return true;
}
/**
* Radius used to clear the caster hitbox on launch.
* Subclasses may override if their effective launch footprint differs from {@code size}.
*/
protected float getLaunchClearanceRadius() {
return Math.max(0.1f, size * 0.5f);
}
private float computeLaunchFrontDistance(EntityLivingBase owner, Vec3 look, double originX, double originY, double originZ) {
// Legacy baseline offset retained as lower bound.
float baseline = size * 0.4f;
float clearance = getLaunchClearanceRadius() + 0.05f;
AxisAlignedBB bb = owner.boundingBox;
if (bb == null) {
return Math.max(baseline, owner.width * 0.5f + clearance);
}
double dx = look.xCoord;
double dy = look.yCoord;
double dz = look.zCoord;
double tx = computeRayExitT(originX, dx, bb.minX, bb.maxX);
double ty = computeRayExitT(originY, dy, bb.minY, bb.maxY);
double tz = computeRayExitT(originZ, dz, bb.minZ, bb.maxZ);
double tExit = Math.min(tx, Math.min(ty, tz));
if (Double.isInfinite(tExit) || tExit < 0) {
return Math.max(baseline, owner.width * 0.5f + clearance);
}
return (float) Math.max(baseline, tExit + clearance);
}
private double computeRayExitT(double origin, double dir, double min, double max) {
final double EPS = 1.0e-6;
if (dir > EPS) {
return (max - origin) / dir;
} else if (dir < -EPS) {
return (min - origin) / dir;
}
return Double.POSITIVE_INFINITY;
}
/**
* Set projectile motion along owner look vector.
*
* @return true if owner/look were available.
*/
protected boolean setMotionAlongLookVector(float speed) {
Vec3 look = getOwnerLookVector();
if (look == null) return false;
motionX = look.xCoord * speed;
motionY = look.yCoord * speed;
motionZ = look.zCoord * speed;
return true;
}
/**
* Set motion along owner look vector or apply explicit fallback values.
*/
protected void setMotionAlongLookVectorOrFallback(float speed, double fallbackX, double fallbackY, double fallbackZ) {
if (!setMotionAlongLookVector(speed)) {
motionX = fallbackX;
motionY = fallbackY;
motionZ = fallbackZ;
}
}
/**
* Set projectile motion toward a target from a given source point.
*
* @return true if target was valid and motion was updated.
*/
protected boolean setMotionTowardTarget(EntityLivingBase target, double sourceX, double sourceY, double sourceZ, float speed) {
if (target == null) return false;
double dx = target.posX - sourceX;
double dy = (target.posY + target.getEyeHeight()) - sourceY;
double dz = target.posZ - sourceZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len <= 0.0001) return false;
motionX = (dx / len) * speed;
motionY = (dy / len) * speed;
motionZ = (dz / len) * speed;
return true;
}
/**
* Set motion toward target from source point; when target is unavailable,
* fall back to owner look vector, then explicit fallback values.
*/
protected void setMotionTowardTargetOrLookVector(EntityLivingBase target, double sourceX, double sourceY, double sourceZ, float speed,
double fallbackX, double fallbackY, double fallbackZ) {
if (!setMotionTowardTarget(target, sourceX, sourceY, sourceZ, speed)) {
setMotionAlongLookVectorOrFallback(speed, fallbackX, fallbackY, fallbackZ);
}
}
/**
* Sync start position (distance origin) to the current entity position.
*/
protected void syncStartPositionToCurrent() {
startX = posX;
startY = posY;
startZ = posZ;
}
/**
* Sync prev/lastTick and optional interpolation state to an explicit position.
*/
protected void syncPositionState(double x, double y, double z, boolean resetInterpolation) {
prevPosX = x;
prevPosY = y;
prevPosZ = z;
lastTickPosX = x;
lastTickPosY = y;
lastTickPosZ = z;
if (resetInterpolation) {
interpTargetX = x;
interpTargetY = y;
interpTargetZ = z;
interpTargetMotionX = motionX;
interpTargetMotionY = motionY;
interpTargetMotionZ = motionZ;
interpSteps = 0;
}
}
/**
* Sync prev/lastTick and optional interpolation state to current entity position.
*/
protected void syncPositionStateToCurrent(boolean resetInterpolation) {
syncPositionState(posX, posY, posZ, resetInterpolation);
}
// ==================== CHARGING METHODS (projectile-specific) ====================
@@ -1118,6 +1328,23 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
this.motionY = nbt.getDouble("MotionY");
this.motionZ = nbt.getDouble("MotionZ");
// Authoritative launch origin comes from spawn NBT (set during fire/startMoving).
// Sync full client position state to this origin to avoid first-tick spawn/interp pops.
this.setPosition(this.startX, this.startY, this.startZ);
this.prevPosX = this.startX;
this.prevPosY = this.startY;
this.prevPosZ = this.startZ;
this.lastTickPosX = this.startX;
this.lastTickPosY = this.startY;
this.lastTickPosZ = this.startZ;
this.interpTargetX = this.startX;
this.interpTargetY = this.startY;
this.interpTargetZ = this.startZ;
this.interpTargetMotionX = this.motionX;
this.interpTargetMotionY = this.motionY;
this.interpTargetMotionZ = this.motionZ;
this.interpSteps = 0;
// Always initialize render size from NBT size so entities
// loading in from outside render distance render correctly
this.renderCurrentSize = this.size;
@@ -102,55 +102,16 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
* Start preview firing.
*/
public void startPreviewFiring() {
setCharging(false);
startX = posX;
startY = posY;
startZ = posZ;
prevPosX = posX;
prevPosY = posY;
prevPosZ = posZ;
Entity owner = getOwnerEntity();
if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
motionX = -Math.sin(yaw) * getSpeed();
motionY = 0;
motionZ = Math.cos(yaw) * getSpeed();
} else {
motionX = getSpeed();
motionY = 0;
motionZ = 0;
}
beginLookVectorLaunch(true);
setMotionAlongLookVectorOrFallback(getSpeed(), getSpeed(), 0, 0);
}
/**
* Start the slicer moving (exit charging mode).
*/
public void startMoving(EntityLivingBase target) {
setCharging(false);
snapToPlayerLookVector();
startX = posX;
startY = posY;
startZ = posZ;
Entity owner = getOwnerEntity();
if (target != null) {
double dx = target.posX - posX;
double dy = (target.posY + target.getEyeHeight()) - posY;
double dz = target.posZ - posZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
motionX = (dx / len) * getSpeed();
motionY = (dy / len) * getSpeed();
motionZ = (dz / len) * getSpeed();
}
} else if (owner != null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
float pitch = (float) Math.toRadians(owner.rotationPitch);
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
motionY = -Math.sin(pitch) * getSpeed();
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
}
beginLookVectorLaunch(false);
setMotionTowardTargetOrLookVector(target, posX, posY, posZ, getSpeed(), getSpeed(), 0, 0);
}
@Override
@@ -36,6 +36,8 @@ import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket;
import kamkeel.npcs.network.packets.data.script.ScriptOverlayClosePacket;
import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket;
import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket;
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeRemovePacket;
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeSpawnPacket;
import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket;
import kamkeel.npcs.network.packets.player.AuctionActionPacket;
@@ -585,6 +587,8 @@ public class PacketHandler {
// Data | Ability Packets
DATA_PACKET.registerPacket(new TelegraphSpawnPacket());
DATA_PACKET.registerPacket(new TelegraphRemovePacket());
DATA_PACKET.registerPacket(new EnergyChargeSpawnPacket());
DATA_PACKET.registerPacket(new EnergyChargeRemovePacket());
DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket());
DATA_PACKET.registerPacket(new PlayerAbilityStatePacket());
DATA_PACKET.registerPacket(new AbilityHotbarSyncPacket());
@@ -70,4 +70,8 @@ public enum EnumDataPacket {
// Auction System
AUCTION_DATA,
// Energy Charge Preview
ENERGY_CHARGE_SPAWN,
ENERGY_CHARGE_REMOVE,
}
@@ -0,0 +1,60 @@
package kamkeel.npcs.network.packets.data.energycharge;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumDataPacket;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import java.io.IOException;
/**
* Removes a client-only charging preview entity.
*/
public final class EnergyChargeRemovePacket extends AbstractPacket {
public static final String packetName = "Data|EnergyChargeRemove";
private String instanceId;
public EnergyChargeRemovePacket() {
}
public EnergyChargeRemovePacket(String instanceId) {
this.instanceId = instanceId;
}
@Override
public Enum getType() {
return EnumDataPacket.ENERGY_CHARGE_REMOVE;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.DATA_PACKET;
}
@Override
public void sendData(ByteBuf out) throws IOException {
ByteBufUtils.writeString(out, instanceId);
}
@SideOnly(Side.CLIENT)
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
String id = ByteBufUtils.readString(in);
if (id != null && EnergyChargePreviewManager.ClientInstance != null) {
EnergyChargePreviewManager.ClientInstance.removePreview(id);
}
}
public static void sendToTracking(String instanceId, Entity trackingEntity) {
if (instanceId == null || trackingEntity == null) return;
PacketHandler.Instance.sendTracking(new EnergyChargeRemovePacket(instanceId), trackingEntity);
}
}
@@ -0,0 +1,108 @@
package kamkeel.npcs.network.packets.data.energycharge;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumDataPacket;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
import java.io.IOException;
import java.lang.reflect.Constructor;
/**
* Spawns a client-only charging preview entity from serialized spawn data.
*/
public final class EnergyChargeSpawnPacket extends AbstractPacket {
public static final String packetName = "Data|EnergyChargeSpawn";
private String instanceId;
private String entityClassName;
private NBTTagCompound spawnNbt;
public EnergyChargeSpawnPacket() {
}
public EnergyChargeSpawnPacket(String instanceId, String entityClassName, NBTTagCompound spawnNbt) {
this.instanceId = instanceId;
this.entityClassName = entityClassName;
this.spawnNbt = spawnNbt;
}
@Override
public Enum getType() {
return EnumDataPacket.ENERGY_CHARGE_SPAWN;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.DATA_PACKET;
}
@Override
public void sendData(ByteBuf out) throws IOException {
ByteBufUtils.writeString(out, instanceId);
ByteBufUtils.writeString(out, entityClassName);
ByteBufUtils.writeNBT(out, spawnNbt);
}
@SideOnly(Side.CLIENT)
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
String id = ByteBufUtils.readString(in);
String className = ByteBufUtils.readString(in);
NBTTagCompound nbt = ByteBufUtils.readNBT(in);
if (id == null || className == null || nbt == null) return;
if (EnergyChargePreviewManager.ClientInstance == null) return;
World world = Minecraft.getMinecraft().theWorld;
if (world == null) return;
EntityEnergyProjectile preview = createPreviewEntity(className, world);
if (preview == null) return;
preview.setPreviewMode(true);
preview.importSpawnNBT(nbt);
// Ensure the preview follows current owner immediately on first render frame.
Entity owner = preview.getOwnerEntity();
if (owner instanceof net.minecraft.entity.EntityLivingBase) {
preview.setPreviewOwner((net.minecraft.entity.EntityLivingBase) owner);
}
EnergyChargePreviewManager.ClientInstance.addPreview(id, preview);
}
@SideOnly(Side.CLIENT)
private static EntityEnergyProjectile createPreviewEntity(String className, World world) {
try {
Class<?> clazz = Class.forName(className);
if (!EntityEnergyProjectile.class.isAssignableFrom(clazz)) {
return null;
}
Constructor<?> constructor = clazz.getConstructor(World.class);
Object instance = constructor.newInstance(world);
return (EntityEnergyProjectile) instance;
} catch (Exception ignored) {
return null;
}
}
public static void sendToTracking(String instanceId, EntityEnergyProjectile previewEntity, Entity trackingEntity) {
if (previewEntity == null || trackingEntity == null) return;
String className = previewEntity.getClass().getName();
NBTTagCompound nbt = previewEntity.exportSpawnNBT();
PacketHandler.Instance.sendTracking(new EnergyChargeSpawnPacket(instanceId, className, nbt), trackingEntity);
}
}
@@ -14,9 +14,11 @@ import kamkeel.npcs.client.renderer.RenderAbilitySweeper;
import kamkeel.npcs.client.renderer.RenderAbilityZone;
import kamkeel.npcs.client.renderer.RenderAbilityDome;
import kamkeel.npcs.client.renderer.RenderAbilityPanel;
import kamkeel.npcs.client.renderer.EnergyChargePreviewRenderer;
import kamkeel.npcs.client.renderer.RenderEnergySlicer;
import kamkeel.npcs.client.renderer.TelegraphRenderer;
import kamkeel.npcs.client.renderer.lightning.LightningHandler;
import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager;
import kamkeel.npcs.controllers.data.telegraph.TelegraphManager;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityAbilityDisc;
@@ -356,6 +358,10 @@ public class ClientProxy extends CommonProxy {
TelegraphManager.initClient();
MinecraftForge.EVENT_BUS.register(new TelegraphRenderer());
// Energy charge preview rendering system
EnergyChargePreviewManager.initClient();
MinecraftForge.EVENT_BUS.register(new EnergyChargePreviewRenderer());
// Lightning effect rendering system
MinecraftForge.EVENT_BUS.register(new LightningHandler());
@@ -8,6 +8,7 @@ import cpw.mods.fml.common.gameevent.TickEvent.Phase;
import cpw.mods.fml.relauncher.Side;
import kamkeel.npcs.client.renderer.lightning.LightningBolt;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager;
import kamkeel.npcs.controllers.data.telegraph.TelegraphManager;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.data.RequestProperSpawnData;
@@ -62,6 +63,9 @@ public class ClientTickHandler {
ClientCacheHandler.clearCache();
ClientAbilityState.reset();
RequestProperSpawnData.clear();
if (EnergyChargePreviewManager.ClientInstance != null) {
EnergyChargePreviewManager.ClientInstance.clear();
}
}
this.prevWorld = mc.theWorld;
}
@@ -164,6 +168,11 @@ public class ClientTickHandler {
TelegraphManager.ClientInstance.tick(mc.theWorld);
}
// Tick packet-driven energy charge previews
if (EnergyChargePreviewManager.ClientInstance != null) {
EnergyChargePreviewManager.ClientInstance.tick(mc.theWorld);
}
// Update lightning bolts for ability effects
LightningBolt.updateAll();
@@ -1834,12 +1834,15 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
try {
readSpawnData(ByteBufUtils.readNBT(buf));
} catch (IOException e) {
RequestProperSpawnData.reportMissingData(this);
if(this.worldObj != null && this.worldObj.isRemote){
RequestProperSpawnData.reportMissingData(this);
}
}
}
public void readSpawnData(NBTTagCompound compound) {
if ((compound.hasNoTags() || !compound.hasKey("MaxHealth", Constants.NBT.TAG_DOUBLE))) {
if (this.worldObj != null && this.worldObj.isRemote &&
(compound.hasNoTags() || !compound.hasKey("MaxHealth", Constants.NBT.TAG_DOUBLE))) {
RequestProperSpawnData.reportMissingData(this);
return;
}