diff --git a/src/api b/src/api index 06c9daf81..db6a68e43 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 06c9daf81be8df0138a992ca6bcabc05e37189cb +Subproject commit db6a68e43073b2828ec05a3c2ede660453c4544e diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index cf5e1ca58..499f9491d 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -4,9 +4,6 @@ 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; @@ -25,25 +22,23 @@ public class AbilityCommand extends CommandKamkeelBase { } @SubCommand( - desc = "List all saved abilities" + desc = "List all custom abilities" ) public void list(ICommandSender sender, String[] args) { - Set names = AbilityController.Instance.getSavedAbilityNames(); - if (names.isEmpty()) { - sendResult(sender, "No saved abilities found."); + Set uuids = AbilityController.Instance.getCustomAbilityIds(); + if (uuids.isEmpty()) { + sendResult(sender, "No custom 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); + sendResult(sender, "Custom Abilities (" + uuids.size() + "):"); + for (String uuid : uuids) { + Ability ability = AbilityController.Instance.getCustomAbility(uuid); if (ability != null) { - sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ")"); + String name = ability.getName() != null ? ability.getName() : uuid; + sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ") [" + uuid + "]"); } else { - sendResult(sender, " - \u00A7b" + name); + sendResult(sender, " - \u00A7b" + uuid); } } } @@ -79,39 +74,39 @@ public class AbilityCommand extends CommandKamkeelBase { } @SubCommand( - desc = "Delete a saved ability", - usage = "" + desc = "Delete a custom ability by UUID", + usage = "" ) public void delete(ICommandSender sender, String[] args) { if (args.length < 1) { - sendError(sender, "Usage: /kam ability delete "); + 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); + String uuid = args[0]; + if (!AbilityController.Instance.hasCustomAbility(uuid)) { + sendError(sender, "No custom ability with UUID: " + uuid); return; } - AbilityController.Instance.deleteAbility(name); - sendResult(sender, "Deleted ability: \u00A7b" + name); + AbilityController.Instance.deleteCustomAbility(uuid); + sendResult(sender, "Deleted ability: \u00A7b" + uuid); } @SubCommand( - desc = "Get info about a saved ability", - usage = "" + desc = "Get info about an ability", + usage = "" ) public void info(ICommandSender sender, String[] args) { if (args.length < 1) { - sendError(sender, "Usage: /kam ability info "); + sendError(sender, "Usage: /kam ability info "); return; } - String name = args[0]; - Ability ability = AbilityController.Instance.getSavedAbility(name); + String key = args[0]; + Ability ability = AbilityController.Instance.resolveAbility(key); if (ability == null) { - sendError(sender, "No saved ability with name: " + name); + sendError(sender, "No ability found for: " + key); return; } diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 3247498b5..b6010ad86 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -39,11 +39,14 @@ import noppes.npcs.controllers.data.Quest; import noppes.npcs.controllers.data.QuestCategory; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; import java.io.IOException; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -72,7 +75,8 @@ public class SyncController { EnumSyncType.ANVIL_RECIPES, EnumSyncType.CUSTOM_EFFECTS, EnumSyncType.MAGIC, - EnumSyncType.MAGIC_CYCLE + EnumSyncType.MAGIC_CYCLE, + EnumSyncType.CUSTOM_ABILITY }; public static void load() { @@ -88,6 +92,7 @@ public class SyncController { registerCache(EnumSyncType.CUSTOM_EFFECTS, SyncController::customEffectsNBT); registerCache(EnumSyncType.MAGIC, SyncController::magicsNBT); registerCache(EnumSyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT); + registerCache(EnumSyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT); } public static void syncPlayer(EntityPlayerMP player) { @@ -323,6 +328,27 @@ public class SyncController { return compound; } + public static NBTTagCompound customAbilitiesNBT() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { + NBTTagCompound abilityNBT = entry.getValue().writeNBT(); + abilityNBT.setString("CustomAbilityId", entry.getKey()); + list.appendTag(abilityNBT); + } + compound.setTag("Data", list); + return compound; + } + + public static void syncAllCustomAbilities() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_ABILITY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_ABILITY, payload)); + updateAllPlayerRevisions(EnumSyncType.CUSTOM_ABILITY, payload.getRevision()); + } + public static void syncPlayerData(EntityPlayerMP player, boolean update) { PlayerData data = PlayerData.get(player); if (data != null) { @@ -595,6 +621,21 @@ public class SyncController { ce.customEffectsSync.clear(); break; } + case CUSTOM_ABILITY: { + NBTTagList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound nbt = list.getCompoundTagAt(i); + String uuid = nbt.getString("CustomAbilityId"); + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability != null) { + ability.setId(uuid); + sync.put(uuid, ability); + } + } + AbilityController.Instance.setCustomAbilities(sync); + break; + } } ClientCacheHandler.updateClientRevision(enumSyncType, revision); @@ -843,6 +884,8 @@ public class SyncController { return EnumSet.of(EnumSyncType.ANVIL_RECIPES); case CUSTOM_EFFECTS: return EnumSet.of(EnumSyncType.CUSTOM_EFFECTS); + case CUSTOM_ABILITY: + return EnumSet.of(EnumSyncType.CUSTOM_ABILITY); default: return EnumSet.noneOf(EnumSyncType.class); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index eb59edf7c..6f66614e0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -19,9 +19,14 @@ import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; +import net.minecraft.entity.player.EntityPlayer; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.data.PlayerDataScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; +import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import java.util.ArrayList; import java.util.List; @@ -81,6 +86,12 @@ public abstract class Ability implements IAbility { // Custom data for external mods protected NBTTagCompound customData = new NBTTagCompound(); + // User type restriction + protected UserType allowedBy = UserType.BOTH; + + // Cooldown override + protected boolean ignoreCooldown = false; + // Configurable potion effects protected List effects = new ArrayList<>(); @@ -101,12 +112,12 @@ public abstract class Ability implements IAbility { /** * Called first tick of ACTIVE phase */ - public abstract void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world); + public abstract void onExecute(EntityLivingBase caster, EntityLivingBase target, World world); /** * Called every tick of ACTIVE phase */ - public abstract void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick); + public abstract void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick); /** * Write type-specific config to NBT @@ -122,55 +133,55 @@ public abstract class Ability implements IAbility { // OPTIONAL OVERRIDES // ═══════════════════════════════════════════════════════════════════ - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { } - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { } - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, 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 caster The entity executing the ability (NPC or Player) * @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, + protected boolean applyAbilityDamage(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback) { - double dx = hitEntity.posX - npc.posX; - double dz = hitEntity.posZ - npc.posZ; - return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, 0.0f, dx, dz); + double dx = hitEntity.posX - caster.posX; + double dz = hitEntity.posZ - caster.posZ; + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, 0.0f, dx, dz); } /** * Apply damage to an entity with ability hit event support and vertical knockback. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability + * @param caster The entity executing the ability (NPC or Player) * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback * @param knockbackUp The vertical knockback * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, + protected boolean applyAbilityDamage(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { - double dx = hitEntity.posX - npc.posX; - double dz = hitEntity.posZ - npc.posZ; - return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, knockbackUp, dx, dz); + double dx = hitEntity.posX - caster.posX; + double dz = hitEntity.posZ - caster.posZ; + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, knockbackUp, dx, dz); } /** * 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 caster The entity executing the ability (NPC or Player) * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback @@ -178,46 +189,74 @@ public abstract class Ability implements IAbility { * @param knockbackDirZ The Z component of knockback direction * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity, + protected boolean applyAbilityDamageWithDirection(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback, double knockbackDirX, double knockbackDirZ) { - return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, 0.0f, knockbackDirX, knockbackDirZ); + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, 0.0f, knockbackDirX, knockbackDirZ); } /** * Internal unified damage application method. * All public damage methods delegate to this. + * Supports both NPC and Player casters. */ - private boolean applyAbilityDamageInternal(EntityNPCInterface npc, EntityLivingBase hitEntity, + private boolean applyAbilityDamageInternal(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp, double knockbackDirX, double knockbackDirZ) { - DataAbilities dataAbilities = npc.abilities; - AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( - this, currentTarget, hitEntity, damage, knockback, knockbackUp); + // Fire hit event for NPC casters (NPC event system) + if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + DataAbilities dataAbilities = npc.abilities; + AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( + this, currentTarget, hitEntity, damage, knockback, knockbackUp); - if (event == null) { - return false; // Cancelled + if (event == null) { + return false; // Cancelled + } + + damage = event.getDamage(); + knockback = event.getKnockback(); + knockbackUp = event.getKnockbackUp(); + } + // Fire hit event for Player casters (Player event system) + else if (caster instanceof EntityPlayer && ScriptController.Instance != null) { + EntityPlayer player = (EntityPlayer) caster; + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + if (handler != null) { + IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); + PlayerAbilityEvent.HitEvent event = new PlayerAbilityEvent.HitEvent( + iPlayer, this, currentTarget, hitEntity, damage, knockback, knockbackUp); + + if (noppes.npcs.EventHooks.onPlayerAbilityHit(handler, event)) { + return false; // Cancelled + } + + damage = event.getDamage(); + knockback = event.getKnockback(); + knockbackUp = event.getKnockbackUp(); + } } - // Apply damage with potentially modified values - float finalDamage = event.getDamage(); - float finalKnockback = event.getKnockback(); - float finalKnockbackUp = event.getKnockbackUp(); - // Apply damage - if (finalDamage > 0) { - hitEntity.attackEntityFrom(new NpcDamageSource("mob", npc), finalDamage); + if (damage > 0) { + if (caster instanceof EntityNPCInterface) { + hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), damage); + } else if (caster instanceof EntityPlayer) { + hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), damage); + } else { + hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), damage); + } } // Apply knockback if any - if (finalKnockback > 0 || finalKnockbackUp > 0) { - applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback, finalKnockbackUp); + if (knockback > 0 || knockbackUp > 0) { + applyKnockback(hitEntity, knockbackDirX, knockbackDirZ, knockback, knockbackUp); } return true; } - private void applyNpcKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback, float knockbackUp) { + private void applyKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback, float knockbackUp) { double len = Math.sqrt(dirX * dirX + dirZ * dirZ); double x = 0; double z = 0; @@ -334,33 +373,33 @@ public abstract class Ability implements IAbility { * @param target The target (for position calculation) * @return The telegraph instance, or null if no telegraph */ - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE) { return null; } Telegraph telegraph; double x, y, z; - float yaw = npc.rotationYaw; + float yaw = caster.rotationYaw; // Determine position based on targeting mode - boolean positionAtNpc = targetingMode == TargetingMode.AOE_SELF || + boolean positionAtCaster = 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; + if (positionAtCaster) { + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; } else if (target != null) { x = target.posX; - y = findGroundLevel(npc.worldObj, target.posX, target.posY, target.posZ); + y = findGroundLevel(caster.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; + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; } // Create telegraph based on type @@ -391,12 +430,12 @@ public abstract class Ability implements IAbility { telegraph.setHeightOffset(telegraphHeightOffset); TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); - instance.setCasterEntityId(npc.getEntityId()); + instance.setCasterEntityId(caster.getEntityId()); // Set entity to follow based on targeting mode - if (positionAtNpc) { - // AOE_SELF abilities: telegraph follows NPC during windup - instance.setEntityIdToFollow(npc.getEntityId()); + if (positionAtCaster) { + // AOE_SELF abilities: telegraph follows caster during windup + instance.setEntityIdToFollow(caster.getEntityId()); // For LINE/CONE telegraphs: track target direction during windup if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) { @@ -729,9 +768,20 @@ public abstract class Ability implements IAbility { // CONDITION CHECKING // ═══════════════════════════════════════════════════════════════════ - public boolean checkConditions(EntityNPCInterface npc, EntityLivingBase target) { + public boolean checkConditions(EntityLivingBase caster, EntityLivingBase target) { for (Condition c : conditions) { - if (!c.check(npc, target)) return false; + if (!c.check(caster, target)) return false; + } + return true; + } + + /** + * Check conditions for a player caster, skipping target-requiring conditions. + */ + public boolean checkConditionsForPlayer(EntityLivingBase caster) { + for (Condition c : conditions) { + if (c.requiresTarget()) continue; + if (!c.check(caster, null)) return false; } return true; } @@ -768,6 +818,8 @@ public abstract class Ability implements IAbility { nbt.setString("telegraphType", telegraphType.name()); nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset); nbt.setTag("customData", customData); + nbt.setInteger("allowedBy", allowedBy.ordinal()); + nbt.setBoolean("ignoreCooldown", ignoreCooldown); // Conditions NBTTagList condList = new NBTTagList(); @@ -803,38 +855,14 @@ public abstract class Ability implements IAbility { dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("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; - } + windUpColor = nbt.getInteger("windUpColor"); + activeColor = nbt.getInteger("activeColor"); + windUpSound = nbt.getString("windUpSound"); + activeSound = nbt.getString("activeSound"); + windUpAnimationId = nbt.hasKey("windUpAnimationId") ? nbt.getInteger("windUpAnimationId") : -1; activeAnimationId = nbt.hasKey("activeAnimationId") ? nbt.getInteger("activeAnimationId") : -1; - windUpAnimationName = nbt.hasKey("windUpAnimationName") ? nbt.getString("windUpAnimationName") : ""; - activeAnimationName = nbt.hasKey("activeAnimationName") ? nbt.getString("activeAnimationName") : ""; + windUpAnimationName = nbt.getString("windUpAnimationName"); + activeAnimationName = nbt.getString("activeAnimationName"); showTelegraph = !nbt.hasKey("showTelegraph") || nbt.getBoolean("showTelegraph"); if (nbt.hasKey("telegraphType")) { try { @@ -845,6 +873,8 @@ public abstract class Ability implements IAbility { } telegraphHeightOffset = nbt.hasKey("telegraphHeightOffset") ? nbt.getFloat("telegraphHeightOffset") : 0.1f; customData = nbt.getCompoundTag("customData"); + allowedBy = nbt.hasKey("allowedBy") ? UserType.fromOrdinal(nbt.getInteger("allowedBy")) : UserType.BOTH; + ignoreCooldown = nbt.hasKey("ignoreCooldown") && nbt.getBoolean("ignoreCooldown"); // Conditions conditions.clear(); @@ -1170,6 +1200,22 @@ public abstract class Ability implements IAbility { conditions.add(c); } + public UserType getAllowedBy() { + return allowedBy; + } + + public void setAllowedBy(UserType allowedBy) { + this.allowedBy = allowedBy; + } + + public boolean isIgnoreCooldown() { + return ignoreCooldown; + } + + public void setIgnoreCooldown(boolean ignoreCooldown) { + this.ignoreCooldown = ignoreCooldown; + } + public List getEffects() { return effects; } @@ -1192,6 +1238,16 @@ public abstract class Ability implements IAbility { // IAbility API METHODS // ═══════════════════════════════════════════════════════════════════ + @Override + public int getAllowedByType() { + return allowedBy.ordinal(); + } + + @Override + public void setAllowedByType(int type) { + this.allowedBy = UserType.fromOrdinal(type); + } + @Override public INbt getNbt() { return NpcAPI.Instance().getINbt(writeNBT()); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index a61df06fa..62d226bf7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -18,6 +18,7 @@ import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; +import kamkeel.npcs.controllers.SyncController; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; @@ -29,30 +30,46 @@ import java.util.*; import java.util.function.Supplier; /** - * Controller for ability types. Allows external mods to register custom ability types. - * Initialize during FMLInitializationEvent. + * Central controller for ability types and presets. *

- * Also manages saved ability presets that can be reused across NPCs. + * Three maps: + *

    + *
  • {@code typeFactories} — keyed by typeId (lang key), creates new instances of each ability type
  • + *
  • {@code abilities} — built-in pre-configured abilities, keyed by unique name
  • + *
  • {@code customAbilities} — user-created presets, keyed by UUID
  • + *
*/ public class AbilityController implements IAbilityHandler { - public static AbilityController Instance; + public static AbilityController Instance = new AbilityController(); - private final Map> factories = new LinkedHashMap<>(); - private final Map> factoriesByTypeId = new LinkedHashMap<>(); - private final Map savedAbilities = new LinkedHashMap<>(); + /** Type factories keyed by typeId (lang key, e.g. "ability.cnpc.slam"). */ + private final Map> typeFactories = new LinkedHashMap<>(); + + /** Built-in pre-configured abilities keyed by unique name. */ + private final Map abilities = new LinkedHashMap<>(); + + /** User-created custom ability presets keyed by UUID. */ + private final Map customAbilities = new LinkedHashMap<>(); + + /** Version counter — incremented on any custom ability add/modify/delete. + * Used by AbilitySlot for cache invalidation. */ + private int version = 0; public AbilityController() { - Instance = this; - registerBuiltins(); + registerBuiltinTypes(); } + // ═══════════════════════════════════════════════════════════════════════════ + // LOADING / SAVING CUSTOM ABILITIES + // ═══════════════════════════════════════════════════════════════════════════ + /** - * Load saved abilities from world directory. - * Called when world loads. + * Load custom ability presets from world directory. + * Called when world loads. Files are named {@code .json}. */ public void load() { - savedAbilities.clear(); + customAbilities.clear(); File dir = getDir(); if (!dir.exists()) { return; @@ -64,22 +81,22 @@ public class AbilityController implements IAbilityHandler { for (File file : files) { if (!file.isFile() || !file.getName().endsWith(".json")) continue; try { + String filename = file.getName(); + String uuid = filename.substring(0, filename.length() - 5); // strip .json + 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()); + if (ability != null) { + ability.setId(uuid); + customAbilities.put(uuid, ability); } } catch (Exception e) { - LogWriter.error("Error loading ability preset: " + file.getAbsolutePath(), e); + LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e); } } - LogWriter.info("Loaded " + savedAbilities.size() + " ability presets"); + LogWriter.info("Loaded " + customAbilities.size() + " custom abilities"); } - /** - * Get the ability save directory. - */ private File getDir() { File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities"); if (!dir.exists()) { @@ -89,26 +106,26 @@ public class AbilityController implements IAbilityHandler { } /** - * Save an ability preset to disk. - * - * @param ability The ability to save - * @return true if successful + * Save a custom ability preset. If the ability has no id, generates a UUID. + * Stores in customAbilities map and writes to disk. */ - public boolean saveAbility(Ability ability) { - if (ability == null || ability.getName() == null || ability.getName().isEmpty()) { - return false; + public boolean saveCustomAbility(Ability ability) { + if (ability == null) return false; + + String uuid = ability.getId(); + if (uuid == null || uuid.isEmpty()) { + uuid = UUID.randomUUID().toString(); + ability.setId(uuid); } 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"); + File fileNew = new File(dir, uuid + ".json_new"); + File fileCurrent = new File(dir, uuid + ".json"); try { NBTTagCompound nbt = ability.writeNBT(); NBTJsonUtil.SaveFile(fileNew, nbt); - // Atomic rename if (fileCurrent.exists()) { fileCurrent.delete(); } @@ -117,76 +134,144 @@ public class AbilityController implements IAbilityHandler { fileNew.delete(); } - savedAbilities.put(ability.getName(), ability); - LogWriter.info("Saved ability preset: " + ability.getName()); + customAbilities.put(uuid, ability); + version++; + LogWriter.info("Saved custom ability: " + ability.getName() + " [" + uuid + "]"); + SyncController.syncAllCustomAbilities(); return true; } catch (Exception e) { - LogWriter.error("Error saving ability preset: " + ability.getName(), e); + LogWriter.error("Error saving custom ability: " + uuid, e); return false; } } /** - * Delete a saved ability preset. - * - * @param name The name of the ability to delete - * @return true if successful + * Delete a custom ability preset by UUID. */ - public boolean deleteAbility(String name) { - if (name == null || name.isEmpty()) { - return false; - } + public boolean deleteCustomAbility(String uuid) { + if (uuid == null || uuid.isEmpty()) return false; - Ability removed = savedAbilities.remove(name); - if (removed == null) { - return false; - } + Ability removed = customAbilities.remove(uuid); + if (removed == null) return false; File dir = getDir(); - String safeName = name.replaceAll("[^a-zA-Z0-9_-]", "_"); - File file = new File(dir, safeName + ".json"); + File file = new File(dir, uuid + ".json"); if (file.exists()) { file.delete(); } - LogWriter.info("Deleted ability preset: " + name); + version++; + LogWriter.info("Deleted custom ability: " + uuid); + SyncController.syncAllCustomAbilities(); return true; } + // ═══════════════════════════════════════════════════════════════════════════ + // BUILT-IN ABILITY REGISTRY + // ═══════════════════════════════════════════════════════════════════════════ + /** - * Get a saved ability preset by name. + * Register a pre-configured built-in ability by name. + * These are looked up by name in {@link #resolveAbility(String)}. * - * @param name The name of the ability - * @return A copy of the ability, or null if not found + * @param name Unique name (e.g., "Jump Attack", "Ki Blast") + * @param ability Pre-configured ability instance */ - public Ability getSavedAbility(String name) { - Ability saved = savedAbilities.get(name); - if (saved == null) { - return null; + public void registerAbility(String name, Ability ability) { + if (abilities.containsKey(name)) { + LogWriter.info("AbilityController: Overwriting built-in ability: " + name); } - // Return a copy so modifications don't affect the saved version - return fromNBT(saved.writeNBT()); + ability.setName(name); + abilities.put(name, ability); + LogWriter.info("Registered ability: " + name); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // RESOLUTION (used by AbilitySlot, PlayerAbilityData, etc.) + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Resolve an ability by key. Checks built-in abilities first (by name), + * then custom abilities (by UUID). Returns a copy safe to modify. + * + * @param key The ability name (built-in) or UUID (custom) + * @return A copy of the resolved ability, or null if not found + */ + public Ability resolveAbility(String key) { + if (key == null || key.isEmpty()) return null; + + // Check built-in abilities first + Ability builtIn = abilities.get(key); + if (builtIn != null) { + return fromNBT(builtIn.writeNBT()); + } + + // Check custom abilities + Ability custom = customAbilities.get(key); + if (custom != null) { + return fromNBT(custom.writeNBT()); + } + + return null; } /** - * Check if a saved ability exists. + * Check if an ability key exists (either built-in or custom). */ - public boolean hasSavedAbility(String name) { - return savedAbilities.containsKey(name); + public boolean hasAbility(String key) { + return abilities.containsKey(key) || customAbilities.containsKey(key); } - /** - * Get all saved ability names. - */ - public Set getSavedAbilityNames() { - return new LinkedHashSet<>(savedAbilities.keySet()); + // ═══════════════════════════════════════════════════════════════════════════ + // ACCESSORS + // ═══════════════════════════════════════════════════════════════════════════ + + /** Get a built-in ability by name. Returns the stored instance (not a copy). */ + public Ability getAbility(String name) { + return abilities.get(name); } - /** - * Get all saved abilities. - */ - public Collection getSavedAbilities() { - return savedAbilities.values(); + /** Get all built-in ability names. */ + public Set getAbilityNames() { + return new LinkedHashSet<>(abilities.keySet()); + } + + /** Get a custom ability by UUID. Returns the stored instance (not a copy). */ + public Ability getCustomAbility(String uuid) { + return customAbilities.get(uuid); + } + + /** Check if a custom ability exists by UUID. */ + public boolean hasCustomAbility(String uuid) { + return customAbilities.containsKey(uuid); + } + + /** Get all custom ability UUIDs. */ + public Set getCustomAbilityIds() { + return new LinkedHashSet<>(customAbilities.keySet()); + } + + /** Get the display name for a custom ability by UUID. */ + public String getCustomAbilityName(String uuid) { + Ability a = customAbilities.get(uuid); + return a != null ? a.getName() : null; + } + + /** Get the custom abilities map (used by SyncController). */ + public Map getCustomAbilities() { + return customAbilities; + } + + /** Replace the custom abilities map (used by client-side sync). */ + public void setCustomAbilities(Map synced) { + customAbilities.clear(); + customAbilities.putAll(synced); + version++; + } + + /** Get the version counter (incremented on custom ability changes). */ + public int getVersion() { + return version; } // ═══════════════════════════════════════════════════════════════════════════ @@ -195,28 +280,37 @@ public class AbilityController implements IAbilityHandler { @Override public String[] getTypes() { - // Return the ability typeIds (lang keys), not the factory keys - return factoriesByTypeId.keySet().toArray(new String[0]); + return typeFactories.keySet().toArray(new String[0]); } @Override public boolean hasType(String typeId) { - return factories.containsKey(typeId) || factoriesByTypeId.containsKey(typeId); + return typeFactories.containsKey(typeId); } @Override - public String[] getSavedNames() { - return savedAbilities.keySet().toArray(new String[0]); + public String[] getAbilityNameArray() { + return abilities.keySet().toArray(new String[0]); } @Override - public boolean has(String name) { - return savedAbilities.containsKey(name); + public boolean hasAbilityName(String name) { + return abilities.containsKey(name); } @Override - public boolean delete(String name) { - return deleteAbility(name); + public String[] getCustomAbilityIdArray() { + return customAbilities.keySet().toArray(new String[0]); + } + + @Override + public boolean hasCustomAbilityId(String uuid) { + return customAbilities.containsKey(uuid); + } + + @Override + public boolean deleteCustomAbilityById(String uuid) { + return deleteCustomAbility(uuid); } // ═══════════════════════════════════════════════════════════════════════════ @@ -225,26 +319,23 @@ public class AbilityController implements IAbilityHandler { /** * Register a new ability type. - * Call during FMLInitializationEvent. + * Extracts the typeId from a temporary instance and stores in typeFactories. * - * @param factoryKey The factory key (e.g., "cnpc:slam", "mymod:custom_ability") + * @param factoryKey Unused legacy key — the typeId from the created instance is used * @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); + String typeId = temp.getTypeId(); + if (typeFactories.containsKey(typeId)) { + LogWriter.info("AbilityController: Overwriting type: " + typeId); + } + typeFactories.put(typeId, factory); } /** * Create an ability from NBT data. - * Uses the "typeId" field to determine which type to create. + * Uses the "typeId" field to look up in typeFactories. */ public Ability fromNBT(NBTTagCompound nbt) { if (nbt == null || !nbt.hasKey("typeId")) { @@ -252,11 +343,7 @@ public class AbilityController implements IAbilityHandler { } 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); - } + Supplier factory = typeFactories.get(typeId); if (factory == null) { LogWriter.info("AbilityController: Unknown ability type: " + typeId); return null; @@ -270,15 +357,10 @@ public class AbilityController implements IAbilityHandler { /** * 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") + * @param typeId 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); - } + public Ability create(String typeId) { + Supplier factory = typeFactories.get(typeId); if (factory == null) { return null; } @@ -288,7 +370,7 @@ public class AbilityController implements IAbilityHandler { /** * Register built-in ability types. */ - private void registerBuiltins() { + private void registerBuiltinTypes() { // Melee/AOE damage abilities registerType("cnpc:slam", AbilitySlam::new); registerType("cnpc:heavy_hit", AbilityHeavyHit::new); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java new file mode 100644 index 000000000..ca4e7471e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java @@ -0,0 +1,124 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Wrapper that stores either an inline ability (full NBT) or a reference + * to a built-in/custom ability (by name or UUID). + *

+ * References are lazy-resolved via {@link AbilityController#resolveAbility(String)} + * and cached until the controller version changes. + */ +public class AbilitySlot { + + private Ability inlineAbility; + private String referenceId; + + private transient Ability cachedAbility; + private transient int cachedVersion = -1; + + private AbilitySlot() {} + + /** + * Create a slot with an inline (embedded) ability. + */ + public static AbilitySlot inline(Ability ability) { + AbilitySlot slot = new AbilitySlot(); + slot.inlineAbility = ability; + slot.referenceId = null; + return slot; + } + + /** + * Create a slot that references a built-in ability (by name) or custom ability (by UUID). + */ + public static AbilitySlot reference(String referenceId) { + AbilitySlot slot = new AbilitySlot(); + slot.inlineAbility = null; + slot.referenceId = referenceId; + return slot; + } + + public boolean isReference() { + return referenceId != null; + } + + public String getReferenceId() { + return referenceId; + } + + /** + * Get the resolved ability. For inline slots, returns the ability directly. + * For reference slots, lazily resolves from AbilityController and caches. + * Returns null if the reference cannot be resolved (broken ref). + */ + public Ability getAbility() { + if (!isReference()) { + return inlineAbility; + } + + // Lazy resolve with version-based cache invalidation + AbilityController controller = AbilityController.Instance; + if (controller == null) return null; + + int currentVersion = controller.getVersion(); + if (cachedAbility != null && cachedVersion == currentVersion) { + return cachedAbility; + } + + cachedAbility = controller.resolveAbility(referenceId); + cachedVersion = currentVersion; + return cachedAbility; + } + + /** + * Get the inline ability directly (null for reference slots). + */ + public Ability getInlineAbility() { + return inlineAbility; + } + + /** + * Convert this reference slot to an inline slot by copying the resolved ability. + * No-op if already inline. Returns false if resolution fails. + */ + public boolean convertToInline() { + if (!isReference()) return true; + + Ability resolved = getAbility(); + if (resolved == null) return false; + + // Deep copy via NBT round-trip + inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT()); + referenceId = null; + cachedAbility = null; + cachedVersion = -1; + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + public NBTTagCompound writeNBT() { + if (isReference()) { + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setString("Reference", referenceId); + return nbt; + } + return inlineAbility != null ? inlineAbility.writeNBT() : new NBTTagCompound(); + } + + public static AbilitySlot fromNBT(NBTTagCompound nbt) { + if (nbt == null) return null; + + if (nbt.hasKey("Reference")) { + return reference(nbt.getString("Reference")); + } + + // Inline ability + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability == null) return null; + return inline(ability); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java index 9bf408c27..01100c318 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java @@ -5,40 +5,33 @@ package kamkeel.npcs.controllers.data.ability; * Determines where the charging visual (orb, disc, etc.) appears relative to the entity. */ public enum AnchorPoint { - FRONT(0, "Front"), // In front of entity face (default) - CENTER(1, "Center"), // Entity center - RIGHT_HAND(2, "Right Hand"), // Right arm/hand position - LEFT_HAND(3, "Left Hand"), // Left arm/hand position - ABOVE_HEAD(4, "Above Head"), // Above the entity - CHEST(5, "Chest"); // Chest level, centered + FRONT("Front"), // In front of entity face (default) + CENTER("Center"), // Entity center + RIGHT_HAND("Right Hand"), // Right arm/hand position + LEFT_HAND("Left Hand"), // Left arm/hand position + ABOVE_HEAD("Above Head"), // Above the entity + CHEST("Chest"); // Chest level, centered - private final int id; private final String displayName; - AnchorPoint(int id, String displayName) { - this.id = id; + AnchorPoint(String displayName) { this.displayName = displayName; } - public int getId() { - return id; - } - public String getDisplayName() { return displayName; } /** - * Get AnchorPoint from NBT id value. - * Returns FRONT if id is invalid. + * Get AnchorPoint from ordinal value. + * Returns FRONT if ordinal is out of bounds. */ - public static AnchorPoint fromId(int id) { - for (AnchorPoint anchor : values()) { - if (anchor.id == id) { - return anchor; - } + public static AnchorPoint fromOrdinal(int ordinal) { + AnchorPoint[] vals = values(); + if (ordinal < 0 || ordinal >= vals.length) { + return FRONT; } - return FRONT; + return vals[ordinal]; } /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java new file mode 100644 index 000000000..69151291f --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java @@ -0,0 +1,63 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Abstract base class for built-in (non-customizable) abilities. + * Built-in abilities are registered by key, have fixed configurations, + * and cannot be modified through the preset GUI system. + *

+ * Examples: simple utility abilities like Jump, Sprint, etc. + * Third-party mods can extend this class to create their own built-in abilities. + */ +public abstract class BuiltInAbility extends Ability { + + /** + * The registry key used to look up this ability (e.g., "cnpc:jump", "dbc:ki_blast"). + */ + private final String registryKey; + + public BuiltInAbility(String registryKey) { + this.registryKey = registryKey; + this.id = registryKey; + this.name = registryKey; + this.typeId = registryKey; + } + + /** + * Get the registry key for this built-in ability. + */ + public String getRegistryKey() { + return registryKey; + } + + /** + * Built-in abilities are not customizable. + */ + @Override + public boolean isCustomizable() { + return false; + } + + /** + * Built-in abilities do not write type-specific NBT (they have fixed configs). + */ + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + // No-op: built-in abilities have fixed configurations + } + + /** + * Built-in abilities do not read type-specific NBT. + */ + @Override + public void readTypeNBT(NBTTagCompound nbt) { + // No-op: built-in abilities have fixed configurations + } + + /** + * Get the duration of the active phase in ticks. + * Built-in abilities must define their own active duration. + */ + public abstract int getActiveDurationTicks(); +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java index 8d078421a..1183fe858 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java @@ -13,11 +13,19 @@ 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) + * @param caster The entity that would use the ability (NPC or Player) + * @param target The current target (may be null for self-targeting abilities or players) * @return true if the condition is satisfied */ - boolean check(EntityNPCInterface npc, EntityLivingBase target); + boolean check(EntityLivingBase caster, EntityLivingBase target); + + /** + * Whether this condition requires a target entity to evaluate. + * Conditions that return true here will be skipped for player ability checks. + */ + default boolean requiresTarget() { + return false; + } /** * Get the condition type ID for serialization. @@ -75,7 +83,7 @@ public interface Condition { // ═══════════════════════════════════════════════════════════════════ /** - * Condition: NPC HP is above a percentage threshold. + * Condition: Caster HP is above a percentage threshold. */ class ConditionHPAbove implements Condition { private float threshold = 0.5f; // 50% @@ -88,8 +96,8 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { - return npc.getHealth() / npc.getMaxHealth() > threshold; + public boolean check(EntityLivingBase caster, EntityLivingBase target) { + return caster.getHealth() / caster.getMaxHealth() > threshold; } @Override @@ -112,7 +120,7 @@ public interface Condition { } /** - * Condition: NPC HP is below a percentage threshold. + * Condition: Caster HP is below a percentage threshold. */ class ConditionHPBelow implements Condition { private float threshold = 0.5f; @@ -125,8 +133,8 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { - return npc.getHealth() / npc.getMaxHealth() < threshold; + public boolean check(EntityLivingBase caster, EntityLivingBase target) { + return caster.getHealth() / caster.getMaxHealth() < threshold; } @Override @@ -150,6 +158,7 @@ public interface Condition { /** * Condition: Target HP is above a percentage threshold. + * Requires a target - will be skipped for player ability checks. */ class ConditionTargetHPAbove implements Condition { private float threshold = 0.5f; @@ -162,11 +171,16 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { if (target == null) return false; return target.getHealth() / target.getMaxHealth() > threshold; } + @Override + public boolean requiresTarget() { + return true; + } + @Override public String getTypeId() { return "target_hp_above"; @@ -188,6 +202,7 @@ public interface Condition { /** * Condition: Target HP is below a percentage threshold. + * Requires a target - will be skipped for player ability checks. */ class ConditionTargetHPBelow implements Condition { private float threshold = 0.5f; @@ -200,11 +215,16 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { if (target == null) return false; return target.getHealth() / target.getMaxHealth() < threshold; } + @Override + public boolean requiresTarget() { + return true; + } + @Override public String getTypeId() { return "target_hp_below"; @@ -225,8 +245,9 @@ public interface Condition { } /** - * Condition: NPC has been hit X times within a time window. + * Condition: Caster has been hit X times within a time window. * Useful for reactive abilities that trigger after receiving multiple hits. + * Currently only works for NPCs (requires DataAbilities hit tracking). */ class ConditionHitCount implements Condition { private int requiredHits = 3; @@ -241,11 +262,15 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { // Check NPC's recent hit count from DataAbilities - if (npc.abilities != null) { - return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits; + if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + if (npc.abilities != null) { + return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits; + } } + // For players, hit count tracking is not yet supported return false; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java new file mode 100644 index 000000000..d68693f59 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java @@ -0,0 +1,26 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines which entity types can use an ability. + */ +public enum UserType { + NPC_ONLY, + PLAYER_ONLY, + BOTH; + + public boolean allowsNpc() { + return this == NPC_ONLY || this == BOTH; + } + + public boolean allowsPlayer() { + return this == PLAYER_ONLY || this == BOTH; + } + + public static UserType fromOrdinal(int ordinal) { + UserType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return BOTH; + } +} 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 index 25941e0a5..37d88e1f7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -88,106 +88,106 @@ public class AbilityCharge extends Ability implements IAbilityCharge { * 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) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (tick == 1) { // Lock direction on first windup tick (same time telegraph is created) - lockChargeDirection(npc, target); + lockChargeDirection(caster, target); } - // Keep NPC facing the locked direction during windup - enforceLockedRotation(npc); + // Keep caster facing the locked direction during windup + enforceLockedRotation(caster); } /** * 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) { + private void lockChargeDirection(EntityLivingBase caster, EntityLivingBase target) { if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dz * dz); if (len > 0) { chargeDirection = Vec3.createVectorHelper(dx / len, 0, dz / len); } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); + float yaw = (float) Math.toRadians(caster.rotationYaw); chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); } } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); + float yaw = (float) Math.toRadians(caster.rotationYaw); chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); } lockedYaw = (float) Math.toDegrees(Math.atan2(-chargeDirection.xCoord, chargeDirection.zCoord)); } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Initialize charge - direction was already locked during windup - startX = npc.posX; - startY = npc.posY; - startZ = npc.posZ; + startX = caster.posX; + startY = caster.posY; + startZ = caster.posZ; hitEntities.clear(); // If direction wasn't set during windup (shouldn't happen), set it now if (chargeDirection == null) { - lockChargeDirection(npc, target); + lockChargeDirection(caster, target); } - enforceLockedRotation(npc); + enforceLockedRotation(caster); } - private void enforceLockedRotation(EntityNPCInterface npc) { - npc.rotationYaw = lockedYaw; - npc.rotationYawHead = lockedYaw; - npc.prevRotationYaw = lockedYaw; - npc.prevRotationYawHead = lockedYaw; - npc.renderYawOffset = lockedYaw; - npc.prevRenderYawOffset = lockedYaw; + private void enforceLockedRotation(EntityLivingBase caster) { + caster.rotationYaw = lockedYaw; + caster.rotationYawHead = lockedYaw; + caster.prevRotationYaw = lockedYaw; + caster.prevRotationYawHead = lockedYaw; + caster.renderYawOffset = lockedYaw; + caster.prevRenderYawOffset = lockedYaw; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (chargeDirection == null) return; // Enforce rotation every tick - enforceLockedRotation(npc); + enforceLockedRotation(caster); // Calculate distance traveled double distanceTraveled = Math.sqrt( - Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(caster.posX - startX, 2) + + Math.pow(caster.posZ - startZ, 2) ); // Check if reached max distance if (distanceTraveled >= maxRange) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; signalCompletion(); return; } - if (isChargeBlocked(npc)) { - stopMomentum(npc); + if (isChargeBlocked(caster)) { + stopMomentum(caster); signalCompletion(); return; } - // Move NPC - npc.motionX = chargeDirection.xCoord * chargeSpeed; - npc.motionY = 0; - npc.motionZ = chargeDirection.zCoord * chargeSpeed; - npc.velocityChanged = true; + // Move caster + caster.motionX = chargeDirection.xCoord * chargeSpeed; + caster.motionY = 0; + caster.motionZ = chargeDirection.zCoord * chargeSpeed; + caster.velocityChanged = true; // Server-side collision damage if (!world.isRemote) { - AxisAlignedBB hitBox = npc.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth); + AxisAlignedBB hitBox = caster.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 (entity == caster) continue; if (hitEntities.contains(entity.getEntityId())) continue; EntityLivingBase livingEntity = (EntityLivingBase) entity; @@ -196,7 +196,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge { hitEntities.add(entity.getEntityId()); // Apply damage with scripted event support - boolean wasHit = applyAbilityDamageWithDirection(npc, livingEntity, damage, knockback, + boolean wasHit = applyAbilityDamageWithDirection(caster, livingEntity, damage, knockback, chargeDirection.xCoord, chargeDirection.zCoord); // Play impact sound if hit wasn't cancelled @@ -208,28 +208,28 @@ public class AbilityCharge extends Ability implements IAbilityCharge { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - stopMomentum(npc); - super.onComplete(npc, target); + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + stopMomentum(caster); + super.onComplete(caster, target); } @Override - public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) { - stopMomentum(npc); - super.onInterrupt(npc, source, damage); + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + stopMomentum(caster); + super.onInterrupt(caster, source, damage); } - private void stopMomentum(EntityNPCInterface npc) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + private void stopMomentum(EntityLivingBase caster) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; } - private boolean isChargeBlocked(EntityNPCInterface npc) { + private boolean isChargeBlocked(EntityLivingBase caster) { 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(); + AxisAlignedBB nextBox = caster.boundingBox.copy().offset(nextX, 0, nextZ); + return !caster.worldObj.getCollidingBoundingBoxes(caster, nextBox).isEmpty(); } @Override @@ -250,12 +250,12 @@ public class AbilityCharge extends Ability implements IAbilityCharge { } /** - * Creates a LINE telegraph from NPC position towards target. - * Telegraph follows NPC during windup (so NPC can reposition before charging). + * Creates a LINE telegraph from caster position towards target. + * Telegraph follows caster during windup (so caster can reposition before charging). * Direction is locked at creation based on target position. */ @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { return null; } @@ -263,11 +263,11 @@ public class AbilityCharge extends Ability implements IAbilityCharge { // 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; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; yaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); } else { - yaw = npc.rotationYaw; + yaw = caster.rotationYaw; } // Create LINE telegraph @@ -278,12 +278,12 @@ public class AbilityCharge extends Ability implements IAbilityCharge { 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()); + // Position at caster ground level, direction towards target + double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, yaw); + instance.setCasterEntityId(caster.getEntityId()); + // Telegraph follows caster during windup - allows caster to reposition + instance.setEntityIdToFollow(caster.getEntityId()); return instance; } 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 index 9db9d7700..ef2d2f687 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -100,13 +100,13 @@ public class AbilityCutter extends Ability implements IAbilityCutter { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { hitEntities.clear(); currentRotation = -arcAngle / 2.0f; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; switch (sweepMode) { @@ -115,7 +115,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter { signalCompletion(); // Swipe arc complete return; } - performSweepDamage(npc, world, innerRadius, range, currentRotation); + performSweepDamage(caster, world, innerRadius, range, currentRotation); currentRotation += sweepSpeed; break; @@ -126,30 +126,30 @@ public class AbilityCutter extends Ability implements IAbilityCutter { } currentRotation = (currentRotation + sweepSpeed) % 360.0f; hitEntities.clear(); - performSweepDamage(npc, world, innerRadius, range, currentRotation); + performSweepDamage(caster, world, innerRadius, range, currentRotation); break; } } - private void performSweepDamage(EntityNPCInterface npc, World world, float minDist, float maxDist, float angleOffset) { - float casterYaw = npc.rotationYaw + angleOffset; + private void performSweepDamage(EntityLivingBase caster, World world, float minDist, float maxDist, float angleOffset) { + float casterYaw = caster.rotationYaw + angleOffset; AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( - npc.posX - maxDist, npc.posY - 1, npc.posZ - maxDist, - npc.posX + maxDist, npc.posY + 3, npc.posZ + maxDist + caster.posX - maxDist, caster.posY - 1, caster.posZ - maxDist, + caster.posX + maxDist, caster.posY + 3, caster.posZ + maxDist ); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) 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 dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; double dist = Math.sqrt(dx * dx + dz * dz); if (dist < minDist || dist > maxDist) continue; @@ -161,7 +161,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter { float actualDamage = damage * distFactor; // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, actualDamage, knockback); + boolean wasHit = applyAbilityDamage(caster, entity, actualDamage, knockback); // Apply effects if hit wasn't cancelled if (wasHit) { @@ -183,13 +183,13 @@ public class AbilityCutter extends Ability implements IAbilityCutter { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { hitEntities.clear(); currentRotation = 0.0f; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { hitEntities.clear(); currentRotation = 0.0f; } 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 index 054176a7b..e070b099e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -8,6 +8,8 @@ import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -127,10 +129,10 @@ public class AbilityDash extends Ability implements IAbilityDash { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - startX = npc.posX; - startY = npc.posY; - startZ = npc.posZ; + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + startX = caster.posX; + startY = caster.posY; + startZ = caster.posZ; // Choose random direction based on mode DashDirection[] directions = dashMode == DashMode.AGGRESSIVE @@ -141,11 +143,11 @@ public class AbilityDash extends Ability implements IAbilityDash { // Calculate dash direction relative to target float baseYaw; if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); } else { - baseYaw = npc.rotationYaw; + baseYaw = caster.rotationYaw; } // Apply direction offset @@ -161,54 +163,54 @@ public class AbilityDash extends Ability implements IAbilityDash { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, 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) + Math.pow(caster.posX - startX, 2) + + Math.pow(caster.posZ - startZ, 2) ); // Check if reached max distance if (distanceTraveled >= dashDistance) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; signalCompletion(); return; } - if (isDashBlocked(npc)) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + if (isDashBlocked(caster)) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; signalCompletion(); return; } - // Move NPC - npc.motionX = dashDirection.xCoord * dashSpeed; - npc.motionY = 0; - npc.motionZ = dashDirection.zCoord * dashSpeed; - npc.velocityChanged = true; + // Move caster + caster.motionX = dashDirection.xCoord * dashSpeed; + caster.motionY = 0; + caster.motionZ = dashDirection.zCoord * dashSpeed; + caster.velocityChanged = true; // Trail particles - world.spawnParticle("smoke", npc.posX, npc.posY + 0.5, npc.posZ, 0, 0, 0); + world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0); } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; } @Override - public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; } @Override @@ -217,11 +219,11 @@ public class AbilityDash extends Ability implements IAbilityDash { chosenDirection = null; } - private boolean isDashBlocked(EntityNPCInterface npc) { + private boolean isDashBlocked(EntityLivingBase caster) { if (dashDirection == null) return true; double nextX = dashDirection.xCoord * dashSpeed; double nextZ = dashDirection.zCoord * dashSpeed; - return !npc.worldObj.getCollidingBoundingBoxes(npc, npc.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); + return !caster.worldObj.getCollidingBoundingBoxes(caster, caster.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); } // ==================== PREVIEW MODE ==================== diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 3ef2e164d..74ce14e34 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -86,7 +86,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); return; @@ -102,15 +102,15 @@ public class AbilityDisc extends Ability implements IAbilityDisc { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; // Spawn disc in charging mode on first tick of windup if (tick == 1) { - // Create disc in charging mode - follows NPC based on anchor point during windup - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorData); + // Create disc in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); discEntity = new EntityAbilityDisc( - world, npc, target, + world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, discRadius, discThickness, colorData, combatData, homingData, lightningData, lifespanData, @@ -123,7 +123,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Signal completion when entity dies if (discEntity == null || discEntity.isDead) { discEntity = null; @@ -132,7 +132,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } @Override @@ -145,7 +145,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { return null; } @@ -159,8 +159,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { telegraph.setHeightOffset(telegraphHeightOffset); // Position at target and follow target during windup - TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); instance.setEntityIdToFollow(target.getEntityId()); return instance; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 53377161a..59e84f8f0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -83,7 +83,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); return; @@ -99,17 +99,17 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; // Spawn beam in charging mode on first tick of windup if (tick == 1) { float offsetDist = 1.0f; - // Create beam in charging mode - follows NPC based on anchor point during windup - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorData, offsetDist); + // Create beam in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist); beamEntity = new EntityAbilityBeam( - world, npc, target, + world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, beamWidth, headSize, colorData, combatData, homingData, lightningData, lifespanData, @@ -122,7 +122,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Signal completion when entity dies if (beamEntity == null || beamEntity.isDead) { beamEntity = null; @@ -131,7 +131,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } @Override @@ -144,7 +144,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { return null; } @@ -158,8 +158,8 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { telegraph.setHeightOffset(telegraphHeightOffset); // Position at target and follow target during windup - TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); instance.setEntityIdToFollow(target.getEntityId()); return instance; 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 index 92c7ca58e..223a1180c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -60,6 +61,7 @@ public class AbilityGuard extends Ability implements IAbilityGuard { this.showTelegraph = false; this.windUpSound = "random.anvil_use"; this.activeSound = "random.anvil_land"; + this.allowedBy = UserType.NPC_ONLY; } @Override @@ -85,17 +87,17 @@ public class AbilityGuard extends Ability implements IAbilityGuard { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { lastAttacker = null; counterTriggered = false; lastDamageTaken = 0.0f; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Counter attack logic if triggered if (canCounter && counterTriggered && lastAttacker != null && !world.isRemote) { - performCounter(npc, world); + performCounter(caster, world); } // Check if guard duration has ended @@ -105,13 +107,13 @@ public class AbilityGuard extends Ability implements IAbilityGuard { } /** - * Called externally when the NPC takes damage while guarding. + * Called externally when the caster 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) { + public void onDamageTaken(EntityLivingBase caster, EntityLivingBase attacker, DamageSource source, float damage) { if (!canCounter || attacker == null) return; if (!isDirectHit(source)) return; if (RANDOM.nextFloat() >= counterChance) return; @@ -122,17 +124,19 @@ public class AbilityGuard extends Ability implements IAbilityGuard { // Counter will be performed in onActiveTick - don't self-interrupt } - private void performCounter(EntityNPCInterface npc, World world) { + private void performCounter(EntityLivingBase caster, World world) { float counterDamage = counterType == CounterType.PERCENT ? lastDamageTaken * (counterValue / 100.0f) : counterValue; - boolean wasHit = applyAbilityDamage(npc, lastAttacker, counterDamage, 0.5f); + boolean wasHit = applyAbilityDamage(caster, lastAttacker, counterDamage, 0.5f); if (wasHit && counterSound != null && !counterSound.isEmpty()) { - world.playSoundAtEntity(npc, counterSound, 1.0f, 1.2f); + world.playSoundAtEntity(caster, counterSound, 1.0f, 1.2f); + } + // Use shared animation utility - only available for NPCs + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).abilities.playAbilityAnimation(counterAnimationId); } - // Use shared animation utility - npc.abilities.playAbilityAnimation(counterAnimationId); counterTriggered = false; lastAttacker = null; @@ -157,7 +161,7 @@ public class AbilityGuard extends Ability implements IAbilityGuard { } /** - * Check if the NPC is currently in guard stance. + * Check if the caster is currently in guard stance. */ public boolean isGuarding() { return isExecuting() && getPhase() == kamkeel.npcs.controllers.data.ability.AbilityPhase.ACTIVE; 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 index 61841ded4..0700b1f01 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -118,8 +118,8 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(npc, target); + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + TelegraphInstance instance = super.createTelegraph(caster, target); if (instance == null) return null; // Control telegraph following based on placement mode @@ -128,9 +128,9 @@ public class AbilityHazard extends Ability implements IAbilityHazard { 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); + instance.setX(caster.posX); + instance.setY(caster.posY); + instance.setZ(caster.posZ); break; case AT_TARGET: // Telegraph follows target during windup, locks on execute @@ -149,14 +149,14 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, 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; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; positionLocked = true; break; case AT_TARGET: @@ -172,7 +172,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Lock position from telegraph (which was following target) or calculate it now TelegraphInstance telegraph = getTelegraphInstance(); @@ -180,9 +180,9 @@ public class AbilityHazard extends Ability implements IAbilityHazard { case AT_CASTER: // Already locked during windup if (!positionLocked) { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; case AT_TARGET: @@ -204,15 +204,15 @@ public class AbilityHazard extends Ability implements IAbilityHazard { zoneY = pos[1]; zoneZ = pos[2]; } else { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; case FOLLOW_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; break; case FOLLOW_TARGET: if (target != null) { @@ -220,9 +220,9 @@ public class AbilityHazard extends Ability implements IAbilityHazard { zoneY = target.posY; zoneZ = target.posZ; } else { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; } @@ -231,7 +231,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; // Check if hazard duration has ended @@ -245,9 +245,9 @@ public class AbilityHazard extends Ability implements IAbilityHazard { switch (placement) { case FOLLOW_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; break; case FOLLOW_TARGET: if (target != null) { @@ -272,16 +272,16 @@ public class AbilityHazard extends Ability implements IAbilityHazard { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase entity : entities) { - if (entity == npc && !affectsCaster) continue; + if (entity == caster && !affectsCaster) continue; if (damagedThisTick.contains(entity.getEntityId())) continue; - if (!isInZone(entity, npc)) continue; + if (!isInZone(entity, caster)) continue; if (damagePerSecond > 0) { if (ignoreInvulnFrames) { entity.hurtResistantTime = 0; } // Apply damage with scripted event support (no knockback for hazard) - boolean wasHit = applyAbilityDamage(npc, entity, damagePerSecond, 0); + boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0); if (!wasHit) continue; // Skip debuffs if hit was cancelled } @@ -291,7 +291,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } } - private boolean isInZone(EntityLivingBase entity, EntityNPCInterface npc) { + private boolean isInZone(EntityLivingBase entity, EntityLivingBase caster) { double dx = entity.posX - zoneX; double dz = entity.posZ - zoneZ; double dist = Math.sqrt(dx * dx + dz * dz); @@ -303,7 +303,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { return dist >= innerRadius && dist <= radius; case CONE: if (dist > radius) return false; - float casterYaw = npc.rotationYaw; + float casterYaw = caster.rotationYaw; double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz)); double angleDiff = Math.abs(normalizeAngle(angleToEntity - casterYaw)); return angleDiff <= coneAngle / 2; @@ -337,13 +337,13 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { damagedThisTick.clear(); positionLocked = false; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { damagedThisTick.clear(); positionLocked = false; } 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 index 406be2f3a..898d54f57 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -80,21 +80,21 @@ public class AbilityHeal extends Ability implements IAbilityHeal { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) return; getHealedAllies().clear(); // Always find allies if we're healing them (needed for both instant and HoT) if (healAllies && healRadius > 0) { - findAlliesInRadius(npc, world); + findAlliesInRadius(caster, world); } if (instantHeal) { // Instant heal - apply all healing now if (healSelf) { - healEntity(npc); - spawnHealParticles(world, npc); + healEntity(caster); + spawnHealParticles(world, caster); } for (EntityLivingBase ally : getHealedAllies()) { @@ -109,7 +109,7 @@ public class AbilityHeal extends Ability implements IAbilityHeal { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote || instantHeal) return; // Heal over time - distribute heal across duration ticks @@ -121,11 +121,11 @@ public class AbilityHeal extends Ability implements IAbilityHeal { float selfTickHeal = tickHeal; // Add percentage-based heal portion if (healPercent > 0) { - selfTickHeal += (npc.getMaxHealth() * healPercent / (float) durationTicks) * 10; + selfTickHeal += (caster.getMaxHealth() * healPercent / (float) durationTicks) * 10; } - npc.heal(selfTickHeal); + caster.heal(selfTickHeal); if (tick % 20 == 0) { - spawnHealParticles(world, npc); + spawnHealParticles(world, caster); } } @@ -155,10 +155,10 @@ public class AbilityHeal extends Ability implements IAbilityHeal { /** * Find all allies in radius and add them to healedAllies list. */ - private void findAlliesInRadius(EntityNPCInterface npc, World world) { + private void findAlliesInRadius(EntityLivingBase caster, World world) { AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( - npc.posX - healRadius, npc.posY - 2, npc.posZ - healRadius, - npc.posX + healRadius, npc.posY + 3, npc.posZ + healRadius + caster.posX - healRadius, caster.posY - 2, caster.posZ - healRadius, + caster.posX + healRadius, caster.posY + 3, caster.posZ + healRadius ); @SuppressWarnings("unchecked") @@ -166,13 +166,13 @@ public class AbilityHeal extends Ability implements IAbilityHeal { for (Entity entity : entities) { if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; + if (entity == caster) continue; EntityLivingBase living = (EntityLivingBase) entity; // Only heal other NPCs (allies) if (living instanceof EntityNPCInterface) { - float dist = npc.getDistanceToEntity(living); + float dist = caster.getDistanceToEntity(living); if (dist <= healRadius) { getHealedAllies().add(living); } 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 index 4ed4a0e78..7d0c217c0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -62,14 +62,14 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote || target == null) { signalCompletion(); return; } // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, target, damage, knockback); + boolean wasHit = applyAbilityDamage(caster, target, damage, knockback); // Apply effects if hit wasn't cancelled if (wasHit) { @@ -81,7 +81,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Heavy Hit is instant, no active tick needed } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 3b45894b4..acf5f0aa4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -90,18 +90,18 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); return; } - double spawnX = npc.posX; - double spawnY = npc.posY + npc.getEyeHeight() * 0.8; - double spawnZ = npc.posZ; + double spawnX = caster.posX; + double spawnY = caster.posY + caster.getEyeHeight(); + double spawnZ = caster.posZ; laserEntity = new EntityAbilityLaser( - world, npc, target, + world, caster, target, spawnX, spawnY, spawnZ, laserWidth, colorData, combatData, lightningData, lifespanData, @@ -116,7 +116,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Signal completion when entity dies if (laserEntity == null || laserEntity.isDead) { laserEntity = null; @@ -125,11 +125,11 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { } @Override 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 index 78936adbf..b489f4b9d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -82,7 +82,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); return; @@ -98,15 +98,15 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; // Spawn orb in charging mode on first tick of windup if (tick == 1) { - // Create orb in charging mode - follows NPC based on anchor point during windup - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorData); + // Create orb in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); orbEntity = new EntityAbilityOrb( - world, npc, target, + world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, colorData, combatData, homingData, lightningData, lifespanData); orbEntity.setupCharging(anchorData, windUpTicks); @@ -117,7 +117,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Signal completion when entity dies if (orbEntity == null || orbEntity.isDead) { orbEntity = null; @@ -126,7 +126,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { // Nothing to clean up - entity manages itself } @@ -140,7 +140,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { return null; } @@ -154,8 +154,8 @@ public class AbilityOrb extends Ability implements IAbilityOrb { telegraph.setHeightOffset(telegraphHeightOffset); // Position at target and follow target during windup - TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); instance.setEntityIdToFollow(target.getEntityId()); return instance; 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 index 6b81fabe7..c465bc0a5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -13,8 +13,6 @@ import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityProjectile; import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.entity.EntityNPCInterface; - import noppes.npcs.api.ability.type.IAbilityProjectile; import java.util.List; @@ -71,13 +69,13 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, 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 dx = target.posX - caster.posX; + double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); + double dz = target.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -88,10 +86,10 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { // Deal instant damage with scripted event support // TODO: Use custom EntityAbilityProjectile for actual tracking - applyAbilityDamageWithDirection(npc, target, damage, knockback, dx, dz); + applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); // Play sound - world.playSoundAtEntity(npc, "random.bow", 1.0f, 0.8f); + world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); // Handle splash damage for explosive projectiles if (explosive && explosionRadius > 0) { @@ -100,13 +98,13 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != npc) { + if (entity instanceof EntityLivingBase && entity != caster) { 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); + applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); } } } @@ -123,16 +121,16 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { } // Spawn projectile particles (visual trail) - spawnProjectileParticles(world, npc, target); + spawnProjectileParticles(world, caster, target); // Projectile is instant signalCompletion(); } - private void spawnProjectileParticles(World world, EntityNPCInterface npc, EntityLivingBase target) { - double startX = npc.posX; - double startY = npc.posY + npc.getEyeHeight(); - double startZ = npc.posZ; + private void spawnProjectileParticles(World world, EntityLivingBase caster, EntityLivingBase target) { + double startX = caster.posX; + double startY = caster.posY + caster.getEyeHeight(); + double startZ = caster.posZ; double endX = target.posX; double endY = target.posY + target.height / 2; double endZ = target.posZ; @@ -161,7 +159,7 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Projectile is instant, nothing to do per-tick } 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 index 006084f5e..6416cc0ba 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -71,28 +71,28 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Shockwave is instant - apply effect immediately after windup // Get all entities in radius - AxisAlignedBB box = npc.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); + AxisAlignedBB box = caster.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 == caster) continue; if (entity.isDead) continue; - double dist = npc.getDistanceToEntity(entity); + double dist = caster.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 dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dz * dz); if (len > 0) { @@ -109,7 +109,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { 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); + boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz); // Apply effects if hit connected if (wasHit) { @@ -122,7 +122,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Shockwave is instant - nothing to do per-tick } 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 index 2dae47d05..64e0255ca 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -94,13 +94,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, 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; + // AOE_SELF: slam at caster's current position + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { // AOE_TARGET: telegraph follows target via setEntityIdToFollow targetX = target.posX; @@ -110,13 +110,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, 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; + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { // AOE_TARGET: leap to target's current position targetX = target.posX; @@ -124,9 +124,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam { targetZ = target.posZ; } else { // Fallback: slam in place - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } hasLaunched = false; @@ -134,31 +134,33 @@ public class AbilitySlam extends Ability implements IAbilitySlam { airTicks = 0; // Reset fall distance to prevent fall damage during slam - npc.fallDistance = 0; + caster.fallDistance = 0; // Calculate and apply leap velocity - launchTowardTarget(npc); + launchTowardTarget(caster); } /** - * Calculate ballistic arc and launch NPC toward target. + * Calculate ballistic arc and launch caster 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; + private void launchTowardTarget(EntityLivingBase caster) { + double dx = targetX - caster.posX; + double dy = targetY - caster.posY; + double dz = targetZ - caster.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; + caster.motionX = 0; + caster.motionZ = 0; + caster.motionY = 0.8 * leapSpeed; hasLaunched = true; - npc.setNpcJumpingState(true); - npc.velocityChanged = true; - npc.worldObj.playSoundAtEntity(npc, "mob.irongolem.throw", 0.8f, 0.8f); + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(true); + } + caster.velocityChanged = true; + caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); return; } @@ -168,8 +170,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam { dx *= scale; dz *= scale; horizontalDist = maxRange; - targetX = npc.posX + dx; - targetZ = npc.posZ + dz; + targetX = caster.posX + dx; + targetZ = caster.posZ + dz; } // Choose flight time based on distance - shorter distances = faster @@ -215,96 +217,100 @@ public class AbilitySlam extends Ability implements IAbilitySlam { double dirX = dx / horizontalDist; double dirZ = dz / horizontalDist; - npc.motionX = dirX * vHorizontal; - npc.motionZ = dirZ * vHorizontal; - npc.motionY = vy; + caster.motionX = dirX * vHorizontal; + caster.motionZ = dirZ * vHorizontal; + caster.motionY = vy; hasLaunched = true; - npc.setNpcJumpingState(true); - npc.velocityChanged = true; + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(true); + } + caster.velocityChanged = true; // Face the target float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - npc.rotationYaw = targetYaw; - npc.rotationYawHead = targetYaw; + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, 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; + caster.fallDistance = 0; // Check for landing - if (npc.onGround && airTicks > 3) { + if (caster.onGround && airTicks > 3) { // Landed! - onLanding(npc, world); + onLanding(caster, world); return; } // Timeout protection - force landing after max air time if (airTicks >= maxAirTicks) { - onLanding(npc, world); + onLanding(caster, world); return; } // While in air, face toward target - double dx = targetX - npc.posX; - double dz = targetZ - npc.posZ; + double dx = targetX - caster.posX; + double dz = targetZ - caster.posZ; float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - npc.rotationYaw = targetYaw; - npc.rotationYawHead = targetYaw; + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; } /** - * Called when NPC lands - deal AOE damage and signal completion. + * Called when caster lands - deal AOE damage and signal completion. */ - private void onLanding(EntityNPCInterface npc, World world) { + private void onLanding(EntityLivingBase caster, World world) { hasLanded = true; - npc.setNpcJumpingState(false); + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } // Signal that the ability has completed its active phase signalCompletion(); // Reset fall distance to prevent fall damage on landing - npc.fallDistance = 0; + caster.fallDistance = 0; // Stop horizontal momentum - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; if (world.isRemote) return; // Play slam impact sound on landing - world.playSoundAtEntity(npc, "random.explode", 1.0f, 1.0f); + world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f); // Find all entities in radius @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABBExcludingEntity( - npc, npc.boundingBox.expand(radius, radius / 2, radius)); + caster, caster.boundingBox.expand(radius, radius / 2, radius)); for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != npc) { + if (entity instanceof EntityLivingBase && entity != caster) { 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; + double dx = livingTarget.posX - caster.posX; + double dz = livingTarget.posZ - caster.posZ; if (dx * dx + dz * dz <= radius * radius) { // Apply damage with scripted event support - applyAbilityDamage(npc, livingTarget, damage, knockbackStrength); + applyAbilityDamage(caster, livingTarget, damage, knockbackStrength); } } } // Spawn particles - spawnSlamParticles(world, npc.posX, npc.posY, npc.posZ); + spawnSlamParticles(world, caster.posX, caster.posY, caster.posZ); } /** @@ -344,16 +350,20 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - npc.setNpcJumpingState(false); + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } hasLaunched = false; hasLanded = false; airTicks = 0; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - npc.setNpcJumpingState(false); + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } hasLaunched = false; hasLanded = false; airTicks = 0; @@ -462,7 +472,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { // Check if telegraph should be shown if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { return null; @@ -470,24 +480,24 @@ public class AbilitySlam extends Ability implements IAbilitySlam { // 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; + // AOE_SELF: telegraph at caster position, does NOT follow + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.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; + // Fallback to caster position + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } // Create telegraph at the appropriate position - double groundY = findGroundLevel(npc.worldObj, targetX, targetY, targetZ); + double groundY = findGroundLevel(caster.worldObj, targetX, targetY, targetZ); Telegraph telegraph = Telegraph.circle(radius); telegraph.setDurationTicks(windUpTicks); @@ -496,14 +506,14 @@ public class AbilitySlam extends Ability implements IAbilitySlam { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); + TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, caster.rotationYaw); + instance.setCasterEntityId(caster.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 + // AOE_SELF: telegraph stays at caster position, no follow return instance; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 976d9e44e..d2acc4df3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -18,7 +18,6 @@ import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySweeper; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.api.ability.type.IAbilitySweeper; -import noppes.npcs.entity.EntityNPCInterface; /** * Sweeper ability: A low sweeping beam that rotates around the NPC. @@ -83,14 +82,14 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); return; } // Spawn the entity that handles BOTH visuals AND damage - activeEntity = new EntityAbilitySweeper(world, npc, target, + activeEntity = new EntityAbilitySweeper(world, caster, target, beamLength, beamWidth, beamHeight, colorData, sweepSpeed, numberOfRotations, @@ -103,7 +102,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Signal completion when entity dies if (activeEntity == null || activeEntity.isDead) { activeEntity = null; @@ -129,12 +128,12 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { * Override to position telegraph at NPC (not target) since the sweep is centered on NPC. */ @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE) { return null; } - // Create circle telegraph centered on NPC + // Create circle telegraph centered on caster Telegraph telegraph = Telegraph.circle(beamLength); telegraph.setDurationTicks(windUpTicks); telegraph.setColor(windUpColor); @@ -142,10 +141,10 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - // Position at NPC, not target - TelegraphInstance instance = new TelegraphInstance(telegraph, npc.posX, npc.posY, npc.posZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); - instance.setEntityIdToFollow(npc.getEntityId()); // Follow NPC + // Position at caster, not target + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, caster.posY, caster.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(caster.getEntityId()); // Follow caster return instance; } 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 index 1fef00a55..4e4a5dff7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -95,13 +95,13 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { currentBlink = 0; ticksSinceLastBlink = blinkDelayTicks; // Trigger first blink immediately } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1; if (currentBlink >= blinkLimit) { signalCompletion(); @@ -111,7 +111,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { ticksSinceLastBlink++; if (ticksSinceLastBlink >= blinkDelayTicks) { - performBlink(npc, target, world); + performBlink(caster, target, world); currentBlink++; ticksSinceLastBlink = 0; @@ -122,20 +122,20 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { } } - private void performBlink(EntityNPCInterface npc, EntityLivingBase target, World world) { + private void performBlink(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) return; - double oldX = npc.posX; - double oldY = npc.posY; - double oldZ = npc.posZ; + double oldX = caster.posX; + double oldY = caster.posY; + double oldZ = caster.posZ; - Vec3 destination = calculateDestination(npc, target, world); + Vec3 destination = calculateDestination(caster, 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, + if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ, + destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) { + destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ, destination.xCoord, destination.yCoord, destination.zCoord); if (destination == null) return; } @@ -146,22 +146,22 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { // Damage at origin if (damageAtStart) { - dealDamageAt(npc, world, oldX, oldY, oldZ); + dealDamageAt(caster, 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; + caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); + caster.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); + dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord); } // Face target after teleport @@ -169,8 +169,8 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { 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; + caster.rotationYaw = newYaw; + caster.rotationYawHead = newYaw; } } @@ -186,29 +186,50 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { } } - private Vec3 calculateDestination(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (target == null) return null; - + private Vec3 calculateDestination(EntityLivingBase caster, EntityLivingBase target, World world) { double newX; double newY; double newZ; + if (target != null) { + 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); + } + } + + // No target (player use) - teleport in caster's look direction + Vec3 look = caster.getLookVec(); 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; + // Teleport forward in look direction by behindDistance + newX = caster.posX + look.xCoord * behindDistance; + newZ = caster.posZ + look.zCoord * behindDistance; + newY = caster.posY; return Vec3.createVectorHelper(newX, newY, newZ); case SINGLE: case BLINK: default: - double angle = RANDOM.nextDouble() * Math.PI * 2; + // Teleport forward in look direction within blinkRadius 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; + double offset = (RANDOM.nextDouble() * 0.5 + 0.5) * dist; // 50-100% of max distance + newX = caster.posX + look.xCoord * offset; + newZ = caster.posZ + look.zCoord * offset; + newY = caster.posY; return Vec3.createVectorHelper(newX, newY, newZ); } } @@ -285,7 +306,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { return true; } - private Vec3 findValidPositionAlongLine(World world, EntityNPCInterface npc, + private Vec3 findValidPositionAlongLine(World world, EntityLivingBase caster, double x1, double y1, double z1, double x2, double y2, double z2) { double dx = x2 - x1; @@ -307,8 +328,8 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { double checkY = y1 + dy * d; double checkZ = z1 + dz * d; - if (hasLineOfSight(world, x1, y1 + npc.getEyeHeight(), z1, - checkX, checkY + npc.getEyeHeight(), checkZ)) { + if (hasLineOfSight(world, x1, y1 + caster.getEyeHeight(), z1, + checkX, checkY + caster.getEyeHeight(), checkZ)) { int blockX = MathHelper.floor_double(checkX); int blockY = MathHelper.floor_double(checkY); int blockZ = MathHelper.floor_double(checkZ); @@ -361,7 +382,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { !headBlock.getMaterial().isSolid(); } - private void dealDamageAt(EntityNPCInterface npc, World world, double x, double y, double z) { + private void dealDamageAt(EntityLivingBase caster, World world, double x, double y, double z) { AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( x - damageRadius, y - 1, z - damageRadius, x + damageRadius, y + 2, z + damageRadius @@ -372,13 +393,13 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { for (Entity entity : entities) { if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; + if (entity == caster) 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) - boolean wasHit = applyAbilityDamage(npc, living, damage, 0); + boolean wasHit = applyAbilityDamage(caster, living, damage, 0); if (wasHit) { applyEffects(living); } 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 index 6bf69a60b..a50fe57a1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -100,8 +100,8 @@ public class AbilityTrap extends Ability implements IAbilityTrap { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(npc, target); + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + TelegraphInstance instance = super.createTelegraph(caster, target); if (instance == null) return null; // Control telegraph following based on placement mode @@ -111,14 +111,14 @@ public class AbilityTrap extends Ability implements IAbilityTrap { // 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); + double yaw = Math.toRadians(caster.rotationYaw); + instance.setX(caster.posX - Math.sin(yaw) * placementDistance); + instance.setY(caster.posY); + instance.setZ(caster.posZ + Math.cos(yaw) * placementDistance); } else { - instance.setX(npc.posX); - instance.setY(npc.posY); - instance.setZ(npc.posZ); + instance.setX(caster.posX); + instance.setY(caster.posY); + instance.setZ(caster.posZ); } break; case AT_TARGET: @@ -132,7 +132,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { armed = false; triggerCount = 0; ticksSinceLastTrigger = armTime; @@ -143,9 +143,9 @@ public class AbilityTrap extends Ability implements IAbilityTrap { switch (placement) { case AT_CASTER: - trapX = npc.posX; - trapY = npc.posY; - trapZ = npc.posZ; + trapX = caster.posX; + trapY = caster.posY; + trapZ = caster.posZ; break; case AT_TARGET: // Use telegraph position with offset @@ -166,22 +166,22 @@ public class AbilityTrap extends Ability implements IAbilityTrap { trapY = pos[1]; trapZ = pos[2]; } else { - trapX = npc.posX; - trapY = npc.posY; - trapZ = npc.posZ; + trapX = caster.posX; + trapY = caster.posY; + trapZ = caster.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; + double yaw = Math.toRadians(caster.rotationYaw); + trapX = caster.posX - Math.sin(yaw) * placementDistance; + trapY = caster.posY; + trapZ = caster.posZ + Math.cos(yaw) * placementDistance; break; } } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Check if trap duration has ended if (tick >= durationTicks) { signalCompletion(); @@ -215,7 +215,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (entity.isDead) continue; if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; @@ -224,13 +224,13 @@ public class AbilityTrap extends Ability implements IAbilityTrap { double dist = Math.sqrt(dx * dx + dz * dz); if (dist <= triggerRadius) { - triggerTrap(npc, entity, world); + triggerTrap(caster, entity, world); return; } } } - private void triggerTrap(EntityNPCInterface npc, EntityLivingBase triggerer, World world) { + private void triggerTrap(EntityLivingBase caster, EntityLivingBase triggerer, World world) { triggerCount++; ticksSinceLastTrigger = 0; triggeredEntities.add(triggerer.getUniqueID()); @@ -247,7 +247,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; double dx = entity.posX - trapX; double dz = entity.posZ - trapZ; double dist = Math.sqrt(dx * dx + dz * dz); @@ -261,7 +261,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap { for (EntityLivingBase entity : affected) { // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback); + boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback); // Apply effects if the hit wasn't cancelled if (wasHit) { @@ -271,14 +271,14 @@ public class AbilityTrap extends Ability implements IAbilityTrap { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { armed = false; triggerCount = 0; triggeredEntities.clear(); } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { armed = false; triggerCount = 0; triggeredEntities.clear(); 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 index 1bcfb7cbc..f289f0f72 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -90,22 +90,22 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { getPulledEntities().clear(); pullComplete = false; ticksSincePullDamage = 0; if (aoe) { - AxisAlignedBB box = npc.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + AxisAlignedBB box = caster.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 == caster) continue; if (entity.isDead) continue; - double dist = npc.getDistanceToEntity(entity); + double dist = caster.getDistanceToEntity(entity); if (dist <= pullRadius) { getPulledEntities().add(entity.getUniqueID()); count++; @@ -115,7 +115,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } else { // Single target mode - still check pullRadius if (target != null && !target.isDead) { - double dist = npc.getDistanceToEntity(target); + double dist = caster.getDistanceToEntity(target); if (dist <= pullRadius) { getPulledEntities().add(target.getUniqueID()); } @@ -124,21 +124,21 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (pullComplete || getPulledEntities().isEmpty()) { signalCompletion(); return; } - double destX = npc.posX; - double destY = npc.posY; - double destZ = npc.posZ; + double destX = caster.posX; + double destY = caster.posY; + double destZ = caster.posZ; boolean anyStillPulling = false; ticksSincePullDamage++; for (UUID uuid : new HashSet<>(getPulledEntities())) { - EntityLivingBase entity = findEntity(npc, world, uuid); + EntityLivingBase entity = findEntity(caster, world, uuid); if (entity == null || entity.isDead) { getPulledEntities().remove(uuid); continue; @@ -151,7 +151,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { if (dist <= 1.0f) { getPulledEntities().remove(uuid); - onTargetArrived(npc, entity, world); + onTargetArrived(caster, entity, world); continue; } @@ -176,7 +176,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { if (damageOnPull && pullDamage > 0 && ticksSincePullDamage >= 10) { ticksSincePullDamage = 0; entity.hurtResistantTime = 0; - applyAbilityDamage(npc, entity, pullDamage * 0.5f, 0); + applyAbilityDamage(caster, entity, pullDamage * 0.5f, 0); } } @@ -186,9 +186,9 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } } - private void onTargetArrived(EntityNPCInterface npc, EntityLivingBase entity, World world) { + private void onTargetArrived(EntityLivingBase caster, EntityLivingBase entity, World world) { // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback * 0.5f); + boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback * 0.5f); // Only apply effects if hit wasn't cancelled if (wasHit) { @@ -201,9 +201,9 @@ public class AbilityVortex extends Ability implements IAbilityVortex { * Uses AABB search instead of iterating all loaded entities. */ @SuppressWarnings("unchecked") - private EntityLivingBase findEntity(EntityNPCInterface npc, World world, UUID uuid) { + private EntityLivingBase findEntity(EntityLivingBase caster, World world, UUID uuid) { // Search within pull radius (entities being pulled should be within this area) - AxisAlignedBB searchBox = npc.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + AxisAlignedBB searchBox = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); List nearbyEntities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase entity : nearbyEntities) { diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 75fa6ef7d..a9cfffc43 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -12,7 +12,6 @@ import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.LogWriter; -import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; import java.util.List; @@ -108,7 +107,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { * @param anchoredMode If true, origin follows owner and tail orb is rendered. * If false, beam is free-moving with trailing length (no tail orb). */ - public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, + public EntityAbilityBeam(World world, EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float beamWidth, float headSize, EnergyColorData color, EnergyCombatData combat, @@ -179,7 +178,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { this.motionZ = 0; } - public void setupPreview(EntityNPCInterface owner, float beamWidth, float headSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { + public void setupPreview(EntityLivingBase owner, float beamWidth, float headSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -459,11 +458,13 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { ); } - setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + // Offset downward by half headSize to center + double centeredY = pos.yCoord - headSize * 0.5; + setPosition(pos.xCoord, centeredY, pos.zCoord); // Also update origin for when firing starts startX = pos.xCoord; - startY = pos.yCoord; + startY = centeredY; startZ = pos.zCoord; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index f6003df80..7dc3a8a82 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -10,7 +10,6 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -84,7 +83,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { /** * Full constructor with all parameters using data classes. */ - public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, + public EntityAbilityDisc(World world, EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float discRadius, float discThickness, EnergyColorData color, EnergyCombatData combat, @@ -155,7 +154,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { * Follows anchor point and animations like in the real game. * Can be fired when transitioning to active phase. */ - public void setupPreview(EntityNPCInterface owner, float discRadius, float discThickness, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { + public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -512,7 +511,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.discThickness = targetDiscThickness * progress; this.size = this.discRadius; // Base size for interpolation - // Calculate position based on anchor point + // Calculate position based on anchor point, offset downward by half radius to center if (owner instanceof EntityLivingBase) { Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); setPosition(pos.xCoord, pos.yCoord, pos.zCoord); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index c45a6ee9e..3d888c698 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -11,7 +11,6 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; import java.util.List; @@ -51,7 +50,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { /** * Full constructor with all parameters using data classes. */ - public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, + public EntityAbilityLaser(World world, EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float laserWidth, EnergyColorData color, EnergyCombatData combat, @@ -327,7 +326,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { * Setup this laser in preview mode for GUI display. * Laser doesn't have charging state - spawns at active phase and fires immediately. */ - public void setupPreview(EntityNPCInterface owner, float laserWidth, EnergyColorData color, + public void setupPreview(EntityLivingBase owner, float laserWidth, EnergyColorData color, EnergyLightningData lightning, float expansionSpeed, float maxDistance) { this.setPreviewMode(true); this.setPreviewOwner(owner); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index eff147448..c403f25de 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -10,7 +10,6 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -73,7 +72,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { /** * Full constructor using data classes for grouped parameters. */ - public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, + public EntityAbilityOrb(World world, EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float orbSize, EnergyColorData color, EnergyCombatData combat, EnergyHomingData homing, EnergyLightningData lightning, @@ -134,7 +133,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { * Follows anchor point and animations like in the real game. * Can be fired when transitioning to active phase. */ - public void setupPreview(EntityNPCInterface owner, float orbSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { + public void setupPreview(EntityLivingBase owner, float orbSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -394,7 +393,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { float progress = getChargeProgress(); this.size = targetSize * progress; - // Calculate position based on anchor point + // Calculate position based on anchor point, offset downward by half size to center if (owner instanceof EntityLivingBase) { Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); setPosition(pos.xCoord, pos.yCoord, pos.zCoord); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 5508ecd94..57f99e1c8 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -11,9 +11,11 @@ import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.NpcDamageSource; @@ -75,7 +77,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA // ==================== STATE ==================== protected boolean hasHit = false; protected boolean previewMode = false; // Client-side preview mode (no damage/effects) - protected EntityNPCInterface previewOwner = null; // Direct reference for GUI preview (no world lookup) + protected EntityLivingBase previewOwner = null; // Direct reference for GUI preview (no world lookup) // ==================== ROTATION INTERPOLATION ==================== public float prevRotationValX, prevRotationValY, prevRotationValZ; @@ -101,7 +103,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA /** * Initialize common properties using data classes. Call from subclass constructors. */ - protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, + protected void initProjectile(EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float size, EnergyColorData color, EnergyCombatData combat, EnergyLightningData lightning, EnergyLifespanData lifespan) { @@ -318,10 +320,13 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected void applyDamage(EntityLivingBase target, float dmg, float kb) { if (previewMode) return; // Skip damage in preview mode Entity owner = getOwner(); - EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null; - if (npc != null) { - target.attackEntityFrom(new NpcDamageSource("npc_ability", npc), dmg); + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg); + } else if (owner instanceof EntityLivingBase) { + target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg); } else { target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); } @@ -400,7 +405,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA * Set the preview owner for GUI preview mode. * This allows anchor point calculations without world entity lookup. */ - public void setPreviewOwner(EntityNPCInterface owner) { + public void setPreviewOwner(EntityLivingBase owner) { this.previewOwner = owner; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index ffb5f558a..8540d4df9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -7,8 +7,10 @@ import io.netty.buffer.ByteBuf; import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.LogWriter; import noppes.npcs.NpcDamageSource; @@ -70,7 +72,7 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa this.ignoreFrustumCheck = true; } - public EntityAbilitySweeper(World world, EntityNPCInterface owner, EntityLivingBase target, + public EntityAbilitySweeper(World world, EntityLivingBase owner, EntityLivingBase target, float beamLength, float beamWidth, float beamHeight, EnergyColorData colorData, float sweepSpeed, int numberOfRotations, @@ -264,10 +266,12 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa } private void applyDamage(EntityLivingBase target, Entity owner) { - EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null; - - if (npc != null) { - target.attackEntityFrom(new NpcDamageSource("npc_ability", npc), damage); + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), damage); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), damage); + } else if (owner instanceof EntityLivingBase) { + target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), damage); } else { target.attackEntityFrom(new NpcDamageSource("npc_ability", null), damage); } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 68c021559..63ab9b192 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -26,6 +26,7 @@ import kamkeel.npcs.network.packets.data.SoundManagementPacket; import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket; import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket; import kamkeel.npcs.network.packets.data.VillagerListPacket; +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.gui.GuiClosePacket; @@ -92,10 +93,10 @@ import kamkeel.npcs.network.packets.request.TraderMarketSavePacket; 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.ability.SavedAbilitiesGetPacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; @@ -505,10 +506,10 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new AbilitiesGetAllPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcGetPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcSavePacket()); - REQUEST_PACKET.registerPacket(new SavedAbilitiesGetPacket()); - REQUEST_PACKET.registerPacket(new SavedAbilityGetPacket()); - REQUEST_PACKET.registerPacket(new SavedAbilityRemovePacket()); - REQUEST_PACKET.registerPacket(new SavedAbilitySavePacket()); + REQUEST_PACKET.registerPacket(new CustomAbilitiesGetPacket()); + REQUEST_PACKET.registerPacket(new CustomAbilityGetPacket()); + REQUEST_PACKET.registerPacket(new CustomAbilityRemovePacket()); + REQUEST_PACKET.registerPacket(new CustomAbilitySavePacket()); // Other Packets REQUEST_PACKET.registerPacket(new IsGuiOpenInform()); @@ -578,6 +579,7 @@ public class PacketHandler { // Data | Ability Packets DATA_PACKET.registerPacket(new TelegraphSpawnPacket()); DATA_PACKET.registerPacket(new TelegraphRemovePacket()); + DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket()); } 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 a5c6c8ba9..18321d0a4 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -62,6 +62,7 @@ public enum EnumDataPacket { // Ability System TELEGRAPH_SPAWN, TELEGRAPH_REMOVE, + PLAYER_ABILITY_SYNC, // Auction System AUCTION_DATA, diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index d4968bc5c..8183941d0 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -198,10 +198,10 @@ public enum EnumRequestPacket { NpcAbilitiesGet, NpcAbilitiesSave, AbilitiesGetAll, - SavedAbilitiesGet, - SavedAbilityGet, - SavedAbilityRemove, - SavedAbilitySave, + CustomAbilitiesGet, + CustomAbilityGet, + CustomAbilityRemove, + CustomAbilitySave, Hammer, RecipeScript, diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java index a84e62392..1ac0ec828 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java @@ -12,5 +12,6 @@ public enum EnumSyncType { WORKBENCH_RECIPES, CARPENTRY_RECIPES, ANVIL_RECIPES, - CUSTOM_EFFECTS + CUSTOM_EFFECTS, + CUSTOM_ABILITY } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java new file mode 100644 index 000000000..08df836aa --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java @@ -0,0 +1,73 @@ +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.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 noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; + +/** + * Syncs the player's ability data (unlocked abilities, selected index) to the client. + * Sent on login and whenever abilities change (unlock/lock/select). + */ +public final class PlayerAbilitySyncPacket extends AbstractPacket { + public static final String packetName = "Data|PlayerAbilitySync"; + + private NBTTagCompound abilityNBT; + + public PlayerAbilitySyncPacket() { + } + + public PlayerAbilitySyncPacket(NBTTagCompound abilityNBT) { + this.abilityNBT = abilityNBT; + } + + @Override + public Enum getType() { + return EnumDataPacket.PLAYER_ABILITY_SYNC; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeNBT(out, abilityNBT); + } + + @SideOnly(Side.CLIENT) + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + NBTTagCompound nbt = ByteBufUtils.readNBT(in); + if (nbt == null) return; + + PlayerData data = PlayerData.get(player); + if (data != null) { + data.abilityData.readFromNBT(nbt); + } + } + + /** + * Send the player's ability data to their client. + */ + public static void sendToPlayer(EntityPlayerMP player) { + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (data == null) return; + + NBTTagCompound nbt = new NBTTagCompound(); + data.abilityData.writeToNBT(nbt); + PacketHandler.Instance.sendToPlayer(new PlayerAbilitySyncPacket(nbt), player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java index 3b9f56690..48ac3391b 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java @@ -54,7 +54,13 @@ public final class SpecialKeyStatePacket extends AbstractPacket { PlayerData data = PlayerDataController.Instance.getPlayerData((EntityPlayerMP) player); if (data != null) { + boolean wasDown = data.isSpecialKeyDown(); data.setSpecialKeyDown(keyDown); + + // Trigger ability activation on key press (rising edge) + if (keyDown && !wasDown) { + data.abilityData.activateAbility(player); + } } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java similarity index 75% rename from src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java rename to src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java index 0e8b547a3..8febd9fc0 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java @@ -16,17 +16,17 @@ import noppes.npcs.NoppesUtilServer; import java.io.IOException; /** - * Request packet to get list of all saved ability preset names. + * Request packet to get list of all custom ability preset UUIDs/names. */ -public final class SavedAbilitiesGetPacket extends AbstractPacket { - public static String packetName = "Request|SavedAbilitiesGet"; +public final class CustomAbilitiesGetPacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilitiesGet"; - public SavedAbilitiesGetPacket() { + public CustomAbilitiesGetPacket() { } @Override public Enum getType() { - return EnumRequestPacket.SavedAbilitiesGet; + return EnumRequestPacket.CustomAbilitiesGet; } @Override @@ -46,6 +46,6 @@ public final class SavedAbilitiesGetPacket extends AbstractPacket { if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) return; - NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java similarity index 70% rename from src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java rename to src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java index 7bb2f1826..d95051a66 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java @@ -20,23 +20,23 @@ import net.minecraft.nbt.NBTTagCompound; import java.io.IOException; /** - * Request packet to get a specific saved ability by name. + * Request packet to get a specific custom ability by UUID. */ -public final class SavedAbilityGetPacket extends AbstractPacket { - public static String packetName = "Request|SavedAbilityGet"; +public final class CustomAbilityGetPacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilityGet"; - private String abilityName; + private String uuid; - public SavedAbilityGetPacket() { + public CustomAbilityGetPacket() { } - public SavedAbilityGetPacket(String abilityName) { - this.abilityName = abilityName; + public CustomAbilityGetPacket(String uuid) { + this.uuid = uuid; } @Override public Enum getType() { - return EnumRequestPacket.SavedAbilityGet; + return EnumRequestPacket.CustomAbilityGet; } @Override @@ -47,7 +47,7 @@ public final class SavedAbilityGetPacket extends AbstractPacket { @SideOnly(Side.CLIENT) @Override public void sendData(ByteBuf out) throws IOException { - ByteBufUtils.writeString(out, abilityName); + ByteBufUtils.writeString(out, uuid); } @Override @@ -58,10 +58,12 @@ public final class SavedAbilityGetPacket extends AbstractPacket { if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) return; - String name = ByteBufUtils.readString(in); - Ability ability = AbilityController.Instance.getSavedAbility(name); + String id = ByteBufUtils.readString(in); + Ability ability = AbilityController.Instance.getCustomAbility(id); if (ability != null) { NBTTagCompound compound = ability.writeNBT(); + // Include the UUID so the client can track it + compound.setString("_uuid", id); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java similarity index 69% rename from src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java rename to src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java index 3fe4f5a0c..3ef040700 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java @@ -19,23 +19,23 @@ import noppes.npcs.NoppesUtilServer; import java.io.IOException; /** - * Request packet to remove a saved ability by name. + * Request packet to remove a custom ability by UUID. */ -public final class SavedAbilityRemovePacket extends AbstractPacket { - public static String packetName = "Request|SavedAbilityRemove"; +public final class CustomAbilityRemovePacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilityRemove"; - private String abilityName; + private String uuid; - public SavedAbilityRemovePacket() { + public CustomAbilityRemovePacket() { } - public SavedAbilityRemovePacket(String abilityName) { - this.abilityName = abilityName; + public CustomAbilityRemovePacket(String uuid) { + this.uuid = uuid; } @Override public Enum getType() { - return EnumRequestPacket.SavedAbilityRemove; + return EnumRequestPacket.CustomAbilityRemove; } @Override @@ -51,7 +51,7 @@ public final class SavedAbilityRemovePacket extends AbstractPacket { @SideOnly(Side.CLIENT) @Override public void sendData(ByteBuf out) throws IOException { - ByteBufUtils.writeString(out, abilityName); + ByteBufUtils.writeString(out, uuid); } @Override @@ -62,8 +62,8 @@ public final class SavedAbilityRemovePacket extends AbstractPacket { if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) return; - String name = ByteBufUtils.readString(in); - AbilityController.Instance.deleteAbility(name); - NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); + String id = ByteBufUtils.readString(in); + AbilityController.Instance.deleteCustomAbility(id); + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java similarity index 73% rename from src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java rename to src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java index 2cf2fa489..d5ca1c556 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java @@ -22,23 +22,23 @@ import noppes.npcs.NoppesUtilServer; import java.io.IOException; /** - * Request packet to save an ability preset. + * Request packet to save a custom ability preset. */ -public final class SavedAbilitySavePacket extends AbstractPacket { - public static String packetName = "Request|SavedAbilitySave"; +public final class CustomAbilitySavePacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilitySave"; private NBTTagCompound abilityNBT; - public SavedAbilitySavePacket() { + public CustomAbilitySavePacket() { } - public SavedAbilitySavePacket(NBTTagCompound abilityNBT) { + public CustomAbilitySavePacket(NBTTagCompound abilityNBT) { this.abilityNBT = abilityNBT; } @Override public Enum getType() { - return EnumRequestPacket.SavedAbilitySave; + return EnumRequestPacket.CustomAbilitySave; } @Override @@ -68,9 +68,11 @@ public final class SavedAbilitySavePacket extends AbstractPacket { NBTTagCompound compound = ByteBufUtils.readNBT(in); Ability ability = AbilityController.Instance.fromNBT(compound); if (ability != null) { - AbilityController.Instance.saveAbility(ability); - NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); - GuiDataPacket.sendGuiData((EntityPlayerMP) player, ability.writeNBT()); + AbilityController.Instance.saveCustomAbility(ability); + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); + NBTTagCompound responseNbt = ability.writeNBT(); + responseNbt.setString("_uuid", ability.getId()); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, responseNbt); } } } diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index d9d584b64..e095dc61b 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -75,9 +75,10 @@ public class AnchorPointHelper { switch (anchor) { case FRONT: - x -= Math.sin(headYaw) * frontOffset * scale; - y += height * FRONT_HEIGHT; - z += Math.cos(headYaw) * frontOffset * scale; + Vec3 look = entity.getLookVec(); + x += look.xCoord * frontOffset * scale; + y += height * FRONT_HEIGHT + look.yCoord * frontOffset * scale; + z += look.zCoord * frontOffset * scale; break; case CENTER: diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 0005341f8..ff4617545 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -340,7 +340,6 @@ public class CustomNpcs { new SpawnController(); new LinkedNpcController(); new AnimationController(); - new AbilityController(); AbilityController.Instance.load(); TelegraphController.init(); diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 526a3bdd7..8c44c482e 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -3,6 +3,7 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; @@ -35,9 +36,9 @@ public class DataAbilities { // ═══════════════════════════════════════════════════════════════════ /** - * List of abilities this NPC can use + * List of ability slots this NPC can use (inline or reference). */ - private List abilities = new ArrayList<>(); + private List abilitySlots = new ArrayList<>(); /** * Whether the ability system is enabled for this NPC @@ -294,7 +295,7 @@ public class DataAbilities { * Check if an ability can be selected right now. */ public boolean canSelectAbility() { - if (!enabled || abilities.isEmpty()) { + if (!enabled || abilitySlots.isEmpty()) { return false; } if (currentAbility != null && currentAbility.isExecuting()) { @@ -314,7 +315,7 @@ public class DataAbilities { List eligible = new ArrayList<>(); int totalWeight = 0; - for (Ability ability : abilities) { + for (Ability ability : getAbilities()) { if (isAbilityEligible(ability, target)) { eligible.add(ability); totalWeight += ability.getWeight(); @@ -636,24 +637,24 @@ public class DataAbilities { } /** - * Execute a preset ability on this NPC. + * Execute an ability on this NPC by key (built-in name or custom UUID). * The NPC does NOT need to have this ability assigned. * - * @param presetName The name of the saved ability preset + * @param key The ability key (built-in name or custom UUID) * @param target The target entity (can be null for self-targeted abilities) * @return true if the ability was started successfully */ - public boolean executePresetAbility(String presetName, EntityLivingBase target) { - if (presetName == null || presetName.isEmpty() || npc.worldObj.isRemote) { + public boolean executeAbility(String key, EntityLivingBase target) { + if (key == null || key.isEmpty() || npc.worldObj.isRemote) { return false; } - Ability preset = AbilityController.Instance.getSavedAbility(presetName); - if (preset == null) { + Ability resolved = AbilityController.Instance.resolveAbility(key); + if (resolved == null) { return false; } - return forceStartAbility(preset, target); + return forceStartAbility(resolved, target); } // ═══════════════════════════════════════════════════════════════════ @@ -715,8 +716,8 @@ public class DataAbilities { // Roll cooldown so NPC doesn't immediately attack after reset rollCooldown(); - // Reset execution state on all abilities - for (Ability ability : abilities) { + // Reset execution state on all resolved abilities + for (Ability ability : getAbilities()) { ability.reset(); } } @@ -737,39 +738,79 @@ public class DataAbilities { // ABILITY LIST MANAGEMENT // ═══════════════════════════════════════════════════════════════════ + /** + * Get resolved abilities from all slots, filtering out broken references. + */ public List getAbilities() { - return abilities; + List resolved = new ArrayList<>(); + for (AbilitySlot slot : abilitySlots) { + Ability a = slot.getAbility(); + if (a != null) { + resolved.add(a); + } + } + return resolved; } + /** Get the raw slot list (for GUI access). */ + public List getAbilitySlots() { + return abilitySlots; + } + + /** Add an inline ability. */ public void addAbility(Ability ability) { - abilities.add(ability); + abilitySlots.add(AbilitySlot.inline(ability)); + } + + /** Add a reference ability by key (built-in name or custom UUID). */ + public void addAbilityReference(String key) { + abilitySlots.add(AbilitySlot.reference(key)); } public void removeAbility(int index) { - if (index >= 0 && index < abilities.size()) { - abilities.remove(index); + if (index >= 0 && index < abilitySlots.size()) { + abilitySlots.remove(index); } } public void removeAbility(String id) { - abilities.removeIf(a -> a.getId().equals(id)); + abilitySlots.removeIf(slot -> { + if (slot.isReference()) { + return slot.getReferenceId().equals(id); + } + Ability a = slot.getAbility(); + return a != null && a.getId().equals(id); + }); } public Ability getAbility(String id) { - for (Ability ability : abilities) { - if (ability.getId().equals(id)) { - return ability; + for (AbilitySlot slot : abilitySlots) { + Ability a = slot.getAbility(); + if (a != null && a.getId().equals(id)) { + return a; } } return null; } + /** Check if a slot at a given index is a reference. */ + public boolean isSlotReference(int index) { + if (index < 0 || index >= abilitySlots.size()) return false; + return abilitySlots.get(index).isReference(); + } + + /** Convert a reference slot to inline. Returns false if resolution fails. */ + public boolean convertToInline(int index) { + if (index < 0 || index >= abilitySlots.size()) return false; + return abilitySlots.get(index).convertToInline(); + } + public void clearAbilities() { - abilities.clear(); + abilitySlots.clear(); } public boolean isEmpty() { - return abilities.isEmpty(); + return abilitySlots.isEmpty(); } // ═══════════════════════════════════════════════════════════════════ @@ -920,8 +961,8 @@ public class DataAbilities { compound.setInteger("AbilityMaxCooldown", maxCooldown); NBTTagList abilityList = new NBTTagList(); - for (Ability ability : abilities) { - abilityList.appendTag(ability.writeNBT()); + for (AbilitySlot slot : abilitySlots) { + abilityList.appendTag(slot.writeNBT()); } compound.setTag("Abilities", abilityList); @@ -930,28 +971,16 @@ public class DataAbilities { public void readFromNBT(NBTTagCompound compound) { enabled = compound.getBoolean("AbilitiesEnabled"); + minCooldown = compound.getInteger("AbilityMinCooldown"); + maxCooldown = compound.getInteger("AbilityMaxCooldown"); - // Support old globalCooldown for backwards compatibility - if (compound.hasKey("AbilityMinCooldown")) { - minCooldown = compound.getInteger("AbilityMinCooldown"); - maxCooldown = compound.getInteger("AbilityMaxCooldown"); - } else if (compound.hasKey("AbilityGlobalCooldown")) { - // Migrate old single cooldown to min/max range - int oldCooldown = compound.getInteger("AbilityGlobalCooldown"); - minCooldown = oldCooldown; - maxCooldown = oldCooldown * 2; // Give some range - } else { - minCooldown = 20; - maxCooldown = 60; - } - - abilities.clear(); + abilitySlots.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); + AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT); + if (slot != null) { + abilitySlots.add(slot); } } } diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 5e086abce..5f9d1388f 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -62,6 +62,7 @@ import noppes.npcs.scripted.event.RecipeScriptEvent; import noppes.npcs.scripted.event.player.CustomGuiEvent; import noppes.npcs.scripted.event.player.DialogEvent; import noppes.npcs.scripted.event.player.FactionEvent; +import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import noppes.npcs.scripted.event.player.PlayerEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ChatEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ContainerOpen; @@ -692,6 +693,11 @@ public class EventHooks { return NpcAPI.EVENT_BUS.post(event); } + public static boolean onPlayerAbilityHit(PlayerDataScript handler, PlayerAbilityEvent.HitEvent event) { + handler.callScript(EnumScriptType.ABILITY_HIT, event); + return NpcAPI.EVENT_BUS.post(event); + } + public static void onPlayerChangeDim(PlayerDataScript handler, IPlayer player, int fromDim, int toDim) { PlayerEvent.ChangedDimension event = new PlayerEvent.ChangedDimension(player, fromDim, toDim); handler.callScript(EnumScriptType.CHANGED_DIM, event); diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index ae97f70b4..0799e7a6c 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -785,16 +785,18 @@ public class NoppesUtilServer { sendScrollData(player, map, EnumScrollData.ABILITY_TYPES); } - public static void sendSavedAbilitiesData(EntityPlayerMP player) { + public static void sendCustomAbilitiesData(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 name : controller.getSavedAbilityNames()) { - map.put(name, index++); + for (String uuid : controller.getCustomAbilityIds()) { + String name = controller.getCustomAbilityName(uuid); + if (name == null || name.isEmpty()) name = uuid; + map.put(name + "\t" + uuid, index++); } } - sendScrollData(player, map, EnumScrollData.ABILITIES); + sendScrollData(player, map, EnumScrollData.CUSTOM_ABILITIES); } public static DialogOption setNpcDialog(int slot, int dialogId, EntityPlayer player) throws IOException { diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index a37e6ba08..0541f64c4 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -97,6 +97,13 @@ public class ScriptPlayerEventHandler { if (PlayerDataController.Instance != null) { PlayerData playerData = PlayerData.get(player); playerData.actionManager.tick(); + playerData.abilityData.tick(player); + + // Lock player movement during ability phases that require it + if (playerData.abilityData.isMovementLocked()) { + player.motionX = 0; + player.motionZ = 0; + } if (playerData.updateClient) { SyncController.syncPlayerData((EntityPlayerMP) player, true); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java index 72481f133..807a3e719 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java @@ -98,7 +98,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getTextField(18).setVisible(dialog.renderGradual); getLabel(18).enabled = dialog.renderGradual; - addTextField(new GuiNpcTextField(19, this, guiLeft + 125, y += 22, 40, 20, String.valueOf(dialog.textPitch))); + addTextField(new GuiNpcTextField(19, this, guiLeft + 125, y += 22, 40, 20, "" + dialog.textPitch)); addLabel(new GuiNpcLabel(19, "dialog.textPitch", guiLeft + 4, y + 5)); getTextField(19).floatsOnly = true; getTextField(19).setVisible(dialog.renderGradual); @@ -110,48 +110,48 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis addButton(new GuiNpcButton(24, guiLeft + 192, y, 50, 20, new String[]{"display.bottom", "display.top"}, dialog.alignment)); addLabel(new GuiNpcLabel(27, "display.alignment", guiLeft + 126, y + 5)); - addTextField(new GuiNpcTextField(11, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.textWidth))); - addTextField(new GuiNpcTextField(12, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.textHeight))); + addTextField(new GuiNpcTextField(11, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.textWidth)); + addTextField(new GuiNpcTextField(12, this, guiLeft + 165, y, 40, 20, "" + dialog.textHeight)); addLabel(new GuiNpcLabel(12, "gui.widthHeight", guiLeft + 4, y + 5)); getTextField(11).integersOnly = true; getTextField(11).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(12).integersOnly = true; getTextField(12).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(13, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.textOffsetX))); - addTextField(new GuiNpcTextField(14, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.textOffsetY))); + addTextField(new GuiNpcTextField(13, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.textOffsetX)); + addTextField(new GuiNpcTextField(14, this, guiLeft + 165, y, 40, 20, "" + dialog.textOffsetY)); addLabel(new GuiNpcLabel(15, "dialog.textOffset", guiLeft + 4, y + 5)); getTextField(13).integersOnly = true; getTextField(13).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.titleOffsetX))); - addTextField(new GuiNpcTextField(16, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.titleOffsetY))); + addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.titleOffsetX)); + addTextField(new GuiNpcTextField(16, this, guiLeft + 165, y, 40, 20, "" + dialog.titleOffsetY)); addLabel(new GuiNpcLabel(18, "dialog.titleOffset", guiLeft + 4, y + 5)); getTextField(15).integersOnly = true; getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(16).integersOnly = true; getTextField(16).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(17, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.optionOffsetX))); - addTextField(new GuiNpcTextField(18, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.optionOffsetY))); + addTextField(new GuiNpcTextField(17, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.optionOffsetX)); + addTextField(new GuiNpcTextField(18, this, guiLeft + 165, y, 40, 20, "" + dialog.optionOffsetY)); addLabel(new GuiNpcLabel(21, "dialog.optionOffset", guiLeft + 4, y + 5)); getTextField(17).integersOnly = true; getTextField(17).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(18).integersOnly = true; getTextField(18).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(19, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.optionSpaceX))); - addTextField(new GuiNpcTextField(20, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.optionSpaceY))); + addTextField(new GuiNpcTextField(19, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.optionSpaceX)); + addTextField(new GuiNpcTextField(20, this, guiLeft + 165, y, 40, 20, "" + dialog.optionSpaceY)); addLabel(new GuiNpcLabel(24, "dialog.optionSpacing", guiLeft + 4, y + 5)); getTextField(19).integersOnly = true; getTextField(19).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(20).integersOnly = true; getTextField(20).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(21, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.npcOffsetX))); - addTextField(new GuiNpcTextField(22, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.npcOffsetY))); + addTextField(new GuiNpcTextField(21, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.npcOffsetX)); + addTextField(new GuiNpcTextField(22, this, guiLeft + 165, y, 40, 20, "" + dialog.npcOffsetY)); addLabel(new GuiNpcLabel(25, "dialog.npcOffset", guiLeft + 4, y + 5)); getTextField(21).integersOnly = true; getTextField(21).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); @@ -161,7 +161,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getTextField(22).setVisible(!dialog.hideNPC); getLabel(25).enabled = !dialog.hideNPC; - addTextField(new GuiNpcTextField(23, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.npcScale))); + addTextField(new GuiNpcTextField(23, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.npcScale)); addLabel(new GuiNpcLabel(26, "dialog.npcScale", guiLeft + 4, y + 5)); getTextField(23).floatsOnly = true; getTextField(23).setMinMaxDefaultFloat(-Float.MAX_VALUE, Float.MAX_VALUE, 0); @@ -182,30 +182,30 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis DialogImage dialogImage = getSelectedImage(); addButton(new GuiNpcButton(2, guiLeft + xSize + 24, guiTop + 5, 20, 20, "-")); - addTextField(new GuiNpcTextField(10, this, guiLeft + 17, y += 25, 30, 20, String.valueOf(dialogImage.id))); + addTextField(new GuiNpcTextField(10, this, guiLeft + 17, y += 25, 30, 20, "" + dialogImage.id)); addTextField(new GuiNpcTextField(11, this, guiLeft + 92, y, 150, 20, dialogImage.texture)); addLabel(new GuiNpcLabel(10, "gui.id", guiLeft + 4, y + 5)); addLabel(new GuiNpcLabel(11, "display.texture", guiLeft + 50, y + 5)); getTextField(10).integersOnly = true; - addTextField(new GuiNpcTextField(12, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.x))); - addTextField(new GuiNpcTextField(13, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.y))); + addTextField(new GuiNpcTextField(12, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.x)); + addTextField(new GuiNpcTextField(13, this, guiLeft + 165, y, 40, 20, "" + dialogImage.y)); addLabel(new GuiNpcLabel(12, "gui.position", guiLeft + 4, y + 5)); getTextField(12).integersOnly = true; getTextField(12).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(13).integersOnly = true; getTextField(13).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.width))); - addTextField(new GuiNpcTextField(15, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.height))); + addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.width)); + addTextField(new GuiNpcTextField(15, this, guiLeft + 165, y, 40, 20, "" + dialogImage.height)); addLabel(new GuiNpcLabel(13, "gui.widthHeight", guiLeft + 4, y + 5)); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(15).integersOnly = true; getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(16, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.textureX))); - addTextField(new GuiNpcTextField(17, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.textureY))); + addTextField(new GuiNpcTextField(16, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.textureX)); + addTextField(new GuiNpcTextField(17, this, guiLeft + 165, y, 40, 20, "" + dialogImage.textureY)); addLabel(new GuiNpcLabel(14, "dialog.textureOffset", guiLeft + 4, y + 5)); getTextField(16).integersOnly = true; getTextField(16).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); @@ -229,9 +229,9 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getButton(19).setVisible(dialogImage.imageType == 2); getLabel(16).enabled = dialogImage.imageType == 2; - addTextField(new GuiNpcTextField(20, this, guiLeft + 33, y += 25, 30, 20, String.valueOf(dialogImage.scale))); - addTextField(new GuiNpcTextField(21, this, guiLeft + 102, y, 30, 20, String.valueOf(dialogImage.alpha))); - addTextField(new GuiNpcTextField(22, this, guiLeft + 183, y, 45, 20, String.valueOf(dialogImage.rotation))); + addTextField(new GuiNpcTextField(20, this, guiLeft + 33, y += 25, 30, 20, "" + dialogImage.scale)); + addTextField(new GuiNpcTextField(21, this, guiLeft + 102, y, 30, 20, "" + dialogImage.alpha)); + addTextField(new GuiNpcTextField(22, this, guiLeft + 183, y, 45, 20, "" + dialogImage.rotation)); addLabel(new GuiNpcLabel(17, "model.scale", guiLeft + 4, y + 5)); addLabel(new GuiNpcLabel(18, "display.alpha", guiLeft + 72, y + 5)); addLabel(new GuiNpcLabel(19, "movement.rotation", guiLeft + 140, y + 5)); @@ -306,13 +306,13 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis Collections.sort(ids); ArrayList strings = new ArrayList<>(); for (int i : ids) { - strings.add(String.valueOf(i)); + strings.add("" + i); } imageScroll.setUnsortedList(strings); if (changedId != -1) { - selected = imageScroll.getList().indexOf(String.valueOf(changedId)); - imageScroll.setSelected(String.valueOf(changedId)); + selected = imageScroll.getList().indexOf("" + changedId); + imageScroll.setSelected("" + changedId); changedId = -1; } @@ -402,7 +402,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis dialog.dialogImages.put(addId, new DialogImage(addId)); updateScrollData(); - selected = imageScroll.getList().indexOf(String.valueOf(addId)); + selected = imageScroll.getList().indexOf("" + addId); } if (button.id == 2) { if (imageScroll.getSelected() != null) { @@ -598,8 +598,8 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis } if (textfield.id == 10) { - if (imageScroll.getList().contains(String.valueOf(textfield.getInteger()))) { - textfield.setText(String.valueOf(dialogImage.id)); + if (imageScroll.getList().contains("" + textfield.getInteger())) { + textfield.setText("" + dialogImage.id); return; } diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java index 10420a65d..c5a0769a3 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java @@ -71,7 +71,7 @@ public class SubGuiNpcPartyOptions extends SubGuiInterface implements ITextfield y += 23; //min party size - number field GuiNpcTextField minField = new GuiNpcTextField(21, this, guiLeft + 60, guiTop + 10 + y, - 30, 20, String.valueOf(options.minPartySize)); + 30, 20, "" + options.minPartySize); minField.integersOnly = true; minField.setMinMaxDefault(1, Integer.MAX_VALUE, 1); addTextField(minField); @@ -79,7 +79,7 @@ public class SubGuiNpcPartyOptions extends SubGuiInterface implements ITextfield //max party size - number field GuiNpcTextField maxField = new GuiNpcTextField(20, this, guiLeft + 160, guiTop + 10 + y, - 30, 20, String.valueOf(options.maxPartySize)); + 30, 20, "" + options.maxPartySize); maxField.integersOnly = true; maxField.setMinMaxDefault(1, Integer.MAX_VALUE, 4); addTextField(maxField); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java index c15f9162f..29c33352b 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java @@ -36,9 +36,9 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield addButton(new GuiNpcButtonYesNo(13, guiLeft + 120, y += 22, data.airSpawning)); addLabel(new GuiNpcLabel(13, "spawning.airSpawning", guiLeft + 4, y + 5)); - addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 22, 40, 20, String.valueOf(data.spawnHeightMin))); + addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMin)); addLabel(new GuiNpcLabel(14, "spawning.minHeight", guiLeft + 4, y + 5)); - addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 22, 40, 20, String.valueOf(data.spawnHeightMax))); + addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMax)); addLabel(new GuiNpcLabel(15, "spawning.maxHeight", guiLeft + 4, y + 5)); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index 750fe282e..59046ef77 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; @@ -46,8 +47,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // 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<>(); + // NPC's current ability slots + private final List npcSlots = new ArrayList<>(); // Settings private boolean abilitiesEnabled = false; @@ -75,13 +76,13 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Cooldown range: Min Cooldown and Max Cooldown (spaced apart more) addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 115, y + 5)); - GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 165, y, 40, 20, String.valueOf(minCooldown)); + GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 165, y, 40, 20, "" + minCooldown); minField.setIntegersOnly(); minField.setMinMaxDefault(0, 10000, 20); addTextField(minField); addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 260, y + 5)); - GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 310, y, 40, 20, String.valueOf(maxCooldown)); + GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 310, y, 40, 20, "" + maxCooldown); maxField.setIntegersOnly(); maxField.setMinMaxDefault(0, 10000, 60); addTextField(maxField); @@ -124,8 +125,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addButton(new GuiNpcButton(75, centerX, y + 90, 40, 20, "gui.load")); // On/Off toggle for selected ability (under Load button) - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability selectedAbility = npcAbilities.get(selectedAbilityIndex); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability selectedAbility = npcSlots.get(selectedAbilityIndex).getAbility(); GuiNpcButton toggleBtn = new GuiNpcButton(77, centerX, y + 112, 40, 20, new String[]{"gui.off", "gui.on"}, selectedAbility.isEnabled() ? 1 : 0); addButton(toggleBtn); @@ -139,7 +140,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC 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); + getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.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")); @@ -166,10 +167,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC 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; + npcSlots.add(AbilitySlot.inline(newAbility)); + selectedAbilityIndex = npcSlots.size() - 1; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); @@ -182,8 +181,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Remove ability if (id == 71) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - npcAbilities.remove(selectedAbilityIndex); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + npcSlots.remove(selectedAbilityIndex); selectedAbilityIndex = -1; updateNpcAbilitiesList(); initGui(); @@ -194,9 +193,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Edit ability if (id == 72) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability ability = npcAbilities.get(selectedAbilityIndex); - setSubGui(ability.createConfigGui(this)); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + // Reference slot - ask clone-and-modify vs modify-parent + setSubGui(new SubGuiAbilityEditMode()); + } else { + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } } return; } @@ -204,9 +211,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // 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); + AbilitySlot temp = npcSlots.get(selectedAbilityIndex); + npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex - 1)); + npcSlots.set(selectedAbilityIndex - 1, temp); selectedAbilityIndex--; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); @@ -218,10 +225,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // 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); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1) { + AbilitySlot temp = npcSlots.get(selectedAbilityIndex); + npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex + 1)); + npcSlots.set(selectedAbilityIndex + 1, temp); selectedAbilityIndex++; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); @@ -237,15 +244,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC return; } - // Save selected ability + // Save selected ability as custom preset 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)); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability abilityToSave = npcSlots.get(selectedAbilityIndex).getAbility(); + if (abilityToSave != null) { + if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) { + setSubGui(abilityToSave.createConfigGui(this)); + } else { + setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave)); + } } } return; @@ -253,13 +261,15 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Toggle selected ability on/off if (id == 77) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability ability = npcAbilities.get(selectedAbilityIndex); - ability.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - updateNpcAbilitiesList(); - selectAbilityByIndex(selectedAbilityIndex); - initGui(); - save(); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability ability = npcSlots.get(selectedAbilityIndex).getAbility(); + if (ability != null) { + ability.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); + updateNpcAbilitiesList(); + selectAbilityByIndex(selectedAbilityIndex); + initGui(); + save(); + } } return; } @@ -290,26 +300,20 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC abilitiesEnabled = compound.getBoolean("AbilitiesEnabled"); - // Support old globalCooldown for backwards compatibility - if (compound.hasKey("AbilityMinCooldown")) { - minCooldown = compound.getInteger("AbilityMinCooldown"); - maxCooldown = compound.getInteger("AbilityMaxCooldown"); - } else if (compound.hasKey("AbilityGlobalCooldown")) { - int oldCooldown = compound.getInteger("AbilityGlobalCooldown"); - minCooldown = oldCooldown; - maxCooldown = oldCooldown * 2; - } else { + minCooldown = compound.getInteger("AbilityMinCooldown"); + maxCooldown = compound.getInteger("AbilityMaxCooldown"); + if (minCooldown == 0 && maxCooldown == 0) { minCooldown = 20; maxCooldown = 60; } - npcAbilities.clear(); + npcSlots.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); + AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT); + if (slot != null) { + npcSlots.add(slot); } } @@ -352,7 +356,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC private void updateNpcAbilitiesList() { List list = new ArrayList<>(); - for (int i = 0; i < npcAbilities.size(); i++) { + for (int i = 0; i < npcSlots.size(); i++) { list.add(getAbilityListEntry(i)); } if (npcAbilitiesScroll != null) { @@ -361,26 +365,41 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } /** - * 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 + * Creates the display entry for an ability slot in the list. + * Inline: "[●] 1. Custom Name (Type)" or "[●] 1. Type" + * Reference: "[●] 1. > Name (Type)" in yellow + * Broken: "[●] 1. > [Missing: id...]" in red */ private String getAbilityListEntry(int index) { - Ability ability = npcAbilities.get(index); + AbilitySlot slot = npcSlots.get(index); + Ability ability = slot.getAbility(); + + if (ability == null) { + // Broken reference + String refId = slot.getReferenceId(); + String shortId = refId != null && refId.length() > 8 ? refId.substring(0, 8) + "..." : refId; + return "\u00A7c\u25CF\u00A7r " + (index + 1) + ". \u00A7c> [Missing: " + shortId + "]\u00A7r"; + } + 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 "; + String nameDisplay; if (customName != null && !customName.isEmpty() && !customName.equals(typeName)) { - return colorPrefix + (index + 1) + ". " + customName + " (" + typeName + ")"; + nameDisplay = customName + " (" + typeName + ")"; + } else { + nameDisplay = typeName; } - return colorPrefix + (index + 1) + ". " + typeName; + + if (slot.isReference()) { + return colorPrefix + (index + 1) + ". \u00A7e> " + nameDisplay + "\u00A7r"; + } + return colorPrefix + (index + 1) + ". " + nameDisplay; } private void selectAbilityByIndex(int index) { - if (index >= 0 && index < npcAbilities.size()) { + if (index >= 0 && index < npcSlots.size()) { String entry = getAbilityListEntry(index); if (npcAbilitiesScroll != null) { npcAbilitiesScroll.setSelected(entry); @@ -403,8 +422,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC compound.setInteger("AbilityMaxCooldown", maxCooldown); NBTTagList abilityList = new NBTTagList(); - for (Ability ability : npcAbilities) { - abilityList.appendTag(ability.writeNBT()); + for (AbilitySlot slot : npcSlots) { + abilityList.appendTag(slot.writeNBT()); } compound.setTag("Abilities", abilityList); @@ -412,15 +431,29 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } /** - * Called from SubGuiAbilityLoad when a saved ability is loaded. - * Adds the ability to the NPC's ability list. + * Called from SubGuiAbilityLoad when a cloned ability is loaded. + * Adds as an inline slot. */ 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; + npcSlots.add(AbilitySlot.inline(loadedAbility)); + selectedAbilityIndex = npcSlots.size() - 1; + updateNpcAbilitiesList(); + selectAbilityByIndex(selectedAbilityIndex); + initGui(); + save(); + } + } + + /** + * Called from SubGuiAbilityLoad when a reference is loaded. + * Adds as a reference slot. + */ + public void loadAbilityReference(String referenceId) { + if (referenceId != null && !referenceId.isEmpty()) { + npcSlots.add(AbilitySlot.reference(referenceId)); + selectedAbilityIndex = npcSlots.size() - 1; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); @@ -460,9 +493,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @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)); + if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + setSubGui(new SubGuiAbilityEditMode()); + } else { + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } } } @@ -489,8 +529,32 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiAbilityConfig) { - // Ability was saved in the SubGui, refresh list + if (subgui instanceof SubGuiAbilityEditMode) { + int mode = ((SubGuiAbilityEditMode) subgui).getResult(); + if (mode < 0) return; // cancelled + + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) { + // Convert reference to inline, then edit + if (slot.convertToInline()) { + Ability ability = slot.getAbility(); + if (ability != null) { + ability.setId(UUID.randomUUID().toString()); + setSubGui(ability.createConfigGui(this)); + } + updateNpcAbilitiesList(); + save(); + } + } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) { + // Edit the resolved ability directly (changes propagate to controller) + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } + } + } else if (subgui instanceof SubGuiAbilityConfig) { updateNpcAbilitiesList(); save(); } @@ -500,8 +564,15 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC * Called from SubGuiAbilityConfig when ability is saved. */ public void onAbilitySaved(Ability ability) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - npcAbilities.set(selectedAbilityIndex, ability); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + // For reference slots, save back to controller + // (The resolved copy was edited in-place) + } else { + // For inline slots, replace with the updated ability + npcSlots.set(selectedAbilityIndex, AbilitySlot.inline(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 index 684138891..611f8c918 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -250,7 +250,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // 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)); + GuiNpcTextField weightField = new GuiNpcTextField(3, this, fontRendererObj, col1FieldX, y, 40, 20, "" + weight); weightField.setIntegersOnly(); weightField.setMinMaxDefault(1, 1000, 10); addTextField(weightField); @@ -271,7 +271,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Only show dazed ticks if interruptible is enabled if (interruptible) { addLabel(new GuiNpcLabel(12, "ability.dazed", col2LabelX, y + 5)); - GuiNpcTextField dazedField = new GuiNpcTextField(13, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(dazedTicks)); + GuiNpcTextField dazedField = new GuiNpcTextField(13, this, fontRendererObj, col2FieldX, y, 40, 20, "" + dazedTicks); dazedField.setIntegersOnly(); dazedField.setMinMaxDefault(0, 1000, 80); addTextField(dazedField); @@ -291,13 +291,13 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Only show windup textbox if sync is off or no animation selected if (!syncWindupWithAnimation || !hasWindupAnimation) { - GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks)); + GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, "" + windUpTicks); windupField.setIntegersOnly(); windupField.setMinMaxDefault(0, 1000, 20); addTextField(windupField); } else { // Show calculated value as label when synced - addLabel(new GuiNpcLabel(11, String.valueOf(windUpTicks), col1FieldX + 5, y + 5)); + addLabel(new GuiNpcLabel(11, "" + windUpTicks, col1FieldX + 5, y + 5)); } // Sync toggle button - only show if windup animation is selected @@ -309,7 +309,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5)); - GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks)); + GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, "" + cooldownTicks); cooldownField.setIntegersOnly(); cooldownField.setMinMaxDefault(0, 10000, 0); addTextField(cooldownField); @@ -399,13 +399,13 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // 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)); + GuiNpcTextField minRangeField = new GuiNpcTextField(5, this, fontRendererObj, col1FieldX, y, 40, 20, "" + (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)); + GuiNpcTextField maxRangeField = new GuiNpcTextField(6, this, fontRendererObj, col2FieldX, y, 40, 20, "" + (int) maxRange); maxRangeField.setIntegersOnly(); maxRangeField.setMinMaxDefault(1, 100, 20); addTextField(maxRangeField); @@ -1039,7 +1039,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi * 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)); + GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, "" + value); field.setIntegersOnly(); return field; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java index 3760666b4..c0517257b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java @@ -1,7 +1,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -51,7 +51,7 @@ public class SubGuiAbilityDeleteConfirm extends SubGuiInterface { if (id == 0) { // Delete the ability via packet - PacketClient.sendClient(new SavedAbilityRemovePacket(abilityName)); + PacketClient.sendClient(new CustomAbilityRemovePacket(abilityName)); if (onDeleted != null) { onDeleted.run(); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java new file mode 100644 index 000000000..4d21f12ca --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java @@ -0,0 +1,56 @@ +package noppes.npcs.client.gui.advanced; + +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; + +/** + * Dialog asking the user whether to clone-and-modify (convert to inline) or modify the parent ability. + */ +public class SubGuiAbilityEditMode extends SubGuiInterface { + + public static final int MODE_CLONE_MODIFY = 0; + public static final int MODE_MODIFY_PARENT = 1; + + private int result = -1; + + public SubGuiAbilityEditMode() { + setBackground("menubg.png"); + xSize = 220; + ySize = 90; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.edit.mode", guiLeft + 10, y)); + + y += 25; + addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "ability.cloneModify")); + addButton(new GuiNpcButton(1, guiLeft + 115, y, 95, 20, "ability.modifyParent")); + + y += 25; + addButton(new GuiNpcButton(2, guiLeft + 65, y, 90, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + result = MODE_CLONE_MODIFY; + close(); + } else if (guibutton.id == 1) { + result = MODE_MODIFY_PARENT; + close(); + } else if (guibutton.id == 2) { + result = -1; + close(); + } + } + + public int getResult() { + return result; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java index 1e9133d9d..68a74cb8b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -3,8 +3,8 @@ 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.SavedAbilitiesGetPacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.client.gui.util.GuiCustomScroll; @@ -13,28 +13,34 @@ import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.ICustomScrollListener; import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.client.gui.util.IScrollData; +import noppes.npcs.client.gui.util.ISubGuiListener; 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.UUID; import java.util.Vector; /** * Dialog for loading a saved ability preset. - * Uses packet-based communication for proper client-server architecture. + * Shows custom abilities from the server, then asks Clone/Reference via SubGuiAbilityLoadMode. */ -public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener, IScrollData, IGuiData { +public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener, IScrollData, IGuiData, ISubGuiListener { private final SubGuiAbilityConfig parentConfig; private final GuiNPCAbilities parentAbilities; - private String selectedName = null; - private HashMap abilityData = new HashMap<>(); + private String selectedDisplayName = null; + private String selectedUuid = null; + + /** Maps display name -> UUID for custom abilities. */ + private final HashMap displayToUuid = new HashMap<>(); + private HashMap rawData = new HashMap<>(); private GuiCustomScroll scroll; - // Track if we're waiting for a load private boolean waitingForLoad = false; + private int pendingLoadMode = -1; public SubGuiAbilityLoad(SubGuiAbilityConfig parentConfig) { this.parentConfig = parentConfig; @@ -44,8 +50,7 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL xSize = 220; ySize = 180; - // Request ability list from server - PacketClient.sendClient(new SavedAbilitiesGetPacket()); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); } public SubGuiAbilityLoad(GuiNPCAbilities parentAbilities) { @@ -56,8 +61,7 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL xSize = 220; ySize = 180; - // Request ability list from server - PacketClient.sendClient(new SavedAbilitiesGetPacket()); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); } @Override @@ -69,7 +73,6 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL addLabel(new GuiNpcLabel(0, "ability.load.select", guiLeft + 10, y)); y += 14; - // Scroll list of saved abilities if (scroll == null) { scroll = new GuiCustomScroll(this, 0); scroll.setSize(200, 110); @@ -77,42 +80,70 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL scroll.guiLeft = guiLeft + 10; scroll.guiTop = y; scroll.setList(getFilteredList()); - if (selectedName != null && abilityData.containsKey(selectedName)) { - scroll.setSelected(selectedName); + if (selectedDisplayName != null) { + scroll.setSelected(selectedDisplayName); } addScroll(scroll); y += 115; - // Buttons addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.load")); addButton(new GuiNpcButton(2, guiLeft + 115, y, 95, 20, "gui.cancel")); - // Disable load if nothing selected - getButton(0).setEnabled(selectedName != null && !selectedName.isEmpty()); + getButton(0).setEnabled(selectedUuid != null); } private List getFilteredList() { - return new ArrayList<>(abilityData.keySet()); + return new ArrayList<>(displayToUuid.keySet()); } @Override public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; - if (id == 0 && selectedName != null) { - // Request ability data from server - waitingForLoad = true; - PacketClient.sendClient(new SavedAbilityGetPacket(selectedName)); + if (id == 0 && selectedUuid != null) { + if (parentAbilities != null) { + // From NPC abilities GUI - ask clone/reference + setSubGui(new SubGuiAbilityLoadMode()); + } else { + // From config GUI - always clone + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } } else if (id == 2) { close(); } } + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiAbilityLoadMode) { + int mode = ((SubGuiAbilityLoadMode) subgui).getResult(); + if (mode < 0) return; // cancelled + + if (mode == SubGuiAbilityLoadMode.MODE_REFERENCE) { + // Reference mode - just pass the reference ID + if (parentAbilities != null && selectedUuid != null) { + parentAbilities.loadAbilityReference(selectedUuid); + } + close(); + } else { + // Clone mode - request full data + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } + } + } + @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { if (scroll.id == 0) { - selectedName = scroll.getSelected(); + selectedDisplayName = scroll.getSelected(); + if (selectedDisplayName != null) { + selectedUuid = displayToUuid.get(selectedDisplayName); + } initGui(); } } @@ -120,22 +151,40 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { if (scroll.id == 0 && selection != null && !selection.isEmpty()) { - // Double-click to load - request from server - selectedName = selection; - waitingForLoad = true; - PacketClient.sendClient(new SavedAbilityGetPacket(selection)); + selectedDisplayName = selection; + selectedUuid = displayToUuid.get(selectedDisplayName); + if (selectedUuid != null) { + if (parentAbilities != null) { + setSubGui(new SubGuiAbilityLoadMode()); + } else { + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } + } } } @Override public void setData(Vector list, HashMap data, EnumScrollData type) { - if (type == EnumScrollData.ABILITIES) { - String name = scroll != null ? scroll.getSelected() : null; - this.abilityData = data; + if (type == EnumScrollData.CUSTOM_ABILITIES) { + displayToUuid.clear(); + rawData = data; + for (String key : data.keySet()) { + // key format: "displayName\tUUID" + int tabIndex = key.indexOf('\t'); + if (tabIndex > 0) { + String displayName = key.substring(0, tabIndex); + String uuid = key.substring(tabIndex + 1); + displayToUuid.put(displayName, uuid); + } else { + displayToUuid.put(key, key); + } + } if (scroll != null) { scroll.setList(getFilteredList()); - if (name != null && abilityData.containsKey(name)) { - scroll.setSelected(name); + if (selectedDisplayName != null && displayToUuid.containsKey(selectedDisplayName)) { + scroll.setSelected(selectedDisplayName); } } initGui(); @@ -144,11 +193,15 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL @Override public void setGuiData(NBTTagCompound compound) { - // Received ability data from server if (waitingForLoad) { waitingForLoad = false; Ability loaded = AbilityController.Instance.fromNBT(compound); if (loaded != null) { + if (pendingLoadMode == SubGuiAbilityLoadMode.MODE_CLONE) { + // Give it a new UUID so it's a unique inline copy + loaded.setId(UUID.randomUUID().toString()); + } + if (parentConfig != null) { parentConfig.loadAbility(loaded); } else if (parentAbilities != null) { @@ -161,7 +214,7 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL @Override public void setSelected(String selected) { - this.selectedName = selected; + this.selectedDisplayName = selected; if (scroll != null) { scroll.setSelected(selected); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java new file mode 100644 index 000000000..28a7bd0bb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java @@ -0,0 +1,56 @@ +package noppes.npcs.client.gui.advanced; + +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; + +/** + * Dialog asking the user whether to Clone (inline copy) or Reference the loaded ability. + */ +public class SubGuiAbilityLoadMode extends SubGuiInterface { + + public static final int MODE_CLONE = 0; + public static final int MODE_REFERENCE = 1; + + private int result = -1; + + public SubGuiAbilityLoadMode() { + setBackground("menubg.png"); + xSize = 200; + ySize = 90; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.load.mode", guiLeft + 10, y)); + + y += 25; + addButton(new GuiNpcButton(0, guiLeft + 15, y, 80, 20, "ability.clone")); + addButton(new GuiNpcButton(1, guiLeft + 105, y, 80, 20, "ability.reference")); + + y += 25; + addButton(new GuiNpcButton(2, guiLeft + 55, y, 90, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + result = MODE_CLONE; + close(); + } else if (guibutton.id == 1) { + result = MODE_REFERENCE; + close(); + } else if (guibutton.id == 2) { + result = -1; + close(); + } + } + + public int getResult() { + return result; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java index 760f3eb9b..9407d20c5 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java @@ -2,7 +2,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -53,7 +53,7 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { if (id == 0) { // Save the ability via packet - PacketClient.sendClient(new SavedAbilitySavePacket(ability.writeNBT())); + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); if (callback != null) { callback.onAbilitySaved(ability); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java index f45858a99..d941f19ec 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java @@ -98,7 +98,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi 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)); + GuiNpcTextField hitsField = new GuiNpcTextField(2, this, fontRendererObj, fieldX, y, 40, 20, "" + requiredHits); hitsField.setIntegersOnly(); hitsField.setMinMaxDefault(1, 100, 3); hitsField.setHoverText("condition.hover.hits"); @@ -106,7 +106,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi 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)); + GuiNpcTextField ticksField = new GuiNpcTextField(3, this, fontRendererObj, fieldX, y, 40, 20, "" + withinTicks); ticksField.setIntegersOnly(); ticksField.setMinMaxDefault(1, 1200, 60); ticksField.setHoverText("condition.hover.within"); @@ -115,7 +115,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi // 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)); + GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, "" + percent); thresholdField.setIntegersOnly(); thresholdField.setMinMaxDefault(1, 99, 50); thresholdField.setHoverText("condition.hover.threshold"); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java index 5faf662b0..7f3d04d9e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -122,7 +122,7 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPointEnum().getId()); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPointEnum().ordinal()); anchorBtn.setHoverText("ability.hover.anchorPoint"); addButton(anchorBtn); @@ -182,12 +182,12 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { // Row 6: Density + Radius addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(disc.getLightningDensity())); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, "" + disc.getLightningDensity()); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(disc.getLightningRadius())); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, "" + disc.getLightningRadius()); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } @@ -236,7 +236,7 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { initGui(); break; case 210: - disc.setAnchorPointEnum(AnchorPoint.fromId(value)); + disc.setAnchorPointEnum(AnchorPoint.fromOrdinal(value)); break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java index b4eced168..56ddaf9f6 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java @@ -80,7 +80,7 @@ public class SubGuiAbilityEffects extends SubGuiInterface implements ITextfieldL // Duration text field GuiNpcTextField durField = new GuiNpcTextField(baseId + 1, this, fontRendererObj, - guiLeft + 112, y, 50, 20, String.valueOf(effect.getDurationTicks())); + guiLeft + 112, y, 50, 20, "" + effect.getDurationTicks()); durField.setIntegersOnly(); durField.setMinMaxDefault(1, 12000, 60); durField.setHoverText("effect.hover.duration"); @@ -89,7 +89,7 @@ public class SubGuiAbilityEffects extends SubGuiInterface implements ITextfieldL // Amplifier selector (0-10) String[] ampValues = new String[11]; for (int a = 0; a <= 10; a++) { - ampValues[a] = String.valueOf(a); + ampValues[a] = "" + a; } GuiNpcButton ampBtn = new GuiNpcButton(baseId + 2, guiLeft + 168, y, 40, 20, ampValues, effect.getAmplifier()); ampBtn.setHoverText("effect.hover.amplifier"); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java index 49ab42e5d..3f70e5aca 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -118,7 +118,7 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPointEnum().getId()); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPointEnum().ordinal()); anchorBtn.setHoverText("ability.hover.anchorPoint"); addButton(anchorBtn); @@ -175,12 +175,12 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { // Row 5: Density + Radius addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(beam.getLightningDensity())); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, "" + beam.getLightningDensity()); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(beam.getLightningRadius())); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, "" + beam.getLightningRadius()); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } @@ -225,7 +225,7 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { initGui(); break; case 210: - beam.setAnchorPointEnum(AnchorPoint.fromId(value)); + beam.setAnchorPointEnum(AnchorPoint.fromOrdinal(value)); break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java index e5184a3bb..b594a83c7 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -144,12 +144,12 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { // Row 4: Density + Radius addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(laser.getLightningDensity())); + GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, "" + laser.getLightningDensity()); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(laser.getLightningRadius())); + GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, "" + laser.getLightningRadius()); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } 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 index 7cc0f9f2f..780661c6f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -106,7 +106,7 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPointEnum().getId()); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPointEnum().ordinal()); anchorBtn.setHoverText("ability.hover.anchorPoint"); addButton(anchorBtn); @@ -163,12 +163,12 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { // Row 61: Density + Radius addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(orb.getLightningDensity())); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, "" + orb.getLightningDensity()); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(orb.getLightningRadius())); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, "" + orb.getLightningRadius()); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } @@ -213,7 +213,7 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { initGui(); break; case 210: - orb.setAnchorPointEnum(AnchorPoint.fromId(value)); + orb.setAnchorPointEnum(AnchorPoint.fromOrdinal(value)); break; } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java index e07d2583c..52e78f7d6 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java @@ -338,21 +338,21 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Health label = StatCollector.translateToLocal("stats.health") + ": "; - value = String.valueOf(npc.stats.maxHealth); + value = "" + npc.stats.maxHealth; fontRendererObj.drawString(label, xLabel, y, 0x29d6b9, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // Damage (using getAttackStrength) label = StatCollector.translateToLocal("stats.meleestrength") + ": "; - value = String.valueOf(npc.stats.getAttackStrength()); + value = "" + npc.stats.getAttackStrength(); fontRendererObj.drawString(label, xLabel, y, 0xff5714, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // Attack Speed label = StatCollector.translateToLocal("stats.meleespeed") + ": "; - value = String.valueOf(npc.stats.attackSpeed); + value = "" + npc.stats.attackSpeed; fontRendererObj.drawString(label, xLabel, y, 0xf7ca28, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -381,7 +381,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Walk Speed label = StatCollector.translateToLocal("stats.speed") + ": "; - value = String.valueOf(npc.ais.getWalkingSpeed()); + value = "" + npc.ais.getWalkingSpeed(); fontRendererObj.drawString(label, xLabel, y, 0xffae0d, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -415,7 +415,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Version label = StatCollector.translateToLocal("display.version") + ": "; - value = String.valueOf(linkedItem.version); + value = "" + linkedItem.version; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -423,7 +423,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Max Stack Size labelColor = 0xff5714; label = StatCollector.translateToLocal("display.maxStack") + ": "; - value = String.valueOf(linkedItem.stackSize); + value = "" + linkedItem.stackSize; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -431,7 +431,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Dig Speed labelColor = 0xf7ca28; label = StatCollector.translateToLocal("display.digSpeed") + ": "; - value = String.valueOf(linkedItem.digSpeed); + value = "" + linkedItem.digSpeed; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -498,14 +498,14 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, labelColor = 0x7cff54; // isTool label = StatCollector.translateToLocal("display.isTool") + ": "; - value = String.valueOf(linkedItem.isTool).toUpperCase(); + value = ("" + linkedItem.isTool).toUpperCase(); fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // isNormalItem label = StatCollector.translateToLocal("display.isNormalItem") + ": "; - value = String.valueOf(linkedItem.isNormalItem).toUpperCase(); + value = ("" + linkedItem.isNormalItem).toUpperCase(); fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index cb5220d54..8fa2e4e41 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -5,10 +5,10 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.packets.request.ability.SavedAbilitiesGetPacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; @@ -53,7 +53,10 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // ==================== DATA ==================== private GuiCustomScroll scroll; private HashMap abilityData = new HashMap<>(); + /** Maps display name -> UUID for custom abilities */ + private final HashMap displayToUuid = new HashMap<>(); private String selected = null; + private String selectedUuid = null; private String search = ""; private Ability selectedAbility = null; @@ -73,7 +76,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface previewExecutor.setParentGui(this); // Request ability data from server - PacketClient.sendClient(new SavedAbilitiesGetPacket()); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); // Enable animation data AnimationData data = npc.display.animationData; @@ -90,7 +93,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // Remove button addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove")); - getButton(1).setEnabled(selected != null && !selected.isEmpty() && abilityData.containsKey(selected)); + getButton(1).setEnabled(selected != null && !selected.isEmpty() && displayToUuid.containsKey(selected)); // Scroll list of saved abilities (right side) if (scroll == null) { @@ -103,7 +106,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // Update scroll list scroll.setList(getSearchList()); - if (selected != null && abilityData.containsKey(selected)) { + if (selected != null && displayToUuid.containsKey(selected)) { scroll.setSelected(selected); } @@ -144,10 +147,10 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private List getSearchList() { if (search.isEmpty()) { - return new ArrayList<>(abilityData.keySet()); + return new ArrayList<>(displayToUuid.keySet()); } List list = new ArrayList<>(); - for (String name : abilityData.keySet()) { + for (String name : displayToUuid.keySet()) { if (name.toLowerCase().contains(search.toLowerCase())) { list.add(name); } @@ -208,8 +211,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface int id = guibutton.id; - if (id == 1 && selected != null && abilityData.containsKey(selected)) { - // Remove - show confirmation + if (id == 1 && selected != null && selectedUuid != null) { GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1); displayGuiScreen(guiyesno); } else if (id == 100 && selectedAbility != null) { @@ -254,12 +256,13 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface if (scroll.id == 0) { String newSelection = scroll.getSelected(); if (newSelection != null && !newSelection.equals(selected)) { - // Stop any playing preview previewExecutor.stop(); selected = newSelection; - // Request ability data from server - PacketClient.sendClient(new SavedAbilityGetPacket(selected)); + selectedUuid = displayToUuid.get(selected); + if (selectedUuid != null) { + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } } } } @@ -277,13 +280,25 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void setData(Vector list, HashMap data, EnumScrollData type) { - if (type == EnumScrollData.ABILITIES) { - String name = scroll != null ? scroll.getSelected() : null; + if (type == EnumScrollData.CUSTOM_ABILITIES) { + String prevSelected = scroll != null ? scroll.getSelected() : null; this.abilityData = data; + displayToUuid.clear(); + for (String key : data.keySet()) { + // key format: "displayName\tUUID" + int tabIndex = key.indexOf('\t'); + if (tabIndex > 0) { + String displayName = key.substring(0, tabIndex); + String uuid = key.substring(tabIndex + 1); + displayToUuid.put(displayName, uuid); + } else { + displayToUuid.put(key, key); + } + } if (scroll != null) { scroll.setList(getSearchList()); - if (name != null && abilityData.containsKey(name)) { - scroll.setSelected(name); + if (prevSelected != null && displayToUuid.containsKey(prevSelected)) { + scroll.setSelected(prevSelected); } } initGui(); @@ -322,11 +337,11 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface NoppesUtil.openGUI(player, this); if (!result) return; - if (id == 1 && selected != null && abilityData.containsKey(selected)) { - // Delete ability via packet - PacketClient.sendClient(new SavedAbilityRemovePacket(selected)); + if (id == 1 && selected != null && selectedUuid != null) { + PacketClient.sendClient(new CustomAbilityRemovePacket(selectedUuid)); scroll.clear(); selected = null; + selectedUuid = null; selectedAbility = null; initGui(); } @@ -334,8 +349,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void onAbilitySaved(Ability ability) { - // Save the edited ability via packet - PacketClient.sendClient(new SavedAbilitySavePacket(ability.writeNBT())); + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java index 41e105a38..ecdec2990 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java @@ -90,7 +90,7 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I ArrayList list = new ArrayList<>(); Set keySet = this.spawn.spawnCompounds.keySet(); for (int i : keySet) { - list.add(String.valueOf(i)); + list.add("" + i); } this.spawnEntryScroll.setList(list); addScroll(this.spawnEntryScroll); @@ -99,7 +99,7 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I if (this.spawnEntryScroll.hasSelected()) { int selected = Integer.parseInt(this.spawnEntryScroll.getSelected()); addButton(new GuiNpcButton(22, guiLeft + 32, y, 20, 20, "-")); - GuiNpcTextField num = new GuiNpcTextField(25, this, guiLeft + 60, y += 10, 30, 20, String.valueOf(selected)); + GuiNpcTextField num = new GuiNpcTextField(25, this, guiLeft + 60, y += 10, 30, 20, "" + selected); num.integersOnly = true; addTextField(num); addButton(new GuiNpcButton(26, guiLeft + 92, y, 100, 20, this.getTitle(this.spawn.spawnCompounds.get(selected)))); @@ -215,8 +215,8 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I } if (textfield.id == 25 && this.spawnEntryScroll.hasSelected()) { int selected = Integer.parseInt(spawnEntryScroll.getSelected()); - if (spawnEntryScroll.getList().contains(String.valueOf(textfield.getInteger()))) { - textfield.setText(String.valueOf(selected)); + if (spawnEntryScroll.getList().contains("" + textfield.getInteger())) { + textfield.setText("" + selected); return; } NBTTagCompound compound = spawn.spawnCompounds.get(selected); diff --git a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java index 33966d1e2..a50622529 100644 --- a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -192,7 +192,7 @@ public class AuctionTooltipHandler { tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.claim.expiresSoon")); } else { tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.expires") - .replace("%s", String.valueOf(daysLeft))); + .replace("%s", "" + daysLeft)); } tooltip.add(""); @@ -238,7 +238,7 @@ public class AuctionTooltipHandler { return amount + " " + AuctionClientConfig.getCurrencyName(); } StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) { diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java b/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java index 683e21798..00551911a 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java @@ -237,7 +237,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { fontRendererObj.drawString(percentText, percentTextX, percentTextY, CustomNpcResourceListener.DefaultTextColor); // Add text overlay for percentage with the full unrounded value. if (func_146978_c(percentTextX - guiLeft, percentTextY - guiTop, percentWidth, fontRendererObj.FONT_HEIGHT, mouseX, mouseY)) { - textOverlays.add(new TextOverlayData(percentTextX, percentTextY, percentWidth, fontRendererObj.FONT_HEIGHT, String.valueOf(recipe.getRepairPercentage()))); + textOverlays.add(new TextOverlayData(percentTextX, percentTextY, percentWidth, fontRendererObj.FONT_HEIGHT, "" + recipe.getRepairPercentage())); } if (!recipe.availability.isDefault()) { @@ -303,7 +303,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { int xpBoxWidth = scaledTextWidth; int xpBoxHeight = fontRendererObj.FONT_HEIGHT; if (func_146978_c(xpTextX - guiLeft, xpTextY - guiTop, xpBoxWidth, xpBoxHeight, mouseX, mouseY)) { - textOverlays.add(new TextOverlayData(xpTextX, xpTextY, xpBoxWidth, xpBoxHeight, String.valueOf(recipe.getXpCost()))); + textOverlays.add(new TextOverlayData(xpTextX, xpTextY, xpBoxWidth, xpBoxHeight, "" + recipe.getXpCost())); } } @@ -326,7 +326,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { private String formatXpCost(int xp) { if (xp < 1000) { - return String.valueOf(xp); + return "" + xp; } double xpK = xp / 1000.0; if (xpK == (int) xpK) { diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java index cc5e5d804..afe47fb44 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java @@ -255,7 +255,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis String bidCountLabel = StatCollector.translateToLocal("auction.info.bidCount"); fontRendererObj.drawString(EnumChatFormatting.GRAY + bidCountLabel, x, y, 0xFFFFFF); - label = String.valueOf(listing.bidCount); + label = "" + listing.bidCount; length = fontRendererObj.getStringWidth(label); fontRendererObj.drawString(EnumChatFormatting.WHITE + label, xRight - length, y, 0xFFFFFF); } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java index 851c92263..7efdbe9c9 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java @@ -196,9 +196,9 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface { /** Format currency with commas (e.g., 1000 -> "1,000") */ protected String formatCurrency(long amount) { - if (amount < 1000) return String.valueOf(amount); + if (amount < 1000) return "" + amount; StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) sb.insert(0, ','); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java index 8b288c83f..a8784b3b0 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java @@ -67,7 +67,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis // Starting Price label and field addLabel(new GuiNpcLabel(1, "auction.sell.startingPrice", guiLeft + contentX, y + 5, 0xFFFFFF)); GuiNpcTextField startingField = new GuiNpcTextField(txtStartingPriceId, this, fontRendererObj, - guiLeft + contentX + 80, y, 80, 18, startingPrice > 0 ? String.valueOf(startingPrice) : ""); + guiLeft + contentX + 80, y, 80, 18, startingPrice > 0 ? "" + startingPrice : ""); startingField.setIntegersOnly(); addTextField(startingField); @@ -78,7 +78,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis addLabel(lblBuyout); txtBuyout = new GuiNpcTextField(txtBuyoutPriceId, this, fontRendererObj, - guiLeft + contentX + 80, y, 80, 18, buyoutPrice > 0 ? String.valueOf(buyoutPrice) : ""); + guiLeft + contentX + 80, y, 80, 18, buyoutPrice > 0 ? "" + buyoutPrice : ""); txtBuyout.setIntegersOnly(); addTextField(txtBuyout); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java index f11bbb2e6..b0a1c6c32 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java @@ -172,7 +172,7 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { // Draw stock count to the RIGHT of the trade (after output slot) if (stockEnabled && availableStock[slotIdx] < Integer.MAX_VALUE) { - String stockText = String.valueOf(availableStock[slotIdx]); + String stockText = "" + availableStock[slotIdx]; int stockColor = availableStock[slotIdx] > 0 ? 0x00AA00 : 0xAA0000; fontRendererObj.drawString(stockText, x + OUTPUT_OFFSET + 20, y + 5, stockColor); } @@ -317,10 +317,10 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { */ private String formatCurrency(long amount) { if (amount < 1000) { - return String.valueOf(amount); + return "" + amount; } StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index de7ab50e4..a1793c545 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -62,7 +62,7 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe // Bid amount field addLabel(new GuiNpcLabel(3, "auction.bid.yourBid", guiLeft + 15, y + 4)); GuiNpcTextField bidField = new GuiNpcTextField(txtBidId, this, fontRendererObj, - guiLeft + 80, y, 100, 18, String.valueOf(minimumBid)); + guiLeft + 80, y, 100, 18, "" + minimumBid); bidField.setIntegersOnly(); addTextField(bidField); y += 28; @@ -146,9 +146,9 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe } private String formatCurrency(long amount) { - if (amount < 1000) return String.valueOf(amount); + if (amount < 1000) return "" + amount; StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) sb.insert(0, ','); diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java index f4061299b..b63471047 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java @@ -107,9 +107,9 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface { } private String formatCurrency(long amount) { - if (amount < 1000) return String.valueOf(amount); + if (amount < 1000) return "" + amount; StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) sb.insert(0, ','); diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java index 7ffb7aff2..f9e627fe7 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java @@ -180,7 +180,7 @@ public class GuiProfiles extends GuiCNPCInventory implements ISubGuiListener, IC if (slot.isTemporary()) { label = StatCollector.translateToLocal("profile.temporary"); - result = (String.valueOf(slot.isTemporary())).toUpperCase(); + result = ("" + slot.isTemporary()).toUpperCase(); labelColor = 0xfc60dd; resultColor = 0x35fc81; fontRendererObj.drawString(label, xLabel, y, labelColor, false); diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java index 6c4745d42..7b54518e3 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java @@ -56,7 +56,7 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex int x = guiLeft + i % 3 * 130 + 15; // First position after "$" label int y = guiTop + i / 3 * 22 + 14; long cost = role.getCurrencyCost(i); - GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? String.valueOf(cost) : ""); + GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? "" + cost : ""); field.setMaxStringLength(8); field.setIntegersOnly(); addTextField(field); diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java index 2e369b7e6..4e88c9b21 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java @@ -95,7 +95,7 @@ public class SubGuiNpcTraderStock extends SubGuiInterface implements ISubGuiList return "-1"; // Mixed values, return unlimited } } - return String.valueOf(first); + return "" + first; } @Override diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java index b4a76ba48..c9d40c5df 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java @@ -47,7 +47,7 @@ public class SubGuiNpcTraderStockSlots extends SubGuiInterface implements ITextf // Max stock textfield int maxStock = role.stock.maxStock[i]; - String value = maxStock < 0 ? "-1" : String.valueOf(maxStock); + String value = maxStock < 0 ? "-1" : "" + maxStock; GuiNpcTextField field = new GuiNpcTextField(i, this, fontRendererObj, x + 42, y, 32, 18, value); field.setIntegersOnly(); field.setMinMaxDefault(-1, Integer.MAX_VALUE, -1); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java index c9c44ff93..ea8dcc0b5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java @@ -169,7 +169,7 @@ public class GuiAbilityInterface extends GuiNPCInterface2 { GL11.glPushMatrix(); // Move to NPC render position on screen - GL11.glTranslatef(npcScreenX, npcScreenY, 50F); + GL11.glTranslatef(npcScreenX, npcScreenY, 300F); // Apply zoom (scale) GL11.glScalef(-CAMERA_ZOOM, CAMERA_ZOOM, CAMERA_ZOOM); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 77be0dd7c..be148c11d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -477,7 +477,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Dynamically calculate gutter width based on line count digits if (container != null && container.linesCount > 0) { int maxLineNum = container.linesCount; - String maxLineStr = String.valueOf(maxLineNum); + String maxLineStr = "" + maxLineNum; int digitWidth = ClientProxy.Font.width(maxLineStr); LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10; // 10px total padding (5px left + 5px right) } @@ -645,7 +645,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Render LINE GUTTER numbers for (int i = renderStart; i <= renderEnd; i++) { int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; - String lineNum = String.valueOf(i + 1); + String lineNum = "" + (i + 1); int lineNumWidth = ClientProxy.Font.width(lineNum); int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5; // right-align with 5px padding int lineNumY = posY + 1; diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index 7ebc098be..5d9cfdaa0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -361,7 +361,7 @@ public class OverlayKeyPresetViewer { GL11.glPushMatrix(); GL11.glScalef(scale, scale, 1); GL11.glTranslatef(letterX / scale, nameY / scale, 1); - font.drawStringWithShadow(String.valueOf(letter), 0, 0, isDefault ? 0xff8a8a8a : 0xffffffff); + font.drawStringWithShadow("" + letter, 0, 0, isDefault ? 0xff8a8a8a : 0xffffffff); GL11.glPopMatrix(); //////////////////////////////////////////// diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java index f054f32a7..e3e023dee 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -470,7 +470,7 @@ public class GoToLineDialog { // Type character - only allow digits and colon if (ChatAllowedCharacters.isAllowedCharacter(c)) { if (Character.isDigit(c) || c == ':') { - insertText(String.valueOf(c)); + insertText("" + c); markActivity(); return true; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 3d5c9909d..81c346f8f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -366,7 +366,7 @@ public class RenameRefactorHandler { if (isValidIdentifierChar(c, isFirstChar)) { if (hasSelection) { // Replace entire selection with this character - currentWord = String.valueOf(c); + currentWord = "" + c; applyLiveRename(); callback.setCursorPosition(primaryOccurrenceStart + 1); sel.setSelection(0, 0); // Clear selection diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index baf994b4e..06d5a6ec3 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -7,7 +7,7 @@ public enum EnumScrollData { DIALOG_GROUP, QUEST_GROUP, ABILITY_TYPES, - ABILITIES, + CUSTOM_ABILITIES, ANIMATIONS, BUILTIN_ANIMATIONS } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java new file mode 100644 index 000000000..45f9dfe44 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -0,0 +1,503 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.UserType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; +import net.minecraft.util.DamageSource; +import noppes.npcs.api.ability.IPlayerAbilityData; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; + +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages player ability data - unlocked abilities, selection, cooldowns, and execution. + * Players reference abilities by key (built-in keys or preset names), never customized copies. + *

+ * Key differences from NPC DataAbilities: + * - Universal cooldown (shared across all abilities) + * - Player manually selects ability (no weighted random) + * - Conditions requiring a target are skipped + * - Uses ability's base cooldownTicks only + */ +public class PlayerAbilityData implements IPlayerAbilityData { + + private final PlayerData playerData; + + // ═══════════════════════════════════════════════════════════════════ + // CONFIGURATION (saved to NBT) + // ═══════════════════════════════════════════════════════════════════ + + /** + * List of unlocked ability keys (preset names or built-in keys). + * Order matters - determines selection index. + */ + private List unlockedAbilities = new ArrayList<>(); + + /** + * Currently selected ability index in the unlocked list. + */ + private int selectedIndex = 0; + + // ═══════════════════════════════════════════════════════════════════ + // RUNTIME STATE (not saved) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Universal cooldown end time (shared across all abilities). + */ + private transient long cooldownEndTime = 0; + + /** + * Currently executing ability instance (null if none). + */ + private transient Ability currentAbility; + + /** + * Current ability's key (for cooldown tracking after completion). + */ + private transient String currentAbilityKey; + + /** + * Target for the current ability (can be null for self-targeting). + */ + private transient EntityLivingBase currentTarget; + + // ═══════════════════════════════════════════════════════════════════ + // CONSTRUCTOR + // ═══════════════════════════════════════════════════════════════════ + + public PlayerAbilityData(PlayerData playerData) { + this.playerData = playerData; + } + + // ═══════════════════════════════════════════════════════════════════ + // TICK - Called from player tick handler + // ═══════════════════════════════════════════════════════════════════ + + /** + * Main tick method - handles ability execution. + * Should be called every server tick. + */ + public void tick(EntityPlayer player) { + if (player.worldObj.isRemote || player.isDead) { + return; + } + + if (currentAbility != null && currentAbility.isExecuting()) { + tickCurrentAbility(player); + } + } + + /** + * Tick the currently executing ability. + */ + private void tickCurrentAbility(EntityPlayer player) { + AbilityPhase oldPhase = currentAbility.getPhase(); + + // Tick advances time and possibly changes phase + boolean phaseChanged = currentAbility.tick(); + + switch (currentAbility.getPhase()) { + case WINDUP: + currentAbility.onWindUpTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick()); + break; + + case ACTIVE: + if (phaseChanged && oldPhase == AbilityPhase.WINDUP) { + // Just entered ACTIVE phase + TelegraphInstance telegraph = currentAbility.getTelegraphInstance(); + if (telegraph != null) { + telegraph.lockPosition(); + } + removeTelegraph(currentAbility, player); + + // Play active sound and animation + playAbilitySound(player, currentAbility.getActiveSound()); + playAbilityAnimation(currentAbility.getActiveAnimation()); + + // Call onExecute + currentAbility.onExecute(player, currentTarget, player.worldObj); + + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(player); + return; + } + } + currentAbility.onActiveTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick()); + + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(player); + return; + } + break; + + case DAZED: + // Player is dazed - just wait + break; + + case IDLE: + handleAbilityCompletion(player); + break; + } + } + + /** + * Handle ability completion. + */ + private void handleAbilityCompletion(EntityPlayer player) { + if (currentAbility == null) return; + + currentAbility.onComplete(player, currentTarget); + + // Apply universal cooldown (using base cooldownTicks only, not min/max random) + if (!currentAbility.isIgnoreCooldown()) { + cooldownEndTime = player.worldObj.getTotalWorldTime() + currentAbility.getCooldownTicks(); + } + + // Stop animation + stopAbilityAnimation(); + + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + } + + // ═══════════════════════════════════════════════════════════════════ + // ABILITY ACTIVATION - Called on Special Key press + // ═══════════════════════════════════════════════════════════════════ + + /** + * Activate the currently selected ability. + * Called when the player presses the Special Key. + * + * @param player The player + * @return true if the ability was started + */ + public boolean activateAbility(EntityPlayer player) { + if (player.worldObj.isRemote) return false; + if (unlockedAbilities.isEmpty()) return false; + if (selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) return false; + + String key = unlockedAbilities.get(selectedIndex); + return activateAbility(player, key); + } + + /** + * Activate a specific ability by key. + * + * @param player The player + * @param key The ability key (built-in or preset name) + * @return true if the ability was started + */ + public boolean activateAbility(EntityPlayer player, String key) { + if (player.worldObj.isRemote || key == null || key.isEmpty()) return false; + + // Can't activate if already executing + if (currentAbility != null && currentAbility.isExecuting()) return false; + + // Resolve ability from controller + if (AbilityController.Instance == null) return false; + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null) return false; + + // Check user type allows player + if (!ability.getAllowedBy().allowsPlayer()) return false; + + // Check universal cooldown + if (!ability.isIgnoreCooldown() && isOnCooldown(player)) return false; + + // Check conditions (skip target-requiring ones) + if (!ability.checkConditionsForPlayer(player)) return false; + + // Start the ability + currentAbility = ability; + currentAbilityKey = key; + currentTarget = null; // Players don't have auto-targets + ability.start(null); + + // Spawn telegraph + spawnTelegraph(ability, player, null); + + // Play wind up sound and animation + playAbilitySound(player, ability.getWindUpSound()); + playAbilityAnimation(ability.getWindUpAnimation()); + + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // CLIENT SYNC + // ═══════════════════════════════════════════════════════════════════ + + /** + * Sync ability data to the client. + * Called when abilities change (unlock/lock/select) and on login. + */ + public void syncToClient() { + EntityPlayer player = playerData.player; + if (player instanceof EntityPlayerMP) { + PlayerAbilitySyncPacket.sendToPlayer((EntityPlayerMP) player); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // SELECTION + // ═══════════════════════════════════════════════════════════════════ + + public int getSelectedIndex() { + return selectedIndex; + } + + public void setSelectedIndex(int index) { + if (index >= 0 && index < unlockedAbilities.size()) { + this.selectedIndex = index; + syncToClient(); + } + } + + public void selectNext() { + if (unlockedAbilities.isEmpty()) return; + selectedIndex = (selectedIndex + 1) % unlockedAbilities.size(); + syncToClient(); + } + + public void selectPrevious() { + if (unlockedAbilities.isEmpty()) return; + selectedIndex = (selectedIndex - 1 + unlockedAbilities.size()) % unlockedAbilities.size(); + syncToClient(); + } + + public String getSelectedAbilityKey() { + if (unlockedAbilities.isEmpty() || selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) { + return null; + } + return unlockedAbilities.get(selectedIndex); + } + + // ═══════════════════════════════════════════════════════════════════ + // UNLOCKED ABILITIES + // ═══════════════════════════════════════════════════════════════════ + + public List getUnlockedAbilityList() { + return unlockedAbilities; + } + + @Override + public String[] getUnlockedAbilities() { + return unlockedAbilities.toArray(new String[0]); + } + + public void setUnlockedAbilities(List abilities) { + this.unlockedAbilities = abilities != null ? new ArrayList<>(abilities) : new ArrayList<>(); + // Clamp selected index + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + syncToClient(); + } + + public void unlockAbility(String key) { + if (key != null && !key.isEmpty() && !unlockedAbilities.contains(key)) { + unlockedAbilities.add(key); + syncToClient(); + } + } + + public void lockAbility(String key) { + unlockedAbilities.remove(key); + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + syncToClient(); + } + + public boolean hasUnlockedAbility(String key) { + return unlockedAbilities.contains(key); + } + + // ═══════════════════════════════════════════════════════════════════ + // COOLDOWNS + // ═══════════════════════════════════════════════════════════════════ + + public boolean isOnCooldown(EntityPlayer player) { + return player.worldObj.getTotalWorldTime() < cooldownEndTime; + } + + /** + * Check if a specific ability key is on cooldown. + * Currently uses universal cooldown (same for all abilities). + */ + public boolean isOnCooldown(String key, EntityPlayer player) { + return isOnCooldown(player); + } + + public long getRemainingCooldown(EntityPlayer player) { + long remaining = cooldownEndTime - player.worldObj.getTotalWorldTime(); + return remaining > 0 ? remaining : 0; + } + + /** + * Reset cooldown for a specific ability key. + * Currently resets the universal cooldown. + */ + public void resetCooldown(String key) { + cooldownEndTime = 0; + } + + /** + * Reset all cooldowns. + */ + public void resetAllCooldowns() { + cooldownEndTime = 0; + } + + @Override + public boolean activateAbility() { + return activateAbility(playerData.player); + } + + @Override + public boolean activateAbility(String key) { + return activateAbility(playerData.player, key); + } + + // ═══════════════════════════════════════════════════════════════════ + // STATE QUERIES + // ═══════════════════════════════════════════════════════════════════ + + public boolean isExecutingAbility() { + return currentAbility != null && currentAbility.isExecuting(); + } + + public Ability getCurrentAbility() { + return currentAbility; + } + + public void interruptCurrentAbility() { + if (currentAbility != null && currentAbility.isExecuting()) { + currentAbility.interrupt(); + stopAbilityAnimation(); + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + } + } + + @Override + public boolean isOnCooldown() { + EntityPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(player); + } + + @Override + public boolean isOnCooldown(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(key, player); + } + + @Override + public void resetCooldown() { + cooldownEndTime = 0; + } + + /** + * Check if the player's movement should be locked due to an executing ability. + * Used by the tick handler to restrict player movement during ability phases. + */ + public boolean isMovementLocked() { + return currentAbility != null && currentAbility.isExecuting() + && currentAbility.isMovementLockedForCurrentPhase(); + } + + // ═══════════════════════════════════════════════════════════════════ + // TELEGRAPH + // ═══════════════════════════════════════════════════════════════════ + + private void spawnTelegraph(Ability ability, EntityPlayer player, EntityLivingBase target) { + TelegraphInstance telegraph = ability.createTelegraph(player, target); + if (telegraph != null) { + ability.setTelegraphInstance(telegraph); + if (player instanceof EntityPlayerMP) { + TelegraphSpawnPacket.sendToTracking(telegraph, player); + } + } + } + + private void removeTelegraph(Ability ability, EntityPlayer player) { + TelegraphInstance telegraph = ability.getTelegraphInstance(); + if (telegraph != null) { + if (player instanceof EntityPlayerMP) { + TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player); + } + ability.setTelegraphInstance(null); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // ANIMATION + // ═══════════════════════════════════════════════════════════════════ + + private void playAbilityAnimation(Animation animation) { + if (animation == null || AnimationController.Instance == null) return; + playerData.animationData.setEnabled(true); + playerData.animationData.setAnimation(animation); + playerData.animationData.updateClient(); + } + + private void stopAbilityAnimation() { + playerData.animationData.setAnimation(null); + playerData.animationData.updateClient(); + } + + private void playAbilitySound(EntityPlayer player, String sound) { + if (sound != null && !sound.isEmpty()) { + player.worldObj.playSoundAtEntity(player, sound, 1.0f, 1.0f); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT SERIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + public void writeToNBT(NBTTagCompound compound) { + NBTTagList list = new NBTTagList(); + for (String key : unlockedAbilities) { + list.appendTag(new NBTTagString(key)); + } + compound.setTag("PlayerAbilities", list); + compound.setInteger("PlayerAbilitySelected", selectedIndex); + } + + public void readFromNBT(NBTTagCompound compound) { + unlockedAbilities.clear(); + if (compound.hasKey("PlayerAbilities")) { + NBTTagList list = compound.getTagList("PlayerAbilities", 8); // 8 = string tag + for (int i = 0; i < list.tagCount(); i++) { + String key = list.getStringTagAt(i); + if (key != null && !key.isEmpty()) { + unlockedAbilities.add(key); + } + } + } + selectedIndex = compound.getInteger("PlayerAbilitySelected"); + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 04e06a41e..fa4e7d961 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -15,6 +15,7 @@ import net.minecraft.world.World; import net.minecraftforge.common.IExtendedEntityProperties; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; +import noppes.npcs.api.ability.IPlayerAbilityData; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.handler.IPlayerBankData; import noppes.npcs.api.handler.IPlayerTradeData; @@ -65,6 +66,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { // Trade data (currency + auction claims) - shared across all profile slots public PlayerTradeData tradeData = new PlayerTradeData(this); + public PlayerAbilityData abilityData = new PlayerAbilityData(this); public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -108,6 +110,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { if (!controller.playerEffects.containsKey(playerID)) { controller.playerEffects.put(playerID, effectData.getEffects()); } + + // Sync player ability data to client + abilityData.syncToClient(); } public void onLogout() { @@ -137,6 +142,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { effectData.readFromNBT(data); magicData.readToNBT(data); tradeData.readFromNBT(data); + abilityData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); @@ -180,6 +186,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { effectData.writeToNBT(compound); magicData.writeToNBT(compound); tradeData.writeToNBT(compound); + abilityData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); @@ -411,6 +418,10 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return tradeData; } + public IPlayerAbilityData getAbilityData() { + return abilityData; + } + public synchronized void save() { // Don't Save this is a Modification of a Profile's PlayerData Profile profile = ProfileController.Instance.getProfile(UUID.fromString(uuid)); diff --git a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java new file mode 100644 index 000000000..cb7def405 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java @@ -0,0 +1,101 @@ +package noppes.npcs.scripted.event.player; + +import cpw.mods.fml.common.eventhandler.Cancelable; +import kamkeel.npcs.controllers.data.ability.Ability; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.event.IPlayerAbilityEvent; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.scripted.NpcAPI; + +/** + * Events fired during player ability execution lifecycle. + */ +public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEvent { + protected final Ability ability; + protected final IEntityLivingBase target; + + public PlayerAbilityEvent(IPlayer player, Ability ability, EntityLivingBase target) { + super(player); + 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 "playerAbilityEvent"; + } + + /** + * Fired when a player's ability hits an entity with damage. + * Canceling prevents the damage from being applied. + */ + @Cancelable + public static class HitEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.HitEvent { + private final IEntityLivingBase hitEntity; + private float damage; + private float knockback; + private float knockbackUp; + + public HitEvent(IPlayer player, Ability ability, EntityLivingBase target, + EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { + super(player, 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; + } + } +} diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java index d097a892c..6cbbb4dae 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -43,6 +43,11 @@ public class ScriptDataAbilities implements IDataAbilities { } } + @Override + public void addAbilityReference(String key) { + data.addAbilityReference(key); + } + @Override public void removeAbility(String abilityId) { data.removeAbility(abilityId); @@ -58,6 +63,33 @@ public class ScriptDataAbilities implements IDataAbilities { return data.getAbility(abilityId) != null; } + @Override + public boolean isAbilityReference(String abilityId) { + List slots = data.getAbilitySlots(); + for (int i = 0; i < slots.size(); i++) { + kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i); + if (slot.isReference()) { + if (slot.getReferenceId().equals(abilityId)) return true; + } else { + Ability a = slot.getAbility(); + if (a != null && a.getId().equals(abilityId)) return false; + } + } + return false; + } + + @Override + public boolean convertToInline(String abilityId) { + List slots = data.getAbilitySlots(); + for (int i = 0; i < slots.size(); i++) { + kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i); + if (slot.isReference() && slot.getReferenceId().equals(abilityId)) { + return data.convertToInline(i); + } + } + return false; + } + @Override public void clearAbilities() { data.clearAbilities(); @@ -117,17 +149,14 @@ public class ScriptDataAbilities implements IDataAbilities { } @Override - public boolean executePresetAbility(String presetName) { - return executePresetAbility(presetName, null); + public boolean executeAbility(String key) { + return executeAbility(key, null); } @Override - public boolean executePresetAbility(String presetName, Object target) { - // Resolve target + public boolean executeAbility(String key, Object target) { EntityLivingBase targetEntity = resolveTarget(target); - - // Use the direct preset execution method - return data.executePresetAbility(presetName, targetEntity); + return data.executeAbility(key, targetEntity); } @Override diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java new file mode 100644 index 000000000..2570653fc --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -0,0 +1,112 @@ +package noppes.npcs.scripted.wrapper; + +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.ability.IPlayerAbilityData; +import noppes.npcs.controllers.data.PlayerAbilityData; +import noppes.npcs.controllers.data.PlayerData; + +/** + * Script wrapper for PlayerAbilityData providing API access to player abilities. + */ +public class ScriptPlayerAbilityData implements IPlayerAbilityData { + + private final PlayerAbilityData data; + private final PlayerData playerData; + + public ScriptPlayerAbilityData(PlayerData playerData) { + this.playerData = playerData; + this.data = playerData.abilityData; + } + + @Override + public String[] getUnlockedAbilities() { + return data.getUnlockedAbilities().toArray(new String[0]); + } + + @Override + public void unlockAbility(String key) { + data.unlockAbility(key); + } + + @Override + public void lockAbility(String key) { + data.lockAbility(key); + } + + @Override + public boolean hasUnlockedAbility(String key) { + return data.hasUnlockedAbility(key); + } + + @Override + public int getSelectedIndex() { + return data.getSelectedIndex(); + } + + @Override + public void setSelectedIndex(int index) { + data.setSelectedIndex(index); + } + + @Override + public String getSelectedAbilityKey() { + return data.getSelectedAbilityKey(); + } + + @Override + public void selectNext() { + data.selectNext(); + } + + @Override + public void selectPrevious() { + data.selectPrevious(); + } + + @Override + public boolean isExecutingAbility() { + return data.isExecutingAbility(); + } + + @Override + public IAbility getCurrentAbility() { + return data.getCurrentAbility(); + } + + @Override + public void interruptCurrentAbility() { + data.interruptCurrentAbility(); + } + + @Override + public boolean isOnCooldown(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.isOnCooldown(key, player); + } + + @Override + public void resetCooldown(String key) { + data.resetCooldown(key); + } + + @Override + public void resetAllCooldowns() { + data.resetAllCooldowns(); + } + + @Override + public boolean activateAbility() { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.activateAbility(player); + } + + @Override + public boolean activateAbility(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.activateAbility(player, key); + } +}