Merge branch 'dev' into syntax-highlighter-unified

This commit is contained in:
Kam committed 2026-02-16 01:59:25 -05:00
commit 2a5dd6dfd6
53 files changed
+2769 -773

No files matched your search

+1 -1
View File
@@ -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"),
@@ -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);
}
}
+1 -1
View File
@@ -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")
+19 -1
View File
@@ -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);
+134
View File
@@ -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