mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 20:07:32 +00:00
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:
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);
|
||||
}
|
||||
}
|
||||
+69
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user