mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 20:44:08 +00:00
Merge branch 'dev' into syntax-highlighter-unified
This commit is contained in:
commit
2a5dd6dfd6
53 files changed
+2769
-773
No files matched your search
+1
-1
@@ -6,7 +6,7 @@ plugins {
|
||||
id "dts.typescript-generator"
|
||||
}
|
||||
|
||||
version = "1.11-beta2"
|
||||
version = "1.11-beta3"
|
||||
group= "kamkeel.customnpc-plus"
|
||||
archivesBaseName = "CustomNPC-Plus"
|
||||
|
||||
|
||||
+1
-1
Submodule src/api updated: 2b95d3ca47...efa8721d36.
@@ -22,10 +22,6 @@ 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 {
|
||||
|
||||
@@ -33,43 +29,35 @@ public class RenderAbilityZone extends Render {
|
||||
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 final int TRIGGER_REVEAL_TICKS = 10;
|
||||
|
||||
private static class Ctx {
|
||||
EntityAbilityZone zone;
|
||||
ZoneShape shape;
|
||||
float radius, height;
|
||||
float ir, ig, ib; // inner color
|
||||
float ir, ig, ib;
|
||||
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;
|
||||
boolean trapVisible;
|
||||
boolean triggerReveal;
|
||||
float revealFade;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 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();
|
||||
@@ -92,7 +80,6 @@ public class RenderAbilityZone extends Render {
|
||||
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();
|
||||
@@ -113,7 +100,27 @@ public class RenderAbilityZone extends Render {
|
||||
c.rotation = c.time * 1.5f * c.animSpeed;
|
||||
|
||||
c.brightness = 1.0f;
|
||||
c.trapVisible = true;
|
||||
c.triggerReveal = false;
|
||||
c.revealFade = 0.0f;
|
||||
|
||||
if (zone.getZoneType() == ZoneType.TRAP) {
|
||||
c.trapVisible = zone.isVisible();
|
||||
|
||||
// Trigger reveal: 10-tick full visual burst that fades out
|
||||
if (zone.getTriggerFlashTick() >= 0) {
|
||||
float ticksSinceTrigger = zone.ticksExisted - zone.getTriggerFlashTick() + partialTicks;
|
||||
if (ticksSinceTrigger < TRIGGER_REVEAL_TICKS) {
|
||||
c.triggerReveal = true;
|
||||
c.revealFade = Math.max(0.0f, 1.0f - ticksSinceTrigger / TRIGGER_REVEAL_TICKS);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip render entirely if invisible and not in reveal
|
||||
if (!c.trapVisible && !c.triggerReveal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!zone.isArmed()) {
|
||||
c.brightness = 0.4f;
|
||||
} else {
|
||||
@@ -121,16 +128,14 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
c.flash = zone.getTriggerFlashTick() >= 0
|
||||
&& zone.ticksExisted - zone.getTriggerFlashTick() < 4;
|
||||
// White flash during first 2 ticks of reveal
|
||||
c.flash = c.triggerReveal && c.revealFade > 0.8f;
|
||||
|
||||
c.newTick = isNewTick(zone);
|
||||
|
||||
// Render
|
||||
setupRenderState();
|
||||
GL11.glPushMatrix();
|
||||
GL11.glTranslated(x, y, z);
|
||||
|
||||
GL11.glDepthMask(false);
|
||||
|
||||
renderZone(c);
|
||||
@@ -161,14 +166,21 @@ public class RenderAbilityZone extends Render {
|
||||
return state;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// UNIFIED RENDER — Composable visual layers
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void renderZone(Ctx c) {
|
||||
// During trigger reveal, override visual layers for dramatic burst
|
||||
if (c.triggerReveal) {
|
||||
c.brightness = c.revealFade;
|
||||
c.groundFill = true;
|
||||
c.groundAlpha = 0.35f * c.revealFade;
|
||||
c.rings = true;
|
||||
c.ringCount = 3;
|
||||
c.accents = true;
|
||||
c.accentStyle = 2; // FLICKERING
|
||||
c.border = true;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -178,7 +190,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Pulse rings
|
||||
if (c.rings) {
|
||||
int count = Math.max(1, Math.min(5, c.ringCount));
|
||||
for (int i = 0; i < count; i++) {
|
||||
@@ -188,18 +199,15 @@ public class RenderAbilityZone extends Render {
|
||||
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)];
|
||||
@@ -216,7 +224,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Lightning
|
||||
if (c.lightning) {
|
||||
int innerColor = c.zone.getInnerColor();
|
||||
int outerColor = c.outerEnabled ? c.zone.getOuterColor() : innerColor;
|
||||
@@ -246,10 +253,9 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
state.render();
|
||||
GL11.glDepthMask(false); // Reset after lightning
|
||||
GL11.glDepthMask(false);
|
||||
}
|
||||
|
||||
// 6. Particles
|
||||
if (c.particles && c.newTick && c.particleDensity > 0) {
|
||||
if (c.particleDir != null && !c.particleDir.isEmpty()) {
|
||||
if (c.particleDir.startsWith("mc:")) {
|
||||
@@ -263,7 +269,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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);
|
||||
@@ -276,7 +281,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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++) {
|
||||
@@ -308,7 +312,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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)];
|
||||
@@ -346,7 +349,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
@@ -371,18 +373,12 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 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);
|
||||
@@ -436,7 +432,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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);
|
||||
@@ -492,7 +487,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
@@ -507,7 +501,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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);
|
||||
@@ -525,7 +518,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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,
|
||||
@@ -537,10 +529,6 @@ public class RenderAbilityZone extends Render {
|
||||
Minecraft.getMinecraft().effectRenderer.addEffect(particle);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GL STATE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void setupRenderState() {
|
||||
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
|
||||
GL11.glDisable(GL11.GL_TEXTURE_2D);
|
||||
@@ -561,10 +549,6 @@ public class RenderAbilityZone extends Render {
|
||||
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);
|
||||
@@ -602,10 +586,6 @@ public class RenderAbilityZone extends Render {
|
||||
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) {
|
||||
@@ -640,18 +620,13 @@ public class RenderAbilityZone extends Render {
|
||||
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 perimeter = radius * 8;
|
||||
float dashLen = perimeter / dashCount;
|
||||
float offset = (rotationDeg % 360.0f) / 360.0f * perimeter;
|
||||
|
||||
@@ -691,7 +666,6 @@ public class RenderAbilityZone extends Render {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
@@ -713,10 +687,6 @@ public class RenderAbilityZone extends Render {
|
||||
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) {
|
||||
@@ -725,7 +695,6 @@ public class RenderAbilityZone extends Render {
|
||||
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++) {
|
||||
@@ -745,7 +714,6 @@ public class RenderAbilityZone extends Render {
|
||||
tess.draw();
|
||||
}
|
||||
}
|
||||
// Corner accent lines
|
||||
float[][] corners = {{-radius, -radius}, {radius, -radius}, {radius, radius}, {-radius, radius}};
|
||||
for (float[] corner : corners) {
|
||||
tess.startDrawing(GL11.GL_LINES);
|
||||
|
||||
@@ -306,8 +306,8 @@ public class AbilityCommand extends CommandKamkeelBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of player-usable ability keys (for tab completion).
|
||||
* Uses registry keys for built-in abilities (no spaces) and UUIDs for custom.
|
||||
* Get list of player-usable ability names (for tab completion).
|
||||
* Uses registry keys for built-in abilities and display names for custom abilities.
|
||||
* Used by CommandKamkeel for <ability> usage token.
|
||||
*/
|
||||
public static List<String> getPlayerAbilityNames() {
|
||||
|
||||
@@ -329,13 +329,10 @@ public abstract class Ability implements IAbility {
|
||||
|
||||
// Apply damage
|
||||
if (damage > 0) {
|
||||
// Check for external damage handler first (e.g., DBC Addon)
|
||||
IAbilityDamageHandler handler = AbilityController.Instance.getDamageHandler();
|
||||
boolean handled = false;
|
||||
if (handler != null) {
|
||||
handled = handler.handleDamage(this, caster, hitEntity, damage, knockback, knockbackUp,
|
||||
knockbackDirX, knockbackDirZ);
|
||||
}
|
||||
// Check for ability extenders (e.g., DBC Addon damage routing)
|
||||
boolean handled = AbilityController.Instance.fireOnAbilityDamage(
|
||||
this, caster, hitEntity, damage, knockback, knockbackUp,
|
||||
knockbackDirX, knockbackDirZ);
|
||||
if (!handled) {
|
||||
// Default damage path
|
||||
if (caster instanceof EntityNPCInterface) {
|
||||
@@ -913,9 +910,9 @@ public abstract class Ability implements IAbility {
|
||||
if (phase == AbilityPhase.ACTIVE) {
|
||||
// Check if more burst iterations remain
|
||||
if (burstEnabled && burstAmount > 0 && burstIndex < burstAmount) {
|
||||
if (!burstOverlap) {
|
||||
cleanup();
|
||||
}
|
||||
// Don't cleanup here - entities may still be in flight
|
||||
// For non-overlap: cleaned up when next burst starts (in onExecute)
|
||||
// For overlap: entities continue flying indefinitely
|
||||
burstIndex++;
|
||||
phase = AbilityPhase.BURST_DELAY;
|
||||
currentTick = 0;
|
||||
|
||||
@@ -2,14 +2,18 @@ package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.type.*;
|
||||
import kamkeel.npcs.controllers.SyncController;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.LogWriter;
|
||||
import noppes.npcs.api.handler.IAbilityHandler;
|
||||
import noppes.npcs.util.NBTJsonUtil;
|
||||
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class AbilityController implements IAbilityHandler {
|
||||
@@ -24,7 +28,8 @@ public class AbilityController implements IAbilityHandler {
|
||||
// ── Extension Points ─────────────────────────────────────────────────────
|
||||
private final Map<String, List<AbilityVariant>> externalVariants = new LinkedHashMap<>();
|
||||
private final List<IAbilityFieldProvider> fieldProviders = new ArrayList<>();
|
||||
private IAbilityDamageHandler damageHandler = null;
|
||||
private final List<IAbilityExtender> extenders = new ArrayList<>();
|
||||
private final List<Predicate<EntityPlayer>> flightCheckers = new ArrayList<>();
|
||||
|
||||
// ── Derived State ────────────────────────────────────────────────────────
|
||||
private final Set<String> builtInTypeIds = new HashSet<>();
|
||||
@@ -303,7 +308,13 @@ public class AbilityController implements IAbilityHandler {
|
||||
keys.add(id);
|
||||
}
|
||||
}
|
||||
keys.addAll(customAbilities.keySet());
|
||||
// Use ability names for custom abilities (instead of UUIDs) for readable tab completion
|
||||
for (Ability ability : customAbilities.values()) {
|
||||
String name = ability.getName();
|
||||
if (name != null && !name.isEmpty()) {
|
||||
keys.add(name);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
@@ -317,9 +328,13 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (Map.Entry<String, Ability> entry : customAbilities.entrySet()) {
|
||||
if (entry.getValue().getAllowedBy().allowsPlayer()) {
|
||||
keys.add(entry.getKey());
|
||||
// Use ability names for custom abilities (instead of UUIDs) for readable tab completion
|
||||
for (Ability ability : customAbilities.values()) {
|
||||
if (ability.getAllowedBy().allowsPlayer()) {
|
||||
String name = ability.getName();
|
||||
if (name != null && !name.isEmpty()) {
|
||||
keys.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
@@ -329,6 +344,37 @@ public class AbilityController implements IAbilityHandler {
|
||||
return builtAbilities.containsKey(key) || customAbilities.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key can be resolved to a valid ability without creating a deep copy.
|
||||
* Uses the same lookup chain as {@link #resolveAbility(String)}.
|
||||
*/
|
||||
public boolean canResolveAbility(String key) {
|
||||
if (key == null || key.isEmpty()) return false;
|
||||
|
||||
// Built-in: exact name
|
||||
if (builtAbilities.containsKey(key)) return true;
|
||||
|
||||
// Built-in: case-insensitive name
|
||||
for (String name : builtAbilities.keySet()) {
|
||||
if (name.equalsIgnoreCase(key)) return true;
|
||||
}
|
||||
|
||||
// Built-in: registry key / ID
|
||||
for (Ability ability : builtAbilities.values()) {
|
||||
if (ability.getId() != null && ability.getId().equalsIgnoreCase(key)) return true;
|
||||
}
|
||||
|
||||
// Custom: exact UUID
|
||||
if (customAbilities.containsKey(key)) return true;
|
||||
|
||||
// Custom: case-insensitive name
|
||||
for (Ability ability : customAbilities.values()) {
|
||||
if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// EXTENSION POINTS
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
@@ -345,19 +391,16 @@ public class AbilityController implements IAbilityHandler {
|
||||
}
|
||||
List<AbilityVariant> ext = externalVariants.get(typeId);
|
||||
if (ext != null) {
|
||||
// If no built-in variants exist but external ones do,
|
||||
// inject a "Base" variant so the user always gets a choice
|
||||
if (result.isEmpty()) {
|
||||
result.add(new AbilityVariant("ability.variant.base", a -> {}));
|
||||
}
|
||||
result.addAll(ext);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void registerDamageHandler(IAbilityDamageHandler handler) {
|
||||
this.damageHandler = handler;
|
||||
}
|
||||
|
||||
public IAbilityDamageHandler getDamageHandler() {
|
||||
return damageHandler;
|
||||
}
|
||||
|
||||
public void registerFieldProvider(IAbilityFieldProvider provider) {
|
||||
fieldProviders.add(provider);
|
||||
}
|
||||
@@ -366,6 +409,76 @@ public class AbilityController implements IAbilityHandler {
|
||||
return fieldProviders;
|
||||
}
|
||||
|
||||
public void registerExtender(IAbilityExtender extender) {
|
||||
extenders.add(extender);
|
||||
}
|
||||
|
||||
public List<IAbilityExtender> getExtenders() {
|
||||
return extenders;
|
||||
}
|
||||
|
||||
public void registerFlightChecker(Predicate<EntityPlayer> checker) {
|
||||
flightCheckers.add(checker);
|
||||
}
|
||||
|
||||
public boolean isPlayerFlying(EntityPlayer player) {
|
||||
if (player.capabilities.isFlying) return true;
|
||||
for (Predicate<EntityPlayer> checker : flightCheckers) {
|
||||
if (checker.test(player)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire onAbilityStart on all extenders. Returns false if ANY extender cancels.
|
||||
*/
|
||||
public boolean fireOnAbilityStart(Ability ability, EntityLivingBase caster, EntityLivingBase target) {
|
||||
for (IAbilityExtender ext : extenders) {
|
||||
if (!ext.onAbilityStart(ability, caster, target)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire onAbilityTick on all extenders. Returns false if ANY extender says to interrupt.
|
||||
*/
|
||||
public boolean fireOnAbilityTick(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
||||
AbilityPhase phase, int tick) {
|
||||
for (IAbilityExtender ext : extenders) {
|
||||
if (!ext.onAbilityTick(ability, caster, target, phase, tick)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire onAbilityComplete on all extenders.
|
||||
*/
|
||||
public void fireOnAbilityComplete(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
||||
boolean interrupted) {
|
||||
for (IAbilityExtender ext : extenders) {
|
||||
ext.onAbilityComplete(ability, caster, target, interrupted);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire onAbilityDamage on all extenders. Chain of responsibility — first true wins.
|
||||
*/
|
||||
public boolean fireOnAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
||||
float damage, float knockback, float knockbackUp,
|
||||
double knockbackDirX, double knockbackDirZ) {
|
||||
for (IAbilityExtender ext : extenders) {
|
||||
if (ext.onAbilityDamage(ability, caster, target, damage, knockback, knockbackUp,
|
||||
knockbackDirX, knockbackDirZ)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// IAbilityHandler
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -14,6 +14,9 @@ public class AbilitySlot {
|
||||
private Ability inlineAbility;
|
||||
private String referenceId;
|
||||
|
||||
/** Per-slot enabled override for reference slots (null = use master's default). */
|
||||
private Boolean enabledOverride;
|
||||
|
||||
private transient Ability cachedAbility;
|
||||
private transient int cachedVersion = -1;
|
||||
|
||||
@@ -67,10 +70,32 @@ public class AbilitySlot {
|
||||
}
|
||||
|
||||
cachedAbility = controller.resolveAbility(referenceId);
|
||||
// Apply per-slot enabled override so NPC-specific enable/disable persists
|
||||
if (cachedAbility != null && enabledOverride != null) {
|
||||
cachedAbility.setEnabled(enabledOverride);
|
||||
}
|
||||
cachedVersion = currentVersion;
|
||||
return cachedAbility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the per-slot enabled override for reference slots.
|
||||
* For inline slots, delegates directly to the ability.
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
if (!isReference()) {
|
||||
if (inlineAbility != null) {
|
||||
inlineAbility.setEnabled(enabled);
|
||||
}
|
||||
} else {
|
||||
this.enabledOverride = enabled;
|
||||
// Also update the cached ability immediately if present
|
||||
if (cachedAbility != null) {
|
||||
cachedAbility.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inline ability directly (null for reference slots).
|
||||
*/
|
||||
@@ -104,6 +129,9 @@ public class AbilitySlot {
|
||||
if (isReference()) {
|
||||
NBTTagCompound nbt = new NBTTagCompound();
|
||||
nbt.setString("Reference", referenceId);
|
||||
if (enabledOverride != null) {
|
||||
nbt.setBoolean("RefEnabled", enabledOverride);
|
||||
}
|
||||
return nbt;
|
||||
}
|
||||
return inlineAbility != null ? inlineAbility.writeNBT() : new NBTTagCompound();
|
||||
@@ -113,7 +141,11 @@ public class AbilitySlot {
|
||||
if (nbt == null) return null;
|
||||
|
||||
if (nbt.hasKey("Reference")) {
|
||||
return reference(nbt.getString("Reference"));
|
||||
AbilitySlot slot = reference(nbt.getString("Reference"));
|
||||
if (nbt.hasKey("RefEnabled")) {
|
||||
slot.enabledOverride = nbt.getBoolean("RefEnabled");
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
// Inline ability
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
|
||||
/**
|
||||
* External damage handler for abilities.
|
||||
* When registered, called AFTER script events but BEFORE default attackEntityFrom.
|
||||
* If handleDamage returns true, default damage is skipped.
|
||||
*/
|
||||
public interface IAbilityDamageHandler {
|
||||
/**
|
||||
* Handle damage from an ability.
|
||||
*
|
||||
* @param ability The ability dealing damage
|
||||
* @param caster The entity executing the ability
|
||||
* @param target The entity being hit
|
||||
* @param damage The damage amount (after script event modifications)
|
||||
* @param knockback The horizontal knockback
|
||||
* @param knockbackUp The vertical knockback
|
||||
* @param knockbackDirX The X component of knockback direction
|
||||
* @param knockbackDirZ The Z component of knockback direction
|
||||
* @return true if damage was handled (skip default), false to use default damage
|
||||
*/
|
||||
boolean handleDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
||||
float damage, float knockback, float knockbackUp,
|
||||
double knockbackDirX, double knockbackDirZ);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package kamkeel.npcs.controllers.data.ability;
|
||||
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
|
||||
/**
|
||||
* General-purpose extension point for abilities. Multiple extenders can be registered.
|
||||
* Covers lifecycle hooks (start/tick/complete) and damage handling.
|
||||
* <p>
|
||||
* All methods have no-op defaults so extenders only override what they need.
|
||||
*/
|
||||
public interface IAbilityExtender {
|
||||
|
||||
/**
|
||||
* Called before an ability starts executing.
|
||||
* Return false to cancel the ability (e.g., insufficient resources).
|
||||
*
|
||||
* @param ability The ability about to start
|
||||
* @param caster The entity executing the ability (NPC or Player)
|
||||
* @param target The target entity (may be null for player self-cast)
|
||||
* @return true to allow, false to cancel
|
||||
*/
|
||||
default boolean onAbilityStart(Ability ability, EntityLivingBase caster, EntityLivingBase target) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called each tick during ability execution.
|
||||
* Return false to interrupt the ability (e.g., resource depleted).
|
||||
*
|
||||
* @param ability The executing ability
|
||||
* @param caster The entity executing the ability
|
||||
* @param target The target entity (may be null)
|
||||
* @param phase The current ability phase (WINDUP, ACTIVE, DAZED, BURST_DELAY)
|
||||
* @param tick The current tick within the phase
|
||||
* @return true to continue, false to interrupt
|
||||
*/
|
||||
default boolean onAbilityTick(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
||||
AbilityPhase phase, int tick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an ability completes (normally or via interruption).
|
||||
*
|
||||
* @param ability The completed ability
|
||||
* @param caster The entity that was executing the ability
|
||||
* @param target The target entity (may be null)
|
||||
* @param interrupted true if the ability was interrupted, false if it completed normally
|
||||
*/
|
||||
default void onAbilityComplete(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
||||
boolean interrupted) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an ability deals damage, AFTER script events but BEFORE default attackEntityFrom.
|
||||
* Return true to handle the damage (skips default damage application).
|
||||
* <p>
|
||||
* Chain of responsibility: extenders are called in registration order.
|
||||
* The first extender returning true claims the damage; remaining extenders are skipped.
|
||||
*
|
||||
* @param ability The ability dealing damage
|
||||
* @param caster The entity executing the ability
|
||||
* @param target The entity being hit
|
||||
* @param damage The damage amount (after script event modifications)
|
||||
* @param knockback The horizontal knockback
|
||||
* @param knockbackUp The vertical knockback
|
||||
* @param knockbackDirX The X component of knockback direction
|
||||
* @param knockbackDirZ The Z component of knockback direction
|
||||
* @return true if damage was handled (skip default), false to pass to next extender or default
|
||||
*/
|
||||
default boolean onAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target,
|
||||
float damage, float knockback, float knockbackUp,
|
||||
double knockbackDirX, double knockbackDirZ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
|
||||
@@ -12,8 +11,6 @@ import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.Vec3;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
@@ -25,9 +22,10 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Charge ability: Rush attack where NPC charges in a line, damaging all targets hit.
|
||||
* Charge ability: Rush attack where the caster charges in a line, damaging all targets hit.
|
||||
* Extends AbilityMovement for shared direction locking, stall detection, and velocity application.
|
||||
*/
|
||||
public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
public class AbilityCharge extends AbilityMovement implements IAbilityCharge {
|
||||
|
||||
// Type-specific parameters
|
||||
private float chargeSpeed = 0.8f;
|
||||
@@ -35,13 +33,8 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
private float knockback = 3.0f;
|
||||
private float hitWidth = 1.5f;
|
||||
|
||||
// Runtime state (transient)
|
||||
private transient double startX, startY, startZ;
|
||||
private transient double prevTickX, prevTickZ;
|
||||
private transient Vec3 chargeDirection;
|
||||
// Type-specific runtime state
|
||||
private transient Set<Integer> hitEntities = new HashSet<>();
|
||||
private transient float lockedYaw;
|
||||
private transient int maxActiveTicks;
|
||||
|
||||
public AbilityCharge() {
|
||||
this.typeId = "ability.cnpc.charge";
|
||||
@@ -71,138 +64,68 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAbilityMovement() {
|
||||
return true; // This ability moves the NPC
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on the first tick of windup - lock direction here so it matches telegraph.
|
||||
*/
|
||||
@Override
|
||||
public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
if (tick == 1) {
|
||||
// Lock direction on first windup tick (same time telegraph is created)
|
||||
lockChargeDirection(caster, target);
|
||||
lockDirection(caster, target);
|
||||
}
|
||||
// Keep caster facing the locked direction during windup
|
||||
enforceLockedRotation(caster);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (!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);
|
||||
if (len > 0) {
|
||||
chargeDirection = Vec3.createVectorHelper(dx / len, 0, dz / len);
|
||||
} else {
|
||||
float yaw = (float) Math.toRadians(caster.rotationYaw);
|
||||
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));
|
||||
}
|
||||
lockedYaw = (float) Math.toDegrees(Math.atan2(-chargeDirection.xCoord, chargeDirection.zCoord));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
// Initialize charge - direction was already locked during windup
|
||||
startX = caster.posX;
|
||||
startY = caster.posY;
|
||||
startZ = caster.posZ;
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
initMovement(caster, maxRange, chargeSpeed);
|
||||
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);
|
||||
if (movementDirection == null) {
|
||||
lockDirection(caster, target);
|
||||
}
|
||||
|
||||
enforceLockedRotation(caster);
|
||||
}
|
||||
|
||||
private void enforceLockedRotation(EntityLivingBase caster) {
|
||||
caster.rotationYaw = lockedYaw;
|
||||
caster.rotationYawHead = lockedYaw;
|
||||
caster.prevRotationYaw = lockedYaw;
|
||||
caster.prevRotationYawHead = lockedYaw;
|
||||
caster.renderYawOffset = lockedYaw;
|
||||
caster.prevRenderYawOffset = lockedYaw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
// Safety timeout or missing state: force-complete to prevent stuck NPC
|
||||
if (!isPreview() && (chargeDirection == null || tick > maxActiveTicks)) {
|
||||
if (checkTimeout(tick)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (chargeDirection == null) {
|
||||
if (movementDirection == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Enforce rotation every tick
|
||||
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;
|
||||
}
|
||||
if (checkStall(caster, tick)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
updatePrevPosition(caster);
|
||||
|
||||
// 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 >= maxRange) {
|
||||
if (getDistanceTraveled(caster) >= maxRange) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Block detection (skip in preview - no real world collision)
|
||||
if (!isPreview() && isChargeBlocked(caster)) {
|
||||
if (checkBlocked(caster, chargeSpeed)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move caster
|
||||
caster.motionX = chargeDirection.xCoord * chargeSpeed;
|
||||
caster.motionY = 0;
|
||||
caster.motionZ = chargeDirection.zCoord * chargeSpeed;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
// Move caster (flat: zero vertical motion)
|
||||
applyVelocityFlat(caster, chargeSpeed);
|
||||
|
||||
// Server-side collision damage (skip in preview)
|
||||
if (!caster.worldObj.isRemote && !isPreview()) {
|
||||
@@ -218,14 +141,11 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
|
||||
EntityLivingBase livingEntity = (EntityLivingBase) entity;
|
||||
|
||||
// Hit this entity
|
||||
hitEntities.add(entity.getEntityId());
|
||||
|
||||
// Apply damage with scripted event support
|
||||
boolean wasHit = applyAbilityDamageWithDirection(caster, livingEntity, damage, knockback,
|
||||
chargeDirection.xCoord, chargeDirection.zCoord);
|
||||
movementDirection.xCoord, movementDirection.zCoord);
|
||||
|
||||
// Apply effects and play impact sound if hit wasn't cancelled
|
||||
if (wasHit) {
|
||||
applyEffects(livingEntity);
|
||||
caster.worldObj.playSoundAtEntity(livingEntity, "random.explode", 0.5f, 1.2f);
|
||||
@@ -246,24 +166,10 @@ public class AbilityCharge extends Ability implements IAbilityCharge {
|
||||
super.onInterrupt(caster, source, damage);
|
||||
}
|
||||
|
||||
private void stopMomentum(EntityLivingBase caster) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isChargeBlocked(EntityLivingBase caster) {
|
||||
return isMovementBlocked(caster, chargeDirection.xCoord, chargeDirection.zCoord, chargeSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
super.reset();
|
||||
chargeDirection = null;
|
||||
public void cleanup() {
|
||||
super.cleanup();
|
||||
hitEntities.clear();
|
||||
maxActiveTicks = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,16 +2,12 @@ package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.LockMovementType;
|
||||
import kamkeel.npcs.controllers.data.ability.TargetingMode;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.util.Vec3;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import noppes.npcs.api.ability.type.IAbilityDash;
|
||||
@@ -22,9 +18,9 @@ import java.util.Random;
|
||||
|
||||
/**
|
||||
* Dash ability: Quick evasive sidestep with NO telegraph.
|
||||
* Defensive repositioning move to evade attacks.
|
||||
* Extends AbilityMovement for shared direction locking, stall detection, and velocity application.
|
||||
*/
|
||||
public class AbilityDash extends Ability implements IAbilityDash {
|
||||
public class AbilityDash extends AbilityMovement implements IAbilityDash {
|
||||
|
||||
/**
|
||||
* Dash behavior mode.
|
||||
@@ -88,12 +84,8 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
private float dashDistance = 4.0f;
|
||||
private float dashSpeed = 0.5f;
|
||||
|
||||
// Runtime state
|
||||
private transient Vec3 dashDirection;
|
||||
private transient double startX, startY, startZ;
|
||||
private transient double prevTickX, prevTickZ;
|
||||
// Type-specific runtime state
|
||||
private transient DashDirection chosenDirection;
|
||||
private transient int maxActiveTicks;
|
||||
|
||||
public AbilityDash() {
|
||||
this.typeId = "ability.cnpc.dash";
|
||||
@@ -126,21 +118,9 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasAbilityMovement() {
|
||||
return true; // This ability moves the NPC
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
startX = caster.posX;
|
||||
startY = caster.posY;
|
||||
startZ = caster.posZ;
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
|
||||
// Safety timeout: expected ticks + generous buffer to prevent infinite dash
|
||||
maxActiveTicks = dashSpeed > 0 ? (int)(dashDistance / dashSpeed) + 10 : 10;
|
||||
initMovement(caster, dashDistance, dashSpeed);
|
||||
|
||||
// Choose random direction based on mode
|
||||
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
|
||||
@@ -148,26 +128,10 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
: DEFENSIVE_DIRECTIONS;
|
||||
chosenDirection = directions[RANDOM.nextInt(directions.length)];
|
||||
|
||||
// NPC: dash direction is relative to aggro target facing
|
||||
// Player: dash direction is relative to look direction
|
||||
float baseYaw;
|
||||
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));
|
||||
} else {
|
||||
baseYaw = caster.rotationYaw;
|
||||
}
|
||||
|
||||
// Apply direction offset
|
||||
// Calculate direction: base yaw (toward target or look dir) + direction offset
|
||||
float baseYaw = getBaseYaw(caster, target);
|
||||
float dashYaw = baseYaw + chosenDirection.getAngleOffset();
|
||||
float yawRad = (float) Math.toRadians(dashYaw);
|
||||
|
||||
dashDirection = Vec3.createVectorHelper(
|
||||
-Math.sin(yawRad),
|
||||
0,
|
||||
Math.cos(yawRad)
|
||||
);
|
||||
setDirectionFromYaw(dashYaw);
|
||||
|
||||
// Small upward impulse for a skip/hop arc (gravity handles the descent)
|
||||
caster.motionY = 0.2;
|
||||
@@ -175,86 +139,59 @@ public class AbilityDash extends Ability implements IAbilityDash {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
// Safety timeout or missing state: force-complete to prevent stuck NPC
|
||||
if (!isPreview() && (dashDirection == null || tick > maxActiveTicks)) {
|
||||
stopDash(caster);
|
||||
if (checkTimeout(tick)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
if (dashDirection == null) {
|
||||
if (movementDirection == null) {
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
stopDash(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
if (checkStall(caster, tick)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
updatePrevPosition(caster);
|
||||
|
||||
// Check if reached max distance
|
||||
double travelDx = caster.posX - startX;
|
||||
double travelDz = caster.posZ - startZ;
|
||||
if (travelDx * travelDx + travelDz * travelDz >= (double) dashDistance * dashDistance) {
|
||||
stopDash(caster);
|
||||
if (getDistanceTraveledSq(caster) >= (double) dashDistance * dashDistance) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Block detection (skip in preview - no real world collision)
|
||||
if (!isPreview() && isDashBlocked(caster)) {
|
||||
stopDash(caster);
|
||||
if (checkBlocked(caster, dashSpeed)) {
|
||||
stopMomentum(caster);
|
||||
signalCompletion();
|
||||
return;
|
||||
}
|
||||
|
||||
// Move caster (motionY left to gravity for skip arc)
|
||||
caster.motionX = dashDirection.xCoord * dashSpeed;
|
||||
caster.motionZ = dashDirection.zCoord * dashSpeed;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
applyVelocity(caster, dashSpeed);
|
||||
|
||||
if (!isPreview()) {
|
||||
// Trail particles
|
||||
caster.worldObj.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) {
|
||||
stopDash(caster);
|
||||
stopMomentum(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
|
||||
stopDash(caster);
|
||||
stopMomentum(caster);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
dashDirection = null;
|
||||
super.cleanup();
|
||||
chosenDirection = null;
|
||||
maxActiveTicks = 0;
|
||||
}
|
||||
|
||||
private boolean isDashBlocked(EntityLivingBase caster) {
|
||||
if (dashDirection == null) return true;
|
||||
return isMovementBlocked(caster, dashDirection.xCoord, dashDirection.zCoord, dashSpeed);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -19,14 +19,8 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 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 AbilityZone implements IAbilityHazard {
|
||||
|
||||
// Hazard-specific fields
|
||||
private float radius = 2.0f;
|
||||
private float damagePerSecond = 1.0f;
|
||||
private int damageInterval = 20;
|
||||
@@ -51,7 +45,6 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard {
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
activeEntities.clear();
|
||||
|
||||
// Use pre-calculated positions from telegraph phase, or generate new ones
|
||||
List<double[]> positions;
|
||||
if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) {
|
||||
positions = new ArrayList<>(preCalculatedPositions);
|
||||
@@ -93,10 +86,6 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// NBT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
writeZoneNBT(nbt);
|
||||
@@ -117,10 +106,6 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard {
|
||||
this.affectsCaster = nbt.getBoolean("affectsCaster");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// HAZARD-SPECIFIC GETTERS & SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public float getRadius() { return radius; }
|
||||
public void setRadius(float radius) { this.radius = radius; }
|
||||
|
||||
@@ -136,10 +121,6 @@ public class AbilityHazard extends AbilityZone implements IAbilityHazard {
|
||||
public boolean isAffectsCaster() { return affectsCaster; }
|
||||
public void setAffectsCaster(boolean affectsCaster) { this.affectsCaster = affectsCaster; }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GUI FIELD DEFINITIONS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
public void getAbilityDefinitions(List<FieldDef> defs) {
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.util.Vec3;
|
||||
|
||||
/**
|
||||
* Abstract base class for abilities that move the caster along the ground
|
||||
* in a specific direction (Charge, Dash, etc.).
|
||||
* <p>
|
||||
* Provides shared movement logic: direction locking, stall detection with
|
||||
* player-aware grace period, velocity application, distance tracking,
|
||||
* rotation enforcement, and wall collision checking.
|
||||
* <p>
|
||||
* NOT for ballistic/arc movement (Slam uses a different pattern).
|
||||
*/
|
||||
public abstract class AbilityMovement extends Ability {
|
||||
|
||||
/**
|
||||
* Grace period (in active ticks) before stall detection activates.
|
||||
* Players need more time because:
|
||||
* - Server sets motionX/Z and sends S12 velocity packet to client
|
||||
* - Client receives S12 and starts moving
|
||||
* - Client sends C03 position packet with updated position back to server
|
||||
* Until the round trip completes, the server sees the old (stationary) position,
|
||||
* which would falsely trigger stall detection.
|
||||
*/
|
||||
private static final int PLAYER_STALL_GRACE_TICKS = 5;
|
||||
private static final int NPC_STALL_GRACE_TICKS = 2;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// COMMON MOVEMENT STATE (transient, not saved to NBT)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
protected transient double startX, startY, startZ;
|
||||
protected transient double prevTickX, prevTickZ;
|
||||
protected transient Vec3 movementDirection;
|
||||
protected transient float lockedYaw;
|
||||
protected transient int maxActiveTicks;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// OVERRIDES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
public boolean hasAbilityMovement() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
movementDirection = null;
|
||||
maxActiveTicks = 0;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// DIRECTION HELPERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Lock direction based on caster type.
|
||||
* NPC: toward target. Player: look direction.
|
||||
*/
|
||||
protected void lockDirection(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!isPlayerCaster(caster) && target != null) {
|
||||
lockDirectionToTarget(caster, target);
|
||||
} else {
|
||||
lockDirectionFromLook(caster);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock direction to the caster's look direction (horizontal only).
|
||||
*/
|
||||
protected void lockDirectionFromLook(EntityLivingBase caster) {
|
||||
float yaw = (float) Math.toRadians(caster.rotationYaw);
|
||||
movementDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw));
|
||||
lockedYaw = computeYawFromDirection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock direction toward a target entity.
|
||||
* Falls back to look direction if target is at the same position.
|
||||
*/
|
||||
protected void lockDirectionToTarget(EntityLivingBase caster, EntityLivingBase target) {
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
if (len > 0) {
|
||||
movementDirection = Vec3.createVectorHelper(dx / len, 0, dz / len);
|
||||
lockedYaw = computeYawFromDirection();
|
||||
} else {
|
||||
lockDirectionFromLook(caster);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base yaw for direction calculation.
|
||||
* NPC: yaw toward target. Player: current look yaw.
|
||||
*/
|
||||
protected float getBaseYaw(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!isPlayerCaster(caster) && target != null) {
|
||||
double dx = target.posX - caster.posX;
|
||||
double dz = target.posZ - caster.posZ;
|
||||
return (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
}
|
||||
return caster.rotationYaw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set movement direction from a yaw angle (degrees).
|
||||
*/
|
||||
protected void setDirectionFromYaw(float yawDegrees) {
|
||||
float yawRad = (float) Math.toRadians(yawDegrees);
|
||||
movementDirection = Vec3.createVectorHelper(-Math.sin(yawRad), 0, Math.cos(yawRad));
|
||||
lockedYaw = yawDegrees;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// MOVEMENT INITIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Initialize movement tracking. Call from onExecute().
|
||||
*
|
||||
* @param caster The entity performing the movement
|
||||
* @param distance Maximum travel distance (for timeout computation)
|
||||
* @param speed Movement speed in blocks/tick (for timeout computation)
|
||||
*/
|
||||
protected void initMovement(EntityLivingBase caster, float distance, float speed) {
|
||||
startX = caster.posX;
|
||||
startY = caster.posY;
|
||||
startZ = caster.posZ;
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
maxActiveTicks = speed > 0 ? (int) (distance / speed) + 10 : 10;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ACTIVE TICK CHECKS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Check for timeout or missing movement direction.
|
||||
*
|
||||
* @return true if the ability should stop
|
||||
*/
|
||||
protected boolean checkTimeout(int tick) {
|
||||
return !isPreview() && (movementDirection == null || tick > maxActiveTicks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check stall detection with appropriate grace period for player vs NPC.
|
||||
* Players need a longer grace period because velocity sync via S12 packet
|
||||
* takes time to reach the client, and C03 position packets lag behind.
|
||||
*
|
||||
* @return true if the entity is stalled (not moving)
|
||||
*/
|
||||
protected boolean checkStall(EntityLivingBase caster, int tick) {
|
||||
if (isPreview()) return false;
|
||||
int graceTicks = isPlayerCaster(caster) ? PLAYER_STALL_GRACE_TICKS : NPC_STALL_GRACE_TICKS;
|
||||
if (tick <= graceTicks) return false;
|
||||
|
||||
double dx = caster.posX - prevTickX;
|
||||
double dz = caster.posZ - prevTickZ;
|
||||
return dx * dx + dz * dz < 0.0001;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update previous tick position for stall detection.
|
||||
* Call after stall check, before movement application.
|
||||
*/
|
||||
protected void updatePrevPosition(EntityLivingBase caster) {
|
||||
prevTickX = caster.posX;
|
||||
prevTickZ = caster.posZ;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total linear distance traveled from start position.
|
||||
*/
|
||||
protected double getDistanceTraveled(EntityLivingBase caster) {
|
||||
double dx = caster.posX - startX;
|
||||
double dz = caster.posZ - startZ;
|
||||
return Math.sqrt(dx * dx + dz * dz);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get total distance traveled squared (more efficient for comparison).
|
||||
*/
|
||||
protected double getDistanceTraveledSq(EntityLivingBase caster) {
|
||||
double dx = caster.posX - startX;
|
||||
double dz = caster.posZ - startZ;
|
||||
return dx * dx + dz * dz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if movement direction is blocked by walls.
|
||||
*
|
||||
* @return true if blocked
|
||||
*/
|
||||
protected boolean checkBlocked(EntityLivingBase caster, float speed) {
|
||||
if (isPreview() || movementDirection == null) return false;
|
||||
return isMovementBlocked(caster, movementDirection.xCoord, movementDirection.zCoord, speed);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// VELOCITY APPLICATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Apply movement velocity to the caster (horizontal only, Y unchanged).
|
||||
*/
|
||||
protected void applyVelocity(EntityLivingBase caster, float speed) {
|
||||
caster.motionX = movementDirection.xCoord * speed;
|
||||
caster.motionZ = movementDirection.zCoord * speed;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply movement velocity with zero vertical motion.
|
||||
* Used for ground-locked movement like Charge.
|
||||
*/
|
||||
protected void applyVelocityFlat(EntityLivingBase caster, float speed) {
|
||||
caster.motionX = movementDirection.xCoord * speed;
|
||||
caster.motionY = 0;
|
||||
caster.motionZ = movementDirection.zCoord * speed;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all horizontal momentum.
|
||||
*/
|
||||
protected void stopMomentum(EntityLivingBase caster) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// ROTATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Compute yaw angle (degrees) from current movement direction.
|
||||
*/
|
||||
protected float computeYawFromDirection() {
|
||||
if (movementDirection == null) return 0;
|
||||
return (float) Math.toDegrees(Math.atan2(-movementDirection.xCoord, movementDirection.zCoord));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enforce locked rotation on all yaw fields.
|
||||
* Sets rotationYaw, rotationYawHead, prevRotationYaw, prevRotationYawHead,
|
||||
* renderYawOffset, and prevRenderYawOffset to lockedYaw.
|
||||
*/
|
||||
protected void enforceLockedRotation(EntityLivingBase caster) {
|
||||
caster.rotationYaw = lockedYaw;
|
||||
caster.rotationYawHead = lockedYaw;
|
||||
caster.prevRotationYaw = lockedYaw;
|
||||
caster.prevRotationYawHead = lockedYaw;
|
||||
caster.renderYawOffset = lockedYaw;
|
||||
caster.prevRenderYawOffset = lockedYaw;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
package kamkeel.npcs.controllers.data.ability.type;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.data.EnergyDisplayData;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone;
|
||||
import kamkeel.npcs.entity.EntityAbilityZone.ZoneShape;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
|
||||
|
||||
import noppes.npcs.api.ability.type.IAbilityTrap;
|
||||
|
||||
import noppes.npcs.client.gui.SubGuiTrapPresetSelector;
|
||||
import noppes.npcs.client.gui.builder.FieldDef;
|
||||
import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs;
|
||||
import noppes.npcs.constants.EnumPotionType;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
@@ -19,14 +19,8 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 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 AbilityZone implements IAbilityTrap {
|
||||
|
||||
// Trap-specific fields
|
||||
private float triggerRadius = 2.0f;
|
||||
private int armTime = 20;
|
||||
private int maxTriggers = 1;
|
||||
@@ -54,7 +48,6 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
activeEntities.clear();
|
||||
|
||||
// Use pre-calculated positions from telegraph phase, or generate new ones
|
||||
List<double[]> positions;
|
||||
if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) {
|
||||
positions = new ArrayList<>(preCalculatedPositions);
|
||||
@@ -81,6 +74,7 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
|
||||
colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled,
|
||||
zoneHeight,
|
||||
particleDensity, particleScale, animSpeed, lightningDensity,
|
||||
visible,
|
||||
getEffects());
|
||||
|
||||
applyVisualToEntity(entity);
|
||||
@@ -94,9 +88,81 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// NBT
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@SideOnly(Side.CLIENT)
|
||||
public enum TrapPreset {
|
||||
HIDDEN, VENOM, EXPLOSIVE, CURSED, SHOCK, SNARE;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ability.preset." + name().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyTrapPresetDefaults(String preset) {
|
||||
effects.clear();
|
||||
particleDir = "";
|
||||
|
||||
groundFill = true;
|
||||
rings = false;
|
||||
ringCount = 1;
|
||||
border = false;
|
||||
borderSpeed = 1.0f;
|
||||
accents = false;
|
||||
accentStyle = 0;
|
||||
lightning = false;
|
||||
particleGlow = false;
|
||||
|
||||
switch (preset) {
|
||||
case "VENOM":
|
||||
groundAlpha = 0.04f;
|
||||
particles = true; particleDensity = 0.3f; particleScale = 0.5f;
|
||||
particleMotion = 1; particleDir = "mc:mobSpell";
|
||||
colorData.innerColor = 0x44DD44; colorData.outerColor = 0x116611;
|
||||
windUpColor = 0x6044DD44; activeColor = 0xC044FF44;
|
||||
effects.add(new AbilityEffect(EnumPotionType.Poison, 100, 0));
|
||||
break;
|
||||
case "EXPLOSIVE":
|
||||
groundAlpha = 0.04f;
|
||||
particles = true; particleDensity = 0.3f; particleScale = 0.6f;
|
||||
particleMotion = 0; particleDir = "mc:smoke";
|
||||
colorData.innerColor = 0xFF6611; colorData.outerColor = 0xCC2200;
|
||||
windUpColor = 0x60FF6611; activeColor = 0xC0FF4400;
|
||||
effects.add(new AbilityEffect(EnumPotionType.Fire, 60, 0));
|
||||
break;
|
||||
case "CURSED":
|
||||
groundAlpha = 0.04f;
|
||||
particles = true; particleDensity = 0.3f; particleScale = 0.5f;
|
||||
particleMotion = 1; particleDir = "mc:portal";
|
||||
colorData.innerColor = 0xAA44FF; colorData.outerColor = 0x6622BB;
|
||||
windUpColor = 0x60AA44FF; activeColor = 0xC0CC66FF;
|
||||
effects.add(new AbilityEffect(EnumPotionType.Weakness, 100, 0));
|
||||
break;
|
||||
case "SHOCK":
|
||||
groundAlpha = 0.04f;
|
||||
particles = true; particleDensity = 0.3f; particleScale = 0.4f;
|
||||
particleMotion = 2; particleDir = "mc:enchantmenttable";
|
||||
colorData.innerColor = 0x4488FF; colorData.outerColor = 0x2244BB;
|
||||
windUpColor = 0x604488FF; activeColor = 0xC066AAFF;
|
||||
effects.add(new AbilityEffect(EnumPotionType.MiningFatigue, 80, 1));
|
||||
break;
|
||||
case "SNARE":
|
||||
groundAlpha = 0.04f;
|
||||
particles = true; particleDensity = 0.3f; particleScale = 0.5f;
|
||||
particleMotion = 1; particleDir = "mc:snowshovel";
|
||||
colorData.innerColor = 0x88CCFF; colorData.outerColor = 0x4488CC;
|
||||
windUpColor = 0x6088CCFF; activeColor = 0xC0AADDFF;
|
||||
effects.add(new AbilityEffect(EnumPotionType.Slowness, 100, 1));
|
||||
break;
|
||||
case "HIDDEN":
|
||||
default:
|
||||
groundAlpha = 0.03f;
|
||||
particles = false; particleDensity = 0.0f; particleScale = 1.0f;
|
||||
particleMotion = 0;
|
||||
colorData.innerColor = 0x888888; colorData.outerColor = 0x444444;
|
||||
windUpColor = 0x60888888; activeColor = 0xC0AAAAAA;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTypeNBT(NBTTagCompound nbt) {
|
||||
@@ -124,10 +190,6 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
|
||||
this.visible = nbt.getBoolean("visible");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// TRAP-SPECIFIC GETTERS & SETTERS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
public float getTriggerRadius() { return triggerRadius; }
|
||||
public void setTriggerRadius(float triggerRadius) { this.triggerRadius = triggerRadius; }
|
||||
public int getArmTime() { return armTime; }
|
||||
@@ -145,9 +207,18 @@ public class AbilityTrap extends AbilityZone implements IAbilityTrap {
|
||||
public boolean isVisible() { return visible; }
|
||||
public void setVisible(boolean visible) { this.visible = visible; }
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// GUI FIELD DEFINITIONS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
protected void addPresetFieldDef(List<FieldDef> defs) {
|
||||
defs.add(FieldDef.subGuiField("gui.applyPreset",
|
||||
SubGuiTrapPresetSelector::new,
|
||||
gui -> {
|
||||
SubGuiTrapPresetSelector selector = (SubGuiTrapPresetSelector) gui;
|
||||
if (selector.selectedPreset != null) {
|
||||
applyTrapPresetDefaults(selector.selectedPreset);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
@Override
|
||||
|
||||
@@ -31,27 +31,20 @@ 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;
|
||||
@@ -67,7 +60,6 @@ public abstract class AbilityZone extends Ability {
|
||||
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<>();
|
||||
@@ -82,10 +74,6 @@ public abstract class AbilityZone extends Ability {
|
||||
this.allowedBy = UserType.NPC_ONLY;
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// SHARED OVERRIDES
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
public boolean allowBurst() {
|
||||
return false;
|
||||
@@ -101,17 +89,12 @@ public abstract class AbilityZone extends Ability {
|
||||
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;
|
||||
@@ -122,7 +105,6 @@ public abstract class AbilityZone extends Ability {
|
||||
preCalculatedPositions.add(pos);
|
||||
}
|
||||
|
||||
// Create a telegraph for each zone position
|
||||
List<TelegraphInstance> telegraphs = new ArrayList<>();
|
||||
float zoneRadius = getZoneRadius();
|
||||
|
||||
@@ -152,7 +134,6 @@ public abstract class AbilityZone extends Ability {
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
// Remove dead entities
|
||||
Iterator<EntityAbilityZone> it = activeEntities.iterator();
|
||||
while (it.hasNext()) {
|
||||
EntityAbilityZone entity = it.next();
|
||||
@@ -192,13 +173,6 @@ public abstract class AbilityZone extends Ability {
|
||||
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;
|
||||
@@ -227,9 +201,6 @@ public abstract class AbilityZone extends Ability {
|
||||
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,
|
||||
@@ -238,13 +209,6 @@ public abstract class AbilityZone extends Ability {
|
||||
particleSize, particleGlow);
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// PRESET SUPPORT
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Apply preset defaults by style name.
|
||||
*/
|
||||
private void applyPresetDefaults(String styleName) {
|
||||
effects.clear();
|
||||
particleDir = "";
|
||||
@@ -307,10 +271,6 @@ public abstract class AbilityZone extends Ability {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@@ -321,10 +281,6 @@ public abstract class AbilityZone extends Ability {
|
||||
}
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// SHARED NBT
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
protected void writeZoneNBT(NBTTagCompound nbt) {
|
||||
nbt.setInteger("durationTicks", durationTicks);
|
||||
nbt.setString("zoneShape", zoneShape.name());
|
||||
@@ -337,7 +293,6 @@ public abstract class AbilityZone extends Ability {
|
||||
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);
|
||||
@@ -367,7 +322,6 @@ public abstract class AbilityZone extends Ability {
|
||||
this.lightningDensity = nbt.getFloat("lightningDensity");
|
||||
colorData.readNBT(nbt);
|
||||
|
||||
// Visual layer fields
|
||||
this.groundFill = nbt.getBoolean("groundFill");
|
||||
this.groundAlpha = nbt.getFloat("groundAlpha");
|
||||
this.rings = nbt.getBoolean("rings");
|
||||
@@ -384,10 +338,6 @@ public abstract class AbilityZone extends Ability {
|
||||
this.particleGlow = nbt.getBoolean("particleGlow");
|
||||
}
|
||||
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
// SHARED GETTERS & SETTERS
|
||||
// ═════════════════════════════════════════════════════════════════
|
||||
|
||||
public int getDurationTicks() { return durationTicks; }
|
||||
public void setDurationTicks(int durationTicks) { this.durationTicks = Math.max(1, durationTicks); }
|
||||
|
||||
@@ -416,7 +366,6 @@ public abstract class AbilityZone extends Ability {
|
||||
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; }
|
||||
@@ -424,7 +373,6 @@ public abstract class AbilityZone extends Ability {
|
||||
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; }
|
||||
@@ -458,10 +406,6 @@ public abstract class AbilityZone extends Ability {
|
||||
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",
|
||||
@@ -477,42 +421,36 @@ public abstract class AbilityZone extends Ability {
|
||||
@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"),
|
||||
@@ -527,12 +465,10 @@ public abstract class AbilityZone extends Ability {
|
||||
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"),
|
||||
|
||||
+27
-5
@@ -258,9 +258,13 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
// Create fresh entities if needed:
|
||||
// - entities == null: windup was skipped (burst without replay animations)
|
||||
// - isBurstRefire(): burst overlap mode kept old (stale) entities alive,
|
||||
// - isBurstRefire(): previous burst entities may still be alive,
|
||||
// so we must create new ones for each burst iteration
|
||||
if (entities == null || isBurstRefire()) {
|
||||
// Non-overlap burst refire: kill old entities before creating new ones
|
||||
if (isBurstRefire() && !burstOverlap && entities != null) {
|
||||
cleanup();
|
||||
}
|
||||
entities = createAllEntities(caster, target);
|
||||
for (E entity : entities) {
|
||||
spawnAbilityEntity(entity);
|
||||
@@ -294,14 +298,32 @@ public abstract class AbstractEnergyProjectileAbility<E extends EntityAbilityPro
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all entities are dead
|
||||
// Check if all entities are dead or unreachable
|
||||
boolean allDead = true;
|
||||
for (E entity : entities) {
|
||||
if (entity != null && !entity.isDead) {
|
||||
allDead = false;
|
||||
break;
|
||||
if (entity == null || entity.isDead) continue;
|
||||
|
||||
// Check if entity was removed from world (chunk unload, etc.)
|
||||
if (tick > 5 && entity.worldObj != null
|
||||
&& entity.worldObj.getEntityByID(entity.getEntityId()) != entity) {
|
||||
entity.setDead();
|
||||
continue;
|
||||
}
|
||||
|
||||
allDead = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Failsafe timeout: force completion if active phase exceeded max possible flight time
|
||||
if (!allDead) {
|
||||
int lastFireTick = fireDelay > 0 ? fireDelay * (projectileCount - 1) : 0;
|
||||
int maxFlightTime = lifespanData.maxLifetime > 0 ? lifespanData.maxLifetime : 200;
|
||||
if (tick > lastFireTick + maxFlightTime + 20) {
|
||||
cleanup();
|
||||
allDead = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (allDead) {
|
||||
signalCompletion();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package kamkeel.npcs.entity;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import noppes.npcs.EventHooks;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
@@ -386,33 +387,26 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
|
||||
chargeTick++;
|
||||
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner == null || owner.isDead) {
|
||||
setDead();
|
||||
return;
|
||||
if (owner != null) {
|
||||
Vec3 pos;
|
||||
if (owner instanceof EntityLivingBase) {
|
||||
pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance);
|
||||
} else {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
double offsetX = -Math.sin(yaw) * chargeOffsetDistance;
|
||||
double offsetZ = Math.cos(yaw) * chargeOffsetDistance;
|
||||
pos = Vec3.createVectorHelper(
|
||||
owner.posX + offsetX,
|
||||
owner.posY + owner.getEyeHeight() * 0.7,
|
||||
owner.posZ + offsetZ
|
||||
);
|
||||
}
|
||||
|
||||
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
|
||||
startX = pos.xCoord;
|
||||
startY = pos.yCoord;
|
||||
startZ = pos.zCoord;
|
||||
}
|
||||
|
||||
// Calculate position based on anchor point
|
||||
Vec3 pos;
|
||||
if (owner instanceof EntityLivingBase) {
|
||||
pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance);
|
||||
} else {
|
||||
// Fallback for non-living entities (shouldn't happen normally)
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
double offsetX = -Math.sin(yaw) * chargeOffsetDistance;
|
||||
double offsetZ = Math.cos(yaw) * chargeOffsetDistance;
|
||||
pos = Vec3.createVectorHelper(
|
||||
owner.posX + offsetX,
|
||||
owner.posY + owner.getEyeHeight() * 0.7,
|
||||
owner.posZ + offsetZ
|
||||
);
|
||||
}
|
||||
|
||||
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
|
||||
|
||||
// Also update origin for when firing starts
|
||||
startX = pos.xCoord;
|
||||
startY = pos.yCoord;
|
||||
startZ = pos.zCoord;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -551,6 +545,9 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
|
||||
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
|
||||
|
||||
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
|
||||
if (!worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
|
||||
}
|
||||
hasHit = true;
|
||||
if (isExplosive()) {
|
||||
posX = blockHit.hitVec.xCoord;
|
||||
@@ -600,10 +597,22 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
|
||||
return beamWidth;
|
||||
}
|
||||
|
||||
public void setBeamWidth(float beamWidth) {
|
||||
this.beamWidth = beamWidth;
|
||||
}
|
||||
|
||||
public float getHeadSize() {
|
||||
return headSize;
|
||||
}
|
||||
|
||||
public void setHeadSize(float headSize) {
|
||||
this.headSize = headSize;
|
||||
}
|
||||
|
||||
public void setAttachedToOwner(boolean attached) {
|
||||
this.attachedToOwner = attached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interpolated head offset for smooth rendering.
|
||||
* Returns offset RELATIVE to origin.
|
||||
@@ -696,6 +705,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
|
||||
|
||||
// Read trail points (relative to origin)
|
||||
trailPoints.clear();
|
||||
trailPointAges.clear();
|
||||
if (nbt.hasKey("Trail")) {
|
||||
NBTTagList trailList = nbt.getTagList("Trail", 10);
|
||||
for (int i = 0; i < trailList.tagCount(); i++) {
|
||||
@@ -707,6 +717,18 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Read trail point ages (must match trailPoints size for fading trail)
|
||||
if (nbt.hasKey("TrailAges")) {
|
||||
int[] ages = nbt.getIntArray("TrailAges");
|
||||
for (int age : ages) {
|
||||
trailPointAges.add(age);
|
||||
}
|
||||
}
|
||||
// If trail points exist but ages don't (legacy data), initialize ages to 0
|
||||
while (trailPointAges.size() < trailPoints.size()) {
|
||||
trailPointAges.add(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -733,5 +755,12 @@ public class EntityAbilityBeam extends EntityAbilityProjectile {
|
||||
trailList.appendTag(pointNbt);
|
||||
}
|
||||
nbt.setTag("Trail", trailList);
|
||||
|
||||
// Write trail point ages for fading trail sync
|
||||
int[] ages = new int[trailPointAges.size()];
|
||||
for (int i = 0; i < trailPointAges.size(); i++) {
|
||||
ages[i] = trailPointAges.get(i);
|
||||
}
|
||||
nbt.setIntArray("TrailAges", ages);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package kamkeel.npcs.entity;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import noppes.npcs.EventHooks;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
@@ -344,6 +345,9 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
|
||||
|
||||
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
|
||||
if (!worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
|
||||
}
|
||||
hasHit = true;
|
||||
if (isExplosive()) {
|
||||
posX = blockHit.hitVec.xCoord;
|
||||
@@ -391,19 +395,13 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
protected void updateCharging() {
|
||||
chargeTick++;
|
||||
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner == null || owner.isDead) {
|
||||
setDead();
|
||||
return;
|
||||
}
|
||||
|
||||
// Grow size based on charge progress
|
||||
float progress = getChargeProgress();
|
||||
this.discRadius = targetDiscRadius * progress;
|
||||
this.discThickness = targetDiscThickness * progress;
|
||||
this.size = this.discRadius; // Base size for interpolation
|
||||
|
||||
// Calculate position based on anchor point, offset downward by half radius to center
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner instanceof EntityLivingBase) {
|
||||
Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData);
|
||||
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
|
||||
@@ -419,14 +417,34 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
return discRadius;
|
||||
}
|
||||
|
||||
public void setDiscRadius(float radius) {
|
||||
this.discRadius = radius;
|
||||
}
|
||||
|
||||
public float getDiscThickness() {
|
||||
return discThickness;
|
||||
}
|
||||
|
||||
public void setDiscThickness(float thickness) {
|
||||
this.discThickness = thickness;
|
||||
}
|
||||
|
||||
public boolean isBoomerang() {
|
||||
return boomerang;
|
||||
}
|
||||
|
||||
public void setBoomerang(boolean boomerang) {
|
||||
this.boomerang = boomerang;
|
||||
}
|
||||
|
||||
public int getBoomerangDelay() {
|
||||
return boomerangDelay;
|
||||
}
|
||||
|
||||
public void setBoomerangDelay(int delay) {
|
||||
this.boomerangDelay = delay;
|
||||
}
|
||||
|
||||
public boolean isReturning() {
|
||||
return returning;
|
||||
}
|
||||
@@ -435,6 +453,10 @@ public class EntityAbilityDisc extends EntityAbilityProjectile {
|
||||
return vertical;
|
||||
}
|
||||
|
||||
public void setVertical(boolean vertical) {
|
||||
this.vertical = vertical;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the yaw angle (degrees) from motion vector for vertical disc orientation.
|
||||
* During charging, uses owner's facing direction.
|
||||
|
||||
@@ -3,6 +3,7 @@ package kamkeel.npcs.entity;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import noppes.npcs.EventHooks;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
@@ -145,6 +146,9 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
endZ = startZ + dirZ * currentLength;
|
||||
|
||||
if (ticksSinceFullExtension >= lingerTicks) {
|
||||
if (!worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileExpired(this);
|
||||
}
|
||||
this.setDead();
|
||||
}
|
||||
}
|
||||
@@ -156,8 +160,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
*/
|
||||
private void updateLaserOriginAndDirection() {
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner == null || owner.isDead) return;
|
||||
if (!(owner instanceof EntityLivingBase)) return;
|
||||
if (owner == null || !(owner instanceof EntityLivingBase)) return;
|
||||
|
||||
EntityLivingBase livingOwner = (EntityLivingBase) owner;
|
||||
|
||||
@@ -237,6 +240,9 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
MovingObjectPosition blockHit = worldObj.func_147447_a(start, end, false, true, false);
|
||||
|
||||
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
|
||||
if (!worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
|
||||
}
|
||||
// Laser stops at block
|
||||
currentLength = (float) Math.sqrt(
|
||||
(blockHit.hitVec.xCoord - startX) * (blockHit.hitVec.xCoord - startX) +
|
||||
@@ -356,6 +362,32 @@ public class EntityAbilityLaser extends EntityAbilityProjectile {
|
||||
return laserWidth;
|
||||
}
|
||||
|
||||
public void setLaserWidth(float width) {
|
||||
this.laserWidth = width;
|
||||
}
|
||||
|
||||
public float getExpansionSpeed() {
|
||||
return expansionSpeed;
|
||||
}
|
||||
|
||||
public void setExpansionSpeed(float speed) {
|
||||
this.expansionSpeed = speed;
|
||||
}
|
||||
|
||||
public int getLingerTicks() {
|
||||
return lingerTicks;
|
||||
}
|
||||
|
||||
public void setLingerTicks(int ticks) {
|
||||
this.lingerTicks = ticks;
|
||||
}
|
||||
|
||||
public void setDirection(double x, double y, double z) {
|
||||
this.dirX = x;
|
||||
this.dirY = y;
|
||||
this.dirZ = z;
|
||||
}
|
||||
|
||||
public float getCurrentLength() {
|
||||
return currentLength;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package kamkeel.npcs.entity;
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
import noppes.npcs.EventHooks;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.util.AxisAlignedBB;
|
||||
@@ -232,6 +233,9 @@ public class EntityAbilityOrb extends EntityAbilityProjectile {
|
||||
MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false);
|
||||
|
||||
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
|
||||
if (!worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
|
||||
}
|
||||
hasHit = true;
|
||||
if (isExplosive()) {
|
||||
posX = blockHit.hitVec.xCoord;
|
||||
|
||||
@@ -8,7 +8,7 @@ import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityEffect;
|
||||
import kamkeel.npcs.controllers.data.ability.AnchorPoint;
|
||||
import kamkeel.npcs.controllers.data.ability.IAbilityDamageHandler;
|
||||
|
||||
import kamkeel.npcs.controllers.data.ability.data.*;
|
||||
import kamkeel.npcs.util.AnchorPointHelper;
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -20,13 +20,17 @@ import net.minecraft.util.AxisAlignedBB;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.util.Vec3;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.EventHooks;
|
||||
import noppes.npcs.NpcDamageSource;
|
||||
import noppes.npcs.api.entity.IEnergyProjectile;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
import noppes.npcs.scripted.NpcAPI;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -36,7 +40,38 @@ import java.util.Map;
|
||||
*
|
||||
* Design inspired by LouisXIV's energy attack system.
|
||||
*/
|
||||
public abstract class EntityAbilityProjectile extends Entity implements IEnergyProjectile, IEntityAdditionalSpawnData {
|
||||
public abstract class EntityAbilityProjectile extends Entity implements IEntityAdditionalSpawnData {
|
||||
|
||||
// ==================== ACTIVE PROJECTILE TRACKING ====================
|
||||
private static final Map<Integer, List<WeakReference<EntityAbilityProjectile>>> activeProjectiles = new HashMap<>();
|
||||
|
||||
public static void trackProjectile(EntityAbilityProjectile projectile) {
|
||||
int ownerId = projectile.getOwnerEntityId();
|
||||
if (ownerId < 0) return;
|
||||
List<WeakReference<EntityAbilityProjectile>> refs = activeProjectiles.get(ownerId);
|
||||
if (refs == null) {
|
||||
refs = new ArrayList<>();
|
||||
activeProjectiles.put(ownerId, refs);
|
||||
}
|
||||
refs.add(new WeakReference<>(projectile));
|
||||
}
|
||||
|
||||
public static List<EntityAbilityProjectile> getActiveProjectiles(int ownerEntityId) {
|
||||
List<WeakReference<EntityAbilityProjectile>> refs = activeProjectiles.get(ownerEntityId);
|
||||
if (refs == null) return Collections.emptyList();
|
||||
List<EntityAbilityProjectile> result = new ArrayList<>();
|
||||
Iterator<WeakReference<EntityAbilityProjectile>> it = refs.iterator();
|
||||
while (it.hasNext()) {
|
||||
EntityAbilityProjectile p = it.next().get();
|
||||
if (p == null || p.isDead) {
|
||||
it.remove();
|
||||
} else {
|
||||
result.add(p);
|
||||
}
|
||||
}
|
||||
if (refs.isEmpty()) activeProjectiles.remove(ownerEntityId);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== VISUAL PROPERTIES ====================
|
||||
protected float size = 1.0f;
|
||||
@@ -174,6 +209,11 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
// Skip super.onUpdate() in preview mode to avoid world checks
|
||||
if (!previewMode) {
|
||||
super.onUpdate();
|
||||
|
||||
// Track on first server tick
|
||||
if (ticksExisted == 1 && !worldObj.isRemote) {
|
||||
trackProjectile(this);
|
||||
}
|
||||
}
|
||||
|
||||
// Update rotation
|
||||
@@ -184,16 +224,20 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
|
||||
// Skip lifetime/distance checks in preview mode
|
||||
if (!previewMode) {
|
||||
// Failsafe: if owner entity is gone, dead, or NPC was killed/reset, self-destruct
|
||||
// Only self-destruct if the owner is confirmed dead/killed.
|
||||
// A null owner (unloaded/out of chunk) is NOT treated as dead —
|
||||
// the projectile keeps acting normally and deathWorldTime handles lifetime.
|
||||
if (ownerEntityId >= 0 && ticksExisted > 5) {
|
||||
Entity owner = worldObj.getEntityByID(ownerEntityId);
|
||||
if (owner == null || owner.isDead) {
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) {
|
||||
this.setDead();
|
||||
return;
|
||||
if (owner != null) {
|
||||
if (owner.isDead) {
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) {
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,12 +248,18 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
|
||||
// Check lifespan using world time (survives chunk unload/reload)
|
||||
if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) {
|
||||
if (!worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileExpired(this);
|
||||
}
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check max distance (subclass can override if needed)
|
||||
if (checkMaxDistance()) {
|
||||
if (!worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileExpired(this);
|
||||
}
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
@@ -222,6 +272,11 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
|
||||
// Subclass-specific update
|
||||
updateProjectile();
|
||||
|
||||
// Fire tick event (server-side, non-preview only)
|
||||
if (!previewMode && !worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileTick(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,19 +379,25 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
|
||||
protected void applyDamage(EntityLivingBase target, float dmg, float kb) {
|
||||
if (previewMode) return; // Skip damage in preview mode
|
||||
|
||||
// Fire entity impact event (may cancel or modify damage)
|
||||
if (!worldObj.isRemote) {
|
||||
float result = EventHooks.onEnergyProjectileEntityImpact(this, target, dmg);
|
||||
if (result < 0) return; // Event was cancelled
|
||||
dmg = result;
|
||||
}
|
||||
|
||||
Entity owner = getOwnerEntity();
|
||||
|
||||
// Check for external damage handler (e.g., DBC Addon)
|
||||
// Check for ability extenders (e.g., DBC Addon damage routing)
|
||||
boolean handled = false;
|
||||
if (sourceAbility != null && owner instanceof EntityLivingBase) {
|
||||
IAbilityDamageHandler handler = AbilityController.Instance.getDamageHandler();
|
||||
if (handler != null) {
|
||||
double dx = target.posX - posX;
|
||||
double dz = target.posZ - posZ;
|
||||
float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f;
|
||||
handled = handler.handleDamage(sourceAbility, (EntityLivingBase) owner, target,
|
||||
dmg, kb, kbUp, dx, dz);
|
||||
}
|
||||
double dx = target.posX - posX;
|
||||
double dz = target.posZ - posZ;
|
||||
float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f;
|
||||
handled = AbilityController.Instance.fireOnAbilityDamage(
|
||||
sourceAbility, (EntityLivingBase) owner, target,
|
||||
dmg, kb, kbUp, dx, dz);
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
@@ -508,16 +569,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
* Update during charging state - grow size and follow anchor.
|
||||
* Default implementation grows {@code size} from 0 to {@code targetSize}.
|
||||
* Override in subclasses for type-specific charging (Disc, Beam).
|
||||
*
|
||||
* Note: Owner death checks are handled by onUpdate() before this is called.
|
||||
*/
|
||||
protected void updateCharging() {
|
||||
chargeTick++;
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner == null || owner.isDead) {
|
||||
setDead();
|
||||
return;
|
||||
}
|
||||
float progress = getChargeProgress();
|
||||
this.size = targetSize * progress;
|
||||
Entity owner = getOwnerEntity();
|
||||
if (owner instanceof EntityLivingBase) {
|
||||
Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData);
|
||||
setPosition(pos.xCoord, pos.yCoord, pos.zCoord);
|
||||
@@ -627,12 +686,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
return worldObj.getEntityByID(ownerEntityId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOwnerEntityId() {
|
||||
return ownerEntityId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOwnerEntityId(int id) {
|
||||
this.ownerEntityId = id;
|
||||
}
|
||||
|
||||
public IEntity getOwner() {
|
||||
if (previewMode) return null;
|
||||
if (getOwnerEntity() == null) return null;
|
||||
@@ -647,17 +708,19 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
this.previewOwner = owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTargetEntityId() {
|
||||
return targetEntityId;
|
||||
}
|
||||
|
||||
public void setTargetEntityId(int id) {
|
||||
this.targetEntityId = id;
|
||||
}
|
||||
|
||||
public Entity getTargetEntity() {
|
||||
if (targetEntityId == -1) return null;
|
||||
return worldObj.getEntityByID(targetEntityId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IEntity getTarget() {
|
||||
if (getTargetEntity() == null) return null;
|
||||
return NpcAPI.Instance().getIEntity(getTargetEntity());
|
||||
@@ -686,162 +749,135 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
return false;
|
||||
}
|
||||
|
||||
// ==================== VISUAL GETTERS ====================
|
||||
// ==================== VISUAL GETTERS & SETTERS ====================
|
||||
|
||||
@Override
|
||||
public float getSize() {
|
||||
return size;
|
||||
public float getSize() { return size; }
|
||||
public void setProjectileSize(float size) {
|
||||
this.size = size;
|
||||
this.renderCurrentSize = size;
|
||||
this.prevRenderSize = size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInnerColor() {
|
||||
return displayData.getInnerColor();
|
||||
/**
|
||||
* Set the start position for distance calculations.
|
||||
* Used by factory methods when creating projectiles without the full constructor.
|
||||
*/
|
||||
public void setStartPosition(double x, double y, double z) {
|
||||
this.startX = x;
|
||||
this.startY = y;
|
||||
this.startZ = z;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOuterColor() {
|
||||
return displayData.getOuterColor();
|
||||
}
|
||||
public int getInnerColor() { return displayData.getInnerColor(); }
|
||||
public void setInnerColor(int color) { displayData.setInnerColor(color); }
|
||||
|
||||
@Override
|
||||
public boolean isOuterColorEnabled() {
|
||||
return displayData.isOuterColorEnabled();
|
||||
}
|
||||
public int getOuterColor() { return displayData.getOuterColor(); }
|
||||
public void setOuterColor(int color) { displayData.setOuterColor(color); }
|
||||
|
||||
@Override
|
||||
public float getOuterColorWidth() {
|
||||
return displayData.getOuterColorWidth();
|
||||
}
|
||||
public boolean isOuterColorEnabled() { return displayData.isOuterColorEnabled(); }
|
||||
public void setOuterColorEnabled(boolean enabled) { displayData.setOuterColorEnabled(enabled); }
|
||||
|
||||
@Override
|
||||
public float getOuterColorAlpha() {
|
||||
return displayData.getOuterColorAlpha();
|
||||
}
|
||||
public float getOuterColorWidth() { return displayData.getOuterColorWidth(); }
|
||||
public void setOuterColorWidth(float width) { displayData.setOuterColorWidth(width); }
|
||||
|
||||
@Override
|
||||
public float getRotationSpeed() {
|
||||
return displayData.getRotationSpeed();
|
||||
}
|
||||
public float getOuterColorAlpha() { return displayData.getOuterColorAlpha(); }
|
||||
public void setOuterColorAlpha(float alpha) { displayData.setOuterColorAlpha(alpha); }
|
||||
|
||||
public float getRotationSpeed() { return displayData.getRotationSpeed(); }
|
||||
public void setRotationSpeed(float speed) { displayData.setRotationSpeed(speed); }
|
||||
|
||||
// ==================== ROTATION GETTERS ====================
|
||||
|
||||
@Override
|
||||
public float getInterpolatedRotationX(float partialTicks) {
|
||||
return this.prevRotationValX + (this.rotationValX - this.prevRotationValX) * partialTicks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getInterpolatedRotationY(float partialTicks) {
|
||||
return this.prevRotationValY + (this.rotationValY - this.prevRotationValY) * partialTicks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getInterpolatedRotationZ(float partialTicks) {
|
||||
return this.prevRotationValZ + (this.rotationValZ - this.prevRotationValZ) * partialTicks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getInterpolatedSize(float partialTicks) {
|
||||
return this.prevRenderSize + (this.renderCurrentSize - this.prevRenderSize) * partialTicks;
|
||||
}
|
||||
|
||||
// ==================== LIGHTNING GETTERS ====================
|
||||
// ==================== LIGHTNING GETTERS & SETTERS ====================
|
||||
|
||||
@Override
|
||||
public boolean hasLightningEffect() {
|
||||
return lightningData.isLightningEffect();
|
||||
}
|
||||
public boolean hasLightningEffect() { return lightningData.isLightningEffect(); }
|
||||
public void setLightningEffect(boolean enabled) { lightningData.setLightningEffect(enabled); }
|
||||
|
||||
@Override
|
||||
public float getLightningDensity() {
|
||||
return lightningData.getLightningDensity();
|
||||
}
|
||||
public float getLightningDensity() { return lightningData.getLightningDensity(); }
|
||||
public void setLightningDensity(float density) { lightningData.setLightningDensity(density); }
|
||||
|
||||
@Override
|
||||
public float getLightningRadius() {
|
||||
return lightningData.getLightningRadius();
|
||||
}
|
||||
public float getLightningRadius() { return lightningData.getLightningRadius(); }
|
||||
public void setLightningRadius(float radius) { lightningData.setLightningRadius(radius); }
|
||||
|
||||
@Override
|
||||
public int getLightningFadeTime() {
|
||||
return lightningData.getLightningFadeTime();
|
||||
}
|
||||
public int getLightningFadeTime() { return lightningData.getLightningFadeTime(); }
|
||||
public void setLightningFadeTime(int ticks) { lightningData.setLightningFadeTime(ticks); }
|
||||
|
||||
// ==================== LIFESPAN GETTERS ====================
|
||||
// ==================== LIFESPAN GETTERS & SETTERS ====================
|
||||
|
||||
@Override
|
||||
public float getMaxDistance() { return lifespanData.getMaxDistance(); }
|
||||
public void setMaxDistance(float distance) { lifespanData.setMaxDistance(distance); }
|
||||
|
||||
@Override
|
||||
public int getMaxLifetime() { return lifespanData.getMaxLifetime(); }
|
||||
public void setMaxLifetime(int ticks) { lifespanData.setMaxLifetime(ticks); }
|
||||
|
||||
// ==================== COMBAT GETTERS ====================
|
||||
// ==================== COMBAT GETTERS & SETTERS ====================
|
||||
|
||||
@Override
|
||||
public float getDamage() { return combatData.getDamage(); }
|
||||
public void setCombatDamage(float damage) { combatData.setDamage(damage); }
|
||||
|
||||
@Override
|
||||
public float getKnockback() { return combatData.knockback; }
|
||||
public void setCombatKnockback(float knockback) { combatData.setKnockback(knockback); }
|
||||
|
||||
@Override
|
||||
public float getKnockbackUp() { return combatData.knockbackUp; }
|
||||
public void setCombatKnockbackUp(float knockbackUp) { combatData.setKnockbackUp(knockbackUp); }
|
||||
|
||||
@Override
|
||||
public boolean isExplosive() { return combatData.isExplosive(); }
|
||||
public void setExplosive(boolean explosive) { combatData.setExplosive(explosive); }
|
||||
|
||||
@Override
|
||||
public float getExplosionRadius() { return combatData.explosionRadius; }
|
||||
public void setExplosionRadius(float radius) { combatData.setExplosionRadius(radius); }
|
||||
|
||||
@Override
|
||||
public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; }
|
||||
public void setExplosionDamageFalloff(float falloff) { combatData.setExplosionDamageFalloff(falloff); }
|
||||
|
||||
// ==================== HOMING GETTERS ====================
|
||||
// ==================== HOMING GETTERS & SETTERS ====================
|
||||
|
||||
@Override
|
||||
public boolean isHoming() { return homingData.isHoming(); }
|
||||
public void setHomingEnabled(boolean homing) { homingData.setHoming(homing); }
|
||||
|
||||
@Override
|
||||
public float getHomingStrength() { return homingData.getHomingStrength(); }
|
||||
public void setHomingStrength(float strength) { homingData.setHomingStrength(strength); }
|
||||
|
||||
@Override
|
||||
public float getHomingRange() { return homingData.getHomingRange(); }
|
||||
public void setHomingRange(float range) { homingData.setHomingRange(range); }
|
||||
|
||||
// ==================== TRAJECTORY GETTERS ====================
|
||||
// ==================== SPEED & ANCHOR GETTERS & SETTERS ====================
|
||||
|
||||
|
||||
|
||||
// ==================== ANCHOR GETTERS ====================
|
||||
|
||||
@Override
|
||||
public float getSpeed() { return homingData.getSpeed(); }
|
||||
public void setSpeed(float speed) { homingData.setSpeed(speed); }
|
||||
|
||||
public AnchorPoint getAnchorPoint() { return anchorData.getAnchorPoint(); }
|
||||
|
||||
@Override
|
||||
public int getAnchor() { return anchorData.getAnchor(); }
|
||||
|
||||
@Override
|
||||
public float getAnchorOffsetX() { return anchorData.getAnchorOffsetX(); }
|
||||
|
||||
@Override
|
||||
public float getAnchorOffsetY() { return anchorData.getAnchorOffsetY(); }
|
||||
|
||||
@Override
|
||||
public float getAnchorOffsetZ() { return anchorData.getAnchorOffsetZ(); }
|
||||
|
||||
// ==================== MOVEMENT GETTERS ====================
|
||||
|
||||
@Override public double getStartX() { return startX; }
|
||||
public double getStartX() { return startX; }
|
||||
public double getStartY() { return startY; }
|
||||
public double getStartZ() { return startZ; }
|
||||
|
||||
@Override public double getStartY() { return startY; }
|
||||
// ==================== STATE GETTERS ====================
|
||||
|
||||
@Override public double getStartZ() { return startZ; }
|
||||
|
||||
@Override public double getMotionX() { return motionX; }
|
||||
|
||||
@Override public double getMotionY() { return motionY; }
|
||||
|
||||
@Override public double getMotionZ() { return motionZ; }
|
||||
public boolean getHasHit() { return hasHit; }
|
||||
|
||||
// ==================== BRIGHTNESS ====================
|
||||
|
||||
@@ -898,10 +934,10 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
this.motionY = nbt.getDouble("MotionY");
|
||||
this.motionZ = nbt.getDouble("MotionZ");
|
||||
|
||||
if (this.worldObj != null && this.worldObj.isRemote) {
|
||||
this.renderCurrentSize = this.size;
|
||||
this.prevRenderSize = this.size;
|
||||
}
|
||||
// Always initialize render size from NBT size so entities
|
||||
// loading in from outside render distance render correctly
|
||||
this.renderCurrentSize = this.size;
|
||||
this.prevRenderSize = this.size;
|
||||
|
||||
anchorData.readNBT(nbt);
|
||||
combatData.readNBT(nbt);
|
||||
@@ -967,7 +1003,9 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
NBTTagCompound compound = new NBTTagCompound();
|
||||
this.writeEntityToNBT(compound);
|
||||
cpw.mods.fml.common.network.ByteBufUtils.writeTag(buffer, compound);
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception e) {
|
||||
noppes.npcs.LogWriter.error("Error writing ability projectile spawn data", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -977,7 +1015,9 @@ public abstract class EntityAbilityProjectile extends Entity implements IEnergyP
|
||||
if (compound != null) {
|
||||
this.readEntityFromNBT(compound);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception e) {
|
||||
noppes.npcs.LogWriter.error("Error reading ability projectile spawn data", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== COLLISION SETTINGS ====================
|
||||
|
||||
@@ -144,15 +144,19 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa
|
||||
return;
|
||||
}
|
||||
|
||||
// Follow owner
|
||||
Entity owner = getOwner();
|
||||
if (owner == null) {
|
||||
if (owner != null && owner.isDead) {
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
if (owner instanceof EntityNPCInterface && ((EntityNPCInterface) owner).isKilled()) {
|
||||
this.setDead();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update position to follow owner
|
||||
this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ);
|
||||
if (owner != null) {
|
||||
this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ);
|
||||
}
|
||||
|
||||
// Check if sweep is done
|
||||
if (completedRotations >= numberOfRotations) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Shared zone entity for Trap and Hazard abilities.
|
||||
* Handles rendering, positioning, and damage logic.
|
||||
* ZoneType distinguishes behavior: TRAP (proximity-triggered burst) vs HAZARD (persistent AoE).
|
||||
*/
|
||||
public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnData {
|
||||
|
||||
@@ -76,11 +76,11 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
}
|
||||
}
|
||||
|
||||
/** Tiny Y offset above ground to prevent z-clipping */
|
||||
private static final double GROUND_OFFSET = 0.005;
|
||||
private static final byte TRIGGER_FLASH_STATUS = 60;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// COMMON PROPERTIES (synced via ByteBuf)
|
||||
// COMMON PROPERTIES
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private ZoneType zoneType = ZoneType.TRAP;
|
||||
@@ -99,13 +99,12 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
|
||||
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)
|
||||
// Visual layer fields
|
||||
private boolean groundFill = true;
|
||||
private float groundAlpha = 0.25f;
|
||||
private boolean rings = true;
|
||||
@@ -113,18 +112,15 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
private boolean border = true;
|
||||
private float borderSpeed = 1.0f;
|
||||
private boolean accents = true;
|
||||
private int accentStyle = 0; // AccentStyle ordinal
|
||||
private int accentStyle = 0;
|
||||
private boolean lightning = false;
|
||||
private boolean particles = true;
|
||||
private int particleMotion = 0; // ParticleMotion ordinal
|
||||
private int particleMotion = 0;
|
||||
private String particleDir = "";
|
||||
private int particleSize = 32;
|
||||
private boolean particleGlow = true;
|
||||
|
||||
// Lifetime
|
||||
private long deathWorldTime = -1;
|
||||
|
||||
// Effects
|
||||
private List<AbilityEffect> effects = new ArrayList<>();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -138,6 +134,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
private float damage = 6.0f;
|
||||
private float damageRadius = 0.0f;
|
||||
private float knockback = 0.5f;
|
||||
private boolean visible = true;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// HAZARD-SPECIFIC PROPERTIES
|
||||
@@ -149,21 +146,18 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
private boolean affectsCaster = false;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// RUNTIME STATE (transient)
|
||||
// RUNTIME STATE
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
// 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
|
||||
private transient int triggerFlashTick = -1;
|
||||
|
||||
// Hazard runtime
|
||||
private transient int ticksSinceDamage = 0;
|
||||
private transient Set<Integer> damagedThisTick = new HashSet<>();
|
||||
|
||||
// Preview mode
|
||||
private boolean previewMode = false;
|
||||
private EntityLivingBase previewOwner = null;
|
||||
|
||||
@@ -179,11 +173,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
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);
|
||||
@@ -194,9 +183,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
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,
|
||||
@@ -207,6 +193,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
float zoneHeight,
|
||||
float particleDensity, float particleScale,
|
||||
float animSpeed, float lightningDensity,
|
||||
boolean visible,
|
||||
List<AbilityEffect> effects) {
|
||||
EntityAbilityZone zone = new EntityAbilityZone(world, ZoneType.TRAP, owner, x, y, z);
|
||||
zone.shape = shape;
|
||||
@@ -228,19 +215,16 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
zone.particleScale = particleScale;
|
||||
zone.animSpeed = animSpeed;
|
||||
zone.lightningDensity = lightningDensity;
|
||||
zone.visible = visible;
|
||||
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,
|
||||
@@ -275,7 +259,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
zone.effects.add(e.copy());
|
||||
}
|
||||
}
|
||||
// Ready to deal damage immediately
|
||||
zone.ticksSinceDamage = damageInterval;
|
||||
return zone;
|
||||
}
|
||||
@@ -285,8 +268,15 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
@Override
|
||||
protected void entityInit() {
|
||||
// No DataWatcher needed
|
||||
protected void entityInit() {}
|
||||
|
||||
@Override
|
||||
public void handleHealthUpdate(byte id) {
|
||||
if (id == TRIGGER_FLASH_STATUS) {
|
||||
this.triggerFlashTick = this.ticksExisted;
|
||||
} else {
|
||||
super.handleHealthUpdate(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -297,18 +287,15 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
|
||||
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:
|
||||
@@ -322,11 +309,10 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// TRAP LOGIC (server-only)
|
||||
// TRAP LOGIC
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void tickTrap() {
|
||||
// Arm time countdown
|
||||
if (!armed) {
|
||||
if (ticksExisted >= armTime) {
|
||||
armed = true;
|
||||
@@ -336,18 +322,15 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
|
||||
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
|
||||
@@ -419,7 +402,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
anyDamaged = true;
|
||||
}
|
||||
|
||||
// Apply knockback only if damage landed
|
||||
if (hit && knockback > 0) {
|
||||
double dx = entity.posX - posX;
|
||||
double dz = entity.posZ - posZ;
|
||||
@@ -430,23 +412,22 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
worldObj.setEntityState(this, TRIGGER_FLASH_STATUS);
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// HAZARD LOGIC (server-only)
|
||||
// HAZARD LOGIC
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
private void tickHazard() {
|
||||
@@ -545,12 +526,12 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public boolean isInRangeToRenderDist(double distance) {
|
||||
return distance < 16384.0D; // 128 blocks
|
||||
return distance < 16384.0D;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldRenderInPass(int pass) {
|
||||
return pass == 1; // Translucent pass
|
||||
return pass == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -576,6 +557,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
public boolean isOuterColorEnabled() { return outerColorEnabled; }
|
||||
public float getZoneHeight() { return zoneHeight; }
|
||||
public boolean isArmed() { return armed || ticksExisted >= armTime; }
|
||||
public boolean isVisible() { return visible; }
|
||||
public int getTriggerFlashTick() { return triggerFlashTick; }
|
||||
public int getDurationTicks() { return durationTicks; }
|
||||
public int getMaxTicks() { return maxTicks; }
|
||||
@@ -584,7 +566,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
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; }
|
||||
@@ -600,9 +581,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
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,
|
||||
@@ -626,44 +604,6 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
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
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -671,16 +611,9 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
@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
|
||||
try {
|
||||
this.shape = ZoneShape.valueOf(nbt.getString("ShapeName"));
|
||||
} catch (Exception e) {
|
||||
this.shape = ZoneShape.CIRCLE;
|
||||
}
|
||||
|
||||
@@ -691,51 +624,28 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
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;
|
||||
this.zoneHeight = nbt.getFloat("ZoneHeight");
|
||||
this.particleDensity = nbt.getFloat("ParticleDensity");
|
||||
this.particleScale = nbt.getFloat("ParticleScale");
|
||||
this.animSpeed = nbt.getFloat("AnimSpeed");
|
||||
this.lightningDensity = nbt.getFloat("LightningDensity");
|
||||
this.deathWorldTime = nbt.getLong("DeathWorldTime");
|
||||
|
||||
// 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");
|
||||
}
|
||||
// Visual layer fields
|
||||
this.groundFill = nbt.getBoolean("GroundFill");
|
||||
this.groundAlpha = nbt.getFloat("GroundAlpha");
|
||||
this.rings = nbt.getBoolean("Rings");
|
||||
this.ringCount = nbt.getInteger("RingCount");
|
||||
this.border = nbt.getBoolean("Border");
|
||||
this.borderSpeed = nbt.getFloat("BorderSpeed");
|
||||
this.accents = nbt.getBoolean("Accents");
|
||||
this.accentStyle = nbt.getInteger("AccentStyle");
|
||||
this.lightning = nbt.getBoolean("Lightning");
|
||||
this.particles = nbt.getBoolean("Particles");
|
||||
this.particleMotion = nbt.getInteger("ParticleMotion");
|
||||
this.particleDir = nbt.getString("ParticleDir");
|
||||
this.particleSize = nbt.getInteger("ParticleSize");
|
||||
this.particleGlow = nbt.getBoolean("ParticleGlow");
|
||||
|
||||
// Trap
|
||||
this.armTime = nbt.getInteger("ArmTime");
|
||||
@@ -745,6 +655,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
this.damage = nbt.getFloat("Damage");
|
||||
this.damageRadius = nbt.getFloat("DamageRadius");
|
||||
this.knockback = nbt.getFloat("Knockback");
|
||||
this.visible = nbt.getBoolean("Visible");
|
||||
|
||||
// Hazard
|
||||
this.damagePerSecond = nbt.getFloat("DamagePerSecond");
|
||||
@@ -783,7 +694,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
nbt.setFloat("LightningDensity", lightningDensity);
|
||||
nbt.setLong("DeathWorldTime", deathWorldTime);
|
||||
|
||||
// Visual fields — always written as top-level keys
|
||||
// Visual layer fields
|
||||
nbt.setBoolean("GroundFill", groundFill);
|
||||
nbt.setFloat("GroundAlpha", groundAlpha);
|
||||
nbt.setBoolean("Rings", rings);
|
||||
@@ -807,6 +718,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
nbt.setFloat("Damage", damage);
|
||||
nbt.setFloat("DamageRadius", damageRadius);
|
||||
nbt.setFloat("Knockback", knockback);
|
||||
nbt.setBoolean("Visible", visible);
|
||||
|
||||
// Hazard
|
||||
nbt.setFloat("DamagePerSecond", damagePerSecond);
|
||||
@@ -843,7 +755,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
buffer.writeFloat(animSpeed);
|
||||
buffer.writeFloat(lightningDensity);
|
||||
|
||||
// Visual fields — always written individually
|
||||
// Visual layer fields
|
||||
buffer.writeBoolean(groundFill);
|
||||
buffer.writeFloat(groundAlpha);
|
||||
buffer.writeBoolean(rings);
|
||||
@@ -859,10 +771,11 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
buffer.writeInt(particleSize);
|
||||
buffer.writeBoolean(particleGlow);
|
||||
|
||||
// Trap-specific (client needs armTime for visual state)
|
||||
// Trap-specific
|
||||
buffer.writeInt(armTime);
|
||||
buffer.writeBoolean(visible);
|
||||
|
||||
// Effects via NBT in buffer
|
||||
// Effects
|
||||
NBTTagCompound effectsNbt = new NBTTagCompound();
|
||||
NBTTagList effectList = new NBTTagList();
|
||||
for (AbilityEffect effect : effects) {
|
||||
@@ -894,7 +807,7 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
this.animSpeed = buffer.readFloat();
|
||||
this.lightningDensity = buffer.readFloat();
|
||||
|
||||
// Visual fields — always read individually
|
||||
// Visual layer fields
|
||||
this.groundFill = buffer.readBoolean();
|
||||
this.groundAlpha = buffer.readFloat();
|
||||
this.rings = buffer.readBoolean();
|
||||
@@ -911,8 +824,9 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD
|
||||
this.particleGlow = buffer.readBoolean();
|
||||
|
||||
this.armTime = buffer.readInt();
|
||||
this.visible = buffer.readBoolean();
|
||||
|
||||
// Effects via NBT
|
||||
// Effects
|
||||
NBTTagCompound effectsNbt = ByteBufUtils.readTag(buffer);
|
||||
effects.clear();
|
||||
if (effectsNbt != null && effectsNbt.hasKey("Effects")) {
|
||||
|
||||
@@ -119,6 +119,20 @@ public class AnchorPointHelper {
|
||||
y += rotatedOffset.yCoord;
|
||||
z += rotatedOffset.zCoord;
|
||||
|
||||
// Apply FULL_MODEL rotation if active
|
||||
FramePart fullModel = getFullModelPart(entity);
|
||||
if (fullModel != null) {
|
||||
double dx = x - entity.posX;
|
||||
double dy = y - entity.posY;
|
||||
double dz = z - entity.posZ;
|
||||
Vec3 rotated = applyFullModelToWorldOffset(fullModel, entity, dx, dy, dz, bodyYaw, scale);
|
||||
return Vec3.createVectorHelper(
|
||||
entity.posX + rotated.xCoord,
|
||||
entity.posY + rotated.yCoord,
|
||||
entity.posZ + rotated.zCoord
|
||||
);
|
||||
}
|
||||
|
||||
return Vec3.createVectorHelper(x, y, z);
|
||||
}
|
||||
|
||||
@@ -162,16 +176,32 @@ public class AnchorPointHelper {
|
||||
|
||||
/**
|
||||
* Calculate hand position using animation data if available.
|
||||
* Applies FULL_MODEL rotation if an active animation has it.
|
||||
*/
|
||||
private static Vec3 calculateHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) {
|
||||
// Try to get animated position first
|
||||
Vec3 animatedPos = getAnimatedHandPosition(entity, anchor, rightHand, scale);
|
||||
if (animatedPos != null) {
|
||||
return animatedPos;
|
||||
Vec3 pos = getAnimatedHandPosition(entity, anchor, rightHand, scale);
|
||||
if (pos == null) {
|
||||
// Fallback to static position
|
||||
pos = calculateFallbackHandPosition(entity, anchor, rightHand, scale);
|
||||
}
|
||||
|
||||
// Fallback to static position
|
||||
return calculateFallbackHandPosition(entity, anchor, rightHand, scale);
|
||||
// Apply FULL_MODEL rotation if active
|
||||
FramePart fullModel = getFullModelPart(entity);
|
||||
if (fullModel != null) {
|
||||
double dx = pos.xCoord - entity.posX;
|
||||
double dy = pos.yCoord - entity.posY;
|
||||
double dz = pos.zCoord - entity.posZ;
|
||||
float bodyYaw = (float) Math.toRadians(entity.renderYawOffset);
|
||||
Vec3 rotated = applyFullModelToWorldOffset(fullModel, entity, dx, dy, dz, bodyYaw, scale);
|
||||
return Vec3.createVectorHelper(
|
||||
entity.posX + rotated.xCoord,
|
||||
entity.posY + rotated.yCoord,
|
||||
entity.posZ + rotated.zCoord
|
||||
);
|
||||
}
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,4 +420,100 @@ public class AnchorPointHelper {
|
||||
double worldZ = -offsetX * sin + offsetZ * cos;
|
||||
return Vec3.createVectorHelper(worldX, offsetY, worldZ);
|
||||
}
|
||||
|
||||
// ==================== FULL_MODEL ANIMATION SUPPORT ====================
|
||||
|
||||
/**
|
||||
* Get the FULL_MODEL FramePart if the entity has an active animation with it.
|
||||
* Returns null if no FULL_MODEL animation is active.
|
||||
*/
|
||||
private static FramePart getFullModelPart(EntityLivingBase entity) {
|
||||
AnimationData animData = AnimationData.getData(entity);
|
||||
if (animData == null || !animData.isActive() || animData.animation == null) {
|
||||
return null;
|
||||
}
|
||||
Frame frame = (Frame) animData.animation.currentFrame();
|
||||
if (frame == null) {
|
||||
return null;
|
||||
}
|
||||
FramePart part = frame.frameParts.get(EnumAnimationPart.FULL_MODEL);
|
||||
return part;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply FULL_MODEL rotation to a world-space offset from entity position.
|
||||
* Converts the offset to model space, applies FULL_MODEL rotation and pivots,
|
||||
* then converts back to world space.
|
||||
*
|
||||
* The rendering pipeline is: entity_pos * body_yaw * scale(-1,-1,1) * FULL_MODEL * parts
|
||||
* Combined body_yaw + scale maps model space to world space:
|
||||
* worldX = mx*cos(yaw) + mz*sin(yaw)
|
||||
* worldY = -my (model Y-down → world Y-up)
|
||||
* worldZ = mx*sin(yaw) - mz*cos(yaw)
|
||||
* This transform is its own inverse for X,Z.
|
||||
*
|
||||
* FULL_MODEL rotation order matches renderer: vertex sees Z, then Y, then X.
|
||||
*/
|
||||
private static Vec3 applyFullModelToWorldOffset(FramePart fullModel, EntityLivingBase entity,
|
||||
double worldDX, double worldDY, double worldDZ,
|
||||
float bodyYaw, float scale) {
|
||||
float[] rotations = getRotations(fullModel, entity);
|
||||
float[] pivots = getPivots(fullModel, entity);
|
||||
|
||||
// Quick exit if no transform needed
|
||||
boolean hasRotation = rotations[0] != 0 || rotations[1] != 0 || rotations[2] != 0;
|
||||
boolean hasPivot = pivots[0] != 0 || pivots[1] != 0 || pivots[2] != 0;
|
||||
if (!hasRotation && !hasPivot) {
|
||||
return Vec3.createVectorHelper(worldDX, worldDY, worldDZ);
|
||||
}
|
||||
|
||||
// Convert world offset to model space
|
||||
// Body yaw + scale combined transform is an involution (self-inverse) for X,Z
|
||||
double cosYaw = Math.cos(bodyYaw);
|
||||
double sinYaw = Math.sin(bodyYaw);
|
||||
|
||||
double mx = worldDX * cosYaw + worldDZ * sinYaw;
|
||||
double mz = worldDX * sinYaw - worldDZ * cosYaw;
|
||||
double my = -worldDY; // Invert Y (world Y-up → model Y-down)
|
||||
|
||||
// Apply FULL_MODEL rotations (vertex order: Z first, then Y, then X)
|
||||
// Matches renderer: glRotatef(X), glRotatef(Y), glRotatef(Z) → vertex sees Z,Y,X
|
||||
|
||||
// Z rotation
|
||||
double cosZ = Math.cos(rotations[2]);
|
||||
double sinZ = Math.sin(rotations[2]);
|
||||
double nx = mx * cosZ - my * sinZ;
|
||||
double ny = mx * sinZ + my * cosZ;
|
||||
mx = nx;
|
||||
my = ny;
|
||||
|
||||
// Y rotation
|
||||
double cosY = Math.cos(rotations[1]);
|
||||
double sinY = Math.sin(rotations[1]);
|
||||
nx = mx * cosY + mz * sinY;
|
||||
double nz = -mx * sinY + mz * cosY;
|
||||
mx = nx;
|
||||
mz = nz;
|
||||
|
||||
// X rotation
|
||||
double cosX = Math.cos(rotations[0]);
|
||||
double sinX = Math.sin(rotations[0]);
|
||||
ny = my * cosX - mz * sinX;
|
||||
nz = my * sinX + mz * cosX;
|
||||
my = ny;
|
||||
mz = nz;
|
||||
|
||||
// Apply pivots in model space (after rotation, matching GL vertex ordering)
|
||||
// Renderer: glTranslatef(px, -py, pz) → pivot Y is negated for model Y-down space
|
||||
mx += pivots[0] * MODEL_SCALE * scale;
|
||||
my -= pivots[1] * MODEL_SCALE * scale;
|
||||
mz += pivots[2] * MODEL_SCALE * scale;
|
||||
|
||||
// Convert back to world space (same involution for X,Z; invert Y back)
|
||||
double newDX = mx * cosYaw + mz * sinYaw;
|
||||
double newDZ = mx * sinYaw - mz * cosYaw;
|
||||
double newDY = -my;
|
||||
|
||||
return Vec3.createVectorHelper(newDX, newDY, newDZ);
|
||||
}
|
||||
}
|
||||
@@ -118,7 +118,7 @@ import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta2")
|
||||
@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta3")
|
||||
public class CustomNpcs {
|
||||
|
||||
@SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy")
|
||||
|
||||
@@ -171,6 +171,13 @@ public class DataAbilities {
|
||||
// Fire tick event for scripts
|
||||
fireTickEvent(currentAbility, target);
|
||||
|
||||
// Fire extender tick hook (e.g., per-tick resource drain)
|
||||
if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, npc, target,
|
||||
currentAbility.getPhase(), currentAbility.getCurrentTick())) {
|
||||
interruptCurrentAbility(null, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle phase-specific logic
|
||||
switch (currentAbility.getPhase()) {
|
||||
case WINDUP:
|
||||
@@ -262,7 +269,7 @@ public class DataAbilities {
|
||||
// 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()
|
||||
if (currentAbility.isBurstEnabled()
|
||||
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
|
||||
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
|
||||
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
|
||||
@@ -342,6 +349,9 @@ public class DataAbilities {
|
||||
// Remove any remaining telegraphs (for abilities that keep telegraph during active)
|
||||
removeTelegraph(currentAbility);
|
||||
|
||||
// Fire extender complete hook
|
||||
AbilityController.Instance.fireOnAbilityComplete(currentAbility, npc, target, false);
|
||||
|
||||
// Call onComplete callback
|
||||
currentAbility.onComplete(npc, target);
|
||||
|
||||
@@ -501,6 +511,11 @@ public class DataAbilities {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fire extender start hook (e.g., resource cost checks)
|
||||
if (!AbilityController.Instance.fireOnAbilityStart(ability, npc, target)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
currentAbility = ability;
|
||||
lastTarget = target;
|
||||
ability.start(target);
|
||||
@@ -750,6 +765,9 @@ public class DataAbilities {
|
||||
releaseRotationControl();
|
||||
releaseLockedPosition();
|
||||
|
||||
// Fire extender complete hook (interrupted)
|
||||
AbilityController.Instance.fireOnAbilityComplete(currentAbility, npc, lastTarget, true);
|
||||
|
||||
// Fire interrupt event
|
||||
AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent(
|
||||
npc.wrappedNPC, currentAbility, lastTarget, source, damage);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package noppes.npcs;
|
||||
|
||||
import cpw.mods.fml.common.eventhandler.Event;
|
||||
import kamkeel.npcs.entity.EntityAbilityProjectile;
|
||||
import kamkeel.npcs.network.packets.data.AchievementPacket;
|
||||
import net.minecraft.command.ICommandSender;
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -16,7 +17,10 @@ import net.minecraft.world.World;
|
||||
import net.minecraft.world.WorldServer;
|
||||
import net.minecraftforge.event.entity.EntityEvent;
|
||||
import net.minecraftforge.event.world.WorldEvent;
|
||||
import noppes.npcs.api.IBlock;
|
||||
import noppes.npcs.api.IPos;
|
||||
import noppes.npcs.api.IWorld;
|
||||
import noppes.npcs.api.entity.IEnergyProjectile;
|
||||
import noppes.npcs.api.entity.IAnimatable;
|
||||
import noppes.npcs.api.entity.ICustomNpc;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
@@ -52,6 +56,7 @@ import noppes.npcs.scripted.NpcAPI;
|
||||
import noppes.npcs.scripted.event.AnimationEvent;
|
||||
import noppes.npcs.scripted.event.BlockEvent;
|
||||
import noppes.npcs.scripted.event.CustomNPCsEvent;
|
||||
import noppes.npcs.scripted.event.EnergyProjectileEvent;
|
||||
import noppes.npcs.scripted.event.ForgeEvent;
|
||||
import noppes.npcs.scripted.event.ItemEvent;
|
||||
import noppes.npcs.scripted.event.LinkedItemEvent;
|
||||
@@ -448,6 +453,110 @@ public class EventHooks {
|
||||
NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
// ==================== ENERGY PROJECTILE EVENTS ====================
|
||||
|
||||
public static void onEnergyProjectileFired(EntityAbilityProjectile projectile) {
|
||||
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
|
||||
EnergyProjectileEvent.FiredEvent event = new EnergyProjectileEvent.FiredEvent(wrapped);
|
||||
ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_FIRED, event);
|
||||
|
||||
Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId());
|
||||
if (owner instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) owner).script.callScript(EnumScriptType.ENERGY_PROJECTILE_FIRED, event);
|
||||
} else if (owner instanceof EntityPlayer) {
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner);
|
||||
if (handler != null) {
|
||||
handler.callScript(EnumScriptType.ENERGY_PROJECTILE_FIRED, event);
|
||||
}
|
||||
}
|
||||
|
||||
NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
public static void onEnergyProjectileTick(EntityAbilityProjectile projectile) {
|
||||
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
|
||||
EnergyProjectileEvent.UpdateEvent event = new EnergyProjectileEvent.UpdateEvent(wrapped);
|
||||
ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_TICK, event);
|
||||
|
||||
Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId());
|
||||
if (owner instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) owner).script.callScript(EnumScriptType.ENERGY_PROJECTILE_TICK, event);
|
||||
} else if (owner instanceof EntityPlayer) {
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner);
|
||||
if (handler != null) {
|
||||
handler.callScript(EnumScriptType.ENERGY_PROJECTILE_TICK, event);
|
||||
}
|
||||
}
|
||||
|
||||
NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires the entity impact event. Returns the (possibly modified) damage, or -1 if cancelled.
|
||||
*/
|
||||
public static float onEnergyProjectileEntityImpact(EntityAbilityProjectile projectile, EntityLivingBase target, float damage) {
|
||||
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
|
||||
IEntity wrappedTarget = NpcAPI.Instance().getIEntity(target);
|
||||
EnergyProjectileEvent.EntityImpactEvent event = new EnergyProjectileEvent.EntityImpactEvent(wrapped, wrappedTarget, damage);
|
||||
ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_ENTITY_IMPACT, event);
|
||||
|
||||
Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId());
|
||||
if (owner instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) owner).script.callScript(EnumScriptType.ENERGY_PROJECTILE_ENTITY_IMPACT, event);
|
||||
} else if (owner instanceof EntityPlayer) {
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner);
|
||||
if (handler != null) {
|
||||
handler.callScript(EnumScriptType.ENERGY_PROJECTILE_ENTITY_IMPACT, event);
|
||||
}
|
||||
}
|
||||
|
||||
NpcAPI.EVENT_BUS.post(event);
|
||||
|
||||
if (event.isCanceled()) {
|
||||
return -1;
|
||||
}
|
||||
return event.getDamage();
|
||||
}
|
||||
|
||||
public static void onEnergyProjectileBlockImpact(EntityAbilityProjectile projectile, int blockX, int blockY, int blockZ) {
|
||||
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
|
||||
IPos pos = NpcAPI.Instance().getIPos(blockX, blockY, blockZ);
|
||||
IWorld world = NpcAPI.Instance().getIWorld(projectile.worldObj);
|
||||
IBlock block = NpcAPI.Instance().getIBlock(world, pos);
|
||||
EnergyProjectileEvent.BlockImpactEvent event = new EnergyProjectileEvent.BlockImpactEvent(wrapped, block);
|
||||
ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_BLOCK_IMPACT, event);
|
||||
|
||||
Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId());
|
||||
if (owner instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) owner).script.callScript(EnumScriptType.ENERGY_PROJECTILE_BLOCK_IMPACT, event);
|
||||
} else if (owner instanceof EntityPlayer) {
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner);
|
||||
if (handler != null) {
|
||||
handler.callScript(EnumScriptType.ENERGY_PROJECTILE_BLOCK_IMPACT, event);
|
||||
}
|
||||
}
|
||||
|
||||
NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
public static void onEnergyProjectileExpired(EntityAbilityProjectile projectile) {
|
||||
IEnergyProjectile wrapped = (IEnergyProjectile) NpcAPI.Instance().getIEntity(projectile);
|
||||
EnergyProjectileEvent.ExpiredEvent event = new EnergyProjectileEvent.ExpiredEvent(wrapped);
|
||||
ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ENERGY_PROJECTILE_EXPIRED, event);
|
||||
|
||||
Entity owner = projectile.worldObj.getEntityByID(projectile.getOwnerEntityId());
|
||||
if (owner instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) owner).script.callScript(EnumScriptType.ENERGY_PROJECTILE_EXPIRED, event);
|
||||
} else if (owner instanceof EntityPlayer) {
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) owner);
|
||||
if (handler != null) {
|
||||
handler.callScript(EnumScriptType.ENERGY_PROJECTILE_EXPIRED, event);
|
||||
}
|
||||
}
|
||||
|
||||
NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
public static void onPlayerInit(PlayerDataScript handler, IPlayer player) {
|
||||
PlayerEvent.InitEvent event = new PlayerEvent.InitEvent(player);
|
||||
handler.callScript(EnumScriptType.INIT, event);
|
||||
@@ -693,6 +802,31 @@ public class EventHooks {
|
||||
return NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
public static boolean onPlayerAbilityStart(PlayerDataScript handler, PlayerAbilityEvent.StartEvent event) {
|
||||
handler.callScript(EnumScriptType.ABILITY_START, event);
|
||||
return NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
public static boolean onPlayerAbilityExecute(PlayerDataScript handler, PlayerAbilityEvent.ExecuteEvent event) {
|
||||
handler.callScript(EnumScriptType.ABILITY_EXECUTE, event);
|
||||
return NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
public static void onPlayerAbilityTick(PlayerDataScript handler, PlayerAbilityEvent.TickEvent event) {
|
||||
handler.callScript(EnumScriptType.ABILITY_TICK, event);
|
||||
NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
public static void onPlayerAbilityInterrupt(PlayerDataScript handler, PlayerAbilityEvent.InterruptEvent event) {
|
||||
handler.callScript(EnumScriptType.ABILITY_INTERRUPT, event);
|
||||
NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
public static void onPlayerAbilityComplete(PlayerDataScript handler, PlayerAbilityEvent.CompleteEvent event) {
|
||||
handler.callScript(EnumScriptType.ABILITY_COMPLETE, event);
|
||||
NpcAPI.EVENT_BUS.post(event);
|
||||
}
|
||||
|
||||
public static boolean onPlayerAbilityHit(PlayerDataScript handler, PlayerAbilityEvent.HitEvent event) {
|
||||
handler.callScript(EnumScriptType.ABILITY_HIT, event);
|
||||
return NpcAPI.EVENT_BUS.post(event);
|
||||
|
||||
@@ -9,6 +9,7 @@ import kamkeel.npcs.addon.DBCAddon;
|
||||
import kamkeel.npcs.controllers.AttributeController;
|
||||
import kamkeel.npcs.controllers.SyncController;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.util.AttributeAttackUtil;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
@@ -106,6 +107,9 @@ public class ScriptPlayerEventHandler {
|
||||
Ability current = playerData.abilityData.getCurrentAbility();
|
||||
if (current == null || !current.hasAbilityMovement()) {
|
||||
player.motionX = 0;
|
||||
if (!AbilityController.Instance.isPlayerFlying(player)) {
|
||||
player.motionY = Math.min(player.motionY, 0);
|
||||
}
|
||||
player.motionZ = 0;
|
||||
player.velocityChanged = true;
|
||||
}
|
||||
@@ -328,6 +332,12 @@ public class ScriptPlayerEventHandler {
|
||||
return;
|
||||
|
||||
if (event.player.worldObj instanceof WorldServer && event.player instanceof EntityPlayerMP) {
|
||||
// Cancel any executing ability on dimension change
|
||||
PlayerData playerData = PlayerData.get(event.player);
|
||||
if (playerData != null) {
|
||||
playerData.abilityData.interruptCurrentAbility();
|
||||
}
|
||||
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(event.player);
|
||||
IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(event.player);
|
||||
EventHooks.onPlayerChangeDim(handler, scriptPlayer, event.fromDim, event.toDim);
|
||||
@@ -478,6 +488,13 @@ public class ScriptPlayerEventHandler {
|
||||
|
||||
if (event.entityLiving.worldObj instanceof WorldServer) {
|
||||
if (event.entityLiving instanceof EntityPlayerMP) {
|
||||
// Cancel jump if movement is locked by an ability
|
||||
PlayerData playerData = PlayerData.get((EntityPlayer) event.entityLiving);
|
||||
if (playerData != null && playerData.abilityData.isMovementLocked()) {
|
||||
event.entityLiving.motionY = 0;
|
||||
event.entityLiving.velocityChanged = true;
|
||||
}
|
||||
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.entityLiving);
|
||||
IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(event.entityLiving);
|
||||
EventHooks.onPlayerJump(handler, scriptPlayer);
|
||||
@@ -532,6 +549,8 @@ public class ScriptPlayerEventHandler {
|
||||
|
||||
EntityPlayer player = (EntityPlayer) event.entityLiving;
|
||||
PlayerData playerData = PlayerData.get(player);
|
||||
// Cancel any executing ability on death
|
||||
playerData.abilityData.interruptCurrentAbility();
|
||||
if (ConfigScript.ClearActionsOnDeath)
|
||||
playerData.actionManager.clear();
|
||||
}
|
||||
@@ -631,6 +650,12 @@ public class ScriptPlayerEventHandler {
|
||||
return;
|
||||
|
||||
if (event.player.worldObj instanceof WorldServer && event.player instanceof EntityPlayerMP) {
|
||||
// Cancel any executing ability on respawn
|
||||
PlayerData playerData = PlayerData.get(event.player);
|
||||
if (playerData != null) {
|
||||
playerData.abilityData.interruptCurrentAbility();
|
||||
}
|
||||
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(event.player);
|
||||
IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(event.player);
|
||||
EventHooks.onPlayerRespawn(handler, scriptPlayer);
|
||||
|
||||
@@ -84,9 +84,20 @@ public class ClientEventHandler {
|
||||
|
||||
@SubscribeEvent
|
||||
public void onMouse(MouseEvent event) {
|
||||
if (Minecraft.getMinecraft().currentScreen != null)
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
if (mc.currentScreen != null)
|
||||
return;
|
||||
|
||||
// Block mouse movement during ability rotation lock.
|
||||
// Allow clicks (button >= 0) and scroll (dwheel != 0) to pass through.
|
||||
if (mc.theWorld != null && mc.thePlayer != null
|
||||
&& ClientAbilityState.shouldLockRotation()
|
||||
&& (event.dx != 0 || event.dy != 0)
|
||||
&& event.button == -1 && event.dwheel == 0) {
|
||||
event.setCanceled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayList<Integer> removeList = new ArrayList<>();
|
||||
for (Map.Entry<Integer, Long> entry : disabledButtonTimes.entrySet()) {
|
||||
if (entry.getValue() > 0) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import cpw.mods.fml.common.gameevent.InputEvent;
|
||||
import cpw.mods.fml.common.gameevent.TickEvent;
|
||||
import cpw.mods.fml.common.gameevent.TickEvent.Phase;
|
||||
import kamkeel.npcs.client.renderer.lightning.LightningBolt;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphManager;
|
||||
import kamkeel.npcs.network.PacketClient;
|
||||
import kamkeel.npcs.network.packets.player.CheckPlayerValue;
|
||||
@@ -13,6 +14,7 @@ import kamkeel.npcs.network.packets.player.InputDevicePacket;
|
||||
import kamkeel.npcs.network.packets.player.ScreenSizePacket;
|
||||
import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.client.settings.KeyBinding;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.inventory.ContainerPlayer;
|
||||
@@ -63,7 +65,9 @@ public class ClientTickHandler {
|
||||
if (event.phase == Phase.START) {
|
||||
EntityPlayer player = mc.thePlayer;
|
||||
if (player != null) {
|
||||
boolean specialKeyDown = mc.currentScreen == null && ClientProxy.SpecialKey != null && Keyboard.isKeyDown(ClientProxy.SpecialKey.getKeyCode());
|
||||
// Only process special key when no GUI screen is open
|
||||
boolean specialKeyDown = mc.currentScreen == null
|
||||
&& ClientProxy.SpecialKey != null && Keyboard.isKeyDown(ClientProxy.SpecialKey.getKeyCode());
|
||||
if (specialKeyDown != lastSpecialKeyDown) {
|
||||
PlayerData data = CustomNpcs.proxy.getPlayerData(player);
|
||||
if (data != null) {
|
||||
@@ -73,13 +77,27 @@ public class ClientTickHandler {
|
||||
lastSpecialKeyDown = specialKeyDown;
|
||||
}
|
||||
|
||||
// Suppress player input during ability-controlled phases
|
||||
if (ClientAbilityState.shouldSuppressMovementInput()) {
|
||||
// Suppress player input during ability-controlled phases.
|
||||
// Only suppress when no GUI screen is open (screens already capture input).
|
||||
if (mc.currentScreen == null && ClientAbilityState.shouldSuppressMovementInput()) {
|
||||
// Unpress movement keybinds at the source BEFORE updatePlayerMoveState() reads them.
|
||||
// This is more robust than just zeroing movementInput fields, because
|
||||
// updatePlayerMoveState() reads directly from keybind pressed state.
|
||||
KeyBinding.setKeyBindState(mc.gameSettings.keyBindForward.getKeyCode(), false);
|
||||
KeyBinding.setKeyBindState(mc.gameSettings.keyBindBack.getKeyCode(), false);
|
||||
KeyBinding.setKeyBindState(mc.gameSettings.keyBindLeft.getKeyCode(), false);
|
||||
KeyBinding.setKeyBindState(mc.gameSettings.keyBindRight.getKeyCode(), false);
|
||||
KeyBinding.setKeyBindState(mc.gameSettings.keyBindJump.getKeyCode(), false);
|
||||
KeyBinding.setKeyBindState(mc.gameSettings.keyBindSneak.getKeyCode(), false);
|
||||
KeyBinding.setKeyBindState(mc.gameSettings.keyBindSprint.getKeyCode(), false);
|
||||
|
||||
// Also zero the movementInput values directly as a safety net
|
||||
mc.thePlayer.movementInput.moveForward = 0;
|
||||
mc.thePlayer.movementInput.moveStrafe = 0;
|
||||
mc.thePlayer.movementInput.jump = false;
|
||||
mc.thePlayer.movementInput.sneak = false;
|
||||
}
|
||||
if (ClientAbilityState.shouldLockRotation()) {
|
||||
if (mc.currentScreen == null && ClientAbilityState.shouldLockRotation()) {
|
||||
mc.thePlayer.rotationYaw = ClientAbilityState.lockedYaw;
|
||||
mc.thePlayer.rotationPitch = ClientAbilityState.lockedPitch;
|
||||
mc.thePlayer.prevRotationYaw = ClientAbilityState.lockedYaw;
|
||||
@@ -98,8 +116,43 @@ public class ClientTickHandler {
|
||||
}
|
||||
}
|
||||
if (event.phase == Phase.END) {
|
||||
if (mc.thePlayer != null && mc.theWorld != null && !mc.isGamePaused() && ClientEventHandler.hasOverlays(mc.thePlayer)) {
|
||||
renderCNPCPlayer.itemRenderer.updateEquippedItem();
|
||||
if (mc.thePlayer != null) {
|
||||
// Re-enforce ability locks AFTER entity update.
|
||||
// EntityPlayerSP.onLivingUpdate() calls updatePlayerMoveState() which
|
||||
// overwrites Phase.START suppression with actual keyboard state, causing
|
||||
// client-predicted movement that fights the server lock. Undo it here.
|
||||
// Skip when a GUI screen is open (screens already capture all input).
|
||||
if (mc.currentScreen == null && ClientAbilityState.shouldSuppressMovementInput()) {
|
||||
mc.thePlayer.movementInput.moveForward = 0;
|
||||
mc.thePlayer.movementInput.moveStrafe = 0;
|
||||
mc.thePlayer.movementInput.jump = false;
|
||||
mc.thePlayer.movementInput.sneak = false;
|
||||
|
||||
// Zero horizontal motion (unless ability provides its own movement)
|
||||
if (!ClientAbilityState.hasAbilityMovement) {
|
||||
mc.thePlayer.motionX = 0;
|
||||
mc.thePlayer.motionZ = 0;
|
||||
}
|
||||
|
||||
// Prevent jumping when locked (allow gravity only if not flying)
|
||||
if ((ClientAbilityState.movementLocked || ClientAbilityState.positionLocked)
|
||||
&& !ClientAbilityState.hasAbilityMovement
|
||||
&& !AbilityController.Instance.isPlayerFlying(mc.thePlayer)) {
|
||||
mc.thePlayer.motionY = Math.min(mc.thePlayer.motionY, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (mc.currentScreen == null && ClientAbilityState.shouldLockRotation()) {
|
||||
mc.thePlayer.rotationYaw = ClientAbilityState.lockedYaw;
|
||||
mc.thePlayer.rotationPitch = ClientAbilityState.lockedPitch;
|
||||
mc.thePlayer.prevRotationYaw = ClientAbilityState.lockedYaw;
|
||||
mc.thePlayer.prevRotationPitch = ClientAbilityState.lockedPitch;
|
||||
mc.thePlayer.rotationYawHead = ClientAbilityState.lockedYaw;
|
||||
}
|
||||
|
||||
if (mc.theWorld != null && !mc.isGamePaused() && ClientEventHandler.hasOverlays(mc.thePlayer)) {
|
||||
renderCNPCPlayer.itemRenderer.updateEquippedItem();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -157,8 +210,9 @@ public class ClientTickHandler {
|
||||
|
||||
@SubscribeEvent
|
||||
public void onKey(InputEvent.KeyInputEvent event) {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
|
||||
if (ClientProxy.NPCButton.isPressed()) {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
if (mc.currentScreen == null) {
|
||||
InventoryTabCustomNpc.tabHelper();
|
||||
} else if (mc.currentScreen instanceof GuiCNPCInventory)
|
||||
@@ -169,10 +223,33 @@ public class ClientTickHandler {
|
||||
int key = Keyboard.getEventKey();
|
||||
boolean keyDown = Keyboard.isKeyDown(key);
|
||||
|
||||
// Block movement keys during ability lock (only when no GUI is open and world exists).
|
||||
// Non-movement keys (ESC, chat, inventory, etc.) always pass through.
|
||||
if (mc.theWorld != null && mc.currentScreen == null && keyDown
|
||||
&& ClientAbilityState.shouldSuppressMovementInput() && isMovementKey(key)) {
|
||||
// Unpress the keybind so vanilla doesn't process it
|
||||
KeyBinding.setKeyBindState(key, false);
|
||||
return;
|
||||
}
|
||||
|
||||
InputDevicePacket.sendKeyboard(key, keyDown);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key code corresponds to a movement keybind (WASD, jump, sneak, sprint).
|
||||
*/
|
||||
private boolean isMovementKey(int keyCode) {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
return keyCode == mc.gameSettings.keyBindForward.getKeyCode()
|
||||
|| keyCode == mc.gameSettings.keyBindBack.getKeyCode()
|
||||
|| keyCode == mc.gameSettings.keyBindLeft.getKeyCode()
|
||||
|| keyCode == mc.gameSettings.keyBindRight.getKeyCode()
|
||||
|| keyCode == mc.gameSettings.keyBindJump.getKeyCode()
|
||||
|| keyCode == mc.gameSettings.keyBindSneak.getKeyCode()
|
||||
|| keyCode == mc.gameSettings.keyBindSprint.getKeyCode();
|
||||
}
|
||||
|
||||
private boolean isIgnoredKey(int key) {
|
||||
int[] var2 = this.ignoreKeys;
|
||||
int var3 = var2.length;
|
||||
|
||||
@@ -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 SubGuiTrapPresetSelector extends SubGuiInterface {
|
||||
|
||||
public String selectedPreset = null;
|
||||
|
||||
private static final String[] PRESET_NAMES = {"HIDDEN", "VENOM", "EXPLOSIVE", "CURSED", "SHOCK", "SNARE"};
|
||||
private static final String[] PRESET_LABELS = {
|
||||
"ability.preset.hidden", "ability.preset.venom", "ability.preset.explosive",
|
||||
"ability.preset.cursed", "ability.preset.shock", "ability.preset.snare"
|
||||
};
|
||||
private static final int[] PRESET_COLORS = {0x888888, 0x44DD44, 0xFF6611, 0xAA44FF, 0x4488FF, 0x88CCFF};
|
||||
|
||||
public SubGuiTrapPresetSelector() {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,9 +317,11 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
// Toggle selected ability on/off
|
||||
if (id == 77) {
|
||||
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) {
|
||||
Ability ability = npcSlots.get(selectedAbilityIndex).getAbility();
|
||||
AbilitySlot slot = npcSlots.get(selectedAbilityIndex);
|
||||
Ability ability = slot.getAbility();
|
||||
if (ability != null) {
|
||||
ability.setEnabled(((GuiNpcButton) guibutton).getValue() == 1);
|
||||
boolean enabled = ((GuiNpcButton) guibutton).getValue() == 1;
|
||||
slot.setEnabled(enabled);
|
||||
updateNpcAbilitiesList();
|
||||
selectAbilityByIndex(selectedAbilityIndex);
|
||||
initGui();
|
||||
@@ -489,8 +491,31 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC
|
||||
for (int i = 0; i < npcSlots.size(); i++) {
|
||||
list.add(getAbilityListEntry(i));
|
||||
}
|
||||
|
||||
// Sort: enabled first, then numerically by slot index
|
||||
Collections.sort(list, new Comparator<String>() {
|
||||
@Override
|
||||
public int compare(String a, String b) {
|
||||
boolean aEnabled = a.startsWith("\u00A7a");
|
||||
boolean bEnabled = b.startsWith("\u00A7a");
|
||||
if (aEnabled != bEnabled) return aEnabled ? -1 : 1;
|
||||
return extractIndex(a) - extractIndex(b);
|
||||
}
|
||||
|
||||
private int extractIndex(String entry) {
|
||||
String stripped = entry.replaceAll("\u00A7.", "").trim();
|
||||
int dotIndex = stripped.indexOf(".");
|
||||
if (dotIndex > 0) {
|
||||
try {
|
||||
return Integer.parseInt(stripped.substring(0, dotIndex).replaceAll("[^0-9]", ""));
|
||||
} catch (NumberFormatException e) { }
|
||||
}
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
});
|
||||
|
||||
if (npcAbilitiesScroll != null) {
|
||||
npcAbilitiesScroll.setList(list);
|
||||
npcAbilitiesScroll.setUnsortedList(list);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -337,18 +337,45 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData {
|
||||
protected void handleMouseClick(Slot slot, int slotId, int mouseButton, int modifier) {
|
||||
// Client-side balance prediction: immediately subtract currency cost on click
|
||||
// so the displayed balance updates instantly without waiting for server response.
|
||||
// Must mirror ALL server-side checks in ContainerNPCTrader.slotClick() to avoid
|
||||
// predicting a deduction for trades the server will reject.
|
||||
if (slotId >= 0 && slotId < 18 && mouseButton == 0) {
|
||||
long cost = currencyCost[slotId];
|
||||
if (cost > 0 && playerBalance >= cost) {
|
||||
ItemStack sold = role.inventorySold.items.get(slotId);
|
||||
if (sold != null && container.canBuy(slotId, player)) {
|
||||
if (sold != null
|
||||
&& canGivePlayerClient(sold)
|
||||
&& container.isSlotEnabled(slotId, player)
|
||||
&& (!stockEnabled || availableStock[slotId] > 0)
|
||||
&& container.canBuy(slotId, player)) {
|
||||
playerBalance -= cost;
|
||||
if (stockEnabled && availableStock[slotId] < Integer.MAX_VALUE) {
|
||||
availableStock[slotId]--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.handleMouseClick(slot, slotId, mouseButton, modifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side mirror of ContainerNPCTrader.canGivePlayer.
|
||||
* Checks if the item can be added to the player's cursor stack.
|
||||
*/
|
||||
private boolean canGivePlayerClient(ItemStack item) {
|
||||
ItemStack held = player.inventory.getItemStack();
|
||||
if (held == null) {
|
||||
return true;
|
||||
}
|
||||
if (NoppesUtilPlayer.compareItems(held, item, false, false)) {
|
||||
int k1 = item.stackSize;
|
||||
if (k1 > 0 && k1 + held.stackSize <= held.getMaxStackSize()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void save() {
|
||||
}
|
||||
|
||||
@@ -125,7 +125,13 @@ public enum EnumScriptType {
|
||||
ABILITY_INTERRUPT("abilityInterrupt"),
|
||||
ABILITY_COMPLETE("abilityComplete"),
|
||||
ABILITY_HIT("abilityHit"),
|
||||
ABILITY_TICK("abilityTick");
|
||||
ABILITY_TICK("abilityTick"),
|
||||
|
||||
ENERGY_PROJECTILE_FIRED("energyProjectileFired"),
|
||||
ENERGY_PROJECTILE_TICK("energyProjectileTick"),
|
||||
ENERGY_PROJECTILE_ENTITY_IMPACT("energyProjectileEntityImpact"),
|
||||
ENERGY_PROJECTILE_BLOCK_IMPACT("energyProjectileBlockImpact"),
|
||||
ENERGY_PROJECTILE_EXPIRED("energyProjectileExpired");
|
||||
|
||||
public String function;
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package noppes.npcs.constants;
|
||||
|
||||
import noppes.npcs.api.event.IAbilityEvent;
|
||||
import noppes.npcs.api.event.IAnimationEvent;
|
||||
import noppes.npcs.api.event.IBlockEvent;
|
||||
import noppes.npcs.api.event.ICustomGuiEvent;
|
||||
import noppes.npcs.api.event.IDialogEvent;
|
||||
import noppes.npcs.api.event.IEnergyProjectileEvent;
|
||||
import noppes.npcs.api.event.IFactionEvent;
|
||||
import noppes.npcs.api.event.IForgeEvent;
|
||||
import noppes.npcs.api.event.IItemEvent;
|
||||
import noppes.npcs.api.event.INpcEvent;
|
||||
import noppes.npcs.api.event.IPartyEvent;
|
||||
import noppes.npcs.api.event.IPlayerAbilityEvent;
|
||||
import noppes.npcs.api.event.IPlayerEvent;
|
||||
import noppes.npcs.api.event.IProjectileEvent;
|
||||
import noppes.npcs.api.event.IQuestEvent;
|
||||
@@ -52,7 +55,10 @@ public class ScriptContext {
|
||||
|
||||
public static final ScriptContext NPC = register("NPC", "npc",
|
||||
INpcEvent.class,
|
||||
IProjectileEvent.class
|
||||
IProjectileEvent.class,
|
||||
IEnergyProjectileEvent.class,
|
||||
IAbilityEvent.class,
|
||||
IAnimationEvent.class
|
||||
);
|
||||
|
||||
public static final ScriptContext PLAYER = register("PLAYER", "player",
|
||||
@@ -62,7 +68,9 @@ public class ScriptContext {
|
||||
IDialogEvent.class,
|
||||
IQuestEvent.class,
|
||||
IFactionEvent.class,
|
||||
ICustomGuiEvent.class
|
||||
ICustomGuiEvent.class,
|
||||
IEnergyProjectileEvent.class,
|
||||
IPlayerAbilityEvent.class
|
||||
);
|
||||
|
||||
public static final ScriptContext BLOCK = register("BLOCK", "block",
|
||||
|
||||
@@ -2,10 +2,12 @@ package noppes.npcs.controllers;
|
||||
|
||||
import noppes.npcs.api.event.IAbilityEvent;
|
||||
import noppes.npcs.api.event.IAnimationEvent;
|
||||
import noppes.npcs.api.event.IPlayerAbilityEvent;
|
||||
import noppes.npcs.api.event.IBlockEvent;
|
||||
import noppes.npcs.api.event.ICustomGuiEvent;
|
||||
import noppes.npcs.api.event.ICustomNPCsEvent;
|
||||
import noppes.npcs.api.event.IDialogEvent;
|
||||
import noppes.npcs.api.event.IEnergyProjectileEvent;
|
||||
import noppes.npcs.api.event.IFactionEvent;
|
||||
import noppes.npcs.api.event.IForgeEvent;
|
||||
import noppes.npcs.api.event.IItemEvent;
|
||||
@@ -106,6 +108,13 @@ public class ScriptHookController implements IScriptHookHandler {
|
||||
hook(NPC, PROJECTILE_TICK, IProjectileEvent.UpdateEvent.class);
|
||||
hook(NPC, PROJECTILE_IMPACT, IProjectileEvent.ImpactEvent.class);
|
||||
|
||||
// Energy Projectile
|
||||
hook(NPC, ENERGY_PROJECTILE_FIRED, IEnergyProjectileEvent.FiredEvent.class);
|
||||
hook(NPC, ENERGY_PROJECTILE_TICK, IEnergyProjectileEvent.UpdateEvent.class);
|
||||
hook(NPC, ENERGY_PROJECTILE_ENTITY_IMPACT, IEnergyProjectileEvent.EntityImpactEvent.class);
|
||||
hook(NPC, ENERGY_PROJECTILE_BLOCK_IMPACT, IEnergyProjectileEvent.BlockImpactEvent.class);
|
||||
hook(NPC, ENERGY_PROJECTILE_EXPIRED, IEnergyProjectileEvent.ExpiredEvent.class);
|
||||
|
||||
// Ability
|
||||
hook(NPC, ABILITY_START, IAbilityEvent.StartEvent.class);
|
||||
hook(NPC, ABILITY_EXECUTE, IAbilityEvent.ExecuteEvent.class);
|
||||
@@ -113,6 +122,12 @@ public class ScriptHookController implements IScriptHookHandler {
|
||||
hook(NPC, ABILITY_TICK, IAbilityEvent.TickEvent.class);
|
||||
hook(NPC, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class);
|
||||
hook(NPC, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class);
|
||||
|
||||
// Animation
|
||||
hook(NPC, ANIMATION_START, IAnimationEvent.Started.class);
|
||||
hook(NPC, ANIMATION_END, IAnimationEvent.Ended.class);
|
||||
hook(NPC, ANIMATION_FRAME_ENTER, IAnimationEvent.IFrameEvent.Entered.class);
|
||||
hook(NPC, ANIMATION_FRAME_EXIT, IAnimationEvent.IFrameEvent.Exited.class);
|
||||
}
|
||||
|
||||
private void initializePlayerHooks() {
|
||||
@@ -208,6 +223,21 @@ public class ScriptHookController implements IScriptHookHandler {
|
||||
hook(PLAYER, ANIMATION_FRAME_ENTER, IAnimationEvent.IFrameEvent.Entered.class);
|
||||
hook(PLAYER, ANIMATION_FRAME_EXIT, IAnimationEvent.IFrameEvent.Exited.class);
|
||||
|
||||
// Energy Projectile
|
||||
hook(PLAYER, ENERGY_PROJECTILE_FIRED, IEnergyProjectileEvent.FiredEvent.class);
|
||||
hook(PLAYER, ENERGY_PROJECTILE_TICK, IEnergyProjectileEvent.UpdateEvent.class);
|
||||
hook(PLAYER, ENERGY_PROJECTILE_ENTITY_IMPACT, IEnergyProjectileEvent.EntityImpactEvent.class);
|
||||
hook(PLAYER, ENERGY_PROJECTILE_BLOCK_IMPACT, IEnergyProjectileEvent.BlockImpactEvent.class);
|
||||
hook(PLAYER, ENERGY_PROJECTILE_EXPIRED, IEnergyProjectileEvent.ExpiredEvent.class);
|
||||
|
||||
// Ability
|
||||
hook(PLAYER, ABILITY_START, IPlayerAbilityEvent.StartEvent.class);
|
||||
hook(PLAYER, ABILITY_EXECUTE, IPlayerAbilityEvent.ExecuteEvent.class);
|
||||
hook(PLAYER, ABILITY_HIT, IPlayerAbilityEvent.HitEvent.class);
|
||||
hook(PLAYER, ABILITY_TICK, IPlayerAbilityEvent.TickEvent.class);
|
||||
hook(PLAYER, ABILITY_INTERRUPT, IPlayerAbilityEvent.InterruptEvent.class);
|
||||
hook(PLAYER, ABILITY_COMPLETE, IPlayerAbilityEvent.CompleteEvent.class);
|
||||
|
||||
// Profile
|
||||
hook(PLAYER, PROFILE_CHANGE, IPlayerEvent.ProfileEvent.Changed.class);
|
||||
hook(PLAYER, PROFILE_REMOVE, IPlayerEvent.ProfileEvent.Removed.class);
|
||||
|
||||
@@ -15,14 +15,21 @@ import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import net.minecraft.nbt.NBTTagString;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import noppes.npcs.LogWriter;
|
||||
import noppes.npcs.EventHooks;
|
||||
import noppes.npcs.api.ability.IPlayerAbilityData;
|
||||
import noppes.npcs.api.entity.IPlayer;
|
||||
import noppes.npcs.controllers.AnimationController;
|
||||
import noppes.npcs.controllers.ScriptController;
|
||||
import noppes.npcs.controllers.data.Animation;
|
||||
import noppes.npcs.scripted.NpcAPI;
|
||||
import noppes.npcs.scripted.event.player.PlayerAbilityEvent;
|
||||
|
||||
import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket;
|
||||
import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -54,6 +61,13 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
*/
|
||||
private int selectedIndex = 0;
|
||||
|
||||
/**
|
||||
* Tracks whether the current animation was started by an ability.
|
||||
* Persisted to NBT so orphaned ability animations can be detected and
|
||||
* cleared on login (ability state is transient, but animation state is not).
|
||||
*/
|
||||
private boolean playingAbilityAnimation = false;
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// RUNTIME STATE (not saved)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -147,6 +161,16 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
// Tick advances time and possibly changes phase
|
||||
boolean phaseChanged = currentAbility.tick();
|
||||
|
||||
// Fire tick event for scripts
|
||||
firePlayerTickEvent(player, currentAbility, currentTarget);
|
||||
|
||||
// Fire extender tick hook (e.g., per-tick resource drain)
|
||||
if (!AbilityController.Instance.fireOnAbilityTick(currentAbility, player, currentTarget,
|
||||
currentAbility.getPhase(), currentAbility.getCurrentTick())) {
|
||||
interruptCurrentAbility();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (currentAbility.getPhase()) {
|
||||
case WINDUP:
|
||||
if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) {
|
||||
@@ -197,6 +221,11 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
playAbilitySound(player, currentAbility.getActiveSound());
|
||||
playAbilityAnimation(currentAbility.getActiveAnimation());
|
||||
|
||||
// Fire execute event (cancelable)
|
||||
if (firePlayerExecuteEvent(player, currentAbility, currentTarget)) {
|
||||
return; // Cancelled
|
||||
}
|
||||
|
||||
// Call onExecute
|
||||
currentAbility.onExecute(player, currentTarget);
|
||||
|
||||
@@ -213,7 +242,7 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
}
|
||||
|
||||
// Auto-complete for burst overlap mode (entities fly independently)
|
||||
if (currentAbility.isBurstEnabled() && currentAbility.isBurstOverlap()
|
||||
if (currentAbility.isBurstEnabled()
|
||||
&& currentAbility.getBurstIndex() < currentAbility.getBurstAmount()
|
||||
&& currentAbility.getPhase() == AbilityPhase.ACTIVE
|
||||
&& currentAbility.isReadyForBurstCompletion(currentAbility.getCurrentTick())) {
|
||||
@@ -249,8 +278,14 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
private void handleAbilityCompletion(EntityPlayer player) {
|
||||
if (currentAbility == null) return;
|
||||
|
||||
// Fire extender complete hook
|
||||
AbilityController.Instance.fireOnAbilityComplete(currentAbility, player, currentTarget, false);
|
||||
|
||||
currentAbility.onComplete(player, currentTarget);
|
||||
|
||||
// Fire complete event
|
||||
firePlayerCompleteEvent(player, currentAbility, currentTarget);
|
||||
|
||||
// Release all locks
|
||||
releaseRotationControl();
|
||||
releaseLockedPosition();
|
||||
@@ -318,6 +353,16 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
// Check conditions (skip target-requiring ones)
|
||||
if (!ability.checkConditionsForPlayer(player)) return false;
|
||||
|
||||
// Fire extender start hook (e.g., resource cost checks)
|
||||
if (!AbilityController.Instance.fireOnAbilityStart(ability, player, null)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fire start event (cancelable)
|
||||
if (firePlayerStartEvent(player, ability)) {
|
||||
return false; // Cancelled
|
||||
}
|
||||
|
||||
// Start the ability
|
||||
currentAbility = ability;
|
||||
currentAbilityKey = key;
|
||||
@@ -358,6 +403,11 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
playAbilitySound(player, ability.getActiveSound());
|
||||
playAbilityAnimation(ability.getActiveAnimation());
|
||||
|
||||
// Fire execute event (cancelable)
|
||||
if (firePlayerExecuteEvent(player, ability, currentTarget)) {
|
||||
return; // Cancelled
|
||||
}
|
||||
|
||||
// Call onExecute
|
||||
ability.onExecute(player, currentTarget);
|
||||
|
||||
@@ -516,7 +566,17 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
}
|
||||
|
||||
public void interruptCurrentAbility() {
|
||||
interruptCurrentAbility(null, 0);
|
||||
}
|
||||
|
||||
public void interruptCurrentAbility(DamageSource source, float damage) {
|
||||
if (currentAbility != null && currentAbility.isExecuting()) {
|
||||
// Fire extender complete hook (interrupted)
|
||||
AbilityController.Instance.fireOnAbilityComplete(currentAbility, playerData.player, currentTarget, true);
|
||||
|
||||
// Fire interrupt event
|
||||
firePlayerInterruptEvent(playerData.player, currentAbility, currentTarget, source, damage);
|
||||
|
||||
currentAbility.interrupt();
|
||||
stopAbilityAnimation();
|
||||
releaseRotationControl();
|
||||
@@ -569,7 +629,7 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
|
||||
// Check for ability interruption
|
||||
if (currentAbility != null && currentAbility.canInterrupt(source)) {
|
||||
interruptCurrentAbility();
|
||||
interruptCurrentAbility(source, amount);
|
||||
}
|
||||
|
||||
return amount;
|
||||
@@ -674,6 +734,7 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
player.prevPosY = lockedPosY;
|
||||
player.prevPosZ = lockedPosZ;
|
||||
player.motionX = 0;
|
||||
player.motionY = 0; // Full vertical freeze for position lock
|
||||
player.motionZ = 0;
|
||||
}
|
||||
|
||||
@@ -761,6 +822,7 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
playerData.animationData.setEnabled(true);
|
||||
playerData.animationData.setAnimation(animation);
|
||||
playerData.animationData.updateClient();
|
||||
playingAbilityAnimation = true;
|
||||
}
|
||||
|
||||
public void playAbilityAnimation(int animation) {
|
||||
@@ -782,6 +844,23 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
private void stopAbilityAnimation() {
|
||||
playerData.animationData.setAnimation(null);
|
||||
playerData.animationData.updateClient();
|
||||
playingAbilityAnimation = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if an ability-driven animation is currently playing.
|
||||
* Used by PlayerData.onLogin() to detect orphaned ability animations.
|
||||
*/
|
||||
public boolean isPlayingAbilityAnimation() {
|
||||
return playingAbilityAnimation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears an orphaned ability animation (e.g. after relog when the ability is gone).
|
||||
* Stops the animation and resets the tracking flag.
|
||||
*/
|
||||
public void clearOrphanedAbilityAnimation() {
|
||||
stopAbilityAnimation();
|
||||
}
|
||||
|
||||
private void playAbilitySound(EntityPlayer player, String sound) {
|
||||
@@ -790,6 +869,73 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// SCRIPT EVENTS
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* Fire the ability start event. Returns true if cancelled.
|
||||
*/
|
||||
private boolean firePlayerStartEvent(EntityPlayer player, Ability ability) {
|
||||
if (ScriptController.Instance == null) return false;
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
||||
if (handler == null) return false;
|
||||
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
||||
PlayerAbilityEvent.StartEvent event = new PlayerAbilityEvent.StartEvent(iPlayer, ability, null);
|
||||
return EventHooks.onPlayerAbilityStart(handler, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the ability execute event. Returns true if cancelled.
|
||||
*/
|
||||
private boolean firePlayerExecuteEvent(EntityPlayer player, Ability ability, EntityLivingBase target) {
|
||||
if (ScriptController.Instance == null) return false;
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
||||
if (handler == null) return false;
|
||||
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
||||
PlayerAbilityEvent.ExecuteEvent event = new PlayerAbilityEvent.ExecuteEvent(iPlayer, ability, target);
|
||||
return EventHooks.onPlayerAbilityExecute(handler, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the ability tick event.
|
||||
*/
|
||||
private void firePlayerTickEvent(EntityPlayer player, Ability ability, EntityLivingBase target) {
|
||||
if (ScriptController.Instance == null) return;
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
||||
if (handler == null) return;
|
||||
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
||||
PlayerAbilityEvent.TickEvent event = new PlayerAbilityEvent.TickEvent(
|
||||
iPlayer, ability, target, ability.getPhase().ordinal(), ability.getCurrentTick());
|
||||
EventHooks.onPlayerAbilityTick(handler, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the ability interrupt event.
|
||||
*/
|
||||
private void firePlayerInterruptEvent(EntityPlayer player, Ability ability, EntityLivingBase target,
|
||||
DamageSource source, float damage) {
|
||||
if (ScriptController.Instance == null) return;
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
||||
if (handler == null) return;
|
||||
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
||||
PlayerAbilityEvent.InterruptEvent event = new PlayerAbilityEvent.InterruptEvent(
|
||||
iPlayer, ability, target, source, damage);
|
||||
EventHooks.onPlayerAbilityInterrupt(handler, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the ability complete event.
|
||||
*/
|
||||
private void firePlayerCompleteEvent(EntityPlayer player, Ability ability, EntityLivingBase target) {
|
||||
if (ScriptController.Instance == null) return;
|
||||
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player);
|
||||
if (handler == null) return;
|
||||
IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player);
|
||||
PlayerAbilityEvent.CompleteEvent event = new PlayerAbilityEvent.CompleteEvent(iPlayer, ability, target);
|
||||
EventHooks.onPlayerAbilityComplete(handler, event);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// NBT SERIALIZATION
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
@@ -801,6 +947,7 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
}
|
||||
compound.setTag("PlayerAbilities", list);
|
||||
compound.setInteger("PlayerAbilitySelected", selectedIndex);
|
||||
compound.setBoolean("AbilityAnimating", playingAbilityAnimation);
|
||||
}
|
||||
|
||||
public void readFromNBT(NBTTagCompound compound) {
|
||||
@@ -815,8 +962,30 @@ public class PlayerAbilityData implements IPlayerAbilityData {
|
||||
}
|
||||
}
|
||||
selectedIndex = compound.getInteger("PlayerAbilitySelected");
|
||||
|
||||
// Prune abilities that no longer exist (deleted custom abilities or removed built-ins)
|
||||
validateUnlockedAbilities();
|
||||
|
||||
if (selectedIndex >= unlockedAbilities.size()) {
|
||||
selectedIndex = Math.max(0, unlockedAbilities.size() - 1);
|
||||
}
|
||||
playingAbilityAnimation = compound.getBoolean("AbilityAnimating");
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove any unlocked ability keys that can no longer be resolved.
|
||||
* Called during load to clean up references to deleted abilities.
|
||||
*/
|
||||
private void validateUnlockedAbilities() {
|
||||
if (AbilityController.Instance == null) return;
|
||||
|
||||
Iterator<String> it = unlockedAbilities.iterator();
|
||||
while (it.hasNext()) {
|
||||
String key = it.next();
|
||||
if (!AbilityController.Instance.canResolveAbility(key)) {
|
||||
it.remove();
|
||||
LogWriter.info("Removed invalid ability reference from player data: " + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,13 +95,21 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData {
|
||||
private boolean specialKeyDown = false;
|
||||
|
||||
public void onLogin() {
|
||||
// Continue playing animation for self when re-logging
|
||||
// Handle animation state from previous session
|
||||
AnimationData animationData = this.animationData;
|
||||
if (animationData != null && animationData.isClientAnimating()) {
|
||||
Animation currentAnimation = animationData.currentClientAnimation;
|
||||
NBTTagCompound compound = currentAnimation.writeToNBT();
|
||||
animationData.viewAnimation(currentAnimation, animationData, compound,
|
||||
animationData.isClientAnimating(), currentAnimation.currentFrame, currentAnimation.currentFrameTime);
|
||||
if (abilityData.isPlayingAbilityAnimation()) {
|
||||
// Ability animation was playing when player disconnected.
|
||||
// Ability state is transient (lost on disconnect), so this animation
|
||||
// is orphaned - clear it instead of replaying a stuck animation.
|
||||
abilityData.clearOrphanedAbilityAnimation();
|
||||
} else {
|
||||
// Non-ability animation (e.g. script-driven) - continue playing
|
||||
Animation currentAnimation = animationData.currentClientAnimation;
|
||||
NBTTagCompound compound = currentAnimation.writeToNBT();
|
||||
animationData.viewAnimation(currentAnimation, animationData, compound,
|
||||
animationData.isClientAnimating(), currentAnimation.currentFrame, currentAnimation.currentFrameTime);
|
||||
}
|
||||
}
|
||||
|
||||
CustomEffectController controller = CustomEffectController.getInstance();
|
||||
@@ -116,6 +124,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData {
|
||||
}
|
||||
|
||||
public void onLogout() {
|
||||
// Cancel any executing ability and release all locks
|
||||
abilityData.interruptCurrentAbility();
|
||||
this.partyInvites.clear();
|
||||
this.actionManager.clear();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,10 @@ import kamkeel.npcs.controllers.AttributeController;
|
||||
import kamkeel.npcs.controllers.ProfileController;
|
||||
import kamkeel.npcs.controllers.TelegraphController;
|
||||
import kamkeel.npcs.controllers.data.ability.AbilityController;
|
||||
import kamkeel.npcs.entity.EntityAbilityBeam;
|
||||
import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import kamkeel.npcs.entity.EntityAbilityLaser;
|
||||
import kamkeel.npcs.entity.EntityAbilityOrb;
|
||||
import net.minecraft.block.Block;
|
||||
import net.minecraft.command.CommandHandler;
|
||||
import net.minecraft.entity.Entity;
|
||||
@@ -119,6 +123,10 @@ import noppes.npcs.items.ItemScripted;
|
||||
import noppes.npcs.scripted.entity.ScriptAnimal;
|
||||
import noppes.npcs.scripted.entity.ScriptArrow;
|
||||
import noppes.npcs.scripted.entity.ScriptDBCPlayer;
|
||||
import noppes.npcs.scripted.entity.ScriptEnergyBeam;
|
||||
import noppes.npcs.scripted.entity.ScriptEnergyDisc;
|
||||
import noppes.npcs.scripted.entity.ScriptEnergyLaser;
|
||||
import noppes.npcs.scripted.entity.ScriptEnergyOrb;
|
||||
import noppes.npcs.scripted.entity.ScriptEntity;
|
||||
import noppes.npcs.scripted.entity.ScriptEntityItem;
|
||||
import noppes.npcs.scripted.entity.ScriptFishHook;
|
||||
@@ -454,6 +462,14 @@ public class NpcAPI extends AbstractNpcAPI {
|
||||
data = new ScriptEntityData(new ScriptArrow<>((EntityArrow) entity));
|
||||
else if (entity instanceof EntityFishHook)
|
||||
data = new ScriptEntityData(new ScriptFishHook<>((EntityFishHook) entity));
|
||||
else if (entity instanceof EntityAbilityOrb)
|
||||
data = new ScriptEntityData(new ScriptEnergyOrb<>((EntityAbilityOrb) entity));
|
||||
else if (entity instanceof EntityAbilityBeam)
|
||||
data = new ScriptEntityData(new ScriptEnergyBeam<>((EntityAbilityBeam) entity));
|
||||
else if (entity instanceof EntityAbilityDisc)
|
||||
data = new ScriptEntityData(new ScriptEnergyDisc<>((EntityAbilityDisc) entity));
|
||||
else if (entity instanceof EntityAbilityLaser)
|
||||
data = new ScriptEntityData(new ScriptEnergyLaser<>((EntityAbilityLaser) entity));
|
||||
else
|
||||
data = new ScriptEntityData(new ScriptEntity<>(entity));
|
||||
entity.registerExtendedProperties("ScriptedObject", data);
|
||||
|
||||
@@ -21,6 +21,10 @@ import net.minecraftforge.common.MinecraftForge;
|
||||
import net.minecraftforge.common.util.BlockSnapshot;
|
||||
import net.minecraftforge.common.util.FakePlayer;
|
||||
import net.minecraftforge.event.world.BlockEvent;
|
||||
import kamkeel.npcs.entity.EntityAbilityBeam;
|
||||
import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import kamkeel.npcs.entity.EntityAbilityLaser;
|
||||
import kamkeel.npcs.entity.EntityAbilityOrb;
|
||||
import noppes.npcs.CustomNpcs;
|
||||
import noppes.npcs.NoppesUtilServer;
|
||||
import noppes.npcs.api.IBlock;
|
||||
@@ -28,6 +32,10 @@ import noppes.npcs.api.IParticle;
|
||||
import noppes.npcs.api.IPos;
|
||||
import noppes.npcs.api.ITileEntity;
|
||||
import noppes.npcs.api.IWorld;
|
||||
import noppes.npcs.api.entity.IEnergyBeam;
|
||||
import noppes.npcs.api.entity.IEnergyDisc;
|
||||
import noppes.npcs.api.entity.IEnergyLaser;
|
||||
import noppes.npcs.api.entity.IEnergyOrb;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
import noppes.npcs.api.entity.IPlayer;
|
||||
import noppes.npcs.api.handler.data.ISound;
|
||||
@@ -1242,6 +1250,48 @@ public class ScriptWorld implements IWorld {
|
||||
return "DIM" + this.getDimensionID();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IEnergyOrb createEnergyOrb(IEntity owner, double x, double y, double z, float size) {
|
||||
EntityAbilityOrb entity = new EntityAbilityOrb(world);
|
||||
entity.setPosition(x, y, z);
|
||||
entity.setStartPosition(x, y, z);
|
||||
entity.setOwnerEntityId(owner.getMCEntity().getEntityId());
|
||||
entity.setProjectileSize(size);
|
||||
return (IEnergyOrb) NpcAPI.Instance().getIEntity(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IEnergyBeam createEnergyBeam(IEntity owner, double x, double y, double z, float beamWidth, float headSize) {
|
||||
EntityAbilityBeam entity = new EntityAbilityBeam(world);
|
||||
entity.setPosition(x, y, z);
|
||||
entity.setStartPosition(x, y, z);
|
||||
entity.setOwnerEntityId(owner.getMCEntity().getEntityId());
|
||||
entity.setBeamWidth(beamWidth);
|
||||
entity.setHeadSize(headSize);
|
||||
return (IEnergyBeam) NpcAPI.Instance().getIEntity(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IEnergyDisc createEnergyDisc(IEntity owner, double x, double y, double z, float radius, float thickness) {
|
||||
EntityAbilityDisc entity = new EntityAbilityDisc(world);
|
||||
entity.setPosition(x, y, z);
|
||||
entity.setStartPosition(x, y, z);
|
||||
entity.setOwnerEntityId(owner.getMCEntity().getEntityId());
|
||||
entity.setDiscRadius(radius);
|
||||
entity.setDiscThickness(thickness);
|
||||
return (IEnergyDisc) NpcAPI.Instance().getIEntity(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IEnergyLaser createEnergyLaser(IEntity owner, double x, double y, double z, float laserWidth) {
|
||||
EntityAbilityLaser entity = new EntityAbilityLaser(world);
|
||||
entity.setPosition(x, y, z);
|
||||
entity.setStartPosition(x, y, z);
|
||||
entity.setOwnerEntityId(owner.getMCEntity().getEntityId());
|
||||
entity.setLaserWidth(laserWidth);
|
||||
return (IEnergyLaser) NpcAPI.Instance().getIEntity(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void broadcast(String message) {
|
||||
world.func_73046_m().getConfigurationManager().sendChatMsg(new ChatComponentText(message));
|
||||
|
||||
@@ -15,4 +15,8 @@ public class EntityType {
|
||||
public static final int THROWABLE = 11;
|
||||
public static final int FISH_HOOK = 12;
|
||||
public static final int ENERGY_PROJECTILE = 13;
|
||||
public static final int ENERGY_ORB = 14;
|
||||
public static final int ENERGY_BEAM = 15;
|
||||
public static final int ENERGY_DISC = 16;
|
||||
public static final int ENERGY_LASER = 17;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package noppes.npcs.scripted.entity;
|
||||
|
||||
import kamkeel.npcs.entity.EntityAbilityBeam;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import noppes.npcs.api.entity.IEnergyBeam;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
import noppes.npcs.scripted.constants.EntityType;
|
||||
|
||||
public class ScriptEnergyBeam<T extends EntityAbilityBeam> extends ScriptEnergyProjectile<T> implements IEnergyBeam {
|
||||
|
||||
public ScriptEnergyBeam(T entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return EntityType.ENERGY_BEAM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean typeOf(int type) {
|
||||
return type == EntityType.ENERGY_BEAM || super.typeOf(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnergyType() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ==================== BEAM-SPECIFIC ====================
|
||||
|
||||
public float getBeamWidth() { return entity.getBeamWidth(); }
|
||||
public void setBeamWidth(float width) { entity.setBeamWidth(width); }
|
||||
|
||||
public float getHeadSize() { return entity.getHeadSize(); }
|
||||
public void setHeadSize(float size) { entity.setHeadSize(size); }
|
||||
|
||||
public boolean isAttachedToOwner() { return entity.isAttachedToOwner(); }
|
||||
public void setAttachedToOwner(boolean attached) { entity.setAttachedToOwner(attached); }
|
||||
|
||||
public boolean shouldRenderTailOrb() { return entity.shouldRenderTailOrb(); }
|
||||
|
||||
// ==================== FIRE ====================
|
||||
|
||||
@Override
|
||||
public void fire(IEntity target) {
|
||||
ensureSpawned();
|
||||
EntityLivingBase mcTarget = null;
|
||||
if (target != null && target.getMCEntity() instanceof EntityLivingBase) {
|
||||
mcTarget = (EntityLivingBase) target.getMCEntity();
|
||||
}
|
||||
entity.startFiring(mcTarget);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire(double x, double y, double z) {
|
||||
ensureSpawned();
|
||||
double dx = x - entity.posX;
|
||||
double dy = y - entity.posY;
|
||||
double dz = z - entity.posZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len > 0) {
|
||||
entity.motionX = (dx / len) * entity.getSpeed();
|
||||
entity.motionY = (dy / len) * entity.getSpeed();
|
||||
entity.motionZ = (dz / len) * entity.getSpeed();
|
||||
}
|
||||
entity.startFiring(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package noppes.npcs.scripted.entity;
|
||||
|
||||
import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import noppes.npcs.api.entity.IEnergyDisc;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
import noppes.npcs.scripted.constants.EntityType;
|
||||
|
||||
public class ScriptEnergyDisc<T extends EntityAbilityDisc> extends ScriptEnergyProjectile<T> implements IEnergyDisc {
|
||||
|
||||
public ScriptEnergyDisc(T entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return EntityType.ENERGY_DISC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean typeOf(int type) {
|
||||
return type == EntityType.ENERGY_DISC || super.typeOf(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnergyType() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
// ==================== DISC-SPECIFIC ====================
|
||||
|
||||
public float getDiscRadius() { return entity.getDiscRadius(); }
|
||||
public void setDiscRadius(float radius) { entity.setDiscRadius(radius); }
|
||||
|
||||
public float getDiscThickness() { return entity.getDiscThickness(); }
|
||||
public void setDiscThickness(float thickness) { entity.setDiscThickness(thickness); }
|
||||
|
||||
public boolean isVertical() { return entity.isVertical(); }
|
||||
public void setVertical(boolean vertical) { entity.setVertical(vertical); }
|
||||
|
||||
public boolean isBoomerang() { return entity.isBoomerang(); }
|
||||
public void setBoomerang(boolean boomerang) { entity.setBoomerang(boomerang); }
|
||||
|
||||
public int getBoomerangDelay() { return entity.getBoomerangDelay(); }
|
||||
public void setBoomerangDelay(int ticks) { entity.setBoomerangDelay(ticks); }
|
||||
|
||||
public boolean isReturning() { return entity.isReturning(); }
|
||||
|
||||
// ==================== FIRE ====================
|
||||
|
||||
@Override
|
||||
public void fire(IEntity target) {
|
||||
ensureSpawned();
|
||||
EntityLivingBase mcTarget = null;
|
||||
if (target != null && target.getMCEntity() instanceof EntityLivingBase) {
|
||||
mcTarget = (EntityLivingBase) target.getMCEntity();
|
||||
}
|
||||
entity.startMoving(mcTarget);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire(double x, double y, double z) {
|
||||
ensureSpawned();
|
||||
double dx = x - entity.posX;
|
||||
double dy = y - entity.posY;
|
||||
double dz = z - entity.posZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len > 0) {
|
||||
entity.motionX = (dx / len) * entity.getSpeed();
|
||||
entity.motionY = (dy / len) * entity.getSpeed();
|
||||
entity.motionZ = (dz / len) * entity.getSpeed();
|
||||
}
|
||||
entity.startMoving(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package noppes.npcs.scripted.entity;
|
||||
|
||||
import kamkeel.npcs.entity.EntityAbilityLaser;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import noppes.npcs.api.entity.IEnergyLaser;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
import noppes.npcs.scripted.constants.EntityType;
|
||||
|
||||
public class ScriptEnergyLaser<T extends EntityAbilityLaser> extends ScriptEnergyProjectile<T> implements IEnergyLaser {
|
||||
|
||||
public ScriptEnergyLaser(T entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return EntityType.ENERGY_LASER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean typeOf(int type) {
|
||||
return type == EntityType.ENERGY_LASER || super.typeOf(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnergyType() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
// ==================== LASER-SPECIFIC ====================
|
||||
|
||||
public float getLaserWidth() { return entity.getLaserWidth(); }
|
||||
public void setLaserWidth(float width) { entity.setLaserWidth(width); }
|
||||
|
||||
public float getExpansionSpeed() { return entity.getExpansionSpeed(); }
|
||||
public void setExpansionSpeed(float speed) { entity.setExpansionSpeed(speed); }
|
||||
|
||||
public int getLingerTicks() { return entity.getLingerTicks(); }
|
||||
public void setLingerTicks(int ticks) { entity.setLingerTicks(ticks); }
|
||||
|
||||
public float getCurrentLength() { return entity.getCurrentLength(); }
|
||||
public boolean isFullyExtended() { return entity.isFullyExtended(); }
|
||||
|
||||
public double getDirX() { return entity.getDirX(); }
|
||||
public double getDirY() { return entity.getDirY(); }
|
||||
public double getDirZ() { return entity.getDirZ(); }
|
||||
public void setDirection(double x, double y, double z) { entity.setDirection(x, y, z); }
|
||||
|
||||
public double getEndX() { return entity.getEndX(); }
|
||||
public double getEndY() { return entity.getEndY(); }
|
||||
public double getEndZ() { return entity.getEndZ(); }
|
||||
|
||||
public float getLingerAlpha() { return entity.getLingerAlpha(); }
|
||||
|
||||
public boolean isLockVerticalDirection() { return entity.isLockVerticalDirection(); }
|
||||
public void setLockVerticalDirection(boolean lock) { entity.setLockVerticalDirection(lock); }
|
||||
|
||||
// ==================== FIRE ====================
|
||||
|
||||
@Override
|
||||
public void fire(IEntity target) {
|
||||
ensureSpawned();
|
||||
EntityLivingBase mcTarget = null;
|
||||
if (target != null && target.getMCEntity() instanceof EntityLivingBase) {
|
||||
mcTarget = (EntityLivingBase) target.getMCEntity();
|
||||
}
|
||||
entity.startMoving(mcTarget);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire(double x, double y, double z) {
|
||||
ensureSpawned();
|
||||
double dx = x - entity.posX;
|
||||
double dy = y - entity.posY;
|
||||
double dz = z - entity.posZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len > 0) {
|
||||
entity.setDirection(dx / len, dy / len, dz / len);
|
||||
}
|
||||
entity.startMoving(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package noppes.npcs.scripted.entity;
|
||||
|
||||
import kamkeel.npcs.entity.EntityAbilityOrb;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import noppes.npcs.api.entity.IEnergyOrb;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
import noppes.npcs.scripted.constants.EntityType;
|
||||
|
||||
public class ScriptEnergyOrb<T extends EntityAbilityOrb> extends ScriptEnergyProjectile<T> implements IEnergyOrb {
|
||||
|
||||
public ScriptEnergyOrb(T entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return EntityType.ENERGY_ORB;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean typeOf(int type) {
|
||||
return type == EntityType.ENERGY_ORB || super.typeOf(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnergyType() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire(IEntity target) {
|
||||
ensureSpawned();
|
||||
EntityLivingBase mcTarget = null;
|
||||
if (target != null && target.getMCEntity() instanceof EntityLivingBase) {
|
||||
mcTarget = (EntityLivingBase) target.getMCEntity();
|
||||
}
|
||||
entity.startMoving(mcTarget);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fire(double x, double y, double z) {
|
||||
ensureSpawned();
|
||||
double dx = x - entity.posX;
|
||||
double dy = y - entity.posY;
|
||||
double dz = z - entity.posZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len > 0) {
|
||||
entity.motionX = (dx / len) * entity.getSpeed();
|
||||
entity.motionY = (dy / len) * entity.getSpeed();
|
||||
entity.motionZ = (dz / len) * entity.getSpeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package noppes.npcs.scripted.entity;
|
||||
|
||||
import kamkeel.npcs.entity.EntityAbilityProjectile;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import noppes.npcs.api.entity.IEnergyProjectile;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
import noppes.npcs.api.entity.IEntityLivingBase;
|
||||
import noppes.npcs.EventHooks;
|
||||
import noppes.npcs.scripted.NpcAPI;
|
||||
import noppes.npcs.scripted.constants.EntityType;
|
||||
|
||||
public class ScriptEnergyProjectile<T extends EntityAbilityProjectile> extends ScriptEntity<T> implements IEnergyProjectile {
|
||||
protected T entity;
|
||||
|
||||
public ScriptEnergyProjectile(T entity) {
|
||||
super(entity);
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
// ==================== TYPE ====================
|
||||
|
||||
@Override
|
||||
public int getType() {
|
||||
return EntityType.ENERGY_PROJECTILE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean typeOf(int type) {
|
||||
return type == EntityType.ENERGY_PROJECTILE || super.typeOf(type);
|
||||
}
|
||||
|
||||
public int getEnergyType() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ==================== OWNER & TARGET ====================
|
||||
|
||||
public int getOwnerEntityId() {
|
||||
return entity.getOwnerEntityId();
|
||||
}
|
||||
|
||||
public IEntity getOwner() {
|
||||
return entity.getOwner();
|
||||
}
|
||||
|
||||
public void setOwner(IEntity owner) {
|
||||
if (owner != null) {
|
||||
entity.setOwnerEntityId(owner.getMCEntity().getEntityId());
|
||||
}
|
||||
}
|
||||
|
||||
public int getTargetEntityId() {
|
||||
return entity.getTargetEntityId();
|
||||
}
|
||||
|
||||
public IEntity getTarget() {
|
||||
return entity.getTarget();
|
||||
}
|
||||
|
||||
public void setTarget(IEntity target) {
|
||||
if (target != null) {
|
||||
entity.setTargetEntityId(target.getMCEntity().getEntityId());
|
||||
} else {
|
||||
entity.setTargetEntityId(-1);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== SIZE ====================
|
||||
|
||||
public float getSize() {
|
||||
return entity.getSize();
|
||||
}
|
||||
|
||||
public void setSize(float size) {
|
||||
entity.setProjectileSize(size);
|
||||
}
|
||||
|
||||
// ==================== DISPLAY ====================
|
||||
|
||||
public int getInnerColor() { return entity.getInnerColor(); }
|
||||
public void setInnerColor(int color) { entity.setInnerColor(color); }
|
||||
|
||||
public int getOuterColor() { return entity.getOuterColor(); }
|
||||
public void setOuterColor(int color) { entity.setOuterColor(color); }
|
||||
|
||||
public boolean isOuterColorEnabled() { return entity.isOuterColorEnabled(); }
|
||||
public void setOuterColorEnabled(boolean enabled) { entity.setOuterColorEnabled(enabled); }
|
||||
|
||||
public float getOuterColorWidth() { return entity.getOuterColorWidth(); }
|
||||
public void setOuterColorWidth(float width) { entity.setOuterColorWidth(width); }
|
||||
|
||||
public float getOuterColorAlpha() { return entity.getOuterColorAlpha(); }
|
||||
public void setOuterColorAlpha(float alpha) { entity.setOuterColorAlpha(alpha); }
|
||||
|
||||
public float getRotationSpeed() { return entity.getRotationSpeed(); }
|
||||
public void setRotationSpeed(float speed) { entity.setRotationSpeed(speed); }
|
||||
|
||||
// ==================== INTERPOLATION ====================
|
||||
|
||||
public float getInterpolatedRotationX(float partialTicks) { return entity.getInterpolatedRotationX(partialTicks); }
|
||||
public float getInterpolatedRotationY(float partialTicks) { return entity.getInterpolatedRotationY(partialTicks); }
|
||||
public float getInterpolatedRotationZ(float partialTicks) { return entity.getInterpolatedRotationZ(partialTicks); }
|
||||
public float getInterpolatedSize(float partialTicks) { return entity.getInterpolatedSize(partialTicks); }
|
||||
|
||||
// ==================== LIGHTNING ====================
|
||||
|
||||
public boolean hasLightningEffect() { return entity.hasLightningEffect(); }
|
||||
public void setLightningEffect(boolean enabled) { entity.setLightningEffect(enabled); }
|
||||
|
||||
public float getLightningDensity() { return entity.getLightningDensity(); }
|
||||
public void setLightningDensity(float density) { entity.setLightningDensity(density); }
|
||||
|
||||
public float getLightningRadius() { return entity.getLightningRadius(); }
|
||||
public void setLightningRadius(float radius) { entity.setLightningRadius(radius); }
|
||||
|
||||
public int getLightningFadeTime() { return entity.getLightningFadeTime(); }
|
||||
public void setLightningFadeTime(int ticks) { entity.setLightningFadeTime(ticks); }
|
||||
|
||||
// ==================== LIFESPAN ====================
|
||||
|
||||
public float getMaxDistance() { return entity.getMaxDistance(); }
|
||||
public void setMaxDistance(float distance) { entity.setMaxDistance(distance); }
|
||||
|
||||
public int getMaxLifetime() { return entity.getMaxLifetime(); }
|
||||
public void setMaxLifetime(int ticks) { entity.setMaxLifetime(ticks); }
|
||||
|
||||
// ==================== COMBAT ====================
|
||||
|
||||
public float getDamage() { return entity.getDamage(); }
|
||||
public void setDamage(float damage) { entity.setCombatDamage(damage); }
|
||||
|
||||
public float getKnockback() { return entity.getKnockback(); }
|
||||
public void setKnockback(float knockback) { entity.setCombatKnockback(knockback); }
|
||||
|
||||
public float getKnockbackUp() { return entity.getKnockbackUp(); }
|
||||
public void setKnockbackUp(float knockbackUp) { entity.setCombatKnockbackUp(knockbackUp); }
|
||||
|
||||
public boolean isExplosive() { return entity.isExplosive(); }
|
||||
public void setExplosive(boolean explosive) { entity.setExplosive(explosive); }
|
||||
|
||||
public float getExplosionRadius() { return entity.getExplosionRadius(); }
|
||||
public void setExplosionRadius(float radius) { entity.setExplosionRadius(radius); }
|
||||
|
||||
public float getExplosionDamageFalloff() { return entity.getExplosionDamageFalloff(); }
|
||||
public void setExplosionDamageFalloff(float falloff) { entity.setExplosionDamageFalloff(falloff); }
|
||||
|
||||
// ==================== MOVEMENT ====================
|
||||
|
||||
public float getSpeed() { return entity.getSpeed(); }
|
||||
public void setSpeed(float speed) { entity.setSpeed(speed); }
|
||||
|
||||
public boolean isHoming() { return entity.isHoming(); }
|
||||
public void setHoming(boolean homing) { entity.setHomingEnabled(homing); }
|
||||
|
||||
public float getHomingStrength() { return entity.getHomingStrength(); }
|
||||
public void setHomingStrength(float strength) { entity.setHomingStrength(strength); }
|
||||
|
||||
public float getHomingRange() { return entity.getHomingRange(); }
|
||||
public void setHomingRange(float range) { entity.setHomingRange(range); }
|
||||
|
||||
// ==================== ANCHOR ====================
|
||||
|
||||
public int getAnchor() { return entity.getAnchor(); }
|
||||
public float getAnchorOffsetX() { return entity.getAnchorOffsetX(); }
|
||||
public float getAnchorOffsetY() { return entity.getAnchorOffsetY(); }
|
||||
public float getAnchorOffsetZ() { return entity.getAnchorOffsetZ(); }
|
||||
|
||||
// ==================== POSITION ====================
|
||||
|
||||
public double getStartX() { return entity.getStartX(); }
|
||||
public double getStartY() { return entity.getStartY(); }
|
||||
public double getStartZ() { return entity.getStartZ(); }
|
||||
|
||||
// ==================== STATE ====================
|
||||
|
||||
public boolean hasHit() { return entity.getHasHit(); }
|
||||
public boolean isCharging() { return entity.isCharging(); }
|
||||
public float getChargeProgress() { return entity.getChargeProgress(); }
|
||||
|
||||
// ==================== FIRE ====================
|
||||
|
||||
public void fire(IEntity target) {
|
||||
if (target != null) {
|
||||
Entity mcTarget = target.getMCEntity();
|
||||
setMotionToward(mcTarget.posX, mcTarget.posY + mcTarget.getEyeHeight(), mcTarget.posZ);
|
||||
}
|
||||
ensureSpawned();
|
||||
}
|
||||
|
||||
public void fire(double x, double y, double z) {
|
||||
setMotionToward(x, y, z);
|
||||
ensureSpawned();
|
||||
}
|
||||
|
||||
public void fire(float yaw, float pitch) {
|
||||
float yawRad = (float) Math.toRadians(yaw);
|
||||
float pitchRad = (float) Math.toRadians(pitch);
|
||||
entity.motionX = -Math.sin(yawRad) * Math.cos(pitchRad) * entity.getSpeed();
|
||||
entity.motionY = -Math.sin(pitchRad) * entity.getSpeed();
|
||||
entity.motionZ = Math.cos(yawRad) * Math.cos(pitchRad) * entity.getSpeed();
|
||||
ensureSpawned();
|
||||
}
|
||||
|
||||
public void fire(IEntityLivingBase caster) {
|
||||
if (caster == null) return;
|
||||
initFromCaster(caster);
|
||||
fire(caster.getRotation(), caster.getPitch());
|
||||
}
|
||||
|
||||
public void fire(IEntityLivingBase caster, IEntity target) {
|
||||
if (caster == null) return;
|
||||
initFromCaster(caster);
|
||||
fire(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets owner from caster and positions projectile at caster's eye level.
|
||||
*/
|
||||
private void initFromCaster(IEntityLivingBase caster) {
|
||||
EntityLivingBase mc = (EntityLivingBase) caster.getMCEntity();
|
||||
entity.setOwnerEntityId(mc.getEntityId());
|
||||
double eyeY = mc.posY + mc.getEyeHeight();
|
||||
entity.setPosition(mc.posX, eyeY, mc.posZ);
|
||||
entity.setStartPosition(mc.posX, eyeY, mc.posZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates and sets motion from the projectile's current position toward a target point.
|
||||
*/
|
||||
private void setMotionToward(double x, double y, double z) {
|
||||
double dx = x - entity.posX;
|
||||
double dy = y - entity.posY;
|
||||
double dz = z - entity.posZ;
|
||||
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
if (len > 0) {
|
||||
entity.motionX = (dx / len) * entity.getSpeed();
|
||||
entity.motionY = (dy / len) * entity.getSpeed();
|
||||
entity.motionZ = (dz / len) * entity.getSpeed();
|
||||
}
|
||||
}
|
||||
|
||||
protected void ensureSpawned() {
|
||||
if (!entity.addedToChunk && entity.worldObj != null) {
|
||||
entity.worldObj.spawnEntityInWorld(entity);
|
||||
if (!entity.worldObj.isRemote) {
|
||||
EventHooks.onEnergyProjectileFired(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
package noppes.npcs.scripted.entity;
|
||||
|
||||
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.block.material.Material;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityList;
|
||||
@@ -526,6 +531,16 @@ public class ScriptEntity<T extends Entity> implements IEntity {
|
||||
cls = EntityMob.class;
|
||||
else if (type == EntityType.NPC)
|
||||
cls = EntityNPCInterface.class;
|
||||
else if (type == EntityType.ENERGY_PROJECTILE)
|
||||
cls = EntityAbilityProjectile.class;
|
||||
else if (type == EntityType.ENERGY_ORB)
|
||||
cls = EntityAbilityOrb.class;
|
||||
else if (type == EntityType.ENERGY_BEAM)
|
||||
cls = EntityAbilityBeam.class;
|
||||
else if (type == EntityType.ENERGY_DISC)
|
||||
cls = EntityAbilityDisc.class;
|
||||
else if (type == EntityType.ENERGY_LASER)
|
||||
cls = EntityAbilityLaser.class;
|
||||
|
||||
List<Entity> entities = entity.worldObj.getEntitiesWithinAABB(cls, entity.boundingBox.expand(range, range, range));
|
||||
List<IEntity> list = new ArrayList<IEntity>();
|
||||
|
||||
@@ -6,6 +6,7 @@ import net.minecraft.potion.Potion;
|
||||
import net.minecraft.potion.PotionEffect;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.util.EntityDamageSource;
|
||||
import net.minecraft.util.MathHelper;
|
||||
import net.minecraft.util.Vec3;
|
||||
import noppes.npcs.api.IBlock;
|
||||
import noppes.npcs.api.IDamageSource;
|
||||
@@ -380,4 +381,53 @@ public class ScriptLivingBase<T extends EntityLivingBase> extends ScriptEntity<T
|
||||
this.entity.hurtResistantTime = time;
|
||||
this.entity.hurtTime = time;
|
||||
}
|
||||
|
||||
public void applyKnockback(float strength, IEntity source) {
|
||||
double dirX = entity.posX - source.getX();
|
||||
double dirZ = entity.posZ - source.getZ();
|
||||
applyKnockbackInternal(strength, dirX, dirZ, false);
|
||||
}
|
||||
|
||||
public void applyKnockback(float strength, double dirX, double dirZ) {
|
||||
applyKnockbackInternal(strength, dirX, dirZ, false);
|
||||
}
|
||||
|
||||
public void forceKnockback(float strength, IEntity source) {
|
||||
double dirX = entity.posX - source.getX();
|
||||
double dirZ = entity.posZ - source.getZ();
|
||||
applyKnockbackInternal(strength, dirX, dirZ, true);
|
||||
}
|
||||
|
||||
public void forceKnockback(float strength, double dirX, double dirZ) {
|
||||
applyKnockbackInternal(strength, dirX, dirZ, true);
|
||||
}
|
||||
|
||||
private void applyKnockbackInternal(float strength, double dirX, double dirZ, boolean ignoreResistance) {
|
||||
if (!ignoreResistance) {
|
||||
double resistance = entity.getEntityAttribute(SharedMonsterAttributes.knockbackResistance).getAttributeValue();
|
||||
if (entity.worldObj.rand.nextDouble() < resistance) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
double len = MathHelper.sqrt_double(dirX * dirX + dirZ * dirZ);
|
||||
if (len <= 0.0D) {
|
||||
return;
|
||||
}
|
||||
|
||||
entity.isAirBorne = true;
|
||||
float base = 0.4F * strength;
|
||||
entity.motionX /= 2.0D;
|
||||
entity.motionY /= 2.0D;
|
||||
entity.motionZ /= 2.0D;
|
||||
entity.motionX -= (dirX / len) * base;
|
||||
entity.motionY += base;
|
||||
entity.motionZ -= (dirZ / len) * base;
|
||||
|
||||
if (entity.motionY > 0.4D) {
|
||||
entity.motionY = 0.4D;
|
||||
}
|
||||
|
||||
entity.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import noppes.npcs.api.IPos;
|
||||
import noppes.npcs.api.ITimers;
|
||||
import noppes.npcs.api.ability.IDataAbilities;
|
||||
import noppes.npcs.api.entity.ICustomNpc;
|
||||
import noppes.npcs.api.entity.IEnergyProjectile;
|
||||
import noppes.npcs.api.entity.IEntityLivingBase;
|
||||
import noppes.npcs.api.entity.IPlayer;
|
||||
import noppes.npcs.api.entity.data.IModelData;
|
||||
@@ -28,6 +29,7 @@ import noppes.npcs.constants.EnumMovingType;
|
||||
import noppes.npcs.constants.EnumNavType;
|
||||
import noppes.npcs.constants.EnumRoleType;
|
||||
import noppes.npcs.constants.EnumStandingType;
|
||||
import kamkeel.npcs.entity.EntityAbilityProjectile;
|
||||
import noppes.npcs.controllers.FactionController;
|
||||
import noppes.npcs.controllers.data.DialogOption;
|
||||
import noppes.npcs.controllers.data.Line;
|
||||
@@ -1463,4 +1465,13 @@ public class ScriptNpc<T extends EntityNPCInterface> extends ScriptLiving<T> imp
|
||||
public IDataAbilities getAbilityData() {
|
||||
return new ScriptDataAbilities(npc);
|
||||
}
|
||||
|
||||
public IEnergyProjectile[] getActiveEnergyProjectiles() {
|
||||
java.util.List<EntityAbilityProjectile> entities = EntityAbilityProjectile.getActiveProjectiles(npc.getEntityId());
|
||||
IEnergyProjectile[] result = new IEnergyProjectile[entities.size()];
|
||||
for (int i = 0; i < entities.size(); i++) {
|
||||
result[i] = (IEnergyProjectile) NpcAPI.Instance().getIEntity(entities.get(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package noppes.npcs.scripted.entity;
|
||||
|
||||
import kamkeel.npcs.controllers.AttributeController;
|
||||
import kamkeel.npcs.entity.EntityAbilityProjectile;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.packets.data.AchievementPacket;
|
||||
import kamkeel.npcs.network.packets.data.ChatAlertPacket;
|
||||
@@ -31,6 +32,7 @@ import noppes.npcs.api.IPos;
|
||||
import noppes.npcs.api.IScreenSize;
|
||||
import noppes.npcs.api.ITimers;
|
||||
import noppes.npcs.api.IWorld;
|
||||
import noppes.npcs.api.entity.IEnergyProjectile;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
import noppes.npcs.api.entity.IPlayer;
|
||||
import noppes.npcs.api.gui.ICustomGui;
|
||||
@@ -933,4 +935,13 @@ public class ScriptPlayer<T extends EntityPlayerMP> extends ScriptLivingBase<T>
|
||||
public String getFormattedCurrencyBalance() {
|
||||
return getData().tradeData.formatBalance();
|
||||
}
|
||||
|
||||
public IEnergyProjectile[] getActiveEnergyProjectiles() {
|
||||
List<EntityAbilityProjectile> entities = EntityAbilityProjectile.getActiveProjectiles(player.getEntityId());
|
||||
IEnergyProjectile[] result = new IEnergyProjectile[entities.size()];
|
||||
for (int i = 0; i < entities.size(); i++) {
|
||||
result[i] = (IEnergyProjectile) NpcAPI.Instance().getIEntity(entities.get(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package noppes.npcs.scripted.event;
|
||||
|
||||
import cpw.mods.fml.common.eventhandler.Cancelable;
|
||||
import noppes.npcs.api.IBlock;
|
||||
import noppes.npcs.api.entity.IEnergyProjectile;
|
||||
import noppes.npcs.api.entity.IEntity;
|
||||
import noppes.npcs.api.event.IEnergyProjectileEvent;
|
||||
import noppes.npcs.constants.EnumScriptType;
|
||||
|
||||
public class EnergyProjectileEvent extends CustomNPCsEvent implements IEnergyProjectileEvent {
|
||||
public final IEnergyProjectile projectile;
|
||||
public final IEntity owner;
|
||||
|
||||
public EnergyProjectileEvent(IEnergyProjectile projectile) {
|
||||
this.projectile = projectile;
|
||||
this.owner = projectile.getOwner();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IEnergyProjectile getProjectile() {
|
||||
return projectile;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IEntity getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return "energyProjectileEvent";
|
||||
}
|
||||
|
||||
/**
|
||||
* energyProjectileFired
|
||||
*/
|
||||
public static class FiredEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.FiredEvent {
|
||||
public FiredEvent(IEnergyProjectile projectile) {
|
||||
super(projectile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return EnumScriptType.ENERGY_PROJECTILE_FIRED.function;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* energyProjectileTick
|
||||
*/
|
||||
public static class UpdateEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.UpdateEvent {
|
||||
public UpdateEvent(IEnergyProjectile projectile) {
|
||||
super(projectile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return EnumScriptType.ENERGY_PROJECTILE_TICK.function;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* energyProjectileEntityImpact
|
||||
*/
|
||||
@Cancelable
|
||||
public static class EntityImpactEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.EntityImpactEvent {
|
||||
private final IEntity target;
|
||||
private float damage;
|
||||
|
||||
public EntityImpactEvent(IEnergyProjectile projectile, IEntity target, float damage) {
|
||||
super(projectile);
|
||||
this.target = target;
|
||||
this.damage = damage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IEntity getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getDamage() {
|
||||
return damage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDamage(float damage) {
|
||||
this.damage = damage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return EnumScriptType.ENERGY_PROJECTILE_ENTITY_IMPACT.function;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* energyProjectileBlockImpact
|
||||
*/
|
||||
public static class BlockImpactEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.BlockImpactEvent {
|
||||
private final IBlock block;
|
||||
|
||||
public BlockImpactEvent(IEnergyProjectile projectile, IBlock block) {
|
||||
super(projectile);
|
||||
this.block = block;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBlock getBlock() {
|
||||
return block;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return EnumScriptType.ENERGY_PROJECTILE_BLOCK_IMPACT.function;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* energyProjectileExpired
|
||||
*/
|
||||
public static class ExpiredEvent extends EnergyProjectileEvent implements IEnergyProjectileEvent.ExpiredEvent {
|
||||
public ExpiredEvent(IEnergyProjectile projectile) {
|
||||
super(projectile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return EnumScriptType.ENERGY_PROJECTILE_EXPIRED.function;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package noppes.npcs.scripted.event.player;
|
||||
import cpw.mods.fml.common.eventhandler.Cancelable;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import noppes.npcs.api.IDamageSource;
|
||||
import noppes.npcs.api.ability.IAbility;
|
||||
import noppes.npcs.api.entity.IEntityLivingBase;
|
||||
import noppes.npcs.api.entity.IPlayer;
|
||||
@@ -38,6 +40,108 @@ public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEve
|
||||
return "playerAbilityEvent";
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when a player's ability starts executing (enters WINDUP phase).
|
||||
*/
|
||||
@Cancelable
|
||||
public static class StartEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.StartEvent {
|
||||
public StartEvent(IPlayer player, Ability ability, EntityLivingBase target) {
|
||||
super(player, ability, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return EnumScriptType.ABILITY_START.function;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when a player's ability enters the ACTIVE phase and performs its effect.
|
||||
*/
|
||||
@Cancelable
|
||||
public static class ExecuteEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.ExecuteEvent {
|
||||
public ExecuteEvent(IPlayer player, Ability ability, EntityLivingBase target) {
|
||||
super(player, ability, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return EnumScriptType.ABILITY_EXECUTE.function;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when a player's ability is interrupted by damage.
|
||||
*/
|
||||
public static class InterruptEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.InterruptEvent {
|
||||
private final IDamageSource damageSource;
|
||||
private final float damage;
|
||||
|
||||
public InterruptEvent(IPlayer player, Ability ability, EntityLivingBase target, DamageSource source, float damage) {
|
||||
super(player, ability, target);
|
||||
this.damageSource = source != null ? NpcAPI.Instance().getIDamageSource(source) : null;
|
||||
this.damage = damage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IDamageSource getDamageSource() {
|
||||
return damageSource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getDamage() {
|
||||
return damage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return EnumScriptType.ABILITY_INTERRUPT.function;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when a player's ability completes its full execution cycle.
|
||||
*/
|
||||
public static class CompleteEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.CompleteEvent {
|
||||
public CompleteEvent(IPlayer player, Ability ability, EntityLivingBase target) {
|
||||
super(player, ability, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return EnumScriptType.ABILITY_COMPLETE.function;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired every tick while a player's ability is executing.
|
||||
*/
|
||||
public static class TickEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.TickEvent {
|
||||
private final int abilityPhase;
|
||||
private final int tick;
|
||||
|
||||
public TickEvent(IPlayer player, Ability ability, EntityLivingBase target, int phase, int tick) {
|
||||
super(player, ability, target);
|
||||
this.abilityPhase = phase;
|
||||
this.tick = tick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAbilityPhase() {
|
||||
return abilityPhase;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTick() {
|
||||
return tick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHookName() {
|
||||
return EnumScriptType.ABILITY_TICK.function;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fired when a player's ability hits an entity with damage.
|
||||
* Canceling prevents the damage from being applied.
|
||||
|
||||
@@ -1578,6 +1578,12 @@ ability.preset.inferno=Inferno
|
||||
ability.preset.arcane=Arcane
|
||||
ability.preset.electric=Electric
|
||||
ability.preset.frost=Frost
|
||||
ability.preset.hidden=Hidden
|
||||
ability.preset.venom=Venom
|
||||
ability.preset.explosive=Explosive
|
||||
ability.preset.cursed=Cursed
|
||||
ability.preset.shock=Shock
|
||||
ability.preset.snare=Snare
|
||||
|
||||
# Accent Styles
|
||||
ability.accentStyle.static=Static
|
||||
@@ -1910,6 +1916,7 @@ ability.modifyParent=Edit Parent
|
||||
|
||||
# Ability Variant Selection
|
||||
ability.selectVariant=Select Variant
|
||||
ability.variant.base=Base
|
||||
ability.variant.single=Single
|
||||
ability.variant.dual=Dual
|
||||
ability.variant.barrage=Barrage
|
||||
|
||||
Reference in new issue
Block a user