Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
luisc committed 2026-02-23 16:15:56 -06:00
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();
@@ -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();
@@ -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 &amp; 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;
}
}
@@ -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,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,
@@ -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";
}
}
@@ -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;
}
}
@@ -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); }
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
@@ -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 &amp; 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;
@@ -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,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,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,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,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;
@@ -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;
}
@@ -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);
}
}
@@ -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,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,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,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,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,4 +1,4 @@
package kamkeel.npcs.controllers.data.ability.data;
package kamkeel.npcs.controllers.data.ability.data.energy;
import net.minecraft.nbt.NBTTagCompound;
@@ -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;
@@ -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;
@@ -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);
@@ -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,4 +1,4 @@
package kamkeel.npcs.controllers.data.ability;
package kamkeel.npcs.controllers.data.ability.enums;
public enum AbilityPhase {
IDLE, // Not executing
@@ -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;
}
}
@@ -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];
}
@@ -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,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,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,4 +1,4 @@
package kamkeel.npcs.controllers.data.ability;
package kamkeel.npcs.controllers.data.ability.enums;
/**
* Defines which entity types can use an ability.
@@ -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;
@@ -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)
*/
@@ -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;
@@ -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;
@@ -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,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();
@@ -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;
@@ -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);
@@ -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));
}
}
@@ -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);
@@ -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));
}
}
@@ -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()
@@ -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;
@@ -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();
}
}
}
}
@@ -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