mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 00:47:50 +00:00
Fix Preview Handling for Abilities, Fix Disc Positioning in Real World
This commit is contained in:
1 parent
70583743ce
commit
2e5c4c3bf4
29 files changed
+714
-801
No files matched your search
@@ -110,6 +110,13 @@ public abstract class Ability implements IAbility {
|
||||
protected transient long executionStartTime;
|
||||
protected transient TelegraphInstance telegraphInstance;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// PREVIEW STATE (client-side only, not saved)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
protected transient boolean previewMode = false;
|
||||
protected transient PreviewEntityHandler previewEntityHandler;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ABSTRACT METHODS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -837,6 +844,8 @@ public abstract class Ability implements IAbility {
|
||||
currentTick = 0;
|
||||
currentTarget = null;
|
||||
telegraphInstance = null;
|
||||
previewMode = false;
|
||||
previewEntityHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -849,90 +858,57 @@ public abstract class Ability implements IAbility {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// CLIENT-SIDE PREVIEW METHODS
|
||||
// PREVIEW MODE SUPPORT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Get total active phase duration for preview (in ticks).
|
||||
* Override in subclasses to provide accurate preview duration.
|
||||
*
|
||||
* @return Duration of active phase in ticks (default: 40 = 2 seconds)
|
||||
* Whether this ability is running in preview mode (GUI preview).
|
||||
* When true, abilities should skip damage, effects, sounds, and particles
|
||||
* but still run their core logic (entity spawning, movement, timing).
|
||||
*/
|
||||
public int getPreviewActiveDuration() {
|
||||
return 40;
|
||||
public boolean isPreview() {
|
||||
return previewMode;
|
||||
}
|
||||
|
||||
public void setPreviewMode(boolean preview) {
|
||||
this.previewMode = preview;
|
||||
}
|
||||
|
||||
public void setPreviewEntityHandler(PreviewEntityHandler handler) {
|
||||
this.previewEntityHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during preview windup tick (client-side only).
|
||||
* Override for visual updates during windup preview.
|
||||
* Do NOT apply damage or world effects here.
|
||||
*
|
||||
* @param npc The preview NPC
|
||||
* @param tick Current tick within windup phase
|
||||
* Spawn an entity during ability execution.
|
||||
* In preview mode, routes to PreviewEntityHandler instead of world.spawnEntityInWorld().
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewWindUpTick(EntityNPCInterface npc, int tick) {
|
||||
// Default: no-op. Override in subclasses for visual effects.
|
||||
protected void spawnAbilityEntity(World world, Entity entity) {
|
||||
if (previewMode && previewEntityHandler != null) {
|
||||
previewEntityHandler.onEntitySpawned(entity);
|
||||
} else {
|
||||
world.spawnEntityInWorld(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during preview active tick (client-side only).
|
||||
* Override for visual updates during active preview.
|
||||
* Do NOT apply damage or world effects here.
|
||||
*
|
||||
* @param npc The preview NPC
|
||||
* @param tick Current tick within active phase
|
||||
* Kill an entity spawned by this ability.
|
||||
* In preview mode, notifies PreviewEntityHandler of removal.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
|
||||
// Default: no-op. Override in subclasses for visual effects.
|
||||
protected void killAbilityEntity(Entity entity) {
|
||||
if (entity == null) return;
|
||||
entity.setDead();
|
||||
if (previewMode && previewEntityHandler != null) {
|
||||
previewEntityHandler.onEntityRemoved(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a preview entity for GUI display (client-side only).
|
||||
* The entity should be in preview mode and not apply damage.
|
||||
* Override in abilities that spawn entities (Beam, Orb, Disc, etc.)
|
||||
*
|
||||
* @param npc The preview NPC
|
||||
* @return Preview entity, or null if ability doesn't spawn entities
|
||||
* Maximum duration (in ticks) for preview before auto-stopping.
|
||||
* Override in movement abilities that have variable duration.
|
||||
* Default: 200 ticks (10 seconds).
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
return null; // Override in entity-spawning abilities
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to spawn preview entity during WINDUP phase (true) or ACTIVE phase (false).
|
||||
* Most abilities (Orb, Beam, Disc) spawn during windup for charging effect.
|
||||
* Laser spawns at active phase since it has no charging state.
|
||||
*/
|
||||
public boolean spawnPreviewDuringWindup() {
|
||||
return true; // Default: spawn during windup for charging
|
||||
}
|
||||
|
||||
// ── Preview target (set by executor for abilities that need targeting) ──
|
||||
|
||||
protected transient EntityLivingBase previewTarget;
|
||||
|
||||
/**
|
||||
* Set the fake target entity for preview mode.
|
||||
* Called by AbilityPreviewExecutor before starting preview.
|
||||
*/
|
||||
public void setPreviewTarget(EntityLivingBase target) {
|
||||
this.previewTarget = target;
|
||||
}
|
||||
|
||||
public EntityLivingBase getPreviewTarget() {
|
||||
return previewTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once when transitioning from WINDUP to ACTIVE in preview mode.
|
||||
* Override in movement-based abilities to initiate movement.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewExecute(EntityNPCInterface npc) {
|
||||
// Default: no-op. Override in movement abilities.
|
||||
public int getMaxPreviewDuration() {
|
||||
return 200;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
|
||||
/**
|
||||
* Interface for routing entity spawn/kill calls during preview mode.
|
||||
* In preview mode, entities are not added to the world but instead
|
||||
* tracked by the GUI for rendering in the 3D viewport.
|
||||
*/
|
||||
public interface PreviewEntityHandler {
|
||||
void onEntitySpawned(Entity entity);
|
||||
void onEntityRemoved(Entity entity);
|
||||
}
|
||||
@@ -76,29 +76,29 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start firing the beam that was spawned during windup
|
||||
if (beamEntity != null && !beamEntity.isDead) {
|
||||
beamEntity.startFiring(target);
|
||||
if (isPreview()) {
|
||||
beamEntity.startPreviewFiring();
|
||||
} else {
|
||||
beamEntity.startFiring(target);
|
||||
}
|
||||
}
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Spawn beam in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
float offsetDist = 1.0f;
|
||||
|
||||
// Create beam in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist);
|
||||
beamEntity = new EntityAbilityBeam(
|
||||
world, caster, target,
|
||||
@@ -106,11 +106,16 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
beamWidth, headSize,
|
||||
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData,
|
||||
lockMovement.locksActive());
|
||||
beamEntity.setupCharging(anchorData, windUpTicks, offsetDist);
|
||||
|
||||
if (isPreview()) {
|
||||
beamEntity.setupPreview(caster, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, offsetDist);
|
||||
} else {
|
||||
beamEntity.setupCharging(anchorData, windUpTicks, offsetDist);
|
||||
}
|
||||
|
||||
beamEntity.setEffects(this.effects);
|
||||
beamEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(beamEntity);
|
||||
spawnAbilityEntity(world, beamEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,17 +272,7 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj);
|
||||
beam.setupPreview(npc, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, 1.0f);
|
||||
return beam;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,37 +81,41 @@ public class AbilityBeamDual extends Ability {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start firing the beam that was spawned during windup
|
||||
if (beamEntity != null && !beamEntity.isDead) {
|
||||
beamEntity.startFiring(target);
|
||||
if (isPreview()) {
|
||||
beamEntity.startPreviewFiring();
|
||||
} else {
|
||||
beamEntity.startFiring(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (dualFire) {
|
||||
if (beamEntity2 != null && !beamEntity2.isDead && dualFireDelay <= 0) {
|
||||
beamEntity2.startFiring(target);
|
||||
if (isPreview()) {
|
||||
beamEntity2.startPreviewFiring();
|
||||
} else {
|
||||
beamEntity2.startFiring(target);
|
||||
}
|
||||
}
|
||||
} else if (beamEntity2 != null) {
|
||||
beamEntity2.setDead();
|
||||
}
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Spawn beam in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
float offsetDist = 1.0f;
|
||||
|
||||
// Create beam in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0], offsetDist);
|
||||
beamEntity = new EntityAbilityBeam(
|
||||
world, caster, target,
|
||||
@@ -131,16 +135,21 @@ public class AbilityBeamDual extends Ability {
|
||||
beamEntity.setSiblingEntityId(beamEntity2.getEntityId());
|
||||
beamEntity2.setSiblingEntityId(beamEntity.getEntityId());
|
||||
|
||||
beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist);
|
||||
beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist);
|
||||
if (isPreview()) {
|
||||
beamEntity.setupPreview(caster, beamWidth, headSize, displayData[0], lightningData[0], anchorData[0], windUpTicks, offsetDist);
|
||||
beamEntity2.setupPreview(caster, beamWidth, headSize, displayData[1], lightningData[1], anchorData[1], windUpTicks, offsetDist);
|
||||
} else {
|
||||
beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist);
|
||||
beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist);
|
||||
}
|
||||
|
||||
beamEntity.setEffects(this.effects);
|
||||
if (dualFire) beamEntity2.setEffects(this.effects);
|
||||
|
||||
beamEntity.setSourceAbility(this);
|
||||
beamEntity2.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(beamEntity);
|
||||
world.spawnEntityInWorld(beamEntity2);
|
||||
spawnAbilityEntity(world, beamEntity);
|
||||
spawnAbilityEntity(world, beamEntity2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -501,17 +510,7 @@ public class AbilityBeamDual extends Ability {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj);
|
||||
beam.setupPreview(npc, beamWidth, headSize, displayData[0], lightningData[0], anchorData[0], windUpTicks, 1.0f);
|
||||
return beam;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
}
|
||||
@@ -138,7 +138,9 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
if (chargeDirection == null) return;
|
||||
|
||||
// Enforce rotation every tick
|
||||
enforceLockedRotation(caster);
|
||||
if (!isPreview()) {
|
||||
enforceLockedRotation(caster);
|
||||
}
|
||||
|
||||
// Calculate distance traveled
|
||||
double distanceTraveled = Math.sqrt(
|
||||
@@ -150,12 +152,15 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
if (distanceTraveled >= maxRange) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isChargeBlocked(caster)) {
|
||||
// Block detection (skip in preview - no real world collision)
|
||||
if (!isPreview() && isChargeBlocked(caster)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
@@ -165,10 +170,12 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
caster.motionX = chargeDirection.xCoord * chargeSpeed;
|
||||
caster.motionY = 0;
|
||||
caster.motionZ = chargeDirection.zCoord * chargeSpeed;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
|
||||
// Server-side collision damage
|
||||
if (!world.isRemote) {
|
||||
// Server-side collision damage (skip in preview)
|
||||
if (!world.isRemote && !isPreview()) {
|
||||
AxisAlignedBB hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -211,7 +218,9 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
private void stopMomentum(EntityLivingBase caster) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isChargeBlocked(EntityLivingBase caster) {
|
||||
@@ -277,66 +286,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ==================== PREVIEW MODE ====================
|
||||
|
||||
private transient double previewDirX, previewDirZ;
|
||||
private transient double previewStartX, previewStartZ;
|
||||
private transient boolean previewCharging = false;
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewExecute(EntityNPCInterface npc) {
|
||||
previewStartX = npc.posX;
|
||||
previewStartZ = npc.posZ;
|
||||
previewCharging = false;
|
||||
|
||||
// Calculate direction toward fake target
|
||||
if (previewTarget != null) {
|
||||
double dx = previewTarget.posX - npc.posX;
|
||||
double dz = previewTarget.posZ - npc.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
if (len > 0) {
|
||||
previewDirX = dx / len;
|
||||
previewDirZ = dz / len;
|
||||
} else {
|
||||
float yaw = (float) Math.toRadians(npc.rotationYaw);
|
||||
previewDirX = -Math.sin(yaw);
|
||||
previewDirZ = Math.cos(yaw);
|
||||
}
|
||||
} else {
|
||||
float yaw = (float) Math.toRadians(npc.rotationYaw);
|
||||
previewDirX = -Math.sin(yaw);
|
||||
previewDirZ = Math.cos(yaw);
|
||||
}
|
||||
previewCharging = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
|
||||
if (!previewCharging) return;
|
||||
|
||||
// Check distance traveled
|
||||
double distTraveled = Math.sqrt(
|
||||
Math.pow(npc.posX - previewStartX, 2) +
|
||||
Math.pow(npc.posZ - previewStartZ, 2)
|
||||
);
|
||||
|
||||
if (distTraveled >= maxRange) {
|
||||
previewCharging = false;
|
||||
return;
|
||||
}
|
||||
|
||||
npc.prevPosX = npc.posX;
|
||||
npc.prevPosY = npc.posY;
|
||||
npc.prevPosZ = npc.posZ;
|
||||
|
||||
npc.posX += previewDirX * chargeSpeed;
|
||||
npc.posZ += previewDirZ * chargeSpeed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return (int) Math.ceil(maxRange / chargeSpeed) + 5;
|
||||
}
|
||||
|
||||
|
||||
@@ -109,31 +109,35 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
switch (sweepMode) {
|
||||
case SWIPE:
|
||||
float prevRotation = currentRotation;
|
||||
currentRotation += sweepSpeed;
|
||||
if (currentRotation > arcAngle / 2.0f) {
|
||||
// Final sweep — check from prevRotation to end of arc
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f);
|
||||
if (!isPreview()) {
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f);
|
||||
}
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
// Check entities between previous and current sweep position
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation);
|
||||
if (!isPreview()) {
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation);
|
||||
}
|
||||
break;
|
||||
|
||||
case SPIN:
|
||||
if (tick >= spinDurationTicks) {
|
||||
signalCompletion(); // Spin duration complete
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
float prevSpin = currentRotation;
|
||||
currentRotation = (currentRotation + sweepSpeed) % 360.0f;
|
||||
hitEntities.clear();
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation);
|
||||
if (!isPreview()) {
|
||||
hitEntities.clear();
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
if (dashDirection == null) return;
|
||||
|
||||
// Stall detection: if entity hasn't moved since last tick, it's stuck against a wall
|
||||
if (tick > 1) {
|
||||
if (!isPreview() && tick > 1) {
|
||||
double movedThisTick = Math.sqrt(
|
||||
Math.pow(caster.posX - prevTickX, 2) +
|
||||
Math.pow(caster.posZ - prevTickZ, 2));
|
||||
@@ -197,12 +197,15 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
if (distanceTraveled >= dashDistance) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDashBlocked(caster)) {
|
||||
// Block detection (skip in preview - no real world collision)
|
||||
if (!isPreview() && isDashBlocked(caster)) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
@@ -214,24 +217,30 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
caster.motionX = dashDirection.xCoord * dashSpeed;
|
||||
caster.motionY = 0;
|
||||
caster.motionZ = dashDirection.zCoord * dashSpeed;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
|
||||
// Trail particles
|
||||
world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0);
|
||||
// Trail particles
|
||||
world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -247,66 +256,8 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
return !caster.worldObj.getCollidingBoundingBoxes(caster, caster.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty();
|
||||
}
|
||||
|
||||
// ==================== PREVIEW MODE ====================
|
||||
|
||||
private transient double previewDirX, previewDirZ;
|
||||
private transient double previewStartX, previewStartZ;
|
||||
private transient boolean previewDashing = false;
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewExecute(EntityNPCInterface npc) {
|
||||
previewStartX = npc.posX;
|
||||
previewStartZ = npc.posZ;
|
||||
previewDashing = false;
|
||||
|
||||
// Pick a random direction based on mode
|
||||
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
|
||||
? AGGRESSIVE_DIRECTIONS
|
||||
: DEFENSIVE_DIRECTIONS;
|
||||
DashDirection dir = directions[RANDOM.nextInt(directions.length)];
|
||||
|
||||
float baseYaw;
|
||||
if (previewTarget != null) {
|
||||
double dx = previewTarget.posX - npc.posX;
|
||||
double dz = previewTarget.posZ - npc.posZ;
|
||||
baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
} else {
|
||||
baseYaw = npc.rotationYaw;
|
||||
}
|
||||
|
||||
float dashYaw = baseYaw + dir.getAngleOffset();
|
||||
float yawRad = (float) Math.toRadians(dashYaw);
|
||||
previewDirX = -Math.sin(yawRad);
|
||||
previewDirZ = Math.cos(yawRad);
|
||||
previewDashing = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
|
||||
if (!previewDashing) return;
|
||||
|
||||
double distTraveled = Math.sqrt(
|
||||
Math.pow(npc.posX - previewStartX, 2) +
|
||||
Math.pow(npc.posZ - previewStartZ, 2)
|
||||
);
|
||||
|
||||
if (distTraveled >= dashDistance) {
|
||||
previewDashing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
npc.prevPosX = npc.posX;
|
||||
npc.prevPosY = npc.posY;
|
||||
npc.prevPosZ = npc.posZ;
|
||||
|
||||
npc.posX += previewDirX * dashSpeed;
|
||||
npc.posZ += previewDirZ * dashSpeed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return (int) Math.ceil(dashDistance / dashSpeed) + 5;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.Vec3;
|
||||
@@ -23,7 +22,6 @@ import noppes.npcs.api.ability.type.IAbilityDisc;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
/**
|
||||
* Disc ability: Spawns a flat spinning disc projectile.
|
||||
@@ -79,27 +77,27 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start moving the disc that was spawned during windup
|
||||
if (discEntity != null && !discEntity.isDead) {
|
||||
discEntity.startMoving(target);
|
||||
if (isPreview()) {
|
||||
discEntity.startPreviewFiring();
|
||||
} else {
|
||||
discEntity.startMoving(target);
|
||||
}
|
||||
}
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Spawn disc in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
// Create disc in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
|
||||
discEntity = new EntityAbilityDisc(
|
||||
world, caster, target,
|
||||
@@ -107,11 +105,16 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
|
||||
discRadius, discThickness,
|
||||
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData,
|
||||
boomerang, boomerangDelay);
|
||||
discEntity.setupCharging(anchorData, windUpTicks);
|
||||
|
||||
if (isPreview()) {
|
||||
discEntity.setupPreview(caster, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks);
|
||||
} else {
|
||||
discEntity.setupCharging(anchorData, windUpTicks);
|
||||
}
|
||||
|
||||
discEntity.setEffects(this.effects);
|
||||
discEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(discEntity);
|
||||
spawnAbilityEntity(world, discEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,17 +267,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
|
||||
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj);
|
||||
disc.setupPreview(npc, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks);
|
||||
return disc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,10 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import noppes.npcs.util.ValueUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
@@ -89,19 +87,27 @@ public class AbilityDiscDual extends Ability {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start moving the orb that was spawned during windup
|
||||
// Start moving the disc that was spawned during windup
|
||||
if (discEntity != null && !discEntity.isDead) {
|
||||
discEntity.startMoving(target);
|
||||
if (isPreview()) {
|
||||
discEntity.startPreviewFiring();
|
||||
} else {
|
||||
discEntity.startMoving(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (dualFire) {
|
||||
if (discEntity2 != null && !discEntity2.isDead && dualFireDelay <= 0) {
|
||||
discEntity2.startMoving(target);
|
||||
if (isPreview()) {
|
||||
discEntity2.startPreviewFiring();
|
||||
} else {
|
||||
discEntity2.startMoving(target);
|
||||
}
|
||||
}
|
||||
} else if (discEntity2 != null) {
|
||||
discEntity2.setDead();
|
||||
@@ -113,7 +119,7 @@ public class AbilityDiscDual extends Ability {
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Spawn disc in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
@@ -137,8 +143,13 @@ public class AbilityDiscDual extends Ability {
|
||||
discEntity.setSiblingEntityId(discEntity2.getEntityId());
|
||||
discEntity2.setSiblingEntityId(discEntity.getEntityId());
|
||||
|
||||
discEntity.setupCharging(anchorData[0], windUpTicks);
|
||||
discEntity2.setupCharging(anchorData[1], windUpTicks);
|
||||
if (isPreview()) {
|
||||
discEntity.setupPreview(caster, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks);
|
||||
discEntity2.setupPreview(caster, discRadius, discThickness, displayData[1], lightningData[1], anchorData[1], windUpTicks);
|
||||
} else {
|
||||
discEntity.setupCharging(anchorData[0], windUpTicks);
|
||||
discEntity2.setupCharging(anchorData[1], windUpTicks);
|
||||
}
|
||||
|
||||
discEntity.setEffects(this.effects);
|
||||
|
||||
@@ -148,8 +159,8 @@ public class AbilityDiscDual extends Ability {
|
||||
|
||||
discEntity.setSourceAbility(this);
|
||||
discEntity2.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(discEntity);
|
||||
world.spawnEntityInWorld(discEntity2);
|
||||
spawnAbilityEntity(world, discEntity);
|
||||
spawnAbilityEntity(world, discEntity2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +175,11 @@ public class AbilityDiscDual extends Ability {
|
||||
}
|
||||
|
||||
if (tick == dualFireDelay && discEntity2 != null && !discEntity2.isDead) {
|
||||
discEntity2.startMoving(target);
|
||||
if (isPreview()) {
|
||||
discEntity2.startPreviewFiring();
|
||||
} else {
|
||||
discEntity2.startMoving(target);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (discEntity == null || discEntity.isDead) {
|
||||
@@ -511,17 +526,7 @@ public class AbilityDiscDual extends Ability {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj);
|
||||
disc.setupPreview(npc, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks);
|
||||
return disc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
}
|
||||
@@ -243,7 +243,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Check if hazard duration has ended
|
||||
if (tick >= durationTicks) {
|
||||
@@ -251,53 +251,55 @@ public class AbilityHazard extends Ability implements IAbilityHazard {
|
||||
return;
|
||||
}
|
||||
|
||||
damagedThisTick.clear();
|
||||
ticksSinceDamage++;
|
||||
if (!isPreview()) {
|
||||
damagedThisTick.clear();
|
||||
ticksSinceDamage++;
|
||||
|
||||
switch (placement) {
|
||||
case FOLLOW_CASTER:
|
||||
zoneX = caster.posX;
|
||||
zoneY = caster.posY;
|
||||
zoneZ = caster.posZ;
|
||||
break;
|
||||
case FOLLOW_TARGET:
|
||||
if (target != null) {
|
||||
zoneX = target.posX;
|
||||
zoneY = target.posY;
|
||||
zoneZ = target.posZ;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (ticksSinceDamage >= damageInterval) {
|
||||
ticksSinceDamage = 0;
|
||||
|
||||
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
|
||||
zoneX - radius, zoneY - heightBelow, zoneZ - radius,
|
||||
zoneX + radius, zoneY + heightAbove, zoneZ + radius
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster && !affectsCaster) continue;
|
||||
if (damagedThisTick.contains(entity.getEntityId())) continue;
|
||||
if (!isInZone(entity, caster)) continue;
|
||||
|
||||
if (damagePerSecond > 0) {
|
||||
if (ignoreInvulnFrames) {
|
||||
entity.hurtResistantTime = 0;
|
||||
switch (placement) {
|
||||
case FOLLOW_CASTER:
|
||||
zoneX = caster.posX;
|
||||
zoneY = caster.posY;
|
||||
zoneZ = caster.posZ;
|
||||
break;
|
||||
case FOLLOW_TARGET:
|
||||
if (target != null) {
|
||||
zoneX = target.posX;
|
||||
zoneY = target.posY;
|
||||
zoneZ = target.posZ;
|
||||
}
|
||||
// Apply damage with scripted event support (no knockback for hazard)
|
||||
boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0);
|
||||
if (!wasHit) continue; // Skip debuffs if hit was cancelled
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
applyDebuffs(entity);
|
||||
damagedThisTick.add(entity.getEntityId());
|
||||
if (ticksSinceDamage >= damageInterval) {
|
||||
ticksSinceDamage = 0;
|
||||
|
||||
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
|
||||
zoneX - radius, zoneY - heightBelow, zoneZ - radius,
|
||||
zoneX + radius, zoneY + heightAbove, zoneZ + radius
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster && !affectsCaster) continue;
|
||||
if (damagedThisTick.contains(entity.getEntityId())) continue;
|
||||
if (!isInZone(entity, caster)) continue;
|
||||
|
||||
if (damagePerSecond > 0) {
|
||||
if (ignoreInvulnFrames) {
|
||||
entity.hurtResistantTime = 0;
|
||||
}
|
||||
// Apply damage with scripted event support (no knockback for hazard)
|
||||
boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0);
|
||||
if (!wasHit) continue; // Skip debuffs if hit was cancelled
|
||||
}
|
||||
|
||||
applyDebuffs(entity);
|
||||
damagedThisTick.add(entity.getEntityId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,27 +75,31 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
getHealedAllies().clear();
|
||||
if (!isPreview()) {
|
||||
getHealedAllies().clear();
|
||||
|
||||
// Always find allies if we're healing them (needed for both instant and HoT)
|
||||
if (healAllies && healRadius > 0) {
|
||||
findAlliesInRadius(caster, world);
|
||||
// Always find allies if we're healing them (needed for both instant and HoT)
|
||||
if (healAllies && healRadius > 0) {
|
||||
findAlliesInRadius(caster, world);
|
||||
}
|
||||
|
||||
if (instantHeal) {
|
||||
// Instant heal - apply all healing now
|
||||
if (healSelf) {
|
||||
healEntity(caster);
|
||||
spawnHealParticles(world, caster);
|
||||
}
|
||||
|
||||
for (EntityLivingBase ally : getHealedAllies()) {
|
||||
healEntity(ally);
|
||||
spawnHealParticles(world, ally);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (instantHeal) {
|
||||
// Instant heal - apply all healing now
|
||||
if (healSelf) {
|
||||
healEntity(caster);
|
||||
spawnHealParticles(world, caster);
|
||||
}
|
||||
|
||||
for (EntityLivingBase ally : getHealedAllies()) {
|
||||
healEntity(ally);
|
||||
spawnHealParticles(world, ally);
|
||||
}
|
||||
|
||||
// Instant heal completes immediately
|
||||
signalCompletion();
|
||||
}
|
||||
@@ -104,7 +108,7 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote || instantHeal) return;
|
||||
if ((world.isRemote && !isPreview()) || instantHeal) return;
|
||||
|
||||
// Heal over time - distribute heal across duration ticks
|
||||
if (tick % 10 == 0) {
|
||||
|
||||
@@ -68,51 +68,53 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate forward and right vectors from caster yaw
|
||||
float yawRad = (float) Math.toRadians(caster.rotationYaw);
|
||||
double forwardX = -Math.sin(yawRad);
|
||||
double forwardZ = Math.cos(yawRad);
|
||||
double rightX = forwardZ; // perpendicular right
|
||||
double rightZ = -forwardX;
|
||||
if (!isPreview()) {
|
||||
// Calculate forward and right vectors from caster yaw
|
||||
float yawRad = (float) Math.toRadians(caster.rotationYaw);
|
||||
double forwardX = -Math.sin(yawRad);
|
||||
double forwardZ = Math.cos(yawRad);
|
||||
double rightX = forwardZ; // perpendicular right
|
||||
double rightZ = -forwardX;
|
||||
|
||||
// Search area: AABB that encompasses the rectangle
|
||||
float searchDist = Math.max(hitLength, hitWidth) + 1.0f;
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(
|
||||
caster, caster.boundingBox.expand(searchDist, 2, searchDist));
|
||||
// Search area: AABB that encompasses the rectangle
|
||||
float searchDist = Math.max(hitLength, hitWidth) + 1.0f;
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(
|
||||
caster, caster.boundingBox.expand(searchDist, 2, searchDist));
|
||||
|
||||
boolean anyHit = false;
|
||||
for (Entity entity : entities) {
|
||||
if (!(entity instanceof EntityLivingBase) || entity == caster) continue;
|
||||
EntityLivingBase livingTarget = (EntityLivingBase) entity;
|
||||
boolean anyHit = false;
|
||||
for (Entity entity : entities) {
|
||||
if (!(entity instanceof EntityLivingBase) || entity == caster) continue;
|
||||
EntityLivingBase livingTarget = (EntityLivingBase) entity;
|
||||
|
||||
double dx = livingTarget.posX - caster.posX;
|
||||
double dz = livingTarget.posZ - caster.posZ;
|
||||
double dx = livingTarget.posX - caster.posX;
|
||||
double dz = livingTarget.posZ - caster.posZ;
|
||||
|
||||
// Project onto forward direction (must be in front, within hitLength)
|
||||
double forwardDist = dx * forwardX + dz * forwardZ;
|
||||
if (forwardDist < 0 || forwardDist > hitLength) continue;
|
||||
// Project onto forward direction (must be in front, within hitLength)
|
||||
double forwardDist = dx * forwardX + dz * forwardZ;
|
||||
if (forwardDist < 0 || forwardDist > hitLength) continue;
|
||||
|
||||
// Project onto right direction (must be within hitWidth to each side)
|
||||
double sideDist = dx * rightX + dz * rightZ;
|
||||
if (Math.abs(sideDist) > hitWidth) continue;
|
||||
// Project onto right direction (must be within hitWidth to each side)
|
||||
double sideDist = dx * rightX + dz * rightZ;
|
||||
if (Math.abs(sideDist) > hitWidth) continue;
|
||||
|
||||
// Entity is within the rectangle - apply damage
|
||||
boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback);
|
||||
if (wasHit) {
|
||||
applyEffects(livingTarget);
|
||||
anyHit = true;
|
||||
// Entity is within the rectangle - apply damage
|
||||
boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback);
|
||||
if (wasHit) {
|
||||
applyEffects(livingTarget);
|
||||
anyHit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Play hit sound even if nothing was hit (the attack still happens)
|
||||
if (!anyHit) {
|
||||
world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f);
|
||||
// Play hit sound even if nothing was hit (the attack still happens)
|
||||
if (!anyHit) {
|
||||
world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityAbilityLaser;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.DamageSource;
|
||||
@@ -22,7 +21,6 @@ import noppes.npcs.api.ability.type.IAbilityLaserShot;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
/**
|
||||
* Laser Shot ability: Fast expanding thin line that pierces through targets.
|
||||
@@ -84,7 +82,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
if (tick == 1) {
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
|
||||
@@ -96,17 +94,21 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
expansionSpeed, lingerTicks
|
||||
);
|
||||
|
||||
laserEntity.setupCharging(anchorData, windUpTicks);
|
||||
if (isPreview()) {
|
||||
laserEntity.setupPreview(caster, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance);
|
||||
} else {
|
||||
laserEntity.setupCharging(anchorData, windUpTicks);
|
||||
}
|
||||
|
||||
laserEntity.setEffects(this.effects);
|
||||
laserEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(laserEntity);
|
||||
spawnAbilityEntity(world, laserEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
@@ -235,25 +237,10 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityLaser laser = new EntityAbilityLaser(npc.worldObj);
|
||||
laser.setupPreview(npc, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance);
|
||||
return laser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawnPreviewDuringWindup() {
|
||||
return false; // Laser has no charging, spawns at active phase
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
|
||||
@@ -75,14 +75,18 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start moving the orb that was spawned during windup
|
||||
if (orbEntity != null && !orbEntity.isDead) {
|
||||
orbEntity.startMoving(target);
|
||||
if (isPreview()) {
|
||||
orbEntity.startPreviewFiring();
|
||||
} else {
|
||||
orbEntity.startMoving(target);
|
||||
}
|
||||
}
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
@@ -91,21 +95,25 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Spawn orb in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
// Create orb in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
|
||||
orbEntity = new EntityAbilityOrb(
|
||||
world, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
|
||||
displayData, combatData, homingData, lightningData, lifespanData, trajectoryData);
|
||||
orbEntity.setupCharging(anchorData, windUpTicks);
|
||||
|
||||
if (isPreview()) {
|
||||
orbEntity.setupPreview(caster, orbSize, displayData, lightningData, anchorData, windUpTicks);
|
||||
} else {
|
||||
orbEntity.setupCharging(anchorData, windUpTicks);
|
||||
}
|
||||
|
||||
orbEntity.setEffects(this.effects);
|
||||
orbEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(orbEntity);
|
||||
spawnAbilityEntity(world, orbEntity);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,17 +413,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj);
|
||||
orb.setupPreview(npc, orbSize, displayData, lightningData, anchorData, windUpTicks);
|
||||
return orb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ public class AbilityOrbBarrage extends Ability {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
@@ -107,12 +107,10 @@ public class AbilityOrbBarrage extends Ability {
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Spawn orb in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
// Create orb in charging mode - follows caster based on anchor point during windup
|
||||
|
||||
if (dualCharging) {
|
||||
chargingEntities = new EntityAbilityOrb[2];
|
||||
} else {
|
||||
@@ -128,11 +126,15 @@ public class AbilityOrbBarrage extends Ability {
|
||||
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData
|
||||
);
|
||||
|
||||
orbEntity.setupCharging(anchorData[i], windUpTicks);
|
||||
if (isPreview()) {
|
||||
orbEntity.setupPreview(caster, orbSize, colorData, lightningData, anchorData[i], windUpTicks);
|
||||
} else {
|
||||
orbEntity.setupCharging(anchorData[i], windUpTicks);
|
||||
}
|
||||
orbEntity.setSourceAbility(this);
|
||||
|
||||
chargingEntities[i] = orbEntity;
|
||||
world.spawnEntityInWorld(orbEntity);
|
||||
spawnAbilityEntity(world, orbEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -540,17 +542,7 @@ public class AbilityOrbBarrage extends Ability {
|
||||
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj);
|
||||
orb.setupPreview(npc, orbSize, colorData, lightningData, anchorData[0], windUpTicks);
|
||||
return orb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
|
||||
|
||||
@@ -80,19 +80,27 @@ public class AbilityOrbDual extends Ability {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start moving the orb that was spawned during windup
|
||||
if (orbEntity != null && !orbEntity.isDead) {
|
||||
orbEntity.startMoving(target);
|
||||
if (isPreview()) {
|
||||
orbEntity.startPreviewFiring();
|
||||
} else {
|
||||
orbEntity.startMoving(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (dualFire) {
|
||||
if (orbEntity2 != null && !orbEntity2.isDead && dualFireDelay <= 0) {
|
||||
orbEntity2.startMoving(target);
|
||||
if (isPreview()) {
|
||||
orbEntity2.startPreviewFiring();
|
||||
} else {
|
||||
orbEntity2.startMoving(target);
|
||||
}
|
||||
}
|
||||
} else if (orbEntity2 != null) {
|
||||
orbEntity2.setDead();
|
||||
@@ -104,11 +112,10 @@ public class AbilityOrbDual extends Ability {
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Spawn orb in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
// Create orb in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]);
|
||||
orbEntity = new EntityAbilityOrb(
|
||||
world, caster, target,
|
||||
@@ -124,8 +131,13 @@ public class AbilityOrbDual extends Ability {
|
||||
orbEntity.setSiblingEntityId(orbEntity2.getEntityId());
|
||||
orbEntity2.setSiblingEntityId(orbEntity.getEntityId());
|
||||
|
||||
orbEntity.setupCharging(anchorData[0], windUpTicks);
|
||||
orbEntity2.setupCharging(anchorData[1], windUpTicks);
|
||||
if (isPreview()) {
|
||||
orbEntity.setupPreview(caster, orbSize, displayData[0], lightningData[0], anchorData[0], windUpTicks);
|
||||
orbEntity2.setupPreview(caster, orbSize, displayData[1], lightningData[1], anchorData[1], windUpTicks);
|
||||
} else {
|
||||
orbEntity.setupCharging(anchorData[0], windUpTicks);
|
||||
orbEntity2.setupCharging(anchorData[1], windUpTicks);
|
||||
}
|
||||
|
||||
orbEntity.setEffects(this.effects);
|
||||
|
||||
@@ -135,8 +147,8 @@ public class AbilityOrbDual extends Ability {
|
||||
|
||||
orbEntity.setSourceAbility(this);
|
||||
orbEntity2.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(orbEntity);
|
||||
world.spawnEntityInWorld(orbEntity2);
|
||||
spawnAbilityEntity(world, orbEntity);
|
||||
spawnAbilityEntity(world, orbEntity2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,17 +582,7 @@ public class AbilityOrbDual extends Ability {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj);
|
||||
orb.setupPreview(npc, orbSize, displayData[0], lightningData[0], anchorData[0], windUpTicks);
|
||||
return orb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
}
|
||||
@@ -62,59 +62,64 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote || target == null) return;
|
||||
|
||||
// Calculate direction to target
|
||||
double dx = target.posX - caster.posX;
|
||||
double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight());
|
||||
double dz = target.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
if (len > 0) {
|
||||
dx /= len;
|
||||
dy /= len;
|
||||
dz /= len;
|
||||
if ((world.isRemote && !isPreview()) || target == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Deal instant damage with scripted event support
|
||||
// TODO: Use custom EntityAbilityProjectile for actual tracking
|
||||
applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz);
|
||||
if (!isPreview()) {
|
||||
// Calculate direction to target
|
||||
double dx = target.posX - caster.posX;
|
||||
double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight());
|
||||
double dz = target.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
// Play sound
|
||||
world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f);
|
||||
if (len > 0) {
|
||||
dx /= len;
|
||||
dy /= len;
|
||||
dz /= len;
|
||||
}
|
||||
|
||||
// Handle splash damage for explosive projectiles
|
||||
if (explosive && explosionRadius > 0) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(target,
|
||||
target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius));
|
||||
// Deal instant damage with scripted event support
|
||||
// TODO: Use custom EntityAbilityProjectile for actual tracking
|
||||
applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz);
|
||||
|
||||
for (Entity entity : entities) {
|
||||
if (entity instanceof EntityLivingBase && entity != caster) {
|
||||
EntityLivingBase living = (EntityLivingBase) entity;
|
||||
float dist = target.getDistanceToEntity(living);
|
||||
if (dist < explosionRadius) {
|
||||
float falloff = 1.0f - (dist / explosionRadius);
|
||||
// Apply splash damage with scripted event support (no knockback)
|
||||
applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0);
|
||||
// Play sound
|
||||
world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f);
|
||||
|
||||
// Handle splash damage for explosive projectiles
|
||||
if (explosive && explosionRadius > 0) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(target,
|
||||
target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius));
|
||||
|
||||
for (Entity entity : entities) {
|
||||
if (entity instanceof EntityLivingBase && entity != caster) {
|
||||
EntityLivingBase living = (EntityLivingBase) entity;
|
||||
float dist = target.getDistanceToEntity(living);
|
||||
if (dist < explosionRadius) {
|
||||
float falloff = 1.0f - (dist / explosionRadius);
|
||||
// Apply splash damage with scripted event support (no knockback)
|
||||
applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Explosion particles
|
||||
world.playSoundAtEntity(target, "random.explode", 0.5f, 1.0f);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
world.spawnParticle("explode",
|
||||
target.posX + (world.rand.nextDouble() - 0.5) * explosionRadius,
|
||||
target.posY + world.rand.nextDouble() * target.height,
|
||||
target.posZ + (world.rand.nextDouble() - 0.5) * explosionRadius,
|
||||
0, 0.1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// Explosion particles
|
||||
world.playSoundAtEntity(target, "random.explode", 0.5f, 1.0f);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
world.spawnParticle("explode",
|
||||
target.posX + (world.rand.nextDouble() - 0.5) * explosionRadius,
|
||||
target.posY + world.rand.nextDouble() * target.height,
|
||||
target.posZ + (world.rand.nextDouble() - 0.5) * explosionRadius,
|
||||
0, 0.1, 0);
|
||||
}
|
||||
// Spawn projectile particles (visual trail)
|
||||
spawnProjectileParticles(world, caster, target);
|
||||
}
|
||||
|
||||
// Spawn projectile particles (visual trail)
|
||||
spawnProjectileParticles(world, caster, target);
|
||||
|
||||
// Projectile is instant
|
||||
signalCompletion();
|
||||
}
|
||||
|
||||
@@ -63,48 +63,50 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
// Shockwave is instant - apply effect immediately after windup
|
||||
if (!isPreview()) {
|
||||
// Shockwave is instant - apply effect immediately after windup
|
||||
|
||||
// Get all entities in radius
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
// Get all entities in radius
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
int count = 0;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
int count = 0;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist > pushRadius) continue;
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist > pushRadius) continue;
|
||||
|
||||
count++;
|
||||
if (count > maxTargets) break;
|
||||
count++;
|
||||
if (count > maxTargets) break;
|
||||
|
||||
// Calculate push direction (away from caster)
|
||||
double dx = entity.posX - caster.posX;
|
||||
double dz = entity.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
// Calculate push direction (away from caster)
|
||||
double dx = entity.posX - caster.posX;
|
||||
double dz = entity.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (len > 0) {
|
||||
dx /= len;
|
||||
dz /= len;
|
||||
} else {
|
||||
// Entity is directly on top of caster, push in random direction
|
||||
double angle = Math.random() * Math.PI * 2;
|
||||
dx = Math.cos(angle);
|
||||
dz = Math.sin(angle);
|
||||
}
|
||||
if (len > 0) {
|
||||
dx /= len;
|
||||
dz /= len;
|
||||
} else {
|
||||
// Entity is directly on top of caster, push in random direction
|
||||
double angle = Math.random() * Math.PI * 2;
|
||||
dx = Math.cos(angle);
|
||||
dz = Math.sin(angle);
|
||||
}
|
||||
|
||||
// Scale knockback by distance (closer = stronger)
|
||||
float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f;
|
||||
float finalPush = pushStrength * distFactor;
|
||||
// Apply damage with custom knockback direction
|
||||
boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz);
|
||||
// Scale knockback by distance (closer = stronger)
|
||||
float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f;
|
||||
float finalPush = pushStrength * distFactor;
|
||||
// Apply damage with custom knockback direction
|
||||
boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz);
|
||||
|
||||
// Apply effects if hit connected
|
||||
if (wasHit) {
|
||||
applyEffects(entity);
|
||||
// Apply effects if hit connected
|
||||
if (wasHit) {
|
||||
applyEffects(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,11 +187,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
caster.motionZ = 0;
|
||||
caster.motionY = vy;
|
||||
hasLaunched = true;
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(true);
|
||||
}
|
||||
caster.velocityChanged = true;
|
||||
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -233,10 +235,12 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
caster.motionY = vy;
|
||||
|
||||
hasLaunched = true;
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(true);
|
||||
}
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
|
||||
// Face the target
|
||||
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
|
||||
@@ -252,7 +256,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
airTicks++;
|
||||
|
||||
// Continuously reset fall distance during slam to prevent fall damage
|
||||
caster.fallDistance = 0;
|
||||
if (!isPreview()) {
|
||||
caster.fallDistance = 0;
|
||||
}
|
||||
|
||||
// Check for landing
|
||||
if (caster.onGround && airTicks > 3) {
|
||||
@@ -268,11 +274,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
}
|
||||
|
||||
// While in air, face toward target
|
||||
double dx = targetX - caster.posX;
|
||||
double dz = targetZ - caster.posZ;
|
||||
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
|
||||
caster.rotationYaw = targetYaw;
|
||||
caster.rotationYawHead = targetYaw;
|
||||
if (!isPreview()) {
|
||||
double dx = targetX - caster.posX;
|
||||
double dz = targetZ - caster.posZ;
|
||||
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
|
||||
caster.rotationYaw = targetYaw;
|
||||
caster.rotationYawHead = targetYaw;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,22 +288,25 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
*/
|
||||
private void onLanding(EntityLivingBase caster, World world) {
|
||||
hasLanded = true;
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
|
||||
// Signal that the ability has completed its active phase
|
||||
signalCompletion();
|
||||
|
||||
// Reset fall distance to prevent fall damage on landing
|
||||
caster.fallDistance = 0;
|
||||
|
||||
// Stop horizontal momentum
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
|
||||
if (world.isRemote) return;
|
||||
if (!isPreview()) {
|
||||
// Reset fall distance to prevent fall damage on landing
|
||||
caster.fallDistance = 0;
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
if (isPreview()) return;
|
||||
|
||||
// Play slam impact sound on landing
|
||||
world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f);
|
||||
@@ -361,7 +372,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
hasLaunched = false;
|
||||
@@ -371,7 +382,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
hasLaunched = false;
|
||||
@@ -387,97 +398,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
airTicks = 0;
|
||||
}
|
||||
|
||||
// ==================== PREVIEW MODE ====================
|
||||
|
||||
private transient double previewVelX, previewVelY, previewVelZ;
|
||||
private transient boolean previewLaunched = false;
|
||||
private transient double previewGroundY;
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewExecute(EntityNPCInterface npc) {
|
||||
previewLaunched = false;
|
||||
|
||||
double tx, ty, tz;
|
||||
if (previewTarget != null && targetingMode == TargetingMode.AOE_TARGET) {
|
||||
tx = previewTarget.posX;
|
||||
ty = previewTarget.posY;
|
||||
tz = previewTarget.posZ;
|
||||
} else {
|
||||
tx = npc.posX;
|
||||
ty = npc.posY;
|
||||
tz = npc.posZ;
|
||||
}
|
||||
|
||||
previewGroundY = npc.posY;
|
||||
|
||||
double dx = tx - npc.posX;
|
||||
double dz = tz - npc.posZ;
|
||||
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
// Calculate vertical velocity for exact leapHeight
|
||||
double arcHeight = Math.max(1.0, leapHeight);
|
||||
double vy = calculateLaunchVelocity(arcHeight);
|
||||
|
||||
if (horizontalDist < 0.5) {
|
||||
previewVelX = 0;
|
||||
previewVelZ = 0;
|
||||
previewVelY = vy;
|
||||
} else {
|
||||
if (horizontalDist > maxRange) {
|
||||
double scale = maxRange / horizontalDist;
|
||||
dx *= scale;
|
||||
dz *= scale;
|
||||
horizontalDist = maxRange;
|
||||
}
|
||||
|
||||
double speedFactor = Math.max(0.1, leapSpeed);
|
||||
int flightTicks = (int) Math.max(10, Math.min(horizontalDist * 1.5 / speedFactor, 40));
|
||||
double drag = 0.91;
|
||||
double dragPowN = Math.pow(drag, flightTicks);
|
||||
double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN);
|
||||
|
||||
double dirX = dx / horizontalDist;
|
||||
double dirZ = dz / horizontalDist;
|
||||
previewVelX = dirX * vHorizontal;
|
||||
previewVelZ = dirZ * vHorizontal;
|
||||
previewVelY = vy;
|
||||
}
|
||||
previewLaunched = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
|
||||
if (!previewLaunched) return;
|
||||
|
||||
// Apply velocity
|
||||
npc.prevPosX = npc.posX;
|
||||
npc.prevPosY = npc.posY;
|
||||
npc.prevPosZ = npc.posZ;
|
||||
|
||||
npc.posX += previewVelX;
|
||||
npc.posY += previewVelY;
|
||||
npc.posZ += previewVelZ;
|
||||
|
||||
// Gravity and drag
|
||||
previewVelY -= 0.08;
|
||||
previewVelY *= 0.98;
|
||||
previewVelX *= 0.91;
|
||||
previewVelZ *= 0.91;
|
||||
|
||||
// Ground clamp - stop falling below starting Y
|
||||
if (npc.posY < previewGroundY && previewVelY < 0) {
|
||||
npc.posY = previewGroundY;
|
||||
previewVelY = 0;
|
||||
previewVelX = 0;
|
||||
previewVelZ = 0;
|
||||
previewLaunched = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return 60;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
@@ -85,7 +85,12 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
|
||||
sweepSpeed, numberOfRotations,
|
||||
damage, damageInterval, piercing,
|
||||
lockOnTarget);
|
||||
world.spawnEntityInWorld(activeEntity);
|
||||
|
||||
if (isPreview()) {
|
||||
activeEntity.setupPreview(caster);
|
||||
}
|
||||
|
||||
spawnAbilityEntity(world, activeEntity);
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
@@ -200,6 +205,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
|
||||
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
|
||||
public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; }
|
||||
|
||||
@Override
|
||||
public int getMaxPreviewDuration() {
|
||||
return (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
|
||||
@@ -128,7 +128,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
}
|
||||
|
||||
private void performBlink(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
double oldX = caster.posX;
|
||||
double oldY = caster.posY;
|
||||
@@ -137,36 +137,47 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
Vec3 destination = calculateDestination(caster, target, world);
|
||||
if (destination == null) return;
|
||||
|
||||
boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight;
|
||||
if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ,
|
||||
destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) {
|
||||
destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ,
|
||||
if (!isPreview()) {
|
||||
boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight;
|
||||
if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ,
|
||||
destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) {
|
||||
destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ,
|
||||
destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
if (destination == null) return;
|
||||
}
|
||||
|
||||
destination = findSafeDestination(world, oldX, oldY, oldZ,
|
||||
destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
if (destination == null) return;
|
||||
|
||||
// Damage at origin
|
||||
if (damageAtStart) {
|
||||
dealDamageAt(caster, world, oldX, oldY, oldZ);
|
||||
}
|
||||
|
||||
// Spawn particles at origin
|
||||
spawnTeleportParticles(world, oldX, oldY, oldZ);
|
||||
}
|
||||
|
||||
destination = findSafeDestination(world, oldX, oldY, oldZ,
|
||||
destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
if (destination == null) return;
|
||||
|
||||
// Damage at origin
|
||||
if (damageAtStart) {
|
||||
dealDamageAt(caster, world, oldX, oldY, oldZ);
|
||||
}
|
||||
|
||||
// Spawn particles at origin
|
||||
spawnTeleportParticles(world, oldX, oldY, oldZ);
|
||||
|
||||
// Teleport
|
||||
caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
if (isPreview()) {
|
||||
caster.setPosition(destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
caster.prevPosX = destination.xCoord;
|
||||
caster.prevPosY = destination.yCoord;
|
||||
caster.prevPosZ = destination.zCoord;
|
||||
} else {
|
||||
caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
}
|
||||
caster.fallDistance = 0;
|
||||
|
||||
// Spawn particles at destination
|
||||
spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
if (!isPreview()) {
|
||||
// Spawn particles at destination
|
||||
spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
|
||||
// Damage at destination
|
||||
if (damageAtEnd) {
|
||||
dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
// Damage at destination
|
||||
if (damageAtEnd) {
|
||||
dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
}
|
||||
}
|
||||
|
||||
// Face target after teleport
|
||||
@@ -561,6 +572,12 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
this.damageRadius = damageRadius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxPreviewDuration() {
|
||||
int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1;
|
||||
return blinkLimit * blinkDelayTicks + 10;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
|
||||
@@ -207,26 +207,28 @@ public class AbilityTrap extends Ability implements IAbilityTrap {
|
||||
return;
|
||||
}
|
||||
|
||||
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
|
||||
trapX - triggerRadius, trapY - 1, trapZ - triggerRadius,
|
||||
trapX + triggerRadius, trapY + 2, trapZ + triggerRadius
|
||||
);
|
||||
if (!isPreview()) {
|
||||
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
|
||||
trapX - triggerRadius, trapY - 1, trapZ - triggerRadius,
|
||||
trapX + triggerRadius, trapY + 2, trapZ + triggerRadius
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue;
|
||||
|
||||
double dx = entity.posX - trapX;
|
||||
double dz = entity.posZ - trapZ;
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
double dx = entity.posX - trapX;
|
||||
double dz = entity.posZ - trapZ;
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (dist <= triggerRadius) {
|
||||
triggerTrap(caster, entity, world);
|
||||
return;
|
||||
if (dist <= triggerRadius) {
|
||||
triggerTrap(caster, entity, world);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,29 +84,31 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
pullComplete = false;
|
||||
ticksSincePullDamage = 0;
|
||||
|
||||
if (aoe) {
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
if (!isPreview()) {
|
||||
if (aoe) {
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
int count = 0;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
int count = 0;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(entity.getUniqueID());
|
||||
count++;
|
||||
if (count >= maxTargets) break;
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(entity.getUniqueID());
|
||||
count++;
|
||||
if (count >= maxTargets) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single target mode - still check pullRadius
|
||||
if (target != null && !target.isDead) {
|
||||
double dist = caster.getDistanceToEntity(target);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(target.getUniqueID());
|
||||
} else {
|
||||
// Single target mode - still check pullRadius
|
||||
if (target != null && !target.isDead) {
|
||||
double dist = caster.getDistanceToEntity(target);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(target.getUniqueID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +116,12 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (isPreview()) {
|
||||
// No entities to pull in preview, just run animation for a duration
|
||||
if (tick >= 60) signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pullComplete || getPulledEntities().isEmpty()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
|
||||
@@ -222,24 +222,39 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
|
||||
|
||||
setCharging(false);
|
||||
|
||||
// Update start position to current position
|
||||
// Origin (tail) stays at the charged position
|
||||
startX = posX;
|
||||
startY = posY;
|
||||
startZ = posZ;
|
||||
|
||||
// Reset head offset to origin
|
||||
headOffsetX = 0;
|
||||
headOffsetY = 0;
|
||||
headOffsetZ = 0;
|
||||
prevHeadOffsetX = 0;
|
||||
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();
|
||||
trailPoints.add(Vec3.createVectorHelper(0, 0, 0));
|
||||
|
||||
// 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();
|
||||
motionX = -Math.sin(yaw) * getSpeed();
|
||||
motionY = 0;
|
||||
motionZ = Math.cos(yaw) * getSpeed();
|
||||
} else {
|
||||
motionX = getSpeed();
|
||||
motionY = 0;
|
||||
|
||||
@@ -38,7 +38,6 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
private int chargeTick = 0;
|
||||
private float targetDiscRadius = 1.0f; // Full radius to grow to during charging
|
||||
private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging
|
||||
private EnergyAnchorData anchorData = new EnergyAnchorData();
|
||||
|
||||
// Data watcher index for charging state (synced to clients)
|
||||
private static final int DW_CHARGING = 20;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package kamkeel.npcs.entity;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -46,7 +45,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
private boolean charging = false;
|
||||
private int chargeDuration = 40;
|
||||
private int chargeTick = 0;
|
||||
private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT);
|
||||
private float targetSize = 1.0f; // Full size to grow to during charging
|
||||
|
||||
// Data watcher index for charging state (synced to clients)
|
||||
@@ -454,7 +452,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40;
|
||||
this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0;
|
||||
this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size;
|
||||
anchorData.readNBT(nbt);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -476,7 +473,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
nbt.setInteger("ChargeDuration", chargeDuration);
|
||||
nbt.setInteger("ChargeTick", chargeTick);
|
||||
nbt.setFloat("TargetSize", targetSize);
|
||||
anchorData.writeNBT(nbt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,6 +60,10 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
|
||||
private int maxTicks = 400;
|
||||
private long deathWorldTime = -1;
|
||||
|
||||
// Preview mode
|
||||
private boolean previewMode = false;
|
||||
private EntityLivingBase previewOwner = null;
|
||||
|
||||
// Damage state
|
||||
private transient int ticksSinceDamage = 0;
|
||||
private transient Set<Integer> hitThisTick = new HashSet<>();
|
||||
@@ -281,7 +285,16 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
|
||||
target.velocityChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up preview mode. Owner is stored directly since world entity lookup won't work.
|
||||
*/
|
||||
public void setupPreview(EntityLivingBase owner) {
|
||||
this.previewMode = true;
|
||||
this.previewOwner = owner;
|
||||
}
|
||||
|
||||
private Entity getOwner() {
|
||||
if (previewMode && previewOwner != null) return previewOwner;
|
||||
if (ownerEntityId == -1) return null;
|
||||
return worldObj.getEntityByID(ownerEntityId);
|
||||
}
|
||||
|
||||
@@ -509,6 +509,8 @@ public class ClientProxy extends CommonProxy {
|
||||
} else {
|
||||
abilityNpc = new EntityCustomNpc(Minecraft.getMinecraft().theWorld);
|
||||
abilityNpc.display.texture = "customnpcs:textures/entity/humanmale/AnimationBody.png";
|
||||
abilityNpc.height = 1.8f;
|
||||
abilityNpc.width = 0.6f;
|
||||
}
|
||||
return new GuiNpcManageAbilities(abilityNpc, npc != null);
|
||||
}
|
||||
|
||||
@@ -4,15 +4,10 @@ import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.PreviewEntityHandler;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.entity.EntityAbilityBeam;
|
||||
import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import kamkeel.npcs.entity.EntityAbilityLaser;
|
||||
import kamkeel.npcs.entity.EntityAbilityOrb;
|
||||
import kamkeel.npcs.entity.EntityAbilityProjectile;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.controllers.data.Animation;
|
||||
import noppes.npcs.controllers.data.AnimationData;
|
||||
import noppes.npcs.entity.EntityCustomNpc;
|
||||
@@ -25,38 +20,39 @@ import java.util.List;
|
||||
/**
|
||||
* Client-side executor for previewing abilities in the GUI.
|
||||
*
|
||||
* Lifecycle:
|
||||
* 1. WINDUP phase - plays windup animation, entities spawn and charge
|
||||
* 2. ACTIVE phase - plays active animation, entities fire and move
|
||||
* 3. EXTENDED phase - continues for +100 ticks after animations to see results
|
||||
* 4. IDLE - preview complete
|
||||
* Uses unified execution: calls the REAL ability methods (onWindUpTick, onExecute, onActiveTick)
|
||||
* with previewMode=true. Abilities skip damage/effects/sounds when isPreview() returns true.
|
||||
*
|
||||
* Features:
|
||||
* - Invisible fake target NPC placed in front of the preview NPC for targeting
|
||||
* - NPC movement tracking for abilities that move the NPC (e.g. slam)
|
||||
* Lifecycle:
|
||||
* 1. WINDUP phase - real onWindUpTick() called, telegraph shown, entities may spawn
|
||||
* 2. ACTIVE phase - real onExecute() then onActiveTick() called, physics simulated
|
||||
* 3. EXTENDED phase - 100 extra ticks to observe results (entities continue updating)
|
||||
* 4. IDLE - preview complete
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class AbilityPreviewExecutor {
|
||||
public class AbilityPreviewExecutor implements PreviewEntityHandler {
|
||||
|
||||
// ==================== PREVIEW STATE ====================
|
||||
private Ability previewAbility;
|
||||
private EntityNPCInterface previewNpc;
|
||||
private AbilityPhase phase = AbilityPhase.IDLE;
|
||||
|
||||
/** Executor's own phase tracking (separate from ability.getPhase()) */
|
||||
private enum ExecutorPhase { IDLE, WINDUP, ACTIVE, EXTENDED }
|
||||
private ExecutorPhase executorPhase = ExecutorPhase.IDLE;
|
||||
|
||||
private int currentTick = 0;
|
||||
private boolean playing = false;
|
||||
private boolean paused = false;
|
||||
|
||||
// ==================== TIMING ====================
|
||||
private static final int EXTENDED_DURATION = 100;
|
||||
private int activeDuration = 40;
|
||||
private int maxPreviewDuration = 200;
|
||||
|
||||
// ==================== FAKE TARGET ====================
|
||||
/** Invisible fake target NPC for ability targeting (placed 5 blocks in front of preview NPC) */
|
||||
private EntityNPCInterface fakeTarget;
|
||||
private static final double TARGET_DISTANCE = 5.0;
|
||||
|
||||
// ==================== NPC START POSITION ====================
|
||||
/** Saved NPC position for restoring after movement abilities */
|
||||
private double npcStartX, npcStartY, npcStartZ;
|
||||
|
||||
// ==================== PREVIEW CONTENT ====================
|
||||
@@ -74,7 +70,8 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start previewing an ability.
|
||||
* Start previewing an ability using unified execution.
|
||||
* Sets previewMode=true on the ability, then calls its real methods.
|
||||
*/
|
||||
public void startPreview(Ability ability, EntityNPCInterface npc) {
|
||||
stop();
|
||||
@@ -84,31 +81,35 @@ public class AbilityPreviewExecutor {
|
||||
this.currentTick = 0;
|
||||
this.playing = true;
|
||||
this.paused = false;
|
||||
this.activeDuration = ability.getPreviewActiveDuration();
|
||||
this.maxPreviewDuration = ability.getMaxPreviewDuration();
|
||||
|
||||
// Save NPC start position for restoring later
|
||||
this.npcStartX = npc.posX;
|
||||
this.npcStartY = npc.posY;
|
||||
this.npcStartZ = npc.posZ;
|
||||
|
||||
// Enable preview mode on the ability
|
||||
ability.setPreviewMode(true);
|
||||
ability.setPreviewEntityHandler(this);
|
||||
|
||||
// Create invisible fake target NPC for targeting
|
||||
createFakeTarget(npc);
|
||||
|
||||
// Set preview target on the ability
|
||||
ability.setPreviewTarget(fakeTarget);
|
||||
|
||||
// Start tracking NPC movement
|
||||
if (parentGui != null) {
|
||||
parentGui.startTrackingMovement();
|
||||
}
|
||||
|
||||
// Use the ability's real start() method with the fake target
|
||||
ability.start(fakeTarget);
|
||||
|
||||
if (ability.getWindUpTicks() <= 0) {
|
||||
// No windup — go straight to active
|
||||
this.phase = AbilityPhase.ACTIVE;
|
||||
this.executorPhase = ExecutorPhase.ACTIVE;
|
||||
transitionToActive();
|
||||
} else {
|
||||
// Normal windup flow
|
||||
this.phase = AbilityPhase.WINDUP;
|
||||
this.executorPhase = ExecutorPhase.WINDUP;
|
||||
|
||||
// Create telegraph for preview
|
||||
previewTelegraph = ability.createTelegraph(npc, fakeTarget);
|
||||
@@ -121,24 +122,17 @@ public class AbilityPreviewExecutor {
|
||||
data.setAnimation(windUpAnim);
|
||||
data.animation.paused = false;
|
||||
}
|
||||
|
||||
// Spawn preview entity on first tick if ability spawns during windup
|
||||
if (ability.spawnPreviewDuringWindup()) {
|
||||
spawnPreviewEntity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an invisible fake target NPC placed in front of the preview NPC.
|
||||
* This entity is NOT rendered - it only exists for ability targeting logic.
|
||||
*/
|
||||
private void createFakeTarget(EntityNPCInterface npc) {
|
||||
if (npc.worldObj == null) return;
|
||||
|
||||
try {
|
||||
fakeTarget = new EntityCustomNpc(npc.worldObj);
|
||||
// Position target in front of NPC based on its facing direction
|
||||
float facingYaw = GuiAbilityInterface.NPC_FACING_YAW;
|
||||
double yawRad = Math.toRadians(facingYaw);
|
||||
double targetX = npc.posX - Math.sin(yawRad) * TARGET_DISTANCE;
|
||||
@@ -153,15 +147,12 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fake target entity for abilities that need a target.
|
||||
*/
|
||||
public EntityLivingBase getFakeTarget() {
|
||||
return fakeTarget;
|
||||
}
|
||||
|
||||
public void play() {
|
||||
if (previewAbility == null || phase == AbilityPhase.IDLE) {
|
||||
if (previewAbility == null || executorPhase == ExecutorPhase.IDLE) {
|
||||
return;
|
||||
}
|
||||
playing = true;
|
||||
@@ -187,9 +178,17 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
// Clean up the ability
|
||||
if (previewAbility != null) {
|
||||
previewAbility.cleanup();
|
||||
previewAbility.setPreviewMode(false);
|
||||
previewAbility.setPreviewEntityHandler(null);
|
||||
previewAbility.reset();
|
||||
}
|
||||
|
||||
playing = false;
|
||||
paused = false;
|
||||
phase = AbilityPhase.IDLE;
|
||||
executorPhase = ExecutorPhase.IDLE;
|
||||
currentTick = 0;
|
||||
|
||||
previewTelegraph = null;
|
||||
@@ -211,9 +210,11 @@ public class AbilityPreviewExecutor {
|
||||
previewNpc.prevPosX = npcStartX;
|
||||
previewNpc.prevPosY = npcStartY;
|
||||
previewNpc.prevPosZ = npcStartZ;
|
||||
previewNpc.motionX = 0;
|
||||
previewNpc.motionY = 0;
|
||||
previewNpc.motionZ = 0;
|
||||
}
|
||||
|
||||
// Remove fake target
|
||||
fakeTarget = null;
|
||||
|
||||
if (previewNpc != null) {
|
||||
@@ -224,7 +225,7 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick the preview forward.
|
||||
* Tick the preview forward. Calls real ability methods.
|
||||
*/
|
||||
public void tick() {
|
||||
if (!playing || paused || previewAbility == null || previewNpc == null) {
|
||||
@@ -239,14 +240,14 @@ public class AbilityPreviewExecutor {
|
||||
|
||||
tickEntities();
|
||||
|
||||
switch (phase) {
|
||||
switch (executorPhase) {
|
||||
case WINDUP:
|
||||
tickWindup();
|
||||
break;
|
||||
case ACTIVE:
|
||||
tickActive();
|
||||
break;
|
||||
case IDLE:
|
||||
case EXTENDED:
|
||||
tickExtended();
|
||||
break;
|
||||
default:
|
||||
@@ -255,7 +256,8 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
private void tickWindup() {
|
||||
previewAbility.onPreviewWindUpTick(previewNpc, currentTick);
|
||||
// Call the REAL windup tick method
|
||||
previewAbility.onWindUpTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick);
|
||||
|
||||
if (currentTick >= previewAbility.getWindUpTicks()) {
|
||||
transitionToActive();
|
||||
@@ -263,35 +265,41 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
private void tickActive() {
|
||||
previewAbility.onPreviewActiveTick(previewNpc, currentTick);
|
||||
// Call the REAL active tick method
|
||||
previewAbility.onActiveTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick);
|
||||
|
||||
if (currentTick == 1 && !previewAbility.spawnPreviewDuringWindup()) {
|
||||
spawnPreviewEntity();
|
||||
fireEntities();
|
||||
// Simulate physics for movement abilities
|
||||
simulatePhysics();
|
||||
|
||||
// Check if the ability signaled completion (phase went to IDLE)
|
||||
if (previewAbility.getPhase() == AbilityPhase.IDLE) {
|
||||
transitionToExtended();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTick >= activeDuration) {
|
||||
// Safety timeout
|
||||
if (currentTick >= maxPreviewDuration) {
|
||||
transitionToExtended();
|
||||
}
|
||||
}
|
||||
|
||||
private void tickExtended() {
|
||||
// Continue ticking entities so they can finish their animations
|
||||
if (currentTick >= EXTENDED_DURATION) {
|
||||
playing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void transitionToActive() {
|
||||
phase = AbilityPhase.ACTIVE;
|
||||
executorPhase = ExecutorPhase.ACTIVE;
|
||||
currentTick = 0;
|
||||
|
||||
previewTelegraph = null;
|
||||
|
||||
// Notify ability of execution (for movement abilities like Slam/Charge/Dash)
|
||||
previewAbility.onPreviewExecute(previewNpc);
|
||||
|
||||
fireEntities();
|
||||
// Call the REAL onExecute method (fires entities, initiates movement, etc.)
|
||||
previewAbility.onExecute(previewNpc, fakeTarget, previewNpc.worldObj);
|
||||
|
||||
// Start active animation
|
||||
Animation activeAnim = previewAbility.getActiveAnimation();
|
||||
if (activeAnim != null && previewNpc != null) {
|
||||
AnimationData data = previewNpc.display.animationData;
|
||||
@@ -301,7 +309,7 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
private void transitionToExtended() {
|
||||
phase = AbilityPhase.IDLE;
|
||||
executorPhase = ExecutorPhase.EXTENDED;
|
||||
currentTick = 0;
|
||||
|
||||
if (previewNpc != null) {
|
||||
@@ -310,26 +318,40 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnPreviewEntity() {
|
||||
Entity entity = previewAbility.createPreviewEntity(previewNpc);
|
||||
if (entity != null) {
|
||||
previewEntities.add(entity);
|
||||
if (parentGui != null) {
|
||||
parentGui.addPreviewEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Simulate basic physics for movement abilities (Slam, Charge, Dash).
|
||||
* Applies gravity, drag, and ground clamping to NPC position.
|
||||
*/
|
||||
private void simulatePhysics() {
|
||||
if (!previewAbility.hasAbilityMovement()) return;
|
||||
if (previewNpc == null) return;
|
||||
|
||||
private void fireEntities() {
|
||||
for (Entity entity : previewEntities) {
|
||||
if (entity instanceof EntityAbilityOrb) {
|
||||
((EntityAbilityOrb) entity).startPreviewFiring();
|
||||
} else if (entity instanceof EntityAbilityBeam) {
|
||||
((EntityAbilityBeam) entity).startPreviewFiring();
|
||||
} else if (entity instanceof EntityAbilityDisc) {
|
||||
((EntityAbilityDisc) entity).startPreviewFiring();
|
||||
}
|
||||
// Save previous position for interpolation
|
||||
previewNpc.prevPosX = previewNpc.posX;
|
||||
previewNpc.prevPosY = previewNpc.posY;
|
||||
previewNpc.prevPosZ = previewNpc.posZ;
|
||||
|
||||
// Apply velocity
|
||||
previewNpc.posX += previewNpc.motionX;
|
||||
previewNpc.posY += previewNpc.motionY;
|
||||
previewNpc.posZ += previewNpc.motionZ;
|
||||
|
||||
// Apply gravity
|
||||
previewNpc.motionY -= 0.08;
|
||||
previewNpc.motionY *= 0.98;
|
||||
|
||||
// Apply horizontal drag
|
||||
previewNpc.motionX *= 0.91;
|
||||
previewNpc.motionZ *= 0.91;
|
||||
|
||||
// Ground clamp: if below start position and falling, stop
|
||||
if (previewNpc.posY < npcStartY && previewNpc.motionY < 0) {
|
||||
previewNpc.posY = npcStartY;
|
||||
previewNpc.motionY = 0;
|
||||
previewNpc.onGround = true;
|
||||
}
|
||||
|
||||
previewNpc.fallDistance = 0;
|
||||
}
|
||||
|
||||
private void tickEntities() {
|
||||
@@ -347,12 +369,38 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PreviewEntityHandler ====================
|
||||
|
||||
@Override
|
||||
public void onEntitySpawned(Entity entity) {
|
||||
if (entity == null) return;
|
||||
previewEntities.add(entity);
|
||||
if (parentGui != null) {
|
||||
parentGui.addPreviewEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemoved(Entity entity) {
|
||||
if (entity == null) return;
|
||||
previewEntities.remove(entity);
|
||||
if (parentGui != null) {
|
||||
parentGui.removePreviewEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public boolean isPlaying() { return playing; }
|
||||
public boolean isPaused() { return paused; }
|
||||
public boolean isActive() { return playing || paused; }
|
||||
public AbilityPhase getPhase() { return phase; }
|
||||
public AbilityPhase getPhase() {
|
||||
switch (executorPhase) {
|
||||
case WINDUP: return AbilityPhase.WINDUP;
|
||||
case ACTIVE: return AbilityPhase.ACTIVE;
|
||||
default: return AbilityPhase.IDLE;
|
||||
}
|
||||
}
|
||||
public int getCurrentTick() { return currentTick; }
|
||||
public TelegraphInstance getTelegraph() { return previewTelegraph; }
|
||||
public List<Entity> getPreviewEntities() { return previewEntities; }
|
||||
@@ -364,11 +412,11 @@ public class AbilityPreviewExecutor {
|
||||
return "Stopped";
|
||||
} else if (paused) {
|
||||
return "Paused";
|
||||
} else if (phase == AbilityPhase.WINDUP) {
|
||||
} else if (executorPhase == ExecutorPhase.WINDUP) {
|
||||
return "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0);
|
||||
} else if (phase == AbilityPhase.ACTIVE) {
|
||||
return "Active: " + currentTick + "/" + activeDuration;
|
||||
} else if (phase == AbilityPhase.IDLE && playing) {
|
||||
} else if (executorPhase == ExecutorPhase.ACTIVE) {
|
||||
return "Active: " + currentTick + "/" + maxPreviewDuration;
|
||||
} else if (executorPhase == ExecutorPhase.EXTENDED) {
|
||||
return "Extended: " + currentTick + "/" + EXTENDED_DURATION;
|
||||
}
|
||||
return "";
|
||||
|
||||
Reference in new issue
Block a user