Counter, Dodge, Guard Abilities [WIP]

This commit is contained in:
Kam committed 2026-02-28 23:23:52 -05:00
1 parent d3d9dd352a
commit fc71fb5a33
12 files changed
+624 -421

No files matched your search

+1 -1
Submodule src/api updated: e504fc4b4a...27a41e3a8e.
@@ -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);
@@ -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.
@@ -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<FieldDef> 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 : "";
}
}
@@ -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).
* <p>
* {@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<FieldDef> 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<FieldDef> 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;
}
}
@@ -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<FieldDef> 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 : "";
}
}
@@ -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<FieldDef> 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<FieldDef> 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);
}
}
+2 -32
View File
@@ -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.
*/
@@ -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;
}
@@ -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);
}
@@ -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<String> it = unlockedAbilities.iterator();
@@ -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