diff --git a/src/api b/src/api index 48cfcb3ea..866be1fef 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 48cfcb3ea529e79b0ba18fdd4ba768bf6275c75e +Subproject commit 866be1fef5bf37a002379140771096cebe29f286 diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java new file mode 100644 index 000000000..37e1232c4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java @@ -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(); + } +} diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java new file mode 100644 index 000000000..4a783f551 --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -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 names = AbilityController.Instance.getSavedAbilityNames(); + if (names.isEmpty()) { + sendResult(sender, "No saved abilities found."); + return; + } + + List 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 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 = "" + ) + public void delete(ICommandSender sender, String[] args) { + if (args.length < 1) { + sendError(sender, "Usage: /kam ability delete "); + 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 = "" + ) + public void info(ICommandSender sender, String[] args) { + if (args.length < 1) { + sendError(sender, "Usage: /kam ability info "); + 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")); + } +} diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java index 76bc80b94..356ca8a31 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java @@ -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()); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java new file mode 100644 index 000000000..4d62a934f --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -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 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 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()); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java new file mode 100644 index 000000000..685e5403d --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -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> factories = new LinkedHashMap<>(); + private final Map> factoriesByTypeId = new LinkedHashMap<>(); + private final Map 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 getSavedAbilityNames() { + return new LinkedHashSet<>(savedAbilities.keySet()); + } + + /** + * Get all saved abilities. + */ + public Collection 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 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 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 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); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java new file mode 100644 index 000000000..6338279ba --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java @@ -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 +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java new file mode 100644 index 000000000..70b3b290f --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java @@ -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; } + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java new file mode 100644 index 000000000..a639702e1 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java @@ -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 +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java new file mode 100644 index 000000000..1b42b55e1 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java @@ -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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java new file mode 100644 index 000000000..b16a9f0f2 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java @@ -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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java new file mode 100644 index 000000000..b82afffd2 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java @@ -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 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 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 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; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java new file mode 100644 index 000000000..dee9a249a --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java @@ -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 +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java new file mode 100644 index 000000000..f094a4f0d --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -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 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 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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java new file mode 100644 index 000000000..841a8dc7e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -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 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 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 hitRadius * 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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java new file mode 100644 index 000000000..b258e6127 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -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 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 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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java new file mode 100644 index 000000000..db34b0bb8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java new file mode 100644 index 000000000..2ef0a446c --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java new file mode 100644 index 000000000..d211e7516 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -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 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 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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java new file mode 100644 index 000000000..4a393682b --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -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 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 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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java new file mode 100644 index 000000000..288af63d7 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java new file mode 100644 index 000000000..cf1555898 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -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 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 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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java new file mode 100644 index 000000000..4d2143b36 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -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 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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java new file mode 100644 index 000000000..340b92826 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -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 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; } + +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java new file mode 100644 index 000000000..321b9058f --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -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 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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java new file mode 100644 index 000000000..72971c572 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -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 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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java new file mode 100644 index 000000000..9d1090da6 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -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 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 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 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 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; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java new file mode 100644 index 000000000..ccd8d4a19 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -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 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 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; } +} diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 745962aba..2bdf70426 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -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; @@ -486,6 +491,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()); @@ -550,6 +560,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() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index 874677af9..ebcbdfc59 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -58,4 +58,8 @@ public enum EnumDataPacket { SWING_PLAYER_ARM, DISABLE_MOUSE_INPUT, LOGIN, + + // Ability System + TELEGRAPH_SPAWN, + TELEGRAPH_REMOVE, } diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 0159fe719..1defc81c6 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -193,6 +193,9 @@ public enum EnumRequestPacket { MagicSave, NpcMagicGet, NpcMagicSave, + NpcAbilitiesGet, + NpcAbilitiesSave, + AbilitiesGetAll, Hammer, RecipeScript, diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java new file mode 100644 index 000000000..e0c5023cb --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java @@ -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); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java new file mode 100644 index 000000000..bbf342ade --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java @@ -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); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/AbilitiesGetAllPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/AbilitiesGetAllPacket.java new file mode 100644 index 000000000..eb74bbce4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/AbilitiesGetAllPacket.java @@ -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); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/AbilitiesNpcGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/AbilitiesNpcGetPacket.java new file mode 100644 index 000000000..3ca2f59e1 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/AbilitiesNpcGetPacket.java @@ -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); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/AbilitiesNpcSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/AbilitiesNpcSavePacket.java new file mode 100644 index 000000000..a7a032d9f --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/AbilitiesNpcSavePacket.java @@ -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); + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 934d13385..0b3ac2feb 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -69,6 +69,7 @@ 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; @@ -270,6 +271,8 @@ public class CustomNpcs { new SpawnController(); new LinkedNpcController(); new AnimationController(); + new AbilityController(); + AbilityController.Instance.load(); LinkedItemController.getInstance().load(); diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 3363c0351..3a1113c05 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -47,7 +47,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"); diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java new file mode 100644 index 000000000..d9655b20a --- /dev/null +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -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 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 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 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 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 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); + } + } + } +} diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 7d06d4e52..89bdf58c8 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -763,6 +763,18 @@ public class NoppesUtilServer { } } + public static void sendAbilityTypesInfo(EntityPlayerMP player) { + HashMap map = new HashMap(); + 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)); } diff --git a/src/main/java/noppes/npcs/ai/CombatHandler.java b/src/main/java/noppes/npcs/ai/CombatHandler.java index 2a2319c4f..e3eb85fbe 100644 --- a/src/main/java/noppes/npcs/ai/CombatHandler.java +++ b/src/main/java/noppes/npcs/ai/CombatHandler.java @@ -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(); + } } diff --git a/src/main/java/noppes/npcs/ai/EntityAIAttackTarget.java b/src/main/java/noppes/npcs/ai/EntityAIAttackTarget.java index 967c3cf73..5fe559fc2 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIAttackTarget.java +++ b/src/main/java/noppes/npcs/ai/EntityAIAttackTarget.java @@ -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; diff --git a/src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java b/src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java index 437dbd6d3..e14f2e833 100644 --- a/src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java +++ b/src/main/java/noppes/npcs/ai/EntityAILeapAtTargetNpc.java @@ -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; diff --git a/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java b/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java index 4f87575bf..d3354cdfe 100644 --- a/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java +++ b/src/main/java/noppes/npcs/ai/EntityAIPounceTarget.java @@ -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; diff --git a/src/main/java/noppes/npcs/ai/EntityAISprintToTarget.java b/src/main/java/noppes/npcs/ai/EntityAISprintToTarget.java index 0d8d100c2..2ba100d10 100644 --- a/src/main/java/noppes/npcs/ai/EntityAISprintToTarget.java +++ b/src/main/java/noppes/npcs/ai/EntityAISprintToTarget.java @@ -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()) { diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 7cc3d7b76..4e64f8351 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -136,6 +136,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; @@ -306,6 +308,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()) { diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index af44405dc..7c5ec98f2 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -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; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java new file mode 100644 index 000000000..584bd8238 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -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 allAbilityTypes = new HashMap<>(); + + // Display name to typeId mapping for the available types scroll + private final HashMap displayNameToTypeId = new HashMap<>(); + + // NPC's current abilities + private final List 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 list, HashMap 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 getFilteredTypeList() { + List 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 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(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java new file mode 100644 index 000000000..6d12693bb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -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 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; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java new file mode 100644 index 000000000..ee2561b4e --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java @@ -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(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java new file mode 100644 index 000000000..069c9f1b4 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -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 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 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(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java new file mode 100644 index 000000000..3be13f0be --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java @@ -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(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java new file mode 100644 index 000000000..f0d4f2734 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java @@ -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; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java new file mode 100644 index 000000000..e951e5fbd --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java new file mode 100644 index 000000000..a7b19b05c --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java new file mode 100644 index 000000000..b825789a8 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java new file mode 100644 index 000000000..2d0d5c33a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java new file mode 100644 index 000000000..4858445fb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java new file mode 100644 index 000000000..def67e71d --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java new file mode 100644 index 000000000..365460672 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java new file mode 100644 index 000000000..c045833b9 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java new file mode 100644 index 000000000..5c82c42f8 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java new file mode 100644 index 000000000..cbde41b7f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java new file mode 100644 index 000000000..9230e242c --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java new file mode 100644 index 000000000..2fdb0d7fb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java new file mode 100644 index 000000000..c4e9154e5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java new file mode 100644 index 000000000..8c8d92bb4 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java new file mode 100644 index 000000000..6978abce7 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java @@ -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; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java new file mode 100644 index 000000000..e5ae8c436 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -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 abilityNames = new ArrayList<>(); + private String search = ""; + + public GuiNpcManageAbilities(EntityNPCInterface npc) { + super(npc); + loadAbilityNames(); + } + + private void loadAbilityNames() { + Set 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 getFilteredList() { + if (search.isEmpty()) { + return new ArrayList<>(abilityNames); + } + List 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 + } +} diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java index 8295618dd..bca447b1f 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java @@ -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 diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java index 6452145a6..4cd861448 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -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 diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java index 8577a080c..027f51a60 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java @@ -17,7 +17,6 @@ public class GuiNPCEventScripts extends GuiScriptInterface { public GuiNPCEventScripts(EntityNPCInterface npc) { this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_NPC)); - this.script = new DataScript(npc); this.handler = this.script; EventScriptPacket.Get(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java index fd2e366fc..cef9096bd 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java @@ -15,8 +15,7 @@ public class GuiScriptAllNPCs extends GuiScriptInterface { private final GlobalNPCDataScript script = new GlobalNPCDataScript(null); public GuiScriptAllNPCs() { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_GLOBAL_NPC)); - + this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_NPC)); this.handler = this.script; GlobalNPCScriptPacket.Get(); } diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java new file mode 100644 index 000000000..c0fbb0295 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java @@ -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 animationData = new HashMap(); + + 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 list, HashMap 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 getSearchList() { + if (search.isEmpty()) { + return new ArrayList(animationData.keySet()); + } + List list = new ArrayList(); + 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; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java b/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java new file mode 100644 index 000000000..ec186d066 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java @@ -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); +} diff --git a/src/main/java/noppes/npcs/constants/EnumGuiType.java b/src/main/java/noppes/npcs/constants/EnumGuiType.java index d0919b9b1..f2d911561 100644 --- a/src/main/java/noppes/npcs/constants/EnumGuiType.java +++ b/src/main/java/noppes/npcs/constants/EnumGuiType.java @@ -63,5 +63,6 @@ public enum EnumGuiType { ScriptItem, ScriptBlock, Paintbrush, - MagicBook; + MagicBook, + ManageAbilities; } diff --git a/src/main/java/noppes/npcs/constants/EnumScriptType.java b/src/main/java/noppes/npcs/constants/EnumScriptType.java index 14ce38f41..b1052c86c 100644 --- a/src/main/java/noppes/npcs/constants/EnumScriptType.java +++ b/src/main/java/noppes/npcs/constants/EnumScriptType.java @@ -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; diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index 52a9575f1..abf55a84e 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -5,5 +5,6 @@ public enum EnumScrollData { MAGIC, MAGIC_CYCLES, DIALOG_GROUP, - QUEST_GROUP + QUEST_GROUP, + ABILITY_TYPES } diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index bf382e768..702d27cd0 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -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) diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 95e1e277c..26a0eb89a 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -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; @@ -84,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; @@ -362,6 +364,12 @@ public class NpcAPI extends AbstractNpcAPI { return ScriptHookController.Instance; } + @Override + public IAbilityHandler getAbilities() { + this.checkWorld(); + return AbilityController.Instance; + } + @Override public String[] getAllBiomeNames() { List biomes = new ArrayList<>(); diff --git a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java new file mode 100644 index 000000000..9baf245a2 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java @@ -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; + } + } +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 466acfb99..73124d9bf 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1437,3 +1437,232 @@ trail.heart=Heart trail.angryvillager=AngryVillager trail.happyvillager=HappyVillager trail.custom=Custom + +menu.abilities=Abilities + +ability.availableTypes=Available Types +ability.npcAbilities=NPC Abilities +ability.globalCooldown=Global Cooldown +ability.edit=Edit Ability +ability.weight=Weight +ability.targeting=Targeting +ability.minRange=Min Range +ability.maxRange=Max Range +ability.timing=Timing (ticks) +ability.cooldown=Cooldown +ability.windup=Wind Up +ability.active=Active +ability.recovery=Recovery +ability.interruptible=Interruptible +ability.threshold=Threshold +ability.lockMove=Lock Move +ability.telegraph=Telegraph +ability.telegraphType=Telegraph Type + +ability.target.aggro_target=Target +ability.target.self=Self +ability.target.aoe_self=AOE Self +ability.target.aoe_target=AOE Target + +ability.telegraph.none=None +ability.telegraph.circle=Circle +ability.telegraph.ring=Ring +ability.telegraph.line=Line +ability.telegraph.cone=Cone +ability.telegraph.point=Point + +ability.tab.general=General +ability.tab.timing=Timing +ability.tab.effects=Effects +ability.tab.telegraph=Telegraph +ability.showTelegraph=Show Telegraph +ability.telegraphColor=Color +ability.telegraphSizeNote=Size is based on ability parameters + +# Ability Effects Tab +ability.windUpSound=Wind Up Sound +ability.activeSound=Active Sound +ability.windUpAnim=Wind Up Anim +ability.activeAnim=Active Anim +gui.animations=Animations + +ability.cnpc.slam=Slam +ability.cnpc.heavy_hit=Heavy Hit +ability.cnpc.cutter=Cutter +ability.cnpc.beam=Beam +ability.cnpc.projectile=Projectile +ability.cnpc.orb=Orb +ability.cnpc.charge=Charge +ability.cnpc.dash=Dash +ability.cnpc.teleport=Teleport +ability.cnpc.vortex=Vortex +ability.cnpc.shockwave=Shockwave +ability.cnpc.guard=Guard +ability.cnpc.heal=Heal +ability.cnpc.hazard=Hazard +ability.cnpc.trap=Trap + +# Ability GUI - Type Tab +ability.tab.type=Type + +# Common Type Settings +ability.damage=Damage +ability.radius=Radius +ability.knockback=Knockback +ability.knockbackUp=KB Up +ability.speed=Speed +ability.range=Range +ability.maxDist=Max Dist +ability.minDist=Min Dist +ability.distance=Distance +ability.lifetime=Lifetime +ability.stunTicks=Stun +ability.stunDur=Stun Dur +ability.rootDur=Root Dur +ability.rootDuration=Root Duration +ability.stunDuration=Stun Duration +ability.leapHeight=Leap Height +ability.hitWidth=Hit Width +ability.potionTime=Potion Time (s) +ability.weaknessLvl=Weakness Lvl +ability.poisonTime=Poison Time (s) +ability.slownessLvl=Slowness Lvl +ability.poisonLvl=Poison Lvl + +# Slam Settings +ability.leapSpeed=Leap Speed + +# Charge Settings +ability.hitRadius=Hit Radius + +# Cutter Settings +ability.arcAngle=Arc Angle +ability.sweepWaves=Waves +ability.waveInterval=Interval +ability.rotSpeed=Rot Speed +ability.piercing=Piercing + +# Orb Settings +ability.orbSpeed=Speed +ability.orbSize=Size +ability.homing=Homing +ability.explosive=Explosive + +# Vortex Settings +ability.pullRadius=Radius +ability.pullStrength=Strength +ability.pullTo=Pull To +ability.aoe=AOE +ability.maxTargets=Max Targets + +# Dash Settings +ability.dashMode=Mode +ability.dash.aggressive=Aggressive +ability.dash.defensive=Defensive +ability.mode=Mode +ability.behindDistance=Behind Dist + +# Teleport Settings +ability.blinkCount=Blinks +ability.blinkDelay=Delay +ability.blinkRadius=Radius +ability.minRadius=Min Rad +ability.dmgRadius=Dmg Radius +ability.lineOfSight=Line Sight +ability.dmgAtStart=Dmg Start + +# Projectile Settings +ability.explRadius=Expl Rad + +# Beam Settings +ability.beamLength=Length +ability.beamWidth=Width +ability.dmgInterval=Interval +ability.sweepAngle=Sweep Ang +ability.sweepSpeed=Sweep Spd +ability.lockTarget=Lock Tgt + +# Guard Settings +ability.dmgReduce=Dmg Reduce +ability.counterDmg=Counter Dmg +ability.canCounter=Counter +ability.counterChance=Counter % +ability.counterType=Counter Type +ability.counterValue=Counter Value +ability.counterSound=Counter Sound +ability.counterAnim=Counter Anim + +# Heal Settings +ability.healAmount=Amount +ability.healPercent=Percent +ability.healRadius=Radius +ability.instant=Instant +ability.healSelf=Heal Self +ability.healAllies=Heal Allies + +# Hazard Settings +ability.dmgPerTick=Dmg/Tick +ability.debuffDur=Debuff Dur +ability.shape=Shape +ability.placement=Placement +ability.affectsCaster=Hit Caster +ability.ignoreInvuln=Ign Invuln +ability.shape.circle=Circle +ability.shape.ring=Ring +ability.shape.cone=Cone +ability.place.caster=At Caster +ability.place.target=At Target +ability.place.followCaster=Follow Caster +ability.place.followTarget=Follow Target +ability.place.ahead=Ahead + +# Trap Settings +ability.triggerRadius=Trig Rad +ability.armTime=Arm Time +ability.maxTriggers=Max Trig +ability.visible=Visible + +# Ability Save/Load +ability.save.confirm=Save ability preset +ability.save.overwrite=Overwrite existing preset +ability.load.select=Select ability to load: +ability.delete.confirm=Delete ability preset +gui.load=Load + +# Global Abilities Manager +global.abilities=Abilities +ability.manager.title=Ability Manager + +# Explosive Hover Text +ability.explosive.noBlockDamage=Explosions do not destroy blocks + +# Additional Orb Settings +ability.explRadius=Expl Radius + +# Telegraph Colors (for abilities that support separate colors) +ability.windUpColor=Wind Up Color +ability.activeColor=Active Color + +# Shockwave Settings +ability.pushRadius=Push Radius +ability.pushStrength=Push Str +ability.shockwaveFalloff=Damage/knockback drop off with distance + +# Ability Config GUI +ability.typeId=Type ID +ability.tab.target=Target +ability.conditions=Conditions +ability.noTypeSettings=No type-specific settings +ability.sounds=Sounds +ability.animations=Animations + +# Conditions +condition.edit=Edit Condition +condition.hp_above=NPC HP Above +condition.hp_below=NPC HP Below +condition.target_hp_above=Target HP Above +condition.target_hp_below=Target HP Below +condition.hit_count=Hit Count +condition.percent=Threshold +condition.hits=Required Hits +condition.within=Within (ticks)