From d9f100d2459823d8377aaec89629cf85839fab49 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 3 Mar 2026 14:55:50 -0500 Subject: [PATCH] Cleanup Damage Handling Logic for CNPC+ and DBC Addon for better readability / context. Added Magic Data Support to Abilities --- src/api | 2 +- .../controllers/data/ability/Ability.java | 102 ++++++- .../ability/type/energy/AbilityBarrier.java | 15 ++ .../type/energy/AbilityEnergyProjectile.java | 5 + .../npcs/entity/EntityEnergyAbility.java | 14 + .../npcs/entity/EntityEnergyBarrier.java | 33 +++ .../npcs/entity/EntityEnergyProjectile.java | 18 +- .../npcs/util/AttributeAttackUtil.java | 149 ++++++++++- .../gui/advanced/SubGuiAbilityMagic.java | 250 ++++++++++++++++++ .../npcs/controllers/data/MagicData.java | 11 + .../scripted/entity/ScriptEnergyAbility.java | 14 + .../assets/customnpcs/lang/en_US.lang | 4 + 12 files changed, 604 insertions(+), 13 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java diff --git a/src/api b/src/api index b3dc23a23..e4e2466a6 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit b3dc23a23e4c5ea21c67f67ab5aecb1a17f841f4 +Subproject commit e4e2466a6ee5b7cc61243eb805627b767bfb5ea3 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 8812e2d35..33d50ed1e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -22,6 +22,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityEnergyBarrier; import kamkeel.npcs.entity.EntityEnergyDome; import kamkeel.npcs.entity.EntityEnergyPanel; +import kamkeel.npcs.util.AttributeAttackUtil; import kamkeel.npcs.util.FileNameHelper; import net.minecraft.block.Block; import net.minecraft.entity.Entity; @@ -37,6 +38,7 @@ import net.minecraft.util.StatCollector; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.NpcDamageSource; +import noppes.npcs.config.ConfigMain; import noppes.npcs.api.INbt; import noppes.npcs.api.ability.IAbility; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -49,11 +51,18 @@ import noppes.npcs.controllers.data.AbilityScript; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.EventHooks; import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.data.Magic; +import noppes.npcs.controllers.data.MagicData; +import noppes.npcs.controllers.data.MagicEntry; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.client.gui.advanced.SubGuiAbilityMagic; import noppes.npcs.scripted.event.AbilityEvent; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; public abstract class Ability implements IAbility, IAbilityAction { @@ -117,6 +126,9 @@ public abstract class Ability implements IAbility, IAbilityAction { // Custom data for external mods protected NBTTagCompound customData = new NBTTagCompound(); + // Magic data — defines magic types and damage splits for this ability + protected MagicData magicData = new MagicData(); + // User type restriction protected UserType allowedBy = UserType.BOTH; @@ -281,6 +293,15 @@ public abstract class Ability implements IAbility, IAbilityAction { return true; } + /** + * Returns true if this ability type uses magic data. + * By default, any ability that deals damage also has magic. + * Override in subclasses like Barrier that need magic but don't deal direct damage. + */ + public boolean hasMagic() { + return hasDamage(); + } + /** * Override to provide variant templates for this ability type. * When a user creates this ability type, they will be shown a selection dialog @@ -472,13 +493,22 @@ public abstract class Ability implements IAbility, IAbilityAction { this, caster, hitEntity, damage, knockback, knockbackUp, knockbackDirX, knockbackDirZ, 1.0f); if (!handled) { + float finalDamage = damage; + + // Apply magic pipeline for player and NPC casters + MagicData resolved = resolveMagicData(caster); + if (resolved != null && !resolved.isEmpty()) { + finalDamage = AttributeAttackUtil.calculateAbilityDamage( + caster, hitEntity, damage, resolved); + } + // Default damage path if (caster instanceof EntityNPCInterface) { - hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), damage); + hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), finalDamage); } else if (caster instanceof EntityPlayer) { - hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), damage); + hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), finalDamage); } else { - hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), damage); + hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), finalDamage); } } } finally { @@ -737,6 +767,41 @@ public abstract class Ability implements IAbility, IAbilityAction { } } + // ── Magic section ──────────────────────────────────────────── + if (hasMagic()) { + defs.add(FieldDef.section("ability.section.magic").tab("General")); + + defs.add(FieldDef.labelField("ability.magic.note", () -> { + if (magicData.isEmpty()) { + return StatCollector.translateToLocal("ability.magic.usesCaster"); + } + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : magicData.getMagics().entrySet()) { + Magic magic = MagicController.getInstance().getMagic(entry.getKey()); + if (magic != null) { + if (sb.length() > 0) sb.append(", "); + sb.append(magic.getDisplayName()).append(": ") + .append(Math.round(entry.getValue().split * 100)).append("%"); + } + } + return sb.toString(); + }).tab("General")); + + defs.add(FieldDef.subGuiField("ability.magic.editor", + () -> new SubGuiAbilityMagic(magicData.copy()), + gui -> { + MagicData result = ((SubGuiAbilityMagic) gui).magicData; + magicData.getMagics().clear(); + for (Map.Entry e : result.getMagics().entrySet()) { + magicData.getMagics().put(e.getKey(), e.getValue()); + } + }) + .buttonLabel(() -> magicData.isEmpty() + ? StatCollector.translateToLocal("ability.magic.usesCaster") + : magicData.getMagics().size() + " Magic(s)") + .tab("General")); + } + // ── Target tab ─────────────────────────────────────────────── defs.add(FieldDef.row( FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)).range(0, 100), @@ -1410,6 +1475,7 @@ public abstract class Ability implements IAbility, IAbilityAction { nbt.setString("telegraphType", telegraphType.name()); nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset); nbt.setTag("customData", (NBTTagCompound) customData.copy()); + magicData.writeToNBT(nbt); nbt.setInteger("allowedBy", allowedBy.ordinal()); nbt.setBoolean("ignoreCooldown", ignoreCooldown); nbt.setBoolean("perAbilityCooldown", perAbilityCooldown); @@ -1508,6 +1574,7 @@ public abstract class Ability implements IAbility, IAbilityAction { } telegraphHeightOffset = nbt.getFloat("telegraphHeightOffset"); customData = (NBTTagCompound) nbt.getCompoundTag("customData").copy(); + magicData.readToNBT(nbt); allowedBy = UserType.fromOrdinal(nbt.getInteger("allowedBy")); ignoreCooldown = nbt.getBoolean("ignoreCooldown"); perAbilityCooldown = nbt.getBoolean("perAbilityCooldown"); @@ -2205,6 +2272,35 @@ public abstract class Ability implements IAbility, IAbilityAction { return customData; } + public MagicData getMagicData() { + return magicData; + } + + /** + * Resolve magic data for this ability + caster combination. + * If the ability has its own magic data, use that (override). + * Otherwise, fall back to the caster's magic data (player or NPC). + * + * @return Resolved MagicData, or null if neither has magic + */ + public MagicData resolveMagicData(EntityLivingBase caster) { + if (!this.magicData.isEmpty()) { + return this.magicData; + } + if (caster instanceof EntityPlayer) { + PlayerData data = PlayerData.get((EntityPlayer) caster); + if (data != null && data.magicData != null && !data.magicData.isEmpty()) { + return data.magicData; + } + } else if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + if (npc.stats != null && npc.stats.magicData != null && !npc.stats.magicData.isEmpty()) { + return npc.stats.magicData; + } + } + return null; + } + public List getConditions() { return conditions; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java index 1a7734a1e..d4bdfa924 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java @@ -10,6 +10,7 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.controllers.data.MagicData; import java.util.List; @@ -69,6 +70,11 @@ public abstract class AbilityBarrier extends AbilityEnergy { return false; // Barriers are defensive } + @Override + public boolean hasMagic() { + return true; // Barriers use magic for defense interactions + } + @Override public boolean allowFreeOnCast() { return true; @@ -86,6 +92,7 @@ public abstract class AbilityBarrier extends AbilityEnergy { if (tick == 1) { barrierEntity = createBarrierEntity(caster, target); if (barrierEntity != null) { + inheritMagicData(barrierEntity, caster); applyBarrierHealthModifiers(caster); barrierEntity.setupCharging(getWindUpTicks()); spawnAbilityEntity(barrierEntity); @@ -102,12 +109,20 @@ public abstract class AbilityBarrier extends AbilityEnergy { // No windup or entity died — create fresh barrierEntity = createBarrierEntity(caster, target); if (barrierEntity != null) { + inheritMagicData(barrierEntity, caster); applyBarrierHealthModifiers(caster); spawnAbilityEntity(barrierEntity); } } } + private void inheritMagicData(EntityEnergyBarrier entity, EntityLivingBase caster) { + MagicData resolved = resolveMagicData(caster); + if (resolved != null) { + entity.setMagicData(resolved.copy()); + } + } + @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { if (barrierEntity == null || barrierEntity.isDead) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java index 0e08fab02..2745f1a2f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -25,6 +25,7 @@ import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import noppes.npcs.api.ability.type.IAbilityEnergyProjectile; import noppes.npcs.client.gui.builder.FieldDef; +import noppes.npcs.controllers.data.MagicData; import java.util.List; @@ -214,6 +215,10 @@ public abstract class AbilityEnergyProjectile entity.setEffects(this.effects); entity.setIgnoreIFrames(this.isIgnoreIFrames()); entity.setSourceAbility(this); + MagicData resolvedMagic = resolveMagicData(caster); + if (resolvedMagic != null) { + entity.setMagicData(resolvedMagic.copy()); + } return entity; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java index dcf435904..46c2647e8 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyAbility.java @@ -12,6 +12,7 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; +import noppes.npcs.controllers.data.MagicData; /** * Universal base class for all energy ability entities (Projectiles, Barriers, Sweeper). @@ -51,6 +52,9 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit */ protected NBTTagCompound customDamageData = null; + /** Magic data for this entity — defines magic types for outgoing damage or barrier defense. */ + protected MagicData magicData = new MagicData(); + protected boolean ignoreIFrames = false; protected boolean previewMode = false; protected EntityLivingBase previewOwner = null; @@ -159,6 +163,14 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit this.customDamageData = data; } + public MagicData getMagicData() { + return magicData; + } + + public void setMagicData(MagicData data) { + this.magicData = data != null ? data : new MagicData(); + } + public boolean isIgnoreIFrames() { return ignoreIFrames; } @@ -346,6 +358,7 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit if (customDamageData != null) { nbt.setTag("CustomDamageData", customDamageData); } + magicData.writeToNBT(nbt); } /** @@ -360,6 +373,7 @@ public abstract class EntityEnergyAbility extends Entity implements IEntityAddit if (nbt.hasKey("CustomDamageData")) { this.customDamageData = nbt.getCompoundTag("CustomDamageData"); } + magicData.readToNBT(nbt); } // ==================== SPAWN DATA ==================== diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java index b16448c33..9111e7dd8 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java @@ -2,6 +2,7 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper; import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData; +import kamkeel.npcs.util.AttributeAttackUtil; import kamkeel.npcs.util.CNPCDebug; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; @@ -12,6 +13,8 @@ import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.CustomNpcs; import noppes.npcs.EventHooks; +import noppes.npcs.controllers.data.MagicData; +import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; import java.lang.ref.WeakReference; @@ -248,6 +251,13 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { float multiplier = barrierData.getMultiplier(typeId); float finalDamage = damage * multiplier; + // Apply magic interactions if projectile is a CNPC+ energy entity + if (projectileEntity instanceof EntityEnergyAbility) { + float magicMultiplier = AttributeAttackUtil.calculateMagicInteractionMultiplier( + ((EntityEnergyAbility) projectileEntity).getMagicData(), this.magicData); + finalDamage *= magicMultiplier; + } + if (!worldObj.isRemote) { float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, null, finalDamage); if (eventDamage < 0) { @@ -329,6 +339,11 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { float multiplier = barrierData.getMultiplier(typeId); float damage = baseDamage * multiplier; + // Apply magic interactions (e.g., Water projectile vs Fire barrier) + float magicMultiplier = AttributeAttackUtil.calculateMagicInteractionMultiplier( + projectile.getMagicData(), this.magicData); + damage *= magicMultiplier; + // Fire hit event (may cancel or modify damage) if (!worldObj.isRemote) { float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, projectile, damage); @@ -419,6 +434,24 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { float damage = amount * barrierData.meleeDamageMultiplier; + // Apply magic interactions from attacker's magic vs barrier's magic + Entity attacker = source.getEntity(); + if (attacker != null) { + MagicData attackerMagic = null; + if (attacker instanceof EntityPlayer) { + PlayerData data = PlayerData.get((EntityPlayer) attacker); + if (data != null) attackerMagic = data.magicData; + } else if (attacker instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) attacker; + if (npc.stats != null) attackerMagic = npc.stats.magicData; + } + if (attackerMagic != null) { + float magicMultiplier = AttributeAttackUtil.calculateMagicInteractionMultiplier( + attackerMagic, this.magicData); + damage *= magicMultiplier; + } + } + // Fire hit event with null projectile (melee hit) float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, null, damage); if (eventDamage < 0) return false; diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index b9b6560e9..4b55b82e0 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -18,6 +18,7 @@ import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData; import kamkeel.npcs.network.packets.data.energy.ProjectileReflectPacket; import kamkeel.npcs.network.packets.data.energyexplosion.EnergyExplosionSpawnPacket; import kamkeel.npcs.util.AnchorPointHelper; +import kamkeel.npcs.util.AttributeAttackUtil; import kamkeel.npcs.util.CNPCDebug; import net.minecraft.block.Block; import net.minecraft.block.material.Material; @@ -39,6 +40,7 @@ import noppes.npcs.EventHooks; import noppes.npcs.NpcDamageSource; import noppes.npcs.api.entity.IEntity; import noppes.npcs.config.ConfigEnergy; +import noppes.npcs.config.ConfigMain; import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -940,15 +942,23 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { int previousHurtResistantTime = Ability.clearHurtResistanceIfNeeded(target, ignoreIFrames); try { if (!handled) { + float finalDmg = dmg; + + // Apply magic pipeline for player and NPC casters + if (this.magicData != null && !this.magicData.isEmpty() && owner instanceof EntityLivingBase) { + finalDmg = AttributeAttackUtil.calculateAbilityDamage( + (EntityLivingBase) owner, target, dmg, this.magicData); + } + // Default damage path if (owner instanceof EntityNPCInterface) { - defaultDamageApplied = target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg); + defaultDamageApplied = target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), finalDmg); } else if (owner instanceof EntityPlayer) { - defaultDamageApplied = target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg); + defaultDamageApplied = target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), finalDmg); } else if (owner instanceof EntityLivingBase) { - defaultDamageApplied = target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg); + defaultDamageApplied = target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), finalDmg); } else { - defaultDamageApplied = target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); + defaultDamageApplied = target.attackEntityFrom(new NpcDamageSource("npc_ability", null), finalDmg); } } } finally { diff --git a/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java b/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java index 5ee635058..76f6fbffa 100644 --- a/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java +++ b/src/main/java/kamkeel/npcs/util/AttributeAttackUtil.java @@ -7,7 +7,9 @@ import noppes.npcs.config.ConfigMain; import noppes.npcs.controllers.MagicController; import noppes.npcs.controllers.data.Magic; import noppes.npcs.controllers.data.MagicEntry; +import noppes.npcs.controllers.data.MagicData; import noppes.npcs.controllers.data.PlayerData; +import net.minecraft.entity.EntityLivingBase; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashMap; @@ -30,7 +32,7 @@ public class AttributeAttackUtil { // --- Helper Classes & Methods --- // Container for allocation results. - private static class AllocationResult { + public static class AllocationResult { public Map allocation; public float leftover; @@ -44,7 +46,7 @@ public class AttributeAttackUtil { * Allocates physical damage into magic allocations based on MagicData splits. * Returns both a mapping (magic ID -> allocated damage) and any leftover physical damage. */ - private static AllocationResult allocateMagicDamage(float physicalDamage, Map magicData) { + public static AllocationResult allocateMagicDamage(float physicalDamage, Map magicData) { Map allocation = new HashMap<>(); float totalSplit = 0f; if (magicData != null) { @@ -64,7 +66,7 @@ public class AttributeAttackUtil { /** * Adds extra magic damage from attribute sources if attributes are enabled. */ - private static void addAttributeMagicDamage(Map allocation, + public static void addAttributeMagicDamage(Map allocation, Map attributeDamage, Map magicBoost) { if (!ConfigMain.AttributesEnabled) return; @@ -81,7 +83,7 @@ public class AttributeAttackUtil { * Applies magic interactions. For each magic in the allocation, multiplies its damage by (1 + bonus) * for each opposing magic the defender possesses. */ - private static void applyMagicInteractions(Map allocation, + public static void applyMagicInteractions(Map allocation, Set defenderMagicIDs, MagicController magicController) { for (Map.Entry entry : allocation.entrySet()) { @@ -104,7 +106,7 @@ public class AttributeAttackUtil { * Applies defender magic defenses if attributes are enabled. * Subtracts tracker-based magic defense (scaled by resistance) from each magic allocation. */ - private static float applyDefenderMagicDefense(Map allocation, + public static float applyDefenderMagicDefense(Map allocation, PlayerAttributeTracker defender, MagicController magicController) { if (!ConfigMain.AttributesEnabled) return 0f; @@ -284,4 +286,141 @@ public class AttributeAttackUtil { return mainDamage + gearDamage; } + + // --- Magic Interaction Utility --- + + /** + * Calculate magic interaction multiplier between attacker's magic types + * and defender's magic types. Used for barrier defense and entity-vs-entity interactions. + * + * @param attackerMagic Attacker's magic data (projectile or weapon) + * @param defenderMagic Defender's magic data (barrier or entity) + * @return Multiplier (1.0 = no interaction, >1.0 = bonus damage, <1.0 = resistance) + */ + public static float calculateMagicInteractionMultiplier(MagicData attackerMagic, MagicData defenderMagic) { + if (attackerMagic == null || defenderMagic == null) return 1.0f; + if (attackerMagic.isEmpty() || defenderMagic.isEmpty()) return 1.0f; + + MagicController mc = MagicController.getInstance(); + float multiplier = 1.0f; + + for (int attackMagicId : attackerMagic.getMagics().keySet()) { + Magic magic = mc.getMagic(attackMagicId); + if (magic == null || magic.interactions == null) continue; + for (Map.Entry interaction : magic.interactions.entrySet()) { + if (defenderMagic.getMagics().containsKey(interaction.getKey())) { + multiplier *= (1 + interaction.getValue()); + } + } + } + return multiplier; + } + + // --- Ability Damage Pipeline --- + + /** + * Apply gear magic boost percentages to the split-allocated magic damage. + * This multiplies each magic type's allocated damage by (1 + boost/100). + * Unlike {@link #addAttributeMagicDamage}, this does NOT add flat gear damage — + * it only applies the percentage multiplier from gear to the existing allocation. + */ + public static void applyMagicBoostToAllocation(Map allocation, Map magicBoost) { + if (!ConfigMain.AttributesEnabled) return; + for (Map.Entry entry : allocation.entrySet()) { + float boost = magicBoost.getOrDefault(entry.getKey(), 0f); + if (boost != 0f) { + entry.setValue(entry.getValue() * (1 + boost / 100f)); + } + } + } + + /** + * Apply gear magic boost percentages to barrier/dome health. + * Each magic type's split determines what portion of health is affected by its boost. + * E.g., Thunder [100%] dome + 50% Thunder gear boost = health * 1.5 + */ + public static float applyMagicBoostToHealth(EntityPlayer caster, MagicData magicData, float health) { + if (!ConfigMain.AttributesEnabled) return health; + if (magicData == null || magicData.isEmpty()) return health; + + PlayerAttributeTracker tracker = getTracker(caster); + if (tracker == null) return health; + + float multiplier = 1.0f; + for (Map.Entry entry : magicData.getMagics().entrySet()) { + float split = entry.getValue().split; + float boost = tracker.magicBoost.getOrDefault(entry.getKey(), 0f); + multiplier += split * (boost / 100f); + } + return health * multiplier; + } + + /** + * Apply the magic pipeline to ability damage. Works for both player and NPC casters. + * Abilities do NOT get main attack or critical boosts — they have their own scaling. + * This handles: + *
    + *
  • Magic allocation (splits from ability/caster magic data)
  • + *
  • Gear magic boost multipliers on split damage (player casters only, no flat gear damage)
  • + *
  • Magic interactions vs defender
  • + *
  • Defender magic defense/resistance (player defenders only)
  • + *
+ * If no magic data is present, damage passes through unchanged. + * + * @param caster Caster (player or NPC) + * @param target Target entity + * @param damage Base ability damage (already scaled by ability's own system) + * @param abilityMagic Resolved magic data (ability's own, or caster's fallback) + * @return Final damage after magic splits, boosts, interactions, and defense + */ + public static float calculateAbilityDamage(EntityLivingBase caster, EntityLivingBase target, + float damage, MagicData abilityMagic) { + if (abilityMagic == null || abilityMagic.isEmpty()) return damage; + + // 1. Allocate base damage into magic types using ability's splits + AllocationResult result = allocateMagicDamage(damage, abilityMagic.getMagics()); + float leftover = result.leftover; + + // 2. Apply gear magic boost multipliers to the split allocation (player casters only) + if (caster instanceof EntityPlayer && ConfigMain.AttributesEnabled) { + PlayerAttributeTracker tracker = getTracker((EntityPlayer) caster); + if (tracker != null) { + applyMagicBoostToAllocation(result.allocation, tracker.magicBoost); + } + } + + // 3. Magic interactions against target + MagicController magicController = MagicController.getInstance(); + Set defenderMagicIDs = null; + + if (target instanceof EntityPlayer) { + PlayerData defenderData = PlayerData.get((EntityPlayer) target); + if (defenderData != null) { + defenderMagicIDs = new HashSet<>(defenderData.magicData.getMagics().keySet()); + } + } else if (target instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) target; + if (npc.stats != null && npc.stats.magicData != null) { + defenderMagicIDs = new HashSet<>(npc.stats.magicData.getMagics().keySet()); + } + } + + if (defenderMagicIDs != null) { + applyMagicInteractions(result.allocation, defenderMagicIDs, magicController); + } + + // 4. Apply defender magic defense (player defenders with attributes only) + if (target instanceof EntityPlayer && ConfigMain.AttributesEnabled) { + PlayerAttributeTracker defenderTracker = getTracker((EntityPlayer) target); + if (defenderTracker != null) { + float adjustedMagic = applyDefenderMagicDefense(result.allocation, defenderTracker, magicController); + return leftover + adjustedMagic; + } + } + + // Sum magic damage + float magicTotal = 0f; + for (float val : result.allocation.values()) magicTotal += val; + return leftover + magicTotal; + } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java new file mode 100644 index 000000000..8543ad208 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityMagic.java @@ -0,0 +1,250 @@ +package noppes.npcs.client.gui.advanced; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.controllers.MagicController; +import noppes.npcs.controllers.data.Magic; +import noppes.npcs.controllers.data.MagicData; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Sub-GUI for editing magic splits on an Ability. + * Dual-scroll layout: available magics on left, assigned magics on right. + * Only split values (no flat damage) are editable. + */ +public class SubGuiAbilityMagic extends SubGuiInterface implements ICustomScrollListener, ITextfieldListener { + + public MagicData magicData; + + private GuiCustomScroll availableScroll; + private GuiCustomScroll assignedScroll; + + /** Maps display name → magic ID for all available magics. */ + private final HashMap allMagic = new HashMap<>(); + /** Maps formatted display string (right scroll) → base display name. */ + private final HashMap assignedDisplayToName = new HashMap<>(); + private String search = ""; + + private GuiNpcTextField splitField; + + public SubGuiAbilityMagic(MagicData data) { + this.magicData = data; + setBackground("menubg.png"); + xSize = 420; + ySize = 222; + + // Load all available magics from the controller + MagicController controller = MagicController.getInstance(); + for (Map.Entry entry : controller.magics.entrySet()) { + allMagic.put(entry.getValue().getDisplayName(), entry.getKey()); + } + } + + @Override + public void initGui() { + super.initGui(); + + int scrollW = 150; + int scrollH = 140; + int leftX = guiLeft + 10; + int rightX = guiLeft + 250; + int topY = guiTop + 20; + + // Title + addLabel(new GuiNpcLabel(0, "ability.magic.editor", guiLeft + 10, guiTop + 5)); + + // Left scroll: available magics + if (availableScroll == null) { + availableScroll = new GuiCustomScroll(this, 0); + availableScroll.setSize(scrollW, scrollH); + } + availableScroll.guiLeft = leftX; + availableScroll.guiTop = topY; + availableScroll.setList(getSearchList()); + addScroll(availableScroll); + + // Search field below left scroll + addTextField(new GuiNpcTextField(4, this, fontRendererObj, leftX, topY + scrollH + 5, scrollW, 20, search)); + + // Right scroll: assigned magics + if (assignedScroll == null) { + assignedScroll = new GuiCustomScroll(this, 1); + assignedScroll.setSize(scrollW, scrollH); + } + assignedScroll.guiLeft = rightX; + assignedScroll.guiTop = topY; + updateAssignedList(); + addScroll(assignedScroll); + + // Arrow buttons between scrolls + int midX = guiLeft + 175; + addButton(new GuiNpcButton(70, midX, topY + 40, 55, 20, ">")); + addButton(new GuiNpcButton(71, midX, topY + 62, 55, 20, "<")); + + // Standardize button + addButton(new GuiNpcButton(72, midX, topY + 90, 55, 20, "magic.dist")); + getButton(72).setHoverText("magic.distInfo"); + + // Split field below right scroll + int tfY = topY + scrollH + 5; + addLabel(new GuiNpcLabel(5002, "magic.split", rightX - 57, tfY + 5)); + splitField = new GuiNpcTextField(73, this, fontRendererObj, rightX, tfY, 60, 20, ""); + splitField.setFloatsOnly(); + splitField.setMinMaxDefaultFloat(0, 1, 0); + splitField.enabled = false; + addTextField(splitField); + + // Done button + addButton(new GuiNpcButton(80, guiLeft + xSize / 2 - 40, guiTop + ySize - 34, 80, 20, "gui.done")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + // Add magic + if (guibutton.id == 70) { + if (availableScroll.hasSelected()) { + String selected = availableScroll.getSelected(); + Integer id = allMagic.get(selected); + if (id != null && !magicData.hasMagic(id)) { + magicData.addMagic(id, 0, 0); + } + } + updateAssignedList(); + return; + } + // Remove magic + if (guibutton.id == 71) { + if (assignedScroll.hasSelected()) { + Integer id = resolveAssignedId(assignedScroll.getSelected()); + if (id != null && magicData.hasMagic(id)) { + magicData.removeMagic(id); + } + } + updateAssignedList(); + if (splitField != null) { + splitField.setText(""); + splitField.enabled = false; + } + return; + } + // Standardize splits + if (guibutton.id == 72) { + if (magicData.getMagics().size() > 0) { + int count = magicData.getMagics().size(); + float stdSplit = 1.0f / count; + for (Integer key : magicData.getMagics().keySet()) { + magicData.getMagic(key).split = stdSplit; + } + // Update field if something is selected + if (assignedScroll.getSelected() != null && splitField != null) { + Integer id = resolveAssignedId(assignedScroll.getSelected()); + if (id != null && magicData.hasMagic(id)) { + splitField.setText(magicData.getMagic(id).split + ""); + } + } + updateAssignedList(); + } + return; + } + // Done + if (guibutton.id == 80) { + close(); + return; + } + } + + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + if (getTextField(4) != null && getTextField(4).isFocused()) { + if (search.equals(getTextField(4).getText())) + return; + search = getTextField(4).getText().toLowerCase(); + availableScroll.setList(getSearchList()); + availableScroll.resetScroll(); + } + } + + @Override + public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { + if (guiCustomScroll == assignedScroll) { + if (assignedScroll.getSelected() != null) { + Integer id = resolveAssignedId(assignedScroll.getSelected()); + if (id != null && magicData.hasMagic(id)) { + if (splitField != null) { + splitField.setText(magicData.getMagic(id).split + ""); + splitField.enabled = true; + } + } + } + } + } + + @Override + public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { + } + + @Override + public void unFocused(GuiNpcTextField textField) { + if (textField.id == 73) { + if (assignedScroll != null && assignedScroll.getSelected() != null) { + Integer id = resolveAssignedId(assignedScroll.getSelected()); + if (id != null && magicData.hasMagic(id)) { + try { + float split = Float.parseFloat(textField.getText()); + magicData.getMagic(id).split = split; + updateAssignedList(); + } catch (NumberFormatException e) { + } + } + } + } + } + + private List getSearchList() { + if (search.isEmpty()) { + return new ArrayList<>(allMagic.keySet()); + } + List list = new ArrayList<>(); + for (String name : allMagic.keySet()) { + if (name.toLowerCase().contains(search)) + list.add(name); + } + return list; + } + + /** + * Resolves the magic ID from a right-scroll selection (which may be formatted as "Name - XX%"). + */ + private Integer resolveAssignedId(String selection) { + if (selection == null) return null; + String baseName = assignedDisplayToName.get(selection); + if (baseName == null) baseName = selection; + return allMagic.get(baseName); + } + + private void updateAssignedList() { + assignedDisplayToName.clear(); + List selected = new ArrayList<>(); + for (Map.Entry entry : allMagic.entrySet()) { + if (magicData.hasMagic(entry.getValue())) { + float split = magicData.getMagic(entry.getValue()).split; + String display = entry.getKey() + " - " + Math.round(split * 100) + "%"; + assignedDisplayToName.put(display, entry.getKey()); + selected.add(display); + } + } + if (assignedScroll != null) { + assignedScroll.setList(selected); + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/MagicData.java b/src/main/java/noppes/npcs/controllers/data/MagicData.java index fa81eb61e..f70e6ec75 100644 --- a/src/main/java/noppes/npcs/controllers/data/MagicData.java +++ b/src/main/java/noppes/npcs/controllers/data/MagicData.java @@ -76,4 +76,15 @@ public class MagicData implements IMagicData { MagicEntry entry = magics.get(id); return entry == null ? 0 : entry.split; } + + public MagicData copy() { + MagicData copy = new MagicData(); + for (java.util.Map.Entry e : magics.entrySet()) { + MagicEntry entryCopy = new MagicEntry(); + entryCopy.damage = e.getValue().damage; + entryCopy.split = e.getValue().split; + copy.magics.put(e.getKey(), entryCopy); + } + return copy; + } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java index 1c6c28ad2..54bc7fd42 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyAbility.java @@ -6,6 +6,8 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.api.INbt; import noppes.npcs.api.entity.IEnergyAbility; import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.handler.data.IMagicData; +import noppes.npcs.controllers.data.MagicData; import noppes.npcs.scripted.NpcAPI; /** @@ -149,4 +151,16 @@ public abstract class ScriptEnergyAbility extends entity.setCustomDamageData((NBTTagCompound) data.getMCNBT()); } } + + // ==================== MAGIC DATA ==================== + + public IMagicData getMagicData() { + return entity.getMagicData(); + } + + public void setMagicData(IMagicData data) { + if (data instanceof MagicData) { + entity.setMagicData((MagicData) data); + } + } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 9a566859e..fd4923a57 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1791,6 +1791,10 @@ ability.section.explosive=Explosive ability.section.colors=Colors ability.section.effects=Effects ability.section.burst=Burst +ability.section.magic=Magic +ability.magic.note=Magic Note +ability.magic.usesCaster=Uses the Caster's Magic Data +ability.magic.editor=Magic Editor ability.section.dodgeAnimations=Dodge Animations ability.section.visual=Visual ability.section.projectile1=Projectile 1