mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 21:54:08 +00:00
Merge branch 'dev' into syntax-highlighter-unified
This commit is contained in:
commit
105d274955
97 files changed
+8850
-6462
No files matched your search
+1
-1
Submodule src/api updated: f383fc3037...456bb2cbfa.
@@ -43,7 +43,16 @@ public class RenderAbilityDisc extends RenderAbilityProjectile {
|
||||
GL11.glPushMatrix();
|
||||
GL11.glScalef(scale, scale, scale);
|
||||
|
||||
// Apply ONLY Y-axis rotation for flat spin
|
||||
if (disc.isVertical()) {
|
||||
// Vertical mode: thin edge faces direction of travel (like a saw blade)
|
||||
// 1. Spin in local space (Y-axis)
|
||||
// 2. Tilt 90° to stand up (X-axis)
|
||||
// 3. Orient to face travel direction (Y-axis world yaw)
|
||||
GL11.glRotatef(disc.getTravelYaw(), 0.0f, 1.0f, 0.0f);
|
||||
GL11.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// Apply Y-axis rotation for spin
|
||||
GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f);
|
||||
|
||||
// Inner scale defines the core size
|
||||
|
||||
@@ -31,7 +31,7 @@ public abstract class RenderAbilityProjectile extends Render {
|
||||
*/
|
||||
protected void setupRenderState() {
|
||||
GL11.glPushMatrix();
|
||||
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
|
||||
GL11.glPushAttrib(GL11.GL_ENABLE_BIT | GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT | GL11.GL_LIGHTING_BIT);
|
||||
GL11.glDisable(GL11.GL_CULL_FACE);
|
||||
GL11.glEnable(GL12.GL_RESCALE_NORMAL);
|
||||
GL11.glEnable(GL11.GL_ALPHA_TEST);
|
||||
|
||||
@@ -0,0 +1,779 @@
|
||||
package kamkeel.npcs.client.renderer;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.AccentStyle;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.ParticleMotion;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.ZoneType;
|
||||
import noppes.npcs.client.fx.CustomFX;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.renderer.OpenGlHelper;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.client.renderer.entity.Render;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.util.ResourceLocation;
|
||||
import noppes.npcs.client.fx.ZoneParticleFX;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Renders zone effects for Trap and Hazard abilities.
|
||||
* Supports CIRCLE and SQUARE shapes with composable visual layers.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class RenderAbilityZone extends Render {
|
||||
|
||||
private static final int CIRCLE_SEGMENTS = 32;
|
||||
private static final int ACCENT_LINE_COUNT = 12;
|
||||
private static final Random RANDOM = new Random();
|
||||
|
||||
// Track last particle spawn tick per entity to avoid duplicate spawns
|
||||
private final Map<Integer, Integer> lastParticleTick = new HashMap<>();
|
||||
|
||||
// Persistent lightning state
|
||||
private final Map<Integer, AttachedLightningRenderer.LightningState> lightningStates = new HashMap<>();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// RENDER CONTEXT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private static class Ctx {
|
||||
EntityAbilityZone zone;
|
||||
ZoneShape shape;
|
||||
float radius, height;
|
||||
float ir, ig, ib; // inner color
|
||||
float outerR, outerG, outerB;
|
||||
boolean outerEnabled;
|
||||
float time, pulse, rotation, brightness;
|
||||
boolean flash, newTick;
|
||||
float particleDensity, particleScale, animSpeed, lightningDensity;
|
||||
// Visual layer fields (always populated)
|
||||
boolean groundFill, rings, border, accents, lightning, particles;
|
||||
float groundAlpha, borderSpeed;
|
||||
int ringCount, accentStyle, particleMotion, particleSize;
|
||||
boolean particleGlow;
|
||||
String particleDir;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// MAIN RENDER
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) {
|
||||
EntityAbilityZone zone = (EntityAbilityZone) entity;
|
||||
|
||||
// Build context
|
||||
Ctx c = new Ctx();
|
||||
c.zone = zone;
|
||||
c.shape = zone.getShape();
|
||||
c.radius = zone.getRadius();
|
||||
c.height = zone.getZoneHeight();
|
||||
|
||||
int inner = zone.getInnerColor();
|
||||
c.ir = ((inner >> 16) & 0xFF) / 255.0f;
|
||||
c.ig = ((inner >> 8) & 0xFF) / 255.0f;
|
||||
c.ib = (inner & 0xFF) / 255.0f;
|
||||
|
||||
int outer = zone.getOuterColor();
|
||||
c.outerR = ((outer >> 16) & 0xFF) / 255.0f;
|
||||
c.outerG = ((outer >> 8) & 0xFF) / 255.0f;
|
||||
c.outerB = (outer & 0xFF) / 255.0f;
|
||||
c.outerEnabled = zone.isOuterColorEnabled();
|
||||
|
||||
c.particleDensity = zone.getParticleDensity();
|
||||
c.particleScale = zone.getParticleScale();
|
||||
c.animSpeed = zone.getAnimSpeed();
|
||||
c.lightningDensity = zone.getLightningDensity();
|
||||
|
||||
// Visual layer fields — always populated
|
||||
c.groundFill = zone.isGroundFill();
|
||||
c.groundAlpha = zone.getGroundAlpha();
|
||||
c.rings = zone.isRings();
|
||||
c.ringCount = zone.getRingCount();
|
||||
c.border = zone.isBorder();
|
||||
c.borderSpeed = zone.getBorderSpeed();
|
||||
c.accents = zone.isAccents();
|
||||
c.accentStyle = zone.getAccentStyle();
|
||||
c.lightning = zone.isLightning();
|
||||
c.particles = zone.isParticles();
|
||||
c.particleMotion = zone.getParticleMotion();
|
||||
c.particleDir = zone.getParticleDir();
|
||||
c.particleSize = zone.getParticleSize();
|
||||
c.particleGlow = zone.isParticleGlow();
|
||||
|
||||
c.time = zone.ticksExisted + partialTicks;
|
||||
c.pulse = c.time * 0.1f * c.animSpeed;
|
||||
c.rotation = c.time * 1.5f * c.animSpeed;
|
||||
|
||||
c.brightness = 1.0f;
|
||||
if (zone.getZoneType() == ZoneType.TRAP) {
|
||||
if (!zone.isArmed()) {
|
||||
c.brightness = 0.4f;
|
||||
} else {
|
||||
c.pulse = c.time * 0.2f;
|
||||
}
|
||||
}
|
||||
|
||||
c.flash = zone.getTriggerFlashTick() >= 0
|
||||
&& zone.ticksExisted - zone.getTriggerFlashTick() < 4;
|
||||
|
||||
c.newTick = isNewTick(zone);
|
||||
|
||||
// Render
|
||||
setupRenderState();
|
||||
GL11.glPushMatrix();
|
||||
GL11.glTranslated(x, y, z);
|
||||
|
||||
GL11.glDepthMask(false);
|
||||
|
||||
renderZone(c);
|
||||
|
||||
GL11.glDepthMask(true);
|
||||
GL11.glPopMatrix();
|
||||
restoreRenderState();
|
||||
}
|
||||
|
||||
private boolean isNewTick(EntityAbilityZone zone) {
|
||||
int id = zone.getEntityId();
|
||||
int tick = zone.ticksExisted;
|
||||
Integer last = lastParticleTick.get(id);
|
||||
if (last == null || last != tick) {
|
||||
lastParticleTick.put(id, tick);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityZone zone) {
|
||||
int id = zone.getEntityId();
|
||||
AttachedLightningRenderer.LightningState state = lightningStates.get(id);
|
||||
if (state == null) {
|
||||
state = new AttachedLightningRenderer.LightningState();
|
||||
lightningStates.put(id, state);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// UNIFIED RENDER — Composable visual layers
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void renderZone(Ctx c) {
|
||||
float rR = ringR(c), rG = ringG(c), rB = ringB(c);
|
||||
|
||||
// 1. Ground fill
|
||||
if (c.groundFill) {
|
||||
if (c.flash) {
|
||||
renderShapeFill(c.shape, c.radius, 1, 1, 1, 0.5f);
|
||||
} else {
|
||||
renderShapeFill(c.shape, c.radius,
|
||||
c.ir, c.ig, c.ib, c.groundAlpha * c.brightness);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Pulse rings
|
||||
if (c.rings) {
|
||||
int count = Math.max(1, Math.min(5, c.ringCount));
|
||||
for (int i = 0; i < count; i++) {
|
||||
float frac = (i + 1.0f) / (count + 1);
|
||||
float ra = (0.4f + 0.3f * sin(c.pulse + i * Math.PI / 3)) * c.brightness;
|
||||
float yOff = 0.02f + i * 0.01f;
|
||||
renderRingLine(c.shape, c.radius * frac,
|
||||
rR, rG, rB, ra, yOff);
|
||||
}
|
||||
// Always include outer edge ring
|
||||
float edgeA = (0.5f + 0.4f * sin(c.pulse + count * Math.PI / 3)) * c.brightness;
|
||||
renderRingLine(c.shape, c.radius, rR, rG, rB, edgeA, 0.02f + count * 0.01f);
|
||||
}
|
||||
|
||||
// 3. Rotating border
|
||||
if (c.border) {
|
||||
renderRotatingBorder(c.shape, c.radius, rR, rG, rB,
|
||||
0.7f * c.brightness, c.rotation * c.borderSpeed, 0.05f);
|
||||
}
|
||||
|
||||
// 4. Vertical accents
|
||||
if (c.accents) {
|
||||
float aa = 0.5f * c.brightness * (0.3f + 0.2f * sin(c.pulse * 0.7f));
|
||||
AccentStyle style = AccentStyle.values()[Math.min(c.accentStyle, AccentStyle.values().length - 1)];
|
||||
switch (style) {
|
||||
case SWAYING:
|
||||
renderSwayingAccents(c, c.ir, c.ig, c.ib, aa, c.height);
|
||||
break;
|
||||
case FLICKERING:
|
||||
renderFlickeringAccents(c, c.ir, c.ig, c.ib, aa, c.height);
|
||||
break;
|
||||
default:
|
||||
renderVerticalAccents(c.shape, c.radius, c.ir, c.ig, c.ib, aa, c.height, c.rotation * 0.3f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Lightning
|
||||
if (c.lightning) {
|
||||
int innerColor = c.zone.getInnerColor();
|
||||
int outerColor = c.outerEnabled ? c.zone.getOuterColor() : innerColor;
|
||||
AttachedLightningRenderer.LightningState state = getLightningState(c.zone);
|
||||
if (c.newTick) {
|
||||
state.tick();
|
||||
float density = 0.4f * c.brightness * c.lightningDensity;
|
||||
if (RANDOM.nextFloat() < density) {
|
||||
double[] p1 = randomPosInZone(c);
|
||||
double[] p2 = randomPosInZone(c);
|
||||
double y1 = RANDOM.nextDouble() * c.height * 0.4;
|
||||
double y2 = RANDOM.nextDouble() * c.height * 0.4;
|
||||
float disp = c.radius * 0.15f;
|
||||
state.addArc(AttachedLightningRenderer.createArcBetween(
|
||||
p1[0], y1, p1[2], p2[0], y2, p2[2], disp, outerColor, innerColor, 6));
|
||||
}
|
||||
if (RANDOM.nextFloat() < density * 0.5f * c.lightningDensity) {
|
||||
double[] edgePos = randomEdgePos(c);
|
||||
double ex = edgePos[0];
|
||||
double ez = edgePos[1];
|
||||
float disp = c.radius * 0.12f;
|
||||
state.addArc(AttachedLightningRenderer.createArcBetween(
|
||||
ex, 0, ez, ex + (RANDOM.nextDouble() - 0.5) * 0.5,
|
||||
c.height * (0.3 + RANDOM.nextDouble() * 0.4),
|
||||
ez + (RANDOM.nextDouble() - 0.5) * 0.5,
|
||||
disp, outerColor, innerColor, 5));
|
||||
}
|
||||
}
|
||||
state.render();
|
||||
GL11.glDepthMask(false); // Reset after lightning
|
||||
}
|
||||
|
||||
// 6. Particles
|
||||
if (c.particles && c.newTick && c.particleDensity > 0) {
|
||||
if (c.particleDir != null && !c.particleDir.isEmpty()) {
|
||||
if (c.particleDir.startsWith("mc:")) {
|
||||
spawnVanillaParticle(c);
|
||||
} else {
|
||||
spawnCustomFXParticle(c);
|
||||
}
|
||||
} else {
|
||||
spawnDefaultParticles(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Spawn vanilla Minecraft particles using mc: prefix (e.g. "mc:flame"). */
|
||||
private void spawnVanillaParticle(Ctx c) {
|
||||
String particleName = c.particleDir.substring(3);
|
||||
int count = Math.round(3 * c.particleDensity);
|
||||
for (int i = 0; i < count; i++) {
|
||||
double[] pos = randomPosInZone(c);
|
||||
double[] motion = getMotionForStyle(c.particleMotion);
|
||||
c.zone.worldObj.spawnParticle(particleName,
|
||||
c.zone.posX + pos[0], c.zone.posY + pos[1], c.zone.posZ + pos[2],
|
||||
motion[0], motion[1], motion[2]);
|
||||
}
|
||||
}
|
||||
|
||||
/** Spawn CustomFX particles using a texture directory. */
|
||||
private void spawnCustomFXParticle(Ctx c) {
|
||||
int count = Math.round(3 * c.particleDensity);
|
||||
for (int i = 0; i < count; i++) {
|
||||
double[] pos = randomPosInZone(c);
|
||||
double[] motion = getMotionForStyle(c.particleMotion);
|
||||
CustomFX fx = new CustomFX(c.zone.worldObj, null, c.particleDir,
|
||||
c.zone.posX + pos[0], c.zone.posY + pos[1], c.zone.posZ + pos[2],
|
||||
0, 0, 0);
|
||||
fx.motionX = motion[0];
|
||||
fx.motionY = motion[1];
|
||||
fx.motionZ = motion[2];
|
||||
fx.setMaxAge(20 + RANDOM.nextInt(20));
|
||||
fx.width = -1;
|
||||
fx.height = -1;
|
||||
fx.scaleX1 = c.particleScale * 10.0f;
|
||||
fx.scaleY1 = c.particleScale * 10.0f;
|
||||
fx.scaleX2 = fx.scaleX1;
|
||||
fx.scaleY2 = fx.scaleY1;
|
||||
fx.scaleXRate = 0;
|
||||
fx.scaleYRate = 0;
|
||||
fx.HEXColor = c.zone.getInnerColor();
|
||||
fx.facePlayer = true;
|
||||
fx.glows = c.particleGlow;
|
||||
fx.noClip = true;
|
||||
fx.alpha1 = 0.8f;
|
||||
fx.alpha2 = 0.0f;
|
||||
fx.alphaRate = -0.04f;
|
||||
Minecraft.getMinecraft().effectRenderer.addEffect(fx);
|
||||
}
|
||||
}
|
||||
|
||||
/** Spawn default ZoneParticleFX with motion style from ParticleMotion enum. */
|
||||
private void spawnDefaultParticles(Ctx c) {
|
||||
int count = Math.round(3 * c.particleDensity);
|
||||
ParticleMotion motion = ParticleMotion.values()[Math.min(c.particleMotion, ParticleMotion.values().length - 1)];
|
||||
for (int i = 0; i < count; i++) {
|
||||
double[] pos = randomPosInZone(c);
|
||||
double mx, my, mz;
|
||||
float gravity;
|
||||
int age;
|
||||
switch (motion) {
|
||||
case DRIFTING:
|
||||
mx = (RANDOM.nextDouble() - 0.5) * 0.005;
|
||||
my = 0.005 + RANDOM.nextDouble() * 0.01;
|
||||
mz = (RANDOM.nextDouble() - 0.5) * 0.005;
|
||||
gravity = 0;
|
||||
age = 40 + RANDOM.nextInt(20);
|
||||
break;
|
||||
case SPARKS:
|
||||
mx = (RANDOM.nextDouble() - 0.5) * 0.08;
|
||||
my = RANDOM.nextDouble() * 0.06;
|
||||
mz = (RANDOM.nextDouble() - 0.5) * 0.08;
|
||||
gravity = 0;
|
||||
age = 4 + RANDOM.nextInt(6);
|
||||
break;
|
||||
default: // RISING
|
||||
mx = (RANDOM.nextDouble() - 0.5) * 0.01;
|
||||
my = 0.01 + RANDOM.nextDouble() * 0.02;
|
||||
mz = (RANDOM.nextDouble() - 0.5) * 0.01;
|
||||
gravity = -0.01f;
|
||||
age = 30 + RANDOM.nextInt(20);
|
||||
break;
|
||||
}
|
||||
float scale = (1.5f + RANDOM.nextFloat() * 1.0f) * c.particleScale;
|
||||
spawnParticle(c.zone, pos[0], pos[1], pos[2], mx, my, mz,
|
||||
c.ir, c.ig, c.ib, 0.7f, scale, age, gravity, c.particleGlow);
|
||||
}
|
||||
}
|
||||
|
||||
/** Get motion vector for a CustomFX particle based on ParticleMotion ordinal. */
|
||||
private double[] getMotionForStyle(int motionOrdinal) {
|
||||
ParticleMotion motion = ParticleMotion.values()[Math.min(motionOrdinal, ParticleMotion.values().length - 1)];
|
||||
switch (motion) {
|
||||
case DRIFTING:
|
||||
return new double[]{
|
||||
(RANDOM.nextDouble() - 0.5) * 0.005,
|
||||
0.005 + RANDOM.nextDouble() * 0.01,
|
||||
(RANDOM.nextDouble() - 0.5) * 0.005
|
||||
};
|
||||
case SPARKS:
|
||||
return new double[]{
|
||||
(RANDOM.nextDouble() - 0.5) * 0.08,
|
||||
RANDOM.nextDouble() * 0.06,
|
||||
(RANDOM.nextDouble() - 0.5) * 0.08
|
||||
};
|
||||
default: // RISING
|
||||
return new double[]{
|
||||
(RANDOM.nextDouble() - 0.5) * 0.01,
|
||||
0.01 + RANDOM.nextDouble() * 0.02,
|
||||
(RANDOM.nextDouble() - 0.5) * 0.01
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// STYLE HELPERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Get the ring/outer color (falls back to inner if outer not enabled). */
|
||||
private float ringR(Ctx c) { return c.outerEnabled ? c.outerR : c.ir; }
|
||||
private float ringG(Ctx c) { return c.outerEnabled ? c.outerG : c.ig; }
|
||||
private float ringB(Ctx c) { return c.outerEnabled ? c.outerB : c.ib; }
|
||||
|
||||
private static float sin(double v) { return (float) Math.sin(v); }
|
||||
|
||||
/** Vertical accents with sinusoidal sway per line. */
|
||||
private void renderSwayingAccents(Ctx c, float r, float g, float b, float a, float height) {
|
||||
if (height <= 0.1f) return;
|
||||
float rotRad = (float) Math.toRadians(c.rotation * 0.2f);
|
||||
Tessellator tess = Tessellator.instance;
|
||||
|
||||
if (c.shape == ZoneShape.SQUARE) {
|
||||
renderSquareSwayingAccents(c, r, g, b, a, height, rotRad, tess);
|
||||
} else {
|
||||
for (int i = 0; i < ACCENT_LINE_COUNT; i++) {
|
||||
float baseAngle = (float) (Math.PI * 2 * i) / ACCENT_LINE_COUNT + rotRad;
|
||||
double lx = Math.cos(baseAngle) * c.radius;
|
||||
double lz = Math.sin(baseAngle) * c.radius;
|
||||
|
||||
float swayX = 0.15f * sin(c.time * 0.08f + i * 1.7f);
|
||||
float swayZ = 0.15f * sin(c.time * 0.06f + i * 2.3f);
|
||||
|
||||
tess.startDrawing(GL11.GL_LINES);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
tess.addVertex(lx, 0.01, lz);
|
||||
tess.setColorRGBA_F(r, g, b, 0.0f);
|
||||
tess.addVertex(lx + swayX, height, lz + swayZ);
|
||||
tess.draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void renderSquareSwayingAccents(Ctx c, float r, float g, float b, float a, float height, float rotRad, Tessellator tess) {
|
||||
int perSide = Math.max(1, ACCENT_LINE_COUNT / 4);
|
||||
int idx = 0;
|
||||
for (int side = 0; side < 4; side++) {
|
||||
for (int j = 0; j < perSide; j++) {
|
||||
float frac = (j + 0.5f) / perSide;
|
||||
double lx, lz;
|
||||
switch (side) {
|
||||
case 0: lx = -c.radius + frac * 2 * c.radius; lz = -c.radius; break;
|
||||
case 1: lx = c.radius; lz = -c.radius + frac * 2 * c.radius; break;
|
||||
case 2: lx = c.radius - frac * 2 * c.radius; lz = c.radius; break;
|
||||
default: lx = -c.radius; lz = c.radius - frac * 2 * c.radius; break;
|
||||
}
|
||||
float swayX = 0.15f * sin(c.time * 0.08f + idx * 1.7f);
|
||||
float swayZ = 0.15f * sin(c.time * 0.06f + idx * 2.3f);
|
||||
|
||||
tess.startDrawing(GL11.GL_LINES);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
tess.addVertex(lx, 0.01, lz);
|
||||
tess.setColorRGBA_F(r, g, b, 0.0f);
|
||||
tess.addVertex(lx + swayX, height, lz + swayZ);
|
||||
tess.draw();
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Vertical accents with per-line random alpha flicker. */
|
||||
private void renderFlickeringAccents(Ctx c, float r, float g, float b, float baseAlpha, float height) {
|
||||
if (height <= 0.1f) return;
|
||||
float rotRad = (float) Math.toRadians(c.rotation * 0.4f);
|
||||
Tessellator tess = Tessellator.instance;
|
||||
|
||||
if (c.shape == ZoneShape.SQUARE) {
|
||||
renderSquareFlickeringAccents(c, r, g, b, baseAlpha, height, tess);
|
||||
} else {
|
||||
for (int i = 0; i < ACCENT_LINE_COUNT; i++) {
|
||||
float baseAngle = (float) (Math.PI * 2 * i) / ACCENT_LINE_COUNT + rotRad;
|
||||
double lx = Math.cos(baseAngle) * c.radius;
|
||||
double lz = Math.sin(baseAngle) * c.radius;
|
||||
|
||||
float flicker = 0.3f + 0.7f * Math.abs(sin(c.time * 0.7f + i * 3.14f));
|
||||
float lineAlpha = baseAlpha * flicker;
|
||||
float lineHeight = height * (0.6f + 0.4f * flicker);
|
||||
|
||||
tess.startDrawing(GL11.GL_LINES);
|
||||
tess.setColorRGBA_F(r, g, b, lineAlpha);
|
||||
tess.addVertex(lx, 0.01, lz);
|
||||
tess.setColorRGBA_F(r, g, b, 0.0f);
|
||||
tess.addVertex(lx, lineHeight, lz);
|
||||
tess.draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void renderSquareFlickeringAccents(Ctx c, float r, float g, float b, float baseAlpha, float height, Tessellator tess) {
|
||||
int perSide = Math.max(1, ACCENT_LINE_COUNT / 4);
|
||||
int idx = 0;
|
||||
for (int side = 0; side < 4; side++) {
|
||||
for (int j = 0; j < perSide; j++) {
|
||||
float frac = (j + 0.5f) / perSide;
|
||||
double lx, lz;
|
||||
switch (side) {
|
||||
case 0: lx = -c.radius + frac * 2 * c.radius; lz = -c.radius; break;
|
||||
case 1: lx = c.radius; lz = -c.radius + frac * 2 * c.radius; break;
|
||||
case 2: lx = c.radius - frac * 2 * c.radius; lz = c.radius; break;
|
||||
default: lx = -c.radius; lz = c.radius - frac * 2 * c.radius; break;
|
||||
}
|
||||
float flicker = 0.3f + 0.7f * Math.abs(sin(c.time * 0.7f + idx * 3.14f));
|
||||
float lineAlpha = baseAlpha * flicker;
|
||||
float lineHeight = height * (0.6f + 0.4f * flicker);
|
||||
|
||||
tess.startDrawing(GL11.GL_LINES);
|
||||
tess.setColorRGBA_F(r, g, b, lineAlpha);
|
||||
tess.addVertex(lx, 0.01, lz);
|
||||
tess.setColorRGBA_F(r, g, b, 0.0f);
|
||||
tess.addVertex(lx, lineHeight, lz);
|
||||
tess.draw();
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Pick a random position within the zone's area. Returns {x, y, z} offsets from zone center. */
|
||||
private double[] randomPosInZone(Ctx c) {
|
||||
if (c.shape == ZoneShape.SQUARE) {
|
||||
double px = (RANDOM.nextDouble() * 2 - 1) * c.radius;
|
||||
double pz = (RANDOM.nextDouble() * 2 - 1) * c.radius;
|
||||
return new double[]{px, 0, pz};
|
||||
} else {
|
||||
double angle = RANDOM.nextDouble() * Math.PI * 2;
|
||||
double dist = Math.sqrt(RANDOM.nextDouble()) * c.radius;
|
||||
double px = Math.cos(angle) * dist;
|
||||
double pz = Math.sin(angle) * dist;
|
||||
return new double[]{px, 0, pz};
|
||||
}
|
||||
}
|
||||
|
||||
/** Pick a random position on the edge of the zone. Returns {x, z}. */
|
||||
private double[] randomEdgePos(Ctx c) {
|
||||
if (c.shape == ZoneShape.SQUARE) {
|
||||
int side = RANDOM.nextInt(4);
|
||||
double frac = RANDOM.nextDouble() * 2 - 1;
|
||||
switch (side) {
|
||||
case 0: return new double[]{frac * c.radius, -c.radius};
|
||||
case 1: return new double[]{frac * c.radius, c.radius};
|
||||
case 2: return new double[]{-c.radius, frac * c.radius};
|
||||
default: return new double[]{c.radius, frac * c.radius};
|
||||
}
|
||||
} else {
|
||||
double angle = RANDOM.nextDouble() * Math.PI * 2;
|
||||
double dist = c.radius * (0.5 + RANDOM.nextDouble() * 0.5);
|
||||
return new double[]{Math.cos(angle) * dist, Math.sin(angle) * dist};
|
||||
}
|
||||
}
|
||||
|
||||
/** Spawn a zone particle at world position. */
|
||||
private void spawnParticle(EntityAbilityZone zone, double offsetX, double offsetY, double offsetZ,
|
||||
double mx, double my, double mz,
|
||||
float r, float g, float b, float alpha,
|
||||
float scale, int maxAge, float gravity, boolean glow) {
|
||||
ZoneParticleFX particle = new ZoneParticleFX(
|
||||
zone.worldObj,
|
||||
zone.posX + offsetX, zone.posY + offsetY, zone.posZ + offsetZ,
|
||||
mx, my, mz, r, g, b, alpha, scale, maxAge, gravity, glow);
|
||||
Minecraft.getMinecraft().effectRenderer.addEffect(particle);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GL STATE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void setupRenderState() {
|
||||
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
|
||||
GL11.glDisable(GL11.GL_TEXTURE_2D);
|
||||
GL11.glDisable(GL11.GL_CULL_FACE);
|
||||
GL11.glEnable(GL11.GL_BLEND);
|
||||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
GL11.glDisable(GL11.GL_LIGHTING);
|
||||
GL11.glShadeModel(GL11.GL_SMOOTH);
|
||||
OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0f, 240.0f);
|
||||
}
|
||||
|
||||
private void restoreRenderState() {
|
||||
GL11.glShadeModel(GL11.GL_FLAT);
|
||||
GL11.glEnable(GL11.GL_LIGHTING);
|
||||
GL11.glDisable(GL11.GL_BLEND);
|
||||
GL11.glEnable(GL11.GL_CULL_FACE);
|
||||
GL11.glEnable(GL11.GL_TEXTURE_2D);
|
||||
GL11.glPopAttrib();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SHAPE FILL (Ground plane)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void renderShapeFill(ZoneShape shape, float radius, float r, float g, float b, float a) {
|
||||
GL11.glEnable(GL11.GL_POLYGON_OFFSET_FILL);
|
||||
GL11.glPolygonOffset(-1.0f, -1.0f);
|
||||
switch (shape) {
|
||||
case CIRCLE:
|
||||
renderCircleFill(radius, r, g, b, a);
|
||||
break;
|
||||
case SQUARE:
|
||||
renderSquareFill(radius, r, g, b, a);
|
||||
break;
|
||||
}
|
||||
GL11.glDisable(GL11.GL_POLYGON_OFFSET_FILL);
|
||||
}
|
||||
|
||||
private void renderCircleFill(float radius, float r, float g, float b, float a) {
|
||||
Tessellator tess = Tessellator.instance;
|
||||
tess.startDrawing(GL11.GL_TRIANGLE_FAN);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
tess.addVertex(0, 0, 0);
|
||||
for (int i = 0; i <= CIRCLE_SEGMENTS; i++) {
|
||||
double angle = (Math.PI * 2 * i) / CIRCLE_SEGMENTS;
|
||||
tess.addVertex(Math.cos(angle) * radius, 0, Math.sin(angle) * radius);
|
||||
}
|
||||
tess.draw();
|
||||
}
|
||||
|
||||
private void renderSquareFill(float radius, float r, float g, float b, float a) {
|
||||
Tessellator tess = Tessellator.instance;
|
||||
tess.startDrawing(GL11.GL_QUADS);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
tess.addVertex(-radius, 0, -radius);
|
||||
tess.addVertex(radius, 0, -radius);
|
||||
tess.addVertex(radius, 0, radius);
|
||||
tess.addVertex(-radius, 0, radius);
|
||||
tess.draw();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// RING LINES (Concentric pulse rings)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void renderRingLine(ZoneShape shape, float radius,
|
||||
float r, float g, float b, float a, float yOffset) {
|
||||
switch (shape) {
|
||||
case CIRCLE:
|
||||
renderCircleRingLine(radius, r, g, b, a, yOffset);
|
||||
break;
|
||||
case SQUARE:
|
||||
renderSquareRingLine(radius, r, g, b, a, yOffset);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void renderCircleRingLine(float radius, float r, float g, float b, float a, float yOffset) {
|
||||
Tessellator tess = Tessellator.instance;
|
||||
tess.startDrawing(GL11.GL_LINE_LOOP);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
for (int i = 0; i < CIRCLE_SEGMENTS; i++) {
|
||||
double angle = (Math.PI * 2 * i) / CIRCLE_SEGMENTS;
|
||||
tess.addVertex(Math.cos(angle) * radius, yOffset, Math.sin(angle) * radius);
|
||||
}
|
||||
tess.draw();
|
||||
}
|
||||
|
||||
private void renderSquareRingLine(float radius, float r, float g, float b, float a, float yOffset) {
|
||||
Tessellator tess = Tessellator.instance;
|
||||
tess.startDrawing(GL11.GL_LINE_LOOP);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
tess.addVertex(-radius, yOffset, -radius);
|
||||
tess.addVertex(radius, yOffset, -radius);
|
||||
tess.addVertex(radius, yOffset, radius);
|
||||
tess.addVertex(-radius, yOffset, radius);
|
||||
tess.draw();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ROTATING BORDER
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void renderRotatingBorder(ZoneShape shape, float radius,
|
||||
float r, float g, float b, float a,
|
||||
float rotationDeg, float yOffset) {
|
||||
int dashCount = 16;
|
||||
|
||||
if (shape == ZoneShape.SQUARE) {
|
||||
// Dashes along 4 rectangle edges with offset
|
||||
float perimeter = radius * 8; // total perimeter = 4 * (2*radius)
|
||||
float dashLen = perimeter / dashCount;
|
||||
float offset = (rotationDeg % 360.0f) / 360.0f * perimeter;
|
||||
|
||||
Tessellator tess = Tessellator.instance;
|
||||
for (int i = 0; i < dashCount; i += 2) {
|
||||
float startDist = (i * dashLen + offset) % perimeter;
|
||||
float endDist = startDist + dashLen;
|
||||
|
||||
tess.startDrawing(GL11.GL_LINE_STRIP);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
int steps = 4;
|
||||
for (int s = 0; s <= steps; s++) {
|
||||
float d = startDist + (endDist - startDist) * s / steps;
|
||||
double[] pos = perimeterPos(d % perimeter, radius);
|
||||
tess.addVertex(pos[0], yOffset, pos[1]);
|
||||
}
|
||||
tess.draw();
|
||||
}
|
||||
} else {
|
||||
float dashAngle = (float) (Math.PI * 2) / dashCount;
|
||||
float rotRad = (float) Math.toRadians(rotationDeg);
|
||||
|
||||
Tessellator tess = Tessellator.instance;
|
||||
for (int i = 0; i < dashCount; i += 2) {
|
||||
float startAngle = i * dashAngle + rotRad;
|
||||
float endAngle = startAngle + dashAngle;
|
||||
|
||||
tess.startDrawing(GL11.GL_LINE_STRIP);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
int steps = 4;
|
||||
for (int s = 0; s <= steps; s++) {
|
||||
float segAngle = startAngle + (endAngle - startAngle) * s / steps;
|
||||
tess.addVertex(Math.cos(segAngle) * radius, yOffset, Math.sin(segAngle) * radius);
|
||||
}
|
||||
tess.draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert a distance along the square perimeter to an x,z position. */
|
||||
private double[] perimeterPos(float dist, float radius) {
|
||||
float sideLen = radius * 2;
|
||||
if (dist < sideLen) {
|
||||
// Bottom edge: (-radius, -radius) to (radius, -radius)
|
||||
return new double[]{-radius + dist, -radius};
|
||||
}
|
||||
dist -= sideLen;
|
||||
if (dist < sideLen) {
|
||||
// Right edge: (radius, -radius) to (radius, radius)
|
||||
return new double[]{radius, -radius + dist};
|
||||
}
|
||||
dist -= sideLen;
|
||||
if (dist < sideLen) {
|
||||
// Top edge: (radius, radius) to (-radius, radius)
|
||||
return new double[]{radius - dist, radius};
|
||||
}
|
||||
dist -= sideLen;
|
||||
// Left edge: (-radius, radius) to (-radius, -radius)
|
||||
return new double[]{-radius, radius - dist};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// VERTICAL ACCENT LINES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void renderVerticalAccents(ZoneShape shape, float radius,
|
||||
float r, float g, float b, float a,
|
||||
float height, float rotationDeg) {
|
||||
if (height <= 0.1f) return;
|
||||
|
||||
Tessellator tess = Tessellator.instance;
|
||||
|
||||
if (shape == ZoneShape.SQUARE) {
|
||||
// Lines at corners + distributed along edges
|
||||
int perSide = Math.max(1, ACCENT_LINE_COUNT / 4);
|
||||
for (int side = 0; side < 4; side++) {
|
||||
for (int j = 0; j < perSide; j++) {
|
||||
float frac = (j + 0.5f) / perSide;
|
||||
double lx, lz;
|
||||
switch (side) {
|
||||
case 0: lx = -radius + frac * 2 * radius; lz = -radius; break;
|
||||
case 1: lx = radius; lz = -radius + frac * 2 * radius; break;
|
||||
case 2: lx = radius - frac * 2 * radius; lz = radius; break;
|
||||
default: lx = -radius; lz = radius - frac * 2 * radius; break;
|
||||
}
|
||||
tess.startDrawing(GL11.GL_LINES);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
tess.addVertex(lx, 0.01, lz);
|
||||
tess.setColorRGBA_F(r, g, b, 0.0f);
|
||||
tess.addVertex(lx, height, lz);
|
||||
tess.draw();
|
||||
}
|
||||
}
|
||||
// Corner accent lines
|
||||
float[][] corners = {{-radius, -radius}, {radius, -radius}, {radius, radius}, {-radius, radius}};
|
||||
for (float[] corner : corners) {
|
||||
tess.startDrawing(GL11.GL_LINES);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
tess.addVertex(corner[0], 0.01, corner[1]);
|
||||
tess.setColorRGBA_F(r, g, b, 0.0f);
|
||||
tess.addVertex(corner[0], height, corner[1]);
|
||||
tess.draw();
|
||||
}
|
||||
} else {
|
||||
float rotRad = (float) Math.toRadians(rotationDeg);
|
||||
for (int i = 0; i < ACCENT_LINE_COUNT; i++) {
|
||||
float angle = (float) (Math.PI * 2 * i) / ACCENT_LINE_COUNT + rotRad;
|
||||
double lx = Math.cos(angle) * radius;
|
||||
double lz = Math.sin(angle) * radius;
|
||||
|
||||
tess.startDrawing(GL11.GL_LINES);
|
||||
tess.setColorRGBA_F(r, g, b, a);
|
||||
tess.addVertex(lx, 0.01, lz);
|
||||
tess.setColorRGBA_F(r, g, b, 0.0f);
|
||||
tess.addVertex(lx, height, lz);
|
||||
tess.draw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResourceLocation getEntityTexture(Entity entity) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -110,6 +110,9 @@ public class TelegraphRenderer {
|
||||
case POINT:
|
||||
renderPoint(red, green, blue, alpha);
|
||||
break;
|
||||
case SQUARE:
|
||||
renderSquare(telegraph.getRadius(), red, green, blue, alpha);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -258,6 +261,30 @@ public class TelegraphRenderer {
|
||||
tessellator.draw();
|
||||
}
|
||||
|
||||
private void renderSquare(float radius, float r, float g, float b, float a) {
|
||||
Tessellator tessellator = Tessellator.instance;
|
||||
tessellator.startDrawingQuads();
|
||||
tessellator.setColorRGBA_F(r, g, b, a);
|
||||
|
||||
tessellator.addVertex(-radius, 0, -radius);
|
||||
tessellator.addVertex(radius, 0, -radius);
|
||||
tessellator.addVertex(radius, 0, radius);
|
||||
tessellator.addVertex(-radius, 0, radius);
|
||||
|
||||
tessellator.draw();
|
||||
|
||||
// Draw border
|
||||
tessellator.startDrawing(GL11.GL_LINE_LOOP);
|
||||
tessellator.setColorRGBA_F(r, g, b, Math.min(1.0f, a * 2));
|
||||
|
||||
tessellator.addVertex(-radius, 0.01, -radius);
|
||||
tessellator.addVertex(radius, 0.01, -radius);
|
||||
tessellator.addVertex(radius, 0.01, radius);
|
||||
tessellator.addVertex(-radius, 0.01, radius);
|
||||
|
||||
tessellator.draw();
|
||||
}
|
||||
|
||||
// ==================== GUI PREVIEW RENDERING ====================
|
||||
|
||||
/**
|
||||
@@ -322,6 +349,9 @@ public class TelegraphRenderer {
|
||||
case POINT:
|
||||
renderPoint(red, green, blue, alpha);
|
||||
break;
|
||||
case SQUARE:
|
||||
renderSquare(telegraph.getRadius(), red, green, blue, alpha);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
+39
-15
@@ -85,20 +85,39 @@ public class AttachedLightningRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Age existing arcs and remove dead ones without spawning new arcs.
|
||||
* Use with addArc() for custom arc generation.
|
||||
*/
|
||||
public void tick() {
|
||||
Iterator<LightningArc> iter = arcs.iterator();
|
||||
while (iter.hasNext()) {
|
||||
LightningArc arc = iter.next();
|
||||
arc.tick();
|
||||
if (arc.isDead()) {
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom arc to the state.
|
||||
*/
|
||||
public void addArc(LightningArc arc) {
|
||||
arcs.add(arc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all active arcs in local space.
|
||||
* Call this after glTranslated to entity position.
|
||||
* Note: Expects the caller to have already set up GL state (disabled textures,
|
||||
* disabled lighting, disabled culling, enabled blending). This avoids nesting
|
||||
* glPushAttrib calls which can overflow the GL attrib stack (depth 16) when
|
||||
* other mods also push attribs during entity rendering.
|
||||
*/
|
||||
public void render() {
|
||||
if (arcs.isEmpty()) return;
|
||||
|
||||
// Setup GL state
|
||||
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
|
||||
GL11.glDisable(GL11.GL_TEXTURE_2D);
|
||||
GL11.glDisable(GL11.GL_LIGHTING);
|
||||
GL11.glDisable(GL11.GL_CULL_FACE);
|
||||
GL11.glEnable(GL11.GL_BLEND);
|
||||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
GL11.glDepthMask(false);
|
||||
|
||||
Tessellator tess = Tessellator.instance;
|
||||
@@ -114,7 +133,6 @@ public class AttachedLightningRenderer {
|
||||
}
|
||||
|
||||
GL11.glDepthMask(true);
|
||||
GL11.glPopAttrib();
|
||||
}
|
||||
|
||||
public int getArcCount() {
|
||||
@@ -235,10 +253,23 @@ public class AttachedLightningRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a lightning arc between two arbitrary points.
|
||||
*/
|
||||
public static LightningArc createArcBetween(double x1, double y1, double z1,
|
||||
double x2, double y2, double z2,
|
||||
float displacement, int outerColor, int innerColor, int maxAge) {
|
||||
int segments = 4 + rand.nextInt(3);
|
||||
List<double[]> points = generateLightningPath(x1, y1, z1, x2, y2, z2, segments, displacement);
|
||||
return new LightningArc(points, maxAge, outerColor, innerColor);
|
||||
}
|
||||
|
||||
// ==================== INSTANT RENDERING (no persistence) ====================
|
||||
|
||||
/**
|
||||
* Render a single instant lightning arc (no fading, drawn this frame only).
|
||||
* Expects the caller to have already set up GL state (disabled textures,
|
||||
* disabled lighting, disabled culling, enabled blending).
|
||||
*/
|
||||
public static void renderInstantArc(float radius, int outerColor, int innerColor) {
|
||||
double theta = rand.nextDouble() * Math.PI * 2;
|
||||
@@ -254,12 +285,6 @@ public class AttachedLightningRenderer {
|
||||
|
||||
List<double[]> points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement);
|
||||
|
||||
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
|
||||
GL11.glDisable(GL11.GL_TEXTURE_2D);
|
||||
GL11.glDisable(GL11.GL_LIGHTING);
|
||||
GL11.glDisable(GL11.GL_CULL_FACE);
|
||||
GL11.glEnable(GL11.GL_BLEND);
|
||||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
GL11.glDepthMask(false);
|
||||
|
||||
Tessellator tess = Tessellator.instance;
|
||||
@@ -267,7 +292,6 @@ public class AttachedLightningRenderer {
|
||||
renderBoltPath(tess, points, innerColor, 0.012f, 0.7f);
|
||||
|
||||
GL11.glDepthMask(true);
|
||||
GL11.glPopAttrib();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,12 @@ package kamkeel.npcs.command;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
import net.minecraft.command.ICommandSender;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import noppes.npcs.NoppesUtilServer;
|
||||
import noppes.npcs.controllers.PlayerDataController;
|
||||
import noppes.npcs.controllers.data.PlayerData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -122,5 +127,192 @@ public class AbilityCommand extends CommandKamkeelBase {
|
||||
}
|
||||
sendResult(sender, " Range: \u00A7e" + ability.getMinRange() + " - " + ability.getMaxRange());
|
||||
sendResult(sender, " Telegraph: \u00A7e" + (ability.isShowTelegraph() ? "Yes" : "No"));
|
||||
sendResult(sender, " Allowed By: \u00A7e" + ability.getAllowedBy().name());
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "List all built-in abilities available for players"
|
||||
)
|
||||
public void prebuilts(ICommandSender sender, String[] args) {
|
||||
Set<String> names = AbilityController.Instance.getAbilityNames();
|
||||
if (names.isEmpty()) {
|
||||
sendResult(sender, "No built-in abilities found.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> playerAbilities = new ArrayList<>();
|
||||
for (String name : names) {
|
||||
Ability ability = AbilityController.Instance.getAbility(name);
|
||||
if (ability != null && ability.getAllowedBy().allowsPlayer()) {
|
||||
playerAbilities.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (playerAbilities.isEmpty()) {
|
||||
sendResult(sender, "No player-usable built-in abilities found.");
|
||||
return;
|
||||
}
|
||||
|
||||
Collections.sort(playerAbilities);
|
||||
sendResult(sender, "Built-in Player Abilities (" + playerAbilities.size() + "):");
|
||||
for (String name : playerAbilities) {
|
||||
Ability ability = AbilityController.Instance.getAbility(name);
|
||||
String userType = ability.getAllowedBy() == UserType.PLAYER_ONLY ? "\u00A7d[PLAYER]" : "\u00A7a[BOTH]";
|
||||
sendResult(sender, " - \u00A7b" + name + " " + userType);
|
||||
}
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "Unlock an ability for a player",
|
||||
usage = "<player> <ability>"
|
||||
)
|
||||
public void unlock(ICommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
sendError(sender, "Usage: /kam ability unlock <player> <ability>");
|
||||
return;
|
||||
}
|
||||
|
||||
String playerName = args[0];
|
||||
String abilityKey = joinArgs(args, 1);
|
||||
|
||||
// Find player
|
||||
EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName);
|
||||
if (player == null) {
|
||||
sendError(sender, "Player not found: " + playerName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve ability
|
||||
Ability ability = AbilityController.Instance.resolveAbility(abilityKey);
|
||||
if (ability == null) {
|
||||
sendError(sender, "Ability not found: " + abilityKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the ability's canonical ID for storage (registry key for built-in, UUID for custom)
|
||||
String canonicalKey = ability.getId();
|
||||
if (canonicalKey == null || canonicalKey.isEmpty()) {
|
||||
canonicalKey = abilityKey; // Fallback to user-provided key
|
||||
}
|
||||
String displayName = ability.getName() != null ? ability.getName() : canonicalKey;
|
||||
|
||||
// Check if ability allows players
|
||||
if (!ability.getAllowedBy().allowsPlayer()) {
|
||||
sendError(sender, "Ability '\u00A7b" + displayName + "\u00A7c' is NPC-only and cannot be given to players.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Get player data and unlock
|
||||
PlayerData data = PlayerDataController.Instance.getPlayerData(player);
|
||||
if (data.abilityData.hasUnlockedAbility(canonicalKey)) {
|
||||
sendError(sender, "Player already has ability: " + displayName);
|
||||
return;
|
||||
}
|
||||
|
||||
data.abilityData.unlockAbility(canonicalKey);
|
||||
sendResult(sender, "Unlocked ability '\u00A7b" + displayName + "\u00A77' for player \u00A7a" + playerName);
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "Lock (revoke) an ability from a player",
|
||||
usage = "<player> <ability>"
|
||||
)
|
||||
public void lock(ICommandSender sender, String[] args) {
|
||||
if (args.length < 2) {
|
||||
sendError(sender, "Usage: /kam ability lock <player> <ability>");
|
||||
return;
|
||||
}
|
||||
|
||||
String playerName = args[0];
|
||||
String abilityKey = joinArgs(args, 1);
|
||||
|
||||
// Find player
|
||||
EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName);
|
||||
if (player == null) {
|
||||
sendError(sender, "Player not found: " + playerName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve ability to get canonical key
|
||||
Ability ability = AbilityController.Instance.resolveAbility(abilityKey);
|
||||
String canonicalKey = abilityKey;
|
||||
String displayName = abilityKey;
|
||||
if (ability != null) {
|
||||
canonicalKey = ability.getId() != null ? ability.getId() : abilityKey;
|
||||
displayName = ability.getName() != null ? ability.getName() : canonicalKey;
|
||||
}
|
||||
|
||||
// Get player data and lock
|
||||
PlayerData data = PlayerDataController.Instance.getPlayerData(player);
|
||||
if (!data.abilityData.hasUnlockedAbility(canonicalKey)) {
|
||||
sendError(sender, "Player doesn't have ability: " + displayName);
|
||||
return;
|
||||
}
|
||||
|
||||
data.abilityData.lockAbility(canonicalKey);
|
||||
sendResult(sender, "Locked ability '\u00A7b" + displayName + "\u00A77' from player \u00A7a" + playerName);
|
||||
}
|
||||
|
||||
@SubCommand(
|
||||
desc = "List abilities unlocked for a player",
|
||||
usage = "<player>"
|
||||
)
|
||||
public void player(ICommandSender sender, String[] args) {
|
||||
if (args.length < 1) {
|
||||
sendError(sender, "Usage: /kam ability player <player>");
|
||||
return;
|
||||
}
|
||||
|
||||
String playerName = args[0];
|
||||
|
||||
// Find player
|
||||
EntityPlayerMP player = (EntityPlayerMP) NoppesUtilServer.getPlayerByName(playerName);
|
||||
if (player == null) {
|
||||
sendError(sender, "Player not found: " + playerName);
|
||||
return;
|
||||
}
|
||||
|
||||
PlayerData data = PlayerDataController.Instance.getPlayerData(player);
|
||||
String[] abilityKeys = data.abilityData.getUnlockedAbilities();
|
||||
|
||||
if (abilityKeys.length == 0) {
|
||||
sendResult(sender, "Player \u00A7a" + playerName + "\u00A77 has no unlocked abilities.");
|
||||
return;
|
||||
}
|
||||
|
||||
sendResult(sender, "Abilities for \u00A7a" + playerName + "\u00A77 (" + abilityKeys.length + "):");
|
||||
int selected = data.abilityData.getSelectedIndex();
|
||||
for (int i = 0; i < abilityKeys.length; i++) {
|
||||
String key = abilityKeys[i];
|
||||
Ability ability = AbilityController.Instance.resolveAbility(key);
|
||||
String displayName = ability != null && ability.getName() != null ? ability.getName() : key;
|
||||
String prefix = (i == selected) ? "\u00A7e> " : " ";
|
||||
// Show display name and key if different
|
||||
if (!displayName.equals(key)) {
|
||||
sendResult(sender, prefix + "\u00A7b" + displayName + " \u00A78[" + key + "]");
|
||||
} else {
|
||||
sendResult(sender, prefix + "\u00A7b" + displayName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String joinArgs(String[] args, int startIndex) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = startIndex; i < args.length; i++) {
|
||||
if (i > startIndex) sb.append(" ");
|
||||
sb.append(args[i]);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of player-usable ability keys (for tab completion).
|
||||
* Uses registry keys for built-in abilities (no spaces) and UUIDs for custom.
|
||||
* Used by CommandKamkeel for <ability> usage token.
|
||||
*/
|
||||
public static List<String> getPlayerAbilityNames() {
|
||||
List<String> keys = new ArrayList<>(AbilityController.Instance.getPlayerAbilityKeys());
|
||||
Collections.sort(keys);
|
||||
return keys;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static kamkeel.npcs.command.AbilityCommand.getPlayerAbilityNames;
|
||||
import static kamkeel.npcs.command.EffectCommand.getSortedEffectNames;
|
||||
|
||||
public class CommandKamkeel extends CommandBase {
|
||||
@@ -135,6 +136,9 @@ public class CommandKamkeel extends CommandBase {
|
||||
} else if (usage.equals("<effectName>")) {
|
||||
List<String> keys = getSortedEffectNames();
|
||||
return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()]));
|
||||
} else if (usage.equals("<ability>")) {
|
||||
List<String> keys = getPlayerAbilityNames();
|
||||
return CommandBase.getListOfStringsMatchingLastWord(args, keys.toArray(new String[keys.size()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import noppes.npcs.scripted.event.player.PlayerAbilityEvent;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -67,7 +68,8 @@ public abstract class Ability implements IAbility {
|
||||
|
||||
// Feedback
|
||||
protected LockMovementType lockMovement = LockMovementType.WINDUP;
|
||||
protected LockType lockType = LockType.BOTH;
|
||||
protected RotationMode rotationMode = RotationMode.FREE;
|
||||
protected LockMovementType rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
protected int windUpColor = 0x80FF4400; // Telegraph color during wind up
|
||||
protected int activeColor = 0xC0FF0000; // Telegraph warning/active color
|
||||
|
||||
@@ -100,6 +102,13 @@ public abstract class Ability implements IAbility {
|
||||
// Configurable potion effects
|
||||
protected List<AbilityEffect> effects = new ArrayList<>();
|
||||
|
||||
// Burst
|
||||
protected boolean burstEnabled = false;
|
||||
protected int burstAmount = 0; // number of EXTRA repetitions
|
||||
protected int burstDelay = 0; // ticks between bursts
|
||||
protected boolean burstReplayAnimations = true;
|
||||
protected boolean burstOverlap = false; // if true, don't wait for entity death between bursts
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// EXECUTION STATE (not saved, reset each combat)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -108,7 +117,18 @@ public abstract class Ability implements IAbility {
|
||||
protected transient int currentTick = 0;
|
||||
protected transient EntityLivingBase currentTarget;
|
||||
protected transient long executionStartTime;
|
||||
protected transient TelegraphInstance telegraphInstance;
|
||||
protected transient List<TelegraphInstance> telegraphInstances = new ArrayList<>();
|
||||
|
||||
// Burst execution state
|
||||
protected transient int burstIndex = 0;
|
||||
protected transient List<Entity> burstEntities = new ArrayList<>();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// PREVIEW STATE (client-side only, not saved)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
protected transient boolean previewMode = false;
|
||||
protected transient PreviewEntityHandler previewEntityHandler;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ABSTRACT METHODS
|
||||
@@ -148,6 +168,50 @@ public abstract class Ability implements IAbility {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override to provide variant templates for this ability type.
|
||||
* When a user creates this ability type, they will be shown a selection dialog
|
||||
* with the returned variants. If empty or single-entry, the dialog is skipped.
|
||||
*/
|
||||
public List<AbilityVariant> getVariants() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this ability type supports the burst system.
|
||||
* Override to return false for abilities that shouldn't repeat (Guard, Heal, Sweeper, etc.).
|
||||
*/
|
||||
public boolean allowBurst() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this ability supports overlap mode for burst (entities from previous bursts keep flying).
|
||||
* Override to return true for entity-spawning projectile abilities (Orb, Beam, Disc, LaserShot).
|
||||
*/
|
||||
public boolean allowOverlap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this ability is ready for auto-completion during burst overlap mode.
|
||||
* Override to delay auto-completion until all staggered projectiles have been fired.
|
||||
* @param activeTick the current tick within the ACTIVE phase
|
||||
*/
|
||||
public boolean isReadyForBurstCompletion(int activeTick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when burst re-fires without replay animations (burstReplayAnimations=false).
|
||||
* Entity-spawning abilities should override to spawn and fire entities in one step,
|
||||
* since onWindUpTick was not called (windup was skipped).
|
||||
* Default: does nothing (ability goes directly to onExecute with existing state).
|
||||
*/
|
||||
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
// Override in entity-spawning abilities (Orb, Beam, Disc, LaserShot)
|
||||
}
|
||||
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
}
|
||||
|
||||
@@ -328,6 +392,28 @@ public abstract class Ability implements IAbility {
|
||||
return 45.0f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the caster is a player (not an NPC).
|
||||
* Use this instead of checking {@code target != null} to make NPC/Player
|
||||
* code paths explicit and readable.
|
||||
* <p>
|
||||
* NPCs always have an aggro target passed to ability methods.
|
||||
* Players never do — they use look direction for aiming instead.
|
||||
*/
|
||||
protected final boolean isPlayerCaster(EntityLivingBase caster) {
|
||||
return caster instanceof EntityPlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the telegraph should remain visible during the ACTIVE phase.
|
||||
* Default: false (telegraph removed at WINDUP→ACTIVE transition).
|
||||
* Override for abilities where the active phase IS the attack (e.g., sweeping/spinning attacks)
|
||||
* so the telegraph stays visible while damage is being dealt.
|
||||
*/
|
||||
public boolean keepTelegraphDuringActive() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this ability controls NPC movement during ACTIVE phase.
|
||||
* When true, AI pathfinding will be blocked during ability execution.
|
||||
@@ -411,26 +497,33 @@ public abstract class Ability implements IAbility {
|
||||
defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks)
|
||||
.tab("General").range(0, 10000));
|
||||
defs.add(FieldDef.section("ability.section.movement").tab("General"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.stringEnumField("ability.lockPosition", LockMovementType.getDisplayKeys(),
|
||||
defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(),
|
||||
() -> this.getLockMovement().getDisplayKey(),
|
||||
v -> {
|
||||
String[] keys = LockMovementType.getDisplayKeys();
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; }
|
||||
for (LockMovementType t : LockMovementType.values()) {
|
||||
if (t.getDisplayKey().equals(v)) { this.setLockMovement(t); break; }
|
||||
}
|
||||
})
|
||||
.hover("ability.hover.lockPosition"),
|
||||
FieldDef.stringEnumField("ability.lockType", LockType.getDisplayKeys(),
|
||||
() -> this.getLockType().getDisplayKey(),
|
||||
.hover("ability.hover.lockMovement")
|
||||
.tab("General"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.stringEnumField("ability.rotationMode", RotationMode.getDisplayKeys(),
|
||||
() -> this.getRotationMode().getDisplayKey(),
|
||||
v -> {
|
||||
String[] keys = LockType.getDisplayKeys();
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
if (keys[i].equals(v)) { this.setLockType(LockType.fromOrdinal(i)); break; }
|
||||
for (RotationMode m : RotationMode.values()) {
|
||||
if (m.getDisplayKey().equals(v)) { this.setRotationMode(m); break; }
|
||||
}
|
||||
})
|
||||
.hover("ability.hover.lockType")
|
||||
.visibleWhen(() -> this.lockMovement != LockMovementType.NO)
|
||||
.hover("ability.hover.rotationMode"),
|
||||
FieldDef.stringEnumField("ability.rotationPhase", getRotationPhaseKeys(),
|
||||
() -> this.getRotationPhase().getDisplayKey(),
|
||||
v -> {
|
||||
for (LockMovementType t : LockMovementType.values()) {
|
||||
if (t.getDisplayKey().equals(v)) { this.setRotationPhase(t); break; }
|
||||
}
|
||||
})
|
||||
.hover("ability.hover.rotationPhase")
|
||||
.visibleWhen(() -> this.rotationMode != RotationMode.FREE)
|
||||
).tab("General"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible)
|
||||
@@ -439,6 +532,25 @@ public abstract class Ability implements IAbility {
|
||||
.range(0, 1000).visibleWhen(this::isInterruptible)
|
||||
).tab("General"));
|
||||
|
||||
// ── Burst section ────────────────────────────────────────────
|
||||
if (allowBurst()) {
|
||||
defs.add(FieldDef.section("ability.section.burst").tab("General"));
|
||||
defs.add(FieldDef.boolField("ability.burstEnabled", this::isBurstEnabled, this::setBurstEnabled)
|
||||
.tab("General"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.intField("ability.burstAmount", this::getBurstAmount, this::setBurstAmount).range(1, 100),
|
||||
FieldDef.intField("ability.burstDelay", this::getBurstDelay, this::setBurstDelay).range(0, 1000)
|
||||
).tab("General").visibleWhen(this::isBurstEnabled));
|
||||
defs.add(FieldDef.boolField("ability.burstReplayAnimations", this::isBurstReplayAnimations, this::setBurstReplayAnimations)
|
||||
.tab("General").visibleWhen(this::isBurstEnabled)
|
||||
.hover("ability.hover.burstReplay"));
|
||||
if (allowOverlap()) {
|
||||
defs.add(FieldDef.boolField("ability.burstOverlap", this::isBurstOverlap, this::setBurstOverlap)
|
||||
.tab("General").visibleWhen(this::isBurstEnabled)
|
||||
.hover("ability.hover.burstOverlap"));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Target tab ───────────────────────────────────────────────
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)).range(0, 100),
|
||||
@@ -580,6 +692,9 @@ public abstract class Ability implements IAbility {
|
||||
case POINT:
|
||||
telegraph = new Telegraph("", TelegraphType.POINT);
|
||||
break;
|
||||
case SQUARE:
|
||||
telegraph = Telegraph.square(getTelegraphRadius());
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -599,12 +714,12 @@ public abstract class Ability implements IAbility {
|
||||
instance.setEntityIdToFollow(caster.getEntityId());
|
||||
|
||||
// For LINE/CONE telegraphs: track target direction during windup
|
||||
// Only track if movement is NOT locked during windup (locked = NPC can't rotate)
|
||||
// Only track if rotation is NOT locked during windup (NPC can still turn)
|
||||
if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) {
|
||||
if (!lockMovement.locksWindup()) {
|
||||
if (!isRotationLockedDuringWindup()) {
|
||||
instance.setTargetEntityId(target.getEntityId());
|
||||
}
|
||||
// If windup is locked, yaw stays fixed at creation time
|
||||
// If rotation is locked, yaw stays fixed at creation time
|
||||
}
|
||||
} else if (target != null) {
|
||||
// AOE_TARGET abilities: telegraph follows target during windup
|
||||
@@ -614,6 +729,17 @@ public abstract class Ability implements IAbility {
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all telegraph instances for this ability.
|
||||
* Override for abilities that need multiple telegraphs (e.g., zone abilities).
|
||||
* Default wraps createTelegraph() in a singleton list.
|
||||
*/
|
||||
public List<TelegraphInstance> createTelegraphs(EntityLivingBase caster, EntityLivingBase target) {
|
||||
TelegraphInstance instance = createTelegraph(caster, target);
|
||||
if (instance == null) return new ArrayList<>();
|
||||
return new ArrayList<>(Collections.singletonList(instance));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the ground level at a given position.
|
||||
* Searches downward from the given Y to find a solid block.
|
||||
@@ -682,17 +808,17 @@ public abstract class Ability implements IAbility {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current telegraph instance.
|
||||
* Get the current telegraph instances.
|
||||
*/
|
||||
public TelegraphInstance getTelegraphInstance() {
|
||||
return telegraphInstance;
|
||||
public List<TelegraphInstance> getTelegraphInstances() {
|
||||
return telegraphInstances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the telegraph instance (called by DataAbilities).
|
||||
* Set the telegraph instances (called by DataAbilities).
|
||||
*/
|
||||
public void setTelegraphInstance(TelegraphInstance instance) {
|
||||
this.telegraphInstance = instance;
|
||||
public void setTelegraphInstances(List<TelegraphInstance> instances) {
|
||||
this.telegraphInstances = instances != null ? instances : new ArrayList<>();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -707,6 +833,8 @@ public abstract class Ability implements IAbility {
|
||||
this.currentTick = 0;
|
||||
this.currentTarget = target;
|
||||
this.executionStartTime = System.currentTimeMillis();
|
||||
this.burstIndex = 0;
|
||||
this.burstEntities.clear();
|
||||
|
||||
if (windUpTicks <= 0) {
|
||||
this.phase = AbilityPhase.ACTIVE;
|
||||
@@ -749,6 +877,19 @@ public abstract class Ability implements IAbility {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case BURST_DELAY:
|
||||
// Waiting between burst repetitions - free movement/rotation
|
||||
if (currentTick >= burstDelay) {
|
||||
if (burstReplayAnimations && getWindUpTicks() > 0) {
|
||||
phase = AbilityPhase.WINDUP;
|
||||
} else {
|
||||
phase = AbilityPhase.ACTIVE;
|
||||
}
|
||||
currentTick = 0;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -762,6 +903,19 @@ public abstract class Ability implements IAbility {
|
||||
*/
|
||||
public boolean signalCompletion() {
|
||||
if (phase == AbilityPhase.ACTIVE) {
|
||||
// Check if more burst iterations remain
|
||||
if (burstEnabled && burstAmount > 0 && burstIndex < burstAmount) {
|
||||
if (!burstOverlap) {
|
||||
cleanup();
|
||||
}
|
||||
burstIndex++;
|
||||
phase = AbilityPhase.BURST_DELAY;
|
||||
currentTick = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Final completion - let burst entities die naturally (don't force-kill)
|
||||
burstEntities.clear();
|
||||
cleanup();
|
||||
phase = AbilityPhase.IDLE;
|
||||
currentTick = 0;
|
||||
@@ -775,9 +929,10 @@ public abstract class Ability implements IAbility {
|
||||
* Called when NPC takes direct damage during WINDUP.
|
||||
*/
|
||||
public void interrupt() {
|
||||
cleanupBurstEntities();
|
||||
cleanup();
|
||||
if (interruptible && phase == AbilityPhase.WINDUP) {
|
||||
// Interrupted during windup - go to dazed state
|
||||
if (interruptible && (phase == AbilityPhase.WINDUP || phase == AbilityPhase.BURST_DELAY)) {
|
||||
// Interrupted during windup or burst delay - go to dazed state
|
||||
phase = AbilityPhase.DAZED;
|
||||
currentTick = 0;
|
||||
} else {
|
||||
@@ -785,8 +940,9 @@ public abstract class Ability implements IAbility {
|
||||
phase = AbilityPhase.IDLE;
|
||||
currentTick = 0;
|
||||
}
|
||||
burstIndex = 0;
|
||||
currentTarget = null;
|
||||
telegraphInstance = null;
|
||||
telegraphInstances.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -805,7 +961,7 @@ public abstract class Ability implements IAbility {
|
||||
* @return true if the ability should be interrupted
|
||||
*/
|
||||
public boolean canInterrupt(DamageSource source) {
|
||||
if (!interruptible || phase != AbilityPhase.WINDUP) {
|
||||
if (!interruptible || (phase != AbilityPhase.WINDUP && phase != AbilityPhase.BURST_DELAY)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -832,11 +988,15 @@ public abstract class Ability implements IAbility {
|
||||
* Reset all execution state (called on combat end, NPC death, target lost, etc.)
|
||||
*/
|
||||
public void reset() {
|
||||
cleanupBurstEntities();
|
||||
cleanup();
|
||||
phase = AbilityPhase.IDLE;
|
||||
currentTick = 0;
|
||||
burstIndex = 0;
|
||||
currentTarget = null;
|
||||
telegraphInstance = null;
|
||||
telegraphInstances.clear();
|
||||
previewMode = false;
|
||||
previewEntityHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -848,91 +1008,73 @@ public abstract class Ability implements IAbility {
|
||||
// Override in subclasses to clean up spawned entities, etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill all entities tracked during burst overlap and clear the list.
|
||||
*/
|
||||
protected void cleanupBurstEntities() {
|
||||
for (Entity e : burstEntities) {
|
||||
if (e != null && !e.isDead) {
|
||||
killAbilityEntity(e);
|
||||
}
|
||||
}
|
||||
burstEntities.clear();
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// CLIENT-SIDE PREVIEW METHODS
|
||||
// PREVIEW MODE SUPPORT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Get total active phase duration for preview (in ticks).
|
||||
* Override in subclasses to provide accurate preview duration.
|
||||
*
|
||||
* @return Duration of active phase in ticks (default: 40 = 2 seconds)
|
||||
* Whether this ability is running in preview mode (GUI preview).
|
||||
* When true, abilities should skip damage, effects, sounds, and particles
|
||||
* but still run their core logic (entity spawning, movement, timing).
|
||||
*/
|
||||
public int getPreviewActiveDuration() {
|
||||
return 40;
|
||||
public boolean isPreview() {
|
||||
return previewMode;
|
||||
}
|
||||
|
||||
public void setPreviewMode(boolean preview) {
|
||||
this.previewMode = preview;
|
||||
}
|
||||
|
||||
public void setPreviewEntityHandler(PreviewEntityHandler handler) {
|
||||
this.previewEntityHandler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during preview windup tick (client-side only).
|
||||
* Override for visual updates during windup preview.
|
||||
* Do NOT apply damage or world effects here.
|
||||
*
|
||||
* @param npc The preview NPC
|
||||
* @param tick Current tick within windup phase
|
||||
* Spawn an entity during ability execution.
|
||||
* In preview mode, routes to PreviewEntityHandler instead of world.spawnEntityInWorld().
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewWindUpTick(EntityNPCInterface npc, int tick) {
|
||||
// Default: no-op. Override in subclasses for visual effects.
|
||||
protected void spawnAbilityEntity(World world, Entity entity) {
|
||||
if (previewMode && previewEntityHandler != null) {
|
||||
previewEntityHandler.onEntitySpawned(entity);
|
||||
} else {
|
||||
world.spawnEntityInWorld(entity);
|
||||
}
|
||||
if (burstEnabled && burstOverlap) {
|
||||
burstEntities.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called during preview active tick (client-side only).
|
||||
* Override for visual updates during active preview.
|
||||
* Do NOT apply damage or world effects here.
|
||||
*
|
||||
* @param npc The preview NPC
|
||||
* @param tick Current tick within active phase
|
||||
* Kill an entity spawned by this ability.
|
||||
* In preview mode, notifies PreviewEntityHandler of removal.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
|
||||
// Default: no-op. Override in subclasses for visual effects.
|
||||
protected void killAbilityEntity(Entity entity) {
|
||||
if (entity == null) return;
|
||||
entity.setDead();
|
||||
if (previewMode && previewEntityHandler != null) {
|
||||
previewEntityHandler.onEntityRemoved(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a preview entity for GUI display (client-side only).
|
||||
* The entity should be in preview mode and not apply damage.
|
||||
* Override in abilities that spawn entities (Beam, Orb, Disc, etc.)
|
||||
*
|
||||
* @param npc The preview NPC
|
||||
* @return Preview entity, or null if ability doesn't spawn entities
|
||||
* Maximum duration (in ticks) for preview before auto-stopping.
|
||||
* Override in movement abilities that have variable duration.
|
||||
* Default: 200 ticks (10 seconds).
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
return null; // Override in entity-spawning abilities
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to spawn preview entity during WINDUP phase (true) or ACTIVE phase (false).
|
||||
* Most abilities (Orb, Beam, Disc) spawn during windup for charging effect.
|
||||
* Laser spawns at active phase since it has no charging state.
|
||||
*/
|
||||
public boolean spawnPreviewDuringWindup() {
|
||||
return true; // Default: spawn during windup for charging
|
||||
}
|
||||
|
||||
// ── Preview target (set by executor for abilities that need targeting) ──
|
||||
|
||||
protected transient EntityLivingBase previewTarget;
|
||||
|
||||
/**
|
||||
* Set the fake target entity for preview mode.
|
||||
* Called by AbilityPreviewExecutor before starting preview.
|
||||
*/
|
||||
public void setPreviewTarget(EntityLivingBase target) {
|
||||
this.previewTarget = target;
|
||||
}
|
||||
|
||||
public EntityLivingBase getPreviewTarget() {
|
||||
return previewTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called once when transitioning from WINDUP to ACTIVE in preview mode.
|
||||
* Override in movement-based abilities to initiate movement.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewExecute(EntityNPCInterface npc) {
|
||||
// Default: no-op. Override in movement abilities.
|
||||
public int getMaxPreviewDuration() {
|
||||
return 200;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -978,7 +1120,8 @@ public abstract class Ability implements IAbility {
|
||||
nbt.setInteger("recovery", dazedTicks);
|
||||
nbt.setBoolean("interruptible", interruptible);
|
||||
nbt.setInteger("lockMovement", lockMovement.ordinal());
|
||||
nbt.setInteger("lockType", lockType.ordinal());
|
||||
nbt.setInteger("rotationMode", rotationMode.ordinal());
|
||||
nbt.setInteger("rotationPhase", rotationPhase.ordinal());
|
||||
nbt.setInteger("windUpColor", windUpColor);
|
||||
nbt.setInteger("activeColor", activeColor);
|
||||
nbt.setString("windUpSound", windUpSound);
|
||||
@@ -994,6 +1137,13 @@ public abstract class Ability implements IAbility {
|
||||
nbt.setInteger("allowedBy", allowedBy.ordinal());
|
||||
nbt.setBoolean("ignoreCooldown", ignoreCooldown);
|
||||
|
||||
// Burst
|
||||
nbt.setBoolean("burstEnabled", burstEnabled);
|
||||
nbt.setInteger("burstAmount", burstAmount);
|
||||
nbt.setInteger("burstDelay", burstDelay);
|
||||
nbt.setBoolean("burstReplayAnimations", burstReplayAnimations);
|
||||
nbt.setBoolean("burstOverlap", burstOverlap);
|
||||
|
||||
// Conditions
|
||||
NBTTagList condList = new NBTTagList();
|
||||
for (Condition c : conditions) condList.appendTag(c.writeNBT());
|
||||
@@ -1029,7 +1179,30 @@ public abstract class Ability implements IAbility {
|
||||
dazedTicks = nbt.getInteger("recovery");
|
||||
interruptible = nbt.getBoolean("interruptible");
|
||||
lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement"));
|
||||
lockType = nbt.hasKey("lockType") ? LockType.fromOrdinal(nbt.getInteger("lockType")) : LockType.BOTH;
|
||||
if (nbt.hasKey("rotationMode")) {
|
||||
// Current format
|
||||
rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode"));
|
||||
rotationPhase = LockMovementType.fromOrdinal(nbt.getInteger("rotationPhase"));
|
||||
} else if (nbt.hasKey("lockType")) {
|
||||
// Legacy format migration: lockType was LockType enum (0=MOVEMENT, 1=ROTATION, 2=BOTH)
|
||||
int oldLockType = nbt.getInteger("lockType");
|
||||
if (oldLockType == 0) {
|
||||
// MOVEMENT only: movement locked, rotation was free
|
||||
rotationMode = RotationMode.FREE;
|
||||
} else if (oldLockType == 1) {
|
||||
// ROTATION only: rotation locked same timing as old lockMovement, movement was free
|
||||
rotationMode = RotationMode.LOCKED;
|
||||
rotationPhase = lockMovement;
|
||||
lockMovement = LockMovementType.NO;
|
||||
} else {
|
||||
// BOTH: both locked same timing
|
||||
rotationMode = RotationMode.LOCKED;
|
||||
rotationPhase = lockMovement;
|
||||
}
|
||||
} else {
|
||||
rotationMode = RotationMode.FREE;
|
||||
rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
}
|
||||
windUpColor = nbt.getInteger("windUpColor");
|
||||
activeColor = nbt.getInteger("activeColor");
|
||||
windUpSound = nbt.getString("windUpSound");
|
||||
@@ -1051,6 +1224,13 @@ public abstract class Ability implements IAbility {
|
||||
allowedBy = nbt.hasKey("allowedBy") ? UserType.fromOrdinal(nbt.getInteger("allowedBy")) : UserType.BOTH;
|
||||
ignoreCooldown = nbt.hasKey("ignoreCooldown") && nbt.getBoolean("ignoreCooldown");
|
||||
|
||||
// Burst
|
||||
burstEnabled = nbt.hasKey("burstEnabled") && nbt.getBoolean("burstEnabled");
|
||||
burstAmount = nbt.hasKey("burstAmount") ? nbt.getInteger("burstAmount") : 0;
|
||||
burstDelay = nbt.hasKey("burstDelay") ? nbt.getInteger("burstDelay") : 0;
|
||||
burstReplayAnimations = !nbt.hasKey("burstReplayAnimations") || nbt.getBoolean("burstReplayAnimations");
|
||||
burstOverlap = nbt.hasKey("burstOverlap") && nbt.getBoolean("burstOverlap");
|
||||
|
||||
// Conditions
|
||||
conditions.clear();
|
||||
NBTTagList condList = nbt.getTagList("conditions", 10);
|
||||
@@ -1098,6 +1278,10 @@ public abstract class Ability implements IAbility {
|
||||
return typeId;
|
||||
}
|
||||
|
||||
public boolean isBuiltIn() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getWeight() {
|
||||
return weight;
|
||||
}
|
||||
@@ -1194,12 +1378,20 @@ public abstract class Ability implements IAbility {
|
||||
this.lockMovement = lockMovement;
|
||||
}
|
||||
|
||||
public LockType getLockType() {
|
||||
return lockType;
|
||||
public RotationMode getRotationMode() {
|
||||
return rotationMode;
|
||||
}
|
||||
|
||||
public void setLockType(LockType lockType) {
|
||||
this.lockType = lockType;
|
||||
public void setRotationMode(RotationMode rotationMode) {
|
||||
this.rotationMode = rotationMode;
|
||||
}
|
||||
|
||||
public LockMovementType getRotationPhase() {
|
||||
return rotationPhase;
|
||||
}
|
||||
|
||||
public void setRotationPhase(LockMovementType rotationPhase) {
|
||||
this.rotationPhase = rotationPhase;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1221,21 +1413,39 @@ public abstract class Ability implements IAbility {
|
||||
}
|
||||
|
||||
/**
|
||||
* API method: Get lock type as integer.
|
||||
* @return 0=MOVEMENT, 1=ROTATION, 2=BOTH
|
||||
* API method: Get rotation mode as integer.
|
||||
* @return 0=FREE, 1=LOCKED, 2=TRACK
|
||||
*/
|
||||
@Override
|
||||
public int getLockTypeOrdinal() {
|
||||
return lockType.ordinal();
|
||||
public int getRotationModeType() {
|
||||
return rotationMode.ordinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* API method: Set lock type from integer.
|
||||
* @param type 0=MOVEMENT, 1=ROTATION, 2=BOTH
|
||||
* API method: Set rotation mode from integer.
|
||||
* @param type 0=FREE, 1=LOCKED, 2=TRACK
|
||||
*/
|
||||
@Override
|
||||
public void setLockTypeOrdinal(int type) {
|
||||
this.lockType = LockType.fromOrdinal(type);
|
||||
public void setRotationModeType(int type) {
|
||||
this.rotationMode = RotationMode.fromOrdinal(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* API method: Get rotation phase as integer.
|
||||
* @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
|
||||
*/
|
||||
@Override
|
||||
public int getRotationPhaseType() {
|
||||
return rotationPhase.ordinal();
|
||||
}
|
||||
|
||||
/**
|
||||
* API method: Set rotation phase from integer.
|
||||
* @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE
|
||||
*/
|
||||
@Override
|
||||
public void setRotationPhaseType(int type) {
|
||||
this.rotationPhase = LockMovementType.fromOrdinal(type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1243,7 +1453,7 @@ public abstract class Ability implements IAbility {
|
||||
*/
|
||||
@Override
|
||||
public boolean isMovementLockedDuringWindup() {
|
||||
return lockMovement.locksWindup() && lockType.locksMovement();
|
||||
return lockMovement.locksWindup();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1251,7 +1461,7 @@ public abstract class Ability implements IAbility {
|
||||
*/
|
||||
@Override
|
||||
public boolean isMovementLockedDuringActive() {
|
||||
return lockMovement.locksActive() && lockType.locksMovement();
|
||||
return lockMovement.locksActive();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1259,7 +1469,7 @@ public abstract class Ability implements IAbility {
|
||||
*/
|
||||
@Override
|
||||
public boolean isRotationLockedDuringWindup() {
|
||||
return lockMovement.locksWindup() && lockType.locksRotation();
|
||||
return rotationMode == RotationMode.LOCKED && rotationPhase.locksWindup();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1267,7 +1477,7 @@ public abstract class Ability implements IAbility {
|
||||
*/
|
||||
@Override
|
||||
public boolean isRotationLockedDuringActive() {
|
||||
return lockMovement.locksActive() && lockType.locksRotation();
|
||||
return rotationMode == RotationMode.LOCKED && rotationPhase.locksActive();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1279,6 +1489,8 @@ public abstract class Ability implements IAbility {
|
||||
return isMovementLockedDuringWindup();
|
||||
case ACTIVE:
|
||||
return isMovementLockedDuringActive();
|
||||
case BURST_DELAY:
|
||||
return false; // Free movement during burst delay
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -1293,6 +1505,25 @@ public abstract class Ability implements IAbility {
|
||||
return isRotationLockedDuringWindup();
|
||||
case ACTIVE:
|
||||
return isRotationLockedDuringActive();
|
||||
case BURST_DELAY:
|
||||
return false; // Free rotation during burst delay
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if hit scan (force face target) is active during the current phase.
|
||||
*/
|
||||
public boolean isHitScanForCurrentPhase() {
|
||||
if (rotationMode != RotationMode.TRACK) return false;
|
||||
switch (phase) {
|
||||
case WINDUP:
|
||||
return rotationPhase.locksWindup();
|
||||
case ACTIVE:
|
||||
return rotationPhase.locksActive();
|
||||
case BURST_DELAY:
|
||||
return false; // Free rotation during burst delay
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -1481,6 +1712,63 @@ public abstract class Ability implements IAbility {
|
||||
effects.clear();
|
||||
}
|
||||
|
||||
// Burst getters/setters
|
||||
|
||||
public boolean isBurstEnabled() {
|
||||
return burstEnabled;
|
||||
}
|
||||
|
||||
public void setBurstEnabled(boolean burstEnabled) {
|
||||
this.burstEnabled = burstEnabled;
|
||||
}
|
||||
|
||||
public int getBurstAmount() {
|
||||
return burstAmount;
|
||||
}
|
||||
|
||||
public void setBurstAmount(int burstAmount) {
|
||||
this.burstAmount = Math.max(0, burstAmount);
|
||||
}
|
||||
|
||||
public int getBurstDelay() {
|
||||
return burstDelay;
|
||||
}
|
||||
|
||||
public void setBurstDelay(int burstDelay) {
|
||||
this.burstDelay = Math.max(0, burstDelay);
|
||||
}
|
||||
|
||||
public boolean isBurstReplayAnimations() {
|
||||
return burstReplayAnimations;
|
||||
}
|
||||
|
||||
public void setBurstReplayAnimations(boolean burstReplayAnimations) {
|
||||
this.burstReplayAnimations = burstReplayAnimations;
|
||||
}
|
||||
|
||||
public boolean isBurstOverlap() {
|
||||
return burstOverlap;
|
||||
}
|
||||
|
||||
public void setBurstOverlap(boolean burstOverlap) {
|
||||
this.burstOverlap = burstOverlap;
|
||||
}
|
||||
|
||||
public int getBurstIndex() {
|
||||
return burstIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get display keys for rotation phase dropdown (excludes "None" since mode=FREE handles that).
|
||||
*/
|
||||
private static String[] getRotationPhaseKeys() {
|
||||
return new String[]{
|
||||
"ability.lockMove.windup",
|
||||
"ability.lockMove.active",
|
||||
"ability.lockMove.both"
|
||||
};
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SHIT FROM THE ASS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -45,6 +45,12 @@ public class AbilityController implements IAbilityHandler {
|
||||
/** External field providers for ability GUI (e.g., DBC Addon). Client-side only. */
|
||||
private final List<IAbilityFieldProvider> fieldProviders = new ArrayList<>();
|
||||
|
||||
/** External variant definitions registered by addons, keyed by typeId. */
|
||||
private final Map<String, List<AbilityVariant>> externalVariants = new LinkedHashMap<>();
|
||||
|
||||
/** TypeIds that produce BuiltInAbility instances (non-editable in GUI). */
|
||||
private final Set<String> builtInTypeIds = new HashSet<>();
|
||||
|
||||
public AbilityController() {
|
||||
registerBuiltinTypes();
|
||||
}
|
||||
@@ -170,7 +176,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
if (abilities.containsKey(name)) {
|
||||
LogWriter.info("AbilityController: Overwriting built-in ability: " + name);
|
||||
}
|
||||
ability.setName(name);
|
||||
if (!ability.isBuiltIn()) {
|
||||
ability.setName(name);
|
||||
}
|
||||
abilities.put(name, ability);
|
||||
LogWriter.info("Registered ability: " + name);
|
||||
}
|
||||
@@ -180,30 +188,93 @@ public class AbilityController implements IAbilityHandler {
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Resolve an ability by key. Checks built-in abilities first (by name),
|
||||
* then custom abilities (by UUID). Returns a copy safe to modify.
|
||||
* Resolve an ability by key. Checks built-in abilities first (by name or registry key),
|
||||
* then custom abilities (by UUID or name). Returns a copy safe to modify for
|
||||
* custom abilities, or the singleton instance for built-in abilities.
|
||||
*
|
||||
* @param key The ability name (built-in) or UUID (custom)
|
||||
* @return A copy of the resolved ability, or null if not found
|
||||
* @param key The ability name, registry key (built-in) or UUID (custom)
|
||||
* @return The resolved ability, or null if not found
|
||||
*/
|
||||
public Ability resolveAbility(String key) {
|
||||
if (key == null || key.isEmpty()) return null;
|
||||
|
||||
// Check built-in abilities first
|
||||
// Check built-in abilities first (exact match by name)
|
||||
Ability builtIn = abilities.get(key);
|
||||
if (builtIn != null) {
|
||||
return fromNBT(builtIn.writeNBT());
|
||||
return builtIn;
|
||||
}
|
||||
|
||||
// Check custom abilities
|
||||
// Check built-in abilities by name (case-insensitive)
|
||||
for (Map.Entry<String, Ability> entry : abilities.entrySet()) {
|
||||
if (entry.getKey().equalsIgnoreCase(key)) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
|
||||
// Check built-in abilities by registry key / ID
|
||||
for (Ability ability : abilities.values()) {
|
||||
if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) {
|
||||
return ability;
|
||||
}
|
||||
}
|
||||
|
||||
// Check custom abilities (exact UUID match)
|
||||
Ability custom = customAbilities.get(key);
|
||||
if (custom != null) {
|
||||
return fromNBT(custom.writeNBT());
|
||||
}
|
||||
|
||||
// Check custom abilities by name (case-insensitive)
|
||||
for (Ability ability : customAbilities.values()) {
|
||||
if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) {
|
||||
return fromNBT(ability.writeNBT());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keys that can be used to look up abilities (for tab completion).
|
||||
* Returns registry keys for built-in abilities (no spaces) and UUIDs for custom.
|
||||
*/
|
||||
public Set<String> getAbilityKeys() {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
// Built-in abilities - use the ID (registry key for BuiltInAbility)
|
||||
for (Ability ability : abilities.values()) {
|
||||
String id = ability.getId();
|
||||
if (id != null && !id.isEmpty()) {
|
||||
keys.add(id);
|
||||
}
|
||||
}
|
||||
// Custom abilities - use UUID
|
||||
keys.addAll(customAbilities.keySet());
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get keys for player-usable abilities only (for tab completion).
|
||||
*/
|
||||
public Set<String> getPlayerAbilityKeys() {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
// Built-in abilities - use the ID (registry key for BuiltInAbility)
|
||||
for (Ability ability : abilities.values()) {
|
||||
if (ability.getAllowedBy().allowsPlayer()) {
|
||||
String id = ability.getId();
|
||||
if (id != null && !id.isEmpty()) {
|
||||
keys.add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Custom abilities - use UUID (if player-usable)
|
||||
for (Map.Entry<String, Ability> entry : customAbilities.entrySet()) {
|
||||
if (entry.getValue().getAllowedBy().allowsPlayer()) {
|
||||
keys.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an ability key exists (either built-in or custom).
|
||||
*/
|
||||
@@ -215,12 +286,22 @@ public class AbilityController implements IAbilityHandler {
|
||||
// ACCESSORS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Get a built-in ability by name. Returns the stored instance (not a copy). */
|
||||
/** Get a built-in ability by map key. Returns the stored instance (not a copy). */
|
||||
public Ability getAbility(String name) {
|
||||
return abilities.get(name);
|
||||
}
|
||||
|
||||
/** Get all built-in ability names. */
|
||||
/** Get a built-in ability by its display name (ability.getName()). */
|
||||
public Ability getAbilityByDisplayName(String displayName) {
|
||||
for (Ability ability : abilities.values()) {
|
||||
if (ability.getName().equals(displayName)) {
|
||||
return ability;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Get all built-in ability map keys. */
|
||||
public Set<String> getAbilityNames() {
|
||||
return new LinkedHashSet<>(abilities.keySet());
|
||||
}
|
||||
@@ -272,6 +353,10 @@ public class AbilityController implements IAbilityHandler {
|
||||
return typeFactories.keySet().toArray(new String[0]);
|
||||
}
|
||||
|
||||
public boolean isBuiltInType(String typeId) {
|
||||
return builtInTypeIds.contains(typeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasType(String typeId) {
|
||||
return typeFactories.containsKey(typeId);
|
||||
@@ -320,6 +405,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
LogWriter.info("AbilityController: Overwriting type: " + typeId);
|
||||
}
|
||||
typeFactories.put(typeId, factory);
|
||||
if (temp.isBuiltIn()) {
|
||||
builtInTypeIds.add(typeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -369,13 +457,9 @@ public class AbilityController implements IAbilityHandler {
|
||||
registerType("cnpc:sweeper", AbilitySweeper::new);
|
||||
registerType("cnpc:projectile", AbilityProjectile::new);
|
||||
registerType("cnpc:orb", AbilityOrb::new);
|
||||
registerType("cnpc:dual_orb", AbilityOrbDual::new);
|
||||
registerType("cnpc:orb_barrage", AbilityOrbBarrage::new);
|
||||
registerType("cnpc:dual_disc", AbilityDisc::new);
|
||||
registerType("cnpc:dual_disc", AbilityDiscDual::new);
|
||||
registerType("cnpc:disc", AbilityDisc::new);
|
||||
registerType("cnpc:laser_shot", AbilityLaserShot::new);
|
||||
registerType("cnpc:beam", AbilityBeam::new);
|
||||
registerType("cnpc:dual_beam", AbilityBeamDual::new);
|
||||
|
||||
// Movement abilities
|
||||
registerType("cnpc:charge", AbilityCharge::new);
|
||||
@@ -427,4 +511,41 @@ public class AbilityController implements IAbilityHandler {
|
||||
public List<IAbilityFieldProvider> getFieldProviders() {
|
||||
return fieldProviders;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// EXTERNAL VARIANT REGISTRY
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Register an external variant for an ability type.
|
||||
* External mods can call this to add variant presets that appear
|
||||
* alongside built-in variants in the variant selection SubGui.
|
||||
*
|
||||
* @param typeId The ability typeId (e.g., "ability.cnpc.beam")
|
||||
* @param variant The variant to register
|
||||
*/
|
||||
public void registerVariant(String typeId, AbilityVariant variant) {
|
||||
externalVariants.computeIfAbsent(typeId, k -> new ArrayList<>()).add(variant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all variants for a given ability type (built-in + external).
|
||||
* Returns the combined list of variants from the ability's getVariants()
|
||||
* and any externally registered variants.
|
||||
*
|
||||
* @param typeId The ability typeId
|
||||
* @return Combined list of variants, empty if none
|
||||
*/
|
||||
public List<AbilityVariant> getVariantsForType(String typeId) {
|
||||
List<AbilityVariant> result = new ArrayList<>();
|
||||
Ability temp = create(typeId);
|
||||
if (temp != null) {
|
||||
result.addAll(temp.getVariants());
|
||||
}
|
||||
List<AbilityVariant> ext = externalVariants.get(typeId);
|
||||
if (ext != null) {
|
||||
result.addAll(ext);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ public class AbilityEffect {
|
||||
BLINDNESS("potion.blindness", Potion.blindness.id),
|
||||
NAUSEA("potion.confusion", Potion.confusion.id),
|
||||
HUNGER("potion.hunger", Potion.hunger.id),
|
||||
MINING_FATIGUE("potion.digSlowDown", Potion.digSlowdown.id);
|
||||
MINING_FATIGUE("potion.digSlowDown", Potion.digSlowdown.id),
|
||||
BURN("ability.effect.burn", -1);
|
||||
|
||||
private final String langKey;
|
||||
private final int potionId;
|
||||
@@ -83,6 +84,11 @@ public class AbilityEffect {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == EffectType.BURN) {
|
||||
entity.setFire(Math.max(1, durationTicks / 20));
|
||||
return;
|
||||
}
|
||||
|
||||
int potionId = type.getPotionId();
|
||||
if (potionId >= 0) {
|
||||
entity.addPotionEffect(new PotionEffect(potionId, durationTicks, amplifier));
|
||||
|
||||
@@ -4,5 +4,6 @@ public enum AbilityPhase {
|
||||
IDLE, // Not executing
|
||||
WINDUP, // Charging up (telegraph visible)
|
||||
ACTIVE, // Executing (damage happens)
|
||||
DAZED // Stunned after interrupt during WINDUP (cannot attack)
|
||||
DAZED, // Stunned after interrupt during WINDUP (cannot attack)
|
||||
BURST_DELAY // Waiting between burst repetitions (free movement/rotation)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Represents a variant/template for an ability type.
|
||||
* When a user creates an ability that has registered variants,
|
||||
* they are presented with a selection dialog before the ability is created.
|
||||
*
|
||||
* Each variant has a display name (lang key) and a configurator that
|
||||
* applies preset values to the newly created ability.
|
||||
*/
|
||||
public class AbilityVariant {
|
||||
|
||||
private final String displayKey;
|
||||
private final String group;
|
||||
private final Consumer<Ability> configurator;
|
||||
|
||||
/**
|
||||
* @param displayKey Lang key for the variant display name (e.g., "ability.variant.dual")
|
||||
* @param configurator Lambda that configures the ability with preset values
|
||||
*/
|
||||
public AbilityVariant(String displayKey, Consumer<Ability> configurator) {
|
||||
this(displayKey, null, configurator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param displayKey Lang key for the variant display name
|
||||
* @param group Optional group header shown in the variant selection GUI (e.g., "DBC Addon")
|
||||
* @param configurator Lambda that configures the ability with preset values
|
||||
*/
|
||||
public AbilityVariant(String displayKey, String group, Consumer<Ability> configurator) {
|
||||
this.displayKey = displayKey;
|
||||
this.group = group;
|
||||
this.configurator = configurator;
|
||||
}
|
||||
|
||||
public String getDisplayKey() {
|
||||
return displayKey;
|
||||
}
|
||||
|
||||
public String getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply this variant's preset configuration to the given ability.
|
||||
*/
|
||||
public void apply(Ability ability) {
|
||||
if (configurator != null) {
|
||||
configurator.accept(ability);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ public enum AnchorPoint {
|
||||
RIGHT_HAND("Right Hand"), // Right arm/hand position
|
||||
LEFT_HAND("Left Hand"), // Left arm/hand position
|
||||
ABOVE_HEAD("Above Head"), // Above the entity
|
||||
CHEST("Chest"); // Chest level, centered
|
||||
CHEST("Chest"), // Chest level, centered
|
||||
EYE("Eye"); // Eye level, centered
|
||||
|
||||
private final String displayName;
|
||||
|
||||
@@ -55,6 +56,7 @@ public enum AnchorPoint {
|
||||
case LEFT_HAND: return "ability.anchor.leftHand";
|
||||
case ABOVE_HEAD: return "ability.anchor.aboveHead";
|
||||
case CHEST: return "ability.anchor.chest";
|
||||
case EYE: return "ability.anchor.eye";
|
||||
default: return name();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,13 @@ public abstract class BuiltInAbility extends Ability {
|
||||
this.registryKey = registryKey;
|
||||
this.id = registryKey;
|
||||
this.name = registryKey;
|
||||
this.typeId = registryKey;
|
||||
// Convert "namespace:name" to "ability.namespace.name" for GUI filter compatibility
|
||||
this.typeId = "ability." + registryKey.replace(':', '.');
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBuiltIn() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import net.minecraft.entity.Entity;
|
||||
|
||||
/**
|
||||
* Interface for routing entity spawn/kill calls during preview mode.
|
||||
* In preview mode, entities are not added to the world but instead
|
||||
* tracked by the GUI for rendering in the 3D viewport.
|
||||
*/
|
||||
public interface PreviewEntityHandler {
|
||||
void onEntitySpawned(Entity entity);
|
||||
void onEntityRemoved(Entity entity);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
/**
|
||||
* Defines what happens to NPC rotation during ability phases.
|
||||
* Used with a LockMovementType to control WHEN it applies.
|
||||
*/
|
||||
public enum RotationMode {
|
||||
/**
|
||||
* NPC rotates normally via AI look helper.
|
||||
*/
|
||||
FREE,
|
||||
|
||||
/**
|
||||
* Freeze rotation at the moment the phase starts.
|
||||
* NPC stays facing the direction it was looking when the phase began.
|
||||
*/
|
||||
LOCKED,
|
||||
|
||||
/**
|
||||
* Force NPC to continuously face its target (hit scan).
|
||||
* Overrides AI look helper every tick for precise aiming.
|
||||
*/
|
||||
TRACK;
|
||||
|
||||
public String getDisplayKey() {
|
||||
switch (this) {
|
||||
case FREE:
|
||||
return "ability.rotation.free";
|
||||
case LOCKED:
|
||||
return "ability.rotation.locked";
|
||||
case TRACK:
|
||||
return "ability.rotation.track";
|
||||
default:
|
||||
return "ability.rotation.free";
|
||||
}
|
||||
}
|
||||
|
||||
public static String[] getDisplayKeys() {
|
||||
return new String[]{
|
||||
"ability.rotation.free",
|
||||
"ability.rotation.locked",
|
||||
"ability.rotation.track"
|
||||
};
|
||||
}
|
||||
|
||||
public static RotationMode fromOrdinal(int ordinal) {
|
||||
RotationMode[] values = values();
|
||||
if (ordinal >= 0 && ordinal < values.length) {
|
||||
return values[ordinal];
|
||||
}
|
||||
return FREE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package kamkeel.npcs.controllers.data.ability.data;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
/**
|
||||
* Per-projectile data: anchor point + optional color override.
|
||||
* All projectiles share the primary EnergyDisplayData; this class
|
||||
* allows individual projectiles to override just the inner/outer colors.
|
||||
*/
|
||||
public class ProjectileData {
|
||||
public EnergyAnchorData anchor;
|
||||
public boolean colorOverride = false;
|
||||
public int innerColor = 0xFFFFFF;
|
||||
public int outerColor = 0x8888FF;
|
||||
|
||||
public ProjectileData(AnchorPoint defaultAnchor) {
|
||||
this.anchor = new EnergyAnchorData(defaultAnchor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the display data to use for this projectile.
|
||||
* If colorOverride is false, returns the primary display data directly.
|
||||
* If colorOverride is true, returns a copy with overridden inner/outer colors.
|
||||
*/
|
||||
public EnergyDisplayData resolveDisplay(EnergyDisplayData primary) {
|
||||
if (!colorOverride) return primary;
|
||||
EnergyDisplayData resolved = primary.copy();
|
||||
resolved.innerColor = this.innerColor;
|
||||
resolved.outerColor = this.outerColor;
|
||||
return resolved;
|
||||
}
|
||||
|
||||
public void writeNBT(NBTTagCompound nbt) {
|
||||
anchor.writeNBT(nbt);
|
||||
nbt.setBoolean("colorOverride", colorOverride);
|
||||
if (colorOverride) {
|
||||
nbt.setInteger("overrideInnerColor", innerColor);
|
||||
nbt.setInteger("overrideOuterColor", outerColor);
|
||||
}
|
||||
}
|
||||
|
||||
public void readNBT(NBTTagCompound nbt) {
|
||||
anchor.readNBT(nbt);
|
||||
colorOverride = nbt.getBoolean("colorOverride");
|
||||
if (colorOverride) {
|
||||
innerColor = nbt.hasKey("overrideInnerColor") ? nbt.getInteger("overrideInnerColor") : 0xFFFFFF;
|
||||
outerColor = nbt.hasKey("overrideOuterColor") ? nbt.getInteger("overrideOuterColor") : 0x8888FF;
|
||||
}
|
||||
}
|
||||
|
||||
public ProjectileData copy() {
|
||||
ProjectileData copy = new ProjectileData(anchor.anchorPoint);
|
||||
copy.anchor = anchor.copy();
|
||||
copy.colorOverride = colorOverride;
|
||||
copy.innerColor = innerColor;
|
||||
copy.outerColor = outerColor;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -23,4 +23,5 @@ public class AbilityFieldDefs {
|
||||
() -> getter.get(),
|
||||
v -> setter.accept((List<AbilityEffect>) v));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
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.*;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
@@ -12,7 +13,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityAbilityBeam;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.Vec3;
|
||||
@@ -23,30 +23,39 @@ import noppes.npcs.api.ability.type.IAbilityEnergyBeam;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
|
||||
/**
|
||||
* Energy Beam ability: A homing head with a trailing path.
|
||||
* The beam stays attached to the origin and curves as it homes in on target.
|
||||
* Cannot cross over itself (self-intersection prevention).
|
||||
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
|
||||
*/
|
||||
public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
|
||||
// Shape properties (standalone)
|
||||
private static final int MAX_PROJECTILES = 8;
|
||||
|
||||
// Beam shape properties (shared)
|
||||
private float beamWidth = 0.4f;
|
||||
private float headSize = 0.6f;
|
||||
|
||||
// Data classes
|
||||
private final EnergyDisplayData colorData = new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f);
|
||||
// Projectile count and fire stagger
|
||||
private int projectileCount = 1;
|
||||
private int fireDelay = 0;
|
||||
|
||||
// Shared visual data (all projectiles use these)
|
||||
private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f);
|
||||
private EnergyLightningData lightningData = new EnergyLightningData();
|
||||
private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
|
||||
|
||||
// Shared combat/movement data
|
||||
private final EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f);
|
||||
private final EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f);
|
||||
private final EnergyLightningData lightningData = new EnergyLightningData();
|
||||
private final EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200);
|
||||
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
|
||||
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
|
||||
|
||||
// Transient state for beam entity (used during windup charging)
|
||||
private transient EntityAbilityBeam beamEntity = null;
|
||||
// Per-projectile data (anchor + optional color override)
|
||||
private ProjectileData[] projectiles;
|
||||
|
||||
// Transient entity state
|
||||
private transient EntityAbilityBeam[] beamEntities;
|
||||
|
||||
public AbilityBeam() {
|
||||
this.typeId = "ability.cnpc.beam";
|
||||
@@ -57,11 +66,93 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 40;
|
||||
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.rotationMode = RotationMode.TRACK;
|
||||
this.rotationPhase = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.showTelegraph = true;
|
||||
// Default built-in animations
|
||||
this.windUpAnimationName = "Ability_Beam_Windup";
|
||||
this.activeAnimationName = "Ability_Beam_Active";
|
||||
initProjectiles(1);
|
||||
}
|
||||
|
||||
private void initProjectiles(int count) {
|
||||
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
|
||||
this.projectiles = new ProjectileData[projectileCount];
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
|
||||
}
|
||||
}
|
||||
|
||||
private AnchorPoint getDefaultAnchor(int index) {
|
||||
switch (index) {
|
||||
case 0: return AnchorPoint.RIGHT_HAND;
|
||||
case 1: return AnchorPoint.LEFT_HAND;
|
||||
default: return AnchorPoint.FRONT;
|
||||
}
|
||||
}
|
||||
|
||||
public void setProjectileCount(int count) {
|
||||
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
|
||||
if (newCount == projectileCount) return;
|
||||
|
||||
ProjectileData[] oldProjectiles = projectiles;
|
||||
initProjectiles(newCount);
|
||||
|
||||
if (oldProjectiles != null) {
|
||||
int copyCount = Math.min(oldProjectiles.length, newCount);
|
||||
for (int i = 0; i < copyCount; i++) {
|
||||
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getProjectileCount() {
|
||||
return projectileCount;
|
||||
}
|
||||
|
||||
public int getFireDelay() {
|
||||
return fireDelay;
|
||||
}
|
||||
|
||||
public void setFireDelay(int delay) {
|
||||
this.fireDelay = Math.max(0, delay);
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #getFireDelay()} */
|
||||
public int getDualFireDelay() { return fireDelay; }
|
||||
/** @deprecated Use {@link #setFireDelay(int)} */
|
||||
public void setDualFireDelay(int delay) { setFireDelay(delay); }
|
||||
|
||||
@Override
|
||||
public List<AbilityVariant> getVariants() {
|
||||
return Arrays.asList(
|
||||
new AbilityVariant("ability.variant.single", a -> {
|
||||
a.setName("Beam");
|
||||
}),
|
||||
new AbilityVariant("ability.variant.dual", a -> {
|
||||
AbilityBeam beam = (AbilityBeam) a;
|
||||
a.setName("Dual Beam");
|
||||
beam.setProjectileCount(2);
|
||||
beam.setFireDelay(0);
|
||||
// Set color override for second beam
|
||||
beam.projectiles[1].colorOverride = true;
|
||||
beam.projectiles[1].innerColor = 0xFFFFFF;
|
||||
beam.projectiles[1].outerColor = 0xFF0000;
|
||||
a.setWindUpTicks(80);
|
||||
a.setWindUpAnimationName("Ability_BeamDual_Windup");
|
||||
a.setActiveAnimationName("Ability_BeamDual_Active");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowOverlap() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadyForBurstCompletion(int activeTick) {
|
||||
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,51 +165,118 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
signalCompletion();
|
||||
return;
|
||||
/**
|
||||
* Create beam entities.
|
||||
* NPC: target is the aggro target — beams will home toward it.
|
||||
* Player: target is null — beams fire in caster's look direction.
|
||||
*/
|
||||
private EntityAbilityBeam[] createBeamEntities(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
EntityAbilityBeam[] entities = new EntityAbilityBeam[projectileCount];
|
||||
float offsetDist = 1.0f;
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor, offsetDist);
|
||||
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
|
||||
entities[i] = new EntityAbilityBeam(
|
||||
world, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
|
||||
beamWidth, headSize,
|
||||
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
|
||||
lockMovement.locksActive());
|
||||
entities[i].setEffects(this.effects);
|
||||
entities[i].setSourceAbility(this);
|
||||
}
|
||||
|
||||
// Start firing the beam that was spawned during windup
|
||||
if (beamEntity != null && !beamEntity.isDead) {
|
||||
beamEntity.startFiring(target);
|
||||
if (projectileCount == 2 && entities[0] != null && entities[1] != null) {
|
||||
entities[0].setSiblingEntityId(entities[1].getEntityId());
|
||||
entities[1].setSiblingEntityId(entities[0].getEntityId());
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
private void fireBeamEntity(EntityAbilityBeam beam, EntityLivingBase target) {
|
||||
if (beam == null || beam.isDead) return;
|
||||
if (isPreview()) {
|
||||
beam.startPreviewFiring();
|
||||
} else {
|
||||
beam.startFiring(target);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Spawn beam in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
beamEntities = createBeamEntities(caster, target, world);
|
||||
float offsetDist = 1.0f;
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
|
||||
if (isPreview()) {
|
||||
beamEntities[i].setupPreview(caster, beamWidth, headSize, resolved, lightningData, projectiles[i].anchor, windUpTicks, offsetDist);
|
||||
} else {
|
||||
beamEntities[i].setupCharging(projectiles[i].anchor, windUpTicks, offsetDist);
|
||||
}
|
||||
spawnAbilityEntity(world, beamEntities[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create beam in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist);
|
||||
beamEntity = new EntityAbilityBeam(
|
||||
world, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
|
||||
beamWidth, headSize,
|
||||
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData,
|
||||
lockMovement.locksActive());
|
||||
beamEntity.setupCharging(anchorData, windUpTicks, offsetDist);
|
||||
@Override
|
||||
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
beamEntities = createBeamEntities(caster, target, world);
|
||||
for (EntityAbilityBeam beam : beamEntities) {
|
||||
spawnAbilityEntity(world, beam);
|
||||
}
|
||||
|
||||
beamEntity.setEffects(this.effects);
|
||||
beamEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(beamEntity);
|
||||
// Fire first projectile immediately, let onActiveTick handle the rest with fireDelay
|
||||
fireBeamEntity(beamEntities[0], target);
|
||||
if (fireDelay <= 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
fireBeamEntity(beamEntities[i], target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (beamEntities == null) return;
|
||||
|
||||
fireBeamEntity(beamEntities[0], target);
|
||||
|
||||
if (fireDelay <= 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
fireBeamEntity(beamEntities[i], target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// Signal completion when entity dies
|
||||
if (beamEntity == null || beamEntity.isDead) {
|
||||
beamEntity = null;
|
||||
if (beamEntities == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (fireDelay > 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
if (tick == fireDelay * i) {
|
||||
fireBeamEntity(beamEntities[i], target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean allDead = true;
|
||||
for (EntityAbilityBeam beam : beamEntities) {
|
||||
if (beam != null && !beam.isDead) {
|
||||
allDead = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allDead) {
|
||||
signalCompletion();
|
||||
}
|
||||
}
|
||||
@@ -134,20 +292,25 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn beam entity if still alive
|
||||
if (beamEntity != null && !beamEntity.isDead) {
|
||||
beamEntity.setDead();
|
||||
if (beamEntities != null) {
|
||||
for (int i = 0; i < beamEntities.length; i++) {
|
||||
if (beamEntities[i] != null && !beamEntities[i].isDead) {
|
||||
beamEntities[i].setDead();
|
||||
}
|
||||
beamEntities[i] = null;
|
||||
}
|
||||
}
|
||||
beamEntity = null;
|
||||
beamEntities = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
|
||||
// NPC: telegraph on aggro target position
|
||||
// Player: no telegraph (beam fires in look direction, no fixed position to mark)
|
||||
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create circle telegraph at target position
|
||||
Telegraph telegraph = Telegraph.circle(headSize * 2.0f);
|
||||
telegraph.setDurationTicks(windUpTicks);
|
||||
telegraph.setColor(windUpColor);
|
||||
@@ -155,7 +318,6 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
|
||||
telegraph.setHeightOffset(telegraphHeightOffset);
|
||||
|
||||
// Position at target and follow target during windup
|
||||
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
|
||||
instance.setCasterEntityId(caster.getEntityId());
|
||||
instance.setEntityIdToFollow(target.getEntityId());
|
||||
@@ -172,176 +334,276 @@ public class AbilityBeam extends Ability implements IAbilityEnergyBeam {
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("beamWidth", beamWidth);
|
||||
nbt.setFloat("headSize", headSize);
|
||||
anchorData.writeNBT(nbt);
|
||||
colorData.writeNBT(nbt);
|
||||
nbt.setInteger("projectileCount", projectileCount);
|
||||
nbt.setInteger("fireDelay", fireDelay);
|
||||
|
||||
// Shared visual data
|
||||
displayData.writeNBT(nbt);
|
||||
lightningData.writeNBT(nbt);
|
||||
|
||||
// Shared combat/movement data
|
||||
combatData.writeNBT(nbt);
|
||||
homingData.writeNBT(nbt);
|
||||
lightningData.writeNBT(nbt);
|
||||
lifespanData.writeNBT(nbt);
|
||||
|
||||
// Per-projectile data
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
NBTTagCompound projNbt = new NBTTagCompound();
|
||||
projectiles[i].writeNBT(projNbt);
|
||||
nbt.setTag("Projectile_" + i, projNbt);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f;
|
||||
this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f;
|
||||
anchorData.readNBT(nbt);
|
||||
colorData.readNBT(nbt);
|
||||
|
||||
int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1;
|
||||
initProjectiles(count);
|
||||
|
||||
this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0;
|
||||
|
||||
combatData.readNBT(nbt);
|
||||
homingData.readNBT(nbt);
|
||||
lightningData.readNBT(nbt);
|
||||
lifespanData.readNBT(nbt);
|
||||
|
||||
// Shared visual data
|
||||
displayData.readNBT(nbt);
|
||||
lightningData.readNBT(nbt);
|
||||
|
||||
// Per-projectile data
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
if (nbt.hasKey("Projectile_" + i)) {
|
||||
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getters & Setters - Standalone fields
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// API GETTERS & SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public float getBeamWidth() { return beamWidth; }
|
||||
public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; }
|
||||
|
||||
public float getHeadSize() { return headSize; }
|
||||
public void setHeadSize(float headSize) { this.headSize = headSize; }
|
||||
|
||||
// Getters & Setters - Color data
|
||||
public int getInnerColor() { return colorData.innerColor; }
|
||||
public void setInnerColor(int innerColor) { this.colorData.innerColor = innerColor; }
|
||||
public int getOuterColor() { return colorData.outerColor; }
|
||||
public void setOuterColor(int outerColor) { this.colorData.outerColor = outerColor; }
|
||||
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
|
||||
public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; }
|
||||
public float getOuterColorWidth() { return colorData.outerColorWidth; }
|
||||
public void setOuterColorWidth(float outerColorWidth) { this.colorData.outerColorWidth = outerColorWidth; }
|
||||
public float getOuterColorAlpha() { return colorData.outerColorAlpha; }
|
||||
public void setOuterColorAlpha(float outerColorAlpha) { this.colorData.outerColorAlpha = outerColorAlpha; }
|
||||
public float getRotationSpeed() { return colorData.rotationSpeed; }
|
||||
public void setRotationSpeed(float rotationSpeed) { this.colorData.rotationSpeed = rotationSpeed; }
|
||||
|
||||
// Getters & Setters - Combat data
|
||||
public float getDamage() { return combatData.damage; }
|
||||
public void setDamage(float damage) { this.combatData.damage = damage; }
|
||||
public float getKnockback() { return combatData.knockback; }
|
||||
public void setKnockback(float knockback) { this.combatData.knockback = knockback; }
|
||||
public float getKnockbackUp() { return combatData.knockbackUp; }
|
||||
public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; }
|
||||
public boolean isExplosive() { return combatData.explosive; }
|
||||
public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; }
|
||||
public float getExplosionRadius() { return combatData.explosionRadius; }
|
||||
public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; }
|
||||
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
|
||||
public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; }
|
||||
|
||||
// Getters & Setters - Homing data
|
||||
public float getSpeed() { return homingData.speed; }
|
||||
public void setSpeed(float speed) { this.homingData.speed = speed; }
|
||||
public boolean isHoming() { return homingData.homing; }
|
||||
public void setHoming(boolean homing) { this.homingData.homing = homing; }
|
||||
public float getHomingStrength() { return homingData.homingStrength; }
|
||||
public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; }
|
||||
public float getHomingRange() { return homingData.homingRange; }
|
||||
public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; }
|
||||
|
||||
// Getters & Setters - Lightning data
|
||||
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
|
||||
public void setLightningEffect(boolean lightningEffect) { this.lightningData.lightningEffect = lightningEffect; }
|
||||
public float getLightningDensity() { return lightningData.lightningDensity; }
|
||||
public void setLightningDensity(float lightningDensity) { this.lightningData.lightningDensity = lightningDensity; }
|
||||
public float getLightningRadius() { return lightningData.lightningRadius; }
|
||||
public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; }
|
||||
|
||||
// Getters & Setters - Lifespan data
|
||||
public float getMaxDistance() { return lifespanData.maxDistance; }
|
||||
public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; }
|
||||
|
||||
public int getMaxLifetime() { return lifespanData.maxLifetime; }
|
||||
public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; }
|
||||
|
||||
// Getters & Setters - Anchor point
|
||||
public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; }
|
||||
public float getAnchorOffsetX() { return anchorData.anchorOffsetX; }
|
||||
public float getAnchorOffsetY() { return anchorData.anchorOffsetY; }
|
||||
public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; }
|
||||
public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; }
|
||||
public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; }
|
||||
public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; }
|
||||
public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; }
|
||||
public float getDamage() { return combatData.damage; }
|
||||
public void setDamage(float damage) { this.combatData.damage = damage; }
|
||||
|
||||
public float getKnockback() { return combatData.knockback; }
|
||||
public void setKnockback(float knockback) { this.combatData.knockback = knockback; }
|
||||
|
||||
public float getKnockbackUp() { return combatData.knockbackUp; }
|
||||
public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; }
|
||||
|
||||
public boolean isHoming() { return homingData.homing; }
|
||||
public void setHoming(boolean homing) { this.homingData.homing = homing; }
|
||||
|
||||
public float getHomingStrength() { return homingData.homingStrength; }
|
||||
public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; }
|
||||
|
||||
public float getHomingRange() { return homingData.homingRange; }
|
||||
public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; }
|
||||
|
||||
public boolean isExplosive() { return combatData.explosive; }
|
||||
public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; }
|
||||
|
||||
public float getExplosionRadius() { return combatData.explosionRadius; }
|
||||
public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; }
|
||||
|
||||
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
|
||||
public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; }
|
||||
|
||||
// Shared display data (primary colors)
|
||||
public int getInnerColor() { return displayData.innerColor; }
|
||||
public void setInnerColor(int innerColor) { displayData.innerColor = innerColor; }
|
||||
|
||||
public int getOuterColor() { return displayData.outerColor; }
|
||||
public void setOuterColor(int outerColor) { displayData.outerColor = outerColor; }
|
||||
|
||||
public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
|
||||
public void setOuterColorEnabled(boolean outerColorEnabled) { displayData.outerColorEnabled = outerColorEnabled; }
|
||||
|
||||
public float getOuterColorWidth() { return displayData.outerColorWidth; }
|
||||
public void setOuterColorWidth(float outerColorWidth) { displayData.outerColorWidth = outerColorWidth; }
|
||||
|
||||
public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
|
||||
public void setOuterColorAlpha(float outerColorAlpha) { displayData.outerColorAlpha = outerColorAlpha; }
|
||||
|
||||
public float getRotationSpeed() { return displayData.rotationSpeed; }
|
||||
public void setRotationSpeed(float rotationSpeed) { displayData.rotationSpeed = rotationSpeed; }
|
||||
|
||||
// Shared lightning data
|
||||
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
|
||||
public void setLightningEffect(boolean lightningEffect) { lightningData.lightningEffect = lightningEffect; }
|
||||
|
||||
public float getLightningDensity() { return lightningData.lightningDensity; }
|
||||
public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; }
|
||||
|
||||
public float getLightningRadius() { return lightningData.lightningRadius; }
|
||||
public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; }
|
||||
|
||||
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
|
||||
public void setLightningFadeTime(int lightningFadeTime) { lightningData.lightningFadeTime = lightningFadeTime; }
|
||||
|
||||
// Anchor - default to projectile 0
|
||||
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
|
||||
public void setAnchorPointEnum(AnchorPoint anchorPoint) { projectiles[0].anchor.anchorPoint = anchorPoint; }
|
||||
|
||||
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
|
||||
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
|
||||
|
||||
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
|
||||
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
|
||||
|
||||
public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
|
||||
public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
|
||||
|
||||
@Override
|
||||
public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); }
|
||||
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
|
||||
|
||||
@Override
|
||||
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
// Indexed API methods
|
||||
public int getInnerColor(int index) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
return p.colorOverride ? p.innerColor : displayData.innerColor;
|
||||
}
|
||||
public void setInnerColor(int index, int color) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
p.colorOverride = true;
|
||||
p.innerColor = color;
|
||||
}
|
||||
|
||||
EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj);
|
||||
beam.setupPreview(npc, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, 1.0f);
|
||||
return beam;
|
||||
public int getOuterColor(int index) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
return p.colorOverride ? p.outerColor : displayData.outerColor;
|
||||
}
|
||||
public void setOuterColor(int index, int color) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
p.colorOverride = true;
|
||||
p.outerColor = color;
|
||||
}
|
||||
|
||||
public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); }
|
||||
public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; }
|
||||
public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; }
|
||||
public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; }
|
||||
|
||||
private int clampIndex(int index) {
|
||||
return Math.max(0, Math.min(index, projectileCount - 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GUI FIELD DEFINITIONS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
defs.addAll(Arrays.asList(
|
||||
// Type tab
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
|
||||
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
|
||||
),
|
||||
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
|
||||
FieldDef.section("ability.section.beam"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth),
|
||||
FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
|
||||
),
|
||||
FieldDef.section("ability.section.homing"),
|
||||
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
|
||||
.hover("ability.hover.homing"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
|
||||
.visibleWhen(this::isHoming),
|
||||
FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange)
|
||||
.visibleWhen(this::isHoming)
|
||||
),
|
||||
FieldDef.section("ability.section.explosive"),
|
||||
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive)
|
||||
.hover("ability.hover.explosive"),
|
||||
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.visibleWhen(this::isExplosive),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
|
||||
// Visual tab
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
|
||||
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
|
||||
.visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
|
||||
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.section("ability.section.effects").tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
|
||||
.range(0.01f, 100f).visibleWhen(this::hasLightningEffect),
|
||||
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
|
||||
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
|
||||
).tab("ability.tab.visual")
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
|
||||
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
|
||||
.range(0, 200).visibleWhen(() -> projectileCount > 1)
|
||||
));
|
||||
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
|
||||
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
|
||||
));
|
||||
defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback));
|
||||
defs.add(FieldDef.section("ability.section.beam"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth),
|
||||
FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize)
|
||||
));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
|
||||
));
|
||||
defs.add(FieldDef.section("ability.section.homing"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
|
||||
FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange).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(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
|
||||
// Visual tab - shared primary colors
|
||||
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
|
||||
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
|
||||
.visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
|
||||
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
|
||||
).tab("ability.tab.visual"));
|
||||
|
||||
// Visual tab - shared effects
|
||||
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
|
||||
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
|
||||
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
|
||||
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
|
||||
).tab("ability.tab.visual"));
|
||||
|
||||
// Visual tab - per-projectile sections
|
||||
for (int i = 0; i < MAX_PROJECTILES; i++) {
|
||||
final int idx = i;
|
||||
String sectionKey = "ability.section.projectile" + (i + 1);
|
||||
|
||||
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
|
||||
.visibleWhen(() -> idx < projectileCount));
|
||||
|
||||
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
|
||||
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v),
|
||||
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v)
|
||||
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
|
||||
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
|
||||
|
||||
// Color override (only when multiple projectiles)
|
||||
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
|
||||
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
|
||||
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,517 +0,0 @@
|
||||
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.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
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 kamkeel.npcs.entity.EntityAbilityBeam;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import noppes.npcs.util.ValueUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AbilityBeamDual extends Ability {
|
||||
private float beamWidth = 0.4f;
|
||||
private float headSize = 0.6f;
|
||||
private boolean dualFire= true;
|
||||
private int dualFireDelay = 0;
|
||||
|
||||
// Data classes
|
||||
private final EnergyDisplayData[] displayData = new EnergyDisplayData[]{
|
||||
new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f),
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 6.0f),
|
||||
};
|
||||
private final EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f);
|
||||
private final EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f);
|
||||
private final EnergyLightningData[] lightningData = new EnergyLightningData[]{
|
||||
new EnergyLightningData(), new EnergyLightningData()
|
||||
};
|
||||
private final EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200);
|
||||
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
|
||||
new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND)
|
||||
};
|
||||
private final EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{
|
||||
new EnergyTrajectoryData(), new EnergyTrajectoryData()
|
||||
};
|
||||
|
||||
// Transient state for beam entity (used during windup charging)
|
||||
private transient EntityAbilityBeam beamEntity = null;
|
||||
private transient EntityAbilityBeam beamEntity2 = null;
|
||||
|
||||
public AbilityBeamDual() {
|
||||
this.typeId = "ability.cnpc.beam_dual";
|
||||
this.name = "Beam Dual";
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 20.0f;
|
||||
this.minRange = 5.0f;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 80;
|
||||
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.showTelegraph = true;
|
||||
// Default built-in animations
|
||||
this.windUpAnimationName = "Ability_BeamDual_Windup";
|
||||
this.activeAnimationName = "Ability_BeamDual_Active";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetingMode[] getAllowedTargetingModes() {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start firing the beam that was spawned during windup
|
||||
if (beamEntity != null && !beamEntity.isDead) {
|
||||
beamEntity.startFiring(target);
|
||||
}
|
||||
|
||||
if (dualFire) {
|
||||
if (beamEntity2 != null && !beamEntity2.isDead && dualFireDelay <= 0) {
|
||||
beamEntity2.startFiring(target);
|
||||
}
|
||||
} else if (beamEntity2 != null) {
|
||||
beamEntity2.setDead();
|
||||
}
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
|
||||
// Spawn beam in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
float offsetDist = 1.0f;
|
||||
|
||||
// Create beam in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0], offsetDist);
|
||||
beamEntity = new EntityAbilityBeam(
|
||||
world, caster, target,
|
||||
spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord,
|
||||
beamWidth, headSize,
|
||||
displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0],
|
||||
lockMovement.locksActive());
|
||||
|
||||
Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1], offsetDist);
|
||||
beamEntity2 = new EntityAbilityBeam(
|
||||
world, caster, target,
|
||||
spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord,
|
||||
beamWidth, headSize,
|
||||
displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1],
|
||||
lockMovement.locksActive());
|
||||
|
||||
beamEntity.setSiblingEntityId(beamEntity2.getEntityId());
|
||||
beamEntity2.setSiblingEntityId(beamEntity.getEntityId());
|
||||
|
||||
beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist);
|
||||
beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist);
|
||||
|
||||
beamEntity.setEffects(this.effects);
|
||||
if (dualFire) beamEntity2.setEffects(this.effects);
|
||||
|
||||
beamEntity.setSourceAbility(this);
|
||||
beamEntity2.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(beamEntity);
|
||||
world.spawnEntityInWorld(beamEntity2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// Signal completion when entity dies
|
||||
if (dualFire) {
|
||||
if ((beamEntity == null || beamEntity.isDead) && (beamEntity2 == null || beamEntity2.isDead)) {
|
||||
beamEntity = null;
|
||||
beamEntity2 = null;
|
||||
signalCompletion();
|
||||
}
|
||||
|
||||
if (tick == dualFireDelay && beamEntity2 != null && !beamEntity2.isDead) {
|
||||
beamEntity2.startFiring(target);
|
||||
}
|
||||
} else {
|
||||
if (beamEntity == null || beamEntity.isDead) {
|
||||
beamEntity = null;
|
||||
signalCompletion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn beam entity if still alive
|
||||
if (beamEntity != null && !beamEntity.isDead) {
|
||||
beamEntity.setDead();
|
||||
}
|
||||
|
||||
if (beamEntity2 != null && !beamEntity2.isDead) {
|
||||
beamEntity2.setDead();
|
||||
}
|
||||
|
||||
beamEntity = null;
|
||||
beamEntity2 = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create circle telegraph at target position
|
||||
Telegraph telegraph = Telegraph.circle(headSize * 2.0f);
|
||||
telegraph.setDurationTicks(windUpTicks);
|
||||
telegraph.setColor(windUpColor);
|
||||
telegraph.setWarningColor(activeColor);
|
||||
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
|
||||
telegraph.setHeightOffset(telegraphHeightOffset);
|
||||
|
||||
// Position at target and follow target during windup
|
||||
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
|
||||
instance.setCasterEntityId(caster.getEntityId());
|
||||
instance.setEntityIdToFollow(target.getEntityId());
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTelegraphRadius() {
|
||||
return headSize * 2.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("beamWidth", beamWidth);
|
||||
nbt.setFloat("headSize", headSize);
|
||||
nbt.setBoolean("dualFire", dualFire);
|
||||
nbt.setInteger("dualFireDelay", dualFireDelay);
|
||||
combatData.writeNBT(nbt);
|
||||
homingData.writeNBT(nbt);
|
||||
lifespanData.writeNBT(nbt);
|
||||
|
||||
NBTTagCompound beam1 = new NBTTagCompound();
|
||||
NBTTagCompound beam2 = new NBTTagCompound();
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
NBTTagCompound beam = i == 0 ? beam1 : beam2;
|
||||
anchorData[i].writeNBT(beam);
|
||||
displayData[i].writeNBT(beam);
|
||||
lightningData[i].writeNBT(beam);
|
||||
trajectoryData[i].writeNBT(beam);
|
||||
}
|
||||
|
||||
nbt.setTag("Beam_1", beam1);
|
||||
nbt.setTag("Beam_2", beam2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
NBTTagCompound beam1 = nbt.hasKey("Beam_1") ? nbt.getCompoundTag("Beam_1") : new NBTTagCompound();
|
||||
NBTTagCompound beam2 = nbt.hasKey("Beam_2") ? nbt.getCompoundTag("Beam_2") : new NBTTagCompound();
|
||||
|
||||
this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f;
|
||||
this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f;
|
||||
this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire");
|
||||
this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0;
|
||||
combatData.readNBT(nbt);
|
||||
homingData.readNBT(nbt);
|
||||
lifespanData.readNBT(nbt);
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
NBTTagCompound beam = i == 0 ? beam1 : beam2;
|
||||
anchorData[i].readNBT(beam);
|
||||
displayData[i].readNBT(beam);
|
||||
lightningData[i].readNBT(beam);
|
||||
trajectoryData[i].readNBT(beam);
|
||||
}
|
||||
}
|
||||
|
||||
// Getters & Setters - Standalone fields
|
||||
public float getBeamWidth() { return beamWidth; }
|
||||
public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; }
|
||||
public float getHeadSize() { return headSize; }
|
||||
public void setHeadSize(float headSize) { this.headSize = headSize; }
|
||||
public boolean isDualFire() { return dualFire; }
|
||||
public void setDualFire(boolean dualFire) { this.dualFire = dualFire; }
|
||||
public int getDualFireDelay() { return dualFireDelay; }
|
||||
public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; }
|
||||
|
||||
private EnergyDisplayData getColorData(int beam) {
|
||||
beam = ValueUtil.clamp(beam, 0, 1);
|
||||
return displayData[beam];
|
||||
}
|
||||
private EnergyLightningData getLightningData(int beam) {
|
||||
beam = ValueUtil.clamp(beam, 0, 1);
|
||||
return lightningData[beam];
|
||||
}
|
||||
private EnergyAnchorData getAnchorData(int beam) {
|
||||
beam = ValueUtil.clamp(beam, 0, 1);
|
||||
return anchorData[beam];
|
||||
}
|
||||
|
||||
// Getters & Setters - Color data
|
||||
public int getInnerColor(int beam) { return getColorData(beam).innerColor; }
|
||||
public void setInnerColor(int beam, int innerColor) { getColorData(beam).innerColor = innerColor; }
|
||||
public int getOuterColor(int beam) { return getColorData(beam).outerColor; }
|
||||
public void setOuterColor(int beam, int outerColor) { getColorData(beam).outerColor = outerColor; }
|
||||
public boolean isOuterColorEnabled(int beam) { return getColorData(beam).outerColorEnabled; }
|
||||
public void setOuterColorEnabled(int beam, boolean enabled) { getColorData(beam).outerColorEnabled = enabled; }
|
||||
public float getOuterColorWidth(int beam) { return getColorData(beam).outerColorWidth; }
|
||||
public void setOuterColorWidth(int beam, float width) { getColorData(beam).outerColorWidth = width; }
|
||||
public float getOuterColorAlpha(int beam) { return getColorData(beam).outerColorAlpha; }
|
||||
public void setOuterColorAlpha(int beam, float alpha) { getColorData(beam).outerColorAlpha = alpha; }
|
||||
public float getRotationSpeed(int beam) { return getColorData(beam).rotationSpeed; }
|
||||
public void setRotationSpeed(int beam, float speed) { getColorData(beam).rotationSpeed = speed; }
|
||||
|
||||
|
||||
public int getInnerColor() { return getInnerColor(0); }
|
||||
public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); }
|
||||
public int getOuterColor() { return getOuterColor(0); }
|
||||
public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); }
|
||||
public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); }
|
||||
public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); }
|
||||
public float getOuterColorWidth() { return getOuterColorWidth(0); }
|
||||
public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); }
|
||||
public float getOuterColorAlpha() { return getOuterColorAlpha(0); }
|
||||
public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); }
|
||||
public float getRotationSpeed() { return getRotationSpeed(0); }
|
||||
public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); }
|
||||
|
||||
// Getters & Setters - Combat data
|
||||
public float getDamage() { return combatData.damage; }
|
||||
public void setDamage(float damage) { this.combatData.damage = damage; }
|
||||
public float getKnockback() { return combatData.knockback; }
|
||||
public void setKnockback(float knockback) { this.combatData.knockback = knockback; }
|
||||
public float getKnockbackUp() { return combatData.knockbackUp; }
|
||||
public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; }
|
||||
public boolean isExplosive() { return combatData.explosive; }
|
||||
public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; }
|
||||
public float getExplosionRadius() { return combatData.explosionRadius; }
|
||||
public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; }
|
||||
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
|
||||
public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; }
|
||||
|
||||
// Getters & Setters - Homing data
|
||||
public float getSpeed() { return homingData.speed; }
|
||||
public void setSpeed(float speed) { this.homingData.speed = speed; }
|
||||
public boolean isHoming() { return homingData.homing; }
|
||||
public void setHoming(boolean homing) { this.homingData.homing = homing; }
|
||||
public float getHomingStrength() { return homingData.homingStrength; }
|
||||
public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; }
|
||||
public float getHomingRange() { return homingData.homingRange; }
|
||||
public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; }
|
||||
|
||||
// Getters & Setters - Lightning data
|
||||
public boolean hasLightningEffect(int beam) { return getLightningData(beam).lightningEffect; }
|
||||
public void setLightningEffect(int beam, boolean lightningEffect) { getLightningData(beam).lightningEffect = lightningEffect; }
|
||||
public float getLightningDensity(int beam) { return getLightningData(beam).lightningDensity; }
|
||||
public void setLightningDensity(int beam, float lightningDensity) { getLightningData(beam).lightningDensity = lightningDensity; }
|
||||
public float getLightningRadius(int beam) { return getLightningData(beam).lightningRadius; }
|
||||
public void setLightningRadius(int beam, float lightningRadius) { getLightningData(beam).lightningRadius = lightningRadius; }
|
||||
|
||||
public boolean hasLightningEffect() { return hasLightningEffect(0); }
|
||||
public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); }
|
||||
public float getLightningDensity() { return getLightningDensity(0); }
|
||||
public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); }
|
||||
public float getLightningRadius() { return getLightningRadius(0); }
|
||||
public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); }
|
||||
|
||||
|
||||
// Getters & Setters - Lifespan data
|
||||
public float getMaxDistance() { return lifespanData.maxDistance; }
|
||||
public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; }
|
||||
public int getMaxLifetime() { return lifespanData.maxLifetime; }
|
||||
public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; }
|
||||
|
||||
// Getters & Setters - Anchor point
|
||||
public AnchorPoint getAnchorPointEnum(int beam) { return getAnchorData(beam).anchorPoint; }
|
||||
public void setAnchorPointEnum(int beam, AnchorPoint anchorPoint) { getAnchorData(beam).anchorPoint = anchorPoint; }
|
||||
public float getAnchorOffsetX(int beam) { return getAnchorData(beam).anchorOffsetX; }
|
||||
public void setAnchorOffsetX(int beam, float x) { getAnchorData(beam).anchorOffsetX = x; }
|
||||
public float getAnchorOffsetY(int beam) { return getAnchorData(beam).anchorOffsetY; }
|
||||
public void setAnchorOffsetY(int beam, float y) { getAnchorData(beam).anchorOffsetY = y; }
|
||||
public float getAnchorOffsetZ(int beam) { return getAnchorData(beam).anchorOffsetZ; }
|
||||
public void setAnchorOffsetZ(int beam, float z) { getAnchorData(beam).anchorOffsetZ = z; }
|
||||
|
||||
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
|
||||
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
|
||||
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
|
||||
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
|
||||
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
|
||||
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
|
||||
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
|
||||
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
|
||||
|
||||
//@Override
|
||||
public int getAnchorPoint(int beam) { return getAnchorData(beam).anchorPoint.ordinal(); }
|
||||
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
|
||||
|
||||
//@Override
|
||||
public void setAnchorPoint(int beam, int point) { getAnchorData(beam).anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
defs.addAll(Arrays.asList(
|
||||
// Type tab
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
|
||||
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
|
||||
),
|
||||
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
|
||||
FieldDef.section("ability.section.beam"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth),
|
||||
FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
|
||||
),
|
||||
FieldDef.section("ability.section.dual"),
|
||||
FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire),
|
||||
FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay)
|
||||
.visibleWhen(this::isDualFire),
|
||||
FieldDef.section("ability.section.homing"),
|
||||
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
|
||||
.hover("ability.hover.homing"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
|
||||
.visibleWhen(this::isHoming),
|
||||
FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange)
|
||||
.visibleWhen(this::isHoming)
|
||||
),
|
||||
FieldDef.section("ability.section.explosive"),
|
||||
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive)
|
||||
.hover("ability.hover.explosive"),
|
||||
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.visibleWhen(this::isExplosive),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
|
||||
// Visual tab - Beam 1
|
||||
FieldDef.section("ability.section.beam1").tab("ability.tab.visual"),
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
|
||||
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor",
|
||||
() -> getInnerColor(0), v -> setInnerColor(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled",
|
||||
() -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor",
|
||||
() -> getOuterColor(0), v -> setOuterColor(0, v))
|
||||
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth",
|
||||
() -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v))
|
||||
.visibleWhen(() -> isOuterColorEnabled(0)),
|
||||
FieldDef.floatField("ability.outerAlpha",
|
||||
() -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v))
|
||||
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(0))
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.rotationSpeed",
|
||||
() -> getRotationSpeed(0), v -> setRotationSpeed(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.lightning",
|
||||
() -> hasLightningEffect(0), v -> setLightningEffect(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density",
|
||||
() -> getLightningDensity(0), v -> setLightningDensity(0, v))
|
||||
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(0)),
|
||||
FieldDef.floatField("gui.radius",
|
||||
() -> getLightningRadius(0), v -> setLightningRadius(0, v))
|
||||
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(0))
|
||||
).tab("ability.tab.visual"),
|
||||
// Visual tab - Beam 2
|
||||
FieldDef.section("ability.section.beam2").tab("ability.tab.visual"),
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
|
||||
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor",
|
||||
() -> getInnerColor(1), v -> setInnerColor(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled",
|
||||
() -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor",
|
||||
() -> getOuterColor(1), v -> setOuterColor(1, v))
|
||||
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth",
|
||||
() -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v))
|
||||
.visibleWhen(() -> isOuterColorEnabled(1)),
|
||||
FieldDef.floatField("ability.outerAlpha",
|
||||
() -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v))
|
||||
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(1))
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.rotationSpeed",
|
||||
() -> getRotationSpeed(1), v -> setRotationSpeed(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.lightning",
|
||||
() -> hasLightningEffect(1), v -> setLightningEffect(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density",
|
||||
() -> getLightningDensity(1), v -> setLightningDensity(1, v))
|
||||
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)),
|
||||
FieldDef.floatField("gui.radius",
|
||||
() -> getLightningRadius(1), v -> setLightningRadius(1, v))
|
||||
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1))
|
||||
).tab("ability.tab.visual")
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj);
|
||||
beam.setupPreview(npc, beamWidth, headSize, displayData[0], lightningData[0], anchorData[0], windUpTicks, 1.0f);
|
||||
return beam;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,11 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
|
||||
// Runtime state (transient)
|
||||
private transient double startX, startY, startZ;
|
||||
private transient double prevTickX, prevTickZ;
|
||||
private transient Vec3 chargeDirection;
|
||||
private transient Set<Integer> hitEntities = new HashSet<>();
|
||||
private transient float lockedYaw;
|
||||
private transient int maxActiveTicks;
|
||||
|
||||
public AbilityCharge() {
|
||||
this.typeId = "ability.cnpc.charge";
|
||||
@@ -56,6 +58,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
this.showTelegraph = true;
|
||||
this.windUpSound = "mob.zombie.wood";
|
||||
this.activeSound = "mob.zombie.attack";
|
||||
this.windUpAnimationName = "Ability_Charge_Windup";
|
||||
this.activeAnimationName = "Ability_Active_Windup";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,11 +91,13 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the charge direction based on current target position.
|
||||
* Called once at windup start - direction won't change even if target moves.
|
||||
* Locks the charge direction. Called once at windup start — direction won't change even if target moves.
|
||||
* NPC: charges toward aggro target.
|
||||
* Player: charges in look direction.
|
||||
*/
|
||||
private void lockChargeDirection(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (target != null) {
|
||||
if (!isPlayerCaster(caster) && target != null) {
|
||||
// NPC: charge toward aggro target
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
@@ -102,6 +108,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw));
|
||||
}
|
||||
} else {
|
||||
// Player: charge in look direction
|
||||
float yaw = (float) Math.toRadians(caster.rotationYaw);
|
||||
chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw));
|
||||
}
|
||||
@@ -114,8 +121,13 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
startX = caster.posX;
|
||||
startY = caster.posY;
|
||||
startZ = caster.posZ;
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
hitEntities.clear();
|
||||
|
||||
// Safety timeout: expected ticks + buffer to prevent infinite charge
|
||||
maxActiveTicks = chargeSpeed > 0 ? (int)(maxRange / chargeSpeed) + 10 : 10;
|
||||
|
||||
// If direction wasn't set during windup (shouldn't happen), set it now
|
||||
if (chargeDirection == null) {
|
||||
lockChargeDirection(caster, target);
|
||||
@@ -135,10 +147,35 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (chargeDirection == null) return;
|
||||
// Safety timeout or missing state: force-complete to prevent stuck NPC
|
||||
if (!isPreview() && (chargeDirection == null || tick > maxActiveTicks)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (chargeDirection == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enforce rotation every tick
|
||||
enforceLockedRotation(caster);
|
||||
if (!isPreview()) {
|
||||
enforceLockedRotation(caster);
|
||||
}
|
||||
|
||||
// Stall detection: if entity hasn't moved since last tick, it's stuck against a wall
|
||||
if (!isPreview() && tick > 1) {
|
||||
double dx = caster.posX - prevTickX;
|
||||
double dz = caster.posZ - prevTickZ;
|
||||
if (dx * dx + dz * dz < 0.0001) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
}
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
|
||||
// Calculate distance traveled
|
||||
double distanceTraveled = Math.sqrt(
|
||||
@@ -148,14 +185,13 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
|
||||
// Check if reached max distance
|
||||
if (distanceTraveled >= maxRange) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isChargeBlocked(caster)) {
|
||||
// Block detection (skip in preview - no real world collision)
|
||||
if (!isPreview() && isChargeBlocked(caster)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
@@ -165,10 +201,12 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
caster.motionX = chargeDirection.xCoord * chargeSpeed;
|
||||
caster.motionY = 0;
|
||||
caster.motionZ = chargeDirection.zCoord * chargeSpeed;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
|
||||
// Server-side collision damage
|
||||
if (!world.isRemote) {
|
||||
// Server-side collision damage (skip in preview)
|
||||
if (!world.isRemote && !isPreview()) {
|
||||
AxisAlignedBB hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -211,7 +249,9 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
private void stopMomentum(EntityLivingBase caster) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isChargeBlocked(EntityLivingBase caster) {
|
||||
@@ -226,6 +266,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
super.reset();
|
||||
chargeDirection = null;
|
||||
hitEntities.clear();
|
||||
maxActiveTicks = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -249,9 +290,10 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate direction to target at this moment (locked)
|
||||
// NPC: telegraph points toward aggro target
|
||||
// Player: telegraph points in look direction
|
||||
float yaw;
|
||||
if (target != null) {
|
||||
if (!isPlayerCaster(caster) && target != null) {
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
yaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
@@ -277,66 +319,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
return instance;
|
||||
}
|
||||
|
||||
// ==================== PREVIEW MODE ====================
|
||||
|
||||
private transient double previewDirX, previewDirZ;
|
||||
private transient double previewStartX, previewStartZ;
|
||||
private transient boolean previewCharging = false;
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewExecute(EntityNPCInterface npc) {
|
||||
previewStartX = npc.posX;
|
||||
previewStartZ = npc.posZ;
|
||||
previewCharging = false;
|
||||
|
||||
// Calculate direction toward fake target
|
||||
if (previewTarget != null) {
|
||||
double dx = previewTarget.posX - npc.posX;
|
||||
double dz = previewTarget.posZ - npc.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
if (len > 0) {
|
||||
previewDirX = dx / len;
|
||||
previewDirZ = dz / len;
|
||||
} else {
|
||||
float yaw = (float) Math.toRadians(npc.rotationYaw);
|
||||
previewDirX = -Math.sin(yaw);
|
||||
previewDirZ = Math.cos(yaw);
|
||||
}
|
||||
} else {
|
||||
float yaw = (float) Math.toRadians(npc.rotationYaw);
|
||||
previewDirX = -Math.sin(yaw);
|
||||
previewDirZ = Math.cos(yaw);
|
||||
}
|
||||
previewCharging = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
|
||||
if (!previewCharging) return;
|
||||
|
||||
// Check distance traveled
|
||||
double distTraveled = Math.sqrt(
|
||||
Math.pow(npc.posX - previewStartX, 2) +
|
||||
Math.pow(npc.posZ - previewStartZ, 2)
|
||||
);
|
||||
|
||||
if (distTraveled >= maxRange) {
|
||||
previewCharging = false;
|
||||
return;
|
||||
}
|
||||
|
||||
npc.prevPosX = npc.posX;
|
||||
npc.prevPosY = npc.posY;
|
||||
npc.prevPosZ = npc.posZ;
|
||||
|
||||
npc.posX += previewDirX * chargeSpeed;
|
||||
npc.posZ += previewDirZ * chargeSpeed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return (int) Math.ceil(maxRange / chargeSpeed) + 5;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.telegraph.Telegraph;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
@@ -60,6 +62,8 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
|
||||
// Runtime state
|
||||
private transient Set<Integer> hitEntities = new HashSet<>();
|
||||
private transient float currentRotation = 0.0f;
|
||||
private transient boolean activeSoundPlayed = false;
|
||||
private transient float sweepBaseYaw = 0.0f;
|
||||
|
||||
public AbilityCutter() {
|
||||
this.typeId = "ability.cnpc.cutter";
|
||||
@@ -86,6 +90,54 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
|
||||
return new TargetingMode[]{TargetingMode.AOE_SELF};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate how many ticks the active sweep phase takes.
|
||||
* SWIPE: arcAngle / sweepSpeed ticks
|
||||
* SPIN: spinDurationTicks
|
||||
*/
|
||||
private int getActiveDurationTicks() {
|
||||
if (sweepMode == SweepMode.SPIN) {
|
||||
return spinDurationTicks;
|
||||
}
|
||||
return sweepSpeed > 0 ? (int) Math.ceil(arcAngle / sweepSpeed) : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keepTelegraphDuringActive() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a CONE telegraph that lasts through both windup and active sweep phases.
|
||||
* Warning color transitions when the sweep actually begins (at windUpTicks).
|
||||
*/
|
||||
@Override
|
||||
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Telegraph telegraph = Telegraph.cone(getTelegraphLength(), getTelegraphAngle());
|
||||
int totalDuration = windUpTicks + getActiveDurationTicks();
|
||||
telegraph.setDurationTicks(totalDuration);
|
||||
telegraph.setColor(windUpColor);
|
||||
telegraph.setWarningColor(activeColor);
|
||||
// Warning transition starts when the sweep begins (active phase)
|
||||
telegraph.setWarningStartTick(windUpTicks);
|
||||
telegraph.setHeightOffset(telegraphHeightOffset);
|
||||
|
||||
double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ);
|
||||
TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, caster.rotationYaw);
|
||||
instance.setCasterEntityId(caster.getEntityId());
|
||||
instance.setEntityIdToFollow(caster.getEntityId());
|
||||
|
||||
if (target != null && !isRotationLockedDuringWindup()) {
|
||||
instance.setTargetEntityId(target.getEntityId());
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTelegraphRadius() {
|
||||
return range;
|
||||
@@ -105,39 +157,76 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
hitEntities.clear();
|
||||
currentRotation = -arcAngle / 2.0f;
|
||||
activeSoundPlayed = false;
|
||||
|
||||
// Lock sweep direction toward target at execute time to prevent
|
||||
// AI look-at jitter from misdirecting the sweep at close range
|
||||
if (target != null) {
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
if (dx * dx + dz * dz > 0.0001) {
|
||||
sweepBaseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
} else {
|
||||
sweepBaseYaw = caster.rotationYaw;
|
||||
}
|
||||
} else {
|
||||
sweepBaseYaw = caster.rotationYaw;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
switch (sweepMode) {
|
||||
case SWIPE:
|
||||
float prevRotation = currentRotation;
|
||||
currentRotation += sweepSpeed;
|
||||
|
||||
// Play active sound when sweep reaches center (where target typically stands)
|
||||
if (!activeSoundPlayed && currentRotation >= 0) {
|
||||
playActiveSound(caster, world);
|
||||
}
|
||||
|
||||
if (currentRotation > arcAngle / 2.0f) {
|
||||
// Final sweep — check from prevRotation to end of arc
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f);
|
||||
if (!isPreview()) {
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, arcAngle / 2.0f);
|
||||
}
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
// Check entities between previous and current sweep position
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation);
|
||||
if (!isPreview()) {
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevRotation, currentRotation);
|
||||
}
|
||||
break;
|
||||
|
||||
case SPIN:
|
||||
// Play active sound at spin start
|
||||
if (!activeSoundPlayed) {
|
||||
playActiveSound(caster, world);
|
||||
}
|
||||
|
||||
if (tick >= spinDurationTicks) {
|
||||
signalCompletion(); // Spin duration complete
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
float prevSpin = currentRotation;
|
||||
currentRotation = (currentRotation + sweepSpeed) % 360.0f;
|
||||
hitEntities.clear();
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation);
|
||||
if (!isPreview()) {
|
||||
hitEntities.clear();
|
||||
performSweepDamageRange(caster, world, innerRadius, range, prevSpin, currentRotation);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void playActiveSound(EntityLivingBase caster, World world) {
|
||||
activeSoundPlayed = true;
|
||||
if (activeSound != null && !activeSound.isEmpty()) {
|
||||
world.playSoundAtEntity(caster, activeSound, 1.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for entities between two sweep angles, ensuring no gaps between ticks.
|
||||
* For SWIPE mode, this covers the continuous arc from startAngle to endAngle.
|
||||
@@ -167,7 +256,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (dist < minDist || dist > maxDist) continue;
|
||||
if (!isInSweepRange(dx, dz, caster.rotationYaw, minAngle, maxAngle)) continue;
|
||||
if (!isInSweepRange(dx, dz, sweepBaseYaw, minAngle, maxAngle)) continue;
|
||||
|
||||
hitEntities.add(entity.getEntityId());
|
||||
|
||||
@@ -210,12 +299,16 @@ public class AbilityCutter extends Ability implements IAbilityCutter {
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
hitEntities.clear();
|
||||
currentRotation = 0.0f;
|
||||
activeSoundPlayed = false;
|
||||
sweepBaseYaw = 0.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
hitEntities.clear();
|
||||
currentRotation = 0.0f;
|
||||
activeSoundPlayed = false;
|
||||
sweepBaseYaw = 0.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -87,13 +87,14 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
// Type-specific parameters
|
||||
private DashMode dashMode = DashMode.DEFENSIVE;
|
||||
private float dashDistance = 4.0f;
|
||||
private float dashSpeed = 1.5f;
|
||||
private float dashSpeed = 0.5f;
|
||||
|
||||
// Runtime state
|
||||
private transient Vec3 dashDirection;
|
||||
private transient double startX, startY, startZ;
|
||||
private transient double prevTickX, prevTickZ;
|
||||
private transient DashDirection chosenDirection;
|
||||
private transient int maxActiveTicks;
|
||||
|
||||
public AbilityDash() {
|
||||
this.typeId = "ability.cnpc.dash";
|
||||
@@ -139,15 +140,19 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
|
||||
// Safety timeout: expected ticks + generous buffer to prevent infinite dash
|
||||
maxActiveTicks = dashSpeed > 0 ? (int)(dashDistance / dashSpeed) + 10 : 10;
|
||||
|
||||
// Choose random direction based on mode
|
||||
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
|
||||
? AGGRESSIVE_DIRECTIONS
|
||||
: DEFENSIVE_DIRECTIONS;
|
||||
chosenDirection = directions[RANDOM.nextInt(directions.length)];
|
||||
|
||||
// Calculate dash direction relative to target
|
||||
// NPC: dash direction is relative to aggro target facing
|
||||
// Player: dash direction is relative to look direction
|
||||
float baseYaw;
|
||||
if (target != null) {
|
||||
if (!isPlayerCaster(caster) && target != null) {
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
@@ -165,21 +170,30 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
Math.cos(yawRad)
|
||||
);
|
||||
|
||||
// Small upward impulse for a skip/hop arc (gravity handles the descent)
|
||||
caster.motionY = 0.2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (dashDirection == null) return;
|
||||
// Safety timeout or missing state: force-complete to prevent stuck NPC
|
||||
if (!isPreview() && (dashDirection == null || tick > maxActiveTicks)) {
|
||||
stopDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dashDirection == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Stall detection: if entity hasn't moved since last tick, it's stuck against a wall
|
||||
if (tick > 1) {
|
||||
double movedThisTick = Math.sqrt(
|
||||
Math.pow(caster.posX - prevTickX, 2) +
|
||||
Math.pow(caster.posZ - prevTickZ, 2));
|
||||
if (movedThisTick < 0.01) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview() && tick > 1) {
|
||||
double dx = caster.posX - prevTickX;
|
||||
double dz = caster.posZ - prevTickZ;
|
||||
if (dx * dx + dz * dz < 0.0001) {
|
||||
stopDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
@@ -187,57 +201,56 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
|
||||
// Calculate distance traveled
|
||||
double distanceTraveled = Math.sqrt(
|
||||
Math.pow(caster.posX - startX, 2) +
|
||||
Math.pow(caster.posZ - startZ, 2)
|
||||
);
|
||||
|
||||
// Check if reached max distance
|
||||
if (distanceTraveled >= dashDistance) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
double travelDx = caster.posX - startX;
|
||||
double travelDz = caster.posZ - startZ;
|
||||
if (travelDx * travelDx + travelDz * travelDz >= (double) dashDistance * dashDistance) {
|
||||
stopDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDashBlocked(caster)) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
// Block detection (skip in preview - no real world collision)
|
||||
if (!isPreview() && isDashBlocked(caster)) {
|
||||
stopDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move caster
|
||||
// Move caster (motionY left to gravity for skip arc)
|
||||
caster.motionX = dashDirection.xCoord * dashSpeed;
|
||||
caster.motionY = 0;
|
||||
caster.motionZ = dashDirection.zCoord * dashSpeed;
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
|
||||
// Trail particles
|
||||
world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0);
|
||||
// Trail particles
|
||||
world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopDash(EntityLivingBase caster) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
stopDash(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
stopDash(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
dashDirection = null;
|
||||
chosenDirection = null;
|
||||
maxActiveTicks = 0;
|
||||
}
|
||||
|
||||
private boolean isDashBlocked(EntityLivingBase caster) {
|
||||
@@ -247,66 +260,8 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
return !caster.worldObj.getCollidingBoundingBoxes(caster, caster.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty();
|
||||
}
|
||||
|
||||
// ==================== PREVIEW MODE ====================
|
||||
|
||||
private transient double previewDirX, previewDirZ;
|
||||
private transient double previewStartX, previewStartZ;
|
||||
private transient boolean previewDashing = false;
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewExecute(EntityNPCInterface npc) {
|
||||
previewStartX = npc.posX;
|
||||
previewStartZ = npc.posZ;
|
||||
previewDashing = false;
|
||||
|
||||
// Pick a random direction based on mode
|
||||
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
|
||||
? AGGRESSIVE_DIRECTIONS
|
||||
: DEFENSIVE_DIRECTIONS;
|
||||
DashDirection dir = directions[RANDOM.nextInt(directions.length)];
|
||||
|
||||
float baseYaw;
|
||||
if (previewTarget != null) {
|
||||
double dx = previewTarget.posX - npc.posX;
|
||||
double dz = previewTarget.posZ - npc.posZ;
|
||||
baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
} else {
|
||||
baseYaw = npc.rotationYaw;
|
||||
}
|
||||
|
||||
float dashYaw = baseYaw + dir.getAngleOffset();
|
||||
float yawRad = (float) Math.toRadians(dashYaw);
|
||||
previewDirX = -Math.sin(yawRad);
|
||||
previewDirZ = Math.cos(yawRad);
|
||||
previewDashing = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
|
||||
if (!previewDashing) return;
|
||||
|
||||
double distTraveled = Math.sqrt(
|
||||
Math.pow(npc.posX - previewStartX, 2) +
|
||||
Math.pow(npc.posZ - previewStartZ, 2)
|
||||
);
|
||||
|
||||
if (distTraveled >= dashDistance) {
|
||||
previewDashing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
npc.prevPosX = npc.posX;
|
||||
npc.prevPosY = npc.posY;
|
||||
npc.prevPosZ = npc.posZ;
|
||||
|
||||
npc.posX += previewDirX * dashSpeed;
|
||||
npc.posZ += previewDirZ * dashSpeed;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return (int) Math.ceil(dashDistance / dashSpeed) + 5;
|
||||
}
|
||||
|
||||
@@ -330,7 +285,7 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
this.dashMode = DashMode.DEFENSIVE;
|
||||
}
|
||||
this.dashDistance = nbt.hasKey("dashDistance") ? nbt.getFloat("dashDistance") : 4.0f;
|
||||
this.dashSpeed = nbt.hasKey("dashSpeed") ? nbt.getFloat("dashSpeed") : 1.5f;
|
||||
this.dashSpeed = nbt.hasKey("dashSpeed") ? nbt.getFloat("dashSpeed") : 0.5f;
|
||||
}
|
||||
|
||||
// Getters & Setters
|
||||
|
||||
@@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.Vec3;
|
||||
@@ -23,33 +22,45 @@ import noppes.npcs.api.ability.type.IAbilityDisc;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
|
||||
/**
|
||||
* Disc ability: Spawns a flat spinning disc projectile.
|
||||
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
|
||||
* Has optional boomerang behavior to return to owner.
|
||||
*/
|
||||
public class AbilityDisc extends Ability implements IAbilityDisc {
|
||||
|
||||
// Disc geometry
|
||||
private static final int MAX_PROJECTILES = 8;
|
||||
|
||||
// Disc geometry (shared)
|
||||
private float discRadius = 1.0f;
|
||||
private float discThickness = 0.2f;
|
||||
private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward)
|
||||
|
||||
// Boomerang properties
|
||||
// Boomerang properties (shared)
|
||||
private boolean boomerang = false;
|
||||
private int boomerangDelay = 40;
|
||||
|
||||
// Energy data classes
|
||||
private final EnergyDisplayData colorData = new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f);
|
||||
// Projectile count and fire stagger
|
||||
private int projectileCount = 1;
|
||||
private int fireDelay = 0;
|
||||
|
||||
// Shared visual data (all projectiles use these)
|
||||
private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f);
|
||||
private EnergyLightningData lightningData = new EnergyLightningData();
|
||||
private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
|
||||
|
||||
// Shared combat/movement data
|
||||
private final EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f);
|
||||
private final EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f);
|
||||
private final EnergyLightningData lightningData = new EnergyLightningData();
|
||||
private final EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200);
|
||||
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
|
||||
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
|
||||
|
||||
// Transient state for disc entity (used during windup charging)
|
||||
private transient EntityAbilityDisc discEntity = null;
|
||||
// Per-projectile data (anchor + optional color override)
|
||||
private ProjectileData[] projectiles;
|
||||
|
||||
// Transient entity state
|
||||
private transient EntityAbilityDisc[] discEntities;
|
||||
|
||||
public AbilityDisc() {
|
||||
this.typeId = "ability.cnpc.disc";
|
||||
@@ -62,9 +73,91 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.showTelegraph = true;
|
||||
// Default built-in animations
|
||||
this.windUpAnimationName = "Ability_Disc_Windup";
|
||||
this.activeAnimationName = "Ability_Disc_Active";
|
||||
initProjectiles(1);
|
||||
}
|
||||
|
||||
private void initProjectiles(int count) {
|
||||
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
|
||||
this.projectiles = new ProjectileData[projectileCount];
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disc anchor order is REVERSED from Orb/Beam: [0]=LEFT_HAND, [1]=RIGHT_HAND
|
||||
*/
|
||||
private AnchorPoint getDefaultAnchor(int index) {
|
||||
switch (index) {
|
||||
case 0: return AnchorPoint.RIGHT_HAND;
|
||||
case 1: return AnchorPoint.LEFT_HAND;
|
||||
default: return AnchorPoint.FRONT;
|
||||
}
|
||||
}
|
||||
|
||||
public void setProjectileCount(int count) {
|
||||
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
|
||||
if (newCount == projectileCount) return;
|
||||
|
||||
ProjectileData[] oldProjectiles = projectiles;
|
||||
initProjectiles(newCount);
|
||||
|
||||
if (oldProjectiles != null) {
|
||||
int copyCount = Math.min(oldProjectiles.length, newCount);
|
||||
for (int i = 0; i < copyCount; i++) {
|
||||
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getProjectileCount() {
|
||||
return projectileCount;
|
||||
}
|
||||
|
||||
public int getFireDelay() {
|
||||
return fireDelay;
|
||||
}
|
||||
|
||||
public void setFireDelay(int delay) {
|
||||
this.fireDelay = Math.max(0, delay);
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #getFireDelay()} */
|
||||
public int getDualFireDelay() { return fireDelay; }
|
||||
/** @deprecated Use {@link #setFireDelay(int)} */
|
||||
public void setDualFireDelay(int delay) { setFireDelay(delay); }
|
||||
|
||||
@Override
|
||||
public List<AbilityVariant> getVariants() {
|
||||
return Arrays.asList(
|
||||
new AbilityVariant("ability.variant.single", a -> {
|
||||
a.setName("Disc");
|
||||
}),
|
||||
new AbilityVariant("ability.variant.dual", a -> {
|
||||
AbilityDisc disc = (AbilityDisc) a;
|
||||
a.setName("Dual Disc");
|
||||
disc.setProjectileCount(2);
|
||||
disc.setFireDelay(5);
|
||||
// Set color override for second disc
|
||||
disc.projectiles[1].colorOverride = true;
|
||||
disc.projectiles[1].innerColor = 0xFFFFFF;
|
||||
disc.projectiles[1].outerColor = 0x8800FF;
|
||||
a.setWindUpAnimationName("Ability_DiscDual_Windup");
|
||||
a.setActiveAnimationName("Ability_DiscDual_Active");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowOverlap() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadyForBurstCompletion(int activeTick) {
|
||||
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -77,49 +170,120 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
signalCompletion();
|
||||
return;
|
||||
/**
|
||||
* Create disc entities.
|
||||
* NPC: target is the aggro target — discs will home toward it.
|
||||
* Player: target is null — discs fire in caster's look direction.
|
||||
*/
|
||||
private EntityAbilityDisc[] createDiscEntities(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
EntityAbilityDisc[] entities = new EntityAbilityDisc[projectileCount];
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor);
|
||||
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
|
||||
entities[i] = new EntityAbilityDisc(
|
||||
world, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
|
||||
discRadius, discThickness,
|
||||
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData,
|
||||
boomerang, boomerangDelay);
|
||||
entities[i].setEffects(this.effects);
|
||||
entities[i].setSourceAbility(this);
|
||||
}
|
||||
|
||||
// Start moving the disc that was spawned during windup
|
||||
if (discEntity != null && !discEntity.isDead) {
|
||||
discEntity.startMoving(target);
|
||||
if (projectileCount == 2 && entities[0] != null && entities[1] != null) {
|
||||
entities[0].setSiblingEntityId(entities[1].getEntityId());
|
||||
entities[1].setSiblingEntityId(entities[0].getEntityId());
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
private void fireDiscEntity(EntityAbilityDisc disc, EntityLivingBase target) {
|
||||
if (disc == null || disc.isDead) return;
|
||||
if (isPreview()) {
|
||||
disc.startPreviewFiring();
|
||||
} else {
|
||||
disc.startMoving(target);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Spawn disc in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
// Create disc in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
|
||||
discEntity = new EntityAbilityDisc(
|
||||
world, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
|
||||
discRadius, discThickness,
|
||||
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData,
|
||||
boomerang, boomerangDelay);
|
||||
discEntity.setupCharging(anchorData, windUpTicks);
|
||||
discEntities = createDiscEntities(caster, target, world);
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
|
||||
if (isPreview()) {
|
||||
discEntities[i].setupPreview(caster, discRadius, discThickness, resolved, lightningData, projectiles[i].anchor, windUpTicks, vertical);
|
||||
} else {
|
||||
discEntities[i].setupCharging(projectiles[i].anchor, windUpTicks, vertical);
|
||||
}
|
||||
spawnAbilityEntity(world, discEntities[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discEntity.setEffects(this.effects);
|
||||
discEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(discEntity);
|
||||
@Override
|
||||
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
discEntities = createDiscEntities(caster, target, world);
|
||||
for (EntityAbilityDisc disc : discEntities) {
|
||||
spawnAbilityEntity(world, disc);
|
||||
}
|
||||
|
||||
// Fire first projectile immediately, let onActiveTick handle the rest with fireDelay
|
||||
fireDiscEntity(discEntities[0], target);
|
||||
if (fireDelay <= 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
fireDiscEntity(discEntities[i], target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (discEntities == null) return;
|
||||
|
||||
// Fire first projectile immediately
|
||||
fireDiscEntity(discEntities[0], target);
|
||||
|
||||
// Fire remaining projectiles if no delay
|
||||
if (fireDelay <= 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
fireDiscEntity(discEntities[i], target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// Signal completion when entity dies
|
||||
if (discEntity == null || discEntity.isDead) {
|
||||
discEntity = null;
|
||||
if (discEntities == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire staggered projectiles
|
||||
if (fireDelay > 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
if (tick == fireDelay * i) {
|
||||
fireDiscEntity(discEntities[i], target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all entities are dead
|
||||
boolean allDead = true;
|
||||
for (EntityAbilityDisc disc : discEntities) {
|
||||
if (disc != null && !disc.isDead) {
|
||||
allDead = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allDead) {
|
||||
signalCompletion();
|
||||
}
|
||||
}
|
||||
@@ -135,16 +299,22 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn disc entity if still alive
|
||||
if (discEntity != null && !discEntity.isDead) {
|
||||
discEntity.setDead();
|
||||
if (discEntities != null) {
|
||||
for (int i = 0; i < discEntities.length; i++) {
|
||||
if (discEntities[i] != null && !discEntities[i].isDead) {
|
||||
discEntities[i].setDead();
|
||||
}
|
||||
discEntities[i] = null;
|
||||
}
|
||||
}
|
||||
discEntity = null;
|
||||
discEntities = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
|
||||
// NPC: telegraph on aggro target position
|
||||
// Player: no telegraph (discs fire in look direction, no fixed position to mark)
|
||||
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -173,173 +343,297 @@ public class AbilityDisc extends Ability implements IAbilityDisc {
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("discRadius", discRadius);
|
||||
nbt.setFloat("discThickness", discThickness);
|
||||
nbt.setBoolean("vertical", vertical);
|
||||
nbt.setBoolean("boomerang", boomerang);
|
||||
nbt.setInteger("boomerangDelay", boomerangDelay);
|
||||
anchorData.writeNBT(nbt);
|
||||
colorData.writeNBT(nbt);
|
||||
nbt.setInteger("projectileCount", projectileCount);
|
||||
nbt.setInteger("fireDelay", fireDelay);
|
||||
|
||||
// Shared visual data
|
||||
displayData.writeNBT(nbt);
|
||||
lightningData.writeNBT(nbt);
|
||||
|
||||
// Shared combat/movement data
|
||||
combatData.writeNBT(nbt);
|
||||
homingData.writeNBT(nbt);
|
||||
lightningData.writeNBT(nbt);
|
||||
lifespanData.writeNBT(nbt);
|
||||
|
||||
// Per-projectile data
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
NBTTagCompound projNbt = new NBTTagCompound();
|
||||
projectiles[i].writeNBT(projNbt);
|
||||
nbt.setTag("Projectile_" + i, projNbt);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f;
|
||||
this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f;
|
||||
this.vertical = nbt.hasKey("vertical") && nbt.getBoolean("vertical");
|
||||
this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang");
|
||||
this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40;
|
||||
anchorData.readNBT(nbt);
|
||||
colorData.readNBT(nbt);
|
||||
|
||||
int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1;
|
||||
initProjectiles(count);
|
||||
|
||||
this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0;
|
||||
|
||||
// Shared combat/movement data
|
||||
combatData.readNBT(nbt);
|
||||
homingData.readNBT(nbt);
|
||||
lightningData.readNBT(nbt);
|
||||
lifespanData.readNBT(nbt);
|
||||
|
||||
// Shared visual data
|
||||
displayData.readNBT(nbt);
|
||||
lightningData.readNBT(nbt);
|
||||
|
||||
// Per-projectile data
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
if (nbt.hasKey("Projectile_" + i)) {
|
||||
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getters & Setters
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// API GETTERS & SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public float getSpeed() { return homingData.speed; }
|
||||
public void setSpeed(float speed) { homingData.speed = speed; }
|
||||
|
||||
public float getDiscRadius() { return discRadius; }
|
||||
public void setDiscRadius(float discRadius) { this.discRadius = discRadius; }
|
||||
|
||||
public float getDiscThickness() { return discThickness; }
|
||||
public void setDiscThickness(float discThickness) { this.discThickness = discThickness; }
|
||||
public float getMaxDistance() { return lifespanData.maxDistance; }
|
||||
public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; }
|
||||
public int getMaxLifetime() { return lifespanData.maxLifetime; }
|
||||
public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; }
|
||||
public float getDamage() { return combatData.damage; }
|
||||
public void setDamage(float damage) { combatData.damage = damage; }
|
||||
public float getKnockback() { return combatData.knockback; }
|
||||
public void setKnockback(float knockback) { combatData.knockback = knockback; }
|
||||
public float getKnockbackUp() { return combatData.knockbackUp; }
|
||||
public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; }
|
||||
public boolean isHoming() { return homingData.homing; }
|
||||
public void setHoming(boolean homing) { homingData.homing = homing; }
|
||||
public float getHomingStrength() { return homingData.homingStrength; }
|
||||
public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; }
|
||||
public float getHomingRange() { return homingData.homingRange; }
|
||||
public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; }
|
||||
|
||||
public boolean isVertical() { return vertical; }
|
||||
public void setVertical(boolean vertical) { this.vertical = vertical; }
|
||||
|
||||
public boolean isBoomerang() { return boomerang; }
|
||||
public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; }
|
||||
|
||||
public int getBoomerangDelay() { return boomerangDelay; }
|
||||
public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; }
|
||||
|
||||
public float getMaxDistance() { return lifespanData.maxDistance; }
|
||||
public void setMaxDistance(float d) { lifespanData.maxDistance = d; }
|
||||
|
||||
public int getMaxLifetime() { return lifespanData.maxLifetime; }
|
||||
public void setMaxLifetime(int t) { lifespanData.maxLifetime = t; }
|
||||
|
||||
public float getDamage() { return combatData.damage; }
|
||||
public void setDamage(float d) { combatData.damage = d; }
|
||||
|
||||
public float getKnockback() { return combatData.knockback; }
|
||||
public void setKnockback(float k) { combatData.knockback = k; }
|
||||
|
||||
public float getKnockbackUp() { return combatData.knockbackUp; }
|
||||
public void setKnockbackUp(float k) { combatData.knockbackUp = k; }
|
||||
|
||||
public boolean isHoming() { return homingData.homing; }
|
||||
public void setHoming(boolean h) { homingData.homing = h; }
|
||||
|
||||
public float getHomingStrength() { return homingData.homingStrength; }
|
||||
public void setHomingStrength(float s) { homingData.homingStrength = s; }
|
||||
|
||||
public float getHomingRange() { return homingData.homingRange; }
|
||||
public void setHomingRange(float r) { homingData.homingRange = r; }
|
||||
|
||||
public boolean isExplosive() { return combatData.explosive; }
|
||||
public void setExplosive(boolean explosive) { combatData.explosive = explosive; }
|
||||
public void setExplosive(boolean e) { combatData.explosive = e; }
|
||||
|
||||
public float getExplosionRadius() { return combatData.explosionRadius; }
|
||||
public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; }
|
||||
public void setExplosionRadius(float r) { combatData.explosionRadius = r; }
|
||||
|
||||
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
|
||||
public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; }
|
||||
public int getInnerColor() { return colorData.innerColor; }
|
||||
public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; }
|
||||
public int getOuterColor() { return colorData.outerColor; }
|
||||
public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; }
|
||||
public float getOuterColorWidth() { return colorData.outerColorWidth; }
|
||||
public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; }
|
||||
public float getOuterColorAlpha() { return colorData.outerColorAlpha; }
|
||||
public void setOuterColorAlpha(float outerColorAlpha) { colorData.outerColorAlpha = outerColorAlpha; }
|
||||
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
|
||||
public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; }
|
||||
public float getRotationSpeed() { return colorData.rotationSpeed; }
|
||||
public void setRotationSpeed(float rotationSpeed) { colorData.rotationSpeed = rotationSpeed; }
|
||||
public void setExplosionDamageFalloff(float f) { combatData.explosionDamageFalloff = f; }
|
||||
|
||||
// Shared display data (primary colors)
|
||||
public int getInnerColor() { return displayData.innerColor; }
|
||||
public void setInnerColor(int c) { displayData.innerColor = c; }
|
||||
|
||||
public int getOuterColor() { return displayData.outerColor; }
|
||||
public void setOuterColor(int c) { displayData.outerColor = c; }
|
||||
|
||||
public float getOuterColorWidth() { return displayData.outerColorWidth; }
|
||||
public void setOuterColorWidth(float w) { displayData.outerColorWidth = w; }
|
||||
|
||||
public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
|
||||
public void setOuterColorAlpha(float a) { displayData.outerColorAlpha = a; }
|
||||
|
||||
public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
|
||||
public void setOuterColorEnabled(boolean e) { displayData.outerColorEnabled = e; }
|
||||
|
||||
public float getRotationSpeed() { return displayData.rotationSpeed; }
|
||||
public void setRotationSpeed(float s) { displayData.rotationSpeed = s; }
|
||||
|
||||
// Shared lightning data
|
||||
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
|
||||
public void setLightningEffect(boolean lightningEffect) { lightningData.lightningEffect = lightningEffect; }
|
||||
public void setLightningEffect(boolean e) { lightningData.lightningEffect = e; }
|
||||
|
||||
public float getLightningDensity() { return lightningData.lightningDensity; }
|
||||
public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; }
|
||||
public void setLightningDensity(float d) { lightningData.lightningDensity = d; }
|
||||
|
||||
public float getLightningRadius() { return lightningData.lightningRadius; }
|
||||
public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; }
|
||||
public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; }
|
||||
public float getAnchorOffsetX() { return anchorData.anchorOffsetX; }
|
||||
public float getAnchorOffsetY() { return anchorData.anchorOffsetY; }
|
||||
public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; }
|
||||
public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; }
|
||||
public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; }
|
||||
public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; }
|
||||
public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; }
|
||||
public void setLightningRadius(float r) { lightningData.lightningRadius = r; }
|
||||
|
||||
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
|
||||
public void setLightningFadeTime(int t) { lightningData.lightningFadeTime = t; }
|
||||
|
||||
// Anchor - default to projectile 0
|
||||
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
|
||||
public void setAnchorPointEnum(AnchorPoint p) { projectiles[0].anchor.anchorPoint = p; }
|
||||
|
||||
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
|
||||
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
|
||||
|
||||
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
|
||||
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
|
||||
|
||||
public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
|
||||
public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
|
||||
|
||||
@Override
|
||||
public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); }
|
||||
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
|
||||
|
||||
@Override
|
||||
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
// Indexed API methods
|
||||
public int getInnerColor(int index) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
return p.colorOverride ? p.innerColor : displayData.innerColor;
|
||||
}
|
||||
public void setInnerColor(int index, int color) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
p.colorOverride = true;
|
||||
p.innerColor = color;
|
||||
}
|
||||
|
||||
EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj);
|
||||
disc.setupPreview(npc, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks);
|
||||
return disc;
|
||||
public int getOuterColor(int index) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
return p.colorOverride ? p.outerColor : displayData.outerColor;
|
||||
}
|
||||
public void setOuterColor(int index, int color) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
p.colorOverride = true;
|
||||
p.outerColor = color;
|
||||
}
|
||||
|
||||
public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); }
|
||||
public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; }
|
||||
public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; }
|
||||
public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; }
|
||||
|
||||
private int clampIndex(int index) {
|
||||
return Math.max(0, Math.min(index, projectileCount - 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GUI FIELD DEFINITIONS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
defs.addAll(Arrays.asList(
|
||||
// Type tab
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
|
||||
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
|
||||
),
|
||||
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
|
||||
FieldDef.section("ability.section.disc"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
|
||||
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
|
||||
),
|
||||
FieldDef.section("ability.section.homing"),
|
||||
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
|
||||
.hover("ability.hover.homing"),
|
||||
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
|
||||
.visibleWhen(this::isHoming),
|
||||
FieldDef.section("ability.section.boomerang"),
|
||||
FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang)
|
||||
.hover("ability.hover.boomerang"),
|
||||
FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay)
|
||||
.visibleWhen(this::isBoomerang),
|
||||
FieldDef.section("ability.section.explosive"),
|
||||
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive)
|
||||
.hover("ability.hover.explosive"),
|
||||
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.visibleWhen(this::isExplosive),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
|
||||
// Visual tab
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
|
||||
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
|
||||
.visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
|
||||
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.section("ability.section.effects").tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
|
||||
.range(0.01f, 100f).visibleWhen(this::hasLightningEffect),
|
||||
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
|
||||
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
|
||||
).tab("ability.tab.visual")
|
||||
// Type tab - projectile count and fire delay
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
|
||||
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
|
||||
.range(0, 200).visibleWhen(() -> projectileCount > 1)
|
||||
));
|
||||
|
||||
// Type tab - disc properties
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
|
||||
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
|
||||
));
|
||||
defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback));
|
||||
defs.add(FieldDef.section("ability.section.disc"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
|
||||
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
|
||||
));
|
||||
defs.add(FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical)
|
||||
.hover("ability.hover.vertical"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
|
||||
));
|
||||
defs.add(FieldDef.section("ability.section.homing"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
|
||||
defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming));
|
||||
defs.add(FieldDef.section("ability.section.boomerang"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang).hover("ability.hover.boomerang"));
|
||||
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(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
|
||||
// Visual tab - shared primary colors
|
||||
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
|
||||
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
|
||||
.visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
|
||||
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
|
||||
).tab("ability.tab.visual"));
|
||||
|
||||
// Visual tab - shared effects
|
||||
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
|
||||
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
|
||||
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
|
||||
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
|
||||
).tab("ability.tab.visual"));
|
||||
|
||||
// Visual tab - per-projectile sections
|
||||
for (int i = 0; i < MAX_PROJECTILES; i++) {
|
||||
final int idx = i;
|
||||
String sectionKey = "ability.section.projectile" + (i + 1);
|
||||
|
||||
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
|
||||
.visibleWhen(() -> idx < projectileCount));
|
||||
|
||||
// Anchor point
|
||||
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
|
||||
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v),
|
||||
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v)
|
||||
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
|
||||
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
|
||||
|
||||
// Color override (only when multiple projectiles)
|
||||
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
|
||||
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
|
||||
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,527 +0,0 @@
|
||||
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.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
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 kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import noppes.npcs.util.ValueUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AbilityDiscDual extends Ability {
|
||||
|
||||
// Disc geometry
|
||||
private float discRadius = 1.0f;
|
||||
private float discThickness = 0.2f;
|
||||
|
||||
// Boomerang properties
|
||||
private boolean boomerang = false;
|
||||
private int boomerangDelay = 40;
|
||||
|
||||
// Dual properties
|
||||
private boolean dualFire = true;
|
||||
private int dualFireDelay = 10;
|
||||
|
||||
// Energy data classes
|
||||
private final EnergyDisplayData[] displayData = new EnergyDisplayData[]{
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f),
|
||||
new EnergyDisplayData(0xFFFFFF, 0x8800FF, true, 0.4f, 0.5f, 5.0f)
|
||||
};
|
||||
private final EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f);
|
||||
private final EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f);
|
||||
private final EnergyLightningData[] lightningData = new EnergyLightningData[]{
|
||||
new EnergyLightningData(), new EnergyLightningData()
|
||||
};
|
||||
private final EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200);
|
||||
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
|
||||
new EnergyAnchorData(AnchorPoint.LEFT_HAND), new EnergyAnchorData(AnchorPoint.RIGHT_HAND)
|
||||
};
|
||||
private final EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{
|
||||
new EnergyTrajectoryData(), new EnergyTrajectoryData()
|
||||
};
|
||||
|
||||
// Transient state for disc entity (used during windup charging)
|
||||
private transient EntityAbilityDisc discEntity = null;
|
||||
private transient EntityAbilityDisc discEntity2 = null;
|
||||
|
||||
public AbilityDiscDual() {
|
||||
this.typeId = "ability.cnpc.disc_dual";
|
||||
this.name = "Disc Dual";
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 30.0f;
|
||||
this.minRange = 5.0f;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 60;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.showTelegraph = true;
|
||||
// Default built-in animations
|
||||
this.windUpAnimationName = "Ability_DiscDual_Windup";
|
||||
this.activeAnimationName = "Ability_DiscDual_Active";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetingMode[] getAllowedTargetingModes() {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start moving the orb that was spawned during windup
|
||||
if (discEntity != null && !discEntity.isDead) {
|
||||
discEntity.startMoving(target);
|
||||
}
|
||||
|
||||
if (dualFire) {
|
||||
if (discEntity2 != null && !discEntity2.isDead && dualFireDelay <= 0) {
|
||||
discEntity2.startMoving(target);
|
||||
}
|
||||
} else if (discEntity2 != null) {
|
||||
discEntity2.setDead();
|
||||
}
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
|
||||
// Spawn disc in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
// Create disc in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]);
|
||||
discEntity = new EntityAbilityDisc(
|
||||
world, caster, target,
|
||||
spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord,
|
||||
discRadius, discThickness,
|
||||
displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0],
|
||||
boomerang, boomerangDelay);
|
||||
|
||||
Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]);
|
||||
discEntity2 = new EntityAbilityDisc(
|
||||
world, caster, target,
|
||||
spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord,
|
||||
discRadius, discThickness,
|
||||
displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1],
|
||||
boomerang, boomerangDelay);
|
||||
|
||||
discEntity.setSiblingEntityId(discEntity2.getEntityId());
|
||||
discEntity2.setSiblingEntityId(discEntity.getEntityId());
|
||||
|
||||
discEntity.setupCharging(anchorData[0], windUpTicks);
|
||||
discEntity2.setupCharging(anchorData[1], windUpTicks);
|
||||
|
||||
discEntity.setEffects(this.effects);
|
||||
|
||||
if (dualFire) {
|
||||
discEntity2.setEffects(this.effects);
|
||||
}
|
||||
|
||||
discEntity.setSourceAbility(this);
|
||||
discEntity2.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(discEntity);
|
||||
world.spawnEntityInWorld(discEntity2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// Signal completion when entity dies
|
||||
if (dualFire) {
|
||||
if ((discEntity == null || discEntity.isDead) && (discEntity2 == null || discEntity2.isDead)) {
|
||||
discEntity = null;
|
||||
discEntity2 = null;
|
||||
signalCompletion();
|
||||
}
|
||||
|
||||
if (tick == dualFireDelay && discEntity2 != null && !discEntity2.isDead) {
|
||||
discEntity2.startMoving(target);
|
||||
}
|
||||
} else {
|
||||
if (discEntity == null || discEntity.isDead) {
|
||||
discEntity = null;
|
||||
signalCompletion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn disc entity if still alive
|
||||
if (discEntity != null && !discEntity.isDead) {
|
||||
discEntity.setDead();
|
||||
}
|
||||
|
||||
if (discEntity2 != null && !discEntity2.isDead) {
|
||||
discEntity2.setDead();
|
||||
}
|
||||
|
||||
discEntity = null;
|
||||
discEntity2 = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create circle telegraph at target position
|
||||
Telegraph telegraph = Telegraph.circle(discRadius * 1.5f);
|
||||
telegraph.setDurationTicks(windUpTicks);
|
||||
telegraph.setColor(windUpColor);
|
||||
telegraph.setWarningColor(activeColor);
|
||||
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
|
||||
telegraph.setHeightOffset(telegraphHeightOffset);
|
||||
|
||||
// Position at target and follow target during windup
|
||||
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
|
||||
instance.setCasterEntityId(caster.getEntityId());
|
||||
instance.setEntityIdToFollow(target.getEntityId());
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTelegraphRadius() {
|
||||
return discRadius * 1.5f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("discRadius", discRadius);
|
||||
nbt.setFloat("discThickness", discThickness);
|
||||
nbt.setBoolean("boomerang", boomerang);
|
||||
nbt.setInteger("boomerangDelay", boomerangDelay);
|
||||
nbt.setBoolean("dualFire", dualFire);
|
||||
nbt.setInteger("dualFireDelay", dualFireDelay);
|
||||
combatData.writeNBT(nbt);
|
||||
homingData.writeNBT(nbt);
|
||||
lifespanData.writeNBT(nbt);
|
||||
|
||||
NBTTagCompound disc1 = new NBTTagCompound();
|
||||
NBTTagCompound disc2 = new NBTTagCompound();
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
NBTTagCompound disc = i == 0 ? disc1 : disc2;
|
||||
anchorData[i].writeNBT(disc);
|
||||
displayData[i].writeNBT(disc);
|
||||
lightningData[i].writeNBT(disc);
|
||||
trajectoryData[i].writeNBT(disc);
|
||||
}
|
||||
|
||||
nbt.setTag("Disc_1", disc1);
|
||||
nbt.setTag("Disc_2", disc2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
NBTTagCompound disc1 = nbt.hasKey("Disc_1") ? nbt.getCompoundTag("Disc_1") : new NBTTagCompound();
|
||||
NBTTagCompound disc2 = nbt.hasKey("Disc_2") ? nbt.getCompoundTag("Disc_2") : new NBTTagCompound();
|
||||
|
||||
this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f;
|
||||
this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f;
|
||||
this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang");
|
||||
this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40;
|
||||
this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire");
|
||||
this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0;
|
||||
combatData.readNBT(nbt);
|
||||
homingData.readNBT(nbt);
|
||||
lifespanData.readNBT(nbt);
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
NBTTagCompound disc = i == 0 ? disc1 : disc2;
|
||||
anchorData[i].readNBT(disc);
|
||||
displayData[i].readNBT(disc);
|
||||
lightningData[i].readNBT(disc);
|
||||
trajectoryData[i].readNBT(disc);
|
||||
}
|
||||
}
|
||||
|
||||
// Getters & Setters
|
||||
private EnergyDisplayData getColorData(int orb) {
|
||||
orb = ValueUtil.clamp(orb, 0, 1);
|
||||
return displayData[orb];
|
||||
}
|
||||
private EnergyLightningData getLightningData(int orb) {
|
||||
orb = ValueUtil.clamp(orb, 0, 1);
|
||||
return lightningData[orb];
|
||||
}
|
||||
private EnergyAnchorData getAnchorData(int orb) {
|
||||
orb = ValueUtil.clamp(orb, 0, 1);
|
||||
return anchorData[orb];
|
||||
}
|
||||
|
||||
public boolean isDualFire() { return dualFire; }
|
||||
public void setDualFire(boolean dualFire) { this.dualFire = dualFire; }
|
||||
public int getDualFireDelay() { return dualFireDelay; }
|
||||
public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; }
|
||||
public float getSpeed() { return homingData.speed; }
|
||||
public void setSpeed(float speed) { homingData.speed = speed; }
|
||||
public float getDiscRadius() { return discRadius; }
|
||||
public void setDiscRadius(float discRadius) { this.discRadius = discRadius; }
|
||||
public float getDiscThickness() { return discThickness; }
|
||||
public void setDiscThickness(float discThickness) { this.discThickness = discThickness; }
|
||||
public float getMaxDistance() { return lifespanData.maxDistance; }
|
||||
public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; }
|
||||
public int getMaxLifetime() { return lifespanData.maxLifetime; }
|
||||
public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; }
|
||||
public float getDamage() { return combatData.damage; }
|
||||
public void setDamage(float damage) { combatData.damage = damage; }
|
||||
public float getKnockback() { return combatData.knockback; }
|
||||
public void setKnockback(float knockback) { combatData.knockback = knockback; }
|
||||
public float getKnockbackUp() { return combatData.knockbackUp; }
|
||||
public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; }
|
||||
public boolean isHoming() { return homingData.homing; }
|
||||
public void setHoming(boolean homing) { homingData.homing = homing; }
|
||||
public float getHomingStrength() { return homingData.homingStrength; }
|
||||
public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; }
|
||||
public float getHomingRange() { return homingData.homingRange; }
|
||||
public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; }
|
||||
public boolean isBoomerang() { return boomerang; }
|
||||
public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; }
|
||||
public int getBoomerangDelay() { return boomerangDelay; }
|
||||
public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; }
|
||||
public boolean isExplosive() { return combatData.explosive; }
|
||||
public void setExplosive(boolean explosive) { combatData.explosive = explosive; }
|
||||
public float getExplosionRadius() { return combatData.explosionRadius; }
|
||||
public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; }
|
||||
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
|
||||
public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; }
|
||||
|
||||
|
||||
public int getInnerColor(int orb) { return getColorData(orb).innerColor; }
|
||||
public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; }
|
||||
public int getOuterColor(int orb) { return getColorData(orb).outerColor; }
|
||||
public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; }
|
||||
public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; }
|
||||
public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; }
|
||||
public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; }
|
||||
public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; }
|
||||
public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; }
|
||||
public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; }
|
||||
public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; }
|
||||
public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; }
|
||||
|
||||
public int getInnerColor() { return getInnerColor(0); }
|
||||
public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); }
|
||||
public int getOuterColor() { return getOuterColor(0); }
|
||||
public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); }
|
||||
public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); }
|
||||
public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); }
|
||||
public float getOuterColorWidth() { return getOuterColorWidth(0); }
|
||||
public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); }
|
||||
public float getOuterColorAlpha() { return getOuterColorAlpha(0); }
|
||||
public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); }
|
||||
public float getRotationSpeed() { return getRotationSpeed(0); }
|
||||
public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); }
|
||||
|
||||
|
||||
public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; }
|
||||
public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; }
|
||||
public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; }
|
||||
public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; }
|
||||
public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; }
|
||||
public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; }
|
||||
|
||||
public boolean hasLightningEffect() { return hasLightningEffect(0); }
|
||||
public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); }
|
||||
public float getLightningDensity() { return getLightningDensity(0); }
|
||||
public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); }
|
||||
public float getLightningRadius() { return getLightningRadius(0); }
|
||||
public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); }
|
||||
|
||||
|
||||
public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; }
|
||||
public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; }
|
||||
public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; }
|
||||
public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; }
|
||||
public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; }
|
||||
public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; }
|
||||
public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; }
|
||||
public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; }
|
||||
|
||||
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
|
||||
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
|
||||
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
|
||||
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
|
||||
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
|
||||
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
|
||||
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
|
||||
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
|
||||
|
||||
//@Override
|
||||
public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); }
|
||||
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
|
||||
|
||||
//@Override
|
||||
public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
defs.addAll(Arrays.asList(
|
||||
// Type tab
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
|
||||
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
|
||||
),
|
||||
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
|
||||
FieldDef.section("ability.section.disc"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius),
|
||||
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
|
||||
),
|
||||
FieldDef.section("ability.section.dual"),
|
||||
FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire),
|
||||
FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay)
|
||||
.visibleWhen(this::isDualFire),
|
||||
FieldDef.section("ability.section.homing"),
|
||||
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming)
|
||||
.hover("ability.hover.homing"),
|
||||
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength)
|
||||
.visibleWhen(this::isHoming),
|
||||
FieldDef.section("ability.section.boomerang"),
|
||||
FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang)
|
||||
.hover("ability.hover.boomerang"),
|
||||
FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay)
|
||||
.visibleWhen(this::isBoomerang),
|
||||
FieldDef.section("ability.section.explosive"),
|
||||
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive)
|
||||
.hover("ability.hover.explosive"),
|
||||
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.visibleWhen(this::isExplosive),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
|
||||
// Visual tab - Disc 1
|
||||
FieldDef.section("ability.section.disc1").tab("ability.tab.visual"),
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
|
||||
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor",
|
||||
() -> getInnerColor(0), v -> setInnerColor(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled",
|
||||
() -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor",
|
||||
() -> getOuterColor(0), v -> setOuterColor(0, v))
|
||||
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth",
|
||||
() -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v))
|
||||
.visibleWhen(() -> isOuterColorEnabled(0)),
|
||||
FieldDef.floatField("ability.outerAlpha",
|
||||
() -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v))
|
||||
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(0))
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.rotationSpeed",
|
||||
() -> getRotationSpeed(0), v -> setRotationSpeed(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.lightning",
|
||||
() -> hasLightningEffect(0), v -> setLightningEffect(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density",
|
||||
() -> getLightningDensity(0), v -> setLightningDensity(0, v))
|
||||
.range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)),
|
||||
FieldDef.floatField("gui.radius",
|
||||
() -> getLightningRadius(0), v -> setLightningRadius(0, v))
|
||||
.range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0))
|
||||
).tab("ability.tab.visual"),
|
||||
// Visual tab - Disc 2
|
||||
FieldDef.section("ability.section.disc2").tab("ability.tab.visual"),
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
|
||||
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor",
|
||||
() -> getInnerColor(1), v -> setInnerColor(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled",
|
||||
() -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor",
|
||||
() -> getOuterColor(1), v -> setOuterColor(1, v))
|
||||
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth",
|
||||
() -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v))
|
||||
.visibleWhen(() -> isOuterColorEnabled(1)),
|
||||
FieldDef.floatField("ability.outerAlpha",
|
||||
() -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v))
|
||||
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(1))
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.rotationSpeed",
|
||||
() -> getRotationSpeed(1), v -> setRotationSpeed(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.lightning",
|
||||
() -> hasLightningEffect(1), v -> setLightningEffect(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density",
|
||||
() -> getLightningDensity(1), v -> setLightningDensity(1, v))
|
||||
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)),
|
||||
FieldDef.floatField("gui.radius",
|
||||
() -> getLightningRadius(1), v -> setLightningRadius(1, v))
|
||||
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1))
|
||||
).tab("ability.tab.visual")
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj);
|
||||
disc.setupPreview(npc, discRadius, discThickness, displayData[0], lightningData[0], anchorData[0], windUpTicks);
|
||||
return disc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDamage() {
|
||||
public boolean allowBurst() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -128,16 +128,22 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
|
||||
lastAttacker = attacker;
|
||||
lastDamageTaken = damage;
|
||||
|
||||
float reduction = ValueUtil.clamp(getDamageReductionFactor(), 0, damage);
|
||||
float newHealth = ValueUtil.clamp(caster.getHealth() + reduction, 0, caster.getMaxHealth());
|
||||
|
||||
caster.setHealth(newHealth);
|
||||
|
||||
if (!canCounter || !counterEligible || counterTriggered || !isDirectHit(source)) {
|
||||
caster.setHealth(newHealth);
|
||||
// Players: damage reduction handled via LivingHurtEvent (event.ammount modification)
|
||||
// NPCs: called AFTER damageEntity() so health was already reduced — heal back here
|
||||
if (caster instanceof EntityPlayer) {
|
||||
if (canCounter && counterEligible && !counterTriggered && isDirectHit(source)) {
|
||||
counterTriggered = true;
|
||||
}
|
||||
} else {
|
||||
caster.setHealth(ValueUtil.clamp(caster.getHealth() + damage, 0, caster.getMaxHealth()));
|
||||
counterTriggered = true;
|
||||
float reduction = ValueUtil.clamp(getDamageReductionFactor(), 0, damage);
|
||||
float newHealth = ValueUtil.clamp(caster.getHealth() + reduction, 0, caster.getMaxHealth());
|
||||
|
||||
if (canCounter && counterEligible && !counterTriggered && isDirectHit(source)) {
|
||||
caster.setHealth(ValueUtil.clamp(caster.getHealth() + damage, 0, caster.getMaxHealth()));
|
||||
counterTriggered = true;
|
||||
} else {
|
||||
caster.setHealth(newHealth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,6 +343,10 @@ public class AbilityGuard extends Ability implements IAbilityGuard {
|
||||
this.counterAnimationId = counterAnimationId;
|
||||
}
|
||||
|
||||
public boolean isCounterTriggered() {
|
||||
return counterTriggered;
|
||||
}
|
||||
|
||||
public String getCounterAnimationName() {
|
||||
return counterAnimationName;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
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.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.potion.Potion;
|
||||
import net.minecraft.potion.PotionEffect;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import noppes.npcs.api.ability.type.IAbilityHazard;
|
||||
|
||||
@@ -22,640 +15,165 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Hazard ability: Creates persistent ground effect zones.
|
||||
* Deals damage and/or applies debuffs to entities in the zone over time.
|
||||
* Hazard ability: Creates persistent ground effect zones around the caster.
|
||||
* Spawns one or more EntityAbilityZone entities in random positions
|
||||
* within spawnRadius. Damage and zone logic handled by the entity.
|
||||
*/
|
||||
public class AbilityHazard extends Ability implements IAbilityHazard {
|
||||
|
||||
public enum HazardShape {
|
||||
CIRCLE,
|
||||
RING,
|
||||
CONE;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case CIRCLE: return "ability.shape.circle";
|
||||
case RING: return "ability.shape.ring";
|
||||
case CONE: return "ability.shape.cone";
|
||||
default: return name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum PlacementMode {
|
||||
AT_CASTER,
|
||||
AT_TARGET,
|
||||
FOLLOW_CASTER,
|
||||
FOLLOW_TARGET;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case AT_CASTER: return "ability.placement.atCaster";
|
||||
case AT_TARGET: return "ability.placement.atTarget";
|
||||
case FOLLOW_CASTER: return "ability.placement.followCaster";
|
||||
case FOLLOW_TARGET: return "ability.placement.followTarget";
|
||||
default: return name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int durationTicks = 100;
|
||||
private float radius = 4.0f;
|
||||
private float innerRadius = 0.0f;
|
||||
private float coneAngle = 45.0f;
|
||||
private HazardShape shape = HazardShape.CIRCLE;
|
||||
private PlacementMode placement = PlacementMode.AT_TARGET;
|
||||
public class AbilityHazard extends AbilityZone implements IAbilityHazard {
|
||||
|
||||
// Hazard-specific fields
|
||||
private float radius = 2.0f;
|
||||
private float damagePerSecond = 1.0f;
|
||||
private int damageInterval = 20;
|
||||
private boolean ignoreInvulnFrames = false;
|
||||
|
||||
private int slownessLevel = -1;
|
||||
private int weaknessLevel = -1;
|
||||
private int poisonLevel = -1;
|
||||
private int witherLevel = -1;
|
||||
private int blindnessLevel = -1;
|
||||
private int debuffDuration = 40;
|
||||
|
||||
private boolean affectsCaster = false;
|
||||
private float heightAbove = 2.0f;
|
||||
private float heightBelow = 1.0f;
|
||||
|
||||
// Offset parameters - hazard spawns near target, not exactly on them
|
||||
private float minOffset = 0.0f; // Minimum offset from target
|
||||
private float maxOffset = 2.0f; // Maximum offset from target
|
||||
private boolean randomOffset = true; // Random or fixed offset
|
||||
|
||||
// Runtime state
|
||||
private static final Random RANDOM = new Random();
|
||||
private transient double zoneX, zoneY, zoneZ;
|
||||
private transient boolean positionLocked = false;
|
||||
private transient int ticksSinceDamage = 0;
|
||||
private transient Set<Integer> damagedThisTick = new HashSet<>();
|
||||
|
||||
public AbilityHazard() {
|
||||
super(300, new EnergyDisplayData(0x00CC00, 0x006600, true, 1.0f, 0.5f, 1.5f));
|
||||
this.typeId = "ability.cnpc.hazard";
|
||||
this.name = "Hazard";
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 15.0f;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 30;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.windUpAnimationName = "Ability_Zone_Windup";
|
||||
this.activeAnimationName = "Ability_Zone_Active";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetingMode[] getAllowedTargetingModes() {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTelegraphRadius() {
|
||||
public float getZoneRadius() {
|
||||
return radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
|
||||
TelegraphInstance instance = super.createTelegraph(caster, target);
|
||||
if (instance == null) return null;
|
||||
|
||||
// Control telegraph following based on placement mode
|
||||
switch (placement) {
|
||||
case AT_CASTER:
|
||||
case FOLLOW_CASTER:
|
||||
// Telegraph stays at caster position, no following
|
||||
instance.setEntityIdToFollow(-1);
|
||||
instance.setX(caster.posX);
|
||||
instance.setY(caster.posY);
|
||||
instance.setZ(caster.posZ);
|
||||
break;
|
||||
case AT_TARGET:
|
||||
// Telegraph follows target during windup, locks on execute
|
||||
if (target != null) {
|
||||
instance.setEntityIdToFollow(target.getEntityId());
|
||||
}
|
||||
break;
|
||||
case FOLLOW_TARGET:
|
||||
// Telegraph follows target during windup AND active phase
|
||||
if (target != null) {
|
||||
instance.setEntityIdToFollow(target.getEntityId());
|
||||
}
|
||||
break;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// AT_CASTER locks immediately, AT_TARGET follows during windup and locks on execute
|
||||
if (tick == 0) {
|
||||
switch (placement) {
|
||||
case AT_CASTER:
|
||||
zoneX = caster.posX;
|
||||
zoneY = caster.posY;
|
||||
zoneZ = caster.posZ;
|
||||
positionLocked = true;
|
||||
break;
|
||||
case AT_TARGET:
|
||||
// Position will be set from telegraph on execute
|
||||
// Telegraph follows target during windup
|
||||
positionLocked = false;
|
||||
break;
|
||||
default:
|
||||
positionLocked = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
// Lock position from telegraph (which was following target) or calculate it now
|
||||
TelegraphInstance telegraph = getTelegraphInstance();
|
||||
|
||||
switch (placement) {
|
||||
case AT_CASTER:
|
||||
// Already locked during windup
|
||||
if (!positionLocked) {
|
||||
zoneX = caster.posX;
|
||||
zoneY = caster.posY;
|
||||
zoneZ = caster.posZ;
|
||||
}
|
||||
break;
|
||||
case AT_TARGET:
|
||||
// Use telegraph position if available, apply offset
|
||||
if (telegraph != null) {
|
||||
double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(),
|
||||
minOffset, maxOffset, randomOffset, RANDOM);
|
||||
zoneX = pos[0];
|
||||
zoneY = pos[1];
|
||||
zoneZ = pos[2];
|
||||
// Update telegraph to show actual hazard position
|
||||
telegraph.setX(zoneX);
|
||||
telegraph.setY(zoneY);
|
||||
telegraph.setZ(zoneZ);
|
||||
} else if (target != null) {
|
||||
double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ,
|
||||
minOffset, maxOffset, randomOffset, RANDOM);
|
||||
zoneX = pos[0];
|
||||
zoneY = pos[1];
|
||||
zoneZ = pos[2];
|
||||
} else {
|
||||
zoneX = caster.posX;
|
||||
zoneY = caster.posY;
|
||||
zoneZ = caster.posZ;
|
||||
}
|
||||
break;
|
||||
case FOLLOW_CASTER:
|
||||
zoneX = caster.posX;
|
||||
zoneY = caster.posY;
|
||||
zoneZ = caster.posZ;
|
||||
break;
|
||||
case FOLLOW_TARGET:
|
||||
if (target != null) {
|
||||
zoneX = target.posX;
|
||||
zoneY = target.posY;
|
||||
zoneZ = target.posZ;
|
||||
} else {
|
||||
zoneX = caster.posX;
|
||||
zoneY = caster.posY;
|
||||
zoneZ = caster.posZ;
|
||||
}
|
||||
break;
|
||||
}
|
||||
positionLocked = true;
|
||||
ticksSinceDamage = damageInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
|
||||
// Check if hazard duration has ended
|
||||
if (tick >= durationTicks) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
damagedThisTick.clear();
|
||||
ticksSinceDamage++;
|
||||
activeEntities.clear();
|
||||
|
||||
switch (placement) {
|
||||
case FOLLOW_CASTER:
|
||||
zoneX = caster.posX;
|
||||
zoneY = caster.posY;
|
||||
zoneZ = caster.posZ;
|
||||
break;
|
||||
case FOLLOW_TARGET:
|
||||
if (target != null) {
|
||||
zoneX = target.posX;
|
||||
zoneY = target.posY;
|
||||
zoneZ = target.posZ;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (ticksSinceDamage >= damageInterval) {
|
||||
ticksSinceDamage = 0;
|
||||
|
||||
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
|
||||
zoneX - radius, zoneY - heightBelow, zoneZ - radius,
|
||||
zoneX + radius, zoneY + heightAbove, zoneZ + radius
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster && !affectsCaster) continue;
|
||||
if (damagedThisTick.contains(entity.getEntityId())) continue;
|
||||
if (!isInZone(entity, caster)) continue;
|
||||
|
||||
if (damagePerSecond > 0) {
|
||||
if (ignoreInvulnFrames) {
|
||||
entity.hurtResistantTime = 0;
|
||||
}
|
||||
// Apply damage with scripted event support (no knockback for hazard)
|
||||
boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0);
|
||||
if (!wasHit) continue; // Skip debuffs if hit was cancelled
|
||||
}
|
||||
|
||||
applyDebuffs(entity);
|
||||
damagedThisTick.add(entity.getEntityId());
|
||||
// Use pre-calculated positions from telegraph phase, or generate new ones
|
||||
List<double[]> positions;
|
||||
if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) {
|
||||
positions = new ArrayList<>(preCalculatedPositions);
|
||||
preCalculatedPositions.clear();
|
||||
} else {
|
||||
positions = new ArrayList<>();
|
||||
List<double[]> placedPositions = new ArrayList<>();
|
||||
float minSeparation = radius * 2.0f;
|
||||
for (int i = 0; i < zoneCount; i++) {
|
||||
double[] pos = findSpawnPosition(caster, placedPositions, minSeparation);
|
||||
placedPositions.add(pos);
|
||||
positions.add(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInZone(EntityLivingBase entity, EntityLivingBase caster) {
|
||||
double dx = entity.posX - zoneX;
|
||||
double dz = entity.posZ - zoneZ;
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
for (int i = 0; i < zoneCount; i++) {
|
||||
double[] pos = positions.get(i);
|
||||
|
||||
switch (shape) {
|
||||
case CIRCLE:
|
||||
return dist <= radius;
|
||||
case RING:
|
||||
return dist >= innerRadius && dist <= radius;
|
||||
case CONE:
|
||||
if (dist > radius) return false;
|
||||
float casterYaw = caster.rotationYaw;
|
||||
double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz));
|
||||
double angleDiff = Math.abs(normalizeAngle(angleToEntity - casterYaw));
|
||||
return angleDiff <= coneAngle / 2;
|
||||
default:
|
||||
return dist <= radius;
|
||||
EntityAbilityZone entity = EntityAbilityZone.createHazard(world, caster,
|
||||
pos[0], caster.posY, pos[1],
|
||||
zoneShape,
|
||||
radius,
|
||||
damagePerSecond, damageInterval,
|
||||
ignoreInvulnFrames, affectsCaster,
|
||||
durationTicks,
|
||||
colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled,
|
||||
zoneHeight,
|
||||
particleDensity, particleScale, animSpeed, lightningDensity,
|
||||
getEffects());
|
||||
|
||||
applyVisualToEntity(entity);
|
||||
|
||||
if (isPreview()) {
|
||||
entity.setupPreview(caster);
|
||||
}
|
||||
|
||||
spawnAbilityEntity(world, entity);
|
||||
activeEntities.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private double normalizeAngle(double angle) {
|
||||
while (angle > 180) angle -= 360;
|
||||
while (angle < -180) angle += 360;
|
||||
return angle;
|
||||
}
|
||||
|
||||
private void applyDebuffs(EntityLivingBase target) {
|
||||
if (slownessLevel >= 0) {
|
||||
target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, debuffDuration, slownessLevel));
|
||||
}
|
||||
if (weaknessLevel >= 0) {
|
||||
target.addPotionEffect(new PotionEffect(Potion.weakness.id, debuffDuration, weaknessLevel));
|
||||
}
|
||||
if (poisonLevel >= 0) {
|
||||
target.addPotionEffect(new PotionEffect(Potion.poison.id, debuffDuration, poisonLevel));
|
||||
}
|
||||
if (witherLevel >= 0) {
|
||||
target.addPotionEffect(new PotionEffect(Potion.wither.id, debuffDuration, witherLevel));
|
||||
}
|
||||
if (blindnessLevel >= 0) {
|
||||
target.addPotionEffect(new PotionEffect(Potion.blindness.id, debuffDuration, blindnessLevel));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
damagedThisTick.clear();
|
||||
positionLocked = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
damagedThisTick.clear();
|
||||
positionLocked = false;
|
||||
}
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// NBT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setInteger("durationTicks", durationTicks);
|
||||
writeZoneNBT(nbt);
|
||||
nbt.setFloat("radius", radius);
|
||||
nbt.setFloat("innerRadius", innerRadius);
|
||||
nbt.setFloat("coneAngle", coneAngle);
|
||||
nbt.setString("shape", shape.name());
|
||||
nbt.setString("placement", placement.name());
|
||||
nbt.setFloat("damagePerSecond", damagePerSecond);
|
||||
nbt.setInteger("damageInterval", damageInterval);
|
||||
nbt.setBoolean("ignoreInvulnFrames", ignoreInvulnFrames);
|
||||
nbt.setInteger("slownessLevel", slownessLevel);
|
||||
nbt.setInteger("weaknessLevel", weaknessLevel);
|
||||
nbt.setInteger("poisonLevel", poisonLevel);
|
||||
nbt.setInteger("witherLevel", witherLevel);
|
||||
nbt.setInteger("blindnessLevel", blindnessLevel);
|
||||
nbt.setInteger("debuffDuration", debuffDuration);
|
||||
nbt.setBoolean("affectsCaster", affectsCaster);
|
||||
nbt.setFloat("heightAbove", heightAbove);
|
||||
nbt.setFloat("heightBelow", heightBelow);
|
||||
nbt.setFloat("minOffset", minOffset);
|
||||
nbt.setFloat("maxOffset", maxOffset);
|
||||
nbt.setBoolean("randomOffset", randomOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 100;
|
||||
this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 4.0f;
|
||||
this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f;
|
||||
this.coneAngle = nbt.hasKey("coneAngle") ? nbt.getFloat("coneAngle") : 45.0f;
|
||||
try {
|
||||
this.shape = HazardShape.valueOf(nbt.getString("shape"));
|
||||
} catch (Exception e) {
|
||||
this.shape = HazardShape.CIRCLE;
|
||||
}
|
||||
try {
|
||||
this.placement = PlacementMode.valueOf(nbt.getString("placement"));
|
||||
} catch (Exception e) {
|
||||
this.placement = PlacementMode.AT_TARGET;
|
||||
}
|
||||
// Support legacy "damagePerTick" key for backwards compatibility
|
||||
if (nbt.hasKey("damagePerSecond")) {
|
||||
this.damagePerSecond = nbt.getFloat("damagePerSecond");
|
||||
} else if (nbt.hasKey("damagePerTick")) {
|
||||
this.damagePerSecond = nbt.getFloat("damagePerTick");
|
||||
} else {
|
||||
this.damagePerSecond = 1.0f;
|
||||
}
|
||||
readZoneNBT(nbt, 300);
|
||||
this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 2.0f;
|
||||
this.damagePerSecond = nbt.hasKey("damagePerSecond") ? nbt.getFloat("damagePerSecond") : 1.0f;
|
||||
this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 20;
|
||||
this.ignoreInvulnFrames = nbt.hasKey("ignoreInvulnFrames") && nbt.getBoolean("ignoreInvulnFrames");
|
||||
this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : -1;
|
||||
this.weaknessLevel = nbt.hasKey("weaknessLevel") ? nbt.getInteger("weaknessLevel") : -1;
|
||||
this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : -1;
|
||||
this.witherLevel = nbt.hasKey("witherLevel") ? nbt.getInteger("witherLevel") : -1;
|
||||
this.blindnessLevel = nbt.hasKey("blindnessLevel") ? nbt.getInteger("blindnessLevel") : -1;
|
||||
this.debuffDuration = nbt.hasKey("debuffDuration") ? nbt.getInteger("debuffDuration") : 40;
|
||||
this.affectsCaster = nbt.hasKey("affectsCaster") && nbt.getBoolean("affectsCaster");
|
||||
this.heightAbove = nbt.hasKey("heightAbove") ? nbt.getFloat("heightAbove") : 2.0f;
|
||||
this.heightBelow = nbt.hasKey("heightBelow") ? nbt.getFloat("heightBelow") : 1.0f;
|
||||
this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f;
|
||||
this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 2.0f;
|
||||
this.randomOffset = !nbt.hasKey("randomOffset") || nbt.getBoolean("randomOffset");
|
||||
}
|
||||
|
||||
// Getters & Setters
|
||||
public int getDurationTicks() {
|
||||
return durationTicks;
|
||||
}
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// HAZARD-SPECIFIC GETTERS & SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public void setDurationTicks(int durationTicks) {
|
||||
this.durationTicks = Math.max(1, durationTicks);
|
||||
}
|
||||
public float getRadius() { return radius; }
|
||||
public void setRadius(float radius) { this.radius = radius; }
|
||||
|
||||
public float getRadius() {
|
||||
return radius;
|
||||
}
|
||||
public float getDamagePerSecond() { return damagePerSecond; }
|
||||
public void setDamagePerSecond(float damagePerSecond) { this.damagePerSecond = damagePerSecond; }
|
||||
|
||||
public void setRadius(float radius) {
|
||||
this.radius = radius;
|
||||
}
|
||||
public int getDamageInterval() { return damageInterval; }
|
||||
public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; }
|
||||
|
||||
public float getInnerRadius() {
|
||||
return innerRadius;
|
||||
}
|
||||
public boolean isIgnoreInvulnFrames() { return ignoreInvulnFrames; }
|
||||
public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { this.ignoreInvulnFrames = ignoreInvulnFrames; }
|
||||
|
||||
public void setInnerRadius(float innerRadius) {
|
||||
this.innerRadius = innerRadius;
|
||||
}
|
||||
public boolean isAffectsCaster() { return affectsCaster; }
|
||||
public void setAffectsCaster(boolean affectsCaster) { this.affectsCaster = affectsCaster; }
|
||||
|
||||
public float getConeAngle() {
|
||||
return coneAngle;
|
||||
}
|
||||
|
||||
public void setConeAngle(float coneAngle) {
|
||||
this.coneAngle = coneAngle;
|
||||
}
|
||||
|
||||
public HazardShape getShapeEnum() {
|
||||
return shape;
|
||||
}
|
||||
|
||||
public void setShapeEnum(HazardShape shape) {
|
||||
this.shape = shape;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getShape() {
|
||||
return shape.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setShape(int shape) {
|
||||
HazardShape[] values = HazardShape.values();
|
||||
this.shape = shape >= 0 && shape < values.length ? values[shape] : HazardShape.CIRCLE;
|
||||
}
|
||||
|
||||
public PlacementMode getPlacementEnum() {
|
||||
return placement;
|
||||
}
|
||||
|
||||
public void setPlacementEnum(PlacementMode placement) {
|
||||
this.placement = placement;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPlacement() {
|
||||
return placement.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlacement(int placement) {
|
||||
PlacementMode[] values = PlacementMode.values();
|
||||
this.placement = placement >= 0 && placement < values.length ? values[placement] : PlacementMode.AT_CASTER;
|
||||
}
|
||||
|
||||
public float getDamagePerSecond() {
|
||||
return damagePerSecond;
|
||||
}
|
||||
|
||||
public void setDamagePerSecond(float damagePerSecond) {
|
||||
this.damagePerSecond = damagePerSecond;
|
||||
}
|
||||
|
||||
public int getDamageInterval() {
|
||||
return damageInterval;
|
||||
}
|
||||
|
||||
public void setDamageInterval(int damageInterval) {
|
||||
this.damageInterval = damageInterval;
|
||||
}
|
||||
|
||||
public boolean isIgnoreInvulnFrames() {
|
||||
return ignoreInvulnFrames;
|
||||
}
|
||||
|
||||
public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) {
|
||||
this.ignoreInvulnFrames = ignoreInvulnFrames;
|
||||
}
|
||||
|
||||
public int getSlownessLevel() {
|
||||
return slownessLevel;
|
||||
}
|
||||
|
||||
public void setSlownessLevel(int slownessLevel) {
|
||||
this.slownessLevel = slownessLevel;
|
||||
}
|
||||
|
||||
public int getWeaknessLevel() {
|
||||
return weaknessLevel;
|
||||
}
|
||||
|
||||
public void setWeaknessLevel(int weaknessLevel) {
|
||||
this.weaknessLevel = weaknessLevel;
|
||||
}
|
||||
|
||||
public int getPoisonLevel() {
|
||||
return poisonLevel;
|
||||
}
|
||||
|
||||
public void setPoisonLevel(int poisonLevel) {
|
||||
this.poisonLevel = poisonLevel;
|
||||
}
|
||||
|
||||
public int getWitherLevel() {
|
||||
return witherLevel;
|
||||
}
|
||||
|
||||
public void setWitherLevel(int witherLevel) {
|
||||
this.witherLevel = witherLevel;
|
||||
}
|
||||
|
||||
public int getBlindnessLevel() {
|
||||
return blindnessLevel;
|
||||
}
|
||||
|
||||
public void setBlindnessLevel(int blindnessLevel) {
|
||||
this.blindnessLevel = blindnessLevel;
|
||||
}
|
||||
|
||||
public int getDebuffDuration() {
|
||||
return debuffDuration;
|
||||
}
|
||||
|
||||
public void setDebuffDuration(int debuffDuration) {
|
||||
this.debuffDuration = debuffDuration;
|
||||
}
|
||||
|
||||
public boolean isAffectsCaster() {
|
||||
return affectsCaster;
|
||||
}
|
||||
|
||||
public void setAffectsCaster(boolean affectsCaster) {
|
||||
this.affectsCaster = affectsCaster;
|
||||
}
|
||||
|
||||
public float getHeightAbove() {
|
||||
return heightAbove;
|
||||
}
|
||||
|
||||
public void setHeightAbove(float heightAbove) {
|
||||
this.heightAbove = heightAbove;
|
||||
}
|
||||
|
||||
public float getHeightBelow() {
|
||||
return heightBelow;
|
||||
}
|
||||
|
||||
public void setHeightBelow(float heightBelow) {
|
||||
this.heightBelow = heightBelow;
|
||||
}
|
||||
|
||||
public float getMinOffset() {
|
||||
return minOffset;
|
||||
}
|
||||
|
||||
public void setMinOffset(float minOffset) {
|
||||
this.minOffset = minOffset;
|
||||
}
|
||||
|
||||
public float getMaxOffset() {
|
||||
return maxOffset;
|
||||
}
|
||||
|
||||
public void setMaxOffset(float maxOffset) {
|
||||
this.maxOffset = maxOffset;
|
||||
}
|
||||
|
||||
public boolean isRandomOffset() {
|
||||
return randomOffset;
|
||||
}
|
||||
|
||||
public void setRandomOffset(boolean randomOffset) {
|
||||
this.randomOffset = randomOffset;
|
||||
}
|
||||
|
||||
// Runtime getters
|
||||
public double getZoneX() {
|
||||
return zoneX;
|
||||
}
|
||||
|
||||
public double getZoneY() {
|
||||
return zoneY;
|
||||
}
|
||||
|
||||
public double getZoneZ() {
|
||||
return zoneZ;
|
||||
}
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GUI FIELD DEFINITIONS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
addPresetFieldDef(defs);
|
||||
|
||||
defs.addAll(Arrays.asList(
|
||||
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000),
|
||||
FieldDef.enumField("ability.shape", HazardShape.class, this::getShapeEnum, this::setShapeEnum)
|
||||
.hover("ability.hover.shape"),
|
||||
FieldDef.enumField("ability.placement", PlacementMode.class, this::getPlacementEnum, this::setPlacementEnum)
|
||||
.hover("ability.hover.placement"),
|
||||
FieldDef.section("ability.section.area"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.radius", this::getRadius, this::setRadius),
|
||||
FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius)
|
||||
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000),
|
||||
FieldDef.enumField("ability.zoneShape", ZoneShape.class, this::getZoneShapeEnum, this::setZoneShapeEnum)
|
||||
),
|
||||
FieldDef.section("ability.section.zone"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.radius", this::getSpawnRadius, this::setSpawnRadius),
|
||||
FieldDef.intField("gui.count", this::getZoneCount, this::setZoneCount).range(1, 20)
|
||||
),
|
||||
FieldDef.floatField("gui.height", this::getZoneHeight, this::setZoneHeight),
|
||||
FieldDef.section("ability.section.area"),
|
||||
FieldDef.floatField("gui.radius", this::getRadius, this::setRadius),
|
||||
FieldDef.section("ability.section.damage"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.damagePerSecond", this::getDamagePerSecond, this::setDamagePerSecond),
|
||||
FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval)
|
||||
FieldDef.floatField("gui.dps", this::getDamagePerSecond, this::setDamagePerSecond),
|
||||
FieldDef.intField("gui.interval", this::getDamageInterval, this::setDamageInterval)
|
||||
),
|
||||
FieldDef.section("ability.section.debuffs"),
|
||||
FieldDef.row(
|
||||
FieldDef.intField("ability.slownessLevel", this::getSlownessLevel, this::setSlownessLevel),
|
||||
FieldDef.intField("ability.debuffDuration", this::getDebuffDuration, this::setDebuffDuration)
|
||||
),
|
||||
FieldDef.intField("ability.poisonLevel", this::getPoisonLevel, this::setPoisonLevel),
|
||||
FieldDef.boolField("ability.ignoreInvulnFrames", this::isIgnoreInvulnFrames, this::setIgnoreInvulnFrames),
|
||||
FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster)
|
||||
.hover("ability.hover.affectsCaster"),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
|
||||
));
|
||||
|
||||
addVisualFieldDefs(defs);
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,11 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowBurst() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
@@ -75,27 +80,31 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
getHealedAllies().clear();
|
||||
if (!isPreview()) {
|
||||
getHealedAllies().clear();
|
||||
|
||||
// Always find allies if we're healing them (needed for both instant and HoT)
|
||||
if (healAllies && healRadius > 0) {
|
||||
findAlliesInRadius(caster, world);
|
||||
// Always find allies if we're healing them (needed for both instant and HoT)
|
||||
if (healAllies && healRadius > 0) {
|
||||
findAlliesInRadius(caster, world);
|
||||
}
|
||||
|
||||
if (instantHeal) {
|
||||
// Instant heal - apply all healing now
|
||||
if (healSelf) {
|
||||
healEntity(caster);
|
||||
spawnHealParticles(world, caster);
|
||||
}
|
||||
|
||||
for (EntityLivingBase ally : getHealedAllies()) {
|
||||
healEntity(ally);
|
||||
spawnHealParticles(world, ally);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (instantHeal) {
|
||||
// Instant heal - apply all healing now
|
||||
if (healSelf) {
|
||||
healEntity(caster);
|
||||
spawnHealParticles(world, caster);
|
||||
}
|
||||
|
||||
for (EntityLivingBase ally : getHealedAllies()) {
|
||||
healEntity(ally);
|
||||
spawnHealParticles(world, ally);
|
||||
}
|
||||
|
||||
// Instant heal completes immediately
|
||||
signalCompletion();
|
||||
}
|
||||
@@ -104,7 +113,7 @@ public class AbilityHeal extends Ability implements IAbilityHeal {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote || instantHeal) return;
|
||||
if ((world.isRemote && !isPreview()) || instantHeal) return;
|
||||
|
||||
// Heal over time - distribute heal across duration ticks
|
||||
if (tick % 10 == 0) {
|
||||
|
||||
@@ -68,51 +68,53 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate forward and right vectors from caster yaw
|
||||
float yawRad = (float) Math.toRadians(caster.rotationYaw);
|
||||
double forwardX = -Math.sin(yawRad);
|
||||
double forwardZ = Math.cos(yawRad);
|
||||
double rightX = forwardZ; // perpendicular right
|
||||
double rightZ = -forwardX;
|
||||
if (!isPreview()) {
|
||||
// Calculate forward and right vectors from caster yaw
|
||||
float yawRad = (float) Math.toRadians(caster.rotationYaw);
|
||||
double forwardX = -Math.sin(yawRad);
|
||||
double forwardZ = Math.cos(yawRad);
|
||||
double rightX = forwardZ; // perpendicular right
|
||||
double rightZ = -forwardX;
|
||||
|
||||
// Search area: AABB that encompasses the rectangle
|
||||
float searchDist = Math.max(hitLength, hitWidth) + 1.0f;
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(
|
||||
caster, caster.boundingBox.expand(searchDist, 2, searchDist));
|
||||
// Search area: AABB that encompasses the rectangle
|
||||
float searchDist = Math.max(hitLength, hitWidth) + 1.0f;
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(
|
||||
caster, caster.boundingBox.expand(searchDist, 2, searchDist));
|
||||
|
||||
boolean anyHit = false;
|
||||
for (Entity entity : entities) {
|
||||
if (!(entity instanceof EntityLivingBase) || entity == caster) continue;
|
||||
EntityLivingBase livingTarget = (EntityLivingBase) entity;
|
||||
boolean anyHit = false;
|
||||
for (Entity entity : entities) {
|
||||
if (!(entity instanceof EntityLivingBase) || entity == caster) continue;
|
||||
EntityLivingBase livingTarget = (EntityLivingBase) entity;
|
||||
|
||||
double dx = livingTarget.posX - caster.posX;
|
||||
double dz = livingTarget.posZ - caster.posZ;
|
||||
double dx = livingTarget.posX - caster.posX;
|
||||
double dz = livingTarget.posZ - caster.posZ;
|
||||
|
||||
// Project onto forward direction (must be in front, within hitLength)
|
||||
double forwardDist = dx * forwardX + dz * forwardZ;
|
||||
if (forwardDist < 0 || forwardDist > hitLength) continue;
|
||||
// Project onto forward direction (must be in front, within hitLength)
|
||||
double forwardDist = dx * forwardX + dz * forwardZ;
|
||||
if (forwardDist < 0 || forwardDist > hitLength) continue;
|
||||
|
||||
// Project onto right direction (must be within hitWidth to each side)
|
||||
double sideDist = dx * rightX + dz * rightZ;
|
||||
if (Math.abs(sideDist) > hitWidth) continue;
|
||||
// Project onto right direction (must be within hitWidth to each side)
|
||||
double sideDist = dx * rightX + dz * rightZ;
|
||||
if (Math.abs(sideDist) > hitWidth) continue;
|
||||
|
||||
// Entity is within the rectangle - apply damage
|
||||
boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback);
|
||||
if (wasHit) {
|
||||
applyEffects(livingTarget);
|
||||
anyHit = true;
|
||||
// Entity is within the rectangle - apply damage
|
||||
boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback);
|
||||
if (wasHit) {
|
||||
applyEffects(livingTarget);
|
||||
anyHit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Play hit sound even if nothing was hit (the attack still happens)
|
||||
if (!anyHit) {
|
||||
world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f);
|
||||
// Play hit sound even if nothing was hit (the attack still happens)
|
||||
if (!anyHit) {
|
||||
world.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
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.*;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
@@ -12,7 +13,6 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityAbilityLaser;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.DamageSource;
|
||||
@@ -22,7 +22,6 @@ import noppes.npcs.api.ability.type.IAbilityLaserShot;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
/**
|
||||
* Laser Shot ability: Fast expanding thin line that pierces through targets.
|
||||
@@ -40,7 +39,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
private final EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f);
|
||||
private final EnergyLightningData lightningData = new EnergyLightningData();
|
||||
private final EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100);
|
||||
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
|
||||
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT);
|
||||
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
|
||||
|
||||
// Transient state for laser entity (used for movement locking)
|
||||
@@ -55,6 +54,8 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 15;
|
||||
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.rotationMode = RotationMode.TRACK;
|
||||
this.rotationPhase = LockMovementType.WINDUP;
|
||||
this.telegraphType = TelegraphType.LINE;
|
||||
this.showTelegraph = true;
|
||||
// Default built-in animations
|
||||
@@ -62,6 +63,11 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
this.activeAnimationName = "Ability_Laser_Active";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowOverlap() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return false;
|
||||
@@ -82,31 +88,55 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
return laserWidth * 2.0f; // Make telegraph slightly wider for visibility
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn laser entity.
|
||||
* NPC: target is the aggro target — laser expands toward it.
|
||||
* Player: target is null — laser expands in caster's look direction.
|
||||
*/
|
||||
private EntityAbilityLaser spawnLaserEntity(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
|
||||
EntityAbilityLaser entity = new EntityAbilityLaser(
|
||||
world, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
|
||||
laserWidth,
|
||||
colorData, combatData, lightningData, lifespanData, trajectoryData,
|
||||
expansionSpeed, lingerTicks
|
||||
);
|
||||
entity.setEffects(this.effects);
|
||||
entity.setSourceAbility(this);
|
||||
spawnAbilityEntity(world, entity);
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void fireLaserEntity(EntityAbilityLaser laser, EntityLivingBase target) {
|
||||
if (laser == null || laser.isDead) return;
|
||||
laser.setLockVerticalDirection(true);
|
||||
laser.startMoving(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
if (tick == 1) {
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
|
||||
laserEntity = new EntityAbilityLaser(
|
||||
world, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
|
||||
laserWidth,
|
||||
colorData, combatData, lightningData, lifespanData, trajectoryData,
|
||||
expansionSpeed, lingerTicks
|
||||
);
|
||||
|
||||
laserEntity.setupCharging(anchorData, windUpTicks);
|
||||
|
||||
laserEntity.setEffects(this.effects);
|
||||
laserEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(laserEntity);
|
||||
laserEntity = spawnLaserEntity(caster, target, world);
|
||||
if (isPreview()) {
|
||||
laserEntity.setupPreview(caster, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance);
|
||||
} else {
|
||||
laserEntity.setupCharging(anchorData, windUpTicks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
laserEntity = spawnLaserEntity(caster, target, world);
|
||||
fireLaserEntity(laserEntity, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
@@ -116,10 +146,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
return;
|
||||
}
|
||||
|
||||
laserEntity.startMoving(target);
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
fireLaserEntity(laserEntity, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -235,25 +262,10 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityLaser laser = new EntityAbilityLaser(npc.worldObj);
|
||||
laser.setupPreview(npc, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance);
|
||||
return laser;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean spawnPreviewDuringWindup() {
|
||||
return false; // Laser has no charging, spawns at active phase
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
@@ -282,6 +294,12 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot {
|
||||
// Visual tab
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.anchor.offsetX", this::getAnchorOffsetX, this::setAnchorOffsetX),
|
||||
FieldDef.floatField("ability.anchor.offsetY", this::getAnchorOffsetY, this::setAnchorOffsetY)
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.anchor.offsetZ", this::getAnchorOffsetZ, this::setAnchorOffsetZ)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"),
|
||||
|
||||
@@ -12,7 +12,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityAbilityOrb;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.Vec3;
|
||||
@@ -23,29 +22,38 @@ import noppes.npcs.api.ability.type.IAbilityOrb;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
|
||||
/**
|
||||
* Orb ability: Spawns a homing projectile sphere that tracks target.
|
||||
* The EntityAbilityOrb handles all movement, collision, and damage logic.
|
||||
* This ability just configures and spawns the orb entity.
|
||||
* Orb ability: Spawns homing projectile sphere(s) that track target.
|
||||
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
|
||||
*/
|
||||
public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
|
||||
// Ability-specific properties
|
||||
private static final int MAX_PROJECTILES = 8;
|
||||
|
||||
// Orb properties
|
||||
private float orbSize = 1.0f;
|
||||
|
||||
// Data classes for energy properties
|
||||
private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND);
|
||||
private final EnergyCombatData combatData = new EnergyCombatData();
|
||||
private final EnergyDisplayData displayData = new EnergyDisplayData();
|
||||
private final EnergyHomingData homingData = new EnergyHomingData();
|
||||
private final EnergyLightningData lightningData = new EnergyLightningData();
|
||||
private final EnergyLifespanData lifespanData = new EnergyLifespanData();
|
||||
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
|
||||
// Projectile count and fire stagger
|
||||
private int projectileCount = 1;
|
||||
private int fireDelay = 0;
|
||||
|
||||
// Transient state for orb entity (used during windup charging)
|
||||
private transient EntityAbilityOrb orbEntity = null;
|
||||
// Shared visual data (all projectiles use these)
|
||||
private EnergyDisplayData displayData = new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f);
|
||||
private EnergyLightningData lightningData = new EnergyLightningData();
|
||||
private EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
|
||||
|
||||
// Shared combat/movement data
|
||||
private final EnergyCombatData combatData = new EnergyCombatData();
|
||||
private final EnergyHomingData homingData = new EnergyHomingData();
|
||||
private final EnergyLifespanData lifespanData = new EnergyLifespanData();
|
||||
|
||||
// Per-projectile data (anchor + optional color override)
|
||||
private ProjectileData[] projectiles;
|
||||
|
||||
// Transient entity state
|
||||
private transient EntityAbilityOrb[] orbEntities;
|
||||
|
||||
public AbilityOrb() {
|
||||
this.typeId = "ability.cnpc.orb";
|
||||
@@ -58,9 +66,101 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.showTelegraph = true;
|
||||
// Default built-in animation
|
||||
this.windUpAnimationName = "Ability_Orb_Windup";
|
||||
this.activeAnimationName = "Ability_Orb_Active";
|
||||
initProjectiles(1);
|
||||
}
|
||||
|
||||
private void initProjectiles(int count) {
|
||||
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
|
||||
this.projectiles = new ProjectileData[projectileCount];
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
|
||||
}
|
||||
}
|
||||
|
||||
private AnchorPoint getDefaultAnchor(int index) {
|
||||
switch (index) {
|
||||
case 0: return AnchorPoint.RIGHT_HAND;
|
||||
case 1: return AnchorPoint.LEFT_HAND;
|
||||
default: return AnchorPoint.FRONT;
|
||||
}
|
||||
}
|
||||
|
||||
public void setProjectileCount(int count) {
|
||||
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
|
||||
if (newCount == projectileCount) return;
|
||||
|
||||
ProjectileData[] oldProjectiles = projectiles;
|
||||
initProjectiles(newCount);
|
||||
|
||||
// Preserve existing projectile data
|
||||
if (oldProjectiles != null) {
|
||||
int copyCount = Math.min(oldProjectiles.length, newCount);
|
||||
for (int i = 0; i < copyCount; i++) {
|
||||
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int getProjectileCount() {
|
||||
return projectileCount;
|
||||
}
|
||||
|
||||
public int getFireDelay() {
|
||||
return fireDelay;
|
||||
}
|
||||
|
||||
public void setFireDelay(int delay) {
|
||||
this.fireDelay = Math.max(0, delay);
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link #getFireDelay()} */
|
||||
public int getDualFireDelay() { return fireDelay; }
|
||||
/** @deprecated Use {@link #setFireDelay(int)} */
|
||||
public void setDualFireDelay(int delay) { setFireDelay(delay); }
|
||||
|
||||
@Override
|
||||
public List<AbilityVariant> getVariants() {
|
||||
return Arrays.asList(
|
||||
new AbilityVariant("ability.variant.single", a -> {
|
||||
a.setName("Orb");
|
||||
}),
|
||||
new AbilityVariant("ability.variant.dual", a -> {
|
||||
AbilityOrb orb = (AbilityOrb) a;
|
||||
a.setName("Dual Orb");
|
||||
orb.setProjectileCount(2);
|
||||
orb.setFireDelay(5);
|
||||
a.setWindUpAnimationName("Ability_OrbDual_Windup");
|
||||
a.setActiveAnimationName("Ability_OrbDual_Active");
|
||||
}),
|
||||
new AbilityVariant("ability.variant.barrage", a -> {
|
||||
AbilityOrb orb = (AbilityOrb) a;
|
||||
a.setName("Orb Barrage");
|
||||
orb.setOrbSize(0.5f);
|
||||
orb.setDamage(4.0f);
|
||||
orb.setKnockback(0.0f);
|
||||
orb.setKnockbackUp(0.0f);
|
||||
a.setMaxRange(75.0f);
|
||||
a.setBurstEnabled(true);
|
||||
a.setBurstAmount(15);
|
||||
a.setBurstDelay(5);
|
||||
a.setBurstReplayAnimations(false);
|
||||
a.setBurstOverlap(true);
|
||||
a.setWindUpAnimationName("Ability_OrbBarrage_Windup");
|
||||
a.setActiveAnimationName("Ability_OrbBarrage_Active");
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowOverlap() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReadyForBurstCompletion(int activeTick) {
|
||||
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -73,54 +173,124 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
signalCompletion();
|
||||
return;
|
||||
/**
|
||||
* Create orb entities.
|
||||
* NPC: target is the aggro target — orbs will home toward it.
|
||||
* Player: target is null — orbs fire in caster's look direction.
|
||||
*/
|
||||
private EntityAbilityOrb[] createOrbEntities(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
EntityAbilityOrb[] entities = new EntityAbilityOrb[projectileCount];
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, projectiles[i].anchor);
|
||||
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
|
||||
entities[i] = new EntityAbilityOrb(
|
||||
world, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
|
||||
resolved, combatData, homingData, lightningData, lifespanData, trajectoryData);
|
||||
entities[i].setEffects(this.effects);
|
||||
entities[i].setSourceAbility(this);
|
||||
}
|
||||
|
||||
// Start moving the orb that was spawned during windup
|
||||
if (orbEntity != null && !orbEntity.isDead) {
|
||||
orbEntity.startMoving(target);
|
||||
if (projectileCount == 2 && entities[0] != null && entities[1] != null) {
|
||||
entities[0].setSiblingEntityId(entities[1].getEntityId());
|
||||
entities[1].setSiblingEntityId(entities[0].getEntityId());
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
private void fireOrbEntity(EntityAbilityOrb orb, EntityLivingBase target) {
|
||||
if (orb == null || orb.isDead) return;
|
||||
if (isPreview()) {
|
||||
orb.startPreviewFiring();
|
||||
} else {
|
||||
orb.startMoving(target);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
// Spawn orb in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
// Create orb in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData);
|
||||
orbEntity = new EntityAbilityOrb(
|
||||
world, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
|
||||
displayData, combatData, homingData, lightningData, lifespanData, trajectoryData);
|
||||
orbEntity.setupCharging(anchorData, windUpTicks);
|
||||
orbEntities = createOrbEntities(caster, target, world);
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
|
||||
if (isPreview()) {
|
||||
orbEntities[i].setupPreview(caster, orbSize, resolved, lightningData, projectiles[i].anchor, windUpTicks);
|
||||
} else {
|
||||
orbEntities[i].setupCharging(projectiles[i].anchor, windUpTicks);
|
||||
}
|
||||
spawnAbilityEntity(world, orbEntities[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
orbEntity.setEffects(this.effects);
|
||||
orbEntity.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(orbEntity);
|
||||
@Override
|
||||
public void onBurstRefire(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
orbEntities = createOrbEntities(caster, target, world);
|
||||
for (EntityAbilityOrb orb : orbEntities) {
|
||||
spawnAbilityEntity(world, orb);
|
||||
}
|
||||
|
||||
// Fire first projectile immediately, let onActiveTick handle the rest with fireDelay
|
||||
fireOrbEntity(orbEntities[0], target);
|
||||
if (fireDelay <= 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
fireOrbEntity(orbEntities[i], target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (orbEntities == null) return;
|
||||
|
||||
// Fire first projectile immediately
|
||||
fireOrbEntity(orbEntities[0], target);
|
||||
|
||||
// Fire remaining projectiles if no delay
|
||||
if (fireDelay <= 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
fireOrbEntity(orbEntities[i], target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// Signal completion when entity dies
|
||||
if (orbEntity == null || orbEntity.isDead) {
|
||||
orbEntity = null;
|
||||
if (orbEntities == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire staggered projectiles
|
||||
if (fireDelay > 0) {
|
||||
for (int i = 1; i < projectileCount; i++) {
|
||||
if (tick == fireDelay * i) {
|
||||
fireOrbEntity(orbEntities[i], target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all entities are dead
|
||||
boolean allDead = true;
|
||||
for (EntityAbilityOrb orb : orbEntities) {
|
||||
if (orb != null && !orb.isDead) {
|
||||
allDead = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allDead) {
|
||||
signalCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
// Nothing to clean up - entity manages itself
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -130,20 +300,25 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn orb entity if still alive
|
||||
if (orbEntity != null && !orbEntity.isDead) {
|
||||
orbEntity.setDead();
|
||||
if (orbEntities != null) {
|
||||
for (int i = 0; i < orbEntities.length; i++) {
|
||||
if (orbEntities[i] != null && !orbEntities[i].isDead) {
|
||||
orbEntities[i].setDead();
|
||||
}
|
||||
orbEntities[i] = null;
|
||||
}
|
||||
}
|
||||
orbEntity = null;
|
||||
orbEntities = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
|
||||
// NPC: telegraph on aggro target position
|
||||
// Player: no telegraph (orbs fire in look direction, no fixed position to mark)
|
||||
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create small circle telegraph at target position
|
||||
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
|
||||
telegraph.setDurationTicks(windUpTicks);
|
||||
telegraph.setColor(windUpColor);
|
||||
@@ -151,7 +326,6 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
|
||||
telegraph.setHeightOffset(telegraphHeightOffset);
|
||||
|
||||
// Position at target and follow target during windup
|
||||
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
|
||||
instance.setCasterEntityId(caster.getEntityId());
|
||||
instance.setEntityIdToFollow(target.getEntityId());
|
||||
@@ -167,307 +341,270 @@ public class AbilityOrb extends Ability implements IAbilityOrb {
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("orbSize", orbSize);
|
||||
anchorData.writeNBT(nbt);
|
||||
combatData.writeNBT(nbt);
|
||||
nbt.setInteger("projectileCount", projectileCount);
|
||||
nbt.setInteger("fireDelay", fireDelay);
|
||||
|
||||
// Shared visual data
|
||||
displayData.writeNBT(nbt);
|
||||
homingData.writeNBT(nbt);
|
||||
lightningData.writeNBT(nbt);
|
||||
|
||||
// Shared combat/movement data
|
||||
combatData.writeNBT(nbt);
|
||||
homingData.writeNBT(nbt);
|
||||
lifespanData.writeNBT(nbt);
|
||||
trajectoryData.writeNBT(nbt);
|
||||
// Backward compat: old key was "orbSpeed"
|
||||
nbt.setFloat("orbSpeed", homingData.speed);
|
||||
|
||||
// Per-projectile data
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
NBTTagCompound projNbt = new NBTTagCompound();
|
||||
projectiles[i].writeNBT(projNbt);
|
||||
nbt.setTag("Projectile_" + i, projNbt);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f;
|
||||
anchorData.readNBT(nbt);
|
||||
|
||||
int count = nbt.hasKey("projectileCount") ? nbt.getInteger("projectileCount") : 1;
|
||||
initProjectiles(count);
|
||||
|
||||
this.fireDelay = nbt.hasKey("fireDelay") ? nbt.getInteger("fireDelay") : 0;
|
||||
|
||||
// Shared combat/movement data
|
||||
combatData.readNBT(nbt);
|
||||
displayData.readNBT(nbt);
|
||||
homingData.readNBT(nbt);
|
||||
lightningData.readNBT(nbt);
|
||||
lifespanData.readNBT(nbt);
|
||||
trajectoryData.readNBT(nbt);
|
||||
// Backward compat: old key was "orbSpeed"
|
||||
if (nbt.hasKey("orbSpeed")) {
|
||||
homingData.speed = nbt.getFloat("orbSpeed");
|
||||
|
||||
// Shared visual data
|
||||
displayData.readNBT(nbt);
|
||||
lightningData.readNBT(nbt);
|
||||
|
||||
// Per-projectile data
|
||||
for (int i = 0; i < projectileCount; i++) {
|
||||
if (nbt.hasKey("Projectile_" + i)) {
|
||||
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getters & Setters
|
||||
public float getOrbSpeed() {
|
||||
return homingData.speed;
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// API GETTERS & SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public float getOrbSpeed() { return homingData.speed; }
|
||||
public void setOrbSpeed(float speed) { homingData.speed = speed; }
|
||||
|
||||
public float getOrbSize() { return orbSize; }
|
||||
public void setOrbSize(float size) { this.orbSize = size; }
|
||||
|
||||
public float getMaxDistance() { return lifespanData.maxDistance; }
|
||||
public void setMaxDistance(float d) { lifespanData.maxDistance = d; }
|
||||
|
||||
public int getMaxLifetime() { return lifespanData.maxLifetime; }
|
||||
public void setMaxLifetime(int t) { lifespanData.maxLifetime = t; }
|
||||
|
||||
public float getDamage() { return combatData.damage; }
|
||||
public void setDamage(float d) { combatData.damage = d; }
|
||||
|
||||
public float getKnockback() { return combatData.knockback; }
|
||||
public void setKnockback(float k) { combatData.knockback = k; }
|
||||
|
||||
public float getKnockbackUp() { return combatData.knockbackUp; }
|
||||
public void setKnockbackUp(float k) { combatData.knockbackUp = k; }
|
||||
|
||||
public boolean isHoming() { return homingData.homing; }
|
||||
public void setHoming(boolean h) { homingData.homing = h; }
|
||||
|
||||
public float getHomingStrength() { return homingData.homingStrength; }
|
||||
public void setHomingStrength(float s) { homingData.homingStrength = s; }
|
||||
|
||||
public float getHomingRange() { return homingData.homingRange; }
|
||||
public void setHomingRange(float r) { homingData.homingRange = r; }
|
||||
|
||||
public boolean isExplosive() { return combatData.explosive; }
|
||||
public void setExplosive(boolean e) { combatData.explosive = e; }
|
||||
|
||||
public float getExplosionRadius() { return combatData.explosionRadius; }
|
||||
public void setExplosionRadius(float r) { combatData.explosionRadius = r; }
|
||||
|
||||
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
|
||||
public void setExplosionDamageFalloff(float f) { combatData.explosionDamageFalloff = f; }
|
||||
|
||||
// Shared display data (primary colors)
|
||||
public int getInnerColor() { return displayData.innerColor; }
|
||||
public void setInnerColor(int c) { displayData.innerColor = c; }
|
||||
|
||||
public int getOuterColor() { return displayData.outerColor; }
|
||||
public void setOuterColor(int c) { displayData.outerColor = c; }
|
||||
|
||||
public float getOuterColorWidth() { return displayData.outerColorWidth; }
|
||||
public void setOuterColorWidth(float w) { displayData.outerColorWidth = w; }
|
||||
|
||||
public float getOuterColorAlpha() { return displayData.outerColorAlpha; }
|
||||
public void setOuterColorAlpha(float a) { displayData.outerColorAlpha = a; }
|
||||
|
||||
public boolean isOuterColorEnabled() { return displayData.outerColorEnabled; }
|
||||
public void setOuterColorEnabled(boolean e) { displayData.outerColorEnabled = e; }
|
||||
|
||||
public float getRotationSpeed() { return displayData.rotationSpeed; }
|
||||
public void setRotationSpeed(float s) { displayData.rotationSpeed = s; }
|
||||
|
||||
// Shared lightning data
|
||||
public boolean hasLightningEffect() { return lightningData.lightningEffect; }
|
||||
public void setLightningEffect(boolean e) { lightningData.lightningEffect = e; }
|
||||
|
||||
public float getLightningDensity() { return lightningData.lightningDensity; }
|
||||
public void setLightningDensity(float d) { lightningData.lightningDensity = d; }
|
||||
|
||||
public float getLightningRadius() { return lightningData.lightningRadius; }
|
||||
public void setLightningRadius(float r) { lightningData.lightningRadius = r; }
|
||||
|
||||
public int getLightningFadeTime() { return lightningData.lightningFadeTime; }
|
||||
public void setLightningFadeTime(int t) { lightningData.lightningFadeTime = t; }
|
||||
|
||||
// Anchor - default to projectile 0
|
||||
public AnchorPoint getAnchorPointEnum() { return projectiles[0].anchor.anchorPoint; }
|
||||
public void setAnchorPointEnum(AnchorPoint p) { projectiles[0].anchor.anchorPoint = p; }
|
||||
|
||||
public float getAnchorOffsetX() { return projectiles[0].anchor.anchorOffsetX; }
|
||||
public void setAnchorOffsetX(float x) { projectiles[0].anchor.anchorOffsetX = x; }
|
||||
|
||||
public float getAnchorOffsetY() { return projectiles[0].anchor.anchorOffsetY; }
|
||||
public void setAnchorOffsetY(float y) { projectiles[0].anchor.anchorOffsetY = y; }
|
||||
|
||||
public float getAnchorOffsetZ() { return projectiles[0].anchor.anchorOffsetZ; }
|
||||
public void setAnchorOffsetZ(float z) { projectiles[0].anchor.anchorOffsetZ = z; }
|
||||
|
||||
public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); }
|
||||
@Override
|
||||
public void setAnchorPoint(int point) { projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
// Indexed API methods
|
||||
public int getInnerColor(int index) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
return p.colorOverride ? p.innerColor : displayData.innerColor;
|
||||
}
|
||||
public void setInnerColor(int index, int color) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
p.colorOverride = true;
|
||||
p.innerColor = color;
|
||||
}
|
||||
|
||||
public void setOrbSpeed(float orbSpeed) {
|
||||
homingData.speed = orbSpeed;
|
||||
public int getOuterColor(int index) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
return p.colorOverride ? p.outerColor : displayData.outerColor;
|
||||
}
|
||||
public void setOuterColor(int index, int color) {
|
||||
ProjectileData p = projectiles[clampIndex(index)];
|
||||
p.colorOverride = true;
|
||||
p.outerColor = color;
|
||||
}
|
||||
|
||||
public float getOrbSize() {
|
||||
return orbSize;
|
||||
}
|
||||
public int getAnchorPoint(int index) { return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal(); }
|
||||
public void setAnchorPoint(int index, int point) { projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
public void setAnchorOffsetX(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetX = offset; }
|
||||
public void setAnchorOffsetY(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetY = offset; }
|
||||
public void setAnchorOffsetZ(int index, float offset) { projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; }
|
||||
|
||||
public void setOrbSize(float orbSize) {
|
||||
this.orbSize = orbSize;
|
||||
}
|
||||
|
||||
public float getMaxDistance() {
|
||||
return lifespanData.maxDistance;
|
||||
}
|
||||
|
||||
public void setMaxDistance(float maxDistance) {
|
||||
lifespanData.maxDistance = maxDistance;
|
||||
}
|
||||
|
||||
public int getMaxLifetime() {
|
||||
return lifespanData.maxLifetime;
|
||||
}
|
||||
|
||||
public void setMaxLifetime(int maxLifetime) {
|
||||
lifespanData.maxLifetime = maxLifetime;
|
||||
}
|
||||
|
||||
public float getDamage() {
|
||||
return combatData.damage;
|
||||
}
|
||||
|
||||
public void setDamage(float damage) {
|
||||
combatData.damage = damage;
|
||||
}
|
||||
|
||||
public float getKnockback() {
|
||||
return combatData.knockback;
|
||||
}
|
||||
|
||||
public void setKnockback(float knockback) {
|
||||
combatData.knockback = knockback;
|
||||
}
|
||||
|
||||
public float getKnockbackUp() {
|
||||
return combatData.knockbackUp;
|
||||
}
|
||||
|
||||
public void setKnockbackUp(float knockbackUp) {
|
||||
combatData.knockbackUp = knockbackUp;
|
||||
}
|
||||
|
||||
public boolean isHoming() {
|
||||
return homingData.homing;
|
||||
}
|
||||
|
||||
public void setHoming(boolean homing) {
|
||||
homingData.homing = homing;
|
||||
}
|
||||
|
||||
public float getHomingStrength() {
|
||||
return homingData.homingStrength;
|
||||
}
|
||||
|
||||
public void setHomingStrength(float homingStrength) {
|
||||
homingData.homingStrength = homingStrength;
|
||||
}
|
||||
|
||||
public float getHomingRange() {
|
||||
return homingData.homingRange;
|
||||
}
|
||||
|
||||
public void setHomingRange(float homingRange) {
|
||||
homingData.homingRange = homingRange;
|
||||
}
|
||||
|
||||
public boolean isExplosive() {
|
||||
return combatData.explosive;
|
||||
}
|
||||
|
||||
public void setExplosive(boolean explosive) {
|
||||
combatData.explosive = explosive;
|
||||
}
|
||||
|
||||
public float getExplosionRadius() {
|
||||
return combatData.explosionRadius;
|
||||
}
|
||||
|
||||
public void setExplosionRadius(float explosionRadius) {
|
||||
combatData.explosionRadius = explosionRadius;
|
||||
}
|
||||
|
||||
public float getExplosionDamageFalloff() {
|
||||
return combatData.explosionDamageFalloff;
|
||||
}
|
||||
|
||||
public void setExplosionDamageFalloff(float explosionDamageFalloff) {
|
||||
combatData.explosionDamageFalloff = explosionDamageFalloff;
|
||||
}
|
||||
|
||||
public int getInnerColor() {
|
||||
return displayData.innerColor;
|
||||
}
|
||||
|
||||
public void setInnerColor(int innerColor) {
|
||||
displayData.innerColor = innerColor;
|
||||
}
|
||||
|
||||
public int getOuterColor() {
|
||||
return displayData.outerColor;
|
||||
}
|
||||
|
||||
public void setOuterColor(int outerColor) {
|
||||
displayData.outerColor = outerColor;
|
||||
}
|
||||
|
||||
public float getOuterColorWidth() {
|
||||
return displayData.outerColorWidth;
|
||||
}
|
||||
|
||||
public void setOuterColorWidth(float outerColorWidth) {
|
||||
displayData.outerColorWidth = outerColorWidth;
|
||||
}
|
||||
|
||||
public float getOuterColorAlpha() {
|
||||
return displayData.outerColorAlpha;
|
||||
}
|
||||
|
||||
public void setOuterColorAlpha(float outerColorAlpha) {
|
||||
displayData.outerColorAlpha = outerColorAlpha;
|
||||
}
|
||||
|
||||
public boolean isOuterColorEnabled() {
|
||||
return displayData.outerColorEnabled;
|
||||
}
|
||||
|
||||
public void setOuterColorEnabled(boolean outerColorEnabled) {
|
||||
displayData.outerColorEnabled = outerColorEnabled;
|
||||
}
|
||||
|
||||
public float getRotationSpeed() {
|
||||
return displayData.rotationSpeed;
|
||||
}
|
||||
|
||||
public void setRotationSpeed(float rotationSpeed) {
|
||||
displayData.rotationSpeed = rotationSpeed;
|
||||
}
|
||||
|
||||
public boolean hasLightningEffect() {
|
||||
return lightningData.lightningEffect;
|
||||
}
|
||||
|
||||
public void setLightningEffect(boolean lightningEffect) {
|
||||
lightningData.lightningEffect = lightningEffect;
|
||||
}
|
||||
|
||||
public float getLightningDensity() {
|
||||
return lightningData.lightningDensity;
|
||||
}
|
||||
|
||||
public void setLightningDensity(float lightningDensity) {
|
||||
lightningData.lightningDensity = lightningDensity;
|
||||
}
|
||||
|
||||
public float getLightningRadius() {
|
||||
return lightningData.lightningRadius;
|
||||
}
|
||||
|
||||
public void setLightningRadius(float lightningRadius) {
|
||||
lightningData.lightningRadius = lightningRadius;
|
||||
}
|
||||
|
||||
public int getLightningFadeTime() {
|
||||
return lightningData.lightningFadeTime;
|
||||
}
|
||||
|
||||
public void setLightningFadeTime(int lightningFadeTime) {
|
||||
lightningData.lightningFadeTime = lightningFadeTime;
|
||||
}
|
||||
|
||||
public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; }
|
||||
|
||||
public float getAnchorOffsetX() { return anchorData.anchorOffsetX; }
|
||||
|
||||
public float getAnchorOffsetY() { return anchorData.anchorOffsetY; }
|
||||
|
||||
public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; }
|
||||
|
||||
public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; }
|
||||
|
||||
public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; }
|
||||
|
||||
public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; }
|
||||
|
||||
public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; }
|
||||
|
||||
public int getAnchorPoint() {
|
||||
return anchorData.anchorPoint.ordinal();
|
||||
private int clampIndex(int index) {
|
||||
return Math.max(0, Math.min(index, projectileCount - 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAnchorPoint(int point) {
|
||||
this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj);
|
||||
orb.setupPreview(npc, orbSize, displayData, lightningData, anchorData, windUpTicks);
|
||||
return orb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GUI FIELD DEFINITIONS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
defs.addAll(Arrays.asList(
|
||||
// Type tab
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
|
||||
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
|
||||
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
|
||||
),
|
||||
FieldDef.section("ability.section.homing"),
|
||||
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"),
|
||||
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
|
||||
FieldDef.section("ability.section.explosive"),
|
||||
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"),
|
||||
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
|
||||
|
||||
|
||||
// Visual tab
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
|
||||
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
|
||||
.visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
|
||||
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.section("ability.section.effects").tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
|
||||
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
|
||||
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
|
||||
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
|
||||
).tab("ability.tab.visual")
|
||||
// Type tab - projectile count and fire delay
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
|
||||
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
|
||||
.range(0, 200).visibleWhen(() -> projectileCount > 1)
|
||||
));
|
||||
|
||||
// Type tab - orb properties
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
|
||||
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
|
||||
));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
|
||||
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
|
||||
));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
|
||||
));
|
||||
defs.add(FieldDef.section("ability.section.homing"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
|
||||
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(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
|
||||
// Visual tab - shared primary colors
|
||||
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
|
||||
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
|
||||
.visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
|
||||
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
|
||||
).tab("ability.tab.visual"));
|
||||
|
||||
// Visual tab - shared effects
|
||||
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
|
||||
.tab("ability.tab.visual"));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
|
||||
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
|
||||
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
|
||||
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
|
||||
).tab("ability.tab.visual"));
|
||||
|
||||
// Visual tab - per-projectile sections
|
||||
for (int i = 0; i < MAX_PROJECTILES; i++) {
|
||||
final int idx = i;
|
||||
String sectionKey = "ability.section.projectile" + (i + 1);
|
||||
|
||||
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
|
||||
.visibleWhen(() -> idx < projectileCount));
|
||||
|
||||
// Anchor point
|
||||
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
|
||||
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
|
||||
defs.add(FieldDef.row(
|
||||
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v),
|
||||
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v)
|
||||
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
|
||||
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
|
||||
|
||||
// Color override (only when multiple projectiles)
|
||||
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
|
||||
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
|
||||
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
|
||||
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,614 +0,0 @@
|
||||
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.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
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 kamkeel.npcs.entity.EntityAbilityOrb;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import net.minecraftforge.common.util.Constants;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import noppes.npcs.util.ValueUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AbilityOrbBarrage extends Ability {
|
||||
|
||||
// Ability-specific properties
|
||||
private float orbSize = 0.5f;
|
||||
private int orbAmount = 16;
|
||||
private int firingSpeed = 2;
|
||||
private boolean dualCharging = true;
|
||||
|
||||
// Data classes for energy properties
|
||||
private final EnergyDisplayData colorData = new EnergyDisplayData();
|
||||
private final EnergyCombatData combatData = new EnergyCombatData(4.0f, 0.0f, 0.0f, false, 1.5f, 0.5f);
|
||||
private final EnergyHomingData homingData = new EnergyHomingData();
|
||||
private final EnergyLightningData lightningData = new EnergyLightningData();
|
||||
private final EnergyLifespanData lifespanData = new EnergyLifespanData();
|
||||
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
|
||||
new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND)
|
||||
};
|
||||
private final EnergyTrajectoryData trajectoryData = new EnergyTrajectoryData();
|
||||
|
||||
|
||||
// Array to store entities
|
||||
private transient List<EntityAbilityOrb> orbEntities = new ArrayList<>();
|
||||
private transient EntityAbilityOrb[] chargingEntities = null;
|
||||
|
||||
// Field to keep track of the currently firing orb
|
||||
private int currentOrb = 0;
|
||||
|
||||
public AbilityOrbBarrage() {
|
||||
this.typeId = "ability.cnpc.orb_barrage";
|
||||
this.name = "Orb Barrage";
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 75.0f;
|
||||
this.minRange = 5.0f;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 30;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.showTelegraph = true;
|
||||
// Default built-in animation
|
||||
this.windUpAnimationName = "Ability_BeamDual_Windup";
|
||||
this.activeAnimationName = "Ability_BeamDual_Active";
|
||||
}
|
||||
|
||||
// @Override
|
||||
// @SideOnly(Side.CLIENT)
|
||||
// public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
|
||||
// return new SubGuiAbilityOrbBarrage(this, callback);
|
||||
// }
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetingMode[] getAllowedTargetingModes() {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < chargingEntities.length; i++) {
|
||||
EntityAbilityOrb orbEntity = chargingEntities[i];
|
||||
|
||||
if (orbEntity != null && !orbEntity.isDead) {
|
||||
orbEntity.setDead();
|
||||
}
|
||||
|
||||
chargingEntities[i] = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
|
||||
// Spawn orb in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
// Create orb in charging mode - follows caster based on anchor point during windup
|
||||
|
||||
if (dualCharging) {
|
||||
chargingEntities = new EntityAbilityOrb[2];
|
||||
} else {
|
||||
chargingEntities = new EntityAbilityOrb[1];
|
||||
}
|
||||
|
||||
for (int i = 0; i < chargingEntities.length; i++) {
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[i]);
|
||||
|
||||
EntityAbilityOrb orbEntity = new EntityAbilityOrb(
|
||||
world, caster, target,
|
||||
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
|
||||
colorData, combatData, homingData, lightningData, lifespanData, trajectoryData
|
||||
);
|
||||
|
||||
orbEntity.setupCharging(anchorData[i], windUpTicks);
|
||||
orbEntity.setSourceAbility(this);
|
||||
|
||||
chargingEntities[i] = orbEntity;
|
||||
world.spawnEntityInWorld(orbEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (!orbEntities.isEmpty()) {
|
||||
boolean allDead = true;
|
||||
|
||||
for (EntityAbilityOrb orbEntity : orbEntities) {
|
||||
if (orbEntity != null && !orbEntity.isDead) {
|
||||
allDead = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allDead && currentOrb >= orbAmount) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentOrb >= orbAmount) {
|
||||
return;
|
||||
}
|
||||
|
||||
int ticksPerOrb = Math.max(1, 20 / firingSpeed);
|
||||
|
||||
if (tick % ticksPerOrb != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
EnergyAnchorData anchor;
|
||||
|
||||
if (dualCharging) {
|
||||
anchor = anchorData[currentOrb % 2];
|
||||
} else {
|
||||
anchor = anchorData[0];
|
||||
}
|
||||
|
||||
Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchor);
|
||||
|
||||
EntityAbilityOrb orb = new EntityAbilityOrb(
|
||||
world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
|
||||
orbSize, colorData, combatData, homingData, lightningData, lifespanData, trajectoryData
|
||||
);
|
||||
|
||||
orb.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(orb);
|
||||
|
||||
orbEntities.add(orb);
|
||||
currentOrb++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canInterrupt(DamageSource source) {
|
||||
// Can be interrupted if the caster is hit when the beam is moving
|
||||
if (!interruptible) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only direct physical hits can interrupt, not magic, fire, or other indirect damage
|
||||
if (source == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject indirect damage types
|
||||
if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject damage without a direct attacker entity
|
||||
if (source.getEntity() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Direct hit from an entity - can interrupt
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
currentOrb = 0;
|
||||
|
||||
if (chargingEntities != null) {
|
||||
for (int i = 0; i < chargingEntities.length; i++) {
|
||||
if (chargingEntities[i] != null && !chargingEntities[i].isDead) {
|
||||
chargingEntities[i].setDead();
|
||||
}
|
||||
chargingEntities[i] = null;
|
||||
}
|
||||
chargingEntities = null;
|
||||
}
|
||||
|
||||
if (orbEntities != null && !orbEntities.isEmpty()) {
|
||||
for (EntityAbilityOrb orbEntity : orbEntities) {
|
||||
if (orbEntity != null && !orbEntity.isDead) {
|
||||
orbEntity.setDead();
|
||||
}
|
||||
}
|
||||
orbEntities.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create small circle telegraph at target position
|
||||
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
|
||||
telegraph.setDurationTicks(windUpTicks);
|
||||
telegraph.setColor(windUpColor);
|
||||
telegraph.setWarningColor(activeColor);
|
||||
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
|
||||
telegraph.setHeightOffset(telegraphHeightOffset);
|
||||
|
||||
// Position at target and follow target during windup
|
||||
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
|
||||
instance.setCasterEntityId(caster.getEntityId());
|
||||
instance.setEntityIdToFollow(target.getEntityId());
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTelegraphRadius() {
|
||||
return orbSize * 1.5f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("orbSize", orbSize);
|
||||
nbt.setInteger("orbAmount", orbAmount);
|
||||
nbt.setInteger("firingSpeed", firingSpeed);
|
||||
nbt.setBoolean("dualCharging", dualCharging);
|
||||
colorData.writeNBT(nbt);
|
||||
combatData.writeNBT(nbt);
|
||||
homingData.writeNBT(nbt);
|
||||
lightningData.writeNBT(nbt);
|
||||
lifespanData.writeNBT(nbt);
|
||||
trajectoryData.writeNBT(nbt);
|
||||
|
||||
for (int i = 0; i < anchorData.length; i++) {
|
||||
NBTTagCompound comp = new NBTTagCompound();
|
||||
anchorData[i].writeNBT(comp);
|
||||
nbt.setTag("Anchor_" + i, comp);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f;
|
||||
this.orbAmount = nbt.hasKey("orbAmount") ? nbt.getInteger("orbAmount") : 16;
|
||||
this.firingSpeed = nbt.hasKey("firingSpeed") ? nbt.getInteger("firingSpeed") : 2;
|
||||
this.dualCharging = !nbt.hasKey("dualCharging") || nbt.getBoolean("dualCharging");
|
||||
colorData.readNBT(nbt);
|
||||
combatData.readNBT(nbt);
|
||||
homingData.readNBT(nbt);
|
||||
lightningData.readNBT(nbt);
|
||||
lifespanData.readNBT(nbt);
|
||||
trajectoryData.readNBT(nbt);
|
||||
|
||||
for (int i = 0; i < anchorData.length; i++) {
|
||||
if (nbt.hasKey("Anchor_" + i, Constants.NBT.TAG_COMPOUND)) {
|
||||
anchorData[i].readNBT(nbt.getCompoundTag("Anchor_" + i));
|
||||
} else {
|
||||
anchorData[i] = new EnergyAnchorData(i == 0 ? AnchorPoint.RIGHT_HAND : AnchorPoint.LEFT_HAND);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Getters & Setters
|
||||
public float getOrbSpeed() {
|
||||
return homingData.speed;
|
||||
}
|
||||
|
||||
public void setOrbSpeed(float orbSpeed) {
|
||||
homingData.speed = orbSpeed;
|
||||
}
|
||||
|
||||
public float getOrbSize() {
|
||||
return orbSize;
|
||||
}
|
||||
|
||||
public void setOrbSize(float orbSize) {
|
||||
this.orbSize = orbSize;
|
||||
}
|
||||
|
||||
public int getOrbAmount() { return orbAmount; }
|
||||
|
||||
public void setOrbAmount(int orbAmount) { this.orbAmount = orbAmount; }
|
||||
|
||||
public int getFiringSpeed() { return firingSpeed; }
|
||||
|
||||
public void setFiringSpeed(int firingSpeed) { this.firingSpeed = firingSpeed; }
|
||||
|
||||
public boolean isDualCharging() { return dualCharging; }
|
||||
|
||||
public void setDualCharging(boolean dualCharging) { this.dualCharging = dualCharging; }
|
||||
|
||||
public float getMaxDistance() {
|
||||
return lifespanData.maxDistance;
|
||||
}
|
||||
|
||||
public void setMaxDistance(float maxDistance) {
|
||||
lifespanData.maxDistance = maxDistance;
|
||||
}
|
||||
|
||||
public int getMaxLifetime() {
|
||||
return lifespanData.maxLifetime;
|
||||
}
|
||||
|
||||
public void setMaxLifetime(int maxLifetime) {
|
||||
lifespanData.maxLifetime = maxLifetime;
|
||||
}
|
||||
|
||||
public float getDamage() {
|
||||
return combatData.damage;
|
||||
}
|
||||
|
||||
public void setDamage(float damage) {
|
||||
combatData.damage = damage;
|
||||
}
|
||||
|
||||
public float getKnockback() {
|
||||
return combatData.knockback;
|
||||
}
|
||||
|
||||
public void setKnockback(float knockback) {
|
||||
combatData.knockback = knockback;
|
||||
}
|
||||
|
||||
public float getKnockbackUp() {
|
||||
return combatData.knockbackUp;
|
||||
}
|
||||
|
||||
public void setKnockbackUp(float knockbackUp) {
|
||||
combatData.knockbackUp = knockbackUp;
|
||||
}
|
||||
|
||||
public boolean isHoming() {
|
||||
return homingData.homing;
|
||||
}
|
||||
|
||||
public void setHoming(boolean homing) {
|
||||
homingData.homing = homing;
|
||||
}
|
||||
|
||||
public float getHomingStrength() {
|
||||
return homingData.homingStrength;
|
||||
}
|
||||
|
||||
public void setHomingStrength(float homingStrength) {
|
||||
homingData.homingStrength = homingStrength;
|
||||
}
|
||||
|
||||
public float getHomingRange() {
|
||||
return homingData.homingRange;
|
||||
}
|
||||
|
||||
public void setHomingRange(float homingRange) {
|
||||
homingData.homingRange = homingRange;
|
||||
}
|
||||
|
||||
public boolean isExplosive() {
|
||||
return combatData.explosive;
|
||||
}
|
||||
|
||||
public void setExplosive(boolean explosive) {
|
||||
combatData.explosive = explosive;
|
||||
}
|
||||
|
||||
public float getExplosionRadius() {
|
||||
return combatData.explosionRadius;
|
||||
}
|
||||
|
||||
public void setExplosionRadius(float explosionRadius) {
|
||||
combatData.explosionRadius = explosionRadius;
|
||||
}
|
||||
|
||||
public float getExplosionDamageFalloff() {
|
||||
return combatData.explosionDamageFalloff;
|
||||
}
|
||||
|
||||
public void setExplosionDamageFalloff(float explosionDamageFalloff) {
|
||||
combatData.explosionDamageFalloff = explosionDamageFalloff;
|
||||
}
|
||||
|
||||
public int getInnerColor() {
|
||||
return colorData.innerColor;
|
||||
}
|
||||
|
||||
public void setInnerColor(int innerColor) {
|
||||
colorData.innerColor = innerColor;
|
||||
}
|
||||
|
||||
public int getOuterColor() {
|
||||
return colorData.outerColor;
|
||||
}
|
||||
|
||||
public void setOuterColor(int outerColor) {
|
||||
colorData.outerColor = outerColor;
|
||||
}
|
||||
|
||||
public float getOuterColorWidth() {
|
||||
return colorData.outerColorWidth;
|
||||
}
|
||||
|
||||
public void setOuterColorWidth(float outerColorWidth) {
|
||||
colorData.outerColorWidth = outerColorWidth;
|
||||
}
|
||||
|
||||
public float getOuterColorAlpha() {
|
||||
return colorData.outerColorAlpha;
|
||||
}
|
||||
|
||||
public void setOuterColorAlpha(float outerColorAlpha) {
|
||||
colorData.outerColorAlpha = outerColorAlpha;
|
||||
}
|
||||
|
||||
public boolean isOuterColorEnabled() {
|
||||
return colorData.outerColorEnabled;
|
||||
}
|
||||
|
||||
public void setOuterColorEnabled(boolean outerColorEnabled) {
|
||||
colorData.outerColorEnabled = outerColorEnabled;
|
||||
}
|
||||
|
||||
public float getRotationSpeed() {
|
||||
return colorData.rotationSpeed;
|
||||
}
|
||||
|
||||
public void setRotationSpeed(float rotationSpeed) {
|
||||
colorData.rotationSpeed = rotationSpeed;
|
||||
}
|
||||
|
||||
public boolean hasLightningEffect() {
|
||||
return lightningData.lightningEffect;
|
||||
}
|
||||
|
||||
public void setLightningEffect(boolean lightningEffect) {
|
||||
lightningData.lightningEffect = lightningEffect;
|
||||
}
|
||||
|
||||
public float getLightningDensity() {
|
||||
return lightningData.lightningDensity;
|
||||
}
|
||||
|
||||
public void setLightningDensity(float lightningDensity) {
|
||||
lightningData.lightningDensity = lightningDensity;
|
||||
}
|
||||
|
||||
public float getLightningRadius() {
|
||||
return lightningData.lightningRadius;
|
||||
}
|
||||
|
||||
public void setLightningRadius(float lightningRadius) {
|
||||
lightningData.lightningRadius = lightningRadius;
|
||||
}
|
||||
|
||||
public int getLightningFadeTime() {
|
||||
return lightningData.lightningFadeTime;
|
||||
}
|
||||
|
||||
public void setLightningFadeTime(int lightningFadeTime) {
|
||||
lightningData.lightningFadeTime = lightningFadeTime;
|
||||
}
|
||||
|
||||
private EnergyAnchorData getAnchorData(int orb) {
|
||||
orb = ValueUtil.clamp(orb, 0, 1);
|
||||
return anchorData[orb];
|
||||
}
|
||||
|
||||
public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; }
|
||||
public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; }
|
||||
public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; }
|
||||
public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; }
|
||||
public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; }
|
||||
public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; }
|
||||
public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; }
|
||||
public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; }
|
||||
|
||||
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
|
||||
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
|
||||
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
|
||||
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
|
||||
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
|
||||
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
|
||||
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
|
||||
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
|
||||
|
||||
//@Override
|
||||
public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); }
|
||||
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
|
||||
|
||||
//@Override
|
||||
public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj);
|
||||
orb.setupPreview(npc, orbSize, colorData, lightningData, anchorData[0], windUpTicks);
|
||||
return orb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
defs.addAll(Arrays.asList(
|
||||
// Type tab
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
|
||||
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
|
||||
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
|
||||
),
|
||||
FieldDef.section("ability.section.barrage"),
|
||||
FieldDef.row(
|
||||
FieldDef.intField("ability.amount", this::getOrbAmount, this::setOrbAmount),
|
||||
FieldDef.intField("ability.firingSpeed", this::getFiringSpeed, this::setFiringSpeed)
|
||||
),
|
||||
FieldDef.boolField("ability.dualCharge", this::isDualCharging, this::setDualCharging),
|
||||
FieldDef.section("ability.section.homing"),
|
||||
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"),
|
||||
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
|
||||
FieldDef.section("ability.section.explosive"),
|
||||
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"),
|
||||
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
|
||||
|
||||
|
||||
// Visual tab
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
|
||||
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
|
||||
.visibleWhen(this::isOuterColorEnabled),
|
||||
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
|
||||
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.section("ability.section.effects").tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
|
||||
.range(0.01f, 100f).visibleWhen(this::hasLightningEffect),
|
||||
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
|
||||
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
|
||||
).tab("ability.tab.visual")
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,586 +0,0 @@
|
||||
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.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
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 kamkeel.npcs.entity.EntityAbilityOrb;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import noppes.npcs.util.ValueUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public class AbilityOrbDual extends Ability {
|
||||
// Ability-specific properties
|
||||
private float orbSize = 1.0f;
|
||||
private boolean dualFire= true;
|
||||
private int dualFireDelay = 5;
|
||||
|
||||
// Data classes for energy properties
|
||||
public final EnergyDisplayData[] displayData = new EnergyDisplayData[]{
|
||||
new EnergyDisplayData(), new EnergyDisplayData()
|
||||
};
|
||||
public final EnergyCombatData combatData = new EnergyCombatData();
|
||||
public final EnergyHomingData homingData = new EnergyHomingData();
|
||||
public final EnergyLightningData[] lightningData = new EnergyLightningData[]{
|
||||
new EnergyLightningData(), new EnergyLightningData()
|
||||
};
|
||||
public final EnergyLifespanData lifespanData = new EnergyLifespanData();
|
||||
private final EnergyAnchorData[] anchorData = new EnergyAnchorData[]{
|
||||
new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND)
|
||||
};
|
||||
private EnergyTrajectoryData[] trajectoryData = new EnergyTrajectoryData[]{
|
||||
new EnergyTrajectoryData(), new EnergyTrajectoryData()
|
||||
};
|
||||
|
||||
// Transient state for orb entity (used during windup charging)
|
||||
private transient EntityAbilityOrb orbEntity = null;
|
||||
private transient EntityAbilityOrb orbEntity2 = null;
|
||||
|
||||
public AbilityOrbDual() {
|
||||
this.typeId = "ability.cnpc.orb_dual";
|
||||
this.name = "Orb Dual";
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 25.0f;
|
||||
this.minRange = 5.0f;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 30;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.showTelegraph = true;
|
||||
// Default built-in animation
|
||||
this.windUpAnimationName = "Ability_OrbDual_Windup";
|
||||
this.activeAnimationName = "Ability_OrbDual_Active";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetingMode[] getAllowedTargetingModes() {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Start moving the orb that was spawned during windup
|
||||
if (orbEntity != null && !orbEntity.isDead) {
|
||||
orbEntity.startMoving(target);
|
||||
}
|
||||
|
||||
if (dualFire) {
|
||||
if (orbEntity2 != null && !orbEntity2.isDead && dualFireDelay <= 0) {
|
||||
orbEntity2.startMoving(target);
|
||||
}
|
||||
} else if (orbEntity2 != null) {
|
||||
orbEntity2.setDead();
|
||||
}
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (world.isRemote) return;
|
||||
|
||||
// Spawn orb in charging mode on first tick of windup
|
||||
if (tick == 1) {
|
||||
// Create orb in charging mode - follows caster based on anchor point during windup
|
||||
Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]);
|
||||
orbEntity = new EntityAbilityOrb(
|
||||
world, caster, target,
|
||||
spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, orbSize,
|
||||
displayData[0], combatData, homingData, lightningData[0], lifespanData, trajectoryData[0]);
|
||||
|
||||
Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]);
|
||||
orbEntity2 = new EntityAbilityOrb(
|
||||
world, caster, target,
|
||||
spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, orbSize,
|
||||
displayData[1], combatData, homingData, lightningData[1], lifespanData, trajectoryData[1]);
|
||||
|
||||
orbEntity.setSiblingEntityId(orbEntity2.getEntityId());
|
||||
orbEntity2.setSiblingEntityId(orbEntity.getEntityId());
|
||||
|
||||
orbEntity.setupCharging(anchorData[0], windUpTicks);
|
||||
orbEntity2.setupCharging(anchorData[1], windUpTicks);
|
||||
|
||||
orbEntity.setEffects(this.effects);
|
||||
|
||||
if (dualFire) {
|
||||
orbEntity2.setEffects(this.effects);
|
||||
}
|
||||
|
||||
orbEntity.setSourceAbility(this);
|
||||
orbEntity2.setSourceAbility(this);
|
||||
world.spawnEntityInWorld(orbEntity);
|
||||
world.spawnEntityInWorld(orbEntity2);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// Signal completion when entity dies
|
||||
if (dualFire) {
|
||||
if ((orbEntity == null || orbEntity.isDead) && (orbEntity2 == null || orbEntity2.isDead)) {
|
||||
orbEntity = null;
|
||||
orbEntity2 = null;
|
||||
signalCompletion();
|
||||
}
|
||||
|
||||
if (tick == dualFireDelay && orbEntity2 != null && !orbEntity2.isDead) {
|
||||
orbEntity2.startMoving(target);
|
||||
}
|
||||
} else {
|
||||
if (orbEntity == null || orbEntity.isDead) {
|
||||
orbEntity = null;
|
||||
signalCompletion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Despawn orb entity if still alive
|
||||
if (orbEntity != null && !orbEntity.isDead) {
|
||||
orbEntity.setDead();
|
||||
}
|
||||
|
||||
if (orbEntity2 != null && !orbEntity2.isDead) {
|
||||
orbEntity2.setDead();
|
||||
}
|
||||
|
||||
orbEntity = null;
|
||||
orbEntity2 = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create small circle telegraph at target position
|
||||
Telegraph telegraph = Telegraph.circle(orbSize * 1.5f);
|
||||
telegraph.setDurationTicks(windUpTicks);
|
||||
telegraph.setColor(windUpColor);
|
||||
telegraph.setWarningColor(activeColor);
|
||||
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
|
||||
telegraph.setHeightOffset(telegraphHeightOffset);
|
||||
|
||||
// Position at target and follow target during windup
|
||||
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw);
|
||||
instance.setCasterEntityId(caster.getEntityId());
|
||||
instance.setEntityIdToFollow(target.getEntityId());
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTelegraphRadius() {
|
||||
return orbSize * 1.5f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("orbSize", orbSize);
|
||||
nbt.setBoolean("dualFire", dualFire);
|
||||
nbt.setInteger("dualFireDelay", dualFireDelay);
|
||||
combatData.writeNBT(nbt);
|
||||
homingData.writeNBT(nbt);
|
||||
lifespanData.writeNBT(nbt);
|
||||
// Backward compat: old key was "orbSpeed"
|
||||
nbt.setFloat("orbSpeed", homingData.speed);
|
||||
|
||||
NBTTagCompound orb1 = new NBTTagCompound();
|
||||
NBTTagCompound orb2 = new NBTTagCompound();
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
NBTTagCompound orb = i == 0 ? orb1 : orb2;
|
||||
anchorData[i].writeNBT(orb);
|
||||
displayData[i].writeNBT(orb);
|
||||
lightningData[i].writeNBT(orb);
|
||||
trajectoryData[i].writeNBT(orb);
|
||||
}
|
||||
|
||||
nbt.setTag("Orb_1", orb1);
|
||||
nbt.setTag("Orb_2", orb2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
NBTTagCompound orb1 = nbt.hasKey("Orb_1") ? nbt.getCompoundTag("Orb_1") : new NBTTagCompound();
|
||||
NBTTagCompound orb2 = nbt.hasKey("Orb_2") ? nbt.getCompoundTag("Orb_2") : new NBTTagCompound();
|
||||
|
||||
this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f;
|
||||
this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire");
|
||||
this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0;
|
||||
combatData.readNBT(nbt);
|
||||
homingData.readNBT(nbt);
|
||||
lifespanData.readNBT(nbt);
|
||||
// Backward compat: old key was "orbSpeed"
|
||||
if (nbt.hasKey("orbSpeed")) {
|
||||
homingData.speed = nbt.getFloat("orbSpeed");
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
NBTTagCompound orb = i == 0 ? orb1 : orb2;
|
||||
anchorData[i].readNBT(orb);
|
||||
displayData[i].readNBT(orb);
|
||||
lightningData[i].readNBT(orb);
|
||||
trajectoryData[i].readNBT(orb);
|
||||
}
|
||||
}
|
||||
|
||||
// Getters & Setters
|
||||
private EnergyDisplayData getColorData(int orb) {
|
||||
orb = ValueUtil.clamp(orb, 0, 1);
|
||||
return displayData[orb];
|
||||
}
|
||||
private EnergyLightningData getLightningData(int orb) {
|
||||
orb = ValueUtil.clamp(orb, 0, 1);
|
||||
return lightningData[orb];
|
||||
}
|
||||
private EnergyAnchorData getAnchorData(int orb) {
|
||||
orb = ValueUtil.clamp(orb, 0, 1);
|
||||
return anchorData[orb];
|
||||
}
|
||||
|
||||
public float getOrbSpeed() {
|
||||
return homingData.speed;
|
||||
}
|
||||
|
||||
public void setOrbSpeed(float orbSpeed) {
|
||||
homingData.speed = orbSpeed;
|
||||
}
|
||||
|
||||
public float getOrbSize() {
|
||||
return orbSize;
|
||||
}
|
||||
|
||||
public void setOrbSize(float orbSize) {
|
||||
this.orbSize = orbSize;
|
||||
}
|
||||
|
||||
public boolean isDualFire() {
|
||||
return dualFire;
|
||||
}
|
||||
|
||||
public void setDualFire(boolean dualFire) {
|
||||
this.dualFire = dualFire;
|
||||
}
|
||||
|
||||
public int getDualFireDelay() {
|
||||
return dualFireDelay;
|
||||
}
|
||||
|
||||
public void setDualFireDelay(int dualFireDelay) {
|
||||
this.dualFireDelay = dualFireDelay;
|
||||
}
|
||||
|
||||
public float getMaxDistance() {
|
||||
return lifespanData.maxDistance;
|
||||
}
|
||||
|
||||
public void setMaxDistance(float maxDistance) {
|
||||
lifespanData.maxDistance = maxDistance;
|
||||
}
|
||||
|
||||
public int getMaxLifetime() {
|
||||
return lifespanData.maxLifetime;
|
||||
}
|
||||
|
||||
public void setMaxLifetime(int maxLifetime) {
|
||||
lifespanData.maxLifetime = maxLifetime;
|
||||
}
|
||||
|
||||
public float getDamage() {
|
||||
return combatData.damage;
|
||||
}
|
||||
|
||||
public void setDamage(float damage) {
|
||||
combatData.damage = damage;
|
||||
}
|
||||
|
||||
public float getKnockback() {
|
||||
return combatData.knockback;
|
||||
}
|
||||
|
||||
public void setKnockback(float knockback) {
|
||||
combatData.knockback = knockback;
|
||||
}
|
||||
|
||||
public float getKnockbackUp() {
|
||||
return combatData.knockbackUp;
|
||||
}
|
||||
|
||||
public void setKnockbackUp(float knockbackUp) {
|
||||
combatData.knockbackUp = knockbackUp;
|
||||
}
|
||||
|
||||
public boolean isHoming() {
|
||||
return homingData.homing;
|
||||
}
|
||||
|
||||
public void setHoming(boolean homing) {
|
||||
homingData.homing = homing;
|
||||
}
|
||||
|
||||
public float getHomingStrength() {
|
||||
return homingData.homingStrength;
|
||||
}
|
||||
|
||||
public void setHomingStrength(float homingStrength) {
|
||||
homingData.homingStrength = homingStrength;
|
||||
}
|
||||
|
||||
public float getHomingRange() {
|
||||
return homingData.homingRange;
|
||||
}
|
||||
|
||||
public void setHomingRange(float homingRange) {
|
||||
homingData.homingRange = homingRange;
|
||||
}
|
||||
|
||||
public boolean isExplosive() {
|
||||
return combatData.explosive;
|
||||
}
|
||||
|
||||
public void setExplosive(boolean explosive) {
|
||||
combatData.explosive = explosive;
|
||||
}
|
||||
|
||||
public float getExplosionRadius() {
|
||||
return combatData.explosionRadius;
|
||||
}
|
||||
|
||||
public void setExplosionRadius(float explosionRadius) {
|
||||
combatData.explosionRadius = explosionRadius;
|
||||
}
|
||||
|
||||
public float getExplosionDamageFalloff() {
|
||||
return combatData.explosionDamageFalloff;
|
||||
}
|
||||
|
||||
public void setExplosionDamageFalloff(float explosionDamageFalloff) {
|
||||
combatData.explosionDamageFalloff = explosionDamageFalloff;
|
||||
}
|
||||
|
||||
|
||||
public int getInnerColor(int orb) { return getColorData(orb).innerColor; }
|
||||
public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; }
|
||||
public int getOuterColor(int orb) { return getColorData(orb).outerColor; }
|
||||
public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; }
|
||||
public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; }
|
||||
public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; }
|
||||
public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; }
|
||||
public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; }
|
||||
public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; }
|
||||
public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; }
|
||||
public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; }
|
||||
public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; }
|
||||
|
||||
public int getInnerColor() { return getInnerColor(0); }
|
||||
public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); }
|
||||
public int getOuterColor() { return getOuterColor(0); }
|
||||
public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); }
|
||||
public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); }
|
||||
public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); }
|
||||
public float getOuterColorWidth() { return getOuterColorWidth(0); }
|
||||
public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); }
|
||||
public float getOuterColorAlpha() { return getOuterColorAlpha(0); }
|
||||
public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); }
|
||||
public float getRotationSpeed() { return getRotationSpeed(0); }
|
||||
public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); }
|
||||
|
||||
|
||||
public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; }
|
||||
public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; }
|
||||
public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; }
|
||||
public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; }
|
||||
public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; }
|
||||
public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; }
|
||||
|
||||
public boolean hasLightningEffect() { return hasLightningEffect(0); }
|
||||
public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); }
|
||||
public float getLightningDensity() { return getLightningDensity(0); }
|
||||
public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); }
|
||||
public float getLightningRadius() { return getLightningRadius(0); }
|
||||
public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); }
|
||||
|
||||
|
||||
public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; }
|
||||
public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; }
|
||||
public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; }
|
||||
public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; }
|
||||
public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; }
|
||||
public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; }
|
||||
public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; }
|
||||
public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; }
|
||||
|
||||
public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); }
|
||||
public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); }
|
||||
public float getAnchorOffsetX() { return getAnchorOffsetX(0); }
|
||||
public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); }
|
||||
public float getAnchorOffsetY() { return getAnchorOffsetY(0); }
|
||||
public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); }
|
||||
public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); }
|
||||
public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); }
|
||||
|
||||
//@Override
|
||||
public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); }
|
||||
public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); }
|
||||
|
||||
//@Override
|
||||
public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); }
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
defs.addAll(Arrays.asList(
|
||||
// Type tab
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
|
||||
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize),
|
||||
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance),
|
||||
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime)
|
||||
),
|
||||
FieldDef.section("ability.section.dual"),
|
||||
FieldDef.boolField("ability.dualFire", this::isDualFire, this::setDualFire),
|
||||
FieldDef.intField("ability.dualFireDelay", this::getDualFireDelay, this::setDualFireDelay)
|
||||
.visibleWhen(this::isDualFire),
|
||||
FieldDef.section("ability.section.homing"),
|
||||
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"),
|
||||
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
|
||||
FieldDef.section("ability.section.explosive"),
|
||||
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"),
|
||||
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
|
||||
// Visual tab - Orb 1
|
||||
FieldDef.section("ability.section.orb1").tab("ability.tab.visual"),
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
|
||||
() -> getAnchorPointEnum(0), v -> setAnchorPointEnum(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor",
|
||||
() -> getInnerColor(0), v -> setInnerColor(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled",
|
||||
() -> isOuterColorEnabled(0), v -> setOuterColorEnabled(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor",
|
||||
() -> getOuterColor(0), v -> setOuterColor(0, v))
|
||||
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(0)),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth",
|
||||
() -> getOuterColorWidth(0), v -> setOuterColorWidth(0, v))
|
||||
.visibleWhen(() -> isOuterColorEnabled(0)),
|
||||
FieldDef.floatField("ability.outerAlpha",
|
||||
() -> getOuterColorAlpha(0), v -> setOuterColorAlpha(0, v))
|
||||
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(0))
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.rotationSpeed",
|
||||
() -> getRotationSpeed(0), v -> setRotationSpeed(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.lightning",
|
||||
() -> hasLightningEffect(0), v -> setLightningEffect(0, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density",
|
||||
() -> getLightningDensity(0), v -> setLightningDensity(0, v))
|
||||
.range(0.01f, 5.0f).visibleWhen(() -> hasLightningEffect(0)),
|
||||
FieldDef.floatField("gui.radius",
|
||||
() -> getLightningRadius(0), v -> setLightningRadius(0, v))
|
||||
.range(0.1f, 10.0f).visibleWhen(() -> hasLightningEffect(0))
|
||||
).tab("ability.tab.visual"),
|
||||
// Visual tab - Orb 2
|
||||
FieldDef.section("ability.section.orb2").tab("ability.tab.visual"),
|
||||
FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
|
||||
() -> getAnchorPointEnum(1), v -> setAnchorPointEnum(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor",
|
||||
() -> getInnerColor(1), v -> setInnerColor(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled",
|
||||
() -> isOuterColorEnabled(1), v -> setOuterColorEnabled(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor",
|
||||
() -> getOuterColor(1), v -> setOuterColor(1, v))
|
||||
.tab("ability.tab.visual").visibleWhen(() -> isOuterColorEnabled(1)),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("ability.outerWidth",
|
||||
() -> getOuterColorWidth(1), v -> setOuterColorWidth(1, v))
|
||||
.visibleWhen(() -> isOuterColorEnabled(1)),
|
||||
FieldDef.floatField("ability.outerAlpha",
|
||||
() -> getOuterColorAlpha(1), v -> setOuterColorAlpha(1, v))
|
||||
.range(0, 1).visibleWhen(() -> isOuterColorEnabled(1))
|
||||
).tab("ability.tab.visual"),
|
||||
FieldDef.floatField("ability.rotationSpeed",
|
||||
() -> getRotationSpeed(1), v -> setRotationSpeed(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.lightning",
|
||||
() -> hasLightningEffect(1), v -> setLightningEffect(1, v))
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density",
|
||||
() -> getLightningDensity(1), v -> setLightningDensity(1, v))
|
||||
.range(0.01f, 100f).visibleWhen(() -> hasLightningEffect(1)),
|
||||
FieldDef.floatField("gui.radius",
|
||||
() -> getLightningRadius(1), v -> setLightningRadius(1, v))
|
||||
.range(0.1f, 100f).visibleWhen(() -> hasLightningEffect(1))
|
||||
).tab("ability.tab.visual")
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public Entity createPreviewEntity(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null) return null;
|
||||
|
||||
EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj);
|
||||
orb.setupPreview(npc, orbSize, displayData[0], lightningData[0], anchorData[0], windUpTicks);
|
||||
return orb;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
@@ -50,6 +51,11 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
|
||||
this.showTelegraph = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowBurst() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
@@ -62,61 +68,129 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote || target == null) return;
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPlayerCaster(caster)) {
|
||||
// Player: instant hit-scan in look direction
|
||||
executePlayerProjectile(caster, world);
|
||||
} else if (target != null) {
|
||||
// NPC: instant hit-scan on aggro target
|
||||
executeNpcProjectile(caster, target, world);
|
||||
}
|
||||
|
||||
signalCompletion();
|
||||
}
|
||||
|
||||
/**
|
||||
* NPC projectile: instant hit-scan damage on the aggro target.
|
||||
*/
|
||||
private void executeNpcProjectile(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (isPreview()) return;
|
||||
|
||||
// Calculate direction to target
|
||||
double dx = target.posX - caster.posX;
|
||||
double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight());
|
||||
double dz = target.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len > 0) { dx /= len; dy /= len; dz /= len; }
|
||||
|
||||
if (len > 0) {
|
||||
dx /= len;
|
||||
dy /= len;
|
||||
dz /= len;
|
||||
}
|
||||
|
||||
// Deal instant damage with scripted event support
|
||||
// TODO: Use custom EntityAbilityProjectile for actual tracking
|
||||
applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz);
|
||||
|
||||
// Play sound
|
||||
world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f);
|
||||
|
||||
// Handle splash damage for explosive projectiles
|
||||
if (explosive && explosionRadius > 0) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(target,
|
||||
target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius));
|
||||
applyExplosionDamage(caster, target, world, target.posX, target.posY, target.posZ);
|
||||
}
|
||||
|
||||
for (Entity entity : entities) {
|
||||
if (entity instanceof EntityLivingBase && entity != caster) {
|
||||
EntityLivingBase living = (EntityLivingBase) entity;
|
||||
float dist = target.getDistanceToEntity(living);
|
||||
if (dist < explosionRadius) {
|
||||
float falloff = 1.0f - (dist / explosionRadius);
|
||||
// Apply splash damage with scripted event support (no knockback)
|
||||
applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
spawnProjectileParticles(world, caster, target);
|
||||
}
|
||||
|
||||
// Explosion particles
|
||||
world.playSoundAtEntity(target, "random.explode", 0.5f, 1.0f);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
world.spawnParticle("explode",
|
||||
target.posX + (world.rand.nextDouble() - 0.5) * explosionRadius,
|
||||
target.posY + world.rand.nextDouble() * target.height,
|
||||
target.posZ + (world.rand.nextDouble() - 0.5) * explosionRadius,
|
||||
0, 0.1, 0);
|
||||
/**
|
||||
* Player projectile: fires instant hit-scan in look direction.
|
||||
* Raycasts for the first entity in range and applies damage.
|
||||
*/
|
||||
private void executePlayerProjectile(EntityLivingBase caster, World world) {
|
||||
if (isPreview()) return;
|
||||
|
||||
Vec3 look = caster.getLookVec();
|
||||
double eyeX = caster.posX;
|
||||
double eyeY = caster.posY + caster.getEyeHeight();
|
||||
double eyeZ = caster.posZ;
|
||||
|
||||
// Raycast: find the closest entity along look direction
|
||||
EntityLivingBase hitTarget = null;
|
||||
double closestDist = maxRange;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> candidates = world.getEntitiesWithinAABBExcludingEntity(caster,
|
||||
caster.boundingBox.expand(maxRange, maxRange, maxRange));
|
||||
|
||||
for (Entity entity : candidates) {
|
||||
if (!(entity instanceof EntityLivingBase)) continue;
|
||||
if (entity.isDead) continue;
|
||||
|
||||
// Check if entity is roughly along look direction
|
||||
double dx = entity.posX - eyeX;
|
||||
double dy = (entity.posY + entity.height / 2) - eyeY;
|
||||
double dz = entity.posZ - eyeZ;
|
||||
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (dist > maxRange || dist < 0.5) continue;
|
||||
|
||||
// Dot product to check alignment with look vector
|
||||
double dot = (dx * look.xCoord + dy * look.yCoord + dz * look.zCoord) / dist;
|
||||
if (dot < 0.95) continue; // Must be within ~18 degree cone
|
||||
|
||||
if (dist < closestDist) {
|
||||
closestDist = dist;
|
||||
hitTarget = (EntityLivingBase) entity;
|
||||
}
|
||||
}
|
||||
|
||||
// Spawn projectile particles (visual trail)
|
||||
spawnProjectileParticles(world, caster, target);
|
||||
world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f);
|
||||
|
||||
// Projectile is instant
|
||||
signalCompletion();
|
||||
if (hitTarget != null) {
|
||||
applyAbilityDamageWithDirection(caster, hitTarget, damage, knockback, look.xCoord, look.zCoord);
|
||||
|
||||
if (explosive && explosionRadius > 0) {
|
||||
applyExplosionDamage(caster, hitTarget, world, hitTarget.posX, hitTarget.posY, hitTarget.posZ);
|
||||
}
|
||||
|
||||
spawnProjectileParticles(world, caster, hitTarget);
|
||||
} else {
|
||||
// No hit — spawn particles along look direction anyway
|
||||
spawnProjectileParticlesInDirection(world, caster, look);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply explosion splash damage around a point.
|
||||
*/
|
||||
private void applyExplosionDamage(EntityLivingBase caster, EntityLivingBase primaryTarget, World world,
|
||||
double x, double y, double z) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(primaryTarget,
|
||||
primaryTarget.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius));
|
||||
|
||||
for (Entity entity : entities) {
|
||||
if (entity instanceof EntityLivingBase && entity != caster) {
|
||||
EntityLivingBase living = (EntityLivingBase) entity;
|
||||
float dist = primaryTarget.getDistanceToEntity(living);
|
||||
if (dist < explosionRadius) {
|
||||
float falloff = 1.0f - (dist / explosionRadius);
|
||||
applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
world.playSoundAtEntity(primaryTarget, "random.explode", 0.5f, 1.0f);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
world.spawnParticle("explode",
|
||||
x + (world.rand.nextDouble() - 0.5) * explosionRadius,
|
||||
y + world.rand.nextDouble() * 2,
|
||||
z + (world.rand.nextDouble() - 0.5) * explosionRadius,
|
||||
0, 0.1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnProjectileParticles(World world, EntityLivingBase caster, EntityLivingBase target) {
|
||||
@@ -150,6 +224,31 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn projectile particles along a direction (player miss — no entity hit).
|
||||
*/
|
||||
private void spawnProjectileParticlesInDirection(World world, EntityLivingBase caster, Vec3 look) {
|
||||
double startX = caster.posX;
|
||||
double startY = caster.posY + caster.getEyeHeight();
|
||||
double startZ = caster.posZ;
|
||||
|
||||
String particle = "flame";
|
||||
if (projectileType.equals("arrow")) {
|
||||
particle = "crit";
|
||||
} else if (projectileType.equals("magic")) {
|
||||
particle = "witchMagic";
|
||||
}
|
||||
|
||||
int steps = (int) Math.max(5, maxRange * 2);
|
||||
for (int i = 0; i < steps; i++) {
|
||||
double progress = (double) i / steps * maxRange;
|
||||
double px = startX + look.xCoord * progress;
|
||||
double py = startY + look.yCoord * progress;
|
||||
double pz = startZ + look.zCoord * progress;
|
||||
world.spawnParticle(particle, px, py, pz, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// Projectile is instant, nothing to do per-tick
|
||||
|
||||
@@ -35,7 +35,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
this.typeId = "ability.cnpc.shockwave";
|
||||
this.name = "Shockwave";
|
||||
this.targetingMode = TargetingMode.AOE_SELF;
|
||||
this.maxRange = 15.0f;
|
||||
this.maxRange = 8.0f;
|
||||
this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 25;
|
||||
@@ -63,48 +63,50 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
// Shockwave is instant - apply effect immediately after windup
|
||||
if (!isPreview()) {
|
||||
// Shockwave is instant - apply effect immediately after windup
|
||||
|
||||
// Get all entities in radius
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
// Get all entities in radius
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
int count = 0;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
int count = 0;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist > pushRadius) continue;
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist > pushRadius) continue;
|
||||
|
||||
count++;
|
||||
if (count > maxTargets) break;
|
||||
count++;
|
||||
if (count > maxTargets) break;
|
||||
|
||||
// Calculate push direction (away from caster)
|
||||
double dx = entity.posX - caster.posX;
|
||||
double dz = entity.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
// Calculate push direction (away from caster)
|
||||
double dx = entity.posX - caster.posX;
|
||||
double dz = entity.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (len > 0) {
|
||||
dx /= len;
|
||||
dz /= len;
|
||||
} else {
|
||||
// Entity is directly on top of caster, push in random direction
|
||||
double angle = Math.random() * Math.PI * 2;
|
||||
dx = Math.cos(angle);
|
||||
dz = Math.sin(angle);
|
||||
}
|
||||
if (len > 0) {
|
||||
dx /= len;
|
||||
dz /= len;
|
||||
} else {
|
||||
// Entity is directly on top of caster, push in random direction
|
||||
double angle = Math.random() * Math.PI * 2;
|
||||
dx = Math.cos(angle);
|
||||
dz = Math.sin(angle);
|
||||
}
|
||||
|
||||
// Scale knockback by distance (closer = stronger)
|
||||
float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f;
|
||||
float finalPush = pushStrength * distFactor;
|
||||
// Apply damage with custom knockback direction
|
||||
boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz);
|
||||
// Scale knockback by distance (closer = stronger)
|
||||
float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f;
|
||||
float finalPush = pushStrength * distFactor;
|
||||
// Apply damage with custom knockback direction
|
||||
boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz);
|
||||
|
||||
// Apply effects if hit connected
|
||||
if (wasHit) {
|
||||
applyEffects(entity);
|
||||
// Apply effects if hit connected
|
||||
if (wasHit) {
|
||||
applyEffects(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,51 +85,74 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// Update target position during windup
|
||||
if (targetingMode == TargetingMode.AOE_SELF) {
|
||||
// AOE_SELF: slam at caster's current position
|
||||
if (isPlayerCaster(caster)) {
|
||||
// Player: slam will launch from current position, no target tracking needed
|
||||
targetX = caster.posX;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ;
|
||||
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
|
||||
// AOE_TARGET: telegraph follows target via setEntityIdToFollow
|
||||
targetX = target.posX;
|
||||
targetY = target.posY;
|
||||
targetZ = target.posZ;
|
||||
} else {
|
||||
// NPC: update target position during windup for telegraph tracking
|
||||
if (targetingMode == TargetingMode.AOE_SELF) {
|
||||
targetX = caster.posX;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ;
|
||||
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
|
||||
targetX = target.posX;
|
||||
targetY = target.posY;
|
||||
targetZ = target.posZ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
// Lock in the destination at moment of launch
|
||||
hasLaunched = false;
|
||||
hasLanded = false;
|
||||
airTicks = 0;
|
||||
caster.fallDistance = 0;
|
||||
|
||||
if (isPlayerCaster(caster)) {
|
||||
executePlayerSlam(caster);
|
||||
} else {
|
||||
executeNpcSlam(caster, target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NPC slam: Lock destination based on targeting mode, then launch in a ballistic arc toward it.
|
||||
*/
|
||||
private void executeNpcSlam(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (targetingMode == TargetingMode.AOE_SELF) {
|
||||
// AOE_SELF: slam in place (just jump up)
|
||||
targetX = caster.posX;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ;
|
||||
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
|
||||
// AOE_TARGET: leap to target's current position
|
||||
targetX = target.posX;
|
||||
targetY = target.posY;
|
||||
targetZ = target.posZ;
|
||||
} else {
|
||||
// Fallback: slam in place
|
||||
targetX = caster.posX;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ;
|
||||
}
|
||||
|
||||
hasLaunched = false;
|
||||
hasLanded = false;
|
||||
airTicks = 0;
|
||||
|
||||
// Reset fall distance to prevent fall damage during slam
|
||||
caster.fallDistance = 0;
|
||||
|
||||
// Calculate and apply leap velocity
|
||||
launchTowardTarget(caster);
|
||||
}
|
||||
|
||||
/**
|
||||
* Player slam: Launch straight up. Player controls horizontal movement via WASD.
|
||||
* AOE damage triggers wherever the player lands.
|
||||
*/
|
||||
private void executePlayerSlam(EntityLivingBase caster) {
|
||||
targetX = caster.posX;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ;
|
||||
|
||||
double vy = calculateLaunchVelocity(Math.max(1.0, leapHeight));
|
||||
caster.motionY = vy;
|
||||
hasLaunched = true;
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the initial vertical velocity needed to reach a target height
|
||||
* in Minecraft's physics (gravity=0.08, vertical drag=0.98).
|
||||
@@ -187,11 +210,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
caster.motionZ = 0;
|
||||
caster.motionY = vy;
|
||||
hasLaunched = true;
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(true);
|
||||
}
|
||||
caster.velocityChanged = true;
|
||||
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -233,10 +258,12 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
caster.motionY = vy;
|
||||
|
||||
hasLaunched = true;
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(true);
|
||||
}
|
||||
caster.velocityChanged = true;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
|
||||
// Face the target
|
||||
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
|
||||
@@ -252,7 +279,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
airTicks++;
|
||||
|
||||
// Continuously reset fall distance during slam to prevent fall damage
|
||||
caster.fallDistance = 0;
|
||||
if (!isPreview()) {
|
||||
caster.fallDistance = 0;
|
||||
}
|
||||
|
||||
// Check for landing
|
||||
if (caster.onGround && airTicks > 3) {
|
||||
@@ -267,12 +296,15 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
return;
|
||||
}
|
||||
|
||||
// While in air, face toward target
|
||||
double dx = targetX - caster.posX;
|
||||
double dz = targetZ - caster.posZ;
|
||||
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
|
||||
caster.rotationYaw = targetYaw;
|
||||
caster.rotationYawHead = targetYaw;
|
||||
// NPC: face toward target destination while in air
|
||||
// Player: free look — player controls their own camera
|
||||
if (!isPreview() && !isPlayerCaster(caster)) {
|
||||
double dx = targetX - caster.posX;
|
||||
double dz = targetZ - caster.posZ;
|
||||
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
|
||||
caster.rotationYaw = targetYaw;
|
||||
caster.rotationYawHead = targetYaw;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,22 +312,25 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
*/
|
||||
private void onLanding(EntityLivingBase caster, World world) {
|
||||
hasLanded = true;
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
|
||||
// Signal that the ability has completed its active phase
|
||||
signalCompletion();
|
||||
|
||||
// Reset fall distance to prevent fall damage on landing
|
||||
caster.fallDistance = 0;
|
||||
|
||||
// Stop horizontal momentum
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
caster.velocityChanged = true;
|
||||
|
||||
if (world.isRemote) return;
|
||||
if (!isPreview()) {
|
||||
// Reset fall distance to prevent fall damage on landing
|
||||
caster.fallDistance = 0;
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
if (isPreview()) return;
|
||||
|
||||
// Play slam impact sound on landing
|
||||
world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f);
|
||||
@@ -361,7 +396,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
hasLaunched = false;
|
||||
@@ -371,7 +406,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
hasLaunched = false;
|
||||
@@ -387,97 +422,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
airTicks = 0;
|
||||
}
|
||||
|
||||
// ==================== PREVIEW MODE ====================
|
||||
|
||||
private transient double previewVelX, previewVelY, previewVelZ;
|
||||
private transient boolean previewLaunched = false;
|
||||
private transient double previewGroundY;
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewExecute(EntityNPCInterface npc) {
|
||||
previewLaunched = false;
|
||||
|
||||
double tx, ty, tz;
|
||||
if (previewTarget != null && targetingMode == TargetingMode.AOE_TARGET) {
|
||||
tx = previewTarget.posX;
|
||||
ty = previewTarget.posY;
|
||||
tz = previewTarget.posZ;
|
||||
} else {
|
||||
tx = npc.posX;
|
||||
ty = npc.posY;
|
||||
tz = npc.posZ;
|
||||
}
|
||||
|
||||
previewGroundY = npc.posY;
|
||||
|
||||
double dx = tx - npc.posX;
|
||||
double dz = tz - npc.posZ;
|
||||
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
// Calculate vertical velocity for exact leapHeight
|
||||
double arcHeight = Math.max(1.0, leapHeight);
|
||||
double vy = calculateLaunchVelocity(arcHeight);
|
||||
|
||||
if (horizontalDist < 0.5) {
|
||||
previewVelX = 0;
|
||||
previewVelZ = 0;
|
||||
previewVelY = vy;
|
||||
} else {
|
||||
if (horizontalDist > maxRange) {
|
||||
double scale = maxRange / horizontalDist;
|
||||
dx *= scale;
|
||||
dz *= scale;
|
||||
horizontalDist = maxRange;
|
||||
}
|
||||
|
||||
double speedFactor = Math.max(0.1, leapSpeed);
|
||||
int flightTicks = (int) Math.max(10, Math.min(horizontalDist * 1.5 / speedFactor, 40));
|
||||
double drag = 0.91;
|
||||
double dragPowN = Math.pow(drag, flightTicks);
|
||||
double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN);
|
||||
|
||||
double dirX = dx / horizontalDist;
|
||||
double dirZ = dz / horizontalDist;
|
||||
previewVelX = dirX * vHorizontal;
|
||||
previewVelZ = dirZ * vHorizontal;
|
||||
previewVelY = vy;
|
||||
}
|
||||
previewLaunched = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void onPreviewActiveTick(EntityNPCInterface npc, int tick) {
|
||||
if (!previewLaunched) return;
|
||||
|
||||
// Apply velocity
|
||||
npc.prevPosX = npc.posX;
|
||||
npc.prevPosY = npc.posY;
|
||||
npc.prevPosZ = npc.posZ;
|
||||
|
||||
npc.posX += previewVelX;
|
||||
npc.posY += previewVelY;
|
||||
npc.posZ += previewVelZ;
|
||||
|
||||
// Gravity and drag
|
||||
previewVelY -= 0.08;
|
||||
previewVelY *= 0.98;
|
||||
previewVelX *= 0.91;
|
||||
previewVelZ *= 0.91;
|
||||
|
||||
// Ground clamp - stop falling below starting Y
|
||||
if (npc.posY < previewGroundY && previewVelY < 0) {
|
||||
npc.posY = previewGroundY;
|
||||
previewVelY = 0;
|
||||
previewVelX = 0;
|
||||
previewVelZ = 0;
|
||||
previewLaunched = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPreviewActiveDuration() {
|
||||
public int getMaxPreviewDuration() {
|
||||
return 60;
|
||||
}
|
||||
|
||||
@@ -519,11 +465,11 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, caster.rotationYaw);
|
||||
instance.setCasterEntityId(caster.getEntityId());
|
||||
|
||||
// AOE_TARGET: telegraph follows target during windup
|
||||
if (targetingMode == TargetingMode.AOE_TARGET && target != null) {
|
||||
// NPC AOE_TARGET: telegraph follows target during windup
|
||||
// Player: telegraph stays at caster position (no target to follow)
|
||||
if (!isPlayerCaster(caster) && targetingMode == TargetingMode.AOE_TARGET && target != null) {
|
||||
instance.setEntityIdToFollow(target.getEntityId());
|
||||
}
|
||||
// AOE_SELF: telegraph stays at caster position, no follow
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
@@ -61,6 +61,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
|
||||
this.showTelegraph = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean allowBurst() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
@@ -73,19 +78,26 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Spawn the entity that handles BOTH visuals AND damage
|
||||
// Spawn the sweeper entity that handles BOTH visuals AND damage.
|
||||
// NPC: target is the aggro target — lockOnTarget tracks it during sweep.
|
||||
// Player: target is null — sweep rotates around caster's facing direction, lockOnTarget has no effect.
|
||||
activeEntity = new EntityAbilitySweeper(world, caster, target,
|
||||
beamLength, beamWidth, beamHeight,
|
||||
colorData,
|
||||
sweepSpeed, numberOfRotations,
|
||||
damage, damageInterval, piercing,
|
||||
lockOnTarget);
|
||||
world.spawnEntityInWorld(activeEntity);
|
||||
|
||||
if (isPreview()) {
|
||||
activeEntity.setupPreview(caster);
|
||||
}
|
||||
|
||||
spawnAbilityEntity(world, activeEntity);
|
||||
|
||||
// Ability stays active until entity dies (prevents firing another while projectile is alive)
|
||||
// Movement locking is handled separately by the base class
|
||||
@@ -200,6 +212,11 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper {
|
||||
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
|
||||
public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; }
|
||||
|
||||
@Override
|
||||
public int getMaxPreviewDuration() {
|
||||
return (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
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.telegraph.TelegraphType;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -72,6 +73,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
this.typeId = "ability.cnpc.teleport";
|
||||
this.name = "Teleport";
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.allowedBy = UserType.NPC_ONLY;
|
||||
this.maxRange = 30.0f;
|
||||
this.minRange = 5.0f;
|
||||
this.cooldownTicks = 0;
|
||||
@@ -84,11 +86,6 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
this.activeSound = "mob.endermen.portal";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasDamage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
@@ -128,7 +125,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
}
|
||||
|
||||
private void performBlink(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
if (world.isRemote) return;
|
||||
if (world.isRemote && !isPreview()) return;
|
||||
|
||||
double oldX = caster.posX;
|
||||
double oldY = caster.posY;
|
||||
@@ -137,36 +134,47 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
Vec3 destination = calculateDestination(caster, target, world);
|
||||
if (destination == null) return;
|
||||
|
||||
boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight;
|
||||
if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ,
|
||||
destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) {
|
||||
destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ,
|
||||
if (!isPreview()) {
|
||||
boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight;
|
||||
if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ,
|
||||
destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) {
|
||||
destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ,
|
||||
destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
if (destination == null) return;
|
||||
}
|
||||
|
||||
destination = findSafeDestination(world, oldX, oldY, oldZ,
|
||||
destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
if (destination == null) return;
|
||||
|
||||
// Damage at origin
|
||||
if (damageAtStart) {
|
||||
dealDamageAt(caster, world, oldX, oldY, oldZ);
|
||||
}
|
||||
|
||||
// Spawn particles at origin
|
||||
spawnTeleportParticles(world, oldX, oldY, oldZ);
|
||||
}
|
||||
|
||||
destination = findSafeDestination(world, oldX, oldY, oldZ,
|
||||
destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
if (destination == null) return;
|
||||
|
||||
// Damage at origin
|
||||
if (damageAtStart) {
|
||||
dealDamageAt(caster, world, oldX, oldY, oldZ);
|
||||
}
|
||||
|
||||
// Spawn particles at origin
|
||||
spawnTeleportParticles(world, oldX, oldY, oldZ);
|
||||
|
||||
// Teleport
|
||||
caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
if (isPreview()) {
|
||||
caster.setPosition(destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
caster.prevPosX = destination.xCoord;
|
||||
caster.prevPosY = destination.yCoord;
|
||||
caster.prevPosZ = destination.zCoord;
|
||||
} else {
|
||||
caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
}
|
||||
caster.fallDistance = 0;
|
||||
|
||||
// Spawn particles at destination
|
||||
spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
if (!isPreview()) {
|
||||
// Spawn particles at destination
|
||||
spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
|
||||
// Damage at destination
|
||||
if (damageAtEnd) {
|
||||
dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
// Damage at destination
|
||||
if (damageAtEnd) {
|
||||
dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord);
|
||||
}
|
||||
}
|
||||
|
||||
// Face target after teleport
|
||||
@@ -217,26 +225,8 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
}
|
||||
}
|
||||
|
||||
// No target (player use) - teleport in caster's look direction
|
||||
Vec3 look = caster.getLookVec();
|
||||
switch (mode) {
|
||||
case BEHIND:
|
||||
// Teleport forward in look direction by behindDistance
|
||||
newX = caster.posX + look.xCoord * behindDistance;
|
||||
newZ = caster.posZ + look.zCoord * behindDistance;
|
||||
newY = caster.posY;
|
||||
return Vec3.createVectorHelper(newX, newY, newZ);
|
||||
case SINGLE:
|
||||
case BLINK:
|
||||
default:
|
||||
// Teleport forward in look direction within blinkRadius
|
||||
double dist = Math.min(blinkRadius, maxRange);
|
||||
double offset = (RANDOM.nextDouble() * 0.5 + 0.5) * dist; // 50-100% of max distance
|
||||
newX = caster.posX + look.xCoord * offset;
|
||||
newZ = caster.posZ + look.zCoord * offset;
|
||||
newY = caster.posY;
|
||||
return Vec3.createVectorHelper(newX, newY, newZ);
|
||||
}
|
||||
// NPC-only ability — should not reach here
|
||||
return null;
|
||||
}
|
||||
|
||||
private Vec3 findSafeDestination(World world, double oldX, double oldY, double oldZ,
|
||||
@@ -446,15 +436,6 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
} catch (Exception e) {
|
||||
this.mode = TeleportMode.BLINK;
|
||||
}
|
||||
} else if (nbt.hasKey("pattern")) {
|
||||
String legacy = nbt.getString("pattern");
|
||||
if ("BEHIND_TARGET".equals(legacy)) {
|
||||
this.mode = TeleportMode.BEHIND;
|
||||
} else if ("RANDOM".equals(legacy)) {
|
||||
this.mode = TeleportMode.BLINK;
|
||||
} else {
|
||||
this.mode = TeleportMode.SINGLE;
|
||||
}
|
||||
} else {
|
||||
this.mode = TeleportMode.BLINK;
|
||||
}
|
||||
@@ -561,6 +542,12 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport {
|
||||
this.damageRadius = damageRadius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxPreviewDuration() {
|
||||
int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1;
|
||||
return blinkLimit * blinkDelayTicks + 10;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
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.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import noppes.npcs.api.ability.type.IAbilityTrap;
|
||||
|
||||
@@ -20,38 +15,18 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Trap ability: Places a proximity-triggered trap.
|
||||
* Features arm time, trigger radius, and various effects on trigger.
|
||||
* Trap ability: Places proximity-triggered traps around the caster.
|
||||
* Spawns one or more EntityAbilityZone entities in random positions
|
||||
* within spawnRadius of the caster. Damage and trigger logic handled by the entity.
|
||||
*/
|
||||
public class AbilityTrap extends Ability implements IAbilityTrap {
|
||||
public class AbilityTrap extends AbilityZone implements IAbilityTrap {
|
||||
|
||||
public enum TrapPlacement {
|
||||
AT_CASTER,
|
||||
AT_TARGET,
|
||||
AHEAD_OF_CASTER;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case AT_CASTER: return "ability.trapPlace.atCaster";
|
||||
case AT_TARGET: return "ability.trapPlace.atTarget";
|
||||
case AHEAD_OF_CASTER: return "ability.trapPlace.aheadOfCaster";
|
||||
default: return name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int durationTicks = 200;
|
||||
private TrapPlacement placement = TrapPlacement.AT_TARGET;
|
||||
private float placementDistance = 5.0f;
|
||||
// Trap-specific fields
|
||||
private float triggerRadius = 2.0f;
|
||||
private int armTime = 20;
|
||||
private int maxTriggers = 1;
|
||||
@@ -61,235 +36,76 @@ public class AbilityTrap extends Ability implements IAbilityTrap {
|
||||
private float knockback = 0.5f;
|
||||
private boolean visible = true;
|
||||
|
||||
// Offset parameters - trap spawns near target, not exactly on them
|
||||
private float minOffset = 0.0f;
|
||||
private float maxOffset = 1.5f;
|
||||
private boolean randomOffset = true;
|
||||
|
||||
// Runtime state
|
||||
private static final Random RANDOM = new Random();
|
||||
private transient double trapX, trapY, trapZ;
|
||||
private transient boolean armed = false;
|
||||
private transient int triggerCount = 0;
|
||||
private transient int ticksSinceLastTrigger = 0;
|
||||
private transient Set<UUID> triggeredEntities = new HashSet<>();
|
||||
|
||||
public AbilityTrap() {
|
||||
super(300, new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.0f, 0.5f, 1.5f));
|
||||
this.typeId = "ability.cnpc.trap";
|
||||
this.name = "Trap";
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 15.0f;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.cooldownTicks = 0;
|
||||
this.windUpTicks = 20;
|
||||
this.telegraphType = TelegraphType.CIRCLE;
|
||||
this.windUpTicks = 30;
|
||||
this.windUpAnimationName = "Ability_Zone_Windup";
|
||||
this.activeAnimationName = "Ability_Zone_Active";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetingMode[] getAllowedTargetingModes() {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getTelegraphRadius() {
|
||||
public float getZoneRadius() {
|
||||
return triggerRadius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) {
|
||||
TelegraphInstance instance = super.createTelegraph(caster, target);
|
||||
if (instance == null) return null;
|
||||
|
||||
// Control telegraph following based on placement mode
|
||||
switch (placement) {
|
||||
case AT_CASTER:
|
||||
case AHEAD_OF_CASTER:
|
||||
// Telegraph at caster or ahead, no following
|
||||
instance.setEntityIdToFollow(-1);
|
||||
if (placement == TrapPlacement.AHEAD_OF_CASTER) {
|
||||
double yaw = Math.toRadians(caster.rotationYaw);
|
||||
instance.setX(caster.posX - Math.sin(yaw) * placementDistance);
|
||||
instance.setY(caster.posY);
|
||||
instance.setZ(caster.posZ + Math.cos(yaw) * placementDistance);
|
||||
} else {
|
||||
instance.setX(caster.posX);
|
||||
instance.setY(caster.posY);
|
||||
instance.setZ(caster.posZ);
|
||||
}
|
||||
break;
|
||||
case AT_TARGET:
|
||||
// Telegraph follows target during windup, locks on execute
|
||||
if (target != null) {
|
||||
instance.setEntityIdToFollow(target.getEntityId());
|
||||
}
|
||||
break;
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) {
|
||||
armed = false;
|
||||
triggerCount = 0;
|
||||
ticksSinceLastTrigger = armTime;
|
||||
triggeredEntities.clear();
|
||||
|
||||
// Use telegraph position if available (for AT_TARGET, it was following target)
|
||||
TelegraphInstance telegraph = getTelegraphInstance();
|
||||
|
||||
switch (placement) {
|
||||
case AT_CASTER:
|
||||
trapX = caster.posX;
|
||||
trapY = caster.posY;
|
||||
trapZ = caster.posZ;
|
||||
break;
|
||||
case AT_TARGET:
|
||||
// Use telegraph position with offset
|
||||
if (telegraph != null) {
|
||||
double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(),
|
||||
minOffset, maxOffset, randomOffset, RANDOM);
|
||||
trapX = pos[0];
|
||||
trapY = pos[1];
|
||||
trapZ = pos[2];
|
||||
// Update telegraph to show actual trap position
|
||||
telegraph.setX(trapX);
|
||||
telegraph.setY(trapY);
|
||||
telegraph.setZ(trapZ);
|
||||
} else if (target != null) {
|
||||
double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ,
|
||||
minOffset, maxOffset, randomOffset, RANDOM);
|
||||
trapX = pos[0];
|
||||
trapY = pos[1];
|
||||
trapZ = pos[2];
|
||||
} else {
|
||||
trapX = caster.posX;
|
||||
trapY = caster.posY;
|
||||
trapZ = caster.posZ;
|
||||
}
|
||||
break;
|
||||
case AHEAD_OF_CASTER:
|
||||
double yaw = Math.toRadians(caster.rotationYaw);
|
||||
trapX = caster.posX - Math.sin(yaw) * placementDistance;
|
||||
trapY = caster.posY;
|
||||
trapZ = caster.posZ + Math.cos(yaw) * placementDistance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// Check if trap duration has ended
|
||||
if (tick >= durationTicks) {
|
||||
if (world.isRemote && !isPreview()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!armed) {
|
||||
if (tick >= armTime) {
|
||||
armed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
activeEntities.clear();
|
||||
|
||||
ticksSinceLastTrigger++;
|
||||
|
||||
if (maxTriggers > 0 && triggerCount >= maxTriggers) {
|
||||
signalCompletion(); // All triggers used, trap is done
|
||||
return;
|
||||
}
|
||||
|
||||
if (ticksSinceLastTrigger < triggerCooldown) {
|
||||
return;
|
||||
}
|
||||
|
||||
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
|
||||
trapX - triggerRadius, trapY - 1, trapZ - triggerRadius,
|
||||
trapX + triggerRadius, trapY + 2, trapZ + triggerRadius
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue;
|
||||
|
||||
double dx = entity.posX - trapX;
|
||||
double dz = entity.posZ - trapZ;
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (dist <= triggerRadius) {
|
||||
triggerTrap(caster, entity, world);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void triggerTrap(EntityLivingBase caster, EntityLivingBase triggerer, World world) {
|
||||
triggerCount++;
|
||||
ticksSinceLastTrigger = 0;
|
||||
triggeredEntities.add(triggerer.getUniqueID());
|
||||
|
||||
Set<EntityLivingBase> affected = new HashSet<>();
|
||||
|
||||
if (damageRadius > 0) {
|
||||
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
|
||||
trapX - damageRadius, trapY - 1, trapZ - damageRadius,
|
||||
trapX + damageRadius, trapY + 3, trapZ + damageRadius
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
double dx = entity.posX - trapX;
|
||||
double dz = entity.posZ - trapZ;
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist <= damageRadius) {
|
||||
affected.add(entity);
|
||||
}
|
||||
}
|
||||
// Use pre-calculated positions from telegraph phase, or generate new ones
|
||||
List<double[]> positions;
|
||||
if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) {
|
||||
positions = new ArrayList<>(preCalculatedPositions);
|
||||
preCalculatedPositions.clear();
|
||||
} else {
|
||||
affected.add(triggerer);
|
||||
}
|
||||
|
||||
for (EntityLivingBase entity : affected) {
|
||||
// Apply damage with scripted event support
|
||||
boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback);
|
||||
|
||||
// Apply effects if the hit wasn't cancelled
|
||||
if (wasHit) {
|
||||
applyEffects(entity);
|
||||
positions = new ArrayList<>();
|
||||
List<double[]> placedPositions = new ArrayList<>();
|
||||
float minSeparation = triggerRadius * 2.0f;
|
||||
for (int i = 0; i < zoneCount; i++) {
|
||||
double[] pos = findSpawnPosition(caster, placedPositions, minSeparation);
|
||||
placedPositions.add(pos);
|
||||
positions.add(pos);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < zoneCount; i++) {
|
||||
double[] pos = positions.get(i);
|
||||
|
||||
EntityAbilityZone entity = EntityAbilityZone.createTrap(world, caster,
|
||||
pos[0], caster.posY, pos[1],
|
||||
zoneShape,
|
||||
triggerRadius, armTime, maxTriggers, triggerCooldown,
|
||||
damage, damageRadius, knockback, durationTicks,
|
||||
colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled,
|
||||
zoneHeight,
|
||||
particleDensity, particleScale, animSpeed, lightningDensity,
|
||||
getEffects());
|
||||
|
||||
applyVisualToEntity(entity);
|
||||
|
||||
if (isPreview()) {
|
||||
entity.setupPreview(caster);
|
||||
}
|
||||
|
||||
spawnAbilityEntity(world, entity);
|
||||
activeEntities.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
armed = false;
|
||||
triggerCount = 0;
|
||||
triggeredEntities.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
armed = false;
|
||||
triggerCount = 0;
|
||||
triggeredEntities.clear();
|
||||
}
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// NBT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
nbt.setInteger("durationTicks", durationTicks);
|
||||
nbt.setString("placement", placement.name());
|
||||
nbt.setFloat("placementDistance", placementDistance);
|
||||
writeZoneNBT(nbt);
|
||||
nbt.setFloat("triggerRadius", triggerRadius);
|
||||
nbt.setInteger("armTime", armTime);
|
||||
nbt.setInteger("maxTriggers", maxTriggers);
|
||||
@@ -298,20 +114,11 @@ public class AbilityTrap extends Ability implements IAbilityTrap {
|
||||
nbt.setFloat("damageRadius", damageRadius);
|
||||
nbt.setFloat("knockback", knockback);
|
||||
nbt.setBoolean("visible", visible);
|
||||
nbt.setFloat("minOffset", minOffset);
|
||||
nbt.setFloat("maxOffset", maxOffset);
|
||||
nbt.setBoolean("randomOffset", randomOffset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readTypeNBT(NBTTagCompound nbt) {
|
||||
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 200;
|
||||
try {
|
||||
this.placement = TrapPlacement.valueOf(nbt.getString("placement"));
|
||||
} catch (Exception e) {
|
||||
this.placement = TrapPlacement.AT_TARGET;
|
||||
}
|
||||
this.placementDistance = nbt.hasKey("placementDistance") ? nbt.getFloat("placementDistance") : 5.0f;
|
||||
readZoneNBT(nbt, 300);
|
||||
this.triggerRadius = nbt.hasKey("triggerRadius") ? nbt.getFloat("triggerRadius") : 2.0f;
|
||||
this.armTime = nbt.hasKey("armTime") ? nbt.getInteger("armTime") : 20;
|
||||
this.maxTriggers = nbt.hasKey("maxTriggers") ? nbt.getInteger("maxTriggers") : 1;
|
||||
@@ -320,168 +127,60 @@ public class AbilityTrap extends Ability implements IAbilityTrap {
|
||||
this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 0.0f;
|
||||
this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f;
|
||||
this.visible = !nbt.hasKey("visible") || nbt.getBoolean("visible");
|
||||
this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f;
|
||||
this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 1.5f;
|
||||
this.randomOffset = !nbt.hasKey("randomOffset") || nbt.getBoolean("randomOffset");
|
||||
}
|
||||
|
||||
// Getters & Setters
|
||||
public int getDurationTicks() {
|
||||
return durationTicks;
|
||||
}
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// TRAP-SPECIFIC GETTERS & SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public void setDurationTicks(int durationTicks) {
|
||||
this.durationTicks = Math.max(1, durationTicks);
|
||||
}
|
||||
public float getTriggerRadius() { return triggerRadius; }
|
||||
public void setTriggerRadius(float triggerRadius) { this.triggerRadius = triggerRadius; }
|
||||
public int getArmTime() { return armTime; }
|
||||
public void setArmTime(int armTime) { this.armTime = armTime; }
|
||||
public int getMaxTriggers() { return maxTriggers; }
|
||||
public void setMaxTriggers(int maxTriggers) { this.maxTriggers = maxTriggers; }
|
||||
public int getTriggerCooldown() { return triggerCooldown; }
|
||||
public void setTriggerCooldown(int triggerCooldown) { this.triggerCooldown = triggerCooldown; }
|
||||
public float getDamage() { return damage; }
|
||||
public void setDamage(float damage) { this.damage = damage; }
|
||||
public float getDamageRadius() { return damageRadius; }
|
||||
public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; }
|
||||
public float getKnockback() { return knockback; }
|
||||
public void setKnockback(float knockback) { this.knockback = knockback; }
|
||||
public boolean isVisible() { return visible; }
|
||||
public void setVisible(boolean visible) { this.visible = visible; }
|
||||
|
||||
public TrapPlacement getPlacementEnum() {
|
||||
return placement;
|
||||
}
|
||||
|
||||
public void setPlacementEnum(TrapPlacement placement) {
|
||||
this.placement = placement;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPlacement() {
|
||||
return placement.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlacement(int placement) {
|
||||
TrapPlacement[] values = TrapPlacement.values();
|
||||
this.placement = placement >= 0 && placement < values.length ? values[placement] : TrapPlacement.AT_TARGET;
|
||||
}
|
||||
|
||||
public float getPlacementDistance() {
|
||||
return placementDistance;
|
||||
}
|
||||
|
||||
public void setPlacementDistance(float placementDistance) {
|
||||
this.placementDistance = placementDistance;
|
||||
}
|
||||
|
||||
public float getTriggerRadius() {
|
||||
return triggerRadius;
|
||||
}
|
||||
|
||||
public void setTriggerRadius(float triggerRadius) {
|
||||
this.triggerRadius = triggerRadius;
|
||||
}
|
||||
|
||||
public int getArmTime() {
|
||||
return armTime;
|
||||
}
|
||||
|
||||
public void setArmTime(int armTime) {
|
||||
this.armTime = armTime;
|
||||
}
|
||||
|
||||
public int getMaxTriggers() {
|
||||
return maxTriggers;
|
||||
}
|
||||
|
||||
public void setMaxTriggers(int maxTriggers) {
|
||||
this.maxTriggers = maxTriggers;
|
||||
}
|
||||
|
||||
public int getTriggerCooldown() {
|
||||
return triggerCooldown;
|
||||
}
|
||||
|
||||
public void setTriggerCooldown(int triggerCooldown) {
|
||||
this.triggerCooldown = triggerCooldown;
|
||||
}
|
||||
|
||||
public float getDamage() {
|
||||
return damage;
|
||||
}
|
||||
|
||||
public void setDamage(float damage) {
|
||||
this.damage = damage;
|
||||
}
|
||||
|
||||
public float getDamageRadius() {
|
||||
return damageRadius;
|
||||
}
|
||||
|
||||
public void setDamageRadius(float damageRadius) {
|
||||
this.damageRadius = damageRadius;
|
||||
}
|
||||
|
||||
public float getKnockback() {
|
||||
return knockback;
|
||||
}
|
||||
|
||||
public void setKnockback(float knockback) {
|
||||
this.knockback = knockback;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
public void setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
public float getMinOffset() {
|
||||
return minOffset;
|
||||
}
|
||||
|
||||
public void setMinOffset(float minOffset) {
|
||||
this.minOffset = minOffset;
|
||||
}
|
||||
|
||||
public float getMaxOffset() {
|
||||
return maxOffset;
|
||||
}
|
||||
|
||||
public void setMaxOffset(float maxOffset) {
|
||||
this.maxOffset = maxOffset;
|
||||
}
|
||||
|
||||
public boolean isRandomOffset() {
|
||||
return randomOffset;
|
||||
}
|
||||
|
||||
public void setRandomOffset(boolean randomOffset) {
|
||||
this.randomOffset = randomOffset;
|
||||
}
|
||||
|
||||
// Runtime getters
|
||||
public double getTrapX() {
|
||||
return trapX;
|
||||
}
|
||||
|
||||
public double getTrapY() {
|
||||
return trapY;
|
||||
}
|
||||
|
||||
public double getTrapZ() {
|
||||
return trapZ;
|
||||
}
|
||||
|
||||
public boolean isArmed() {
|
||||
return armed;
|
||||
}
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GUI FIELD DEFINITIONS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
addPresetFieldDef(defs);
|
||||
|
||||
defs.addAll(Arrays.asList(
|
||||
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks)
|
||||
.range(1, 2000),
|
||||
FieldDef.enumField("ability.placement", TrapPlacement.class, this::getPlacementEnum, this::setPlacementEnum)
|
||||
.hover("ability.hover.placement"),
|
||||
FieldDef.row(
|
||||
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000),
|
||||
FieldDef.enumField("ability.zoneShape", ZoneShape.class, this::getZoneShapeEnum, this::setZoneShapeEnum)
|
||||
),
|
||||
FieldDef.section("ability.section.zone"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.radius", this::getSpawnRadius, this::setSpawnRadius),
|
||||
FieldDef.intField("gui.count", this::getZoneCount, this::setZoneCount).range(1, 20)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.height", this::getZoneHeight, this::setZoneHeight),
|
||||
FieldDef.boolField("ability.visible", this::isVisible, this::setVisible)
|
||||
),
|
||||
FieldDef.section("ability.section.trigger"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.radius", this::getTriggerRadius, this::setTriggerRadius),
|
||||
FieldDef.intField("ability.armTime", this::getArmTime, this::setArmTime)
|
||||
),
|
||||
FieldDef.row(
|
||||
FieldDef.intField("ability.maxTriggers", this::getMaxTriggers, this::setMaxTriggers),
|
||||
FieldDef.intField("ability.triggerCooldown", this::getTriggerCooldown, this::setTriggerCooldown)
|
||||
FieldDef.intField("gui.max", this::getMaxTriggers, this::setMaxTriggers),
|
||||
FieldDef.intField("gui.cooldown", this::getTriggerCooldown, this::setTriggerCooldown)
|
||||
),
|
||||
FieldDef.section("ability.section.damage"),
|
||||
FieldDef.row(
|
||||
@@ -491,5 +190,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap {
|
||||
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
|
||||
));
|
||||
|
||||
addVisualFieldDefs(defs);
|
||||
}
|
||||
}
|
||||
@@ -84,29 +84,32 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
pullComplete = false;
|
||||
ticksSincePullDamage = 0;
|
||||
|
||||
if (aoe) {
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
if (!isPreview()) {
|
||||
if (aoe) {
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
int count = 0;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
int count = 0;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(entity.getUniqueID());
|
||||
count++;
|
||||
if (count >= maxTargets) break;
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(entity.getUniqueID());
|
||||
count++;
|
||||
if (count >= maxTargets) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single target mode - still check pullRadius
|
||||
if (target != null && !target.isDead) {
|
||||
double dist = caster.getDistanceToEntity(target);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(target.getUniqueID());
|
||||
} else {
|
||||
// NPC single-target mode: pull the aggro target
|
||||
// Player: single-target mode has no effect (no target to pull — use AOE mode instead)
|
||||
if (!isPlayerCaster(caster) && target != null && !target.isDead) {
|
||||
double dist = caster.getDistanceToEntity(target);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(target.getUniqueID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,6 +117,12 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
if (isPreview()) {
|
||||
// No entities to pull in preview, just run animation for a duration
|
||||
if (tick >= 60) signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (pullComplete || getPulledEntities().isEmpty()) {
|
||||
signalCompletion();
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,549 @@
|
||||
package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
|
||||
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.telegraph.Telegraph;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.AccentStyle;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.ParticleMotion;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
import noppes.npcs.client.gui.SubGuiZonePresetSelector;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
/**
|
||||
* Abstract base class for zone-based abilities (Trap, Hazard).
|
||||
* Extracts shared fields: duration, shape, spawn, visual, lifecycle, and FieldDefs.
|
||||
*/
|
||||
public abstract class AbilityZone extends Ability {
|
||||
|
||||
// Zone properties
|
||||
protected int durationTicks;
|
||||
protected ZoneShape zoneShape = ZoneShape.CIRCLE;
|
||||
protected float spawnRadius = 10.0f;
|
||||
protected int zoneCount = 3;
|
||||
protected float zoneHeight = 2.0f;
|
||||
|
||||
// Visual parameters
|
||||
protected float particleDensity = 1.0f;
|
||||
protected float particleScale = 1.0f;
|
||||
protected float animSpeed = 1.0f;
|
||||
protected float lightningDensity = 1.0f;
|
||||
protected EnergyDisplayData colorData;
|
||||
|
||||
// Visual layer fields (always active)
|
||||
protected boolean groundFill = true;
|
||||
protected float groundAlpha = 0.25f;
|
||||
protected boolean rings = true;
|
||||
protected int ringCount = 3;
|
||||
protected boolean border = true;
|
||||
protected float borderSpeed = 1.0f;
|
||||
protected boolean accents = true;
|
||||
protected int accentStyle = 0;
|
||||
protected boolean lightning = false;
|
||||
protected boolean particles = true;
|
||||
protected int particleMotion = 0;
|
||||
protected String particleDir = "";
|
||||
protected int particleSize = 32;
|
||||
protected boolean particleGlow = true;
|
||||
|
||||
// Runtime state
|
||||
protected static final Random RANDOM = new Random();
|
||||
protected transient List<EntityAbilityZone> activeEntities = new ArrayList<>();
|
||||
protected transient List<double[]> preCalculatedPositions = new ArrayList<>();
|
||||
|
||||
protected AbilityZone(int defaultDuration, EnergyDisplayData defaultColors) {
|
||||
this.durationTicks = defaultDuration;
|
||||
this.colorData = defaultColors;
|
||||
this.targetingMode = TargetingMode.AGGRO_TARGET;
|
||||
this.maxRange = 20.0f;
|
||||
this.lockMovement = LockMovementType.WINDUP;
|
||||
this.cooldownTicks = 0;
|
||||
this.allowedBy = UserType.NPC_ONLY;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// SHARED OVERRIDES
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
public boolean allowBurst() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTargetingModeLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TargetingMode[] getAllowedTargetingModes() {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual zone effect radius for telegraph sizing.
|
||||
* AbilityHazard returns its damage radius, AbilityTrap returns triggerRadius.
|
||||
*/
|
||||
public abstract float getZoneRadius();
|
||||
|
||||
@Override
|
||||
public List<TelegraphInstance> createTelegraphs(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!showTelegraph || windUpTicks <= 0) return new ArrayList<>();
|
||||
|
||||
// Pre-calculate zone positions
|
||||
preCalculatedPositions.clear();
|
||||
List<double[]> placedPositions = new ArrayList<>();
|
||||
float minSeparation = getZoneRadius() * 2.0f;
|
||||
|
||||
for (int i = 0; i < zoneCount; i++) {
|
||||
double[] pos = findSpawnPosition(caster, placedPositions, minSeparation);
|
||||
placedPositions.add(pos);
|
||||
preCalculatedPositions.add(pos);
|
||||
}
|
||||
|
||||
// Create a telegraph for each zone position
|
||||
List<TelegraphInstance> telegraphs = new ArrayList<>();
|
||||
float zoneRadius = getZoneRadius();
|
||||
|
||||
for (double[] pos : preCalculatedPositions) {
|
||||
Telegraph telegraph;
|
||||
if (zoneShape == ZoneShape.SQUARE) {
|
||||
telegraph = Telegraph.square(zoneRadius);
|
||||
} else {
|
||||
telegraph = Telegraph.circle(zoneRadius);
|
||||
}
|
||||
|
||||
telegraph.setDurationTicks(windUpTicks);
|
||||
telegraph.setColor(windUpColor);
|
||||
telegraph.setWarningColor(activeColor);
|
||||
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
|
||||
telegraph.setHeightOffset(telegraphHeightOffset);
|
||||
|
||||
double groundY = findGroundLevel(caster.worldObj, pos[0], caster.posY, pos[1]);
|
||||
TelegraphInstance instance = new TelegraphInstance(telegraph, pos[0], groundY, pos[1], 0);
|
||||
instance.setCasterEntityId(caster.getEntityId());
|
||||
instance.setEntityIdToFollow(-1);
|
||||
telegraphs.add(instance);
|
||||
}
|
||||
|
||||
return telegraphs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) {
|
||||
// Remove dead entities
|
||||
Iterator<EntityAbilityZone> it = activeEntities.iterator();
|
||||
while (it.hasNext()) {
|
||||
EntityAbilityZone entity = it.next();
|
||||
if (entity == null || entity.isDead) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
|
||||
if (activeEntities.isEmpty()) {
|
||||
signalCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
for (EntityAbilityZone entity : activeEntities) {
|
||||
if (entity != null && !entity.isDead) {
|
||||
killAbilityEntity(entity);
|
||||
}
|
||||
}
|
||||
activeEntities.clear();
|
||||
preCalculatedPositions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxPreviewDuration() {
|
||||
return durationTicks + 10;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// SHARED SPAWN HELPER
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Find a non-overlapping spawn position within spawnRadius of the caster.
|
||||
*/
|
||||
protected double[] findSpawnPosition(EntityLivingBase caster, List<double[]> placedPositions, float minSeparation) {
|
||||
double spawnX = caster.posX;
|
||||
double spawnZ = caster.posZ;
|
||||
|
||||
for (int attempt = 0; attempt < 15; attempt++) {
|
||||
double angle = RANDOM.nextDouble() * Math.PI * 2;
|
||||
double dist = Math.sqrt(RANDOM.nextDouble()) * spawnRadius;
|
||||
double candidateX = caster.posX + Math.cos(angle) * dist;
|
||||
double candidateZ = caster.posZ + Math.sin(angle) * dist;
|
||||
|
||||
boolean overlaps = false;
|
||||
for (double[] placed : placedPositions) {
|
||||
double dx = candidateX - placed[0];
|
||||
double dz = candidateZ - placed[1];
|
||||
if (Math.sqrt(dx * dx + dz * dz) < minSeparation) {
|
||||
overlaps = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
spawnX = candidateX;
|
||||
spawnZ = candidateZ;
|
||||
if (!overlaps) break;
|
||||
}
|
||||
|
||||
return new double[]{spawnX, spawnZ};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply visual settings to a zone entity.
|
||||
*/
|
||||
protected void applyVisualToEntity(EntityAbilityZone entity) {
|
||||
entity.applyVisual(groundFill, groundAlpha,
|
||||
rings, ringCount, border, borderSpeed,
|
||||
accents, accentStyle, lightning,
|
||||
particles, particleMotion, particleDir,
|
||||
particleSize, particleGlow);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// PRESET SUPPORT
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Apply preset defaults by style name.
|
||||
*/
|
||||
private void applyPresetDefaults(String styleName) {
|
||||
effects.clear();
|
||||
particleDir = "";
|
||||
switch (styleName) {
|
||||
case "TOXIC":
|
||||
groundFill = true; groundAlpha = 0.30f; rings = true; ringCount = 3;
|
||||
border = false; borderSpeed = 1.0f; accents = true; accentStyle = 1;
|
||||
lightning = false; particles = true; particleDensity = 1.5f; particleScale = 0.8f;
|
||||
particleMotion = 1; particleGlow = true; particleDir = "mc:mobSpell";
|
||||
colorData.innerColor = 0x44DD44; colorData.outerColor = 0x116611;
|
||||
windUpColor = 0x6044DD44; activeColor = 0xC044FF44;
|
||||
effects.add(new AbilityEffect(AbilityEffect.EffectType.POISON, 100, 0));
|
||||
break;
|
||||
case "INFERNO":
|
||||
groundFill = true; groundAlpha = 0.35f; rings = true; ringCount = 3;
|
||||
border = true; borderSpeed = 2.0f; accents = true; accentStyle = 2;
|
||||
lightning = false; particles = true; particleDensity = 2.0f; particleScale = 1.2f;
|
||||
particleMotion = 0; particleGlow = true; particleDir = "mc:flame";
|
||||
colorData.innerColor = 0xFF6611; colorData.outerColor = 0xCC2200;
|
||||
windUpColor = 0x60FF6611; activeColor = 0xC0FF4400;
|
||||
effects.add(new AbilityEffect(AbilityEffect.EffectType.BURN, 60, 0));
|
||||
break;
|
||||
case "ARCANE":
|
||||
groundFill = true; groundAlpha = 0.20f; rings = true; ringCount = 1;
|
||||
border = true; borderSpeed = 0.8f; accents = true; accentStyle = 0;
|
||||
lightning = false; particles = true; particleDensity = 1.0f; particleScale = 1.0f;
|
||||
particleMotion = 1; particleGlow = true; particleDir = "mc:portal";
|
||||
colorData.innerColor = 0xAA44FF; colorData.outerColor = 0x6622BB;
|
||||
windUpColor = 0x60AA44FF; activeColor = 0xC0CC66FF;
|
||||
effects.add(new AbilityEffect(AbilityEffect.EffectType.WEAKNESS, 100, 0));
|
||||
break;
|
||||
case "ELECTRIC":
|
||||
groundFill = true; groundAlpha = 0.15f; rings = false; ringCount = 1;
|
||||
border = false; borderSpeed = 1.0f; accents = false; accentStyle = 0;
|
||||
lightning = true; particles = true; particleDensity = 0.8f; particleScale = 0.6f;
|
||||
particleMotion = 2; particleGlow = true; particleDir = "mc:enchantmenttable";
|
||||
colorData.innerColor = 0x4488FF; colorData.outerColor = 0x2244BB;
|
||||
windUpColor = 0x604488FF; activeColor = 0xC066AAFF;
|
||||
effects.add(new AbilityEffect(AbilityEffect.EffectType.MINING_FATIGUE, 80, 1));
|
||||
break;
|
||||
case "FROST":
|
||||
groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3;
|
||||
border = true; borderSpeed = 0.3f; accents = true; accentStyle = 0;
|
||||
lightning = false; particles = true; particleDensity = 1.5f; particleScale = 1.0f;
|
||||
particleMotion = 1; particleGlow = true; particleDir = "mc:snowshovel";
|
||||
colorData.innerColor = 0x88CCFF; colorData.outerColor = 0x4488CC;
|
||||
windUpColor = 0x6088CCFF; activeColor = 0xC0AADDFF;
|
||||
effects.add(new AbilityEffect(AbilityEffect.EffectType.SLOWNESS, 100, 1));
|
||||
break;
|
||||
case "DEFAULT":
|
||||
groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3;
|
||||
border = true; borderSpeed = 1.0f; accents = true; accentStyle = 0;
|
||||
lightning = false; particles = false; particleDensity = 1.0f; particleScale = 1.0f;
|
||||
particleMotion = 0; particleGlow = true;
|
||||
colorData.innerColor = 0xCCCCCC; colorData.outerColor = 0x666666;
|
||||
windUpColor = 0x60CCCCCC; activeColor = 0xC0FFFFFF;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-only preset enum for the GUI dropdown.
|
||||
* Not persisted — presets simply set field values.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public enum ZonePreset {
|
||||
DEFAULT, TOXIC, INFERNO, ARCANE, ELECTRIC, FROST;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ability.preset." + name().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// SHARED NBT
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
protected void writeZoneNBT(NBTTagCompound nbt) {
|
||||
nbt.setInteger("durationTicks", durationTicks);
|
||||
nbt.setString("zoneShape", zoneShape.name());
|
||||
nbt.setFloat("spawnRadius", spawnRadius);
|
||||
nbt.setInteger("zoneCount", zoneCount);
|
||||
nbt.setFloat("zoneHeight", zoneHeight);
|
||||
nbt.setFloat("particleDensity", particleDensity);
|
||||
nbt.setFloat("particleScale", particleScale);
|
||||
nbt.setFloat("animSpeed", animSpeed);
|
||||
nbt.setFloat("lightningDensity", lightningDensity);
|
||||
colorData.writeNBT(nbt);
|
||||
|
||||
// Visual layer fields — always written as top-level keys
|
||||
nbt.setBoolean("groundFill", groundFill);
|
||||
nbt.setFloat("groundAlpha", groundAlpha);
|
||||
nbt.setBoolean("rings", rings);
|
||||
nbt.setInteger("ringCount", ringCount);
|
||||
nbt.setBoolean("border", border);
|
||||
nbt.setFloat("borderSpeed", borderSpeed);
|
||||
nbt.setBoolean("accents", accents);
|
||||
nbt.setInteger("accentStyle", accentStyle);
|
||||
nbt.setBoolean("lightning", lightning);
|
||||
nbt.setBoolean("particles", particles);
|
||||
nbt.setInteger("particleMotion", particleMotion);
|
||||
nbt.setString("particleDir", particleDir);
|
||||
nbt.setInteger("particleSize", particleSize);
|
||||
nbt.setBoolean("particleGlow", particleGlow);
|
||||
}
|
||||
|
||||
protected void readZoneNBT(NBTTagCompound nbt, int defaultDuration) {
|
||||
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : defaultDuration;
|
||||
if (nbt.hasKey("zoneShape")) {
|
||||
try { this.zoneShape = ZoneShape.valueOf(nbt.getString("zoneShape")); }
|
||||
catch (Exception e) { this.zoneShape = ZoneShape.CIRCLE; }
|
||||
} else {
|
||||
this.zoneShape = ZoneShape.CIRCLE;
|
||||
}
|
||||
this.spawnRadius = nbt.hasKey("spawnRadius") ? nbt.getFloat("spawnRadius") : 10.0f;
|
||||
this.zoneCount = nbt.hasKey("zoneCount") ? nbt.getInteger("zoneCount") : 3;
|
||||
this.zoneHeight = nbt.hasKey("zoneHeight") ? nbt.getFloat("zoneHeight") : 2.0f;
|
||||
this.particleDensity = nbt.hasKey("particleDensity") ? nbt.getFloat("particleDensity") : 1.0f;
|
||||
this.particleScale = nbt.hasKey("particleScale") ? nbt.getFloat("particleScale") : 1.0f;
|
||||
this.animSpeed = nbt.hasKey("animSpeed") ? nbt.getFloat("animSpeed") : 1.0f;
|
||||
this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 1.0f;
|
||||
colorData.readNBT(nbt);
|
||||
|
||||
// Visual layer fields
|
||||
this.groundFill = !nbt.hasKey("groundFill") || nbt.getBoolean("groundFill");
|
||||
this.groundAlpha = nbt.hasKey("groundAlpha") ? nbt.getFloat("groundAlpha") : 0.25f;
|
||||
this.rings = !nbt.hasKey("rings") || nbt.getBoolean("rings");
|
||||
this.ringCount = nbt.hasKey("ringCount") ? nbt.getInteger("ringCount") : 3;
|
||||
this.border = !nbt.hasKey("border") || nbt.getBoolean("border");
|
||||
this.borderSpeed = nbt.hasKey("borderSpeed") ? nbt.getFloat("borderSpeed") : 1.0f;
|
||||
this.accents = !nbt.hasKey("accents") || nbt.getBoolean("accents");
|
||||
this.accentStyle = nbt.hasKey("accentStyle") ? nbt.getInteger("accentStyle") : 0;
|
||||
this.lightning = nbt.hasKey("lightning") && nbt.getBoolean("lightning");
|
||||
this.particles = !nbt.hasKey("particles") || nbt.getBoolean("particles");
|
||||
this.particleMotion = nbt.hasKey("particleMotion") ? nbt.getInteger("particleMotion") : 0;
|
||||
this.particleDir = nbt.hasKey("particleDir") ? nbt.getString("particleDir") : "";
|
||||
this.particleSize = nbt.hasKey("particleSize") ? nbt.getInteger("particleSize") : 32;
|
||||
this.particleGlow = !nbt.hasKey("particleGlow") || nbt.getBoolean("particleGlow");
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// SHARED GETTERS & SETTERS
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
public int getDurationTicks() { return durationTicks; }
|
||||
public void setDurationTicks(int durationTicks) { this.durationTicks = Math.max(1, durationTicks); }
|
||||
|
||||
public ZoneShape getZoneShapeEnum() { return zoneShape; }
|
||||
public void setZoneShapeEnum(ZoneShape shape) { this.zoneShape = shape; }
|
||||
|
||||
public int getZoneShapeOrdinal() { return zoneShape.ordinal(); }
|
||||
public void setZoneShapeOrdinal(int shape) {
|
||||
ZoneShape[] values = ZoneShape.values();
|
||||
this.zoneShape = shape >= 0 && shape < values.length ? values[shape] : ZoneShape.CIRCLE;
|
||||
}
|
||||
|
||||
public float getSpawnRadius() { return spawnRadius; }
|
||||
public void setSpawnRadius(float spawnRadius) { this.spawnRadius = Math.max(0, spawnRadius); }
|
||||
public int getZoneCount() { return zoneCount; }
|
||||
public void setZoneCount(int zoneCount) { this.zoneCount = Math.max(1, zoneCount); }
|
||||
public float getZoneHeight() { return zoneHeight; }
|
||||
public void setZoneHeight(float zoneHeight) { this.zoneHeight = Math.max(0.5f, zoneHeight); }
|
||||
|
||||
public float getParticleDensity() { return particleDensity; }
|
||||
public void setParticleDensity(float v) { this.particleDensity = Math.max(0, Math.min(5, v)); }
|
||||
public float getParticleScale() { return particleScale; }
|
||||
public void setParticleScale(float v) { this.particleScale = Math.max(0.1f, Math.min(5, v)); }
|
||||
public float getAnimSpeed() { return animSpeed; }
|
||||
public void setAnimSpeed(float v) { this.animSpeed = Math.max(0.1f, Math.min(5, v)); }
|
||||
public float getLightningDensity() { return lightningDensity; }
|
||||
public void setLightningDensity(float v) { this.lightningDensity = Math.max(0, Math.min(3, v)); }
|
||||
|
||||
// Color getters/setters
|
||||
public int getInnerColor() { return colorData.innerColor; }
|
||||
public void setInnerColor(int color) { colorData.innerColor = color; }
|
||||
public int getOuterColor() { return colorData.outerColor; }
|
||||
public void setOuterColor(int color) { colorData.outerColor = color; }
|
||||
public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; }
|
||||
public void setOuterColorEnabled(boolean enabled) { colorData.outerColorEnabled = enabled; }
|
||||
|
||||
// Visual layer getters/setters
|
||||
public boolean isGroundFill() { return groundFill; }
|
||||
public void setGroundFill(boolean v) { this.groundFill = v; }
|
||||
public float getGroundAlpha() { return groundAlpha; }
|
||||
public void setGroundAlpha(float v) { this.groundAlpha = Math.max(0, Math.min(1, v)); }
|
||||
public boolean isRings() { return rings; }
|
||||
public void setRings(boolean v) { this.rings = v; }
|
||||
public int getRingCount() { return ringCount; }
|
||||
public void setRingCount(int v) { this.ringCount = Math.max(1, Math.min(5, v)); }
|
||||
public boolean isBorder() { return border; }
|
||||
public void setBorder(boolean v) { this.border = v; }
|
||||
public float getBorderSpeed() { return borderSpeed; }
|
||||
public void setBorderSpeed(float v) { this.borderSpeed = Math.max(0.1f, Math.min(5, v)); }
|
||||
public boolean isAccents() { return accents; }
|
||||
public void setAccents(boolean v) { this.accents = v; }
|
||||
public int getAccentStyle() { return accentStyle; }
|
||||
public void setAccentStyle(int v) { this.accentStyle = Math.max(0, Math.min(AccentStyle.values().length - 1, v)); }
|
||||
public AccentStyle getAccentStyleEnum() { return AccentStyle.values()[Math.max(0, Math.min(accentStyle, AccentStyle.values().length - 1))]; }
|
||||
public void setAccentStyleEnum(AccentStyle s) { this.accentStyle = s.ordinal(); }
|
||||
public boolean isLightning() { return lightning; }
|
||||
public void setLightning(boolean v) { this.lightning = v; }
|
||||
public boolean isParticles() { return particles; }
|
||||
public void setParticles(boolean v) { this.particles = v; }
|
||||
public int getParticleMotion() { return particleMotion; }
|
||||
public void setParticleMotion(int v) { this.particleMotion = Math.max(0, Math.min(ParticleMotion.values().length - 1, v)); }
|
||||
public ParticleMotion getParticleMotionEnum() { return ParticleMotion.values()[Math.max(0, Math.min(particleMotion, ParticleMotion.values().length - 1))]; }
|
||||
public void setParticleMotionEnum(ParticleMotion m) { this.particleMotion = m.ordinal(); }
|
||||
public String getParticleDir() { return particleDir; }
|
||||
public void setParticleDir(String v) { this.particleDir = v != null ? v : ""; }
|
||||
public int getParticleSize() { return particleSize; }
|
||||
public void setParticleSize(int v) { this.particleSize = Math.max(1, Math.min(256, v)); }
|
||||
public boolean isParticleGlow() { return particleGlow; }
|
||||
public void setParticleGlow(boolean v) { this.particleGlow = v; }
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// SHARED FIELD DEFINITIONS (client-only)
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
protected void addPresetFieldDef(List<FieldDef> defs) {
|
||||
defs.add(FieldDef.subGuiField("gui.applyPreset",
|
||||
SubGuiZonePresetSelector::new,
|
||||
gui -> {
|
||||
SubGuiZonePresetSelector selector = (SubGuiZonePresetSelector) gui;
|
||||
if (selector.selectedPreset != null) {
|
||||
applyPresetDefaults(selector.selectedPreset);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
protected void addVisualFieldDefs(List<FieldDef> defs) {
|
||||
defs.addAll(Arrays.asList(
|
||||
// Ground Fill
|
||||
FieldDef.section("ability.section.ground").tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.boolField("gui.enabled", this::isGroundFill, this::setGroundFill),
|
||||
FieldDef.floatField("gui.alpha", this::getGroundAlpha, this::setGroundAlpha)
|
||||
).tab("ability.tab.visual"),
|
||||
|
||||
// Rings
|
||||
FieldDef.section("ability.section.rings").tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.boolField("gui.enabled", this::isRings, this::setRings),
|
||||
FieldDef.intField("gui.count", this::getRingCount, this::setRingCount).range(1, 5)
|
||||
).tab("ability.tab.visual"),
|
||||
|
||||
// Border
|
||||
FieldDef.section("ability.section.border").tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.boolField("gui.enabled", this::isBorder, this::setBorder),
|
||||
FieldDef.floatField("gui.speed", this::getBorderSpeed, this::setBorderSpeed)
|
||||
).tab("ability.tab.visual"),
|
||||
|
||||
// Accents
|
||||
FieldDef.section("ability.section.accents").tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.boolField("gui.enabled", this::isAccents, this::setAccents),
|
||||
FieldDef.enumField("gui.style", AccentStyle.class, this::getAccentStyleEnum, this::setAccentStyleEnum)
|
||||
).tab("ability.tab.visual"),
|
||||
|
||||
// Lightning
|
||||
FieldDef.section("ability.section.lightning").tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.boolField("gui.enabled", this::isLightning, this::setLightning),
|
||||
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
|
||||
).tab("ability.tab.visual"),
|
||||
|
||||
// Particles
|
||||
FieldDef.section("ability.section.particles").tab("ability.tab.visual"),
|
||||
FieldDef.boolField("gui.enabled", this::isParticles, this::setParticles)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.row(
|
||||
FieldDef.floatField("gui.density", this::getParticleDensity, this::setParticleDensity),
|
||||
FieldDef.floatField("gui.scale", this::getParticleScale, this::setParticleScale)
|
||||
).tab("ability.tab.visual").visibleWhen(this::isParticles),
|
||||
FieldDef.row(
|
||||
FieldDef.enumField("gui.motion", ParticleMotion.class, this::getParticleMotionEnum, this::setParticleMotionEnum),
|
||||
FieldDef.boolField("gui.glow", this::isParticleGlow, this::setParticleGlow)
|
||||
).tab("ability.tab.visual").visibleWhen(this::isParticles),
|
||||
FieldDef.stringField("gui.texture", this::getParticleDir, this::setParticleDir)
|
||||
.tab("ability.tab.visual").visibleWhen(this::isParticles),
|
||||
|
||||
// Animation
|
||||
FieldDef.section("ability.section.animation").tab("ability.tab.visual"),
|
||||
FieldDef.floatField("gui.speed", this::getAnimSpeed, this::setAnimSpeed)
|
||||
.tab("ability.tab.visual"),
|
||||
|
||||
// Colors
|
||||
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
|
||||
.tab("ability.tab.visual"),
|
||||
FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
|
||||
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled)
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -150,6 +150,12 @@ public class Telegraph {
|
||||
return new Telegraph("", TelegraphType.POINT);
|
||||
}
|
||||
|
||||
public static Telegraph square(float radius) {
|
||||
Telegraph t = new Telegraph("", TelegraphType.SQUARE);
|
||||
t.radius = radius;
|
||||
return t;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GETTERS & SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -29,6 +29,11 @@ public enum TelegraphType {
|
||||
*/
|
||||
POINT,
|
||||
|
||||
/**
|
||||
* Filled square (uses radius as half-size)
|
||||
*/
|
||||
SQUARE,
|
||||
|
||||
/**
|
||||
* No telegraph
|
||||
*/
|
||||
|
||||
@@ -222,24 +222,39 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
|
||||
|
||||
setCharging(false);
|
||||
|
||||
// Update start position to current position
|
||||
// Origin (tail) stays at the charged position
|
||||
startX = posX;
|
||||
startY = posY;
|
||||
startZ = posZ;
|
||||
|
||||
// Reset head offset to origin
|
||||
headOffsetX = 0;
|
||||
headOffsetY = 0;
|
||||
headOffsetZ = 0;
|
||||
prevHeadOffsetX = 0;
|
||||
prevHeadOffsetY = 0;
|
||||
prevHeadOffsetZ = 0;
|
||||
|
||||
// Origin no longer follows owner after firing
|
||||
attachedToOwner = false;
|
||||
|
||||
// Sync prev position to prevent visual jump on first frame
|
||||
prevPosX = posX;
|
||||
prevPosY = posY;
|
||||
prevPosZ = posZ;
|
||||
|
||||
// Reset trail
|
||||
trailPoints.clear();
|
||||
trailPointAges.clear();
|
||||
trailPoints.add(Vec3.createVectorHelper(0, 0, 0));
|
||||
|
||||
// Fire forward based on owner facing direction
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner != null) {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = 0; // Fire horizontally
|
||||
motionX = -Math.sin(yaw) * Math.cos(pitch) * getSpeed();
|
||||
motionY = -Math.sin(pitch) * getSpeed();
|
||||
motionZ = Math.cos(yaw) * Math.cos(pitch) * getSpeed();
|
||||
motionX = -Math.sin(yaw) * getSpeed();
|
||||
motionY = 0;
|
||||
motionZ = Math.cos(yaw) * getSpeed();
|
||||
} else {
|
||||
motionX = getSpeed();
|
||||
motionY = 0;
|
||||
|
||||
@@ -31,6 +31,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
// Disc shape properties
|
||||
private float discRadius = 1.0f; // Width of disc
|
||||
private float discThickness = 0.2f; // Height of disc
|
||||
private boolean vertical = false; // false = horizontal (flat), true = vertical (thin edge forward)
|
||||
|
||||
// Charging state (during windup)
|
||||
private boolean charging = false;
|
||||
@@ -38,7 +39,6 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
private int chargeTick = 0;
|
||||
private float targetDiscRadius = 1.0f; // Full radius to grow to during charging
|
||||
private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging
|
||||
private EnergyAnchorData anchorData = new EnergyAnchorData();
|
||||
|
||||
// Data watcher index for charging state (synced to clients)
|
||||
private static final int DW_CHARGING = 20;
|
||||
@@ -123,11 +123,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
* The disc will grow from 0 to discRadius over chargeDuration ticks.
|
||||
* Position follows the owner based on anchor point.
|
||||
*/
|
||||
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
|
||||
public void setupCharging(EnergyAnchorData anchor, int chargeDuration, boolean vertical) {
|
||||
setCharging(true);
|
||||
this.chargeDuration = chargeDuration;
|
||||
this.chargeTick = 0;
|
||||
this.anchorData = anchor;
|
||||
this.vertical = vertical;
|
||||
this.targetDiscRadius = this.discRadius;
|
||||
this.targetDiscThickness = this.discThickness;
|
||||
this.discRadius = 0.01f;
|
||||
@@ -145,7 +146,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
* Follows anchor point and animations like in the real game.
|
||||
* 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) {
|
||||
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);
|
||||
|
||||
@@ -158,6 +159,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
this.chargeDuration = chargeDuration;
|
||||
this.chargeTick = 0;
|
||||
this.anchorData = anchor;
|
||||
this.vertical = vertical;
|
||||
|
||||
// Store target size and start at 0 for grow effect
|
||||
this.targetDiscRadius = discRadius;
|
||||
@@ -542,6 +544,28 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
return returning;
|
||||
}
|
||||
|
||||
public boolean isVertical() {
|
||||
return vertical;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the yaw angle (degrees) from motion vector for vertical disc orientation.
|
||||
* During charging, uses owner's facing direction.
|
||||
*/
|
||||
public float getTravelYaw() {
|
||||
if (isCharging()) {
|
||||
Entity owner = previewMode ? previewOwner : getOwnerEntity();
|
||||
if (owner != null) {
|
||||
return owner.rotationYaw;
|
||||
}
|
||||
}
|
||||
double speed = Math.sqrt(motionX * motionX + motionZ * motionZ);
|
||||
if (speed > 0.001) {
|
||||
return (float) (Math.atan2(-motionX, motionZ) * 180.0 / Math.PI);
|
||||
}
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// ==================== NBT ====================
|
||||
|
||||
@Override
|
||||
@@ -550,6 +574,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
this.boomerangDelay = nbt.hasKey("BoomerangDelay") ? nbt.getInteger("BoomerangDelay") : 40;
|
||||
this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f;
|
||||
this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f;
|
||||
this.vertical = nbt.hasKey("Vertical") ? nbt.getBoolean("Vertical") : false;
|
||||
this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning");
|
||||
// Charging state
|
||||
boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging");
|
||||
@@ -567,6 +592,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
nbt.setInteger("BoomerangDelay", boomerangDelay);
|
||||
nbt.setFloat("DiscRadius", discRadius);
|
||||
nbt.setFloat("DiscThickness", discThickness);
|
||||
nbt.setBoolean("Vertical", vertical);
|
||||
nbt.setBoolean("Returning", returning);
|
||||
// Charging state
|
||||
nbt.setBoolean("Charging", isCharging());
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package kamkeel.npcs.entity;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -36,6 +35,9 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
// Direction (normalized)
|
||||
private double dirX, dirY, dirZ;
|
||||
|
||||
// Lock vertical direction after firing (only update yaw, keep pitch fixed)
|
||||
private boolean lockVerticalDirection = false;
|
||||
|
||||
// Track hit entities to avoid double-damage
|
||||
private Set<Integer> hitEntities = new HashSet<>();
|
||||
|
||||
@@ -46,7 +48,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
private boolean charging = false;
|
||||
private int chargeDuration = 40;
|
||||
private int chargeTick = 0;
|
||||
private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT);
|
||||
private float targetSize = 1.0f; // Full size to grow to during charging
|
||||
|
||||
// Data watcher index for charging state (synced to clients)
|
||||
@@ -167,6 +168,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
return;
|
||||
}
|
||||
|
||||
// Track owner's anchor position and rotation each tick
|
||||
// so the laser follows the NPC's facing direction
|
||||
updateLaserOriginAndDirection();
|
||||
|
||||
if (!fullyExtended) {
|
||||
// Expand the laser
|
||||
currentLength += expansionSpeed;
|
||||
@@ -189,27 +194,76 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
} else {
|
||||
// Laser is fully extended, count down linger time
|
||||
ticksSinceFullExtension++;
|
||||
|
||||
// Update end point during linger (origin/direction may still change)
|
||||
endX = startX + dirX * currentLength;
|
||||
endY = startY + dirY * currentLength;
|
||||
endZ = startZ + dirZ * currentLength;
|
||||
|
||||
if (ticksSinceFullExtension >= lingerTicks) {
|
||||
this.setDead();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private void updateLaserOriginAndDirection() {
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner == null || owner.isDead) return;
|
||||
if (!(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 {
|
||||
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);
|
||||
}
|
||||
|
||||
// Keep entity positioned at origin
|
||||
prevPosX = startX;
|
||||
prevPosY = startY;
|
||||
prevPosZ = startZ;
|
||||
setPosition(startX, startY, startZ);
|
||||
}
|
||||
|
||||
public void startMoving(EntityLivingBase target) {
|
||||
if (!isCharging()) return;
|
||||
|
||||
setCharging(false);
|
||||
|
||||
// Update start position to current position
|
||||
// Initialize from current anchor position
|
||||
// updateLaserOriginAndDirection() will keep these updated each tick
|
||||
startX = posX;
|
||||
startY = posY;
|
||||
startZ = posZ;
|
||||
|
||||
// Always derive direction from the owner's current yaw/pitch.
|
||||
// This ensures the laser fires in the direction the NPC is visually facing,
|
||||
// which matches the telegraph line direction regardless of Lock Movement mode.
|
||||
// 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);
|
||||
@@ -217,7 +271,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
this.dirY = -Math.sin(pitch);
|
||||
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
|
||||
} else if (target != null) {
|
||||
// Fallback: aim at target if owner is somehow unavailable
|
||||
double dx = target.posX - startX;
|
||||
double dy = (target.posY + target.getEyeHeight() - 0.4) - startY;
|
||||
double dz = target.posZ - startZ;
|
||||
@@ -229,12 +282,10 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
}
|
||||
}
|
||||
|
||||
setPosition(endX, endY, endZ);
|
||||
|
||||
// Initialize end point
|
||||
this.endX = posX;
|
||||
this.endY = posY;
|
||||
this.endZ = posZ;
|
||||
// Initialize end point at start (will expand from here)
|
||||
this.endX = startX;
|
||||
this.endY = startY;
|
||||
this.endZ = startZ;
|
||||
}
|
||||
|
||||
private void checkBlockCollision() {
|
||||
@@ -310,14 +361,14 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
}
|
||||
|
||||
private void checkEntityCollisionAlongLine() {
|
||||
// Check collision along the entire laser line
|
||||
// Use a slightly expanded AABB for the full line
|
||||
double minX = Math.min(startX, endX) - laserWidth;
|
||||
double minY = Math.min(startY, endY) - laserWidth;
|
||||
double minZ = Math.min(startZ, endZ) - laserWidth;
|
||||
double maxX = Math.max(startX, endX) + laserWidth;
|
||||
double maxY = Math.max(startY, endY) + laserWidth;
|
||||
double maxZ = Math.max(startZ, endZ) + laserWidth;
|
||||
// Expand search AABB by entity sizes to ensure we find all potential targets
|
||||
double expand = Math.max(laserWidth, 1.0);
|
||||
double minX = Math.min(startX, endX) - expand;
|
||||
double minY = Math.min(startY, endY) - expand;
|
||||
double minZ = Math.min(startZ, endZ) - expand;
|
||||
double maxX = Math.max(startX, endX) + expand;
|
||||
double maxY = Math.max(startY, endY) + expand;
|
||||
double maxZ = Math.max(startZ, endZ) + expand;
|
||||
|
||||
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
|
||||
@@ -328,7 +379,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
if (shouldIgnoreEntity(entity)) continue;
|
||||
if (hitEntities.contains(entity.getEntityId())) continue;
|
||||
|
||||
// Check if entity is close to the laser line
|
||||
// Check if entity's bounding box intersects with the laser line
|
||||
if (isEntityOnLine(entity)) {
|
||||
hitEntities.add(entity.getEntityId());
|
||||
applyDamage(entity);
|
||||
@@ -339,42 +390,64 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity intersects with the laser line.
|
||||
* 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.
|
||||
*/
|
||||
private boolean isEntityOnLine(EntityLivingBase entity) {
|
||||
// Get entity center
|
||||
double ex = entity.posX;
|
||||
double ey = entity.posY + entity.height * 0.5;
|
||||
double ez = entity.posZ;
|
||||
|
||||
// Vector from start to entity
|
||||
// Project entity XZ position onto the laser line direction
|
||||
double vx = ex - startX;
|
||||
double vy = ey - startY;
|
||||
double vz = ez - startZ;
|
||||
|
||||
// Project onto line direction
|
||||
double dot = vx * dirX + vy * dirY + vz * dirZ;
|
||||
|
||||
// Clamp to line segment
|
||||
// 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));
|
||||
|
||||
// Closest point on line
|
||||
// Closest point on laser line at this parameter
|
||||
double closestX = startX + dirX * dot;
|
||||
double closestY = startY + dirY * dot;
|
||||
double closestZ = startZ + dirZ * dot;
|
||||
|
||||
// Distance from entity to closest point
|
||||
double distSq = (ex - closestX) * (ex - closestX) +
|
||||
(ey - closestY) * (ey - closestY) +
|
||||
(ez - closestZ) * (ez - closestZ);
|
||||
// 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;
|
||||
|
||||
// Hit if within laser width + entity radius
|
||||
double hitRadius = laserWidth * 0.5 + entity.width * 0.5;
|
||||
return distSq <= hitRadius * hitRadius;
|
||||
// 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;
|
||||
}
|
||||
|
||||
// ==================== GETTERS FOR RENDERER ====================
|
||||
|
||||
public void setLockVerticalDirection(boolean lock) {
|
||||
this.lockVerticalDirection = lock;
|
||||
}
|
||||
|
||||
public boolean isLockVerticalDirection() {
|
||||
return lockVerticalDirection;
|
||||
}
|
||||
|
||||
public float getLaserWidth() {
|
||||
return laserWidth;
|
||||
}
|
||||
@@ -454,7 +527,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40;
|
||||
this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0;
|
||||
this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size;
|
||||
anchorData.readNBT(nbt);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -476,7 +548,6 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
nbt.setInteger("ChargeDuration", chargeDuration);
|
||||
nbt.setInteger("ChargeTick", chargeTick);
|
||||
nbt.setFloat("TargetSize", targetSize);
|
||||
anchorData.writeNBT(nbt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,6 +60,10 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
|
||||
private int maxTicks = 400;
|
||||
private long deathWorldTime = -1;
|
||||
|
||||
// Preview mode
|
||||
private boolean previewMode = false;
|
||||
private EntityLivingBase previewOwner = null;
|
||||
|
||||
// Damage state
|
||||
private transient int ticksSinceDamage = 0;
|
||||
private transient Set<Integer> hitThisTick = new HashSet<>();
|
||||
@@ -281,7 +285,16 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
|
||||
target.velocityChanged = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up preview mode. Owner is stored directly since world entity lookup won't work.
|
||||
*/
|
||||
public void setupPreview(EntityLivingBase owner) {
|
||||
this.previewMode = true;
|
||||
this.previewOwner = owner;
|
||||
}
|
||||
|
||||
private Entity getOwner() {
|
||||
if (previewMode && previewOwner != null) return previewOwner;
|
||||
if (ownerEntityId == -1) return null;
|
||||
return worldObj.getEntityByID(ownerEntityId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,928 @@
|
||||
package kamkeel.npcs.entity;
|
||||
|
||||
import cpw.mods.fml.common.network.ByteBufUtils;
|
||||
import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData;
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.NpcDamageSource;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Shared zone entity for Trap and Hazard abilities.
|
||||
* Handles rendering, positioning, and damage logic.
|
||||
*/
|
||||
public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnData {
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ENUMS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public enum ZoneType { TRAP, HAZARD }
|
||||
public enum ZoneShape {
|
||||
CIRCLE, SQUARE;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case CIRCLE: return "ability.shape.circle";
|
||||
case SQUARE: return "ability.shape.square";
|
||||
default: return name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum AccentStyle {
|
||||
STATIC, SWAYING, FLICKERING;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case STATIC: return "ability.accentStyle.static";
|
||||
case SWAYING: return "ability.accentStyle.swaying";
|
||||
case FLICKERING: return "ability.accentStyle.flickering";
|
||||
default: return name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum ParticleMotion {
|
||||
RISING, DRIFTING, SPARKS;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case RISING: return "ability.particleMotion.rising";
|
||||
case DRIFTING: return "ability.particleMotion.drifting";
|
||||
case SPARKS: return "ability.particleMotion.sparks";
|
||||
default: return name();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Tiny Y offset above ground to prevent z-clipping */
|
||||
private static final double GROUND_OFFSET = 0.005;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// COMMON PROPERTIES (synced via ByteBuf)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private ZoneType zoneType = ZoneType.TRAP;
|
||||
private ZoneShape shape = ZoneShape.CIRCLE;
|
||||
|
||||
private int ownerEntityId = -1;
|
||||
|
||||
private float radius = 4.0f;
|
||||
|
||||
private int durationTicks = 200;
|
||||
private int maxTicks = 200;
|
||||
|
||||
private int innerColor = 0xFF6600;
|
||||
private int outerColor = 0xFF0000;
|
||||
private boolean outerColorEnabled = true;
|
||||
|
||||
private float zoneHeight = 2.0f;
|
||||
|
||||
// Visual parameters (synced for renderer)
|
||||
private float particleDensity = 1.0f;
|
||||
private float particleScale = 1.0f;
|
||||
private float animSpeed = 1.0f;
|
||||
private float lightningDensity = 1.0f;
|
||||
|
||||
// Visual layer parameters (always active)
|
||||
private boolean groundFill = true;
|
||||
private float groundAlpha = 0.25f;
|
||||
private boolean rings = true;
|
||||
private int ringCount = 3;
|
||||
private boolean border = true;
|
||||
private float borderSpeed = 1.0f;
|
||||
private boolean accents = true;
|
||||
private int accentStyle = 0; // AccentStyle ordinal
|
||||
private boolean lightning = false;
|
||||
private boolean particles = true;
|
||||
private int particleMotion = 0; // ParticleMotion ordinal
|
||||
private String particleDir = "";
|
||||
private int particleSize = 32;
|
||||
private boolean particleGlow = true;
|
||||
|
||||
// Lifetime
|
||||
private long deathWorldTime = -1;
|
||||
|
||||
// Effects
|
||||
private List<AbilityEffect> effects = new ArrayList<>();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// TRAP-SPECIFIC PROPERTIES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private int armTime = 20;
|
||||
private float triggerRadius = 2.0f;
|
||||
private int maxTriggers = 1;
|
||||
private int triggerCooldown = 20;
|
||||
private float damage = 6.0f;
|
||||
private float damageRadius = 0.0f;
|
||||
private float knockback = 0.5f;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// HAZARD-SPECIFIC PROPERTIES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private float damagePerSecond = 1.0f;
|
||||
private int damageInterval = 20;
|
||||
private boolean ignoreInvulnFrames = false;
|
||||
private boolean affectsCaster = false;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// RUNTIME STATE (transient)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// Trap runtime
|
||||
private transient boolean armed = false;
|
||||
private transient int triggerCount = 0;
|
||||
private transient int ticksSinceLastTrigger = 0;
|
||||
private transient Set<UUID> triggeredEntities = new HashSet<>();
|
||||
private transient int triggerFlashTick = -1; // For visual flash on trigger
|
||||
|
||||
// Hazard runtime
|
||||
private transient int ticksSinceDamage = 0;
|
||||
private transient Set<Integer> damagedThisTick = new HashSet<>();
|
||||
|
||||
// Preview mode
|
||||
private boolean previewMode = false;
|
||||
private EntityLivingBase previewOwner = null;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// CONSTRUCTORS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public EntityAbilityZone(World world) {
|
||||
super(world);
|
||||
this.setSize(0.1f, 0.1f);
|
||||
this.noClip = true;
|
||||
this.isImmuneToFire = true;
|
||||
this.ignoreFrustumCheck = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full constructor for creating a zone entity.
|
||||
* Use the static factory methods for cleaner creation.
|
||||
* Position is snapped to ground level with a tiny offset to avoid z-clipping.
|
||||
*/
|
||||
private EntityAbilityZone(World world, ZoneType type, EntityLivingBase owner,
|
||||
double x, double y, double z) {
|
||||
this(world);
|
||||
this.zoneType = type;
|
||||
this.ownerEntityId = owner.getEntityId();
|
||||
// Snap to ground with tiny offset to prevent z-clipping
|
||||
double groundY = Ability.findGroundLevel(world, x, y, z) + GROUND_OFFSET;
|
||||
this.setPosition(x, groundY, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Trap zone entity.
|
||||
*/
|
||||
public static EntityAbilityZone createTrap(World world, EntityLivingBase owner,
|
||||
double x, double y, double z,
|
||||
ZoneShape shape,
|
||||
float triggerRadius, int armTime, int maxTriggers,
|
||||
int triggerCooldown, float damage, float damageRadius,
|
||||
float knockback, int durationTicks,
|
||||
int innerColor, int outerColor, boolean outerColorEnabled,
|
||||
float zoneHeight,
|
||||
float particleDensity, float particleScale,
|
||||
float animSpeed, float lightningDensity,
|
||||
List<AbilityEffect> effects) {
|
||||
EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.TRAP, owner, x, y, z);
|
||||
zone.shape = shape;
|
||||
zone.radius = triggerRadius;
|
||||
zone.triggerRadius = triggerRadius;
|
||||
zone.armTime = armTime;
|
||||
zone.maxTriggers = maxTriggers;
|
||||
zone.triggerCooldown = triggerCooldown;
|
||||
zone.damage = damage;
|
||||
zone.damageRadius = damageRadius;
|
||||
zone.knockback = knockback;
|
||||
zone.durationTicks = durationTicks;
|
||||
zone.maxTicks = durationTicks;
|
||||
zone.innerColor = innerColor;
|
||||
zone.outerColor = outerColor;
|
||||
zone.outerColorEnabled = outerColorEnabled;
|
||||
zone.zoneHeight = zoneHeight;
|
||||
zone.particleDensity = particleDensity;
|
||||
zone.particleScale = particleScale;
|
||||
zone.animSpeed = animSpeed;
|
||||
zone.lightningDensity = lightningDensity;
|
||||
if (effects != null) {
|
||||
for (AbilityEffect e : effects) {
|
||||
zone.effects.add(e.copy());
|
||||
}
|
||||
}
|
||||
// Ready to trigger immediately after arm time
|
||||
zone.ticksSinceLastTrigger = triggerCooldown;
|
||||
return zone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Hazard zone entity.
|
||||
*/
|
||||
public static EntityAbilityZone createHazard(World world, EntityLivingBase owner,
|
||||
double x, double y, double z,
|
||||
ZoneShape shape,
|
||||
float radius,
|
||||
float damagePerSecond, int damageInterval,
|
||||
boolean ignoreInvulnFrames, boolean affectsCaster,
|
||||
int durationTicks,
|
||||
int innerColor, int outerColor, boolean outerColorEnabled,
|
||||
float zoneHeight,
|
||||
float particleDensity, float particleScale,
|
||||
float animSpeed, float lightningDensity,
|
||||
List<AbilityEffect> effects) {
|
||||
EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.HAZARD, owner, x, y, z);
|
||||
zone.shape = shape;
|
||||
zone.radius = radius;
|
||||
zone.damagePerSecond = damagePerSecond;
|
||||
zone.damageInterval = damageInterval;
|
||||
zone.ignoreInvulnFrames = ignoreInvulnFrames;
|
||||
zone.affectsCaster = affectsCaster;
|
||||
zone.durationTicks = durationTicks;
|
||||
zone.maxTicks = durationTicks;
|
||||
zone.innerColor = innerColor;
|
||||
zone.outerColor = outerColor;
|
||||
zone.outerColorEnabled = outerColorEnabled;
|
||||
zone.zoneHeight = zoneHeight;
|
||||
zone.particleDensity = particleDensity;
|
||||
zone.particleScale = particleScale;
|
||||
zone.animSpeed = animSpeed;
|
||||
zone.lightningDensity = lightningDensity;
|
||||
if (effects != null) {
|
||||
for (AbilityEffect e : effects) {
|
||||
zone.effects.add(e.copy());
|
||||
}
|
||||
}
|
||||
// Ready to deal damage immediately
|
||||
zone.ticksSinceDamage = damageInterval;
|
||||
return zone;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ENTITY LIFECYCLE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
protected void entityInit() {
|
||||
// No DataWatcher needed
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate() {
|
||||
this.prevPosX = this.posX;
|
||||
this.prevPosY = this.posY;
|
||||
this.prevPosZ = this.posZ;
|
||||
|
||||
super.onUpdate();
|
||||
|
||||
// Set death time on first tick
|
||||
if (deathWorldTime < 0 && worldObj != null) {
|
||||
deathWorldTime = worldObj.getTotalWorldTime() + maxTicks;
|
||||
}
|
||||
|
||||
// Check lifetime
|
||||
if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) {
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
|
||||
// Server handles damage
|
||||
if (!worldObj.isRemote && !previewMode) {
|
||||
switch (zoneType) {
|
||||
case TRAP:
|
||||
tickTrap();
|
||||
break;
|
||||
case HAZARD:
|
||||
tickHazard();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// TRAP LOGIC (server-only)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void tickTrap() {
|
||||
// Arm time countdown
|
||||
if (!armed) {
|
||||
if (ticksExisted >= armTime) {
|
||||
armed = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
ticksSinceLastTrigger++;
|
||||
|
||||
// All triggers used
|
||||
if (maxTriggers > 0 && triggerCount >= maxTriggers) {
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
|
||||
// Cooldown between triggers
|
||||
if (ticksSinceLastTrigger < triggerCooldown) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Proximity detection
|
||||
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
|
||||
posX - triggerRadius, posY - 0.5, posZ - triggerRadius,
|
||||
posX + triggerRadius, posY + zoneHeight, posZ + triggerRadius
|
||||
);
|
||||
|
||||
Entity owner = getOwner();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (owner != null && entity == owner) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue;
|
||||
|
||||
double dx = entity.posX - posX;
|
||||
double dz = entity.posZ - posZ;
|
||||
|
||||
boolean inRange;
|
||||
if (shape == ZoneShape.SQUARE) {
|
||||
inRange = Math.abs(dx) <= triggerRadius && Math.abs(dz) <= triggerRadius;
|
||||
} else {
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
inRange = dist <= triggerRadius;
|
||||
}
|
||||
|
||||
if (inRange) {
|
||||
triggerTrap(entity);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void triggerTrap(EntityLivingBase triggerer) {
|
||||
Entity owner = getOwner();
|
||||
Set<EntityLivingBase> affected = new HashSet<>();
|
||||
|
||||
if (damageRadius > 0) {
|
||||
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
|
||||
posX - damageRadius, posY - 0.5, posZ - damageRadius,
|
||||
posX + damageRadius, posY + zoneHeight, posZ + damageRadius
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (owner != null && entity == owner) continue;
|
||||
double dx = entity.posX - posX;
|
||||
double dz = entity.posZ - posZ;
|
||||
if (shape == ZoneShape.SQUARE) {
|
||||
if (Math.abs(dx) <= damageRadius && Math.abs(dz) <= damageRadius) {
|
||||
affected.add(entity);
|
||||
}
|
||||
} else {
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist <= damageRadius) {
|
||||
affected.add(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
affected.add(triggerer);
|
||||
}
|
||||
|
||||
boolean anyDamaged = false;
|
||||
for (EntityLivingBase entity : affected) {
|
||||
boolean hit = applyDamage(entity, owner, damage);
|
||||
if (hit) {
|
||||
anyDamaged = true;
|
||||
}
|
||||
|
||||
// Apply knockback only if damage landed
|
||||
if (hit && knockback > 0) {
|
||||
double dx = entity.posX - posX;
|
||||
double dz = entity.posZ - posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
if (len > 0) {
|
||||
entity.addVelocity((dx / len) * knockback * 0.5, 0.1, (dz / len) * knockback * 0.5);
|
||||
entity.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply effects only if damage landed
|
||||
if (hit) {
|
||||
applyEffects(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// Only count the trigger if at least one entity was actually damaged
|
||||
if (anyDamaged) {
|
||||
triggerCount++;
|
||||
ticksSinceLastTrigger = 0;
|
||||
triggeredEntities.add(triggerer.getUniqueID());
|
||||
triggerFlashTick = ticksExisted;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// HAZARD LOGIC (server-only)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void tickHazard() {
|
||||
damagedThisTick.clear();
|
||||
ticksSinceDamage++;
|
||||
|
||||
if (ticksSinceDamage < damageInterval) {
|
||||
return;
|
||||
}
|
||||
ticksSinceDamage = 0;
|
||||
|
||||
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
|
||||
posX - radius, posY - 0.5, posZ - radius,
|
||||
posX + radius, posY + zoneHeight, posZ + radius
|
||||
);
|
||||
|
||||
Entity owner = getOwner();
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (owner != null && entity == owner && !affectsCaster) continue;
|
||||
if (damagedThisTick.contains(entity.getEntityId())) continue;
|
||||
if (!isInZone(entity)) continue;
|
||||
|
||||
if (damagePerSecond > 0) {
|
||||
if (ignoreInvulnFrames) {
|
||||
entity.hurtResistantTime = 0;
|
||||
}
|
||||
applyDamage(entity, owner, damagePerSecond);
|
||||
}
|
||||
|
||||
applyEffects(entity);
|
||||
damagedThisTick.add(entity.getEntityId());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInZone(EntityLivingBase entity) {
|
||||
double dx = entity.posX - posX;
|
||||
double dz = entity.posZ - posZ;
|
||||
|
||||
switch (shape) {
|
||||
case CIRCLE:
|
||||
return Math.sqrt(dx * dx + dz * dz) <= radius;
|
||||
case SQUARE:
|
||||
return Math.abs(dx) <= radius && Math.abs(dz) <= radius;
|
||||
default:
|
||||
return Math.sqrt(dx * dx + dz * dz) <= radius;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// DAMAGE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private boolean applyDamage(EntityLivingBase target, Entity owner, float dmg) {
|
||||
if (dmg <= 0) return false;
|
||||
|
||||
if (owner instanceof EntityNPCInterface) {
|
||||
return target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg);
|
||||
} else if (owner instanceof EntityPlayer) {
|
||||
return target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg);
|
||||
} else if (owner instanceof EntityLivingBase) {
|
||||
return target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg);
|
||||
} else {
|
||||
return target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyEffects(EntityLivingBase entity) {
|
||||
if (entity == null || effects.isEmpty()) return;
|
||||
for (AbilityEffect effect : effects) {
|
||||
effect.apply(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ENTITY HELPERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private Entity getOwner() {
|
||||
if (previewMode && previewOwner != null) return previewOwner;
|
||||
if (ownerEntityId == -1) return null;
|
||||
return worldObj.getEntityByID(ownerEntityId);
|
||||
}
|
||||
|
||||
public void setupPreview(EntityLivingBase owner) {
|
||||
this.previewMode = true;
|
||||
this.previewOwner = owner;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// RENDERING SUPPORT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public boolean isInRangeToRenderDist(double distance) {
|
||||
return distance < 16384.0D; // 128 blocks
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRenderInPass(int pass) {
|
||||
return pass == 1; // Translucent pass
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getBrightness(float partialTicks) {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public int getBrightnessForRender(float partialTicks) {
|
||||
return 0xF000F0;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GETTERS FOR RENDERER
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public ZoneType getZoneType() { return zoneType; }
|
||||
public ZoneShape getShape() { return shape; }
|
||||
public float getRadius() { return radius; }
|
||||
public int getInnerColor() { return innerColor; }
|
||||
public int getOuterColor() { return outerColor; }
|
||||
public boolean isOuterColorEnabled() { return outerColorEnabled; }
|
||||
public float getZoneHeight() { return zoneHeight; }
|
||||
public boolean isArmed() { return armed || ticksExisted >= armTime; }
|
||||
public int getTriggerFlashTick() { return triggerFlashTick; }
|
||||
public int getDurationTicks() { return durationTicks; }
|
||||
public int getMaxTicks() { return maxTicks; }
|
||||
public float getParticleDensity() { return particleDensity; }
|
||||
public float getParticleScale() { return particleScale; }
|
||||
public float getAnimSpeed() { return animSpeed; }
|
||||
public float getLightningDensity() { return lightningDensity; }
|
||||
|
||||
// Visual layer getters
|
||||
public boolean isGroundFill() { return groundFill; }
|
||||
public float getGroundAlpha() { return groundAlpha; }
|
||||
public boolean isRings() { return rings; }
|
||||
public int getRingCount() { return ringCount; }
|
||||
public boolean isBorder() { return border; }
|
||||
public float getBorderSpeed() { return borderSpeed; }
|
||||
public boolean isAccents() { return accents; }
|
||||
public int getAccentStyle() { return accentStyle; }
|
||||
public boolean isLightning() { return lightning; }
|
||||
public boolean isParticles() { return particles; }
|
||||
public int getParticleMotion() { return particleMotion; }
|
||||
public String getParticleDir() { return particleDir; }
|
||||
public int getParticleSize() { return particleSize; }
|
||||
public boolean isParticleGlow() { return particleGlow; }
|
||||
|
||||
/**
|
||||
* Apply visual settings from ability data.
|
||||
*/
|
||||
public void applyVisual(boolean groundFill, float groundAlpha,
|
||||
boolean rings, int ringCount,
|
||||
boolean border, float borderSpeed,
|
||||
boolean accents, int accentStyle,
|
||||
boolean lightning,
|
||||
boolean particles, int particleMotion,
|
||||
String particleDir, int particleSize, boolean particleGlow) {
|
||||
this.groundFill = groundFill;
|
||||
this.groundAlpha = groundAlpha;
|
||||
this.rings = rings;
|
||||
this.ringCount = ringCount;
|
||||
this.border = border;
|
||||
this.borderSpeed = borderSpeed;
|
||||
this.accents = accents;
|
||||
this.accentStyle = accentStyle;
|
||||
this.lightning = lightning;
|
||||
this.particles = particles;
|
||||
this.particleMotion = particleMotion;
|
||||
this.particleDir = particleDir != null ? particleDir : "";
|
||||
this.particleSize = particleSize;
|
||||
this.particleGlow = particleGlow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply preset defaults by old style name (for backward compatibility).
|
||||
*/
|
||||
private void applyPresetDefaults(String styleName) {
|
||||
switch (styleName) {
|
||||
case "TOXIC":
|
||||
groundFill = true; groundAlpha = 0.30f; rings = true; ringCount = 3;
|
||||
border = false; borderSpeed = 1.0f; accents = true; accentStyle = 1;
|
||||
lightning = false; particles = true; particleMotion = 0; particleGlow = true;
|
||||
break;
|
||||
case "INFERNO":
|
||||
groundFill = true; groundAlpha = 0.35f; rings = true; ringCount = 3;
|
||||
border = true; borderSpeed = 2.0f; accents = true; accentStyle = 2;
|
||||
lightning = false; particles = true; particleMotion = 0; particleGlow = true;
|
||||
break;
|
||||
case "ARCANE":
|
||||
groundFill = true; groundAlpha = 0.20f; rings = true; ringCount = 1;
|
||||
border = true; borderSpeed = 0.8f; accents = true; accentStyle = 0;
|
||||
lightning = false; particles = true; particleMotion = 1; particleGlow = true;
|
||||
break;
|
||||
case "ELECTRIC":
|
||||
groundFill = true; groundAlpha = 0.15f; rings = false; ringCount = 1;
|
||||
border = false; borderSpeed = 1.0f; accents = false; accentStyle = 0;
|
||||
lightning = true; particles = true; particleMotion = 2; particleGlow = true;
|
||||
break;
|
||||
case "FROST":
|
||||
groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3;
|
||||
border = true; borderSpeed = 0.3f; accents = true; accentStyle = 0;
|
||||
lightning = false; particles = true; particleMotion = 1; particleGlow = true;
|
||||
break;
|
||||
default: // DEFAULT
|
||||
groundFill = true; groundAlpha = 0.25f; rings = true; ringCount = 3;
|
||||
border = true; borderSpeed = 1.0f; accents = true; accentStyle = 0;
|
||||
lightning = false; particles = false; particleMotion = 0; particleGlow = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// NBT SERIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
protected void readEntityFromNBT(NBTTagCompound nbt) {
|
||||
this.zoneType = ZoneType.values()[nbt.getInteger("ZoneType")];
|
||||
|
||||
// Shape: read new string key first, fall back to old ordinal (always CIRCLE for old RING/CONE)
|
||||
if (nbt.hasKey("ShapeName")) {
|
||||
try {
|
||||
this.shape = ZoneShape.valueOf(nbt.getString("ShapeName"));
|
||||
} catch (Exception e) {
|
||||
this.shape = ZoneShape.CIRCLE;
|
||||
}
|
||||
} else {
|
||||
// Legacy: old ordinal data — RING(1) and CONE(2) map to CIRCLE
|
||||
this.shape = ZoneShape.CIRCLE;
|
||||
}
|
||||
|
||||
this.ownerEntityId = nbt.getInteger("OwnerId");
|
||||
this.radius = nbt.getFloat("Radius");
|
||||
this.durationTicks = nbt.getInteger("Duration");
|
||||
this.maxTicks = nbt.getInteger("MaxTicks");
|
||||
this.innerColor = nbt.getInteger("InnerColor");
|
||||
this.outerColor = nbt.getInteger("OuterColor");
|
||||
this.outerColorEnabled = nbt.getBoolean("OuterColorEnabled");
|
||||
this.zoneHeight = nbt.hasKey("ZoneHeight") ? nbt.getFloat("ZoneHeight") : 2.0f;
|
||||
this.particleDensity = nbt.hasKey("ParticleDensity") ? nbt.getFloat("ParticleDensity") : 1.0f;
|
||||
this.particleScale = nbt.hasKey("ParticleScale") ? nbt.getFloat("ParticleScale") : 1.0f;
|
||||
this.animSpeed = nbt.hasKey("AnimSpeed") ? nbt.getFloat("AnimSpeed") : 1.0f;
|
||||
this.lightningDensity = nbt.hasKey("LightningDensity") ? nbt.getFloat("LightningDensity") : 1.0f;
|
||||
this.deathWorldTime = nbt.hasKey("DeathWorldTime") ? nbt.getLong("DeathWorldTime") : -1;
|
||||
|
||||
// Visual fields — backward compat with old ZoneStyle format
|
||||
if (nbt.hasKey("ZoneStyle")) {
|
||||
// OLD FORMAT: apply preset defaults, then overlay CustomVisual
|
||||
applyPresetDefaults(nbt.getString("ZoneStyle"));
|
||||
if (nbt.hasKey("CustomVisual")) {
|
||||
NBTTagCompound cv = nbt.getCompoundTag("CustomVisual");
|
||||
this.groundFill = !cv.hasKey("GroundFill") || cv.getBoolean("GroundFill");
|
||||
this.groundAlpha = cv.hasKey("GroundAlpha") ? cv.getFloat("GroundAlpha") : 0.25f;
|
||||
this.rings = !cv.hasKey("Rings") || cv.getBoolean("Rings");
|
||||
this.ringCount = cv.hasKey("RingCount") ? cv.getInteger("RingCount") : 3;
|
||||
this.border = !cv.hasKey("Border") || cv.getBoolean("Border");
|
||||
this.borderSpeed = cv.hasKey("BorderSpeed") ? cv.getFloat("BorderSpeed") : 1.0f;
|
||||
this.accents = !cv.hasKey("Accents") || cv.getBoolean("Accents");
|
||||
this.accentStyle = cv.hasKey("AccentStyle") ? cv.getInteger("AccentStyle") : 0;
|
||||
this.lightning = cv.hasKey("Lightning") && cv.getBoolean("Lightning");
|
||||
this.particles = !cv.hasKey("Particles") || cv.getBoolean("Particles");
|
||||
this.particleMotion = cv.hasKey("ParticleMotion") ? cv.getInteger("ParticleMotion") : 0;
|
||||
this.particleDir = cv.hasKey("ParticleDir") ? cv.getString("ParticleDir") : "";
|
||||
this.particleSize = cv.hasKey("ParticleSize") ? cv.getInteger("ParticleSize") : 32;
|
||||
this.particleGlow = !cv.hasKey("ParticleGlow") || cv.getBoolean("ParticleGlow");
|
||||
}
|
||||
} else {
|
||||
// NEW FORMAT: read individual top-level keys
|
||||
this.groundFill = !nbt.hasKey("GroundFill") || nbt.getBoolean("GroundFill");
|
||||
this.groundAlpha = nbt.hasKey("GroundAlpha") ? nbt.getFloat("GroundAlpha") : 0.25f;
|
||||
this.rings = !nbt.hasKey("Rings") || nbt.getBoolean("Rings");
|
||||
this.ringCount = nbt.hasKey("RingCount") ? nbt.getInteger("RingCount") : 3;
|
||||
this.border = !nbt.hasKey("Border") || nbt.getBoolean("Border");
|
||||
this.borderSpeed = nbt.hasKey("BorderSpeed") ? nbt.getFloat("BorderSpeed") : 1.0f;
|
||||
this.accents = !nbt.hasKey("Accents") || nbt.getBoolean("Accents");
|
||||
this.accentStyle = nbt.hasKey("AccentStyle") ? nbt.getInteger("AccentStyle") : 0;
|
||||
this.lightning = nbt.hasKey("Lightning") && nbt.getBoolean("Lightning");
|
||||
this.particles = !nbt.hasKey("Particles") || nbt.getBoolean("Particles");
|
||||
this.particleMotion = nbt.hasKey("ParticleMotion") ? nbt.getInteger("ParticleMotion") : 0;
|
||||
this.particleDir = nbt.hasKey("ParticleDir") ? nbt.getString("ParticleDir") : "";
|
||||
this.particleSize = nbt.hasKey("ParticleSize") ? nbt.getInteger("ParticleSize") : 32;
|
||||
this.particleGlow = !nbt.hasKey("ParticleGlow") || nbt.getBoolean("ParticleGlow");
|
||||
}
|
||||
|
||||
// Trap
|
||||
this.armTime = nbt.getInteger("ArmTime");
|
||||
this.triggerRadius = nbt.getFloat("TriggerRadius");
|
||||
this.maxTriggers = nbt.getInteger("MaxTriggers");
|
||||
this.triggerCooldown = nbt.getInteger("TriggerCooldown");
|
||||
this.damage = nbt.getFloat("Damage");
|
||||
this.damageRadius = nbt.getFloat("DamageRadius");
|
||||
this.knockback = nbt.getFloat("Knockback");
|
||||
|
||||
// Hazard
|
||||
this.damagePerSecond = nbt.getFloat("DamagePerSecond");
|
||||
this.damageInterval = nbt.getInteger("DamageInterval");
|
||||
this.ignoreInvulnFrames = nbt.getBoolean("IgnoreInvuln");
|
||||
this.affectsCaster = nbt.getBoolean("AffectsCaster");
|
||||
|
||||
// Effects
|
||||
effects.clear();
|
||||
if (nbt.hasKey("Effects")) {
|
||||
NBTTagList effectList = nbt.getTagList("Effects", 10);
|
||||
for (int i = 0; i < effectList.tagCount(); i++) {
|
||||
AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i));
|
||||
if (effect != null && effect.isValid()) {
|
||||
effects.add(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writeEntityToNBT(NBTTagCompound nbt) {
|
||||
nbt.setInteger("ZoneType", zoneType.ordinal());
|
||||
nbt.setString("ShapeName", shape.name());
|
||||
nbt.setInteger("OwnerId", ownerEntityId);
|
||||
nbt.setFloat("Radius", radius);
|
||||
nbt.setInteger("Duration", durationTicks);
|
||||
nbt.setInteger("MaxTicks", maxTicks);
|
||||
nbt.setInteger("InnerColor", innerColor);
|
||||
nbt.setInteger("OuterColor", outerColor);
|
||||
nbt.setBoolean("OuterColorEnabled", outerColorEnabled);
|
||||
nbt.setFloat("ZoneHeight", zoneHeight);
|
||||
nbt.setFloat("ParticleDensity", particleDensity);
|
||||
nbt.setFloat("ParticleScale", particleScale);
|
||||
nbt.setFloat("AnimSpeed", animSpeed);
|
||||
nbt.setFloat("LightningDensity", lightningDensity);
|
||||
nbt.setLong("DeathWorldTime", deathWorldTime);
|
||||
|
||||
// Visual fields — always written as top-level keys
|
||||
nbt.setBoolean("GroundFill", groundFill);
|
||||
nbt.setFloat("GroundAlpha", groundAlpha);
|
||||
nbt.setBoolean("Rings", rings);
|
||||
nbt.setInteger("RingCount", ringCount);
|
||||
nbt.setBoolean("Border", border);
|
||||
nbt.setFloat("BorderSpeed", borderSpeed);
|
||||
nbt.setBoolean("Accents", accents);
|
||||
nbt.setInteger("AccentStyle", accentStyle);
|
||||
nbt.setBoolean("Lightning", lightning);
|
||||
nbt.setBoolean("Particles", particles);
|
||||
nbt.setInteger("ParticleMotion", particleMotion);
|
||||
nbt.setString("ParticleDir", particleDir);
|
||||
nbt.setInteger("ParticleSize", particleSize);
|
||||
nbt.setBoolean("ParticleGlow", particleGlow);
|
||||
|
||||
// Trap
|
||||
nbt.setInteger("ArmTime", armTime);
|
||||
nbt.setFloat("TriggerRadius", triggerRadius);
|
||||
nbt.setInteger("MaxTriggers", maxTriggers);
|
||||
nbt.setInteger("TriggerCooldown", triggerCooldown);
|
||||
nbt.setFloat("Damage", damage);
|
||||
nbt.setFloat("DamageRadius", damageRadius);
|
||||
nbt.setFloat("Knockback", knockback);
|
||||
|
||||
// Hazard
|
||||
nbt.setFloat("DamagePerSecond", damagePerSecond);
|
||||
nbt.setInteger("DamageInterval", damageInterval);
|
||||
nbt.setBoolean("IgnoreInvuln", ignoreInvulnFrames);
|
||||
nbt.setBoolean("AffectsCaster", affectsCaster);
|
||||
|
||||
// Effects
|
||||
NBTTagList effectList = new NBTTagList();
|
||||
for (AbilityEffect effect : effects) {
|
||||
effectList.appendTag(effect.writeNBT());
|
||||
}
|
||||
nbt.setTag("Effects", effectList);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SPAWN DATA (ByteBuf for client sync)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
public void writeSpawnData(ByteBuf buffer) {
|
||||
buffer.writeInt(zoneType.ordinal());
|
||||
ByteBufUtils.writeUTF8String(buffer, shape.name());
|
||||
buffer.writeInt(ownerEntityId);
|
||||
buffer.writeFloat(radius);
|
||||
buffer.writeInt(durationTicks);
|
||||
buffer.writeInt(maxTicks);
|
||||
buffer.writeInt(innerColor);
|
||||
buffer.writeInt(outerColor);
|
||||
buffer.writeBoolean(outerColorEnabled);
|
||||
buffer.writeFloat(zoneHeight);
|
||||
buffer.writeFloat(particleDensity);
|
||||
buffer.writeFloat(particleScale);
|
||||
buffer.writeFloat(animSpeed);
|
||||
buffer.writeFloat(lightningDensity);
|
||||
|
||||
// Visual fields — always written individually
|
||||
buffer.writeBoolean(groundFill);
|
||||
buffer.writeFloat(groundAlpha);
|
||||
buffer.writeBoolean(rings);
|
||||
buffer.writeInt(ringCount);
|
||||
buffer.writeBoolean(border);
|
||||
buffer.writeFloat(borderSpeed);
|
||||
buffer.writeBoolean(accents);
|
||||
buffer.writeInt(accentStyle);
|
||||
buffer.writeBoolean(lightning);
|
||||
buffer.writeBoolean(particles);
|
||||
buffer.writeInt(particleMotion);
|
||||
ByteBufUtils.writeUTF8String(buffer, particleDir);
|
||||
buffer.writeInt(particleSize);
|
||||
buffer.writeBoolean(particleGlow);
|
||||
|
||||
// Trap-specific (client needs armTime for visual state)
|
||||
buffer.writeInt(armTime);
|
||||
|
||||
// Effects via NBT in buffer
|
||||
NBTTagCompound effectsNbt = new NBTTagCompound();
|
||||
NBTTagList effectList = new NBTTagList();
|
||||
for (AbilityEffect effect : effects) {
|
||||
effectList.appendTag(effect.writeNBT());
|
||||
}
|
||||
effectsNbt.setTag("Effects", effectList);
|
||||
ByteBufUtils.writeTag(buffer, effectsNbt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void readSpawnData(ByteBuf buffer) {
|
||||
this.zoneType = ZoneType.values()[buffer.readInt()];
|
||||
String shapeName = ByteBufUtils.readUTF8String(buffer);
|
||||
try {
|
||||
this.shape = ZoneShape.valueOf(shapeName);
|
||||
} catch (Exception e) {
|
||||
this.shape = ZoneShape.CIRCLE;
|
||||
}
|
||||
this.ownerEntityId = buffer.readInt();
|
||||
this.radius = buffer.readFloat();
|
||||
this.durationTicks = buffer.readInt();
|
||||
this.maxTicks = buffer.readInt();
|
||||
this.innerColor = buffer.readInt();
|
||||
this.outerColor = buffer.readInt();
|
||||
this.outerColorEnabled = buffer.readBoolean();
|
||||
this.zoneHeight = buffer.readFloat();
|
||||
this.particleDensity = buffer.readFloat();
|
||||
this.particleScale = buffer.readFloat();
|
||||
this.animSpeed = buffer.readFloat();
|
||||
this.lightningDensity = buffer.readFloat();
|
||||
|
||||
// Visual fields — always read individually
|
||||
this.groundFill = buffer.readBoolean();
|
||||
this.groundAlpha = buffer.readFloat();
|
||||
this.rings = buffer.readBoolean();
|
||||
this.ringCount = buffer.readInt();
|
||||
this.border = buffer.readBoolean();
|
||||
this.borderSpeed = buffer.readFloat();
|
||||
this.accents = buffer.readBoolean();
|
||||
this.accentStyle = buffer.readInt();
|
||||
this.lightning = buffer.readBoolean();
|
||||
this.particles = buffer.readBoolean();
|
||||
this.particleMotion = buffer.readInt();
|
||||
this.particleDir = ByteBufUtils.readUTF8String(buffer);
|
||||
this.particleSize = buffer.readInt();
|
||||
this.particleGlow = buffer.readBoolean();
|
||||
|
||||
this.armTime = buffer.readInt();
|
||||
|
||||
// Effects via NBT
|
||||
NBTTagCompound effectsNbt = ByteBufUtils.readTag(buffer);
|
||||
effects.clear();
|
||||
if (effectsNbt != null && effectsNbt.hasKey("Effects")) {
|
||||
NBTTagList effectList = effectsNbt.getTagList("Effects", 10);
|
||||
for (int i = 0; i < effectList.tagCount(); i++) {
|
||||
AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i));
|
||||
if (effect != null && effect.isValid()) {
|
||||
effects.add(effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.BuiltInAbilityGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket;
|
||||
import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket;
|
||||
@@ -516,6 +517,7 @@ public class PacketHandler {
|
||||
REQUEST_PACKET.registerPacket(new CustomAbilityGetPacket());
|
||||
REQUEST_PACKET.registerPacket(new CustomAbilityRemovePacket());
|
||||
REQUEST_PACKET.registerPacket(new CustomAbilitySavePacket());
|
||||
REQUEST_PACKET.registerPacket(new BuiltInAbilityGetPacket());
|
||||
|
||||
// Other Packets
|
||||
REQUEST_PACKET.registerPacket(new IsGuiOpenInform());
|
||||
|
||||
@@ -205,6 +205,7 @@ public enum EnumRequestPacket {
|
||||
CustomAbilityGet,
|
||||
CustomAbilityRemove,
|
||||
CustomAbilitySave,
|
||||
BuiltInAbilityGet,
|
||||
Hammer,
|
||||
|
||||
RecipeScript,
|
||||
|
||||
@@ -59,6 +59,16 @@ public final class SpecialKeyStatePacket extends AbstractPacket {
|
||||
|
||||
// Trigger ability activation on key press (rising edge)
|
||||
if (keyDown && !wasDown) {
|
||||
// Don't activate if player has a GUI open (not the default inventory)
|
||||
if (player.openContainer != null && !(player.openContainer instanceof net.minecraft.inventory.ContainerPlayer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't activate if player is riding an entity (mount)
|
||||
if (player.ridingEntity != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.abilityData.activateAbility(player);
|
||||
}
|
||||
}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package kamkeel.npcs.network.packets.request.ability;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.network.AbstractPacket;
|
||||
import kamkeel.npcs.network.PacketChannel;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.PacketUtil;
|
||||
import kamkeel.npcs.network.enums.EnumItemPacketType;
|
||||
import kamkeel.npcs.network.enums.EnumRequestPacket;
|
||||
import kamkeel.npcs.network.packets.data.large.GuiDataPacket;
|
||||
import kamkeel.npcs.util.ByteBufUtils;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Request packet to get a specific built-in ability by name.
|
||||
* Built-in abilities are registered presets with fixed configurations.
|
||||
*/
|
||||
public final class BuiltInAbilityGetPacket extends AbstractPacket {
|
||||
public static String packetName = "Request|BuiltInAbilityGet";
|
||||
|
||||
private String abilityName;
|
||||
|
||||
public BuiltInAbilityGetPacket() {
|
||||
}
|
||||
|
||||
public BuiltInAbilityGetPacket(String abilityName) {
|
||||
this.abilityName = abilityName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enum getType() {
|
||||
return EnumRequestPacket.BuiltInAbilityGet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketChannel getChannel() {
|
||||
return PacketHandler.REQUEST_PACKET;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
ByteBufUtils.writeString(out, abilityName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
|
||||
if (!(player instanceof EntityPlayerMP))
|
||||
return;
|
||||
|
||||
if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player))
|
||||
return;
|
||||
|
||||
String name = ByteBufUtils.readString(in);
|
||||
Ability ability = AbilityController.Instance.getAbilityByDisplayName(name);
|
||||
if (ability != null) {
|
||||
NBTTagCompound compound = ability.writeNBT();
|
||||
compound.setBoolean("BuiltIn", true);
|
||||
GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -47,5 +47,6 @@ public final class CustomAbilitiesGetPacket extends AbstractPacket {
|
||||
return;
|
||||
|
||||
NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player);
|
||||
NoppesUtilServer.sendBuiltInAbilitiesData((EntityPlayerMP) player);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ public class AnchorPointHelper {
|
||||
private static final float ARM_HEIGHT = 0.75f;
|
||||
private static final float ABOVE_HEAD_HEIGHT = 1.2f;
|
||||
private static final float CHEST_HEIGHT = 0.65f;
|
||||
private static final float EYE_HEIGHT = 0.85f;
|
||||
|
||||
// Distance in front for FRONT anchor
|
||||
private static final float DEFAULT_FRONT_DISTANCE = 1.0f;
|
||||
@@ -98,11 +99,18 @@ public class AnchorPointHelper {
|
||||
case CHEST:
|
||||
y += height * CHEST_HEIGHT;
|
||||
break;
|
||||
|
||||
case EYE:
|
||||
y += entity.getEyeHeight();
|
||||
break;
|
||||
}
|
||||
|
||||
x += anchorX * scale;
|
||||
y += anchorY * scale;
|
||||
z += anchorZ * scale;
|
||||
// Rotate offsets relative to entity's body yaw
|
||||
// +X = entity's right, +Y = up, +Z = entity's forward
|
||||
Vec3 rotatedOffset = rotateOffsetByYaw(anchorX * scale, anchorY * scale, anchorZ * scale, bodyYaw);
|
||||
x += rotatedOffset.xCoord;
|
||||
y += rotatedOffset.yCoord;
|
||||
z += rotatedOffset.zCoord;
|
||||
|
||||
return Vec3.createVectorHelper(x, y, z);
|
||||
}
|
||||
@@ -246,9 +254,11 @@ public class AnchorPointHelper {
|
||||
double worldY = entity.posY + shoulderHeight - blockY;
|
||||
double worldZ = entity.posZ + worldOffsetZ;
|
||||
|
||||
worldX += anchor.anchorOffsetX * scale;
|
||||
worldY += anchor.anchorOffsetY * scale;
|
||||
worldZ += anchor.anchorOffsetZ * scale;
|
||||
// Rotate offsets relative to entity's body yaw
|
||||
Vec3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw);
|
||||
worldX += rotatedOffset.xCoord;
|
||||
worldY += rotatedOffset.yCoord;
|
||||
worldZ += rotatedOffset.zCoord;
|
||||
|
||||
return Vec3.createVectorHelper(worldX, worldY, worldZ);
|
||||
}
|
||||
@@ -317,9 +327,11 @@ public class AnchorPointHelper {
|
||||
double y = entity.posY + entity.height * FALLBACK_ARM_HEIGHT + armLengthOffset;
|
||||
double z = entity.posZ + lateralZ + forwardZ;
|
||||
|
||||
x += anchor.anchorOffsetX * scale;
|
||||
y += anchor.anchorOffsetY * scale;
|
||||
z += anchor.anchorOffsetZ * scale;
|
||||
// Rotate offsets relative to entity's body yaw
|
||||
Vec3 rotatedOffset = rotateOffsetByYaw(anchor.anchorOffsetX * scale, anchor.anchorOffsetY * scale, anchor.anchorOffsetZ * scale, bodyYaw);
|
||||
x += rotatedOffset.xCoord;
|
||||
y += rotatedOffset.yCoord;
|
||||
z += rotatedOffset.zCoord;
|
||||
|
||||
return Vec3.createVectorHelper(x, y, z);
|
||||
}
|
||||
@@ -340,8 +352,23 @@ public class AnchorPointHelper {
|
||||
return ABOVE_HEAD_HEIGHT;
|
||||
case CHEST:
|
||||
return CHEST_HEIGHT;
|
||||
case EYE:
|
||||
return EYE_HEIGHT;
|
||||
default:
|
||||
return FRONT_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate an offset vector by the entity's body yaw so offsets are entity-relative.
|
||||
* +X = entity's right, +Y = up, +Z = entity's forward.
|
||||
* @param bodyYawRad body yaw in radians
|
||||
*/
|
||||
private static Vec3 rotateOffsetByYaw(double offsetX, double offsetY, double offsetZ, float bodyYawRad) {
|
||||
double cos = Math.cos(bodyYawRad);
|
||||
double sin = Math.sin(bodyYawRad);
|
||||
double worldX = -offsetX * cos - offsetZ * sin;
|
||||
double worldZ = -offsetX * sin + offsetZ * cos;
|
||||
return Vec3.createVectorHelper(worldX, offsetY, worldZ);
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@ import java.util.function.Supplier;
|
||||
|
||||
public class Register<T> {
|
||||
public static final Map<String, List<String>> REGISTERED_NAMESPACES = new LinkedHashMap<>();
|
||||
private final String registryKey;
|
||||
public static final Map<String, String> NAMESPACE_DISPLAY_NAMES = new LinkedHashMap<>();
|
||||
protected final String registryKey;
|
||||
protected final String namespace;
|
||||
protected final Map<String, Supplier<T>> entries = new LinkedHashMap<>();
|
||||
|
||||
@@ -28,50 +29,51 @@ public class Register<T> {
|
||||
public static boolean isEmpty(String registryKey) {
|
||||
if (REGISTERED_NAMESPACES.isEmpty()) return true;
|
||||
|
||||
if (REGISTERED_NAMESPACES.get(registryKey).isEmpty()) return true;
|
||||
List<String> list = REGISTERED_NAMESPACES.get(registryKey);
|
||||
if (list == null || list.isEmpty()) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public List<T> catalogue() {
|
||||
List<T> list = new ArrayList<>();
|
||||
for (String regKey : REGISTERED_NAMESPACES.keySet()) {
|
||||
List<String> nameSp = REGISTERED_NAMESPACES.get(regKey);
|
||||
for (String name : nameSp) {
|
||||
List<T> nameSpContent = catalogue(regKey, name);
|
||||
list.addAll(nameSpContent);
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public List<T> catalogue(String namespace) {
|
||||
return catalogue(registryKey, namespace);
|
||||
}
|
||||
|
||||
public List<T> catalogue(String registryKey, String namespace) {
|
||||
List<T> list = new ArrayList<>();
|
||||
for (Map.Entry<String, Supplier<T>> entry : entries.entrySet()) {
|
||||
if (entry.getKey().startsWith(registryKey + "." + namespace + ":"))
|
||||
list.add(entry.getValue().get());
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static class Abilities extends Register<Ability> {
|
||||
protected final Map<String, String> uniqueNames = new LinkedHashMap<>();
|
||||
protected final Set<String> typeOnly = new HashSet<>();
|
||||
|
||||
private Abilities(String namespace) {
|
||||
super("ability", namespace);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Ability register(String factoryName, Supplier<Ability> factory) {
|
||||
String name = registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_");
|
||||
entries.put(name, factory);
|
||||
uniqueNames.put(name, factoryName);
|
||||
return factory.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a type only (shell template, no built-in preset).
|
||||
* The type will appear in the ability type picker but won't be
|
||||
* available as a pre-configured ability by name.
|
||||
*/
|
||||
public Ability registerType(String factoryName, Supplier<Ability> factory) {
|
||||
String name = registryKey + "." + namespace + "." + factoryName.trim().toLowerCase().replaceAll(" ", "_");
|
||||
entries.put(name, factory);
|
||||
uniqueNames.put(name, factoryName);
|
||||
typeOnly.add(name);
|
||||
return factory.get();
|
||||
}
|
||||
|
||||
public void register() {
|
||||
for (Map.Entry<String, Supplier<Ability>> entry : entries.entrySet()) {
|
||||
AbilityController.Instance.registerType(entry.getValue().get().getTypeId(), entry.getValue());
|
||||
AbilityController.Instance.registerType(entry.getKey(), entry.getValue());
|
||||
if (!typeOnly.contains(entry.getKey())) {
|
||||
AbilityController.Instance.registerAbility(uniqueNames.get(entry.getKey()), entry.getValue().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Register.Abilities create(String namespace) {
|
||||
public static Register.Abilities create(String namespace, String displayName) {
|
||||
if (!REGISTERED_NAMESPACES.containsKey("ability"))
|
||||
REGISTERED_NAMESPACES.put("ability", new ArrayList<>());
|
||||
|
||||
@@ -80,6 +82,7 @@ public class Register<T> {
|
||||
}
|
||||
|
||||
REGISTERED_NAMESPACES.get("ability").add(namespace);
|
||||
NAMESPACE_DISPLAY_NAMES.put(namespace, displayName);
|
||||
|
||||
return new Register.Abilities(namespace);
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import kamkeel.npcs.entity.EntityAbilityLaser;
|
||||
import kamkeel.npcs.entity.EntityAbilityBeam;
|
||||
import kamkeel.npcs.entity.EntityAbilitySweeper;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone;
|
||||
import noppes.npcs.entity.old.EntityNPCDwarfFemale;
|
||||
import noppes.npcs.entity.old.EntityNPCDwarfMale;
|
||||
import noppes.npcs.entity.old.EntityNPCElfFemale;
|
||||
@@ -292,6 +293,7 @@ public class CustomNpcs {
|
||||
registerNewEntity(EntityAbilityLaser.class, "abilitylaser", 64, 3, true);
|
||||
registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 64, 3, true);
|
||||
registerNewEntity(EntityAbilitySweeper.class, "abilitysweeper", 64, 3, true);
|
||||
registerNewEntity(EntityAbilityZone.class, "abilityzone", 64, 3, true);
|
||||
|
||||
new RecipeController();
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ public class DataAbilities {
|
||||
/**
|
||||
* Whether the ability system is enabled for this NPC
|
||||
*/
|
||||
public boolean enabled = false;
|
||||
public boolean enabled = true;
|
||||
|
||||
/**
|
||||
* Minimum cooldown ticks between abilities (global minimum)
|
||||
@@ -90,6 +90,24 @@ public class DataAbilities {
|
||||
private transient float lockedRenderYawOffset = 0;
|
||||
private transient float lockedPitch = 0;
|
||||
|
||||
/**
|
||||
* Locked position values for movement lock.
|
||||
* Stored when entering a locked phase, applied after super.onLivingUpdate()
|
||||
* to override any position changes from AI/physics.
|
||||
*/
|
||||
private transient boolean positionLocked = false;
|
||||
private transient double lockedPosX = 0;
|
||||
private transient double lockedPosY = 0;
|
||||
private transient double lockedPosZ = 0;
|
||||
|
||||
/**
|
||||
* Hit scan state - forces NPC to face target.
|
||||
* Set during tickCurrentAbility(), applied after super.onLivingUpdate()
|
||||
* to override AI look helper rotation.
|
||||
*/
|
||||
private transient boolean hitScanActive = false;
|
||||
private transient EntityLivingBase hitScanTarget = null;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// CONSTRUCTOR
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -107,10 +125,33 @@ public class DataAbilities {
|
||||
* Should be called every tick from onLivingUpdate().
|
||||
*/
|
||||
public void tick() {
|
||||
if (!enabled || npc.worldObj.isRemote || npc.isKilled()) {
|
||||
if (npc.worldObj.isRemote) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force-stop any running ability when NPC is killed or abilities are disabled
|
||||
if (!enabled || npc.isKilled()) {
|
||||
if (currentAbility != null) {
|
||||
removeTelegraph(currentAbility);
|
||||
stopAbilityAnimation();
|
||||
releaseRotationControl();
|
||||
releaseLockedPosition();
|
||||
currentAbility = null;
|
||||
lastTarget = null;
|
||||
} else {
|
||||
// No ability but locks could be orphaned
|
||||
if (rotationLocked || hitScanActive) releaseRotationControl();
|
||||
if (positionLocked) releaseLockedPosition();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Safety: release orphaned locks if no ability is actively executing
|
||||
if (currentAbility == null || !currentAbility.isExecuting()) {
|
||||
if (rotationLocked || hitScanActive) releaseRotationControl();
|
||||
if (positionLocked) releaseLockedPosition();
|
||||
}
|
||||
|
||||
// Tick current ability if executing
|
||||
if (currentAbility != null && currentAbility.isExecuting()) {
|
||||
tickCurrentAbility();
|
||||
@@ -133,36 +174,70 @@ public class DataAbilities {
|
||||
// Handle phase-specific logic
|
||||
switch (currentAbility.getPhase()) {
|
||||
case WINDUP:
|
||||
if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) {
|
||||
// Burst replay: re-enter windup - set up telegraph, sound, animation, locks
|
||||
if (currentAbility.isRotationLockedDuringWindup()) {
|
||||
captureLockedRotation();
|
||||
}
|
||||
if (currentAbility.isMovementLockedDuringWindup() && !currentAbility.hasAbilityMovement()) {
|
||||
captureLockedPosition();
|
||||
}
|
||||
spawnTelegraph(currentAbility, target);
|
||||
playAbilitySound(currentAbility.getWindUpSound());
|
||||
playAbilityAnimation(currentAbility.getWindUpAnimation());
|
||||
}
|
||||
currentAbility.onWindUpTick(npc, target, npc.worldObj, currentAbility.getCurrentTick());
|
||||
break;
|
||||
|
||||
case ACTIVE:
|
||||
if (phaseChanged && oldPhase == AbilityPhase.WINDUP) {
|
||||
// Just entered ACTIVE phase - lock telegraph position if it was following
|
||||
if (phaseChanged && (oldPhase == AbilityPhase.WINDUP || oldPhase == AbilityPhase.BURST_DELAY)) {
|
||||
// Just entered ACTIVE phase - lock telegraph positions if they were following
|
||||
// This commits the ability to its current target position
|
||||
TelegraphInstance telegraph = currentAbility.getTelegraphInstance();
|
||||
if (telegraph != null) {
|
||||
for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) {
|
||||
telegraph.lockPosition();
|
||||
}
|
||||
|
||||
// Remove telegraph - it has served its purpose
|
||||
removeTelegraph(currentAbility);
|
||||
// Remove telegraph unless the ability keeps it during active phase
|
||||
// (e.g., sweeping attacks where the telegraph shows the ongoing danger zone)
|
||||
if (!currentAbility.keepTelegraphDuringActive()) {
|
||||
removeTelegraph(currentAbility);
|
||||
}
|
||||
|
||||
// Handle rotation lock transition from WINDUP to ACTIVE
|
||||
// Handle rotation control transition from WINDUP to ACTIVE
|
||||
if (currentAbility.isRotationLockedDuringActive()) {
|
||||
// ACTIVE or WINDUP_AND_ACTIVE: capture (or keep) rotation lock
|
||||
if (!rotationLocked) {
|
||||
captureLockedRotation();
|
||||
}
|
||||
} else if (rotationLocked) {
|
||||
// WINDUP only: release the lock now that windup is over
|
||||
releaseLockedRotation();
|
||||
releaseRotationControl();
|
||||
}
|
||||
|
||||
// Handle position lock transition from WINDUP to ACTIVE
|
||||
if (currentAbility.isMovementLockedDuringActive() && !currentAbility.hasAbilityMovement()) {
|
||||
if (!positionLocked) {
|
||||
captureLockedPosition();
|
||||
}
|
||||
} else if (positionLocked) {
|
||||
releaseLockedPosition();
|
||||
}
|
||||
|
||||
// Burst re-fire without replay: let entity-spawning abilities spawn+fire in one step
|
||||
if (oldPhase == AbilityPhase.BURST_DELAY && !currentAbility.isBurstReplayAnimations()) {
|
||||
currentAbility.onBurstRefire(npc, target, npc.worldObj);
|
||||
}
|
||||
|
||||
// Play active sound and animation
|
||||
playAbilitySound(currentAbility.getActiveSound());
|
||||
// Abilities that keep telegraph during active manage their own sound timing
|
||||
if (!currentAbility.keepTelegraphDuringActive()) {
|
||||
playAbilitySound(currentAbility.getActiveSound());
|
||||
}
|
||||
playAbilityAnimation(currentAbility.getActiveAnimation());
|
||||
|
||||
// Snap NPC to face target before execute so startMoving() reads correct rotation
|
||||
if (currentAbility.isHitScanForCurrentPhase() && target != null) {
|
||||
faceTarget(target);
|
||||
}
|
||||
|
||||
// Fire execute event (cancelable)
|
||||
AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent(
|
||||
npc.wrappedNPC, currentAbility, target);
|
||||
@@ -187,6 +262,31 @@ public class DataAbilities {
|
||||
handleAbilityCompletion(target);
|
||||
return;
|
||||
}
|
||||
|
||||
// Auto-complete for burst overlap mode (entities fly independently)
|
||||
// This allows entity-spawning abilities to immediately chain next burst
|
||||
// without each type needing to check burst state in their onActiveTick()
|
||||
// Wait until all staggered projectiles have been fired before auto-completing
|
||||
if (currentAbility.isBurstEnabled() && currentAbility.isBurstOverlap()
|
||||
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
|
||||
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
|
||||
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
|
||||
currentAbility.signalCompletion();
|
||||
}
|
||||
|
||||
// Check if ability entered burst delay during onActiveTick or overlap auto-complete
|
||||
if (currentAbility.getPhase() == AbilityPhase.BURST_DELAY) {
|
||||
// Release all locks - free movement/rotation during delay
|
||||
if (rotationLocked) releaseRotationControl();
|
||||
if (positionLocked) releaseLockedPosition();
|
||||
if (hitScanActive) releaseRotationControl();
|
||||
}
|
||||
break;
|
||||
|
||||
case BURST_DELAY:
|
||||
// Free movement and rotation during burst delay
|
||||
if (rotationLocked) releaseRotationControl();
|
||||
if (positionLocked) releaseLockedPosition();
|
||||
break;
|
||||
|
||||
case DAZED:
|
||||
@@ -199,6 +299,15 @@ public class DataAbilities {
|
||||
break;
|
||||
}
|
||||
|
||||
// Update hit scan state - actual facing is deferred to applyRotationControl()
|
||||
// which runs AFTER super.onLivingUpdate() to override AI look helper
|
||||
if (currentAbility != null && currentAbility.isExecuting()
|
||||
&& currentAbility.isHitScanForCurrentPhase() && target != null) {
|
||||
enableHitScan(target);
|
||||
} else if (hitScanActive) {
|
||||
releaseRotationControl();
|
||||
}
|
||||
|
||||
// Apply movement control if ability is still executing
|
||||
if (currentAbility != null && currentAbility.isExecuting()) {
|
||||
applyMovementControl();
|
||||
@@ -235,6 +344,9 @@ public class DataAbilities {
|
||||
private void handleAbilityCompletion(EntityLivingBase target) {
|
||||
if (currentAbility == null) return;
|
||||
|
||||
// Remove any remaining telegraphs (for abilities that keep telegraph during active)
|
||||
removeTelegraph(currentAbility);
|
||||
|
||||
// Call onComplete callback
|
||||
currentAbility.onComplete(npc, target);
|
||||
|
||||
@@ -243,8 +355,9 @@ public class DataAbilities {
|
||||
npc.wrappedNPC, currentAbility, target);
|
||||
NpcAPI.EVENT_BUS.post(completeEvent);
|
||||
|
||||
// Release rotation lock before cleanup
|
||||
releaseLockedRotation();
|
||||
// Release locks before cleanup
|
||||
releaseRotationControl();
|
||||
releaseLockedPosition();
|
||||
|
||||
// Clean up and roll cooldown (this sets currentAbility to null)
|
||||
onAbilityComplete();
|
||||
@@ -259,8 +372,9 @@ public class DataAbilities {
|
||||
// Stop any ability animation
|
||||
stopAbilityAnimation();
|
||||
|
||||
// Release rotation lock
|
||||
releaseLockedRotation();
|
||||
// Release locks
|
||||
releaseRotationControl();
|
||||
releaseLockedPosition();
|
||||
|
||||
// Calculate cooldown: random(min, max) + ability offset
|
||||
int abilityCooldownOffset = currentAbility.getCooldownTicks();
|
||||
@@ -349,6 +463,11 @@ public class DataAbilities {
|
||||
* Check if a specific ability is eligible for use.
|
||||
*/
|
||||
private boolean isAbilityEligible(Ability ability, EntityLivingBase target) {
|
||||
// Check UserType allows NPCs
|
||||
if (!ability.getAllowedBy().allowsNpc()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check enabled
|
||||
if (!ability.isEnabled()) {
|
||||
return false;
|
||||
@@ -399,6 +518,9 @@ public class DataAbilities {
|
||||
if (ability.isRotationLockedDuringWindup()) {
|
||||
captureLockedRotation();
|
||||
}
|
||||
if (ability.isMovementLockedDuringWindup() && !ability.hasAbilityMovement()) {
|
||||
captureLockedPosition();
|
||||
}
|
||||
spawnTelegraph(ability, target);
|
||||
playAbilitySound(ability.getWindUpSound());
|
||||
playAbilityAnimation(ability.getWindUpAnimation());
|
||||
@@ -416,6 +538,9 @@ public class DataAbilities {
|
||||
if (ability.isRotationLockedDuringActive()) {
|
||||
captureLockedRotation();
|
||||
}
|
||||
if (ability.isMovementLockedDuringActive() && !ability.hasAbilityMovement()) {
|
||||
captureLockedPosition();
|
||||
}
|
||||
|
||||
// Play active sound and animation
|
||||
playAbilitySound(ability.getActiveSound());
|
||||
@@ -486,26 +611,27 @@ public class DataAbilities {
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn and send telegraph for an ability.
|
||||
* Spawn and send telegraphs for an ability.
|
||||
*/
|
||||
private void spawnTelegraph(Ability ability, EntityLivingBase target) {
|
||||
TelegraphInstance telegraph = ability.createTelegraph(npc, target);
|
||||
if (telegraph != null) {
|
||||
ability.setTelegraphInstance(telegraph);
|
||||
// Send to all nearby players
|
||||
TelegraphSpawnPacket.sendToTracking(telegraph, npc);
|
||||
List<TelegraphInstance> telegraphs = ability.createTelegraphs(npc, target);
|
||||
if (!telegraphs.isEmpty()) {
|
||||
ability.setTelegraphInstances(telegraphs);
|
||||
for (TelegraphInstance telegraph : telegraphs) {
|
||||
TelegraphSpawnPacket.sendToTracking(telegraph, npc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove telegraph when ability ends or is interrupted.
|
||||
* Remove telegraphs when ability ends or is interrupted.
|
||||
*/
|
||||
private void removeTelegraph(Ability ability) {
|
||||
TelegraphInstance telegraph = ability.getTelegraphInstance();
|
||||
if (telegraph != null) {
|
||||
List<TelegraphInstance> telegraphs = ability.getTelegraphInstances();
|
||||
for (TelegraphInstance telegraph : telegraphs) {
|
||||
TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), npc);
|
||||
ability.setTelegraphInstance(null);
|
||||
}
|
||||
ability.setTelegraphInstances(null);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -625,8 +751,9 @@ public class DataAbilities {
|
||||
// Stop any ability animation
|
||||
stopAbilityAnimation();
|
||||
|
||||
// Release rotation lock
|
||||
releaseLockedRotation();
|
||||
// Release locks
|
||||
releaseRotationControl();
|
||||
releaseLockedPosition();
|
||||
|
||||
// Fire interrupt event
|
||||
AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent(
|
||||
@@ -648,7 +775,8 @@ public class DataAbilities {
|
||||
if (currentAbility != null) {
|
||||
removeTelegraph(currentAbility);
|
||||
stopAbilityAnimation();
|
||||
releaseLockedRotation();
|
||||
releaseRotationControl();
|
||||
releaseLockedPosition();
|
||||
currentAbility.interrupt();
|
||||
currentAbility = null;
|
||||
lastTarget = null;
|
||||
@@ -668,6 +796,11 @@ public class DataAbilities {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check UserType allows NPCs
|
||||
if (!ability.getAllowedBy().allowsNpc()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stop any currently executing ability
|
||||
stopCurrentAbility();
|
||||
|
||||
@@ -940,15 +1073,15 @@ public class DataAbilities {
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ROTATION LOCKING
|
||||
// ROTATION CONTROL
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Bit flag for rotation lock in the existing data watcher slot 15 bitfield */
|
||||
private static final int ROTATION_LOCKED_FLAG = 16;
|
||||
/** Bit flag for rotation control (LOCKED or TRACK) in data watcher slot 15 */
|
||||
private static final int ROTATION_CONTROLLED_FLAG = 16;
|
||||
|
||||
/**
|
||||
* Capture current rotation values to lock NPC's look direction.
|
||||
* Called when entering a locked phase (WINDUP or ACTIVE depending on LockMovementType).
|
||||
* Called when entering a LOCKED rotation phase.
|
||||
*/
|
||||
private void captureLockedRotation() {
|
||||
lockedYaw = npc.rotationYaw;
|
||||
@@ -956,41 +1089,79 @@ public class DataAbilities {
|
||||
lockedRenderYawOffset = npc.renderYawOffset;
|
||||
lockedPitch = npc.rotationPitch;
|
||||
rotationLocked = true;
|
||||
npc.setBoolFlag(true, ROTATION_LOCKED_FLAG);
|
||||
npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the rotation lock.
|
||||
* Called when leaving the locked phase or ability completes.
|
||||
* Release rotation control (both locked and hit scan).
|
||||
*/
|
||||
private void releaseLockedRotation() {
|
||||
private void releaseRotationControl() {
|
||||
rotationLocked = false;
|
||||
npc.setBoolFlag(false, ROTATION_LOCKED_FLAG);
|
||||
hitScanActive = false;
|
||||
hitScanTarget = null;
|
||||
npc.setBoolFlag(false, ROTATION_CONTROLLED_FLAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply locked rotation values to the NPC.
|
||||
* Called AFTER super.onLivingUpdate() in EntityNPCInterface to override
|
||||
* any rotation changes made by the look helper, AI tasks, or body smoothing.
|
||||
* Runs on both client and server.
|
||||
* Enable hit scan tracking for the given target.
|
||||
*/
|
||||
public void applyLockedRotation() {
|
||||
private void enableHitScan(EntityLivingBase target) {
|
||||
if (!hitScanActive) {
|
||||
hitScanActive = true;
|
||||
npc.setBoolFlag(true, ROTATION_CONTROLLED_FLAG);
|
||||
}
|
||||
hitScanTarget = target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Snap the NPC to face the target instantly.
|
||||
*/
|
||||
private void faceTarget(EntityLivingBase target) {
|
||||
double dx = target.posX - npc.posX;
|
||||
double dz = target.posZ - npc.posZ;
|
||||
double dy = (target.posY + target.getEyeHeight() * 0.5) - (npc.posY + npc.getEyeHeight());
|
||||
double distXZ = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
float yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI);
|
||||
float pitch = (float) (-(Math.atan2(dy, distXZ)) * 180.0 / Math.PI);
|
||||
|
||||
npc.rotationYaw = yaw;
|
||||
npc.rotationYawHead = yaw;
|
||||
npc.renderYawOffset = yaw;
|
||||
npc.rotationPitch = pitch;
|
||||
npc.prevRotationYaw = yaw;
|
||||
npc.prevRotationYawHead = yaw;
|
||||
npc.prevRenderYawOffset = yaw;
|
||||
npc.prevRotationPitch = pitch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply rotation control after super.onLivingUpdate() and super.onUpdate().
|
||||
* Handles both LOCKED (freeze at captured values) and TRACK (face target) modes.
|
||||
*
|
||||
* Server: computes the correct rotation (locked values or target facing).
|
||||
* Client: trusts the server-synced rotation values and prevents body smoothing override.
|
||||
*/
|
||||
public void applyRotationControl() {
|
||||
if (npc.worldObj.isRemote) {
|
||||
// Client-side: check bit 16 in the existing flags data watcher (slot 15)
|
||||
boolean flagActive = npc.getBoolFlag(ROTATION_LOCKED_FLAG);
|
||||
if (!flagActive) {
|
||||
// Client: check single flag for any rotation control
|
||||
if (!npc.getBoolFlag(ROTATION_CONTROLLED_FLAG)) {
|
||||
rotationLocked = false;
|
||||
return;
|
||||
}
|
||||
// First tick the flag is active: capture current rotation values
|
||||
if (!rotationLocked) {
|
||||
lockedYaw = npc.rotationYaw;
|
||||
lockedYawHead = npc.rotationYawHead;
|
||||
lockedRenderYawOffset = npc.renderYawOffset;
|
||||
lockedPitch = npc.rotationPitch;
|
||||
rotationLocked = true;
|
||||
}
|
||||
// Force all rotation values back to locked state
|
||||
// Trust server rotation, prevent body smoothing from overriding
|
||||
npc.prevRotationYaw = npc.rotationYaw;
|
||||
npc.prevRotationYawHead = npc.rotationYawHead;
|
||||
npc.renderYawOffset = npc.rotationYawHead;
|
||||
npc.prevRenderYawOffset = npc.rotationYawHead;
|
||||
npc.prevRotationPitch = npc.rotationPitch;
|
||||
return;
|
||||
}
|
||||
|
||||
// Server: apply the appropriate rotation
|
||||
if (hitScanActive && hitScanTarget != null && !hitScanTarget.isDead) {
|
||||
faceTarget(hitScanTarget);
|
||||
} else if (rotationLocked) {
|
||||
npc.rotationYaw = lockedYaw;
|
||||
npc.rotationYawHead = lockedYawHead;
|
||||
npc.renderYawOffset = lockedRenderYawOffset;
|
||||
@@ -999,32 +1170,89 @@ public class DataAbilities {
|
||||
npc.prevRotationYawHead = lockedYawHead;
|
||||
npc.prevRenderYawOffset = lockedRenderYawOffset;
|
||||
npc.prevRotationPitch = lockedPitch;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rotation is currently controlled (locked or tracking target).
|
||||
* Used by AI tasks to determine if they should try to change NPC look direction.
|
||||
*/
|
||||
public boolean isRotationLocked() {
|
||||
return rotationLocked || hitScanActive;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// POSITION LOCKING
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/** Bit flag for position lock in data watcher slot 15 */
|
||||
private static final int POSITION_LOCKED_FLAG = 32;
|
||||
|
||||
/**
|
||||
* Capture current position to lock NPC in place.
|
||||
* Called when entering a phase with movement lock.
|
||||
*/
|
||||
private void captureLockedPosition() {
|
||||
lockedPosX = npc.posX;
|
||||
lockedPosY = npc.posY;
|
||||
lockedPosZ = npc.posZ;
|
||||
positionLocked = true;
|
||||
npc.setBoolFlag(true, POSITION_LOCKED_FLAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release the position lock.
|
||||
* Called when leaving the locked phase or ability completes.
|
||||
*/
|
||||
private void releaseLockedPosition() {
|
||||
positionLocked = false;
|
||||
npc.setBoolFlag(false, POSITION_LOCKED_FLAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply locked position values to the NPC.
|
||||
* Called AFTER super.onLivingUpdate() in EntityNPCInterface to override
|
||||
* any position changes from AI pathfinding, physics, or entity collisions.
|
||||
* Runs on both client and server.
|
||||
*/
|
||||
public void applyLockedPosition() {
|
||||
if (npc.worldObj.isRemote) {
|
||||
// Client-side: check flag via data watcher
|
||||
boolean flagActive = npc.getBoolFlag(POSITION_LOCKED_FLAG);
|
||||
if (!flagActive) {
|
||||
positionLocked = false;
|
||||
return;
|
||||
}
|
||||
// First tick the flag is active: capture current position
|
||||
if (!positionLocked) {
|
||||
lockedPosX = npc.posX;
|
||||
lockedPosY = npc.posY;
|
||||
lockedPosZ = npc.posZ;
|
||||
positionLocked = true;
|
||||
}
|
||||
// Snap position back and set prev to prevent interpolation jitter
|
||||
npc.setPosition(lockedPosX, lockedPosY, lockedPosZ);
|
||||
npc.prevPosX = lockedPosX;
|
||||
npc.prevPosY = lockedPosY;
|
||||
npc.prevPosZ = lockedPosZ;
|
||||
npc.motionX = 0;
|
||||
npc.motionY = 0;
|
||||
npc.motionZ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Server-side: use stored locked values
|
||||
if (!rotationLocked) {
|
||||
if (!positionLocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Force rotation back to locked values
|
||||
npc.rotationYaw = lockedYaw;
|
||||
npc.rotationYawHead = lockedYawHead;
|
||||
npc.renderYawOffset = lockedRenderYawOffset;
|
||||
npc.rotationPitch = lockedPitch;
|
||||
|
||||
// Also set prev values to prevent interpolation jitter
|
||||
npc.prevRotationYaw = lockedYaw;
|
||||
npc.prevRotationYawHead = lockedYawHead;
|
||||
npc.prevRenderYawOffset = lockedRenderYawOffset;
|
||||
npc.prevRotationPitch = lockedPitch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if rotation is currently locked.
|
||||
*/
|
||||
public boolean isRotationLocked() {
|
||||
return rotationLocked;
|
||||
npc.setPosition(lockedPosX, lockedPosY, lockedPosZ);
|
||||
npc.prevPosX = lockedPosX;
|
||||
npc.prevPosY = lockedPosY;
|
||||
npc.prevPosZ = lockedPosZ;
|
||||
npc.motionX = 0;
|
||||
npc.motionY = 0;
|
||||
npc.motionZ = 0;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -2,6 +2,7 @@ package noppes.npcs;
|
||||
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.controllers.SyncController;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.packets.data.ParticlePacket;
|
||||
@@ -780,18 +781,38 @@ public class NoppesUtilServer {
|
||||
if (controller != null) {
|
||||
int index = 0;
|
||||
for (String typeId : controller.getTypes()) {
|
||||
// Exclude built-in types from the NPC type list
|
||||
if (controller.isBuiltInType(typeId)) continue;
|
||||
map.put(typeId, index++);
|
||||
}
|
||||
}
|
||||
sendScrollData(player, map, EnumScrollData.ABILITY_TYPES);
|
||||
}
|
||||
|
||||
public static void sendBuiltInAbilitiesData(EntityPlayerMP player) {
|
||||
HashMap<String, Integer> map = new HashMap<String, Integer>();
|
||||
AbilityController controller = AbilityController.Instance;
|
||||
if (controller != null) {
|
||||
int index = 0;
|
||||
for (String name : controller.getAbilityNames()) {
|
||||
Ability ability = controller.getAbility(name);
|
||||
if (ability != null) {
|
||||
map.put(ability.getName(), index++);
|
||||
}
|
||||
}
|
||||
}
|
||||
sendScrollData(player, map, EnumScrollData.BUILTIN_ABILITIES);
|
||||
}
|
||||
|
||||
public static void sendCustomAbilitiesData(EntityPlayerMP player) {
|
||||
HashMap<String, Integer> map = new HashMap<String, Integer>();
|
||||
kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance;
|
||||
if (controller != null) {
|
||||
int index = 0;
|
||||
for (String uuid : controller.getCustomAbilityIds()) {
|
||||
Ability ability = controller.getCustomAbility(uuid);
|
||||
if (ability == null || !ability.getAllowedBy().allowsNpc()) continue;
|
||||
|
||||
String name = controller.getCustomAbilityName(uuid);
|
||||
if (name == null || name.isEmpty()) name = uuid;
|
||||
map.put(name + "\t" + uuid, index++);
|
||||
|
||||
@@ -596,6 +596,12 @@ public class ScriptPlayerEventHandler {
|
||||
}
|
||||
|
||||
if (event.entityLiving instanceof EntityPlayerMP) {
|
||||
// Handle ability damage interactions (Guard counter, damage reduction)
|
||||
PlayerData pData = PlayerData.get((EntityPlayer) event.entityLiving);
|
||||
if (pData != null && pData.abilityData != null && pData.abilityData.isExecutingAbility()) {
|
||||
event.ammount = pData.abilityData.onDamage(event.source, event.ammount);
|
||||
}
|
||||
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.entityLiving);
|
||||
noppes.npcs.scripted.event.player.PlayerEvent.DamagedEvent pevent = new noppes.npcs.scripted.event.player.PlayerEvent.DamagedEvent((IPlayer) NpcAPI.Instance().getIEntity((EntityPlayer) event.entityLiving), source, event.ammount, event.source);
|
||||
cancel = EventHooks.onPlayerDamaged(handler, pevent);
|
||||
|
||||
@@ -153,12 +153,14 @@ import kamkeel.npcs.client.renderer.RenderAbilityDisc;
|
||||
import kamkeel.npcs.client.renderer.RenderAbilityLaser;
|
||||
import kamkeel.npcs.client.renderer.RenderAbilityBeam;
|
||||
import kamkeel.npcs.client.renderer.RenderAbilitySweeper;
|
||||
import kamkeel.npcs.client.renderer.RenderAbilityZone;
|
||||
import kamkeel.npcs.client.renderer.lightning.LightningHandler;
|
||||
import kamkeel.npcs.entity.EntityAbilityOrb;
|
||||
import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import kamkeel.npcs.entity.EntityAbilityLaser;
|
||||
import kamkeel.npcs.entity.EntityAbilityBeam;
|
||||
import kamkeel.npcs.entity.EntityAbilitySweeper;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone;
|
||||
import noppes.npcs.client.renderer.blocks.BlockBannerRenderer;
|
||||
import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer;
|
||||
import noppes.npcs.client.renderer.blocks.BlockBeamRenderer;
|
||||
@@ -272,6 +274,7 @@ public class ClientProxy extends CommonProxy {
|
||||
RenderingRegistry.registerEntityRenderingHandler(EntityAbilityLaser.class, new RenderAbilityLaser());
|
||||
RenderingRegistry.registerEntityRenderingHandler(EntityAbilityBeam.class, new RenderAbilityBeam());
|
||||
RenderingRegistry.registerEntityRenderingHandler(EntityAbilitySweeper.class, new RenderAbilitySweeper());
|
||||
RenderingRegistry.registerEntityRenderingHandler(EntityAbilityZone.class, new RenderAbilityZone());
|
||||
|
||||
RenderingRegistry.registerEntityRenderingHandler(EntityCustomNpc.class, new RenderCustomNpc());
|
||||
|
||||
@@ -511,6 +514,8 @@ public class ClientProxy extends CommonProxy {
|
||||
} else {
|
||||
abilityNpc = new EntityCustomNpc(Minecraft.getMinecraft().theWorld);
|
||||
abilityNpc.display.texture = "customnpcs:textures/entity/humanmale/AnimationBody.png";
|
||||
abilityNpc.height = 1.8f;
|
||||
abilityNpc.width = 0.6f;
|
||||
}
|
||||
return new GuiNpcManageAbilities(abilityNpc, npc != null);
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ public class CustomFX extends EntityFX {
|
||||
this.imageData = ClientCacheHandler.getImageData(this.directory);
|
||||
}
|
||||
|
||||
public void setMaxAge(int age) {
|
||||
this.particleMaxAge = age;
|
||||
}
|
||||
|
||||
public static CustomFX fromScriptedParticle(ScriptParticle particle, World worldObj, Entity entity) {
|
||||
EntityClientPlayerMP player = Minecraft.getMinecraft().thePlayer;
|
||||
if (entity == player) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package noppes.npcs.client.fx;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import net.minecraft.client.particle.EntityFX;
|
||||
import net.minecraft.world.World;
|
||||
|
||||
/**
|
||||
* Simple particle for zone visual effects (Trap/Hazard styles).
|
||||
* Uses vanilla particle atlas with color tinting. Fades out over lifetime.
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class ZoneParticleFX extends EntityFX {
|
||||
|
||||
private final float startAlpha;
|
||||
private final boolean glow;
|
||||
|
||||
public ZoneParticleFX(World world, double x, double y, double z,
|
||||
double motionX, double motionY, double motionZ,
|
||||
float r, float g, float b, float alpha,
|
||||
float scale, int maxAge, float gravity, boolean glow) {
|
||||
super(world, x, y, z, 0, 0, 0);
|
||||
this.motionX = motionX;
|
||||
this.motionY = motionY;
|
||||
this.motionZ = motionZ;
|
||||
this.particleRed = r;
|
||||
this.particleGreen = g;
|
||||
this.particleBlue = b;
|
||||
this.particleAlpha = alpha;
|
||||
this.startAlpha = alpha;
|
||||
this.particleScale = scale;
|
||||
this.particleMaxAge = maxAge;
|
||||
this.particleGravity = gravity;
|
||||
this.glow = glow;
|
||||
this.noClip = true;
|
||||
this.setParticleTextureIndex(0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getBrightnessForRender(float partialTick) {
|
||||
return glow ? 0xF000F0 : super.getBrightnessForRender(partialTick);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate() {
|
||||
this.prevPosX = this.posX;
|
||||
this.prevPosY = this.posY;
|
||||
this.prevPosZ = this.posZ;
|
||||
|
||||
if (this.particleAge++ >= this.particleMaxAge) {
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
|
||||
// Linear fade out
|
||||
float lifeRatio = (float) this.particleAge / (float) this.particleMaxAge;
|
||||
this.particleAlpha = this.startAlpha * (1.0f - lifeRatio);
|
||||
|
||||
// Movement
|
||||
this.motionY -= 0.04D * (double) this.particleGravity;
|
||||
this.posX += this.motionX;
|
||||
this.posY += this.motionY;
|
||||
this.posZ += this.motionZ;
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,5 @@ public class SubGuiNpcDialog extends SubGuiInterface implements ISubGuiListener,
|
||||
}
|
||||
|
||||
public void save() {
|
||||
PacketClient.sendClient(new DialogSavePacket(this.dialogCategoryID, dialog.writeToNBT(new NBTTagCompound()), false));
|
||||
}
|
||||
}
|
||||
@@ -521,8 +521,6 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis
|
||||
|
||||
public void save() {
|
||||
GuiNpcTextField.unfocus();
|
||||
if (dialog.id >= 0)
|
||||
PacketClient.sendClient(new DialogSavePacket(((SubGuiNpcDialog) parent).dialogCategoryID, dialog.writeToNBT(new NBTTagCompound()), false));
|
||||
}
|
||||
|
||||
private DialogImage getSelectedImage() {
|
||||
|
||||
@@ -239,6 +239,5 @@ public class SubGuiNpcQuest extends SubGuiInterface implements ISubGuiListener,
|
||||
}
|
||||
|
||||
public void save() {
|
||||
PacketClient.sendClient(new QuestSavePacket(this.questCategoryID, quest.writeToNBT(new NBTTagCompound()), false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package noppes.npcs.client.gui;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class SubGuiZonePresetSelector extends SubGuiInterface {
|
||||
|
||||
public String selectedPreset = null;
|
||||
|
||||
private static final String[] PRESET_NAMES = {"DEFAULT", "TOXIC", "INFERNO", "ARCANE", "ELECTRIC", "FROST"};
|
||||
private static final String[] PRESET_LABELS = {
|
||||
"ability.preset.default", "ability.preset.toxic", "ability.preset.inferno",
|
||||
"ability.preset.arcane", "ability.preset.electric", "ability.preset.frost"
|
||||
};
|
||||
private static final int[] PRESET_COLORS = {0xCCCCCC, 0x44DD44, 0xFF6611, 0xAA44FF, 0x4488FF, 0x88CCFF};
|
||||
|
||||
public SubGuiZonePresetSelector() {
|
||||
xSize = 170;
|
||||
ySize = 135;
|
||||
setBackground("menubg.png");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
int btnW = 72;
|
||||
int btnH = 20;
|
||||
int gapY = 4;
|
||||
int col1 = guiLeft + 7;
|
||||
int col2 = col1 + btnW + 6;
|
||||
int startY = guiTop + 22;
|
||||
|
||||
for (int i = 0; i < 6; i++) {
|
||||
int x = (i % 2 == 0) ? col1 : col2;
|
||||
int y = startY + (i / 2) * (btnH + gapY);
|
||||
addButton(new GuiNpcButton(i, x, y, btnW, btnH, PRESET_LABELS[i]));
|
||||
}
|
||||
|
||||
int cancelY = startY + 3 * (btnH + gapY) + 4;
|
||||
addButton(new GuiNpcButton(10, guiLeft + xSize / 2 - 36, cancelY, 72, btnH, "gui.cancel"));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void actionPerformed(GuiButton btn) {
|
||||
super.actionPerformed(btn);
|
||||
if (btn.id >= 0 && btn.id < 6) {
|
||||
selectedPreset = PRESET_NAMES[btn.id];
|
||||
close();
|
||||
} else if (btn.id == 10) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawScreen(int mouseX, int mouseY, float f) {
|
||||
super.drawScreen(mouseX, mouseY, f);
|
||||
drawCenteredString(fontRendererObj, StatCollector.translateToLocal("gui.applyPreset"), guiLeft + xSize / 2, guiTop + 8, 0xFFFFFF);
|
||||
for (int i = 0; i < 6; i++) {
|
||||
GuiNpcButton btn = getButton(i);
|
||||
if (btn != null && btn.visible) {
|
||||
int x = btn.xPosition + 4;
|
||||
int y = btn.yPosition + 5;
|
||||
drawRect(x, y, x + 10, y + 10, 0xFF000000 | PRESET_COLORS[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,12 @@ package noppes.npcs.client.gui.advanced;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilitySlot;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import kamkeel.npcs.network.PacketClient;
|
||||
import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket;
|
||||
import kamkeel.npcs.util.Register;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
@@ -57,6 +59,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
private String search = "";
|
||||
private int selectedAbilityIndex = -1;
|
||||
|
||||
// Existing preset names for duplicate checking
|
||||
private final Set<String> existingPresetNames = new HashSet<>();
|
||||
|
||||
// Pending variant selection
|
||||
private String pendingTypeId = null;
|
||||
|
||||
public static int modIndex = 0;
|
||||
public static ScrollType scrollType = ScrollType.CNPC;
|
||||
|
||||
@@ -64,6 +72,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
super(npc);
|
||||
PacketClient.sendClient(new AbilitiesGetAllPacket());
|
||||
PacketClient.sendClient(new AbilitiesNpcGetPacket());
|
||||
PacketClient.sendClient(new CustomAbilitiesGetPacket());
|
||||
}
|
||||
|
||||
public enum ScrollType {
|
||||
@@ -79,7 +88,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
if (Register.isEmpty("ability"))
|
||||
return "modded";
|
||||
|
||||
return Register.REGISTERED_NAMESPACES.get("ability").get(modIndex);
|
||||
String namespace = Register.REGISTERED_NAMESPACES.get("ability").get(modIndex);
|
||||
String displayName = Register.NAMESPACE_DISPLAY_NAMES.get(namespace);
|
||||
return displayName != null ? displayName : namespace;
|
||||
case ALL: return "filter.all";
|
||||
default: return name();
|
||||
}
|
||||
@@ -92,38 +103,38 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
|
||||
int y = guiTop + 10;
|
||||
|
||||
// Enabled checkbox
|
||||
addLabel(new GuiNpcLabel(100, "gui.enabled", guiLeft + 5, y + 5));
|
||||
addButton(new GuiNpcButton(100, guiLeft + 55, y, 50, 20, new String[]{"gui.no", "gui.yes"}, abilitiesEnabled ? 1 : 0));
|
||||
addButton(new GuiNpcButton(50, guiLeft + 5, y, 140, 20, scrollType.toString()));
|
||||
|
||||
// Cooldown range: Min Cooldown and Max Cooldown (spaced apart more)
|
||||
addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 115, y + 5));
|
||||
GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 165, y, 40, 20, "" + minCooldown);
|
||||
addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 210, y + 5));
|
||||
GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 260, y, 40, 20, "" + minCooldown);
|
||||
minField.setIntegersOnly();
|
||||
minField.setMinMaxDefault(0, 10000, 20);
|
||||
addTextField(minField);
|
||||
|
||||
addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 260, y + 5));
|
||||
GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 310, y, 40, 20, "" + maxCooldown);
|
||||
addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 320, y + 5));
|
||||
GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 370, y, 40, 20, "" + maxCooldown);
|
||||
maxField.setIntegersOnly();
|
||||
maxField.setMinMaxDefault(0, 10000, 60);
|
||||
addTextField(maxField);
|
||||
|
||||
y += 28;
|
||||
|
||||
// Enabled checkbox
|
||||
addButton(new GuiNpcButton(100, guiLeft + 334, y + 145, 76, 20, new String[]{"gui.disabled", "gui.enabled"}, abilitiesEnabled ? 1 : 0));
|
||||
getButton(100).packedFGColour = abilitiesEnabled ? 0x00FF00 : 0xFF0000;
|
||||
|
||||
// Left scroll: available ability types
|
||||
addLabel(new GuiNpcLabel(1, "ability.availableTypes", guiLeft + 5, y));
|
||||
if (availableTypesScroll == null) {
|
||||
availableTypesScroll = new GuiCustomScroll(this, 0);
|
||||
availableTypesScroll.setSize(140, 103);
|
||||
availableTypesScroll.setSize(140, 130);
|
||||
}
|
||||
availableTypesScroll.guiLeft = guiLeft + 5;
|
||||
availableTypesScroll.guiTop = y + 12;
|
||||
availableTypesScroll.setList(getFilteredTypeList());
|
||||
addScroll(availableTypesScroll);
|
||||
|
||||
addButton(new GuiNpcButton(50, guiLeft + 5, y + 120, 140, 20, scrollType.toString()));
|
||||
|
||||
// Search bar for types
|
||||
addTextField(new GuiNpcTextField(4, this, fontRendererObj, guiLeft + 5, y + 145, 140, 18, search));
|
||||
|
||||
@@ -131,9 +142,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
addLabel(new GuiNpcLabel(2, "ability.npcAbilities", guiLeft + 210, y));
|
||||
if (npcAbilitiesScroll == null) {
|
||||
npcAbilitiesScroll = new GuiCustomScroll(this, 1);
|
||||
npcAbilitiesScroll.setSize(160, 130);
|
||||
npcAbilitiesScroll.setSize(200, 130);
|
||||
} else {
|
||||
npcAbilitiesScroll.setSize(160, 130);
|
||||
npcAbilitiesScroll.setSize(200, 130);
|
||||
}
|
||||
npcAbilitiesScroll.guiLeft = guiLeft + 210;
|
||||
npcAbilitiesScroll.guiTop = y + 12;
|
||||
@@ -142,11 +153,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
|
||||
// Center buttons: Add/Remove
|
||||
int centerX = guiLeft + 158;
|
||||
addButton(new GuiNpcButton(70, centerX, y + 40, 40, 20, ">>>")); // Add
|
||||
addButton(new GuiNpcButton(71, centerX, y + 62, 40, 20, "<<<")); // Remove
|
||||
addButton(new GuiNpcButton(70, centerX, y + 15, 40, 20, ">>>")); // Add
|
||||
addButton(new GuiNpcButton(71, centerX, y + 37, 40, 20, "<<<")); // Remove
|
||||
getButton(71).setEnabled(selectedAbilityIndex >= 0);
|
||||
|
||||
// Load button (under the add/remove arrows)
|
||||
addButton(new GuiNpcButton(75, centerX, y + 90, 40, 20, "gui.load"));
|
||||
addButton(new GuiNpcButton(75, centerX, y + 60, 40, 20, "gui.load"));
|
||||
|
||||
// On/Off toggle for selected ability (under Load button)
|
||||
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
|
||||
@@ -156,9 +168,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
addButton(toggleBtn);
|
||||
}
|
||||
|
||||
// Check if selected ability is built-in (non-editable)
|
||||
boolean selectedIsBuiltIn = false;
|
||||
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
|
||||
Ability sel = npcSlots.get(selectedAbilityIndex).getAbility();
|
||||
if (sel != null && sel.isBuiltIn()) selectedIsBuiltIn = true;
|
||||
}
|
||||
|
||||
// Right side buttons: Edit and Up/Down carrots with Save button
|
||||
addButton(new GuiNpcButton(72, guiLeft + 210, y + 145, 55, 20, "gui.edit"));
|
||||
getButton(72).setEnabled(selectedAbilityIndex >= 0);
|
||||
getButton(72).setEnabled(selectedAbilityIndex >= 0 && !selectedIsBuiltIn);
|
||||
|
||||
// Up/Down carrot buttons
|
||||
addButton(new GuiNpcButton(73, guiLeft + 270, y + 145, 20, 20, "<"));
|
||||
@@ -167,8 +186,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1);
|
||||
|
||||
// Save button (to the right of carrots, only enabled when ability is selected)
|
||||
addButton(new GuiNpcButton(76, guiLeft + 320, y + 145, 50, 20, "gui.save"));
|
||||
getButton(76).setEnabled(selectedAbilityIndex >= 0);
|
||||
addButton(new GuiNpcButton(76, centerX, y + 82, 40, 20, "gui.save"));
|
||||
getButton(76).setEnabled(selectedAbilityIndex >= 0 && !selectedIsBuiltIn);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -178,6 +197,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
// Enabled toggle
|
||||
if (id == 100) {
|
||||
abilitiesEnabled = ((GuiNpcButton) guibutton).getValue() == 1;
|
||||
initGui();
|
||||
save();
|
||||
return;
|
||||
}
|
||||
@@ -188,8 +208,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
String displayName = availableTypesScroll.getSelected();
|
||||
String typeId = displayNameToTypeId.get(displayName);
|
||||
if (typeId != null) {
|
||||
java.util.List<AbilityVariant> variants = AbilityController.Instance.getVariantsForType(typeId);
|
||||
if (variants.size() > 1) {
|
||||
pendingTypeId = typeId;
|
||||
setSubGui(new SubGuiAbilityVariantSelect(variants));
|
||||
return;
|
||||
}
|
||||
Ability newAbility = AbilityController.Instance.create(typeId);
|
||||
if (newAbility != null) {
|
||||
if (variants.size() == 1) {
|
||||
variants.get(0).apply(newAbility);
|
||||
}
|
||||
newAbility.setId(UUID.randomUUID().toString());
|
||||
npcSlots.add(AbilitySlot.inline(newAbility));
|
||||
selectedAbilityIndex = npcSlots.size() - 1;
|
||||
@@ -276,7 +305,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) {
|
||||
setSubGui(abilityToSave.createConfigGui(this));
|
||||
} else {
|
||||
setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave));
|
||||
setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave, null, existingPresetNames));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,7 +330,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
if (id == 50) {
|
||||
if (scrollType != ScrollType.MODDED) {
|
||||
ScrollType[] values = ScrollType.values();
|
||||
scrollType = values[(scrollType.ordinal() + 1) % values.length];
|
||||
ScrollType next = values[(scrollType.ordinal() + 1) % values.length];
|
||||
// Skip MODDED if no namespaces are registered
|
||||
if (next == ScrollType.MODDED && Register.isEmpty("ability")) {
|
||||
next = ScrollType.ALL;
|
||||
}
|
||||
scrollType = next;
|
||||
} else {
|
||||
List<String> list = Register.REGISTERED_NAMESPACES.get("ability");
|
||||
|
||||
@@ -320,8 +354,12 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
Map<String, Integer> dummyMap = new HashMap<>(allAbilityTypes);
|
||||
filteredAbilityTypes.clear();
|
||||
filteredAbilityTypes.putAll(getFilteredData(dummyMap));
|
||||
}
|
||||
|
||||
// Reset scroll position when changing category
|
||||
if (availableTypesScroll != null) {
|
||||
availableTypesScroll.resetScroll();
|
||||
}
|
||||
}
|
||||
|
||||
initGui();
|
||||
}
|
||||
@@ -336,6 +374,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
if (availableTypesScroll != null) {
|
||||
availableTypesScroll.setList(getFilteredTypeList());
|
||||
}
|
||||
} else if (type == EnumScrollData.CUSTOM_ABILITIES) {
|
||||
existingPresetNames.clear();
|
||||
for (String key : data.keySet()) {
|
||||
int tabIndex = key.indexOf('\t');
|
||||
if (tabIndex > 0) {
|
||||
existingPresetNames.add(key.substring(0, tabIndex));
|
||||
} else {
|
||||
existingPresetNames.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
initGui();
|
||||
}
|
||||
@@ -423,6 +471,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
String displayName = I18n.format(typeId);
|
||||
// Search matches either the display name or the typeId
|
||||
if (search.isEmpty() || displayName.toLowerCase().contains(search) || typeId.toLowerCase().contains(search)) {
|
||||
// Gray out built-in types to distinguish them from configurable types
|
||||
if (AbilityController.Instance.isBuiltInType(typeId)) {
|
||||
displayName = "\u00A77" + displayName;
|
||||
}
|
||||
list.add(displayName);
|
||||
displayNameToTypeId.put(displayName, typeId);
|
||||
}
|
||||
@@ -471,6 +523,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
if (slot.isReference()) {
|
||||
return colorPrefix + (index + 1) + ". \u00A7e> " + nameDisplay + "\u00A7r";
|
||||
}
|
||||
if (ability.isBuiltIn()) {
|
||||
return colorPrefix + (index + 1) + ". \u00A77" + nameDisplay + "\u00A7r";
|
||||
}
|
||||
return colorPrefix + (index + 1) + ". " + nameDisplay;
|
||||
}
|
||||
|
||||
@@ -571,10 +626,11 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
|
||||
if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
|
||||
AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
|
||||
Ability ability = slot.getAbility();
|
||||
if (ability != null && ability.isBuiltIn()) return;
|
||||
if (slot.isReference()) {
|
||||
setSubGui(new SubGuiAbilityEditMode());
|
||||
} else {
|
||||
Ability ability = slot.getAbility();
|
||||
if (ability != null) {
|
||||
setSubGui(ability.createConfigGui(this));
|
||||
}
|
||||
@@ -605,7 +661,24 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
|
||||
@Override
|
||||
public void subGuiClosed(SubGuiInterface subgui) {
|
||||
if (subgui instanceof SubGuiAbilityEditMode) {
|
||||
if (subgui instanceof SubGuiAbilityVariantSelect) {
|
||||
SubGuiAbilityVariantSelect variantGui = (SubGuiAbilityVariantSelect) subgui;
|
||||
int idx = variantGui.getSelectedIndex();
|
||||
if (idx >= 0 && pendingTypeId != null) {
|
||||
Ability newAbility = AbilityController.Instance.create(pendingTypeId);
|
||||
if (newAbility != null) {
|
||||
variantGui.getVariants().get(idx).apply(newAbility);
|
||||
newAbility.setId(UUID.randomUUID().toString());
|
||||
npcSlots.add(AbilitySlot.inline(newAbility));
|
||||
selectedAbilityIndex = npcSlots.size() - 1;
|
||||
updateNpcAbilitiesList();
|
||||
selectAbilityByIndex(selectedAbilityIndex);
|
||||
save();
|
||||
}
|
||||
}
|
||||
pendingTypeId = null;
|
||||
initGui();
|
||||
} else if (subgui instanceof SubGuiAbilityEditMode) {
|
||||
int mode = ((SubGuiAbilityEditMode) subgui).getResult();
|
||||
if (mode < 0) return; // cancelled
|
||||
|
||||
|
||||
@@ -324,7 +324,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi
|
||||
@Override
|
||||
public void unFocused(GuiNpcTextField textField) {
|
||||
if (textField.id < DECLARATIVE_ID_START) return;
|
||||
builder.handleTextFieldEvent(textField.id, textField);
|
||||
if (builder.handleTextFieldEvent(textField.id, textField)) {
|
||||
initGui();
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -7,24 +7,29 @@ import net.minecraft.client.gui.GuiButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
|
||||
import noppes.npcs.client.gui.util.ISubGuiListener;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Confirmation dialog for saving an ability preset.
|
||||
* Uses packet-based communication for proper client-server architecture.
|
||||
*/
|
||||
public class SubGuiAbilitySaveConfirm extends SubGuiInterface {
|
||||
public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGuiListener {
|
||||
|
||||
private final Ability ability;
|
||||
private final IAbilityConfigCallback callback;
|
||||
private final Set<String> existingNames;
|
||||
|
||||
public SubGuiAbilitySaveConfirm(Ability ability) {
|
||||
this(ability, null);
|
||||
this(ability, null, null);
|
||||
}
|
||||
|
||||
public SubGuiAbilitySaveConfirm(Ability ability, IAbilityConfigCallback callback) {
|
||||
public SubGuiAbilitySaveConfirm(Ability ability, IAbilityConfigCallback callback, Set<String> existingNames) {
|
||||
this.ability = ability;
|
||||
this.callback = callback;
|
||||
this.existingNames = existingNames;
|
||||
|
||||
setBackground("menubg.png");
|
||||
xSize = 200;
|
||||
@@ -52,14 +57,32 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface {
|
||||
int id = guibutton.id;
|
||||
|
||||
if (id == 0) {
|
||||
// Save the ability via packet
|
||||
PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
|
||||
if (callback != null) {
|
||||
callback.onAbilitySaved(ability);
|
||||
if (existingNames != null && existingNames.contains(ability.getName())) {
|
||||
setSubGui(new SubGuiDuplicateNameConfirm());
|
||||
return;
|
||||
}
|
||||
close();
|
||||
doSave();
|
||||
} else if (id == 1) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
private void doSave() {
|
||||
PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
|
||||
if (callback != null) {
|
||||
callback.onAbilitySaved(ability);
|
||||
}
|
||||
close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subGuiClosed(SubGuiInterface subgui) {
|
||||
if (subgui instanceof SubGuiDuplicateNameConfirm) {
|
||||
if (((SubGuiDuplicateNameConfirm) subgui).isConfirmed()) {
|
||||
doSave();
|
||||
} else {
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,22 +5,26 @@ import net.minecraft.client.gui.GuiButton;
|
||||
import net.minecraft.client.resources.I18n;
|
||||
import noppes.npcs.client.gui.util.GuiCustomScroll;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.GuiNpcTextField;
|
||||
import noppes.npcs.client.gui.util.ICustomScrollListener;
|
||||
import noppes.npcs.client.gui.util.ITextfieldListener;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SubGui for selecting an ability type when creating a new ability.
|
||||
*/
|
||||
public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomScrollListener {
|
||||
public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomScrollListener, ITextfieldListener {
|
||||
|
||||
private GuiCustomScroll scroll;
|
||||
private final HashMap<String, String> displayNameToTypeId = new HashMap<>();
|
||||
private final HashMap<String, String> allDisplayNameToTypeId = new HashMap<>();
|
||||
private String selectedTypeId = null;
|
||||
private String search = "";
|
||||
|
||||
public SubGuiAbilityTypeSelect() {
|
||||
setBackground("menubg.png");
|
||||
@@ -32,32 +36,44 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
addLabel(new GuiNpcLabel(0, "ability.selectType", guiLeft + 5, guiTop + 5));
|
||||
addTextField(new GuiNpcTextField(10, this, fontRendererObj, guiLeft + 5, guiTop + 5, 190, 18, search));
|
||||
|
||||
if (scroll == null) {
|
||||
scroll = new GuiCustomScroll(this, 0);
|
||||
scroll.setSize(190, 165);
|
||||
scroll.setSize(190, 145);
|
||||
}
|
||||
scroll.guiLeft = guiLeft + 5;
|
||||
scroll.guiTop = guiTop + 16;
|
||||
scroll.guiTop = guiTop + 26;
|
||||
addScroll(scroll);
|
||||
|
||||
List<String> list = buildTypeList();
|
||||
scroll.setList(list);
|
||||
buildAllTypes();
|
||||
scroll.setList(getFilteredTypeList());
|
||||
|
||||
addButton(new GuiNpcButton(0, guiLeft + 5, guiTop + 188, 90, 20, "gui.add"));
|
||||
getButton(0).setEnabled(scroll.hasSelected());
|
||||
addButton(new GuiNpcButton(1, guiLeft + 105, guiTop + 188, 90, 20, "gui.cancel"));
|
||||
}
|
||||
|
||||
private List<String> buildTypeList() {
|
||||
List<String> list = new ArrayList<>();
|
||||
displayNameToTypeId.clear();
|
||||
private void buildAllTypes() {
|
||||
allDisplayNameToTypeId.clear();
|
||||
String[] types = AbilityController.Instance.getTypes();
|
||||
for (String typeId : types) {
|
||||
// Exclude built-in types — they cannot be created as custom presets
|
||||
if (AbilityController.Instance.isBuiltInType(typeId)) continue;
|
||||
String displayName = I18n.format(typeId);
|
||||
list.add(displayName);
|
||||
displayNameToTypeId.put(displayName, typeId);
|
||||
allDisplayNameToTypeId.put(displayName, typeId);
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getFilteredTypeList() {
|
||||
List<String> list = new ArrayList<>();
|
||||
displayNameToTypeId.clear();
|
||||
for (Map.Entry<String, String> entry : allDisplayNameToTypeId.entrySet()) {
|
||||
String displayName = entry.getKey();
|
||||
if (search.isEmpty() || displayName.toLowerCase().contains(search) || entry.getValue().toLowerCase().contains(search)) {
|
||||
list.add(displayName);
|
||||
displayNameToTypeId.put(displayName, entry.getValue());
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -89,6 +105,22 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyTyped(char c, int i) {
|
||||
super.keyTyped(c, i);
|
||||
if (getTextField(10) != null && getTextField(10).isFocused()) {
|
||||
if (!search.equals(getTextField(10).getText())) {
|
||||
search = getTextField(10).getText().toLowerCase();
|
||||
scroll.setList(getFilteredTypeList());
|
||||
scroll.resetScroll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unFocused(GuiNpcTextField textField) {
|
||||
}
|
||||
|
||||
public String getSelectedTypeId() {
|
||||
return selectedTypeId;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package noppes.npcs.client.gui.advanced;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import noppes.npcs.client.gui.util.GuiCustomScroll;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.ICustomScrollListener;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* SubGui for selecting a variant/template when creating a new ability.
|
||||
* Uses a scroll list with group headers for external addon variants.
|
||||
*/
|
||||
public class SubGuiAbilityVariantSelect extends SubGuiInterface implements ICustomScrollListener {
|
||||
|
||||
private final List<AbilityVariant> variants;
|
||||
private int selectedIndex = -1;
|
||||
|
||||
// Maps each scroll list index to a variant list index (-1 for non-selectable entries)
|
||||
private final List<Integer> scrollToVariantIndex = new ArrayList<>();
|
||||
|
||||
public SubGuiAbilityVariantSelect(List<AbilityVariant> variants) {
|
||||
this.variants = variants;
|
||||
setBackground("menubg.png");
|
||||
xSize = 200;
|
||||
ySize = 216;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
addLabel(new GuiNpcLabel(0, "ability.selectVariant", guiLeft + 10, guiTop + 8));
|
||||
|
||||
// Build the unsorted scroll list with group headers
|
||||
List<String> scrollList = new ArrayList<>();
|
||||
scrollToVariantIndex.clear();
|
||||
|
||||
boolean hasBaseVariants = false;
|
||||
boolean hasGroupedVariants = false;
|
||||
|
||||
for (AbilityVariant v : variants) {
|
||||
if (v.getGroup() == null)
|
||||
hasBaseVariants = true;
|
||||
else
|
||||
hasGroupedVariants = true;
|
||||
}
|
||||
|
||||
// Add base (non-grouped) variants first
|
||||
for (int i = 0; i < variants.size(); i++) {
|
||||
if (variants.get(i).getGroup() == null) {
|
||||
scrollList.add(variants.get(i).getDisplayKey());
|
||||
scrollToVariantIndex.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Blank separator between base and grouped variants
|
||||
if (hasBaseVariants && hasGroupedVariants) {
|
||||
scrollList.add("");
|
||||
scrollToVariantIndex.add(-1);
|
||||
}
|
||||
|
||||
// Add grouped variants with group headers
|
||||
String lastGroup = null;
|
||||
for (int i = 0; i < variants.size(); i++) {
|
||||
AbilityVariant v = variants.get(i);
|
||||
if (v.getGroup() == null)
|
||||
continue;
|
||||
|
||||
if (!v.getGroup().equals(lastGroup)) {
|
||||
scrollList.add(v.getGroup());
|
||||
scrollToVariantIndex.add(-1);
|
||||
lastGroup = v.getGroup();
|
||||
}
|
||||
|
||||
scrollList.add(v.getDisplayKey());
|
||||
scrollToVariantIndex.add(i);
|
||||
}
|
||||
|
||||
GuiCustomScroll scroll = new GuiCustomScroll(this, 0);
|
||||
scroll.guiLeft = guiLeft + 5;
|
||||
scroll.guiTop = guiTop + 22;
|
||||
scroll.setUnsortedList(scrollList);
|
||||
scroll.setSize(190, 155);
|
||||
|
||||
// Mark blank separators and group headers as non-interactive colored entries
|
||||
scroll.colors.put("", 0x000000);
|
||||
if (lastGroup != null) {
|
||||
for (int i = 0; i < variants.size(); i++) {
|
||||
String group = variants.get(i).getGroup();
|
||||
if (group != null)
|
||||
scroll.colors.put(group, 0xFFFF00); // Yellow
|
||||
}
|
||||
}
|
||||
|
||||
// Restore selection in scroll
|
||||
if (selectedIndex >= 0) {
|
||||
for (int s = 0; s < scrollToVariantIndex.size(); s++) {
|
||||
if (scrollToVariantIndex.get(s) == selectedIndex) {
|
||||
scroll.selected = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addScroll(scroll);
|
||||
|
||||
// Select / Cancel buttons
|
||||
addButton(new GuiNpcButton(0, guiLeft + 5, guiTop + 188, 90, 20, "gui.select"));
|
||||
getButton(0).setEnabled(selectedIndex >= 0);
|
||||
addButton(new GuiNpcButton(1, guiLeft + 105, guiTop + 188, 90, 20, "gui.cancel"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) {
|
||||
if (scroll.id == 0 && scroll.selected >= 0 && scroll.selected < scrollToVariantIndex.size()) {
|
||||
int variantIdx = scrollToVariantIndex.get(scroll.selected);
|
||||
if (variantIdx >= 0) {
|
||||
selectedIndex = variantIdx;
|
||||
getButton(0).setEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
|
||||
if (selectedIndex >= 0) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonEvent(GuiButton guibutton) {
|
||||
int id = guibutton.id;
|
||||
if (id == 0 && selectedIndex >= 0) {
|
||||
close();
|
||||
} else if (id == 1) {
|
||||
selectedIndex = -1;
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
public int getSelectedIndex() {
|
||||
return selectedIndex;
|
||||
}
|
||||
|
||||
public List<AbilityVariant> getVariants() {
|
||||
return variants;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package noppes.npcs.client.gui.advanced;
|
||||
|
||||
import net.minecraft.client.gui.GuiButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
|
||||
/**
|
||||
* Confirmation dialog shown when saving an ability preset with a name that already exists.
|
||||
*/
|
||||
public class SubGuiDuplicateNameConfirm extends SubGuiInterface {
|
||||
|
||||
private boolean confirmed = false;
|
||||
|
||||
public SubGuiDuplicateNameConfirm() {
|
||||
setBackground("menubg.png");
|
||||
xSize = 220;
|
||||
ySize = 80;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
int y = guiTop + 10;
|
||||
addLabel(new GuiNpcLabel(0, "ability.duplicate.name", guiLeft + 10, y));
|
||||
|
||||
y += 30;
|
||||
addButton(new GuiNpcButton(0, guiLeft + 20, y, 80, 20, "gui.continue"));
|
||||
addButton(new GuiNpcButton(1, guiLeft + 120, y, 80, 20, "gui.cancel"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void buttonEvent(GuiButton guibutton) {
|
||||
if (guibutton.id == 0) {
|
||||
confirmed = true;
|
||||
close();
|
||||
} else if (guibutton.id == 1) {
|
||||
confirmed = false;
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConfirmed() {
|
||||
return confirmed;
|
||||
}
|
||||
}
|
||||
@@ -154,8 +154,8 @@ public class FieldDef {
|
||||
|
||||
public static FieldDef colorSubGui(String label, Supplier<Integer> getter, Consumer<Integer> setter) {
|
||||
return subGuiField(label,
|
||||
() -> new SubGuiColorSelector(getter.get()),
|
||||
gui -> setter.accept(((SubGuiColorSelector) gui).color & 0x00FFFFFF))
|
||||
() -> new SubGuiColorSelector(getter.get() & 0xFFFFFF),
|
||||
gui -> setter.accept((((SubGuiColorSelector) gui).color & 0x00FFFFFF) | (getter.get() & 0xFF000000)))
|
||||
.buttonLabel(() -> String.format("%06X", getter.get() & 0xFFFFFF))
|
||||
.buttonTextColor(() -> getter.get() & 0xFFFFFF);
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ public class GuiFieldBuilder {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip invisible fields
|
||||
if (!def.isVisible()) continue;
|
||||
|
||||
// Section header
|
||||
if (def.getType() == FieldType.SECTION_HEADER) {
|
||||
y += 3;
|
||||
@@ -391,7 +394,7 @@ public class GuiFieldBuilder {
|
||||
|
||||
/**
|
||||
* Handles text field unfocus events for declarative fields.
|
||||
* Call from parent GUI's unFocused(). Returns true if the event was handled.
|
||||
* Call from parent GUI's unFocused(). Returns true if the value actually changed.
|
||||
*/
|
||||
public boolean handleTextFieldEvent(int textFieldId, GuiNpcTextField field) {
|
||||
FieldDef def = textFieldMap.get(textFieldId);
|
||||
@@ -399,14 +402,36 @@ public class GuiFieldBuilder {
|
||||
|
||||
switch (def.getType()) {
|
||||
case FLOAT:
|
||||
try { def.setValue(Float.parseFloat(field.getText())); } catch (NumberFormatException ignored) {}
|
||||
return true;
|
||||
try {
|
||||
float newVal = Float.parseFloat(field.getText());
|
||||
Object old = def.getValue();
|
||||
float oldVal = old instanceof Number ? ((Number) old).floatValue() : 0f;
|
||||
if (newVal != oldVal) {
|
||||
def.setValue(newVal);
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
return false;
|
||||
case INT:
|
||||
try { def.setValue(Integer.parseInt(field.getText())); } catch (NumberFormatException ignored) {}
|
||||
return true;
|
||||
try {
|
||||
int newVal = Integer.parseInt(field.getText());
|
||||
Object old = def.getValue();
|
||||
int oldVal = old instanceof Number ? ((Number) old).intValue() : 0;
|
||||
if (newVal != oldVal) {
|
||||
def.setValue(newVal);
|
||||
return true;
|
||||
}
|
||||
} catch (NumberFormatException ignored) {}
|
||||
return false;
|
||||
case STRING:
|
||||
def.setValue(field.getText());
|
||||
return true;
|
||||
String newVal = field.getText();
|
||||
Object old = def.getValue();
|
||||
String oldVal = old != null ? old.toString() : "";
|
||||
if (!newVal.equals(oldVal)) {
|
||||
def.setValue(newVal);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -65,16 +65,17 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
// Toggle button - added FIRST to maintain stable buttonList index
|
||||
// (prevents double-toggle when initGui() is called from actionPerformed)
|
||||
String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom";
|
||||
this.addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 56, 45, 20, toggleLabel));
|
||||
|
||||
// Add/Remove buttons - only show for custom animations
|
||||
if (!showingBuiltIn) {
|
||||
this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add"));
|
||||
this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove"));
|
||||
}
|
||||
|
||||
// Toggle button for Custom/Built-in - under Remove button
|
||||
String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom";
|
||||
this.addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 56, 45, 20, toggleLabel));
|
||||
|
||||
if (scrollAnimations == null) {
|
||||
scrollAnimations = new GuiCustomScroll(this, 0, 0);
|
||||
scrollAnimations.setSize(143, 185);
|
||||
@@ -200,11 +201,15 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol
|
||||
// Toggle button
|
||||
if (button.id == 10) {
|
||||
showingBuiltIn = !showingBuiltIn;
|
||||
// Clear selection when switching views
|
||||
// Clear selection and search when switching views
|
||||
selected = null;
|
||||
search = "";
|
||||
animation = new Animation();
|
||||
currentIsBuiltIn = false;
|
||||
playingAnimation = false;
|
||||
scrollAnimations.clear();
|
||||
// Re-request data to ensure both lists are populated
|
||||
PacketClient.sendClient(new AnimationsGetPacket());
|
||||
initGui();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -85,11 +85,13 @@ public class GuiNPCManageFactions extends GuiNPCInterface2 implements IScrollDat
|
||||
|
||||
addLabel(new GuiNpcLabel(3, "faction.hidden", guiLeft + 8, y + 5));
|
||||
this.addButton(new GuiNpcButton(3, guiLeft + 100, y, 45, 20, new String[]{"gui.no", "gui.yes"}, faction.hideFaction ? 1 : 0));
|
||||
getButton(3).setHoverText("faction.hidden.hover");
|
||||
|
||||
y += 23;
|
||||
|
||||
addLabel(new GuiNpcLabel(4, "faction.attacked", guiLeft + 8, y + 5));
|
||||
this.addButton(new GuiNpcButton(4, guiLeft + 100, y, 45, 20, new String[]{"gui.no", "gui.yes"}, faction.getsAttacked ? 1 : 0));
|
||||
getButton(4).setHoverText("faction.attacked.hover");
|
||||
|
||||
y += 23;
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import java.util.UUID;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.network.PacketClient;
|
||||
import kamkeel.npcs.network.packets.request.ability.BuiltInAbilityGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket;
|
||||
@@ -16,7 +17,11 @@ import net.minecraft.client.gui.GuiYesNoCallback;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import noppes.npcs.client.NoppesUtil;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
|
||||
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
|
||||
import noppes.npcs.client.gui.advanced.SubGuiAbilityTypeSelect;
|
||||
import noppes.npcs.client.gui.advanced.SubGuiAbilityVariantSelect;
|
||||
import noppes.npcs.client.gui.advanced.SubGuiDuplicateNameConfirm;
|
||||
import noppes.npcs.client.gui.util.AbilityPreviewExecutor;
|
||||
import noppes.npcs.client.gui.util.GuiAbilityInterface;
|
||||
import noppes.npcs.client.gui.util.GuiCustomScroll;
|
||||
@@ -47,6 +52,7 @@ import java.util.Vector;
|
||||
* - Left side: 3D preview of NPC with ability effects
|
||||
* - Right side: Ability list with search
|
||||
* - Play/Pause/Stop buttons to preview abilities
|
||||
* - Toggle between Custom and Built-in ability views
|
||||
*/
|
||||
public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback,
|
||||
@@ -54,7 +60,8 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
|
||||
// ==================== DATA ====================
|
||||
private GuiCustomScroll scroll;
|
||||
private HashMap<String, Integer> abilityData = new HashMap<>();
|
||||
private HashMap<String, Integer> customData = new HashMap<>();
|
||||
private HashMap<String, Integer> builtInData = new HashMap<>();
|
||||
/** Maps display name -> UUID for custom abilities */
|
||||
private final HashMap<String, String> displayToUuid = new HashMap<>();
|
||||
private String selected = null;
|
||||
@@ -62,6 +69,17 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
private String search = "";
|
||||
private Ability selectedAbility = null;
|
||||
|
||||
// ==================== BUILT-IN TOGGLE ====================
|
||||
private boolean showingBuiltIn = false;
|
||||
private boolean currentIsBuiltIn = false;
|
||||
|
||||
// ==================== PENDING SAVE ====================
|
||||
private Ability pendingSaveAbility = null;
|
||||
private boolean pendingNewCreation = false;
|
||||
|
||||
// ==================== PENDING VARIANT ====================
|
||||
private String pendingTypeId = null;
|
||||
|
||||
// ==================== PREVIEW ====================
|
||||
private AbilityPreviewExecutor previewExecutor;
|
||||
private long prevTick = 0;
|
||||
@@ -77,7 +95,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
previewExecutor = new AbilityPreviewExecutor();
|
||||
previewExecutor.setParentGui(this);
|
||||
|
||||
// Request ability data from server
|
||||
// Request ability data from server (sends both custom and built-in)
|
||||
PacketClient.sendClient(new CustomAbilitiesGetPacket());
|
||||
|
||||
// Enable animation data
|
||||
@@ -89,18 +107,26 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
|
||||
// Add button
|
||||
addButton(new GuiNpcButton(2, guiLeft + 368, guiTop + 8, 45, 20, "gui.add"));
|
||||
// Toggle button — added FIRST to maintain stable buttonList index
|
||||
String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom";
|
||||
addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 52, 45, 20, toggleLabel));
|
||||
|
||||
// Remove button
|
||||
addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove"));
|
||||
getButton(1).setEnabled(selected != null && !selected.isEmpty() && displayToUuid.containsKey(selected));
|
||||
if (!showingBuiltIn) {
|
||||
// Add button — only for custom view
|
||||
addButton(new GuiNpcButton(2, guiLeft + 368, guiTop + 8, 45, 20, "gui.add"));
|
||||
|
||||
// Edit button
|
||||
addButton(new GuiNpcButton(100, guiLeft + 368, guiTop + 52, 45, 20, "gui.edit"));
|
||||
getButton(100).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null);
|
||||
// Remove button — only for custom view
|
||||
addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove"));
|
||||
getButton(1).setEnabled(selected != null && !selected.isEmpty() && displayToUuid.containsKey(selected));
|
||||
}
|
||||
|
||||
// Scroll list of saved abilities (right side)
|
||||
// Edit button — hidden when viewing built-in
|
||||
if (!showingBuiltIn && !currentIsBuiltIn) {
|
||||
addButton(new GuiNpcButton(100, guiLeft + 368, guiTop + 74, 45, 20, "gui.edit"));
|
||||
getButton(100).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null);
|
||||
}
|
||||
|
||||
// Scroll list of abilities (right side)
|
||||
if (scroll == null) {
|
||||
scroll = new GuiCustomScroll(this, 0);
|
||||
scroll.setSize(143, 185);
|
||||
@@ -111,7 +137,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
|
||||
// Update scroll list
|
||||
scroll.setList(getSearchList());
|
||||
if (selected != null && displayToUuid.containsKey(selected)) {
|
||||
if (selected != null) {
|
||||
scroll.setSelected(selected);
|
||||
}
|
||||
|
||||
@@ -123,9 +149,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
return;
|
||||
}
|
||||
|
||||
// ID/Name label (below search bar)
|
||||
addLabel(new GuiNpcLabel(10, "ID: " + selectedAbility.getId(), guiLeft + 220, guiTop + 215));
|
||||
|
||||
// Play/Pause/Stop buttons (below model preview area)
|
||||
String animTexture = "customnpcs:textures/gui/animation.png";
|
||||
int playButtonOffsetX = 60;
|
||||
@@ -150,17 +173,34 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
}
|
||||
}
|
||||
|
||||
private HashMap<String, Integer> getCurrentData() {
|
||||
return showingBuiltIn ? builtInData : customData;
|
||||
}
|
||||
|
||||
private List<String> getSearchList() {
|
||||
if (search.isEmpty()) {
|
||||
return new ArrayList<>(displayToUuid.keySet());
|
||||
}
|
||||
List<String> list = new ArrayList<>();
|
||||
for (String name : displayToUuid.keySet()) {
|
||||
if (name.toLowerCase().contains(search.toLowerCase())) {
|
||||
list.add(name);
|
||||
if (showingBuiltIn) {
|
||||
if (search.isEmpty()) {
|
||||
return new ArrayList<>(builtInData.keySet());
|
||||
}
|
||||
List<String> list = new ArrayList<>();
|
||||
for (String name : builtInData.keySet()) {
|
||||
if (name.toLowerCase().contains(search.toLowerCase())) {
|
||||
list.add(name);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
} else {
|
||||
if (search.isEmpty()) {
|
||||
return new ArrayList<>(displayToUuid.keySet());
|
||||
}
|
||||
List<String> list = new ArrayList<>();
|
||||
for (String name : displayToUuid.keySet()) {
|
||||
if (name.toLowerCase().contains(search.toLowerCase())) {
|
||||
list.add(name);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -216,13 +256,30 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
|
||||
int id = guibutton.id;
|
||||
|
||||
if (id == 2) {
|
||||
// Toggle between custom and built-in views
|
||||
if (id == 10) {
|
||||
showingBuiltIn = !showingBuiltIn;
|
||||
selected = null;
|
||||
selectedUuid = null;
|
||||
selectedAbility = null;
|
||||
currentIsBuiltIn = false;
|
||||
search = "";
|
||||
previewExecutor.stop();
|
||||
if (scroll != null) {
|
||||
scroll.clear();
|
||||
}
|
||||
PacketClient.sendClient(new CustomAbilitiesGetPacket());
|
||||
initGui();
|
||||
return;
|
||||
}
|
||||
|
||||
if (id == 2 && !showingBuiltIn) {
|
||||
// Add — open type selection
|
||||
setSubGui(new SubGuiAbilityTypeSelect());
|
||||
} else if (id == 1 && selected != null && selectedUuid != null) {
|
||||
} else if (id == 1 && !showingBuiltIn && selected != null && selectedUuid != null) {
|
||||
GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1);
|
||||
displayGuiScreen(guiyesno);
|
||||
} else if (id == 100 && selectedAbility != null) {
|
||||
} else if (id == 100 && !showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) {
|
||||
// Edit
|
||||
previewExecutor.stop();
|
||||
setSubGui(selectedAbility.createConfigGui(this));
|
||||
@@ -265,11 +322,20 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
String newSelection = scroll.getSelected();
|
||||
if (newSelection != null && !newSelection.equals(selected)) {
|
||||
previewExecutor.stop();
|
||||
|
||||
selected = newSelection;
|
||||
selectedUuid = displayToUuid.get(selected);
|
||||
if (selectedUuid != null) {
|
||||
PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid));
|
||||
|
||||
if (showingBuiltIn) {
|
||||
// Fetch built-in ability by display name
|
||||
selectedUuid = null;
|
||||
currentIsBuiltIn = true;
|
||||
PacketClient.sendClient(new BuiltInAbilityGetPacket(selected));
|
||||
} else {
|
||||
// Fetch custom ability by UUID
|
||||
selectedUuid = displayToUuid.get(selected);
|
||||
currentIsBuiltIn = false;
|
||||
if (selectedUuid != null) {
|
||||
PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,8 +344,8 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
@Override
|
||||
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
|
||||
if (scroll.id == 0 && selection != null && !selection.isEmpty()) {
|
||||
// Double-click to edit
|
||||
if (selectedAbility != null) {
|
||||
// Double-click to edit — only for custom, non-built-in abilities
|
||||
if (!showingBuiltIn && !currentIsBuiltIn && selectedAbility != null) {
|
||||
previewExecutor.stop();
|
||||
setSubGui(selectedAbility.createConfigGui(this));
|
||||
}
|
||||
@@ -290,7 +356,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
public void setData(Vector<String> list, HashMap<String, Integer> data, EnumScrollData type) {
|
||||
if (type == EnumScrollData.CUSTOM_ABILITIES) {
|
||||
String prevSelected = scroll != null ? scroll.getSelected() : null;
|
||||
this.abilityData = data;
|
||||
this.customData = data;
|
||||
displayToUuid.clear();
|
||||
for (String key : data.keySet()) {
|
||||
// key format: "displayName\tUUID"
|
||||
@@ -303,18 +369,30 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
displayToUuid.put(key, key);
|
||||
}
|
||||
}
|
||||
if (scroll != null) {
|
||||
if (!showingBuiltIn && scroll != null) {
|
||||
scroll.setList(getSearchList());
|
||||
if (prevSelected != null && displayToUuid.containsKey(prevSelected)) {
|
||||
scroll.setSelected(prevSelected);
|
||||
}
|
||||
}
|
||||
initGui();
|
||||
} else if (type == EnumScrollData.BUILTIN_ABILITIES) {
|
||||
this.builtInData = data;
|
||||
if (showingBuiltIn && scroll != null) {
|
||||
scroll.setList(getSearchList());
|
||||
if (selected != null && builtInData.containsKey(selected)) {
|
||||
scroll.setSelected(selected);
|
||||
}
|
||||
}
|
||||
initGui();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setGuiData(NBTTagCompound compound) {
|
||||
// Check if this is a built-in ability response
|
||||
currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn");
|
||||
|
||||
// Received ability data from server
|
||||
selectedAbility = AbilityController.Instance.fromNBT(compound);
|
||||
if (selectedAbility != null) {
|
||||
@@ -337,27 +415,111 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
|
||||
@Override
|
||||
public void subGuiClosed(SubGuiInterface subgui) {
|
||||
if (subgui instanceof SubGuiAbilityTypeSelect) {
|
||||
String typeId = ((SubGuiAbilityTypeSelect) subgui).getSelectedTypeId();
|
||||
if (typeId != null) {
|
||||
Ability newAbility = AbilityController.Instance.create(typeId);
|
||||
if (subgui instanceof SubGuiAbilityVariantSelect) {
|
||||
SubGuiAbilityVariantSelect variantGui = (SubGuiAbilityVariantSelect) subgui;
|
||||
int idx = variantGui.getSelectedIndex();
|
||||
if (idx >= 0 && pendingTypeId != null) {
|
||||
Ability newAbility = AbilityController.Instance.create(pendingTypeId);
|
||||
if (newAbility != null) {
|
||||
variantGui.getVariants().get(idx).apply(newAbility);
|
||||
newAbility.setId(UUID.randomUUID().toString());
|
||||
newAbility.setName(newAbility.getTypeId());
|
||||
// Save to server, then open config GUI
|
||||
PacketClient.sendClient(new CustomAbilitySavePacket(newAbility.writeNBT()));
|
||||
selectedAbility = newAbility;
|
||||
selected = newAbility.getName();
|
||||
selectedUuid = newAbility.getId();
|
||||
previewExecutor.stop();
|
||||
setSubGui(selectedAbility.createConfigGui(this));
|
||||
if (hasDuplicateName(newAbility)) {
|
||||
pendingSaveAbility = newAbility;
|
||||
pendingNewCreation = true;
|
||||
pendingTypeId = null;
|
||||
setSubGui(new SubGuiDuplicateNameConfirm());
|
||||
return;
|
||||
}
|
||||
pendingTypeId = null;
|
||||
openConfig(newAbility);
|
||||
return;
|
||||
}
|
||||
}
|
||||
pendingTypeId = null;
|
||||
} else if (subgui instanceof SubGuiAbilityTypeSelect) {
|
||||
String typeId = ((SubGuiAbilityTypeSelect) subgui).getSelectedTypeId();
|
||||
if (typeId != null) {
|
||||
java.util.List<AbilityVariant> variants = AbilityController.Instance.getVariantsForType(typeId);
|
||||
if (variants.size() > 1) {
|
||||
pendingTypeId = typeId;
|
||||
setSubGui(new SubGuiAbilityVariantSelect(variants));
|
||||
return;
|
||||
}
|
||||
Ability newAbility = AbilityController.Instance.create(typeId);
|
||||
if (newAbility != null) {
|
||||
if (variants.size() == 1) {
|
||||
variants.get(0).apply(newAbility);
|
||||
}
|
||||
newAbility.setId(UUID.randomUUID().toString());
|
||||
if (hasDuplicateName(newAbility)) {
|
||||
pendingSaveAbility = newAbility;
|
||||
pendingNewCreation = true;
|
||||
setSubGui(new SubGuiDuplicateNameConfirm());
|
||||
return;
|
||||
}
|
||||
openConfig(newAbility);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (subgui instanceof SubGuiDuplicateNameConfirm) {
|
||||
SubGuiDuplicateNameConfirm confirm = (SubGuiDuplicateNameConfirm) subgui;
|
||||
if (confirm.isConfirmed() && pendingSaveAbility != null) {
|
||||
if (pendingNewCreation) {
|
||||
openConfig(pendingSaveAbility);
|
||||
pendingSaveAbility = null;
|
||||
pendingNewCreation = false;
|
||||
return;
|
||||
} else {
|
||||
PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT()));
|
||||
PacketClient.sendClient(new CustomAbilitiesGetPacket());
|
||||
pendingSaveAbility = null;
|
||||
}
|
||||
} else if (pendingSaveAbility != null) {
|
||||
if (pendingNewCreation) {
|
||||
// Cancel new creation - discard
|
||||
pendingSaveAbility = null;
|
||||
pendingNewCreation = false;
|
||||
} else {
|
||||
// Cancel edit save - re-open config GUI to change name
|
||||
Ability ability = pendingSaveAbility;
|
||||
pendingSaveAbility = null;
|
||||
setSubGui(ability.createConfigGui(this));
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if (pendingSaveAbility != null) {
|
||||
// Config GUI closed with a pending save (set by onAbilitySaved)
|
||||
if (hasDuplicateName(pendingSaveAbility)) {
|
||||
pendingNewCreation = false;
|
||||
setSubGui(new SubGuiDuplicateNameConfirm());
|
||||
return;
|
||||
}
|
||||
PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT()));
|
||||
PacketClient.sendClient(new CustomAbilitiesGetPacket());
|
||||
pendingSaveAbility = null;
|
||||
}
|
||||
initGui();
|
||||
}
|
||||
|
||||
private void openConfig(Ability ability) {
|
||||
selectedAbility = ability;
|
||||
selected = ability.getName();
|
||||
selectedUuid = ability.getId();
|
||||
previewExecutor.stop();
|
||||
setSubGui(ability.createConfigGui(this));
|
||||
}
|
||||
|
||||
private boolean hasDuplicateName(Ability ability) {
|
||||
String name = ability.getName();
|
||||
if (name == null || name.isEmpty()) return false;
|
||||
for (java.util.Map.Entry<String, String> entry : displayToUuid.entrySet()) {
|
||||
if (entry.getKey().equals(name) && !entry.getValue().equals(ability.getId())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void confirmClicked(boolean result, int id) {
|
||||
NoppesUtil.openGUI(player, this);
|
||||
@@ -375,7 +537,8 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
|
||||
|
||||
@Override
|
||||
public void onAbilitySaved(Ability ability) {
|
||||
PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT()));
|
||||
// Defer save until subGuiClosed where we can check for duplicate names
|
||||
pendingSaveAbility = ability;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -4,15 +4,10 @@ import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.PreviewEntityHandler;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.entity.EntityAbilityBeam;
|
||||
import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import kamkeel.npcs.entity.EntityAbilityLaser;
|
||||
import kamkeel.npcs.entity.EntityAbilityOrb;
|
||||
import kamkeel.npcs.entity.EntityAbilityProjectile;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.controllers.data.Animation;
|
||||
import noppes.npcs.controllers.data.AnimationData;
|
||||
import noppes.npcs.entity.EntityCustomNpc;
|
||||
@@ -25,38 +20,39 @@ import java.util.List;
|
||||
/**
|
||||
* Client-side executor for previewing abilities in the GUI.
|
||||
*
|
||||
* Lifecycle:
|
||||
* 1. WINDUP phase - plays windup animation, entities spawn and charge
|
||||
* 2. ACTIVE phase - plays active animation, entities fire and move
|
||||
* 3. EXTENDED phase - continues for +100 ticks after animations to see results
|
||||
* 4. IDLE - preview complete
|
||||
* Uses unified execution: calls the REAL ability methods (onWindUpTick, onExecute, onActiveTick)
|
||||
* with previewMode=true. Abilities skip damage/effects/sounds when isPreview() returns true.
|
||||
*
|
||||
* Features:
|
||||
* - Invisible fake target NPC placed in front of the preview NPC for targeting
|
||||
* - NPC movement tracking for abilities that move the NPC (e.g. slam)
|
||||
* Lifecycle:
|
||||
* 1. WINDUP phase - real onWindUpTick() called, telegraph shown, entities may spawn
|
||||
* 2. ACTIVE phase - real onExecute() then onActiveTick() called, physics simulated
|
||||
* 3. EXTENDED phase - 100 extra ticks to observe results (entities continue updating)
|
||||
* 4. IDLE - preview complete
|
||||
*/
|
||||
@SideOnly(Side.CLIENT)
|
||||
public class AbilityPreviewExecutor {
|
||||
public class AbilityPreviewExecutor implements PreviewEntityHandler {
|
||||
|
||||
// ==================== PREVIEW STATE ====================
|
||||
private Ability previewAbility;
|
||||
private EntityNPCInterface previewNpc;
|
||||
private AbilityPhase phase = AbilityPhase.IDLE;
|
||||
|
||||
/** Executor's own phase tracking (separate from ability.getPhase()) */
|
||||
private enum ExecutorPhase { IDLE, WINDUP, ACTIVE, EXTENDED }
|
||||
private ExecutorPhase executorPhase = ExecutorPhase.IDLE;
|
||||
|
||||
private int currentTick = 0;
|
||||
private boolean playing = false;
|
||||
private boolean paused = false;
|
||||
|
||||
// ==================== TIMING ====================
|
||||
private static final int EXTENDED_DURATION = 100;
|
||||
private int activeDuration = 40;
|
||||
private int maxPreviewDuration = 200;
|
||||
|
||||
// ==================== FAKE TARGET ====================
|
||||
/** Invisible fake target NPC for ability targeting (placed 5 blocks in front of preview NPC) */
|
||||
private EntityNPCInterface fakeTarget;
|
||||
private static final double TARGET_DISTANCE = 5.0;
|
||||
|
||||
// ==================== NPC START POSITION ====================
|
||||
/** Saved NPC position for restoring after movement abilities */
|
||||
private double npcStartX, npcStartY, npcStartZ;
|
||||
|
||||
// ==================== PREVIEW CONTENT ====================
|
||||
@@ -74,7 +70,8 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start previewing an ability.
|
||||
* Start previewing an ability using unified execution.
|
||||
* Sets previewMode=true on the ability, then calls its real methods.
|
||||
*/
|
||||
public void startPreview(Ability ability, EntityNPCInterface npc) {
|
||||
stop();
|
||||
@@ -84,31 +81,35 @@ public class AbilityPreviewExecutor {
|
||||
this.currentTick = 0;
|
||||
this.playing = true;
|
||||
this.paused = false;
|
||||
this.activeDuration = ability.getPreviewActiveDuration();
|
||||
this.maxPreviewDuration = ability.getMaxPreviewDuration();
|
||||
|
||||
// Save NPC start position for restoring later
|
||||
this.npcStartX = npc.posX;
|
||||
this.npcStartY = npc.posY;
|
||||
this.npcStartZ = npc.posZ;
|
||||
|
||||
// Enable preview mode on the ability
|
||||
ability.setPreviewMode(true);
|
||||
ability.setPreviewEntityHandler(this);
|
||||
|
||||
// Create invisible fake target NPC for targeting
|
||||
createFakeTarget(npc);
|
||||
|
||||
// Set preview target on the ability
|
||||
ability.setPreviewTarget(fakeTarget);
|
||||
|
||||
// Start tracking NPC movement
|
||||
if (parentGui != null) {
|
||||
parentGui.startTrackingMovement();
|
||||
}
|
||||
|
||||
// Use the ability's real start() method with the fake target
|
||||
ability.start(fakeTarget);
|
||||
|
||||
if (ability.getWindUpTicks() <= 0) {
|
||||
// No windup — go straight to active
|
||||
this.phase = AbilityPhase.ACTIVE;
|
||||
this.executorPhase = ExecutorPhase.ACTIVE;
|
||||
transitionToActive();
|
||||
} else {
|
||||
// Normal windup flow
|
||||
this.phase = AbilityPhase.WINDUP;
|
||||
this.executorPhase = ExecutorPhase.WINDUP;
|
||||
|
||||
// Create telegraph for preview
|
||||
previewTelegraph = ability.createTelegraph(npc, fakeTarget);
|
||||
@@ -121,24 +122,17 @@ public class AbilityPreviewExecutor {
|
||||
data.setAnimation(windUpAnim);
|
||||
data.animation.paused = false;
|
||||
}
|
||||
|
||||
// Spawn preview entity on first tick if ability spawns during windup
|
||||
if (ability.spawnPreviewDuringWindup()) {
|
||||
spawnPreviewEntity();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an invisible fake target NPC placed in front of the preview NPC.
|
||||
* This entity is NOT rendered - it only exists for ability targeting logic.
|
||||
*/
|
||||
private void createFakeTarget(EntityNPCInterface npc) {
|
||||
if (npc.worldObj == null) return;
|
||||
|
||||
try {
|
||||
fakeTarget = new EntityCustomNpc(npc.worldObj);
|
||||
// Position target in front of NPC based on its facing direction
|
||||
float facingYaw = GuiAbilityInterface.NPC_FACING_YAW;
|
||||
double yawRad = Math.toRadians(facingYaw);
|
||||
double targetX = npc.posX - Math.sin(yawRad) * TARGET_DISTANCE;
|
||||
@@ -153,15 +147,12 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fake target entity for abilities that need a target.
|
||||
*/
|
||||
public EntityLivingBase getFakeTarget() {
|
||||
return fakeTarget;
|
||||
}
|
||||
|
||||
public void play() {
|
||||
if (previewAbility == null || phase == AbilityPhase.IDLE) {
|
||||
if (previewAbility == null || executorPhase == ExecutorPhase.IDLE) {
|
||||
return;
|
||||
}
|
||||
playing = true;
|
||||
@@ -178,6 +169,9 @@ public class AbilityPreviewExecutor {
|
||||
public void pause() {
|
||||
paused = true;
|
||||
|
||||
// Immediately sync prevPos to pos for NPC and all entities to prevent interpolation jitter
|
||||
syncPrevPositions();
|
||||
|
||||
if (previewNpc != null) {
|
||||
AnimationData data = previewNpc.display.animationData;
|
||||
if (data.animation != null) {
|
||||
@@ -186,10 +180,37 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync prevPos to pos for the NPC and all preview entities.
|
||||
* Prevents rendering interpolation jitter when standing still or paused.
|
||||
*/
|
||||
private void syncPrevPositions() {
|
||||
if (previewNpc != null) {
|
||||
previewNpc.prevPosX = previewNpc.posX;
|
||||
previewNpc.prevPosY = previewNpc.posY;
|
||||
previewNpc.prevPosZ = previewNpc.posZ;
|
||||
}
|
||||
for (Entity entity : previewEntities) {
|
||||
if (entity != null && !entity.isDead) {
|
||||
entity.prevPosX = entity.posX;
|
||||
entity.prevPosY = entity.posY;
|
||||
entity.prevPosZ = entity.posZ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
// Clean up the ability
|
||||
if (previewAbility != null) {
|
||||
previewAbility.cleanup();
|
||||
previewAbility.setPreviewMode(false);
|
||||
previewAbility.setPreviewEntityHandler(null);
|
||||
previewAbility.reset();
|
||||
}
|
||||
|
||||
playing = false;
|
||||
paused = false;
|
||||
phase = AbilityPhase.IDLE;
|
||||
executorPhase = ExecutorPhase.IDLE;
|
||||
currentTick = 0;
|
||||
|
||||
previewTelegraph = null;
|
||||
@@ -211,9 +232,11 @@ public class AbilityPreviewExecutor {
|
||||
previewNpc.prevPosX = npcStartX;
|
||||
previewNpc.prevPosY = npcStartY;
|
||||
previewNpc.prevPosZ = npcStartZ;
|
||||
previewNpc.motionX = 0;
|
||||
previewNpc.motionY = 0;
|
||||
previewNpc.motionZ = 0;
|
||||
}
|
||||
|
||||
// Remove fake target
|
||||
fakeTarget = null;
|
||||
|
||||
if (previewNpc != null) {
|
||||
@@ -224,13 +247,24 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Tick the preview forward.
|
||||
* Tick the preview forward. Calls real ability methods.
|
||||
*/
|
||||
public void tick() {
|
||||
if (!playing || paused || previewAbility == null || previewNpc == null) {
|
||||
if (!playing || previewAbility == null || previewNpc == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (paused) {
|
||||
// Sync prevPos to pos for NPC and all entities to prevent interpolation jitter while paused
|
||||
syncPrevPositions();
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync NPC's prevPos to current pos BEFORE any updates to prevent jitter when standing still
|
||||
previewNpc.prevPosX = previewNpc.posX;
|
||||
previewNpc.prevPosY = previewNpc.posY;
|
||||
previewNpc.prevPosZ = previewNpc.posZ;
|
||||
|
||||
currentTick++;
|
||||
|
||||
if (previewTelegraph != null) {
|
||||
@@ -239,14 +273,14 @@ public class AbilityPreviewExecutor {
|
||||
|
||||
tickEntities();
|
||||
|
||||
switch (phase) {
|
||||
switch (executorPhase) {
|
||||
case WINDUP:
|
||||
tickWindup();
|
||||
break;
|
||||
case ACTIVE:
|
||||
tickActive();
|
||||
break;
|
||||
case IDLE:
|
||||
case EXTENDED:
|
||||
tickExtended();
|
||||
break;
|
||||
default:
|
||||
@@ -255,7 +289,8 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
private void tickWindup() {
|
||||
previewAbility.onPreviewWindUpTick(previewNpc, currentTick);
|
||||
// Call the REAL windup tick method
|
||||
previewAbility.onWindUpTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick);
|
||||
|
||||
if (currentTick >= previewAbility.getWindUpTicks()) {
|
||||
transitionToActive();
|
||||
@@ -263,35 +298,41 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
private void tickActive() {
|
||||
previewAbility.onPreviewActiveTick(previewNpc, currentTick);
|
||||
// Call the REAL active tick method
|
||||
previewAbility.onActiveTick(previewNpc, fakeTarget, previewNpc.worldObj, currentTick);
|
||||
|
||||
if (currentTick == 1 && !previewAbility.spawnPreviewDuringWindup()) {
|
||||
spawnPreviewEntity();
|
||||
fireEntities();
|
||||
// Simulate physics for movement abilities
|
||||
simulatePhysics();
|
||||
|
||||
// Check if the ability signaled completion (phase went to IDLE)
|
||||
if (previewAbility.getPhase() == AbilityPhase.IDLE) {
|
||||
transitionToExtended();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTick >= activeDuration) {
|
||||
// Safety timeout
|
||||
if (currentTick >= maxPreviewDuration) {
|
||||
transitionToExtended();
|
||||
}
|
||||
}
|
||||
|
||||
private void tickExtended() {
|
||||
// Continue ticking entities so they can finish their animations
|
||||
if (currentTick >= EXTENDED_DURATION) {
|
||||
playing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void transitionToActive() {
|
||||
phase = AbilityPhase.ACTIVE;
|
||||
executorPhase = ExecutorPhase.ACTIVE;
|
||||
currentTick = 0;
|
||||
|
||||
previewTelegraph = null;
|
||||
|
||||
// Notify ability of execution (for movement abilities like Slam/Charge/Dash)
|
||||
previewAbility.onPreviewExecute(previewNpc);
|
||||
|
||||
fireEntities();
|
||||
// Call the REAL onExecute method (fires entities, initiates movement, etc.)
|
||||
previewAbility.onExecute(previewNpc, fakeTarget, previewNpc.worldObj);
|
||||
|
||||
// Start active animation
|
||||
Animation activeAnim = previewAbility.getActiveAnimation();
|
||||
if (activeAnim != null && previewNpc != null) {
|
||||
AnimationData data = previewNpc.display.animationData;
|
||||
@@ -301,7 +342,7 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
|
||||
private void transitionToExtended() {
|
||||
phase = AbilityPhase.IDLE;
|
||||
executorPhase = ExecutorPhase.EXTENDED;
|
||||
currentTick = 0;
|
||||
|
||||
if (previewNpc != null) {
|
||||
@@ -310,26 +351,40 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private void spawnPreviewEntity() {
|
||||
Entity entity = previewAbility.createPreviewEntity(previewNpc);
|
||||
if (entity != null) {
|
||||
previewEntities.add(entity);
|
||||
if (parentGui != null) {
|
||||
parentGui.addPreviewEntity(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Simulate basic physics for movement abilities (Slam, Charge, Dash).
|
||||
* Applies gravity, drag, and ground clamping to NPC position.
|
||||
*/
|
||||
private void simulatePhysics() {
|
||||
if (!previewAbility.hasAbilityMovement()) return;
|
||||
if (previewNpc == null) return;
|
||||
|
||||
private void fireEntities() {
|
||||
for (Entity entity : previewEntities) {
|
||||
if (entity instanceof EntityAbilityOrb) {
|
||||
((EntityAbilityOrb) entity).startPreviewFiring();
|
||||
} else if (entity instanceof EntityAbilityBeam) {
|
||||
((EntityAbilityBeam) entity).startPreviewFiring();
|
||||
} else if (entity instanceof EntityAbilityDisc) {
|
||||
((EntityAbilityDisc) entity).startPreviewFiring();
|
||||
}
|
||||
// Save previous position for interpolation
|
||||
previewNpc.prevPosX = previewNpc.posX;
|
||||
previewNpc.prevPosY = previewNpc.posY;
|
||||
previewNpc.prevPosZ = previewNpc.posZ;
|
||||
|
||||
// Apply velocity
|
||||
previewNpc.posX += previewNpc.motionX;
|
||||
previewNpc.posY += previewNpc.motionY;
|
||||
previewNpc.posZ += previewNpc.motionZ;
|
||||
|
||||
// Apply gravity
|
||||
previewNpc.motionY -= 0.08;
|
||||
previewNpc.motionY *= 0.98;
|
||||
|
||||
// Apply horizontal drag
|
||||
previewNpc.motionX *= 0.91;
|
||||
previewNpc.motionZ *= 0.91;
|
||||
|
||||
// Ground clamp: if below start position and falling, stop
|
||||
if (previewNpc.posY < npcStartY && previewNpc.motionY < 0) {
|
||||
previewNpc.posY = npcStartY;
|
||||
previewNpc.motionY = 0;
|
||||
previewNpc.onGround = true;
|
||||
}
|
||||
|
||||
previewNpc.fallDistance = 0;
|
||||
}
|
||||
|
||||
private void tickEntities() {
|
||||
@@ -347,12 +402,38 @@ public class AbilityPreviewExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== PreviewEntityHandler ====================
|
||||
|
||||
@Override
|
||||
public void onEntitySpawned(Entity entity) {
|
||||
if (entity == null) return;
|
||||
previewEntities.add(entity);
|
||||
if (parentGui != null) {
|
||||
parentGui.addPreviewEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemoved(Entity entity) {
|
||||
if (entity == null) return;
|
||||
previewEntities.remove(entity);
|
||||
if (parentGui != null) {
|
||||
parentGui.removePreviewEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== GETTERS ====================
|
||||
|
||||
public boolean isPlaying() { return playing; }
|
||||
public boolean isPaused() { return paused; }
|
||||
public boolean isActive() { return playing || paused; }
|
||||
public AbilityPhase getPhase() { return phase; }
|
||||
public AbilityPhase getPhase() {
|
||||
switch (executorPhase) {
|
||||
case WINDUP: return AbilityPhase.WINDUP;
|
||||
case ACTIVE: return AbilityPhase.ACTIVE;
|
||||
default: return AbilityPhase.IDLE;
|
||||
}
|
||||
}
|
||||
public int getCurrentTick() { return currentTick; }
|
||||
public TelegraphInstance getTelegraph() { return previewTelegraph; }
|
||||
public List<Entity> getPreviewEntities() { return previewEntities; }
|
||||
@@ -364,11 +445,11 @@ public class AbilityPreviewExecutor {
|
||||
return "Stopped";
|
||||
} else if (paused) {
|
||||
return "Paused";
|
||||
} else if (phase == AbilityPhase.WINDUP) {
|
||||
} else if (executorPhase == ExecutorPhase.WINDUP) {
|
||||
return "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0);
|
||||
} else if (phase == AbilityPhase.ACTIVE) {
|
||||
return "Active: " + currentTick + "/" + activeDuration;
|
||||
} else if (phase == AbilityPhase.IDLE && playing) {
|
||||
} else if (executorPhase == ExecutorPhase.ACTIVE) {
|
||||
return "Active: " + currentTick + "/" + maxPreviewDuration;
|
||||
} else if (executorPhase == ExecutorPhase.EXTENDED) {
|
||||
return "Extended: " + currentTick + "/" + EXTENDED_DURATION;
|
||||
}
|
||||
return "";
|
||||
|
||||
@@ -11,10 +11,7 @@ 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.EntityUtil;
|
||||
import noppes.npcs.entity.EntityCustomNpc;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import noppes.npcs.entity.data.ModelData;
|
||||
import org.lwjgl.input.Mouse;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
@@ -50,7 +47,6 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
|
||||
private GuiNpcButton btnLeft, btnRight;
|
||||
|
||||
// ==================== NPC STATE ====================
|
||||
public ModelData playerdata;
|
||||
public EntityNPCInterface npc;
|
||||
|
||||
/** NPC starting position (saved when preview starts, used to track movement) */
|
||||
@@ -85,7 +81,6 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
|
||||
public GuiAbilityInterface(EntityNPCInterface npc, boolean hasMenuNpc) {
|
||||
super(hasMenuNpc ? npc : null);
|
||||
this.npc = npc;
|
||||
this.playerdata = ((EntityCustomNpc) npc).modelData;
|
||||
this.drawDefaultBackground = false;
|
||||
|
||||
CAMERA_ZOOM = 30f;
|
||||
@@ -135,20 +130,17 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
|
||||
* Draw the 3D preview with NPC and entities.
|
||||
*/
|
||||
protected void drawPreview(int mouseX, int mouseY, float partialTicks) {
|
||||
if (hasSubGui())
|
||||
return;
|
||||
|
||||
GL11.glColor4f(1, 1, 1, 1);
|
||||
|
||||
// Enable scissor to clip rendering to preview area
|
||||
GL11.glEnable(GL11.GL_SCISSOR_TEST);
|
||||
setScissorClip(previewX, previewY, previewWidth, previewHeight);
|
||||
|
||||
// Get the entity to render (could be transformed player model)
|
||||
EntityLivingBase renderEntity = playerdata.getEntity(npc);
|
||||
if (renderEntity == null) {
|
||||
renderEntity = this.npc;
|
||||
}
|
||||
|
||||
// Sync NPC properties to render entity
|
||||
EntityUtil.Copy(npc, renderEntity);
|
||||
// Always use the standard NPC animation renderer for ability preview
|
||||
EntityLivingBase renderEntity = this.npc;
|
||||
|
||||
// Set NPC facing direction (affects anchor points and visual facing)
|
||||
npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW;
|
||||
@@ -204,11 +196,7 @@ public class GuiAbilityInterface extends GuiNPCInterface2 {
|
||||
ClientEventHandler.renderingEntityInGUI = true;
|
||||
|
||||
// Render the NPC at its position delta (for movement like slam jumps)
|
||||
try {
|
||||
RenderManager.instance.renderEntityWithPosYaw(renderEntity, npcDeltaX, npcDeltaY, npcDeltaZ, 0.0F, partialTicks);
|
||||
} catch (Exception e) {
|
||||
playerdata.setEntityClass(null);
|
||||
}
|
||||
RenderManager.instance.renderEntityWithPosYaw(renderEntity, npcDeltaX, npcDeltaY, npcDeltaZ, 0.0F, partialTicks);
|
||||
|
||||
// Render preview entities at their positions relative to NPC start position
|
||||
renderPreviewEntities(partialTicks);
|
||||
|
||||
@@ -200,7 +200,8 @@ public class GuiCustomScroll extends GuiScreen {
|
||||
int k = (14 * i + 4) - scrollY;
|
||||
if (k >= 4 && k + 12 < ySize) {
|
||||
int xOffset = scrollHeight < ySize - 8 ? 0 : 10;
|
||||
String displayString = StatCollector.translateToLocal(list.get(i));
|
||||
String rawEntry = list.get(i);
|
||||
String displayString = StatCollector.translateToLocal(rawEntry);
|
||||
|
||||
String text = "";
|
||||
float maxWidth = (xSize + xOffset - 8) * 0.8f;
|
||||
@@ -215,7 +216,13 @@ public class GuiCustomScroll extends GuiScreen {
|
||||
text += "...";
|
||||
} else
|
||||
text = displayString;
|
||||
if ((multipleSelection && selectedList.contains(text)) || (!multipleSelection && selected == i)) {
|
||||
|
||||
// Entries with a custom color are non-interactive (headers/separators)
|
||||
Integer customColor = colors.get(rawEntry);
|
||||
if (customColor != null) {
|
||||
if (!text.isEmpty())
|
||||
fontRendererObj.drawString(text, j, k, customColor);
|
||||
} else if ((multipleSelection && selectedList.contains(text)) || (!multipleSelection && selected == i)) {
|
||||
drawVerticalLine(j - 2, k - 4, k + 10, 0xffffffff);
|
||||
drawVerticalLine(j + xSize - 18 + xOffset, k - 4, k + 10, 0xffffffff);
|
||||
drawHorizontalLine(j - 2, j + xSize - 18 + xOffset, k - 3, 0xffffffff);
|
||||
@@ -239,13 +246,17 @@ public class GuiCustomScroll extends GuiScreen {
|
||||
private int getMouseOver(int i, int j) {
|
||||
i -= guiLeft;
|
||||
j -= guiTop;
|
||||
//fontRenderer.drawString(".", i , j, 0xffffff);
|
||||
if (i >= 4 && i < xSize - 4 && j >= 4 && j < ySize) {
|
||||
for (int j1 = 0; j1 < list.size(); j1++) {
|
||||
if (!mouseInOption(i, j, j1)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip non-interactive entries (colored headers/separators)
|
||||
if (colors.containsKey(list.get(j1))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return j1;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ public class ImageData {
|
||||
|
||||
public ImageData(String directory) {
|
||||
this.location = new ResourceLocation(directory);
|
||||
if (directory.startsWith("https://")) {
|
||||
if (directory.startsWith("https://") || directory.startsWith("http://")) {
|
||||
this.isUrl = true;
|
||||
TextureManager texturemanager = Minecraft.getMinecraft().getTextureManager();
|
||||
this.imageDownloadAlt = new ImageDownloadAlt(null, directory, new ResourceLocation("customnpcs:textures/gui/invisible.png"), new ImageBufferDownloadAlt(true, false));
|
||||
|
||||
@@ -8,6 +8,7 @@ public enum EnumScrollData {
|
||||
QUEST_GROUP,
|
||||
ABILITY_TYPES,
|
||||
CUSTOM_ABILITIES,
|
||||
BUILTIN_ABILITIES,
|
||||
ANIMATIONS,
|
||||
BUILTIN_ANIMATIONS
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
|
||||
import kamkeel.npcs.controllers.data.ability.UserType;
|
||||
import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
|
||||
import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket;
|
||||
@@ -118,9 +119,8 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
|
||||
case ACTIVE:
|
||||
if (phaseChanged && oldPhase == AbilityPhase.WINDUP) {
|
||||
// Just entered ACTIVE phase
|
||||
TelegraphInstance telegraph = currentAbility.getTelegraphInstance();
|
||||
if (telegraph != null) {
|
||||
// Just entered ACTIVE phase - lock all telegraph positions
|
||||
for (TelegraphInstance telegraph : currentAbility.getTelegraphInstances()) {
|
||||
telegraph.lockPosition();
|
||||
}
|
||||
removeTelegraph(currentAbility, player);
|
||||
@@ -418,6 +418,53 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle damage taken while an ability is executing.
|
||||
* Called from LivingHurtEvent BEFORE damage is applied to health.
|
||||
* Triggers Guard counter detection and handles ability interruption.
|
||||
*
|
||||
* @param source The damage source
|
||||
* @param amount The damage amount (post-armor)
|
||||
* @return The modified damage amount (reduced by guard, or 0 if counter absorbs)
|
||||
*/
|
||||
public float onDamage(DamageSource source, float amount) {
|
||||
if (currentAbility == null || !currentAbility.isExecuting()) {
|
||||
return amount;
|
||||
}
|
||||
|
||||
net.minecraft.entity.Entity sourceEntity = source.getEntity();
|
||||
EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null;
|
||||
|
||||
// Track counter state before calling onDamageTaken
|
||||
boolean wasCounterTriggered = false;
|
||||
if (currentAbility instanceof AbilityGuard) {
|
||||
wasCounterTriggered = ((AbilityGuard) currentAbility).isCounterTriggered();
|
||||
}
|
||||
|
||||
currentAbility.onDamageTaken(playerData.player, attacker, source, amount);
|
||||
|
||||
// Guard: handle damage reduction and counter absorption
|
||||
if (currentAbility instanceof AbilityGuard) {
|
||||
AbilityGuard guard = (AbilityGuard) currentAbility;
|
||||
if (guard.isGuarding()) {
|
||||
// Counter just triggered — absorb full damage
|
||||
if (!wasCounterTriggered && guard.isCounterTriggered()) {
|
||||
return 0;
|
||||
}
|
||||
// Normal guard — apply flat damage reduction
|
||||
float reduction = guard.getDamageReductionFactor();
|
||||
return Math.max(0, amount - reduction);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for ability interruption
|
||||
if (currentAbility != null && currentAbility.canInterrupt(source)) {
|
||||
interruptCurrentAbility();
|
||||
}
|
||||
|
||||
return amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOnCooldown() {
|
||||
EntityPlayer player = playerData.player;
|
||||
@@ -451,23 +498,25 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void spawnTelegraph(Ability ability, EntityPlayer player, EntityLivingBase target) {
|
||||
TelegraphInstance telegraph = ability.createTelegraph(player, target);
|
||||
if (telegraph != null) {
|
||||
ability.setTelegraphInstance(telegraph);
|
||||
List<TelegraphInstance> telegraphs = ability.createTelegraphs(player, target);
|
||||
if (!telegraphs.isEmpty()) {
|
||||
ability.setTelegraphInstances(telegraphs);
|
||||
if (player instanceof EntityPlayerMP) {
|
||||
TelegraphSpawnPacket.sendToTracking(telegraph, player);
|
||||
for (TelegraphInstance telegraph : telegraphs) {
|
||||
TelegraphSpawnPacket.sendToTracking(telegraph, player);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeTelegraph(Ability ability, EntityPlayer player) {
|
||||
TelegraphInstance telegraph = ability.getTelegraphInstance();
|
||||
if (telegraph != null) {
|
||||
List<TelegraphInstance> telegraphs = ability.getTelegraphInstances();
|
||||
for (TelegraphInstance telegraph : telegraphs) {
|
||||
if (player instanceof EntityPlayerMP) {
|
||||
TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player);
|
||||
}
|
||||
ability.setTelegraphInstance(null);
|
||||
}
|
||||
ability.setTelegraphInstances(null);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -308,6 +308,13 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
@Override
|
||||
public void onUpdate() {
|
||||
super.onUpdate();
|
||||
|
||||
// Re-apply locks after super.onUpdate() to override body rotation smoothing
|
||||
// in EntityLivingBase.onUpdate() (func_110146_f) which runs AFTER onLivingUpdate()
|
||||
// and overwrites renderYawOffset, causing a visible twitch each frame.
|
||||
abilities.applyRotationControl();
|
||||
abilities.applyLockedPosition();
|
||||
|
||||
if (!isRemote()) {
|
||||
if (this.ticksExisted % 10 == 0) {
|
||||
EventHooks.onNPCUpdate(this);
|
||||
@@ -481,8 +488,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
|
||||
super.onLivingUpdate();
|
||||
|
||||
// Apply locked rotation after super.onLivingUpdate() to override look helper changes
|
||||
abilities.applyLockedRotation();
|
||||
// Apply locks after super.onLivingUpdate() to override AI/physics changes
|
||||
abilities.applyRotationControl();
|
||||
abilities.applyLockedPosition();
|
||||
|
||||
handleMountRiderState();
|
||||
|
||||
@@ -670,6 +678,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
if (entity instanceof EntityLivingBase)
|
||||
attackingEntity = (EntityLivingBase) entity;
|
||||
|
||||
if (faction.isPassive)
|
||||
return false;
|
||||
|
||||
if (attackingEntity != null && attackingEntity == getOwner())
|
||||
return false;
|
||||
else if (attackingEntity instanceof EntityNPCInterface) {
|
||||
|
||||
@@ -42,12 +42,25 @@ public class ScriptDataAbilities implements IDataAbilities {
|
||||
@Override
|
||||
public void addAbility(IAbility ability) {
|
||||
if (ability instanceof Ability) {
|
||||
data.addAbility((Ability) ability);
|
||||
Ability a = (Ability) ability;
|
||||
// Validate ability allows NPCs
|
||||
if (!a.getAllowedBy().allowsNpc()) {
|
||||
return; // Ability is PLAYER_ONLY
|
||||
}
|
||||
data.addAbility(a);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAbilityReference(String key) {
|
||||
// Validate ability exists and allows NPCs
|
||||
Ability ability = AbilityController.Instance.resolveAbility(key);
|
||||
if (ability == null) {
|
||||
return; // Ability doesn't exist
|
||||
}
|
||||
if (!ability.getAllowedBy().allowsNpc()) {
|
||||
return; // Ability is PLAYER_ONLY
|
||||
}
|
||||
data.addAbilityReference(key);
|
||||
}
|
||||
|
||||
@@ -158,6 +171,14 @@ public class ScriptDataAbilities implements IDataAbilities {
|
||||
|
||||
@Override
|
||||
public boolean executeAbility(String key, Object target) {
|
||||
// Validate ability exists and allows NPCs
|
||||
Ability ability = AbilityController.Instance.resolveAbility(key);
|
||||
if (ability == null) {
|
||||
return false; // Ability doesn't exist
|
||||
}
|
||||
if (!ability.getAllowedBy().allowsNpc()) {
|
||||
return false; // Ability is PLAYER_ONLY
|
||||
}
|
||||
EntityLivingBase targetEntity = resolveTarget(target);
|
||||
return data.executeAbility(key, targetEntity);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package noppes.npcs.scripted.wrapper;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import noppes.npcs.api.ability.IAbility;
|
||||
import noppes.npcs.api.ability.IPlayerAbilityData;
|
||||
@@ -26,7 +28,17 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData {
|
||||
|
||||
@Override
|
||||
public void unlockAbility(String key) {
|
||||
data.unlockAbility(key);
|
||||
// Validate ability exists and allows players
|
||||
Ability ability = AbilityController.Instance.resolveAbility(key);
|
||||
if (ability == null) {
|
||||
return; // Ability doesn't exist
|
||||
}
|
||||
if (!ability.getAllowedBy().allowsPlayer()) {
|
||||
return; // Ability is NPC-only
|
||||
}
|
||||
// Use canonical key (registry key for built-in, UUID for custom)
|
||||
String canonicalKey = ability.getId() != null ? ability.getId() : key;
|
||||
data.unlockAbility(canonicalKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"Loop": 0,
|
||||
"Speed": 1.0f,
|
||||
"Frames": [
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 3.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": -50.0f,
|
||||
"Rotation0": -75.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": -45.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 50.0f,
|
||||
"Rotation0": -75.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 45.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 31.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 2
|
||||
}
|
||||
],
|
||||
"CurrentFrameTime": 0,
|
||||
"CurrentFrame": 0,
|
||||
"Smooth": 0b,
|
||||
"ID": 28,
|
||||
"WhileAttacking": 1b,
|
||||
"WhileWalking": 1b,
|
||||
"Name": "Ability_Charge_Active",
|
||||
"WhileStanding": 1b
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
{
|
||||
"Loop": -1,
|
||||
"Speed": 1.0f,
|
||||
"Frames": [
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 0.8f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 69.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 5.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -8.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 44.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -8.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": -18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -10.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -10.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 75.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 18
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 3.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": -50.0f,
|
||||
"Rotation0": -75.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": -45.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 50.0f,
|
||||
"Rotation0": -75.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 45.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 31.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 2
|
||||
}
|
||||
],
|
||||
"CurrentFrameTime": 0,
|
||||
"CurrentFrame": 0,
|
||||
"Smooth": 0b,
|
||||
"ID": 27,
|
||||
"WhileAttacking": 1b,
|
||||
"WhileWalking": 1b,
|
||||
"Name": "Ability_Charge_Windup",
|
||||
"WhileStanding": 1b
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"Loop": 5,
|
||||
"Loop": -1,
|
||||
"Speed": 1.0f,
|
||||
"Frames": [
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 2.0f,
|
||||
"Speed": 3.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -5.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": -37.0f,
|
||||
"Rotation0": 37.0f,
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": 3.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
@@ -25,15 +25,6 @@
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 56.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -37.0f,
|
||||
"Rotation0": 31.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -25.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
@@ -43,33 +34,6 @@
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -1.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -18.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": 3.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 2
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 1.5f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -5.0f,
|
||||
"Part": "BODY",
|
||||
@@ -79,33 +43,6 @@
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -7.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 3.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 56.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -37.0f,
|
||||
"Rotation0": 31.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -25.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 120.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -1.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
@@ -115,42 +52,6 @@
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": 3.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 4
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 0.5f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -5.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": -37.0f,
|
||||
"Rotation0": 37.0f,
|
||||
"Pivot0": 3.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -7.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 3.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 56.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
@@ -159,37 +60,10 @@
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -25.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 120.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -1.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -18.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": 3.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 4
|
||||
"Duration": 3
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
@@ -198,8 +72,8 @@
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": -25.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
@@ -214,15 +88,6 @@
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 75.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
@@ -232,6 +97,15 @@
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": -12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
@@ -242,17 +116,17 @@
|
||||
"Rotation2": 6.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 2
|
||||
"Duration": 3
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
@@ -261,8 +135,8 @@
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": -25.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
@@ -277,15 +151,6 @@
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 75.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
@@ -295,6 +160,15 @@
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": -12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
@@ -305,27 +179,27 @@
|
||||
"Rotation2": 6.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 4
|
||||
"Duration": 3
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 0.5f,
|
||||
"Speed": 0.25f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": -25.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
@@ -340,15 +214,6 @@
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 75.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
@@ -358,6 +223,15 @@
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": -12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
@@ -368,17 +242,17 @@
|
||||
"Rotation2": 6.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 2.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -12.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 4
|
||||
"Duration": 3
|
||||
}
|
||||
],
|
||||
"CurrentFrameTime": 0,
|
||||
|
||||
@@ -7,51 +7,6 @@
|
||||
"Comment": "",
|
||||
"Speed": 0.5f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -8.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 25.0f,
|
||||
"Rotation0": 31.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -5.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 50.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -2.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -31.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -8.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 31.0f,
|
||||
"Rotation0": -56.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -3.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": 25.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "FULL_MODEL",
|
||||
@@ -69,6 +24,51 @@
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -5.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 50.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -8.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 31.0f,
|
||||
"Rotation0": -56.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -8.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 25.0f,
|
||||
"Rotation0": 31.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -3.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": 25.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -2.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -31.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
@@ -79,51 +79,6 @@
|
||||
"Comment": "",
|
||||
"Speed": 0.35f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 50.0f,
|
||||
"Rotation0": -6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 5.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 50.0f,
|
||||
"Pivot0": 2.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 170.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -3.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 75.0f,
|
||||
"Rotation0": -107.0f,
|
||||
"Pivot0": -6.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -4.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -6.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": 50.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": -3.0f,
|
||||
"Pivot1": -4.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "FULL_MODEL",
|
||||
@@ -141,6 +96,51 @@
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 5.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 50.0f,
|
||||
"Pivot0": 2.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 170.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -3.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 75.0f,
|
||||
"Rotation0": -107.0f,
|
||||
"Pivot0": -6.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 50.0f,
|
||||
"Rotation0": -6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": 50.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": -3.0f,
|
||||
"Pivot1": -4.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -4.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -6.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
@@ -153,9 +153,18 @@
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 50.0f,
|
||||
"Rotation0": -6.0f,
|
||||
"Part": "FULL_MODEL",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
@@ -179,10 +188,10 @@
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -4.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -6.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 50.0f,
|
||||
"Rotation0": -6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
@@ -197,19 +206,10 @@
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "FULL_MODEL",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot2": -4.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -6.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
@@ -221,13 +221,13 @@
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 2.0f,
|
||||
"Speed": 3.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -5.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": -37.0f,
|
||||
"Rotation0": 37.0f,
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": 3.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
@@ -241,15 +241,6 @@
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 56.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -37.0f,
|
||||
"Rotation0": 31.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -25.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
@@ -259,6 +250,15 @@
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -5.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": -37.0f,
|
||||
"Rotation0": 37.0f,
|
||||
"Pivot0": 3.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -1.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
@@ -269,13 +269,13 @@
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 18.0f,
|
||||
"Pivot0": 3.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -37.0f,
|
||||
"Rotation0": 31.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -25.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
@@ -283,7 +283,7 @@
|
||||
}
|
||||
],
|
||||
"CurrentFrameTime": 0,
|
||||
"CurrentFrame": 1,
|
||||
"CurrentFrame": 3,
|
||||
"Smooth": 0b,
|
||||
"ID": 17,
|
||||
"WhileAttacking": 1b,
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"Loop": 1,
|
||||
"Speed": 1.0f,
|
||||
"Frames": [
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 3.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -7.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -7.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -90.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -1.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": -90.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": -25.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 2
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 3.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -7.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -1.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 25.0f,
|
||||
"Rotation0": -90.0f,
|
||||
"Pivot0": 1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 25.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -7.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -90.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 5
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 3.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -7.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -7.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -90.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -1.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": -90.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": -25.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 5
|
||||
}
|
||||
],
|
||||
"CurrentFrameTime": 0,
|
||||
"CurrentFrame": 0,
|
||||
"Smooth": 0b,
|
||||
"ID": 26,
|
||||
"WhileAttacking": 1b,
|
||||
"WhileWalking": 1b,
|
||||
"Name": "Ability_OrbBarrage_Active",
|
||||
"WhileStanding": 1b
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
{
|
||||
"Loop": -1,
|
||||
"Speed": 1.0f,
|
||||
"Frames": [
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": 31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 56.0f,
|
||||
"Rotation0": -126.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -56.0f,
|
||||
"Rotation0": -126.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 12.0f
|
||||
}
|
||||
],
|
||||
"Duration": 8
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 0.25f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": 31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 56.0f,
|
||||
"Rotation0": -126.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": -31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -56.0f,
|
||||
"Rotation0": -126.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 12.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 19
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 3.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -7.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 2.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -7.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -90.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 31.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -18.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -6.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -1.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": -90.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": -25.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 3
|
||||
}
|
||||
],
|
||||
"CurrentFrameTime": 0,
|
||||
"CurrentFrame": 2,
|
||||
"Smooth": 0b,
|
||||
"ID": 25,
|
||||
"WhileAttacking": 1b,
|
||||
"WhileWalking": 1b,
|
||||
"Name": "Ability_OrbBarrage_Windup",
|
||||
"WhileStanding": 1b
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
{
|
||||
"Loop": 3,
|
||||
"Speed": 1.0f,
|
||||
"Frames": [
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 1.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -12.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 82.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 25.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -10.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 6.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -11.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 101.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -10.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": -6.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 1.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 10
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 2.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": 2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 3
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 0.5f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": 2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 6
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 0.5f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 1.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 1.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -56.0f,
|
||||
"Rotation0": -37.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 56.0f,
|
||||
"Rotation0": -37.0f,
|
||||
"Pivot0": 2.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 5
|
||||
}
|
||||
],
|
||||
"CurrentFrameTime": 0,
|
||||
"CurrentFrame": 0,
|
||||
"Smooth": 0b,
|
||||
"ID": 24,
|
||||
"WhileAttacking": 1b,
|
||||
"WhileWalking": 1b,
|
||||
"Name": "Ability_Zone_Active",
|
||||
"WhileStanding": 1b
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
{
|
||||
"Loop": -1,
|
||||
"Speed": 1.0f,
|
||||
"Frames": [
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 2.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -3.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 12.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 12.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -6.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -3.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 37.0f,
|
||||
"Rotation0": -37.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -3.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 31.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -12.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 6.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -3.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": -37.0f,
|
||||
"Rotation0": -37.0f,
|
||||
"Pivot0": 1.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 6
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 0.25f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": 2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 6
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 0.5f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": 2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 6
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 0.75f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": 2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 6
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 1.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 3.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": -18.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": -56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": -2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -18.0f,
|
||||
"Rotation0": 6.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 0.0f,
|
||||
"Rotation2": 12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 4.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": 56.0f,
|
||||
"Rotation0": -132.0f,
|
||||
"Pivot0": 2.0f,
|
||||
"Pivot1": 3.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 3
|
||||
},
|
||||
{
|
||||
"ColorMarker": 16777215,
|
||||
"Comment": "",
|
||||
"Speed": 3.0f,
|
||||
"FrameParts": [
|
||||
{
|
||||
"Pivot2": -12.0f,
|
||||
"Part": "BODY",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 82.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "LEFT_LEG",
|
||||
"Rotation1": 25.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": -12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -10.0f,
|
||||
"Part": "LEFT_ARM",
|
||||
"Rotation1": 6.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": -1.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -11.0f,
|
||||
"Part": "HEAD",
|
||||
"Rotation1": 0.0f,
|
||||
"Rotation0": 101.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 0.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": 0.0f,
|
||||
"Part": "RIGHT_LEG",
|
||||
"Rotation1": -25.0f,
|
||||
"Rotation0": 25.0f,
|
||||
"Pivot0": 0.0f,
|
||||
"Pivot1": 1.0f,
|
||||
"Rotation2": 12.0f
|
||||
},
|
||||
{
|
||||
"Pivot2": -10.0f,
|
||||
"Part": "RIGHT_ARM",
|
||||
"Rotation1": -6.0f,
|
||||
"Rotation0": 0.0f,
|
||||
"Pivot0": 1.0f,
|
||||
"Pivot1": 12.0f,
|
||||
"Rotation2": 0.0f
|
||||
}
|
||||
],
|
||||
"Smooth": 0b,
|
||||
"Duration": 3
|
||||
}
|
||||
],
|
||||
"CurrentFrameTime": 0,
|
||||
"CurrentFrame": 5,
|
||||
"Smooth": 0b,
|
||||
"ID": 23,
|
||||
"WhileAttacking": 1b,
|
||||
"WhileWalking": 1b,
|
||||
"Name": "Ability_Zone_Windup",
|
||||
"WhileStanding": 1b
|
||||
}
|
||||
@@ -164,6 +164,7 @@ gui.playerMap=Regen PlayerMap
|
||||
gui.unused=Unused
|
||||
gui.apply=Apply
|
||||
gui.cancel=Cancel
|
||||
gui.continue=Continue
|
||||
gui.yes=Yes
|
||||
|
||||
gui.lineColor1=Upper line color
|
||||
@@ -638,6 +639,7 @@ faction.friendly=Friendly
|
||||
faction.points=Points
|
||||
faction.ondeath=On death
|
||||
faction.hidden=Hidden
|
||||
faction.hidden.hover=Hides this faction from the player's faction standings UI
|
||||
faction.default=Default
|
||||
faction.attackHostile=Attack hostile factions
|
||||
faction.defend=Defence faction members
|
||||
@@ -646,8 +648,9 @@ faction.options=Faction Options
|
||||
faction.increasepoints=Faction %s increased by %s points
|
||||
faction.decreasepoints=Faction %s decreased by %s points
|
||||
faction.attacked=Attacked by mobs
|
||||
faction.attacked.hover=Whether vanilla mobs will target and attack NPCs in this faction
|
||||
faction.passive=Is Passive
|
||||
faction.passive.hover=Passive factions will not queue any attacking tasks [Good for Performance]
|
||||
faction.passive.hover=Passive faction NPCs cannot be damaged and will not queue any attacking tasks [Good for Performance]
|
||||
|
||||
menu.inventory=Inventory
|
||||
menu.display=Display
|
||||
@@ -1546,6 +1549,7 @@ ability.anchor.rightHand=Right Hand
|
||||
ability.anchor.leftHand=Left Hand
|
||||
ability.anchor.aboveHead=Above Head
|
||||
ability.anchor.chest=Chest
|
||||
ability.anchor.eye=Eye
|
||||
|
||||
# Sweep Mode
|
||||
ability.sweep.swipe=Swipe
|
||||
@@ -1559,10 +1563,46 @@ ability.dash.defensive=Defensive
|
||||
ability.counter.flat=Flat
|
||||
ability.counter.percent=Percent
|
||||
|
||||
# Hazard Shape
|
||||
# Zone Shape
|
||||
ability.shape.circle=Circle
|
||||
ability.shape.square=Square
|
||||
ability.shape.ring=Ring
|
||||
ability.shape.cone=Cone
|
||||
ability.zoneShape=Shape
|
||||
|
||||
# Zone Presets
|
||||
ability.preset.custom=Custom
|
||||
ability.preset.default=Default
|
||||
ability.preset.toxic=Toxic
|
||||
ability.preset.inferno=Inferno
|
||||
ability.preset.arcane=Arcane
|
||||
ability.preset.electric=Electric
|
||||
ability.preset.frost=Frost
|
||||
|
||||
# Accent Styles
|
||||
ability.accentStyle.static=Static
|
||||
ability.accentStyle.swaying=Swaying
|
||||
ability.accentStyle.flickering=Flickering
|
||||
|
||||
# Particle Motion Styles
|
||||
ability.particleMotion.rising=Rising
|
||||
ability.particleMotion.drifting=Drifting
|
||||
ability.particleMotion.sparks=Sparks
|
||||
|
||||
# Zone Settings
|
||||
ability.section.zone=Zone Placement
|
||||
ability.telegraphSize=Telegraph Size
|
||||
ability.coneAngle=Cone Angle
|
||||
ability.ignoreInvulnFrames=Ignore Invuln Frames
|
||||
ability.visible=Visible
|
||||
|
||||
# Visual Layer Section Headers
|
||||
ability.section.ground=Ground Fill
|
||||
ability.section.rings=Rings
|
||||
ability.section.border=Border
|
||||
ability.section.accents=Accents
|
||||
ability.section.lightning=Lightning
|
||||
ability.section.particles=Particles
|
||||
|
||||
# Placement Mode
|
||||
ability.placement.atCaster=At Caster
|
||||
@@ -1604,11 +1644,8 @@ ability.cnpc.slam=Slam
|
||||
ability.cnpc.heavy_hit=Heavy Hit
|
||||
ability.cnpc.cutter=Cutter
|
||||
ability.cnpc.beam=Beam
|
||||
ability.cnpc.beam_dual=Beam Dual
|
||||
ability.cnpc.projectile=Projectile
|
||||
ability.cnpc.orb=Orb
|
||||
ability.cnpc.orb_dual=Orb Dual
|
||||
ability.cnpc.orb_barrage=Orb Barrage
|
||||
ability.cnpc.charge=Charge
|
||||
ability.cnpc.dash=Dash
|
||||
ability.cnpc.teleport=Teleport
|
||||
@@ -1619,7 +1656,6 @@ ability.cnpc.heal=Heal
|
||||
ability.cnpc.hazard=Hazard
|
||||
ability.cnpc.trap=Trap
|
||||
ability.cnpc.disc=Disc
|
||||
ability.cnpc.disc_dual=Disc Dual
|
||||
ability.cnpc.laser_shot=Laser Shot
|
||||
ability.cnpc.sweeper=Sweeper
|
||||
|
||||
@@ -1631,11 +1667,12 @@ ability.cnpc.sweeper=Sweeper
|
||||
ability.weight=Weight
|
||||
ability.windUpTicks=Wind Up
|
||||
ability.cooldownTicks=Cooldown
|
||||
ability.lockPosition=Lock Position
|
||||
ability.lockType=Lock Type
|
||||
ability.lockType.movement=Movement
|
||||
ability.lockType.rotation=Rotation
|
||||
ability.lockType.both=Both
|
||||
ability.lockMovement=Lock Movement
|
||||
ability.rotationMode=Rotation
|
||||
ability.rotationPhase=Phase
|
||||
ability.rotation.free=Free
|
||||
ability.rotation.locked=Locked
|
||||
ability.rotation.track=Track
|
||||
ability.interruptible=Interruptible
|
||||
ability.dazedTicks=Dazed Ticks
|
||||
ability.targetingMode=Targeting
|
||||
@@ -1661,6 +1698,7 @@ ability.section.timing=Timing
|
||||
ability.section.movement=Movement
|
||||
ability.section.sounds=Sounds
|
||||
ability.section.animations=Animations
|
||||
ability.section.animation=Animation
|
||||
ability.section.telegraph=Telegraph
|
||||
ability.section.beam=Beam
|
||||
ability.section.sweep=Sweep
|
||||
@@ -1672,14 +1710,16 @@ ability.section.boomerang=Boomerang
|
||||
ability.section.explosive=Explosive
|
||||
ability.section.colors=Colors
|
||||
ability.section.effects=Effects
|
||||
ability.section.dual=Dual
|
||||
ability.section.beam1=Beam 1
|
||||
ability.section.beam2=Beam 2
|
||||
ability.section.disc1=Disc 1
|
||||
ability.section.disc2=Disc 2
|
||||
ability.section.orb1=Orb 1
|
||||
ability.section.orb2=Orb 2
|
||||
ability.section.barrage=Barrage
|
||||
ability.section.burst=Burst
|
||||
ability.section.visual=Visual
|
||||
ability.section.projectile1=Projectile 1
|
||||
ability.section.projectile2=Projectile 2
|
||||
ability.section.projectile3=Projectile 3
|
||||
ability.section.projectile4=Projectile 4
|
||||
ability.section.projectile5=Projectile 5
|
||||
ability.section.projectile6=Projectile 6
|
||||
ability.section.projectile7=Projectile 7
|
||||
ability.section.projectile8=Projectile 8
|
||||
ability.section.counter=Counter
|
||||
ability.section.area=Area
|
||||
ability.section.damage=Damage
|
||||
@@ -1708,6 +1748,20 @@ gui.animation=Animation
|
||||
gui.duration=Duration
|
||||
gui.amp=Amp
|
||||
gui.speed=Speed
|
||||
gui.size=Size
|
||||
gui.alpha=Alpha
|
||||
gui.count=Count
|
||||
gui.style=Style
|
||||
gui.scale=Scale
|
||||
gui.motion=Motion
|
||||
gui.glow=Glow
|
||||
gui.texture=Texture
|
||||
gui.dps=DPS
|
||||
gui.interval=Interval
|
||||
gui.cooldown=Cooldown
|
||||
gui.max=Max
|
||||
gui.preset=Preset
|
||||
gui.applyPreset=Apply Preset
|
||||
|
||||
# Common Type Settings
|
||||
ability.radius=Radius
|
||||
@@ -1720,9 +1774,14 @@ ability.stunTicks=Stun Ticks
|
||||
ability.hitWidth=Hit Width
|
||||
ability.hitLength=Hit Length
|
||||
ability.effects=Effects
|
||||
ability.effect.burn=Burn
|
||||
ability.mode=Mode
|
||||
ability.duration=Duration
|
||||
ability.anchorPoint=Anchor Point
|
||||
ability.anchor.offsetX=Offset X
|
||||
ability.anchor.offsetY=Offset Y
|
||||
ability.anchor.offsetZ=Offset Z
|
||||
ability.vertical=Vertical
|
||||
ability.rotationSpeed=Rotation Speed
|
||||
|
||||
# Slam
|
||||
@@ -1786,7 +1845,6 @@ ability.healAllies=Heal Allies
|
||||
# Hazard
|
||||
ability.shape=Shape
|
||||
ability.placement=Placement
|
||||
ability.damagePerSecond=Damage/Sec
|
||||
ability.damageInterval=Damage Interval
|
||||
ability.slownessLevel=Slowness Level
|
||||
ability.debuffDuration=Debuff Duration
|
||||
@@ -1795,8 +1853,6 @@ ability.affectsCaster=Affects Caster
|
||||
|
||||
# Trap
|
||||
ability.armTime=Arm Time
|
||||
ability.maxTriggers=Max Triggers
|
||||
ability.triggerCooldown=Trigger Cooldown
|
||||
|
||||
# Sweeper
|
||||
ability.sweepSpeed=Sweep Speed
|
||||
@@ -1819,16 +1875,27 @@ ability.outerEnabled=Outer Glow
|
||||
ability.outerWidth=Outer Width
|
||||
ability.outerAlpha=Outer Alpha
|
||||
ability.lightning=Lightning
|
||||
ability.lightningDensity=Density
|
||||
ability.lightningRadius=Radius
|
||||
|
||||
# Dual Energy Projectile Settings
|
||||
ability.dualFire=Dual Fire
|
||||
# Projectile Count / Settings
|
||||
ability.projectileCount=Count
|
||||
ability.dualFireDelay=Fire Delay
|
||||
ability.fireDelay=Fire Delay
|
||||
ability.colorOverride=Color Override
|
||||
|
||||
# Burst System
|
||||
ability.burstEnabled=Burst
|
||||
ability.burstAmount=Amount
|
||||
ability.burstDelay=Delay
|
||||
ability.burstReplayAnimations=Replay Animations
|
||||
ability.burstOverlap=Overlap
|
||||
ability.hover.burstOverlap=When enabled, projectiles from previous bursts keep flying while new ones fire
|
||||
ability.hover.burstReplay=Replay windup and active animations for each burst iteration
|
||||
|
||||
# Ability Save/Load
|
||||
ability.save.confirm=Save ability preset
|
||||
ability.save.overwrite=Overwrite existing preset
|
||||
ability.duplicate.name=A preset with this name already exists
|
||||
ability.load.select=Select ability to load:
|
||||
ability.load.mode=How should this ability be loaded?
|
||||
ability.clone=Clone
|
||||
@@ -1841,6 +1908,12 @@ ability.edit.mode=How should this ability be edited?
|
||||
ability.cloneModify=Clone & Edit
|
||||
ability.modifyParent=Edit Parent
|
||||
|
||||
# Ability Variant Selection
|
||||
ability.selectVariant=Select Variant
|
||||
ability.variant.single=Single
|
||||
ability.variant.dual=Dual
|
||||
ability.variant.barrage=Barrage
|
||||
|
||||
# Ability Type Selection
|
||||
ability.selectType=Select Ability Type:
|
||||
|
||||
@@ -1862,8 +1935,9 @@ ability.noTypeSettings=No type-specific settings
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
ability.hover.weight=Higher weight = more likely to be chosen when multiple abilities available
|
||||
ability.hover.lockPosition=Prevents NPC from moving or rotating while ability is charging or active
|
||||
ability.hover.lockType=What to lock: Movement (pathfinding), Rotation (facing direction), or Both
|
||||
ability.hover.lockMovement=Prevents NPC pathfinding during the selected phases (Windup, Active, or Both)
|
||||
ability.hover.rotationMode=Free: normal AI rotation. Locked: freeze facing direction. Track: force face target for precise aiming.
|
||||
ability.hover.rotationPhase=When the rotation mode applies (Windup, Active, or Both)
|
||||
ability.hover.interruptible=If enabled, ability can be interrupted by taking damage
|
||||
ability.hover.dazed=Stun duration in ticks if ability is interrupted (20 ticks = 1 second)
|
||||
ability.hover.windup=Charge-up time before ability activates (20 ticks = 1 second)
|
||||
@@ -1884,6 +1958,7 @@ ability.hover.piercing=Hits multiple targets instead of stopping on first hit
|
||||
ability.hover.lightning=Add electrical effect to visuals
|
||||
ability.hover.outerGlow=Enable outer glow layer around projectile
|
||||
ability.hover.anchorPoint=Where on the NPC the ability spawns from
|
||||
ability.hover.vertical=Disc flies standing up with thin edge facing target (like a saw blade)
|
||||
ability.hover.dualCharge=Whether or not should create two anchor points
|
||||
ability.hover.damage=Base damage dealt to targets
|
||||
ability.hover.speed=Movement speed of projectile
|
||||
@@ -1903,7 +1978,6 @@ ability.hover.placement=Where the ability effect is placed (Caster, Target, etc.
|
||||
ability.hover.lockTarget=Sweeper beam tracks the target's position
|
||||
ability.hover.canCounter=Enable counterattack when blocking damage
|
||||
ability.hover.counterType=Flat = fixed damage, Percent = % of blocked damage
|
||||
ability.hover.shape=Shape of the hazard zone (Circle, Ring, Cone)
|
||||
ability.hover.affectsCaster=Hazard can damage the NPC that created it
|
||||
ability.hover.sweepMode=Swipe = single arc attack, Spin = continuous rotation
|
||||
|
||||
|
||||
Reference in new issue
Block a user