mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 17:47:32 +00:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
088b1c211e
195 files changed
+8227
-3310
No files matched your search
@@ -0,0 +1,38 @@
|
||||
package kamkeel.npcs.client.renderer;
|
||||
|
||||
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
|
||||
import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager;
|
||||
import kamkeel.npcs.entity.EntityEnergyProjectile;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.entity.RenderManager;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraftforge.client.event.RenderWorldLastEvent;
|
||||
|
||||
/**
|
||||
* Renders packet-driven charging previews through RenderManager.
|
||||
*/
|
||||
public class EnergyChargePreviewRenderer {
|
||||
|
||||
@SubscribeEvent
|
||||
public void onRenderWorldLast(RenderWorldLastEvent event) {
|
||||
if (EnergyChargePreviewManager.ClientInstance == null || !EnergyChargePreviewManager.ClientInstance.hasPreviews()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
EntityPlayer player = mc.thePlayer;
|
||||
if (player == null) return;
|
||||
|
||||
float partialTicks = event.partialTicks;
|
||||
|
||||
for (EntityEnergyProjectile entity : EnergyChargePreviewManager.ClientInstance.getPreviews()) {
|
||||
if (entity == null || entity.isDead) continue;
|
||||
|
||||
double x = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks - RenderManager.renderPosX;
|
||||
double y = entity.prevPosY + (entity.posY - entity.prevPosY) * partialTicks - RenderManager.renderPosY;
|
||||
double z = entity.prevPosZ + (entity.posZ - entity.prevPosZ) * partialTicks - RenderManager.renderPosZ;
|
||||
|
||||
RenderManager.instance.renderEntityWithPosYaw(entity, x, y, z, entity.rotationYaw, partialTicks);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,9 @@ public class RenderAbilityBeam extends RenderEnergyAbility {
|
||||
|
||||
@Override
|
||||
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
|
||||
if (shouldSkipInitialActiveRender(entity)) {
|
||||
return;
|
||||
}
|
||||
EntityAbilityBeam beam = (EntityAbilityBeam) entity;
|
||||
|
||||
setupRenderState();
|
||||
|
||||
@@ -19,6 +19,9 @@ public class RenderAbilityDisc extends RenderEnergyAbility {
|
||||
|
||||
@Override
|
||||
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
|
||||
if (shouldSkipInitialActiveRender(entity)) {
|
||||
return;
|
||||
}
|
||||
EntityAbilityDisc disc = (EntityAbilityDisc) entity;
|
||||
|
||||
setupRenderState();
|
||||
|
||||
+6
-3
@@ -2,7 +2,7 @@ package kamkeel.npcs.client.renderer;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.entity.EntityEnergyDome;
|
||||
import kamkeel.npcs.entity.EntityAbilityDome;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.entity.Entity;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
@@ -13,11 +13,14 @@ import org.lwjgl.opengl.GL11;
|
||||
* Optional skybox texture rendered on the interior surface.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class RenderEnergyDome extends RenderEnergyBarrier {
|
||||
public class RenderAbilityDome extends RenderEnergyBarrier {
|
||||
|
||||
@Override
|
||||
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
|
||||
EntityEnergyDome dome = (EntityEnergyDome) entity;
|
||||
if (shouldSkipInitialActiveRender(entity)) {
|
||||
return;
|
||||
}
|
||||
EntityAbilityDome dome = (EntityAbilityDome) entity;
|
||||
float radius = dome.getDomeRadius();
|
||||
float healthPercent = dome.getHealthPercent();
|
||||
|
||||
@@ -23,6 +23,9 @@ public class RenderAbilityLaser extends RenderEnergyAbility {
|
||||
|
||||
@Override
|
||||
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
|
||||
if (shouldSkipInitialActiveRender(entity)) {
|
||||
return;
|
||||
}
|
||||
EntityAbilityLaser laser = (EntityAbilityLaser) entity;
|
||||
|
||||
setupRenderState();
|
||||
|
||||
@@ -21,6 +21,10 @@ public class RenderAbilityOrb extends RenderEnergyAbility {
|
||||
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
|
||||
EntityAbilityOrb orb = (EntityAbilityOrb) entity;
|
||||
|
||||
if (shouldSkipInitialActiveRender(entity)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setupRenderState();
|
||||
|
||||
GL11.glPushMatrix();
|
||||
|
||||
+17
-6
@@ -2,7 +2,7 @@ package kamkeel.npcs.client.renderer;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.entity.EntityEnergyPanel;
|
||||
import kamkeel.npcs.entity.EntityAbilityPanel;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.entity.Entity;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
@@ -13,17 +13,28 @@ import org.lwjgl.opengl.GL11;
|
||||
* Voxel-blocky style with inner/outer layers.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class RenderEnergyPanel extends RenderEnergyBarrier {
|
||||
public class RenderAbilityPanel extends RenderEnergyBarrier {
|
||||
|
||||
@Override
|
||||
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
|
||||
EntityEnergyPanel panel = (EntityEnergyPanel) entity;
|
||||
if (shouldSkipInitialActiveRender(entity)) {
|
||||
return;
|
||||
}
|
||||
EntityAbilityPanel panel = (EntityAbilityPanel) entity;
|
||||
|
||||
float width = panel.getPanelData().panelWidth;
|
||||
float height = panel.getPanelData().panelHeight;
|
||||
float panelYaw = panel.getPanelYaw();
|
||||
float healthPercent = panel.getHealthPercent();
|
||||
|
||||
// Interpolate panel yaw for smooth rotation between ticks
|
||||
float prevYaw = panel.getPrevPanelYaw();
|
||||
float currentYaw = panel.getPanelYaw();
|
||||
float yawDelta = currentYaw - prevYaw;
|
||||
// Wrap angle delta to -180..180 for shortest rotation path
|
||||
while (yawDelta > 180.0f) yawDelta -= 360.0f;
|
||||
while (yawDelta < -180.0f) yawDelta += 360.0f;
|
||||
float renderYaw = prevYaw + yawDelta * partialTicks;
|
||||
|
||||
setupRenderState();
|
||||
|
||||
GL11.glPushMatrix();
|
||||
@@ -34,8 +45,8 @@ public class RenderEnergyPanel extends RenderEnergyBarrier {
|
||||
renderAttachedLightning(panel, 0.9f, Math.max(width, height) * 0.5f);
|
||||
}
|
||||
|
||||
// Rotate panel to face the correct direction
|
||||
GL11.glRotatef(-panelYaw, 0.0f, 1.0f, 0.0f);
|
||||
// Rotate panel to face the correct direction (interpolated)
|
||||
GL11.glRotatef(-renderYaw, 0.0f, 1.0f, 0.0f);
|
||||
|
||||
// Hit flash
|
||||
float flashAlpha = computeFlashAlpha(panel);
|
||||
@@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer;
|
||||
import kamkeel.npcs.entity.EntityEnergyAbility;
|
||||
import kamkeel.npcs.entity.EntityEnergyProjectile;
|
||||
import net.minecraft.client.renderer.OpenGlHelper;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.client.renderer.entity.Render;
|
||||
@@ -22,11 +23,22 @@ import org.lwjgl.opengl.GL12;
|
||||
public abstract class RenderEnergyAbility extends Render {
|
||||
|
||||
protected static final ResourceLocation WHITE_TEXTURE = new ResourceLocation("customnpcs", "textures/entity/white.png");
|
||||
protected static final int HIDE_INITIAL_ACTIVE_TICKS = 1;
|
||||
|
||||
public RenderEnergyAbility() {
|
||||
this.shadowSize = 0.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the first active tick to avoid the visible spawn-frame pause at fire time.
|
||||
* Charging previews are never hidden.
|
||||
*/
|
||||
protected boolean shouldSkipInitialActiveRender(Entity entity) {
|
||||
if (!(entity instanceof EntityEnergyAbility)) return false;
|
||||
EntityEnergyAbility ability = (EntityEnergyAbility) entity;
|
||||
return !ability.isPreviewMode() && !ability.isCharging() && ability.ticksExisted <= HIDE_INITIAL_ACTIVE_TICKS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup GL state for translucent, self-illuminated rendering.
|
||||
* Forces full brightness so world lighting does not affect projectiles.
|
||||
@@ -285,6 +297,18 @@ public abstract class RenderEnergyAbility extends Render {
|
||||
int innerColor = entity.getInnerColor();
|
||||
int fadeTime = entity.getLightningFadeTime();
|
||||
|
||||
// Barrier impact spark: briefly force brighter white crackles at the contact point.
|
||||
if (entity instanceof EntityEnergyProjectile) {
|
||||
int sparkTicks = ((EntityEnergyProjectile) entity).getBarrierSparkTicks();
|
||||
if (sparkTicks > 0) {
|
||||
density = Math.max(density, 4.0f + sparkTicks * 0.6f);
|
||||
radius = Math.max(radius, innerRadius + Math.max(0.35f, baseSize * 0.65f));
|
||||
outerColor = 0xF0F6FF;
|
||||
innerColor = 0xFFFFFF;
|
||||
fadeTime = Math.max(fadeTime, 6);
|
||||
}
|
||||
}
|
||||
|
||||
state.update(density, radius, outerColor, innerColor, fadeTime);
|
||||
state.render();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package kamkeel.npcs.client.renderer;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.entity.EntityEnergyBarrier;
|
||||
import kamkeel.npcs.entity.EntityAbilityBarrier;
|
||||
|
||||
/**
|
||||
* Intermediate renderer base for energy barrier entities (Dome, Panel).
|
||||
@@ -15,7 +15,7 @@ public abstract class RenderEnergyBarrier extends RenderEnergyAbility {
|
||||
* Compute the alpha boost from a hit flash effect.
|
||||
* Flash value decays from 4 to 0, producing alpha from 0.3 to 0.
|
||||
*/
|
||||
protected float computeFlashAlpha(EntityEnergyBarrier barrier) {
|
||||
protected float computeFlashAlpha(EntityAbilityBarrier barrier) {
|
||||
byte flash = barrier.getHitFlash();
|
||||
if (flash <= 0) return 0.0f;
|
||||
return flash / 4.0f * 0.3f;
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
package kamkeel.npcs.client.renderer;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.entity.EntityEnergyExplosion;
|
||||
import net.minecraft.entity.Entity;
|
||||
import noppes.npcs.config.ConfigClient;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
/**
|
||||
* Voxel-style renderer for {@link EntityEnergyExplosion}.
|
||||
* Draws expanding cubic shells with a short fade-out.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class RenderEnergyExplosion extends RenderEnergyAbility {
|
||||
|
||||
private static final float MAX_OUTER_ALPHA = 0.85f;
|
||||
private static final float MAX_INNER_ALPHA = 1.0f;
|
||||
private static final float MAX_SMOKE_CUBE_ALPHA = 0.55f;
|
||||
|
||||
@Override
|
||||
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
|
||||
if (shouldSkipInitialActiveRender(entity)) return;
|
||||
if (!(entity instanceof EntityEnergyExplosion)) return;
|
||||
if (ConfigClient.LowResExplosion) return;
|
||||
EntityEnergyExplosion explosion = (EntityEnergyExplosion) entity;
|
||||
|
||||
setupRenderState();
|
||||
|
||||
GL11.glPushMatrix();
|
||||
GL11.glTranslated(x, y, z);
|
||||
|
||||
float radius = explosion.getInterpolatedRadius(partialTicks);
|
||||
float life = explosion.getLifeProgress(partialTicks);
|
||||
if (radius > 0.01f && life < 1.0f) {
|
||||
renderVoxelBurst(explosion, radius, life);
|
||||
if (explosion.hasLightningEffect()) {
|
||||
renderAttachedLightning(explosion, 0.55f, Math.max(0.7f, radius * 0.45f));
|
||||
}
|
||||
}
|
||||
|
||||
GL11.glPopMatrix();
|
||||
restoreRenderState();
|
||||
}
|
||||
|
||||
private void renderVoxelBurst(EntityEnergyExplosion explosion, float radius, float life) {
|
||||
int outerColor = explosion.getOuterColor();
|
||||
int innerColor = explosion.getInnerColor();
|
||||
boolean hasOuter = explosion.isOuterColorEnabled();
|
||||
|
||||
float fade = 1.0f - life;
|
||||
float outerAlpha = Math.min(MAX_OUTER_ALPHA, (0.25f + fade * 0.75f) * explosion.getOuterColorAlpha());
|
||||
float innerAlpha = Math.min(MAX_INNER_ALPHA, (0.30f + fade * 0.80f) * explosion.getInnerAlpha());
|
||||
|
||||
int steps = Math.max(2, Math.min(6, (int) Math.ceil(radius * 0.65f) + 1));
|
||||
float cell = radius / steps;
|
||||
float cubeHalf = Math.max(0.04f, cell * 0.22f * (0.90f - life * 0.25f));
|
||||
long seed = explosion.getRenderSeed();
|
||||
|
||||
GL11.glDepthMask(false);
|
||||
renderSmokeVoxelShell(radius, life, seed);
|
||||
|
||||
for (int ix = -steps; ix <= steps; ix++) {
|
||||
for (int iy = -steps; iy <= steps; iy++) {
|
||||
for (int iz = -steps; iz <= steps; iz++) {
|
||||
int edge = Math.max(Math.abs(ix), Math.max(Math.abs(iy), Math.abs(iz)));
|
||||
if (edge != steps) continue;
|
||||
if (!shouldRenderVoxel(ix, iy, iz, steps, seed, life)) continue;
|
||||
|
||||
float px = ix * cell + jitter(ix, iy, iz, seed, 1) * cell * 0.18f;
|
||||
float py = iy * cell + jitter(ix, iy, iz, seed, 2) * cell * 0.18f;
|
||||
float pz = iz * cell + jitter(ix, iy, iz, seed, 3) * cell * 0.18f;
|
||||
|
||||
GL11.glPushMatrix();
|
||||
GL11.glTranslatef(px, py, pz);
|
||||
|
||||
if (hasOuter) {
|
||||
GL11.glPushMatrix();
|
||||
float outerScale = 1.0f + explosion.getOuterColorWidth() * 0.35f;
|
||||
GL11.glScalef(outerScale, outerScale, outerScale);
|
||||
renderCube(outerColor, outerAlpha * fade, cubeHalf);
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
|
||||
renderCube(innerColor, innerAlpha * fade, cubeHalf * 0.9f);
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
}
|
||||
}
|
||||
renderCoreFlash(innerColor, innerAlpha, radius, cubeHalf, life);
|
||||
GL11.glDepthMask(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds large translucent white/gray cubes to fake a smoky volumetric expansion.
|
||||
*/
|
||||
private void renderSmokeVoxelShell(float radius, float life, long seed) {
|
||||
int smokeSteps = Math.max(2, Math.min(5, (int) Math.ceil(radius * 0.5f) + 1));
|
||||
float smokeCell = radius / smokeSteps * 1.2f;
|
||||
float smokeJitter = smokeCell * 0.28f;
|
||||
float smokeCubeHalf = Math.max(0.07f, smokeCell * 0.24f * (0.95f - life * 0.25f));
|
||||
float smokeAlpha = Math.min(MAX_SMOKE_CUBE_ALPHA, (0.15f + (1.0f - life) * 0.55f));
|
||||
|
||||
for (int ix = -smokeSteps; ix <= smokeSteps; ix++) {
|
||||
for (int iy = -smokeSteps; iy <= smokeSteps; iy++) {
|
||||
for (int iz = -smokeSteps; iz <= smokeSteps; iz++) {
|
||||
int edge = Math.max(Math.abs(ix), Math.max(Math.abs(iy), Math.abs(iz)));
|
||||
if (edge < smokeSteps - 1) continue;
|
||||
|
||||
int h = hash(ix * 5, iy * 7, iz * 11, seed ^ 0x4f1bbcdcL);
|
||||
if ((h & 3) != 0) continue;
|
||||
if (life > 0.72f && ((h >>> 5) & 7) > 2) continue;
|
||||
|
||||
float px = ix * smokeCell + jitter(ix, iy, iz, seed ^ 0x2299aa11L, 1) * smokeJitter;
|
||||
float py = iy * smokeCell + jitter(ix, iy, iz, seed ^ 0x2299aa11L, 2) * smokeJitter;
|
||||
float pz = iz * smokeCell + jitter(ix, iy, iz, seed ^ 0x2299aa11L, 3) * smokeJitter;
|
||||
|
||||
int smokeColor = ((h >>> 9) & 1) == 0 ? 0xFFFFFF : 0xDCDCDC;
|
||||
float alpha = smokeAlpha * (0.78f + (((h >>> 10) & 15) / 15.0f) * 0.22f);
|
||||
|
||||
GL11.glPushMatrix();
|
||||
GL11.glTranslatef(px, py, pz);
|
||||
renderCube(smokeColor, alpha, smokeCubeHalf);
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* White-hot core flash pass for the initial frames.
|
||||
*/
|
||||
private void renderCoreFlash(int innerColor, float innerAlpha, float radius, float cubeHalf, float life) {
|
||||
float flash = saturate((0.60f - life) / 0.60f);
|
||||
if (flash <= 0.0f) return;
|
||||
|
||||
float coreHalf = Math.max(cubeHalf, radius * 0.07f) * (1.0f + flash * 0.55f);
|
||||
float whiteAlpha = Math.min(1.0f, 0.25f + flash * 0.95f);
|
||||
float innerFlashAlpha = Math.min(1.0f, innerAlpha * (0.45f + flash * 0.95f));
|
||||
|
||||
GL11.glPushMatrix();
|
||||
renderCube(0xFFFFFF, whiteAlpha, coreHalf * 1.45f);
|
||||
GL11.glScalef(0.82f, 0.82f, 0.82f);
|
||||
renderCube(innerColor, innerFlashAlpha, coreHalf);
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
|
||||
private float saturate(float value) {
|
||||
if (value < 0.0f) return 0.0f;
|
||||
if (value > 1.0f) return 1.0f;
|
||||
return value;
|
||||
}
|
||||
|
||||
private boolean shouldRenderVoxel(int ix, int iy, int iz, int steps, long seed, float life) {
|
||||
int h = hash(ix, iy, iz, seed);
|
||||
|
||||
int keepMask = steps >= 5 ? 3 : 1;
|
||||
if ((h & keepMask) != 0) return false;
|
||||
|
||||
if (life > 0.55f) {
|
||||
int decay = (h >>> 3) & 7;
|
||||
if (decay > 2) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private float jitter(int ix, int iy, int iz, long seed, int channel) {
|
||||
int h = hash(ix + channel * 17, iy - channel * 31, iz + channel * 47, seed);
|
||||
return (((h >>> 8) & 1023) / 1023.0f) - 0.5f;
|
||||
}
|
||||
|
||||
private int hash(int x, int y, int z, long seed) {
|
||||
long h = seed;
|
||||
h ^= (long) x * 73428767L;
|
||||
h ^= (long) y * 912931L;
|
||||
h ^= (long) z * 19349663L;
|
||||
h ^= (h >>> 33);
|
||||
h *= 0xff51afd7ed558ccdL;
|
||||
h ^= (h >>> 33);
|
||||
return (int) h;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ public class RenderEnergySlicer extends RenderEnergyAbility {
|
||||
|
||||
@Override
|
||||
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
|
||||
if (shouldSkipInitialActiveRender(entity)) {
|
||||
return;
|
||||
}
|
||||
EntityEnergySlicer slicer = (EntityEnergySlicer) entity;
|
||||
|
||||
float width = slicer.getSliceWidth();
|
||||
|
||||
@@ -2,8 +2,8 @@ package kamkeel.npcs.command;
|
||||
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import net.minecraft.command.ICommandSender;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import noppes.npcs.NoppesUtilServer;
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
package kamkeel.npcs.controllers;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.*;
|
||||
import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition;
|
||||
import kamkeel.npcs.controllers.data.ability.conditions.ConditionHPThreshold;
|
||||
import kamkeel.npcs.controllers.data.ability.conditions.ConditionHitCount;
|
||||
import kamkeel.npcs.controllers.data.ability.conditions.ConditionItem;
|
||||
import kamkeel.npcs.controllers.data.ability.conditions.ConditionHasEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.conditions.ConditionQuestCompleted;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.controllers.data.ability.data.IAbilityAction;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.IEffectAction;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.extender.IAbilityExtender;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityCharge;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityCutter;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityDash;
|
||||
@@ -11,7 +24,6 @@ import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityHazard;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilitySlam;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport;
|
||||
@@ -23,7 +35,6 @@ import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDome;
|
||||
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityLaserShot;
|
||||
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityOrb;
|
||||
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityShield;
|
||||
import kamkeel.npcs.controllers.data.ability.type.energy.AbilitySlicer;
|
||||
import kamkeel.npcs.controllers.data.ability.type.energy.AbilitySweeper;
|
||||
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityWall;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
@@ -35,6 +46,7 @@ import noppes.npcs.api.handler.IAbilityHandler;
|
||||
import noppes.npcs.controllers.PlayerDataController;
|
||||
import noppes.npcs.controllers.data.PlayerData;
|
||||
import noppes.npcs.util.NBTJsonUtil;
|
||||
import kamkeel.npcs.util.FileNameHelper;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
@@ -121,19 +133,23 @@ public class AbilityController implements IAbilityHandler {
|
||||
|
||||
private void registerBuiltinTypes() {
|
||||
registerType("cnpc:slam", AbilitySlam::new);
|
||||
registerType("cnpc:charge", AbilityCharge::new);
|
||||
|
||||
registerType("cnpc:heavy_hit", AbilityHeavyHit::new);
|
||||
registerType("cnpc:cutter", AbilityCutter::new);
|
||||
|
||||
// registerType("cnpc:projectile", AbilityProjectile::new);
|
||||
|
||||
registerType("cnpc:sweeper", AbilitySweeper::new);
|
||||
registerType("cnpc:projectile", AbilityProjectile::new);
|
||||
registerType("cnpc:orb", AbilityOrb::new);
|
||||
registerType("cnpc:disc", AbilityDisc::new);
|
||||
registerType("cnpc:laser_shot", AbilityLaserShot::new);
|
||||
registerType("cnpc:beam", AbilityBeam::new);
|
||||
// registerType("cnpc:slicer", AbilitySlicer::new);
|
||||
|
||||
registerType("cnpc:charge", AbilityCharge::new);
|
||||
registerType("cnpc:dash", AbilityDash::new);
|
||||
registerType("cnpc:teleport", AbilityTeleport::new);
|
||||
|
||||
registerType("cnpc:vortex", AbilityVortex::new);
|
||||
registerType("cnpc:shockwave", AbilityShockwave::new);
|
||||
|
||||
@@ -146,12 +162,14 @@ public class AbilityController implements IAbilityHandler {
|
||||
registerType("cnpc:dome", AbilityDome::new);
|
||||
registerType("cnpc:wall", AbilityWall::new);
|
||||
registerType("cnpc:shield", AbilityShield::new);
|
||||
registerType("cnpc:slicer", AbilitySlicer::new);
|
||||
}
|
||||
|
||||
private void registerBuiltinConditionTypes() {
|
||||
registerCondition(ConditionHPThreshold::new);
|
||||
registerCondition(ConditionHitCount::new);
|
||||
registerCondition(ConditionItem::new);
|
||||
registerCondition(ConditionQuestCompleted::new);
|
||||
registerCondition(ConditionHasEffect::new);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -197,6 +215,7 @@ public class AbilityController implements IAbilityHandler {
|
||||
customAbilities.clear();
|
||||
customAbilitiesById.clear();
|
||||
int migrated = 0;
|
||||
Map<String, String> migratedAbilityNames = new HashMap<String, String>();
|
||||
|
||||
File dir = getDir();
|
||||
File[] files = dir.exists() ? dir.listFiles() : null;
|
||||
@@ -205,7 +224,7 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
|
||||
try {
|
||||
String filename = file.getName();
|
||||
String key = filename.substring(0, filename.length() - 5);
|
||||
String fileKey = filename.substring(0, filename.length() - 5);
|
||||
|
||||
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
|
||||
Ability ability = fromNBT(nbt);
|
||||
@@ -218,48 +237,55 @@ public class AbilityController implements IAbilityHandler {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Legacy migration: UUID-named files get renamed to name-based
|
||||
if (UUID_PATTERN.matcher(key).matches()) {
|
||||
String name = ability.getName();
|
||||
if (name == null || name.isEmpty()) {
|
||||
name = key; // Fallback to UUID if no name stored
|
||||
}
|
||||
String storedName = ability.getName();
|
||||
String preferredName;
|
||||
if (UUID_PATTERN.matcher(fileKey).matches()) {
|
||||
preferredName = (storedName != null && !storedName.isEmpty()) ? storedName : fileKey;
|
||||
} else {
|
||||
preferredName = fileKey;
|
||||
}
|
||||
String name = FileNameHelper.sanitizeName(preferredName, "Ability");
|
||||
name = makeUniqueNameForLoad(customAbilities, dir, file, name, fileKey);
|
||||
|
||||
// Ensure unique name
|
||||
while (hasCustomAbilityName(name) || nameFileExists(dir, name)) {
|
||||
name = name + "_";
|
||||
}
|
||||
ability.setName(name);
|
||||
|
||||
// Rename file from UUID.json to name.json
|
||||
File newFile = new File(dir, name + ".json");
|
||||
if (file.renameTo(newFile)) {
|
||||
LogWriter.info("Migrated ability file: " + key + ".json -> " + name + ".json");
|
||||
boolean dirty = false;
|
||||
if (!name.equals(fileKey)) {
|
||||
File renamedFile = new File(dir, name + ".json");
|
||||
if (file.renameTo(renamedFile)) {
|
||||
LogWriter.info("Migrated ability file: " + fileKey + ".json -> " + name + ".json");
|
||||
file = renamedFile;
|
||||
migrated++;
|
||||
dirty = true;
|
||||
} else {
|
||||
LogWriter.error("Failed to rename ability file: " + key + ".json -> " + name + ".json");
|
||||
LogWriter.error("Failed to rename ability file: " + fileKey + ".json -> " + name + ".json");
|
||||
}
|
||||
}
|
||||
|
||||
key = name;
|
||||
migrated++;
|
||||
ability.setName(name);
|
||||
if (storedName != null && !storedName.equals(name)) {
|
||||
dirty = true;
|
||||
if (!storedName.isEmpty()) {
|
||||
migratedAbilityNames.put(storedName, name);
|
||||
}
|
||||
}
|
||||
if (!fileKey.equals(name)) {
|
||||
migratedAbilityNames.put(fileKey, name);
|
||||
}
|
||||
|
||||
// Ensure ability has a UUID; generate one for legacy abilities
|
||||
String uuid = ability.getId();
|
||||
if (uuid == null || uuid.isEmpty() || uuid.equals(key)) {
|
||||
if (uuid == null || uuid.isEmpty() || uuid.equals(fileKey) || uuid.equals(name)) {
|
||||
// Legacy: id was the name or empty — generate a real UUID
|
||||
uuid = UUID.randomUUID().toString();
|
||||
ability.setId(uuid);
|
||||
// Re-save file with the new UUID
|
||||
try {
|
||||
NBTJsonUtil.SaveFile(file, ability.writeNBT());
|
||||
} catch (Exception ex) {
|
||||
LogWriter.error("Failed to save UUID for ability: " + key, ex);
|
||||
}
|
||||
migrated++;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
ability.setName(key);
|
||||
customAbilities.put(key, ability);
|
||||
if (dirty) {
|
||||
NBTJsonUtil.SaveFile(file, ability.writeNBT());
|
||||
}
|
||||
|
||||
customAbilities.put(name, ability);
|
||||
customAbilitiesById.put(uuid, ability);
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e);
|
||||
@@ -269,23 +295,68 @@ public class AbilityController implements IAbilityHandler {
|
||||
|
||||
customAbilityRevision++;
|
||||
if (migrated > 0) {
|
||||
LogWriter.info("Migrated " + migrated + " abilities (UUID-named files or missing UUIDs)");
|
||||
LogWriter.info("Migrated " + migrated + " abilities (filename normalization / UUID repair)");
|
||||
}
|
||||
LogWriter.info("Loaded " + customAbilities.size() + " custom abilities");
|
||||
|
||||
loadChainedAbilities();
|
||||
loadChainedAbilities(migratedAbilityNames);
|
||||
}
|
||||
|
||||
private boolean nameFileExists(File dir, String name) {
|
||||
return new File(dir, name + ".json").exists();
|
||||
private boolean nameFileExists(File dir, String name, File ignoredFile) {
|
||||
File existing = new File(dir, name + ".json");
|
||||
if (!existing.exists()) return false;
|
||||
return ignoredFile == null || !existing.getAbsolutePath().equals(ignoredFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
private String makeUniqueNameForLoad(Map<String, ?> loadedMap, File dir, File currentFile, String baseName, String selfKey) {
|
||||
String candidate = baseName;
|
||||
while ((loadedMap.containsKey(candidate) && !candidate.equals(selfKey))
|
||||
|| nameFileExists(dir, candidate, currentFile)) {
|
||||
candidate = candidate + "_";
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private String makeUniqueNameForMap(Map<String, ?> map, String baseName, String selfName) {
|
||||
String candidate = baseName;
|
||||
while (map.containsKey(candidate) && (selfName == null || !candidate.equals(selfName))) {
|
||||
candidate = candidate + "_";
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private boolean remapChainEntryReferences(ChainedAbility chain, Map<String, String> migratedAbilityNames) {
|
||||
if (chain == null || migratedAbilityNames == null || migratedAbilityNames.isEmpty()) return false;
|
||||
boolean changed = false;
|
||||
for (ChainedAbilityEntry entry : chain.getEntries()) {
|
||||
if (entry == null || !entry.isReference()) continue;
|
||||
String ref = entry.getAbilityReference();
|
||||
if (ref == null || ref.isEmpty()) continue;
|
||||
|
||||
String mapped = migratedAbilityNames.get(ref);
|
||||
if (mapped == null) {
|
||||
for (Map.Entry<String, String> e : migratedAbilityNames.entrySet()) {
|
||||
if (e.getKey().equalsIgnoreCase(ref)) {
|
||||
mapped = e.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mapped != null && !mapped.equals(ref)) {
|
||||
entry.setAbilityReference(mapped);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public boolean saveCustomAbility(Ability ability) {
|
||||
if (ability == null) return false;
|
||||
if (ability.isBuiltIn()) return false;
|
||||
|
||||
String name = ability.getName();
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
String name = FileNameHelper.sanitizeName(ability.getName(), "Ability");
|
||||
ability.setName(name);
|
||||
|
||||
String uuid = ability.getId();
|
||||
boolean isNew = (uuid == null || uuid.isEmpty());
|
||||
@@ -296,26 +367,17 @@ public class AbilityController implements IAbilityHandler {
|
||||
ability.setId(uuid);
|
||||
|
||||
// Ensure unique name
|
||||
while (hasCustomAbilityName(name)) {
|
||||
name = name + "_";
|
||||
}
|
||||
name = makeUniqueNameForMap(customAbilities, name, null);
|
||||
ability.setName(name);
|
||||
} else {
|
||||
// Existing ability — check for rename via UUID lookup
|
||||
Ability existing = customAbilitiesById.get(uuid);
|
||||
if (existing != null) {
|
||||
String oldName = existing.getName();
|
||||
if (!oldName.equals(name)) {
|
||||
// Name changed! Ensure new name is unique (excluding self)
|
||||
String testName = name;
|
||||
while (customAbilities.containsKey(testName) && !testName.equals(oldName)) {
|
||||
testName = testName + "_";
|
||||
}
|
||||
if (!testName.equals(name)) {
|
||||
name = testName;
|
||||
ability.setName(name);
|
||||
}
|
||||
name = makeUniqueNameForMap(customAbilities, name, oldName);
|
||||
ability.setName(name);
|
||||
|
||||
if (!oldName.equals(name)) {
|
||||
// Delete old file and remove old map entry
|
||||
File oldFile = new File(getDir(), oldName + ".json");
|
||||
if (oldFile.exists()) {
|
||||
@@ -323,6 +385,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
customAbilities.remove(oldName);
|
||||
}
|
||||
} else {
|
||||
name = makeUniqueNameForMap(customAbilities, name, null);
|
||||
ability.setName(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,6 +423,15 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
|
||||
Ability removed = customAbilities.remove(name);
|
||||
if (removed == null) {
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
if (!sanitized.equals(name)) {
|
||||
removed = customAbilities.remove(sanitized);
|
||||
if (removed != null) {
|
||||
name = sanitized;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removed == null) return false;
|
||||
|
||||
// Also remove from UUID index
|
||||
@@ -396,7 +470,13 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
|
||||
public Ability getCustomAbility(String name) {
|
||||
return customAbilities.get(name);
|
||||
Ability ability = customAbilities.get(name);
|
||||
if (ability != null) return ability;
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
if (!sanitized.equals(name)) {
|
||||
return customAbilities.get(sanitized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Set<String> getCustomAbilityNames() {
|
||||
@@ -436,9 +516,10 @@ public class AbilityController implements IAbilityHandler {
|
||||
// CHAINED ABILITIES
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
private void loadChainedAbilities() {
|
||||
private void loadChainedAbilities(Map<String, String> migratedAbilityNames) {
|
||||
chainedAbilities.clear();
|
||||
chainedAbilitiesById.clear();
|
||||
int migrated = 0;
|
||||
|
||||
File dir = getChainedDir();
|
||||
File[] files = dir.exists() ? dir.listFiles() : null;
|
||||
@@ -447,19 +528,51 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
|
||||
try {
|
||||
String filename = file.getName();
|
||||
String key = filename.substring(0, filename.length() - 5);
|
||||
String fileKey = filename.substring(0, filename.length() - 5);
|
||||
|
||||
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
|
||||
ChainedAbility chain = new ChainedAbility();
|
||||
chain.readNBT(nbt);
|
||||
|
||||
chain.setName(key);
|
||||
chainedAbilities.put(key, chain);
|
||||
String storedName = chain.getName();
|
||||
String name = FileNameHelper.sanitizeName(fileKey, "Chain");
|
||||
name = makeUniqueNameForLoad(chainedAbilities, dir, file, name, fileKey);
|
||||
|
||||
boolean dirty = false;
|
||||
if (!name.equals(fileKey)) {
|
||||
File renamedFile = new File(dir, name + ".json");
|
||||
if (file.renameTo(renamedFile)) {
|
||||
LogWriter.info("Migrated chained ability file: " + fileKey + ".json -> " + name + ".json");
|
||||
file = renamedFile;
|
||||
dirty = true;
|
||||
} else {
|
||||
LogWriter.error("Failed to rename chained ability file: " + fileKey + ".json -> " + name + ".json");
|
||||
}
|
||||
}
|
||||
|
||||
chain.setName(name);
|
||||
if (storedName != null && !storedName.equals(name)) {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (remapChainEntryReferences(chain, migratedAbilityNames)) {
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
String uuid = chain.getId();
|
||||
if (uuid != null && !uuid.isEmpty()) {
|
||||
chainedAbilitiesById.put(uuid, chain);
|
||||
if (uuid == null || uuid.isEmpty() || uuid.equals(fileKey) || uuid.equals(name)) {
|
||||
uuid = UUID.randomUUID().toString();
|
||||
chain.setId(uuid);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
if (dirty) {
|
||||
NBTJsonUtil.SaveFile(file, chain.writeNBT());
|
||||
migrated++;
|
||||
}
|
||||
|
||||
chainedAbilities.put(name, chain);
|
||||
chainedAbilitiesById.put(uuid, chain);
|
||||
} catch (Exception e) {
|
||||
LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e);
|
||||
}
|
||||
@@ -467,14 +580,17 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
|
||||
chainedAbilityRevision++;
|
||||
if (migrated > 0) {
|
||||
LogWriter.info("Migrated " + migrated + " chained abilities");
|
||||
}
|
||||
LogWriter.info("Loaded " + chainedAbilities.size() + " chained abilities");
|
||||
}
|
||||
|
||||
public boolean saveChainedAbility(ChainedAbility chain) {
|
||||
if (chain == null) return false;
|
||||
|
||||
String name = chain.getName();
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
String name = FileNameHelper.sanitizeName(chain.getName(), "Chain");
|
||||
chain.setName(name);
|
||||
|
||||
String uuid = chain.getId();
|
||||
boolean isNew = (uuid == null || uuid.isEmpty());
|
||||
@@ -483,30 +599,25 @@ public class AbilityController implements IAbilityHandler {
|
||||
uuid = UUID.randomUUID().toString();
|
||||
chain.setId(uuid);
|
||||
|
||||
while (hasChainedAbilityName(name)) {
|
||||
name = name + "_";
|
||||
}
|
||||
name = makeUniqueNameForMap(chainedAbilities, name, null);
|
||||
chain.setName(name);
|
||||
} else {
|
||||
ChainedAbility existing = chainedAbilitiesById.get(uuid);
|
||||
if (existing != null) {
|
||||
String oldName = existing.getName();
|
||||
if (!oldName.equals(name)) {
|
||||
String testName = name;
|
||||
while (chainedAbilities.containsKey(testName) && !testName.equals(oldName)) {
|
||||
testName = testName + "_";
|
||||
}
|
||||
if (!testName.equals(name)) {
|
||||
name = testName;
|
||||
chain.setName(name);
|
||||
}
|
||||
name = makeUniqueNameForMap(chainedAbilities, name, oldName);
|
||||
chain.setName(name);
|
||||
|
||||
if (!oldName.equals(name)) {
|
||||
File oldFile = new File(getChainedDir(), oldName + ".json");
|
||||
if (oldFile.exists()) {
|
||||
oldFile.delete();
|
||||
}
|
||||
chainedAbilities.remove(oldName);
|
||||
}
|
||||
} else {
|
||||
name = makeUniqueNameForMap(chainedAbilities, name, null);
|
||||
chain.setName(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,6 +653,15 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
|
||||
ChainedAbility removed = chainedAbilities.remove(name);
|
||||
if (removed == null) {
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
if (!sanitized.equals(name)) {
|
||||
removed = chainedAbilities.remove(sanitized);
|
||||
if (removed != null) {
|
||||
name = sanitized;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (removed == null) return false;
|
||||
|
||||
String uuid = removed.getId();
|
||||
@@ -592,6 +712,12 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
}
|
||||
|
||||
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
|
||||
if (!sanitizedKey.equals(key)) {
|
||||
ChainedAbility sanitized = chainedAbilities.get(sanitizedKey);
|
||||
if (sanitized != null) return sanitized.deepCopy();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -602,12 +728,20 @@ public class AbilityController implements IAbilityHandler {
|
||||
for (String name : chainedAbilities.keySet()) {
|
||||
if (name.equalsIgnoreCase(key)) return true;
|
||||
}
|
||||
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
|
||||
if (!sanitizedKey.equals(key) && chainedAbilities.containsKey(sanitizedKey)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChainedAbility getChainedAbility(String name) {
|
||||
ChainedAbility chain = chainedAbilities.get(name);
|
||||
if (chain == null) {
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
if (!sanitized.equals(name)) {
|
||||
chain = chainedAbilities.get(sanitized);
|
||||
}
|
||||
}
|
||||
return chain != null ? chain.deepCopy() : null;
|
||||
}
|
||||
|
||||
@@ -630,7 +764,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
|
||||
@Override
|
||||
public boolean hasChainedAbilityName(String name) {
|
||||
return chainedAbilities.containsKey(name);
|
||||
if (chainedAbilities.containsKey(name)) return true;
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
return !sanitized.equals(name) && chainedAbilities.containsKey(sanitized);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -718,6 +854,12 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
}
|
||||
|
||||
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
|
||||
if (!sanitizedKey.equals(key)) {
|
||||
Ability sanitized = customAbilities.get(sanitizedKey);
|
||||
if (sanitized != null) return sanitized.deepCopy();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -762,8 +904,14 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
|
||||
public boolean hasAbility(String key) {
|
||||
return builtAbilities.containsKey(key) || customAbilities.containsKey(key)
|
||||
|| customAbilitiesById.containsKey(key);
|
||||
if (builtAbilities.containsKey(key) || customAbilities.containsKey(key)
|
||||
|| customAbilitiesById.containsKey(key)) {
|
||||
return true;
|
||||
}
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(key);
|
||||
return !sanitized.equals(key)
|
||||
&& (builtAbilities.containsKey(sanitized) || customAbilities.containsKey(sanitized)
|
||||
|| customAbilitiesById.containsKey(sanitized));
|
||||
}
|
||||
|
||||
public Ability getCustomAbilityByUUID(String uuid) {
|
||||
@@ -771,7 +919,13 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
|
||||
public Ability getCustomAbilityByName(String name) {
|
||||
return customAbilities.get(name);
|
||||
Ability ability = customAbilities.get(name);
|
||||
if (ability != null) return ability;
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
if (!sanitized.equals(name)) {
|
||||
return customAbilities.get(sanitized);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -805,6 +959,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) return true;
|
||||
}
|
||||
|
||||
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
|
||||
if (!sanitizedKey.equals(key) && customAbilities.containsKey(sanitizedKey)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -901,6 +1058,28 @@ public class AbilityController implements IAbilityHandler {
|
||||
return conditionTypes.keySet().toArray(new String[0]);
|
||||
}
|
||||
|
||||
public List<String> getConditionNamespaces() {
|
||||
LinkedHashSet<String> seen = new LinkedHashSet<>();
|
||||
for (String typeId : conditionTypes.keySet()) {
|
||||
String[] parts = typeId.split("\\.", 3);
|
||||
if (parts.length >= 2) {
|
||||
seen.add(parts[1]);
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(seen);
|
||||
}
|
||||
|
||||
public String[] getConditionTypesByNamespace(String namespace) {
|
||||
String prefix = "condition." + namespace + ".";
|
||||
List<String> result = new ArrayList<>();
|
||||
for (String typeId : conditionTypes.keySet()) {
|
||||
if (typeId.startsWith(prefix)) {
|
||||
result.add(typeId);
|
||||
}
|
||||
}
|
||||
return result.toArray(new String[0]);
|
||||
}
|
||||
|
||||
public void registerFlightChecker(Predicate<EntityPlayer> checker) {
|
||||
flightCheckers.add(checker);
|
||||
}
|
||||
@@ -974,6 +1153,29 @@ public class AbilityController implements IAbilityHandler {
|
||||
return damage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire modifyBarrierHealth on all extenders. Cumulative — each extender's output feeds the next.
|
||||
*/
|
||||
public float fireModifyBarrierHealth(Ability ability, EntityLivingBase caster, float baseHealth) {
|
||||
float health = baseHealth;
|
||||
for (IAbilityExtender ext : extenders) {
|
||||
health = ext.modifyBarrierHealth(ability, caster, health);
|
||||
}
|
||||
return health;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire onAbilityHeal on all extenders. Chain of responsibility — first true wins.
|
||||
*/
|
||||
public boolean fireOnAbilityHeal(Ability ability, EntityLivingBase caster, EntityLivingBase target, float healAmount) {
|
||||
for (IAbilityExtender ext : extenders) {
|
||||
if (ext.onAbilityHeal(ability, caster, target, healAmount)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// IAbilityHandler
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -1067,7 +1269,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
|
||||
@Override
|
||||
public boolean hasCustomAbilityName(String name) {
|
||||
return customAbilities.containsKey(name);
|
||||
if (customAbilities.containsKey(name)) return true;
|
||||
String sanitized = FileNameHelper.sanitizeTextInput(name);
|
||||
return !sanitized.equals(name) && customAbilities.containsKey(sanitized);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -832,15 +832,6 @@ public class ProfileController implements IProfileHandler {
|
||||
else
|
||||
compound = (NBTTagCompound) compound.copy();
|
||||
playerData.setNBT(compound);
|
||||
|
||||
// Trade data is shared across all profile slots - copy from player's live data
|
||||
PlayerData liveData = PlayerData.get(player);
|
||||
if (liveData != null) {
|
||||
NBTTagCompound tradeCompound = new NBTTagCompound();
|
||||
liveData.tradeData.writeToNBT(tradeCompound);
|
||||
playerData.tradeData.readFromNBT(tradeCompound);
|
||||
}
|
||||
|
||||
return playerData;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.Unpooled;
|
||||
import kamkeel.npcs.addon.DBCAddon;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
|
||||
import kamkeel.npcs.network.LargeAbstractPacket;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.enums.EnumSyncAction;
|
||||
|
||||
@@ -3,10 +3,26 @@ package kamkeel.npcs.controllers.data.ability;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.data.AbilityIconData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.IAbilityAction;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.InvulnerableMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
|
||||
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.RotationMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityAbilityBarrier;
|
||||
import kamkeel.npcs.entity.EntityAbilityDome;
|
||||
import kamkeel.npcs.entity.EntityAbilityPanel;
|
||||
import kamkeel.npcs.util.FileNameHelper;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
@@ -20,9 +36,6 @@ import net.minecraft.util.MovingObjectPosition;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import kamkeel.npcs.entity.EntityEnergyBarrier;
|
||||
import kamkeel.npcs.entity.EntityEnergyDome;
|
||||
import kamkeel.npcs.entity.EntityEnergyPanel;
|
||||
import noppes.npcs.DataAbilities;
|
||||
import noppes.npcs.NpcDamageSource;
|
||||
import noppes.npcs.api.INbt;
|
||||
@@ -74,11 +87,12 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
|
||||
// Interruption
|
||||
protected boolean interruptible = true;
|
||||
protected InvulnerableMode invulnerableMode = InvulnerableMode.NONE;
|
||||
|
||||
// Feedback
|
||||
protected LockMovementType lockMovement = LockMovementType.WINDUP;
|
||||
protected LockMode lockMovement = LockMode.WINDUP;
|
||||
protected RotationMode rotationMode = RotationMode.FREE;
|
||||
protected LockMovementType rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
protected LockMode rotationPhase = LockMode.WINDUP_AND_ACTIVE;
|
||||
protected int windUpColor = 0x80FF4400; // Telegraph color during wind up
|
||||
protected int activeColor = 0xC0FF0000; // Telegraph warning/active color
|
||||
|
||||
@@ -114,6 +128,10 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
// instead of sharing the global cooldown with other abilities
|
||||
protected boolean perAbilityCooldown = false;
|
||||
|
||||
// Free on Cast: if true, ability enters cooldown immediately after entities are spawned,
|
||||
// allowing the caster to use other abilities while summoned entities remain active
|
||||
protected boolean freeOnCast = false;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// BUILT-IN (set in constructor via configureAsBuiltIn, NOT persisted)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -255,6 +273,22 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this ability type supports the Free on Cast option.
|
||||
* Override to return true for abilities that spawn persistent entities (projectiles, barriers, zones, sweeper).
|
||||
*/
|
||||
public boolean allowFreeOnCast() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach all spawned entities (let them live independently) without killing them.
|
||||
* Called during signalCompletion() when freeOnCast is enabled.
|
||||
* Override in subclasses to null entity references without calling setDead().
|
||||
*/
|
||||
public void detach() {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// TOGGLE CALLBACKS (optional overrides for toggleable abilities)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -431,6 +465,11 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
}
|
||||
}
|
||||
|
||||
// Save velocity before damage to cancel vanilla knockback when ability knockback is zero
|
||||
double prevMotionX = hitEntity.motionX;
|
||||
double prevMotionY = hitEntity.motionY;
|
||||
double prevMotionZ = hitEntity.motionZ;
|
||||
|
||||
// Apply damage
|
||||
if (damage > 0) {
|
||||
// Check for ability extenders (e.g., DBC Addon damage routing)
|
||||
@@ -449,9 +488,14 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply knockback if any
|
||||
// Apply knockback if any, otherwise restore velocity to cancel vanilla knockback
|
||||
if (knockback > 0 || knockbackUp > 0) {
|
||||
applyKnockback(hitEntity, knockbackDirX, knockbackDirZ, knockback, knockbackUp);
|
||||
} else {
|
||||
hitEntity.motionX = prevMotionX;
|
||||
hitEntity.motionY = prevMotionY;
|
||||
hitEntity.motionZ = prevMotionZ;
|
||||
hitEntity.velocityChanged = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -612,11 +656,15 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks).range(0, 10000),
|
||||
FieldDef.boolField("ability.perAbilityCooldown", this::isPerAbilityCooldown, this::setPerAbilityCooldown)
|
||||
).tab("General"));
|
||||
if (allowFreeOnCast()) {
|
||||
defs.add(FieldDef.boolField("ability.freeOnCast", this::isFreeOnCast, this::setFreeOnCast)
|
||||
.tab("General").hover("ability.hover.freeOnCast"));
|
||||
}
|
||||
defs.add(FieldDef.section("ability.section.movement").tab("General"));
|
||||
defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(),
|
||||
defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMode.getDisplayKeys(),
|
||||
() -> this.getLockMovement().getDisplayKey(),
|
||||
v -> {
|
||||
for (LockMovementType t : LockMovementType.values()) {
|
||||
for (LockMode t : LockMode.values()) {
|
||||
if (t.getDisplayKey().equals(v)) {
|
||||
this.setLockMovement(t);
|
||||
break;
|
||||
@@ -640,7 +688,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
FieldDef.stringEnumField("ability.rotationPhase", getRotationPhaseKeys(),
|
||||
() -> this.getRotationPhase().getDisplayKey(),
|
||||
v -> {
|
||||
for (LockMovementType t : LockMovementType.values()) {
|
||||
for (LockMode t : LockMode.values()) {
|
||||
if (t.getDisplayKey().equals(v)) {
|
||||
this.setRotationPhase(t);
|
||||
break;
|
||||
@@ -652,10 +700,24 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
).tab("General"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible)
|
||||
.hover("ability.hover.interruptible"),
|
||||
.hover("ability.hover.interruptible")
|
||||
.enabledWhen(() -> !invulnerableMode.invulnerableDuringWindup()),
|
||||
FieldDef.intField("ability.dazedTicks", this::getDazedTicks, this::setDazedTicks)
|
||||
.range(0, 1000).visibleWhen(this::isInterruptible)
|
||||
.range(0, 1000)
|
||||
.visibleWhen(() -> isInterruptible() && !invulnerableMode.invulnerableDuringWindup())
|
||||
).tab("General"));
|
||||
defs.add(FieldDef.stringEnumField("ability.invulnerable", InvulnerableMode.getDisplayKeys(),
|
||||
() -> this.getInvulnerableMode().getDisplayKey(),
|
||||
v -> {
|
||||
for (InvulnerableMode mode : InvulnerableMode.values()) {
|
||||
if (mode.getDisplayKey().equals(v)) {
|
||||
this.setInvulnerableMode(mode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
})
|
||||
.hover("ability.hover.invulnerable")
|
||||
.tab("General"));
|
||||
|
||||
// ── Burst section ────────────────────────────────────────────
|
||||
if (allowBurst()) {
|
||||
@@ -863,11 +925,17 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
// AOE_SELF abilities: telegraph follows caster during windup
|
||||
instance.setEntityIdToFollow(caster.getEntityId());
|
||||
|
||||
// For LINE/CONE telegraphs: track target direction during windup
|
||||
// For LINE/CONE telegraphs: track direction during windup
|
||||
// Only track if rotation is NOT locked during windup (NPC can still turn)
|
||||
if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) {
|
||||
if (telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) {
|
||||
if (!isRotationLockedDuringWindup()) {
|
||||
instance.setTargetEntityId(target.getEntityId());
|
||||
if (target != null) {
|
||||
// Face the target entity
|
||||
instance.setTargetEntityId(target.getEntityId());
|
||||
} else {
|
||||
// No target (AOE_SELF etc.): track caster's own rotationYaw
|
||||
instance.setTrackFollowedEntityYaw(true);
|
||||
}
|
||||
}
|
||||
// If rotation is locked, yaw stays fixed at creation time
|
||||
}
|
||||
@@ -1066,7 +1134,12 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
|
||||
// Final completion - let burst entities die naturally (don't force-kill)
|
||||
burstEntities.clear();
|
||||
cleanup();
|
||||
if (freeOnCast) {
|
||||
// Free on Cast: detach entities (let them live independently) instead of killing
|
||||
detach();
|
||||
} else {
|
||||
cleanup();
|
||||
}
|
||||
phase = AbilityPhase.IDLE;
|
||||
currentTick = 0;
|
||||
return true;
|
||||
@@ -1095,6 +1168,20 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
telegraphInstances.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel this ability (voluntary player action). Goes directly to IDLE, skipping DAZED.
|
||||
* Entities are killed via cleanup(). Unlike interrupt(), this never enters DAZED phase.
|
||||
*/
|
||||
public void cancel() {
|
||||
cleanupBurstEntities();
|
||||
cleanup();
|
||||
phase = AbilityPhase.IDLE;
|
||||
currentTick = 0;
|
||||
burstIndex = 0;
|
||||
currentTarget = null;
|
||||
telegraphInstances.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if executing
|
||||
*/
|
||||
@@ -1114,6 +1201,9 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
if (!interruptible || (phase != AbilityPhase.WINDUP && phase != AbilityPhase.BURST_DELAY)) {
|
||||
return false;
|
||||
}
|
||||
if (isInvulnerableForCurrentPhase()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only direct physical hits can interrupt, not magic, fire, or other indirect damage
|
||||
if (source == null) {
|
||||
@@ -1272,6 +1362,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
nbt.setBoolean("syncWindup", syncWindupWithAnimation);
|
||||
nbt.setInteger("recovery", dazedTicks);
|
||||
nbt.setBoolean("interruptible", interruptible);
|
||||
nbt.setInteger("invulnerableMode", invulnerableMode.ordinal());
|
||||
nbt.setInteger("lockMovement", lockMovement.ordinal());
|
||||
nbt.setInteger("rotationMode", rotationMode.ordinal());
|
||||
nbt.setInteger("rotationPhase", rotationPhase.ordinal());
|
||||
@@ -1292,6 +1383,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
nbt.setInteger("allowedBy", allowedBy.ordinal());
|
||||
nbt.setBoolean("ignoreCooldown", ignoreCooldown);
|
||||
nbt.setBoolean("perAbilityCooldown", perAbilityCooldown);
|
||||
nbt.setBoolean("freeOnCast", freeOnCast);
|
||||
|
||||
// Toggle
|
||||
nbt.setInteger("toggleStates", toggleStates);
|
||||
@@ -1350,9 +1442,12 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
syncWindupWithAnimation = nbt.hasKey("syncWindup") ? nbt.getBoolean("syncWindup") : true;
|
||||
dazedTicks = nbt.hasKey("recovery") ? nbt.getInteger("recovery") : 80;
|
||||
interruptible = nbt.hasKey("interruptible") ? nbt.getBoolean("interruptible") : true;
|
||||
lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement"));
|
||||
invulnerableMode = nbt.hasKey("invulnerableMode")
|
||||
? InvulnerableMode.fromOrdinal(nbt.getInteger("invulnerableMode"))
|
||||
: InvulnerableMode.NONE;
|
||||
lockMovement = LockMode.fromOrdinal(nbt.getInteger("lockMovement"));
|
||||
rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode"));
|
||||
rotationPhase = LockMovementType.fromOrdinal(nbt.getInteger("rotationPhase"));
|
||||
rotationPhase = LockMode.fromOrdinal(nbt.getInteger("rotationPhase"));
|
||||
windUpColor = nbt.getInteger("windUpColor");
|
||||
activeColor = nbt.getInteger("activeColor");
|
||||
windUpSound = nbt.getString("windUpSound");
|
||||
@@ -1374,6 +1469,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
allowedBy = UserType.fromOrdinal(nbt.getInteger("allowedBy"));
|
||||
ignoreCooldown = nbt.getBoolean("ignoreCooldown");
|
||||
perAbilityCooldown = nbt.getBoolean("perAbilityCooldown");
|
||||
freeOnCast = nbt.getBoolean("freeOnCast");
|
||||
|
||||
// Toggle
|
||||
toggleStates = nbt.getInteger("toggleStates");
|
||||
@@ -1439,7 +1535,12 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
if (builtIn) {
|
||||
this.name = name != null ? name : "";
|
||||
return;
|
||||
}
|
||||
String fallback = (this.name != null && !this.name.isEmpty()) ? this.name : "Ability";
|
||||
this.name = FileNameHelper.sanitizeName(name, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1448,7 +1549,9 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
* Converts & color codes to § for rendering.
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
String result = (displayName != null && !displayName.isEmpty()) ? displayName : name;
|
||||
String result = (displayName != null && !displayName.isEmpty())
|
||||
? displayName
|
||||
: FileNameHelper.toDisplayName(name);
|
||||
return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : "";
|
||||
}
|
||||
|
||||
@@ -1602,11 +1705,27 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
this.interruptible = interruptible;
|
||||
}
|
||||
|
||||
public LockMovementType getLockMovement() {
|
||||
public InvulnerableMode getInvulnerableMode() {
|
||||
return invulnerableMode;
|
||||
}
|
||||
|
||||
public void setInvulnerableMode(InvulnerableMode invulnerableMode) {
|
||||
this.invulnerableMode = invulnerableMode != null ? invulnerableMode : InvulnerableMode.NONE;
|
||||
}
|
||||
|
||||
public boolean isInvulnerableDuringWindup() {
|
||||
return invulnerableMode.invulnerableDuringWindup();
|
||||
}
|
||||
|
||||
public boolean isInvulnerableDuringActive() {
|
||||
return invulnerableMode.invulnerableDuringActive();
|
||||
}
|
||||
|
||||
public LockMode getLockMovement() {
|
||||
return lockMovement;
|
||||
}
|
||||
|
||||
public void setLockMovement(LockMovementType lockMovement) {
|
||||
public void setLockMovement(LockMode lockMovement) {
|
||||
this.lockMovement = lockMovement;
|
||||
}
|
||||
|
||||
@@ -1618,11 +1737,11 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
this.rotationMode = rotationMode;
|
||||
}
|
||||
|
||||
public LockMovementType getRotationPhase() {
|
||||
public LockMode getRotationPhase() {
|
||||
return rotationPhase;
|
||||
}
|
||||
|
||||
public void setRotationPhase(LockMovementType rotationPhase) {
|
||||
public void setRotationPhase(LockMode rotationPhase) {
|
||||
this.rotationPhase = rotationPhase;
|
||||
}
|
||||
|
||||
@@ -1643,7 +1762,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
*/
|
||||
@Override
|
||||
public void setLockMovementType(int type) {
|
||||
this.lockMovement = LockMovementType.fromOrdinal(type);
|
||||
this.lockMovement = LockMode.fromOrdinal(type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1683,7 +1802,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
*/
|
||||
@Override
|
||||
public void setRotationPhaseType(int type) {
|
||||
this.rotationPhase = LockMovementType.fromOrdinal(type);
|
||||
this.rotationPhase = LockMode.fromOrdinal(type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1734,6 +1853,20 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this ability grants invulnerability during its current execution phase.
|
||||
*/
|
||||
public boolean isInvulnerableForCurrentPhase() {
|
||||
return invulnerableMode.isInvulnerableInPhase(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this ability grants invulnerability for a specific phase.
|
||||
*/
|
||||
public boolean isInvulnerableForPhase(AbilityPhase phase) {
|
||||
return invulnerableMode.isInvulnerableInPhase(phase);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rotation should be locked during the current phase.
|
||||
*/
|
||||
@@ -1970,6 +2103,14 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
this.perAbilityCooldown = perAbilityCooldown;
|
||||
}
|
||||
|
||||
public boolean isFreeOnCast() {
|
||||
return freeOnCast;
|
||||
}
|
||||
|
||||
public void setFreeOnCast(boolean freeOnCast) {
|
||||
this.freeOnCast = freeOnCast;
|
||||
}
|
||||
|
||||
public boolean isToggleable() {
|
||||
return toggleStates > 0;
|
||||
}
|
||||
@@ -2205,9 +2346,9 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
double maxZ = Math.max(caster.posZ, target.posZ) + searchRange;
|
||||
|
||||
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
List<EntityEnergyBarrier> barriers = world.getEntitiesWithinAABB(EntityEnergyBarrier.class, searchBox);
|
||||
List<EntityAbilityBarrier> barriers = world.getEntitiesWithinAABB(EntityAbilityBarrier.class, searchBox);
|
||||
|
||||
for (EntityEnergyBarrier barrier : barriers) {
|
||||
for (EntityAbilityBarrier barrier : barriers) {
|
||||
if (barrier.isDead) continue;
|
||||
if (barrier.isCharging()) continue;
|
||||
|
||||
@@ -2218,10 +2359,10 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
if (AbilityTargetHelper.isAlly(caster, (EntityLivingBase) barrierOwner)) continue;
|
||||
}
|
||||
|
||||
if (barrier instanceof EntityEnergyDome) {
|
||||
if (isDomeBlocking((EntityEnergyDome) barrier, caster, target)) return true;
|
||||
} else if (barrier instanceof EntityEnergyPanel) {
|
||||
if (isPanelBlocking((EntityEnergyPanel) barrier, caster, target)) return true;
|
||||
if (barrier instanceof EntityAbilityDome) {
|
||||
if (isDomeBlocking((EntityAbilityDome) barrier, caster, target)) return true;
|
||||
} else if (barrier instanceof EntityAbilityPanel) {
|
||||
if (isPanelBlocking((EntityAbilityPanel) barrier, caster, target)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2232,7 +2373,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
* Check if a dome blocks an attack from caster to target.
|
||||
* Blocks when caster is outside and target is inside the dome.
|
||||
*/
|
||||
private static boolean isDomeBlocking(EntityEnergyDome dome, EntityLivingBase caster, EntityLivingBase target) {
|
||||
private static boolean isDomeBlocking(EntityAbilityDome dome, EntityLivingBase caster, EntityLivingBase target) {
|
||||
float radius = dome.getDomeRadius();
|
||||
|
||||
double cdx = caster.posX - dome.posX;
|
||||
@@ -2253,7 +2394,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
* Check if a panel blocks an attack from caster to target.
|
||||
* Checks if the line from caster to target crosses through the panel bounds.
|
||||
*/
|
||||
private static boolean isPanelBlocking(EntityEnergyPanel panel, EntityLivingBase caster, EntityLivingBase target) {
|
||||
private static boolean isPanelBlocking(EntityAbilityPanel panel, EntityLivingBase caster, EntityLivingBase target) {
|
||||
float halfW = panel.getPanelData().panelWidth * 0.5f;
|
||||
float halfH = panel.getPanelData().panelHeight * 0.5f;
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.data.IAbilityAction;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
/**
|
||||
* Unified slot for storing any combat action: inline ability, ability reference, or chain reference.
|
||||
* Replaces {@link AbilitySlot} with support for all {@link IAbilityAction} types.
|
||||
* Support for all {@link IAbilityAction} types.
|
||||
* <p>
|
||||
* References are lazy-resolved via their respective controllers and cached until
|
||||
* the controller revision changes.
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
/**
|
||||
* Wrapper that stores either an inline ability (full NBT) or a reference
|
||||
* to a built-in/custom ability (by name or UUID).
|
||||
* <p>
|
||||
* References are lazy-resolved via {@link AbilityController#resolveAbility(String)}
|
||||
* and cached until the controller version changes.
|
||||
*
|
||||
* @deprecated Use {@link AbilityAction} instead, which supports abilities and chained abilities.
|
||||
*/
|
||||
@Deprecated
|
||||
public class AbilitySlot {
|
||||
|
||||
private Ability inlineAbility;
|
||||
private String referenceId;
|
||||
|
||||
/**
|
||||
* Per-slot enabled override for reference slots (null = use master's default).
|
||||
*/
|
||||
private Boolean enabledOverride;
|
||||
|
||||
private transient Ability cachedAbility;
|
||||
private transient int cachedVersion = -1;
|
||||
|
||||
private AbilitySlot() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a slot with an inline (embedded) ability.
|
||||
*/
|
||||
public static AbilitySlot inline(Ability ability) {
|
||||
AbilitySlot slot = new AbilitySlot();
|
||||
slot.inlineAbility = ability;
|
||||
slot.referenceId = null;
|
||||
return slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a slot that references a built-in ability (by name) or custom ability (by name).
|
||||
*/
|
||||
public static AbilitySlot reference(String referenceId) {
|
||||
AbilitySlot slot = new AbilitySlot();
|
||||
slot.inlineAbility = null;
|
||||
slot.referenceId = referenceId;
|
||||
return slot;
|
||||
}
|
||||
|
||||
public boolean isReference() {
|
||||
return referenceId != null;
|
||||
}
|
||||
|
||||
public String getReferenceId() {
|
||||
return referenceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved ability. For inline slots, returns the ability directly.
|
||||
* For reference slots, lazily resolves from AbilityController and caches.
|
||||
* Returns null if the reference cannot be resolved (broken ref).
|
||||
*/
|
||||
public Ability getAbility() {
|
||||
if (!isReference()) {
|
||||
return inlineAbility;
|
||||
}
|
||||
|
||||
// Lazy resolve with version-based cache invalidation
|
||||
AbilityController controller = AbilityController.Instance;
|
||||
if (controller == null) return null;
|
||||
|
||||
int currentVersion = controller.getCustomAbilityRevision();
|
||||
if (cachedAbility != null && cachedVersion == currentVersion) {
|
||||
return cachedAbility;
|
||||
}
|
||||
|
||||
cachedAbility = controller.resolveAbility(referenceId);
|
||||
// Apply per-slot enabled override so NPC-specific enable/disable persists
|
||||
if (cachedAbility != null && enabledOverride != null) {
|
||||
cachedAbility.setEnabled(enabledOverride);
|
||||
}
|
||||
cachedVersion = currentVersion;
|
||||
return cachedAbility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the per-slot enabled override for reference slots.
|
||||
* For inline slots, delegates directly to the ability.
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
if (!isReference()) {
|
||||
if (inlineAbility != null) {
|
||||
inlineAbility.setEnabled(enabled);
|
||||
}
|
||||
} else {
|
||||
this.enabledOverride = enabled;
|
||||
// Also update the cached ability immediately if present
|
||||
if (cachedAbility != null) {
|
||||
cachedAbility.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inline ability directly (null for reference slots).
|
||||
*/
|
||||
public Ability getInlineAbility() {
|
||||
return inlineAbility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this reference slot to an inline slot by copying the resolved ability.
|
||||
* No-op if already inline. Returns false if resolution fails.
|
||||
*/
|
||||
public boolean convertToInline() {
|
||||
if (!isReference()) return true;
|
||||
|
||||
Ability resolved = getAbility();
|
||||
if (resolved == null) return false;
|
||||
|
||||
// Deep copy via NBT round-trip
|
||||
inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT());
|
||||
referenceId = null;
|
||||
cachedAbility = null;
|
||||
cachedVersion = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// NBT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public NBTTagCompound writeNBT() {
|
||||
if (isReference()) {
|
||||
NBTTagCompound nbt = new NBTTagCompound();
|
||||
nbt.setString("Reference", referenceId);
|
||||
if (enabledOverride != null) {
|
||||
nbt.setBoolean("RefEnabled", enabledOverride);
|
||||
}
|
||||
return nbt;
|
||||
}
|
||||
return inlineAbility != null ? inlineAbility.writeNBT() : new NBTTagCompound();
|
||||
}
|
||||
|
||||
public static AbilitySlot fromNBT(NBTTagCompound nbt) {
|
||||
if (nbt == null) return null;
|
||||
|
||||
if (nbt.hasKey("Reference")) {
|
||||
AbilitySlot slot = reference(nbt.getString("Reference"));
|
||||
if (nbt.hasKey("RefEnabled")) {
|
||||
slot.enabledOverride = nbt.getBoolean("RefEnabled");
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Inline ability
|
||||
Ability ability = AbilityController.Instance.fromNBT(nbt);
|
||||
if (ability == null) return null;
|
||||
return inline(ability);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
/**
|
||||
* Defines what an ability locks when Lock Position is enabled.
|
||||
*/
|
||||
public enum LockType {
|
||||
MOVEMENT,
|
||||
ROTATION,
|
||||
BOTH;
|
||||
|
||||
public boolean locksMovement() {
|
||||
return this == MOVEMENT || this == BOTH;
|
||||
}
|
||||
|
||||
public boolean locksRotation() {
|
||||
return this == ROTATION || this == BOTH;
|
||||
}
|
||||
|
||||
public String getDisplayKey() {
|
||||
switch (this) {
|
||||
case MOVEMENT:
|
||||
return "ability.lockType.movement";
|
||||
case ROTATION:
|
||||
return "ability.lockType.rotation";
|
||||
case BOTH:
|
||||
return "ability.lockType.both";
|
||||
default:
|
||||
return "ability.lockType.both";
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] getDisplayKeys() {
|
||||
return new String[]{
|
||||
"ability.lockType.movement",
|
||||
"ability.lockType.rotation",
|
||||
"ability.lockType.both"
|
||||
};
|
||||
}
|
||||
|
||||
public static LockType fromOrdinal(int ordinal) {
|
||||
LockType[] values = values();
|
||||
if (ordinal >= 0 && ordinal < values.length) {
|
||||
return values[ordinal];
|
||||
}
|
||||
return BOTH;
|
||||
}
|
||||
}
|
||||
+36
-10
@@ -3,9 +3,10 @@ package kamkeel.npcs.controllers.data.ability.conditions;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.LogWriter;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
|
||||
@@ -14,24 +15,40 @@ import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public abstract class AbilityCondition {
|
||||
public static final int MAX_CONDITIONS = 5;
|
||||
|
||||
protected String typeId = "";
|
||||
protected String name = "";
|
||||
protected UserType userType = UserType.BOTH;
|
||||
protected ConditionFilter conditionFilter = ConditionFilter.CASTER;
|
||||
|
||||
/**
|
||||
* Check if this condition is met.
|
||||
*
|
||||
* @param caster The entity that would use the ability (NPC or Player)
|
||||
* @param target The current target (may be null for self-targeting abilities or players)
|
||||
* @return true if the condition is satisfied
|
||||
*/
|
||||
public abstract boolean check(EntityLivingBase caster, EntityLivingBase target);
|
||||
public boolean check(EntityLivingBase caster, EntityLivingBase target) {
|
||||
switch (getFilter()) {
|
||||
case CASTER:
|
||||
return checkEntity(caster);
|
||||
case TARGET:
|
||||
return target != null && checkEntity(target);
|
||||
case BOTH:
|
||||
return checkEntity(caster) && (target != null && checkEntity(target));
|
||||
default:
|
||||
return checkEntity(caster);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract boolean checkEntity(EntityLivingBase entity);
|
||||
|
||||
public boolean requiresTarget() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all required fields are filled in.
|
||||
* Used to disable the Done button in SubGuiConditionEdit when incomplete.
|
||||
*/
|
||||
public boolean isConfigured() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public UserType getUserType() {
|
||||
return userType;
|
||||
}
|
||||
@@ -55,10 +72,19 @@ public abstract class AbilityCondition {
|
||||
@SideOnly(Side.CLIENT)
|
||||
public abstract void getConditionDefinitions(List<FieldDef> defs);
|
||||
|
||||
/**
|
||||
* Returns a human-readable summary of this condition's configuration.
|
||||
* Used for tooltips when hovering over condition buttons.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public abstract String getConditionSummary();
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public final List<FieldDef> getAllDefinitions() {
|
||||
List<FieldDef> defs = new ArrayList<>();
|
||||
|
||||
defs.add(FieldDef.labelField("ability.validFor", () ->
|
||||
"\u00A7e" + StatCollector.translateToLocal("ability.userType." + getUserType().name())));
|
||||
defs.add(FieldDef.enumField("condition.filter", ConditionFilter.class, this::getFilter, this::setFilter));
|
||||
getConditionDefinitions(defs);
|
||||
return defs;
|
||||
@@ -69,7 +95,7 @@ public abstract class AbilityCondition {
|
||||
nbt.setString("typeId", getTypeId());
|
||||
nbt.setString("name", getName());
|
||||
nbt.setInteger("userType", getUserType().ordinal());
|
||||
nbt.setInteger("filter", getUserType().ordinal());
|
||||
nbt.setInteger("filter", getFilter().ordinal());
|
||||
writeTypeNBT(nbt);
|
||||
return nbt;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.conditions;
|
||||
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
@@ -1,7 +1,5 @@
|
||||
package kamkeel.npcs.controllers.data.ability.conditions;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
|
||||
public enum ConditionFilter {
|
||||
CASTER,
|
||||
TARGET,
|
||||
|
||||
+7
-91
@@ -1,51 +1,8 @@
|
||||
package kamkeel.npcs.controllers.data.ability.conditions;
|
||||
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ConditionHPThreshold extends AbilityCondition{
|
||||
private float threshold = 0.5f; // 50%
|
||||
private ThresholdType thresholdType = ThresholdType.ABOVE;
|
||||
|
||||
public enum ThresholdType {
|
||||
ABOVE {
|
||||
@Override
|
||||
public boolean test(float value, float threshold) {
|
||||
return value >= threshold;
|
||||
}
|
||||
},
|
||||
BELOW {
|
||||
@Override
|
||||
public boolean test(float value, float threshold) {
|
||||
return value <= threshold;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract boolean test(float value, float threshold);
|
||||
|
||||
public static ThresholdType fromOrdinal(int ordinal) {
|
||||
ThresholdType[] values = values();
|
||||
if (ordinal >= 0 && ordinal < values.length) {
|
||||
return values[ordinal];
|
||||
}
|
||||
return ABOVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case ABOVE:
|
||||
return "condition.hp_above";
|
||||
case BELOW:
|
||||
return "condition.hp_below";
|
||||
default:
|
||||
return name();
|
||||
}
|
||||
}
|
||||
}
|
||||
public class ConditionHPThreshold extends ConditionThreshold {
|
||||
|
||||
public ConditionHPThreshold() {
|
||||
this.typeId = "condition.cnpc.hp_threshold";
|
||||
@@ -53,58 +10,17 @@ public class ConditionHPThreshold extends AbilityCondition{
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean check(EntityLivingBase caster, EntityLivingBase target) {
|
||||
float casterHP = caster.getHealth() / caster.getMaxHealth();
|
||||
float targetHP = target.getHealth() / target.getMaxHealth();
|
||||
|
||||
|
||||
return compare(casterHP, targetHP);
|
||||
}
|
||||
|
||||
public boolean compare(float casterHp, float targetHp) {
|
||||
boolean casterCheck = thresholdType.test(casterHp, threshold);
|
||||
boolean targetCheck = thresholdType.test(targetHp, threshold);
|
||||
|
||||
switch (getFilter()) {
|
||||
case BOTH:
|
||||
return casterCheck && targetCheck;
|
||||
case CASTER:
|
||||
return casterCheck;
|
||||
default:
|
||||
return targetCheck;
|
||||
}
|
||||
}
|
||||
|
||||
public float getThreshold() {
|
||||
return threshold;
|
||||
}
|
||||
|
||||
public void setThreshold(float threshold) {
|
||||
this.threshold = Math.max(0, threshold);
|
||||
}
|
||||
|
||||
public ThresholdType getThresholdType() {
|
||||
return thresholdType;
|
||||
}
|
||||
|
||||
public void setThresholdType(ThresholdType thresholdType) {
|
||||
this.thresholdType = thresholdType;
|
||||
protected float getEntityValue(EntityLivingBase entity) {
|
||||
return entity.getHealth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getConditionDefinitions(List<FieldDef> defs) {
|
||||
defs.add(FieldDef.floatField("condition.threshold", this::getThreshold, this::setThreshold).min(0));
|
||||
defs.add(FieldDef.enumField("condition.threshold_type", ThresholdType.class,
|
||||
this::getThresholdType, this::setThresholdType));
|
||||
protected float getEntityMaxValue(EntityLivingBase entity) {
|
||||
return entity.getMaxHealth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("threshold", threshold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
threshold = nbt.getFloat("threshold");
|
||||
protected String getStatName() {
|
||||
return "HP";
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package kamkeel.npcs.controllers.data.ability.conditions;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.client.gui.advanced.SubGuiCustomEffectSelect;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import noppes.npcs.controllers.CustomEffectController;
|
||||
import noppes.npcs.controllers.data.CustomEffect;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class ConditionHasEffect extends AbilityCondition {
|
||||
private int effectId = -1;
|
||||
private int effectIndex = 0;
|
||||
|
||||
public ConditionHasEffect() {
|
||||
this.typeId = "condition.cnpc.has_effect";
|
||||
this.name = "condition.has_effect";
|
||||
this.userType = UserType.PLAYER_ONLY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkEntity(EntityLivingBase entity) {
|
||||
if (!(entity instanceof EntityPlayer)) return false;
|
||||
if (effectId < 0) return false;
|
||||
|
||||
EntityPlayer player = (EntityPlayer) entity;
|
||||
return CustomEffectController.getInstance().hasEffect(player, effectId, effectIndex);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getConditionDefinitions(List<FieldDef> defs) {
|
||||
defs.add(FieldDef.subGuiField("condition.select_effect",
|
||||
() -> new SubGuiCustomEffectSelect(effectId, effectIndex),
|
||||
gui -> {
|
||||
SubGuiCustomEffectSelect sel = (SubGuiCustomEffectSelect) gui;
|
||||
if (sel.getSelectedEffectId() >= 0) {
|
||||
effectId = sel.getSelectedEffectId();
|
||||
effectIndex = sel.getSelectedIndex();
|
||||
}
|
||||
})
|
||||
.buttonLabel(() -> {
|
||||
if (effectId < 0) return "None";
|
||||
CustomEffect effect = getEffect();
|
||||
return effect != null ? effect.getName() : "ID:" + effectId;
|
||||
})
|
||||
.clearable(() -> {
|
||||
effectId = -1;
|
||||
effectIndex = 0;
|
||||
}));
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public String getConditionSummary() {
|
||||
String filterLabel = StatCollector.translateToLocal(getFilter().toString());
|
||||
String effectName = "None";
|
||||
if (effectId >= 0) {
|
||||
CustomEffect effect = getEffect();
|
||||
effectName = effect != null ? effect.getName() : "ID:" + effectId;
|
||||
}
|
||||
return "[" + filterLabel + "] Effect: " + effectName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigured() {
|
||||
return effectId >= 0;
|
||||
}
|
||||
|
||||
private CustomEffect getEffect() {
|
||||
HashMap<Integer, CustomEffect> map = CustomEffectController.getInstance().getEffectMap(effectIndex);
|
||||
return map != null ? map.get(effectId) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setInteger("effectId", effectId);
|
||||
nbt.setInteger("effectIndex", effectIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
effectId = nbt.getInteger("effectId");
|
||||
effectIndex = nbt.getInteger("effectIndex");
|
||||
}
|
||||
|
||||
public int getEffectId() {
|
||||
return effectId;
|
||||
}
|
||||
|
||||
public void setEffectId(int effectId) {
|
||||
this.effectId = effectId;
|
||||
}
|
||||
|
||||
public int getEffectIndex() {
|
||||
return effectIndex;
|
||||
}
|
||||
|
||||
public void setEffectIndex(int effectIndex) {
|
||||
this.effectIndex = effectIndex;
|
||||
}
|
||||
}
|
||||
+17
-2
@@ -1,8 +1,10 @@
|
||||
package kamkeel.npcs.controllers.data.ability.conditions;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
@@ -15,7 +17,12 @@ public class ConditionHitCount extends AbilityCondition {
|
||||
public ConditionHitCount() {
|
||||
this.typeId = "condition.cnpc.hit_count";
|
||||
this.name = "condition.hit_count";
|
||||
this.userType = UserType.NPC_ONLY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkEntity(EntityLivingBase entity) {
|
||||
// Not used — this condition overrides check() directly
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -47,12 +54,20 @@ public class ConditionHitCount extends AbilityCondition {
|
||||
this.withinTicks = Math.max(0, withinTicks);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getConditionDefinitions(List<FieldDef> defs) {
|
||||
defs.add(FieldDef.intField("condition.required_hits", this::getRequiredHits, this::setRequiredHits).min(1));
|
||||
defs.add(FieldDef.intField("condition.within_ticks", this::getWithinTicks, this::setWithinTicks).min(0));
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public String getConditionSummary() {
|
||||
String filterLabel = StatCollector.translateToLocal(getFilter().toString());
|
||||
return "[" + filterLabel + "] Hit " + requiredHits + " times in " + withinTicks + " ticks";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setInteger("requiredHits", requiredHits);
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
package kamkeel.npcs.controllers.data.ability.conditions;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ConditionItem extends AbilityCondition {
|
||||
|
||||
public enum UsageType {
|
||||
ARMOR, HOLDING, AMMO, OFFHAND;
|
||||
|
||||
public static UsageType fromOrdinal(int ordinal) {
|
||||
UsageType[] values = values();
|
||||
return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : HOLDING;
|
||||
}
|
||||
}
|
||||
|
||||
public enum ArmorSlot {
|
||||
BOOTS, LEGS, CHEST, HELMET;
|
||||
|
||||
public static ArmorSlot fromOrdinal(int ordinal) {
|
||||
ArmorSlot[] values = values();
|
||||
return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : BOOTS;
|
||||
}
|
||||
}
|
||||
|
||||
// Shared
|
||||
private UsageType usageType = UsageType.HOLDING;
|
||||
private String itemName = "";
|
||||
|
||||
// ARMOR only
|
||||
private ArmorSlot armorSlot = ArmorSlot.BOOTS;
|
||||
private boolean fullArmorSet = false;
|
||||
|
||||
// AMMO + PLAYER only
|
||||
private int requiredCount = 1;
|
||||
|
||||
public ConditionItem() {
|
||||
this.typeId = "condition.cnpc.item";
|
||||
this.name = "condition.item";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkEntity(EntityLivingBase entity) {
|
||||
switch (usageType) {
|
||||
case HOLDING:
|
||||
return checkHolding(entity);
|
||||
case ARMOR:
|
||||
return checkArmor(entity);
|
||||
case AMMO:
|
||||
return checkAmmo(entity);
|
||||
case OFFHAND:
|
||||
return checkOffhand(entity);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkHolding(EntityLivingBase entity) {
|
||||
ItemStack held = entity.getHeldItem();
|
||||
return matchesItem(held);
|
||||
}
|
||||
|
||||
private boolean checkArmor(EntityLivingBase entity) {
|
||||
// MC getEquipmentInSlot: 1=boots, 2=legs, 3=chest, 4=helmet
|
||||
int slot = armorSlot.ordinal() + 1; // BOOTS=0, LEGS=1, CHEST=2, HELMET=3
|
||||
return matchesItem(entity.getEquipmentInSlot(slot));
|
||||
}
|
||||
|
||||
private boolean checkFullArmor(EntityLivingBase entity) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int slot = i + 1;
|
||||
if (!matchesItem(i, entity.getEquipmentInSlot(slot)))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean checkAmmo(EntityLivingBase entity) {
|
||||
if (entity instanceof EntityPlayer) {
|
||||
return checkPlayerInventoryCount((EntityPlayer) entity);
|
||||
}
|
||||
if (entity instanceof EntityNPCInterface) {
|
||||
return checkNPCProjectile((EntityNPCInterface) entity);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkOffhand(EntityLivingBase entity) {
|
||||
// No offhand for players
|
||||
if (entity instanceof EntityPlayer) return false;
|
||||
|
||||
ItemStack item = ((EntityNPCInterface) entity).getOffHand();
|
||||
return matchesItem(item);
|
||||
}
|
||||
|
||||
private boolean checkPlayerInventoryCount(EntityPlayer player) {
|
||||
int count = 0;
|
||||
for (ItemStack stack : player.inventory.mainInventory) {
|
||||
if (matchesItem(stack)) {
|
||||
count += stack.stackSize;
|
||||
}
|
||||
}
|
||||
return count >= requiredCount;
|
||||
}
|
||||
|
||||
private boolean checkNPCProjectile(EntityNPCInterface npc) {
|
||||
ItemStack projectile = npc.inventory.getProjectile();
|
||||
return matchesItem(projectile);
|
||||
}
|
||||
|
||||
private boolean matchesItem(ItemStack stack) {
|
||||
return matchesItem(0, stack);
|
||||
}
|
||||
|
||||
private boolean matchesItem(int index, ItemStack stack) {
|
||||
if (stack == null || itemName == null || itemName.isEmpty()) return false;
|
||||
String registryName = net.minecraft.item.Item.itemRegistry.getNameForObject(stack.getItem());
|
||||
return itemName.equals(registryName);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getConditionDefinitions(List<FieldDef> defs) {
|
||||
defs.add(FieldDef.enumField("condition.usage_type", UsageType.class,
|
||||
this::getUsageType, this::setUsageType));
|
||||
|
||||
defs.add(FieldDef.stringField("condition.item_name",
|
||||
this::getItemName, this::setItemName));
|
||||
// .hover("condition.hover.item_name"));
|
||||
|
||||
|
||||
|
||||
defs.add(FieldDef.enumField("condition.armor_slot", ArmorSlot.class,
|
||||
this::getArmorSlot, this::setArmorSlot)
|
||||
.visibleWhen(() -> usageType == UsageType.ARMOR));
|
||||
// .hover("condition.hover.armor_slots"));
|
||||
|
||||
defs.add(FieldDef.intField("condition.item_count",
|
||||
this::getRequiredCount, this::setRequiredCount)
|
||||
.range(1, 64)
|
||||
.visibleWhen(() -> usageType == UsageType.AMMO && userType.allowsPlayer()));
|
||||
// .hover("condition.hover.required_count"));
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public String getConditionSummary() {
|
||||
String filterLabel = StatCollector.translateToLocal(getFilter().toString());
|
||||
String usage = usageType.name();
|
||||
String item = itemName.isEmpty() ? "None" : itemName;
|
||||
return "[" + filterLabel + "] " + usage + ": " + item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setString("itemName", itemName);
|
||||
nbt.setInteger("usageType", usageType.ordinal());
|
||||
nbt.setInteger("armorSlot", armorSlot.ordinal());
|
||||
nbt.setInteger("requiredCount", requiredCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
itemName = nbt.getString("itemName");
|
||||
usageType = UsageType.fromOrdinal(nbt.getInteger("usageType"));
|
||||
armorSlot = ArmorSlot.fromOrdinal(nbt.getInteger("armorSlot"));
|
||||
requiredCount = nbt.getInteger("requiredCount");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isConfigured() {
|
||||
return itemName != null && !itemName.isEmpty();
|
||||
}
|
||||
|
||||
public UsageType getUsageType() { return usageType; }
|
||||
public void setUsageType(UsageType usageType) { this.usageType = usageType; }
|
||||
|
||||
public String getItemName() { return itemName; }
|
||||
public void setItemName(String itemName) { this.itemName = itemName; }
|
||||
|
||||
public ArmorSlot getArmorSlot() { return armorSlot; }
|
||||
public void setArmorSlot(ArmorSlot armorSlot) { this.armorSlot = armorSlot; }
|
||||
|
||||
public boolean isFullArmorSet() {
|
||||
return fullArmorSet;
|
||||
}
|
||||
|
||||
public void setFullArmorSet(boolean fullArmorSet) {
|
||||
this.fullArmorSet = fullArmorSet;
|
||||
}
|
||||
|
||||
public int getRequiredCount() { return requiredCount; }
|
||||
public void setRequiredCount(int requiredCount) { this.requiredCount = Math.max(1, requiredCount); }
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package kamkeel.npcs.controllers.data.ability.conditions;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import noppes.npcs.client.gui.select.GuiQuestSelection;
|
||||
import noppes.npcs.controllers.QuestController;
|
||||
import noppes.npcs.controllers.data.PlayerData;
|
||||
import noppes.npcs.controllers.data.Quest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ConditionQuestCompleted extends AbilityCondition {
|
||||
private int questId = -1;
|
||||
|
||||
public ConditionQuestCompleted() {
|
||||
this.typeId = "condition.cnpc.quest_completed";
|
||||
this.name = "condition.quest_completed";
|
||||
this.userType = UserType.PLAYER_ONLY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean checkEntity(EntityLivingBase entity) {
|
||||
if (!(entity instanceof EntityPlayer)) return false;
|
||||
if (questId < 0) return false;
|
||||
|
||||
EntityPlayer player = (EntityPlayer) entity;
|
||||
return PlayerData.get(player).questData.hasFinishedQuest(questId);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getConditionDefinitions(List<FieldDef> defs) {
|
||||
defs.add(FieldDef.subGuiField("condition.select_quest",
|
||||
() -> new GuiQuestSelection(questId),
|
||||
gui -> {
|
||||
GuiQuestSelection sel = (GuiQuestSelection) gui;
|
||||
if (sel.selectedQuest != null) {
|
||||
questId = sel.selectedQuest.id;
|
||||
}
|
||||
})
|
||||
.buttonLabel(() -> {
|
||||
if (questId < 0) return "None";
|
||||
Quest q = QuestController.Instance.quests.get(questId);
|
||||
return q != null ? q.title : "ID:" + questId;
|
||||
})
|
||||
.clearable(() -> questId = -1));
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public String getConditionSummary() {
|
||||
String filterLabel = StatCollector.translateToLocal(getFilter().toString());
|
||||
String questName = "None";
|
||||
if (questId >= 0) {
|
||||
Quest q = QuestController.Instance.quests.get(questId);
|
||||
questName = q != null ? q.title : "ID:" + questId;
|
||||
}
|
||||
return "[" + filterLabel + "] Quest: " + questName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setInteger("questId", questId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
questId = nbt.getInteger("questId");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConfigured() {
|
||||
return questId >= 0;
|
||||
}
|
||||
|
||||
public int getQuestId() {
|
||||
return questId;
|
||||
}
|
||||
|
||||
public void setQuestId(int questId) {
|
||||
this.questId = questId;
|
||||
}
|
||||
}
|
||||
+175
@@ -0,0 +1,175 @@
|
||||
package kamkeel.npcs.controllers.data.ability.conditions;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Abstract base for threshold-based conditions (HP, Ki, etc.).
|
||||
* Provides shared percent/flat toggle, ThresholdType enum, and FieldDef generation.
|
||||
*/
|
||||
public abstract class ConditionThreshold extends AbilityCondition {
|
||||
|
||||
public enum ThresholdType {
|
||||
ABOVE {
|
||||
@Override
|
||||
public boolean test(float value, float threshold) {
|
||||
return value >= threshold;
|
||||
}
|
||||
},
|
||||
BELOW {
|
||||
@Override
|
||||
public boolean test(float value, float threshold) {
|
||||
return value <= threshold;
|
||||
}
|
||||
},
|
||||
EQUAL {
|
||||
@Override
|
||||
public boolean test(float value, float threshold) {
|
||||
return value == threshold;
|
||||
}
|
||||
};
|
||||
|
||||
public abstract boolean test(float value, float threshold);
|
||||
|
||||
public static ThresholdType fromOrdinal(int ordinal) {
|
||||
ThresholdType[] values = values();
|
||||
if (ordinal >= 0 && ordinal < values.length) {
|
||||
return values[ordinal];
|
||||
}
|
||||
return ABOVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case ABOVE:
|
||||
return "condition.above";
|
||||
case BELOW:
|
||||
return "condition.below";
|
||||
case EQUAL:
|
||||
return "condition.equal";
|
||||
default:
|
||||
return name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected float thresholdFlat = 100f;
|
||||
protected float thresholdPercent = 0.5f;
|
||||
protected boolean percent = true;
|
||||
protected ThresholdType thresholdType = ThresholdType.ABOVE;
|
||||
|
||||
/**
|
||||
* Returns the raw stat value for the entity (e.g. current HP, current Ki).
|
||||
*/
|
||||
protected abstract float getEntityValue(EntityLivingBase entity);
|
||||
|
||||
/**
|
||||
* Returns the maximum stat value for the entity (e.g. max HP, max Ki).
|
||||
* Used to compute the percentage ratio.
|
||||
*/
|
||||
protected abstract float getEntityMaxValue(EntityLivingBase entity);
|
||||
|
||||
/**
|
||||
* Returns the display name for the stat being checked (e.g. "HP", "Ki").
|
||||
* Used in getConditionSummary().
|
||||
*/
|
||||
protected abstract String getStatName();
|
||||
|
||||
@Override
|
||||
protected boolean checkEntity(EntityLivingBase entity) {
|
||||
float value;
|
||||
float threshold;
|
||||
if (percent) {
|
||||
float max = getEntityMaxValue(entity);
|
||||
value = max > 0 ? getEntityValue(entity) / max : 0;
|
||||
threshold = thresholdPercent;
|
||||
} else {
|
||||
value = getEntityValue(entity);
|
||||
threshold = thresholdFlat;
|
||||
}
|
||||
return thresholdType.test(value, threshold);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getConditionDefinitions(List<FieldDef> defs) {
|
||||
defs.add(FieldDef.floatField("condition.threshold_flat", this::getThresholdFlat, this::setThresholdFlat)
|
||||
.min(0).visibleWhen(() -> !isPercent()));
|
||||
defs.add(FieldDef.floatField("condition.threshold_percent", this::getThresholdPercent, this::setThresholdPercent)
|
||||
.min(0).max(1).visibleWhen(this::isPercent));
|
||||
defs.add(FieldDef.boolField("condition.percent", this::isPercent, this::setIsPercent));
|
||||
defs.add(FieldDef.enumField("condition.threshold_type", ThresholdType.class,
|
||||
this::getThresholdType, this::setThresholdType));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("thresholdFlat", thresholdFlat);
|
||||
nbt.setFloat("thresholdPercent", thresholdPercent);
|
||||
nbt.setBoolean("percent", percent);
|
||||
nbt.setInteger("thresholdType", thresholdType.ordinal());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
thresholdFlat = nbt.getFloat("thresholdFlat");
|
||||
thresholdPercent = nbt.getFloat("thresholdPercent");
|
||||
percent = nbt.getBoolean("percent");
|
||||
thresholdType = ThresholdType.fromOrdinal(nbt.getInteger("thresholdType"));
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public String getConditionSummary() {
|
||||
String filterLabel = StatCollector.translateToLocal(getFilter().toString());
|
||||
String typeName = getStatName();
|
||||
String typeLabel = StatCollector.translateToLocal(thresholdType.toString());
|
||||
String value = percent
|
||||
? String.format("%.0f%%", thresholdPercent * 100)
|
||||
: String.format("%.0f", thresholdFlat);
|
||||
return "[" + filterLabel + "] " + typeName + " " + typeLabel + " " + value;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GETTERS / SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public float getThresholdFlat() {
|
||||
return thresholdFlat;
|
||||
}
|
||||
|
||||
public void setThresholdFlat(float value) {
|
||||
this.thresholdFlat = Math.max(0, value);
|
||||
}
|
||||
|
||||
public float getThresholdPercent() {
|
||||
return thresholdPercent;
|
||||
}
|
||||
|
||||
public void setThresholdPercent(float value) {
|
||||
this.thresholdPercent = Math.max(0, Math.min(1, value));
|
||||
}
|
||||
|
||||
public boolean isPercent() {
|
||||
return percent;
|
||||
}
|
||||
|
||||
public void setIsPercent(boolean percent) {
|
||||
this.percent = percent;
|
||||
}
|
||||
|
||||
public ThresholdType getThresholdType() {
|
||||
return thresholdType;
|
||||
}
|
||||
|
||||
public void setThresholdType(ThresholdType type) {
|
||||
this.thresholdType = type;
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
|
||||
+12
-5
@@ -1,11 +1,15 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import noppes.npcs.api.ability.IChainedAbility;
|
||||
import kamkeel.npcs.util.FileNameHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -74,7 +78,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
|
||||
}
|
||||
|
||||
public ChainedAbility(String name) {
|
||||
this.name = name != null ? name : "";
|
||||
setName(name);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -94,7 +98,8 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name != null ? name : "";
|
||||
String fallback = (this.name != null && !this.name.isEmpty()) ? this.name : "Chain";
|
||||
this.name = FileNameHelper.sanitizeName(name, fallback);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,7 +108,9 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
|
||||
* Converts & color codes to § for rendering.
|
||||
*/
|
||||
public String getDisplayName() {
|
||||
String result = (displayName != null && !displayName.isEmpty()) ? displayName : name;
|
||||
String result = (displayName != null && !displayName.isEmpty())
|
||||
? displayName
|
||||
: FileNameHelper.toDisplayName(name);
|
||||
return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : "";
|
||||
}
|
||||
|
||||
@@ -403,7 +410,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
|
||||
|
||||
public void readNBT(NBTTagCompound nbt) {
|
||||
id = nbt.getString("Id");
|
||||
name = nbt.getString("Name");
|
||||
setName(nbt.getString("Name"));
|
||||
displayName = nbt.getString("DisplayName");
|
||||
enabled = !nbt.hasKey("Enabled") || nbt.getBoolean("Enabled");
|
||||
weight = nbt.hasKey("Weight") ? nbt.getInteger("Weight") : 10;
|
||||
+4
-1
@@ -1,5 +1,8 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityAction;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
@@ -1,6 +1,8 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.data.effect;
|
||||
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.data.effect;
|
||||
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.data.effect;
|
||||
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
package kamkeel.npcs.controllers.data.ability.data.energy;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.api.ability.data.IEnergyAnchorData;
|
||||
|
||||
+40
-37
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
package kamkeel.npcs.controllers.data.ability.data.energy;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
@@ -8,10 +8,14 @@ import java.util.Map;
|
||||
/**
|
||||
* Groups barrier properties shared by energy barrier abilities (Dome, Wall, Shield).
|
||||
* Handles health/durability, duration, and per-projectile-type damage multipliers.
|
||||
*
|
||||
* Properties:
|
||||
* - Solid: Hard wall — entities cannot pass through the barrier boundary (either direction)
|
||||
* - Knockback: Repulsion field — entities near the surface get pushed away
|
||||
* - Absorbing: Caster's incoming damage is redirected to the barrier
|
||||
* - Allows Melee: Barrier can be hit by melee attacks (punching)
|
||||
*/
|
||||
public class EnergyBarrierData {
|
||||
// Knockback target display keys
|
||||
private static final String[] KNOCKBACK_TARGET_KEYS = {"gui.both", "gui.players", "gui.npcs"};
|
||||
|
||||
public float maxHealth = 100.0f;
|
||||
public boolean useHealth = true;
|
||||
@@ -19,22 +23,23 @@ public class EnergyBarrierData {
|
||||
public boolean useDuration = true;
|
||||
public float defaultMultiplier = 1.0f;
|
||||
|
||||
// Knockback settings
|
||||
// Solid: entities cannot pass through the barrier boundary
|
||||
public boolean solid = false;
|
||||
|
||||
// Knockback: repulsion field pushing entities away from surface
|
||||
public boolean knockbackEnabled = false;
|
||||
public float knockbackStrength = 1.0f;
|
||||
/**
|
||||
* 0=Both, 1=Player only, 2=NPC only
|
||||
*/
|
||||
public int knockbackTarget = 0;
|
||||
|
||||
// Melee settings
|
||||
// Absorbing: caster's damage is redirected to the barrier
|
||||
public boolean absorbing = false;
|
||||
|
||||
// Melee: barrier can be hit by melee attacks
|
||||
public boolean meleeEnabled = false;
|
||||
public float meleeDamageMultiplier = 1.0f;
|
||||
|
||||
// Jail settings (dome only - captures entities inside at spawn)
|
||||
public boolean jailEnabled = false;
|
||||
public boolean damageProtection = false;
|
||||
public float jailThreshold = 0.9f;
|
||||
// Reflection: rebound projectiles that fail to break the barrier
|
||||
public boolean reflect = false;
|
||||
public float reflectStrengthPct = 25.0f;
|
||||
|
||||
/**
|
||||
* Per-projectile-type damage multipliers.
|
||||
@@ -118,22 +123,20 @@ public class EnergyBarrierData {
|
||||
this.defaultMultiplier = defaultMultiplier;
|
||||
}
|
||||
|
||||
// Knockback target key helpers
|
||||
public static String[] getKnockbackTargetKeys() {
|
||||
return KNOCKBACK_TARGET_KEYS;
|
||||
public boolean isReflect() {
|
||||
return reflect;
|
||||
}
|
||||
|
||||
public String getKnockbackTargetKey() {
|
||||
return KNOCKBACK_TARGET_KEYS[Math.max(0, Math.min(knockbackTarget, 2))];
|
||||
public void setReflect(boolean reflect) {
|
||||
this.reflect = reflect;
|
||||
}
|
||||
|
||||
public void setKnockbackTargetFromKey(String key) {
|
||||
for (int i = 0; i < KNOCKBACK_TARGET_KEYS.length; i++) {
|
||||
if (KNOCKBACK_TARGET_KEYS[i].equals(key)) {
|
||||
knockbackTarget = i;
|
||||
return;
|
||||
}
|
||||
}
|
||||
public float getReflectStrengthPct() {
|
||||
return reflectStrengthPct;
|
||||
}
|
||||
|
||||
public void setReflectStrengthPct(float reflectStrengthPct) {
|
||||
this.reflectStrengthPct = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
|
||||
}
|
||||
|
||||
// ==================== NBT ====================
|
||||
@@ -144,14 +147,14 @@ public class EnergyBarrierData {
|
||||
nbt.setInteger("barrierDuration", durationTicks);
|
||||
nbt.setBoolean("barrierUseDuration", useDuration);
|
||||
nbt.setFloat("barrierDefaultMult", defaultMultiplier);
|
||||
nbt.setBoolean("barrierSolid", solid);
|
||||
nbt.setBoolean("barrierKnockback", knockbackEnabled);
|
||||
nbt.setFloat("barrierKnockbackStr", knockbackStrength);
|
||||
nbt.setInteger("barrierKnockbackTarget", knockbackTarget);
|
||||
nbt.setBoolean("barrierAbsorbing", absorbing);
|
||||
nbt.setBoolean("barrierMelee", meleeEnabled);
|
||||
nbt.setFloat("barrierMeleeMult", meleeDamageMultiplier);
|
||||
nbt.setBoolean("barrierJail", jailEnabled);
|
||||
nbt.setBoolean("barrierDmgProtect", damageProtection);
|
||||
nbt.setFloat("barrierJailThreshold", jailThreshold);
|
||||
nbt.setBoolean("barrierReflect", reflect);
|
||||
nbt.setFloat("barrierReflectStrPct", reflectStrengthPct);
|
||||
|
||||
NBTTagCompound multNbt = new NBTTagCompound();
|
||||
for (Map.Entry<String, Float> entry : damageMultipliers.entrySet()) {
|
||||
@@ -166,14 +169,14 @@ public class EnergyBarrierData {
|
||||
durationTicks = nbt.hasKey("barrierDuration") ? nbt.getInteger("barrierDuration") : 200;
|
||||
useDuration = !nbt.hasKey("barrierUseDuration") || nbt.getBoolean("barrierUseDuration");
|
||||
defaultMultiplier = nbt.hasKey("barrierDefaultMult") ? nbt.getFloat("barrierDefaultMult") : 1.0f;
|
||||
solid = nbt.hasKey("barrierSolid") && nbt.getBoolean("barrierSolid");
|
||||
knockbackEnabled = nbt.hasKey("barrierKnockback") && nbt.getBoolean("barrierKnockback");
|
||||
knockbackStrength = nbt.hasKey("barrierKnockbackStr") ? nbt.getFloat("barrierKnockbackStr") : 1.0f;
|
||||
knockbackTarget = nbt.hasKey("barrierKnockbackTarget") ? nbt.getInteger("barrierKnockbackTarget") : 0;
|
||||
absorbing = nbt.hasKey("barrierAbsorbing") && nbt.getBoolean("barrierAbsorbing");
|
||||
meleeEnabled = nbt.hasKey("barrierMelee") && nbt.getBoolean("barrierMelee");
|
||||
meleeDamageMultiplier = nbt.hasKey("barrierMeleeMult") ? nbt.getFloat("barrierMeleeMult") : 1.0f;
|
||||
jailEnabled = nbt.hasKey("barrierJail") && nbt.getBoolean("barrierJail");
|
||||
damageProtection = nbt.hasKey("barrierDmgProtect") && nbt.getBoolean("barrierDmgProtect");
|
||||
jailThreshold = nbt.hasKey("barrierJailThreshold") ? nbt.getFloat("barrierJailThreshold") : 0.9f;
|
||||
reflect = nbt.hasKey("barrierReflect") && nbt.getBoolean("barrierReflect");
|
||||
setReflectStrengthPct(nbt.hasKey("barrierReflectStrPct") ? nbt.getFloat("barrierReflectStrPct") : 25.0f);
|
||||
|
||||
damageMultipliers.clear();
|
||||
if (nbt.hasKey("barrierMultipliers")) {
|
||||
@@ -189,14 +192,14 @@ public class EnergyBarrierData {
|
||||
public EnergyBarrierData copy() {
|
||||
EnergyBarrierData copy = new EnergyBarrierData(maxHealth, useHealth, durationTicks, useDuration);
|
||||
copy.defaultMultiplier = defaultMultiplier;
|
||||
copy.solid = solid;
|
||||
copy.knockbackEnabled = knockbackEnabled;
|
||||
copy.knockbackStrength = knockbackStrength;
|
||||
copy.knockbackTarget = knockbackTarget;
|
||||
copy.absorbing = absorbing;
|
||||
copy.meleeEnabled = meleeEnabled;
|
||||
copy.meleeDamageMultiplier = meleeDamageMultiplier;
|
||||
copy.jailEnabled = jailEnabled;
|
||||
copy.damageProtection = damageProtection;
|
||||
copy.jailThreshold = jailThreshold;
|
||||
copy.reflect = reflect;
|
||||
copy.reflectStrengthPct = reflectStrengthPct;
|
||||
copy.damageMultipliers = new HashMap<>(damageMultipliers);
|
||||
return copy;
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data.energy;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
public class EnergyChargingData {
|
||||
|
||||
public int chargeDuration = 60;
|
||||
public int chargeDelay = 0;
|
||||
public int maxCharge = 100;
|
||||
public boolean hasCharge = true;
|
||||
public boolean chargeWindUpSync = true;
|
||||
|
||||
public EnergyChargingData() {
|
||||
}
|
||||
|
||||
public EnergyChargingData(int chargeDuration, int chargeDelay, int maxCharge, boolean hasCharge, boolean chargeWindUpSync) {
|
||||
this.chargeDuration = chargeDuration;
|
||||
this.chargeDelay = chargeDelay;
|
||||
this.maxCharge = maxCharge;
|
||||
this.hasCharge = hasCharge;
|
||||
this.chargeWindUpSync = chargeWindUpSync;
|
||||
}
|
||||
|
||||
public boolean hasCharge() {
|
||||
return hasCharge;
|
||||
}
|
||||
|
||||
public void setHasCharge(boolean hasCharge) {
|
||||
this.hasCharge = hasCharge;
|
||||
}
|
||||
|
||||
public boolean isChargeWindUpSync() {
|
||||
return chargeWindUpSync;
|
||||
}
|
||||
|
||||
public void setChargeWindUpSync(boolean chargeWindUpSync) {
|
||||
this.chargeWindUpSync = chargeWindUpSync;
|
||||
}
|
||||
|
||||
public int getChargeDuration() {
|
||||
return chargeDuration;
|
||||
}
|
||||
|
||||
public void setChargeDuration(int chargeDuration) {
|
||||
this.chargeDuration = chargeDuration;
|
||||
}
|
||||
|
||||
public int getChargeDelay() {
|
||||
return chargeDelay;
|
||||
}
|
||||
|
||||
public void setChargeDelay(int chargeDelay) {
|
||||
this.chargeDelay = chargeDelay;
|
||||
}
|
||||
|
||||
public int getMaxCharge() {
|
||||
return maxCharge;
|
||||
}
|
||||
|
||||
public void setMaxCharge(int maxCharge) {
|
||||
this.maxCharge = Math.max(0, maxCharge);
|
||||
}
|
||||
|
||||
public void writeNBT(NBTTagCompound nbt) {
|
||||
nbt.setInteger("chargeDuration", chargeDuration);
|
||||
nbt.setInteger("chargeDelay", chargeDelay);
|
||||
nbt.setInteger("maxCharge", maxCharge);
|
||||
nbt.setBoolean("hasCharge", hasCharge);
|
||||
nbt.setBoolean("chargeWindUpSync", chargeWindUpSync);
|
||||
}
|
||||
|
||||
public void readNBT(NBTTagCompound nbt) {
|
||||
chargeDuration = nbt.hasKey("chargeDuration") ? nbt.getInteger("chargeDuration") : 60;
|
||||
chargeDelay = nbt.hasKey("chargeDelay") ? nbt.getInteger("chargeDelay") : 0;
|
||||
maxCharge = nbt.hasKey("maxCharge") ? nbt.getInteger("maxCharge") : 100;
|
||||
hasCharge = !nbt.hasKey("hasCharge") || nbt.getBoolean("hasCharge");
|
||||
chargeWindUpSync = !nbt.hasKey("chargeWindUpSync") || nbt.getBoolean("chargeWindUpSync");
|
||||
}
|
||||
|
||||
public EnergyChargingData copy() {
|
||||
return new EnergyChargingData(chargeDuration, chargeDelay, maxCharge, hasCharge, chargeWindUpSync);
|
||||
}
|
||||
}
|
||||
+62
-4
@@ -1,5 +1,6 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
package kamkeel.npcs.controllers.data.ability.data.energy;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.enums.HitType;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.api.ability.data.IEnergyCombatData;
|
||||
|
||||
@@ -8,23 +9,50 @@ import noppes.npcs.api.ability.data.IEnergyCombatData;
|
||||
* Used as a parameter object for entity constructors and ability configuration.
|
||||
*/
|
||||
public class EnergyCombatData implements IEnergyCombatData {
|
||||
public static final float MAX_EXPLOSION_RADIUS = 15.0f;
|
||||
public static final int DEFAULT_MAX_HITS = 5;
|
||||
public static final int MAX_HITS = 200;
|
||||
|
||||
public float damage = 7.0f;
|
||||
public float knockback = 1.0f;
|
||||
public float knockbackUp = 0.1f;
|
||||
public boolean explosive = false;
|
||||
public float explosionRadius = 3.0f;
|
||||
public float explosionDamageFalloff = 0.5f;
|
||||
public HitType hitType = HitType.SINGLE;
|
||||
public int multiHitDelayTicks = 5;
|
||||
public int maxHits = DEFAULT_MAX_HITS;
|
||||
|
||||
public EnergyCombatData() {
|
||||
}
|
||||
|
||||
public EnergyCombatData(float damage, float knockback, float knockbackUp,
|
||||
boolean explosive, float explosionRadius, float explosionDamageFalloff,
|
||||
HitType hitType, int multiHitDelay) {
|
||||
this(damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, hitType, multiHitDelay, DEFAULT_MAX_HITS);
|
||||
}
|
||||
|
||||
public EnergyCombatData(float damage, float knockback, float knockbackUp,
|
||||
boolean explosive, float explosionRadius, float explosionDamageFalloff,
|
||||
HitType hitType, int multiHitDelay, int maxHits) {
|
||||
this.damage = damage;
|
||||
this.knockback = knockback;
|
||||
this.knockbackUp = knockbackUp;
|
||||
this.explosive = explosive;
|
||||
this.explosionRadius = clampExplosionRadius(explosionRadius);
|
||||
this.explosionDamageFalloff = explosionDamageFalloff;
|
||||
this.hitType = hitType;
|
||||
this.multiHitDelayTicks = multiHitDelay;
|
||||
this.maxHits = clampMaxHits(maxHits);
|
||||
}
|
||||
|
||||
public EnergyCombatData(float damage, float knockback, float knockbackUp,
|
||||
boolean explosive, float explosionRadius, float explosionDamageFalloff) {
|
||||
this.damage = damage;
|
||||
this.knockback = knockback;
|
||||
this.knockbackUp = knockbackUp;
|
||||
this.explosive = explosive;
|
||||
this.explosionRadius = explosionRadius;
|
||||
this.explosionRadius = clampExplosionRadius(explosionRadius);
|
||||
this.explosionDamageFalloff = explosionDamageFalloff;
|
||||
}
|
||||
|
||||
@@ -75,7 +103,7 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
|
||||
@Override
|
||||
public void setExplosionRadius(float explosionRadius) {
|
||||
this.explosionRadius = Math.max(0, explosionRadius);
|
||||
this.explosionRadius = clampExplosionRadius(explosionRadius);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,6 +116,14 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
this.explosionDamageFalloff = explosionDamageFalloff;
|
||||
}
|
||||
|
||||
public int getMaxHits() {
|
||||
return maxHits;
|
||||
}
|
||||
|
||||
public void setMaxHits(int maxHits) {
|
||||
this.maxHits = clampMaxHits(maxHits);
|
||||
}
|
||||
|
||||
public void writeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("damage", damage);
|
||||
nbt.setFloat("knockback", knockback);
|
||||
@@ -95,6 +131,9 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
nbt.setBoolean("explosive", explosive);
|
||||
nbt.setFloat("explosionRadius", explosionRadius);
|
||||
nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff);
|
||||
nbt.setInteger("hitType", hitType.ordinal());
|
||||
nbt.setInteger("multiHitDelayTicks", multiHitDelayTicks);
|
||||
nbt.setInteger("maxHits", maxHits);
|
||||
}
|
||||
|
||||
public void readNBT(NBTTagCompound nbt) {
|
||||
@@ -104,15 +143,34 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive");
|
||||
explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f;
|
||||
explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f;
|
||||
hitType = HitType.fromOrdinal(nbt.hasKey("hitType") ? nbt.getInteger("hitType") : 0);
|
||||
multiHitDelayTicks = nbt.hasKey("multiHitDelayTicks") ? nbt.getInteger("multiHitDelayTicks") : 5;
|
||||
maxHits = nbt.hasKey("maxHits") ? nbt.getInteger("maxHits") : DEFAULT_MAX_HITS;
|
||||
|
||||
// Sanitize
|
||||
if (Float.isNaN(damage) || Float.isInfinite(damage)) damage = 7.0f;
|
||||
if (Float.isNaN(knockback) || Float.isInfinite(knockback) || knockback < 0) knockback = 1.0f;
|
||||
if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius) || explosionRadius < 0) explosionRadius = 3.0f;
|
||||
explosionRadius = clampExplosionRadius(explosionRadius);
|
||||
if (multiHitDelayTicks < 1) multiHitDelayTicks = 1;
|
||||
maxHits = clampMaxHits(maxHits);
|
||||
}
|
||||
|
||||
private static float clampExplosionRadius(float explosionRadius) {
|
||||
if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius)) return 0.0f;
|
||||
return Math.max(0.0f, Math.min(MAX_EXPLOSION_RADIUS, explosionRadius));
|
||||
}
|
||||
|
||||
private static int clampMaxHits(int maxHits) {
|
||||
return Math.max(1, Math.min(MAX_HITS, maxHits));
|
||||
}
|
||||
|
||||
public EnergyCombatData copy() {
|
||||
return new EnergyCombatData(damage, knockback, knockbackUp,
|
||||
EnergyCombatData copy = new EnergyCombatData(damage, knockback, knockbackUp,
|
||||
explosive, explosionRadius, explosionDamageFalloff);
|
||||
copy.hitType = hitType;
|
||||
copy.multiHitDelayTicks = multiHitDelayTicks;
|
||||
copy.maxHits = maxHits;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
package kamkeel.npcs.controllers.data.ability.data.energy;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.api.ability.data.IEnergyDisplayData;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
package kamkeel.npcs.controllers.data.ability.data.energy;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.api.ability.data.IEnergyHomingData;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
package kamkeel.npcs.controllers.data.ability.data.energy;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.api.ability.data.IEnergyLifespanData;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
package kamkeel.npcs.controllers.data.ability.data.energy;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.api.ability.data.IEnergyLightningData;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
package kamkeel.npcs.controllers.data.ability.data.energy;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
package kamkeel.npcs.controllers.data.ability.data.energy;
|
||||
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraftforge.common.util.Constants;
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.data.entry;
|
||||
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.IEffectAction;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
+5
-3
@@ -1,4 +1,6 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.data.entry;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
|
||||
/**
|
||||
* Tracks the state of a single active toggle for an entity.
|
||||
@@ -7,12 +9,12 @@ package kamkeel.npcs.controllers.data.ability;
|
||||
* State is 1-based: state 1 = first ON state, state 2 = second, etc.
|
||||
* State 0 means OFF (entry should not exist in the map).
|
||||
*/
|
||||
public class ToggleEntry {
|
||||
public class AbilityToggleEntry {
|
||||
private final Ability ability;
|
||||
private int tickCount;
|
||||
private int state;
|
||||
|
||||
public ToggleEntry(Ability ability, int state) {
|
||||
public AbilityToggleEntry(Ability ability, int state) {
|
||||
this.ability = ability;
|
||||
this.tickCount = 0;
|
||||
this.state = Math.max(1, state);
|
||||
+3
-1
@@ -1,6 +1,8 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.data.entry;
|
||||
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
/**
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.enums;
|
||||
|
||||
public enum AbilityPhase {
|
||||
IDLE, // Not executing
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.enums;
|
||||
|
||||
/**
|
||||
* Defines anchor points for ability charging effects.
|
||||
@@ -0,0 +1,32 @@
|
||||
package kamkeel.npcs.controllers.data.ability.enums;
|
||||
|
||||
/**
|
||||
* Shared hit behavior for energy projectiles.
|
||||
*/
|
||||
public enum HitType {
|
||||
SINGLE,
|
||||
PIERCE,
|
||||
MULTI;
|
||||
|
||||
public static HitType fromOrdinal(int ordinal) {
|
||||
HitType[] values = values();
|
||||
if (ordinal >= 0 && ordinal < values.length) {
|
||||
return values[ordinal];
|
||||
}
|
||||
return SINGLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case SINGLE:
|
||||
return "ability.hitType.single";
|
||||
case PIERCE:
|
||||
return "ability.hitType.pierce";
|
||||
case MULTI:
|
||||
return "ability.hitType.multi";
|
||||
default:
|
||||
return name();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package kamkeel.npcs.controllers.data.ability.enums;
|
||||
|
||||
/**
|
||||
* Defines when a caster is invulnerable while an ability is executing.
|
||||
*/
|
||||
public enum InvulnerableMode {
|
||||
NONE,
|
||||
WINDUP,
|
||||
ACTIVE,
|
||||
BOTH;
|
||||
|
||||
public boolean invulnerableDuringWindup() {
|
||||
return this == WINDUP || this == BOTH;
|
||||
}
|
||||
|
||||
public boolean invulnerableDuringActive() {
|
||||
return this == ACTIVE || this == BOTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Treat BURST_DELAY as windup-like for interruption/damage immunity checks.
|
||||
*/
|
||||
public boolean isInvulnerableInPhase(AbilityPhase phase) {
|
||||
if (phase == null) return false;
|
||||
switch (phase) {
|
||||
case WINDUP:
|
||||
case BURST_DELAY:
|
||||
return invulnerableDuringWindup();
|
||||
case ACTIVE:
|
||||
return invulnerableDuringActive();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public String getDisplayKey() {
|
||||
switch (this) {
|
||||
case WINDUP:
|
||||
return "ability.invulnerable.windup";
|
||||
case ACTIVE:
|
||||
return "ability.invulnerable.active";
|
||||
case BOTH:
|
||||
return "ability.invulnerable.both";
|
||||
case NONE:
|
||||
default:
|
||||
return "ability.invulnerable.none";
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] getDisplayKeys() {
|
||||
return new String[]{
|
||||
"ability.invulnerable.none",
|
||||
"ability.invulnerable.windup",
|
||||
"ability.invulnerable.active",
|
||||
"ability.invulnerable.both"
|
||||
};
|
||||
}
|
||||
|
||||
public static InvulnerableMode fromOrdinal(int ordinal) {
|
||||
InvulnerableMode[] values = values();
|
||||
if (ordinal >= 0 && ordinal < values.length) {
|
||||
return values[ordinal];
|
||||
}
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.enums;
|
||||
|
||||
/**
|
||||
* Defines when an ability locks NPC movement and rotation.
|
||||
*/
|
||||
public enum LockMovementType {
|
||||
public enum LockMode {
|
||||
/**
|
||||
* No movement lock - NPC can move freely during both phases.
|
||||
*/
|
||||
@@ -71,10 +71,10 @@ public enum LockMovementType {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LockMovementType from ordinal, with fallback to WINDUP.
|
||||
* Get LockMode from ordinal, with fallback to WINDUP.
|
||||
*/
|
||||
public static LockMovementType fromOrdinal(int ordinal) {
|
||||
LockMovementType[] values = values();
|
||||
public static LockMode fromOrdinal(int ordinal) {
|
||||
LockMode[] values = values();
|
||||
if (ordinal >= 0 && ordinal < values.length) {
|
||||
return values[ordinal];
|
||||
}
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.enums;
|
||||
|
||||
/**
|
||||
* Defines what happens to NPC rotation during ability phases.
|
||||
* Used with a LockMovementType to control WHEN it applies.
|
||||
* Used with a LockMode to control WHEN it applies.
|
||||
*/
|
||||
public enum RotationMode {
|
||||
/**
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.enums;
|
||||
|
||||
/**
|
||||
* Filter for determining which entities an ability should affect in AOE mode.
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.enums;
|
||||
|
||||
/**
|
||||
* Defines how an ability selects its target(s).
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.enums;
|
||||
|
||||
/**
|
||||
* Defines which entity types can use an ability.
|
||||
+31
-1
@@ -1,5 +1,7 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.extender;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
|
||||
/**
|
||||
@@ -87,4 +89,32 @@ public interface IAbilityExtender {
|
||||
default float modifyProjectileDamage(Ability ability, EntityLivingBase caster, float baseDamage) {
|
||||
return baseDamage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a barrier entity is about to be created, allowing extenders to modify its max health.
|
||||
* Cumulative: all extenders apply in sequence, each receiving the previous output.
|
||||
*
|
||||
* @param ability The barrier ability being executed
|
||||
* @param caster The entity creating the barrier
|
||||
* @param baseHealth The base max health from EnergyBarrierData.maxHealth
|
||||
* @return The modified max health value
|
||||
*/
|
||||
default float modifyBarrierHealth(Ability ability, EntityLivingBase caster, float baseHealth) {
|
||||
return baseHealth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an ability is about to heal an entity.
|
||||
* Return true to handle the healing (skip default entity.heal()).
|
||||
* Chain of responsibility: first returning true wins.
|
||||
*
|
||||
* @param ability The ability performing the heal
|
||||
* @param caster The entity casting the ability
|
||||
* @param target The entity being healed
|
||||
* @param healAmount The heal amount calculated by the ability
|
||||
* @return true if healing was handled (skip default), false to pass to next extender or default
|
||||
*/
|
||||
default boolean onAbilityHeal(Ability ability, EntityLivingBase caster, EntityLivingBase target, float healAmount) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@ package kamkeel.npcs.controllers.data.ability.gui;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityCustomEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityEffectActionEntry;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.IEffectAction;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityCustomEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.data.entry.AbilityEffectActionEntry;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.IEffectAction;
|
||||
import net.minecraft.client.gui.FontRenderer;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import noppes.npcs.client.gui.advanced.SubGuiCustomEffectSelect;
|
||||
|
||||
@@ -2,9 +2,9 @@ package kamkeel.npcs.controllers.data.ability.gui;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityCustomEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityEffectActionEntry;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityCustomEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.data.entry.AbilityEffectActionEntry;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import noppes.npcs.client.gui.builder.FieldType;
|
||||
|
||||
|
||||
+4
-2
@@ -1,4 +1,4 @@
|
||||
package noppes.npcs.client.gui.util;
|
||||
package kamkeel.npcs.controllers.data.ability.gui;
|
||||
|
||||
import kamkeel.npcs.client.renderer.TelegraphRenderer;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
@@ -11,6 +11,8 @@ import net.minecraft.client.renderer.entity.RenderManager;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import noppes.npcs.client.ClientEventHandler;
|
||||
import noppes.npcs.client.gui.util.GuiNPCInterface2;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import org.lwjgl.input.Mouse;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
@@ -40,7 +42,7 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
|
||||
/**
|
||||
* NPC facing direction - affects anchor points and visual facing
|
||||
*/
|
||||
protected static float NPC_FACING_YAW = 160.0f;
|
||||
public static float NPC_FACING_YAW = 160.0f;
|
||||
/**
|
||||
* Camera pitch (slight downward angle)
|
||||
*/
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.gui;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
|
||||
import java.util.List;
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.gui;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
|
||||
import java.util.List;
|
||||
+5
-5
@@ -1,16 +1,16 @@
|
||||
package noppes.npcs.client.gui.util;
|
||||
package kamkeel.npcs.controllers.data.ability.preview;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.ChainedAbilityEntry;
|
||||
import kamkeel.npcs.controllers.data.ability.PreviewEntityHandler;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
|
||||
import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.GuiAbilityInterface;
|
||||
import noppes.npcs.controllers.data.Animation;
|
||||
import noppes.npcs.controllers.data.AnimationData;
|
||||
import noppes.npcs.entity.EntityCustomNpc;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.preview;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
|
||||
@@ -2,10 +2,10 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
@@ -43,7 +43,7 @@ public class AbilityCharge extends AbilityMovement implements IAbilityCharge {
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 20.0f;
|
||||
this.minRange = 4.0f;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.lockMovement = LockMode.WINDUP;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 20;
|
||||
// LINE telegraph showing charge path
|
||||
@@ -52,7 +52,7 @@ public class AbilityCharge extends AbilityMovement implements IAbilityCharge {
|
||||
this.windUpSound = "mob.zombie.wood";
|
||||
this.activeSound = "mob.zombie.attack";
|
||||
this.windUpAnimationName = "Ability_Charge_Windup";
|
||||
this.activeAnimationName = "Ability_Active_Windup";
|
||||
this.activeAnimationName = "Ability_Charge_Active";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,26 +66,25 @@ public class AbilityCharge extends AbilityMovement implements IAbilityCharge {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on the first tick of windup - lock direction here so it matches telegraph.
|
||||
* During windup, NPCs face toward their target for animation purposes.
|
||||
* Players aim freely — the telegraph follows their look direction on the client,
|
||||
* and direction is locked at the end of windup in onExecute().
|
||||
*/
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
if (tick == 1) {
|
||||
lockDirection(caster, target);
|
||||
if (!isPlayerCaster(caster) && target != null) {
|
||||
lockDirectionToTarget(caster, target);
|
||||
enforceLockedRotation(caster);
|
||||
}
|
||||
enforceLockedRotation(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
// Lock final direction at end of windup so it matches the telegraph
|
||||
lockDirection(caster, target);
|
||||
initMovement(caster, maxRange, chargeSpeed);
|
||||
hitEntities.clear();
|
||||
|
||||
// If direction wasn't set during windup (shouldn't happen), set it now
|
||||
if (movementDirection == null) {
|
||||
lockDirection(caster, target);
|
||||
}
|
||||
|
||||
enforceLockedRotation(caster);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
@@ -71,7 +71,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
|
||||
this.name = "Cutter";
|
||||
this.targetingMode = TargetingMode.AOE_SELF;
|
||||
this.maxRange = 8.0f;
|
||||
this.lockMovement = LockMovementType.NO;
|
||||
this.lockMovement = LockMode.NO;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 20;
|
||||
this.telegraphType = TelegraphType.CONE;
|
||||
@@ -164,12 +164,10 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
|
||||
currentRotation = -arcAngle / 2.0f;
|
||||
activeSoundPlayed = false;
|
||||
|
||||
// Use the telegraph's locked yaw if available, so the sweep direction
|
||||
// matches the telegraph the player/NPC was seeing during windup.
|
||||
// The telegraph yaw is locked when entering ACTIVE phase via lockPosition().
|
||||
if (!telegraphInstances.isEmpty()) {
|
||||
sweepBaseYaw = telegraphInstances.get(0).getYaw();
|
||||
} else if (target != null) {
|
||||
// Compute sweep direction from caster/target at execution time.
|
||||
// Server-side telegraph instances are not ticked, so their yaw is stale
|
||||
// from windup start — use the caster's current rotation instead.
|
||||
if (target != null) {
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
if (dx * dx + dz * dz > 0.0001) {
|
||||
|
||||
@@ -2,8 +2,8 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
@@ -26,7 +26,8 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
*/
|
||||
public enum DashMode {
|
||||
AGGRESSIVE,
|
||||
DEFENSIVE;
|
||||
DEFENSIVE,
|
||||
DIRECTIONAL;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
@@ -35,6 +36,8 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
return "ability.dash.aggressive";
|
||||
case DEFENSIVE:
|
||||
return "ability.dash.defensive";
|
||||
case DIRECTIONAL:
|
||||
return "ability.dash.directional";
|
||||
default:
|
||||
return name();
|
||||
}
|
||||
@@ -88,6 +91,8 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
|
||||
// Type-specific runtime state
|
||||
private transient DashDirection chosenDirection;
|
||||
private transient double preDashMotionX;
|
||||
private transient double preDashMotionZ;
|
||||
|
||||
public AbilityDash() {
|
||||
this.typeId = "ability.cnpc.dash";
|
||||
@@ -95,7 +100,7 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 20.0f;
|
||||
this.minRange = 0.0f;
|
||||
this.lockMovement = LockMovementType.NO;
|
||||
this.lockMovement = LockMode.NO;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 5;
|
||||
// No telegraph for dash - it's a quick evasive move
|
||||
@@ -122,18 +127,26 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
preDashMotionX = caster.motionX;
|
||||
preDashMotionZ = caster.motionZ;
|
||||
initMovement(caster, dashDistance, dashSpeed);
|
||||
|
||||
// Choose random direction based on mode
|
||||
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
|
||||
? AGGRESSIVE_DIRECTIONS
|
||||
: DEFENSIVE_DIRECTIONS;
|
||||
chosenDirection = directions[RANDOM.nextInt(directions.length)];
|
||||
if (dashMode == DashMode.DIRECTIONAL) {
|
||||
// Dash straight in the caster's look direction
|
||||
chosenDirection = DashDirection.FORWARD;
|
||||
setDirectionFromYaw(caster.rotationYaw);
|
||||
} else {
|
||||
// Choose random direction based on mode
|
||||
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
|
||||
? AGGRESSIVE_DIRECTIONS
|
||||
: DEFENSIVE_DIRECTIONS;
|
||||
chosenDirection = directions[RANDOM.nextInt(directions.length)];
|
||||
|
||||
// Calculate direction: base yaw (toward target or look dir) + direction offset
|
||||
float baseYaw = getBaseYaw(caster, target);
|
||||
float dashYaw = baseYaw + chosenDirection.getAngleOffset();
|
||||
setDirectionFromYaw(dashYaw);
|
||||
// Calculate direction: base yaw (toward target or look dir) + direction offset
|
||||
float baseYaw = getBaseYaw(caster, target);
|
||||
float dashYaw = baseYaw + chosenDirection.getAngleOffset();
|
||||
setDirectionFromYaw(dashYaw);
|
||||
}
|
||||
|
||||
// Small upward impulse for a skip/hop arc (gravity handles the descent)
|
||||
caster.motionY = 0.2;
|
||||
@@ -142,37 +155,46 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
if (checkTimeout(tick)) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (movementDirection == null) {
|
||||
finishDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkStall(caster, tick)) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
updatePrevPosition(caster);
|
||||
|
||||
if (getDistanceTraveledSq(caster) >= (double) dashDistance * dashDistance) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkBlocked(caster, dashSpeed)) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move caster (motionY left to gravity for skip arc)
|
||||
applyVelocity(caster, dashSpeed);
|
||||
// Move caster (motionY left to gravity for skip arc).
|
||||
// Directional mode layers the dash over incoming movement so momentum
|
||||
// entering/exiting the dash stays fluid.
|
||||
if (dashMode == DashMode.DIRECTIONAL) {
|
||||
applyHorizontalMomentum(caster,
|
||||
preDashMotionX + movementDirection.xCoord * dashSpeed,
|
||||
preDashMotionZ + movementDirection.zCoord * dashSpeed);
|
||||
} else {
|
||||
applyVelocity(caster, dashSpeed);
|
||||
}
|
||||
|
||||
if (!isPreview()) {
|
||||
// Trail particles
|
||||
@@ -182,24 +204,28 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
stopMomentum(caster);
|
||||
finishDash(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
super.cleanup();
|
||||
chosenDirection = null;
|
||||
preDashMotionX = 0;
|
||||
preDashMotionZ = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetForBurst() {
|
||||
super.resetForBurst();
|
||||
chosenDirection = null;
|
||||
preDashMotionX = 0;
|
||||
preDashMotionZ = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -270,6 +296,14 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
return chosenDirection;
|
||||
}
|
||||
|
||||
private void finishDash(EntityLivingBase caster) {
|
||||
if (dashMode == DashMode.DIRECTIONAL) {
|
||||
applyHorizontalMomentum(caster, preDashMotionX, preDashMotionZ);
|
||||
} else {
|
||||
stopMomentum(caster);
|
||||
}
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
|
||||
@@ -4,13 +4,13 @@ import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityCustomEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityEffectActionEntry;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityCustomEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.data.entry.AbilityEffectActionEntry;
|
||||
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -73,7 +73,7 @@ public class AbilityEffect extends Ability implements IAbilityEffect {
|
||||
this.typeId = "ability.cnpc.effect";
|
||||
this.name = "Effect";
|
||||
this.targetingMode = TargetingMode.SELF;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.lockMovement = LockMode.WINDUP;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 30;
|
||||
this.telegraphType = TelegraphType.NONE;
|
||||
@@ -131,7 +131,7 @@ public class AbilityEffect extends Ability implements IAbilityEffect {
|
||||
|
||||
if (instantHeal) {
|
||||
for (EntityLivingBase entity : getAffectedEntities()) {
|
||||
healEntity(entity);
|
||||
healEntity(caster, entity);
|
||||
applyAllEffects(caster, entity);
|
||||
spawnHealParticles(caster.worldObj, entity);
|
||||
}
|
||||
@@ -159,7 +159,9 @@ public class AbilityEffect extends Ability implements IAbilityEffect {
|
||||
}
|
||||
|
||||
if (totalTickHeal > 0) {
|
||||
entity.heal(totalTickHeal);
|
||||
if (!AbilityController.Instance.fireOnAbilityHeal(this, caster, entity, totalTickHeal)) {
|
||||
entity.heal(totalTickHeal);
|
||||
}
|
||||
}
|
||||
|
||||
if (tick % 20 == 0) {
|
||||
@@ -226,13 +228,15 @@ public class AbilityEffect extends Ability implements IAbilityEffect {
|
||||
}
|
||||
}
|
||||
|
||||
private void healEntity(EntityLivingBase entity) {
|
||||
private void healEntity(EntityLivingBase caster, EntityLivingBase entity) {
|
||||
float totalHeal = healAmount;
|
||||
if (healPercent > 0) {
|
||||
totalHeal += entity.getMaxHealth() * healPercent;
|
||||
}
|
||||
if (totalHeal > 0) {
|
||||
entity.heal(totalHeal);
|
||||
if (!AbilityController.Instance.fireOnAbilityHeal(this, caster, entity, totalHeal)) {
|
||||
entity.heal(totalHeal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
@@ -52,7 +52,7 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
|
||||
this.typeId = "ability.cnpc.guard";
|
||||
this.name = "Guard";
|
||||
this.targetingMode = TargetingMode.SELF;
|
||||
this.lockMovement = LockMovementType.ACTIVE;
|
||||
this.lockMovement = LockMode.ACTIVE;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 0;
|
||||
this.interruptible = false; // Hard to interrupt while guarding
|
||||
|
||||
@@ -2,7 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
|
||||
|
||||
@@ -3,10 +3,10 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -38,7 +38,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
|
||||
this.targetingMode = TargetingMode.AOE_SELF;
|
||||
this.maxRange = 5.0f;
|
||||
this.minRange = 0.0f;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.lockMovement = LockMode.WINDUP;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 30;
|
||||
this.telegraphType = TelegraphType.LINE;
|
||||
@@ -90,7 +90,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tick == hitDelayTicks) {
|
||||
if (tick == hitDelayTicks && !caster.worldObj.isRemote) {
|
||||
// Calculate forward and right vectors from caster yaw
|
||||
float yawRad = (float) Math.toRadians(caster.rotationYaw);
|
||||
double forwardX = -Math.sin(yawRad);
|
||||
|
||||
@@ -2,6 +2,8 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.network.play.server.S12PacketEntityVelocity;
|
||||
import net.minecraft.util.Vec3;
|
||||
|
||||
/**
|
||||
@@ -213,39 +215,62 @@ public abstract class AbilityMovement extends Ability {
|
||||
// VELOCITY APPLICATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Send an authoritative velocity packet to a player.
|
||||
* <p>
|
||||
* The ability tick runs BEFORE moveEntityWithHeading (at PlayerTickEvent.START).
|
||||
* If we use velocityChanged, the entity tracker sends S12 AFTER friction has
|
||||
* reduced the motion (~55% of set value). The client then moves at reduced speed,
|
||||
* reports a short position via C04, and the server resets to the client's position.
|
||||
* <p>
|
||||
* By sending S12 manually here — before friction — the client receives the full
|
||||
* velocity and moves at the correct speed.
|
||||
*/
|
||||
protected void sendPlayerVelocity(EntityLivingBase caster) {
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
|
||||
new S12PacketEntityVelocity(caster));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply horizontal momentum and sync to client.
|
||||
*/
|
||||
protected void applyHorizontalMomentum(EntityLivingBase caster, double motionX, double motionZ) {
|
||||
caster.motionX = motionX;
|
||||
caster.motionZ = motionZ;
|
||||
if (!isPreview()) {
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
sendPlayerVelocity(caster);
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply movement velocity to the caster (horizontal only, Y unchanged).
|
||||
*/
|
||||
protected void applyVelocity(EntityLivingBase caster, float speed) {
|
||||
caster.motionX = movementDirection.xCoord * speed;
|
||||
caster.motionZ = movementDirection.zCoord * speed;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply movement velocity with zero vertical motion.
|
||||
* Used for ground-locked movement like Charge.
|
||||
* motionY is set BEFORE sending velocity packet to prevent client-server desync
|
||||
* (which causes "moved too fast" kicks when used in mid-air).
|
||||
*/
|
||||
protected void applyVelocityFlat(EntityLivingBase caster, float speed) {
|
||||
caster.motionX = movementDirection.xCoord * speed;
|
||||
caster.motionY = 0;
|
||||
caster.motionZ = movementDirection.zCoord * speed;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all horizontal momentum.
|
||||
*/
|
||||
protected void stopMomentum(EntityLivingBase caster) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
applyHorizontalMomentum(caster, 0, 0);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -3,8 +3,8 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -42,7 +42,7 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
|
||||
this.maxRange = 20.0f;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 15;
|
||||
this.lockMovement = LockMovementType.NO;
|
||||
this.lockMovement = LockMode.NO;
|
||||
// No telegraph for projectile - it's a ranged attack
|
||||
this.telegraphType = TelegraphType.NONE;
|
||||
this.showTelegraph = false;
|
||||
|
||||
@@ -3,10 +3,10 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
@@ -27,7 +27,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
private float pushRadius = 8.0f;
|
||||
private float pushStrength = 1.5f;
|
||||
private float damage = 8.0f;
|
||||
private int maxTargets = 10;
|
||||
private boolean aoe = true;
|
||||
private int activeDisplayTicks = 10;
|
||||
|
||||
public AbilityShockwave() {
|
||||
@@ -35,7 +35,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
this.name = "Shockwave";
|
||||
this.targetingMode = TargetingMode.AOE_SELF;
|
||||
this.maxRange = 8.0f;
|
||||
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 25;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
@@ -62,7 +62,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!isPreview()) {
|
||||
if (!isPreview() && !caster.worldObj.isRemote) {
|
||||
// Shockwave is instant - apply effect immediately after windup
|
||||
|
||||
// Get all entities in radius
|
||||
@@ -70,47 +70,79 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
int count = 0;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue;
|
||||
if (aoe) {
|
||||
// ALL: push every valid enemy in range
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue;
|
||||
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist > pushRadius) continue;
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist > pushRadius) continue;
|
||||
|
||||
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);
|
||||
|
||||
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);
|
||||
applyShockwavePush(caster, entity, dist);
|
||||
}
|
||||
} else {
|
||||
// SINGULAR: push one target
|
||||
// NPC: push aggro target if valid, otherwise nearest enemy
|
||||
// Player: push nearest enemy in range
|
||||
if (!isPlayerCaster(caster) && target != null && !target.isDead) {
|
||||
double dist = caster.getDistanceToEntity(target);
|
||||
if (dist <= pushRadius && AbilityTargetHelper.shouldAffect(caster, target, TargetFilter.ENEMIES, false)) {
|
||||
applyShockwavePush(caster, target, dist);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
// Find nearest valid enemy
|
||||
EntityLivingBase nearest = null;
|
||||
double nearestDist = Double.MAX_VALUE;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue;
|
||||
|
||||
// Apply effects if hit connected
|
||||
if (wasHit) {
|
||||
applyEffects(entity);
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist <= pushRadius && dist < nearestDist) {
|
||||
nearest = entity;
|
||||
nearestDist = dist;
|
||||
}
|
||||
}
|
||||
if (nearest != null) {
|
||||
applyShockwavePush(caster, nearest, nearestDist);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyShockwavePush(EntityLivingBase caster, EntityLivingBase entity, double dist) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
if (tick >= activeDisplayTicks)
|
||||
@@ -122,7 +154,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
nbt.setFloat("pushRadius", pushRadius);
|
||||
nbt.setFloat("pushStrength", pushStrength);
|
||||
nbt.setFloat("damage", damage);
|
||||
nbt.setInteger("maxTargets", maxTargets);
|
||||
nbt.setBoolean("aoe", aoe);
|
||||
nbt.setInteger("activeDisplayTicks", activeDisplayTicks);
|
||||
}
|
||||
|
||||
@@ -131,7 +163,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
this.pushRadius = nbt.getFloat("pushRadius");
|
||||
this.pushStrength = nbt.getFloat("pushStrength");
|
||||
this.damage = nbt.getFloat("damage");
|
||||
this.maxTargets = nbt.getInteger("maxTargets");
|
||||
this.aoe = !nbt.hasKey("aoe") || nbt.getBoolean("aoe");
|
||||
this.activeDisplayTicks = nbt.hasKey("activeDisplayTicks") ? nbt.getInteger("activeDisplayTicks") : 10;
|
||||
}
|
||||
|
||||
@@ -160,12 +192,12 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
this.damage = damage;
|
||||
}
|
||||
|
||||
public int getMaxTargets() {
|
||||
return maxTargets;
|
||||
public boolean isAoe() {
|
||||
return aoe;
|
||||
}
|
||||
|
||||
public void setMaxTargets(int maxTargets) {
|
||||
this.maxTargets = maxTargets;
|
||||
public void setAoe(boolean aoe) {
|
||||
this.aoe = aoe;
|
||||
}
|
||||
|
||||
public int getActiveDisplayTicks() {
|
||||
@@ -186,7 +218,9 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
FieldDef.floatField("gui.radius", this::getPushRadius, this::setPushRadius),
|
||||
FieldDef.floatField("gui.strength", this::getPushStrength, this::setPushStrength)
|
||||
),
|
||||
FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets),
|
||||
FieldDef.section("ability.section.aoe"),
|
||||
FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe)
|
||||
.hover("ability.hover.aoe"),
|
||||
FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
|
||||
));
|
||||
|
||||
@@ -3,17 +3,19 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.play.server.S12PacketEntityVelocity;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.api.ability.type.IAbilitySlam;
|
||||
@@ -61,6 +63,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
private transient boolean hasRisen = false; // Whether entity has risen above launch position
|
||||
private transient int airTicks = 0;
|
||||
private transient int maxAirTicks = 60; // Timeout to prevent stuck in air
|
||||
private transient boolean airSlam = false; // True when slam initiated while already in the air
|
||||
private transient double airSlamStartY = 0; // Start height for air-slam damage scaling
|
||||
private transient boolean wasFlying = false; // Restore player flight after air slam
|
||||
|
||||
public AbilitySlam() {
|
||||
this.typeId = "ability.cnpc.slam";
|
||||
@@ -68,7 +73,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target
|
||||
this.windUpTicks = 30;
|
||||
this.cooldownTicks = 0;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.lockMovement = LockMode.WINDUP;
|
||||
this.minRange = 2.0f;
|
||||
this.maxRange = 15.0f;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
@@ -165,22 +170,70 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
}
|
||||
|
||||
/**
|
||||
* Player slam: Launch in look direction using same ballistic arc as NPC slam.
|
||||
* AOE damage triggers wherever the player lands.
|
||||
* Player slam: behavior depends on whether the player is on the ground or in the air.
|
||||
* <p>
|
||||
* On ground: Launch in look direction using ballistic arc, full damage on landing.
|
||||
* In air: Slam straight down from current height, damage scales with fall distance.
|
||||
* Works regardless of flying state — position is forced downward each tick.
|
||||
*/
|
||||
private void executePlayerSlam(EntityLivingBase caster) {
|
||||
// Calculate target position along player's look direction (horizontal only)
|
||||
float yawRad = (float) Math.toRadians(caster.rotationYaw);
|
||||
double dirX = -Math.sin(yawRad);
|
||||
double dirZ = Math.cos(yawRad);
|
||||
if (!caster.onGround) {
|
||||
// Air slam: player is already airborne (flying, falling, jumping)
|
||||
airSlam = true;
|
||||
airSlamStartY = caster.posY;
|
||||
targetX = caster.posX;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ;
|
||||
|
||||
double launchDist = Math.max(4.0, maxRange * 0.5);
|
||||
// If the player is flying, temporarily disable it so gravity/velocity apply normally.
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
EntityPlayerMP mp = (EntityPlayerMP) caster;
|
||||
wasFlying = mp.capabilities.isFlying;
|
||||
if (wasFlying) {
|
||||
mp.capabilities.isFlying = false;
|
||||
mp.sendPlayerAbilities();
|
||||
}
|
||||
}
|
||||
|
||||
targetX = caster.posX + dirX * launchDist;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ + dirZ * launchDist;
|
||||
hasLaunched = true;
|
||||
hasRisen = true; // Skip rise detection — we're already above ground
|
||||
|
||||
launchTowardTarget(caster);
|
||||
// Start descent smoothly; physics/collision handles exact landing surface.
|
||||
// Only damp horizontal drift when movement is locked during ACTIVE.
|
||||
if (isMovementLockedDuringActive()) {
|
||||
caster.motionX *= 0.25;
|
||||
caster.motionZ *= 0.25;
|
||||
}
|
||||
caster.motionY = Math.min(caster.motionY, -0.2);
|
||||
|
||||
if (!isPreview()) {
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
|
||||
new S12PacketEntityVelocity(caster));
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
|
||||
}
|
||||
} else {
|
||||
// Ground slam: normal ballistic arc
|
||||
airSlam = false;
|
||||
airSlamStartY = 0;
|
||||
wasFlying = false;
|
||||
|
||||
// Calculate target position along player's look direction (horizontal only)
|
||||
float yawRad = (float) Math.toRadians(caster.rotationYaw);
|
||||
double dirX = -Math.sin(yawRad);
|
||||
double dirZ = Math.cos(yawRad);
|
||||
|
||||
double launchDist = Math.max(4.0, maxRange * 0.5);
|
||||
|
||||
targetX = caster.posX + dirX * launchDist;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ + dirZ * launchDist;
|
||||
|
||||
launchTowardTarget(caster);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,7 +297,12 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(true);
|
||||
}
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
|
||||
new S12PacketEntityVelocity(caster));
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
|
||||
}
|
||||
return;
|
||||
@@ -292,7 +350,14 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(true);
|
||||
}
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
// Use direct S12 packet for players to ensure velocity reaches client
|
||||
// before flight code can override it
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
|
||||
new S12PacketEntityVelocity(caster));
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Face the target
|
||||
@@ -306,6 +371,10 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
if (!hasLaunched) return;
|
||||
if (hasLanded) return;
|
||||
|
||||
boolean playerCaster = isPlayerCaster(caster);
|
||||
boolean activeMovementLocked = isMovementLockedDuringActive();
|
||||
boolean forceHorizontalControl = !playerCaster || activeMovementLocked;
|
||||
|
||||
airTicks++;
|
||||
|
||||
// Continuously reset fall distance during slam to prevent fall damage
|
||||
@@ -313,6 +382,25 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
caster.fallDistance = 0;
|
||||
}
|
||||
|
||||
// Air slam: accelerate descent smoothly via velocity; landing uses normal collision.
|
||||
if (airSlam && !isPreview() && !hasLanded) {
|
||||
if (forceHorizontalControl) {
|
||||
caster.motionX *= 0.2;
|
||||
caster.motionZ *= 0.2;
|
||||
}
|
||||
double nextMotionY = caster.motionY - 0.18;
|
||||
caster.motionY = Math.max(-1.6, nextMotionY);
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
// Only push authoritative velocity every tick when ACTIVE movement is locked.
|
||||
if (forceHorizontalControl) {
|
||||
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
|
||||
new S12PacketEntityVelocity(caster));
|
||||
}
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent NPC navigator from interfering with ballistic arc.
|
||||
// lockMovement=WINDUP means applyMovementControl() doesn't clear
|
||||
// the navigator during ACTIVE, so AI pathfinding could add motion.
|
||||
@@ -351,16 +439,19 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
return;
|
||||
}
|
||||
|
||||
// Face toward target destination while in air (both NPC and Player)
|
||||
// Face toward destination while in air unless player ACTIVE movement is unlocked.
|
||||
// Unlocked players keep free horizontal drift/look control.
|
||||
if (!isPreview()) {
|
||||
double dx = targetX - caster.posX;
|
||||
double dz = targetZ - caster.posZ;
|
||||
if (dx * dx + dz * dz > 0.25) {
|
||||
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
|
||||
caster.rotationYaw = targetYaw;
|
||||
caster.rotationYawHead = targetYaw;
|
||||
if (isPlayerCaster(caster)) {
|
||||
caster.velocityChanged = true;
|
||||
if (forceHorizontalControl) {
|
||||
double dx = targetX - caster.posX;
|
||||
double dz = targetZ - caster.posZ;
|
||||
if (dx * dx + dz * dz > 0.25) {
|
||||
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
|
||||
caster.rotationYaw = targetYaw;
|
||||
caster.rotationYawHead = targetYaw;
|
||||
if (playerCaster) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,13 +465,16 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
restoreFlightIfNeeded(caster);
|
||||
|
||||
// Signal that the ability has completed its active phase
|
||||
signalCompletion();
|
||||
|
||||
// Stop horizontal momentum
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
// Stop horizontal momentum only when movement is controlled by the ability.
|
||||
if (!isPlayerCaster(caster) || isMovementLockedDuringActive()) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
}
|
||||
|
||||
if (!isPreview()) {
|
||||
// Reset fall distance to prevent fall damage on landing
|
||||
@@ -394,6 +488,17 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
// Play slam impact sound on landing
|
||||
world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f);
|
||||
|
||||
// Scale damage based on fall distance for air slams
|
||||
float effectiveDamage = damage;
|
||||
if (airSlam) {
|
||||
double fallStartY = airSlamStartY > 0 ? airSlamStartY : startY;
|
||||
double fallDistance = fallStartY - caster.posY;
|
||||
if (fallDistance < 0) fallDistance = 0;
|
||||
// Scale linearly: full damage at leapHeight distance, minimum 25% for tiny falls
|
||||
float heightFactor = (float) Math.min(1.0, fallDistance / Math.max(1.0, leapHeight));
|
||||
effectiveDamage = damage * Math.max(0.25f, heightFactor);
|
||||
}
|
||||
|
||||
// Find all entities in radius
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(
|
||||
@@ -409,7 +514,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
double dz = livingTarget.posZ - caster.posZ;
|
||||
if (dx * dx + dz * dz <= radius * radius) {
|
||||
// Apply damage with scripted event support
|
||||
boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockbackStrength);
|
||||
boolean wasHit = applyAbilityDamage(caster, livingTarget, effectiveDamage, knockbackStrength);
|
||||
if (wasHit) {
|
||||
applyEffects(livingTarget);
|
||||
}
|
||||
@@ -462,10 +567,14 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
restoreFlightIfNeeded(caster);
|
||||
hasLaunched = false;
|
||||
hasLanded = false;
|
||||
hasRisen = false;
|
||||
airTicks = 0;
|
||||
airSlam = false;
|
||||
airSlamStartY = 0;
|
||||
wasFlying = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -473,18 +582,26 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
restoreFlightIfNeeded(caster);
|
||||
hasLaunched = false;
|
||||
hasLanded = false;
|
||||
hasRisen = false;
|
||||
airTicks = 0;
|
||||
airSlam = false;
|
||||
airSlamStartY = 0;
|
||||
wasFlying = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Caster may be unavailable here; flight restoration is handled in onComplete/onInterrupt.
|
||||
hasLaunched = false;
|
||||
hasLanded = false;
|
||||
hasRisen = false;
|
||||
airTicks = 0;
|
||||
airSlam = false;
|
||||
airSlamStartY = 0;
|
||||
wasFlying = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -494,6 +611,22 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
hasLanded = false;
|
||||
hasRisen = false;
|
||||
airTicks = 0;
|
||||
airSlam = false;
|
||||
airSlamStartY = 0;
|
||||
wasFlying = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore player flight if it was disabled for an air slam.
|
||||
*/
|
||||
private void restoreFlightIfNeeded(EntityLivingBase caster) {
|
||||
if (airSlam && wasFlying && caster instanceof EntityPlayerMP) {
|
||||
EntityPlayerMP mp = (EntityPlayerMP) caster;
|
||||
if (!mp.capabilities.isFlying) {
|
||||
mp.capabilities.isFlying = true;
|
||||
mp.sendPlayerAbilities();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -3,9 +3,9 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.block.Block;
|
||||
@@ -78,7 +78,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
this.minRange = 5.0f;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 10;
|
||||
this.lockMovement = LockMovementType.NO;
|
||||
this.lockMovement = LockMode.NO;
|
||||
// No telegraph for teleport - it's instant repositioning
|
||||
this.telegraphType = TelegraphType.NONE;
|
||||
this.showTelegraph = false;
|
||||
|
||||
@@ -2,8 +2,8 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityPotionEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
|
||||
|
||||
@@ -3,10 +3,10 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
@@ -32,8 +32,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
private float pullStrength = 0.8f;
|
||||
private float damage = 0.0f;
|
||||
private float knockback = 0.0f;
|
||||
private boolean aoe = false;
|
||||
private int maxTargets = 5;
|
||||
private boolean aoe = true;
|
||||
private boolean damageOnPull = false;
|
||||
private float pullDamage = 0.0f;
|
||||
|
||||
@@ -54,7 +53,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
this.name = "Vortex";
|
||||
this.targetingMode = TargetingMode.AOE_SELF;
|
||||
this.maxRange = 15.0f;
|
||||
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 30;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
@@ -83,13 +82,13 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
pullComplete = false;
|
||||
ticksSincePullDamage = 0;
|
||||
|
||||
if (!isPreview()) {
|
||||
if (aoe) {
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
if (!isPreview() && !caster.worldObj.isRemote) {
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
int count = 0;
|
||||
if (aoe) {
|
||||
// ALL: pull every valid enemy in range
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
@@ -98,19 +97,37 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(entity.getUniqueID());
|
||||
count++;
|
||||
if (count >= maxTargets) break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// NPC single-target mode: pull the aggro target
|
||||
// Player: single-target mode has no effect (no target to pull — use AOE mode instead)
|
||||
// SINGULAR: pull one target
|
||||
// NPC: pull aggro target if valid, otherwise nearest enemy
|
||||
// Player: pull nearest enemy in range
|
||||
if (!isPlayerCaster(caster) && target != null && !target.isDead) {
|
||||
double dist = caster.getDistanceToEntity(target);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(target.getUniqueID());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find nearest valid enemy
|
||||
EntityLivingBase nearest = null;
|
||||
double nearestDist = Double.MAX_VALUE;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue;
|
||||
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist <= pullRadius && dist < nearestDist) {
|
||||
nearest = entity;
|
||||
nearestDist = dist;
|
||||
}
|
||||
}
|
||||
if (nearest != null) {
|
||||
getPulledEntities().add(nearest.getUniqueID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +145,8 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
return;
|
||||
}
|
||||
|
||||
if (caster.worldObj.isRemote) return;
|
||||
|
||||
double destX = caster.posX;
|
||||
double destY = caster.posY;
|
||||
double destZ = caster.posZ;
|
||||
@@ -229,7 +248,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
nbt.setFloat("damage", damage);
|
||||
nbt.setFloat("knockback", knockback);
|
||||
nbt.setBoolean("aoe", aoe);
|
||||
nbt.setInteger("maxTargets", maxTargets);
|
||||
nbt.setBoolean("damageOnPull", damageOnPull);
|
||||
nbt.setFloat("pullDamage", pullDamage);
|
||||
}
|
||||
@@ -240,8 +258,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
this.pullStrength = nbt.getFloat("pullStrength");
|
||||
this.damage = nbt.getFloat("damage");
|
||||
this.knockback = nbt.getFloat("knockback");
|
||||
this.aoe = nbt.getBoolean("aoe");
|
||||
this.maxTargets = nbt.getInteger("maxTargets");
|
||||
this.aoe = !nbt.hasKey("aoe") || nbt.getBoolean("aoe");
|
||||
this.damageOnPull = nbt.getBoolean("damageOnPull");
|
||||
this.pullDamage = nbt.getFloat("pullDamage");
|
||||
}
|
||||
@@ -287,14 +304,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
this.aoe = aoe;
|
||||
}
|
||||
|
||||
public int getMaxTargets() {
|
||||
return maxTargets;
|
||||
}
|
||||
|
||||
public void setMaxTargets(int maxTargets) {
|
||||
this.maxTargets = maxTargets;
|
||||
}
|
||||
|
||||
public boolean isDamageOnPull() {
|
||||
return damageOnPull;
|
||||
}
|
||||
@@ -327,8 +336,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
FieldDef.section("ability.section.aoe"),
|
||||
FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe)
|
||||
.hover("ability.hover.aoe"),
|
||||
FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets)
|
||||
.visibleWhen(this::isAoe),
|
||||
FieldDef.section("ability.section.pullDamage"),
|
||||
FieldDef.boolField("gui.enabled", this::isDamageOnPull, this::setDamageOnPull)
|
||||
.hover("ability.hover.dmgOnPull"),
|
||||
|
||||
@@ -3,11 +3,11 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.AbilityPotionEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone;
|
||||
@@ -65,7 +65,7 @@ public abstract class AbilityZone extends Ability {
|
||||
this.colorData = defaultColors;
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 20.0f;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.lockMovement = LockMode.WINDUP;
|
||||
this.cooldownTicks = 0;
|
||||
this.allowedBy = UserType.BOTH;
|
||||
}
|
||||
@@ -75,6 +75,17 @@ public abstract class AbilityZone extends Ability {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowFreeOnCast() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach() {
|
||||
activeEntities.clear();
|
||||
preCalculatedPositions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
@@ -130,6 +141,12 @@ public abstract class AbilityZone extends Ability {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
// Free on Cast: complete immediately — zones live independently
|
||||
if (isFreeOnCast()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
Iterator<EntityAbilityZone> it = activeEntities.iterator();
|
||||
while (it.hasNext()) {
|
||||
EntityAbilityZone entity = it.next();
|
||||
|
||||
+83
-50
@@ -2,9 +2,10 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyBarrierData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.entity.EntityEnergyBarrier;
|
||||
import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.entity.EntityAbilityBarrier;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.DamageSource;
|
||||
@@ -16,14 +17,14 @@ import java.util.List;
|
||||
* Abstract base for energy barrier abilities (Dome, Wall, Shield).
|
||||
* Handles shared visual data, barrier configuration, and lifecycle management.
|
||||
*/
|
||||
public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
public abstract class AbilityBarrier extends AbilityEnergy {
|
||||
|
||||
protected final EnergyBarrierData barrierData;
|
||||
|
||||
// Runtime entity tracking
|
||||
protected transient EntityEnergyBarrier barrierEntity;
|
||||
protected transient EntityAbilityBarrier barrierEntity;
|
||||
|
||||
protected AbilityEnergyBarrier(EnergyDisplayData displayData, EnergyBarrierData barrierData) {
|
||||
protected AbilityBarrier(EnergyDisplayData displayData, EnergyBarrierData barrierData) {
|
||||
super(displayData);
|
||||
this.barrierData = barrierData;
|
||||
}
|
||||
@@ -33,7 +34,7 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
/**
|
||||
* Create and spawn the barrier entity during execution.
|
||||
*/
|
||||
protected abstract EntityEnergyBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target);
|
||||
protected abstract EntityAbilityBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target);
|
||||
|
||||
/**
|
||||
* Add type-specific GUI field definitions.
|
||||
@@ -63,6 +64,16 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
return false; // Barriers are defensive
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowFreeOnCast() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach() {
|
||||
barrierEntity = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
if (caster.worldObj.isRemote) return;
|
||||
@@ -70,6 +81,7 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
if (tick == 1) {
|
||||
barrierEntity = createBarrierEntity(caster, target);
|
||||
if (barrierEntity != null) {
|
||||
applyBarrierHealthModifiers(caster);
|
||||
barrierEntity.setupCharging(getWindUpTicks());
|
||||
spawnAbilityEntity(barrierEntity);
|
||||
}
|
||||
@@ -85,6 +97,7 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
// No windup or entity died — create fresh
|
||||
barrierEntity = createBarrierEntity(caster, target);
|
||||
if (barrierEntity != null) {
|
||||
applyBarrierHealthModifiers(caster);
|
||||
spawnAbilityEntity(barrierEntity);
|
||||
}
|
||||
}
|
||||
@@ -97,6 +110,12 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
return;
|
||||
}
|
||||
|
||||
// Free on Cast: complete immediately after barrier becomes active
|
||||
if (isFreeOnCast()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Duration tracked by entity itself, but check here too as a failsafe
|
||||
if (barrierData.useDuration && tick >= barrierData.durationTicks) {
|
||||
cleanup();
|
||||
@@ -108,6 +127,16 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow extenders to modify the barrier's max health at spawn time.
|
||||
*/
|
||||
private void applyBarrierHealthModifiers(EntityLivingBase caster) {
|
||||
float modHealth = AbilityController.Instance.fireModifyBarrierHealth(this, caster, barrierData.maxHealth);
|
||||
if (modHealth != barrierData.maxHealth) {
|
||||
barrierEntity.setBarrierMaxHealth(modHealth);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
cleanup();
|
||||
@@ -190,6 +219,15 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
return barrierData.getMultiplier(typeId);
|
||||
}
|
||||
|
||||
// Solid data
|
||||
public boolean isSolid() {
|
||||
return barrierData.solid;
|
||||
}
|
||||
|
||||
public void setSolid(boolean solid) {
|
||||
barrierData.solid = solid;
|
||||
}
|
||||
|
||||
// Knockback data
|
||||
public boolean isKnockbackEnabled() {
|
||||
return barrierData.knockbackEnabled;
|
||||
@@ -207,12 +245,30 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
barrierData.knockbackStrength = strength;
|
||||
}
|
||||
|
||||
public String getKnockbackTargetKey() {
|
||||
return barrierData.getKnockbackTargetKey();
|
||||
// Absorbing data
|
||||
public boolean isAbsorbing() {
|
||||
return barrierData.absorbing;
|
||||
}
|
||||
|
||||
public void setKnockbackTargetKey(String key) {
|
||||
barrierData.setKnockbackTargetFromKey(key);
|
||||
public void setAbsorbing(boolean absorbing) {
|
||||
barrierData.absorbing = absorbing;
|
||||
}
|
||||
|
||||
// Reflection data
|
||||
public boolean isReflect() {
|
||||
return barrierData.reflect;
|
||||
}
|
||||
|
||||
public void setReflect(boolean reflect) {
|
||||
barrierData.reflect = reflect;
|
||||
}
|
||||
|
||||
public float getReflectStrengthPct() {
|
||||
return barrierData.reflectStrengthPct;
|
||||
}
|
||||
|
||||
public void setReflectStrengthPct(float strengthPct) {
|
||||
barrierData.setReflectStrengthPct(strengthPct);
|
||||
}
|
||||
|
||||
// Melee data
|
||||
@@ -232,31 +288,6 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
barrierData.meleeDamageMultiplier = mult;
|
||||
}
|
||||
|
||||
// Jail data
|
||||
public boolean isJailEnabled() {
|
||||
return barrierData.jailEnabled;
|
||||
}
|
||||
|
||||
public void setJailEnabled(boolean enabled) {
|
||||
barrierData.jailEnabled = enabled;
|
||||
}
|
||||
|
||||
public boolean isDamageProtection() {
|
||||
return barrierData.damageProtection;
|
||||
}
|
||||
|
||||
public void setDamageProtection(boolean enabled) {
|
||||
barrierData.damageProtection = enabled;
|
||||
}
|
||||
|
||||
public float getJailThreshold() {
|
||||
return barrierData.jailThreshold;
|
||||
}
|
||||
|
||||
public void setJailThreshold(float threshold) {
|
||||
barrierData.jailThreshold = Math.max(0.1f, Math.min(1.0f, threshold));
|
||||
}
|
||||
|
||||
// ==================== GUI ====================
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@@ -275,20 +306,22 @@ public abstract class AbilityEnergyBarrier extends AbilityEnergy {
|
||||
.range(1, 12000).visibleWhen(this::isUseDuration));
|
||||
defs.add(FieldDef.floatField("ability.defaultMultiplier", this::getDefaultMultiplier, this::setDefaultMultiplier));
|
||||
|
||||
// Knockback section
|
||||
defs.add(FieldDef.section("ability.section.knockback"));
|
||||
defs.add(FieldDef.boolField("ability.knockbackEnabled", this::isKnockbackEnabled, this::setKnockbackEnabled));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.knockbackStrength", this::getKnockbackStrength, this::setKnockbackStrength)
|
||||
.range(0, 10).visibleWhen(this::isKnockbackEnabled),
|
||||
FieldDef.stringEnumField("ability.knockbackTarget", EnergyBarrierData.getKnockbackTargetKeys(),
|
||||
this::getKnockbackTargetKey, this::setKnockbackTargetKey)
|
||||
.visibleWhen(this::isKnockbackEnabled)
|
||||
));
|
||||
|
||||
// Melee section
|
||||
defs.add(FieldDef.section("ability.section.melee"));
|
||||
defs.add(FieldDef.boolField("ability.meleeEnabled", this::isMeleeEnabled, this::setMeleeEnabled));
|
||||
// Properties section
|
||||
defs.add(FieldDef.section("ability.section.properties"));
|
||||
defs.add(FieldDef.boolField("ability.solid", this::isSolid, this::setSolid)
|
||||
.hover("ability.hover.solid"));
|
||||
defs.add(FieldDef.boolField("ability.knockbackEnabled", this::isKnockbackEnabled, this::setKnockbackEnabled)
|
||||
.hover("ability.hover.knockbackEnabled"));
|
||||
defs.add(FieldDef.floatField("ability.knockbackStrength", this::getKnockbackStrength, this::setKnockbackStrength)
|
||||
.range(0, 10).visibleWhen(this::isKnockbackEnabled));
|
||||
defs.add(FieldDef.boolField("ability.absorbing", this::isAbsorbing, this::setAbsorbing)
|
||||
.hover("ability.hover.absorbing"));
|
||||
defs.add(FieldDef.boolField("ability.reflect", this::isReflect, this::setReflect)
|
||||
.hover("ability.hover.reflect"));
|
||||
defs.add(FieldDef.floatField("ability.reflectStrength", this::getReflectStrengthPct, this::setReflectStrengthPct)
|
||||
.range(0, 100).visibleWhen(this::isReflect));
|
||||
defs.add(FieldDef.boolField("ability.meleeEnabled", this::isMeleeEnabled, this::setMeleeEnabled)
|
||||
.hover("ability.hover.meleeEnabled"));
|
||||
defs.add(FieldDef.floatField("ability.meleeDamageMultiplier", this::getMeleeDamageMultiplier, this::setMeleeDamageMultiplier)
|
||||
.range(0, 10).visibleWhen(this::isMeleeEnabled));
|
||||
|
||||
@@ -3,13 +3,14 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.RotationMode;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.HitType;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.RotationMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
@@ -35,8 +36,8 @@ public class AbilityBeam extends AbilityEnergyProjectile<EntityAbilityBeam> impl
|
||||
|
||||
public AbilityBeam() {
|
||||
super(
|
||||
new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f),
|
||||
new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f),
|
||||
new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 10.0f),
|
||||
new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f, HitType.SINGLE, 5),
|
||||
new EnergyHomingData(0.4f, true, 0.1f, 15.0f),
|
||||
new EnergyLifespanData(150.0f, 200)
|
||||
);
|
||||
@@ -47,9 +48,9 @@ public class AbilityBeam extends AbilityEnergyProjectile<EntityAbilityBeam> impl
|
||||
this.minRange = 5.0f;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 40;
|
||||
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.rotationMode = RotationMode.TRACK;
|
||||
this.rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
|
||||
this.rotationMode = RotationMode.LOCKED;
|
||||
this.rotationPhase = LockMode.ACTIVE;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.showTelegraph = true;
|
||||
this.windUpAnimationName = "Ability_Beam_Windup";
|
||||
@@ -199,7 +200,9 @@ public class AbilityBeam extends AbilityEnergyProjectile<EntityAbilityBeam> impl
|
||||
));
|
||||
defs.add(FieldDef.section("ability.section.explosive"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
|
||||
.visibleWhen(this::isExplosive));
|
||||
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,13 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.HitType;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
@@ -37,8 +38,8 @@ public class AbilityDisc extends AbilityEnergyProjectile<EntityAbilityDisc> impl
|
||||
|
||||
public AbilityDisc() {
|
||||
super(
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f),
|
||||
new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f),
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 10.0f),
|
||||
new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f, HitType.PIERCE, 5),
|
||||
new EnergyHomingData(0.6f, true, 0.12f, 18.0f),
|
||||
new EnergyLifespanData(150.0f, 200)
|
||||
);
|
||||
@@ -49,7 +50,7 @@ public class AbilityDisc extends AbilityEnergyProjectile<EntityAbilityDisc> impl
|
||||
this.minRange = 5.0f;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 60;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.lockMovement = LockMode.WINDUP;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.showTelegraph = true;
|
||||
this.windUpAnimationName = "Ability_Disc_Windup";
|
||||
@@ -225,7 +226,9 @@ public class AbilityDisc extends AbilityEnergyProjectile<EntityAbilityDisc> impl
|
||||
defs.add(FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay).visibleWhen(this::isBoomerang));
|
||||
defs.add(FieldDef.section("ability.section.explosive"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
|
||||
.visibleWhen(this::isExplosive));
|
||||
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyBarrierData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityEnergyBarrier;
|
||||
import kamkeel.npcs.entity.EntityEnergyDome;
|
||||
import kamkeel.npcs.entity.EntityAbilityBarrier;
|
||||
import kamkeel.npcs.entity.EntityAbilityDome;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
@@ -22,7 +22,7 @@ import java.util.List;
|
||||
* Blocks incoming energy projectiles with configurable damage multipliers.
|
||||
* Duration and/or HP based.
|
||||
*/
|
||||
public class AbilityDome extends AbilityEnergyBarrier {
|
||||
public class AbilityDome extends AbilityBarrier {
|
||||
|
||||
private float domeRadius = 5.0f;
|
||||
private boolean followCaster = false;
|
||||
@@ -37,7 +37,7 @@ public class AbilityDome extends AbilityEnergyBarrier {
|
||||
this.targetingMode = TargetingMode.SELF;
|
||||
this.cooldownTicks = 100;
|
||||
this.windUpTicks = 30;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.lockMovement = LockMode.WINDUP;
|
||||
this.telegraphType = TelegraphType.RING;
|
||||
this.showTelegraph = true;
|
||||
this.windUpAnimationName = "";
|
||||
@@ -47,8 +47,8 @@ public class AbilityDome extends AbilityEnergyBarrier {
|
||||
// ==================== ABSTRACT IMPLEMENTATIONS ====================
|
||||
|
||||
@Override
|
||||
protected EntityEnergyBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target) {
|
||||
EntityEnergyDome dome = new EntityEnergyDome(
|
||||
protected EntityAbilityBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target) {
|
||||
EntityAbilityDome dome = new EntityAbilityDome(
|
||||
caster.worldObj, caster,
|
||||
caster.posX, caster.posY, caster.posZ,
|
||||
domeRadius, displayData.copy(), lightningData.copy(), barrierData.copy()
|
||||
@@ -135,14 +135,5 @@ public class AbilityDome extends AbilityEnergyBarrier {
|
||||
.range(1.0f, 50.0f));
|
||||
defs.add(FieldDef.boolField("ability.followCaster", this::isFollowCaster, this::setFollowCaster)
|
||||
.hover("ability.hover.followCaster"));
|
||||
|
||||
// Jail & protection section (dome-only: only enclosures can jail)
|
||||
defs.add(FieldDef.section("ability.section.jail"));
|
||||
defs.add(FieldDef.boolField("ability.jailEnabled", this::isJailEnabled, this::setJailEnabled)
|
||||
.hover("ability.hover.jailEnabled"));
|
||||
defs.add(FieldDef.floatField("ability.jailThreshold", this::getJailThreshold, this::setJailThreshold)
|
||||
.range(0.1f, 1.0f).visibleWhen(this::isJailEnabled));
|
||||
defs.add(FieldDef.boolField("ability.damageProtection", this::isDamageProtection, this::setDamageProtection)
|
||||
.hover("ability.hover.damageProtection"));
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,8 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
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.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
|
||||
|
||||
+236
-68
@@ -2,18 +2,21 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.HitType;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyTrajectoryData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityEnergyProjectile;
|
||||
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeRemovePacket;
|
||||
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeSpawnPacket;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
@@ -54,6 +57,10 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
|
||||
// Transient entity state
|
||||
protected transient E[] entities;
|
||||
protected transient boolean[] projectileSpawned;
|
||||
protected transient int spawnedCount;
|
||||
protected transient String[] chargeVisualIds;
|
||||
protected transient EntityLivingBase chargeVisualCaster;
|
||||
|
||||
// ==================== CONSTRUCTOR ====================
|
||||
|
||||
@@ -76,7 +83,7 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
|
||||
/**
|
||||
* Create a single entity for the given projectile index.
|
||||
* Called during windup (tick 1) and burst refire (onExecute when entities are null).
|
||||
* Called when a projectile actually fires.
|
||||
*/
|
||||
protected abstract E createEntity(EntityLivingBase caster, EntityLivingBase target,
|
||||
Vec3 spawnPos, EnergyDisplayData resolved, int index);
|
||||
@@ -208,32 +215,100 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
|
||||
// ==================== ENTITY CREATION ====================
|
||||
|
||||
/**
|
||||
* Create all entities for this ability.
|
||||
* Sets effects, source ability, and sibling links.
|
||||
*/
|
||||
protected E[] createAllEntities(EntityLivingBase caster, EntityLivingBase target) {
|
||||
E[] newEntities = createEntityArray(projectileCount);
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
Vec3 spawnPos = getSpawnPosition(caster, i);
|
||||
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
|
||||
newEntities[i] = createEntity(caster, target, spawnPos, resolved, i);
|
||||
newEntities[i].setEffects(this.effects);
|
||||
newEntities[i].setSourceAbility(this);
|
||||
protected void initRuntimeState(EntityLivingBase caster) {
|
||||
entities = createEntityArray(projectileCount);
|
||||
projectileSpawned = new boolean[projectileCount];
|
||||
spawnedCount = 0;
|
||||
chargeVisualIds = new String[projectileCount];
|
||||
chargeVisualCaster = caster;
|
||||
}
|
||||
|
||||
protected E createProjectileEntity(EntityLivingBase caster, EntityLivingBase target, int index) {
|
||||
Vec3 spawnPos = getSpawnPosition(caster, index);
|
||||
EnergyDisplayData resolved = projectiles[index].resolveDisplay(displayData);
|
||||
E entity = createEntity(caster, target, spawnPos, resolved, index);
|
||||
entity.setEffects(this.effects);
|
||||
entity.setSourceAbility(this);
|
||||
return entity;
|
||||
}
|
||||
|
||||
protected void spawnProjectileEntity(E entity, int index) {
|
||||
if (entity == null) return;
|
||||
spawnAbilityEntity(entity);
|
||||
entities[index] = entity;
|
||||
if (!projectileSpawned[index]) {
|
||||
projectileSpawned[index] = true;
|
||||
spawnedCount++;
|
||||
}
|
||||
return newEntities;
|
||||
}
|
||||
|
||||
protected void spawnAndFireProjectile(EntityLivingBase caster, EntityLivingBase target, int index) {
|
||||
if (entities != null && entities[index] != null && !entities[index].isDead) return;
|
||||
E entity = createProjectileEntity(caster, target, index);
|
||||
|
||||
// Initialize launch position/motion BEFORE spawn so the
|
||||
// initial spawn packet already has centered look-vector data on all clients.
|
||||
fireEntitySafe(entity, caster, target);
|
||||
spawnProjectileEntity(entity, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for launch target selection.
|
||||
* Players launch along their look vector (crosshair-aligned), so target is ignored.
|
||||
* NPCs use their resolved target so launch math can track the target immediately.
|
||||
*/
|
||||
protected EntityLivingBase getLaunchTarget(EntityLivingBase caster, EntityLivingBase target) {
|
||||
return isPlayerCaster(caster) ? null : target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire an entity if it's valid.
|
||||
* Clears charging state first so the entity can move and check collisions.
|
||||
*/
|
||||
protected void fireEntitySafe(E entity, EntityLivingBase target) {
|
||||
protected void fireEntitySafe(E entity, EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (entity != null && !entity.isDead) {
|
||||
if (entity.isCharging()) {
|
||||
entity.setCharging(false);
|
||||
}
|
||||
fireEntity(entity, target);
|
||||
fireEntity(entity, getLaunchTarget(caster, target));
|
||||
}
|
||||
}
|
||||
|
||||
protected String getChargeVisualId(EntityLivingBase caster, int index) {
|
||||
return "energy_charge:" + caster.getEntityId() + ":" + executionStartTime + ":" + burstIndex + ":" + index;
|
||||
}
|
||||
|
||||
protected void spawnChargeVisual(EntityLivingBase caster, EntityLivingBase target, int index, int chargeDuration) {
|
||||
if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return;
|
||||
if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return;
|
||||
|
||||
E previewEntity = createProjectileEntity(caster, target, index);
|
||||
previewEntity.setPreviewMode(true);
|
||||
previewEntity.setPreviewOwner(caster);
|
||||
setupEntityCharging(previewEntity, projectiles[index], index);
|
||||
previewEntity.setChargeDuration(Math.max(1, chargeDuration));
|
||||
|
||||
String id = getChargeVisualId(caster, index);
|
||||
chargeVisualIds[index] = id;
|
||||
chargeVisualCaster = caster;
|
||||
EnergyChargeSpawnPacket.sendToTracking(id, previewEntity, caster);
|
||||
}
|
||||
|
||||
protected void removeChargeVisual(EntityLivingBase caster, int index) {
|
||||
if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return;
|
||||
if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return;
|
||||
|
||||
String id = chargeVisualIds[index];
|
||||
if (id != null && !id.isEmpty()) {
|
||||
EnergyChargeRemovePacket.sendToTracking(id, caster);
|
||||
chargeVisualIds[index] = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void removeAllChargeVisuals(EntityLivingBase caster) {
|
||||
if (chargeVisualIds == null || caster == null) return;
|
||||
for (int i = 0; i < chargeVisualIds.length; i++) {
|
||||
removeChargeVisual(caster, i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +319,18 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowFreeOnCast() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach() {
|
||||
entities = null;
|
||||
projectileSpawned = null;
|
||||
spawnedCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadyForBurstCompletion(int activeTick) {
|
||||
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
|
||||
@@ -264,21 +351,31 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
if (caster.worldObj.isRemote && !isPreview()) return;
|
||||
|
||||
if (tick == 1) {
|
||||
entities = createAllEntities(caster, target);
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
|
||||
if (isPreview()) {
|
||||
setupEntityPreview(entities[i], caster, resolved, projectiles[i], i);
|
||||
} else {
|
||||
setupEntityCharging(entities[i], projectiles[i], i);
|
||||
initRuntimeState(caster);
|
||||
|
||||
if (isPreview()) {
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
E previewEntity = createProjectileEntity(caster, target, i);
|
||||
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
|
||||
setupEntityPreview(previewEntity, caster, resolved, projectiles[i], i);
|
||||
spawnAbilityEntity(previewEntity);
|
||||
entities[i] = previewEntity;
|
||||
projectileSpawned[i] = true;
|
||||
spawnedCount++;
|
||||
}
|
||||
spawnAbilityEntity(entities[i]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
spawnChargeVisual(caster, target, i, windUpTicks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetForBurst() {
|
||||
removeAllChargeVisuals(chargeVisualCaster);
|
||||
|
||||
// Non-overlap: kill old entities before next burst creates new ones
|
||||
if (!burstOverlap) {
|
||||
cleanup();
|
||||
@@ -286,73 +383,99 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
// Clear reference so the next iteration creates fresh entities.
|
||||
// For overlap mode, old entities stay alive (tracked in burstEntities).
|
||||
entities = null;
|
||||
projectileSpawned = null;
|
||||
spawnedCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
// Create entities if they don't exist yet.
|
||||
// They'll already exist if onWindUpTick ran (windup replay), otherwise we create here.
|
||||
if (entities == null) {
|
||||
entities = createAllEntities(caster, target);
|
||||
|
||||
// If there's a fire delay, put unfired entities in charging state
|
||||
// to prevent collision checks before they're actually fired.
|
||||
// Entity[0] will be fired immediately below, so skip it.
|
||||
if (fireDelay > 0 && projectileCount > 1) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
if (entities[i] != null) {
|
||||
entities[i].setCharging(true);
|
||||
}
|
||||
// GUI preview keeps the original non-world preview entity flow.
|
||||
if (isPreview()) {
|
||||
if (entities == null) {
|
||||
initRuntimeState(caster);
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
E previewEntity = createProjectileEntity(caster, target, i);
|
||||
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
|
||||
setupEntityPreview(previewEntity, caster, resolved, projectiles[i], i);
|
||||
spawnAbilityEntity(previewEntity);
|
||||
entities[i] = previewEntity;
|
||||
projectileSpawned[i] = true;
|
||||
spawnedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
for (E entity : entities) {
|
||||
spawnAbilityEntity(entity);
|
||||
fireEntitySafe(entities[0], caster, target);
|
||||
if (fireDelay <= 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
fireEntitySafe(entities[i], caster, target);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire first projectile immediately
|
||||
fireEntitySafe(entities[0], target);
|
||||
if (entities == null || projectileSpawned == null || entities.length != projectileCount) {
|
||||
initRuntimeState(caster);
|
||||
}
|
||||
|
||||
// Fire remaining projectiles if no delay
|
||||
removeAllChargeVisuals(caster);
|
||||
|
||||
// Fire first projectile immediately.
|
||||
spawnAndFireProjectile(caster, target, 0);
|
||||
|
||||
// Fire remaining projectiles immediately if there is no stagger delay.
|
||||
if (fireDelay <= 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
fireEntitySafe(entities[i], target);
|
||||
spawnAndFireProjectile(caster, target, i);
|
||||
}
|
||||
} else {
|
||||
// During active stagger, keep client-only charging visuals for unfired projectiles.
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
spawnChargeVisual(caster, target, i, fireDelay * i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
if (entities == null) {
|
||||
if (entities == null || projectileSpawned == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire staggered projectiles
|
||||
// Fire staggered projectiles: spawn real world entity only when it is time to fire.
|
||||
if (fireDelay > 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
if (tick == fireDelay * i) {
|
||||
fireEntitySafe(entities[i], target);
|
||||
removeChargeVisual(caster, i);
|
||||
spawnAndFireProjectile(caster, target, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all entities are dead or unreachable
|
||||
boolean allDead = true;
|
||||
for (E entity : entities) {
|
||||
if (entity == null || entity.isDead) continue;
|
||||
|
||||
// Check if entity was removed from world (chunk unload, etc.)
|
||||
// Skip this check for preview entities - they're not registered in the world
|
||||
if (!isPreview() && tick > 5 && entity.worldObj != null
|
||||
&& entity.worldObj.getEntityByID(entity.getEntityId()) != entity) {
|
||||
entity.setDead();
|
||||
continue;
|
||||
// Free on Cast: complete once all projectiles have been fired
|
||||
if (isFreeOnCast()) {
|
||||
int lastFireTick = fireDelay > 0 ? fireDelay * (projectileCount - 1) : 0;
|
||||
if (tick >= lastFireTick) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
allDead = false;
|
||||
break;
|
||||
// Before all projectiles have fired, the ability is still active by definition.
|
||||
boolean allDead = spawnedCount >= projectileCount;
|
||||
if (allDead) {
|
||||
for (E entity : entities) {
|
||||
if (entity == null || entity.isDead) continue;
|
||||
|
||||
if (!isPreview() && tick > 5 && entity.worldObj != null
|
||||
&& entity.worldObj.getEntityByID(entity.getEntityId()) != entity) {
|
||||
entity.setDead();
|
||||
continue;
|
||||
}
|
||||
|
||||
allDead = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Failsafe timeout: force completion if active phase exceeded max possible flight time
|
||||
@@ -372,15 +495,19 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
removeAllChargeVisuals(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
removeAllChargeVisuals(caster);
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
removeAllChargeVisuals(chargeVisualCaster);
|
||||
|
||||
if (entities != null) {
|
||||
for (int i = 0; i < entities.length; i++) {
|
||||
if (entities[i] != null && !entities[i].isDead) {
|
||||
@@ -390,6 +517,10 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
}
|
||||
}
|
||||
entities = null;
|
||||
projectileSpawned = null;
|
||||
spawnedCount = 0;
|
||||
chargeVisualIds = null;
|
||||
chargeVisualCaster = null;
|
||||
}
|
||||
|
||||
// ==================== TELEGRAPH ====================
|
||||
@@ -520,12 +651,12 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
|
||||
@Override
|
||||
public float getExplosionRadius() {
|
||||
return combatData.explosionRadius;
|
||||
return combatData.getExplosionRadius();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExplosionRadius(float explosionRadius) {
|
||||
combatData.explosionRadius = explosionRadius;
|
||||
combatData.setExplosionRadius(explosionRadius);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -538,6 +669,30 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
combatData.explosionDamageFalloff = falloff;
|
||||
}
|
||||
|
||||
public int getHitType() {
|
||||
return combatData.hitType.ordinal();
|
||||
}
|
||||
|
||||
public void setHitType(int hitType) {
|
||||
this.combatData.hitType = HitType.fromOrdinal(hitType);
|
||||
}
|
||||
|
||||
public int getMultiHitDelayTicks() {
|
||||
return combatData.multiHitDelayTicks;
|
||||
}
|
||||
|
||||
public void setMultiHitDelayTicks(int delay) {
|
||||
this.combatData.multiHitDelayTicks = Math.max(1, delay);
|
||||
}
|
||||
|
||||
public int getMaxHits() {
|
||||
return combatData.getMaxHits();
|
||||
}
|
||||
|
||||
public void setMaxHits(int maxHits) {
|
||||
combatData.setMaxHits(maxHits);
|
||||
}
|
||||
|
||||
// Lifespan data
|
||||
@Override
|
||||
public float getMaxDistance() {
|
||||
@@ -677,6 +832,19 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public final void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
defs.add(FieldDef.enumField("ability.hitType", HitType.class,
|
||||
() -> HitType.fromOrdinal(getHitType()),
|
||||
(v) -> setHitType(v.ordinal()))
|
||||
.hover("ability.hover.hitType"));
|
||||
defs.add(FieldDef.intField("ability.multiHitDelay", this::getMultiHitDelayTicks, this::setMultiHitDelayTicks)
|
||||
.range(1, 200)
|
||||
.visibleWhen(() -> getHitType() == HitType.MULTI.ordinal())
|
||||
.hover("ability.hover.multiHitDelay"));
|
||||
defs.add(FieldDef.intField("ability.maxHits", this::getMaxHits, this::setMaxHits)
|
||||
.range(1, EnergyCombatData.MAX_HITS)
|
||||
.visibleWhen(() -> getHitType() != HitType.SINGLE.ordinal())
|
||||
.hover("ability.hover.maxHits"));
|
||||
|
||||
// Type-specific fields first
|
||||
addTypeDefinitions(defs);
|
||||
|
||||
|
||||
+14
-28
@@ -2,14 +2,11 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.RotationMode;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.*;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
@@ -25,8 +22,8 @@ import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Laser Shot ability: Fast expanding thin line that pierces through targets.
|
||||
* Travels in a straight line from origin, damaging all entities it passes through.
|
||||
* Laser Shot ability: Fast expanding thin line.
|
||||
* Travels in a straight line from origin.
|
||||
* Single-projectile only (always projectileCount=1).
|
||||
*/
|
||||
public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser> implements IAbilityLaserShot {
|
||||
@@ -34,12 +31,11 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
|
||||
private float laserWidth = 0.3f;
|
||||
private float expansionSpeed = 3.0f;
|
||||
private int lingerTicks = 8;
|
||||
private boolean dieOnImpact = false;
|
||||
|
||||
public AbilityLaserShot() {
|
||||
super(
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f),
|
||||
new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f),
|
||||
new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f, HitType.PIERCE, 5),
|
||||
new EnergyHomingData(),
|
||||
new EnergyLifespanData(150.0f, 100)
|
||||
);
|
||||
@@ -50,9 +46,9 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
|
||||
this.minRange = 3.0f;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 15;
|
||||
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
|
||||
this.rotationMode = RotationMode.TRACK;
|
||||
this.rotationPhase = LockMovementType.WINDUP;
|
||||
this.rotationPhase = LockMode.WINDUP;
|
||||
this.telegraphType = TelegraphType.LINE;
|
||||
this.showTelegraph = true;
|
||||
this.windUpAnimationName = "Ability_Laser_Windup";
|
||||
@@ -123,7 +119,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
|
||||
laserWidth,
|
||||
resolved, combatData, lightningData, lifespanData, trajectoryData,
|
||||
expansionSpeed, lingerTicks);
|
||||
laser.setDieOnImpact(dieOnImpact);
|
||||
return laser;
|
||||
}
|
||||
|
||||
@@ -141,7 +136,7 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
|
||||
@Override
|
||||
protected void setupEntityPreview(EntityAbilityLaser laser, EntityLivingBase caster,
|
||||
EnergyDisplayData resolved, ProjectileData projData, int index) {
|
||||
laser.setupPreview(caster, laserWidth, resolved, lightningData, expansionSpeed, lifespanData.maxDistance);
|
||||
laser.setupPreview(caster, laserWidth, resolved, lightningData, projData.anchor, windUpTicks, expansionSpeed, lifespanData.maxDistance);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -161,7 +156,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
|
||||
nbt.setFloat("laserWidth", laserWidth);
|
||||
nbt.setFloat("expansionSpeed", expansionSpeed);
|
||||
nbt.setInteger("lingerTicks", lingerTicks);
|
||||
nbt.setBoolean("dieOnImpact", dieOnImpact);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -169,7 +163,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
|
||||
this.laserWidth = nbt.getFloat("laserWidth");
|
||||
this.expansionSpeed = nbt.getFloat("expansionSpeed");
|
||||
this.lingerTicks = nbt.getInteger("lingerTicks");
|
||||
this.dieOnImpact = nbt.getBoolean("dieOnImpact");
|
||||
}
|
||||
|
||||
// ==================== TYPE-SPECIFIC GETTERS ====================
|
||||
@@ -198,14 +191,6 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
|
||||
this.lingerTicks = lingerTicks;
|
||||
}
|
||||
|
||||
public boolean isDieOnImpact() {
|
||||
return dieOnImpact;
|
||||
}
|
||||
|
||||
public void setDieOnImpact(boolean dieOnImpact) {
|
||||
this.dieOnImpact = dieOnImpact;
|
||||
}
|
||||
|
||||
// ==================== TYPE-SPECIFIC GUI ====================
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@@ -226,10 +211,11 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200)
|
||||
));
|
||||
defs.add(FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f));
|
||||
defs.add(FieldDef.boolField("ability.dieOnImpact", this::isDieOnImpact, this::setDieOnImpact));
|
||||
defs.add(FieldDef.section("ability.section.explosive"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
|
||||
.visibleWhen(this::isExplosive));
|
||||
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
@@ -32,7 +32,7 @@ public class AbilityOrb extends AbilityEnergyProjectile<EntityAbilityOrb> implem
|
||||
|
||||
public AbilityOrb() {
|
||||
super(
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f),
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 10.0f),
|
||||
new EnergyCombatData(),
|
||||
new EnergyHomingData(),
|
||||
new EnergyLifespanData()
|
||||
@@ -44,7 +44,7 @@ public class AbilityOrb extends AbilityEnergyProjectile<EntityAbilityOrb> implem
|
||||
this.minRange = 5.0f;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 30;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.lockMovement = LockMode.WINDUP;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.showTelegraph = true;
|
||||
this.windUpAnimationName = "Ability_Orb_Windup";
|
||||
@@ -115,6 +115,9 @@ public class AbilityOrb extends AbilityEnergyProjectile<EntityAbilityOrb> implem
|
||||
orb.setDamage(4.0f);
|
||||
orb.setKnockback(0.0f);
|
||||
orb.setKnockbackUp(0.0f);
|
||||
orb.setProjectileCount(2);
|
||||
orb.setFireDelay(2);
|
||||
a.setLockMovement(LockMode.WINDUP_AND_ACTIVE);
|
||||
a.setMaxRange(75.0f);
|
||||
a.setBurstEnabled(true);
|
||||
a.setBurstAmount(15);
|
||||
@@ -184,7 +187,9 @@ public class AbilityOrb extends AbilityEnergyProjectile<EntityAbilityOrb> implem
|
||||
defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming));
|
||||
defs.add(FieldDef.section("ability.section.explosive"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
|
||||
.visibleWhen(this::isExplosive));
|
||||
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
}
|
||||
}
|
||||
+13
-13
@@ -3,15 +3,15 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyBarrierData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyPanelData;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyPanelData;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityEnergyBarrier;
|
||||
import kamkeel.npcs.entity.EntityEnergyPanel;
|
||||
import kamkeel.npcs.entity.EntityAbilityBarrier;
|
||||
import kamkeel.npcs.entity.EntityAbilityPanel;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
@@ -24,7 +24,7 @@ import java.util.List;
|
||||
* The shield follows the caster's position and rotates with their look direction.
|
||||
* Cannot be placed — it's always held.
|
||||
*/
|
||||
public class AbilityShield extends AbilityEnergyBarrier {
|
||||
public class AbilityShield extends AbilityBarrier {
|
||||
|
||||
private float shieldWidth = 2.5f;
|
||||
private float shieldHeight = 2.5f;
|
||||
@@ -39,7 +39,7 @@ public class AbilityShield extends AbilityEnergyBarrier {
|
||||
this.targetingMode = TargetingMode.SELF;
|
||||
this.cooldownTicks = 60;
|
||||
this.windUpTicks = 10;
|
||||
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
|
||||
this.telegraphType = TelegraphType.NONE;
|
||||
this.showTelegraph = false;
|
||||
this.windUpAnimationName = "";
|
||||
@@ -50,7 +50,7 @@ public class AbilityShield extends AbilityEnergyBarrier {
|
||||
// ==================== ABSTRACT IMPLEMENTATIONS ====================
|
||||
|
||||
@Override
|
||||
protected EntityEnergyBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target) {
|
||||
protected EntityAbilityBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target) {
|
||||
EnergyPanelData panelData = new EnergyPanelData(shieldWidth, shieldHeight, 0.0f);
|
||||
|
||||
float frontDist = 1.5f;
|
||||
@@ -59,10 +59,10 @@ public class AbilityShield extends AbilityEnergyBarrier {
|
||||
double spawnY = caster.posY + (caster.height * 0.5f);
|
||||
double spawnZ = caster.posZ + (Math.cos(yawRad) * frontDist);
|
||||
|
||||
EntityEnergyPanel panel = new EntityEnergyPanel(
|
||||
EntityAbilityPanel panel = new EntityAbilityPanel(
|
||||
caster.worldObj, caster,
|
||||
spawnX, spawnY, spawnZ, caster.rotationYaw,
|
||||
EntityEnergyPanel.PanelMode.HELD,
|
||||
EntityAbilityPanel.PanelMode.HELD,
|
||||
displayData.copy(), lightningData.copy(), barrierData.copy(), panelData
|
||||
);
|
||||
panel.setSourceAbility(this);
|
||||
|
||||
+12
-24
@@ -3,12 +3,12 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
@@ -23,14 +23,13 @@ import java.util.List;
|
||||
|
||||
/**
|
||||
* Energy Slicer ability: Fires a thin, wide blade projectile in a straight line.
|
||||
* JJK Cleave/Dismantle inspired. Piercing by default.
|
||||
* JJK Cleave/Dismantle inspired.
|
||||
* Extends AbilityEnergyProjectile to leverage the existing projectile infrastructure.
|
||||
*/
|
||||
public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
|
||||
|
||||
private float sliceWidth = 3.0f;
|
||||
private float sliceThickness = 0.15f;
|
||||
private boolean piercing = true;
|
||||
|
||||
public AbilitySlicer() {
|
||||
super(
|
||||
@@ -46,7 +45,7 @@ public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
|
||||
this.minRange = 3.0f;
|
||||
this.cooldownTicks = 40;
|
||||
this.windUpTicks = 15;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.lockMovement = LockMode.WINDUP;
|
||||
this.telegraphType = TelegraphType.LINE;
|
||||
this.showTelegraph = true;
|
||||
this.windUpAnimationName = "";
|
||||
@@ -62,8 +61,7 @@ public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
|
||||
caster.worldObj, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
|
||||
sliceWidth, sliceThickness,
|
||||
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
|
||||
piercing
|
||||
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,14 +120,12 @@ public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
|
||||
protected void writeTypeSpecificNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("sliceWidth", sliceWidth);
|
||||
nbt.setFloat("sliceThickness", sliceThickness);
|
||||
nbt.setBoolean("piercing", piercing);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readTypeSpecificNBT(NBTTagCompound nbt) {
|
||||
this.sliceWidth = nbt.hasKey("sliceWidth") ? nbt.getFloat("sliceWidth") : 3.0f;
|
||||
this.sliceThickness = nbt.hasKey("sliceThickness") ? nbt.getFloat("sliceThickness") : 0.15f;
|
||||
this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing");
|
||||
}
|
||||
|
||||
// ==================== TYPE-SPECIFIC GETTERS ====================
|
||||
@@ -158,14 +154,6 @@ public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
|
||||
this.sliceThickness = Math.max(0.05f, thickness);
|
||||
}
|
||||
|
||||
public boolean isPiercing() {
|
||||
return piercing;
|
||||
}
|
||||
|
||||
public void setPiercing(boolean piercing) {
|
||||
this.piercing = piercing;
|
||||
}
|
||||
|
||||
// ==================== TYPE-SPECIFIC GUI ====================
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@@ -186,15 +174,15 @@ public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
|
||||
FieldDef.floatField("gui.width", this::getSliceWidth, this::setSliceWidth).range(0.5f, 100.0f),
|
||||
FieldDef.floatField("gui.thickness", this::getSliceThickness, this::setSliceThickness).range(0.05f, 100.0f)
|
||||
));
|
||||
defs.add(FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing)
|
||||
.hover("ability.hover.piercing"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200)
|
||||
));
|
||||
defs.add(FieldDef.section("ability.section.explosive"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
|
||||
.visibleWhen(this::isExplosive));
|
||||
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
}
|
||||
}
|
||||
+25
-4
@@ -2,9 +2,9 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
@@ -49,7 +49,7 @@ public class AbilitySweeper extends AbilityEnergy implements IAbilitySweeper {
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 15.0f;
|
||||
this.minRange = 0.0f;
|
||||
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 60;
|
||||
// Circle telegraph to show range
|
||||
@@ -62,6 +62,21 @@ public class AbilitySweeper extends AbilityEnergy implements IAbilitySweeper {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConcurrentCapable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowFreeOnCast() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void detach() {
|
||||
activeEntity = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
@@ -97,6 +112,12 @@ public class AbilitySweeper extends AbilityEnergy implements IAbilitySweeper {
|
||||
if (activeEntity == null || activeEntity.isDead) {
|
||||
activeEntity = null;
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Free on Cast: complete immediately — sweeper lives independently
|
||||
if (isFreeOnCast()) {
|
||||
signalCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ package kamkeel.npcs.controllers.data.ability.type.energy;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyBarrierData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyPanelData;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyPanelData;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityEnergyBarrier;
|
||||
import kamkeel.npcs.entity.EntityEnergyPanel;
|
||||
import kamkeel.npcs.entity.EntityAbilityBarrier;
|
||||
import kamkeel.npcs.entity.EntityAbilityPanel;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
@@ -23,7 +23,7 @@ import java.util.List;
|
||||
* Can float above ground (configurable height offset).
|
||||
* Optional launching mode fires the wall forward with damage/knockback.
|
||||
*/
|
||||
public class AbilityWall extends AbilityEnergyBarrier {
|
||||
public class AbilityWall extends AbilityBarrier {
|
||||
|
||||
private final EnergyPanelData panelData;
|
||||
|
||||
@@ -40,7 +40,7 @@ public class AbilityWall extends AbilityEnergyBarrier {
|
||||
this.minRange = 2.0f;
|
||||
this.cooldownTicks = 80;
|
||||
this.windUpTicks = 20;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.lockMovement = LockMode.WINDUP;
|
||||
this.telegraphType = TelegraphType.LINE;
|
||||
this.showTelegraph = true;
|
||||
this.windUpAnimationName = "";
|
||||
@@ -55,7 +55,7 @@ public class AbilityWall extends AbilityEnergyBarrier {
|
||||
// ==================== ABSTRACT IMPLEMENTATIONS ====================
|
||||
|
||||
@Override
|
||||
protected EntityEnergyBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target) {
|
||||
protected EntityAbilityBarrier createBarrierEntity(EntityLivingBase caster, EntityLivingBase target) {
|
||||
// Place wall between caster and target
|
||||
double placeX, placeY, placeZ;
|
||||
float yaw;
|
||||
@@ -87,11 +87,11 @@ public class AbilityWall extends AbilityEnergyBarrier {
|
||||
yaw = caster.rotationYaw;
|
||||
}
|
||||
|
||||
EntityEnergyPanel.PanelMode mode = panelData.launching
|
||||
? EntityEnergyPanel.PanelMode.LAUNCHED
|
||||
: EntityEnergyPanel.PanelMode.PLACED;
|
||||
EntityAbilityPanel.PanelMode mode = panelData.launching
|
||||
? EntityAbilityPanel.PanelMode.LAUNCHED
|
||||
: EntityAbilityPanel.PanelMode.PLACED;
|
||||
|
||||
EntityEnergyPanel panel = new EntityEnergyPanel(
|
||||
EntityAbilityPanel panel = new EntityAbilityPanel(
|
||||
caster.worldObj, caster,
|
||||
placeX, placeY, placeZ, yaw, mode,
|
||||
displayData.copy(), lightningData.copy(), barrierData.copy(), panelData.copy()
|
||||
|
||||
+3
-2
@@ -1,5 +1,6 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
package kamkeel.npcs.controllers.data.ability.util;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
@@ -84,7 +85,7 @@ public class AbilityTargetHelper {
|
||||
* @return true if the target should be affected
|
||||
*/
|
||||
public static boolean shouldAffect(EntityLivingBase caster, Entity target,
|
||||
TargetFilter filter, boolean includeSelf) {
|
||||
TargetFilter filter, boolean includeSelf) {
|
||||
if (target == caster) return includeSelf;
|
||||
if (!(target instanceof EntityLivingBase)) return false;
|
||||
if (!target.isEntityAlive()) return false;
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package kamkeel.npcs.controllers.data.energycharge;
|
||||
|
||||
import kamkeel.npcs.entity.EntityEnergyProjectile;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Client-side manager for packet-driven charging preview entities.
|
||||
* These entities are never spawned into the world; they are ticked and rendered manually.
|
||||
*/
|
||||
public class EnergyChargePreviewManager {
|
||||
|
||||
public static EnergyChargePreviewManager ClientInstance;
|
||||
|
||||
private final ConcurrentHashMap<String, EntityEnergyProjectile> previews = new ConcurrentHashMap<String, EntityEnergyProjectile>();
|
||||
|
||||
public static void initClient() {
|
||||
ClientInstance = new EnergyChargePreviewManager();
|
||||
}
|
||||
|
||||
public void addPreview(String instanceId, EntityEnergyProjectile entity) {
|
||||
if (instanceId == null || entity == null) return;
|
||||
previews.put(instanceId, entity);
|
||||
}
|
||||
|
||||
public void removePreview(String instanceId) {
|
||||
if (instanceId == null) return;
|
||||
previews.remove(instanceId);
|
||||
}
|
||||
|
||||
public Collection<EntityEnergyProjectile> getPreviews() {
|
||||
return previews.values();
|
||||
}
|
||||
|
||||
public boolean hasPreviews() {
|
||||
return !previews.isEmpty();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
previews.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick all preview entities and remove invalid/expired entries.
|
||||
*/
|
||||
public void tick(World world) {
|
||||
if (world == null) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
|
||||
Iterator<EntityEnergyProjectile> iterator = previews.values().iterator();
|
||||
while (iterator.hasNext()) {
|
||||
EntityEnergyProjectile entity = iterator.next();
|
||||
if (entity == null || entity.isDead || entity.worldObj != world) {
|
||||
iterator.remove();
|
||||
continue;
|
||||
}
|
||||
entity.onUpdate();
|
||||
if (entity.isDead) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+150
-47
@@ -1,10 +1,9 @@
|
||||
package kamkeel.npcs.entity;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyBarrierData;
|
||||
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.DamageSource;
|
||||
@@ -23,24 +22,73 @@ import java.util.List;
|
||||
* and barrier lifecycle management.
|
||||
* Extends EntityEnergyAbility for shared visual/owner/charging state.
|
||||
*/
|
||||
public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
|
||||
public abstract class EntityAbilityBarrier extends EntityEnergyAbility {
|
||||
|
||||
/**
|
||||
* Outcome of a projectile vs barrier interaction.
|
||||
*/
|
||||
public enum ProjectileHitResult {
|
||||
PASS,
|
||||
BLOCKED,
|
||||
BROKEN;
|
||||
|
||||
public boolean isAbsorbed() {
|
||||
return this == BLOCKED || this == BROKEN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full result of a projectile vs barrier hit after multiplier/event processing.
|
||||
*/
|
||||
public static class ProjectileHitOutcome {
|
||||
public final ProjectileHitResult result;
|
||||
public final float barrierHealthBefore;
|
||||
public final float barrierHealthAfter;
|
||||
public final float appliedBarrierDamage;
|
||||
public final float remainingProjectileDamage;
|
||||
public final boolean reflectEnabled;
|
||||
public final float reflectStrengthPct;
|
||||
public final boolean useHealth;
|
||||
|
||||
public ProjectileHitOutcome(ProjectileHitResult result,
|
||||
float barrierHealthBefore,
|
||||
float barrierHealthAfter,
|
||||
float appliedBarrierDamage,
|
||||
float remainingProjectileDamage,
|
||||
boolean reflectEnabled,
|
||||
float reflectStrengthPct,
|
||||
boolean useHealth) {
|
||||
this.result = result;
|
||||
this.barrierHealthBefore = barrierHealthBefore;
|
||||
this.barrierHealthAfter = barrierHealthAfter;
|
||||
this.appliedBarrierDamage = appliedBarrierDamage;
|
||||
this.remainingProjectileDamage = remainingProjectileDamage;
|
||||
this.reflectEnabled = reflectEnabled;
|
||||
this.reflectStrengthPct = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
|
||||
this.useHealth = useHealth;
|
||||
}
|
||||
|
||||
public boolean shouldReflect() {
|
||||
return reflectEnabled && useHealth && result == ProjectileHitResult.BLOCKED;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ACTIVE BARRIER REGISTRY ====================
|
||||
private static final List<WeakReference<EntityEnergyBarrier>> activeBarriers = new ArrayList<>();
|
||||
private static final List<WeakReference<EntityAbilityBarrier>> activeBarriers = new ArrayList<>();
|
||||
private boolean tracked = false;
|
||||
|
||||
public static void trackBarrier(EntityEnergyBarrier barrier) {
|
||||
public static void trackBarrier(EntityAbilityBarrier barrier) {
|
||||
activeBarriers.add(new WeakReference<>(barrier));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all living barriers in the given world. Auto-cleans dead refs.
|
||||
*/
|
||||
public static List<EntityEnergyBarrier> getActiveBarriers(World world) {
|
||||
List<EntityEnergyBarrier> result = new ArrayList<>();
|
||||
Iterator<WeakReference<EntityEnergyBarrier>> it = activeBarriers.iterator();
|
||||
public static List<EntityAbilityBarrier> getActiveBarriers(World world) {
|
||||
List<EntityAbilityBarrier> result = new ArrayList<>();
|
||||
Iterator<WeakReference<EntityAbilityBarrier>> it = activeBarriers.iterator();
|
||||
while (it.hasNext()) {
|
||||
EntityEnergyBarrier b = it.next().get();
|
||||
EntityAbilityBarrier b = it.next().get();
|
||||
if (b == null || b.isDead) {
|
||||
it.remove();
|
||||
} else if (b.worldObj == world) {
|
||||
@@ -63,7 +111,7 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
|
||||
protected static final int DW_HEALTH_PERCENT = 21;
|
||||
protected static final int DW_HIT_FLASH = 22;
|
||||
|
||||
public EntityEnergyBarrier(World world) {
|
||||
public EntityAbilityBarrier(World world) {
|
||||
super(world);
|
||||
this.noClip = true;
|
||||
}
|
||||
@@ -94,9 +142,10 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
|
||||
public abstract void finishCharging();
|
||||
|
||||
/**
|
||||
* Push entities away from the barrier surface.
|
||||
* Process entity physics for this barrier.
|
||||
* Handles solid wall (entity repositioning) and knockback (repulsion force).
|
||||
*/
|
||||
protected abstract void knockbackEntities();
|
||||
protected abstract void processEntityPhysics();
|
||||
|
||||
/**
|
||||
* Get the maximum extent of this barrier for distance pre-filtering.
|
||||
@@ -164,6 +213,20 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
|
||||
* Returns true if the barrier absorbed the hit (projectile should be destroyed).
|
||||
*/
|
||||
public boolean onProjectileHit(EntityEnergyProjectile projectile, float baseDamage) {
|
||||
return onProjectileHitDetailed(projectile, baseDamage).isAbsorbed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply damage to this barrier from a projectile and return detailed outcome.
|
||||
*/
|
||||
public ProjectileHitResult onProjectileHitDetailed(EntityEnergyProjectile projectile, float baseDamage) {
|
||||
return onProjectileHitResolved(projectile, baseDamage).result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply damage to this barrier from a projectile and return full hit context.
|
||||
*/
|
||||
public ProjectileHitOutcome onProjectileHitResolved(EntityEnergyProjectile projectile, float baseDamage) {
|
||||
// Get damage multiplier for this projectile type
|
||||
String typeId = "";
|
||||
if (projectile.getSourceAbility() != null) {
|
||||
@@ -175,25 +238,67 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
|
||||
// Fire hit event (may cancel or modify damage)
|
||||
if (!worldObj.isRemote) {
|
||||
float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, projectile, damage);
|
||||
if (eventDamage < 0) return false; // Event cancelled — don't block
|
||||
if (eventDamage < 0) {
|
||||
return new ProjectileHitOutcome(
|
||||
ProjectileHitResult.PASS,
|
||||
currentHealth,
|
||||
currentHealth,
|
||||
0.0f,
|
||||
0.0f,
|
||||
barrierData.reflect,
|
||||
barrierData.reflectStrengthPct,
|
||||
barrierData.useHealth
|
||||
);
|
||||
}
|
||||
damage = eventDamage;
|
||||
}
|
||||
|
||||
triggerHitFlash();
|
||||
|
||||
float healthBefore = currentHealth;
|
||||
|
||||
if (!barrierData.useHealth) {
|
||||
return true; // Duration-only mode: block but don't take damage
|
||||
return new ProjectileHitOutcome(
|
||||
ProjectileHitResult.BLOCKED,
|
||||
healthBefore,
|
||||
healthBefore,
|
||||
0.0f,
|
||||
0.0f,
|
||||
false,
|
||||
barrierData.reflectStrengthPct,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
currentHealth -= damage;
|
||||
syncHealthPercent();
|
||||
|
||||
if (currentHealth <= 0) {
|
||||
float remaining = Math.max(0.0f, damage - healthBefore);
|
||||
onBarrierDestroyed();
|
||||
this.setDead();
|
||||
return new ProjectileHitOutcome(
|
||||
ProjectileHitResult.BROKEN,
|
||||
healthBefore,
|
||||
0.0f,
|
||||
damage,
|
||||
remaining,
|
||||
barrierData.reflect,
|
||||
barrierData.reflectStrengthPct,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new ProjectileHitOutcome(
|
||||
ProjectileHitResult.BLOCKED,
|
||||
healthBefore,
|
||||
currentHealth,
|
||||
damage,
|
||||
0.0f,
|
||||
barrierData.reflect,
|
||||
barrierData.reflectStrengthPct,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== MELEE DAMAGE ====================
|
||||
@@ -203,6 +308,16 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
|
||||
return barrierData.meleeEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEntityInsideOpaqueBlock() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean func_145771_j(double x, double y, double z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean attackEntityFrom(DamageSource source, float amount) {
|
||||
if (worldObj.isRemote || !barrierData.meleeEnabled) return false;
|
||||
@@ -324,20 +439,9 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
|
||||
return this.dataWatcher.getWatchableObjectFloat(DW_HEALTH_PERCENT);
|
||||
}
|
||||
|
||||
protected boolean isKnockbackTarget(EntityLivingBase entity) {
|
||||
switch (barrierData.knockbackTarget) {
|
||||
case 1:
|
||||
return entity instanceof EntityPlayer;
|
||||
case 2:
|
||||
return entity instanceof EntityNPCInterface;
|
||||
default:
|
||||
return entity instanceof EntityPlayer || entity instanceof EntityNPCInterface;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity is an ally of this barrier's owner.
|
||||
* Allies should not be knocked back by barriers.
|
||||
* Allies are not affected by barrier physics.
|
||||
*/
|
||||
protected boolean isAllyOfOwner(EntityLivingBase entity) {
|
||||
Entity owner = getOwnerEntity();
|
||||
@@ -345,7 +449,7 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
|
||||
return AbilityTargetHelper.isAlly((EntityLivingBase) owner, entity);
|
||||
}
|
||||
|
||||
// ==================== CONTAINMENT & PROTECTION ====================
|
||||
// ==================== CONTAINMENT & ABSORBING ====================
|
||||
|
||||
/**
|
||||
* Check if an entity is geometrically inside this barrier's protected zone.
|
||||
@@ -356,27 +460,16 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a damage source originates from outside this barrier.
|
||||
* Uses geometry-specific containment check via isEntityInside().
|
||||
* Find a barrier that would absorb damage for the given entity (its caster).
|
||||
* Returns the barrier if: absorbing is enabled AND entity is the barrier's owner.
|
||||
*/
|
||||
public boolean isDamageFromOutside(DamageSource source) {
|
||||
Entity attacker = source.getEntity();
|
||||
if (attacker == null) return true; // Environmental damage treated as outside
|
||||
return !isEntityInside(attacker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a barrier that would protect the given entity from outside damage.
|
||||
* Returns the barrier if: damageProtection is enabled AND entity is inside.
|
||||
* Any entity inside a dome with damageProtection gets redirected damage.
|
||||
*/
|
||||
public static EntityEnergyBarrier getProtectingBarrier(Entity entity) {
|
||||
public static EntityAbilityBarrier getAbsorbingBarrier(Entity entity) {
|
||||
if (entity == null || entity.worldObj == null) return null;
|
||||
List<EntityEnergyBarrier> barriers = getActiveBarriers(entity.worldObj);
|
||||
for (EntityEnergyBarrier barrier : barriers) {
|
||||
List<EntityAbilityBarrier> barriers = getActiveBarriers(entity.worldObj);
|
||||
for (EntityAbilityBarrier barrier : barriers) {
|
||||
if (barrier.isDead) continue;
|
||||
if (!barrier.barrierData.damageProtection) continue;
|
||||
if (barrier.isEntityInside(entity)) {
|
||||
if (!barrier.barrierData.absorbing) continue;
|
||||
if (barrier.ownerEntityId == entity.getEntityId()) {
|
||||
return barrier;
|
||||
}
|
||||
}
|
||||
@@ -437,6 +530,16 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility {
|
||||
syncHealthPercent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the barrier's max health and set current health to match.
|
||||
* Used by extenders (e.g., DBC scaling) to dynamically set barrier HP at spawn time.
|
||||
*/
|
||||
public void setBarrierMaxHealth(float maxHealth) {
|
||||
this.barrierData.setMaxHealth(maxHealth);
|
||||
this.currentHealth = maxHealth;
|
||||
syncHealthPercent();
|
||||
}
|
||||
|
||||
public int getTicksAlive() {
|
||||
return ticksAlive;
|
||||
}
|
||||
@@ -1,25 +1,26 @@
|
||||
package kamkeel.npcs.entity;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyTrajectoryData;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.MovingObjectPosition;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.EventHooks;
|
||||
import noppes.npcs.LogWriter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -33,6 +34,11 @@ import java.util.List;
|
||||
*/
|
||||
public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
|
||||
private enum BeamMode {
|
||||
ANCHORED,
|
||||
FREE_TRAIL
|
||||
}
|
||||
|
||||
// Beam shape properties
|
||||
private float beamWidth = 0.3f;
|
||||
private float headSize = 0.5f;
|
||||
@@ -41,22 +47,18 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
private List<Vec3> trailPoints = new ArrayList<>();
|
||||
private static final int MAX_TRAIL_POINTS = 200;
|
||||
private static final double MIN_POINT_DISTANCE = 0.2;
|
||||
private static final int TRAIL_COMPACT_THRESHOLD = 128;
|
||||
private int trailStartIndex = 0;
|
||||
|
||||
// Head position relative to origin
|
||||
private double headOffsetX, headOffsetY, headOffsetZ;
|
||||
private double prevHeadOffsetX, prevHeadOffsetY, prevHeadOffsetZ;
|
||||
|
||||
// Origin stays fixed (or follows owner)
|
||||
private boolean attachedToOwner = true;
|
||||
|
||||
// Whether to render tail orb (only when anchored)
|
||||
private boolean renderTailOrb = true;
|
||||
// Beam behavior mode (single source of truth).
|
||||
private BeamMode beamMode = BeamMode.ANCHORED;
|
||||
|
||||
// Charging state (beam-specific)
|
||||
private float chargeOffsetDistance = 1.0f;
|
||||
|
||||
// Trail fading for non-anchored beams (comet effect)
|
||||
private boolean fadeTrail = false;
|
||||
private int trailFadeTime = 20; // Ticks for trail to fully fade
|
||||
private List<Integer> trailPointAges = new ArrayList<>();
|
||||
|
||||
@@ -67,6 +69,65 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
super(world);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public boolean isInRangeToRenderDist(double distance) {
|
||||
// Beam can extend far from entity position (head); use a generous render distance
|
||||
// that accounts for the full beam length (origin to head)
|
||||
double headDist = Math.sqrt(headOffsetX * headOffsetX + headOffsetY * headOffsetY + headOffsetZ * headOffsetZ);
|
||||
double range = Math.max(128.0D, headDist * 2.0D + 64.0D);
|
||||
return distance < range * range;
|
||||
}
|
||||
|
||||
private static BeamMode modeFromAnchored(boolean anchored) {
|
||||
return anchored ? BeamMode.ANCHORED : BeamMode.FREE_TRAIL;
|
||||
}
|
||||
|
||||
private boolean isAnchoredMode() {
|
||||
return beamMode == BeamMode.ANCHORED;
|
||||
}
|
||||
|
||||
private boolean isFadingMode() {
|
||||
return beamMode == BeamMode.FREE_TRAIL;
|
||||
}
|
||||
|
||||
private int getActiveTrailSize() {
|
||||
return Math.max(0, trailPoints.size() - trailStartIndex);
|
||||
}
|
||||
|
||||
private void resetHeadOffsets() {
|
||||
headOffsetX = 0;
|
||||
headOffsetY = 0;
|
||||
headOffsetZ = 0;
|
||||
prevHeadOffsetX = 0;
|
||||
prevHeadOffsetY = 0;
|
||||
prevHeadOffsetZ = 0;
|
||||
}
|
||||
|
||||
private void resetTrailStorage() {
|
||||
trailPoints.clear();
|
||||
trailPointAges.clear();
|
||||
trailStartIndex = 0;
|
||||
}
|
||||
|
||||
private void setBeamMode(BeamMode mode) {
|
||||
beamMode = mode != null ? mode : BeamMode.ANCHORED;
|
||||
if (isFadingMode()) {
|
||||
while (trailPointAges.size() < trailPoints.size()) {
|
||||
trailPointAges.add(0);
|
||||
}
|
||||
while (trailPointAges.size() > trailPoints.size()) {
|
||||
trailPointAges.remove(trailPointAges.size() - 1);
|
||||
}
|
||||
} else {
|
||||
if (trailStartIndex > 0 && trailStartIndex < trailPoints.size()) {
|
||||
trailPoints = new ArrayList<Vec3>(trailPoints.subList(trailStartIndex, trailPoints.size()));
|
||||
}
|
||||
trailStartIndex = 0;
|
||||
trailPointAges.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full constructor with all parameters using data classes.
|
||||
*
|
||||
@@ -91,17 +152,11 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
this.beamWidth = beamWidth;
|
||||
this.headSize = headSize;
|
||||
|
||||
// Anchored mode controls whether origin follows owner and tail orb is rendered
|
||||
this.attachedToOwner = anchoredMode;
|
||||
this.renderTailOrb = anchoredMode;
|
||||
// Beam mode controls tail attachment/orb/fading behavior.
|
||||
setBeamMode(modeFromAnchored(anchoredMode));
|
||||
|
||||
// Initialize head offset at origin (0,0,0 relative)
|
||||
this.headOffsetX = 0;
|
||||
this.headOffsetY = 0;
|
||||
this.headOffsetZ = 0;
|
||||
this.prevHeadOffsetX = 0;
|
||||
this.prevHeadOffsetY = 0;
|
||||
this.prevHeadOffsetZ = 0;
|
||||
resetHeadOffsets();
|
||||
|
||||
// Add initial trail point at origin (relative 0,0,0)
|
||||
trailPoints.add(Vec3.createVectorHelper(0, 0, 0));
|
||||
@@ -115,166 +170,58 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
}
|
||||
|
||||
public void setupCharging(EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) {
|
||||
setCharging(true);
|
||||
this.chargeDuration = chargeDuration;
|
||||
this.chargeTick = 0;
|
||||
setupChargingState(anchor, chargeDuration);
|
||||
this.chargeOffsetDistance = chargeOffsetDistance;
|
||||
this.anchorData = anchor;
|
||||
this.fadeTrail = !attachedToOwner;
|
||||
this.motionX = 0;
|
||||
this.motionY = 0;
|
||||
this.motionZ = 0;
|
||||
clearMotion();
|
||||
}
|
||||
|
||||
public void setupPreview(EntityLivingBase owner, float beamWidth, float headSize, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) {
|
||||
this.setPreviewMode(true);
|
||||
this.setPreviewOwner(owner);
|
||||
setupPreviewState(owner, display, lightning, anchor, chargeDuration);
|
||||
|
||||
// Set visual properties
|
||||
this.beamWidth = beamWidth;
|
||||
this.headSize = headSize;
|
||||
this.size = headSize;
|
||||
this.displayData = display;
|
||||
this.lightningData = lightning;
|
||||
setVisualSize(headSize);
|
||||
|
||||
// Set charging state
|
||||
this.setCharging(true);
|
||||
this.chargeDuration = chargeDuration;
|
||||
this.chargeTick = 0;
|
||||
this.chargeOffsetDistance = chargeOffsetDistance;
|
||||
this.anchorData = anchor;
|
||||
|
||||
// Initial position at anchor point
|
||||
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData, chargeOffsetDistance);
|
||||
this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
|
||||
this.prevPosX = pos.xCoord;
|
||||
this.prevPosY = pos.yCoord;
|
||||
this.prevPosZ = pos.zCoord;
|
||||
this.startX = pos.xCoord;
|
||||
this.startY = pos.yCoord;
|
||||
this.startZ = pos.zCoord;
|
||||
setChargeOriginFromAnchor(owner, anchorData, chargeOffsetDistance);
|
||||
|
||||
// Initialize head offsets
|
||||
this.headOffsetX = 0;
|
||||
this.headOffsetY = 0;
|
||||
this.headOffsetZ = 0;
|
||||
this.prevHeadOffsetX = 0;
|
||||
this.prevHeadOffsetY = 0;
|
||||
this.prevHeadOffsetZ = 0;
|
||||
|
||||
// Attach to owner for anchor following
|
||||
this.attachedToOwner = true;
|
||||
this.renderTailOrb = true;
|
||||
resetHeadOffsets();
|
||||
|
||||
// Clear motion
|
||||
this.motionX = 0;
|
||||
this.motionY = 0;
|
||||
this.motionZ = 0;
|
||||
clearMotion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start preview firing (simulates firing toward a point in front of NPC).
|
||||
*/
|
||||
public void startPreviewFiring() {
|
||||
setCharging(false);
|
||||
|
||||
// Origin (tail) stays at the charged position
|
||||
startX = posX;
|
||||
startY = posY;
|
||||
startZ = posZ;
|
||||
startPreviewFiringDefault();
|
||||
|
||||
// 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;
|
||||
resetHeadOffsets();
|
||||
|
||||
// Reset trail
|
||||
trailPoints.clear();
|
||||
trailPointAges.clear();
|
||||
resetTrailStorage();
|
||||
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);
|
||||
motionX = -Math.sin(yaw) * getSpeed();
|
||||
motionY = 0;
|
||||
motionZ = Math.cos(yaw) * getSpeed();
|
||||
} else {
|
||||
motionX = getSpeed();
|
||||
motionY = 0;
|
||||
motionZ = 0;
|
||||
}
|
||||
if (isFadingMode()) trailPointAges.add(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the beam firing (exit charging mode).
|
||||
* Called by ability when windup ends.
|
||||
* <p>
|
||||
* For anchored beams: origin follows owner, head starts at charged position
|
||||
* For non-anchored beams: origin fixed at charged position, head starts there
|
||||
*/
|
||||
public void startFiring(EntityLivingBase target) {
|
||||
setCharging(false);
|
||||
|
||||
// For player casters, snap to look vector for crosshair-aligned launch
|
||||
snapToPlayerLookVector();
|
||||
|
||||
// Origin (tail) stays at the charged position - where the orb was
|
||||
// This is the same for both anchored and non-anchored modes
|
||||
startX = posX;
|
||||
startY = posY;
|
||||
startZ = posZ;
|
||||
startMovingTowardTargetFromStartDefault(target);
|
||||
|
||||
// Head starts at the origin (tail position)
|
||||
headOffsetX = 0;
|
||||
headOffsetY = 0;
|
||||
headOffsetZ = 0;
|
||||
prevHeadOffsetX = 0;
|
||||
prevHeadOffsetY = 0;
|
||||
prevHeadOffsetZ = 0;
|
||||
|
||||
// Origin no longer follows owner after firing starts
|
||||
// (for anchored mode, tail is fixed in space; for non-anchored, there's no tail)
|
||||
attachedToOwner = false;
|
||||
resetHeadOffsets();
|
||||
|
||||
// Initialize trail with just the origin point
|
||||
trailPoints.clear();
|
||||
trailPointAges.clear();
|
||||
resetTrailStorage();
|
||||
trailPoints.add(Vec3.createVectorHelper(0, 0, 0));
|
||||
if (fadeTrail) trailPointAges.add(0);
|
||||
|
||||
// Calculate velocity toward target (head starts at origin = startX/Y/Z)
|
||||
Entity owner = getOwnerEntity();
|
||||
|
||||
if (target != null) {
|
||||
double dx = target.posX - startX;
|
||||
double dy = (target.posY + target.getEyeHeight()) - startY;
|
||||
double dz = target.posZ - startZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len > 0) {
|
||||
motionX = (dx / len) * getSpeed();
|
||||
motionY = (dy / len) * getSpeed();
|
||||
motionZ = (dz / len) * getSpeed();
|
||||
}
|
||||
} else if (owner != null) {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
|
||||
motionY = -Math.sin(pitch) * getSpeed();
|
||||
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
|
||||
}
|
||||
if (isFadingMode()) trailPointAges.add(0);
|
||||
|
||||
if (DEBUG_LOGGING && !worldObj.isRemote) {
|
||||
LogWriter.info("[Beam] startFiring: origin=(" + startX + "," + startY + "," + startZ +
|
||||
@@ -312,7 +259,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
prevHeadOffsetZ = headOffsetZ;
|
||||
|
||||
// Age trail points for fading effect
|
||||
if (fadeTrail) {
|
||||
if (isFadingMode()) {
|
||||
ageTrailPoints();
|
||||
}
|
||||
|
||||
@@ -346,7 +293,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
headOffsetZ += motionZ;
|
||||
|
||||
// Update origin if attached to owner
|
||||
if (attachedToOwner) {
|
||||
if (isAnchoredMode()) {
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner != null) {
|
||||
startX = owner.posX;
|
||||
@@ -374,7 +321,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
String.format("%.2f", headOffsetZ) + ") origin=(" +
|
||||
String.format("%.2f", startX) + "," +
|
||||
String.format("%.2f", startY) + "," +
|
||||
String.format("%.2f", startZ) + ") trail=" + trailPoints.size());
|
||||
String.format("%.2f", startZ) + ") trail=" + getActiveTrailSize());
|
||||
}
|
||||
|
||||
// Skip collision checks on first few ticks
|
||||
@@ -420,25 +367,43 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
* Age trail points and remove old ones (for comet effect).
|
||||
*/
|
||||
private void ageTrailPoints() {
|
||||
// Age all trail points
|
||||
for (int i = 0; i < trailPointAges.size(); i++) {
|
||||
if (trailStartIndex >= trailPoints.size()) {
|
||||
resetTrailStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
// Age all active trail points.
|
||||
for (int i = trailStartIndex; i < trailPointAges.size(); i++) {
|
||||
trailPointAges.set(i, trailPointAges.get(i) + 1);
|
||||
}
|
||||
|
||||
// Remove trail points that have fully faded
|
||||
while (!trailPointAges.isEmpty() && trailPointAges.get(0) >= trailFadeTime) {
|
||||
trailPointAges.remove(0);
|
||||
if (!trailPoints.isEmpty()) {
|
||||
trailPoints.remove(0);
|
||||
}
|
||||
// Move logical start index forward for fully faded points.
|
||||
while (trailStartIndex < trailPointAges.size() && trailPointAges.get(trailStartIndex) >= trailFadeTime) {
|
||||
trailStartIndex++;
|
||||
}
|
||||
|
||||
if (trailStartIndex >= trailPoints.size()) {
|
||||
resetTrailStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
// Compact stale prefix to avoid unbounded growth of dead entries.
|
||||
if (trailStartIndex >= TRAIL_COMPACT_THRESHOLD && trailStartIndex * 2 >= trailPoints.size()) {
|
||||
trailPoints = new ArrayList<Vec3>(trailPoints.subList(trailStartIndex, trailPoints.size()));
|
||||
trailPointAges = new ArrayList<Integer>(trailPointAges.subList(trailStartIndex, trailPointAges.size()));
|
||||
trailStartIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void addTrailPoint() {
|
||||
if (trailStartIndex >= trailPoints.size()) {
|
||||
resetTrailStorage();
|
||||
}
|
||||
|
||||
// Trail points are RELATIVE to origin
|
||||
if (trailPoints.isEmpty()) {
|
||||
trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ));
|
||||
if (fadeTrail) {
|
||||
if (isFadingMode()) {
|
||||
trailPointAges.add(0);
|
||||
}
|
||||
return;
|
||||
@@ -453,12 +418,12 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
|
||||
if (dist >= MIN_POINT_DISTANCE) {
|
||||
trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ));
|
||||
if (fadeTrail) {
|
||||
if (isFadingMode()) {
|
||||
trailPointAges.add(0);
|
||||
}
|
||||
|
||||
// Limit trail length (only if not using fading - fading handles its own cleanup)
|
||||
if (!fadeTrail) {
|
||||
if (!isFadingMode()) {
|
||||
while (trailPoints.size() > MAX_TRAIL_POINTS) {
|
||||
trailPoints.remove(0);
|
||||
}
|
||||
@@ -476,7 +441,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
headOffsetZ += motionZ;
|
||||
|
||||
// Update origin if attached to owner
|
||||
if (attachedToOwner) {
|
||||
if (isAnchoredMode()) {
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner != null) {
|
||||
startX = owner.posX;
|
||||
@@ -546,55 +511,26 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
double prevHeadWorldY = startY + prevHeadOffsetY;
|
||||
double prevHeadWorldZ = startZ + prevHeadOffsetZ;
|
||||
|
||||
Vec3 currentPos = Vec3.createVectorHelper(prevHeadWorldX, prevHeadWorldY, prevHeadWorldZ);
|
||||
Vec3 nextPos = Vec3.createVectorHelper(headX, headY, headZ);
|
||||
// Use full raytrace that doesn't stop at liquids and checks all blocks
|
||||
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
|
||||
|
||||
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
|
||||
if (!worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
|
||||
}
|
||||
hasHit = true;
|
||||
if (isExplosive()) {
|
||||
posX = blockHit.hitVec.xCoord;
|
||||
posY = blockHit.hitVec.yCoord;
|
||||
posZ = blockHit.hitVec.zCoord;
|
||||
doExplosion();
|
||||
}
|
||||
this.setDead();
|
||||
}
|
||||
handleBlockImpact(rayTraceBlocks(prevHeadWorldX, prevHeadWorldY, prevHeadWorldZ, headX, headY, headZ), true);
|
||||
}
|
||||
|
||||
private void checkEntityCollision(double headX, double headY, double headZ) {
|
||||
double hitSize = headSize * 0.5;
|
||||
// Swept head hitbox prevents misses when beam head moves quickly between ticks.
|
||||
double prevHeadWorldX = startX + prevHeadOffsetX;
|
||||
double prevHeadWorldY = startY + prevHeadOffsetY;
|
||||
double prevHeadWorldZ = startZ + prevHeadOffsetZ;
|
||||
double hitSize = Math.max(0.05, headSize * 0.5);
|
||||
AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox(
|
||||
headX - hitSize, headY - hitSize, headZ - hitSize,
|
||||
headX + hitSize, headY + hitSize, headZ + hitSize
|
||||
Math.min(prevHeadWorldX, headX) - hitSize,
|
||||
Math.min(prevHeadWorldY, headY) - hitSize,
|
||||
Math.min(prevHeadWorldZ, headZ) - hitSize,
|
||||
Math.max(prevHeadWorldX, headX) + hitSize,
|
||||
Math.max(prevHeadWorldY, headY) + hitSize,
|
||||
Math.max(prevHeadWorldZ, headZ) + hitSize
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (shouldIgnoreEntity(entity)) continue;
|
||||
|
||||
if (DEBUG_LOGGING) {
|
||||
LogWriter.info("[Beam] DEAD: Entity collision with " + entity.getClass().getSimpleName() + " at tick " + ticksExisted);
|
||||
}
|
||||
hasHit = true;
|
||||
|
||||
if (isExplosive()) {
|
||||
posX = headX;
|
||||
posY = headY;
|
||||
posZ = headZ;
|
||||
doExplosion();
|
||||
} else {
|
||||
applyDamage(entity);
|
||||
}
|
||||
|
||||
this.setDead();
|
||||
return;
|
||||
if (processEntitiesInHitBox(hitBox, headX, headY, headZ) && DEBUG_LOGGING) {
|
||||
LogWriter.info("[Beam] DEAD: Entity collision at tick " + ticksExisted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -617,7 +553,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
}
|
||||
|
||||
public void setAttachedToOwner(boolean attached) {
|
||||
this.attachedToOwner = attached;
|
||||
setBeamMode(modeFromAnchored(attached));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -640,7 +576,13 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
* Get trail points. These are RELATIVE to origin (startX/Y/Z).
|
||||
*/
|
||||
public List<Vec3> getTrailPoints() {
|
||||
return trailPoints;
|
||||
if (trailStartIndex <= 0) {
|
||||
return trailPoints;
|
||||
}
|
||||
if (trailStartIndex >= trailPoints.size()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return trailPoints.subList(trailStartIndex, trailPoints.size());
|
||||
}
|
||||
|
||||
public double getOriginX() {
|
||||
@@ -659,28 +601,34 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
* Whether the tail orb should be rendered (only true in anchored mode).
|
||||
*/
|
||||
public boolean shouldRenderTailOrb() {
|
||||
return renderTailOrb;
|
||||
return isAnchoredMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the beam is attached to its owner (anchored mode).
|
||||
*/
|
||||
public boolean isAttachedToOwner() {
|
||||
return attachedToOwner;
|
||||
return isAnchoredMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether trail should fade (comet effect for non-anchored beams).
|
||||
*/
|
||||
public boolean hasFadingTrail() {
|
||||
return fadeTrail;
|
||||
return isFadingMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trail point ages for fading calculation.
|
||||
*/
|
||||
public List<Integer> getTrailPointAges() {
|
||||
return trailPointAges;
|
||||
if (trailStartIndex <= 0) {
|
||||
return trailPointAges;
|
||||
}
|
||||
if (trailStartIndex >= trailPointAges.size()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return trailPointAges.subList(trailStartIndex, trailPointAges.size());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -702,17 +650,26 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
this.prevHeadOffsetX = this.headOffsetX;
|
||||
this.prevHeadOffsetY = this.headOffsetY;
|
||||
this.prevHeadOffsetZ = this.headOffsetZ;
|
||||
this.attachedToOwner = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner");
|
||||
this.renderTailOrb = !nbt.hasKey("RenderTailOrb") || nbt.getBoolean("RenderTailOrb");
|
||||
readChargingNBT(nbt);
|
||||
this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f;
|
||||
this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail");
|
||||
this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20;
|
||||
|
||||
boolean attachedLegacy = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner");
|
||||
BeamMode loadedMode = modeFromAnchored(attachedLegacy);
|
||||
if (nbt.hasKey("BeamMode")) {
|
||||
try {
|
||||
loadedMode = BeamMode.valueOf(nbt.getString("BeamMode"));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
loadedMode = modeFromAnchored(attachedLegacy);
|
||||
}
|
||||
} else if (nbt.hasKey("FadeTrail")) {
|
||||
loadedMode = nbt.getBoolean("FadeTrail") ? BeamMode.FREE_TRAIL : modeFromAnchored(attachedLegacy);
|
||||
}
|
||||
|
||||
// Read trail points (relative to origin)
|
||||
trailPoints.clear();
|
||||
trailPointAges.clear();
|
||||
trailStartIndex = 0;
|
||||
if (nbt.hasKey("Trail")) {
|
||||
NBTTagList trailList = nbt.getTagList("Trail", 10);
|
||||
for (int i = 0; i < trailList.tagCount(); i++) {
|
||||
@@ -732,10 +689,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
trailPointAges.add(age);
|
||||
}
|
||||
}
|
||||
// If trail points exist but ages don't (legacy data), initialize ages to 0
|
||||
while (trailPointAges.size() < trailPoints.size()) {
|
||||
trailPointAges.add(0);
|
||||
}
|
||||
setBeamMode(loadedMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -745,16 +699,18 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
nbt.setDouble("HeadOffsetX", headOffsetX);
|
||||
nbt.setDouble("HeadOffsetY", headOffsetY);
|
||||
nbt.setDouble("HeadOffsetZ", headOffsetZ);
|
||||
nbt.setBoolean("AttachedToOwner", attachedToOwner);
|
||||
nbt.setBoolean("RenderTailOrb", renderTailOrb);
|
||||
nbt.setBoolean("AttachedToOwner", isAnchoredMode());
|
||||
nbt.setBoolean("RenderTailOrb", isAnchoredMode());
|
||||
nbt.setString("BeamMode", beamMode.name());
|
||||
writeChargingNBT(nbt);
|
||||
nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance);
|
||||
nbt.setBoolean("FadeTrail", fadeTrail);
|
||||
nbt.setBoolean("FadeTrail", isFadingMode());
|
||||
nbt.setInteger("TrailFadeTime", trailFadeTime);
|
||||
|
||||
// Write trail points
|
||||
NBTTagList trailList = new NBTTagList();
|
||||
for (Vec3 point : trailPoints) {
|
||||
for (int i = trailStartIndex; i < trailPoints.size(); i++) {
|
||||
Vec3 point = trailPoints.get(i);
|
||||
NBTTagCompound pointNbt = new NBTTagCompound();
|
||||
pointNbt.setDouble("X", point.xCoord);
|
||||
pointNbt.setDouble("Y", point.yCoord);
|
||||
@@ -764,9 +720,11 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
nbt.setTag("Trail", trailList);
|
||||
|
||||
// Write trail point ages for fading trail sync
|
||||
int[] ages = new int[trailPointAges.size()];
|
||||
for (int i = 0; i < trailPointAges.size(); i++) {
|
||||
ages[i] = trailPointAges.get(i);
|
||||
int activeSize = isFadingMode() ? Math.max(0, trailPoints.size() - trailStartIndex) : 0;
|
||||
int[] ages = new int[activeSize];
|
||||
for (int i = 0; i < activeSize; i++) {
|
||||
int idx = trailStartIndex + i;
|
||||
ages[i] = idx < trailPointAges.size() ? trailPointAges.get(idx) : 0;
|
||||
}
|
||||
nbt.setIntArray("TrailAges", ages);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
package kamkeel.npcs.entity;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyTrajectoryData;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.MovingObjectPosition;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.EventHooks;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Disc projectile - flat spinning disc with optional boomerang behavior.
|
||||
@@ -98,18 +94,7 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
|
||||
* Can be fired when transitioning to active phase.
|
||||
*/
|
||||
public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyDisplayData display, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, boolean vertical) {
|
||||
this.setPreviewMode(true);
|
||||
this.setPreviewOwner(owner);
|
||||
|
||||
// Set visual properties
|
||||
this.displayData = display;
|
||||
this.lightningData = lightning;
|
||||
|
||||
// Set charging state
|
||||
this.setCharging(true);
|
||||
this.chargeDuration = chargeDuration;
|
||||
this.chargeTick = 0;
|
||||
this.anchorData = anchor;
|
||||
setupPreviewState(owner, display, lightning, anchor, chargeDuration);
|
||||
this.vertical = vertical;
|
||||
|
||||
// Store target size and start at 0 for grow effect
|
||||
@@ -117,55 +102,16 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
|
||||
this.targetDiscThickness = discThickness;
|
||||
this.discRadius = 0.01f;
|
||||
this.discThickness = 0.01f;
|
||||
this.size = 0.01f;
|
||||
this.renderCurrentSize = 0.01f;
|
||||
this.prevRenderSize = 0.01f;
|
||||
|
||||
// Initial position at anchor point
|
||||
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData);
|
||||
this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
|
||||
this.prevPosX = pos.xCoord;
|
||||
this.prevPosY = pos.yCoord;
|
||||
this.prevPosZ = pos.zCoord;
|
||||
this.startX = pos.xCoord;
|
||||
this.startY = pos.yCoord;
|
||||
this.startZ = pos.zCoord;
|
||||
|
||||
// Clear motion
|
||||
this.motionX = 0;
|
||||
this.motionY = 0;
|
||||
this.motionZ = 0;
|
||||
setVisualSize(0.01f);
|
||||
setChargeOriginFromAnchor(owner, anchorData);
|
||||
clearMotion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start preview firing (simulates firing toward a point in front of NPC).
|
||||
*/
|
||||
public void startPreviewFiring() {
|
||||
setCharging(false);
|
||||
|
||||
// Update start position to current position
|
||||
startX = posX;
|
||||
startY = posY;
|
||||
startZ = posZ;
|
||||
|
||||
// Sync prev position to prevent visual jump on first frame
|
||||
prevPosX = posX;
|
||||
prevPosY = posY;
|
||||
prevPosZ = posZ;
|
||||
|
||||
// Fire forward based on owner facing direction
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner != null) {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = 0; // Fire horizontally
|
||||
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
|
||||
motionY = -Math.sin(pitch) * getSpeed();
|
||||
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
|
||||
} else {
|
||||
motionX = getSpeed();
|
||||
motionY = 0;
|
||||
motionZ = 0;
|
||||
}
|
||||
startPreviewFiringDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,36 +119,7 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
|
||||
* Called by ability when windup ends.
|
||||
*/
|
||||
public void startMoving(EntityLivingBase target) {
|
||||
setCharging(false);
|
||||
|
||||
// For player casters, snap to look vector for crosshair-aligned launch
|
||||
snapToPlayerLookVector();
|
||||
|
||||
// Update start position to current position
|
||||
startX = posX;
|
||||
startY = posY;
|
||||
startZ = posZ;
|
||||
|
||||
// Calculate velocity toward target
|
||||
Entity owner = getOwnerEntity();
|
||||
|
||||
if (target != null) {
|
||||
double dx = target.posX - posX;
|
||||
double dy = (target.posY + target.getEyeHeight()) - posY;
|
||||
double dz = target.posZ - posZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len > 0) {
|
||||
motionX = (dx / len) * getSpeed();
|
||||
motionY = (dy / len) * getSpeed();
|
||||
motionZ = (dz / len) * getSpeed();
|
||||
}
|
||||
} else if (owner != null) {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
|
||||
motionY = -Math.sin(pitch) * getSpeed();
|
||||
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
|
||||
}
|
||||
startMovingTowardTargetDefault(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -248,15 +165,7 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
|
||||
}
|
||||
|
||||
// Check wall collision
|
||||
if (this.isCollidedHorizontally || this.isCollidedVertically) {
|
||||
if (!worldObj.isRemote) {
|
||||
hasHit = true;
|
||||
if (isExplosive()) {
|
||||
doExplosion();
|
||||
}
|
||||
}
|
||||
this.setDead();
|
||||
}
|
||||
handleSolidCollisionTermination();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -352,52 +261,47 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
|
||||
}
|
||||
|
||||
private void checkBlockCollision() {
|
||||
Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ);
|
||||
Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ);
|
||||
// Use full raytrace that doesn't stop at liquids and checks all blocks
|
||||
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
|
||||
|
||||
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
|
||||
if (!worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
|
||||
}
|
||||
hasHit = true;
|
||||
if (isExplosive()) {
|
||||
posX = blockHit.hitVec.xCoord;
|
||||
posY = blockHit.hitVec.yCoord;
|
||||
posZ = blockHit.hitVec.zCoord;
|
||||
doExplosion();
|
||||
}
|
||||
this.setDead();
|
||||
}
|
||||
handleBlockImpact(rayTraceBlocks(posX, posY, posZ, posX + motionX, posY + motionY, posZ + motionZ), true);
|
||||
}
|
||||
|
||||
private void checkEntityCollision() {
|
||||
// Disc hitbox: wider but thinner
|
||||
double halfWidth = discRadius * 0.5;
|
||||
double halfHeight = discThickness * 0.5;
|
||||
// Swept disc hitbox to avoid miss-through at high speed.
|
||||
double nextX = posX + motionX;
|
||||
double nextY = posY + motionY;
|
||||
double nextZ = posZ + motionZ;
|
||||
|
||||
double halfRadius = Math.max(0.05, discRadius * 0.5);
|
||||
double halfThickness = Math.max(0.03, discThickness * 0.5);
|
||||
double halfX;
|
||||
double halfY;
|
||||
double halfZ;
|
||||
|
||||
if (vertical) {
|
||||
// Vertical disc: thin axis follows horizontal travel direction, tall on Y.
|
||||
double horizLen = Math.sqrt(motionX * motionX + motionZ * motionZ);
|
||||
if (horizLen > 1.0e-5) {
|
||||
double nx = motionX / horizLen;
|
||||
double nz = motionZ / horizLen;
|
||||
halfX = Math.abs(nx) * halfThickness + Math.abs(nz) * halfRadius;
|
||||
halfZ = Math.abs(nz) * halfThickness + Math.abs(nx) * halfRadius;
|
||||
} else {
|
||||
halfX = halfRadius;
|
||||
halfZ = halfRadius;
|
||||
}
|
||||
halfY = halfRadius;
|
||||
} else {
|
||||
// Horizontal disc: wide on XZ, thin on Y.
|
||||
halfX = halfRadius;
|
||||
halfY = halfThickness;
|
||||
halfZ = halfRadius;
|
||||
}
|
||||
|
||||
AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox(
|
||||
posX - halfWidth, posY - halfHeight, posZ - halfWidth,
|
||||
posX + halfWidth, posY + halfHeight, posZ + halfWidth
|
||||
Math.min(posX, nextX) - halfX, Math.min(posY, nextY) - halfY, Math.min(posZ, nextZ) - halfZ,
|
||||
Math.max(posX, nextX) + halfX, Math.max(posY, nextY) + halfY, Math.max(posZ, nextZ) + halfZ
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (shouldIgnoreEntity(entity)) continue;
|
||||
|
||||
hasHit = true;
|
||||
|
||||
if (isExplosive()) {
|
||||
doExplosion();
|
||||
} else {
|
||||
applyDamage(entity);
|
||||
}
|
||||
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
processEntitiesInHitBox(hitBox, nextX, nextY, nextZ);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,627 @@
|
||||
package kamkeel.npcs.entity;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Energy Dome entity - a spherical barrier that blocks incoming energy projectiles.
|
||||
* Centered on the caster's position at time of casting.
|
||||
* Only blocks incoming attacks (not outgoing from allies inside).
|
||||
* Extends EntityAbilityBarrier for shared barrier logic.
|
||||
*/
|
||||
public class EntityAbilityDome extends EntityAbilityBarrier {
|
||||
|
||||
// ==================== DOME-SPECIFIC PROPERTIES ====================
|
||||
protected float domeRadius = 5.0f;
|
||||
protected float targetDomeRadius = 5.0f;
|
||||
protected boolean followCaster = false;
|
||||
|
||||
// Server-side melee detection state
|
||||
private boolean inTickMelee = false;
|
||||
private final HashSet<Integer> processedMeleeSwings = new HashSet<>();
|
||||
|
||||
public EntityAbilityDome(World world) {
|
||||
super(world);
|
||||
this.noClip = true;
|
||||
this.stepHeight = 0.0F;
|
||||
this.setSize(1.0f, 1.0f);
|
||||
}
|
||||
|
||||
public EntityAbilityDome(World world, EntityLivingBase owner, double x, double y, double z,
|
||||
float domeRadius, EnergyDisplayData display, EnergyLightningData lightning,
|
||||
EnergyBarrierData barrier) {
|
||||
this(world);
|
||||
this.noClip = true;
|
||||
this.stepHeight = 0.0F;
|
||||
this.ownerEntityId = owner.getEntityId();
|
||||
this.setDomeRadius(domeRadius);
|
||||
this.displayData = display;
|
||||
this.lightningData = lightning;
|
||||
this.barrierData = barrier;
|
||||
this.currentHealth = barrier.maxHealth;
|
||||
this.setPosition(x, y, z);
|
||||
}
|
||||
|
||||
// ==================== POSITION / BOUNDING BOX ====================
|
||||
|
||||
@Override
|
||||
protected void setSize(float width, float height) {
|
||||
super.setSize(width, height);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyEntityCollision(Entity entityIn) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate() {
|
||||
this.prevPosX = this.posX;
|
||||
this.prevPosY = this.posY;
|
||||
this.prevPosZ = this.posZ;
|
||||
|
||||
super.onUpdate();
|
||||
|
||||
// Follow caster: both sides for smooth interpolated rendering
|
||||
if (followCaster) {
|
||||
Entity owner = ownerEntityId >= 0 ? worldObj.getEntityByID(ownerEntityId) : null;
|
||||
if (owner != null) {
|
||||
this.setPosition(owner.posX, owner.posY, owner.posZ);
|
||||
// Sync prevPos with owner's prevPos for smooth interpolation
|
||||
this.prevPosX = owner.prevPosX;
|
||||
this.prevPosY = owner.prevPosY;
|
||||
this.prevPosZ = owner.prevPosZ;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle charging animation (both sides for smooth rendering)
|
||||
if (isCharging()) {
|
||||
chargeTick++;
|
||||
float progress = getChargeProgress();
|
||||
this.setDomeRadius(targetDomeRadius * progress);
|
||||
// Refresh BB for current charge radius
|
||||
this.setPosition(posX, posY, posZ);
|
||||
return; // Don't tick duration/death during charging
|
||||
}
|
||||
|
||||
ticksAlive++;
|
||||
|
||||
if (updateBarrierTick()) return;
|
||||
|
||||
// Server-side: detect and apply melee hits from nearby players.
|
||||
// Bypasses MC's processUseEntity reach check (which uses center-to-center
|
||||
// distance and fails for domes with radius > 5).
|
||||
if (!worldObj.isRemote && barrierData.meleeEnabled) {
|
||||
processMeleeHits();
|
||||
}
|
||||
|
||||
// Process entity physics every tick
|
||||
// Server: solid + knockback for all entities
|
||||
// Client: solid prediction for local player only (smooth movement blocking)
|
||||
if (barrierData.solid || barrierData.knockbackEnabled) {
|
||||
processEntityPhysics();
|
||||
}
|
||||
|
||||
// Client-side: adjust BB for accurate melee targeting at all approach angles.
|
||||
// Must run AFTER all position/BB updates and BEFORE getMouseOver() in the render phase.
|
||||
if (worldObj.isRemote && barrierData.meleeEnabled && !isCharging()) {
|
||||
adjustMeleeBoundingBox();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== INCOMING CHECK ====================
|
||||
|
||||
/**
|
||||
* Swept ray-sphere intersection test.
|
||||
* Tests if the line segment from prevPos to currPos crosses the dome sphere boundary
|
||||
* from outside. Handles fast projectiles that skip through in a single tick.
|
||||
*
|
||||
* @return true if the segment enters the sphere from outside
|
||||
*/
|
||||
private boolean isIncomingRay(
|
||||
double currX, double currY, double currZ,
|
||||
double prevX, double prevY, double prevZ,
|
||||
int projOwnerEntityId)
|
||||
{
|
||||
if (isCharging()) return false;
|
||||
if (projOwnerEntityId == this.ownerEntityId) return false;
|
||||
|
||||
// Ray: P(t) = prev + t * (curr - prev), t in [0, 1]
|
||||
double rayDirX = currX - prevX;
|
||||
double rayDirY = currY - prevY;
|
||||
double rayDirZ = currZ - prevZ;
|
||||
|
||||
// Vector from dome center to ray origin (prevPos)
|
||||
double ocX = prevX - this.posX;
|
||||
double ocY = prevY - this.posY;
|
||||
double ocZ = prevZ - this.posZ;
|
||||
|
||||
double r = domeRadius;
|
||||
|
||||
// Quadratic: a*t^2 + b*t + c = 0
|
||||
double a = rayDirX * rayDirX + rayDirY * rayDirY + rayDirZ * rayDirZ;
|
||||
double b = 2.0 * (ocX * rayDirX + ocY * rayDirY + ocZ * rayDirZ);
|
||||
double c = (ocX * ocX + ocY * ocY + ocZ * ocZ) - r * r;
|
||||
|
||||
// c > 0: prevPos is outside sphere; c <= 0: inside (don't block)
|
||||
if (c <= 0) return false;
|
||||
|
||||
if (a < 1e-10) return false; // No movement
|
||||
|
||||
double discriminant = b * b - 4.0 * a * c;
|
||||
if (discriminant < 0) return false; // Ray misses sphere entirely
|
||||
|
||||
double sqrtDisc = Math.sqrt(discriminant);
|
||||
double t1 = (-b - sqrtDisc) / (2.0 * a); // Entry point (first intersection)
|
||||
|
||||
// Entry must be within this tick's movement segment [0, 1]
|
||||
return t1 >= 0.0 && t1 <= 1.0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIncomingProjectile(EntityEnergyProjectile projectile) {
|
||||
// Faction check: don't block same-faction NPC projectiles
|
||||
Entity owner = getOwnerEntity();
|
||||
Entity projOwner = projectile.getOwnerEntity();
|
||||
if (owner instanceof noppes.npcs.entity.EntityNPCInterface && projOwner instanceof noppes.npcs.entity.EntityNPCInterface) {
|
||||
if (((noppes.npcs.entity.EntityNPCInterface) owner).faction.id == ((noppes.npcs.entity.EntityNPCInterface) projOwner).faction.id) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
double prevX = projectile.posX - projectile.motionX;
|
||||
double prevY = projectile.posY - projectile.motionY;
|
||||
double prevZ = projectile.posZ - projectile.motionZ;
|
||||
|
||||
return isIncomingRay(
|
||||
projectile.posX, projectile.posY, projectile.posZ,
|
||||
prevX, prevY, prevZ,
|
||||
projectile.getOwnerEntityId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isIncomingGenericProjectile(
|
||||
double posX, double posY, double posZ,
|
||||
double motionX, double motionY, double motionZ,
|
||||
double prevPosX, double prevPosY, double prevPosZ,
|
||||
int ownerEntityId)
|
||||
{
|
||||
return isIncomingRay(posX, posY, posZ, prevPosX, prevPosY, prevPosZ, ownerEntityId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getMaxExtent() {
|
||||
return domeRadius;
|
||||
}
|
||||
|
||||
// ==================== CHARGING ====================
|
||||
|
||||
@Override
|
||||
public void setupCharging(int duration) {
|
||||
this.targetDomeRadius = this.domeRadius;
|
||||
this.setDomeRadius(0.01f);
|
||||
this.chargeDuration = duration;
|
||||
this.chargeTick = 0;
|
||||
setCharging(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finishCharging() {
|
||||
this.setDomeRadius(this.targetDomeRadius);
|
||||
setCharging(false);
|
||||
}
|
||||
|
||||
// ==================== ENTITY PHYSICS (Solid + Knockback) ====================
|
||||
|
||||
/**
|
||||
* Processes entity physics for the dome barrier. Two independent systems:
|
||||
* <p>
|
||||
* SOLID: Hard wall — entities cannot cross the dome boundary in either direction.
|
||||
* Uses reactive crossing detection: compares current position to previous tick
|
||||
* to detect boundary crossings, then teleports back and applies velocity correction.
|
||||
* Also preemptively cancels inward velocity when near the surface.
|
||||
* <p>
|
||||
* KNOCKBACK: Repulsion force — entities near the dome surface get pushed away.
|
||||
* This is a push effect only; entities can still pass through with enough effort.
|
||||
* Does NOT prevent passthrough — use solid for that.
|
||||
* <p>
|
||||
* For moving domes (followCaster), dome velocity is accounted for by using
|
||||
* the dome's previous position for previous-tick distance calculations.
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected void processEntityPhysics() {
|
||||
float r = domeRadius;
|
||||
boolean solid = barrierData.solid;
|
||||
boolean knockback = barrierData.knockbackEnabled;
|
||||
float strength = barrierData.knockbackStrength;
|
||||
|
||||
float solidMargin = 1.5f; // Zone around surface for preemptive velocity correction
|
||||
float knockbackMargin = 2.0f; // Zone around surface for knockback push
|
||||
|
||||
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
|
||||
posX - r - knockbackMargin, posY - r - knockbackMargin, posZ - r - knockbackMargin,
|
||||
posX + r + knockbackMargin, posY + r + knockbackMargin, posZ + r + knockbackMargin
|
||||
);
|
||||
|
||||
// On client, identify local player for client-side solid prediction
|
||||
EntityPlayer localPlayer = worldObj.isRemote ? CustomNpcs.proxy.getPlayer() : null;
|
||||
|
||||
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
|
||||
for (EntityLivingBase ent : entities) {
|
||||
if (ent.getEntityId() == ownerEntityId) continue;
|
||||
|
||||
// Client-side: only process local player for solid prediction
|
||||
// Must be checked before isAllyOfOwner which uses server-only PlayerData
|
||||
if (worldObj.isRemote && (localPlayer == null || ent != localPlayer)) continue;
|
||||
|
||||
if (isAllyOfOwner(ent)) continue;
|
||||
|
||||
// Current position relative to dome center
|
||||
double dx = ent.posX - posX;
|
||||
double dy = (ent.posY + ent.height * 0.5) - posY;
|
||||
double dz = ent.posZ - posZ;
|
||||
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (dist < 0.01) continue;
|
||||
|
||||
// Normalized direction from dome center toward entity (outward)
|
||||
double nx = dx / dist;
|
||||
double ny = dy / dist;
|
||||
double nz = dz / dist;
|
||||
|
||||
boolean isInside = dist < r;
|
||||
|
||||
// --- SOLID: hard wall preventing boundary crossing ---
|
||||
if (solid) {
|
||||
// Previous tick: entity position relative to dome's previous position
|
||||
double prevDx = ent.prevPosX - prevPosX;
|
||||
double prevDy = (ent.prevPosY + ent.height * 0.5) - prevPosY;
|
||||
double prevDz = ent.prevPosZ - prevPosZ;
|
||||
double prevDist = Math.sqrt(prevDx * prevDx + prevDy * prevDy + prevDz * prevDz);
|
||||
boolean wasInside = prevDist < r;
|
||||
|
||||
// Radial velocity (positive = moving outward from dome center)
|
||||
double radialVel = ent.motionX * nx + ent.motionY * ny + ent.motionZ * nz;
|
||||
|
||||
if (wasInside != isInside) {
|
||||
// Entity crossed the boundary this tick — push back to original side
|
||||
double pushDist = wasInside ? (r - 0.3) : (r + 0.3);
|
||||
double surfaceX = posX + nx * pushDist;
|
||||
double surfaceY = posY + ny * pushDist - ent.height * 0.5;
|
||||
double surfaceZ = posZ + nz * pushDist;
|
||||
teleportEntity(ent, surfaceX, surfaceY, surfaceZ);
|
||||
|
||||
// Cancel radial velocity and add corrective push
|
||||
ent.motionX -= radialVel * nx;
|
||||
ent.motionY -= radialVel * ny;
|
||||
ent.motionZ -= radialVel * nz;
|
||||
double pushForce = wasInside ? -0.15 : 0.15;
|
||||
ent.motionX += nx * pushForce;
|
||||
ent.motionZ += nz * pushForce;
|
||||
ent.velocityChanged = true;
|
||||
} else {
|
||||
// Preemptive: near surface and moving toward it — cancel radial velocity
|
||||
double surfaceDist = Math.abs(dist - r);
|
||||
if (surfaceDist < solidMargin) {
|
||||
boolean movingTowardSurface = (!isInside && radialVel < -0.01)
|
||||
|| (isInside && radialVel > 0.01);
|
||||
if (movingTowardSurface) {
|
||||
ent.motionX -= radialVel * nx;
|
||||
ent.motionY -= radialVel * ny;
|
||||
ent.motionZ -= radialVel * nz;
|
||||
double pushForce = isInside ? -0.05 : 0.05;
|
||||
ent.motionX += nx * pushForce;
|
||||
ent.motionZ += nz * pushForce;
|
||||
ent.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- KNOCKBACK: repulsion push (server only, synced via velocityChanged) ---
|
||||
if (!worldObj.isRemote && knockback) {
|
||||
double surfaceDist = Math.abs(dist - r);
|
||||
if (surfaceDist < knockbackMargin) {
|
||||
double proximity = 1.0 - (surfaceDist / knockbackMargin);
|
||||
double force = proximity * strength * 0.06;
|
||||
|
||||
if (isInside) {
|
||||
// Push toward center (away from surface)
|
||||
ent.motionX += -nx * force;
|
||||
ent.motionY += -ny * force * 0.5;
|
||||
ent.motionZ += -nz * force;
|
||||
} else {
|
||||
// Push outward (away from surface)
|
||||
ent.motionX += nx * force;
|
||||
ent.motionY += ny * force * 0.5;
|
||||
ent.motionZ += nz * force;
|
||||
}
|
||||
ent.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== CONTAINMENT CHECK ====================
|
||||
|
||||
@Override
|
||||
public boolean isEntityInside(Entity entity) {
|
||||
if (entity == null) return false;
|
||||
double dx = entity.posX - posX;
|
||||
double dy = (entity.posY + entity.height * 0.5) - posY;
|
||||
double dz = entity.posZ - posZ;
|
||||
return dx * dx + dy * dy + dz * dz < domeRadius * domeRadius;
|
||||
}
|
||||
|
||||
// ==================== MELEE (spherical check) ====================
|
||||
|
||||
/**
|
||||
* Controls whether MC's entity picking (crosshair raycast) can target this dome.
|
||||
* Returns false when melee is disabled or the dome is still charging.
|
||||
* <p>
|
||||
* Inside players CAN target the dome because adjustMeleeBoundingBox places a small BB
|
||||
* at the nearest surface point, allowing mobs in other directions to still be targeted.
|
||||
*/
|
||||
@Override
|
||||
public boolean canBeCollidedWith() {
|
||||
if (!barrierData.meleeEnabled) return false;
|
||||
if (isCharging()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side: Repositions the bounding box to a small cube at the nearest sphere surface
|
||||
* point toward the player. This fixes melee targeting at diagonal approach angles where the
|
||||
* player would otherwise be inside the dome-sized cubic BB but outside the actual sphere,
|
||||
* causing MC's AABB raycast to return the far exit point (beyond reach distance).
|
||||
* <p>
|
||||
* With a small BB (2x2x2) at the surface point, even if the player is inside it at close
|
||||
* range, the exit point is at most ~1.7 blocks away — well within melee reach.
|
||||
*/
|
||||
private void adjustMeleeBoundingBox() {
|
||||
EntityPlayer player = CustomNpcs.proxy.getPlayer();
|
||||
if (player == null) {
|
||||
updateBoundingBox();
|
||||
return;
|
||||
}
|
||||
|
||||
double dx = player.posX - posX;
|
||||
double dy = (player.posY + player.getEyeHeight()) - posY;
|
||||
double dz = player.posZ - posZ;
|
||||
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
// Only adjust when player is within reasonable melee range of dome surface
|
||||
if (dist < 0.01 || dist > domeRadius + 6.0) {
|
||||
updateBoundingBox();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nearest point on sphere surface toward player
|
||||
double nx = dx / dist;
|
||||
double ny = dy / dist;
|
||||
double nz = dz / dist;
|
||||
double surfX = posX + nx * domeRadius;
|
||||
double surfY = posY + ny * domeRadius;
|
||||
double surfZ = posZ + nz * domeRadius;
|
||||
|
||||
float s = 1.0f;
|
||||
this.boundingBox.setBounds(
|
||||
surfX - s, surfY - s, surfZ - s,
|
||||
surfX + s, surfY + s, surfZ + s
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Player melee is handled by processMeleeHits (server-side tick detection) to bypass
|
||||
* MC's processUseEntity reach check which uses center-to-center distance.
|
||||
* Direct player melee ("player" damage type) is rejected unless inTickMelee is set,
|
||||
* preventing double-hits when both the C02 path and tick detection fire.
|
||||
* Non-player melee (mobs) and all other damage sources pass through with sphere validation.
|
||||
*/
|
||||
@Override
|
||||
public boolean attackEntityFrom(DamageSource source, float amount) {
|
||||
// Player melee: only allow from tick-based detection
|
||||
if (!inTickMelee && "player".equals(source.damageType)) {
|
||||
return false;
|
||||
}
|
||||
if (source.getEntity() != null) {
|
||||
Entity attacker = source.getEntity();
|
||||
double dx = attacker.posX - this.posX;
|
||||
double dy = (attacker.posY + attacker.height * 0.5) - this.posY;
|
||||
double dz = attacker.posZ - this.posZ;
|
||||
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
double surfaceDist = Math.abs(dist - domeRadius);
|
||||
if (surfaceDist > 5.0) return false;
|
||||
}
|
||||
return super.attackEntityFrom(source, amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side melee detection. Each tick, finds players near the dome surface who are
|
||||
* swinging and looking at the dome, then applies the attack via attackTargetEntityWithCurrentItem
|
||||
* (preserving enchantments, crits, fire aspect, etc.). This bypasses MC's processUseEntity
|
||||
* reach check which uses center-to-center distance and fails for large domes.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void processMeleeHits() {
|
||||
float r = domeRadius;
|
||||
float maxReach = 6.0f;
|
||||
|
||||
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
|
||||
posX - r - maxReach, posY - r - maxReach, posZ - r - maxReach,
|
||||
posX + r + maxReach, posY + r + maxReach, posZ + r + maxReach
|
||||
);
|
||||
|
||||
List<EntityPlayer> players = worldObj.getEntitiesWithinAABB(EntityPlayer.class, searchBox);
|
||||
for (EntityPlayer player : players) {
|
||||
if (player.getEntityId() == ownerEntityId) continue;
|
||||
if (isAllyOfOwner(player)) continue;
|
||||
|
||||
int playerId = player.getEntityId();
|
||||
|
||||
if (!player.isSwingInProgress) {
|
||||
processedMeleeSwings.remove(playerId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (processedMeleeSwings.contains(playerId)) continue;
|
||||
|
||||
// Check surface distance
|
||||
double dx = player.posX - posX;
|
||||
double dy = (player.posY + 1.62) - posY;
|
||||
double dz = player.posZ - posZ;
|
||||
double distSq = dx * dx + dy * dy + dz * dz;
|
||||
double dist = Math.sqrt(distSq);
|
||||
double surfaceDist = Math.abs(dist - r);
|
||||
if (surfaceDist > maxReach) continue;
|
||||
|
||||
// Ray-sphere intersection: check if look direction hits the dome
|
||||
Vec3 look = player.getLook(1.0f);
|
||||
double b = 2.0 * (dx * look.xCoord + dy * look.yCoord + dz * look.zCoord);
|
||||
double c = distSq - (double)(r * r);
|
||||
double discriminant = b * b - 4.0 * c;
|
||||
if (discriminant < 0) continue;
|
||||
|
||||
double sqrtDisc = Math.sqrt(discriminant);
|
||||
double t1 = (-b - sqrtDisc) / 2.0;
|
||||
double t2 = (-b + sqrtDisc) / 2.0;
|
||||
|
||||
// Outside: t1 is entry distance. Inside: t1 < 0, t2 is exit distance.
|
||||
double hitDist = t1 >= 0 ? t1 : t2;
|
||||
if (hitDist < 0 || hitDist > maxReach) continue;
|
||||
|
||||
// Apply the attack with full MC damage calculation
|
||||
processedMeleeSwings.add(playerId);
|
||||
inTickMelee = true;
|
||||
player.attackTargetEntityWithCurrentItem(this);
|
||||
inTickMelee = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== BOUNDING BOX ====================
|
||||
|
||||
/**
|
||||
* No expansion needed — the bounding box is already dome-sized.
|
||||
* Returning 0 ensures players outside the dome sphere are also outside the
|
||||
* ray-testing BB, so MC's raycast returns the near-face hit (within reach)
|
||||
* rather than the far exit point (beyond reach).
|
||||
*/
|
||||
@Override
|
||||
public float getCollisionBorderSize() {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public boolean isInRangeToRenderDist(double distance) {
|
||||
double d = domeRadius * 8.0D;
|
||||
d *= 64.0D;
|
||||
return distance < d * d;
|
||||
}
|
||||
|
||||
// ==================== DISTANCE (for render sorting) ====================
|
||||
|
||||
/**
|
||||
* Returns squared distance from the given point to the nearest point on the dome sphere surface.
|
||||
* Uses absolute distance to surface: |centerDist - radius|.
|
||||
* <p>
|
||||
* Inside: nearest surface is (radius - centerDist) away.
|
||||
* Outside: nearest surface is (centerDist - radius) away.
|
||||
* This ensures entities inside the dome sort correctly relative to the dome shell.
|
||||
*/
|
||||
@Override
|
||||
public double getDistanceSq(double x, double y, double z) {
|
||||
double dx = this.posX - x;
|
||||
double dy = this.posY - y;
|
||||
double dz = this.posZ - z;
|
||||
double centerDist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
double surfaceDist = Math.abs(centerDist - domeRadius);
|
||||
return surfaceDist * surfaceDist;
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public float getDomeRadius() {
|
||||
return domeRadius;
|
||||
}
|
||||
|
||||
public void setDomeRadius(float radius) {
|
||||
this.domeRadius = Math.max(0.1f, radius);
|
||||
this.setPosition(this.posX, this.posY, this.posZ);
|
||||
}
|
||||
|
||||
public boolean isFollowCaster() {
|
||||
return followCaster;
|
||||
}
|
||||
|
||||
public void setFollowCaster(boolean follow) {
|
||||
this.followCaster = follow;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void setPositionAndRotation2(double x, double y, double z, float yaw, float pitch, int increments) {
|
||||
// Don't do the vanilla "push up out of blocks" logic.
|
||||
this.setPosition(x, y, z);
|
||||
this.setRotation(yaw, pitch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPosition(double x, double y, double z) {
|
||||
this.posX = x;
|
||||
this.posY = y;
|
||||
this.posZ = z;
|
||||
updateBoundingBox();
|
||||
}
|
||||
|
||||
protected void updateBoundingBox() {
|
||||
float r = this.domeRadius;
|
||||
this.boundingBox.setBounds(
|
||||
this.posX - r, this.posY - r, this.posZ - r,
|
||||
this.posX + r, this.posY + r, this.posZ + r
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== NBT ====================
|
||||
|
||||
@Override
|
||||
protected void readEntityFromNBT(NBTTagCompound nbt) {
|
||||
// Intentionally empty — ability entities are transient (not saved to world)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeEntityToNBT(NBTTagCompound nbt) {
|
||||
// Intentionally empty — ability entities are transient (not saved to world)
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeSpawnNBT(NBTTagCompound nbt) {
|
||||
writeBarrierBaseNBT(nbt);
|
||||
nbt.setFloat("DomeRadius", domeRadius);
|
||||
nbt.setFloat("TargetDomeRadius", targetDomeRadius);
|
||||
nbt.setBoolean("FollowCaster", followCaster);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readSpawnNBT(NBTTagCompound nbt) {
|
||||
readBarrierBaseNBT(nbt);
|
||||
this.setDomeRadius(sanitize(nbt.getFloat("DomeRadius"), 5.0f, MAX_ENTITY_RADIUS));
|
||||
this.targetDomeRadius = sanitize(nbt.hasKey("TargetDomeRadius") ? nbt.getFloat("TargetDomeRadius") : domeRadius, 5.0f, MAX_ENTITY_RADIUS);
|
||||
this.followCaster = nbt.hasKey("FollowCaster") && nbt.getBoolean("FollowCaster");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package kamkeel.npcs.entity;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyTrajectoryData;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyTrajectoryData;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
@@ -16,12 +16,10 @@ import net.minecraft.world.World;
|
||||
import noppes.npcs.EventHooks;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Laser projectile - fast expanding thin line that pierces through multiple targets.
|
||||
* Laser projectile - fast expanding thin line.
|
||||
* No homing, travels in a straight line from origin to max distance.
|
||||
* <p>
|
||||
* Design inspired by LouisXIV's energy attack system.
|
||||
@@ -43,12 +41,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
|
||||
// Lock vertical direction after firing (only update yaw, keep pitch fixed)
|
||||
private boolean lockVerticalDirection = false;
|
||||
|
||||
// Whether to die on first entity impact (hit-scan mode)
|
||||
private boolean dieOnImpact = false;
|
||||
|
||||
// Track hit entities to avoid double-damage
|
||||
private Set<Integer> hitEntities = new HashSet<>();
|
||||
private int reflectedLockTicks = 0;
|
||||
|
||||
// End point for rendering
|
||||
private double endX, endY, endZ;
|
||||
@@ -89,12 +82,19 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
this.dirZ = dz / len;
|
||||
}
|
||||
} else {
|
||||
// Fire in NPC's facing direction
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
this.dirX = -Math.sin(yaw) * Math.cos(pitch);
|
||||
this.dirY = -Math.sin(pitch);
|
||||
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
|
||||
// Fall back to owner look direction so launch matches visual aim.
|
||||
Vec3 look = owner == null ? null : owner.getLookVec();
|
||||
if (look != null) {
|
||||
this.dirX = look.xCoord;
|
||||
this.dirY = look.yCoord;
|
||||
this.dirZ = look.zCoord;
|
||||
} else {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
this.dirX = -Math.sin(yaw) * Math.cos(pitch);
|
||||
this.dirY = -Math.sin(pitch);
|
||||
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize end point
|
||||
@@ -121,9 +121,14 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
return;
|
||||
}
|
||||
|
||||
// Track owner's anchor position and rotation each tick
|
||||
// so the laser follows the NPC's facing direction
|
||||
updateLaserOriginAndDirection();
|
||||
// After reflection, hold trajectory briefly so the rebound is visible.
|
||||
if (reflectedLockTicks > 0) {
|
||||
reflectedLockTicks--;
|
||||
} else {
|
||||
// Track owner's anchor position and rotation each tick
|
||||
// so the laser follows the NPC's facing direction
|
||||
updateLaserOriginAndDirection();
|
||||
}
|
||||
|
||||
if (!fullyExtended) {
|
||||
// Expand the laser
|
||||
@@ -142,6 +147,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
// Check for block collision along the new segment
|
||||
if (!worldObj.isRemote) {
|
||||
checkBlockCollision();
|
||||
// Re-evaluate barriers against the current beam segment before entity hits.
|
||||
// This prevents first-tick pass-through where the pre-update barrier check
|
||||
// still sees a zero-length beam.
|
||||
if (checkBarrierCollisionForCurrentSegment()) {
|
||||
return;
|
||||
}
|
||||
checkEntityCollisionAlongLine();
|
||||
}
|
||||
} else {
|
||||
@@ -163,75 +174,84 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update laser origin and direction from the owner's current anchor position and rotation.
|
||||
* Called each tick so the laser follows the NPC when tracking a target.
|
||||
* Update laser origin and direction from the owner's current look rotation.
|
||||
* Called each tick so the laser origin stays centered on look vector.
|
||||
*/
|
||||
private void updateLaserOriginAndDirection() {
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner == null || !(owner instanceof EntityLivingBase)) return;
|
||||
|
||||
EntityLivingBase livingOwner = (EntityLivingBase) owner;
|
||||
|
||||
// Update origin from anchor point
|
||||
if (anchorData != null) {
|
||||
Vec3 pos = AnchorPointHelper.calculateAnchorPosition(livingOwner, anchorData);
|
||||
startX = pos.xCoord;
|
||||
startY = pos.yCoord;
|
||||
startZ = pos.zCoord;
|
||||
} else {
|
||||
startX = owner.posX;
|
||||
startY = owner.posY + owner.height * 0.7;
|
||||
startZ = owner.posZ;
|
||||
}
|
||||
|
||||
// Update direction from owner's current rotation
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
if (lockVerticalDirection) {
|
||||
// Only update horizontal direction (yaw), keep vertical (pitch) fixed
|
||||
double horizontalLen = Math.sqrt(dirX * dirX + dirZ * dirZ);
|
||||
if (horizontalLen < 0.001) horizontalLen = 1.0;
|
||||
dirX = -Math.sin(yaw) * horizontalLen;
|
||||
dirZ = Math.cos(yaw) * horizontalLen;
|
||||
} else {
|
||||
// Use owner look vector (head/eye aim) for direction updates so hit logic matches visuals.
|
||||
Vec3 look = livingOwner.getLookVec();
|
||||
if (look == null) {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
dirX = -Math.sin(yaw) * Math.cos(pitch);
|
||||
dirY = -Math.sin(pitch);
|
||||
dirZ = Math.cos(yaw) * Math.cos(pitch);
|
||||
look = Vec3.createVectorHelper(
|
||||
-Math.sin(yaw) * Math.cos(pitch),
|
||||
-Math.sin(pitch),
|
||||
Math.cos(yaw) * Math.cos(pitch)
|
||||
);
|
||||
}
|
||||
|
||||
// Keep entity positioned at origin
|
||||
prevPosX = startX;
|
||||
prevPosY = startY;
|
||||
prevPosZ = startZ;
|
||||
setPosition(startX, startY, startZ);
|
||||
if (lockVerticalDirection) {
|
||||
// Keep current vertical component, but align horizontal direction to current look.
|
||||
double lookHorizontalLen = Math.sqrt(look.xCoord * look.xCoord + look.zCoord * look.zCoord);
|
||||
if (lookHorizontalLen > 0.0001) {
|
||||
double currentHorizontalLen = Math.sqrt(dirX * dirX + dirZ * dirZ);
|
||||
if (currentHorizontalLen < 0.0001) {
|
||||
currentHorizontalLen = Math.sqrt(Math.max(0.0001, 1.0 - dirY * dirY));
|
||||
}
|
||||
dirX = (look.xCoord / lookHorizontalLen) * currentHorizontalLen;
|
||||
dirZ = (look.zCoord / lookHorizontalLen) * currentHorizontalLen;
|
||||
}
|
||||
} else {
|
||||
dirX = look.xCoord;
|
||||
dirY = look.yCoord;
|
||||
dirZ = look.zCoord;
|
||||
}
|
||||
|
||||
// Keep direction normalized for consistent expansion and collision math.
|
||||
Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ);
|
||||
double dirLen = Math.sqrt(direction.xCoord * direction.xCoord + direction.yCoord * direction.yCoord + direction.zCoord * direction.zCoord);
|
||||
if (dirLen > 0.0001) {
|
||||
direction = Vec3.createVectorHelper(
|
||||
direction.xCoord / dirLen,
|
||||
direction.yCoord / dirLen,
|
||||
direction.zCoord / dirLen
|
||||
);
|
||||
} else {
|
||||
direction = look != null ? look : Vec3.createVectorHelper(1.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
dirX = direction.xCoord;
|
||||
dirY = direction.yCoord;
|
||||
dirZ = direction.zCoord;
|
||||
|
||||
// Keep origin centered on look vector while active and clear owner bbox.
|
||||
setLookVectorLaunchPosition(livingOwner, direction, false);
|
||||
syncStartPositionToCurrent();
|
||||
syncPositionStateToCurrent(false);
|
||||
}
|
||||
|
||||
public void startMoving(EntityLivingBase target) {
|
||||
setCharging(false);
|
||||
beginLookVectorLaunch(false);
|
||||
|
||||
// Initialize from current anchor position
|
||||
// updateLaserOriginAndDirection() will keep these updated each tick
|
||||
startX = posX;
|
||||
startY = posY;
|
||||
startZ = posZ;
|
||||
currentLength = 0.0f;
|
||||
fullyExtended = false;
|
||||
ticksSinceFullExtension = 0;
|
||||
|
||||
// Set initial direction from owner's rotation
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner != null) {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
this.dirX = -Math.sin(yaw) * Math.cos(pitch);
|
||||
this.dirY = -Math.sin(pitch);
|
||||
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
|
||||
} else if (target != null) {
|
||||
double dx = target.posX - startX;
|
||||
double dy = (target.posY + target.getEyeHeight() - 0.4) - startY;
|
||||
double dz = target.posZ - startZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len > 0) {
|
||||
this.dirX = dx / len;
|
||||
this.dirY = dy / len;
|
||||
this.dirZ = dz / len;
|
||||
// Set initial direction: target-based when available, otherwise owner look vector.
|
||||
Vec3 look = getOwnerLookVector();
|
||||
if (!setDirectionTowardTarget(target, startX, startY, startZ)) {
|
||||
if (look != null) {
|
||||
dirX = look.xCoord;
|
||||
dirY = look.yCoord;
|
||||
dirZ = look.zCoord;
|
||||
} else {
|
||||
dirX = 1.0;
|
||||
dirY = 0.0;
|
||||
dirZ = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,11 +261,22 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
this.endZ = startZ;
|
||||
}
|
||||
|
||||
private boolean setDirectionTowardTarget(EntityLivingBase target, double sourceX, double sourceY, double sourceZ) {
|
||||
if (target == null) return false;
|
||||
double dx = target.posX - sourceX;
|
||||
double dy = (target.posY + target.getEyeHeight() - 0.4) - sourceY;
|
||||
double dz = target.posZ - sourceZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len <= 0.0001) return false;
|
||||
|
||||
dirX = dx / len;
|
||||
dirY = dy / len;
|
||||
dirZ = dz / len;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void checkBlockCollision() {
|
||||
Vec3 start = Vec3.createVectorHelper(startX, startY, startZ);
|
||||
Vec3 end = Vec3.createVectorHelper(endX, endY, endZ);
|
||||
// Use full raytrace that doesn't stop at liquids and checks all blocks
|
||||
MovingObjectPosition blockHit = worldObj.func_147447_a(start, end, false, true, false);
|
||||
MovingObjectPosition blockHit = rayTraceBlocks(startX, startY, startZ, endX, endY, endZ);
|
||||
|
||||
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
|
||||
if (!worldObj.isRemote) {
|
||||
@@ -295,15 +326,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (shouldIgnoreEntity(entity)) continue;
|
||||
if (hitEntities.contains(entity.getEntityId())) continue;
|
||||
if (!canHitEntityNow(entity)) continue;
|
||||
|
||||
// Check if entity's bounding box intersects with the laser line
|
||||
if (isEntityOnLine(entity)) {
|
||||
hitEntities.add(entity.getEntityId());
|
||||
applyDamage(entity);
|
||||
|
||||
if (dieOnImpact) {
|
||||
// Stop at impact point and begin linger/fade
|
||||
if (processEntityHit(entity, entity.posX, entity.posY + entity.height * 0.5, entity.posZ)) {
|
||||
// Stop at impact point and terminate
|
||||
double dx = entity.posX - startX;
|
||||
double dy = (entity.posY + entity.height * 0.5) - startY;
|
||||
double dz = entity.posZ - startZ;
|
||||
@@ -312,62 +340,34 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
endX = startX + dirX * currentLength;
|
||||
endY = startY + dirY * currentLength;
|
||||
endZ = startZ + dirZ * currentLength;
|
||||
fullyExtended = true;
|
||||
return; // Stop checking further entities
|
||||
return;
|
||||
}
|
||||
|
||||
// Piercing - don't stop, continue to next entity
|
||||
// PIERCE/MULTI continue through remaining entities.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity's bounding box intersects with the laser line.
|
||||
* Uses separate XZ (horizontal) and Y (vertical) checks against
|
||||
* the entity's full bounding box rather than just its center point.
|
||||
* Uses a swept segment vs expanded AABB intercept test for reliable hits.
|
||||
*/
|
||||
private boolean isEntityOnLine(EntityLivingBase entity) {
|
||||
double ex = entity.posX;
|
||||
double ez = entity.posZ;
|
||||
AxisAlignedBB bb = entity.boundingBox;
|
||||
if (bb == null) return false;
|
||||
|
||||
// Project entity XZ position onto the laser line direction
|
||||
double vx = ex - startX;
|
||||
double vz = ez - startZ;
|
||||
// Project using horizontal direction components only
|
||||
double dirLenXZ = Math.sqrt(dirX * dirX + dirZ * dirZ);
|
||||
double dot;
|
||||
if (dirLenXZ > 0.001) {
|
||||
dot = (vx * dirX + vz * dirZ) / (dirLenXZ * dirLenXZ) * dirLenXZ;
|
||||
// Remap to full 3D parameter
|
||||
dot = (vx * dirX + vz * dirZ + (entity.posY + entity.height * 0.5 - startY) * dirY);
|
||||
} else {
|
||||
// Laser fires nearly straight up/down - use full 3D projection
|
||||
dot = vx * dirX + (entity.posY + entity.height * 0.5 - startY) * dirY + vz * dirZ;
|
||||
}
|
||||
dot = Math.max(0, Math.min(dot, currentLength));
|
||||
// Minimum effective width keeps thin lasers from visually clipping targets without hitting.
|
||||
double effectiveWidth = Math.max(laserWidth, 0.65);
|
||||
double expand = effectiveWidth * 0.5;
|
||||
AxisAlignedBB expanded = bb.expand(expand, expand, expand);
|
||||
|
||||
// Closest point on laser line at this parameter
|
||||
double closestX = startX + dirX * dot;
|
||||
double closestY = startY + dirY * dot;
|
||||
double closestZ = startZ + dirZ * dot;
|
||||
Vec3 segmentStart = Vec3.createVectorHelper(startX, startY, startZ);
|
||||
Vec3 segmentEnd = Vec3.createVectorHelper(endX, endY, endZ);
|
||||
MovingObjectPosition intercept = expanded.calculateIntercept(segmentStart, segmentEnd);
|
||||
if (intercept != null) return true;
|
||||
|
||||
// XZ distance check: entity center vs laser line point
|
||||
// Use minimum collision width so visually thin lasers still hit reliably
|
||||
double xzDistSq = (ex - closestX) * (ex - closestX) + (ez - closestZ) * (ez - closestZ);
|
||||
double effectiveWidth = Math.max(laserWidth, 0.5);
|
||||
double hitRadiusXZ = effectiveWidth * 0.5 + entity.width * 0.5;
|
||||
if (xzDistSq > hitRadiusXZ * hitRadiusXZ) return false;
|
||||
|
||||
// Y overlap check: laser point vs entity's full height range
|
||||
double entityMinY = entity.boundingBox.minY;
|
||||
double entityMaxY = entity.boundingBox.maxY;
|
||||
double laserHalfWidth = effectiveWidth * 0.5;
|
||||
|
||||
// Check if laser Y is within entity's height range (expanded by laser half-width)
|
||||
if (closestY < entityMinY - laserHalfWidth) return false;
|
||||
if (closestY > entityMaxY + laserHalfWidth) return false;
|
||||
|
||||
return true;
|
||||
// Handle edge case where the segment starts or ends already inside the target volume.
|
||||
return expanded.isVecInside(segmentStart) || expanded.isVecInside(segmentEnd);
|
||||
}
|
||||
|
||||
// ==================== BARRIER COLLISION ====================
|
||||
@@ -380,10 +380,20 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected boolean checkBarrierCollision() {
|
||||
if (currentLength <= 0 || fullyExtended) return false;
|
||||
return checkBarrierCollisionInternal(false);
|
||||
}
|
||||
|
||||
List<EntityEnergyBarrier> barriers = EntityEnergyBarrier.getActiveBarriers(worldObj);
|
||||
for (EntityEnergyBarrier barrier : barriers) {
|
||||
private boolean checkBarrierCollisionForCurrentSegment() {
|
||||
return checkBarrierCollisionInternal(true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean checkBarrierCollisionInternal(boolean allowWhenFullyExtended) {
|
||||
if (currentLength <= 0) return false;
|
||||
if (fullyExtended && !allowWhenFullyExtended) return false;
|
||||
|
||||
List<EntityAbilityBarrier> barriers = EntityAbilityBarrier.getActiveBarriers(worldObj);
|
||||
for (EntityAbilityBarrier barrier : barriers) {
|
||||
if (barrier.isDead || barrier.isCharging()) continue;
|
||||
if (barrier.getOwnerEntityId() == this.ownerEntityId) continue;
|
||||
|
||||
@@ -396,42 +406,157 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
}
|
||||
}
|
||||
|
||||
if (barrier instanceof EntityEnergyDome) {
|
||||
EntityEnergyDome dome = (EntityEnergyDome) barrier;
|
||||
if (barrier instanceof EntityAbilityDome) {
|
||||
EntityAbilityDome dome = (EntityAbilityDome) barrier;
|
||||
float intersectDist = getLineSphereIntersection(dome);
|
||||
if (intersectDist >= 0) {
|
||||
// Truncate laser at dome surface
|
||||
currentLength = intersectDist;
|
||||
endX = startX + dirX * currentLength;
|
||||
endY = startY + dirY * currentLength;
|
||||
endZ = startZ + dirZ * currentLength;
|
||||
fullyExtended = true;
|
||||
float damage = getModifiedDamage();
|
||||
EntityAbilityBarrier.ProjectileHitOutcome hitOutcome = dome.onProjectileHitResolved(this, damage);
|
||||
if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float damage = getModifiedDamage();
|
||||
dome.onProjectileHit(this, damage);
|
||||
return false; // Don't kill laser — it's truncated and will linger
|
||||
}
|
||||
} else {
|
||||
// Non-dome barriers: fall through to standard check
|
||||
if (barrier.isIncomingProjectile(this)) {
|
||||
float damage = getModifiedDamage();
|
||||
if (barrier.onProjectileHit(this, damage)) {
|
||||
hasHit = true;
|
||||
this.setDead();
|
||||
if (hitOutcome.result != EntityAbilityBarrier.ProjectileHitResult.BROKEN) {
|
||||
currentLength = Math.max(0.0f, intersectDist);
|
||||
endX = startX + dirX * currentLength;
|
||||
endY = startY + dirY * currentLength;
|
||||
endZ = startZ + dirZ * currentLength;
|
||||
}
|
||||
|
||||
if (handleBarrierHitOutcome(dome, hitOutcome)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// For line-based lasers, use a swept segment against generic barrier checks.
|
||||
// Panel/Dome logic expects previous+current positions and motion; map those
|
||||
// to [start -> end] each tick.
|
||||
double segMotionX = endX - startX;
|
||||
double segMotionY = endY - startY;
|
||||
double segMotionZ = endZ - startZ;
|
||||
boolean incoming = barrier.isIncomingGenericProjectile(
|
||||
endX, endY, endZ,
|
||||
segMotionX, segMotionY, segMotionZ,
|
||||
startX, startY, startZ,
|
||||
this.ownerEntityId
|
||||
);
|
||||
|
||||
if (incoming) {
|
||||
float damage = getModifiedDamage();
|
||||
EntityAbilityBarrier.ProjectileHitOutcome hitOutcome = barrier.onProjectileHitResolved(this, damage);
|
||||
if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hitOutcome.result != EntityAbilityBarrier.ProjectileHitResult.BROKEN) {
|
||||
// Keep current segment length at the exact collision tick position.
|
||||
endX = startX + dirX * currentLength;
|
||||
endY = startY + dirY * currentLength;
|
||||
endZ = startZ + dirZ * currentLength;
|
||||
}
|
||||
|
||||
if (handleBarrierHitOutcome(barrier, hitOutcome)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean reflectFromBarrier(EntityAbilityBarrier barrier, float reflectStrengthPct) {
|
||||
if (barrier == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double vx = dirX;
|
||||
double vy = dirY;
|
||||
double vz = dirZ;
|
||||
double vLenSq = vx * vx + vy * vy + vz * vz;
|
||||
if (vLenSq < 1.0e-8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double[] normal = getBarrierImpactNormal(barrier, vx, vy, vz);
|
||||
if (normal == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double dot = vx * normal[0] + vy * normal[1] + vz * normal[2];
|
||||
if (dot > 0.0) {
|
||||
normal[0] = -normal[0];
|
||||
normal[1] = -normal[1];
|
||||
normal[2] = -normal[2];
|
||||
dot = vx * normal[0] + vy * normal[1] + vz * normal[2];
|
||||
}
|
||||
|
||||
double rx = vx - 2.0 * dot * normal[0];
|
||||
double ry = vy - 2.0 * dot * normal[1];
|
||||
double rz = vz - 2.0 * dot * normal[2];
|
||||
if (ry < -0.06) {
|
||||
ry = Math.max(0.08, Math.abs(ry) * 0.35);
|
||||
}
|
||||
double rLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
||||
if (rLen < 1.0e-8) {
|
||||
rx = -vx;
|
||||
ry = -vy;
|
||||
rz = -vz;
|
||||
rLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
||||
if (rLen < 1.0e-8) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
dirX = rx / rLen;
|
||||
dirY = ry / rLen;
|
||||
dirZ = rz / rLen;
|
||||
|
||||
// Start a fresh segment at the impact edge to make the rebound visible.
|
||||
startX = endX;
|
||||
startY = endY;
|
||||
startZ = endZ;
|
||||
setPosition(startX, startY, startZ);
|
||||
syncPositionStateToCurrent(true);
|
||||
|
||||
currentLength = 0.0f;
|
||||
fullyExtended = false;
|
||||
ticksSinceFullExtension = 0;
|
||||
endX = startX;
|
||||
endY = startY;
|
||||
endZ = startZ;
|
||||
|
||||
reflectedLockTicks = Math.max(reflectedLockTicks, 10);
|
||||
barrierImpactPauseTicks = 0;
|
||||
barrierImpactDestroyOnResume = false;
|
||||
|
||||
Entity barrierOwner = barrier.getOwnerEntity();
|
||||
if (barrierOwner != null) {
|
||||
setOwnerEntityId(barrierOwner.getEntityId());
|
||||
trackProjectile(this);
|
||||
}
|
||||
setTargetEntityId(-1);
|
||||
|
||||
setInnerColor(barrier.getInnerColor());
|
||||
setOuterColor(barrier.getOuterColor());
|
||||
|
||||
float clampedStrength = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
|
||||
float reducedDamage = getDamage() * (1.0f - clampedStrength / 100.0f);
|
||||
setCombatDamage(Math.max(0.0f, reducedDamage));
|
||||
|
||||
hitOnceEntities.clear();
|
||||
lastHitTickByEntity.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Line-sphere intersection between the laser line segment and a dome.
|
||||
* Returns the distance along the laser direction to the first entry point,
|
||||
* or -1 if no intersection from outside.
|
||||
*/
|
||||
private float getLineSphereIntersection(EntityEnergyDome dome) {
|
||||
private float getLineSphereIntersection(EntityAbilityDome dome) {
|
||||
double cx = dome.posX;
|
||||
double cy = dome.posY;
|
||||
double cz = dome.posZ;
|
||||
@@ -497,14 +622,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
this.lingerTicks = ticks;
|
||||
}
|
||||
|
||||
public boolean isDieOnImpact() {
|
||||
return dieOnImpact;
|
||||
}
|
||||
|
||||
public void setDieOnImpact(boolean dieOnImpact) {
|
||||
this.dieOnImpact = dieOnImpact;
|
||||
}
|
||||
|
||||
public void setDirection(double x, double y, double z) {
|
||||
this.dirX = x;
|
||||
this.dirY = y;
|
||||
@@ -564,6 +681,24 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
return Math.max(0.0f, 1.0f - ((float) ticksSinceFullExtension / lingerTicks));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected float getLaunchClearanceRadius() {
|
||||
return Math.max(0.1f, laserWidth * 0.5f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
|
||||
// Ensure charging visuals grow from a small orb based on laser width.
|
||||
this.size = laserWidth;
|
||||
this.currentLength = 0.0f;
|
||||
this.fullyExtended = false;
|
||||
this.ticksSinceFullExtension = 0;
|
||||
super.setupCharging(anchor, chargeDuration);
|
||||
this.endX = posX;
|
||||
this.endY = posY;
|
||||
this.endZ = posZ;
|
||||
}
|
||||
|
||||
// ==================== NBT ====================
|
||||
|
||||
@Override
|
||||
@@ -573,7 +708,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
if (Float.isNaN(expansionSpeed) || Float.isInfinite(expansionSpeed) || expansionSpeed <= 0) expansionSpeed = 2.0f;
|
||||
this.lingerTicks = nbt.hasKey("LingerTicks") ? nbt.getInteger("LingerTicks") : 10;
|
||||
if (lingerTicks <= 0) lingerTicks = 1;
|
||||
this.dieOnImpact = nbt.getBoolean("DieOnImpact");
|
||||
this.lockVerticalDirection = nbt.getBoolean("LockVerticalDir");
|
||||
this.ticksSinceFullExtension = nbt.getInteger("TicksSinceExtended");
|
||||
this.dirX = nbt.getDouble("DirX");
|
||||
@@ -593,7 +727,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
nbt.setFloat("LaserWidth", laserWidth);
|
||||
nbt.setFloat("ExpansionSpeed", expansionSpeed);
|
||||
nbt.setInteger("LingerTicks", lingerTicks);
|
||||
nbt.setBoolean("DieOnImpact", dieOnImpact);
|
||||
nbt.setBoolean("LockVerticalDir", lockVerticalDirection);
|
||||
nbt.setInteger("TicksSinceExtended", ticksSinceFullExtension);
|
||||
nbt.setDouble("DirX", dirX);
|
||||
@@ -610,42 +743,47 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
|
||||
/**
|
||||
* Setup this laser in preview mode for GUI display.
|
||||
* Laser doesn't have charging state - spawns at active phase and fires immediately.
|
||||
* Uses charging-orb visuals during windup before firing.
|
||||
*/
|
||||
public void setupPreview(EntityLivingBase owner, float laserWidth, EnergyDisplayData display,
|
||||
EnergyLightningData lightning, float expansionSpeed, float maxDistance) {
|
||||
this.setPreviewMode(true);
|
||||
this.setPreviewOwner(owner);
|
||||
EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration,
|
||||
float expansionSpeed, float maxDistance) {
|
||||
setupPreviewState(owner, display, lightning, anchor, chargeDuration);
|
||||
|
||||
// Set visual properties
|
||||
// Laser-specific visual/behavior state.
|
||||
this.laserWidth = laserWidth;
|
||||
this.size = laserWidth;
|
||||
this.displayData = display;
|
||||
this.expansionSpeed = expansionSpeed;
|
||||
this.lifespanData.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview
|
||||
this.lightningData = lightning;
|
||||
this.currentLength = 0.0f;
|
||||
this.fullyExtended = false;
|
||||
this.ticksSinceFullExtension = 0;
|
||||
|
||||
// Position at chest height
|
||||
double x = owner.posX;
|
||||
double y = owner.posY + owner.height * 0.7;
|
||||
double z = owner.posZ;
|
||||
this.setPosition(x, y, z);
|
||||
this.prevPosX = x;
|
||||
this.prevPosY = y;
|
||||
this.prevPosZ = z;
|
||||
this.startX = x;
|
||||
this.startY = y;
|
||||
this.startZ = z;
|
||||
// Charge as an orb from tiny size to laser width while anchored.
|
||||
this.targetSize = laserWidth;
|
||||
setVisualSize(0.01f);
|
||||
setChargeOriginFromAnchor(owner, anchorData);
|
||||
clearMotion();
|
||||
this.endX = posX;
|
||||
this.endY = posY;
|
||||
this.endZ = posZ;
|
||||
|
||||
// Fire in owner's facing direction
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
this.dirX = -Math.sin(yaw);
|
||||
this.dirY = 0;
|
||||
this.dirZ = Math.cos(yaw);
|
||||
|
||||
// Initialize end point (same as start, will expand)
|
||||
this.endX = x;
|
||||
this.endY = y;
|
||||
this.endZ = z;
|
||||
// Precompute initial direction from owner look for launch.
|
||||
Vec3 look = owner == null ? null : owner.getLookVec();
|
||||
if (look == null) {
|
||||
if (owner != null) {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
look = Vec3.createVectorHelper(
|
||||
-Math.sin(yaw) * Math.cos(pitch),
|
||||
-Math.sin(pitch),
|
||||
Math.cos(yaw) * Math.cos(pitch)
|
||||
);
|
||||
} else {
|
||||
look = Vec3.createVectorHelper(1.0, 0.0, 0.0);
|
||||
}
|
||||
}
|
||||
this.dirX = look.xCoord;
|
||||
this.dirY = look.yCoord;
|
||||
this.dirZ = look.zCoord;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 195 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user