diff --git a/src/api b/src/api index e504fc4b4..27a41e3a8 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit e504fc4b4acd97e97e5c3f5b0850498d7b1fdd95 +Subproject commit 27a41e3a8e55c853ac40242064ee0417102243f6 diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index 5c96d1f56..7acfc96a9 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -21,6 +21,8 @@ import kamkeel.npcs.controllers.data.ability.CustomAbility; import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; import kamkeel.npcs.controllers.data.ability.type.AbilityDash; +import kamkeel.npcs.controllers.data.ability.type.AbilityCounter; +import kamkeel.npcs.controllers.data.ability.type.AbilityDodge; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; import kamkeel.npcs.controllers.data.ability.type.AbilityEffect; @@ -156,6 +158,8 @@ public class AbilityController implements IAbilityHandler { registerType(AbilityShockwave::new); registerType(AbilityGuard::new); + registerType(AbilityCounter::new); + registerType(AbilityDodge::new); registerType(AbilityEffect::new); registerType(AbilityHazard::new); 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 edf222844..5d07d8a27 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -390,11 +390,7 @@ public abstract class Ability implements IAbility, IAbilityAction { * Unlike {@link #cleanup()}, this does NOT kill spawned entities (they may * still be in flight for overlap mode). */ - public void resetForBurst() { - } - - public void onDamageTaken(EntityLivingBase caster, EntityLivingBase attacker, DamageSource source, float damage) { - } + public void resetForBurst() {} /** * Apply damage to an entity with ability hit event support. diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java new file mode 100644 index 000000000..1ef1194ad --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java @@ -0,0 +1,173 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.ability.type.IAbilityCounter; +import noppes.npcs.client.gui.builder.FieldDef; + +import java.util.Arrays; +import java.util.List; + +/** + * Counter ability: Absorbs incoming damage and counter-attacks the attacker. + */ +public class AbilityCounter extends AbilityDefend implements IAbilityCounter { + + private CounterType counterType = CounterType.FLAT; + private float counterValue = 6.0f; + private int counterAnimationId = -1; + private String counterAnimationName = "Ability_Guard_Counter"; + + public AbilityCounter() { + this.typeId = "ability.cnpc.counter"; + this.name = "Counter"; + this.cooldownTicks = 0; + this.windUpTicks = 20; + this.allowedBy = UserType.BOTH; + this.activeSound = "random.anvil_land"; + + this.activeAnimationName = "Ability_Guard_Active"; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/guard.png") + }; + } + + public enum CounterType { + FLAT, + PERCENT; + + @Override + public String toString() { + switch (this) { + case FLAT: + return "ability.counter.flat"; + case PERCENT: + return "ability.counter.percent"; + default: + return name(); + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // DEFEND HOOKS + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void onDefendTick(EntityLivingBase caster, EntityLivingBase target, int tick) { + } + + @Override + protected float performDefend(float amount) { + return amount; + } + + // ═══════════════════════════════════════════════════════════════════ + // OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + @Override + public boolean hasDamage() { + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void writeSubTypeNBT(NBTTagCompound nbt) { + nbt.setString("counterType", counterType.name()); + nbt.setFloat("counterValue", counterValue); + nbt.setInteger("counterAnimationId", counterAnimationId); + nbt.setString("counterAnimationName", resolveAnimationName(counterAnimationId, counterAnimationName)); + } + + @Override + protected void readSubTypeNBT(NBTTagCompound nbt) { + try { + this.counterType = CounterType.valueOf(nbt.getString("counterType")); + } catch (Exception e) { + this.counterType = CounterType.FLAT; + } + this.counterValue = nbt.getFloat("counterValue"); + this.counterAnimationId = nbt.getInteger("counterAnimationId"); + this.counterAnimationName = nbt.getString("counterAnimationName"); + } + + // ═══════════════════════════════════════════════════════════════════ + // GUI + // ═══════════════════════════════════════════════════════════════════ + + @SideOnly(Side.CLIENT) + @Override + protected void getTypeDefinitions(List defs) { + defs.addAll(Arrays.asList( + FieldDef.enumField("gui.type", CounterType.class, this::getCounterTypeEnum, this::setCounterTypeEnum) + .hover("ability.hover.counterType"), + FieldDef.floatField("gui.value", this::getCounterValue, this::setCounterValue), + AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) + )); + + FieldDef.insertAfter(defs, "ability.dazedAnimation", + FieldDef.animSubGui("ability.counterAnimation", + this::getCounterAnimationId, this::setCounterAnimationId, + this::getCounterAnimationName, this::setCounterAnimationName) + .tab("Effects")); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + public CounterType getCounterTypeEnum() { + return counterType; + } + + public void setCounterTypeEnum(CounterType counterType) { + this.counterType = counterType; + } + + @Override + public int getCounterType() { + return counterType.ordinal(); + } + + @Override + public void setCounterType(int type) { + CounterType[] values = CounterType.values(); + this.counterType = type >= 0 && type < values.length ? values[type] : CounterType.FLAT; + } + + @Override + public float getCounterValue() { + return counterValue; + } + + @Override + public void setCounterValue(float counterValue) { + this.counterValue = counterValue; + } + + @Override + public int getCounterAnimationId() { + return counterAnimationId; + } + + @Override + public void setCounterAnimationId(int counterAnimationId) { + this.counterAnimationId = counterAnimationId; + } + + public String getCounterAnimationName() { + return counterAnimationName; + } + + public void setCounterAnimationName(String name) { + this.counterAnimationName = name != null ? name : ""; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java new file mode 100644 index 000000000..7cef96e5c --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java @@ -0,0 +1,240 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.enums.LockMode; +import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.DamageSource; +import noppes.npcs.api.ability.type.IAbilityDefend; +import noppes.npcs.client.gui.builder.FieldDef; + +import java.util.List; + +/** + * Abstract base class for defensive abilities (Guard, Counter, Dodge). + *

+ * {@link #onDefend} handles shared checks (physical melee only) then delegates + * to {@link #performDefend(float)} which subclasses override for their specific behavior. + */ +public abstract class AbilityDefend extends Ability implements IAbilityDefend { + + // ═══════════════════════════════════════════════════════════════════ + // SHARED PERSIST STATE + // ═══════════════════════════════════════════════════════════════════ + + protected int durationTicks = 60; + protected int maxHitAmount = 3; + + // ═══════════════════════════════════════════════════════════════════ + // TRANSIENT RUNTIME STATE + // ═══════════════════════════════════════════════════════════════════ + + protected transient int hitCount; + protected transient EntityLivingBase lastAttacker; + protected transient float lastDamageTaken; + + // ═══════════════════════════════════════════════════════════════════ + // CONSTRUCTOR DEFAULTS + // ═══════════════════════════════════════════════════════════════════ + + protected AbilityDefend() { + this.targetingMode = TargetingMode.SELF; + this.lockMovement = LockMode.ACTIVE; + this.interruptible = false; + this.telegraphType = TelegraphType.NONE; + this.showTelegraph = false; + this.windUpTicks = 0; + } + + // ═══════════════════════════════════════════════════════════════════ + // LIFECYCLE + // ═══════════════════════════════════════════════════════════════════ + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target) { + hitCount = 0; + lastAttacker = null; + lastDamageTaken = 0.0f; + onDefendStart(caster); + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { + onDefendTick(caster, target, tick); + + if (tick >= durationTicks) { + signalCompletion(); + } + } + + @Override + public void reset() { + super.reset(); + hitCount = 0; + lastAttacker = null; + lastDamageTaken = 0.0f; + } + + // ═══════════════════════════════════════════════════════════════════ + // DEFEND HOOK + // ═══════════════════════════════════════════════════════════════════ + + /** + * Called when the caster is hit while defending. + * Checks for physical melee damage only, then delegates to {@link #performDefend(float)}. + * + * @param attacker The entity that hit the caster + * @param source The damage source + * @param amount The original incoming damage + * @return The modified damage to apply (unchanged if not physical melee) + */ + public final float onDefend(EntityLivingBase attacker, DamageSource source, float amount) { + if (!isDefending()) + return amount; + if (attacker == null || source == null) + return amount; + + // Only react to physical melee damage + if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion() || source.isProjectile()) + return amount; + + return performDefend(amount); + } + + /** + * Subclass-specific defend behavior. Called only for physical melee hits while defending. + * + * @param amount The incoming damage + * @return The modified damage the caster should take + */ + protected abstract float performDefend(float amount); + + // ═══════════════════════════════════════════════════════════════════ + // SUBCLASS HOOKS + // ═══════════════════════════════════════════════════════════════════ + + protected void onDefendStart(EntityLivingBase caster) {} + + protected abstract void onDefendTick(EntityLivingBase caster, EntityLivingBase target, int tick); + + /** + * Subclass-specific NBT persistence. + */ + protected abstract void writeSubTypeNBT(NBTTagCompound nbt); + protected abstract void readSubTypeNBT(NBTTagCompound nbt); + + @SideOnly(Side.CLIENT) + protected abstract void getTypeDefinitions(List defs); + + // ═══════════════════════════════════════════════════════════════════ + // STATE QUERIES + // ═══════════════════════════════════════════════════════════════════ + + @Override + public boolean isDefending() { + return isExecuting() && getPhase() == AbilityPhase.ACTIVE; + } + + // ═══════════════════════════════════════════════════════════════════ + // OVERRIDES + // ═══════════════════════════════════════════════════════════════════ + + @Override + public boolean hasDamage() { + return false; + } + + @Override + public boolean allowBurst() { + return false; + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.SELF}; + } + + @Override + public float getTelegraphRadius() { + return 0; + } + + /** + * Suppresses the auto-play of activeSound on ACTIVE phase entry. + * Defend abilities play their sound on hit reaction, not on phase start. + */ + @Override + public boolean keepTelegraphDuringActive() { + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + @Override + public final void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); + nbt.setInteger("maxHitAmount", maxHitAmount); + writeSubTypeNBT(nbt); + } + + @Override + public final void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; + this.maxHitAmount = nbt.hasKey("maxHitAmount") ? nbt.getInteger("maxHitAmount") : 3; + readSubTypeNBT(nbt); + } + + // ═══════════════════════════════════════════════════════════════════ + // GUI + // ═══════════════════════════════════════════════════════════════════ + + @SideOnly(Side.CLIENT) + @Override + public final void getAbilityDefinitions(List defs) { + defs.add(FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 6000)); + defs.add(FieldDef.intField("ability.maxHitAmount", this::getMaxHitAmount, this::setMaxHitAmount).range(0, 100) + .hover("ability.hover.maxHitAmount")); + + getTypeDefinitions(defs); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + @Override + public int getDurationTicks() { + return durationTicks; + } + + @Override + public void setDurationTicks(int ticks) { + this.durationTicks = Math.max(1, ticks); + } + + @Override + public int getMaxHitAmount() { + return maxHitAmount; + } + + @Override + public void setMaxHitAmount(int amount) { + this.maxHitAmount = Math.max(0, amount); + } + + public int getHitCount() { + return hitCount; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDodge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDodge.java new file mode 100644 index 000000000..1b90b00f8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDodge.java @@ -0,0 +1,163 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.enums.UserType; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.ability.type.IAbilityDodge; +import noppes.npcs.client.gui.builder.FieldDef; + +import java.util.List; + +/** + * Dodge ability: Completely cancels incoming attacks and plays a random dodge animation. + */ +public class AbilityDodge extends AbilityDefend implements IAbilityDodge { + + private int dodgeAnimation1Id = -1; + private String dodgeAnimation1Name = ""; + private int dodgeAnimation2Id = -1; + private String dodgeAnimation2Name = ""; + private int dodgeAnimation3Id = -1; + private String dodgeAnimation3Name = ""; + + public AbilityDodge() { + this.typeId = "ability.cnpc.dodge"; + this.name = "Dodge"; + this.cooldownTicks = 0; + this.windUpTicks = 10; + this.allowedBy = UserType.BOTH; + + this.activeAnimationName = ""; + this.defaultIconLayers = new DefaultIconLayer[]{ + new DefaultIconLayer("customnpcs:textures/gui/ability/guard.png") + }; + } + + // ═══════════════════════════════════════════════════════════════════ + // DEFEND HOOKS + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void onDefendTick(EntityLivingBase caster, EntityLivingBase target, int tick) { + } + + @Override + protected float performDefend(float amount) { + return amount; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + @Override + protected void writeSubTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("dodgeAnimation1Id", dodgeAnimation1Id); + nbt.setString("dodgeAnimation1Name", resolveAnimationName(dodgeAnimation1Id, dodgeAnimation1Name)); + nbt.setInteger("dodgeAnimation2Id", dodgeAnimation2Id); + nbt.setString("dodgeAnimation2Name", resolveAnimationName(dodgeAnimation2Id, dodgeAnimation2Name)); + nbt.setInteger("dodgeAnimation3Id", dodgeAnimation3Id); + nbt.setString("dodgeAnimation3Name", resolveAnimationName(dodgeAnimation3Id, dodgeAnimation3Name)); + } + + @Override + protected void readSubTypeNBT(NBTTagCompound nbt) { + this.dodgeAnimation1Id = nbt.getInteger("dodgeAnimation1Id"); + this.dodgeAnimation1Name = nbt.getString("dodgeAnimation1Name"); + this.dodgeAnimation2Id = nbt.getInteger("dodgeAnimation2Id"); + this.dodgeAnimation2Name = nbt.getString("dodgeAnimation2Name"); + this.dodgeAnimation3Id = nbt.getInteger("dodgeAnimation3Id"); + this.dodgeAnimation3Name = nbt.getString("dodgeAnimation3Name"); + } + + // ═══════════════════════════════════════════════════════════════════ + // GUI + // ═══════════════════════════════════════════════════════════════════ + + @SideOnly(Side.CLIENT) + @Override + protected void getTypeDefinitions(List defs) { + defs.add(FieldDef.section("ability.section.dodgeAnimations")); + + FieldDef.insertAfter(defs, "ability.dazedAnimation", + FieldDef.animSubGui("ability.dodgeAnimation1", + this::getDodgeAnimation1Id, this::setDodgeAnimation1Id, + this::getDodgeAnimation1Name, this::setDodgeAnimation1Name) + .tab("Effects")); + + FieldDef.insertAfter(defs, "ability.dodgeAnimation1", + FieldDef.animSubGui("ability.dodgeAnimation2", + this::getDodgeAnimation2Id, this::setDodgeAnimation2Id, + this::getDodgeAnimation2Name, this::setDodgeAnimation2Name) + .tab("Effects")); + + FieldDef.insertAfter(defs, "ability.dodgeAnimation2", + FieldDef.animSubGui("ability.dodgeAnimation3", + this::getDodgeAnimation3Id, this::setDodgeAnimation3Id, + this::getDodgeAnimation3Name, this::setDodgeAnimation3Name) + .tab("Effects")); + + FieldDef.modifyVisibility(defs, "ability.windUpAnimation", () -> false); + FieldDef.modifyVisibility(defs, "ability.windUpSound", () -> false); + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ + + @Override + public int getDodgeAnimation1Id() { + return dodgeAnimation1Id; + } + + @Override + public void setDodgeAnimation1Id(int animationId) { + this.dodgeAnimation1Id = animationId; + } + + public String getDodgeAnimation1Name() { + return dodgeAnimation1Name; + } + + public void setDodgeAnimation1Name(String name) { + this.dodgeAnimation1Name = name != null ? name : ""; + } + + @Override + public int getDodgeAnimation2Id() { + return dodgeAnimation2Id; + } + + @Override + public void setDodgeAnimation2Id(int animationId) { + this.dodgeAnimation2Id = animationId; + } + + public String getDodgeAnimation2Name() { + return dodgeAnimation2Name; + } + + public void setDodgeAnimation2Name(String name) { + this.dodgeAnimation2Name = name != null ? name : ""; + } + + @Override + public int getDodgeAnimation3Id() { + return dodgeAnimation3Id; + } + + @Override + public void setDodgeAnimation3Id(int animationId) { + this.dodgeAnimation3Id = animationId; + } + + public String getDodgeAnimation3Name() { + return dodgeAnimation3Name; + } + + public void setDodgeAnimation3Name(String name) { + this.dodgeAnimation3Name = name != null ? name : ""; + } +} 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 cdac1f9d6..f9d0ab257 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 @@ -2,288 +2,76 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; -import kamkeel.npcs.controllers.data.ability.enums.LockMode; -import kamkeel.npcs.controllers.data.ability.enums.TargetingMode; import kamkeel.npcs.controllers.data.ability.enums.UserType; -import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; -import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.DamageSource; -import net.minecraft.world.World; import noppes.npcs.api.ability.type.IAbilityGuard; -import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.client.gui.builder.FieldDef; -import noppes.npcs.controllers.AnimationController; -import noppes.npcs.controllers.data.Animation; -import noppes.npcs.controllers.data.PlayerData; -import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.util.ValueUtil; -import java.util.Arrays; import java.util.List; /** * Guard ability: Defensive stance that reduces incoming damage. - * Can optionally counter-attack when hit. */ -public class AbilityGuard extends Ability implements IAbilityGuard { +public class AbilityGuard extends AbilityDefend implements IAbilityGuard { - private int durationTicks = 60; private float damageReduction = 0.5f; - private AbilityGuard.CounterType counterType = AbilityGuard.CounterType.FLAT; - private float counterValue = 6.0f; - private String counterSound = "random.anvil_land"; - private boolean canCounter = false; - private int counterWindow = 10; - private int counterAnimationId = -1; - private String counterAnimationName; - - // Runtime state - private transient EntityLivingBase lastAttacker; - private transient boolean counterTriggered; - private transient boolean counterEligible; - private transient float lastDamageTaken; public AbilityGuard() { this.typeId = "ability.cnpc.guard"; this.name = "Guard"; - this.targetingMode = TargetingMode.SELF; - this.lockMovement = LockMode.ACTIVE; this.cooldownTicks = 0; - this.windUpTicks = 0; - this.interruptible = false; // Hard to interrupt while guarding - // No telegraph for guard - it's a defensive stance - this.telegraphType = TelegraphType.NONE; - this.showTelegraph = false; - this.activeSound = "random.anvil_use"; this.allowedBy = UserType.BOTH; this.activeAnimationName = "Ability_Guard_Active"; - this.counterAnimationName = "Ability_Guard_Counter"; this.defaultIconLayers = new DefaultIconLayer[]{ new DefaultIconLayer("customnpcs:textures/gui/ability/guard.png") }; } + // ═══════════════════════════════════════════════════════════════════ + // DEFEND HOOKS + // ═══════════════════════════════════════════════════════════════════ + @Override - public boolean hasDamage() { - return canCounter; + protected void onDefendTick(EntityLivingBase caster, EntityLivingBase target, int tick) { } @Override - public boolean allowBurst() { - return false; + protected float performDefend(float amount) { + return amount; } - public enum CounterType { - FLAT, - PERCENT; - - @Override - public String toString() { - switch (this) { - case FLAT: - return "ability.counter.flat"; - case PERCENT: - return "ability.counter.percent"; - default: - return name(); - } - } - } + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ @Override - public boolean isTargetingModeLocked() { - return true; - } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[]{TargetingMode.SELF}; - } - - @Override - public void onExecute(EntityLivingBase caster, EntityLivingBase target) { - lastAttacker = null; - counterTriggered = false; - lastDamageTaken = 0.0f; - counterEligible = true; - } - - @Override - public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { - if (canCounter && counterEligible && counterTriggered && lastAttacker != null && !caster.worldObj.isRemote) { - performCounter(caster, caster.worldObj); - } - - if (tick > counterWindow && counterEligible) { - counterEligible = false; - } - - if (tick >= durationTicks) { - signalCompletion(); - return; - } - - // Just enough to see the counter animation - if (counterTriggered && tick >= counterWindow + 10) { - signalCompletion(); - } - } - - public void onDamageTaken(EntityLivingBase caster, EntityLivingBase attacker, DamageSource source, float damage) { - if (!isGuarding() || attacker == null) return; - - lastAttacker = attacker; - lastDamageTaken = damage; - - // Players: damage reduction handled via LivingHurtEvent (event.ammount modification) - // NPCs: called AFTER damageEntity() so health was already reduced — heal back here - if (caster instanceof EntityPlayer) { - if (canCounter && counterEligible && !counterTriggered && isDirectHit(source)) { - counterTriggered = true; - } - } else { - float reduction = ValueUtil.clamp(getDamageReductionFactor(), 0, damage); - float newHealth = ValueUtil.clamp(caster.getHealth() + reduction, 0, caster.getMaxHealth()); - - if (canCounter && counterEligible && !counterTriggered && isDirectHit(source)) { - caster.setHealth(ValueUtil.clamp(caster.getHealth() + damage, 0, caster.getMaxHealth())); - counterTriggered = true; - } else { - caster.setHealth(newHealth); - } - } - } - - private boolean isDirectHit(DamageSource source) { - if (source == null) return false; - if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion()) return false; - return source.getEntity() instanceof EntityLivingBase; - } - - private void performCounter(EntityLivingBase caster, World world) { - float counterDamage = counterType == AbilityGuard.CounterType.PERCENT - ? lastDamageTaken * (counterValue / 100.0f) - : counterValue; - - boolean wasHit = applyAbilityDamage(caster, lastAttacker, counterDamage, 0.5f); - if (wasHit && counterSound != null && !counterSound.isEmpty()) { - world.playSoundAtEntity(caster, counterSound, 1.0f, 1.2f); - } - // Use shared animation utility - only available for NPCs - if (caster instanceof EntityNPCInterface) { - if (getCounterAnimation() instanceof IAnimation) { - ((EntityNPCInterface) caster).abilities.playAbilityAnimation((Animation) getCounterAnimation()); - } - } else if (caster instanceof EntityPlayer) { - if (getCounterAnimation() instanceof IAnimation) { - PlayerData.get((EntityPlayer) caster).abilityData.playAbilityAnimation((Animation) getCounterAnimation()); - } - } - - counterEligible = false; - lastAttacker = null; - lastDamageTaken = 0.0f; - } - - private IAnimation getCounterAnimation() { - if (AnimationController.Instance == null) return null; - - if (counterAnimationId >= 0) { - return AnimationController.Instance.get(counterAnimationId); - } - - if (counterAnimationName != null && !counterAnimationName.isEmpty()) { - return AnimationController.Instance.get(counterAnimationName, true); - } - - return null; - } - - /** - * Get the damage reduction factor while guarding. - * Returns 0.5 for 50% reduction. - */ - public float getDamageReductionFactor() { - if (isExecuting() && getPhase() == AbilityPhase.ACTIVE) { - return damageReduction; - } - return 0.0f; - } - - /** - * Check if the caster is currently in guard stance. - */ - public boolean isGuarding() { - return isExecuting() && getPhase() == AbilityPhase.ACTIVE; - } - - @Override - public void reset() { - super.reset(); - lastAttacker = null; - counterTriggered = false; - lastDamageTaken = 0.0f; - } - - @Override - public float getTelegraphRadius() { - return 0; // No telegraph for guard - } - - @Override - public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setInteger("durationTicks", durationTicks); + protected void writeSubTypeNBT(NBTTagCompound nbt) { nbt.setFloat("damageReduction", damageReduction); - nbt.setBoolean("canCounter", canCounter); - nbt.setInteger("counterWindow", counterWindow); - nbt.setString("counterType", counterType.name()); - nbt.setFloat("counterValue", counterValue); - nbt.setString("counterSound", counterSound); - nbt.setInteger("counterAnimationId", counterAnimationId); - nbt.setString("counterAnimationName", resolveAnimationName(counterAnimationId, counterAnimationName)); } @Override - public void readTypeNBT(NBTTagCompound nbt) { - this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; + protected void readSubTypeNBT(NBTTagCompound nbt) { this.damageReduction = nbt.hasKey("damageReduction") ? nbt.getFloat("damageReduction") : 0.5f; - this.canCounter = nbt.getBoolean("canCounter"); - this.counterWindow = nbt.getInteger("counterWindow"); - try { - this.counterType = CounterType.valueOf(nbt.getString("counterType")); - } catch (Exception e) { - this.counterType = CounterType.FLAT; - } - this.counterValue = nbt.getFloat("counterValue"); - this.counterSound = nbt.getString("counterSound"); - this.counterAnimationId = nbt.getInteger("counterAnimationId"); - this.counterAnimationName = nbt.getString("counterAnimationName"); } - // Getters & Setters - public int getDurationTicks() { - return durationTicks; - } - - public void setDurationTicks(int durationTicks) { - this.durationTicks = Math.max(1, durationTicks); - } + // ═══════════════════════════════════════════════════════════════════ + // GUI + // ═══════════════════════════════════════════════════════════════════ + @SideOnly(Side.CLIENT) @Override - public int getCounterWindow() { - return this.counterWindow; + protected void getTypeDefinitions(List defs) { + defs.add(FieldDef.floatField("ability.damageReduction", this::getDamageReduction, this::setDamageReduction)); + + FieldDef.modifyVisibility(defs, "ability.windUpAnimation", () -> false); + FieldDef.modifyVisibility(defs, "ability.windUpSound", () -> false); } - @Override - public void setCounterWindow(int ticks) { - this.counterWindow = Math.min(durationTicks, ticks); - } + // ═══════════════════════════════════════════════════════════════════ + // GETTERS / SETTERS + // ═══════════════════════════════════════════════════════════════════ @Override public float getDamageReduction() { @@ -294,105 +82,4 @@ public class AbilityGuard extends Ability implements IAbilityGuard { public void setDamageReduction(float damageReduction) { this.damageReduction = damageReduction; } - - - @Override - public boolean canCounter() { - return canCounter; - } - - public void setCanCounter(boolean canCounter) { - this.canCounter = canCounter; - } - - public CounterType getCounterTypeEnum() { - return counterType; - } - - public void setCounterTypeEnum(CounterType counterType) { - this.counterType = counterType; - } - - @Override - public int getCounterType() { - return counterType.ordinal(); - } - - @Override - public void setCounterType(int type) { - CounterType[] values = CounterType.values(); - this.counterType = type >= 0 && type < values.length ? values[type] : CounterType.FLAT; - } - - public float getCounterValue() { - return counterValue; - } - - public void setCounterValue(float counterValue) { - this.counterValue = counterValue; - } - - public String getCounterSound() { - return counterSound; - } - - public void setCounterSound(String counterSound) { - this.counterSound = counterSound; - } - - public int getCounterAnimationId() { - return counterAnimationId; - } - - public void setCounterAnimationId(int counterAnimationId) { - this.counterAnimationId = counterAnimationId; - } - - public boolean isCounterTriggered() { - return counterTriggered; - } - - public String getCounterAnimationName() { - return counterAnimationName; - } - - public void setCounterAnimationName(String name) { - this.counterAnimationName = name != null ? name : ""; - } - - @SideOnly(Side.CLIENT) - @Override - public void getAbilityDefinitions(List defs) { - defs.addAll(Arrays.asList( - FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 1000), - FieldDef.floatField("ability.damageReduction", this::getDamageReduction, this::setDamageReduction), - FieldDef.section("ability.section.counter"), - FieldDef.boolField("gui.enabled", this::canCounter, this::setCanCounter).hover("ability.hover.canCounter"), - FieldDef.enumField("gui.type", CounterType.class, this::getCounterTypeEnum, this::setCounterTypeEnum) - .hover("ability.hover.counterType").visibleWhen(this::canCounter), - FieldDef.row( - FieldDef.floatField("gui.value", this::getCounterValue, this::setCounterValue) - .visibleWhen(this::canCounter), - FieldDef.intField("ability.counterWindow", this::getCounterWindow, this::setCounterWindow) - .visibleWhen(this::canCounter) - ), - FieldDef.stringField("gui.sound", this::getCounterSound, this::setCounterSound) - .visibleWhen(this::canCounter), - AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects) - - )); - - FieldDef.insertAfter(defs, "ability.activeSound", - FieldDef.soundSubGui("ability.counterSound", this::getCounterSound, this::setCounterSound) - .tab("Effects").visibleWhen(this::canCounter)); - - FieldDef.insertAfter(defs, "ability.dazedAnimation", - FieldDef.animSubGui("ability.counterAnimation", - this::getCounterAnimationId, this::setCounterAnimationId, - this::getCounterAnimationName, this::setCounterAnimationName) - .tab("Effects").visibleWhen(this::canCounter)); - - FieldDef.modifyVisibility(defs, "ability.windUpAnimation", () -> false); - FieldDef.modifyVisibility(defs, "ability.windUpSound", () -> false); - } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 012b66ec6..e56bf772d 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -7,10 +7,11 @@ import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; import kamkeel.npcs.controllers.data.ability.data.IAbilityAction; import kamkeel.npcs.controllers.data.ability.data.entry.AbilityToggleEntry; -import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; +import kamkeel.npcs.controllers.data.ability.type.AbilityDefend; 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.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -516,11 +517,6 @@ public class DataAbilities extends AbstractDataAbilities { // Track hit for hit count condition recordHit(); - - net.minecraft.entity.Entity sourceEntity = source.getEntity(); - EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null; - currentAbility.onDamageTaken(npc, attacker, source, amount); - if (currentAbility.canInterrupt(source)) { interruptCurrentAbility(source, amount); return true; @@ -529,18 +525,6 @@ public class DataAbilities extends AbstractDataAbilities { return false; } - /** - * Get the damage reduction factor if a Guard ability is currently active. - * - * @return The damage reduction factor (0.0 = no reduction, 1.0 = full immunity), or 0 if not guarding - */ - public float getGuardDamageReduction() { - if (currentAbility instanceof AbilityGuard) { - return ((AbilityGuard) currentAbility).getDamageReductionFactor(); - } - return 0.0f; - } - /** * Record a hit for the hit count condition. * Includes periodic cleanup to prevent memory leak. @@ -650,20 +634,6 @@ public class DataAbilities extends AbstractDataAbilities { // SCRIPT EVENTS // ═══════════════════════════════════════════════════════════════════ - /** - * Fire an ability hit event. Called by abilities when they hit an entity. - * Returns null if the event was cancelled, otherwise returns the (possibly modified) event. - * - * @param ability The ability doing the hit - * @param target The original target of the ability - * @param hitEntity The entity being hit - * @param event The mutable hit event to fire - * @return true if the event was cancelled - */ - public boolean fireHitEvent(Ability ability, AbilityEvent.HitEvent event) { - return EventHooks.onAbilityHit(ability, event); - } - /** * Get the NPC this DataAbilities belongs to. */ diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 3da3ccc56..abb9939a7 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -596,6 +596,7 @@ public class ScriptPlayerEventHandler { if (event.entityLiving.worldObj instanceof WorldServer) { boolean cancel = event.isCanceled(); Entity source = NoppesUtilServer.GetDamageSource(event.source); + if (event.entityLiving instanceof EntityPlayerMP) { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.entityLiving); noppes.npcs.scripted.event.player.PlayerEvent.AttackedEvent pevent = new noppes.npcs.scripted.event.player.PlayerEvent.AttackedEvent((IPlayer) NpcAPI.Instance().getIEntity((EntityPlayer) event.entityLiving), source, event.ammount, event.source); @@ -645,20 +646,21 @@ public class ScriptPlayerEventHandler { } if (event.entityLiving instanceof EntityPlayerMP) { - // Handle ability damage interactions (Guard counter, damage reduction) + // Handle ability damage interactions PlayerData pData = PlayerData.get((EntityPlayer) event.entityLiving); if (pData != null && pData.abilityData != null && pData.abilityData.isExecutingAbility()) { if (pData.abilityData.isCurrentAbilityInvulnerable()) { event.ammount = 0; cancel = true; } else { - event.ammount = pData.abilityData.onDamage(event.source, event.ammount); + float modified = pData.abilityData.onDamage(event.source, event.ammount); + event.ammount = modified; } } PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.entityLiving); noppes.npcs.scripted.event.player.PlayerEvent.DamagedEvent pevent = new noppes.npcs.scripted.event.player.PlayerEvent.DamagedEvent((IPlayer) NpcAPI.Instance().getIEntity((EntityPlayer) event.entityLiving), source, event.ammount, event.source); - cancel = EventHooks.onPlayerDamaged(handler, pevent); + cancel = cancel || EventHooks.onPlayerDamaged(handler, pevent); event.ammount = pevent.damage; } diff --git a/src/main/java/noppes/npcs/ai/CombatHandler.java b/src/main/java/noppes/npcs/ai/CombatHandler.java index 97d9a90eb..6947a986e 100644 --- a/src/main/java/noppes/npcs/ai/CombatHandler.java +++ b/src/main/java/noppes/npcs/ai/CombatHandler.java @@ -79,16 +79,6 @@ public class CombatHandler { aggressors.put(el, f + damageAmount); } - // Handle guard counter before interruption - -// if (npc.abilities.getCurrentAbility() instanceof AbilityGuard) { -// AbilityGuard guard = -// (AbilityGuard) npc.abilities.getCurrentAbility(); -// if (guard.isGuarding() && e instanceof EntityLivingBase) { -// guard.onDamageTaken(npc, (EntityLivingBase) e, source, damageAmount); -// } -// } - // Check for ability interruption npc.abilities.onDamage(source, damageAmount); } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index dc894dec6..600926785 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -7,7 +7,6 @@ import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase; import kamkeel.npcs.controllers.data.ability.data.ChainedAbility; import kamkeel.npcs.controllers.data.ability.data.IAbilityAction; import kamkeel.npcs.controllers.data.ability.data.entry.AbilityToggleEntry; -import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.ability.AbilityCooldownSyncPacket; import kamkeel.npcs.network.packets.data.ability.PlayerAbilityStatePacket; @@ -787,32 +786,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA return 0; } - net.minecraft.entity.Entity sourceEntity = source.getEntity(); - EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null; - - // Track counter state before calling onDamageTaken - boolean wasCounterTriggered = false; - if (currentAbility instanceof AbilityGuard) { - wasCounterTriggered = ((AbilityGuard) currentAbility).isCounterTriggered(); - } - - currentAbility.onDamageTaken(playerData.player, attacker, source, amount); - - // Guard: handle damage reduction and counter absorption - if (currentAbility instanceof AbilityGuard) { - AbilityGuard guard = (AbilityGuard) currentAbility; - if (guard.isGuarding()) { - // Counter just triggered — absorb full damage - if (!wasCounterTriggered && guard.isCounterTriggered()) { - return 0; - } - // Normal guard — apply flat damage reduction - float reduction = guard.getDamageReductionFactor(); - return Math.max(0, amount - reduction); - } - } - - // Check for ability interruption if (currentAbility != null && currentAbility.canInterrupt(source)) { interruptCurrentAbility(source, amount); } @@ -1027,9 +1000,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA */ private void validateUnlockedAbilities() { if (AbilityController.Instance == null) return; - // Only validate on the server - the client trusts server-sent data. - // Custom ability definitions may not have been synced to the client yet, - // which would cause valid abilities to be incorrectly pruned. if (playerData == null || !(playerData.player instanceof EntityPlayerMP)) return; Iterator it = unlockedAbilities.iterator(); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 53debe9b1..bdaa2abdc 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1713,6 +1713,8 @@ ability.cnpc.teleport=Teleport ability.cnpc.vortex=Vortex ability.cnpc.shockwave=Shockwave ability.cnpc.guard=Guard +ability.cnpc.counter=Counter +ability.cnpc.dodge=Dodge ability.cnpc.effect=Effect ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap @@ -1763,6 +1765,9 @@ ability.windUpAnimation=Wind Up Animation ability.activeAnimation=Active Animation ability.dazedAnimation=Dazed Animation ability.counterAnimation=Counter Animation +ability.dodgeAnimation1=Dodge Animation 1 +ability.dodgeAnimation2=Dodge Animation 2 +ability.dodgeAnimation3=Dodge Animation 3 ability.showTelegraph=Show Telegraph ability.windUpColor=Wind Up Color ability.activeColor=Active Color @@ -1785,6 +1790,7 @@ ability.section.explosive=Explosive ability.section.colors=Colors ability.section.effects=Effects ability.section.burst=Burst +ability.section.dodgeAnimations=Dodge Animations ability.section.visual=Visual ability.section.projectile1=Projectile 1 ability.section.projectile2=Projectile 2 @@ -1915,9 +1921,9 @@ ability.maxTargets=Max Targets # Shockwave ability.pushStrength=Push Strength -# Guard +# Defend (Guard / Counter / Dodge) +ability.maxHitAmount=Max Hit Amount ability.damageReduction=Damage Reduction -ability.counterWindow=Counter Ticks # Effect ability.instantHeal=Instant @@ -2061,6 +2067,8 @@ ability.hover.multiHitDelay=Ticks between repeat hits on the same entity when Hi ability.hover.maxHits=Maximum total hits before projectile ends in Pierce/Multi modes. Lower values reduce repeated hit spam and improve performance. ability.hover.explosive=Creates explosion on impact ability.hover.explosionRad=Size of explosion radius +ability.hover.maxHitAmount=Maximum hits absorbed before defend ability ends (0 = unlimited) +ability.hover.counterType=Flat: fixed damage counter-attack. Percent: counter-attack deals a percentage of the absorbed damage. ability.hover.piercing=Hits multiple targets instead of stopping on first hit ability.hover.lightning=Add electrical effect to visuals ability.hover.outerGlow=Enable outer glow layer around projectile