Merge branch 'refs/heads/dev' into syntax-highlighter-unified

# Conflicts:
#	repositories.gradle
#	src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java
#	src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java
#	src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java
#	src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java
#	src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java
#	src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java
This commit is contained in:
bigguy345 committed 2026-01-13 18:12:23 +02:00
commit 83e11a05da
93 files changed
+13093 -179

No files matched your search

@@ -0,0 +1,260 @@
package kamkeel.npcs.client.renderer;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraftforge.client.event.RenderWorldLastEvent;
import org.lwjgl.opengl.GL11;
/**
* Renders telegraph warning shapes in the world.
* Register this as an event handler on the Forge event bus.
*/
public class TelegraphRenderer {
public static TelegraphRenderer Instance;
private static final int CIRCLE_SEGMENTS = 32;
private static final int CONE_SEGMENTS = 16;
public TelegraphRenderer() {
Instance = this;
}
@SubscribeEvent
public void onRenderWorldLast(RenderWorldLastEvent event) {
if (TelegraphManager.ClientInstance == null || !TelegraphManager.ClientInstance.hasTelegraphs()) {
return;
}
Minecraft mc = Minecraft.getMinecraft();
EntityPlayer player = mc.thePlayer;
if (player == null) return;
float partialTicks = event.partialTicks;
// Get player interpolated position for camera offset
double playerX = player.lastTickPosX + (player.posX - player.lastTickPosX) * partialTicks;
double playerY = player.lastTickPosY + (player.posY - player.lastTickPosY) * partialTicks;
double playerZ = player.lastTickPosZ + (player.posZ - player.lastTickPosZ) * partialTicks;
// Setup GL state for transparent rendering
GL11.glPushMatrix();
GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
GL11.glDisable(GL11.GL_TEXTURE_2D);
GL11.glDisable(GL11.GL_CULL_FACE);
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
GL11.glDisable(GL11.GL_LIGHTING);
GL11.glDepthMask(false);
// Render each telegraph
for (TelegraphInstance instance : TelegraphManager.ClientInstance.getTelegraphs()) {
renderTelegraph(instance, playerX, playerY, playerZ, partialTicks);
}
// Restore GL state
GL11.glDepthMask(true);
GL11.glEnable(GL11.GL_LIGHTING);
GL11.glDisable(GL11.GL_BLEND);
GL11.glEnable(GL11.GL_CULL_FACE);
GL11.glEnable(GL11.GL_TEXTURE_2D);
GL11.glPopAttrib();
GL11.glPopMatrix();
}
private void renderTelegraph(TelegraphInstance instance, double playerX, double playerY, double playerZ, float partialTicks) {
Telegraph telegraph = instance.getTelegraph();
if (telegraph == null || telegraph.getType() == TelegraphType.NONE) {
return;
}
// Calculate render position relative to camera
double renderX = instance.getX() - playerX;
double renderY = instance.getY() - playerY + telegraph.getHeightOffset();
double renderZ = instance.getZ() - playerZ;
// Get animated color
int color = instance.getAnimatedColor(partialTicks);
float alpha = ((color >> 24) & 0xFF) / 255.0f;
float red = ((color >> 16) & 0xFF) / 255.0f;
float green = ((color >> 8) & 0xFF) / 255.0f;
float blue = (color & 0xFF) / 255.0f;
GL11.glPushMatrix();
GL11.glTranslated(renderX, renderY, renderZ);
// Rotate for directional shapes
GL11.glRotatef(-instance.getYaw(), 0, 1, 0);
switch (telegraph.getType()) {
case CIRCLE:
renderCircle(telegraph.getRadius(), red, green, blue, alpha);
break;
case RING:
renderRing(telegraph.getRadius(), telegraph.getInnerRadius(), red, green, blue, alpha);
break;
case LINE:
renderLine(telegraph.getLength(), telegraph.getWidth(), red, green, blue, alpha);
break;
case CONE:
renderCone(telegraph.getLength(), telegraph.getAngle(), red, green, blue, alpha);
break;
case POINT:
renderPoint(red, green, blue, alpha);
break;
default:
break;
}
GL11.glPopMatrix();
}
private void renderCircle(float radius, float r, float g, float b, float a) {
Tessellator tessellator = Tessellator.instance;
tessellator.startDrawing(GL11.GL_TRIANGLE_FAN);
tessellator.setColorRGBA_F(r, g, b, a);
// Center point
tessellator.addVertex(0, 0, 0);
// Circle points
for (int i = 0; i <= CIRCLE_SEGMENTS; i++) {
double angle = (Math.PI * 2 * i) / CIRCLE_SEGMENTS;
double x = Math.cos(angle) * radius;
double z = Math.sin(angle) * radius;
tessellator.addVertex(x, 0, z);
}
tessellator.draw();
// Draw border
renderCircleBorder(radius, r, g, b, Math.min(1.0f, a * 2));
}
private void renderCircleBorder(float radius, float r, float g, float b, float a) {
Tessellator tessellator = Tessellator.instance;
tessellator.startDrawing(GL11.GL_LINE_LOOP);
tessellator.setColorRGBA_F(r, g, b, a);
for (int i = 0; i < CIRCLE_SEGMENTS; i++) {
double angle = (Math.PI * 2 * i) / CIRCLE_SEGMENTS;
double x = Math.cos(angle) * radius;
double z = Math.sin(angle) * radius;
tessellator.addVertex(x, 0.01, z);
}
tessellator.draw();
}
private void renderRing(float outerRadius, float innerRadius, float r, float g, float b, float a) {
Tessellator tessellator = Tessellator.instance;
tessellator.startDrawing(GL11.GL_TRIANGLE_STRIP);
tessellator.setColorRGBA_F(r, g, b, a);
for (int i = 0; i <= CIRCLE_SEGMENTS; i++) {
double angle = (Math.PI * 2 * i) / CIRCLE_SEGMENTS;
double cosA = Math.cos(angle);
double sinA = Math.sin(angle);
// Outer point
tessellator.addVertex(cosA * outerRadius, 0, sinA * outerRadius);
// Inner point
tessellator.addVertex(cosA * innerRadius, 0, sinA * innerRadius);
}
tessellator.draw();
// Draw borders
renderCircleBorder(outerRadius, r, g, b, Math.min(1.0f, a * 2));
renderCircleBorder(innerRadius, r, g, b, Math.min(1.0f, a * 2));
}
private void renderLine(float length, float width, float r, float g, float b, float a) {
float halfWidth = width / 2;
Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
tessellator.setColorRGBA_F(r, g, b, a);
// Rectangle from origin forward
tessellator.addVertex(-halfWidth, 0, 0);
tessellator.addVertex(halfWidth, 0, 0);
tessellator.addVertex(halfWidth, 0, length);
tessellator.addVertex(-halfWidth, 0, length);
tessellator.draw();
// Draw border
tessellator.startDrawing(GL11.GL_LINE_LOOP);
tessellator.setColorRGBA_F(r, g, b, Math.min(1.0f, a * 2));
tessellator.addVertex(-halfWidth, 0.01, 0);
tessellator.addVertex(halfWidth, 0.01, 0);
tessellator.addVertex(halfWidth, 0.01, length);
tessellator.addVertex(-halfWidth, 0.01, length);
tessellator.draw();
}
private void renderCone(float length, float angle, float r, float g, float b, float a) {
float halfAngleRad = (float) Math.toRadians(angle / 2);
Tessellator tessellator = Tessellator.instance;
tessellator.startDrawing(GL11.GL_TRIANGLE_FAN);
tessellator.setColorRGBA_F(r, g, b, a);
// Origin point
tessellator.addVertex(0, 0, 0);
// Arc points
for (int i = 0; i <= CONE_SEGMENTS; i++) {
float segmentAngle = -halfAngleRad + (halfAngleRad * 2 * i) / CONE_SEGMENTS;
double x = Math.sin(segmentAngle) * length;
double z = Math.cos(segmentAngle) * length;
tessellator.addVertex(x, 0, z);
}
tessellator.draw();
// Draw border
tessellator.startDrawing(GL11.GL_LINE_STRIP);
tessellator.setColorRGBA_F(r, g, b, Math.min(1.0f, a * 2));
tessellator.addVertex(0, 0.01, 0);
for (int i = 0; i <= CONE_SEGMENTS; i++) {
float segmentAngle = -halfAngleRad + (halfAngleRad * 2 * i) / CONE_SEGMENTS;
double x = Math.sin(segmentAngle) * length;
double z = Math.cos(segmentAngle) * length;
tessellator.addVertex(x, 0.01, z);
}
tessellator.addVertex(0, 0.01, 0);
tessellator.draw();
}
private void renderPoint(float r, float g, float b, float a) {
float size = 0.5f;
Tessellator tessellator = Tessellator.instance;
tessellator.startDrawingQuads();
tessellator.setColorRGBA_F(r, g, b, a);
// Diamond shape
tessellator.addVertex(0, 0, -size);
tessellator.addVertex(size, 0, 0);
tessellator.addVertex(0, 0, size);
tessellator.addVertex(-size, 0, 0);
tessellator.draw();
}
}
@@ -0,0 +1,127 @@
package kamkeel.npcs.command;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import net.minecraft.command.ICommandSender;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static kamkeel.npcs.util.ColorUtil.sendError;
import static kamkeel.npcs.util.ColorUtil.sendResult;
public class AbilityCommand extends CommandKamkeelBase {
@Override
public String getCommandName() {
return "ability";
}
@Override
public String getDescription() {
return "Ability operations";
}
@SubCommand(
desc = "List all saved abilities"
)
public void list(ICommandSender sender, String[] args) {
Set<String> names = AbilityController.Instance.getSavedAbilityNames();
if (names.isEmpty()) {
sendResult(sender, "No saved abilities found.");
return;
}
List<String> sortedNames = new ArrayList<>(names);
Collections.sort(sortedNames);
sendResult(sender, "Saved Abilities (" + sortedNames.size() + "):");
for (String name : sortedNames) {
Ability ability = AbilityController.Instance.getSavedAbility(name);
if (ability != null) {
sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ")");
} else {
sendResult(sender, " - \u00A7b" + name);
}
}
}
@SubCommand(
desc = "List all registered ability types"
)
public void types(ICommandSender sender, String[] args) {
String[] typeIds = AbilityController.Instance.getTypes();
if (typeIds.length == 0) {
sendResult(sender, "No ability types registered.");
return;
}
List<String> sortedTypes = new ArrayList<>();
for (String typeId : typeIds) {
sortedTypes.add(typeId);
}
Collections.sort(sortedTypes);
sendResult(sender, "Registered Ability Types (" + sortedTypes.size() + "):");
for (String typeId : sortedTypes) {
sendResult(sender, " - \u00A7d" + typeId);
}
}
@SubCommand(
desc = "Reload abilities from disk"
)
public void reload(ICommandSender sender, String[] args) {
AbilityController.Instance.load();
sendResult(sender, "Abilities reloaded from disk.");
}
@SubCommand(
desc = "Delete a saved ability",
usage = "<name>"
)
public void delete(ICommandSender sender, String[] args) {
if (args.length < 1) {
sendError(sender, "Usage: /kam ability delete <name>");
return;
}
String name = args[0];
if (!AbilityController.Instance.getSavedAbilityNames().contains(name)) {
sendError(sender, "No saved ability with name: " + name);
return;
}
AbilityController.Instance.deleteAbility(name);
sendResult(sender, "Deleted ability: \u00A7b" + name);
}
@SubCommand(
desc = "Get info about a saved ability",
usage = "<name>"
)
public void info(ICommandSender sender, String[] args) {
if (args.length < 1) {
sendError(sender, "Usage: /kam ability info <name>");
return;
}
String name = args[0];
Ability ability = AbilityController.Instance.getSavedAbility(name);
if (ability == null) {
sendError(sender, "No saved ability with name: " + name);
return;
}
sendResult(sender, "Ability: \u00A7b" + ability.getName());
sendResult(sender, " Type: \u00A7d" + ability.getTypeId());
sendResult(sender, " Cooldown: \u00A7e" + ability.getCooldownTicks() + " ticks");
sendResult(sender, " Wind Up: \u00A7e" + ability.getWindUpTicks() + " ticks");
sendResult(sender, " Active: \u00A7e" + ability.getActiveTicks() + " ticks");
sendResult(sender, " Recovery: \u00A7e" + ability.getRecoveryTicks() + " ticks");
sendResult(sender, " Range: \u00A7e" + ability.getMinRange() + " - " + ability.getMaxRange());
sendResult(sender, " Telegraph: \u00A7e" + (ability.isShowTelegraph() ? "Yes" : "No"));
}
}
@@ -43,6 +43,7 @@ public class CommandKamkeel extends CommandBase {
registerCommand(new OverlayCommand());
registerCommand(new CommandCommand());
registerCommand(new EffectCommand());
registerCommand(new AbilityCommand());
if (ConfigMain.AttributesEnabled)
registerCommand(new AttributeCommand());
}
@@ -0,0 +1,755 @@
package kamkeel.npcs.controllers.data.ability;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.block.Block;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.DataAbilities;
import noppes.npcs.NpcDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.ability.IAbility;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.scripted.NpcAPI;
import noppes.npcs.scripted.event.AbilityEvent;
import java.util.ArrayList;
import java.util.List;
public abstract class Ability implements IAbility {
// ═══════════════════════════════════════════════════════════════════
// CONFIGURATION (saved to NBT)
// ═══════════════════════════════════════════════════════════════════
protected String id;
protected String name;
protected String typeId; // e.g., "ability.cnpc.slam" (also used as lang key)
// Selection
protected int weight = 10;
protected boolean enabled = true;
protected List<Condition> conditions = new ArrayList<>();
// Targeting
protected TargetingMode targetingMode = TargetingMode.AGGRO_TARGET;
protected float minRange = 0;
protected float maxRange = 20;
// Timing (ticks)
protected int cooldownTicks = 100;
protected int windUpTicks = 20;
protected int activeTicks = 10;
protected int recoveryTicks = 20;
// Interruption
protected boolean interruptible = true;
// Feedback
protected boolean lockMovement = true;
protected int windUpColor = 0x80FF4400; // Telegraph color during wind up
protected int activeColor = 0xC0FF0000; // Telegraph warning/active color
// Sounds
protected String windUpSound = ""; // Sound to play when wind up starts
protected String activeSound = ""; // Sound to play when active phase starts
// Animations (global animation IDs, -1 = none)
protected int windUpAnimationId = -1; // Animation to play during wind up
protected int activeAnimationId = -1; // Animation to play during active phase
// Telegraph configuration
protected boolean showTelegraph = true;
protected TelegraphType telegraphType = TelegraphType.CIRCLE;
protected float telegraphHeightOffset = 0.1f;
// Custom data for external mods
protected NBTTagCompound customData = new NBTTagCompound();
// ═══════════════════════════════════════════════════════════════════
// EXECUTION STATE (not saved, reset each combat)
// ═══════════════════════════════════════════════════════════════════
protected transient AbilityPhase phase = AbilityPhase.IDLE;
protected transient int currentTick = 0;
protected transient long cooldownEndTime = 0;
protected transient EntityLivingBase currentTarget;
protected transient long executionStartTime;
protected transient TelegraphInstance telegraphInstance;
// ═══════════════════════════════════════════════════════════════════
// ABSTRACT METHODS
// ═══════════════════════════════════════════════════════════════════
/** Called first tick of ACTIVE phase */
public abstract void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world);
/** Called every tick of ACTIVE phase */
public abstract void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick);
/** Write type-specific config to NBT */
public abstract void writeTypeNBT(NBTTagCompound nbt);
/** Read type-specific config from NBT */
public abstract void readTypeNBT(NBTTagCompound nbt);
// ═══════════════════════════════════════════════════════════════════
// OPTIONAL OVERRIDES
// ═══════════════════════════════════════════════════════════════════
public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {}
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {}
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {}
/**
* Apply damage to an entity with ability hit event support.
* Fires the abilityHit script event, allowing scripts to modify or cancel the damage.
*
* @param npc The NPC executing the ability
* @param hitEntity The entity being hit
* @param damage The damage amount
* @param knockback The horizontal knockback
* @return true if damage was applied (not cancelled), false if cancelled
*/
protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity,
float damage, float knockback) {
return applyAbilityDamage(npc, hitEntity, damage, knockback, 0.0f);
}
/**
* Backward-compatible signature (vertical knockback is ignored).
*/
protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity,
float damage, float knockback, float knockbackUp) {
DataAbilities dataAbilities = npc.abilities;
AbilityEvent.HitEvent event = dataAbilities.fireHitEvent(
this, currentTarget, hitEntity, damage, knockback, knockbackUp);
if (event == null) {
return false; // Cancelled
}
// Apply damage with potentially modified values
float finalDamage = event.getDamage();
float finalKnockback = event.getKnockback();
// Apply damage
if (finalDamage > 0) {
hitEntity.attackEntityFrom(new NpcDamageSource("mob", npc), finalDamage);
}
// Apply knockback if any
if (finalKnockback > 0) {
double dx = hitEntity.posX - npc.posX;
double dz = hitEntity.posZ - npc.posZ;
applyNpcKnockback(hitEntity, dx, dz, finalKnockback);
}
return true;
}
/**
* Apply damage to an entity with ability hit event support and custom knockback direction.
* Fires the abilityHit script event, allowing scripts to modify or cancel the damage.
*
* @param npc The NPC executing the ability
* @param hitEntity The entity being hit
* @param damage The damage amount
* @param knockback The horizontal knockback
* @param knockbackDirX The X component of knockback direction (normalized)
* @param knockbackDirZ The Z component of knockback direction (normalized)
* @return true if damage was applied (not cancelled), false if cancelled
*/
protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity,
float damage, float knockback,
double knockbackDirX, double knockbackDirZ) {
DataAbilities dataAbilities = npc.abilities;
AbilityEvent.HitEvent event = dataAbilities.fireHitEvent(
this, currentTarget, hitEntity, damage, knockback, 0.0f);
if (event == null) {
return false; // Cancelled
}
// Apply damage with potentially modified values
float finalDamage = event.getDamage();
float finalKnockback = event.getKnockback();
// Apply damage
if (finalDamage > 0) {
hitEntity.attackEntityFrom(DamageSource.causeMobDamage(npc), finalDamage);
}
// Apply knockback in specified direction
if (finalKnockback > 0) {
applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback);
}
return true;
}
private void applyNpcKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback) {
double len = Math.sqrt(dirX * dirX + dirZ * dirZ);
if (len <= 0) {
return;
}
double x = (dirX / len) * knockback * 0.5;
double z = (dirZ / len) * knockback * 0.5;
hitEntity.addVelocity(x, 0.1D, z);
hitEntity.velocityChanged = true;
}
public float getTelegraphRadius() { return 5.0f; }
public float getTelegraphLength() { return 5.0f; }
public float getTelegraphWidth() { return 2.0f; }
public float getTelegraphAngle() { return 45.0f; }
/**
* Returns true if this ability controls NPC movement during ACTIVE phase.
* When true, AI pathfinding will be blocked during ability execution.
* Override in abilities that move the NPC (Charge, Dash, Slam, etc.)
*/
public boolean hasAbilityMovement() { return false; }
/**
* Returns true if the targeting mode for this ability type is locked and cannot be changed.
* Most abilities have fixed targeting (e.g., Heal is always SELF, Charge is always AGGRO_TARGET).
* Override and return false for abilities with flexible targeting.
*/
public boolean isTargetingModeLocked() { return true; }
/**
* Returns the allowed targeting modes for this ability, or null if all modes are allowed.
* Override in abilities that only support specific targeting modes.
*/
public TargetingMode[] getAllowedTargetingModes() { return null; }
/**
* Returns true if the telegraph type for this ability is locked and cannot be changed.
* Telegraph type is inherent to how the ability works (e.g., Charge uses LINE, Slam uses CIRCLE).
*/
public boolean isTelegraphTypeLocked() { return true; }
/**
* Returns true if this ability type has custom settings that need a Type-specific tab.
* Override to return true if the ability has settings beyond the base Ability fields.
*/
public boolean hasTypeSettings() { return false; }
/**
* Get the number of rows needed for type-specific settings in the GUI.
* Each row is approximately 24 pixels. Used for GUI layout.
*/
public int getTypeSettingsRowCount() { return 0; }
/**
* Creates the GUI for configuring this ability.
* Override this method to provide a custom GUI subclass for type-specific settings.
*
* Third-party mods can extend SubGuiAbilityConfig and override initTypeTab(),
* handleTypeButton(), and handleTypeTextField() to provide custom type settings.
*
* @param callback The callback to notify when the ability is saved
* @return A SubGuiAbilityConfig instance (or subclass) for configuring this ability
*/
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
return new SubGuiAbilityConfig(this, callback);
}
/**
* Create a telegraph instance for this ability.
* Override for custom telegraph shapes.
*
* @param npc The caster
* @param target The target (for position calculation)
* @return The telegraph instance, or null if no telegraph
*/
public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE) {
return null;
}
Telegraph telegraph;
double x, y, z;
float yaw = npc.rotationYaw;
// Determine position based on targeting mode
boolean positionAtNpc = targetingMode == TargetingMode.AOE_SELF ||
targetingMode == TargetingMode.SELF ||
telegraphType == TelegraphType.LINE ||
telegraphType == TelegraphType.CONE;
if (positionAtNpc) {
x = npc.posX;
y = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ);
z = npc.posZ;
} else if (target != null) {
x = target.posX;
y = findGroundLevel(npc.worldObj, target.posX, target.posY, target.posZ);
z = target.posZ;
} else {
x = npc.posX;
y = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ);
z = npc.posZ;
}
// Create telegraph based on type
switch (telegraphType) {
case CIRCLE:
telegraph = Telegraph.circle(getTelegraphRadius());
break;
case RING:
telegraph = Telegraph.ring(getTelegraphRadius(), getTelegraphRadius() * 0.5f);
break;
case LINE:
telegraph = Telegraph.line(getTelegraphLength(), getTelegraphWidth());
break;
case CONE:
telegraph = Telegraph.cone(getTelegraphLength(), getTelegraphAngle());
break;
case POINT:
telegraph = new Telegraph("", TelegraphType.POINT);
break;
default:
return null;
}
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw);
instance.setCasterEntityId(npc.getEntityId());
// Set entity to follow based on targeting mode
if (positionAtNpc) {
// AOE_SELF abilities: telegraph follows NPC during windup
instance.setEntityIdToFollow(npc.getEntityId());
} else if (target != null) {
// AOE_TARGET abilities: telegraph follows target during windup
instance.setEntityIdToFollow(target.getEntityId());
}
return instance;
}
/**
* Find the ground level at a given position.
* Searches downward from the given Y to find a solid block.
*
* @param world The world
* @param x X coordinate
* @param startY Starting Y coordinate (entity feet position)
* @param z Z coordinate
* @return The Y coordinate of the ground surface
*/
protected double findGroundLevel(World world, double x, double startY, double z) {
if (world == null) return startY;
int blockX = (int) Math.floor(x);
int blockZ = (int) Math.floor(z);
int startBlockY = (int) Math.floor(startY);
// Search downward for solid ground (max 10 blocks down)
for (int checkY = startBlockY; checkY >= startBlockY - 10 && checkY >= 0; checkY--) {
Block block = world.getBlock(blockX, checkY, blockZ);
if (block != null && block.getMaterial().isSolid()) {
// Found solid block, telegraph goes on top of it
return checkY + 1;
}
}
// No ground found, use original position
return startY;
}
/**
* Get the current telegraph instance.
*/
public TelegraphInstance getTelegraphInstance() {
return telegraphInstance;
}
/**
* Set the telegraph instance (called by DataAbilities).
*/
public void setTelegraphInstance(TelegraphInstance instance) {
this.telegraphInstance = instance;
}
// ═══════════════════════════════════════════════════════════════════
// EXECUTION LOGIC (called by CombatHandler)
// ═══════════════════════════════════════════════════════════════════
/** Start executing this ability */
public void start(EntityLivingBase target) {
this.phase = AbilityPhase.WINDUP;
this.currentTick = 0;
this.currentTarget = target;
this.executionStartTime = System.currentTimeMillis();
}
/** Tick the ability. Returns true if phase changed. */
public boolean tick() {
if (phase == AbilityPhase.IDLE) return false;
currentTick++;
AbilityPhase oldPhase = phase;
switch (phase) {
case WINDUP:
if (currentTick >= windUpTicks) {
phase = AbilityPhase.ACTIVE;
currentTick = 0;
return true;
}
break;
case ACTIVE:
if (currentTick >= activeTicks) {
phase = AbilityPhase.RECOVERY;
currentTick = 0;
return true;
}
break;
case RECOVERY:
if (currentTick >= recoveryTicks) {
phase = AbilityPhase.IDLE;
startCooldown();
return true;
}
break;
}
return false;
}
/** Interrupt this ability */
public void interrupt() {
phase = AbilityPhase.IDLE;
currentTick = 0;
currentTarget = null;
telegraphInstance = null;
}
/** Start cooldown timer */
public void startCooldown() {
cooldownEndTime = System.currentTimeMillis() + (cooldownTicks * 50L);
}
/** Check if on cooldown */
public boolean isOnCooldown() {
return System.currentTimeMillis() < cooldownEndTime;
}
/** Check if executing */
public boolean isExecuting() {
return phase != AbilityPhase.IDLE;
}
/**
* Check if can be interrupted by this damage source.
* Only direct physical hits (not magic, fire, or other indirect damage) can interrupt.
* Interruption only occurs during WINDUP phase - if ability is already active, it completes.
*
* @param source The damage source
* @return true if the ability should be interrupted
*/
public boolean canInterrupt(DamageSource source) {
if (!interruptible || phase != AbilityPhase.WINDUP) {
return false;
}
// Only direct physical hits can interrupt, not magic, fire, or other indirect damage
if (source == null) {
return false;
}
// Reject indirect damage types
if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion()) {
return false;
}
// Reject damage without a direct attacker entity
if (source.getEntity() == null) {
return false;
}
// Direct hit from an entity - can interrupt
return true;
}
/** Reset all execution state (called on combat end) */
public void reset() {
phase = AbilityPhase.IDLE;
currentTick = 0;
currentTarget = null;
cooldownEndTime = 0;
telegraphInstance = null;
}
// ═══════════════════════════════════════════════════════════════════
// CONDITION CHECKING
// ═══════════════════════════════════════════════════════════════════
public boolean checkConditions(EntityNPCInterface npc, EntityLivingBase target) {
for (Condition c : conditions) {
if (!c.check(npc, target)) return false;
}
return true;
}
/** Full eligibility check */
public boolean canUse(EntityNPCInterface npc, EntityLivingBase target) {
if (!enabled) return false;
if (isOnCooldown()) return false;
if (isExecuting()) return false;
float distance = npc.getDistanceToEntity(target);
if (distance < minRange || distance > maxRange) return false;
return checkConditions(npc, target);
}
// ═══════════════════════════════════════════════════════════════════
// NBT (config only, execution state is transient)
// ═══════════════════════════════════════════════════════════════════
public NBTTagCompound writeNBT() {
NBTTagCompound nbt = new NBTTagCompound();
nbt.setString("id", id);
nbt.setString("name", name);
nbt.setString("typeId", typeId);
nbt.setInteger("weight", weight);
nbt.setBoolean("enabled", enabled);
nbt.setString("targetingMode", targetingMode.name());
nbt.setFloat("minRange", minRange);
nbt.setFloat("maxRange", maxRange);
nbt.setInteger("cooldown", cooldownTicks);
nbt.setInteger("windUp", windUpTicks);
nbt.setInteger("active", activeTicks);
nbt.setInteger("recovery", recoveryTicks);
nbt.setBoolean("interruptible", interruptible);
nbt.setBoolean("lockMovement", lockMovement);
nbt.setInteger("windUpColor", windUpColor);
nbt.setInteger("activeColor", activeColor);
nbt.setString("windUpSound", windUpSound);
nbt.setString("activeSound", activeSound);
nbt.setInteger("windUpAnimationId", windUpAnimationId);
nbt.setInteger("activeAnimationId", activeAnimationId);
nbt.setBoolean("showTelegraph", showTelegraph);
nbt.setString("telegraphType", telegraphType.name());
nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset);
nbt.setTag("customData", customData);
// Conditions
NBTTagList condList = new NBTTagList();
for (Condition c : conditions) condList.appendTag(c.writeNBT());
nbt.setTag("conditions", condList);
// Type-specific
NBTTagCompound typeNBT = new NBTTagCompound();
writeTypeNBT(typeNBT);
nbt.setTag("typeData", typeNBT);
return nbt;
}
public void readNBT(NBTTagCompound nbt) {
id = nbt.getString("id");
name = nbt.getString("name");
typeId = nbt.getString("typeId");
weight = nbt.getInteger("weight");
enabled = nbt.getBoolean("enabled");
targetingMode = TargetingMode.valueOf(nbt.getString("targetingMode"));
minRange = nbt.getFloat("minRange");
maxRange = nbt.getFloat("maxRange");
cooldownTicks = nbt.getInteger("cooldown");
windUpTicks = nbt.getInteger("windUp");
activeTicks = nbt.getInteger("active");
recoveryTicks = nbt.getInteger("recovery");
interruptible = nbt.getBoolean("interruptible");
lockMovement = nbt.getBoolean("lockMovement");
// Support old telegraphColor key for backwards compatibility
if (nbt.hasKey("windUpColor")) {
windUpColor = nbt.getInteger("windUpColor");
} else if (nbt.hasKey("telegraphColor")) {
windUpColor = nbt.getInteger("telegraphColor");
}
if (nbt.hasKey("activeColor")) {
activeColor = nbt.getInteger("activeColor");
} else {
// Default active color to windUpColor with higher alpha
activeColor = (windUpColor & 0x00FFFFFF) | 0xC0000000;
}
// Sound fields (with backwards compatibility for old castSound)
if (nbt.hasKey("windUpSound")) {
windUpSound = nbt.getString("windUpSound");
} else if (nbt.hasKey("castSound")) {
windUpSound = nbt.getString("castSound"); // Migrate old field
} else {
windUpSound = "";
}
activeSound = nbt.hasKey("activeSound") ? nbt.getString("activeSound") : "";
// Animation fields (with backwards compatibility for old animationId)
if (nbt.hasKey("windUpAnimationId")) {
windUpAnimationId = nbt.getInteger("windUpAnimationId");
} else if (nbt.hasKey("animationId")) {
windUpAnimationId = nbt.getInteger("animationId"); // Migrate old field
} else {
windUpAnimationId = -1;
}
activeAnimationId = nbt.hasKey("activeAnimationId") ? nbt.getInteger("activeAnimationId") : -1;
showTelegraph = !nbt.hasKey("showTelegraph") || nbt.getBoolean("showTelegraph");
if (nbt.hasKey("telegraphType")) {
try {
telegraphType = TelegraphType.valueOf(nbt.getString("telegraphType"));
} catch (Exception e) {
telegraphType = TelegraphType.CIRCLE;
}
}
telegraphHeightOffset = nbt.hasKey("telegraphHeightOffset") ? nbt.getFloat("telegraphHeightOffset") : 0.1f;
customData = nbt.getCompoundTag("customData");
// Conditions
conditions.clear();
NBTTagList condList = nbt.getTagList("conditions", 10);
for (int i = 0; i < condList.tagCount(); i++) {
Condition c = Condition.fromNBT(condList.getCompoundTagAt(i));
if (c != null) conditions.add(c);
}
readTypeNBT(nbt.getCompoundTag("typeData"));
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getTypeId() { return typeId; }
public int getWeight() { return weight; }
public void setWeight(int weight) { this.weight = weight; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public TargetingMode getTargetingMode() { return targetingMode; }
public void setTargetingMode(TargetingMode targetingMode) { this.targetingMode = targetingMode; }
public float getMinRange() { return minRange; }
public void setMinRange(float minRange) { this.minRange = minRange; }
public float getMaxRange() { return maxRange; }
public void setMaxRange(float maxRange) { this.maxRange = maxRange; }
public int getCooldownTicks() { return cooldownTicks; }
public void setCooldownTicks(int cooldownTicks) { this.cooldownTicks = cooldownTicks; }
public int getWindUpTicks() { return windUpTicks; }
public void setWindUpTicks(int windUpTicks) { this.windUpTicks = windUpTicks; }
public int getActiveTicks() { return activeTicks; }
public void setActiveTicks(int activeTicks) { this.activeTicks = activeTicks; }
public int getRecoveryTicks() { return recoveryTicks; }
public void setRecoveryTicks(int recoveryTicks) { this.recoveryTicks = recoveryTicks; }
public boolean isInterruptible() { return interruptible; }
public void setInterruptible(boolean interruptible) { this.interruptible = interruptible; }
public boolean isLockMovement() { return lockMovement; }
public void setLockMovement(boolean lockMovement) { this.lockMovement = lockMovement; }
public int getWindUpColor() { return windUpColor; }
public void setWindUpColor(int windUpColor) { this.windUpColor = windUpColor; }
public int getActiveColor() { return activeColor; }
public void setActiveColor(int activeColor) { this.activeColor = activeColor; }
/** @deprecated Use getWindUpColor() instead */
@Deprecated
public int getTelegraphColor() { return windUpColor; }
/** @deprecated Use setWindUpColor() instead */
@Deprecated
public void setTelegraphColor(int telegraphColor) { this.windUpColor = telegraphColor; }
public String getWindUpSound() { return windUpSound; }
public void setWindUpSound(String windUpSound) { this.windUpSound = windUpSound; }
public String getActiveSound() { return activeSound; }
public void setActiveSound(String activeSound) { this.activeSound = activeSound; }
public int getWindUpAnimationId() { return windUpAnimationId; }
public void setWindUpAnimationId(int windUpAnimationId) { this.windUpAnimationId = windUpAnimationId; }
public int getActiveAnimationId() { return activeAnimationId; }
public void setActiveAnimationId(int activeAnimationId) { this.activeAnimationId = activeAnimationId; }
/** @deprecated Use getWindUpSound() instead */
@Deprecated
public String getCastSound() { return windUpSound; }
/** @deprecated Use setWindUpSound() instead */
@Deprecated
public void setCastSound(String castSound) { this.windUpSound = castSound; }
/** @deprecated Use getWindUpAnimationId() instead */
@Deprecated
public int getAnimationId() { return windUpAnimationId; }
/** @deprecated Use setWindUpAnimationId() instead */
@Deprecated
public void setAnimationId(int animationId) { this.windUpAnimationId = animationId; }
public boolean isShowTelegraph() { return showTelegraph; }
public void setShowTelegraph(boolean showTelegraph) { this.showTelegraph = showTelegraph; }
public TelegraphType getTelegraphType() { return telegraphType; }
public void setTelegraphType(TelegraphType telegraphType) { this.telegraphType = telegraphType; }
public float getTelegraphHeightOffset() { return telegraphHeightOffset; }
public void setTelegraphHeightOffset(float telegraphHeightOffset) { this.telegraphHeightOffset = telegraphHeightOffset; }
public AbilityPhase getPhase() { return phase; }
/**
* Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=RECOVERY).
* Required for IAbility interface.
*/
@Override
public int getPhaseInt() { return phase.ordinal(); }
public int getCurrentTick() { return currentTick; }
public EntityLivingBase getCurrentTarget() { return currentTarget; }
public NBTTagCompound getCustomData() { return customData; }
public List<Condition> getConditions() { return conditions; }
public void addCondition(Condition c) { conditions.add(c); }
// ═══════════════════════════════════════════════════════════════════
// IAbility API METHODS
// ═══════════════════════════════════════════════════════════════════
@Override
public INbt getNbt() {
return NpcAPI.Instance().getINbt(writeNBT());
}
@Override
public void setNbt(INbt nbt) {
readNBT(nbt.getMCNBT());
}
}
@@ -0,0 +1,299 @@
package kamkeel.npcs.controllers.data.ability;
import kamkeel.npcs.controllers.data.ability.type.*;
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 java.io.File;
import java.util.*;
import java.util.function.Supplier;
/**
* Controller for ability types. Allows external mods to register custom ability types.
* Initialize during FMLInitializationEvent.
*
* Also manages saved ability presets that can be reused across NPCs.
*/
public class AbilityController implements IAbilityHandler {
public static AbilityController Instance;
private final Map<String, Supplier<Ability>> factories = new LinkedHashMap<>();
private final Map<String, Supplier<Ability>> factoriesByTypeId = new LinkedHashMap<>();
private final Map<String, Ability> savedAbilities = new LinkedHashMap<>();
public AbilityController() {
Instance = this;
registerBuiltins();
}
/**
* Load saved abilities from world directory.
* Called when world loads.
*/
public void load() {
savedAbilities.clear();
File dir = getDir();
if (!dir.exists()) {
return;
}
File[] files = dir.listFiles();
if (files == null) return;
for (File file : files) {
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
try {
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
Ability ability = fromNBT(nbt);
if (ability != null && ability.getName() != null && !ability.getName().isEmpty()) {
savedAbilities.put(ability.getName(), ability);
LogWriter.info("Loaded ability preset: " + ability.getName());
}
} catch (Exception e) {
LogWriter.error("Error loading ability preset: " + file.getAbsolutePath(), e);
}
}
LogWriter.info("Loaded " + savedAbilities.size() + " ability presets");
}
/**
* Get the ability save directory.
*/
private File getDir() {
File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities");
if (!dir.exists()) {
dir.mkdirs();
}
return dir;
}
/**
* Save an ability preset to disk.
*
* @param ability The ability to save
* @return true if successful
*/
public boolean saveAbility(Ability ability) {
if (ability == null || ability.getName() == null || ability.getName().isEmpty()) {
return false;
}
File dir = getDir();
String safeName = ability.getName().replaceAll("[^a-zA-Z0-9_-]", "_");
File fileNew = new File(dir, safeName + ".json_new");
File fileCurrent = new File(dir, safeName + ".json");
try {
NBTTagCompound nbt = ability.writeNBT();
NBTJsonUtil.SaveFile(fileNew, nbt);
// Atomic rename
if (fileCurrent.exists()) {
fileCurrent.delete();
}
fileNew.renameTo(fileCurrent);
if (fileNew.exists()) {
fileNew.delete();
}
savedAbilities.put(ability.getName(), ability);
LogWriter.info("Saved ability preset: " + ability.getName());
return true;
} catch (Exception e) {
LogWriter.error("Error saving ability preset: " + ability.getName(), e);
return false;
}
}
/**
* Delete a saved ability preset.
*
* @param name The name of the ability to delete
* @return true if successful
*/
public boolean deleteAbility(String name) {
if (name == null || name.isEmpty()) {
return false;
}
Ability removed = savedAbilities.remove(name);
if (removed == null) {
return false;
}
File dir = getDir();
String safeName = name.replaceAll("[^a-zA-Z0-9_-]", "_");
File file = new File(dir, safeName + ".json");
if (file.exists()) {
file.delete();
}
LogWriter.info("Deleted ability preset: " + name);
return true;
}
/**
* Get a saved ability preset by name.
*
* @param name The name of the ability
* @return A copy of the ability, or null if not found
*/
public Ability getSavedAbility(String name) {
Ability saved = savedAbilities.get(name);
if (saved == null) {
return null;
}
// Return a copy so modifications don't affect the saved version
return fromNBT(saved.writeNBT());
}
/**
* Check if a saved ability exists.
*/
public boolean hasSavedAbility(String name) {
return savedAbilities.containsKey(name);
}
/**
* Get all saved ability names.
*/
public Set<String> getSavedAbilityNames() {
return new LinkedHashSet<>(savedAbilities.keySet());
}
/**
* Get all saved abilities.
*/
public Collection<Ability> getSavedAbilities() {
return savedAbilities.values();
}
// ═══════════════════════════════════════════════════════════════════════════
// IAbilityHandler INTERFACE IMPLEMENTATION
// ═══════════════════════════════════════════════════════════════════════════
@Override
public String[] getTypes() {
// Return the ability typeIds (lang keys), not the factory keys
return factoriesByTypeId.keySet().toArray(new String[0]);
}
@Override
public boolean hasType(String typeId) {
return factories.containsKey(typeId) || factoriesByTypeId.containsKey(typeId);
}
@Override
public String[] getSavedNames() {
return savedAbilities.keySet().toArray(new String[0]);
}
@Override
public boolean has(String name) {
return savedAbilities.containsKey(name);
}
@Override
public boolean delete(String name) {
return deleteAbility(name);
}
// ═══════════════════════════════════════════════════════════════════════════
// TYPE REGISTRATION
// ═══════════════════════════════════════════════════════════════════════════
/**
* Register a new ability type.
* Call during FMLInitializationEvent.
*
* @param factoryKey The factory key (e.g., "cnpc:slam", "mymod:custom_ability")
* @param factory Factory lambda that creates a new instance
*/
public void registerType(String factoryKey, Supplier<Ability> factory) {
if (factories.containsKey(factoryKey)) {
LogWriter.info("AbilityController: Overwriting existing ability type: " + factoryKey);
}
factories.put(factoryKey, factory);
// Also register by the ability's actual typeId (lang key)
Ability temp = factory.get();
String abilityTypeId = temp.getTypeId();
factoriesByTypeId.put(abilityTypeId, factory);
}
/**
* Create an ability from NBT data.
* Uses the "typeId" field to determine which type to create.
*/
public Ability fromNBT(NBTTagCompound nbt) {
if (nbt == null || !nbt.hasKey("typeId")) {
return null;
}
String typeId = nbt.getString("typeId");
Supplier<Ability> factory = factories.get(typeId);
if (factory == null) {
// Try looking up by ability typeId (lang key)
factory = factoriesByTypeId.get(typeId);
}
if (factory == null) {
LogWriter.info("AbilityController: Unknown ability type: " + typeId);
return null;
}
Ability ability = factory.get();
ability.readNBT(nbt);
return ability;
}
/**
* Create a new empty ability of the given type.
* @param factoryKey The factory key (e.g., "cnpc:slam") or the ability typeId (e.g., "ability.cnpc.slam")
*/
public Ability create(String factoryKey) {
// Try direct lookup first (factory key)
Supplier<Ability> factory = factories.get(factoryKey);
if (factory == null) {
// Try looking up by ability typeId
factory = factoriesByTypeId.get(factoryKey);
}
if (factory == null) {
return null;
}
return factory.get();
}
/**
* Register built-in ability types.
*/
private void registerBuiltins() {
// Melee/AOE damage abilities
registerType("cnpc:slam", AbilitySlam::new);
registerType("cnpc:heavy_hit", AbilityHeavyHit::new);
registerType("cnpc:cutter", AbilityCutter::new);
// Ranged/Projectile abilities
registerType("cnpc:beam", AbilityBeam::new);
registerType("cnpc:projectile", AbilityProjectile::new);
registerType("cnpc:orb", AbilityOrb::new);
// Movement abilities
registerType("cnpc:charge", AbilityCharge::new);
registerType("cnpc:dash", AbilityDash::new);
registerType("cnpc:teleport", AbilityTeleport::new);
registerType("cnpc:vortex", AbilityVortex::new);
registerType("cnpc:shockwave", AbilityShockwave::new);
// Defensive abilities
registerType("cnpc:guard", AbilityGuard::new);
registerType("cnpc:heal", AbilityHeal::new);
// Zone/Trap abilities
registerType("cnpc:hazard", AbilityHazard::new);
registerType("cnpc:trap", AbilityTrap::new);
}
}
@@ -0,0 +1,8 @@
package kamkeel.npcs.controllers.data.ability;
public enum AbilityPhase {
IDLE, // Not executing
WINDUP, // Charging up (telegraph visible)
ACTIVE, // Executing (damage happens)
RECOVERY // Finishing animation
}
@@ -0,0 +1,263 @@
package kamkeel.npcs.controllers.data.ability;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.entity.EntityNPCInterface;
/**
* Interface for ability usage conditions.
* Conditions determine when an ability is eligible to be used.
*/
public interface Condition {
/**
* Check if this condition is met.
*
* @param npc The NPC that would use the ability
* @param target The current target (may be null for self-targeting abilities)
* @return true if the condition is satisfied
*/
boolean check(EntityNPCInterface npc, EntityLivingBase target);
/**
* Get the condition type ID for serialization.
*/
String getTypeId();
/**
* Write this condition to NBT.
*/
NBTTagCompound writeNBT();
/**
* Read this condition from NBT.
*/
void readNBT(NBTTagCompound nbt);
/**
* Create a Condition from NBT data.
* Uses the "type" field to determine which implementation to create.
*/
static Condition fromNBT(NBTTagCompound nbt) {
if (nbt == null || !nbt.hasKey("type")) {
return null;
}
String type = nbt.getString("type");
Condition condition = null;
switch (type) {
case "hp_above":
condition = new ConditionHPAbove();
break;
case "hp_below":
condition = new ConditionHPBelow();
break;
case "target_hp_above":
condition = new ConditionTargetHPAbove();
break;
case "target_hp_below":
condition = new ConditionTargetHPBelow();
break;
case "hit_count":
condition = new ConditionHitCount();
break;
default:
return null;
}
condition.readNBT(nbt);
return condition;
}
// ═══════════════════════════════════════════════════════════════════
// BUILT-IN CONDITION IMPLEMENTATIONS
// ═══════════════════════════════════════════════════════════════════
/**
* Condition: NPC HP is above a percentage threshold.
*/
class ConditionHPAbove implements Condition {
private float threshold = 0.5f; // 50%
public ConditionHPAbove() {}
public ConditionHPAbove(float threshold) {
this.threshold = threshold;
}
@Override
public boolean check(EntityNPCInterface npc, EntityLivingBase target) {
return npc.getHealth() / npc.getMaxHealth() > threshold;
}
@Override
public String getTypeId() { return "hp_above"; }
@Override
public NBTTagCompound writeNBT() {
NBTTagCompound nbt = new NBTTagCompound();
nbt.setString("type", getTypeId());
nbt.setFloat("threshold", threshold);
return nbt;
}
@Override
public void readNBT(NBTTagCompound nbt) {
threshold = nbt.getFloat("threshold");
}
}
/**
* Condition: NPC HP is below a percentage threshold.
*/
class ConditionHPBelow implements Condition {
private float threshold = 0.5f;
public ConditionHPBelow() {}
public ConditionHPBelow(float threshold) {
this.threshold = threshold;
}
@Override
public boolean check(EntityNPCInterface npc, EntityLivingBase target) {
return npc.getHealth() / npc.getMaxHealth() < threshold;
}
@Override
public String getTypeId() { return "hp_below"; }
@Override
public NBTTagCompound writeNBT() {
NBTTagCompound nbt = new NBTTagCompound();
nbt.setString("type", getTypeId());
nbt.setFloat("threshold", threshold);
return nbt;
}
@Override
public void readNBT(NBTTagCompound nbt) {
threshold = nbt.getFloat("threshold");
}
}
/**
* Condition: Target HP is above a percentage threshold.
*/
class ConditionTargetHPAbove implements Condition {
private float threshold = 0.5f;
public ConditionTargetHPAbove() {}
public ConditionTargetHPAbove(float threshold) {
this.threshold = threshold;
}
@Override
public boolean check(EntityNPCInterface npc, EntityLivingBase target) {
if (target == null) return false;
return target.getHealth() / target.getMaxHealth() > threshold;
}
@Override
public String getTypeId() { return "target_hp_above"; }
@Override
public NBTTagCompound writeNBT() {
NBTTagCompound nbt = new NBTTagCompound();
nbt.setString("type", getTypeId());
nbt.setFloat("threshold", threshold);
return nbt;
}
@Override
public void readNBT(NBTTagCompound nbt) {
threshold = nbt.getFloat("threshold");
}
}
/**
* Condition: Target HP is below a percentage threshold.
*/
class ConditionTargetHPBelow implements Condition {
private float threshold = 0.5f;
public ConditionTargetHPBelow() {}
public ConditionTargetHPBelow(float threshold) {
this.threshold = threshold;
}
@Override
public boolean check(EntityNPCInterface npc, EntityLivingBase target) {
if (target == null) return false;
return target.getHealth() / target.getMaxHealth() < threshold;
}
@Override
public String getTypeId() { return "target_hp_below"; }
@Override
public NBTTagCompound writeNBT() {
NBTTagCompound nbt = new NBTTagCompound();
nbt.setString("type", getTypeId());
nbt.setFloat("threshold", threshold);
return nbt;
}
@Override
public void readNBT(NBTTagCompound nbt) {
threshold = nbt.getFloat("threshold");
}
}
/**
* Condition: NPC has been hit X times within a time window.
* Useful for reactive abilities that trigger after receiving multiple hits.
*/
class ConditionHitCount implements Condition {
private int requiredHits = 3;
private int withinTicks = 60;
public ConditionHitCount() {}
public ConditionHitCount(int requiredHits, int withinTicks) {
this.requiredHits = requiredHits;
this.withinTicks = withinTicks;
}
@Override
public boolean check(EntityNPCInterface npc, EntityLivingBase target) {
// Check NPC's recent hit count from DataAbilities
if (npc.abilities != null) {
return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits;
}
return false;
}
@Override
public String getTypeId() { return "hit_count"; }
@Override
public NBTTagCompound writeNBT() {
NBTTagCompound nbt = new NBTTagCompound();
nbt.setString("type", getTypeId());
nbt.setInteger("requiredHits", requiredHits);
nbt.setInteger("withinTicks", withinTicks);
return nbt;
}
@Override
public void readNBT(NBTTagCompound nbt) {
requiredHits = nbt.getInteger("requiredHits");
withinTicks = nbt.getInteger("withinTicks");
}
public int getRequiredHits() { return requiredHits; }
public void setRequiredHits(int hits) { this.requiredHits = hits; }
public int getWithinTicks() { return withinTicks; }
public void setWithinTicks(int ticks) { this.withinTicks = ticks; }
}
}
@@ -0,0 +1,27 @@
package kamkeel.npcs.controllers.data.ability;
/**
* Defines how an ability selects its target(s).
*/
public enum TargetingMode {
/** Target the current aggro target */
AGGRO_TARGET,
/** Target self (for buffs, heals, guards) */
SELF,
/** AOE centered on self */
AOE_SELF,
/** AOE centered on target */
AOE_TARGET,
/** Random nearby enemy */
RANDOM_ENEMY,
/** Lowest HP enemy in range */
LOWEST_HP,
/** Highest threat enemy */
HIGHEST_THREAT
}
@@ -0,0 +1,202 @@
package kamkeel.npcs.controllers.data.ability.telegraph;
import net.minecraft.nbt.NBTTagCompound;
/**
* Telegraph configuration - defines a visual warning shape.
* Telegraphs warn players about incoming attacks.
*/
public class Telegraph {
// Identity
private String id = "";
private TelegraphType type = TelegraphType.CIRCLE;
// Shape parameters
private float radius = 3.0f;
private float innerRadius = 0.0f;
private float length = 5.0f;
private float width = 2.0f;
private float angle = 45.0f;
// Timing
private int durationTicks = 40;
// Visuals
private int color = 0x80FF0000; // Semi-transparent red
private int warningColor = 0xC0FF0000; // More opaque red
private int warningStartTick = 10;
private boolean animated = true;
private float heightOffset = 0.1f;
// Positioning
private boolean atTarget = true;
private boolean followsTarget = false;
private boolean followsCaster = false;
private float offsetForward = 0.0f;
public Telegraph() {}
public Telegraph(String id, TelegraphType type) {
this.id = id;
this.type = type;
}
public Telegraph(Telegraph other) {
this.id = other.id;
this.type = other.type;
this.radius = other.radius;
this.innerRadius = other.innerRadius;
this.length = other.length;
this.width = other.width;
this.angle = other.angle;
this.durationTicks = other.durationTicks;
this.color = other.color;
this.warningColor = other.warningColor;
this.warningStartTick = other.warningStartTick;
this.animated = other.animated;
this.heightOffset = other.heightOffset;
this.atTarget = other.atTarget;
this.followsTarget = other.followsTarget;
this.followsCaster = other.followsCaster;
this.offsetForward = other.offsetForward;
}
// ═══════════════════════════════════════════════════════════════════
// NBT SERIALIZATION
// ═══════════════════════════════════════════════════════════════════
public NBTTagCompound writeNBT() {
NBTTagCompound nbt = new NBTTagCompound();
nbt.setString("id", id);
nbt.setString("type", type.name());
nbt.setFloat("radius", radius);
nbt.setFloat("innerRadius", innerRadius);
nbt.setFloat("length", length);
nbt.setFloat("width", width);
nbt.setFloat("angle", angle);
nbt.setInteger("durationTicks", durationTicks);
nbt.setInteger("color", color);
nbt.setInteger("warningColor", warningColor);
nbt.setInteger("warningStartTick", warningStartTick);
nbt.setBoolean("animated", animated);
nbt.setFloat("heightOffset", heightOffset);
nbt.setBoolean("atTarget", atTarget);
nbt.setBoolean("followsTarget", followsTarget);
nbt.setBoolean("followsCaster", followsCaster);
nbt.setFloat("offsetForward", offsetForward);
return nbt;
}
public void readNBT(NBTTagCompound nbt) {
this.id = nbt.getString("id");
if (nbt.hasKey("type")) {
try {
this.type = TelegraphType.valueOf(nbt.getString("type"));
} catch (Exception e) {
this.type = TelegraphType.CIRCLE;
}
}
this.radius = nbt.getFloat("radius");
this.innerRadius = nbt.getFloat("innerRadius");
this.length = nbt.getFloat("length");
this.width = nbt.getFloat("width");
this.angle = nbt.getFloat("angle");
this.durationTicks = nbt.getInteger("durationTicks");
this.color = nbt.getInteger("color");
this.warningColor = nbt.getInteger("warningColor");
this.warningStartTick = nbt.getInteger("warningStartTick");
this.animated = nbt.getBoolean("animated");
this.heightOffset = nbt.getFloat("heightOffset");
this.atTarget = nbt.getBoolean("atTarget");
this.followsTarget = nbt.getBoolean("followsTarget");
this.followsCaster = nbt.getBoolean("followsCaster");
this.offsetForward = nbt.getFloat("offsetForward");
}
// ═══════════════════════════════════════════════════════════════════
// FACTORY METHODS
// ═══════════════════════════════════════════════════════════════════
public static Telegraph circle(float radius) {
Telegraph t = new Telegraph("", TelegraphType.CIRCLE);
t.radius = radius;
return t;
}
public static Telegraph ring(float outerRadius, float innerRadius) {
Telegraph t = new Telegraph("", TelegraphType.RING);
t.radius = outerRadius;
t.innerRadius = innerRadius;
return t;
}
public static Telegraph line(float length, float width) {
Telegraph t = new Telegraph("", TelegraphType.LINE);
t.length = length;
t.width = width;
return t;
}
public static Telegraph cone(float length, float angle) {
Telegraph t = new Telegraph("", TelegraphType.CONE);
t.length = length;
t.angle = angle;
return t;
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public TelegraphType getType() { return type; }
public void setType(TelegraphType type) { this.type = type; }
public float getRadius() { return radius; }
public void setRadius(float radius) { this.radius = radius; }
public float getInnerRadius() { return innerRadius; }
public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; }
public float getLength() { return length; }
public void setLength(float length) { this.length = length; }
public float getWidth() { return width; }
public void setWidth(float width) { this.width = width; }
public float getAngle() { return angle; }
public void setAngle(float angle) { this.angle = angle; }
public int getDurationTicks() { return durationTicks; }
public void setDurationTicks(int durationTicks) { this.durationTicks = durationTicks; }
public int getColor() { return color; }
public void setColor(int color) { this.color = color; }
public int getWarningColor() { return warningColor; }
public void setWarningColor(int warningColor) { this.warningColor = warningColor; }
public int getWarningStartTick() { return warningStartTick; }
public void setWarningStartTick(int warningStartTick) { this.warningStartTick = warningStartTick; }
public boolean isAnimated() { return animated; }
public void setAnimated(boolean animated) { this.animated = animated; }
public float getHeightOffset() { return heightOffset; }
public void setHeightOffset(float heightOffset) { this.heightOffset = heightOffset; }
public boolean isAtTarget() { return atTarget; }
public void setAtTarget(boolean atTarget) { this.atTarget = atTarget; }
public boolean isFollowsTarget() { return followsTarget; }
public void setFollowsTarget(boolean followsTarget) { this.followsTarget = followsTarget; }
public boolean isFollowsCaster() { return followsCaster; }
public void setFollowsCaster(boolean followsCaster) { this.followsCaster = followsCaster; }
public float getOffsetForward() { return offsetForward; }
public void setOffsetForward(float offsetForward) { this.offsetForward = offsetForward; }
}
@@ -0,0 +1,248 @@
package kamkeel.npcs.controllers.data.ability.telegraph;
import net.minecraft.block.Block;
import net.minecraft.entity.Entity;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
import java.util.UUID;
/**
* Runtime instance of an active telegraph.
* Tracks position, timing, and state for rendering.
*/
public class TelegraphInstance {
// Identity
private String instanceId;
private Telegraph telegraph;
// Position
private double x;
private double y;
private double z;
private float yaw;
// Entity tracking
private int entityIdToFollow = -1;
private int casterEntityId = -1;
// Timing
private int remainingTicks;
private int totalTicks;
// State
private boolean isWarning = false;
public TelegraphInstance() {
this.instanceId = UUID.randomUUID().toString().substring(0, 8);
}
public TelegraphInstance(Telegraph telegraph, double x, double y, double z, float yaw) {
this();
this.telegraph = new Telegraph(telegraph);
this.x = x;
this.y = y;
this.z = z;
this.yaw = yaw;
this.remainingTicks = telegraph.getDurationTicks();
this.totalTicks = telegraph.getDurationTicks();
}
// ═══════════════════════════════════════════════════════════════════
// TICK LOGIC
// ═══════════════════════════════════════════════════════════════════
/**
* Called every tick to update telegraph state.
* @param world The world (for entity lookup)
* @return false if telegraph should be removed
*/
public boolean tick(World world) {
if (remainingTicks <= 0) {
return false;
}
remainingTicks--;
// Update position if following an entity
if (entityIdToFollow >= 0 && world != null) {
Entity entity = world.getEntityByID(entityIdToFollow);
if (entity != null) {
this.x = entity.posX;
this.z = entity.posZ;
// Find ground level below the entity for proper telegraph placement
this.y = findGroundLevel(world, entity.posX, entity.posY, entity.posZ);
}
}
// Check for warning phase transition
if (!isWarning && telegraph != null) {
if (remainingTicks <= telegraph.getWarningStartTick()) {
isWarning = true;
}
}
return true;
}
/**
* Find the ground level at a given position.
* Searches downward from the given Y to find a solid block.
*
* @param world The world
* @param x X coordinate
* @param startY Starting Y coordinate (entity feet position)
* @param z Z coordinate
* @return The Y coordinate of the ground surface
*/
private double findGroundLevel(World world, double x, double startY, double z) {
int blockX = (int) Math.floor(x);
int blockZ = (int) Math.floor(z);
int startBlockY = (int) Math.floor(startY);
// Search downward for solid ground (max 10 blocks down)
for (int checkY = startBlockY; checkY >= startBlockY - 10 && checkY >= 0; checkY--) {
Block block = world.getBlock(blockX, checkY, blockZ);
if (block != null && block.getMaterial().isSolid()) {
// Found solid block, telegraph goes on top of it
return checkY + 1;
}
}
// No ground found, use original position
return startY;
}
/**
* Get progress from 0.0 (just started) to 1.0 (finished).
*/
public float getProgress() {
if (totalTicks <= 0) return 1.0f;
return 1.0f - ((float) remainingTicks / (float) totalTicks);
}
/**
* Get the current display color (normal or warning).
*/
public int getCurrentColor() {
if (telegraph == null) return 0x80FF0000;
return isWarning ? telegraph.getWarningColor() : telegraph.getColor();
}
/**
* Get alpha-modulated color for pulsing animation.
*/
public int getAnimatedColor(float partialTicks) {
int baseColor = getCurrentColor();
if (telegraph == null || !telegraph.isAnimated()) {
return baseColor;
}
// Pulsing animation: modulate alpha based on time
float time = (totalTicks - remainingTicks) + partialTicks;
float pulse = (float) (Math.sin(time * 0.3) * 0.3 + 0.7); // 0.4 to 1.0 range
int alpha = (baseColor >> 24) & 0xFF;
int r = (baseColor >> 16) & 0xFF;
int g = (baseColor >> 8) & 0xFF;
int b = baseColor & 0xFF;
alpha = (int) (alpha * pulse);
return (alpha << 24) | (r << 16) | (g << 8) | b;
}
// ═══════════════════════════════════════════════════════════════════
// NBT SERIALIZATION
// ═══════════════════════════════════════════════════════════════════
public NBTTagCompound writeNBT() {
NBTTagCompound nbt = new NBTTagCompound();
nbt.setString("instanceId", instanceId);
if (telegraph != null) {
nbt.setTag("telegraph", telegraph.writeNBT());
}
nbt.setDouble("x", x);
nbt.setDouble("y", y);
nbt.setDouble("z", z);
nbt.setFloat("yaw", yaw);
nbt.setInteger("entityIdToFollow", entityIdToFollow);
nbt.setInteger("casterEntityId", casterEntityId);
nbt.setInteger("remainingTicks", remainingTicks);
nbt.setInteger("totalTicks", totalTicks);
nbt.setBoolean("isWarning", isWarning);
return nbt;
}
public void readNBT(NBTTagCompound nbt) {
this.instanceId = nbt.getString("instanceId");
if (nbt.hasKey("telegraph")) {
this.telegraph = new Telegraph();
this.telegraph.readNBT(nbt.getCompoundTag("telegraph"));
}
this.x = nbt.getDouble("x");
this.y = nbt.getDouble("y");
this.z = nbt.getDouble("z");
this.yaw = nbt.getFloat("yaw");
this.entityIdToFollow = nbt.getInteger("entityIdToFollow");
this.casterEntityId = nbt.getInteger("casterEntityId");
this.remainingTicks = nbt.getInteger("remainingTicks");
this.totalTicks = nbt.getInteger("totalTicks");
this.isWarning = nbt.getBoolean("isWarning");
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS & SETTERS
// ═══════════════════════════════════════════════════════════════════
public String getInstanceId() { return instanceId; }
public void setInstanceId(String instanceId) { this.instanceId = instanceId; }
public Telegraph getTelegraph() { return telegraph; }
public void setTelegraph(Telegraph telegraph) { this.telegraph = telegraph; }
public double getX() { return x; }
public void setX(double x) { this.x = x; }
public double getY() { return y; }
public void setY(double y) { this.y = y; }
public double getZ() { return z; }
public void setZ(double z) { this.z = z; }
public float getYaw() { return yaw; }
public void setYaw(float yaw) { this.yaw = yaw; }
public int getEntityIdToFollow() { return entityIdToFollow; }
public void setEntityIdToFollow(int entityIdToFollow) { this.entityIdToFollow = entityIdToFollow; }
/**
* Locks the telegraph at its current position.
* Stops following any entity - used when ability commits to action.
*/
public void lockPosition() { this.entityIdToFollow = -1; }
public int getCasterEntityId() { return casterEntityId; }
public void setCasterEntityId(int casterEntityId) { this.casterEntityId = casterEntityId; }
public int getRemainingTicks() { return remainingTicks; }
public void setRemainingTicks(int remainingTicks) { this.remainingTicks = remainingTicks; }
public int getTotalTicks() { return totalTicks; }
public void setTotalTicks(int totalTicks) { this.totalTicks = totalTicks; }
public boolean isWarning() { return isWarning; }
public void setWarning(boolean warning) { isWarning = warning; }
}
@@ -0,0 +1,139 @@
package kamkeel.npcs.controllers.data.ability.telegraph;
import net.minecraft.world.World;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages active telegraph instances.
* There's one instance per world (client and server).
*/
public class TelegraphManager {
/** Client-side instance for rendering */
public static TelegraphManager ClientInstance;
private final ConcurrentHashMap<String, TelegraphInstance> telegraphs = new ConcurrentHashMap<>();
public TelegraphManager() {}
/**
* Initialize the client-side manager.
* Call from client proxy during mod init.
*/
public static void initClient() {
ClientInstance = new TelegraphManager();
}
// ═══════════════════════════════════════════════════════════════════
// TELEGRAPH MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
/**
* Add a new telegraph instance.
*/
public void addTelegraph(TelegraphInstance instance) {
telegraphs.put(instance.getInstanceId(), instance);
}
/**
* Remove a telegraph by ID.
*/
public void removeTelegraph(String instanceId) {
telegraphs.remove(instanceId);
}
/**
* Get a telegraph by ID.
*/
public TelegraphInstance getTelegraph(String instanceId) {
return telegraphs.get(instanceId);
}
/**
* Get all active telegraphs for rendering.
*/
public Collection<TelegraphInstance> getTelegraphs() {
return telegraphs.values();
}
/**
* Clear all telegraphs.
*/
public void clear() {
telegraphs.clear();
}
/**
* Check if any telegraphs are active.
*/
public boolean hasTelegraphs() {
return !telegraphs.isEmpty();
}
// ═══════════════════════════════════════════════════════════════════
// TICK
// ═══════════════════════════════════════════════════════════════════
/**
* Tick all telegraphs, removing expired ones.
* Call from client tick handler.
*/
public void tick(World world) {
Iterator<TelegraphInstance> iterator = telegraphs.values().iterator();
while (iterator.hasNext()) {
TelegraphInstance instance = iterator.next();
if (!instance.tick(world)) {
iterator.remove();
}
}
}
// ═══════════════════════════════════════════════════════════════════
// HELPER METHODS FOR ABILITIES
// ═══════════════════════════════════════════════════════════════════
/**
* Spawn a circle telegraph at a position.
*/
public TelegraphInstance spawnCircle(double x, double y, double z, float radius, int durationTicks, int color) {
Telegraph telegraph = Telegraph.circle(radius);
telegraph.setDurationTicks(durationTicks);
telegraph.setColor(color);
telegraph.setWarningColor((color & 0x00FFFFFF) | 0xC0000000);
TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, 0);
addTelegraph(instance);
return instance;
}
/**
* Spawn a line telegraph from a position in a direction.
*/
public TelegraphInstance spawnLine(double x, double y, double z, float yaw, float length, float width, int durationTicks, int color) {
Telegraph telegraph = Telegraph.line(length, width);
telegraph.setDurationTicks(durationTicks);
telegraph.setColor(color);
telegraph.setWarningColor((color & 0x00FFFFFF) | 0xC0000000);
TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw);
addTelegraph(instance);
return instance;
}
/**
* Spawn a cone telegraph from a position in a direction.
*/
public TelegraphInstance spawnCone(double x, double y, double z, float yaw, float length, float angle, int durationTicks, int color) {
Telegraph telegraph = Telegraph.cone(length, angle);
telegraph.setDurationTicks(durationTicks);
telegraph.setColor(color);
telegraph.setWarningColor((color & 0x00FFFFFF) | 0xC0000000);
TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw);
addTelegraph(instance);
return instance;
}
}
@@ -0,0 +1,24 @@
package kamkeel.npcs.controllers.data.ability.telegraph;
/**
* Types of telegraph shapes for visual attack warnings.
*/
public enum TelegraphType {
/** Filled circle (uses radius) */
CIRCLE,
/** Donut shape (uses radius and innerRadius) */
RING,
/** Rectangle (uses length, width) */
LINE,
/** Fan/wedge shape (uses length, angle) */
CONE,
/** Single point indicator */
POINT,
/** No telegraph */
NONE
}
@@ -0,0 +1,288 @@
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 noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityBeam;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Beam ability: Continuous line attack that damages all targets in a line.
* Can sweep across an arc or remain fixed.
*/
public class AbilityBeam extends Ability {
// Type-specific parameters
private float beamLength = 10.0f;
private float beamWidth = 1.0f;
private float damage = 5.0f;
private int damageInterval = 5;
private boolean piercing = true;
private float sweepAngle = 0.0f;
private float sweepSpeed = 2.0f;
private boolean sweepBackAndForth = true;
private boolean lockOnTarget = false;
// Runtime state (transient)
private transient float currentSweepAngle = 0;
private transient boolean sweepingRight = true;
private transient int ticksSinceDamage = 0;
private transient Set<Integer> hitThisTick = new HashSet<>();
public AbilityBeam() {
this.typeId = "ability.cnpc.beam";
this.name = "Beam";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.minRange = 0.0f;
this.lockMovement = true;
this.cooldownTicks = 200;
this.windUpTicks = 60;
this.activeTicks = 80;
this.recoveryTicks = 20;
// No telegraph for beam - it's a continuous line attack
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(
IAbilityConfigCallback callback) {
return new SubGuiAbilityBeam(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AGGRO_TARGET };
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
// Initialize beam
currentSweepAngle = -sweepAngle / 2;
sweepingRight = true;
ticksSinceDamage = damageInterval; // Deal damage immediately
hitThisTick.clear();
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
hitThisTick.clear();
ticksSinceDamage++;
// Update sweep angle
if (sweepAngle > 0) {
if (sweepBackAndForth) {
if (sweepingRight) {
currentSweepAngle += sweepSpeed;
if (currentSweepAngle >= sweepAngle / 2) {
sweepingRight = false;
}
} else {
currentSweepAngle -= sweepSpeed;
if (currentSweepAngle <= -sweepAngle / 2) {
sweepingRight = true;
}
}
} else {
currentSweepAngle += sweepSpeed;
if (currentSweepAngle >= sweepAngle) {
currentSweepAngle = 0;
}
}
}
// Calculate beam direction
float baseYaw = npc.rotationYaw;
// If lock on target, point at target
if (lockOnTarget && target != null) {
double dx = target.posX - npc.posX;
double dz = target.posZ - npc.posZ;
baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
}
float beamYaw = baseYaw + currentSweepAngle;
float yawRad = (float) Math.toRadians(beamYaw);
// Calculate beam start position
double startX = npc.posX;
double startY = npc.posY + npc.getEyeHeight() * 0.8;
double startZ = npc.posZ;
double dirX = -Math.sin(yawRad);
double dirZ = Math.cos(yawRad);
// Check for entities along the beam
if (ticksSinceDamage >= damageInterval) {
ticksSinceDamage = 0;
// Sample points along the beam
int samples = (int) (beamLength / 0.5);
for (int i = 1; i <= samples; i++) {
double progress = (double) i / samples;
double checkX = startX + dirX * beamLength * progress;
double checkY = startY;
double checkZ = startZ + dirZ * beamLength * progress;
// Find entities at this point
AxisAlignedBB checkBox = AxisAlignedBB.getBoundingBox(
checkX - beamWidth, checkY - 1, checkZ - beamWidth,
checkX + beamWidth, checkY + 2, checkZ + beamWidth
);
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, checkBox);
for (Entity entity : entities) {
if (!(entity instanceof EntityLivingBase)) continue;
if (entity == npc) continue;
if (hitThisTick.contains(entity.getEntityId())) continue;
EntityLivingBase livingEntity = (EntityLivingBase) entity;
// Check if actually in beam path
if (isInBeamPath(livingEntity, startX, startY, startZ, dirX, dirZ)) {
hitThisTick.add(entity.getEntityId());
// Apply damage with scripted event support (no knockback for beam)
applyAbilityDamage(npc, livingEntity, damage, 0);
if (!piercing) {
return; // Stop at first hit
}
}
}
}
}
// Spawn beam particles
spawnBeamParticles(world, startX, startY, startZ, dirX, dirZ);
}
private boolean isInBeamPath(EntityLivingBase entity, double startX, double startY, double startZ,
double dirX, double dirZ) {
// Project entity position onto beam line
double entityX = entity.posX - startX;
double entityZ = entity.posZ - startZ;
// Dot product to find projection distance
double projectionDist = entityX * dirX + entityZ * dirZ;
// Check if within beam length
if (projectionDist < 0 || projectionDist > beamLength) {
return false;
}
// Calculate perpendicular distance
double projX = dirX * projectionDist;
double projZ = dirZ * projectionDist;
double perpX = entityX - projX;
double perpZ = entityZ - projZ;
double perpDist = Math.sqrt(perpX * perpX + perpZ * perpZ);
// Check if within beam width
return perpDist <= beamWidth + entity.width / 2;
}
private void spawnBeamParticles(World world, double startX, double startY, double startZ,
double dirX, double dirZ) {
// Spawn particles along beam line
for (int i = 0; i < beamLength; i++) {
double px = startX + dirX * i;
double pz = startZ + dirZ * i;
world.spawnParticle("flame", px, startY, pz, 0, 0, 0);
}
}
@Override
public void reset() {
super.reset();
currentSweepAngle = 0;
sweepingRight = true;
ticksSinceDamage = 0;
hitThisTick.clear();
}
@Override
public float getTelegraphRadius() {
return beamLength;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("beamLength", beamLength);
nbt.setFloat("beamWidth", beamWidth);
nbt.setFloat("damage", damage);
nbt.setInteger("damageInterval", damageInterval);
nbt.setBoolean("piercing", piercing);
nbt.setFloat("sweepAngle", sweepAngle);
nbt.setFloat("sweepSpeed", sweepSpeed);
nbt.setBoolean("sweepBackAndForth", sweepBackAndForth);
nbt.setBoolean("lockOnTarget", lockOnTarget);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.beamLength = nbt.hasKey("beamLength") ? nbt.getFloat("beamLength") : 10.0f;
this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 1.0f;
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f;
this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 5;
this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing");
this.sweepAngle = nbt.hasKey("sweepAngle") ? nbt.getFloat("sweepAngle") : 0.0f;
this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 2.0f;
this.sweepBackAndForth = !nbt.hasKey("sweepBackAndForth") || nbt.getBoolean("sweepBackAndForth");
this.lockOnTarget = nbt.hasKey("lockOnTarget") && nbt.getBoolean("lockOnTarget");
}
// Getters & Setters
public float getBeamLength() { return beamLength; }
public void setBeamLength(float beamLength) { this.beamLength = beamLength; }
public float getBeamWidth() { return beamWidth; }
public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; }
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public int getDamageInterval() { return damageInterval; }
public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; }
public boolean isPiercing() { return piercing; }
public void setPiercing(boolean piercing) { this.piercing = piercing; }
public float getSweepAngle() { return sweepAngle; }
public void setSweepAngle(float sweepAngle) { this.sweepAngle = sweepAngle; }
public float getSweepSpeed() { return sweepSpeed; }
public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; }
public boolean isSweepBackAndForth() { return sweepBackAndForth; }
public void setSweepBackAndForth(boolean sweepBackAndForth) { this.sweepBackAndForth = sweepBackAndForth; }
public boolean isLockOnTarget() { return lockOnTarget; }
public void setLockOnTarget(boolean lockOnTarget) { this.lockOnTarget = lockOnTarget; }
}
@@ -0,0 +1,319 @@
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.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.Entity;
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 net.minecraft.world.World;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCharge;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Charge ability: Rush attack where NPC charges in a line, damaging all targets hit.
*/
public class AbilityCharge extends Ability {
// Type-specific parameters
private float chargeSpeed = 0.8f;
private float damage = 15.0f;
private float knockback = 3.0f;
private float hitWidth = 1.5f;
// Runtime state (transient)
private transient double startX, startY, startZ;
private transient Vec3 chargeDirection;
private transient Set<Integer> hitEntities = new HashSet<>();
private transient float lockedYaw;
public AbilityCharge() {
this.typeId = "ability.cnpc.charge";
this.name = "Charge";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 20.0f;
this.minRange = 4.0f;
this.lockMovement = false; // Movement IS the ability
this.cooldownTicks = 80;
this.windUpTicks = 20;
this.activeTicks = 40;
this.recoveryTicks = 20;
// LINE telegraph showing charge path
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
this.windUpSound = "mob.zombie.wood";
this.activeSound = "mob.zombie.attack";
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
return new SubGuiAbilityCharge(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
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(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (tick == 1) {
// Lock direction on first windup tick (same time telegraph is created)
lockChargeDirection(npc, target);
}
// Keep NPC facing the locked direction during windup
enforceLockedRotation(npc);
}
/**
* Locks the charge direction based on current target position.
* Called once at windup start - direction won't change even if target moves.
*/
private void lockChargeDirection(EntityNPCInterface npc, EntityLivingBase target) {
if (target != null) {
double dx = target.posX - npc.posX;
double dz = target.posZ - npc.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(npc.rotationYaw);
chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw));
}
} else {
float yaw = (float) Math.toRadians(npc.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(EntityNPCInterface npc, EntityLivingBase target, World world) {
// Initialize charge - direction was already locked during windup
startX = npc.posX;
startY = npc.posY;
startZ = npc.posZ;
hitEntities.clear();
// If direction wasn't set during windup (shouldn't happen), set it now
if (chargeDirection == null) {
lockChargeDirection(npc, target);
}
enforceLockedRotation(npc);
}
private void enforceLockedRotation(EntityNPCInterface npc) {
npc.rotationYaw = lockedYaw;
npc.rotationYawHead = lockedYaw;
npc.prevRotationYaw = lockedYaw;
npc.prevRotationYawHead = lockedYaw;
npc.renderYawOffset = lockedYaw;
npc.prevRenderYawOffset = lockedYaw;
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (chargeDirection == null) return;
// Enforce rotation every tick
enforceLockedRotation(npc);
// Calculate distance traveled
double distanceTraveled = Math.sqrt(
Math.pow(npc.posX - startX, 2) +
Math.pow(npc.posZ - startZ, 2)
);
// Check if reached max distance
if (distanceTraveled >= maxRange) {
npc.motionX = 0;
npc.motionZ = 0;
npc.velocityChanged = true;
return;
}
if (isChargeBlocked(npc)) {
stopMomentum(npc);
return;
}
// Move NPC
npc.motionX = chargeDirection.xCoord * chargeSpeed;
npc.motionY = 0;
npc.motionZ = chargeDirection.zCoord * chargeSpeed;
npc.velocityChanged = true;
// Server-side collision damage
if (!world.isRemote) {
AxisAlignedBB hitBox = npc.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth);
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
for (Entity entity : entities) {
if (!(entity instanceof EntityLivingBase)) continue;
if (entity == npc) continue;
if (hitEntities.contains(entity.getEntityId())) continue;
EntityLivingBase livingEntity = (EntityLivingBase) entity;
// Hit this entity
hitEntities.add(entity.getEntityId());
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamageWithDirection(npc, livingEntity, damage, knockback,
chargeDirection.xCoord, chargeDirection.zCoord);
// Play impact sound if hit wasn't cancelled
if (wasHit) {
world.playSoundAtEntity(livingEntity, "random.explode", 0.5f, 1.2f);
}
}
}
}
@Override
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {
stopMomentum(npc);
super.onComplete(npc, target);
}
@Override
public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) {
stopMomentum(npc);
super.onInterrupt(npc, source, damage);
}
private void stopMomentum(EntityNPCInterface npc) {
npc.motionX = 0;
npc.motionZ = 0;
npc.velocityChanged = true;
}
private boolean isChargeBlocked(EntityNPCInterface npc) {
double nextX = chargeDirection.xCoord * chargeSpeed;
double nextZ = chargeDirection.zCoord * chargeSpeed;
AxisAlignedBB nextBox = npc.boundingBox.copy().offset(nextX, 0, nextZ);
return !npc.worldObj.getCollidingBoundingBoxes(npc, nextBox).isEmpty();
}
@Override
public void reset() {
super.reset();
chargeDirection = null;
hitEntities.clear();
}
@Override
public float getTelegraphLength() {
return maxRange;
}
@Override
public float getTelegraphWidth() {
return hitWidth * 2;
}
/**
* Creates a LINE telegraph from NPC position towards target.
* Telegraph follows NPC during windup (so NPC can reposition before charging).
* Direction is locked at creation based on target position.
*/
@Override
public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) {
if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) {
return null;
}
// Calculate direction to target at this moment (locked)
float yaw;
if (target != null) {
double dx = target.posX - npc.posX;
double dz = target.posZ - npc.posZ;
yaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
} else {
yaw = npc.rotationYaw;
}
// Create LINE telegraph
Telegraph telegraph = Telegraph.line(getTelegraphLength(), getTelegraphWidth());
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at NPC ground level, direction towards target
double groundY = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ);
TelegraphInstance instance = new TelegraphInstance(telegraph, npc.posX, groundY, npc.posZ, yaw);
instance.setCasterEntityId(npc.getEntityId());
// Telegraph follows NPC during windup - allows NPC to reposition
instance.setEntityIdToFollow(npc.getEntityId());
return instance;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("chargeSpeed", chargeSpeed);
nbt.setFloat("damage", damage);
nbt.setFloat("knockback", knockback);
nbt.setFloat("hitWidth", hitWidth);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.chargeSpeed = nbt.hasKey("chargeSpeed") ? nbt.getFloat("chargeSpeed") : 0.8f;
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f;
this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 3.0f;
if (nbt.hasKey("hitWidth")) {
this.hitWidth = nbt.getFloat("hitWidth");
} else if (nbt.hasKey("hitRadius")) {
this.hitWidth = nbt.getFloat("hitRadius");
} else {
this.hitWidth = 1.5f;
}
}
// Getters & Setters
public float getChargeSpeed() { return chargeSpeed; }
public void setChargeSpeed(float chargeSpeed) { this.chargeSpeed = chargeSpeed; }
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getKnockback() { return knockback; }
public void setKnockback(float knockback) { this.knockback = knockback; }
public float getHitWidth() { return hitWidth; }
public void setHitWidth(float hitWidth) { this.hitWidth = hitWidth; }
}
@@ -0,0 +1,260 @@
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.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.potion.Potion;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCutter;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Cutter ability: Sweeping fan attack in an arc.
* Deals damage to all entities in a cone/arc in front of the caster.
* Can sweep outward over time or hit instantly.
*/
public class AbilityCutter extends Ability {
public enum SweepMode {
SWIPE,
SPIN
}
private float arcAngle = 90.0f;
private float range = 6.0f;
private float damage = 12.0f;
private float knockback = 1.5f;
private SweepMode sweepMode = SweepMode.SWIPE;
private float sweepSpeed = 6.0f;
private int stunDuration = 0;
private int poisonDurationSeconds = 0;
private int poisonLevel = 0;
private boolean piercing = true;
private float innerRadius = 0.0f;
// Runtime state
private transient Set<Integer> hitEntities = new HashSet<>();
private transient float currentRotation = 0.0f;
public AbilityCutter() {
this.typeId = "ability.cnpc.cutter";
this.name = "Cutter";
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 8.0f;
this.lockMovement = true;
this.cooldownTicks = 80;
this.windUpTicks = 20;
this.activeTicks = 15;
this.recoveryTicks = 15;
this.telegraphType = TelegraphType.CONE;
this.windUpSound = "random.bow";
this.activeSound = "random.break";
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
return new SubGuiAbilityCutter(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AOE_SELF };
}
@Override
public float getTelegraphRadius() { return range; }
@Override
public float getTelegraphLength() { return range; }
@Override
public float getTelegraphAngle() { return arcAngle; }
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
hitEntities.clear();
currentRotation = -arcAngle / 2.0f;
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
switch (sweepMode) {
case SWIPE:
if (currentRotation > arcAngle / 2.0f) {
return;
}
performSweepDamage(npc, world, innerRadius, range, currentRotation);
currentRotation += sweepSpeed;
break;
case SPIN:
currentRotation = (currentRotation + sweepSpeed) % 360.0f;
hitEntities.clear();
performSweepDamage(npc, world, innerRadius, range, currentRotation);
break;
}
}
private void performSweepDamage(EntityNPCInterface npc, World world, float minDist, float maxDist, float angleOffset) {
float casterYaw = npc.rotationYaw + angleOffset;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
npc.posX - maxDist, npc.posY - 1, npc.posZ - maxDist,
npc.posX + maxDist, npc.posY + 3, npc.posZ + maxDist
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
for (EntityLivingBase entity : entities) {
if (entity == npc) continue;
if (hitEntities.contains(entity.getEntityId())) continue;
// If not piercing, stop after hitting one entity this sweep
if (!piercing && !hitEntities.isEmpty()) break;
double dx = entity.posX - npc.posX;
double dz = entity.posZ - npc.posZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist < minDist || dist > maxDist) continue;
if (!isInArc(dx, dz, casterYaw, arcAngle)) continue;
hitEntities.add(entity.getEntityId());
float distFactor = 1.0f - ((float)dist / maxDist) * 0.3f;
float actualDamage = damage * distFactor;
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(npc, entity, actualDamage, knockback);
// Only apply effects if hit wasn't cancelled
if (wasHit) {
if (stunDuration > 0) {
entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10));
entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2));
}
if (poisonDurationSeconds > 0) {
entity.addPotionEffect(new PotionEffect(Potion.poison.id, poisonDurationSeconds * 20, poisonLevel));
}
}
}
}
private boolean isInArc(double dx, double dz, float casterYaw, float arcWidth) {
double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz));
double angleDiff = normalizeAngle(angleToEntity - casterYaw);
return Math.abs(angleDiff) <= arcWidth / 2.0;
}
private double normalizeAngle(double angle) {
while (angle > 180) angle -= 360;
while (angle < -180) angle += 360;
return angle;
}
@Override
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {
hitEntities.clear();
currentRotation = 0.0f;
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
hitEntities.clear();
currentRotation = 0.0f;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("arcAngle", arcAngle);
nbt.setFloat("range", range);
nbt.setFloat("damage", damage);
nbt.setFloat("knockback", knockback);
nbt.setString("sweepMode", sweepMode.name());
nbt.setFloat("sweepSpeed", sweepSpeed);
nbt.setInteger("stunDuration", stunDuration);
nbt.setInteger("poisonDurationSeconds", poisonDurationSeconds);
nbt.setInteger("poisonLevel", poisonLevel);
nbt.setBoolean("piercing", piercing);
nbt.setFloat("innerRadius", innerRadius);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.arcAngle = nbt.hasKey("arcAngle") ? nbt.getFloat("arcAngle") : 90.0f;
this.range = nbt.hasKey("range") ? nbt.getFloat("range") : 6.0f;
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 12.0f;
this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f;
try {
this.sweepMode = SweepMode.valueOf(nbt.getString("sweepMode"));
} catch (Exception e) {
this.sweepMode = SweepMode.SWIPE;
}
this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 6.0f;
this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0;
this.poisonDurationSeconds = nbt.hasKey("poisonDurationSeconds") ? nbt.getInteger("poisonDurationSeconds") : 0;
this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : 0;
this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing");
this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f;
}
// Getters & Setters
public float getArcAngle() { return arcAngle; }
public void setArcAngle(float arcAngle) { this.arcAngle = arcAngle; }
public float getRange() { return range; }
public void setRange(float range) { this.range = range; }
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getKnockback() { return knockback; }
public void setKnockback(float knockback) { this.knockback = knockback; }
public SweepMode getSweepMode() { return sweepMode; }
public void setSweepMode(SweepMode sweepMode) { this.sweepMode = sweepMode; }
public float getSweepSpeed() { return sweepSpeed; }
public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; }
public int getStunDuration() { return stunDuration; }
public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; }
public int getPoisonDurationSeconds() { return poisonDurationSeconds; }
public void setPoisonDurationSeconds(int poisonDurationSeconds) { this.poisonDurationSeconds = poisonDurationSeconds; }
public int getPoisonLevel() { return poisonLevel; }
public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; }
public boolean isPiercing() { return piercing; }
public void setPiercing(boolean piercing) { this.piercing = piercing; }
public float getInnerRadius() { return innerRadius; }
public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; }
public float getCurrentRotation() { return currentRotation; }
}
@@ -0,0 +1,259 @@
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 noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDash;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.Random;
/**
* Dash ability: Quick evasive sidestep with NO telegraph.
* Defensive repositioning move to evade attacks.
*/
public class AbilityDash extends Ability {
/**
* Dash behavior mode.
*/
public enum DashMode {
AGGRESSIVE,
DEFENSIVE
}
/**
* Specific dash direction.
*/
public enum DashDirection {
FORWARD(0),
DIAGONAL_FORWARD_LEFT(45),
DIAGONAL_FORWARD_RIGHT(-45),
LEFT(90),
RIGHT(-90),
DIAGONAL_BACK_LEFT(135),
DIAGONAL_BACK_RIGHT(-135),
BACK(180);
private final float angleOffset;
DashDirection(float angleOffset) {
this.angleOffset = angleOffset;
}
public float getAngleOffset() {
return angleOffset;
}
}
private static final Random RANDOM = new Random();
private static final DashDirection[] AGGRESSIVE_DIRECTIONS = {
DashDirection.FORWARD,
DashDirection.DIAGONAL_FORWARD_LEFT,
DashDirection.DIAGONAL_FORWARD_RIGHT
};
private static final DashDirection[] DEFENSIVE_DIRECTIONS = {
DashDirection.LEFT,
DashDirection.RIGHT,
DashDirection.BACK,
DashDirection.DIAGONAL_BACK_LEFT,
DashDirection.DIAGONAL_BACK_RIGHT
};
// Type-specific parameters
private DashMode dashMode = DashMode.DEFENSIVE;
private float dashDistance = 4.0f;
private float dashSpeed = 1.5f;
// Runtime state
private transient Vec3 dashDirection;
private transient double startX, startY, startZ;
private transient DashDirection chosenDirection;
public AbilityDash() {
this.typeId = "ability.cnpc.dash";
this.name = "Dash";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 20.0f;
this.minRange = 0.0f;
this.lockMovement = false;
this.cooldownTicks = 40;
this.windUpTicks = 5;
this.activeTicks = 8;
this.recoveryTicks = 10;
// No telegraph for dash - it's a quick evasive move
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
this.windUpSound = "mob.bat.takeoff";
this.activeSound = "mob.endermen.portal";
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(
IAbilityConfigCallback callback) {
return new SubGuiAbilityDash(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AGGRO_TARGET };
}
@Override
public boolean hasAbilityMovement() {
return true; // This ability moves the NPC
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
startX = npc.posX;
startY = npc.posY;
startZ = npc.posZ;
// Choose random direction based on mode
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
? AGGRESSIVE_DIRECTIONS
: DEFENSIVE_DIRECTIONS;
chosenDirection = directions[RANDOM.nextInt(directions.length)];
// Calculate dash direction relative to target
float baseYaw;
if (target != null) {
double dx = target.posX - npc.posX;
double dz = target.posZ - npc.posZ;
baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
} else {
baseYaw = npc.rotationYaw;
}
// Apply direction offset
float dashYaw = baseYaw + chosenDirection.getAngleOffset();
float yawRad = (float) Math.toRadians(dashYaw);
dashDirection = Vec3.createVectorHelper(
-Math.sin(yawRad),
0,
Math.cos(yawRad)
);
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (dashDirection == null) return;
// Calculate distance traveled
double distanceTraveled = Math.sqrt(
Math.pow(npc.posX - startX, 2) +
Math.pow(npc.posZ - startZ, 2)
);
// Check if reached max distance
if (distanceTraveled >= dashDistance) {
npc.motionX = 0;
npc.motionZ = 0;
npc.velocityChanged = true;
return;
}
if (isDashBlocked(npc)) {
npc.motionX = 0;
npc.motionZ = 0;
npc.velocityChanged = true;
return;
}
// Move NPC
npc.motionX = dashDirection.xCoord * dashSpeed;
npc.motionY = 0;
npc.motionZ = dashDirection.zCoord * dashSpeed;
npc.velocityChanged = true;
// Trail particles
world.spawnParticle("smoke", npc.posX, npc.posY + 0.5, npc.posZ, 0, 0, 0);
}
@Override
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {
npc.motionX = 0;
npc.motionZ = 0;
npc.velocityChanged = true;
dashDirection = null;
chosenDirection = null;
}
@Override
public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) {
npc.motionX = 0;
npc.motionZ = 0;
npc.velocityChanged = true;
dashDirection = null;
chosenDirection = null;
}
private boolean isDashBlocked(EntityNPCInterface npc) {
double nextX = dashDirection.xCoord * dashSpeed;
double nextZ = dashDirection.zCoord * dashSpeed;
return !npc.worldObj.getCollidingBoundingBoxes(npc, npc.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty();
}
@Override
public void reset() {
super.reset();
dashDirection = null;
chosenDirection = null;
}
@Override
public float getTelegraphRadius() {
return 0; // No telegraph for dash
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setString("dashMode", dashMode.name());
nbt.setFloat("dashDistance", dashDistance);
nbt.setFloat("dashSpeed", dashSpeed);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
try {
this.dashMode = DashMode.valueOf(nbt.getString("dashMode"));
} catch (Exception e) {
this.dashMode = DashMode.DEFENSIVE;
}
this.dashDistance = nbt.hasKey("dashDistance") ? nbt.getFloat("dashDistance") : 4.0f;
this.dashSpeed = nbt.hasKey("dashSpeed") ? nbt.getFloat("dashSpeed") : 1.5f;
}
// Getters & Setters
public DashMode getDashMode() { return dashMode; }
public void setDashMode(DashMode dashMode) { this.dashMode = dashMode; }
public float getDashDistance() { return dashDistance; }
public void setDashDistance(float dashDistance) { this.dashDistance = dashDistance; }
public float getDashSpeed() { return dashSpeed; }
public void setDashSpeed(float dashSpeed) { this.dashSpeed = dashSpeed; }
public DashDirection getChosenDirection() { return chosenDirection; }
}
@@ -0,0 +1,235 @@
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 noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityGuard;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.Random;
/**
* Guard ability: Defensive stance that reduces incoming damage.
* Can optionally counter-attack when hit.
*/
public class AbilityGuard extends Ability {
private static final Random RANDOM = new Random();
// Type-specific parameters
private float damageReduction = 0.5f;
private boolean canCounter = false;
private CounterType counterType = CounterType.FLAT;
private float counterValue = 10.0f;
private float counterChance = 0.3f;
private String counterSound = "random.wood_click";
private int counterAnimationId = -1;
// Runtime state
private transient EntityLivingBase lastAttacker;
private transient boolean counterTriggered;
private transient float lastDamageTaken;
public enum CounterType {
FLAT,
PERCENT
}
public AbilityGuard() {
this.typeId = "ability.cnpc.guard";
this.name = "Guard";
this.targetingMode = TargetingMode.SELF;
this.lockMovement = true;
this.cooldownTicks = 120;
this.windUpTicks = 10;
this.activeTicks = 60;
this.recoveryTicks = 20;
this.interruptible = false; // Hard to interrupt while guarding
// No telegraph for guard - it's a defensive stance
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
this.windUpSound = "random.anvil_use";
this.activeSound = "random.anvil_land";
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(
IAbilityConfigCallback callback) {
return new SubGuiAbilityGuard(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.SELF };
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
lastAttacker = null;
counterTriggered = false;
lastDamageTaken = 0.0f;
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
// Counter attack logic if triggered
if (canCounter && counterTriggered && lastAttacker != null && !world.isRemote) {
performCounter(npc, world);
}
}
/**
* Called externally when the NPC takes damage while guarding.
* This should be called from the damage handling code.
*
* @param attacker The entity that attacked
* @param damage The damage amount (after reduction)
*/
public void onDamageTaken(EntityNPCInterface npc, EntityLivingBase attacker, DamageSource source, float damage) {
if (!canCounter || attacker == null) return;
if (!isDirectHit(source)) return;
if (RANDOM.nextFloat() >= counterChance) return;
lastAttacker = attacker;
lastDamageTaken = damage;
counterTriggered = true;
if (interruptible) {
performCounter(npc, npc.worldObj);
npc.abilities.interruptCurrentAbility(source, damage);
}
}
private void performCounter(EntityNPCInterface npc, World world) {
float counterDamage = counterType == CounterType.PERCENT
? lastDamageTaken * (counterValue / 100.0f)
: counterValue;
boolean wasHit = applyAbilityDamage(npc, lastAttacker, counterDamage, 0.5f);
if (wasHit && counterSound != null && !counterSound.isEmpty()) {
world.playSoundAtEntity(npc, counterSound, 1.0f, 1.2f);
}
if (counterAnimationId >= 0) {
if (noppes.npcs.controllers.AnimationController.Instance != null) {
noppes.npcs.controllers.data.Animation animation =
noppes.npcs.controllers.AnimationController.Instance.animations.get(counterAnimationId);
if (animation != null) {
npc.display.animationData.setEnabled(true);
npc.display.animationData.setAnimation(animation);
npc.display.animationData.updateClient();
}
}
}
counterTriggered = false;
lastAttacker = null;
lastDamageTaken = 0.0f;
}
private boolean isDirectHit(DamageSource source) {
if (source == null) return false;
if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion()) return false;
return source.getEntity() instanceof EntityLivingBase;
}
/**
* Get the damage reduction factor while guarding.
* Returns 0.5 for 50% reduction.
*/
public float getDamageReductionFactor() {
if (isExecuting() && getPhase() == kamkeel.npcs.controllers.data.ability.AbilityPhase.ACTIVE) {
return damageReduction;
}
return 0.0f;
}
/**
* Check if the NPC is currently in guard stance.
*/
public boolean isGuarding() {
return isExecuting() && getPhase() == kamkeel.npcs.controllers.data.ability.AbilityPhase.ACTIVE;
}
@Override
public void reset() {
super.reset();
lastAttacker = null;
counterTriggered = false;
lastDamageTaken = 0.0f;
}
@Override
public float getTelegraphRadius() {
return 0; // No telegraph for guard
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("damageReduction", damageReduction);
nbt.setBoolean("canCounter", canCounter);
nbt.setString("counterType", counterType.name());
nbt.setFloat("counterValue", counterValue);
nbt.setFloat("counterChance", counterChance);
nbt.setString("counterSound", counterSound);
nbt.setInteger("counterAnimationId", counterAnimationId);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.damageReduction = nbt.hasKey("damageReduction") ? nbt.getFloat("damageReduction") : 0.5f;
this.canCounter = nbt.hasKey("canCounter") && nbt.getBoolean("canCounter");
try {
this.counterType = CounterType.valueOf(nbt.getString("counterType"));
} catch (Exception e) {
this.counterType = CounterType.FLAT;
}
if (nbt.hasKey("counterValue")) {
this.counterValue = nbt.getFloat("counterValue");
} else if (nbt.hasKey("counterDamage")) {
this.counterValue = nbt.getFloat("counterDamage");
} else {
this.counterValue = 10.0f;
}
this.counterChance = nbt.hasKey("counterChance") ? nbt.getFloat("counterChance") : 0.3f;
this.counterSound = nbt.hasKey("counterSound") ? nbt.getString("counterSound") : "random.wood_click";
this.counterAnimationId = nbt.hasKey("counterAnimationId") ? nbt.getInteger("counterAnimationId") : -1;
}
// Getters & Setters
public float getDamageReduction() { return damageReduction; }
public void setDamageReduction(float damageReduction) { this.damageReduction = damageReduction; }
public boolean isCanCounter() { return canCounter; }
public void setCanCounter(boolean canCounter) { this.canCounter = canCounter; }
public CounterType getCounterType() { return counterType; }
public void setCounterType(CounterType counterType) { this.counterType = counterType; }
public float getCounterValue() { return counterValue; }
public void setCounterValue(float counterValue) { this.counterValue = counterValue; }
public float getCounterChance() { return counterChance; }
public void setCounterChance(float counterChance) { this.counterChance = counterChance; }
public String getCounterSound() { return counterSound; }
public void setCounterSound(String counterSound) { this.counterSound = counterSound; }
public int getCounterAnimationId() { return counterAnimationId; }
public void setCounterAnimationId(int counterAnimationId) { this.counterAnimationId = counterAnimationId; }
}
@@ -0,0 +1,481 @@
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 noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHazard;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.potion.Potion;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
/**
* Hazard ability: Creates persistent ground effect zones.
* Deals damage and/or applies debuffs to entities in the zone over time.
*/
public class AbilityHazard extends Ability {
public enum HazardShape {
CIRCLE,
RING,
CONE
}
public enum PlacementMode {
AT_CASTER,
AT_TARGET,
FOLLOW_CASTER,
FOLLOW_TARGET
}
private float radius = 4.0f;
private float innerRadius = 0.0f;
private float coneAngle = 45.0f;
private HazardShape shape = HazardShape.CIRCLE;
private PlacementMode placement = PlacementMode.AT_TARGET;
private float damagePerTick = 1.0f;
private int damageInterval = 20;
private boolean ignoreInvulnFrames = false;
private int slownessLevel = -1;
private int weaknessLevel = -1;
private int poisonLevel = -1;
private int witherLevel = -1;
private int blindnessLevel = -1;
private int debuffDuration = 40;
private boolean affectsCaster = false;
private float heightAbove = 2.0f;
private float heightBelow = 1.0f;
// Offset parameters - hazard spawns near target, not exactly on them
private float minOffset = 0.0f; // Minimum offset from target
private float maxOffset = 2.0f; // Maximum offset from target
private boolean randomOffset = true; // Random or fixed offset
// Runtime state
private static final Random RANDOM = new Random();
private transient double zoneX, zoneY, zoneZ;
private transient boolean positionLocked = false;
private transient int ticksSinceDamage = 0;
private transient Set<Integer> damagedThisTick = new HashSet<>();
public AbilityHazard() {
this.typeId = "ability.cnpc.hazard";
this.name = "Hazard";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.lockMovement = true;
this.cooldownTicks = 200;
this.windUpTicks = 30;
this.activeTicks = 100;
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.CIRCLE;
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(
IAbilityConfigCallback callback) {
return new SubGuiAbilityHazard(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AGGRO_TARGET };
}
@Override
public float getTelegraphRadius() { return radius; }
@Override
public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) {
TelegraphInstance instance = super.createTelegraph(npc, target);
if (instance == null) return null;
// Control telegraph following based on placement mode
switch (placement) {
case AT_CASTER:
case FOLLOW_CASTER:
// Telegraph stays at caster position, no following
instance.setEntityIdToFollow(-1);
instance.setX(npc.posX);
instance.setY(npc.posY);
instance.setZ(npc.posZ);
break;
case AT_TARGET:
// Telegraph follows target during windup, locks on execute
if (target != null) {
instance.setEntityIdToFollow(target.getEntityId());
}
break;
case FOLLOW_TARGET:
// Telegraph follows target during windup AND active phase
if (target != null) {
instance.setEntityIdToFollow(target.getEntityId());
}
break;
}
return instance;
}
/**
* Calculates offset position near the given coordinates.
* Used to place hazard near target rather than exactly on them.
*/
private double[] calculateOffsetPosition(double baseX, double baseY, double baseZ) {
if (maxOffset <= 0) {
return new double[]{baseX, baseY, baseZ};
}
double offsetDist;
double offsetAngle;
if (randomOffset) {
// Random offset within min-max range
offsetDist = minOffset + RANDOM.nextDouble() * (maxOffset - minOffset);
offsetAngle = RANDOM.nextDouble() * Math.PI * 2;
} else {
// Fixed offset (use max, random angle)
offsetDist = maxOffset;
offsetAngle = RANDOM.nextDouble() * Math.PI * 2;
}
double offsetX = Math.cos(offsetAngle) * offsetDist;
double offsetZ = Math.sin(offsetAngle) * offsetDist;
return new double[]{baseX + offsetX, baseY, baseZ + offsetZ};
}
@Override
public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
// AT_CASTER locks immediately, AT_TARGET follows during windup and locks on execute
if (tick == 0) {
switch (placement) {
case AT_CASTER:
zoneX = npc.posX;
zoneY = npc.posY;
zoneZ = npc.posZ;
positionLocked = true;
break;
case AT_TARGET:
// Position will be set from telegraph on execute
// Telegraph follows target during windup
positionLocked = false;
break;
default:
positionLocked = false;
break;
}
}
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
// Lock position from telegraph (which was following target) or calculate it now
TelegraphInstance telegraph = getTelegraphInstance();
switch (placement) {
case AT_CASTER:
// Already locked during windup
if (!positionLocked) {
zoneX = npc.posX;
zoneY = npc.posY;
zoneZ = npc.posZ;
}
break;
case AT_TARGET:
// Use telegraph position if available, apply offset
if (telegraph != null) {
double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ());
zoneX = pos[0];
zoneY = pos[1];
zoneZ = pos[2];
} else if (target != null) {
double[] pos = calculateOffsetPosition(target.posX, target.posY, target.posZ);
zoneX = pos[0];
zoneY = pos[1];
zoneZ = pos[2];
} else {
zoneX = npc.posX;
zoneY = npc.posY;
zoneZ = npc.posZ;
}
break;
case FOLLOW_CASTER:
zoneX = npc.posX;
zoneY = npc.posY;
zoneZ = npc.posZ;
break;
case FOLLOW_TARGET:
if (target != null) {
zoneX = target.posX;
zoneY = target.posY;
zoneZ = target.posZ;
} else {
zoneX = npc.posX;
zoneY = npc.posY;
zoneZ = npc.posZ;
}
break;
}
positionLocked = true;
ticksSinceDamage = damageInterval;
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (world.isRemote) return;
damagedThisTick.clear();
ticksSinceDamage++;
switch (placement) {
case FOLLOW_CASTER:
zoneX = npc.posX;
zoneY = npc.posY;
zoneZ = npc.posZ;
break;
case FOLLOW_TARGET:
if (target != null) {
zoneX = target.posX;
zoneY = target.posY;
zoneZ = target.posZ;
}
break;
default:
break;
}
if (ticksSinceDamage >= damageInterval) {
ticksSinceDamage = 0;
AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(
zoneX - radius, zoneY - heightBelow, zoneZ - radius,
zoneX + radius, zoneY + heightAbove, zoneZ + radius
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
for (EntityLivingBase entity : entities) {
if (entity == npc && !affectsCaster) continue;
if (damagedThisTick.contains(entity.getEntityId())) continue;
if (!isInZone(entity, npc)) continue;
if (damagePerTick > 0) {
if (ignoreInvulnFrames) {
entity.hurtResistantTime = 0;
}
// Apply damage with scripted event support (no knockback for hazard ticks)
boolean wasHit = applyAbilityDamage(npc, entity, damagePerTick, 0);
if (!wasHit) continue; // Skip debuffs if hit was cancelled
}
applyDebuffs(entity);
damagedThisTick.add(entity.getEntityId());
}
}
}
private boolean isInZone(EntityLivingBase entity, EntityNPCInterface npc) {
double dx = entity.posX - zoneX;
double dz = entity.posZ - zoneZ;
double dist = Math.sqrt(dx * dx + dz * dz);
switch (shape) {
case CIRCLE:
return dist <= radius;
case RING:
return dist >= innerRadius && dist <= radius;
case CONE:
if (dist > radius) return false;
float casterYaw = npc.rotationYaw;
double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz));
double angleDiff = Math.abs(normalizeAngle(angleToEntity - casterYaw));
return angleDiff <= coneAngle / 2;
default:
return dist <= radius;
}
}
private double normalizeAngle(double angle) {
while (angle > 180) angle -= 360;
while (angle < -180) angle += 360;
return angle;
}
private void applyDebuffs(EntityLivingBase target) {
if (slownessLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, debuffDuration, slownessLevel));
}
if (weaknessLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.weakness.id, debuffDuration, weaknessLevel));
}
if (poisonLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.poison.id, debuffDuration, poisonLevel));
}
if (witherLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.wither.id, debuffDuration, witherLevel));
}
if (blindnessLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.blindness.id, debuffDuration, blindnessLevel));
}
}
@Override
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {
damagedThisTick.clear();
positionLocked = false;
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
damagedThisTick.clear();
positionLocked = false;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("radius", radius);
nbt.setFloat("innerRadius", innerRadius);
nbt.setFloat("coneAngle", coneAngle);
nbt.setString("shape", shape.name());
nbt.setString("placement", placement.name());
nbt.setFloat("damagePerTick", damagePerTick);
nbt.setInteger("damageInterval", damageInterval);
nbt.setBoolean("ignoreInvulnFrames", ignoreInvulnFrames);
nbt.setInteger("slownessLevel", slownessLevel);
nbt.setInteger("weaknessLevel", weaknessLevel);
nbt.setInteger("poisonLevel", poisonLevel);
nbt.setInteger("witherLevel", witherLevel);
nbt.setInteger("blindnessLevel", blindnessLevel);
nbt.setInteger("debuffDuration", debuffDuration);
nbt.setBoolean("affectsCaster", affectsCaster);
nbt.setFloat("heightAbove", heightAbove);
nbt.setFloat("heightBelow", heightBelow);
nbt.setFloat("minOffset", minOffset);
nbt.setFloat("maxOffset", maxOffset);
nbt.setBoolean("randomOffset", randomOffset);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 4.0f;
this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f;
this.coneAngle = nbt.hasKey("coneAngle") ? nbt.getFloat("coneAngle") : 45.0f;
try {
this.shape = HazardShape.valueOf(nbt.getString("shape"));
} catch (Exception e) {
this.shape = HazardShape.CIRCLE;
}
try {
this.placement = PlacementMode.valueOf(nbt.getString("placement"));
} catch (Exception e) {
this.placement = PlacementMode.AT_TARGET;
}
this.damagePerTick = nbt.hasKey("damagePerTick") ? nbt.getFloat("damagePerTick") : 1.0f;
this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 20;
this.ignoreInvulnFrames = nbt.hasKey("ignoreInvulnFrames") && nbt.getBoolean("ignoreInvulnFrames");
this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : -1;
this.weaknessLevel = nbt.hasKey("weaknessLevel") ? nbt.getInteger("weaknessLevel") : -1;
this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : -1;
this.witherLevel = nbt.hasKey("witherLevel") ? nbt.getInteger("witherLevel") : -1;
this.blindnessLevel = nbt.hasKey("blindnessLevel") ? nbt.getInteger("blindnessLevel") : -1;
this.debuffDuration = nbt.hasKey("debuffDuration") ? nbt.getInteger("debuffDuration") : 40;
this.affectsCaster = nbt.hasKey("affectsCaster") && nbt.getBoolean("affectsCaster");
this.heightAbove = nbt.hasKey("heightAbove") ? nbt.getFloat("heightAbove") : 2.0f;
this.heightBelow = nbt.hasKey("heightBelow") ? nbt.getFloat("heightBelow") : 1.0f;
this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f;
this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 2.0f;
this.randomOffset = !nbt.hasKey("randomOffset") || nbt.getBoolean("randomOffset");
}
// Getters & Setters
public float getRadius() { return radius; }
public void setRadius(float radius) { this.radius = radius; }
public float getInnerRadius() { return innerRadius; }
public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; }
public float getConeAngle() { return coneAngle; }
public void setConeAngle(float coneAngle) { this.coneAngle = coneAngle; }
public HazardShape getShape() { return shape; }
public void setShape(HazardShape shape) { this.shape = shape; }
public PlacementMode getPlacement() { return placement; }
public void setPlacement(PlacementMode placement) { this.placement = placement; }
public float getDamagePerTick() { return damagePerTick; }
public void setDamagePerTick(float damagePerTick) { this.damagePerTick = damagePerTick; }
public int getDamageInterval() { return damageInterval; }
public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; }
public boolean isIgnoreInvulnFrames() { return ignoreInvulnFrames; }
public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { this.ignoreInvulnFrames = ignoreInvulnFrames; }
public int getSlownessLevel() { return slownessLevel; }
public void setSlownessLevel(int slownessLevel) { this.slownessLevel = slownessLevel; }
public int getWeaknessLevel() { return weaknessLevel; }
public void setWeaknessLevel(int weaknessLevel) { this.weaknessLevel = weaknessLevel; }
public int getPoisonLevel() { return poisonLevel; }
public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; }
public int getWitherLevel() { return witherLevel; }
public void setWitherLevel(int witherLevel) { this.witherLevel = witherLevel; }
public int getBlindnessLevel() { return blindnessLevel; }
public void setBlindnessLevel(int blindnessLevel) { this.blindnessLevel = blindnessLevel; }
public int getDebuffDuration() { return debuffDuration; }
public void setDebuffDuration(int debuffDuration) { this.debuffDuration = debuffDuration; }
public boolean isAffectsCaster() { return affectsCaster; }
public void setAffectsCaster(boolean affectsCaster) { this.affectsCaster = affectsCaster; }
public float getHeightAbove() { return heightAbove; }
public void setHeightAbove(float heightAbove) { this.heightAbove = heightAbove; }
public float getHeightBelow() { return heightBelow; }
public void setHeightBelow(float heightBelow) { this.heightBelow = heightBelow; }
public float getMinOffset() { return minOffset; }
public void setMinOffset(float minOffset) { this.minOffset = minOffset; }
public float getMaxOffset() { return maxOffset; }
public void setMaxOffset(float maxOffset) { this.maxOffset = maxOffset; }
public boolean isRandomOffset() { return randomOffset; }
public void setRandomOffset(boolean randomOffset) { this.randomOffset = randomOffset; }
// Runtime getters
public double getZoneX() { return zoneX; }
public double getZoneY() { return zoneY; }
public double getZoneZ() { return zoneZ; }
}
@@ -0,0 +1,217 @@
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.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.world.World;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeal;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.ArrayList;
import java.util.List;
/**
* Heal ability: Restore health to self or allies.
* Can heal a fixed amount or percentage of max health.
*/
public class AbilityHeal extends Ability {
// Type-specific parameters
private float healAmount = 10.0f;
private float healPercent = 0.0f;
private boolean healSelf = true;
private boolean healAllies = false;
private float healRadius = 0.0f;
private boolean instantHeal = true;
// Runtime state
private transient List<EntityLivingBase> healedAllies = new ArrayList<>();
public AbilityHeal() {
this.typeId = "ability.cnpc.heal";
this.name = "Heal";
this.targetingMode = TargetingMode.SELF;
this.lockMovement = true;
this.cooldownTicks = 200;
this.windUpTicks = 30;
this.activeTicks = 10;
this.recoveryTicks = 10;
// No telegraph for heal - it's a self/ally buff
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
return new SubGuiAbilityHeal(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.SELF };
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (world.isRemote) return;
healedAllies.clear();
if (instantHeal) {
if (healSelf) {
healEntity(npc);
spawnHealParticles(world, npc);
}
if (healAllies && healRadius > 0) {
healAlliesInRadius(npc, world);
}
}
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (world.isRemote || instantHeal) return;
// Heal over time - distribute heal across active ticks
if (tick % 10 == 0) {
// Calculate tick-based heal for fixed amount
float tickHeal = (healAmount / (float) activeTicks) * 10;
if (healSelf) {
float selfTickHeal = tickHeal;
// Add percentage-based heal portion
if (healPercent > 0) {
selfTickHeal += (npc.getMaxHealth() * healPercent / (float) activeTicks) * 10;
}
npc.heal(selfTickHeal);
}
if (healAllies && healRadius > 0) {
for (EntityLivingBase ally : healedAllies) {
if (!ally.isDead) {
float allyTickHeal = tickHeal;
// Add percentage-based heal portion for each ally
if (healPercent > 0) {
allyTickHeal += (ally.getMaxHealth() * healPercent / (float) activeTicks) * 10;
}
ally.heal(allyTickHeal);
}
}
}
}
}
private void healEntity(EntityLivingBase entity) {
float totalHeal = healAmount;
if (healPercent > 0) {
totalHeal += entity.getMaxHealth() * healPercent;
}
entity.heal(totalHeal);
}
private void healAlliesInRadius(EntityNPCInterface npc, World world) {
AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox(
npc.posX - healRadius, npc.posY - 2, npc.posZ - healRadius,
npc.posX + healRadius, npc.posY + 3, npc.posZ + healRadius
);
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, aabb);
for (Entity entity : entities) {
if (!(entity instanceof EntityLivingBase)) continue;
if (entity == npc) continue;
EntityLivingBase living = (EntityLivingBase) entity;
// Only heal other NPCs (allies)
if (living instanceof EntityNPCInterface) {
float dist = npc.getDistanceToEntity(living);
if (dist <= healRadius) {
healEntity(living);
healedAllies.add(living);
spawnHealParticles(world, living);
}
}
}
}
private void spawnHealParticles(World world, EntityLivingBase entity) {
for (int i = 0; i < 10; i++) {
double offsetX = (world.rand.nextDouble() - 0.5) * entity.width;
double offsetY = world.rand.nextDouble() * entity.height;
double offsetZ = (world.rand.nextDouble() - 0.5) * entity.width;
world.spawnParticle("happyVillager",
entity.posX + offsetX,
entity.posY + offsetY,
entity.posZ + offsetZ,
0, 0.1, 0);
}
}
@Override
public void reset() {
super.reset();
healedAllies.clear();
}
@Override
public float getTelegraphRadius() {
return healRadius > 0 ? healRadius : 0;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("healAmount", healAmount);
nbt.setFloat("healPercent", healPercent);
nbt.setBoolean("healSelf", healSelf);
nbt.setBoolean("healAllies", healAllies);
nbt.setFloat("healRadius", healRadius);
nbt.setBoolean("instantHeal", instantHeal);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.healAmount = nbt.hasKey("healAmount") ? nbt.getFloat("healAmount") : 10.0f;
this.healPercent = nbt.hasKey("healPercent") ? nbt.getFloat("healPercent") : 0.0f;
this.healSelf = !nbt.hasKey("healSelf") || nbt.getBoolean("healSelf");
this.healAllies = nbt.hasKey("healAllies") && nbt.getBoolean("healAllies");
this.healRadius = nbt.hasKey("healRadius") ? nbt.getFloat("healRadius") : 0.0f;
this.instantHeal = !nbt.hasKey("instantHeal") || nbt.getBoolean("instantHeal");
}
// Getters & Setters
public float getHealAmount() { return healAmount; }
public void setHealAmount(float healAmount) { this.healAmount = healAmount; }
public float getHealPercent() { return healPercent; }
public void setHealPercent(float healPercent) { this.healPercent = healPercent; }
public boolean isHealSelf() { return healSelf; }
public void setHealSelf(boolean healSelf) { this.healSelf = healSelf; }
public boolean isHealAllies() { return healAllies; }
public void setHealAllies(boolean healAllies) { this.healAllies = healAllies; }
public float getHealRadius() { return healRadius; }
public void setHealRadius(float healRadius) { this.healRadius = healRadius; }
public boolean isInstantHeal() { return instantHeal; }
public void setInstantHeal(boolean instantHeal) { this.instantHeal = instantHeal; }
}
@@ -0,0 +1,122 @@
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 noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeavyHit;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.potion.Potion;
import net.minecraft.potion.PotionEffect;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
/**
* Heavy Hit ability: Single-target melee attack with optional stun.
* Deals high damage to one target and can stun them.
*/
public class AbilityHeavyHit extends Ability {
private float damage = 15.0f;
private float knockback = 2.0f;
private int slownessLevel = 1;
private int weaknessLevel = 0;
private int potionDurationSeconds = 2;
public AbilityHeavyHit() {
this.typeId = "ability.cnpc.heavy_hit";
this.name = "Heavy Hit";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 3.0f;
this.minRange = 0.0f;
this.lockMovement = true;
this.cooldownTicks = 80;
this.windUpTicks = 30;
this.activeTicks = 5;
this.recoveryTicks = 20;
this.telegraphType = TelegraphType.POINT;
this.showTelegraph = false;
this.windUpSound = "random.anvil_use";
this.activeSound = "random.anvil_land";
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(
IAbilityConfigCallback callback) {
return new SubGuiAbilityHeavyHit(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AGGRO_TARGET };
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (world.isRemote || target == null) return;
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(npc, target, damage, knockback);
if (wasHit && potionDurationSeconds > 0) {
int durationTicks = potionDurationSeconds * 20;
if (slownessLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, durationTicks, slownessLevel));
}
if (weaknessLevel >= 0) {
target.addPotionEffect(new PotionEffect(Potion.weakness.id, durationTicks, weaknessLevel));
}
}
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
// Heavy Hit is instant, no active tick needed
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("damage", damage);
nbt.setFloat("knockback", knockback);
nbt.setInteger("slownessLevel", slownessLevel);
nbt.setInteger("weaknessLevel", weaknessLevel);
nbt.setInteger("potionDurationSeconds", potionDurationSeconds);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f;
this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 2.0f;
this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : 1;
this.weaknessLevel = nbt.hasKey("weaknessLevel") ? nbt.getInteger("weaknessLevel") : 0;
this.potionDurationSeconds = nbt.hasKey("potionDurationSeconds") ? nbt.getInteger("potionDurationSeconds") : 2;
}
// Getters & Setters
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getKnockback() { return knockback; }
public void setKnockback(float knockback) { this.knockback = knockback; }
public int getSlownessLevel() { return slownessLevel; }
public void setSlownessLevel(int slownessLevel) { this.slownessLevel = slownessLevel; }
public int getWeaknessLevel() { return weaknessLevel; }
public void setWeaknessLevel(int weaknessLevel) { this.weaknessLevel = weaknessLevel; }
public int getPotionDurationSeconds() { return potionDurationSeconds; }
public void setPotionDurationSeconds(int potionDurationSeconds) { this.potionDurationSeconds = potionDurationSeconds; }
}
@@ -0,0 +1,367 @@
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 noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.potion.Potion;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.List;
/**
* Orb ability: Homing projectile sphere that tracks target.
* Can be explosive on impact and/or apply debuffs.
*/
public class AbilityOrb extends Ability {
private float orbSpeed = 0.5f;
private float orbSize = 1.0f;
private float damage = 15.0f;
private float knockback = 1.0f;
private float maxDistance = 30.0f;
private int maxLifetime = 200;
private boolean homing = true;
private float homingStrength = 0.15f;
private float homingRange = 20.0f;
private boolean explosive = false;
private float explosionRadius = 3.0f;
private float explosionDamageFalloff = 0.5f;
private int stunDuration = 0;
private int slowDuration = 0;
private int slowLevel = 0;
// Runtime state
private transient double orbX, orbY, orbZ;
private transient double velocityX, velocityY, velocityZ;
private transient double startX, startY, startZ;
private transient boolean hasHit = false;
private transient int ticksAlive = 0;
public AbilityOrb() {
this.typeId = "ability.cnpc.orb";
this.name = "Orb";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 25.0f;
this.minRange = 5.0f;
this.cooldownTicks = 100;
this.windUpTicks = 30;
this.activeTicks = 200;
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.POINT;
this.showTelegraph = false;
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(
IAbilityConfigCallback callback) {
return new SubGuiAbilityOrb(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AGGRO_TARGET };
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
startX = orbX = npc.posX;
startY = orbY = npc.posY + npc.getEyeHeight() * 0.8;
startZ = orbZ = npc.posZ;
hasHit = false;
ticksAlive = 0;
if (target != null) {
double dx = target.posX - orbX;
double dy = (target.posY + target.height * 0.5) - orbY;
double dz = target.posZ - orbZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
velocityX = (dx / len) * orbSpeed;
velocityY = (dy / len) * orbSpeed;
velocityZ = (dz / len) * orbSpeed;
}
} else {
float yaw = (float) Math.toRadians(npc.rotationYaw);
float pitch = (float) Math.toRadians(npc.rotationPitch);
velocityX = -Math.sin(yaw) * Math.cos(pitch) * orbSpeed;
velocityY = -Math.sin(pitch) * orbSpeed;
velocityZ = Math.cos(yaw) * Math.cos(pitch) * orbSpeed;
}
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (hasHit) return;
ticksAlive++;
if (ticksAlive >= maxLifetime) {
hasHit = true;
return;
}
double distTraveled = Math.sqrt(
Math.pow(orbX - startX, 2) +
Math.pow(orbY - startY, 2) +
Math.pow(orbZ - startZ, 2)
);
if (distTraveled >= maxDistance) {
hasHit = true;
return;
}
if (homing && target != null && target.isEntityAlive()) {
double targetX = target.posX;
double targetY = target.posY + target.height * 0.5;
double targetZ = target.posZ;
double distToTarget = Math.sqrt(
Math.pow(targetX - orbX, 2) +
Math.pow(targetY - orbY, 2) +
Math.pow(targetZ - orbZ, 2)
);
if (distToTarget <= homingRange) {
double dx = targetX - orbX;
double dy = targetY - orbY;
double dz = targetZ - orbZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
double desiredVX = (dx / len) * orbSpeed;
double desiredVY = (dy / len) * orbSpeed;
double desiredVZ = (dz / len) * orbSpeed;
velocityX += (desiredVX - velocityX) * homingStrength;
velocityY += (desiredVY - velocityY) * homingStrength;
velocityZ += (desiredVZ - velocityZ) * homingStrength;
double vLen = Math.sqrt(velocityX * velocityX + velocityY * velocityY + velocityZ * velocityZ);
if (vLen > 0) {
velocityX = (velocityX / vLen) * orbSpeed;
velocityY = (velocityY / vLen) * orbSpeed;
velocityZ = (velocityZ / vLen) * orbSpeed;
}
}
}
}
orbX += velocityX;
orbY += velocityY;
orbZ += velocityZ;
if (!world.isRemote) {
checkCollision(npc, world);
}
}
private void checkCollision(EntityNPCInterface npc, World world) {
AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox(
orbX - orbSize, orbY - orbSize, orbZ - orbSize,
orbX + orbSize, orbY + orbSize, orbZ + orbSize
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
for (EntityLivingBase entity : entities) {
if (entity == npc) continue;
hasHit = true;
if (explosive) {
doExplosion(npc, world);
} else {
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback);
if (wasHit) {
applyEffects(entity);
}
}
break;
}
if (!hasHit && world.getBlock((int)orbX, (int)orbY, (int)orbZ).getMaterial().isSolid()) {
hasHit = true;
if (explosive) {
doExplosion(npc, world);
}
}
}
private void doExplosion(EntityNPCInterface npc, World world) {
AxisAlignedBB explosionBox = AxisAlignedBB.getBoundingBox(
orbX - explosionRadius, orbY - explosionRadius, orbZ - explosionRadius,
orbX + explosionRadius, orbY + explosionRadius, orbZ + explosionRadius
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> blastTargets = world.getEntitiesWithinAABB(EntityLivingBase.class, explosionBox);
for (EntityLivingBase blastTarget : blastTargets) {
if (blastTarget == npc) continue;
double dist = Math.sqrt(
Math.pow(blastTarget.posX - orbX, 2) +
Math.pow(blastTarget.posY - orbY, 2) +
Math.pow(blastTarget.posZ - orbZ, 2)
);
if (dist <= explosionRadius) {
float falloff = 1.0f - (float)(dist / explosionRadius) * explosionDamageFalloff;
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(npc, blastTarget, damage * falloff, knockback * falloff);
if (wasHit) {
applyEffects(blastTarget);
}
}
}
world.playSoundEffect(orbX, orbY, orbZ, "random.explode", 1.0f, 1.0f);
}
private void applyKnockback(EntityLivingBase target, float strength, float upward) {
double dx = target.posX - orbX;
double dz = target.posZ - orbZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist > 0) {
target.motionX += (dx / dist) * strength;
target.motionZ += (dz / dist) * strength;
}
target.motionY += upward;
target.velocityChanged = true;
}
private void applyEffects(EntityLivingBase target) {
if (stunDuration > 0) {
target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10));
target.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2));
}
if (slowDuration > 0 && slowLevel > 0) {
target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, slowDuration, slowLevel));
}
}
@Override
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {
hasHit = false;
ticksAlive = 0;
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
hasHit = false;
ticksAlive = 0;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("orbSpeed", orbSpeed);
nbt.setFloat("orbSize", orbSize);
nbt.setFloat("damage", damage);
nbt.setFloat("knockback", knockback);
nbt.setFloat("maxDistance", maxDistance);
nbt.setInteger("maxLifetime", maxLifetime);
nbt.setBoolean("homing", homing);
nbt.setFloat("homingStrength", homingStrength);
nbt.setFloat("homingRange", homingRange);
nbt.setBoolean("explosive", explosive);
nbt.setFloat("explosionRadius", explosionRadius);
nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff);
nbt.setInteger("stunDuration", stunDuration);
nbt.setInteger("slowDuration", slowDuration);
nbt.setInteger("slowLevel", slowLevel);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.orbSpeed = nbt.hasKey("orbSpeed") ? nbt.getFloat("orbSpeed") : 0.5f;
this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f;
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f;
this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f;
this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f;
this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200;
this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing");
this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.15f;
this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 20.0f;
this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive");
this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f;
this.explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f;
this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0;
this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0;
this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0;
}
// Getters & Setters
public float getOrbSpeed() { return orbSpeed; }
public void setOrbSpeed(float orbSpeed) { this.orbSpeed = orbSpeed; }
public float getOrbSize() { return orbSize; }
public void setOrbSize(float orbSize) { this.orbSize = orbSize; }
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getKnockback() { return knockback; }
public void setKnockback(float knockback) { this.knockback = knockback; }
public float getMaxDistance() { return maxDistance; }
public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; }
public int getMaxLifetime() { return maxLifetime; }
public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; }
public boolean isHoming() { return homing; }
public void setHoming(boolean homing) { this.homing = homing; }
public float getHomingStrength() { return homingStrength; }
public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; }
public float getHomingRange() { return homingRange; }
public void setHomingRange(float homingRange) { this.homingRange = homingRange; }
public boolean isExplosive() { return explosive; }
public void setExplosive(boolean explosive) { this.explosive = explosive; }
public float getExplosionRadius() { return explosionRadius; }
public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; }
public float getExplosionDamageFalloff() { return explosionDamageFalloff; }
public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; }
public int getStunDuration() { return stunDuration; }
public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; }
public int getSlowDuration() { return slowDuration; }
public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; }
public int getSlowLevel() { return slowLevel; }
public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; }
// Runtime getters for rendering
public double getOrbX() { return orbX; }
public double getOrbY() { return orbY; }
public double getOrbZ() { return orbZ; }
public boolean hasHit() { return hasHit; }
}
@@ -0,0 +1,213 @@
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.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityProjectile;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.List;
/**
* Projectile ability: Ranged attack that deals damage to target.
* Currently instant damage - can be enhanced with custom projectile entities.
*/
public class AbilityProjectile extends Ability {
// Type-specific parameters
private float damage = 8.0f;
private float speed = 1.5f;
private float knockback = 0.5f;
private String projectileType = "fireball";
private boolean explosive = false;
private float explosionRadius = 0.0f;
private boolean homing = false;
private float homingStrength = 0.1f;
public AbilityProjectile() {
this.typeId = "ability.cnpc.projectile";
this.name = "Projectile";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.minRange = 5.0f;
this.maxRange = 20.0f;
this.cooldownTicks = 60;
this.windUpTicks = 15;
this.activeTicks = 5;
this.recoveryTicks = 10;
// No telegraph for projectile - it's a ranged attack
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
return new SubGuiAbilityProjectile(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AGGRO_TARGET };
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (world.isRemote || target == null) return;
// Calculate direction to target
double dx = target.posX - npc.posX;
double dy = (target.posY + target.height / 2) - (npc.posY + npc.getEyeHeight());
double dz = target.posZ - npc.posZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
dx /= len;
dy /= len;
dz /= len;
}
// Deal instant damage with scripted event support
// TODO: Use custom EntityAbilityProjectile for actual tracking
applyAbilityDamageWithDirection(npc, target, damage, knockback, dx, dz);
// Play sound
world.playSoundAtEntity(npc, "random.bow", 1.0f, 0.8f);
// Handle splash damage for explosive projectiles
if (explosive && explosionRadius > 0) {
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(target,
target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius));
for (Entity entity : entities) {
if (entity instanceof EntityLivingBase && entity != npc) {
EntityLivingBase living = (EntityLivingBase) entity;
float dist = target.getDistanceToEntity(living);
if (dist < explosionRadius) {
float falloff = 1.0f - (dist / explosionRadius);
// Apply splash damage with scripted event support (no knockback)
applyAbilityDamage(npc, living, damage * falloff * 0.5f, 0);
}
}
}
// Explosion particles
world.playSoundAtEntity(target, "random.explode", 0.5f, 1.0f);
for (int i = 0; i < 10; i++) {
world.spawnParticle("explode",
target.posX + (world.rand.nextDouble() - 0.5) * explosionRadius,
target.posY + world.rand.nextDouble() * target.height,
target.posZ + (world.rand.nextDouble() - 0.5) * explosionRadius,
0, 0.1, 0);
}
}
// Spawn projectile particles (visual trail)
spawnProjectileParticles(world, npc, target);
}
private void spawnProjectileParticles(World world, EntityNPCInterface npc, EntityLivingBase target) {
double startX = npc.posX;
double startY = npc.posY + npc.getEyeHeight();
double startZ = npc.posZ;
double endX = target.posX;
double endY = target.posY + target.height / 2;
double endZ = target.posZ;
double dx = endX - startX;
double dy = endY - startY;
double dz = endZ - startZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
String particle = "flame";
if (projectileType.equals("arrow")) {
particle = "crit";
} else if (projectileType.equals("magic")) {
particle = "witchMagic";
}
// Spawn particles along path
int steps = (int) Math.max(5, dist * 2);
for (int i = 0; i < steps; i++) {
double progress = (double) i / steps;
double px = startX + dx * progress;
double py = startY + dy * progress;
double pz = startZ + dz * progress;
world.spawnParticle(particle, px, py, pz, 0, 0, 0);
}
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
// Projectile is instant, nothing to do per-tick
}
@Override
public float getTelegraphRadius() {
return 0; // No telegraph for projectile
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("damage", damage);
nbt.setFloat("speed", speed);
nbt.setFloat("knockback", knockback);
nbt.setString("projectileType", projectileType);
nbt.setBoolean("explosive", explosive);
nbt.setFloat("explosionRadius", explosionRadius);
nbt.setBoolean("homing", homing);
nbt.setFloat("homingStrength", homingStrength);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f;
this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 1.5f;
this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f;
this.projectileType = nbt.hasKey("projectileType") ? nbt.getString("projectileType") : "fireball";
this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive");
this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 0.0f;
this.homing = nbt.hasKey("homing") && nbt.getBoolean("homing");
this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.1f;
}
// Getters & Setters
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getSpeed() { return speed; }
public void setSpeed(float speed) { this.speed = speed; }
public float getKnockback() { return knockback; }
public void setKnockback(float knockback) { this.knockback = knockback; }
public String getProjectileType() { return projectileType; }
public void setProjectileType(String projectileType) { this.projectileType = projectileType; }
public boolean isExplosive() { return explosive; }
public void setExplosive(boolean explosive) { this.explosive = explosive; }
public float getExplosionRadius() { return explosionRadius; }
public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; }
public boolean isHoming() { return homing; }
public void setHoming(boolean homing) { this.homing = homing; }
public float getHomingStrength() { return homingStrength; }
public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; }
}
@@ -0,0 +1,171 @@
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.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.potion.Potion;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityShockwave;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.List;
/**
* Shockwave ability: Pushes targets away from the caster with damage.
* Opposite of Vortex - instant knockback rather than gradual pull.
*/
public class AbilityShockwave extends Ability {
private float pushRadius = 8.0f;
private float pushStrength = 1.5f;
private float damage = 10.0f;
private int stunDuration = 0;
private int maxTargets = 10;
// Runtime state
private transient boolean executed = false;
public AbilityShockwave() {
this.typeId = "ability.cnpc.shockwave";
this.name = "Shockwave";
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 15.0f;
this.lockMovement = true;
this.cooldownTicks = 100;
this.windUpTicks = 25;
this.activeTicks = 5;
this.recoveryTicks = 20;
this.telegraphType = TelegraphType.CIRCLE;
this.windUpSound = "random.explode";
this.activeSound = "random.explode";
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
return new SubGuiAbilityShockwave(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AOE_SELF };
}
@Override
public float getTelegraphRadius() { return pushRadius; }
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
executed = false;
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (executed) return;
executed = true;
// Get all entities in radius
AxisAlignedBB box = npc.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
int count = 0;
for (EntityLivingBase entity : entities) {
if (entity == npc) continue;
if (entity.isDead) continue;
double dist = npc.getDistanceToEntity(entity);
if (dist > pushRadius) continue;
count++;
if (count > maxTargets) break;
// Calculate push direction (away from caster)
double dx = entity.posX - npc.posX;
double dz = entity.posZ - npc.posZ;
double len = Math.sqrt(dx * dx + dz * dz);
if (len > 0) {
dx /= len;
dz /= len;
} else {
// Entity is directly on top of caster, push in random direction
double angle = Math.random() * Math.PI * 2;
dx = Math.cos(angle);
dz = Math.sin(angle);
}
// Scale knockback by distance (closer = stronger)
float distFactor = 1.0f - (float)(dist / pushRadius) * 0.5f;
float finalPush = pushStrength * distFactor;
// Apply damage with custom knockback direction
boolean wasHit = applyAbilityDamageWithDirection(npc, entity, damage * distFactor, finalPush, dx, dz);
// Apply stun if hit connected
if (wasHit && stunDuration > 0) {
entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10));
entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2));
}
}
}
@Override
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {
executed = false;
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
executed = false;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("pushRadius", pushRadius);
nbt.setFloat("pushStrength", pushStrength);
nbt.setFloat("damage", damage);
nbt.setInteger("stunDuration", stunDuration);
nbt.setInteger("maxTargets", maxTargets);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.pushRadius = nbt.hasKey("pushRadius") ? nbt.getFloat("pushRadius") : 8.0f;
this.pushStrength = nbt.hasKey("pushStrength") ? nbt.getFloat("pushStrength") : 1.5f;
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f;
this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0;
this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 10;
}
// Getters & Setters
public float getPushRadius() { return pushRadius; }
public void setPushRadius(float pushRadius) { this.pushRadius = pushRadius; }
public float getPushStrength() { return pushStrength; }
public void setPushStrength(float pushStrength) { this.pushStrength = pushStrength; }
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public int getStunDuration() { return stunDuration; }
public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; }
public int getMaxTargets() { return maxTargets; }
public void setMaxTargets(int maxTargets) { this.maxTargets = maxTargets; }
}
@@ -0,0 +1,444 @@
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.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
import net.minecraft.util.MathHelper;
import net.minecraft.world.World;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySlam;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.List;
/**
* Slam ability: NPC leaps toward target in an arc and slams down, dealing AOE damage on landing.
*
* Phases:
* - WINDUP: NPC prepares to leap, telegraph shows landing zone
* - ACTIVE: NPC is in the air, traveling in arc toward destination
* - Landing: When NPC hits ground, deal AOE damage
* - RECOVERY: Landing recovery animation
*/
public class AbilitySlam extends Ability {
// Type-specific config
private float damage = 20.0f;
private float radius = 5.0f;
private float knockbackStrength = 1.5f;
private float leapSpeed = 1.0f;
private float leapHeight = 4.0f;
// Runtime state
private transient double targetX, targetY, targetZ;
private transient boolean hasLaunched = false;
private transient boolean hasLanded = false;
private transient int airTicks = 0;
private transient int maxAirTicks = 60; // Timeout to prevent stuck in air
public AbilitySlam() {
this.typeId = "ability.cnpc.slam";
this.name = "Slam";
this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target
this.windUpTicks = 30;
this.activeTicks = 60; // Max time in air + landing
this.recoveryTicks = 20;
this.cooldownTicks = 100;
this.lockMovement = true;
this.minRange = 2.0f;
this.maxRange = 15.0f;
this.telegraphType = TelegraphType.CIRCLE;
this.windUpSound = "mob.irongolem.throw";
this.activeSound = "random.explode";
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
return new SubGuiAbilitySlam(this, callback);
}
/**
* Slam can be AOE_SELF (jump in place) or AOE_TARGET (leap to target).
*/
@Override
public boolean isTargetingModeLocked() {
return false;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AOE_SELF, TargetingMode.AOE_TARGET };
}
@Override
public boolean hasAbilityMovement() {
return true; // This ability moves the NPC
}
@Override
public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
// Update target position during windup
if (targetingMode == TargetingMode.AOE_SELF) {
// AOE_SELF: slam at NPC's current position
targetX = npc.posX;
targetY = npc.posY;
targetZ = npc.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
// AOE_TARGET: telegraph follows target via setEntityIdToFollow
targetX = target.posX;
targetY = target.posY;
targetZ = target.posZ;
}
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
// Lock in the destination at moment of launch
if (targetingMode == TargetingMode.AOE_SELF) {
// AOE_SELF: slam in place (just jump up)
targetX = npc.posX;
targetY = npc.posY;
targetZ = npc.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
// AOE_TARGET: leap to target's current position
targetX = target.posX;
targetY = target.posY;
targetZ = target.posZ;
} else {
// Fallback: slam in place
targetX = npc.posX;
targetY = npc.posY;
targetZ = npc.posZ;
}
hasLaunched = false;
hasLanded = false;
airTicks = 0;
// Reset fall distance to prevent fall damage during slam
npc.fallDistance = 0;
// Calculate and apply leap velocity
launchTowardTarget(npc);
}
/**
* Calculate ballistic arc and launch NPC toward target.
* Accounts for Minecraft's drag physics to actually reach the destination.
*/
private void launchTowardTarget(EntityNPCInterface npc) {
double dx = targetX - npc.posX;
double dy = targetY - npc.posY;
double dz = targetZ - npc.posZ;
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
// For AOE_SELF or very close targets, just hop in place
if (horizontalDist < 0.5) {
npc.motionX = 0;
npc.motionZ = 0;
npc.motionY = 0.8 * leapSpeed;
hasLaunched = true;
npc.setNpcJumpingState(true);
npc.velocityChanged = true;
npc.worldObj.playSoundAtEntity(npc, "mob.irongolem.throw", 0.8f, 0.8f);
return;
}
// Cap the distance if too far
if (horizontalDist > maxRange) {
double scale = maxRange / horizontalDist;
dx *= scale;
dz *= scale;
horizontalDist = maxRange;
targetX = npc.posX + dx;
targetZ = npc.posZ + dz;
}
// Choose flight time based on distance - shorter distances = faster
int flightTicks = (int) Math.max(15, Math.min(horizontalDist * 1.5, 30));
// Minecraft applies drag of 0.91 per tick to horizontal motion in air
// Total distance traveled with initial velocity v0 over n ticks:
// distance = v0 * (1 - drag^n) / (1 - drag)
// Solving for v0: v0 = distance * (1 - drag) / (1 - drag^n)
double drag = 0.91;
double dragPowN = Math.pow(drag, flightTicks);
double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN);
// Scale by leap speed
vHorizontal *= leapSpeed;
// Vertical velocity calculation:
// Minecraft gravity is 0.08 per tick, with 0.98 drag on vertical motion
// For a nice arc, we want to peak roughly in the middle of the flight
double gravity = 0.08;
double verticalDrag = 0.98;
// Calculate required upward velocity to create a nice arc and land at target height
// Peak height should be proportional to horizontal distance for visual appeal
double arcHeight = Math.max(1.0, leapHeight) * leapSpeed;
// Approximate the required initial vertical velocity
// Account for gravity and drag - use a simplified model
double peakTicks = flightTicks * 0.4; // Peak around 40% through flight
double vy = (arcHeight * 2.0 / peakTicks) + (gravity * peakTicks * 0.5);
// Adjust for height difference
if (dy > 0) {
vy += dy / flightTicks * 1.5; // Going up - need more velocity
} else if (dy < 0) {
vy += dy / flightTicks * 0.3; // Going down - gravity helps
}
// Ensure minimum upward velocity for visible jump
vy = Math.max(vy, 0.6 * leapSpeed);
// Normalize horizontal direction and apply velocity
double dirX = dx / horizontalDist;
double dirZ = dz / horizontalDist;
npc.motionX = dirX * vHorizontal;
npc.motionZ = dirZ * vHorizontal;
npc.motionY = vy;
hasLaunched = true;
npc.setNpcJumpingState(true);
npc.velocityChanged = true;
// Face the target
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
npc.rotationYaw = targetYaw;
npc.rotationYawHead = targetYaw;
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (!hasLaunched) return;
if (hasLanded) return;
airTicks++;
// Continuously reset fall distance during slam to prevent fall damage
npc.fallDistance = 0;
// Check for landing
if (npc.onGround && airTicks > 3) {
// Landed!
onLanding(npc, world);
return;
}
// Timeout protection - force landing after max air time
if (airTicks >= maxAirTicks) {
onLanding(npc, world);
return;
}
// While in air, face toward target
double dx = targetX - npc.posX;
double dz = targetZ - npc.posZ;
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
npc.rotationYaw = targetYaw;
npc.rotationYawHead = targetYaw;
}
/**
* Called when NPC lands - deal AOE damage.
*/
private void onLanding(EntityNPCInterface npc, World world) {
hasLanded = true;
npc.setNpcJumpingState(false);
// Reset fall distance to prevent fall damage on landing
npc.fallDistance = 0;
// Stop horizontal momentum
npc.motionX = 0;
npc.motionZ = 0;
npc.velocityChanged = true;
if (world.isRemote) return;
// Find all entities in radius
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(
npc, npc.boundingBox.expand(radius, radius / 2, radius));
for (Entity entity : entities) {
if (entity instanceof EntityLivingBase && entity != npc) {
EntityLivingBase livingTarget = (EntityLivingBase) entity;
// Check if actually within radius (bounding box is a cube, we want a circle)
double dx = livingTarget.posX - npc.posX;
double dz = livingTarget.posZ - npc.posZ;
if (dx * dx + dz * dz <= radius * radius) {
// Apply damage with scripted event support
applyAbilityDamage(npc, livingTarget, damage, knockbackStrength);
}
}
}
// Spawn particles
spawnSlamParticles(world, npc.posX, npc.posY, npc.posZ);
}
/**
* Spawn visual particles for the slam effect.
*/
private void spawnSlamParticles(World world, double x, double y, double z) {
// Spawn explosion particles in a ring
for (int i = 0; i < 20; i++) {
double angle = (i / 20.0) * Math.PI * 2;
double px = x + Math.cos(angle) * radius * 0.8;
double pz = z + Math.sin(angle) * radius * 0.8;
world.spawnParticle("explode", px, y + 0.5, pz, 0, 0.1, 0);
}
// Spawn smoke at center
for (int i = 0; i < 10; i++) {
world.spawnParticle("smoke", x, y + 0.2, z,
(world.rand.nextDouble() - 0.5) * 0.3,
world.rand.nextDouble() * 0.2,
(world.rand.nextDouble() - 0.5) * 0.3);
}
// Ground crack particles - check if block is valid
net.minecraft.block.Block groundBlock = world.getBlock((int)x, (int)y - 1, (int)z);
if (groundBlock != null && groundBlock.getMaterial().isSolid()) {
int blockId = net.minecraft.block.Block.getIdFromBlock(groundBlock);
if (blockId > 0) {
for (int i = 0; i < 15; i++) {
double angle = world.rand.nextDouble() * Math.PI * 2;
double dist = world.rand.nextDouble() * radius * 0.6;
double px = x + Math.cos(angle) * dist;
double pz = z + Math.sin(angle) * dist;
world.spawnParticle("blockcrack_" + blockId + "_0", px, y + 0.1, pz, 0, 0.2, 0);
}
}
}
}
@Override
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {
npc.setNpcJumpingState(false);
hasLaunched = false;
hasLanded = false;
airTicks = 0;
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
npc.setNpcJumpingState(false);
hasLaunched = false;
hasLanded = false;
airTicks = 0;
}
@Override
public void reset() {
super.reset();
hasLaunched = false;
hasLanded = false;
airTicks = 0;
}
@Override
public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) {
// Check if telegraph should be shown
if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) {
return null;
}
// Telegraph shows at landing zone
if (targetingMode == TargetingMode.AOE_SELF) {
// AOE_SELF: telegraph at NPC position, does NOT follow
targetX = npc.posX;
targetY = npc.posY;
targetZ = npc.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null) {
// AOE_TARGET: telegraph at target position, follows target during windup
targetX = target.posX;
targetY = target.posY;
targetZ = target.posZ;
} else {
// Fallback to NPC position
targetX = npc.posX;
targetY = npc.posY;
targetZ = npc.posZ;
}
// Create telegraph at the appropriate position
double groundY = findGroundLevel(npc.worldObj, targetX, targetY, targetZ);
Telegraph telegraph = Telegraph.circle(radius);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, npc.rotationYaw);
instance.setCasterEntityId(npc.getEntityId());
// AOE_TARGET: telegraph follows target during windup
if (targetingMode == TargetingMode.AOE_TARGET && target != null) {
instance.setEntityIdToFollow(target.getEntityId());
}
// AOE_SELF: telegraph stays at NPC position, no follow
return instance;
}
@Override
public float getTelegraphRadius() {
return radius;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("damage", damage);
nbt.setFloat("radius", radius);
nbt.setFloat("knockback", knockbackStrength);
nbt.setFloat("leapSpeed", leapSpeed);
nbt.setFloat("leapHeight", leapHeight);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 20.0f;
radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 5.0f;
knockbackStrength = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f;
leapSpeed = nbt.hasKey("leapSpeed") ? nbt.getFloat("leapSpeed") : 1.0f;
leapHeight = nbt.hasKey("leapHeight") ? nbt.getFloat("leapHeight") : 4.0f;
}
// Getters & Setters
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getRadius() { return radius; }
public void setRadius(float radius) { this.radius = radius; }
public float getKnockbackStrength() { return knockbackStrength; }
public void setKnockbackStrength(float knockbackStrength) { this.knockbackStrength = knockbackStrength; }
public float getLeapSpeed() { return leapSpeed; }
public void setLeapSpeed(float leapSpeed) { this.leapSpeed = leapSpeed; }
public float getLeapHeight() { return leapHeight; }
public void setLeapHeight(float leapHeight) { this.leapHeight = leapHeight; }
}
@@ -0,0 +1,446 @@
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 noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTeleport;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.block.Block;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.util.MathHelper;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.List;
import java.util.Random;
/**
* Teleport ability: Instant reposition with wall/line-of-sight checks.
*/
public class AbilityTeleport extends Ability {
/**
* Teleport behavior mode.
*/
public enum TeleportMode {
BLINK,
BEHIND,
SINGLE
}
private static final Random RANDOM = new Random();
// Type-specific parameters
private TeleportMode mode = TeleportMode.BLINK;
private int blinkCount = 3;
private int blinkDelayTicks = 10;
private float blinkRadius = 8.0f;
private float behindDistance = 2.0f;
private boolean requireLineOfSight = true;
private boolean damageAtStart = false;
private boolean damageAtEnd = false;
private float damage = 5.0f;
private float damageRadius = 2.0f;
// Runtime state
private transient int currentBlink = 0;
private transient int ticksSinceLastBlink = 0;
public AbilityTeleport() {
this.typeId = "ability.cnpc.teleport";
this.name = "Teleport";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 30.0f;
this.minRange = 5.0f;
this.cooldownTicks = 100;
this.windUpTicks = 10;
this.activeTicks = 40;
this.recoveryTicks = 10;
// No telegraph for teleport - it's instant repositioning
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
this.windUpSound = "mob.endermen.portal";
this.activeSound = "mob.endermen.portal";
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(
IAbilityConfigCallback callback) {
return new SubGuiAbilityTeleport(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AGGRO_TARGET };
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
currentBlink = 0;
ticksSinceLastBlink = blinkDelayTicks; // Trigger first blink immediately
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1;
if (currentBlink >= blinkLimit) return;
ticksSinceLastBlink++;
if (ticksSinceLastBlink >= blinkDelayTicks) {
performBlink(npc, target, world);
currentBlink++;
ticksSinceLastBlink = 0;
}
}
private void performBlink(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (world.isRemote) return;
double oldX = npc.posX;
double oldY = npc.posY;
double oldZ = npc.posZ;
Vec3 destination = calculateDestination(npc, target, world);
if (destination == null) return;
boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight;
if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + npc.getEyeHeight(), oldZ,
destination.xCoord, destination.yCoord + npc.getEyeHeight(), destination.zCoord)) {
destination = findValidPositionAlongLine(world, npc, oldX, oldY, oldZ,
destination.xCoord, destination.yCoord, destination.zCoord);
if (destination == null) return;
}
destination = findSafeDestination(world, oldX, oldY, oldZ,
destination.xCoord, destination.yCoord, destination.zCoord);
if (destination == null) return;
// Damage at origin
if (damageAtStart) {
dealDamageAt(npc, world, oldX, oldY, oldZ);
}
// Spawn particles at origin
spawnTeleportParticles(world, oldX, oldY, oldZ);
// Teleport
npc.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord);
npc.fallDistance = 0;
// Spawn particles at destination
spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord);
// Damage at destination
if (damageAtEnd) {
dealDamageAt(npc, world, destination.xCoord, destination.yCoord, destination.zCoord);
}
// Face target after teleport
if (target != null) {
double dx = target.posX - destination.xCoord;
double dz = target.posZ - destination.zCoord;
float newYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
npc.rotationYaw = newYaw;
npc.rotationYawHead = newYaw;
}
}
private void spawnTeleportParticles(World world, double x, double y, double z) {
for (int i = 0; i < 20; i++) {
world.spawnParticle("portal",
x + (RANDOM.nextDouble() - 0.5) * 2,
y + RANDOM.nextDouble() * 2,
z + (RANDOM.nextDouble() - 0.5) * 2,
(RANDOM.nextDouble() - 0.5) * 0.5,
RANDOM.nextDouble() * 0.5,
(RANDOM.nextDouble() - 0.5) * 0.5);
}
}
private Vec3 calculateDestination(EntityNPCInterface npc, EntityLivingBase target, World world) {
if (target == null) return null;
double newX;
double newY;
double newZ;
switch (mode) {
case BEHIND:
double yaw = Math.toRadians(target.rotationYaw);
newX = target.posX + Math.sin(yaw) * behindDistance;
newZ = target.posZ - Math.cos(yaw) * behindDistance;
newY = target.posY;
return Vec3.createVectorHelper(newX, newY, newZ);
case SINGLE:
case BLINK:
default:
double angle = RANDOM.nextDouble() * Math.PI * 2;
double dist = Math.min(blinkRadius, maxRange);
double offset = RANDOM.nextDouble() * dist;
newX = target.posX + Math.cos(angle) * offset;
newZ = target.posZ + Math.sin(angle) * offset;
newY = target.posY;
return Vec3.createVectorHelper(newX, newY, newZ);
}
}
private Vec3 findSafeDestination(World world, double oldX, double oldY, double oldZ,
double destX, double destY, double destZ) {
int oldBlockX = (int) Math.floor(oldX);
int oldBlockY = (int) Math.floor(oldY);
int oldBlockZ = (int) Math.floor(oldZ);
int destBlockX = (int) Math.floor(destX);
int destBlockY = (int) Math.floor(destY);
int destBlockZ = (int) Math.floor(destZ);
if (oldBlockX == destBlockX && oldBlockY == destBlockY && oldBlockZ == destBlockZ) {
return null;
}
double safeY = findSafeYNear(world, destX, destY, destZ);
Vec3 safe = Vec3.createVectorHelper(destX, safeY, destZ);
if (!isSafeLocation(world, (int) Math.floor(safe.xCoord),
(int) Math.floor(safe.yCoord), (int) Math.floor(safe.zCoord))) {
return null;
}
return safe;
}
private double findSafeYNear(World world, double x, double baseY, double z) {
int[] offsets = new int[]{0, 1, -1, 2, -2};
for (int offset : offsets) {
double y = baseY + offset;
if (isSafeLocation(world, (int) Math.floor(x), (int) Math.floor(y), (int) Math.floor(z))) {
return y;
}
}
return findSafeY(world, x, baseY, z);
}
private boolean hasLineOfSight(World world, double x1, double y1, double z1,
double x2, double y2, double z2) {
double dx = x2 - x1;
double dy = y2 - y1;
double dz = z2 - z1;
double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (distance < 0.1) return true;
dx /= distance;
dy /= distance;
dz /= distance;
double step = 0.5;
for (double d = step; d < distance; d += step) {
double checkX = x1 + dx * d;
double checkY = y1 + dy * d;
double checkZ = z1 + dz * d;
int blockX = MathHelper.floor_double(checkX);
int blockY = MathHelper.floor_double(checkY);
int blockZ = MathHelper.floor_double(checkZ);
Block block = world.getBlock(blockX, blockY, blockZ);
if (block.getMaterial().isSolid() && block.isOpaqueCube()) {
return false;
}
}
return true;
}
private Vec3 findValidPositionAlongLine(World world, EntityNPCInterface npc,
double x1, double y1, double z1,
double x2, double y2, double z2) {
double dx = x2 - x1;
double dy = y2 - y1;
double dz = z2 - z1;
double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (distance < 1.0) return null;
dx /= distance;
dy /= distance;
dz /= distance;
Vec3 lastValid = null;
double step = 0.5;
for (double d = 1.0; d < distance; d += step) {
double checkX = x1 + dx * d;
double checkY = y1 + dy * d;
double checkZ = z1 + dz * d;
if (hasLineOfSight(world, x1, y1 + npc.getEyeHeight(), z1,
checkX, checkY + npc.getEyeHeight(), checkZ)) {
int blockX = MathHelper.floor_double(checkX);
int blockY = MathHelper.floor_double(checkY);
int blockZ = MathHelper.floor_double(checkZ);
if (isSafeLocation(world, blockX, blockY, blockZ)) {
lastValid = Vec3.createVectorHelper(checkX, checkY, checkZ);
}
} else {
break;
}
}
return lastValid;
}
private double findSafeY(World world, double x, double baseY, double z) {
int blockX = MathHelper.floor_double(x);
int blockZ = MathHelper.floor_double(z);
int blockY = MathHelper.floor_double(baseY);
for (int offset = 0; offset <= 5; offset++) {
if (isSafeLocation(world, blockX, blockY + offset, blockZ)) {
return blockY + offset;
}
if (offset > 0 && isSafeLocation(world, blockX, blockY - offset, blockZ)) {
return blockY - offset;
}
}
return baseY;
}
private boolean isSafeLocation(World world, int x, int y, int z) {
Block groundBlock = world.getBlock(x, y - 1, z);
Block feetBlock = world.getBlock(x, y, z);
Block headBlock = world.getBlock(x, y + 1, z);
return groundBlock.getMaterial().isSolid() &&
!feetBlock.getMaterial().isSolid() &&
!headBlock.getMaterial().isSolid();
}
private void dealDamageAt(EntityNPCInterface npc, World world, double x, double y, double z) {
AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox(
x - damageRadius, y - 1, z - damageRadius,
x + damageRadius, y + 2, z + damageRadius
);
@SuppressWarnings("unchecked")
List<Entity> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, aabb);
for (Entity entity : entities) {
if (!(entity instanceof EntityLivingBase)) continue;
if (entity == npc) continue;
EntityLivingBase living = (EntityLivingBase) entity;
double dist = Math.sqrt(Math.pow(living.posX - x, 2) + Math.pow(living.posZ - z, 2));
if (dist <= damageRadius) {
// Apply damage with scripted event support (no knockback for teleport damage)
applyAbilityDamage(npc, living, damage, 0);
}
}
}
@Override
public void reset() {
super.reset();
currentBlink = 0;
ticksSinceLastBlink = 0;
}
@Override
public float getTelegraphRadius() {
return 0; // No telegraph for teleport
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setString("mode", mode.name());
nbt.setInteger("blinkCount", blinkCount);
nbt.setInteger("blinkDelayTicks", blinkDelayTicks);
nbt.setFloat("blinkRadius", blinkRadius);
nbt.setFloat("behindDistance", behindDistance);
nbt.setBoolean("requireLineOfSight", requireLineOfSight);
nbt.setBoolean("damageAtStart", damageAtStart);
nbt.setBoolean("damageAtEnd", damageAtEnd);
nbt.setFloat("damage", damage);
nbt.setFloat("damageRadius", damageRadius);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
if (nbt.hasKey("mode")) {
try {
this.mode = TeleportMode.valueOf(nbt.getString("mode"));
} catch (Exception e) {
this.mode = TeleportMode.BLINK;
}
} else if (nbt.hasKey("pattern")) {
String legacy = nbt.getString("pattern");
if ("BEHIND_TARGET".equals(legacy)) {
this.mode = TeleportMode.BEHIND;
} else if ("RANDOM".equals(legacy)) {
this.mode = TeleportMode.BLINK;
} else {
this.mode = TeleportMode.SINGLE;
}
} else {
this.mode = TeleportMode.BLINK;
}
this.blinkCount = nbt.hasKey("blinkCount") ? nbt.getInteger("blinkCount") : 3;
this.blinkDelayTicks = nbt.hasKey("blinkDelayTicks") ? nbt.getInteger("blinkDelayTicks") : 10;
this.blinkRadius = nbt.hasKey("blinkRadius") ? nbt.getFloat("blinkRadius") : 8.0f;
this.behindDistance = nbt.hasKey("behindDistance") ? nbt.getFloat("behindDistance") : 2.0f;
this.requireLineOfSight = !nbt.hasKey("requireLineOfSight") || nbt.getBoolean("requireLineOfSight");
this.damageAtStart = nbt.hasKey("damageAtStart") && nbt.getBoolean("damageAtStart");
this.damageAtEnd = nbt.hasKey("damageAtEnd") && nbt.getBoolean("damageAtEnd");
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f;
this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 2.0f;
}
// Getters & Setters
public TeleportMode getMode() { return mode; }
public void setMode(TeleportMode mode) { this.mode = mode; }
public int getBlinkCount() { return blinkCount; }
public void setBlinkCount(int blinkCount) { this.blinkCount = blinkCount; }
public int getBlinkDelayTicks() { return blinkDelayTicks; }
public void setBlinkDelayTicks(int blinkDelayTicks) { this.blinkDelayTicks = blinkDelayTicks; }
public float getBlinkRadius() { return blinkRadius; }
public void setBlinkRadius(float blinkRadius) { this.blinkRadius = blinkRadius; }
public float getBehindDistance() { return behindDistance; }
public void setBehindDistance(float behindDistance) { this.behindDistance = behindDistance; }
public boolean isRequireLineOfSight() { return requireLineOfSight; }
public void setRequireLineOfSight(boolean requireLineOfSight) { this.requireLineOfSight = requireLineOfSight; }
public boolean isDamageAtStart() { return damageAtStart; }
public void setDamageAtStart(boolean damageAtStart) { this.damageAtStart = damageAtStart; }
public boolean isDamageAtEnd() { return damageAtEnd; }
public void setDamageAtEnd(boolean damageAtEnd) { this.damageAtEnd = damageAtEnd; }
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getDamageRadius() { return damageRadius; }
public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; }
}
@@ -0,0 +1,427 @@
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.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.potion.Potion;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTrap;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
/**
* Trap ability: Places a proximity-triggered trap.
* Features arm time, trigger radius, and various effects on trigger.
*/
public class AbilityTrap extends Ability {
public enum TrapPlacement {
AT_CASTER,
AT_TARGET,
AHEAD_OF_CASTER
}
private TrapPlacement placement = TrapPlacement.AT_TARGET;
private float placementDistance = 5.0f;
private float triggerRadius = 2.0f;
private int armTime = 20;
private int maxTriggers = 1;
private int triggerCooldown = 20;
private float damage = 10.0f;
private float damageRadius = 0.0f;
private float knockback = 0.5f;
private int stunDuration = 0;
private int rootDuration = 40;
private int slowDuration = 0;
private int slowLevel = 1;
private int poisonDuration = 0;
private int poisonLevel = 0;
private boolean visible = true;
// Offset parameters - trap spawns near target, not exactly on them
private float minOffset = 0.0f;
private float maxOffset = 1.5f;
private boolean randomOffset = true;
// Runtime state
private static final Random RANDOM = new Random();
private transient double trapX, trapY, trapZ;
private transient boolean armed = false;
private transient int triggerCount = 0;
private transient int ticksSinceLastTrigger = 0;
private transient Set<UUID> triggeredEntities = new HashSet<>();
public AbilityTrap() {
this.typeId = "ability.cnpc.trap";
this.name = "Trap";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.lockMovement = true;
this.cooldownTicks = 150;
this.windUpTicks = 20;
this.activeTicks = 200;
this.recoveryTicks = 10;
this.telegraphType = TelegraphType.CIRCLE;
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) {
return new SubGuiAbilityTrap(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AGGRO_TARGET };
}
@Override
public float getTelegraphRadius() { return triggerRadius; }
@Override
public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) {
TelegraphInstance instance = super.createTelegraph(npc, target);
if (instance == null) return null;
// Control telegraph following based on placement mode
switch (placement) {
case AT_CASTER:
case AHEAD_OF_CASTER:
// Telegraph at caster or ahead, no following
instance.setEntityIdToFollow(-1);
if (placement == TrapPlacement.AHEAD_OF_CASTER) {
double yaw = Math.toRadians(npc.rotationYaw);
instance.setX(npc.posX - Math.sin(yaw) * placementDistance);
instance.setY(npc.posY);
instance.setZ(npc.posZ + Math.cos(yaw) * placementDistance);
} else {
instance.setX(npc.posX);
instance.setY(npc.posY);
instance.setZ(npc.posZ);
}
break;
case AT_TARGET:
// Telegraph follows target during windup, locks on execute
if (target != null) {
instance.setEntityIdToFollow(target.getEntityId());
}
break;
}
return instance;
}
/**
* Calculates offset position near the given coordinates.
*/
private double[] calculateOffsetPosition(double baseX, double baseY, double baseZ) {
if (maxOffset <= 0) {
return new double[]{baseX, baseY, baseZ};
}
double offsetDist;
double offsetAngle;
if (randomOffset) {
offsetDist = minOffset + RANDOM.nextDouble() * (maxOffset - minOffset);
offsetAngle = RANDOM.nextDouble() * Math.PI * 2;
} else {
offsetDist = maxOffset;
offsetAngle = RANDOM.nextDouble() * Math.PI * 2;
}
double offsetX = Math.cos(offsetAngle) * offsetDist;
double offsetZ = Math.sin(offsetAngle) * offsetDist;
return new double[]{baseX + offsetX, baseY, baseZ + offsetZ};
}
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
armed = false;
triggerCount = 0;
ticksSinceLastTrigger = armTime;
triggeredEntities.clear();
// Use telegraph position if available (for AT_TARGET, it was following target)
TelegraphInstance telegraph = getTelegraphInstance();
switch (placement) {
case AT_CASTER:
trapX = npc.posX;
trapY = npc.posY;
trapZ = npc.posZ;
break;
case AT_TARGET:
// Use telegraph position with offset
if (telegraph != null) {
double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ());
trapX = pos[0];
trapY = pos[1];
trapZ = pos[2];
} else if (target != null) {
double[] pos = calculateOffsetPosition(target.posX, target.posY, target.posZ);
trapX = pos[0];
trapY = pos[1];
trapZ = pos[2];
} else {
trapX = npc.posX;
trapY = npc.posY;
trapZ = npc.posZ;
}
break;
case AHEAD_OF_CASTER:
double yaw = Math.toRadians(npc.rotationYaw);
trapX = npc.posX - Math.sin(yaw) * placementDistance;
trapY = npc.posY;
trapZ = npc.posZ + Math.cos(yaw) * placementDistance;
break;
}
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (!armed) {
if (tick >= armTime) {
armed = true;
}
return;
}
ticksSinceLastTrigger++;
if (maxTriggers > 0 && triggerCount >= maxTriggers) {
return;
}
if (ticksSinceLastTrigger < triggerCooldown) {
return;
}
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
trapX - triggerRadius, trapY - 1, trapZ - triggerRadius,
trapX + triggerRadius, trapY + 2, trapZ + triggerRadius
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
for (EntityLivingBase entity : entities) {
if (entity == npc) continue;
if (entity.isDead) continue;
if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue;
double dx = entity.posX - trapX;
double dz = entity.posZ - trapZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist <= triggerRadius) {
triggerTrap(npc, entity, world);
return;
}
}
}
private void triggerTrap(EntityNPCInterface npc, EntityLivingBase triggerer, World world) {
triggerCount++;
ticksSinceLastTrigger = 0;
triggeredEntities.add(triggerer.getUniqueID());
Set<EntityLivingBase> affected = new HashSet<>();
if (damageRadius > 0) {
AxisAlignedBB box = AxisAlignedBB.getBoundingBox(
trapX - damageRadius, trapY - 1, trapZ - damageRadius,
trapX + damageRadius, trapY + 3, trapZ + damageRadius
);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
for (EntityLivingBase entity : entities) {
if (entity == npc) continue;
double dx = entity.posX - trapX;
double dz = entity.posZ - trapZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist <= damageRadius) {
affected.add(entity);
}
}
} else {
affected.add(triggerer);
}
for (EntityLivingBase entity : affected) {
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback);
// Only apply effects if the hit wasn't cancelled
if (wasHit) {
if (stunDuration > 0) {
entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10));
entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2));
}
if (rootDuration > 0) {
entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, rootDuration, 127));
}
if (slowDuration > 0) {
entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, slowDuration, slowLevel));
}
if (poisonDuration > 0 && poisonLevel >= 0) {
entity.addPotionEffect(new PotionEffect(Potion.poison.id, poisonDuration, poisonLevel));
}
}
}
}
@Override
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {
armed = false;
triggerCount = 0;
triggeredEntities.clear();
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
armed = false;
triggerCount = 0;
triggeredEntities.clear();
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setString("placement", placement.name());
nbt.setFloat("placementDistance", placementDistance);
nbt.setFloat("triggerRadius", triggerRadius);
nbt.setInteger("armTime", armTime);
nbt.setInteger("maxTriggers", maxTriggers);
nbt.setInteger("triggerCooldown", triggerCooldown);
nbt.setFloat("damage", damage);
nbt.setFloat("damageRadius", damageRadius);
nbt.setFloat("knockback", knockback);
nbt.setInteger("stunDuration", stunDuration);
nbt.setInteger("rootDuration", rootDuration);
nbt.setInteger("slowDuration", slowDuration);
nbt.setInteger("slowLevel", slowLevel);
nbt.setInteger("poisonDuration", poisonDuration);
nbt.setInteger("poisonLevel", poisonLevel);
nbt.setBoolean("visible", visible);
nbt.setFloat("minOffset", minOffset);
nbt.setFloat("maxOffset", maxOffset);
nbt.setBoolean("randomOffset", randomOffset);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
try {
this.placement = TrapPlacement.valueOf(nbt.getString("placement"));
} catch (Exception e) {
this.placement = TrapPlacement.AT_TARGET;
}
this.placementDistance = nbt.hasKey("placementDistance") ? nbt.getFloat("placementDistance") : 5.0f;
this.triggerRadius = nbt.hasKey("triggerRadius") ? nbt.getFloat("triggerRadius") : 2.0f;
this.armTime = nbt.hasKey("armTime") ? nbt.getInteger("armTime") : 20;
this.maxTriggers = nbt.hasKey("maxTriggers") ? nbt.getInteger("maxTriggers") : 1;
this.triggerCooldown = nbt.hasKey("triggerCooldown") ? nbt.getInteger("triggerCooldown") : 20;
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f;
this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 0.0f;
this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f;
this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0;
this.rootDuration = nbt.hasKey("rootDuration") ? nbt.getInteger("rootDuration") : 40;
this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0;
this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 1;
this.poisonDuration = nbt.hasKey("poisonDuration") ? nbt.getInteger("poisonDuration") : 0;
this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : 0;
this.visible = !nbt.hasKey("visible") || nbt.getBoolean("visible");
this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f;
this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 1.5f;
this.randomOffset = !nbt.hasKey("randomOffset") || nbt.getBoolean("randomOffset");
}
// Getters & Setters
public TrapPlacement getPlacement() { return placement; }
public void setPlacement(TrapPlacement placement) { this.placement = placement; }
public float getPlacementDistance() { return placementDistance; }
public void setPlacementDistance(float placementDistance) { this.placementDistance = placementDistance; }
public float getTriggerRadius() { return triggerRadius; }
public void setTriggerRadius(float triggerRadius) { this.triggerRadius = triggerRadius; }
public int getArmTime() { return armTime; }
public void setArmTime(int armTime) { this.armTime = armTime; }
public int getMaxTriggers() { return maxTriggers; }
public void setMaxTriggers(int maxTriggers) { this.maxTriggers = maxTriggers; }
public int getTriggerCooldown() { return triggerCooldown; }
public void setTriggerCooldown(int triggerCooldown) { this.triggerCooldown = triggerCooldown; }
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getDamageRadius() { return damageRadius; }
public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; }
public float getKnockback() { return knockback; }
public void setKnockback(float knockback) { this.knockback = knockback; }
public int getStunDuration() { return stunDuration; }
public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; }
public int getRootDuration() { return rootDuration; }
public void setRootDuration(int rootDuration) { this.rootDuration = rootDuration; }
public int getSlowDuration() { return slowDuration; }
public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; }
public int getSlowLevel() { return slowLevel; }
public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; }
public int getPoisonDuration() { return poisonDuration; }
public void setPoisonDuration(int poisonDuration) { this.poisonDuration = poisonDuration; }
public int getPoisonLevel() { return poisonLevel; }
public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; }
public boolean isVisible() { return visible; }
public void setVisible(boolean visible) { this.visible = visible; }
public float getMinOffset() { return minOffset; }
public void setMinOffset(float minOffset) { this.minOffset = minOffset; }
public float getMaxOffset() { return maxOffset; }
public void setMaxOffset(float maxOffset) { this.maxOffset = maxOffset; }
public boolean isRandomOffset() { return randomOffset; }
public void setRandomOffset(boolean randomOffset) { this.randomOffset = randomOffset; }
// Runtime getters
public double getTrapX() { return trapX; }
public double getTrapY() { return trapY; }
public double getTrapZ() { return trapZ; }
public boolean isArmed() { return armed; }
}
@@ -0,0 +1,281 @@
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 noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityVortex;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.potion.Potion;
import net.minecraft.potion.PotionEffect;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.DamageSource;
import net.minecraft.world.World;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
* Vortex ability: Pulls targets toward the caster.
* Can pull single target or AOE, with optional damage and stun on arrival.
*/
public class AbilityVortex extends Ability {
private float pullRadius = 8.0f;
private float pullStrength = 0.8f;
private float damage = 0.0f;
private float knockback = 0.0f;
private int stunDuration = 0;
private int rootDuration = 0;
private boolean aoe = false;
private int maxTargets = 5;
private boolean damageOnPull = false;
private float pullDamage = 0.0f;
// Runtime state
private transient Set<UUID> pulledEntities = new HashSet<>();
private transient boolean pullComplete = false;
private transient int ticksSincePullDamage = 0;
public AbilityVortex() {
this.typeId = "ability.cnpc.vortex";
this.name = "Vortex";
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 15.0f;
this.lockMovement = true;
this.cooldownTicks = 120;
this.windUpTicks = 30;
this.activeTicks = 40;
this.recoveryTicks = 20;
this.telegraphType = TelegraphType.CIRCLE;
this.windUpSound = "mob.ghast.charge";
this.activeSound = "mob.ghast.fireball";
}
@Override
public boolean hasTypeSettings() { return true; }
@Override
@SideOnly(Side.CLIENT)
public SubGuiAbilityConfig createConfigGui(
IAbilityConfigCallback callback) {
return new SubGuiAbilityVortex(this, callback);
}
@Override
public boolean isTargetingModeLocked() { return true; }
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[] { TargetingMode.AOE_SELF };
}
@Override
public float getTelegraphRadius() { return pullRadius; }
@Override
public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) {
pulledEntities.clear();
pullComplete = false;
ticksSincePullDamage = 0;
if (aoe) {
AxisAlignedBB box = npc.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
@SuppressWarnings("unchecked")
List<EntityLivingBase> entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box);
int count = 0;
for (EntityLivingBase entity : entities) {
if (entity == npc) continue;
if (entity.isDead) continue;
double dist = npc.getDistanceToEntity(entity);
if (dist <= pullRadius) {
pulledEntities.add(entity.getUniqueID());
count++;
if (count >= maxTargets) break;
}
}
} else {
// Single target mode - still check pullRadius
if (target != null && !target.isDead) {
double dist = npc.getDistanceToEntity(target);
if (dist <= pullRadius) {
pulledEntities.add(target.getUniqueID());
}
}
}
}
@Override
public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {
if (pullComplete || pulledEntities.isEmpty()) {
return;
}
double destX = npc.posX;
double destY = npc.posY;
double destZ = npc.posZ;
boolean anyStillPulling = false;
ticksSincePullDamage++;
for (UUID uuid : new HashSet<>(pulledEntities)) {
EntityLivingBase entity = findEntity(world, uuid);
if (entity == null || entity.isDead) {
pulledEntities.remove(uuid);
continue;
}
double dx = destX - entity.posX;
double dy = destY - entity.posY;
double dz = destZ - entity.posZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist <= 1.0f) {
pulledEntities.remove(uuid);
onTargetArrived(npc, entity, world);
continue;
}
anyStillPulling = true;
double factor = pullStrength / dist;
double nextX = dx * factor;
double nextY = dy * factor * 0.5;
double nextZ = dz * factor;
AxisAlignedBB nextBox = entity.boundingBox.copy().offset(nextX, nextY, nextZ);
if (!world.getCollidingBoundingBoxes(entity, nextBox).isEmpty()) {
pulledEntities.remove(uuid);
continue;
}
entity.motionX = nextX;
entity.motionY = nextY;
entity.motionZ = nextZ;
entity.velocityChanged = true;
if (damageOnPull && pullDamage > 0 && ticksSincePullDamage >= 10) {
ticksSincePullDamage = 0;
entity.hurtResistantTime = 0;
applyAbilityDamage(npc, entity, pullDamage * 0.5f, 0);
}
}
if (!anyStillPulling && pulledEntities.isEmpty()) {
pullComplete = true;
}
}
private void onTargetArrived(EntityNPCInterface npc, EntityLivingBase entity, World world) {
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback * 0.5f);
// Only apply effects if hit wasn't cancelled
if (wasHit) {
if (stunDuration > 0) {
entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10));
entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2));
}
if (rootDuration > 0) {
entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, rootDuration, 127));
}
}
}
@SuppressWarnings("unchecked")
private EntityLivingBase findEntity(World world, UUID uuid) {
for (Object obj : world.loadedEntityList) {
if (obj instanceof EntityLivingBase) {
EntityLivingBase entity = (EntityLivingBase) obj;
if (entity.getUniqueID().equals(uuid)) {
return entity;
}
}
}
return null;
}
@Override
public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {
pulledEntities.clear();
pullComplete = false;
ticksSincePullDamage = 0;
}
@Override
public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {
pulledEntities.clear();
pullComplete = false;
ticksSincePullDamage = 0;
}
@Override
public void writeTypeNBT(NBTTagCompound nbt) {
nbt.setFloat("pullRadius", pullRadius);
nbt.setFloat("pullStrength", pullStrength);
nbt.setFloat("damage", damage);
nbt.setFloat("knockback", knockback);
nbt.setInteger("stunDuration", stunDuration);
nbt.setInteger("rootDuration", rootDuration);
nbt.setBoolean("aoe", aoe);
nbt.setInteger("maxTargets", maxTargets);
nbt.setBoolean("damageOnPull", damageOnPull);
nbt.setFloat("pullDamage", pullDamage);
}
@Override
public void readTypeNBT(NBTTagCompound nbt) {
this.pullRadius = nbt.hasKey("pullRadius") ? nbt.getFloat("pullRadius") : 8.0f;
this.pullStrength = nbt.hasKey("pullStrength") ? nbt.getFloat("pullStrength") : 0.8f;
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 0.0f;
this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.0f;
this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0;
this.rootDuration = nbt.hasKey("rootDuration") ? nbt.getInteger("rootDuration") : 0;
this.aoe = nbt.hasKey("aoe") && nbt.getBoolean("aoe");
this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 5;
this.damageOnPull = nbt.hasKey("damageOnPull") && nbt.getBoolean("damageOnPull");
this.pullDamage = nbt.hasKey("pullDamage") ? nbt.getFloat("pullDamage") : 0.0f;
}
// Getters & Setters
public float getPullRadius() { return pullRadius; }
public void setPullRadius(float pullRadius) { this.pullRadius = pullRadius; }
public float getPullStrength() { return pullStrength; }
public void setPullStrength(float pullStrength) { this.pullStrength = pullStrength; }
public float getDamage() { return damage; }
public void setDamage(float damage) { this.damage = damage; }
public float getKnockback() { return knockback; }
public void setKnockback(float knockback) { this.knockback = knockback; }
public int getStunDuration() { return stunDuration; }
public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; }
public int getRootDuration() { return rootDuration; }
public void setRootDuration(int rootDuration) { this.rootDuration = rootDuration; }
public boolean isAoe() { return aoe; }
public void setAoe(boolean aoe) { this.aoe = aoe; }
public int getMaxTargets() { return maxTargets; }
public void setMaxTargets(int maxTargets) { this.maxTargets = maxTargets; }
public boolean isDamageOnPull() { return damageOnPull; }
public void setDamageOnPull(boolean damageOnPull) { this.damageOnPull = damageOnPull; }
public float getPullDamage() { return pullDamage; }
public void setPullDamage(float pullDamage) { this.pullDamage = pullDamage; }
}
@@ -49,6 +49,8 @@ import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket;
import kamkeel.npcs.network.packets.data.script.ScriptOverlayClosePacket;
import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket;
import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket;
import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket;
import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket;
import kamkeel.npcs.network.packets.player.BankActionPacket;
import kamkeel.npcs.network.packets.player.CheckPlayerValue;
import kamkeel.npcs.network.packets.player.CompanionActionPacket;
@@ -138,6 +140,9 @@ import kamkeel.npcs.network.packets.request.linked.LinkedItemSavePacket;
import kamkeel.npcs.network.packets.request.linked.LinkedNPCAddPacket;
import kamkeel.npcs.network.packets.request.linked.LinkedNPCRemovePacket;
import kamkeel.npcs.network.packets.request.linked.LinkedSetPacket;
import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket;
import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket;
import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket;
import kamkeel.npcs.network.packets.request.magic.MagicCycleRemovePacket;
import kamkeel.npcs.network.packets.request.magic.MagicCycleSavePacket;
import kamkeel.npcs.network.packets.request.magic.MagicGetAllPacket;
@@ -479,6 +484,11 @@ public class PacketHandler {
REQUEST_PACKET.registerPacket(new MagicNpcGetPacket());
REQUEST_PACKET.registerPacket(new MagicNpcSavePacket());
// Ability Packets
REQUEST_PACKET.registerPacket(new AbilitiesGetAllPacket());
REQUEST_PACKET.registerPacket(new AbilitiesNpcGetPacket());
REQUEST_PACKET.registerPacket(new AbilitiesNpcSavePacket());
// Other Packets
REQUEST_PACKET.registerPacket(new IsGuiOpenInform());
REQUEST_PACKET.registerPacket(new GuiRequestPacket());
@@ -543,6 +553,10 @@ public class PacketHandler {
DATA_PACKET.registerPacket(new SyncEffectPacket());
DATA_PACKET.registerPacket(new GuiDataPacket());
DATA_PACKET.registerPacket(new PartyDataPacket());
// Data | Ability Packets
DATA_PACKET.registerPacket(new TelegraphSpawnPacket());
DATA_PACKET.registerPacket(new TelegraphRemovePacket());
}
public void registerPlayerPackets() {
@@ -58,4 +58,8 @@ public enum EnumDataPacket {
SWING_PLAYER_ARM,
DISABLE_MOUSE_INPUT,
LOGIN,
// Ability System
TELEGRAPH_SPAWN,
TELEGRAPH_REMOVE,
}
@@ -194,6 +194,9 @@ public enum EnumRequestPacket {
MagicSave,
NpcMagicGet,
NpcMagicSave,
NpcAbilitiesGet,
NpcAbilitiesSave,
AbilitiesGetAll,
Hammer,
RecipeScript,
@@ -0,0 +1,70 @@
package kamkeel.npcs.network.packets.data.ability;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumDataPacket;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import java.io.IOException;
/**
* Packet to remove a telegraph on the client.
* Sent from server to client when an ability's telegraph should be removed early.
*/
public final class TelegraphRemovePacket extends AbstractPacket {
public static final String packetName = "Data|TelegraphRemove";
private String instanceId;
public TelegraphRemovePacket() {
}
public TelegraphRemovePacket(String instanceId) {
this.instanceId = instanceId;
}
@Override
public Enum getType() {
return EnumDataPacket.TELEGRAPH_REMOVE;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.DATA_PACKET;
}
@Override
public void sendData(ByteBuf out) throws IOException {
ByteBufUtils.writeString(out, instanceId);
}
@SideOnly(Side.CLIENT)
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
String id = ByteBufUtils.readString(in);
if (id != null && TelegraphManager.ClientInstance != null) {
TelegraphManager.ClientInstance.removeTelegraph(id);
}
}
/**
* Send remove to a specific player.
*/
public static void send(String instanceId, EntityPlayerMP player) {
PacketHandler.Instance.sendToPlayer(new TelegraphRemovePacket(instanceId), player);
}
/**
* Send remove to all players tracking an entity (within range).
*/
public static void sendToTracking(String instanceId, net.minecraft.entity.Entity entity) {
PacketHandler.Instance.sendTracking(new TelegraphRemovePacket(instanceId), entity);
}
}
@@ -0,0 +1,82 @@
package kamkeel.npcs.network.packets.data.ability;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumDataPacket;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import java.io.IOException;
/**
* Packet to spawn a telegraph on the client.
* Sent from server to client when an ability creates a telegraph.
*/
public final class TelegraphSpawnPacket extends AbstractPacket {
public static final String packetName = "Data|TelegraphSpawn";
private NBTTagCompound telegraphNBT;
public TelegraphSpawnPacket() {
}
public TelegraphSpawnPacket(TelegraphInstance instance) {
this.telegraphNBT = instance.writeNBT();
}
@Override
public Enum getType() {
return EnumDataPacket.TELEGRAPH_SPAWN;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.DATA_PACKET;
}
@Override
public void sendData(ByteBuf out) throws IOException {
ByteBufUtils.writeNBT(out, telegraphNBT);
}
@SideOnly(Side.CLIENT)
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
NBTTagCompound nbt = ByteBufUtils.readNBT(in);
if (nbt != null && TelegraphManager.ClientInstance != null) {
TelegraphInstance instance = new TelegraphInstance();
instance.readNBT(nbt);
TelegraphManager.ClientInstance.addTelegraph(instance);
}
}
/**
* Send telegraph to a specific player.
*/
public static void send(TelegraphInstance instance, EntityPlayerMP player) {
PacketHandler.Instance.sendToPlayer(new TelegraphSpawnPacket(instance), player);
}
/**
* Send telegraph to all players tracking an entity (within range).
*/
public static void sendToTracking(TelegraphInstance instance, net.minecraft.entity.Entity entity) {
PacketHandler.Instance.sendTracking(new TelegraphSpawnPacket(instance), entity);
}
/**
* Send telegraph to all players in a dimension.
*/
public static void sendToDimension(TelegraphInstance instance, int dimensionId) {
PacketHandler.Instance.sendToDimension(new TelegraphSpawnPacket(instance), dimensionId);
}
}
@@ -0,0 +1,45 @@
package kamkeel.npcs.network.packets.request.ability;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumRequestPacket;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import noppes.npcs.NoppesUtilServer;
import java.io.IOException;
public class AbilitiesGetAllPacket extends AbstractPacket {
public static String packetName = "Request|AbilitiesGetAll";
public AbilitiesGetAllPacket() {
}
@Override
public Enum getType() {
return EnumRequestPacket.AbilitiesGetAll;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.REQUEST_PACKET;
}
@SideOnly(Side.CLIENT)
@Override
public void sendData(ByteBuf out) throws IOException {
}
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
if (!(player instanceof EntityPlayerMP))
return;
NoppesUtilServer.sendAbilityTypesInfo((EntityPlayerMP) player);
}
}
@@ -0,0 +1,54 @@
package kamkeel.npcs.network.packets.request.ability;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.PacketUtil;
import kamkeel.npcs.network.enums.EnumItemPacketType;
import kamkeel.npcs.network.enums.EnumRequestPacket;
import kamkeel.npcs.network.packets.data.large.GuiDataPacket;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import java.io.IOException;
public final class AbilitiesNpcGetPacket extends AbstractPacket {
public static final String packetName = "Request|NpcAbilitiesGet";
public AbilitiesNpcGetPacket() {
}
@Override
public Enum getType() {
return EnumRequestPacket.NpcAbilitiesGet;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.REQUEST_PACKET;
}
@Override
public boolean needsNPC() {
return true;
}
@SideOnly(Side.CLIENT)
@Override
public void sendData(ByteBuf out) throws IOException {
}
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.WAND))
return;
NBTTagCompound compound = new NBTTagCompound();
npc.abilities.writeToNBT(compound);
GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound);
}
}
@@ -0,0 +1,69 @@
package kamkeel.npcs.network.packets.request.ability;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.PacketUtil;
import kamkeel.npcs.network.enums.EnumItemPacketType;
import kamkeel.npcs.network.enums.EnumRequestPacket;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.CustomNpcsPermissions;
import java.io.IOException;
public final class AbilitiesNpcSavePacket extends AbstractPacket {
public static final String packetName = "Request|NpcAbilitiesSave";
private NBTTagCompound abilitiesData;
public AbilitiesNpcSavePacket() {
}
public AbilitiesNpcSavePacket(NBTTagCompound abilitiesData) {
this.abilitiesData = abilitiesData;
}
@Override
public Enum getType() {
return EnumRequestPacket.NpcAbilitiesSave;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.REQUEST_PACKET;
}
@Override
public boolean needsNPC() {
return true;
}
@Override
public CustomNpcsPermissions.Permission getPermission() {
return CustomNpcsPermissions.NPC_ADVANCED_ABILITIES;
}
@SideOnly(Side.CLIENT)
@Override
public void sendData(ByteBuf out) throws IOException {
NBTTagCompound compound = new NBTTagCompound();
if (abilitiesData != null) {
compound = abilitiesData;
}
ByteBufUtils.writeNBT(out, compound);
}
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
if (!PacketUtil.verifyItemPacket(packetName, player, EnumItemPacketType.WAND))
return;
NBTTagCompound compound = ByteBufUtils.readNBT(in);
npc.abilities.readFromNBT(compound);
}
}
@@ -0,0 +1,185 @@
package kamkeel.npcs.util;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.reflect.Method;
/**
* Utility class for Bukkit integration detection and common operations.
* Must be initialized after the server has fully started to ensure Bukkit plugins are loaded.
*/
public class BukkitUtil {
private static final Logger logger = LogManager.getLogger(BukkitUtil.class);
private static boolean initialized = false;
private static boolean bukkitEnabled = false;
// Bukkit classes
private static Class<?> bukkitClass;
private static Class<?> serverClass;
private static Class<?> servicesManagerClass;
private static Class<?> registeredServiceProviderClass;
private static Class<?> offlinePlayerClass;
private static Class<?> playerClass;
// Bukkit methods
private static Method getServer;
private static Method getServicesManager;
private static Method getRegistration;
private static Method getProvider;
private static Method getOfflinePlayer;
private static Method getPlayer;
private static Method getPluginManager;
private static Method isPluginEnabled;
/**
* Initializes Bukkit integration using reflection.
* Should be called during FMLServerStartedEvent to ensure Bukkit is fully loaded.
*/
public static void init() {
if (initialized) {
return;
}
initialized = true;
try {
// Load Bukkit classes
bukkitClass = Class.forName("org.bukkit.Bukkit");
serverClass = Class.forName("org.bukkit.Server");
servicesManagerClass = Class.forName("org.bukkit.plugin.ServicesManager");
registeredServiceProviderClass = Class.forName("org.bukkit.plugin.RegisteredServiceProvider");
offlinePlayerClass = Class.forName("org.bukkit.OfflinePlayer");
playerClass = Class.forName("org.bukkit.entity.Player");
// Get Bukkit methods
getServer = bukkitClass.getMethod("getServer");
getServicesManager = bukkitClass.getMethod("getServicesManager");
getRegistration = servicesManagerClass.getMethod("getRegistration", Class.class);
getProvider = registeredServiceProviderClass.getMethod("getProvider");
getOfflinePlayer = bukkitClass.getMethod("getOfflinePlayer", String.class);
getPlayer = bukkitClass.getMethod("getPlayer", String.class);
getPluginManager = bukkitClass.getMethod("getPluginManager");
Class<?> pluginManagerClass = Class.forName("org.bukkit.plugin.PluginManager");
isPluginEnabled = pluginManagerClass.getMethod("isPluginEnabled", String.class);
bukkitEnabled = true;
logger.info("Bukkit integration enabled");
// Initialize Vault after Bukkit is confirmed
VaultUtil.init();
} catch (ClassNotFoundException e) {
logger.debug("Bukkit not found, Bukkit integration disabled");
} catch (NoSuchMethodException e) {
logger.error("Bukkit API method not found", e);
} catch (Exception e) {
logger.error("Error initializing Bukkit integration", e);
}
}
/**
* @return true if Bukkit integration is enabled
*/
public static boolean isEnabled() {
return bukkitEnabled;
}
/**
* @return true if Bukkit has been initialized (may or may not be enabled)
*/
public static boolean isInitialized() {
return initialized;
}
/**
* Checks if a specific Bukkit plugin is enabled.
* @param pluginName the plugin name to check
* @return true if the plugin is enabled
*/
public static boolean isPluginEnabled(String pluginName) {
if (!bukkitEnabled) return false;
try {
Object pluginManager = getPluginManager.invoke(null);
return (Boolean) isPluginEnabled.invoke(pluginManager, pluginName);
} catch (Exception e) {
logger.error("Error checking if plugin is enabled: " + pluginName, e);
return false;
}
}
/**
* Gets a Bukkit OfflinePlayer by name.
* @param playerName the player name
* @return the OfflinePlayer object, or null if not available
*/
public static Object getOfflinePlayer(String playerName) {
if (!bukkitEnabled) return null;
try {
return getOfflinePlayer.invoke(null, playerName);
} catch (Exception e) {
logger.error("Error getting OfflinePlayer: " + playerName, e);
return null;
}
}
/**
* Gets a Bukkit Player by name (online players only).
* @param playerName the player name
* @return the Player object, or null if not online or not available
*/
public static Object getPlayer(String playerName) {
if (!bukkitEnabled) return null;
try {
return getPlayer.invoke(null, playerName);
} catch (Exception e) {
logger.error("Error getting Player: " + playerName, e);
return null;
}
}
/**
* Gets a service provider from Bukkit's ServicesManager.
* @param serviceClass the service class to get
* @return the service provider, or null if not available
*/
public static Object getServiceProvider(Class<?> serviceClass) {
if (!bukkitEnabled) return null;
try {
Object servicesManager = getServicesManager.invoke(null);
Object registration = getRegistration.invoke(servicesManager, serviceClass);
if (registration != null) {
return getProvider.invoke(registration);
}
} catch (Exception e) {
logger.error("Error getting service provider: " + serviceClass.getName(), e);
}
return null;
}
// Getters for classes (for use by other utils like VaultUtil)
public static Class<?> getBukkitClass() {
return bukkitClass;
}
public static Class<?> getServicesManagerClass() {
return servicesManagerClass;
}
public static Class<?> getRegisteredServiceProviderClass() {
return registeredServiceProviderClass;
}
public static Class<?> getOfflinePlayerClass() {
return offlinePlayerClass;
}
public static Class<?> getPlayerClass() {
return playerClass;
}
}
@@ -0,0 +1,575 @@
package kamkeel.npcs.util;
import net.minecraft.entity.player.EntityPlayer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.lang.reflect.Method;
/**
* Utility class for Vault Economy integration.
* Uses reflection to access Vault API when available.
* Requires BukkitUtil to be initialized first.
*/
public class VaultUtil {
private static final Logger logger = LogManager.getLogger(VaultUtil.class);
private static boolean initialized = false;
private static boolean vaultEnabled = false;
// Vault classes
private static Class<?> economyClass;
private static Class<?> economyResponseClass;
// Economy methods
private static Method isEnabled;
private static Method getName;
private static Method hasAccount;
private static Method hasAccountOffline;
private static Method getBalance;
private static Method getBalanceOffline;
private static Method has;
private static Method hasOffline;
private static Method withdrawPlayer;
private static Method withdrawPlayerOffline;
private static Method depositPlayer;
private static Method depositPlayerOffline;
private static Method format;
private static Method currencyNamePlural;
private static Method currencyNameSingular;
private static Method createPlayerAccount;
private static Method createPlayerAccountOffline;
private static Method hasBankSupport;
private static Method getBanks;
private static Method bankBalance;
private static Method bankHas;
private static Method bankWithdraw;
private static Method bankDeposit;
// EconomyResponse methods
private static Method transactionSuccess;
// Cached economy instance
private static Object economyInstance;
/**
* Initializes the Vault integration using reflection.
* Called automatically by BukkitUtil after Bukkit is confirmed available.
*/
public static void init() {
if (initialized) {
return;
}
initialized = true;
if (!BukkitUtil.isEnabled()) {
logger.debug("Bukkit not available, Vault integration disabled");
return;
}
try {
Class<?> offlinePlayerClass = BukkitUtil.getOfflinePlayerClass();
if (offlinePlayerClass == null) {
logger.error("Could not get OfflinePlayer class from BukkitUtil");
return;
}
// Load Vault classes
economyClass = Class.forName("net.milkbowl.vault.economy.Economy");
economyResponseClass = Class.forName("net.milkbowl.vault.economy.EconomyResponse");
// Get Economy methods (using deprecated String methods for broader compatibility)
isEnabled = economyClass.getMethod("isEnabled");
getName = economyClass.getMethod("getName");
hasAccount = economyClass.getMethod("hasAccount", String.class);
hasAccountOffline = economyClass.getMethod("hasAccount", offlinePlayerClass);
getBalance = economyClass.getMethod("getBalance", String.class);
getBalanceOffline = economyClass.getMethod("getBalance", offlinePlayerClass);
has = economyClass.getMethod("has", String.class, double.class);
hasOffline = economyClass.getMethod("has", offlinePlayerClass, double.class);
withdrawPlayer = economyClass.getMethod("withdrawPlayer", String.class, double.class);
withdrawPlayerOffline = economyClass.getMethod("withdrawPlayer", offlinePlayerClass, double.class);
depositPlayer = economyClass.getMethod("depositPlayer", String.class, double.class);
depositPlayerOffline = economyClass.getMethod("depositPlayer", offlinePlayerClass, double.class);
format = economyClass.getMethod("format", double.class);
currencyNamePlural = economyClass.getMethod("currencyNamePlural");
currencyNameSingular = economyClass.getMethod("currencyNameSingular");
createPlayerAccount = economyClass.getMethod("createPlayerAccount", String.class);
createPlayerAccountOffline = economyClass.getMethod("createPlayerAccount", offlinePlayerClass);
hasBankSupport = economyClass.getMethod("hasBankSupport");
getBanks = economyClass.getMethod("getBanks");
bankBalance = economyClass.getMethod("bankBalance", String.class);
bankHas = economyClass.getMethod("bankHas", String.class, double.class);
bankWithdraw = economyClass.getMethod("bankWithdraw", String.class, double.class);
bankDeposit = economyClass.getMethod("bankDeposit", String.class, double.class);
// Get EconomyResponse methods
transactionSuccess = economyResponseClass.getMethod("transactionSuccess");
// Get the economy instance
economyInstance = BukkitUtil.getServiceProvider(economyClass);
if (economyInstance != null) {
vaultEnabled = true;
logger.info("Vault Economy integration enabled - Provider: " + getEconomyName());
} else {
logger.info("Vault found but no Economy provider registered");
}
} catch (ClassNotFoundException e) {
logger.debug("Vault not found, economy integration disabled");
} catch (NoSuchMethodException e) {
logger.error("Vault API method not found, economy integration disabled", e);
} catch (Exception e) {
logger.error("Error initializing Vault integration", e);
}
}
/**
* Refreshes the economy provider. Call this if the economy plugin loads after initialization.
*/
public static void refreshEconomyProvider() {
if (!initialized || !BukkitUtil.isEnabled()) {
return;
}
try {
economyInstance = BukkitUtil.getServiceProvider(economyClass);
if (economyInstance != null) {
vaultEnabled = true;
logger.info("Vault Economy provider refreshed: " + getEconomyName());
}
} catch (Exception e) {
logger.error("Error refreshing Vault economy provider", e);
}
}
/**
* @return true if Vault economy integration is enabled and working
*/
public static boolean isEnabled() {
if (!vaultEnabled || economyInstance == null) return false;
try {
return (Boolean) isEnabled.invoke(economyInstance);
} catch (Exception e) {
logger.error("Error checking if economy is enabled", e);
return false;
}
}
/**
* @return the name of the economy provider, or null if not available
*/
public static String getEconomyName() {
if (!vaultEnabled || economyInstance == null) return null;
try {
return (String) getName.invoke(economyInstance);
} catch (Exception e) {
logger.error("Error getting economy name", e);
return null;
}
}
/**
* Checks if a player has an account in the economy system.
* @param player the player to check
* @return true if the player has an account
*/
public static boolean hasAccount(EntityPlayer player) {
return hasAccount(player.getCommandSenderName());
}
/**
* Checks if a player has an account in the economy system.
* @param playerName the player name to check
* @return true if the player has an account
*/
public static boolean hasAccount(String playerName) {
if (!vaultEnabled || economyInstance == null) return false;
try {
Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName);
if (offlinePlayer != null) {
return (Boolean) hasAccountOffline.invoke(economyInstance, offlinePlayer);
}
return (Boolean) hasAccount.invoke(economyInstance, playerName);
} catch (Exception e) {
logger.error("Error checking if player has account: " + playerName, e);
return false;
}
}
/**
* Gets the balance of a player.
* @param player the player
* @return the player's balance, or 0 if not available
*/
public static double getBalance(EntityPlayer player) {
return getBalance(player.getCommandSenderName());
}
/**
* Gets the balance of a player by name.
* @param playerName the player name
* @return the player's balance, or 0 if not available
*/
public static double getBalance(String playerName) {
if (!vaultEnabled || economyInstance == null) return 0;
try {
Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName);
if (offlinePlayer != null) {
return (Double) getBalanceOffline.invoke(economyInstance, offlinePlayer);
}
return (Double) getBalance.invoke(economyInstance, playerName);
} catch (Exception e) {
logger.error("Error getting balance for player: " + playerName, e);
return 0;
}
}
/**
* Checks if a player has at least the specified amount.
* @param player the player
* @param amount the amount to check
* @return true if the player has at least the specified amount
*/
public static boolean has(EntityPlayer player, double amount) {
return has(player.getCommandSenderName(), amount);
}
/**
* Checks if a player has at least the specified amount.
* @param playerName the player name
* @param amount the amount to check
* @return true if the player has at least the specified amount
*/
public static boolean has(String playerName, double amount) {
if (!vaultEnabled || economyInstance == null) return false;
try {
Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName);
if (offlinePlayer != null) {
return (Boolean) hasOffline.invoke(economyInstance, offlinePlayer, amount);
}
return (Boolean) has.invoke(economyInstance, playerName, amount);
} catch (Exception e) {
logger.error("Error checking if player has amount: " + playerName, e);
return false;
}
}
/**
* Withdraws money from a player's account.
* @param player the player
* @param amount the amount to withdraw
* @return true if the transaction was successful
*/
public static boolean withdrawMoney(EntityPlayer player, double amount) {
return withdrawMoney(player.getCommandSenderName(), amount);
}
/**
* Withdraws money from a player's account.
* @param playerName the player name
* @param amount the amount to withdraw
* @return true if the transaction was successful
*/
public static boolean withdrawMoney(String playerName, double amount) {
if (!vaultEnabled || economyInstance == null) return false;
try {
Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName);
Object response;
if (offlinePlayer != null) {
response = withdrawPlayerOffline.invoke(economyInstance, offlinePlayer, amount);
} else {
response = withdrawPlayer.invoke(economyInstance, playerName, amount);
}
return (Boolean) transactionSuccess.invoke(response);
} catch (Exception e) {
logger.error("Error withdrawing money from player: " + playerName, e);
return false;
}
}
/**
* Adds money to a player's account (deposit).
* @param player the player
* @param amount the amount to add
* @return true if the transaction was successful
*/
public static boolean addMoney(EntityPlayer player, double amount) {
return addMoney(player.getCommandSenderName(), amount);
}
/**
* Adds money to a player's account (deposit).
* @param playerName the player name
* @param amount the amount to add
* @return true if the transaction was successful
*/
public static boolean addMoney(String playerName, double amount) {
if (!vaultEnabled || economyInstance == null) return false;
try {
Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName);
Object response;
if (offlinePlayer != null) {
response = depositPlayerOffline.invoke(economyInstance, offlinePlayer, amount);
} else {
response = depositPlayer.invoke(economyInstance, playerName, amount);
}
return (Boolean) transactionSuccess.invoke(response);
} catch (Exception e) {
logger.error("Error adding money to player: " + playerName, e);
return false;
}
}
/**
* Sets a player's balance to the specified amount.
* This withdraws all money first, then deposits the new amount.
* @param player the player
* @param amount the new balance
* @return true if the transaction was successful
*/
public static boolean setBalance(EntityPlayer player, double amount) {
return setBalance(player.getCommandSenderName(), amount);
}
/**
* Sets a player's balance to the specified amount.
* This withdraws all money first, then deposits the new amount.
* @param playerName the player name
* @param amount the new balance
* @return true if the transaction was successful
*/
public static boolean setBalance(String playerName, double amount) {
if (!vaultEnabled || economyInstance == null) return false;
double currentBalance = getBalance(playerName);
if (currentBalance > 0) {
if (!withdrawMoney(playerName, currentBalance)) {
return false;
}
}
if (amount > 0) {
return addMoney(playerName, amount);
}
return true;
}
/**
* Formats an amount according to the economy plugin's settings.
* @param amount the amount to format
* @return the formatted string, or the raw number as string if not available
*/
public static String format(double amount) {
if (!vaultEnabled || economyInstance == null) return String.valueOf(amount);
try {
return (String) format.invoke(economyInstance, amount);
} catch (Exception e) {
logger.error("Error formatting amount", e);
return String.valueOf(amount);
}
}
/**
* Gets the plural currency name (e.g., "Dollars").
* @return the plural currency name, or "coins" if not available
*/
public static String getCurrencyNamePlural() {
if (!vaultEnabled || economyInstance == null) return "coins";
try {
return (String) currencyNamePlural.invoke(economyInstance);
} catch (Exception e) {
logger.error("Error getting currency name plural", e);
return "coins";
}
}
/**
* Gets the singular currency name (e.g., "Dollar").
* @return the singular currency name, or "coin" if not available
*/
public static String getCurrencyNameSingular() {
if (!vaultEnabled || economyInstance == null) return "coin";
try {
return (String) currencyNameSingular.invoke(economyInstance);
} catch (Exception e) {
logger.error("Error getting currency name singular", e);
return "coin";
}
}
/**
* Creates an account for the player if one doesn't exist.
* @param player the player
* @return true if the account was created or already exists
*/
public static boolean createAccount(EntityPlayer player) {
return createAccount(player.getCommandSenderName());
}
/**
* Creates an account for the player if one doesn't exist.
* @param playerName the player name
* @return true if the account was created or already exists
*/
public static boolean createAccount(String playerName) {
if (!vaultEnabled || economyInstance == null) return false;
if (hasAccount(playerName)) return true;
try {
Object offlinePlayer = BukkitUtil.getOfflinePlayer(playerName);
if (offlinePlayer != null) {
return (Boolean) createPlayerAccountOffline.invoke(economyInstance, offlinePlayer);
}
return (Boolean) createPlayerAccount.invoke(economyInstance, playerName);
} catch (Exception e) {
logger.error("Error creating account for player: " + playerName, e);
return false;
}
}
/**
* @return true if the economy supports bank accounts
*/
public static boolean hasBankSupport() {
if (!vaultEnabled || economyInstance == null) return false;
try {
return (Boolean) hasBankSupport.invoke(economyInstance);
} catch (Exception e) {
logger.error("Error checking bank support", e);
return false;
}
}
/**
* Gets a list of all bank names.
* @return list of bank names, or empty list if not available
*/
@SuppressWarnings("unchecked")
public static java.util.List<String> getBanks() {
if (!vaultEnabled || economyInstance == null) return java.util.Collections.emptyList();
try {
return (java.util.List<String>) getBanks.invoke(economyInstance);
} catch (Exception e) {
logger.error("Error getting banks", e);
return java.util.Collections.emptyList();
}
}
/**
* Gets the balance of a bank.
* @param bankName the bank name
* @return the bank balance, or 0 if not available
*/
public static double getBankBalance(String bankName) {
if (!vaultEnabled || economyInstance == null) return 0;
try {
Object response = bankBalance.invoke(economyInstance, bankName);
return economyResponseClass.getField("balance").getDouble(response);
} catch (Exception e) {
logger.error("Error getting bank balance: " + bankName, e);
return 0;
}
}
/**
* Checks if a bank has at least the specified amount.
* @param bankName the bank name
* @param amount the amount to check
* @return true if the bank has at least the specified amount
*/
public static boolean bankHas(String bankName, double amount) {
if (!vaultEnabled || economyInstance == null) return false;
try {
Object response = bankHas.invoke(economyInstance, bankName, amount);
return (Boolean) transactionSuccess.invoke(response);
} catch (Exception e) {
logger.error("Error checking bank balance: " + bankName, e);
return false;
}
}
/**
* Withdraws money from a bank.
* @param bankName the bank name
* @param amount the amount to withdraw
* @return true if the transaction was successful
*/
public static boolean bankWithdraw(String bankName, double amount) {
if (!vaultEnabled || economyInstance == null) return false;
try {
Object response = bankWithdraw.invoke(economyInstance, bankName, amount);
return (Boolean) transactionSuccess.invoke(response);
} catch (Exception e) {
logger.error("Error withdrawing from bank: " + bankName, e);
return false;
}
}
/**
* Deposits money into a bank.
* @param bankName the bank name
* @param amount the amount to deposit
* @return true if the transaction was successful
*/
public static boolean bankDeposit(String bankName, double amount) {
if (!vaultEnabled || economyInstance == null) return false;
try {
Object response = bankDeposit.invoke(economyInstance, bankName, amount);
return (Boolean) transactionSuccess.invoke(response);
} catch (Exception e) {
logger.error("Error depositing to bank: " + bankName, e);
return false;
}
}
/**
* Transfers money from one player to another.
* @param from the player to take money from
* @param to the player to give money to
* @param amount the amount to transfer
* @return true if the transfer was successful
*/
public static boolean transfer(EntityPlayer from, EntityPlayer to, double amount) {
return transfer(from.getCommandSenderName(), to.getCommandSenderName(), amount);
}
/**
* Transfers money from one player to another.
* @param fromPlayerName the player name to take money from
* @param toPlayerName the player name to give money to
* @param amount the amount to transfer
* @return true if the transfer was successful
*/
public static boolean transfer(String fromPlayerName, String toPlayerName, double amount) {
if (!has(fromPlayerName, amount)) {
return false;
}
if (!withdrawMoney(fromPlayerName, amount)) {
return false;
}
if (!addMoney(toPlayerName, amount)) {
// Rollback
addMoney(fromPlayerName, amount);
return false;
}
return true;
}
}
+10
View File
@@ -21,6 +21,7 @@ import kamkeel.npcs.controllers.SyncController;
import kamkeel.npcs.controllers.data.profile.CNPCData;
import kamkeel.npcs.developer.Developer;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.util.BukkitUtil;
import net.minecraft.block.Block;
import net.minecraft.block.BlockIce;
import net.minecraft.block.BlockLeavesBase;
@@ -60,12 +61,14 @@ import noppes.npcs.controllers.PlayerDataController;
import noppes.npcs.controllers.QuestController;
import noppes.npcs.controllers.RecipeController;
import noppes.npcs.controllers.ScriptController;
import noppes.npcs.controllers.ScriptHookController;
import noppes.npcs.controllers.ServerCloneController;
import noppes.npcs.controllers.ServerTagMapController;
import noppes.npcs.controllers.SpawnController;
import noppes.npcs.controllers.TagController;
import noppes.npcs.controllers.TransportController;
import noppes.npcs.enchants.EnchantInterface;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import noppes.npcs.entity.EntityChairMount;
import noppes.npcs.entity.EntityCustomNpc;
import noppes.npcs.entity.EntityMagicProjectile;
@@ -221,6 +224,7 @@ public class CustomNpcs {
MinecraftForge.EVENT_BUS.register(new ServerEventsHandler());
MinecraftForge.EVENT_BUS.register(new ScriptController());
new ScriptHookController();
ScriptPlayerEventHandler scriptPlayerEventHandler = new ScriptPlayerEventHandler();
MinecraftForge.EVENT_BUS.register(scriptPlayerEventHandler);
@@ -318,6 +322,8 @@ public class CustomNpcs {
new SpawnController();
new LinkedNpcController();
new AnimationController();
new AbilityController();
AbilityController.Instance.load();
LinkedItemController.getInstance().load();
@@ -363,6 +369,10 @@ public class CustomNpcs {
ServerCloneController.Instance = new ServerCloneController();
ServerTagMapController.Instance = new ServerTagMapController();
SyncController.load();
// Initialize Bukkit integration (loads Vault and Permissions)
BukkitUtil.init();
CustomNpcsPermissions.Instance.init();
}
@@ -3,6 +3,7 @@ package noppes.npcs;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.relauncher.Side;
import kamkeel.npcs.developer.Developer;
import kamkeel.npcs.util.BukkitUtil;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import org.apache.logging.log4j.LogManager;
@@ -47,7 +48,8 @@ public class CustomNpcsPermissions {
public static final Permission NPC_ADVANCED_TRANSPORT = new Permission("customnpcs.npc.advanced.transport");
public static final Permission NPC_ADVANCED_TRANSFORM = new Permission("customnpcs.npc.advanced.transform");
public static final Permission NPC_ADVANCED_LINKED = new Permission("customnpcs.npc.advanced.linked");
public static final Permission NPC_ADVANCED_MAGIC = new Permission("customnpcs.npc.advanced.linked");
public static final Permission NPC_ADVANCED_MAGIC = new Permission("customnpcs.npc.advanced.magic");
public static final Permission NPC_ADVANCED_ABILITIES = new Permission("customnpcs.npc.advanced.abilities");
public static final Permission NPC_ADVANCED_TAGS = new Permission("customnpcs.npc.advanced.tags");
public static final Permission GLOBAL_REMOTE = new Permission("customnpcs.global.remote");
@@ -109,24 +111,29 @@ public class CustomNpcsPermissions {
public static final Permission PROFILE_MAX = new Permission("customnpcs.profile.max.*");
public static CustomNpcsPermissions Instance;
private Class<?> bukkit;
private Method getPlayer;
private Method hasPermission;
public CustomNpcsPermissions() {
Instance = this;
}
/**
* Initializes permission checking using BukkitUtil.
* Called after BukkitUtil is initialized on server start.
*/
public void init() {
if (!BukkitUtil.isEnabled()) {
return;
}
try {
bukkit = Class.forName("org.bukkit.Bukkit");
getPlayer = bukkit.getMethod("getPlayer", String.class);
hasPermission = Class.forName("org.bukkit.entity.Player").getMethod("hasPermission", String.class);
hasPermission = BukkitUtil.getPlayerClass().getMethod("hasPermission", String.class);
LogManager.getLogger(CustomNpcs.class).info("Bukkit permissions enabled");
LogManager.getLogger(CustomNpcs.class).info("Permissions available:");
Collections.sort(Permission.permissions, String.CASE_INSENSITIVE_ORDER);
for (String p : Permission.permissions) {
LogManager.getLogger(CustomNpcs.class).info(p);
}
} catch (ClassNotFoundException e) {
// bukkit/mcpc+ is not loaded
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
@@ -141,7 +148,7 @@ public class CustomNpcsPermissions {
if (Developer.Instance.hasUniversal(player.getUniqueID()))
return true;
if (permission != null) {
if (Instance.bukkit != null) {
if (BukkitUtil.isEnabled()) {
return Instance.bukkitPermission(player.getCommandSenderName(), permission.name);
}
}
@@ -155,7 +162,7 @@ public class CustomNpcsPermissions {
return true;
if (Developer.Instance.hasUniversal(player.getUniqueID()))
return true;
if (Instance.bukkit != null) {
if (BukkitUtil.isEnabled()) {
return Instance.bukkitPermission(player.getCommandSenderName(), permission);
}
}
@@ -163,8 +170,11 @@ public class CustomNpcsPermissions {
}
private boolean bukkitPermission(String username, String permission) {
if (hasPermission == null) return false;
try {
Object player = getPlayer.invoke(null, username);
Object player = BukkitUtil.getPlayer(username);
if (player == null) return false;
return (Boolean) hasPermission.invoke(player, permission);
} catch (IllegalAccessException e) {
e.printStackTrace();
@@ -188,13 +198,13 @@ public class CustomNpcsPermissions {
}
public static boolean hasPermissionString(EntityPlayerMP player, String permission) {
if (Instance.bukkit != null) {
if (BukkitUtil.isEnabled()) {
return Instance.bukkitPermission(player.getCommandSenderName(), permission);
}
return true;
}
public static boolean enabled() {
return Instance.bukkit != null;
return BukkitUtil.isEnabled();
}
}
@@ -0,0 +1,725 @@
package noppes.npcs;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityPhase;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance;
import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket;
import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.DamageSource;
import noppes.npcs.controllers.AnimationController;
import noppes.npcs.controllers.data.Animation;
import noppes.npcs.entity.EntityNPCInterface;
import noppes.npcs.scripted.NpcAPI;
import noppes.npcs.scripted.event.AbilityEvent;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* Manages NPC abilities - storage, selection, execution, and cooldowns.
* Follows the DataStats/DataAI pattern for NPC data management.
*/
public class DataAbilities {
private final EntityNPCInterface npc;
private final Random random = new Random();
// ═══════════════════════════════════════════════════════════════════
// CONFIGURATION (saved to NBT)
// ═══════════════════════════════════════════════════════════════════
/** List of abilities this NPC can use */
private List<Ability> abilities = new ArrayList<>();
/** Whether the ability system is enabled for this NPC */
public boolean enabled = false;
/** Minimum ticks between ability selections (global cooldown) */
public int globalCooldown = 20;
// ═══════════════════════════════════════════════════════════════════
// RUNTIME STATE (not saved)
// ═══════════════════════════════════════════════════════════════════
/** Currently executing ability (null if none) */
private transient Ability currentAbility;
/** Per-ability cooldown timers (ability ID -> world time when cooldown ends) */
private transient Map<String, Long> cooldowns = new HashMap<>();
/** Ticks until next ability can be selected (global cooldown timer) */
private transient int globalCooldownTimer = 0;
/** Last target used for ability execution */
private transient EntityLivingBase lastTarget;
/** Recent hit timestamps for hit count condition */
private transient List<Long> recentHitTimes = new ArrayList<>();
// ═══════════════════════════════════════════════════════════════════
// CONSTRUCTOR
// ═══════════════════════════════════════════════════════════════════
public DataAbilities(EntityNPCInterface npc) {
this.npc = npc;
}
// ═══════════════════════════════════════════════════════════════════
// TICK - Called from EntityNPCInterface.onLivingUpdate()
// ═══════════════════════════════════════════════════════════════════
/**
* Main tick method - handles ability execution and cooldown management.
* Should be called every tick from onLivingUpdate().
*/
public void tick() {
if (!enabled || npc.worldObj.isRemote || npc.isKilled()) {
return;
}
// Tick global cooldown
if (globalCooldownTimer > 0) {
globalCooldownTimer--;
}
// Tick current ability if executing
if (currentAbility != null && currentAbility.isExecuting()) {
tickCurrentAbility();
}
}
/**
* Tick the currently executing ability.
*/
private void tickCurrentAbility() {
EntityLivingBase target = lastTarget != null ? lastTarget : npc.getAttackTarget();
AbilityPhase oldPhase = currentAbility.getPhase();
// Tick advances time and possibly changes phase
boolean phaseChanged = currentAbility.tick();
// Fire tick event for scripts
fireTickEvent(currentAbility, target);
// Handle phase-specific logic
switch (currentAbility.getPhase()) {
case WINDUP:
currentAbility.onWindUpTick(npc, target, npc.worldObj, currentAbility.getCurrentTick());
break;
case ACTIVE:
if (phaseChanged && oldPhase == AbilityPhase.WINDUP) {
// Just entered ACTIVE phase - lock telegraph position if it was following
// This commits the ability to its current target position
TelegraphInstance telegraph = currentAbility.getTelegraphInstance();
if (telegraph != null) {
telegraph.lockPosition();
}
// Remove telegraph - it has served its purpose
removeTelegraph(currentAbility);
// Play active sound and animation
playAbilitySound(currentAbility.getActiveSound());
playAbilityAnimation(currentAbility.getActiveAnimationId());
// Fire execute event (cancelable)
AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent(
npc.wrappedNPC, currentAbility, target);
if (NpcAPI.EVENT_BUS.post(executeEvent)) {
// Event was cancelled - skip execution but continue to recovery
return;
}
// Call onExecute
currentAbility.onExecute(npc, target, npc.worldObj);
}
currentAbility.onActiveTick(npc, target, npc.worldObj, currentAbility.getCurrentTick());
break;
case RECOVERY:
// Recovery phase - just wait
break;
case IDLE:
// Ability completed
currentAbility.onComplete(npc, target);
// Fire complete event
AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent(
npc.wrappedNPC, currentAbility, target);
NpcAPI.EVENT_BUS.post(completeEvent);
onAbilityComplete();
break;
}
// Clear AI pathfinding if ability is controlling movement
// But DON'T zero motion - let the ability set it if needed
if (currentAbility != null && currentAbility.isExecuting()) {
if (isAbilityControllingMovement()) {
// Clear navigator so AI doesn't fight with ability movement
npc.getNavigator().clearPathEntity();
// Only zero motion if lockMovement is true AND ability doesn't have its own movement
// This allows stationary abilities to freeze the NPC, while movement abilities can set their own motion
if (currentAbility.isLockMovement() && !currentAbility.hasAbilityMovement()) {
npc.motionX = 0;
npc.motionZ = 0;
}
}
}
}
/**
* Called when an ability completes.
*/
private void onAbilityComplete() {
if (currentAbility != null) {
// Stop any ability animation
stopAbilityAnimation();
// Start per-ability cooldown
startCooldown(currentAbility);
currentAbility = null;
lastTarget = null;
// Start global cooldown
globalCooldownTimer = globalCooldown;
}
}
// ═══════════════════════════════════════════════════════════════════
// ABILITY SELECTION - Called from CombatHandler
// ═══════════════════════════════════════════════════════════════════
/**
* Try to select and start an ability for the given target.
* Called by CombatHandler when NPC is in combat and ready for an ability.
*
* @param target The combat target
* @return true if an ability was started
*/
public boolean trySelectAndStart(EntityLivingBase target) {
if (!canSelectAbility()) {
return false;
}
Ability selected = selectAbility(target);
if (selected == null) {
return false;
}
return startAbility(selected, target);
}
/**
* Check if an ability can be selected right now.
*/
public boolean canSelectAbility() {
if (!enabled || abilities.isEmpty()) {
return false;
}
if (currentAbility != null && currentAbility.isExecuting()) {
return false;
}
if (globalCooldownTimer > 0) {
return false;
}
return true;
}
/**
* Select an ability using weighted random from eligible abilities.
*/
private Ability selectAbility(EntityLivingBase target) {
List<Ability> eligible = new ArrayList<>();
int totalWeight = 0;
for (Ability ability : abilities) {
if (isAbilityEligible(ability, target)) {
eligible.add(ability);
totalWeight += ability.getWeight();
}
}
if (eligible.isEmpty() || totalWeight <= 0) {
return null;
}
// Weighted random selection
int roll = random.nextInt(totalWeight);
int cumulative = 0;
for (Ability ability : eligible) {
cumulative += ability.getWeight();
if (roll < cumulative) {
return ability;
}
}
return eligible.get(eligible.size() - 1);
}
/**
* Check if a specific ability is eligible for use.
*/
private boolean isAbilityEligible(Ability ability, EntityLivingBase target) {
// Check enabled
if (!ability.isEnabled()) {
return false;
}
// Check cooldown
if (isOnCooldown(ability)) {
return false;
}
// Check if already executing
if (ability.isExecuting()) {
return false;
}
// Check range
if (target != null) {
float distance = npc.getDistanceToEntity(target);
if (distance < ability.getMinRange() || distance > ability.getMaxRange()) {
return false;
}
}
// Check conditions
if (!ability.checkConditions(npc, target)) {
return false;
}
return true;
}
/**
* Start executing an ability.
*/
private boolean startAbility(Ability ability, EntityLivingBase target) {
// Fire start event (cancelable)
AbilityEvent.StartEvent startEvent = new AbilityEvent.StartEvent(
npc.wrappedNPC, ability, target);
if (NpcAPI.EVENT_BUS.post(startEvent)) {
// Event was cancelled - don't start the ability
return false;
}
currentAbility = ability;
lastTarget = target;
ability.start(target);
// Spawn telegraph and send to clients
spawnTelegraph(ability, target);
// Play wind up sound if configured
playAbilitySound(ability.getWindUpSound());
// Play wind up animation if configured
playAbilityAnimation(ability.getWindUpAnimationId());
return true;
}
/**
* Play a sound at the NPC's location.
*/
private void playAbilitySound(String sound) {
if (sound != null && !sound.isEmpty()) {
npc.worldObj.playSoundAtEntity(npc, sound, 1.0f, 1.0f);
}
}
/**
* Play an animation on the NPC by ID.
*/
private void playAbilityAnimation(int animationId) {
if (animationId < 0) return;
if (AnimationController.Instance == null) return;
Animation animation = AnimationController.Instance.animations.get(animationId);
if (animation != null) {
npc.display.animationData.setEnabled(true);
npc.display.animationData.setAnimation(animation);
npc.display.animationData.updateClient();
}
}
/**
* Stop any currently playing ability animation.
*/
private void stopAbilityAnimation() {
npc.display.animationData.setAnimation(null);
npc.display.animationData.updateClient();
}
/**
* Spawn and send telegraph for an ability.
*/
private void spawnTelegraph(Ability ability, EntityLivingBase target) {
TelegraphInstance telegraph = ability.createTelegraph(npc, target);
if (telegraph != null) {
ability.setTelegraphInstance(telegraph);
// Send to all nearby players
TelegraphSpawnPacket.sendToTracking(telegraph, npc);
}
}
/**
* Remove telegraph when ability ends or is interrupted.
*/
private void removeTelegraph(Ability ability) {
TelegraphInstance telegraph = ability.getTelegraphInstance();
if (telegraph != null) {
TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), npc);
ability.setTelegraphInstance(null);
}
}
// ═══════════════════════════════════════════════════════════════════
// COOLDOWN MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
/**
* Check if an ability is on cooldown.
*/
public boolean isOnCooldown(Ability ability) {
Long endTime = cooldowns.get(ability.getId());
if (endTime == null) {
return false;
}
return npc.worldObj.getTotalWorldTime() < endTime;
}
/**
* Start cooldown for an ability.
*/
private void startCooldown(Ability ability) {
long endTime = npc.worldObj.getTotalWorldTime() + ability.getCooldownTicks();
cooldowns.put(ability.getId(), endTime);
}
/**
* Reset cooldown for a specific ability.
*/
public void resetCooldown(String abilityId) {
cooldowns.remove(abilityId);
}
/**
* Reset all cooldowns.
*/
public void resetAllCooldowns() {
cooldowns.clear();
globalCooldownTimer = 0;
}
// ═══════════════════════════════════════════════════════════════════
// INTERRUPTION - Called from CombatHandler on damage
// ═══════════════════════════════════════════════════════════════════
/**
* Handle damage taken - may interrupt current ability.
*
* @param source The damage source
* @param amount The damage amount
* @return true if ability was interrupted
*/
public boolean onDamage(DamageSource source, float amount) {
// Track hit for hit count condition
recordHit();
if (currentAbility == null || !currentAbility.isExecuting()) {
return false;
}
if (currentAbility.canInterrupt(source)) {
interruptCurrentAbility(source, amount);
return true;
}
return false;
}
/**
* Record a hit for the hit count condition.
*/
private void recordHit() {
recentHitTimes.add(npc.worldObj.getTotalWorldTime());
}
/**
* Get the number of hits received within the specified tick window.
*
* @param withinTicks The time window in ticks
* @return Number of hits in that window
*/
public int getRecentHitCount(int withinTicks) {
long currentTime = npc.worldObj.getTotalWorldTime();
long cutoff = currentTime - withinTicks;
// Clean up old entries and count
int count = 0;
recentHitTimes.removeIf(time -> time < cutoff);
for (Long time : recentHitTimes) {
if (time >= cutoff) {
count++;
}
}
return count;
}
/**
* Interrupt the currently executing ability.
*/
public void interruptCurrentAbility(DamageSource source, float damage) {
if (currentAbility != null) {
// Remove telegraph early
removeTelegraph(currentAbility);
// Stop any ability animation
stopAbilityAnimation();
// Fire interrupt event
AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent(
npc.wrappedNPC, currentAbility, lastTarget, source, damage);
NpcAPI.EVENT_BUS.post(interruptEvent);
currentAbility.onInterrupt(npc, source, damage);
currentAbility.interrupt();
currentAbility = null;
lastTarget = null;
}
}
/**
* Force stop the current ability (no interrupt event).
*/
public void stopCurrentAbility() {
if (currentAbility != null) {
removeTelegraph(currentAbility);
stopAbilityAnimation();
currentAbility.interrupt();
currentAbility = null;
lastTarget = null;
}
}
// ═══════════════════════════════════════════════════════════════════
// SCRIPT EVENTS
// ═══════════════════════════════════════════════════════════════════
/**
* Fire the ability tick event for scripts.
*/
private void fireTickEvent(Ability ability, EntityLivingBase target) {
AbilityEvent.TickEvent event = new AbilityEvent.TickEvent(
npc.wrappedNPC, ability, target,
ability.getPhase().ordinal(), ability.getCurrentTick());
NpcAPI.EVENT_BUS.post(event);
}
/**
* Fire an ability hit event. Called by abilities when they hit an entity.
* Returns null if the event was cancelled, otherwise returns the (possibly modified) event.
*
* @param ability The ability doing the hit
* @param target The original target of the ability
* @param hitEntity The entity being hit
* @param damage The damage amount
* @param knockback The horizontal knockback
* @param knockbackUp The vertical knockback (deprecated, ignored)
* @return The event (with possibly modified values), or null if cancelled
*/
public AbilityEvent.HitEvent fireHitEvent(Ability ability, EntityLivingBase target,
EntityLivingBase hitEntity, float damage,
float knockback, float knockbackUp) {
AbilityEvent.HitEvent event = new AbilityEvent.HitEvent(
npc.wrappedNPC, ability, target, hitEntity, damage, knockback, knockbackUp);
if (NpcAPI.EVENT_BUS.post(event)) {
return null; // Cancelled
}
return event;
}
/**
* Get the NPC this DataAbilities belongs to.
*/
public EntityNPCInterface getNpc() {
return npc;
}
// ═══════════════════════════════════════════════════════════════════
// RESET - Called on NPC death/respawn
// ═══════════════════════════════════════════════════════════════════
/**
* Reset all runtime state (cooldowns, current ability).
* Called when NPC respawns or combat ends.
*/
public void reset() {
stopCurrentAbility();
resetAllCooldowns();
// Reset execution state on all abilities
for (Ability ability : abilities) {
ability.reset();
}
}
// ═══════════════════════════════════════════════════════════════════
// ABILITY LIST MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
public List<Ability> getAbilities() {
return abilities;
}
public void addAbility(Ability ability) {
abilities.add(ability);
}
public void removeAbility(int index) {
if (index >= 0 && index < abilities.size()) {
abilities.remove(index);
}
}
public void removeAbility(String id) {
abilities.removeIf(a -> a.getId().equals(id));
}
public Ability getAbility(String id) {
for (Ability ability : abilities) {
if (ability.getId().equals(id)) {
return ability;
}
}
return null;
}
public void clearAbilities() {
abilities.clear();
}
public boolean isEmpty() {
return abilities.isEmpty();
}
// ═══════════════════════════════════════════════════════════════════
// STATE QUERIES
// ═══════════════════════════════════════════════════════════════════
/**
* Check if NPC is currently executing an ability.
*/
public boolean isExecutingAbility() {
return currentAbility != null && currentAbility.isExecuting();
}
/**
* Get the currently executing ability.
*/
public Ability getCurrentAbility() {
return currentAbility;
}
/**
* Check if ability is controlling NPC movement (AI pathfinding should be blocked).
* Called by AI tasks to determine if they should try to path.
*
* Rules:
* - WINDUP phase: Block AI if lockMovement is true
* - ACTIVE phase: Block AI if lockMovement OR hasAbilityMovement is true
* - RECOVERY phase: Block AI if lockMovement is true
*/
public boolean isAbilityControllingMovement() {
if (currentAbility == null || !currentAbility.isExecuting()) {
return false;
}
AbilityPhase phase = currentAbility.getPhase();
switch (phase) {
case WINDUP:
// During windup, only block if lockMovement is set
// This allows NPC to keep chasing while winding up if lockMovement=false
return currentAbility.isLockMovement();
case ACTIVE:
// During active phase, block if either:
// - lockMovement is true (stationary ability)
// - hasAbilityMovement is true (ability is moving the NPC itself)
return currentAbility.isLockMovement() || currentAbility.hasAbilityMovement();
case RECOVERY:
// During recovery, block if lockMovement is set
return currentAbility.isLockMovement();
default:
return false;
}
}
/**
* Check if NPC movement should be blocked due to ability execution.
* @deprecated Use isAbilityControllingMovement() instead
*/
@Deprecated
public boolean isMovementBlocked() {
return isAbilityControllingMovement();
}
/**
* Check if NPC should skip normal attacks during ability execution.
*/
public boolean shouldBlockAttack() {
if (currentAbility == null || !currentAbility.isExecuting()) {
return false;
}
AbilityPhase phase = currentAbility.getPhase();
return phase == AbilityPhase.WINDUP || phase == AbilityPhase.ACTIVE;
}
// ═══════════════════════════════════════════════════════════════════
// NBT SERIALIZATION
// ═══════════════════════════════════════════════════════════════════
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
compound.setBoolean("AbilitiesEnabled", enabled);
compound.setInteger("AbilityGlobalCooldown", globalCooldown);
NBTTagList abilityList = new NBTTagList();
for (Ability ability : abilities) {
abilityList.appendTag(ability.writeNBT());
}
compound.setTag("Abilities", abilityList);
return compound;
}
public void readFromNBT(NBTTagCompound compound) {
enabled = compound.getBoolean("AbilitiesEnabled");
globalCooldown = compound.hasKey("AbilityGlobalCooldown") ?
compound.getInteger("AbilityGlobalCooldown") : 20;
abilities.clear();
NBTTagList abilityList = compound.getTagList("Abilities", 10);
for (int i = 0; i < abilityList.tagCount(); i++) {
NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i);
Ability ability = AbilityController.Instance.fromNBT(abilityNBT);
if (ability != null) {
abilities.add(ability);
}
}
}
}
@@ -763,6 +763,18 @@ public class NoppesUtilServer {
}
}
public static void sendAbilityTypesInfo(EntityPlayerMP player) {
HashMap<String, Integer> map = new HashMap<String, Integer>();
kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance;
if (controller != null) {
int index = 0;
for (String typeId : controller.getTypes()) {
map.put(typeId, index++);
}
}
sendScrollData(player, map, EnumScrollData.ABILITY_TYPES);
}
public static DialogOption setNpcDialog(int slot, int dialogId, EntityPlayer player) throws IOException {
return setNpcDialog(slot, dialogId, getEditingNpc(player));
}
@@ -42,8 +42,14 @@ public class CombatHandler {
return;
}
combatResetTimer = 0;
}
// Try to select and start an ability if not currently executing one
// Ability ticking is handled by DataAbilities in onLivingUpdate
EntityLivingBase target = npc.getAttackTarget();
if (target != null && npc.abilities.enabled && !npc.abilities.isExecutingAbility()) {
npc.abilities.trySelectAndStart(target);
}
}
private boolean shouldCombatContinue() {
if (npc.getAttackTarget() == null)
@@ -72,6 +78,18 @@ public class CombatHandler {
}
aggressors.put(el, f + damageAmount);
}
// Handle guard counter before interruption
if (npc.abilities.getCurrentAbility() instanceof kamkeel.npcs.controllers.data.ability.type.AbilityGuard) {
kamkeel.npcs.controllers.data.ability.type.AbilityGuard guard =
(kamkeel.npcs.controllers.data.ability.type.AbilityGuard) npc.abilities.getCurrentAbility();
if (guard.isGuarding() && e instanceof EntityLivingBase) {
guard.onDamageTaken(npc, (EntityLivingBase) e, source, damageAmount);
}
}
// Check for ability interruption
npc.abilities.onDamage(source, damageAmount);
}
public void start() {
@@ -86,6 +104,9 @@ public class CombatHandler {
aggressors.clear();
recentDamages.clear();
npc.setBoolFlag(false, 4);
// Reset ability state when combat ends
npc.abilities.reset();
}
public boolean checkTarget() {
@@ -148,4 +169,26 @@ public class CombatHandler {
}
return newThreat > currentThreat;
}
/**
* Check if NPC is currently executing an ability.
* Used by AI tasks to avoid conflicting with ability execution.
*/
public boolean isExecutingAbility() {
return npc.abilities.isExecutingAbility();
}
/**
* Check if NPC movement should be blocked due to ability execution.
*/
public boolean isMovementBlocked() {
return npc.abilities.isMovementBlocked();
}
/**
* Check if NPC should skip normal attacks during ability execution.
*/
public boolean shouldBlockAttack() {
return npc.abilities.shouldBlockAttack();
}
}
@@ -50,6 +50,11 @@ public class EntityAIAttackTarget extends EntityAIBase {
return false;
}
// Don't start pathfinding if ability is controlling movement
if (this.npc.abilities.isAbilityControllingMovement()) {
return false;
}
double var2 = this.npc.getDistanceSq(entitylivingbase.posX, entitylivingbase.boundingBox.minY, entitylivingbase.posZ);
double var3 = this.npc.ais.distanceToMelee * this.npc.ais.distanceToMelee;
@@ -115,13 +120,18 @@ public class EntityAIAttackTarget extends EntityAIBase {
public void updateTask() {
this.npc.getLookHelper().setLookPositionWithEntity(this.entityTarget, 30.0F, 30.0F);
if (!navOverride && --this.field_75445_i <= 0) {
// Skip pathfinding updates if ability is controlling movement
if (!navOverride && !this.npc.abilities.isAbilityControllingMovement() && --this.field_75445_i <= 0) {
this.field_75445_i = 4 + this.npc.getRNG().nextInt(7);
this.npc.getNavigator().tryMoveToEntityLiving(this.entityTarget, 1.3f);
}
this.attackTick = Math.max(this.attackTick - 1, 0);
// Skip attacks if ability is blocking them
if (this.npc.abilities.shouldBlockAttack()) {
return;
}
double distance = this.npc.getDistanceSq(this.entityTarget.posX, this.entityTarget.boundingBox.minY, this.entityTarget.posZ);
double range = npc.stats.attackRange * npc.stats.attackRange + entityTarget.width;
double minRange = this.npc.width * 2.0F * this.npc.width * 2.0F + entityTarget.width;
@@ -18,6 +18,11 @@ public class EntityAILeapAtTargetNpc extends EntityAIBase {
@Override
public boolean shouldExecute() {
// Don't leap if ability is controlling movement
if (this.npc.abilities.isAbilityControllingMovement()) {
return false;
}
this.leapTarget = this.npc.getAttackTarget();
if (this.leapTarget == null) {
return false;
@@ -17,6 +17,11 @@ public class EntityAIPounceTarget extends EntityAIBase {
@Override
public boolean shouldExecute() {
// Don't pounce if ability is controlling movement
if (this.npc.abilities.isAbilityControllingMovement()) {
return false;
}
if (!this.npc.onGround)
return false;
@@ -15,6 +15,11 @@ public class EntityAISprintToTarget extends EntityAIBase {
@Override
public boolean shouldExecute() {
// Don't sprint if ability is controlling movement
if (this.npc.abilities.isAbilityControllingMovement()) {
return false;
}
EntityLivingBase runTarget = this.npc.getAttackTarget();
if (runTarget == null || this.npc.getNavigator().noPath()) {
@@ -138,6 +138,8 @@ import noppes.npcs.client.renderer.RenderNpcCrystal;
import noppes.npcs.client.renderer.RenderNpcDragon;
import noppes.npcs.client.renderer.RenderNpcSlime;
import noppes.npcs.client.renderer.RenderProjectile;
import kamkeel.npcs.client.renderer.TelegraphRenderer;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager;
import noppes.npcs.client.renderer.blocks.BlockBannerRenderer;
import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer;
import noppes.npcs.client.renderer.blocks.BlockBeamRenderer;
@@ -308,6 +310,10 @@ public class ClientProxy extends CommonProxy {
MinecraftForge.EVENT_BUS.register(new ClientEventHandler());
// Telegraph rendering system
TelegraphManager.initClient();
MinecraftForge.EVENT_BUS.register(new TelegraphRenderer());
if (ConfigClient.InventoryGuiEnabled) {
MinecraftForge.EVENT_BUS.register(new TabRegistry());
if (TabRegistry.getTabList().isEmpty()) {
@@ -18,6 +18,7 @@ import net.minecraft.world.World;
import noppes.npcs.CustomNpcs;
import noppes.npcs.client.controllers.MusicController;
import noppes.npcs.client.controllers.ScriptSoundController;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager;
import noppes.npcs.client.gui.hud.ClientHudManager;
import noppes.npcs.client.gui.hud.CompassHudComponent;
import noppes.npcs.client.gui.hud.EnumHudComponent;
@@ -109,6 +110,11 @@ public class ClientTickHandler {
MusicController.Instance.onUpdate();
ScriptSoundController.Instance.onUpdate();
// Tick telegraph manager for ability warnings
if (TelegraphManager.ClientInstance != null) {
TelegraphManager.ClientInstance.tick(mc.theWorld);
}
if (Minecraft.getMinecraft().thePlayer != null && (prevWidth != mc.displayWidth || prevHeight != mc.displayHeight)) {
prevWidth = mc.displayWidth;
prevHeight = mc.displayHeight;
@@ -0,0 +1,437 @@
package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket;
import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket;
import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.resources.I18n;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import noppes.npcs.client.gui.util.*;
import noppes.npcs.constants.EnumScrollData;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
import java.util.Vector;
/**
* GUI for managing NPC abilities.
* Shows list of assigned abilities with add/remove/edit controls.
*/
public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, ICustomScrollListener, IGuiData, ITextfieldListener, ISubGuiListener, IAbilityConfigCallback {
private GuiCustomScroll availableTypesScroll;
private GuiCustomScroll npcAbilitiesScroll;
// All available ability types (typeId -> index)
private final HashMap<String, Integer> allAbilityTypes = new HashMap<>();
// Display name to typeId mapping for the available types scroll
private final HashMap<String, String> displayNameToTypeId = new HashMap<>();
// NPC's current abilities
private final List<Ability> npcAbilities = new ArrayList<>();
// Settings
private boolean abilitiesEnabled = false;
private int globalCooldown = 20;
private String search = "";
private int selectedAbilityIndex = -1;
public GuiNPCAbilities(EntityNPCInterface npc) {
super(npc);
PacketClient.sendClient(new AbilitiesGetAllPacket());
PacketClient.sendClient(new AbilitiesNpcGetPacket());
}
@Override
public void initGui() {
super.initGui();
int y = guiTop + 10;
// Enabled checkbox
addLabel(new GuiNpcLabel(100, "gui.enabled", guiLeft + 5, y + 5));
addButton(new GuiNpcButton(100, guiLeft + 55, y, 50, 20, new String[]{"gui.no", "gui.yes"}, abilitiesEnabled ? 1 : 0));
// Global cooldown (spaced out since combat only is removed)
addLabel(new GuiNpcLabel(101, "ability.globalCooldown", guiLeft + 140, y + 5));
GuiNpcTextField cooldownField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 230, y, 50, 20, String.valueOf(globalCooldown));
cooldownField.setIntegersOnly();
cooldownField.setMinMaxDefault(0, 10000, 20);
addTextField(cooldownField);
y += 28;
// Left scroll: available ability types
addLabel(new GuiNpcLabel(1, "ability.availableTypes", guiLeft + 5, y));
if (availableTypesScroll == null) {
availableTypesScroll = new GuiCustomScroll(this, 0);
availableTypesScroll.setSize(140, 130);
}
availableTypesScroll.guiLeft = guiLeft + 5;
availableTypesScroll.guiTop = y + 12;
availableTypesScroll.setList(getFilteredTypeList());
addScroll(availableTypesScroll);
// Search bar for types
addTextField(new GuiNpcTextField(4, this, fontRendererObj, guiLeft + 5, y + 145, 140, 18, search));
// Right scroll: NPC's abilities (widened to use available space)
addLabel(new GuiNpcLabel(2, "ability.npcAbilities", guiLeft + 210, y));
if (npcAbilitiesScroll == null) {
npcAbilitiesScroll = new GuiCustomScroll(this, 1);
npcAbilitiesScroll.setSize(160, 130);
} else {
npcAbilitiesScroll.setSize(160, 130);
}
npcAbilitiesScroll.guiLeft = guiLeft + 210;
npcAbilitiesScroll.guiTop = y + 12;
updateNpcAbilitiesList();
addScroll(npcAbilitiesScroll);
// Center buttons: Add/Remove
int centerX = guiLeft + 158;
addButton(new GuiNpcButton(70, centerX, y + 40, 40, 20, ">>>")); // Add
addButton(new GuiNpcButton(71, centerX, y + 62, 40, 20, "<<<")); // Remove
// Load button (under the add/remove arrows)
addButton(new GuiNpcButton(75, centerX, y + 90, 40, 20, "gui.load"));
// Right side buttons: Edit and Up/Down carrots with Save button
addButton(new GuiNpcButton(72, guiLeft + 210, y + 145, 55, 20, "gui.edit"));
getButton(72).setEnabled(selectedAbilityIndex >= 0);
// Up/Down carrot buttons
addButton(new GuiNpcButton(73, guiLeft + 270, y + 145, 20, 20, "<"));
addButton(new GuiNpcButton(74, guiLeft + 292, y + 145, 20, 20, ">"));
getButton(73).setEnabled(selectedAbilityIndex > 0);
getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size() - 1);
// Save button (to the right of carrots, only enabled when ability is selected)
addButton(new GuiNpcButton(76, guiLeft + 320, y + 145, 50, 20, "gui.save"));
getButton(76).setEnabled(selectedAbilityIndex >= 0);
}
@Override
public void buttonEvent(GuiButton guibutton) {
int id = guibutton.id;
// Enabled toggle
if (id == 100) {
abilitiesEnabled = ((GuiNpcButton) guibutton).getValue() == 1;
save();
return;
}
// Add ability
if (id == 70) {
if (availableTypesScroll.hasSelected()) {
String displayName = availableTypesScroll.getSelected();
String typeId = displayNameToTypeId.get(displayName);
if (typeId != null) {
Ability newAbility = AbilityController.Instance.create(typeId);
if (newAbility != null) {
newAbility.setId(UUID.randomUUID().toString());
// Don't set custom name - let it default to type name
npcAbilities.add(newAbility);
// Auto-select the newly added ability
selectedAbilityIndex = npcAbilities.size() - 1;
updateNpcAbilitiesList();
selectAbilityByIndex(selectedAbilityIndex);
initGui();
save();
}
}
}
return;
}
// Remove ability
if (id == 71) {
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) {
npcAbilities.remove(selectedAbilityIndex);
selectedAbilityIndex = -1;
updateNpcAbilitiesList();
initGui();
save();
}
return;
}
// Edit ability
if (id == 72) {
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) {
Ability ability = npcAbilities.get(selectedAbilityIndex);
setSubGui(ability.createConfigGui(this));
}
return;
}
// Move up (carrot left = earlier in list)
if (id == 73) {
if (selectedAbilityIndex > 0) {
Ability temp = npcAbilities.get(selectedAbilityIndex);
npcAbilities.set(selectedAbilityIndex, npcAbilities.get(selectedAbilityIndex - 1));
npcAbilities.set(selectedAbilityIndex - 1, temp);
selectedAbilityIndex--;
updateNpcAbilitiesList();
selectAbilityByIndex(selectedAbilityIndex);
initGui();
save();
}
return;
}
// Move down (carrot right = later in list)
if (id == 74) {
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size() - 1) {
Ability temp = npcAbilities.get(selectedAbilityIndex);
npcAbilities.set(selectedAbilityIndex, npcAbilities.get(selectedAbilityIndex + 1));
npcAbilities.set(selectedAbilityIndex + 1, temp);
selectedAbilityIndex++;
updateNpcAbilitiesList();
selectAbilityByIndex(selectedAbilityIndex);
initGui();
save();
}
return;
}
// Load ability (opens load dialog to add ability directly to NPC)
if (id == 75) {
setSubGui(new SubGuiAbilityLoad(this));
return;
}
// Save selected ability
if (id == 76) {
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) {
Ability abilityToSave = npcAbilities.get(selectedAbilityIndex);
if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) {
// Need a name to save - open edit dialog first
setSubGui(abilityToSave.createConfigGui(this));
} else {
setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave));
}
}
return;
}
initGui();
}
@Override
public void setData(Vector<String> list, HashMap<String, Integer> data, EnumScrollData type) {
if (type == EnumScrollData.ABILITY_TYPES) {
allAbilityTypes.clear();
allAbilityTypes.putAll(data);
if (availableTypesScroll != null) {
availableTypesScroll.setList(getFilteredTypeList());
}
}
initGui();
}
@Override
public void setGuiData(NBTTagCompound compound) {
abilitiesEnabled = compound.getBoolean("AbilitiesEnabled");
globalCooldown = compound.hasKey("AbilityGlobalCooldown") ? compound.getInteger("AbilityGlobalCooldown") : 20;
npcAbilities.clear();
NBTTagList abilityList = compound.getTagList("Abilities", 10);
for (int i = 0; i < abilityList.tagCount(); i++) {
NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i);
Ability ability = AbilityController.Instance.fromNBT(abilityNBT);
if (ability != null) {
npcAbilities.add(ability);
}
}
initGui();
}
@Override
public void mouseClicked(int i, int j, int k) {
super.mouseClicked(i, j, k);
if (k == 0 && availableTypesScroll != null) {
availableTypesScroll.mouseClicked(i, j, k);
}
}
@Override
public void keyTyped(char c, int i) {
super.keyTyped(c, i);
if (getTextField(4) != null && getTextField(4).isFocused()) {
if (search.equals(getTextField(4).getText()))
return;
search = getTextField(4).getText().toLowerCase();
availableTypesScroll.setList(getFilteredTypeList());
availableTypesScroll.resetScroll();
}
}
private List<String> getFilteredTypeList() {
List<String> list = new ArrayList<>();
displayNameToTypeId.clear();
for (String typeId : allAbilityTypes.keySet()) {
String displayName = I18n.format(typeId);
// Search matches either the display name or the typeId
if (search.isEmpty() || displayName.toLowerCase().contains(search) || typeId.toLowerCase().contains(search)) {
list.add(displayName);
displayNameToTypeId.put(displayName, typeId);
}
}
return list;
}
private void updateNpcAbilitiesList() {
List<String> list = new ArrayList<>();
for (int i = 0; i < npcAbilities.size(); i++) {
list.add(getAbilityListEntry(i));
}
if (npcAbilitiesScroll != null) {
npcAbilitiesScroll.setList(list);
}
}
/**
* Creates the display entry for an ability in the list.
* Format: "[●] 1. Custom Name (Type)" or "[●] 1. Type" if no custom name
* Circle is green (§a) for enabled, red (§c) for disabled
*/
private String getAbilityListEntry(int index) {
Ability ability = npcAbilities.get(index);
String typeName = getDisplayName(ability.getTypeId());
String customName = ability.getName();
// Color prefix: green circle for enabled, red for disabled
String colorPrefix = ability.isEnabled() ? "\u00A7a\u25CF\u00A7r " : "\u00A7c\u25CF\u00A7r ";
if (customName != null && !customName.isEmpty() && !customName.equals(typeName)) {
return colorPrefix + (index + 1) + ". " + customName + " (" + typeName + ")";
}
return colorPrefix + (index + 1) + ". " + typeName;
}
private void selectAbilityByIndex(int index) {
if (index >= 0 && index < npcAbilities.size()) {
String entry = getAbilityListEntry(index);
if (npcAbilitiesScroll != null) {
npcAbilitiesScroll.setSelected(entry);
}
}
}
/**
* Gets the localized display name for an ability type.
* The typeId IS the lang key (e.g., "ability.cnpc.slam").
*/
private String getDisplayName(String typeId) {
return I18n.format(typeId);
}
public void save() {
NBTTagCompound compound = new NBTTagCompound();
compound.setBoolean("AbilitiesEnabled", abilitiesEnabled);
compound.setInteger("AbilityGlobalCooldown", globalCooldown);
NBTTagList abilityList = new NBTTagList();
for (Ability ability : npcAbilities) {
abilityList.appendTag(ability.writeNBT());
}
compound.setTag("Abilities", abilityList);
PacketClient.sendClient(new AbilitiesNpcSavePacket(compound));
}
/**
* Called from SubGuiAbilityLoad when a saved ability is loaded.
* Adds the ability to the NPC's ability list.
*/
public void loadAbility(Ability loadedAbility) {
if (loadedAbility != null) {
loadedAbility.setId(UUID.randomUUID().toString());
npcAbilities.add(loadedAbility);
// Auto-select the newly added ability
selectedAbilityIndex = npcAbilities.size() - 1;
updateNpcAbilitiesList();
selectAbilityByIndex(selectedAbilityIndex);
initGui();
save();
}
}
@Override
public void setSelected(String selected) {
if (availableTypesScroll != null) {
availableTypesScroll.setSelected(selected);
}
}
@Override
public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) {
if (guiCustomScroll == npcAbilitiesScroll) {
String selected = npcAbilitiesScroll.getSelected();
if (selected != null) {
// Parse index from "[●] 1. Name" format - strip color codes first
String stripped = selected.replaceAll("\u00A7.", "").trim();
// Now format is "● 1. Name" - find the number after the circle
int dotIndex = stripped.indexOf(".");
if (dotIndex > 0) {
try {
// Extract just the number portion (skip "● " prefix)
String numPart = stripped.substring(0, dotIndex).replaceAll("[^0-9]", "");
selectedAbilityIndex = Integer.parseInt(numPart) - 1;
} catch (NumberFormatException e) {
selectedAbilityIndex = -1;
}
}
}
initGui();
}
}
@Override
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) {
Ability ability = npcAbilities.get(selectedAbilityIndex);
setSubGui(ability.createConfigGui(this));
}
}
@Override
public void unFocused(GuiNpcTextField textField) {
if (textField.id == 101) {
globalCooldown = textField.getInteger();
save();
}
}
@Override
public void subGuiClosed(SubGuiInterface subgui) {
if (subgui instanceof SubGuiAbilityConfig) {
// Ability was saved in the SubGui, refresh list
updateNpcAbilitiesList();
save();
}
}
/**
* Called from SubGuiAbilityConfig when ability is saved.
*/
public void onAbilitySaved(Ability ability) {
if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) {
npcAbilities.set(selectedAbilityIndex, ability);
updateNpcAbilitiesList();
save();
}
}
}
@@ -0,0 +1,902 @@
package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.Condition;
import kamkeel.npcs.controllers.data.ability.TargetingMode;
import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.StatCollector;
import noppes.npcs.client.gui.SubGuiColorSelector;
import noppes.npcs.client.gui.select.GuiAnimationSelection;
import noppes.npcs.client.gui.select.GuiSoundSelection;
import noppes.npcs.client.gui.util.*;
import java.util.ArrayList;
import java.util.List;
/**
* Base SubGui for editing ability configuration with tabbed interface.
* Uses GuiMenuTopButton for navigation tabs.
*
* Tabs:
* - General: Name, Weight, Lock Movement, Interruptible, Timing
* - Type: Type-specific settings per Ability (override in subclasses)
* - Target: Min/Max Range, Targeting Mode, Conditions (up to 3)
* - Effects: Sounds, Animations, Telegraph settings, Colors
*
* Subclasses should override initTypeTab() and handleTypeButton()/handleTypeTextField()
* to provide type-specific settings UI.
*/
public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldListener, ISubGuiListener {
// Tab constants
protected static final int TAB_GENERAL = 0;
protected static final int TAB_TYPE = 1;
protected static final int TAB_TARGET = 2;
protected static final int TAB_EFFECTS = 3;
// Core references
protected final Ability ability;
protected final IAbilityConfigCallback callback;
protected int activeTab = TAB_GENERAL;
// ═══════════════════════════════════════════════════════════════════════════
// CACHED VALUES - General Tab
// ═══════════════════════════════════════════════════════════════════════════
protected String name;
protected boolean enabled;
protected int weight;
protected boolean lockMovement;
protected boolean interruptible;
// Timing
protected int cooldownTicks;
protected int windUpTicks;
protected int activeTicks;
protected int recoveryTicks;
// ═══════════════════════════════════════════════════════════════════════════
// CACHED VALUES - Target Tab
// ═══════════════════════════════════════════════════════════════════════════
protected float minRange;
protected float maxRange;
protected TargetingMode targetingMode;
protected List<Condition> conditions;
// ═══════════════════════════════════════════════════════════════════════════
// CACHED VALUES - Effects Tab
// ═══════════════════════════════════════════════════════════════════════════
protected String windUpSound;
protected String activeSound;
protected int windUpAnimationId;
protected int activeAnimationId;
protected String windUpAnimationName = null;
protected String activeAnimationName = null;
// Telegraph
protected boolean showTelegraph;
protected int windUpColor;
protected int activeColor;
// Editing state
protected int editingColorId = 0;
protected int editingSoundId = 0;
protected int editingAnimationId = 0;
protected int editingConditionIndex = -1;
// ═══════════════════════════════════════════════════════════════════════════
// ABILITY TYPE FEATURES
// ═══════════════════════════════════════════════════════════════════════════
protected final boolean supportsTelegraph;
protected final boolean targetingModeLocked;
protected final boolean hasTypeSettings;
public SubGuiAbilityConfig(Ability ability, IAbilityConfigCallback callback) {
this.ability = ability;
this.callback = callback;
// Cache values from ability - General
this.name = ability.getName() != null ? ability.getName() : "";
this.enabled = ability.isEnabled();
this.weight = ability.getWeight();
this.lockMovement = ability.isLockMovement();
this.interruptible = ability.isInterruptible();
// Timing
this.cooldownTicks = ability.getCooldownTicks();
this.windUpTicks = ability.getWindUpTicks();
this.activeTicks = ability.getActiveTicks();
this.recoveryTicks = ability.getRecoveryTicks();
// Target
this.minRange = ability.getMinRange();
this.maxRange = ability.getMaxRange();
this.targetingMode = ability.getTargetingMode();
this.conditions = new ArrayList<>(ability.getConditions());
// Effects
this.windUpSound = ability.getWindUpSound();
this.activeSound = ability.getActiveSound();
this.windUpAnimationId = ability.getWindUpAnimationId();
this.activeAnimationId = ability.getActiveAnimationId();
// Telegraph
this.showTelegraph = ability.isShowTelegraph();
this.windUpColor = ability.getWindUpColor();
this.activeColor = ability.getActiveColor();
// Determine ability type features
TelegraphType defaultType = ability.getTelegraphType();
this.supportsTelegraph = defaultType != null && defaultType != TelegraphType.NONE;
this.targetingModeLocked = ability.isTargetingModeLocked();
this.hasTypeSettings = ability.hasTypeSettings();
setBackground("menubg.png");
xSize = 356;
ySize = 200;
}
@Override
public void initGui() {
super.initGui();
// ═══════════════════════════════════════════════════════════════════════
// TOP TABS using GuiMenuTopButton
// ═══════════════════════════════════════════════════════════════════════
GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "ability.tab.general");
generalTab.active = (activeTab == TAB_GENERAL);
addTopButton(generalTab);
GuiMenuTopButton lastTab = generalTab;
if (hasTypeSettings) {
GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "ability.tab.type");
typeTab.active = (activeTab == TAB_TYPE);
addTopButton(typeTab);
lastTab = typeTab;
}
GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "ability.tab.target");
targetTab.active = (activeTab == TAB_TARGET);
addTopButton(targetTab);
lastTab = targetTab;
GuiMenuTopButton effectsTab = new GuiMenuTopButton(93, lastTab, "ability.tab.effects");
effectsTab.active = (activeTab == TAB_EFFECTS);
addTopButton(effectsTab);
// Close button (X) in top menu bar - use -1000 to avoid any ID conflicts
GuiMenuTopButton closeBtn = new GuiMenuTopButton(-1000, guiLeft + xSize - 22, guiTop - 17, "X");
addTopButton(closeBtn);
int contentY = guiTop + 5;
// ═══════════════════════════════════════════════════════════════════════
// TAB CONTENT
// ═══════════════════════════════════════════════════════════════════════
switch (activeTab) {
case TAB_GENERAL:
initGeneralTab(contentY);
break;
case TAB_TYPE:
initTypeTab(contentY);
break;
case TAB_TARGET:
initTargetTab(contentY);
break;
case TAB_EFFECTS:
initEffectsTab(contentY);
break;
}
// ═══════════════════════════════════════════════════════════════════════
// BOTTOM ROW - Enabled toggle
// ═══════════════════════════════════════════════════════════════════════
addLabel(new GuiNpcLabel(99, "gui.enabled", guiLeft + 8, guiTop + ySize - 18));
GuiNpcButton enabledBtn = new GuiNpcButton(2, guiLeft + 55, guiTop + ySize - 24, 40, 20, new String[]{"gui.no", "gui.yes"}, enabled ? 1 : 0);
enabledBtn.setTextColor(enabled ? 0x00FF00 : 0xFF0000);
addButton(enabledBtn);
}
// ═══════════════════════════════════════════════════════════════════════════
// GENERAL TAB
// ═══════════════════════════════════════════════════════════════════════════
private void initGeneralTab(int startY) {
int y = startY;
int col1LabelX = guiLeft + 8;
int col1FieldX = guiLeft + 70;
int col2LabelX = guiLeft + 180;
int col2FieldX = guiLeft + 248;
// Row 1: Type (read-only) + Name
addLabel(new GuiNpcLabel(1, "gui.type", col1LabelX, y + 5));
addLabel(new GuiNpcLabel(2, ability.getTypeId(), col1FieldX, y + 5));
addLabel(new GuiNpcLabel(3, "gui.name", col2LabelX, y + 5));
GuiNpcTextField nameField = new GuiNpcTextField(1, this, fontRendererObj, col2FieldX, y, 100, 20, name);
addTextField(nameField);
y += 24;
// Row 2: Weight + Lock Movement
addLabel(new GuiNpcLabel(4, "ability.weight", col1LabelX, y + 5));
GuiNpcTextField weightField = new GuiNpcTextField(3, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(weight));
weightField.setIntegersOnly();
weightField.setMinMaxDefault(1, 1000, 10);
addTextField(weightField);
addLabel(new GuiNpcLabel(5, "ability.lockMove", col2LabelX, y + 5));
addButton(new GuiNpcButton(16, col2FieldX, y, 40, 20, new String[]{"gui.no", "gui.yes"}, lockMovement ? 1 : 0));
y += 24;
// Row 3: Interruptible
addLabel(new GuiNpcLabel(6, "ability.interruptible", col1LabelX, y + 5));
addButton(new GuiNpcButton(14, guiLeft + 85, y, 40, 20, new String[]{"gui.no", "gui.yes"}, interruptible ? 1 : 0));
y += 28;
// Separator - Timing section header
addLabel(new GuiNpcLabel(7, "ability.timing", col1LabelX, y));
y += 14;
// Row 4: Windup Ticks + Active Ticks
addLabel(new GuiNpcLabel(10, "ability.windup", col1LabelX, y + 5));
GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks));
windupField.setIntegersOnly();
windupField.setMinMaxDefault(0, 1000, 20);
addTextField(windupField);
addLabel(new GuiNpcLabel(11, "ability.active", col2LabelX, y + 5));
GuiNpcTextField activeField = new GuiNpcTextField(12, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(activeTicks));
activeField.setIntegersOnly();
activeField.setMinMaxDefault(1, 1000, 10);
addTextField(activeField);
y += 24;
// Row 5: Recovery Ticks + Cooldown Ticks
addLabel(new GuiNpcLabel(12, "ability.recovery", col1LabelX, y + 5));
GuiNpcTextField recoveryField = new GuiNpcTextField(13, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(recoveryTicks));
recoveryField.setIntegersOnly();
recoveryField.setMinMaxDefault(0, 1000, 20);
addTextField(recoveryField);
addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5));
GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks));
cooldownField.setIntegersOnly();
cooldownField.setMinMaxDefault(0, 10000, 100);
addTextField(cooldownField);
}
// ═══════════════════════════════════════════════════════════════════════════
// TYPE TAB - Override in subclasses for type-specific settings
// ═══════════════════════════════════════════════════════════════════════════
/**
* Override this method to render type-specific settings in the Type tab.
* Use the helper methods createFloatField(), createIntField(), etc.
* Button/TextField IDs should start at 100.
*
* @param startY The Y position to start rendering from
*/
protected void initTypeTab(int startY) {
// Default: show message that no type settings are available
addLabel(new GuiNpcLabel(100, "ability.noTypeSettings", guiLeft + 8, startY + 5));
}
/**
* Override this method to handle button clicks for type-specific buttons.
* Button IDs 100+ are reserved for type-specific use.
*/
protected void handleTypeButton(int id, GuiNpcButton button) {
// Default: no-op
}
/**
* Override this method to handle text field changes for type-specific fields.
* Field IDs 100+ are reserved for type-specific use.
*/
protected void handleTypeTextField(int id, GuiNpcTextField field) {
// Default: no-op
}
// ═══════════════════════════════════════════════════════════════════════════
// TARGET TAB
// ═══════════════════════════════════════════════════════════════════════════
private void initTargetTab(int startY) {
int y = startY;
int col1LabelX = guiLeft + 8;
int col1FieldX = guiLeft + 70;
int col2LabelX = guiLeft + 180;
int col2FieldX = guiLeft + 248;
// Row 1: Min Range + Max Range
addLabel(new GuiNpcLabel(20, "ability.minRange", col1LabelX, y + 5));
GuiNpcTextField minRangeField = new GuiNpcTextField(5, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf((int) minRange));
minRangeField.setIntegersOnly();
minRangeField.setMinMaxDefault(0, 100, 0);
addTextField(minRangeField);
addLabel(new GuiNpcLabel(21, "ability.maxRange", col2LabelX, y + 5));
GuiNpcTextField maxRangeField = new GuiNpcTextField(6, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf((int) maxRange));
maxRangeField.setIntegersOnly();
maxRangeField.setMinMaxDefault(1, 100, 20);
addTextField(maxRangeField);
y += 24;
// Row 2: Targeting Mode (if not locked)
addLabel(new GuiNpcLabel(22, "ability.targeting", col1LabelX, y + 5));
if (!targetingModeLocked) {
String[] targetingModes = getAvailableTargetingModes();
int selectedIndex = getTargetingModeIndex(targetingMode);
addButton(new GuiNpcButton(4, col1FieldX, y, 90, 20, targetingModes, selectedIndex));
} else {
String modeName = "ability.target." + targetingMode.name().toLowerCase();
addLabel(new GuiNpcLabel(23, modeName, col1FieldX, y + 5));
}
y += 28;
// ─────────────────────────────────────────────────────────────────────
// CONDITIONS SECTION (up to 3)
// ─────────────────────────────────────────────────────────────────────
addLabel(new GuiNpcLabel(24, "ability.conditions", col1LabelX, y));
y += 14;
// Display existing conditions (similar to Marks pattern)
for (int i = 0; i < conditions.size() && i < 3; i++) {
Condition cond = conditions.get(i);
String condName = getConditionDisplayName(cond);
// Condition type button
addButton(new GuiNpcButton(50 + i * 10, col1LabelX, y, 140, 20, condName));
// Edit button
addButton(new GuiNpcButton(51 + i * 10, col1LabelX + 145, y, 40, 20, "gui.edit"));
// Delete button
addButton(new GuiNpcButton(52 + i * 10, col1LabelX + 190, y, 20, 20, "X"));
y += 22;
}
// Add button (if less than 3 conditions)
if (conditions.size() < 3) {
addButton(new GuiNpcButton(80, col1LabelX, y, 50, 20, "gui.add"));
}
}
private String getConditionDisplayName(Condition cond) {
if (cond == null) return "None";
String typeId = cond.getTypeId();
switch (typeId) {
case "hp_above":
return StatCollector.translateToLocal("condition.hp_above") + " " + (int)(getConditionThreshold(cond) * 100) + "%";
case "hp_below":
return StatCollector.translateToLocal("condition.hp_below") + " " + (int)(getConditionThreshold(cond) * 100) + "%";
case "target_hp_above":
return StatCollector.translateToLocal("condition.target_hp_above") + " " + (int)(getConditionThreshold(cond) * 100) + "%";
case "target_hp_below":
return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int)(getConditionThreshold(cond) * 100) + "%";
case "hit_count":
Condition.ConditionHitCount hitCount = (Condition.ConditionHitCount) cond;
return StatCollector.translateToLocal("condition.hit_count") + ": " + hitCount.getRequiredHits() + "/" + hitCount.getWithinTicks() + "t";
default:
return typeId;
}
}
private float getConditionThreshold(Condition cond) {
// Read threshold from NBT since inner classes don't expose it
NBTTagCompound nbt = cond.writeNBT();
return nbt.hasKey("threshold") ? nbt.getFloat("threshold") : 0.5f;
}
private String[] getAvailableTargetingModes() {
TargetingMode[] modes = ability.getAllowedTargetingModes();
if (modes == null) {
modes = TargetingMode.values();
}
String[] result = new String[modes.length];
for (int i = 0; i < modes.length; i++) {
result[i] = "ability.target." + modes[i].name().toLowerCase();
}
return result;
}
private int getTargetingModeIndex(TargetingMode mode) {
TargetingMode[] modes = ability.getAllowedTargetingModes();
if (modes == null) {
return mode.ordinal();
}
for (int i = 0; i < modes.length; i++) {
if (modes[i] == mode) return i;
}
return 0;
}
// ═══════════════════════════════════════════════════════════════════════════
// EFFECTS TAB
// ═══════════════════════════════════════════════════════════════════════════
private void initEffectsTab(int startY) {
int y = startY;
int col1LabelX = guiLeft + 8;
int col1FieldX = guiLeft + 85;
int col1ClearX = guiLeft + 160;
int col2LabelX = guiLeft + 185;
int col2FieldX = guiLeft + 255;
// ─────────────────────────────────────────────────────────────────────
// SOUNDS SECTION
// ─────────────────────────────────────────────────────────────────────
addLabel(new GuiNpcLabel(30, "ability.sounds", col1LabelX, y));
y += 14;
// Wind Up Sound
addLabel(new GuiNpcLabel(31, "ability.windUpSound", col1LabelX, y + 5));
String windUpSoundDisplay = windUpSound != null && !windUpSound.isEmpty() ? truncateString(windUpSound, 10) : StatCollector.translateToLocal("gui.none");
addButton(new GuiNpcButton(30, col1FieldX, y, 70, 20, windUpSoundDisplay));
addButton(new GuiNpcButton(35, col1ClearX, y, 20, 20, "X"));
// Active Sound
addLabel(new GuiNpcLabel(32, "ability.activeSound", col2LabelX, y + 5));
String activeSoundDisplay = activeSound != null && !activeSound.isEmpty() ? truncateString(activeSound, 10) : StatCollector.translateToLocal("gui.none");
addButton(new GuiNpcButton(31, col2FieldX, y, 70, 20, activeSoundDisplay));
addButton(new GuiNpcButton(36, col2FieldX + 75, y, 20, 20, "X"));
y += 26;
// ─────────────────────────────────────────────────────────────────────
// ANIMATIONS SECTION
// ─────────────────────────────────────────────────────────────────────
addLabel(new GuiNpcLabel(33, "ability.animations", col1LabelX, y));
y += 14;
// Wind Up Animation
addLabel(new GuiNpcLabel(34, "ability.windUpAnim", col1LabelX, y + 5));
String windUpAnimName = getAnimationName(windUpAnimationId, true);
addButton(new GuiNpcButton(32, col1FieldX, y, 70, 20, windUpAnimName));
addButton(new GuiNpcButton(37, col1ClearX, y, 20, 20, "X"));
// Active Animation
addLabel(new GuiNpcLabel(35, "ability.activeAnim", col2LabelX, y + 5));
String activeAnimName = getAnimationName(activeAnimationId, false);
addButton(new GuiNpcButton(33, col2FieldX, y, 70, 20, activeAnimName));
addButton(new GuiNpcButton(38, col2FieldX + 75, y, 20, 20, "X"));
y += 26;
// ─────────────────────────────────────────────────────────────────────
// TELEGRAPH SECTION (only if supported)
// ─────────────────────────────────────────────────────────────────────
if (supportsTelegraph) {
addLabel(new GuiNpcLabel(40, "ability.telegraph", col1LabelX, y));
y += 14;
// Show Telegraph
addLabel(new GuiNpcLabel(41, "ability.showTelegraph", col1LabelX, y + 5));
addButton(new GuiNpcButton(20, guiLeft + 95, y, 40, 20, new String[]{"gui.no", "gui.yes"}, showTelegraph ? 1 : 0));
// Telegraph Type (separate column)
String typeKey = ability.getTelegraphType().name().toLowerCase();
addLabel(new GuiNpcLabel(42, "ability.telegraphType", guiLeft + 200, y + 5));
addLabel(new GuiNpcLabel(43, "ability.telegraph." + typeKey, guiLeft + 280, y + 5));
y += 24;
// Colors
addLabel(new GuiNpcLabel(44, "ability.windUpColor", col1LabelX, y + 5));
String windUpHex = String.format("%06X", windUpColor & 0xFFFFFF);
GuiNpcButton windUpColorBtn = new GuiNpcButton(22, guiLeft + 95, y, 55, 20, windUpHex);
windUpColorBtn.setEnabled(showTelegraph);
windUpColorBtn.setTextColor(windUpColor & 0xFFFFFF);
addButton(windUpColorBtn);
addLabel(new GuiNpcLabel(45, "ability.activeColor", guiLeft + 200, y + 5));
String activeHex = String.format("%06X", activeColor & 0xFFFFFF);
GuiNpcButton activeColorBtn = new GuiNpcButton(24, guiLeft + 280, y, 55, 20, activeHex);
activeColorBtn.setEnabled(showTelegraph);
activeColorBtn.setTextColor(activeColor & 0xFFFFFF);
addButton(activeColorBtn);
}
}
private String truncateString(String str, int maxLen) {
if (str == null) return "";
if (str.length() <= maxLen) return str;
int lastSlash = str.lastIndexOf('/');
if (lastSlash >= 0 && str.length() - lastSlash <= maxLen) {
return "..." + str.substring(lastSlash);
}
return "..." + str.substring(str.length() - maxLen + 3);
}
private String getAnimationName(int animId, boolean isWindUp) {
if (animId < 0) return StatCollector.translateToLocal("gui.none");
String cachedName = isWindUp ? windUpAnimationName : activeAnimationName;
if (cachedName != null && !cachedName.isEmpty()) {
return truncateString(cachedName, 8);
}
return "ID: " + animId;
}
// ═══════════════════════════════════════════════════════════════════════════
// BUTTON EVENTS
// ═══════════════════════════════════════════════════════════════════════════
@Override
public void buttonEvent(GuiButton guibutton) {
int id = guibutton.id;
// Tab switching
if (id == 90) {
activeTab = TAB_GENERAL;
initGui();
return;
}
if (id == 91 && hasTypeSettings) {
activeTab = TAB_TYPE;
initGui();
return;
}
if (id == 92) {
activeTab = TAB_TARGET;
initGui();
return;
}
if (id == 93) {
activeTab = TAB_EFFECTS;
initGui();
return;
}
// Enabled button (bottom left)
if (id == 2) {
enabled = ((GuiNpcButton) guibutton).getValue() == 1;
initGui();
}
// General tab buttons
else if (id == 14) {
interruptible = ((GuiNpcButton) guibutton).getValue() == 1;
}
else if (id == 16) {
lockMovement = ((GuiNpcButton) guibutton).getValue() == 1;
}
// Target tab - Targeting mode
else if (id == 4) {
TargetingMode[] modes = ability.getAllowedTargetingModes();
int idx = ((GuiNpcButton) guibutton).getValue();
if (modes != null && idx < modes.length) {
targetingMode = modes[idx];
} else {
targetingMode = TargetingMode.values()[idx];
}
}
// Close button (X in top menu) - use -1000 to avoid any ID conflicts
else if (id == -1000) {
close();
return;
}
// Target tab - Condition buttons (50-79)
else if (id >= 50 && id < 80) {
int condIndex = (id - 50) / 10;
int action = (id - 50) % 10;
if (action == 0 || action == 1) {
// Click on condition name or Edit button - open edit subgui
if (condIndex < conditions.size()) {
editingConditionIndex = condIndex;
setSubGui(new SubGuiConditionEdit(conditions.get(condIndex)));
}
} else if (action == 2) {
// Delete button
if (condIndex < conditions.size()) {
conditions.remove(condIndex);
initGui();
}
}
}
// Add condition button
else if (id == 80) {
if (conditions.size() < 3) {
editingConditionIndex = conditions.size();
setSubGui(new SubGuiConditionEdit(null));
}
}
// Effects tab - Telegraph
else if (id == 20) {
showTelegraph = ((GuiNpcButton) guibutton).getValue() == 1;
initGui();
}
else if (id == 22) {
editingColorId = 22;
setSubGui(new SubGuiColorSelector(windUpColor));
}
else if (id == 24) {
editingColorId = 24;
setSubGui(new SubGuiColorSelector(activeColor));
}
// Effects tab - Sound buttons
else if (id == 30) {
editingSoundId = 30;
setSubGui(new GuiSoundSelection(windUpSound));
}
else if (id == 31) {
editingSoundId = 31;
setSubGui(new GuiSoundSelection(activeSound));
}
// Sound clear buttons
else if (id == 35) {
windUpSound = "";
initGui();
}
else if (id == 36) {
activeSound = "";
initGui();
}
// Animation buttons
else if (id == 32) {
editingAnimationId = 32;
setSubGui(new GuiAnimationSelection(windUpAnimationId));
}
else if (id == 33) {
editingAnimationId = 33;
setSubGui(new GuiAnimationSelection(activeAnimationId));
}
// Animation clear buttons
else if (id == 37) {
windUpAnimationId = -1;
windUpAnimationName = null;
initGui();
}
else if (id == 38) {
activeAnimationId = -1;
activeAnimationName = null;
initGui();
}
// Type-specific buttons (100+) - delegate to subclass
else if (id >= 100) {
handleTypeButton(id, (GuiNpcButton) guibutton);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// TEXT FIELD EVENTS
// ═══════════════════════════════════════════════════════════════════════════
@Override
public void unFocused(GuiNpcTextField textField) {
int id = textField.id;
// General tab
if (id == 1) {
name = textField.getText();
} else if (id == 3) {
weight = textField.getInteger();
}
// Target tab
else if (id == 5) {
minRange = textField.getInteger();
} else if (id == 6) {
maxRange = textField.getInteger();
}
// Timing (General tab)
else if (id == 10) {
cooldownTicks = textField.getInteger();
} else if (id == 11) {
windUpTicks = textField.getInteger();
} else if (id == 12) {
activeTicks = textField.getInteger();
} else if (id == 13) {
recoveryTicks = textField.getInteger();
}
// Type-specific fields (100+) - delegate to subclass
else if (id >= 100) {
handleTypeTextField(id, textField);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// SUBGUI CLOSED HANDLER
// ═══════════════════════════════════════════════════════════════════════════
@Override
public void subGuiClosed(SubGuiInterface subgui) {
if (subgui instanceof SubGuiColorSelector) {
SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui;
int rgb = colorSelector.color & 0x00FFFFFF;
if (editingColorId == 22) {
windUpColor = 0x80000000 | rgb;
} else if (editingColorId == 24) {
activeColor = 0xC0000000 | rgb;
}
editingColorId = 0;
initGui();
} else if (subgui instanceof GuiSoundSelection) {
GuiSoundSelection soundSelector = (GuiSoundSelection) subgui;
if (soundSelector.selectedResource != null) {
String sound = soundSelector.selectedResource.toString();
if (editingSoundId == 30) {
windUpSound = sound;
} else if (editingSoundId == 31) {
activeSound = sound;
}
}
editingSoundId = 0;
initGui();
} else if (subgui instanceof GuiAnimationSelection) {
GuiAnimationSelection animSelector = (GuiAnimationSelection) subgui;
if (editingAnimationId == 32) {
windUpAnimationId = animSelector.selectedAnimationId;
windUpAnimationName = animSelector.getSelectedName();
} else if (editingAnimationId == 33) {
activeAnimationId = animSelector.selectedAnimationId;
activeAnimationName = animSelector.getSelectedName();
}
editingAnimationId = 0;
initGui();
} else if (subgui instanceof SubGuiConditionEdit) {
SubGuiConditionEdit condEdit = (SubGuiConditionEdit) subgui;
Condition result = condEdit.getResult();
if (result != null && editingConditionIndex >= 0) {
if (editingConditionIndex < conditions.size()) {
conditions.set(editingConditionIndex, result);
} else {
conditions.add(result);
}
}
editingConditionIndex = -1;
initGui();
}
}
// ═══════════════════════════════════════════════════════════════════════════
// CLOSE - Save before closing
// ═══════════════════════════════════════════════════════════════════════════
@Override
public void close() {
// Apply all cached values to the ability before closing
applyToAbility();
// Notify callback that ability was saved
callback.onAbilitySaved(ability);
// Close the GUI
super.close();
}
// ═══════════════════════════════════════════════════════════════════════════
// APPLY TO ABILITY
// ═══════════════════════════════════════════════════════════════════════════
protected void applyToAbility() {
// General
ability.setName(name);
ability.setEnabled(enabled);
ability.setWeight(weight);
ability.setLockMovement(lockMovement);
ability.setInterruptible(interruptible);
// Timing
ability.setCooldownTicks(cooldownTicks);
ability.setWindUpTicks(windUpTicks);
ability.setActiveTicks(activeTicks);
ability.setRecoveryTicks(recoveryTicks);
// Target
ability.setMinRange(minRange);
ability.setMaxRange(maxRange);
ability.setTargetingMode(targetingMode);
// Conditions
ability.getConditions().clear();
for (Condition c : conditions) {
ability.addCondition(c);
}
// Effects
ability.setWindUpSound(windUpSound);
ability.setActiveSound(activeSound);
ability.setWindUpAnimationId(windUpAnimationId);
ability.setActiveAnimationId(activeAnimationId);
// Telegraph
ability.setShowTelegraph(showTelegraph);
ability.setWindUpColor(windUpColor);
ability.setActiveColor(activeColor);
// Type-specific values are applied directly in handleTypeTextField and handleTypeButton
}
/**
* Called when an ability is loaded from saved presets.
*/
public void loadAbility(Ability loadedAbility) {
if (loadedAbility == null) return;
// Copy all values from loaded ability to current ability
NBTTagCompound nbt = loadedAbility.writeNBT();
// Keep the current typeId since we can't change types mid-edit
nbt.setString("typeId", ability.getTypeId());
ability.readNBT(nbt);
// Update cached values
this.name = ability.getName() != null ? ability.getName() : "";
this.enabled = ability.isEnabled();
this.weight = ability.getWeight();
this.lockMovement = ability.isLockMovement();
this.interruptible = ability.isInterruptible();
this.cooldownTicks = ability.getCooldownTicks();
this.windUpTicks = ability.getWindUpTicks();
this.activeTicks = ability.getActiveTicks();
this.recoveryTicks = ability.getRecoveryTicks();
this.minRange = ability.getMinRange();
this.maxRange = ability.getMaxRange();
this.targetingMode = ability.getTargetingMode();
this.conditions = new ArrayList<>(ability.getConditions());
this.showTelegraph = ability.isShowTelegraph();
this.windUpColor = ability.getWindUpColor();
this.activeColor = ability.getActiveColor();
initGui();
}
// ═══════════════════════════════════════════════════════════════════════════
// HELPER METHODS FOR SUBCLASSES
// ═══════════════════════════════════════════════════════════════════════════
/**
* Creates a float text field for type-specific settings.
*/
protected GuiNpcTextField createFloatField(int id, int x, int y, int width, float value) {
GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.format("%.1f", value));
return field;
}
/**
* Creates an integer text field for type-specific settings.
*/
protected GuiNpcTextField createIntField(int id, int x, int y, int width, int value) {
GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.valueOf(value));
field.setIntegersOnly();
return field;
}
/**
* Parses a float from a text field with a fallback default value.
*/
protected float parseFloat(GuiNpcTextField field, float defaultValue) {
try {
return Float.parseFloat(field.getText());
} catch (NumberFormatException e) {
return defaultValue;
}
}
/**
* Gets the ability being edited.
*/
protected Ability getAbility() {
return ability;
}
}
@@ -0,0 +1,55 @@
package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import net.minecraft.client.gui.GuiButton;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.SubGuiInterface;
/**
* Confirmation dialog for deleting an ability preset.
*/
public class SubGuiAbilityDeleteConfirm extends SubGuiInterface {
private final String abilityName;
private final SubGuiAbilityLoad loadGui;
public SubGuiAbilityDeleteConfirm(String abilityName, SubGuiAbilityLoad loadGui) {
this.abilityName = abilityName;
this.loadGui = loadGui;
setBackground("menubg.png");
xSize = 200;
ySize = 100;
}
@Override
public void initGui() {
super.initGui();
int y = guiTop + 10;
addLabel(new GuiNpcLabel(0, "ability.delete.confirm", guiLeft + 10, y));
y += 12;
addLabel(new GuiNpcLabel(1, "'" + abilityName + "'?", guiLeft + 10, y));
y += 30;
addButton(new GuiNpcButton(0, guiLeft + 30, y, 60, 20, "gui.yes"));
addButton(new GuiNpcButton(1, guiLeft + 110, y, 60, 20, "gui.no"));
}
@Override
public void buttonEvent(GuiButton guibutton) {
int id = guibutton.id;
if (id == 0) {
// Delete the ability
AbilityController.Instance.deleteAbility(abilityName);
loadGui.onAbilityDeleted(abilityName);
close();
} else if (id == 1) {
close();
}
}
}
@@ -0,0 +1,134 @@
package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import net.minecraft.client.gui.GuiButton;
import noppes.npcs.client.gui.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Dialog for loading a saved ability preset.
* Can be opened from SubGuiAbilityConfig (to load into editing ability)
* or from GuiNPCAbilities (to add ability directly to NPC).
*/
public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener {
private final SubGuiAbilityConfig parentConfig;
private final GuiNPCAbilities parentAbilities;
private String selectedName = null;
private List<String> abilityNames = new ArrayList<>();
public SubGuiAbilityLoad(SubGuiAbilityConfig parentConfig) {
this.parentConfig = parentConfig;
this.parentAbilities = null;
setBackground("menubg.png");
xSize = 220;
ySize = 180;
}
public SubGuiAbilityLoad(GuiNPCAbilities parentAbilities) {
this.parentConfig = null;
this.parentAbilities = parentAbilities;
setBackground("menubg.png");
xSize = 220;
ySize = 180;
}
@Override
public void initGui() {
super.initGui();
// Load ability names
Set<String> names = AbilityController.Instance.getSavedAbilityNames();
abilityNames = new ArrayList<>(names);
int y = guiTop + 5;
addLabel(new GuiNpcLabel(0, "ability.load.select", guiLeft + 10, y));
y += 14;
// Scroll list of saved abilities
GuiCustomScroll scroll = new GuiCustomScroll(this, 0);
scroll.setSize(200, 110);
scroll.guiLeft = guiLeft + 10;
scroll.guiTop = y;
scroll.setUnsortedList(abilityNames);
if (selectedName != null && abilityNames.contains(selectedName)) {
scroll.setSelected(selectedName);
}
addScroll(scroll);
y += 115;
// Buttons
addButton(new GuiNpcButton(0, guiLeft + 10, y, 60, 20, "gui.load"));
addButton(new GuiNpcButton(1, guiLeft + 80, y, 60, 20, "gui.delete"));
addButton(new GuiNpcButton(2, guiLeft + 150, y, 60, 20, "gui.cancel"));
// Disable load/delete if nothing selected
getButton(0).setEnabled(selectedName != null && !selectedName.isEmpty());
getButton(1).setEnabled(selectedName != null && !selectedName.isEmpty());
}
@Override
public void buttonEvent(GuiButton guibutton) {
int id = guibutton.id;
if (id == 0 && selectedName != null) {
// Load the selected ability
Ability loaded = AbilityController.Instance.getSavedAbility(selectedName);
if (loaded != null) {
if (parentConfig != null) {
parentConfig.loadAbility(loaded);
} else if (parentAbilities != null) {
parentAbilities.loadAbility(loaded);
}
}
close();
} else if (id == 1 && selectedName != null) {
// Delete confirmation
setSubGui(new SubGuiAbilityDeleteConfirm(selectedName, this));
} else if (id == 2) {
close();
}
}
@Override
public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) {
if (scroll.id == 0) {
selectedName = scroll.getSelected();
initGui();
}
}
@Override
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
if (scroll.id == 0 && selection != null && !selection.isEmpty()) {
// Double-click to load immediately
Ability loaded = AbilityController.Instance.getSavedAbility(selection);
if (loaded != null) {
if (parentConfig != null) {
parentConfig.loadAbility(loaded);
} else if (parentAbilities != null) {
parentAbilities.loadAbility(loaded);
}
}
close();
}
}
/**
* Called after an ability is deleted to refresh the list.
*/
public void onAbilityDeleted(String name) {
if (name.equals(selectedName)) {
selectedName = null;
}
initGui();
}
}
@@ -0,0 +1,61 @@
package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import net.minecraft.client.gui.GuiButton;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.SubGuiInterface;
/**
* Confirmation dialog for saving an ability preset.
*/
public class SubGuiAbilitySaveConfirm extends SubGuiInterface {
private final Ability ability;
private final boolean exists;
public SubGuiAbilitySaveConfirm(Ability ability) {
this.ability = ability;
this.exists = AbilityController.Instance.hasSavedAbility(ability.getName());
setBackground("menubg.png");
xSize = 200;
ySize = 100;
}
@Override
public void initGui() {
super.initGui();
int y = guiTop + 10;
if (exists) {
addLabel(new GuiNpcLabel(0, "ability.save.overwrite", guiLeft + 10, y));
y += 12;
addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y));
} else {
addLabel(new GuiNpcLabel(0, "ability.save.confirm", guiLeft + 10, y));
y += 12;
addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y));
}
y += 30;
addButton(new GuiNpcButton(0, guiLeft + 30, y, 60, 20, "gui.yes"));
addButton(new GuiNpcButton(1, guiLeft + 110, y, 60, 20, "gui.no"));
}
@Override
public void buttonEvent(GuiButton guibutton) {
int id = guibutton.id;
if (id == 0) {
// Save the ability
AbilityController.Instance.saveAbility(ability);
close();
} else if (id == 1) {
close();
}
}
}
@@ -0,0 +1,181 @@
package noppes.npcs.client.gui.advanced;
import kamkeel.npcs.controllers.data.ability.Condition;
import net.minecraft.client.gui.GuiButton;
import noppes.npcs.client.gui.util.*;
/**
* SubGui for editing a single ability condition.
* Allows selecting condition type and configuring its parameters.
*/
public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldListener {
// Condition types
private static final String[] CONDITION_TYPES = {
"condition.hp_above",
"condition.hp_below",
"condition.target_hp_above",
"condition.target_hp_below",
"condition.hit_count"
};
private static final String[] CONDITION_TYPE_IDS = {
"hp_above",
"hp_below",
"target_hp_above",
"target_hp_below",
"hit_count"
};
// Current condition being edited
private Condition condition;
private Condition result;
// State
private int selectedTypeIndex = 0;
private float threshold = 0.5f; // For HP conditions (0.0-1.0)
private int requiredHits = 3; // For hit_count
private int withinTicks = 60; // For hit_count
public SubGuiConditionEdit(Condition existing) {
this.condition = existing;
this.result = null;
if (existing != null) {
String typeId = existing.getTypeId();
for (int i = 0; i < CONDITION_TYPE_IDS.length; i++) {
if (CONDITION_TYPE_IDS[i].equals(typeId)) {
selectedTypeIndex = i;
break;
}
}
loadFromCondition(existing);
}
setBackground("menubg.png");
xSize = 220;
ySize = 140;
}
private void loadFromCondition(Condition cond) {
String typeId = cond.getTypeId();
if (typeId.equals("hit_count")) {
Condition.ConditionHitCount hitCount = (Condition.ConditionHitCount) cond;
requiredHits = hitCount.getRequiredHits();
withinTicks = hitCount.getWithinTicks();
} else {
// HP-based conditions
net.minecraft.nbt.NBTTagCompound nbt = cond.writeNBT();
threshold = nbt.hasKey("threshold") ? nbt.getFloat("threshold") : 0.5f;
}
}
@Override
public void initGui() {
super.initGui();
int y = guiTop + 8;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 90;
// Title
addLabel(new GuiNpcLabel(0, "condition.edit", labelX, y));
y += 18;
// Condition Type selector
addLabel(new GuiNpcLabel(1, "gui.type", labelX, y + 5));
addButton(new GuiNpcButton(1, fieldX, y, 115, 20, CONDITION_TYPES, selectedTypeIndex));
y += 26;
// Type-specific fields
String typeId = CONDITION_TYPE_IDS[selectedTypeIndex];
if (typeId.equals("hit_count")) {
// Hit count fields
addLabel(new GuiNpcLabel(2, "condition.hits", labelX, y + 5));
GuiNpcTextField hitsField = new GuiNpcTextField(2, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(requiredHits));
hitsField.setIntegersOnly();
hitsField.setMinMaxDefault(1, 100, 3);
addTextField(hitsField);
y += 24;
addLabel(new GuiNpcLabel(3, "condition.within", labelX, y + 5));
GuiNpcTextField ticksField = new GuiNpcTextField(3, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(withinTicks));
ticksField.setIntegersOnly();
ticksField.setMinMaxDefault(1, 1200, 60);
addTextField(ticksField);
} else {
// HP threshold field (as percentage)
addLabel(new GuiNpcLabel(2, "condition.percent", labelX, y + 5));
int percent = (int)(threshold * 100);
GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(percent));
thresholdField.setIntegersOnly();
thresholdField.setMinMaxDefault(1, 99, 50);
addTextField(thresholdField);
addLabel(new GuiNpcLabel(4, "%", fieldX + 45, y + 5));
}
// Bottom buttons
addButton(new GuiNpcButton(10, guiLeft + 8, guiTop + ySize - 28, 60, 20, "gui.cancel"));
addButton(new GuiNpcButton(11, guiLeft + xSize - 68, guiTop + ySize - 28, 60, 20, "gui.done"));
}
@Override
public void buttonEvent(GuiButton guibutton) {
int id = guibutton.id;
if (id == 1) {
// Type changed
selectedTypeIndex = ((GuiNpcButton) guibutton).getValue();
initGui();
} else if (id == 10) {
// Cancel - close without saving
result = null;
close();
} else if (id == 11) {
// Done - create condition and close
result = createCondition();
close();
}
}
@Override
public void unFocused(GuiNpcTextField textField) {
int id = textField.id;
if (id == 2) {
requiredHits = textField.getInteger();
} else if (id == 3) {
withinTicks = textField.getInteger();
} else if (id == 4) {
int percent = textField.getInteger();
threshold = percent / 100.0f;
}
}
private Condition createCondition() {
String typeId = CONDITION_TYPE_IDS[selectedTypeIndex];
switch (typeId) {
case "hp_above":
return new Condition.ConditionHPAbove(threshold);
case "hp_below":
return new Condition.ConditionHPBelow(threshold);
case "target_hp_above":
return new Condition.ConditionTargetHPAbove(threshold);
case "target_hp_below":
return new Condition.ConditionTargetHPBelow(threshold);
case "hit_count":
return new Condition.ConditionHitCount(requiredHits, withinTicks);
default:
return null;
}
}
/**
* Get the resulting condition after dialog closes.
* Returns null if cancelled or no valid condition.
*/
public Condition getResult() {
return result;
}
}
@@ -0,0 +1,92 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityBeam;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Beam ability type-specific settings.
*/
public class SubGuiAbilityBeam extends SubGuiAbilityConfig {
private final AbilityBeam beam;
public SubGuiAbilityBeam(AbilityBeam ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.beam = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Damage + Damage Interval
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, beam.getDamage()));
addLabel(new GuiNpcLabel(101, "ability.dmgInterval", col2LabelX, y + 5));
addTextField(createIntField(101, col2FieldX, y, 50, beam.getDamageInterval()));
y += 24;
// Row 2: Beam Length + Beam Width
addLabel(new GuiNpcLabel(102, "ability.beamLength", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, beam.getBeamLength()));
addLabel(new GuiNpcLabel(103, "ability.beamWidth", col2LabelX, y + 5));
addTextField(createFloatField(103, col2FieldX, y, 50, beam.getBeamWidth()));
y += 24;
// Row 3: Sweep Angle + Sweep Speed
addLabel(new GuiNpcLabel(104, "ability.sweepAngle", labelX, y + 5));
addTextField(createFloatField(104, fieldX, y, 50, beam.getSweepAngle()));
addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5));
addTextField(createFloatField(105, col2FieldX, y, 50, beam.getSweepSpeed()));
y += 24;
// Row 4: Piercing + Lock On Target
addLabel(new GuiNpcLabel(106, "ability.piercing", labelX, y + 5));
addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isPiercing() ? 1 : 0));
addLabel(new GuiNpcLabel(107, "ability.lockTarget", col2LabelX, y + 5));
addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isLockOnTarget() ? 1 : 0));
y += 24;
// Row 5: Sweep Back & Forth
addLabel(new GuiNpcLabel(108, "ability.sweepBackForth", labelX, y + 5));
addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isSweepBackAndForth() ? 1 : 0));
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
int value = button.getValue();
switch (id) {
case 106: beam.setPiercing(value == 1); break;
case 107: beam.setLockOnTarget(value == 1); break;
case 108: beam.setSweepBackAndForth(value == 1); break;
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: beam.setDamage(parseFloat(field, beam.getDamage())); break;
case 101: beam.setDamageInterval(field.getInteger()); break;
case 102: beam.setBeamLength(parseFloat(field, beam.getBeamLength())); break;
case 103: beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); break;
case 104: beam.setSweepAngle(parseFloat(field, beam.getSweepAngle())); break;
case 105: beam.setSweepSpeed(parseFloat(field, beam.getSweepSpeed())); break;
}
}
}
@@ -0,0 +1,59 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityCharge;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Charge ability type-specific settings.
*/
public class SubGuiAbilityCharge extends SubGuiAbilityConfig {
private final AbilityCharge charge;
public SubGuiAbilityCharge(AbilityCharge ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.charge = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Damage + Speed
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, charge.getDamage()));
addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5));
addTextField(createFloatField(101, col2FieldX, y, 50, charge.getChargeSpeed()));
y += 24;
// Row 2: Knockback
addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, charge.getKnockback()));
y += 24;
// Row 3: Hit Width
addLabel(new GuiNpcLabel(104, "ability.hitWidth", labelX, y + 5));
addTextField(createFloatField(104, fieldX, y, 50, charge.getHitWidth()));
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: charge.setDamage(parseFloat(field, charge.getDamage())); break;
case 101: charge.setChargeSpeed(parseFloat(field, charge.getChargeSpeed())); break;
case 102: charge.setKnockback(parseFloat(field, charge.getKnockback())); break;
case 104: charge.setHitWidth(parseFloat(field, charge.getHitWidth())); break;
}
}
}
@@ -0,0 +1,104 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityCutter;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Cutter ability type-specific settings.
*/
public class SubGuiAbilityCutter extends SubGuiAbilityConfig {
private final AbilityCutter cutter;
public SubGuiAbilityCutter(AbilityCutter ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.cutter = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Damage + Range
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, cutter.getDamage()));
addLabel(new GuiNpcLabel(101, "ability.range", col2LabelX, y + 5));
addTextField(createFloatField(101, col2FieldX, y, 50, cutter.getRange()));
y += 24;
// Row 2: Arc Angle + Inner Radius
addLabel(new GuiNpcLabel(102, "ability.arcAngle", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, cutter.getArcAngle()));
addLabel(new GuiNpcLabel(103, "ability.innerRadius", col2LabelX, y + 5));
addTextField(createFloatField(103, col2FieldX, y, 50, cutter.getInnerRadius()));
y += 24;
// Row 3: Knockback + Sweep Speed
addLabel(new GuiNpcLabel(104, "ability.knockback", labelX, y + 5));
addTextField(createFloatField(104, fieldX, y, 50, cutter.getKnockback()));
addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5));
addTextField(createFloatField(105, col2FieldX, y, 50, cutter.getSweepSpeed()));
y += 24;
// Row 4: Sweep Mode + Piercing
addLabel(new GuiNpcLabel(106, "ability.sweepMode", labelX, y + 5));
String[] sweepModes = {"Swipe", "Spin"};
addButton(new GuiNpcButton(106, fieldX, y, 70, 20, sweepModes, cutter.getSweepMode().ordinal()));
addLabel(new GuiNpcLabel(107, "ability.piercing", col2LabelX, y + 5));
addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, cutter.isPiercing() ? 1 : 0));
y += 24;
// Row 5: Stun Duration + Poison Time
addLabel(new GuiNpcLabel(108, "ability.stunDuration", labelX, y + 5));
addTextField(createIntField(108, fieldX, y, 50, cutter.getStunDuration()));
addLabel(new GuiNpcLabel(109, "ability.poisonTime", col2LabelX, y + 5));
addTextField(createIntField(109, col2FieldX, y, 50, cutter.getPoisonDurationSeconds()));
y += 24;
// Row 6: Poison Level
addLabel(new GuiNpcLabel(110, "ability.poisonLvl", labelX, y + 5));
addTextField(createIntField(110, fieldX, y, 50, cutter.getPoisonLevel()));
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
int value = button.getValue();
switch (id) {
case 106: cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]); break;
case 107: cutter.setPiercing(value == 1); break;
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: cutter.setDamage(parseFloat(field, cutter.getDamage())); break;
case 101: cutter.setRange(parseFloat(field, cutter.getRange())); break;
case 102: cutter.setArcAngle(parseFloat(field, cutter.getArcAngle())); break;
case 103: cutter.setInnerRadius(parseFloat(field, cutter.getInnerRadius())); break;
case 104: cutter.setKnockback(parseFloat(field, cutter.getKnockback())); break;
case 105: cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); break;
case 108: cutter.setStunDuration(field.getInteger()); break;
case 109: cutter.setPoisonDurationSeconds(field.getInteger()); break;
case 110: cutter.setPoisonLevel(field.getInteger()); break;
}
}
}
@@ -0,0 +1,59 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityDash;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Dash ability type-specific settings.
*/
public class SubGuiAbilityDash extends SubGuiAbilityConfig {
private final AbilityDash dash;
public SubGuiAbilityDash(AbilityDash ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.dash = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Dash Mode
addLabel(new GuiNpcLabel(100, "ability.dashMode", labelX, y + 5));
String[] dashModes = {"Aggressive", "Defensive"};
addButton(new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashMode().ordinal()));
y += 24;
// Row 2: Dash Distance + Dash Speed
addLabel(new GuiNpcLabel(101, "ability.dashDist", labelX, y + 5));
addTextField(createFloatField(101, fieldX, y, 50, dash.getDashDistance()));
addLabel(new GuiNpcLabel(102, "ability.dashSpeed", col2LabelX, y + 5));
addTextField(createFloatField(102, col2FieldX, y, 50, dash.getDashSpeed()));
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
if (id == 100) {
dash.setDashMode(AbilityDash.DashMode.values()[button.getValue()]);
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 101: dash.setDashDistance(parseFloat(field, dash.getDashDistance())); break;
case 102: dash.setDashSpeed(parseFloat(field, dash.getDashSpeed())); break;
}
}
}
@@ -0,0 +1,96 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityGuard;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Guard ability type-specific settings.
*/
public class SubGuiAbilityGuard extends SubGuiAbilityConfig {
private final AbilityGuard guard;
public SubGuiAbilityGuard(AbilityGuard ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.guard = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 105;
int col2LabelX = guiLeft + 165;
int col2FieldX = guiLeft + 225;
// Row 1: Damage Reduction
addLabel(new GuiNpcLabel(100, "ability.dmgReduce", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, guard.getDamageReduction()));
y += 24;
// Row 2: Can Counter + Counter Type
addLabel(new GuiNpcLabel(101, "ability.canCounter", labelX, y + 5));
addButton(new GuiNpcButton(101, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, guard.isCanCounter() ? 1 : 0));
addLabel(new GuiNpcLabel(102, "ability.counterType", col2LabelX, y + 5));
String[] types = {"Flat", "Percent"};
GuiNpcButton counterTypeButton = new GuiNpcButton(102, col2FieldX, y, 60, 20, types, guard.getCounterType().ordinal());
counterTypeButton.setEnabled(guard.isCanCounter());
addButton(counterTypeButton);
y += 24;
// Row 3: Counter Value + Counter Chance
addLabel(new GuiNpcLabel(103, "ability.counterValue", labelX, y + 5));
GuiNpcTextField counterValueField = createFloatField(103, fieldX, y, 50, guard.getCounterValue());
counterValueField.setEnabled(guard.isCanCounter());
addTextField(counterValueField);
addLabel(new GuiNpcLabel(104, "ability.counterChance", col2LabelX, y + 5));
GuiNpcTextField counterChanceField = createFloatField(104, col2FieldX, y, 50, guard.getCounterChance());
counterChanceField.setEnabled(guard.isCanCounter());
addTextField(counterChanceField);
y += 24;
// Row 4: Counter Sound + Counter Animation
addLabel(new GuiNpcLabel(105, "ability.counterSound", labelX, y + 5));
GuiNpcTextField counterSoundField = new GuiNpcTextField(105, this, fontRendererObj, fieldX, y, 100, 20, guard.getCounterSound());
counterSoundField.setEnabled(guard.isCanCounter());
addTextField(counterSoundField);
addLabel(new GuiNpcLabel(106, "ability.counterAnim", col2LabelX, y + 5));
GuiNpcTextField counterAnimField = createIntField(106, col2FieldX, y, 50, guard.getCounterAnimationId());
counterAnimField.setEnabled(guard.isCanCounter());
addTextField(counterAnimField);
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
switch (id) {
case 101:
guard.setCanCounter(button.getValue() == 1);
initGui();
break;
case 102:
guard.setCounterType(AbilityGuard.CounterType.values()[button.getValue()]);
break;
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: guard.setDamageReduction(parseFloat(field, guard.getDamageReduction())); break;
case 103: guard.setCounterValue(parseFloat(field, guard.getCounterValue())); break;
case 104: guard.setCounterChance(parseFloat(field, guard.getCounterChance())); break;
case 105: guard.setCounterSound(field.getText()); break;
case 106: guard.setCounterAnimationId(field.getInteger()); break;
}
}
}
@@ -0,0 +1,98 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityHazard;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Hazard ability type-specific settings.
*/
public class SubGuiAbilityHazard extends SubGuiAbilityConfig {
private final AbilityHazard hazard;
public SubGuiAbilityHazard(AbilityHazard ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.hazard = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Shape + Placement
addLabel(new GuiNpcLabel(100, "ability.shape", labelX, y + 5));
String[] shapes = {"Circle", "Ring", "Cone"};
addButton(new GuiNpcButton(100, fieldX, y, 55, 20, shapes, hazard.getShape().ordinal()));
addLabel(new GuiNpcLabel(101, "ability.placement", col2LabelX, y + 5));
String[] placements = {"Caster", "Target", "FollowCast", "FollowTgt"};
addButton(new GuiNpcButton(101, col2FieldX, y, 60, 20, placements, hazard.getPlacement().ordinal()));
y += 24;
// Row 2: Radius + Inner Radius
addLabel(new GuiNpcLabel(102, "ability.radius", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, hazard.getRadius()));
addLabel(new GuiNpcLabel(103, "ability.innerRadius", col2LabelX, y + 5));
addTextField(createFloatField(103, col2FieldX, y, 50, hazard.getInnerRadius()));
y += 24;
// Row 3: Damage Per Tick + Damage Interval
addLabel(new GuiNpcLabel(104, "ability.dmgPerTick", labelX, y + 5));
addTextField(createFloatField(104, fieldX, y, 50, hazard.getDamagePerTick()));
addLabel(new GuiNpcLabel(105, "ability.dmgInterval", col2LabelX, y + 5));
addTextField(createIntField(105, col2FieldX, y, 50, hazard.getDamageInterval()));
y += 24;
// Row 4: Slowness Level + Debuff Duration
addLabel(new GuiNpcLabel(106, "ability.slownessLvl", labelX, y + 5));
addTextField(createIntField(106, fieldX, y, 50, hazard.getSlownessLevel()));
addLabel(new GuiNpcLabel(107, "ability.debuffDur", col2LabelX, y + 5));
addTextField(createIntField(107, col2FieldX, y, 50, hazard.getDebuffDuration()));
y += 24;
// Row 5: Poison Level + Affects Caster
addLabel(new GuiNpcLabel(108, "ability.poisonLvl", labelX, y + 5));
addTextField(createIntField(108, fieldX, y, 50, hazard.getPoisonLevel()));
addLabel(new GuiNpcLabel(109, "ability.affectsCaster", col2LabelX, y + 5));
addButton(new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, hazard.isAffectsCaster() ? 1 : 0));
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
int value = button.getValue();
switch (id) {
case 100: hazard.setShape(AbilityHazard.HazardShape.values()[value]); break;
case 101: hazard.setPlacement(AbilityHazard.PlacementMode.values()[value]); break;
case 109: hazard.setAffectsCaster(value == 1); break;
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 102: hazard.setRadius(parseFloat(field, hazard.getRadius())); break;
case 103: hazard.setInnerRadius(parseFloat(field, hazard.getInnerRadius())); break;
case 104: hazard.setDamagePerTick(parseFloat(field, hazard.getDamagePerTick())); break;
case 105: hazard.setDamageInterval(field.getInteger()); break;
case 106: hazard.setSlownessLevel(field.getInteger()); break;
case 107: hazard.setDebuffDuration(field.getInteger()); break;
case 108: hazard.setPoisonLevel(field.getInteger()); break;
}
}
}
@@ -0,0 +1,74 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityHeal;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Heal ability type-specific settings.
*/
public class SubGuiAbilityHeal extends SubGuiAbilityConfig {
private final AbilityHeal heal;
public SubGuiAbilityHeal(AbilityHeal ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.heal = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Heal Amount + Heal Percent
addLabel(new GuiNpcLabel(100, "ability.healAmount", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, heal.getHealAmount()));
addLabel(new GuiNpcLabel(101, "ability.healPercent", col2LabelX, y + 5));
addTextField(createFloatField(101, col2FieldX, y, 50, heal.getHealPercent()));
y += 24;
// Row 2: Heal Radius + Instant Heal
addLabel(new GuiNpcLabel(102, "ability.healRadius", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, heal.getHealRadius()));
addLabel(new GuiNpcLabel(103, "ability.instant", col2LabelX, y + 5));
addButton(new GuiNpcButton(103, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0));
y += 24;
// Row 3: Heal Self + Heal Allies
addLabel(new GuiNpcLabel(104, "ability.healSelf", labelX, y + 5));
addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealSelf() ? 1 : 0));
addLabel(new GuiNpcLabel(105, "ability.healAllies", col2LabelX, y + 5));
addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealAllies() ? 1 : 0));
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
int value = button.getValue();
switch (id) {
case 103: heal.setInstantHeal(value == 1); break;
case 104: heal.setHealSelf(value == 1); break;
case 105: heal.setHealAllies(value == 1); break;
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: heal.setHealAmount(parseFloat(field, heal.getHealAmount())); break;
case 101: heal.setHealPercent(parseFloat(field, heal.getHealPercent())); break;
case 102: heal.setHealRadius(parseFloat(field, heal.getHealRadius())); break;
}
}
}
@@ -0,0 +1,62 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Heavy Hit ability type-specific settings.
*/
public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig {
private final AbilityHeavyHit heavyHit;
public SubGuiAbilityHeavyHit(AbilityHeavyHit ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.heavyHit = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Damage + Potion Time
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, heavyHit.getDamage()));
addLabel(new GuiNpcLabel(101, "ability.potionTime", col2LabelX, y + 5));
addTextField(createIntField(101, col2FieldX, y, 50, heavyHit.getPotionDurationSeconds()));
y += 24;
// Row 2: Knockback + Slowness Level
addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, heavyHit.getKnockback()));
addLabel(new GuiNpcLabel(103, "ability.slownessLvl", col2LabelX, y + 5));
addTextField(createIntField(103, col2FieldX, y, 50, heavyHit.getSlownessLevel()));
y += 24;
// Row 3: Weakness Level
addLabel(new GuiNpcLabel(104, "ability.weaknessLvl", labelX, y + 5));
addTextField(createIntField(104, fieldX, y, 50, heavyHit.getWeaknessLevel()));
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: heavyHit.setDamage(parseFloat(field, heavyHit.getDamage())); break;
case 101: heavyHit.setPotionDurationSeconds(field.getInteger()); break;
case 102: heavyHit.setKnockback(parseFloat(field, heavyHit.getKnockback())); break;
case 103: heavyHit.setSlownessLevel(field.getInteger()); break;
case 104: heavyHit.setWeaknessLevel(field.getInteger()); break;
}
}
}
@@ -0,0 +1,96 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityOrb;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Orb ability type-specific settings.
*/
public class SubGuiAbilityOrb extends SubGuiAbilityConfig {
private final AbilityOrb orb;
public SubGuiAbilityOrb(AbilityOrb ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.orb = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Damage + Speed
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, orb.getDamage()));
addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5));
addTextField(createFloatField(101, col2FieldX, y, 50, orb.getOrbSpeed()));
y += 24;
// Row 2: Orb Size + Max Distance
addLabel(new GuiNpcLabel(102, "ability.orbSize", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, orb.getOrbSize()));
addLabel(new GuiNpcLabel(103, "ability.maxDist", col2LabelX, y + 5));
addTextField(createFloatField(103, col2FieldX, y, 50, orb.getMaxDistance()));
y += 24;
// Row 3: Homing + Homing Strength
addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5));
addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isHoming() ? 1 : 0));
addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5));
addTextField(createFloatField(105, col2FieldX, y, 50, orb.getHomingStrength()));
y += 24;
// Row 4: Explosive + Explosion Radius
addLabel(new GuiNpcLabel(106, "ability.explosive", labelX, y + 5));
addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isExplosive() ? 1 : 0));
addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5));
addTextField(createFloatField(107, col2FieldX, y, 50, orb.getExplosionRadius()));
y += 24;
// Row 5: Knockback + Max Lifetime
addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5));
addTextField(createFloatField(108, fieldX, y, 50, orb.getKnockback()));
addLabel(new GuiNpcLabel(109, "ability.lifetime", col2LabelX, y + 5));
addTextField(createIntField(109, col2FieldX, y, 50, orb.getMaxLifetime()));
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
int value = button.getValue();
switch (id) {
case 104: orb.setHoming(value == 1); break;
case 106: orb.setExplosive(value == 1); break;
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: orb.setDamage(parseFloat(field, orb.getDamage())); break;
case 101: orb.setOrbSpeed(parseFloat(field, orb.getOrbSpeed())); break;
case 102: orb.setOrbSize(parseFloat(field, orb.getOrbSize())); break;
case 103: orb.setMaxDistance(parseFloat(field, orb.getMaxDistance())); break;
case 105: orb.setHomingStrength(parseFloat(field, orb.getHomingStrength())); break;
case 107: orb.setExplosionRadius(parseFloat(field, orb.getExplosionRadius())); break;
case 108: orb.setKnockback(parseFloat(field, orb.getKnockback())); break;
case 109: orb.setMaxLifetime(field.getInteger()); break;
}
}
}
@@ -0,0 +1,97 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Projectile ability type-specific settings.
*/
public class SubGuiAbilityProjectile extends SubGuiAbilityConfig {
private final AbilityProjectile projectile;
public SubGuiAbilityProjectile(AbilityProjectile ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.projectile = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Damage + Speed
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, projectile.getDamage()));
addLabel(new GuiNpcLabel(101, "ability.speed", col2LabelX, y + 5));
addTextField(createFloatField(101, col2FieldX, y, 50, projectile.getSpeed()));
y += 24;
// Row 2: Knockback + Projectile Type
addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, projectile.getKnockback()));
addLabel(new GuiNpcLabel(103, "ability.projType", col2LabelX, y + 5));
String[] projTypes = {"fireball", "arrow", "magic"};
int typeIdx = 0;
String currType = projectile.getProjectileType();
for (int i = 0; i < projTypes.length; i++) {
if (projTypes[i].equals(currType)) {
typeIdx = i;
break;
}
}
addButton(new GuiNpcButton(103, col2FieldX, y, 60, 20, projTypes, typeIdx));
y += 24;
// Row 3: Explosive + Explosion Radius
addLabel(new GuiNpcLabel(104, "ability.explosive", labelX, y + 5));
addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isExplosive() ? 1 : 0));
addLabel(new GuiNpcLabel(105, "ability.explosionRad", col2LabelX, y + 5));
addTextField(createFloatField(105, col2FieldX, y, 50, projectile.getExplosionRadius()));
y += 24;
// Row 4: Homing + Homing Strength
addLabel(new GuiNpcLabel(106, "ability.homing", labelX, y + 5));
addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isHoming() ? 1 : 0));
addLabel(new GuiNpcLabel(107, "ability.homingStr", col2LabelX, y + 5));
addTextField(createFloatField(107, col2FieldX, y, 50, projectile.getHomingStrength()));
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
int value = button.getValue();
switch (id) {
case 103:
String[] projTypes = {"fireball", "arrow", "magic"};
projectile.setProjectileType(projTypes[value]);
break;
case 104: projectile.setExplosive(value == 1); break;
case 106: projectile.setHoming(value == 1); break;
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: projectile.setDamage(parseFloat(field, projectile.getDamage())); break;
case 101: projectile.setSpeed(parseFloat(field, projectile.getSpeed())); break;
case 102: projectile.setKnockback(parseFloat(field, projectile.getKnockback())); break;
case 105: projectile.setExplosionRadius(parseFloat(field, projectile.getExplosionRadius())); break;
case 107: projectile.setHomingStrength(parseFloat(field, projectile.getHomingStrength())); break;
}
}
}
@@ -0,0 +1,65 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Shockwave ability type-specific settings.
*/
public class SubGuiAbilityShockwave extends SubGuiAbilityConfig {
private final AbilityShockwave shockwave;
public SubGuiAbilityShockwave(AbilityShockwave ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.shockwave = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Damage + Push Radius
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, shockwave.getDamage()));
addLabel(new GuiNpcLabel(101, "ability.pushRadius", col2LabelX, y + 5));
addTextField(createFloatField(101, col2FieldX, y, 50, shockwave.getPushRadius()));
y += 24;
// Row 2: Push Strength
addLabel(new GuiNpcLabel(102, "ability.pushStrength", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, shockwave.getPushStrength()));
y += 24;
// Row 3: Stun Duration + Max Targets
addLabel(new GuiNpcLabel(103, "ability.stunDuration", labelX, y + 5));
addTextField(createIntField(103, fieldX, y, 50, shockwave.getStunDuration()));
addLabel(new GuiNpcLabel(104, "ability.maxTargets", col2LabelX, y + 5));
addTextField(createIntField(104, col2FieldX, y, 50, shockwave.getMaxTargets()));
y += 24;
addLabel(new GuiNpcLabel(105, "ability.shockwaveFalloff", labelX, y + 5));
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: shockwave.setDamage(parseFloat(field, shockwave.getDamage())); break;
case 101: shockwave.setPushRadius(parseFloat(field, shockwave.getPushRadius())); break;
case 102: shockwave.setPushStrength(parseFloat(field, shockwave.getPushStrength())); break;
case 103: shockwave.setStunDuration(field.getInteger()); break;
case 104: shockwave.setMaxTargets(field.getInteger()); break;
}
}
}
@@ -0,0 +1,62 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilitySlam;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Slam ability type-specific settings.
*/
public class SubGuiAbilitySlam extends SubGuiAbilityConfig {
private final AbilitySlam slam;
public SubGuiAbilitySlam(AbilitySlam ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.slam = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Damage + Radius
addLabel(new GuiNpcLabel(100, "ability.damage", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, slam.getDamage()));
addLabel(new GuiNpcLabel(101, "ability.radius", col2LabelX, y + 5));
addTextField(createFloatField(101, col2FieldX, y, 50, slam.getRadius()));
y += 24;
// Row 2: Knockback + Leap Speed
addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, slam.getKnockbackStrength()));
addLabel(new GuiNpcLabel(103, "ability.leapSpeed", col2LabelX, y + 5));
addTextField(createFloatField(103, col2FieldX, y, 50, slam.getLeapSpeed()));
y += 24;
// Row 3: Leap Height
addLabel(new GuiNpcLabel(104, "ability.leapHeight", labelX, y + 5));
addTextField(createFloatField(104, fieldX, y, 50, slam.getLeapHeight()));
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: slam.setDamage(parseFloat(field, slam.getDamage())); break;
case 101: slam.setRadius(parseFloat(field, slam.getRadius())); break;
case 102: slam.setKnockbackStrength(parseFloat(field, slam.getKnockbackStrength())); break;
case 103: slam.setLeapSpeed(parseFloat(field, slam.getLeapSpeed())); break;
case 104: slam.setLeapHeight(parseFloat(field, slam.getLeapHeight())); break;
}
}
}
@@ -0,0 +1,115 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Teleport ability type-specific settings.
*/
public class SubGuiAbilityTeleport extends SubGuiAbilityConfig {
private final AbilityTeleport teleport;
public SubGuiAbilityTeleport(AbilityTeleport ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.teleport = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Mode
addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5));
String[] modes = {"Blink", "Behind", "Single"};
addButton(new GuiNpcButton(100, fieldX, y, 80, 20, modes, teleport.getMode().ordinal()));
y += 24;
switch (teleport.getMode()) {
case BLINK:
// Row 2: Blink Radius + Blink Count
addLabel(new GuiNpcLabel(101, "ability.blinkRadius", labelX, y + 5));
addTextField(createFloatField(101, fieldX, y, 50, teleport.getBlinkRadius()));
addLabel(new GuiNpcLabel(102, "ability.blinkCount", col2LabelX, y + 5));
addTextField(createIntField(102, col2FieldX, y, 50, teleport.getBlinkCount()));
y += 24;
// Row 3: Blink Delay + Line of Sight
addLabel(new GuiNpcLabel(103, "ability.blinkDelay", labelX, y + 5));
addTextField(createIntField(103, fieldX, y, 50, teleport.getBlinkDelayTicks()));
addLabel(new GuiNpcLabel(105, "ability.lineOfSight", col2LabelX, y + 5));
addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0));
y += 24;
break;
case SINGLE:
// Row 2: Blink Radius + Line of Sight
addLabel(new GuiNpcLabel(101, "ability.blinkRadius", labelX, y + 5));
addTextField(createFloatField(101, fieldX, y, 50, teleport.getBlinkRadius()));
addLabel(new GuiNpcLabel(105, "ability.lineOfSight", col2LabelX, y + 5));
addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0));
y += 24;
break;
case BEHIND:
// Row 2: Behind Distance
addLabel(new GuiNpcLabel(104, "ability.behindDistance", labelX, y + 5));
addTextField(createFloatField(104, fieldX, y, 50, teleport.getBehindDistance()));
y += 24;
break;
}
// Damage + Damage Radius
addLabel(new GuiNpcLabel(106, "ability.damage", labelX, y + 5));
addTextField(createFloatField(106, fieldX, y, 50, teleport.getDamage()));
addLabel(new GuiNpcLabel(107, "ability.dmgRadius", col2LabelX, y + 5));
addTextField(createFloatField(107, col2FieldX, y, 50, teleport.getDamageRadius()));
y += 24;
// Damage at Start/End
addLabel(new GuiNpcLabel(108, "ability.dmgAtStart", labelX, y + 5));
addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtStart() ? 1 : 0));
addLabel(new GuiNpcLabel(109, "ability.dmgAtEnd", col2LabelX, y + 5));
addButton(new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtEnd() ? 1 : 0));
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
int value = button.getValue();
switch (id) {
case 100:
teleport.setMode(AbilityTeleport.TeleportMode.values()[value]);
initGui();
break;
case 105: teleport.setRequireLineOfSight(value == 1); break;
case 108: teleport.setDamageAtStart(value == 1); break;
case 109: teleport.setDamageAtEnd(value == 1); break;
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 101: teleport.setBlinkRadius(parseFloat(field, teleport.getBlinkRadius())); break;
case 102: teleport.setBlinkCount(field.getInteger()); break;
case 103: teleport.setBlinkDelayTicks(field.getInteger()); break;
case 104: teleport.setBehindDistance(parseFloat(field, teleport.getBehindDistance())); break;
case 106: teleport.setDamage(parseFloat(field, teleport.getDamage())); break;
case 107: teleport.setDamageRadius(parseFloat(field, teleport.getDamageRadius())); break;
}
}
}
@@ -0,0 +1,92 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityTrap;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Trap ability type-specific settings.
*/
public class SubGuiAbilityTrap extends SubGuiAbilityConfig {
private final AbilityTrap trap;
public SubGuiAbilityTrap(AbilityTrap ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.trap = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Placement + Trigger Radius
addLabel(new GuiNpcLabel(100, "ability.placement", labelX, y + 5));
String[] placements = {"Caster", "Target", "Ahead"};
addButton(new GuiNpcButton(100, fieldX, y, 55, 20, placements, trap.getPlacement().ordinal()));
addLabel(new GuiNpcLabel(101, "ability.triggerRad", col2LabelX, y + 5));
addTextField(createFloatField(101, col2FieldX, y, 50, trap.getTriggerRadius()));
y += 24;
// Row 2: Damage + Damage Radius
addLabel(new GuiNpcLabel(102, "ability.damage", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, trap.getDamage()));
addLabel(new GuiNpcLabel(103, "ability.dmgRadius", col2LabelX, y + 5));
addTextField(createFloatField(103, col2FieldX, y, 50, trap.getDamageRadius()));
y += 24;
// Row 3: Arm Time + Max Triggers
addLabel(new GuiNpcLabel(104, "ability.armTime", labelX, y + 5));
addTextField(createIntField(104, fieldX, y, 50, trap.getArmTime()));
addLabel(new GuiNpcLabel(105, "ability.maxTriggers", col2LabelX, y + 5));
addTextField(createIntField(105, col2FieldX, y, 50, trap.getMaxTriggers()));
y += 24;
// Row 4: Root Duration + Stun Duration
addLabel(new GuiNpcLabel(106, "ability.rootDuration", labelX, y + 5));
addTextField(createIntField(106, fieldX, y, 50, trap.getRootDuration()));
addLabel(new GuiNpcLabel(107, "ability.stunDuration", col2LabelX, y + 5));
addTextField(createIntField(107, col2FieldX, y, 50, trap.getStunDuration()));
y += 24;
// Row 5: Knockback
addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5));
addTextField(createFloatField(108, fieldX, y, 50, trap.getKnockback()));
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
if (id == 100) {
trap.setPlacement(AbilityTrap.TrapPlacement.values()[button.getValue()]);
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 101: trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius())); break;
case 102: trap.setDamage(parseFloat(field, trap.getDamage())); break;
case 103: trap.setDamageRadius(parseFloat(field, trap.getDamageRadius())); break;
case 104: trap.setArmTime(field.getInteger()); break;
case 105: trap.setMaxTriggers(field.getInteger()); break;
case 106: trap.setRootDuration(field.getInteger()); break;
case 107: trap.setStunDuration(field.getInteger()); break;
case 108: trap.setKnockback(parseFloat(field, trap.getKnockback())); break;
}
}
}
@@ -0,0 +1,96 @@
package noppes.npcs.client.gui.advanced.ability;
import kamkeel.npcs.controllers.data.ability.type.AbilityVortex;
import noppes.npcs.client.gui.util.IAbilityConfigCallback;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
/**
* GUI for configuring Vortex ability type-specific settings.
*/
public class SubGuiAbilityVortex extends SubGuiAbilityConfig {
private final AbilityVortex vortex;
public SubGuiAbilityVortex(AbilityVortex ability, IAbilityConfigCallback callback) {
super(ability, callback);
this.vortex = ability;
}
@Override
protected void initTypeTab(int startY) {
int y = startY;
int labelX = guiLeft + 8;
int fieldX = guiLeft + 85;
int col2LabelX = guiLeft + 145;
int col2FieldX = guiLeft + 205;
// Row 1: Pull Radius + Pull Strength
addLabel(new GuiNpcLabel(100, "ability.pullRadius", labelX, y + 5));
addTextField(createFloatField(100, fieldX, y, 50, vortex.getPullRadius()));
addLabel(new GuiNpcLabel(101, "ability.pullStrength", col2LabelX, y + 5));
addTextField(createFloatField(101, col2FieldX, y, 50, vortex.getPullStrength()));
y += 24;
// Row 2: Damage + Knockback
addLabel(new GuiNpcLabel(102, "ability.damage", labelX, y + 5));
addTextField(createFloatField(102, fieldX, y, 50, vortex.getDamage()));
addLabel(new GuiNpcLabel(103, "ability.knockback", col2LabelX, y + 5));
addTextField(createFloatField(103, col2FieldX, y, 50, vortex.getKnockback()));
y += 24;
// Row 3: AOE + Max Targets
addLabel(new GuiNpcLabel(104, "ability.aoe", labelX, y + 5));
addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isAoe() ? 1 : 0));
addLabel(new GuiNpcLabel(105, "ability.maxTargets", col2LabelX, y + 5));
addTextField(createIntField(105, col2FieldX, y, 50, vortex.getMaxTargets()));
y += 24;
// Row 4: Stun Duration + Root Duration
addLabel(new GuiNpcLabel(106, "ability.stunDuration", labelX, y + 5));
addTextField(createIntField(106, fieldX, y, 50, vortex.getStunDuration()));
addLabel(new GuiNpcLabel(107, "ability.rootDuration", col2LabelX, y + 5));
addTextField(createIntField(107, col2FieldX, y, 50, vortex.getRootDuration()));
y += 24;
// Row 5: Damage on Pull + Pull Damage
addLabel(new GuiNpcLabel(108, "ability.dmgOnPull", labelX, y + 5));
addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isDamageOnPull() ? 1 : 0));
addLabel(new GuiNpcLabel(109, "ability.pullDamage", col2LabelX, y + 5));
addTextField(createFloatField(109, col2FieldX, y, 50, vortex.getPullDamage()));
}
@Override
protected void handleTypeButton(int id, GuiNpcButton button) {
int value = button.getValue();
switch (id) {
case 104: vortex.setAoe(value == 1); break;
case 108: vortex.setDamageOnPull(value == 1); break;
}
}
@Override
protected void handleTypeTextField(int id, GuiNpcTextField field) {
switch (id) {
case 100: vortex.setPullRadius(parseFloat(field, vortex.getPullRadius())); break;
case 101: vortex.setPullStrength(parseFloat(field, vortex.getPullStrength())); break;
case 102: vortex.setDamage(parseFloat(field, vortex.getDamage())); break;
case 103: vortex.setKnockback(parseFloat(field, vortex.getKnockback())); break;
case 105: vortex.setMaxTargets(field.getInteger()); break;
case 106: vortex.setStunDuration(field.getInteger()); break;
case 107: vortex.setRootDuration(field.getInteger()); break;
case 109: vortex.setPullDamage(parseFloat(field, vortex.getPullDamage())); break;
}
}
}
@@ -0,0 +1,163 @@
package noppes.npcs.client.gui.global;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import net.minecraft.client.gui.GuiButton;
import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig;
import noppes.npcs.client.gui.advanced.SubGuiAbilityDeleteConfirm;
import noppes.npcs.client.gui.advanced.SubGuiAbilityLoad;
import noppes.npcs.client.gui.util.*;
import noppes.npcs.entity.EntityNPCInterface;
import java.util.*;
/**
* Global GUI for managing saved ability presets.
* Allows viewing, editing, and deleting saved abilities.
*/
public class GuiNpcManageAbilities extends GuiNPCInterface2 implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback {
private GuiCustomScroll scroll;
private String selectedName = null;
private List<String> abilityNames = new ArrayList<>();
private String search = "";
public GuiNpcManageAbilities(EntityNPCInterface npc) {
super(npc);
loadAbilityNames();
}
private void loadAbilityNames() {
Set<String> names = AbilityController.Instance.getSavedAbilityNames();
abilityNames = new ArrayList<>(names);
}
@Override
public void initGui() {
super.initGui();
int y = guiTop + 10;
// Title label
addLabel(new GuiNpcLabel(0, "ability.manager.title", guiLeft + 10, y));
y += 20;
// Scroll list of saved abilities
if (scroll == null) {
scroll = new GuiCustomScroll(this, 0);
}
scroll.setSize(xSize - 90, 160);
scroll.guiLeft = guiLeft + 10;
scroll.guiTop = y;
scroll.setUnsortedList(getFilteredList());
if (selectedName != null && abilityNames.contains(selectedName)) {
scroll.setSelected(selectedName);
}
addScroll(scroll);
// Search bar
addTextField(new GuiNpcTextField(1, this, fontRendererObj, guiLeft + 10, y + 163, xSize - 90, 18, search));
// Right side buttons
int btnX = guiLeft + xSize - 70;
addButton(new GuiNpcButton(1, btnX, y, 60, 20, "gui.edit"));
addButton(new GuiNpcButton(2, btnX, y + 25, 60, 20, "gui.delete"));
addButton(new GuiNpcButton(3, btnX, y + 60, 60, 20, "selectServer.refresh"));
// Enable/disable buttons based on selection
getButton(1).setEnabled(selectedName != null && !selectedName.isEmpty());
getButton(2).setEnabled(selectedName != null && !selectedName.isEmpty());
}
private List<String> getFilteredList() {
if (search.isEmpty()) {
return new ArrayList<>(abilityNames);
}
List<String> filtered = new ArrayList<>();
for (String name : abilityNames) {
if (name.toLowerCase().contains(search.toLowerCase())) {
filtered.add(name);
}
}
return filtered;
}
@Override
public void buttonEvent(GuiButton guibutton) {
int id = guibutton.id;
if (id == 1 && selectedName != null) {
// Edit
Ability ability = AbilityController.Instance.getSavedAbility(selectedName);
if (ability != null) {
setSubGui(ability.createConfigGui(this));
}
} else if (id == 2 && selectedName != null) {
// Delete
setSubGui(new SubGuiAbilityDeleteConfirm(selectedName, null));
} else if (id == 3) {
// Refresh
AbilityController.Instance.load();
loadAbilityNames();
selectedName = null;
initGui();
}
}
@Override
public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) {
if (scroll.id == 0) {
selectedName = scroll.getSelected();
initGui();
}
}
@Override
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
if (scroll.id == 0 && selection != null && !selection.isEmpty()) {
// Double-click to edit
Ability ability = AbilityController.Instance.getSavedAbility(selection);
if (ability != null) {
setSubGui(ability.createConfigGui(this));
}
}
}
@Override
public void keyTyped(char c, int i) {
super.keyTyped(c, i);
if (getTextField(1) != null && getTextField(1).isFocused()) {
if (!search.equals(getTextField(1).getText())) {
search = getTextField(1).getText();
scroll.setUnsortedList(getFilteredList());
scroll.resetScroll();
}
}
}
@Override
public void subGuiClosed(SubGuiInterface subgui) {
// Refresh list after any sub gui closes
loadAbilityNames();
if (subgui instanceof SubGuiAbilityDeleteConfirm) {
// After delete, clear selection if it was deleted
if (selectedName != null && !abilityNames.contains(selectedName)) {
selectedName = null;
}
}
initGui();
}
@Override
public void onAbilitySaved(Ability ability) {
// Save the edited ability
AbilityController.Instance.saveAbility(ability);
loadAbilityNames();
initGui();
}
@Override
public void save() {
// Nothing to save - abilities are saved individually
}
}
@@ -38,6 +38,7 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 {
private GuiNpcSquareButton tagButton;
private GuiNpcSquareButton effectsButton;
private GuiNpcSquareButton magicsButton;
private GuiNpcSquareButton abilitiesButton;
public GuiNPCGlobalMainMenu(EntityNPCInterface npc) {
super(npc, 5);
@@ -94,6 +95,8 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 {
registerButton(magicsButton = new GuiNpcSquareButton(20, 0, 0, 20, "global.magic", 0xFF333333));
magicsButton.setIconPos(24, 24, 48, 56).setIconTexture(specialIcons);
registerButton(abilitiesButton = new GuiNpcSquareButton(21, 0, 0, 20, "global.abilities", 0xFF333333));
abilitiesButton.setIconPos(24, 24, 72, 56).setIconTexture(specialIcons); // Placeholder icon position
// Layout composite buttons optimally.
layoutButtons();
@@ -236,6 +239,9 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 {
if (id == 19) {
NoppesUtil.requestOpenGUI(EnumGuiType.ManageEffects);
}
if (id == 21) {
NoppesUtil.openGUI(player, new noppes.npcs.client.gui.global.GuiNpcManageAbilities(npc));
}
}
@Override
@@ -13,6 +13,7 @@ import noppes.npcs.client.gui.advanced.GuiNPCAdvancedLinkedNpc;
import noppes.npcs.client.gui.advanced.GuiNPCDialogNpcOptions;
import noppes.npcs.client.gui.advanced.GuiNPCFactionSetup;
import noppes.npcs.client.gui.advanced.GuiNPCLinesMenu;
import noppes.npcs.client.gui.advanced.GuiNPCAbilities;
import noppes.npcs.client.gui.advanced.GuiNPCMagic;
import noppes.npcs.client.gui.advanced.GuiNPCMarks;
import noppes.npcs.client.gui.advanced.GuiNPCNightSetup;
@@ -69,6 +70,7 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData {
this.addButton(new GuiNpcButton(15, guiLeft + 208, y, 190, 20, "advanced.marks"));
this.addButton(new GuiNpcButton(16, guiLeft + 15, y += 22, 190, 20, "menu.magics"));
this.addButton(new GuiNpcButton(17, guiLeft + 208, y, 190, 20, "menu.abilities"));
}
@Override
@@ -130,6 +132,10 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData {
save();
NoppesUtil.openGUI(player, new GuiNPCMagic(npc));
}
if (button.id == 17) {
save();
NoppesUtil.openGUI(player, new GuiNPCAbilities(npc));
}
}
@Override
@@ -3,7 +3,9 @@ package noppes.npcs.client.gui.script;
import kamkeel.npcs.network.packets.request.script.EventScriptPacket;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.NBTTags;
import noppes.npcs.api.handler.IScriptHookHandler;
import noppes.npcs.controllers.ScriptContainer;
import noppes.npcs.controllers.ScriptHookController;
import noppes.npcs.controllers.data.DataScript;
import noppes.npcs.controllers.data.IScriptUnit;
import noppes.npcs.entity.EntityNPCInterface;
@@ -15,25 +17,7 @@ public class GuiNPCEventScripts extends GuiScriptInterface {
private final DataScript script;
public GuiNPCEventScripts(EntityNPCInterface npc) {
this.hookList = new ArrayList<>();
hookList.add("init");
hookList.add("tick");
hookList.add("interact");
hookList.add("dialog");
hookList.add("damaged");
hookList.add("killed");
hookList.add("meleeAttack");
hookList.add("meleeSwing");
hookList.add("rangedLaunched");
hookList.add("target");
hookList.add("collide");
hookList.add("kills");
hookList.add("dialogClose");
hookList.add("timer");
hookList.add("targetLost");
hookList.add("projectileTick");
hookList.add("projectileImpact");
this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_NPC));
this.script = new DataScript(npc);
this.handler = this.script;
EventScriptPacket.Get();
@@ -3,34 +3,20 @@ package noppes.npcs.client.gui.script;
import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.NBTTags;
import noppes.npcs.api.handler.IScriptHookHandler;
import noppes.npcs.controllers.ScriptContainer;
import noppes.npcs.controllers.ScriptHookController;
import noppes.npcs.controllers.data.GlobalNPCDataScript;
import noppes.npcs.controllers.data.IScriptUnit;
import java.util.ArrayList;
import java.util.List;
public class GuiScriptAllNPCs extends GuiScriptInterface {
private final GlobalNPCDataScript script = new GlobalNPCDataScript(null);
public GuiScriptAllNPCs() {
hookList.add("init");
hookList.add("tick");
hookList.add("interact");
hookList.add("dialog");
hookList.add("damaged");
hookList.add("killed");
hookList.add("meleeAttack");
hookList.add("meleeSwing");
hookList.add("rangedLaunched");
hookList.add("target");
hookList.add("collide");
hookList.add("kills");
hookList.add("dialogClose");
hookList.add("timer");
hookList.add("targetLost");
hookList.add("projectileTick");
hookList.add("projectileImpact");
this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_NPC));
this.handler = this.script;
GlobalNPCScriptPacket.Get();
}
@@ -10,19 +10,7 @@ public class GuiScriptBlock extends GuiScriptInterface {
private final TileScripted tileScripted;
public GuiScriptBlock(int x, int y, int z) {
hookList.add("init");
hookList.add("tick");
hookList.add("interact");
hookList.add("fallenUpon");
hookList.add("redstone");
hookList.add("broken");
hookList.add("exploded");
hookList.add("rainFilled");
hookList.add("neighborChanged");
hookList.add("clicked");
hookList.add("harvested");
hookList.add("collide");
hookList.add("timer");
this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_BLOCK));
this.handler = this.tileScripted = (TileScripted) player.worldObj.getTileEntity(x, y, z);
BlockScriptPacket.Get(tileScripted.xCoord, tileScripted.yCoord, tileScripted.zCoord);
@@ -21,9 +21,7 @@ public class GuiScriptEffect extends GuiScriptInterface {
this.handler = this.scriptHandler;
this.singleContainer = true;
for (EffectScript.ScriptType type : EffectScript.ScriptType.values()) {
this.hookList.add(type.function);
}
this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_EFFECT));
EffectScriptPacket.Get(effect.id);
}
@@ -50,4 +48,4 @@ public class GuiScriptEffect extends GuiScriptInterface {
public void save() {
saveWithPackets();
}
}
}
@@ -13,7 +13,9 @@ import net.minecraftforge.event.world.ChunkEvent;
import net.minecraftforge.event.world.ChunkWatchEvent;
import net.minecraftforge.event.world.WorldEvent;
import noppes.npcs.NBTTags;
import noppes.npcs.api.handler.IScriptHookHandler;
import noppes.npcs.controllers.ScriptContainer;
import noppes.npcs.controllers.ScriptHookController;
import noppes.npcs.controllers.data.ForgeDataScript;
import noppes.npcs.controllers.data.IScriptUnit;
import org.apache.commons.lang3.StringUtils;
@@ -29,8 +31,10 @@ public class GuiScriptForge extends GuiScriptInterface {
private final ForgeDataScript script = new ForgeDataScript();
public GuiScriptForge() {
hookList.add("init");
// Add built-in "init" hook from controller
hookList.addAll(ScriptHookController.Instance.getBuiltInHooks(IScriptHookHandler.CONTEXT_FORGE));
// Dynamically discover Forge event hooks
ArrayList<ClassPath.ClassInfo> list = new ArrayList();
try {
list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("cpw.mods.fml.common.gameevent"));
@@ -61,6 +65,13 @@ public class GuiScriptForge extends GuiScriptInterface {
}
hookList.add("onCNPCNaturalSpawn");
// Add any addon-registered hooks for the forge context
for (String addonHook : ScriptHookController.Instance.getAddonHooks(IScriptHookHandler.CONTEXT_FORGE)) {
if (!hookList.contains(addonHook)) {
hookList.add(addonHook);
}
}
this.handler = this.script;
ForgeScriptPacket.Get();
}
@@ -19,18 +19,7 @@ public class GuiScriptItem extends GuiScriptInterface {
public static Map<Long, String> consoleText = new HashMap<>();
public GuiScriptItem() {
hookList.add("init");
hookList.add("tick");
hookList.add("tossed");
hookList.add("pickedUp");
hookList.add("spawn");
hookList.add("interact");
hookList.add("rightClick");
hookList.add("attack");
hookList.add("startItem");
hookList.add("usingItem");
hookList.add("stopItem");
hookList.add("finishItem");
this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_ITEM));
this.handler = this.item = new ScriptCustomItem(new ItemStack(CustomItems.scripted_item));
ItemScriptErrorPacket.Get();
@@ -1,6 +1,10 @@
package noppes.npcs.client.gui.script;
import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiConfirmOpenLink;
import net.minecraft.client.gui.GuiYesNo;
import net.minecraft.client.gui.GuiYesNoCallback;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.client.gui.global.GuiNPCManageLinked;
import noppes.npcs.constants.EnumScriptType;
@@ -20,20 +24,7 @@ public class GuiScriptLinkedItem extends GuiScriptInterface {
this.handler = new LinkedItemScript();
this.singleContainer = true;
hookList.add(EnumScriptType.LINKED_ITEM_BUILD.function);
hookList.add(EnumScriptType.LINKED_ITEM_VERSION.function);
hookList.add(EnumScriptType.INIT.function);
hookList.add(EnumScriptType.TICK.function);
hookList.add(EnumScriptType.TOSSED.function);
hookList.add(EnumScriptType.PICKEDUP.function);
hookList.add(EnumScriptType.SPAWN.function);
hookList.add(EnumScriptType.INTERACT.function);
hookList.add(EnumScriptType.RIGHT_CLICK.function);
hookList.add(EnumScriptType.ATTACK.function);
hookList.add(EnumScriptType.START_USING_ITEM.function);
hookList.add(EnumScriptType.USING_ITEM.function);
hookList.add(EnumScriptType.STOP_USING_ITEM.function);
hookList.add(EnumScriptType.FINISH_USING_ITEM.function);
this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_LINKED_ITEM));
LinkedItemScriptPacket.Get(linkedItem.id);
}
@@ -4,89 +4,23 @@ import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.NBTTags;
import noppes.npcs.api.handler.IScriptHookHandler;
import noppes.npcs.constants.ScriptContext;
import noppes.npcs.constants.EnumScriptType;
import noppes.npcs.controllers.ScriptContainer;
import noppes.npcs.controllers.ScriptHookController;
import noppes.npcs.controllers.data.EffectScript;
import noppes.npcs.controllers.data.IScriptUnit;
import noppes.npcs.controllers.data.PlayerDataScript;
import java.util.ArrayList;
import java.util.List;
public class GuiScriptPlayers extends GuiScriptInterface {
private PlayerDataScript script = new PlayerDataScript((EntityPlayer) null);
public GuiScriptPlayers() {
hookList.add(EnumScriptType.INIT.function);
hookList.add(EnumScriptType.TICK.function);
hookList.add(EnumScriptType.INTERACT.function);
hookList.add(EnumScriptType.RIGHT_CLICK.function);
hookList.add(EnumScriptType.ATTACK.function);
hookList.add(EnumScriptType.ATTACKED.function);
hookList.add(EnumScriptType.DAMAGED_ENTITY.function);
hookList.add(EnumScriptType.DAMAGED.function);
hookList.add(EnumScriptType.KILLS.function);
hookList.add(EnumScriptType.KILLED.function);
hookList.add(EnumScriptType.DROP.function);
hookList.add(EnumScriptType.RESPAWN.function);
hookList.add(EnumScriptType.BREAK_BLOCK.function);
hookList.add(EnumScriptType.CHAT.function);
hookList.add(EnumScriptType.LOGIN.function);
hookList.add(EnumScriptType.LOGOUT.function);
hookList.add(EnumScriptType.KEY_PRESSED.function);
hookList.add(EnumScriptType.MOUSE_CLICKED.function);
hookList.add(EnumScriptType.TOSS.function);
hookList.add(EnumScriptType.PICKUP.function);
hookList.add(EnumScriptType.PICKUP_XP.function);
hookList.add(EnumScriptType.RANGED_CHARGE.function);
hookList.add(EnumScriptType.RANGED_LAUNCHED.function);
hookList.add(EnumScriptType.TIMER.function);
hookList.add(EnumScriptType.START_USING_ITEM.function);
hookList.add(EnumScriptType.USING_ITEM.function);
hookList.add(EnumScriptType.STOP_USING_ITEM.function);
hookList.add(EnumScriptType.FINISH_USING_ITEM.function);
hookList.add(EnumScriptType.CONTAINER_OPEN.function);
hookList.add(EnumScriptType.USE_HOE.function);
hookList.add(EnumScriptType.BONEMEAL.function);
hookList.add(EnumScriptType.FILL_BUCKET.function);
hookList.add(EnumScriptType.JUMP.function);
hookList.add(EnumScriptType.FALL.function);
hookList.add(EnumScriptType.WAKE_UP.function);
hookList.add(EnumScriptType.SLEEP.function);
hookList.add(EnumScriptType.PLAYSOUND.function);
hookList.add(EnumScriptType.LIGHTNING.function);
hookList.add(EnumScriptType.CHANGED_DIM.function);
hookList.add(EnumScriptType.QUEST_START.function);
hookList.add(EnumScriptType.QUEST_COMPLETED.function);
hookList.add(EnumScriptType.QUEST_TURNIN.function);
hookList.add(EnumScriptType.FACTION_POINTS.function);
hookList.add(EnumScriptType.DIALOG_OPEN.function);
hookList.add(EnumScriptType.DIALOG_OPTION.function);
hookList.add(EnumScriptType.DIALOG_CLOSE.function);
hookList.add(EnumScriptType.SCRIPT_COMMAND.function);
hookList.add(EnumScriptType.CUSTOM_GUI_CLOSED.function);
hookList.add(EnumScriptType.CUSTOM_GUI_BUTTON.function);
hookList.add(EnumScriptType.CUSTOM_GUI_SLOT.function);
hookList.add(EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function);
hookList.add(EnumScriptType.CUSTOM_GUI_SCROLL.function);
hookList.add(EnumScriptType.CUSTOM_GUI_TEXTFIELD.function);
hookList.add(EnumScriptType.PARTY_QUEST_COMPLETED.function);
hookList.add(EnumScriptType.PARTY_QUEST_SET.function);
hookList.add(EnumScriptType.PARTY_QUEST_TURNED_IN.function);
hookList.add(EnumScriptType.PARTY_INVITE.function);
hookList.add(EnumScriptType.PARTY_KICK.function);
hookList.add(EnumScriptType.PARTY_LEAVE.function);
hookList.add(EnumScriptType.PARTY_DISBAND.function);
hookList.add(EnumScriptType.ANIMATION_START.function);
hookList.add(EnumScriptType.ANIMATION_END.function);
hookList.add(EnumScriptType.ANIMATION_FRAME_ENTER.function);
hookList.add(EnumScriptType.ANIMATION_FRAME_EXIT.function);
hookList.add(EnumScriptType.PROFILE_CHANGE.function);
hookList.add(EnumScriptType.PROFILE_REMOVE.function);
hookList.add(EnumScriptType.PROFILE_CREATE.function);
hookList.add(EffectScript.ScriptType.OnEffectAdd.function);
hookList.add(EffectScript.ScriptType.OnEffectTick.function);
hookList.add(EffectScript.ScriptType.OnEffectRemove.function);
this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_PLAYER));
this.handler = this.script;
PlayerScriptPacket.Get();
@@ -1,6 +1,10 @@
package noppes.npcs.client.gui.script;
import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.GuiConfirmOpenLink;
import net.minecraft.client.gui.GuiYesNo;
import net.minecraft.client.gui.GuiYesNoCallback;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.client.gui.global.GuiNpcManageRecipes;
import noppes.npcs.constants.ScriptContext;
@@ -22,8 +26,7 @@ public class GuiScriptRecipe extends GuiScriptInterface {
this.recipeId = recipe.id;
this.singleContainer = true;
hookList.add(RecipeScript.ScriptType.PRE.function);
hookList.add(RecipeScript.ScriptType.POST.function);
this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_RECIPE));
RecipeScriptPacket.Get(false, recipeId);
}
@@ -35,8 +38,7 @@ public class GuiScriptRecipe extends GuiScriptInterface {
this.anvil = true;
this.recipeId = recipe.id;
hookList.add(RecipeScript.ScriptType.PRE.function);
hookList.add(RecipeScript.ScriptType.POST.function);
this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_RECIPE));
RecipeScriptPacket.Get(true, recipeId);
}
@@ -0,0 +1,175 @@
package noppes.npcs.client.gui.select;
import kamkeel.npcs.network.PacketClient;
import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket;
import net.minecraft.client.gui.GuiButton;
import noppes.npcs.client.gui.util.GuiCustomScroll;
import noppes.npcs.client.gui.util.GuiNpcButton;
import noppes.npcs.client.gui.util.GuiNpcLabel;
import noppes.npcs.client.gui.util.GuiNpcTextField;
import noppes.npcs.client.gui.util.ICustomScrollListener;
import noppes.npcs.client.gui.util.IScrollData;
import noppes.npcs.client.gui.util.SubGuiInterface;
import noppes.npcs.constants.EnumScrollData;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Vector;
/**
* Animation selection dialog for choosing global animations.
* Requests animation data from the server via packets.
*/
public class GuiAnimationSelection extends SubGuiInterface implements ICustomScrollListener, IScrollData {
private GuiCustomScroll scrollAnimations;
private HashMap<String, Integer> animationData = new HashMap<String, Integer>();
private int initialAnimationId;
public int selectedAnimationId = -1;
private String selectedName = null;
private String search = "";
public GuiAnimationSelection(int animationId) {
drawDefaultBackground = false;
title = "";
setBackground("menubg.png");
xSize = 220;
ySize = 226;
this.initialAnimationId = animationId;
this.selectedAnimationId = animationId;
// Request animation data from server
PacketClient.sendClient(new AnimationsGetPacket());
}
@Override
public void initGui() {
super.initGui();
this.addLabel(new GuiNpcLabel(0, "gui.animations", guiLeft + 8, guiTop + 4));
this.addButton(new GuiNpcButton(1, guiLeft + 4, guiTop + ySize - 35, 50, 20, "gui.clear"));
this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, guiTop + ySize - 35, 50, 20, "gui.done"));
this.addButton(new GuiNpcButton(3, guiLeft + xSize - 108, guiTop + ySize - 35, 50, 20, "gui.cancel"));
if (scrollAnimations == null) {
scrollAnimations = new GuiCustomScroll(this, 0, 0);
scrollAnimations.setSize(212, 163);
}
scrollAnimations.setList(getSearchList());
if (selectedName != null) {
scrollAnimations.setSelected(selectedName);
}
scrollAnimations.guiLeft = guiLeft + 4;
scrollAnimations.guiTop = guiTop + 14;
this.addScroll(scrollAnimations);
addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, guiTop + 179, 212, 20, search));
}
@Override
public void setData(Vector<String> list, HashMap<String, Integer> data, EnumScrollData type) {
this.animationData = data;
// Find the name for our initial animation ID
if (initialAnimationId >= 0) {
for (String name : data.keySet()) {
if (data.get(name) == initialAnimationId) {
selectedName = name;
break;
}
}
}
if (scrollAnimations != null) {
scrollAnimations.setList(getSearchList());
if (selectedName != null) {
scrollAnimations.setSelected(selectedName);
}
}
}
@Override
public void setSelected(String selected) {
this.selectedName = selected;
if (scrollAnimations != null) {
scrollAnimations.setSelected(selected);
}
}
@Override
public void keyTyped(char c, int i) {
super.keyTyped(c, i);
if (getTextField(55) != null) {
if (getTextField(55).isFocused()) {
if (search.equals(getTextField(55).getText()))
return;
search = getTextField(55).getText().toLowerCase();
scrollAnimations.resetScroll();
scrollAnimations.setList(getSearchList());
}
}
}
private List<String> getSearchList() {
if (search.isEmpty()) {
return new ArrayList<String>(animationData.keySet());
}
List<String> list = new ArrayList<String>();
for (String name : animationData.keySet()) {
if (name.toLowerCase().contains(search))
list.add(name);
}
return list;
}
@Override
public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) {
if (guiCustomScroll.id == 0) {
String selected = scrollAnimations.getSelected();
if (selected != null && animationData.containsKey(selected)) {
selectedName = selected;
selectedAnimationId = animationData.get(selected);
}
}
initGui();
}
@Override
public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) {
if (selectedAnimationId < 0)
return;
close();
}
@Override
protected void actionPerformed(GuiButton guibutton) {
int id = guibutton.id;
if (id == 1) {
// Clear selection
selectedName = null;
selectedAnimationId = -1;
close();
}
if (id == 2) {
// Done - keep selection
close();
}
if (id == 3) {
// Cancel - restore initial value
selectedAnimationId = initialAnimationId;
close();
}
}
/**
* Get the name of the selected animation (for display purposes).
*/
public String getSelectedName() {
return selectedName;
}
}
@@ -0,0 +1,15 @@
package noppes.npcs.client.gui.util;
import kamkeel.npcs.controllers.data.ability.Ability;
/**
* Callback interface for SubGuiAbilityConfig.
* Implemented by GUIs that open the ability config dialog.
*/
public interface IAbilityConfigCallback {
/**
* Called when an ability has been edited and saved.
* @param ability The edited ability
*/
void onAbilitySaved(Ability ability);
}
@@ -63,5 +63,6 @@ public enum EnumGuiType {
ScriptItem,
ScriptBlock,
Paintbrush,
MagicBook;
MagicBook,
ManageAbilities;
}
@@ -114,7 +114,14 @@ public enum EnumScriptType {
PROFILE("profile"),
PROFILE_CHANGE("profileChange"),
PROFILE_REMOVE("profileRemove"),
PROFILE_CREATE("profileCreate");
PROFILE_CREATE("profileCreate"),
ABILITY_START("abilityStart"),
ABILITY_EXECUTE("abilityExecute"),
ABILITY_INTERRUPT("abilityInterrupt"),
ABILITY_COMPLETE("abilityComplete"),
ABILITY_HIT("abilityHit"),
ABILITY_TICK("abilityTick");
public String function;
@@ -5,5 +5,6 @@ public enum EnumScrollData {
MAGIC,
MAGIC_CYCLES,
DIALOG_GROUP,
QUEST_GROUP
QUEST_GROUP,
ABILITY_TYPES
}
@@ -0,0 +1,346 @@
package noppes.npcs.controllers;
import noppes.npcs.api.handler.IScriptHookHandler;
import noppes.npcs.constants.EnumScriptType;
import noppes.npcs.controllers.data.EffectScript;
import noppes.npcs.controllers.data.RecipeScript;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Controller for managing script hooks.
* Allows addon mods to register custom hooks that appear in script editor GUIs.
*/
public class ScriptHookController implements IScriptHookHandler {
public static ScriptHookController Instance;
// Built-in hooks per context
private final Map<String, List<String>> builtInHooks = new HashMap<>();
// Addon-registered hooks per context
private final Map<String, List<String>> addonHooks = new HashMap<>();
// All available contexts
private static final String[] CONTEXTS = {
CONTEXT_NPC,
CONTEXT_PLAYER,
CONTEXT_BLOCK,
CONTEXT_ITEM,
CONTEXT_LINKED_ITEM,
CONTEXT_FORGE,
CONTEXT_RECIPE,
CONTEXT_EFFECT
};
public ScriptHookController() {
Instance = this;
initializeBuiltInHooks();
}
private void initializeBuiltInHooks() {
// Initialize empty lists for all contexts
for (String context : CONTEXTS) {
builtInHooks.put(context, new ArrayList<>());
addonHooks.put(context, new ArrayList<>());
}
// NPC hooks
registerBuiltIn(CONTEXT_NPC,
EnumScriptType.INIT.function,
EnumScriptType.TICK.function,
EnumScriptType.INTERACT.function,
EnumScriptType.DIALOG.function,
EnumScriptType.DAMAGED.function,
EnumScriptType.KILLED.function,
EnumScriptType.ATTACK_MELEE.function,
EnumScriptType.ATTACK_SWING.function,
EnumScriptType.RANGED_LAUNCHED.function,
EnumScriptType.TARGET.function,
EnumScriptType.COLLIDE.function,
EnumScriptType.KILLS.function,
EnumScriptType.DIALOG_CLOSE.function,
EnumScriptType.TIMER.function,
EnumScriptType.TARGET_LOST.function,
EnumScriptType.PROJECTILE_TICK.function,
EnumScriptType.PROJECTILE_IMPACT.function,
EnumScriptType.ABILITY_TICK.function,
EnumScriptType.ABILITY_EXECUTE.function,
EnumScriptType.ABILITY_HIT.function,
EnumScriptType.ABILITY_START.function,
EnumScriptType.ABILITY_INTERRUPT.function,
EnumScriptType.ABILITY_COMPLETE.function
);
// Player hooks
registerBuiltIn(CONTEXT_PLAYER,
EnumScriptType.INIT.function,
EnumScriptType.TICK.function,
EnumScriptType.INTERACT.function,
EnumScriptType.RIGHT_CLICK.function,
EnumScriptType.ATTACK.function,
EnumScriptType.ATTACKED.function,
EnumScriptType.DAMAGED_ENTITY.function,
EnumScriptType.DAMAGED.function,
EnumScriptType.KILLS.function,
EnumScriptType.KILLED.function,
EnumScriptType.DROP.function,
EnumScriptType.RESPAWN.function,
EnumScriptType.BREAK_BLOCK.function,
EnumScriptType.CHAT.function,
EnumScriptType.LOGIN.function,
EnumScriptType.LOGOUT.function,
EnumScriptType.KEY_PRESSED.function,
EnumScriptType.MOUSE_CLICKED.function,
EnumScriptType.TOSS.function,
EnumScriptType.PICKUP.function,
EnumScriptType.PICKUP_XP.function,
EnumScriptType.RANGED_CHARGE.function,
EnumScriptType.RANGED_LAUNCHED.function,
EnumScriptType.TIMER.function,
EnumScriptType.START_USING_ITEM.function,
EnumScriptType.USING_ITEM.function,
EnumScriptType.STOP_USING_ITEM.function,
EnumScriptType.FINISH_USING_ITEM.function,
EnumScriptType.CONTAINER_OPEN.function,
EnumScriptType.USE_HOE.function,
EnumScriptType.BONEMEAL.function,
EnumScriptType.FILL_BUCKET.function,
EnumScriptType.JUMP.function,
EnumScriptType.FALL.function,
EnumScriptType.WAKE_UP.function,
EnumScriptType.SLEEP.function,
EnumScriptType.PLAYSOUND.function,
EnumScriptType.LIGHTNING.function,
EnumScriptType.CHANGED_DIM.function,
EnumScriptType.QUEST_START.function,
EnumScriptType.QUEST_COMPLETED.function,
EnumScriptType.QUEST_TURNIN.function,
EnumScriptType.FACTION_POINTS.function,
EnumScriptType.DIALOG_OPEN.function,
EnumScriptType.DIALOG_OPTION.function,
EnumScriptType.DIALOG_CLOSE.function,
EnumScriptType.SCRIPT_COMMAND.function,
EnumScriptType.CUSTOM_GUI_CLOSED.function,
EnumScriptType.CUSTOM_GUI_BUTTON.function,
EnumScriptType.CUSTOM_GUI_SLOT.function,
EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function,
EnumScriptType.CUSTOM_GUI_SCROLL.function,
EnumScriptType.CUSTOM_GUI_TEXTFIELD.function,
EnumScriptType.PARTY_QUEST_COMPLETED.function,
EnumScriptType.PARTY_QUEST_SET.function,
EnumScriptType.PARTY_QUEST_TURNED_IN.function,
EnumScriptType.PARTY_INVITE.function,
EnumScriptType.PARTY_KICK.function,
EnumScriptType.PARTY_LEAVE.function,
EnumScriptType.PARTY_DISBAND.function,
EnumScriptType.ANIMATION_START.function,
EnumScriptType.ANIMATION_END.function,
EnumScriptType.ANIMATION_FRAME_ENTER.function,
EnumScriptType.ANIMATION_FRAME_EXIT.function,
EnumScriptType.PROFILE_CHANGE.function,
EnumScriptType.PROFILE_REMOVE.function,
EnumScriptType.PROFILE_CREATE.function,
EffectScript.ScriptType.OnEffectAdd.function,
EffectScript.ScriptType.OnEffectTick.function,
EffectScript.ScriptType.OnEffectRemove.function
);
// Block hooks
registerBuiltIn(CONTEXT_BLOCK,
EnumScriptType.INIT.function,
EnumScriptType.TICK.function,
EnumScriptType.INTERACT.function,
EnumScriptType.FALLEN_UPON.function,
EnumScriptType.REDSTONE.function,
EnumScriptType.BROKEN.function,
EnumScriptType.EXPLODED.function,
EnumScriptType.RAIN_FILLED.function,
EnumScriptType.NEIGHBOR_CHANGED.function,
EnumScriptType.CLICKED.function,
EnumScriptType.HARVESTED.function,
EnumScriptType.COLLIDE.function,
EnumScriptType.TIMER.function
);
// Item hooks
registerBuiltIn(CONTEXT_ITEM,
EnumScriptType.INIT.function,
EnumScriptType.TICK.function,
EnumScriptType.TOSSED.function,
EnumScriptType.PICKEDUP.function,
EnumScriptType.SPAWN.function,
EnumScriptType.INTERACT.function,
EnumScriptType.RIGHT_CLICK.function,
EnumScriptType.ATTACK.function,
EnumScriptType.START_USING_ITEM.function,
EnumScriptType.USING_ITEM.function,
EnumScriptType.STOP_USING_ITEM.function,
EnumScriptType.FINISH_USING_ITEM.function
);
// Linked item hooks
registerBuiltIn(CONTEXT_LINKED_ITEM,
EnumScriptType.LINKED_ITEM_BUILD.function,
EnumScriptType.LINKED_ITEM_VERSION.function,
EnumScriptType.INIT.function,
EnumScriptType.TICK.function,
EnumScriptType.TOSSED.function,
EnumScriptType.PICKEDUP.function,
EnumScriptType.SPAWN.function,
EnumScriptType.INTERACT.function,
EnumScriptType.RIGHT_CLICK.function,
EnumScriptType.ATTACK.function,
EnumScriptType.START_USING_ITEM.function,
EnumScriptType.USING_ITEM.function,
EnumScriptType.STOP_USING_ITEM.function,
EnumScriptType.FINISH_USING_ITEM.function
);
// Recipe hooks
registerBuiltIn(CONTEXT_RECIPE,
RecipeScript.ScriptType.PRE.function,
RecipeScript.ScriptType.POST.function
);
// Effect hooks
registerBuiltIn(CONTEXT_EFFECT,
EffectScript.ScriptType.OnEffectAdd.function,
EffectScript.ScriptType.OnEffectTick.function,
EffectScript.ScriptType.OnEffectRemove.function
);
// Forge hooks - just init, the rest are dynamically discovered
registerBuiltIn(CONTEXT_FORGE,
EnumScriptType.INIT.function
);
}
private void registerBuiltIn(String context, String... hooks) {
List<String> list = builtInHooks.get(context);
if (list != null) {
for (String hook : hooks) {
if (!list.contains(hook)) {
list.add(hook);
}
}
}
}
@Override
public void registerHook(String context, String functionName) {
if (context == null || functionName == null || functionName.isEmpty()) {
return;
}
List<String> hooks = addonHooks.get(context);
if (hooks == null) {
// Custom context - create it
hooks = new ArrayList<>();
addonHooks.put(context, hooks);
}
if (!hooks.contains(functionName) && !isBuiltInHook(context, functionName)) {
hooks.add(functionName);
}
}
@Override
public void registerHooks(String functionName, String... contexts) {
if (functionName == null || functionName.isEmpty() || contexts == null) {
return;
}
for (String context : contexts) {
registerHook(context, functionName);
}
}
@Override
public void unregisterHook(String context, String functionName) {
if (context == null || functionName == null) {
return;
}
List<String> hooks = addonHooks.get(context);
if (hooks != null) {
hooks.remove(functionName);
}
}
@Override
public void unregisterHookFromAll(String functionName) {
if (functionName == null) {
return;
}
for (List<String> hooks : addonHooks.values()) {
hooks.remove(functionName);
}
}
@Override
public List<String> getAddonHooks(String context) {
List<String> hooks = addonHooks.get(context);
return hooks != null ? Collections.unmodifiableList(hooks) : Collections.emptyList();
}
@Override
public List<String> getAllHooks(String context) {
List<String> result = new ArrayList<>();
// Add built-in hooks
List<String> builtIn = builtInHooks.get(context);
if (builtIn != null) {
result.addAll(builtIn);
}
// Add addon hooks
List<String> addon = addonHooks.get(context);
if (addon != null) {
for (String hook : addon) {
if (!result.contains(hook)) {
result.add(hook);
}
}
}
return result;
}
@Override
public boolean hasHook(String context, String functionName) {
if (context == null || functionName == null) {
return false;
}
return isBuiltInHook(context, functionName) || isAddonHook(context, functionName);
}
@Override
public String[] getContexts() {
return CONTEXTS.clone();
}
private boolean isBuiltInHook(String context, String functionName) {
List<String> hooks = builtInHooks.get(context);
return hooks != null && hooks.contains(functionName);
}
private boolean isAddonHook(String context, String functionName) {
List<String> hooks = addonHooks.get(context);
return hooks != null && hooks.contains(functionName);
}
/**
* Get built-in hooks for a context (used by GUIs for backwards compatibility).
*/
public List<String> getBuiltInHooks(String context) {
List<String> hooks = builtInHooks.get(context);
return hooks != null ? Collections.unmodifiableList(hooks) : Collections.emptyList();
}
}
@@ -73,6 +73,7 @@ import net.minecraftforge.common.util.FakePlayer;
import net.minecraftforge.event.ServerChatEvent;
import noppes.npcs.CustomItems;
import noppes.npcs.CustomNpcs;
import noppes.npcs.DataAbilities;
import noppes.npcs.DataAI;
import noppes.npcs.DataAdvanced;
import noppes.npcs.DataDisplay;
@@ -183,6 +184,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
public DataScript script;
public DataTransform transform;
public DataTimers timers;
public DataAbilities abilities;
public CombatHandler combatHandler = new CombatHandler(this);
public ActionManager actionManager = new ActionManager();
@@ -271,6 +273,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
transform = new DataTransform(this);
script = new DataScript(this);
timers = new DataTimers(this);
abilities = new DataAbilities(this);
this.getAttributeMap().registerAttribute(SharedMonsterAttributes.attackDamage);
this.getEntityAttribute(SharedMonsterAttributes.maxHealth).setBaseValue(stats.maxHealth);
@@ -460,6 +463,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
onCollide();
actionManager.tick();
abilities.tick();
}
if (wasKilled != isKilled() && wasKilled) {
@@ -1179,6 +1183,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
script.readEventsFromNBT(compound);
timers.readFromNBT(compound);
advanced.readToNBT(compound);
abilities.readFromNBT(compound);
if (advanced.role != EnumRoleType.None && roleInterface != null)
roleInterface.readFromNBT(compound);
if (advanced.job != EnumJobType.None && jobInterface != null)
@@ -1211,6 +1216,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
script.writeEventsToNBT(compound);
timers.writeToNBT(compound);
advanced.writeToNBT(compound);
abilities.writeToNBT(compound);
if (advanced.role != EnumRoleType.None && roleInterface != null)
roleInterface.writeToNBT(compound);
if (advanced.job != EnumJobType.None && jobInterface != null)
@@ -60,6 +60,7 @@ import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.gui.ICustomGui;
import noppes.npcs.api.handler.IActionManager;
import noppes.npcs.api.handler.IAbilityHandler;
import noppes.npcs.api.handler.IAnimationHandler;
import noppes.npcs.api.handler.IAttributeHandler;
import noppes.npcs.api.handler.ICloneHandler;
@@ -72,6 +73,7 @@ import noppes.npcs.api.handler.IPartyHandler;
import noppes.npcs.api.handler.IProfileHandler;
import noppes.npcs.api.handler.IQuestHandler;
import noppes.npcs.api.handler.IRecipeHandler;
import noppes.npcs.api.handler.IScriptHookHandler;
import noppes.npcs.api.handler.ITransportHandler;
import noppes.npcs.api.handler.data.IAnimation;
import noppes.npcs.api.handler.data.IFrame;
@@ -83,6 +85,7 @@ import noppes.npcs.compat.PixelmonHelper;
import noppes.npcs.config.ConfigScript;
import noppes.npcs.constants.EnumAnimationPart;
import noppes.npcs.containers.ContainerNpcInterface;
import kamkeel.npcs.controllers.data.ability.AbilityController;
import noppes.npcs.controllers.AnimationController;
import noppes.npcs.controllers.ChunkController;
import noppes.npcs.controllers.CustomEffectController;
@@ -93,6 +96,7 @@ import noppes.npcs.controllers.PartyController;
import noppes.npcs.controllers.QuestController;
import noppes.npcs.controllers.RecipeController;
import noppes.npcs.controllers.ScriptController;
import noppes.npcs.controllers.ScriptHookController;
import noppes.npcs.controllers.ScriptEntityData;
import noppes.npcs.controllers.ServerCloneController;
import noppes.npcs.controllers.SpawnController;
@@ -356,6 +360,16 @@ public class NpcAPI extends AbstractNpcAPI {
return AnimationController.Instance;
}
public IScriptHookHandler getScriptHooks() {
return ScriptHookController.Instance;
}
@Override
public IAbilityHandler getAbilities() {
this.checkWorld();
return AbilityController.Instance;
}
@Override
public String[] getAllBiomeNames() {
List<String> biomes = new ArrayList<>();
@@ -0,0 +1,206 @@
package noppes.npcs.scripted.event;
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.ICustomNpc;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.event.IAbilityEvent;
import noppes.npcs.constants.EnumScriptType;
import noppes.npcs.scripted.NpcAPI;
/**
* Events fired during NPC ability execution lifecycle.
*/
public class AbilityEvent extends NpcEvent implements IAbilityEvent {
protected final Ability ability;
protected final IEntityLivingBase target;
public AbilityEvent(ICustomNpc npc, Ability ability, EntityLivingBase target) {
super(npc);
this.ability = ability;
this.target = target != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(target) : null;
}
@Override
public IAbility getAbility() {
return ability;
}
@Override
public IEntityLivingBase getTarget() {
return target;
}
@Override
public String getHookName() {
return "abilityEvent";
}
/**
* Fired when an ability starts executing (enters WINDUP phase).
*/
@Cancelable
public static class StartEvent extends AbilityEvent implements IAbilityEvent.StartEvent {
public StartEvent(ICustomNpc npc, Ability ability, EntityLivingBase target) {
super(npc, ability, target);
}
@Override
public String getHookName() {
return EnumScriptType.ABILITY_START.function;
}
}
/**
* Fired when an ability enters the ACTIVE phase and performs its effect.
*/
@Cancelable
public static class ExecuteEvent extends AbilityEvent implements IAbilityEvent.ExecuteEvent {
public ExecuteEvent(ICustomNpc npc, Ability ability, EntityLivingBase target) {
super(npc, ability, target);
}
@Override
public String getHookName() {
return EnumScriptType.ABILITY_EXECUTE.function;
}
}
/**
* Fired when an ability is interrupted by damage.
*/
public static class InterruptEvent extends AbilityEvent implements IAbilityEvent.InterruptEvent {
private final IDamageSource damageSource;
private final float damage;
public InterruptEvent(ICustomNpc npc, Ability ability, EntityLivingBase target, DamageSource source, float damage) {
super(npc, ability, target);
this.damageSource = NpcAPI.Instance().getIDamageSource(source);
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 an ability completes its full execution cycle.
*/
public static class CompleteEvent extends AbilityEvent implements IAbilityEvent.CompleteEvent {
public CompleteEvent(ICustomNpc npc, Ability ability, EntityLivingBase target) {
super(npc, ability, target);
}
@Override
public String getHookName() {
return EnumScriptType.ABILITY_COMPLETE.function;
}
}
/**
* Fired when an ability hits an entity with damage.
* This is a cancelable event - canceling prevents the damage from being applied.
*/
@Cancelable
public static class HitEvent extends AbilityEvent implements IAbilityEvent.HitEvent {
private final IEntityLivingBase hitEntity;
private float damage;
private float knockback;
private float knockbackUp;
public HitEvent(ICustomNpc npc, Ability ability, EntityLivingBase target,
EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) {
super(npc, ability, target);
this.hitEntity = hitEntity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(hitEntity) : null;
this.damage = damage;
this.knockback = knockback;
this.knockbackUp = knockbackUp;
}
@Override
public IEntityLivingBase getHitEntity() {
return hitEntity;
}
@Override
public float getDamage() {
return damage;
}
@Override
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getKnockback() {
return knockback;
}
@Override
public void setKnockback(float knockback) {
this.knockback = knockback;
}
@Override
public float getKnockbackUp() {
return knockbackUp;
}
@Override
public void setKnockbackUp(float knockbackUp) {
this.knockbackUp = knockbackUp;
}
@Override
public String getHookName() {
return EnumScriptType.ABILITY_HIT.function;
}
}
/**
* Fired every tick while an ability is executing.
* Provides information about the current phase and tick.
*/
public static class TickEvent extends AbilityEvent implements IAbilityEvent.TickEvent {
private final int abilityPhase;
private final int tick;
public TickEvent(ICustomNpc npc, Ability ability, EntityLivingBase target, int phase, int tick) {
super(npc, 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;
}
}
}