diff --git a/src/api b/src/api index b28a7a5c4..f969b9f54 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit b28a7a5c4d025ec3d5207cd8d4714f14c3a5de72 +Subproject commit f969b9f5403836489c815cef025d0a55b821eaf3 diff --git a/src/main/java/kamkeel/npcs/addon/DBCAddon.java b/src/main/java/kamkeel/npcs/addon/DBCAddon.java index ff290e9d2..08c493a3c 100644 --- a/src/main/java/kamkeel/npcs/addon/DBCAddon.java +++ b/src/main/java/kamkeel/npcs/addon/DBCAddon.java @@ -37,6 +37,15 @@ public class DBCAddon { public void doDBCDamage(EntityNPCInterface npc, float attackStrength, Entity receiver) { } + /** + * Returns the pre-calculated DBC attack damage of the current attacker, + * or the vanilla damage if DBC damage was not calculated. + * Used by Counter (PERCENT mode) to base counter damage on the full DBC attack, not vanilla base. + */ + public float getAttackerDBCDamage(float vanillaDamage) { + return vanillaDamage; + } + public boolean isKO(EntityNPCInterface npc, EntityPlayer player) { return false; } diff --git a/src/main/java/kamkeel/npcs/controllers/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/AbilityController.java index 7acfc96a9..7840621f7 100644 --- a/src/main/java/kamkeel/npcs/controllers/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/AbilityController.java @@ -395,7 +395,8 @@ public class AbilityController implements IAbilityHandler { // Clean up online players' unlocked ability lists if (PlayerDataController.Instance != null) { - for (PlayerData pData : PlayerDataController.Instance.getAllPlayerData()) { + List snapshot = new ArrayList<>(PlayerDataController.Instance.getAllPlayerData()); + for (PlayerData pData : snapshot) { if (pData.abilityData != null) { boolean changed = false; if (uuid != null && !uuid.isEmpty() && pData.abilityData.hasUnlockedAbility(uuid)) { @@ -437,7 +438,7 @@ public class AbilityController implements IAbilityHandler { return customAbilities; } - public void setCustomAbilities(Map synced) { + public synchronized void setCustomAbilities(Map synced) { customAbilities.clear(); customAbilitiesById.clear(); customAbilities.putAll(synced); @@ -613,7 +614,8 @@ public class AbilityController implements IAbilityHandler { // Clean up online players' unlocked ability lists (chain keys use "chain:" prefix) if (PlayerDataController.Instance != null) { - for (PlayerData pData : PlayerDataController.Instance.getAllPlayerData()) { + List snapshot = new ArrayList<>(PlayerDataController.Instance.getAllPlayerData()); + for (PlayerData pData : snapshot) { if (pData.abilityData != null) { String chainKey = "chain:" + (uuid != null && !uuid.isEmpty() ? uuid : name); if (pData.abilityData.hasUnlockedAbility(chainKey)) { @@ -722,7 +724,7 @@ public class AbilityController implements IAbilityHandler { return chainedAbilityRevision; } - public void setChainedAbilities(Map synced) { + public synchronized void setChainedAbilities(Map synced) { chainedAbilities.clear(); chainedAbilitiesById.clear(); chainedAbilities.putAll(synced); 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 0b78a5b2f..70e286298 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -1531,7 +1531,7 @@ public abstract class Ability implements IAbility, IAbilityAction { dazedAnimationId = nbt.hasKey("dazedAnimationId") ? nbt.getInteger("dazedAnimationId") : -1; windUpAnimationName = nbt.getString("windUpAnimationName"); activeAnimationName = nbt.getString("activeAnimationName"); - dazedAnimationName = nbt.getString("dazedAnimationName"); + dazedAnimationName = nbt.hasKey("dazedAnimationName") ? nbt.getString("dazedAnimationName") : "Ability_Generic_Dazed"; showTelegraph = nbt.hasKey("showTelegraph") ? nbt.getBoolean("showTelegraph") : true; try { telegraphType = TelegraphType.valueOf(nbt.getString("telegraphType")); @@ -2356,7 +2356,7 @@ public abstract class Ability implements IAbility, IAbilityAction { } // ═══════════════════════════════════════════════════════════════════ - // SHIT FROM THE ASS + // WINDUP DURATION CALCULATION // ═══════════════════════════════════════════════════════════════════ private int calculateWindupFromAnimation() { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java index 571b2fd35..5c156a056 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAction.java @@ -308,6 +308,7 @@ public class AbilityAction { } // Inline ability + if (AbilityController.Instance == null) return null; Ability ability = AbilityController.Instance.fromNBT(nbt); if (ability == null) return null; return inline(ability); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java index c59eb9765..045a12e5d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/CustomAbility.java @@ -126,7 +126,7 @@ public class CustomAbility extends Ability implements ICustomAbility { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.durationTicks = nbt.getInteger("durationTicks"); + this.durationTicks = Math.max(0, nbt.getInteger("durationTicks")); } // ==================== GUI ==================== diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHitCount.java b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHitCount.java index 5d02d51a7..f94d465e1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHitCount.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionHitCount.java @@ -76,7 +76,7 @@ public class ConditionHitCount extends AbilityCondition { @Override public void readTypeNBT(NBTTagCompound nbt) { - requiredHits = nbt.getInteger("requiredHits"); - withinTicks = nbt.getInteger("withinTicks"); + requiredHits = Math.max(0, nbt.getInteger("requiredHits")); + withinTicks = Math.max(0, nbt.getInteger("withinTicks")); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java index 552bb5de9..221a68f63 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionItem.java @@ -174,7 +174,7 @@ public class ConditionItem extends AbilityCondition { itemName = nbt.getString("itemName"); usageType = UsageType.fromOrdinal(nbt.getInteger("usageType")); armorSlot = ArmorSlot.fromOrdinal(nbt.getInteger("armorSlot")); - requiredCount = nbt.getInteger("requiredCount"); + requiredCount = Math.max(1, nbt.getInteger("requiredCount")); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java index 263cd28af..2955b7610 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/conditions/ConditionQuestCompleted.java @@ -30,7 +30,9 @@ public class ConditionQuestCompleted extends AbilityCondition { if (questId < 0) return false; EntityPlayer player = (EntityPlayer) entity; - return PlayerData.get(player).questData.hasFinishedQuest(questId); + PlayerData data = PlayerData.get(player); + if (data == null || data.questData == null) return false; + return data.questData.hasFinishedQuest(questId); } @SideOnly(Side.CLIENT) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java index e3aa63346..cfc90d38f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/ChainedAbility.java @@ -289,9 +289,15 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { */ public boolean checkConditions(EntityLivingBase caster, EntityLivingBase target) { for (AbilityCondition c : conditions) { - if (!c.check(caster, target)) { - return false; + if (!c.getUserType().allowsNpc()) continue; + Boolean extendedCondition = AbilityController.Instance.fireCheckConditions(c, caster, target); + + if (extendedCondition != null) { + if (!extendedCondition) return false; + continue; } + + if (!c.check(caster, target)) return false; } return true; } @@ -301,10 +307,16 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction { */ public boolean checkConditionsForPlayer(EntityLivingBase caster) { for (AbilityCondition c : conditions) { + if (!c.getUserType().allowsPlayer()) continue; if (c.requiresTarget()) continue; - if (!c.check(caster, null)) { - return false; + Boolean extendedCondition = AbilityController.Instance.fireCheckConditionsForPlayer(c, caster); + + if (extendedCondition != null) { + if (!extendedCondition) return false; + continue; } + + if (!c.check(caster, null)) return false; } return true; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyAnchorData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyAnchorData.java index b4ceb830f..f618ae3f7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyAnchorData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyAnchorData.java @@ -9,6 +9,7 @@ public class EnergyAnchorData implements IEnergyAnchorData { public float anchorOffsetX = 0; public float anchorOffsetY = 0; public float anchorOffsetZ = 0; + public boolean launchFromAnchor = false; public EnergyAnchorData() { } @@ -83,11 +84,22 @@ public class EnergyAnchorData implements IEnergyAnchorData { this.anchorOffsetZ = anchorOffsetZ; } + @Override + public boolean getLaunchFromAnchor() { + return launchFromAnchor; + } + + @Override + public void setLaunchFromAnchor(boolean launchFromAnchor) { + this.launchFromAnchor = launchFromAnchor; + } + public void writeNBT(NBTTagCompound nbt) { nbt.setInteger("anchorPoint", anchorPoint.ordinal()); nbt.setFloat("anchorOffsetX", anchorOffsetX); nbt.setFloat("anchorOffsetY", anchorOffsetY); nbt.setFloat("anchorOffsetZ", anchorOffsetZ); + nbt.setBoolean("launchFromAnchor", launchFromAnchor); } public void readNBT(NBTTagCompound nbt) { @@ -95,9 +107,12 @@ public class EnergyAnchorData implements IEnergyAnchorData { this.anchorOffsetX = nbt.getFloat("anchorOffsetX"); this.anchorOffsetY = nbt.getFloat("anchorOffsetY"); this.anchorOffsetZ = nbt.getFloat("anchorOffsetZ"); + this.launchFromAnchor = nbt.getBoolean("launchFromAnchor"); } public EnergyAnchorData copy() { - return new EnergyAnchorData(anchorPoint, anchorOffsetX, anchorOffsetY, anchorOffsetZ); + EnergyAnchorData copy = new EnergyAnchorData(anchorPoint, anchorOffsetX, anchorOffsetY, anchorOffsetZ); + copy.launchFromAnchor = this.launchFromAnchor; + return copy; } } 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 index f0d5ab4b6..b13d78ce8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCounter.java @@ -9,10 +9,7 @@ import kamkeel.npcs.controllers.data.ability.gui.AbilityFieldDefs; import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.DamageSource; -import noppes.npcs.NpcDamageSource; import noppes.npcs.controllers.data.Animation; -import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityCounter; import noppes.npcs.client.gui.builder.FieldDef; @@ -66,27 +63,24 @@ public class AbilityCounter extends AbilityDefend implements IAbilityCounter { @Override protected float performDefend(EntityLivingBase attacker, float amount) { + // For PERCENT mode with DBC, use the full DBC-calculated damage, not vanilla base + float incomingDamage = amount; + if (DBCAddon.IsAvailable() && attacker instanceof EntityPlayer) { + incomingDamage = DBCAddon.instance.getAttackerDBCDamage(amount); + } + // Calculate counter damage float damage; if (counterType == CounterType.FLAT) { damage = counterValue; } else { - damage = amount * (counterValue / 100.0f); + damage = incomingDamage * (counterValue / 100.0f); } - // Hit back - if (caster != null && attacker.isEntityAlive()) { - if (caster instanceof EntityNPCInterface && DBCAddon.instance.canDBCAttack((EntityNPCInterface) caster, damage, attacker)) { - // NPC with DBC: dummy hit for events/knockback, then apply DBC damage with NPC's stats - attacker.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), 0.001f); - DBCAddon.instance.doDBCDamage((EntityNPCInterface) caster, damage, attacker); - } else { - // Player or non-DBC NPC: normal damage - DamageSource counterSource = caster instanceof EntityPlayer - ? DamageSource.causePlayerDamage((EntityPlayer) caster) - : DamageSource.causeMobDamage(caster); - attacker.attackEntityFrom(counterSource, damage); - } + // Counter-attack through the standard ability damage pipeline. + // DBCAbilityExtender handles DBC stat routing (ignore flags, scaling, etc.) automatically. + if (caster != null && attacker.isEntityAlive() && damage > 0) { + applyAbilityDamage(caster, attacker, damage, 0); } return 0; 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 index c9f6e1e0a..7ca8aa3e7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDefend.java @@ -41,6 +41,10 @@ public abstract class AbilityDefend extends Ability implements IAbilityDefend { protected transient float lastDamageTaken; protected transient Animation pendingDefendAnimation; protected transient int defendAnimEndTick = -1; + /** Prevents double-counting hits when multiple damage paths call onDefend for the same hit in a single tick. */ + protected transient long lastDefendTick = -1; + /** Defers signalCompletion to the next tick so all damage paths for the last hit still see isDefending()=true. */ + protected transient boolean pendingCompletion; // ═══════════════════════════════════════════════════════════════════ // CONSTRUCTOR DEFAULTS @@ -69,7 +73,7 @@ public abstract class AbilityDefend extends Ability implements IAbilityDefend { @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) { - if (tick >= durationTicks) { + if (pendingCompletion || tick >= durationTicks) { signalCompletion(); } } @@ -83,6 +87,8 @@ public abstract class AbilityDefend extends Ability implements IAbilityDefend { lastDamageTaken = 0.0f; pendingDefendAnimation = null; defendAnimEndTick = -1; + lastDefendTick = -1; + pendingCompletion = false; } // ═══════════════════════════════════════════════════════════════════ @@ -99,30 +105,47 @@ public abstract class AbilityDefend extends Ability implements IAbilityDefend { * @return The modified damage to apply (unchanged if not physical melee) */ public final float onDefend(EntityLivingBase attacker, DamageSource source, float amount) { - if (!isDefending()) + if (!isDefending()) { return amount; - if (attacker == null || source == null) + } + if (attacker == null || source == null) { return amount; - - // Only react to physical melee damage - if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion() || source.isProjectile()) - return amount; - - lastAttacker = attacker; - lastDamageTaken = amount; - hitCount++; - - float result = performDefend(attacker, amount); - - // Queue defend reaction animation (played on next tick by AbstractDataAbilities) - pendingDefendAnimation = getDefendAnimation(); - - // Auto-complete after max hits - if (maxHitAmount > 0 && hitCount >= maxHitAmount) { - signalCompletion(); } - return result; + // Only react to physical melee damage + if (source.isMagicDamage() || source.isFireDamage() || source.isExplosion() || source.isProjectile()) { + return amount; + } + + // Deduplicate per tick: when multiple damage paths (e.g. vanilla + DBC) call onDefend + // for the same hit, only count it once for hitCount / signalCompletion / animation. + long currentTick = (caster != null && caster.worldObj != null) + ? caster.worldObj.getTotalWorldTime() : -1; + boolean firstCallThisTick = (currentTick < 0 || currentTick != lastDefendTick); + + // If max hits reached on a previous tick and this is a NEW hit, reject it + if (pendingCompletion && firstCallThisTick) { + return amount; + } + + if (firstCallThisTick) { + lastDefendTick = currentTick; + lastAttacker = attacker; + lastDamageTaken = amount; + hitCount++; + + // Queue defend reaction animation (played on next tick by AbstractDataAbilities) + pendingDefendAnimation = getDefendAnimation(); + + // Defer completion to next tick so all damage paths in this tick + // still see isDefending()=true and apply guard reduction. + if (maxHitAmount > 0 && hitCount >= maxHitAmount) { + pendingCompletion = true; + } + } + + // Always calculate the correct reduction for the caller's damage amount + return performDefend(attacker, amount); } /** 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 8d8949f07..91cf185b2 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 @@ -787,6 +787,14 @@ public abstract class AbilityEnergyProjectile projectiles[0].anchor.anchorOffsetZ = z; } + public boolean getLaunchFromAnchor() { + return projectiles[0].anchor.launchFromAnchor; + } + + public void setLaunchFromAnchor(boolean launchFromAnchor) { + projectiles[0].anchor.launchFromAnchor = launchFromAnchor; + } + @Override public int getAnchorPoint() { return projectiles[0].anchor.anchorPoint.ordinal(); @@ -840,6 +848,14 @@ public abstract class AbilityEnergyProjectile projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset; } + public boolean getLaunchFromAnchor(int index) { + return projectiles[clampIndex(index)].anchor.launchFromAnchor; + } + + public void setLaunchFromAnchor(int index, boolean launchFromAnchor) { + projectiles[clampIndex(index)].anchor.launchFromAnchor = launchFromAnchor; + } + // ==================== GUI FIELD DEFINITIONS ==================== @SideOnly(Side.CLIENT) @@ -889,6 +905,9 @@ public abstract class AbilityEnergyProjectile ).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)); defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v) .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount).min(Float.NEGATIVE_INFINITY)); + defs.add(FieldDef.boolField("ability.launchFromAnchor", () -> projectiles[idx].anchor.launchFromAnchor, v -> projectiles[idx].anchor.launchFromAnchor = v) + .tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount) + .hover("ability.hover.launchFromAnchor")); // Color override (only when multiple projectiles) defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java index 7ef631dda..2d9b162e5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityOrb.java @@ -51,8 +51,7 @@ public class AbilityOrb extends AbilityEnergyProjectile implem this.activeAnimationName = "Ability_Orb_Active"; this.defaultIconLayers = new DefaultIconLayer[]{ - new DefaultIconLayer("customnpcs:textures/gui/ability/orb.png"), - new DefaultIconLayer("customnpcs:textures/gui/ability/orb_overlay.png", + new DefaultIconLayer("customnpcs:textures/gui/ability/orb.png", () -> isOuterColorEnabled() ? getOuterColor() : getInnerColor()) }; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 15b1e5cbf..7744fcd27 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -809,7 +809,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile { nbt.setDouble("HeadOffsetY", headOffsetY); nbt.setDouble("HeadOffsetZ", headOffsetZ); nbt.setBoolean("AttachedToOwner", isAnchoredMode()); - nbt.setBoolean("RenderTailOrb", isAnchoredMode()); nbt.setString("BeamMode", beamMode.name()); writeChargingNBT(nbt); nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 4581340a9..e84e0df00 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -779,10 +779,8 @@ public class EntityAbilityLaser extends EntityEnergyProjectile { @Override protected void readProjectileNBT(NBTTagCompound nbt) { this.laserWidth = sanitize(nbt.hasKey("LaserWidth") ? nbt.getFloat("LaserWidth") : 0.2f, 0.2f, MAX_ENTITY_SIZE); - this.expansionSpeed = nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f; - if (Float.isNaN(expansionSpeed) || Float.isInfinite(expansionSpeed) || expansionSpeed <= 0) expansionSpeed = 2.0f; - this.maxLength = nbt.hasKey("MaxLength") ? nbt.getFloat("MaxLength") : 32.0f; - if (Float.isNaN(maxLength) || Float.isInfinite(maxLength) || maxLength <= 0) maxLength = 32.0f; + this.expansionSpeed = sanitize(nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f, 0.1f, MAX_ENTITY_SIZE); + this.maxLength = sanitize(nbt.hasKey("MaxLength") ? nbt.getFloat("MaxLength") : 32.0f, 1.0f, MAX_ENTITY_SIZE); this.dirX = nbt.getDouble("DirX"); this.dirY = nbt.getDouble("DirY"); this.dirZ = nbt.getDouble("DirZ"); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java index a3c034fcb..4d83945e4 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityZone.java @@ -790,7 +790,8 @@ public class EntityAbilityZone extends Entity implements IEntityAdditionalSpawnD this.shape = ZoneShape.CIRCLE; } this.ownerEntityId = buffer.readInt(); - this.radius = buffer.readFloat(); + this.radius = Math.max(0.1f, Math.min(100.0f, buffer.readFloat())); + if (Float.isNaN(radius) || Float.isInfinite(radius)) radius = 5.0f; this.durationTicks = buffer.readInt(); this.maxTicks = buffer.readInt(); this.innerColor = buffer.readInt(); diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java index 82781eed7..b16448c33 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyBarrier.java @@ -171,6 +171,32 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { double prevPosX, double prevPosY, double prevPosZ, int ownerEntityId); + /** + * Compute the outward surface normal at a given hit point on this barrier. + * Used by external systems (e.g., mixin-based projectile reflection) that cannot + * access barrier-subtype internals. + * + * @param hitX, hitY, hitZ Position near the barrier surface + * @param velX, velY, velZ Incoming velocity (used as fallback direction) + * @return double[3] normalized surface normal pointing outward from the barrier + */ + public abstract double[] getSurfaceNormal(double hitX, double hitY, double hitZ, + double velX, double velY, double velZ); + + /** + * Compute a position just outside the barrier surface, along the outward normal + * from the given position. Used to snap paused/reflected projectiles outside + * the collision zone. + * + * @param px, py, pz Current projectile position + * @param velX, velY, velZ Current velocity (used as fallback for direction) + * @param bias Distance outside the surface to snap to + * @return double[3] position outside the barrier surface + */ + public abstract double[] getOutsideSurfacePoint(double px, double py, double pz, + double velX, double velY, double velZ, + float bias); + // ==================== GENERIC PROJECTILE HIT ==================== /** @@ -209,6 +235,71 @@ public abstract class EntityEnergyBarrier extends EntityEnergyAbility { return true; } + /** + * Apply damage from a generic (non-CNPC+) projectile and return full hit context. + * Same logic as onProjectileHitResolved() but for raw Entity + typeId. + * + * @param projectileEntity The entity that hit the barrier (for event context) + * @param damage Base damage to apply + * @param typeId Type identifier for damage multiplier lookup (e.g., "dbc.ki_attack") + * @return Full ProjectileHitOutcome with result, reflect data, remaining damage + */ + public ProjectileHitOutcome onGenericProjectileHitResolved(Entity projectileEntity, float damage, String typeId) { + float multiplier = barrierData.getMultiplier(typeId); + float finalDamage = damage * multiplier; + + if (!worldObj.isRemote) { + float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, null, finalDamage); + if (eventDamage < 0) { + return new ProjectileHitOutcome( + ProjectileHitResult.PASS, + currentHealth, currentHealth, + 0.0f, 0.0f, + barrierData.reflect, barrierData.reflectStrengthPct, + barrierData.useHealth + ); + } + finalDamage = eventDamage; + } + + triggerHitFlash(); + float healthBefore = currentHealth; + + if (!barrierData.useHealth) { + return new ProjectileHitOutcome( + ProjectileHitResult.BLOCKED, + healthBefore, healthBefore, + 0.0f, 0.0f, + barrierData.reflect, barrierData.reflectStrengthPct, + false + ); + } + + currentHealth -= finalDamage; + syncHealthPercent(); + + if (currentHealth <= 0) { + float remaining = Math.max(0.0f, finalDamage - healthBefore); + onBarrierDestroyed(); + this.setDead(); + return new ProjectileHitOutcome( + ProjectileHitResult.BROKEN, + healthBefore, 0.0f, + finalDamage, remaining, + barrierData.reflect, barrierData.reflectStrengthPct, + true + ); + } + + return new ProjectileHitOutcome( + ProjectileHitResult.BLOCKED, + healthBefore, currentHealth, + finalDamage, 0.0f, + barrierData.reflect, barrierData.reflectStrengthPct, + true + ); + } + // ==================== PROJECTILE HIT ==================== /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java index 0e8f0bf56..535d1cdff 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyDome.java @@ -213,6 +213,56 @@ public class EntityEnergyDome extends EntityEnergyBarrier { return isIncomingRay(posX, posY, posZ, prevPosX, prevPosY, prevPosZ, ownerEntityId); } + @Override + public double[] getSurfaceNormal(double hitX, double hitY, double hitZ, + double velX, double velY, double velZ) { + double nx = hitX - this.posX; + double ny = hitY - this.posY; + double nz = hitZ - this.posZ; + double lenSq = nx * nx + ny * ny + nz * nz; + if (lenSq > 1.0e-8) { + double invLen = 1.0 / Math.sqrt(lenSq); + return new double[]{nx * invLen, ny * invLen, nz * invLen}; + } + // Fallback: use negative velocity direction + double vLenSq = velX * velX + velY * velY + velZ * velZ; + if (vLenSq > 1.0e-8) { + double invVLen = 1.0 / Math.sqrt(vLenSq); + return new double[]{-velX * invVLen, -velY * invVLen, -velZ * invVLen}; + } + return new double[]{0.0, 1.0, 0.0}; + } + + @Override + public double[] getOutsideSurfacePoint(double px, double py, double pz, + double velX, double velY, double velZ, + float bias) { + double nx = px - this.posX; + double ny = py - this.posY; + double nz = pz - this.posZ; + double len = Math.sqrt(nx * nx + ny * ny + nz * nz); + + if (len < 1.0e-5) { + double vLen = Math.sqrt(velX * velX + velY * velY + velZ * velZ); + if (vLen > 1.0e-5) { + nx = -velX / vLen; + ny = -velY / vLen; + nz = -velZ / vLen; + } else { + nx = 0.0; ny = 1.0; nz = 0.0; + } + len = 1.0; + } + + double invLen = 1.0 / len; + double target = domeRadius + bias; + return new double[]{ + this.posX + nx * invLen * target, + this.posY + ny * invLen * target, + this.posZ + nz * invLen * target + }; + } + @Override public float getMaxExtent() { return domeRadius; diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java index ad25afa0b..8ed770f8a 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyPanel.java @@ -362,6 +362,63 @@ public class EntityEnergyPanel extends EntityEnergyBarrier { return isIncomingRay(posX, posY, posZ, prevPosX, prevPosY, prevPosZ, ownerEntityId); } + @Override + public double[] getSurfaceNormal(double hitX, double hitY, double hitZ, + double velX, double velY, double velZ) { + float yawRad = (float) Math.toRadians(panelYaw); + double nx = -Math.sin(yawRad); + double nz = Math.cos(yawRad); + + // Determine which side the hit is on + double relX = hitX - this.posX; + double relZ = hitZ - this.posZ; + double side = relX * nx + relZ * nz; + if (Math.abs(side) < 1.0e-5) { + // Near the plane: use incoming velocity to determine side + side = velX * nx + velZ * nz; + } + if (side < 0.0) { + nx = -nx; + nz = -nz; + } + + return new double[]{nx, 0.0, nz}; + } + + @Override + public double[] getOutsideSurfacePoint(double px, double py, double pz, + double velX, double velY, double velZ, + float bias) { + float yawRad = (float) Math.toRadians(panelYaw); + double normalX = -Math.sin(yawRad); + double normalZ = Math.cos(yawRad); + + double relX = px - this.posX; + double relZ = pz - this.posZ; + double signedDist = relX * normalX + relZ * normalZ; + + double halfThickness = 0.25; + double side; + if (Math.abs(signedDist) > 0.035) { + side = signedDist >= 0 ? 1.0 : -1.0; + } else { + double vDot = velX * normalX + velZ * normalZ; + if (Math.abs(vDot) > 1.0e-5) { + side = vDot < 0 ? 1.0 : -1.0; + } else { + side = signedDist >= 0 ? 1.0 : -1.0; + } + } + + double targetDist = side * (halfThickness + bias); + double delta = targetDist - signedDist; + return new double[]{ + px + normalX * delta, + py, + pz + normalZ * delta + }; + } + @Override public float getMaxExtent() { return Math.max(panelData.panelWidth, panelData.panelHeight) * 0.5f + 1.0f; diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index fbfc91d79..59dd346e4 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -116,8 +116,6 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { // ==================== HOMING PROPERTIES ==================== protected EnergyHomingData homingData = new EnergyHomingData(); - protected Map pathDelays; - // ==================== TRACKING ==================== protected double startX, startY, startZ; protected int targetEntityId = -1; @@ -681,47 +679,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { } protected double[] getBarrierImpactNormal(EntityEnergyBarrier barrier, double velocityX, double velocityY, double velocityZ) { - if (barrier instanceof EntityEnergyDome) { - EntityEnergyDome dome = (EntityEnergyDome) barrier; - return normalizeVector(posX - dome.posX, posY - dome.posY, posZ - dome.posZ, velocityX, velocityY, velocityZ); - } - - if (barrier instanceof EntityEnergyPanel) { - EntityEnergyPanel panel = (EntityEnergyPanel) barrier; - float yawRad = (float) Math.toRadians(panel.getPanelYaw()); - double nx = -Math.sin(yawRad); - double nz = Math.cos(yawRad); - double relX = posX - panel.posX; - double relZ = posZ - panel.posZ; - double signedDist = relX * nx + relZ * nz; - double side = signedDist; - if (Math.abs(side) < 1.0e-5) { - side = velocityX * nx + velocityZ * nz; - } - if (side < 0.0) { - nx = -nx; - nz = -nz; - } - return normalizeVector(nx, 0.0, nz, velocityX, velocityY, velocityZ); - } - - return normalizeVector(posX - barrier.posX, posY - barrier.posY, posZ - barrier.posZ, velocityX, velocityY, velocityZ); - } - - protected double[] normalizeVector(double x, double y, double z, double fallbackX, double fallbackY, double fallbackZ) { - double lenSq = x * x + y * y + z * z; - if (lenSq > 1.0e-8) { - double invLen = 1.0 / Math.sqrt(lenSq); - return new double[]{x * invLen, y * invLen, z * invLen}; - } - - double fallbackLenSq = fallbackX * fallbackX + fallbackY * fallbackY + fallbackZ * fallbackZ; - if (fallbackLenSq > 1.0e-8) { - double invFallbackLen = 1.0 / Math.sqrt(fallbackLenSq); - return new double[]{-fallbackX * invFallbackLen, -fallbackY * invFallbackLen, -fallbackZ * invFallbackLen}; - } - - return new double[]{0.0, 1.0, 0.0}; + return barrier.getSurfaceNormal(posX, posY, posZ, velocityX, velocityY, velocityZ); } protected void beginBarrierImpactPause(boolean destroyOnResume, EntityEnergyBarrier barrier) { @@ -752,73 +710,8 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { if (barrier == null) return; float bias = getBarrierPauseOutsideDistance(); - if (barrier instanceof EntityEnergyDome) { - EntityEnergyDome dome = (EntityEnergyDome) barrier; - double nx = posX - dome.posX; - double ny = posY - dome.posY; - double nz = posZ - dome.posZ; - double len = Math.sqrt(nx * nx + ny * ny + nz * nz); - - if (len < 1.0e-5) { - double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); - if (vLen > 1.0e-5) { - nx = -motionX / vLen; - ny = -motionY / vLen; - nz = -motionZ / vLen; - len = 1.0; - } else { - nx = 0.0; - ny = 1.0; - nz = 0.0; - len = 1.0; - } - } - - double invLen = 1.0 / len; - double target = dome.getDomeRadius() + bias; - setPosition( - dome.posX + nx * invLen * target, - dome.posY + ny * invLen * target, - dome.posZ + nz * invLen * target - ); - return; - } - - if (barrier instanceof EntityEnergyPanel) { - EntityEnergyPanel panel = (EntityEnergyPanel) barrier; - float yawRad = (float) Math.toRadians(panel.getPanelYaw()); - double normalX = -Math.sin(yawRad); - double normalZ = Math.cos(yawRad); - double relX = posX - panel.posX; - double relZ = posZ - panel.posZ; - double signedDist = relX * normalX + relZ * normalZ; - double halfThickness = 0.25; - double side; - if (Math.abs(signedDist) > 0.035) { - side = signedDist >= 0 ? 1.0 : -1.0; - } else { - // Near the plane, use incoming direction so we don't snap through and "fling". - double vDot = motionX * normalX + motionZ * normalZ; - if (Math.abs(vDot) > 1.0e-5) { - side = vDot < 0 ? 1.0 : -1.0; - } else { - side = signedDist >= 0 ? 1.0 : -1.0; - } - } - double targetDist = side * (halfThickness + bias); - double delta = targetDist - signedDist; - setPosition(posX + normalX * delta, posY, posZ + normalZ * delta); - return; - } - - double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); - if (vLen > 1.0e-5) { - setPosition( - posX - (motionX / vLen) * bias, - posY - (motionY / vLen) * bias, - posZ - (motionZ / vLen) * bias - ); - } + double[] point = barrier.getOutsideSurfacePoint(posX, posY, posZ, motionX, motionY, motionZ, bias); + setPosition(point[0], point[1], point[2]); } protected boolean tickBarrierImpactPause() { @@ -1436,10 +1329,16 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility { } /** - * Only player-cast projectiles should be recentered to look-vector launch origin. - * NPC projectiles keep their anchor charge origin so casts originate from the configured anchor. + * Determines whether the projectile should snap to the look-vector launch origin on fire. + * By default, player-cast projectiles snap (launch from eye position along crosshair), + * and NPC projectiles keep their anchor charge origin. + * When {@code anchorData.launchFromAnchor} is true, snapping is disabled so the + * projectile launches from its configured anchor position instead. */ protected boolean shouldSnapLaunchToLookVector(EntityLivingBase owner) { + if (anchorData.launchFromAnchor) { + return false; + } return owner instanceof EntityPlayer; } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityHotbarSyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityHotbarSyncPacket.java index a30682ecc..7ce46f97b 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityHotbarSyncPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/AbilityHotbarSyncPacket.java @@ -81,6 +81,10 @@ public final class AbilityHotbarSyncPacket extends AbstractPacket { if (valid && data.abilityData != null) { valid = data.abilityData.hasUnlockedAbility(slot.abilityKey); + // For chains, also check with resolved key in case key format differs + if (!valid && slot.isChainKey()) { + valid = data.abilityData.hasUnlockedAbility("chain:" + slot.getResolveKey()); + } } if (!valid) { diff --git a/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSavePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSavePacket.java index 1522763fc..687a00a1f 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/ability/AbilityHotbarSavePacket.java @@ -51,6 +51,7 @@ public final class AbilityHotbarSavePacket extends AbstractPacket { public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { int slot = in.readInt(); NBTTagCompound compound = ByteBufUtils.readNBT(in); + if (compound == null) return; if (slot < 0 || slot >= AbilityHotbarData.TOTAL_SLOTS) return; @@ -73,6 +74,10 @@ public final class AbilityHotbarSavePacket extends AbstractPacket { if (valid && data.abilityData != null) { valid = data.abilityData.hasUnlockedAbility(slotData.abilityKey); + // For chains, also check with resolved key in case key format differs + if (!valid && slotData.isChainKey()) { + valid = data.abilityData.hasUnlockedAbility("chain:" + slotData.getResolveKey()); + } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java index 3d91f5a86..8c075bb25 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java @@ -66,6 +66,7 @@ public final class CustomAbilitySavePacket extends AbstractPacket { return; NBTTagCompound compound = ByteBufUtils.readNBT(in); + if (compound == null) return; Ability ability = AbilityController.Instance.fromNBT(compound); if (ability != null) { AbilityController.Instance.saveCustomAbility(ability); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java index 411d98a00..ec3f6ff20 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/AbilityScriptPacket.java @@ -81,7 +81,9 @@ public final class AbilityScriptPacket extends AbstractPacket { if (!ConfigScript.canScript(player, CustomNpcsPermissions.SCRIPT)) return; - Action requestedAction = Action.values()[in.readInt()]; + int actionOrd = in.readInt(); + if (actionOrd < 0 || actionOrd >= Action.values().length) return; + Action requestedAction = Action.values()[actionOrd]; String id = ByteBufUtils.readUTF8String(in); Ability ability = AbilityController.Instance.resolveAbility(id); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java index 2589b0e13..45640bbfb 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ChainedAbilityScriptPacket.java @@ -81,7 +81,9 @@ public final class ChainedAbilityScriptPacket extends AbstractPacket { if (!ConfigScript.canScript(player, CustomNpcsPermissions.SCRIPT)) return; - Action requestedAction = Action.values()[in.readInt()]; + int actionOrd = in.readInt(); + if (actionOrd < 0 || actionOrd >= Action.values().length) return; + Action requestedAction = Action.values()[actionOrd]; String id = ByteBufUtils.readUTF8String(in); ChainedAbility chain = AbilityController.Instance.resolveChainedAbility(id); diff --git a/src/main/java/noppes/npcs/AbstractDataAbilities.java b/src/main/java/noppes/npcs/AbstractDataAbilities.java index 3f44394f3..3f041ecdc 100644 --- a/src/main/java/noppes/npcs/AbstractDataAbilities.java +++ b/src/main/java/noppes/npcs/AbstractDataAbilities.java @@ -1113,11 +1113,8 @@ public abstract class AbstractDataAbilities { if (hasExecutingAbility) { AbilityPhase phase = currentAbility.getPhase(); - // Cannot cancel during DAZED (already interrupted) - if (phase == AbilityPhase.DAZED) return; - // Only allow cancel during active phases - if (phase != AbilityPhase.WINDUP && phase != AbilityPhase.ACTIVE - && phase != AbilityPhase.BURST_DELAY) return; + // Only allow cancel during WINDUP phase + if (phase != AbilityPhase.WINDUP) return; // Remove telegraph removeTelegraph(currentAbility); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index b1e58d94a..295716a92 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -649,9 +649,11 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Search matches either the display name or the typeId (strip color codes for matching) String stripped = displayName.replaceAll("\u00A7.", ""); if (search.isEmpty() || stripped.toLowerCase().contains(search) || typeId.toLowerCase().contains(search)) { - // Lime green for concurrent-capable types, gray for other built-in types - if (AbilityController.Instance.isConcurrentCapableType(typeId)) { - displayName = "\u00A7a" + displayName; + // Pink for custom ability, light blue for concurrent-capable types, gray for other built-in types + if (typeId.equals("ability.cnpc.custom")) { + displayName = "\u00A7d" + displayName; + } else if (AbilityController.Instance.isConcurrentCapableType(typeId)) { + displayName = "\u00A7b" + displayName; } else if (AbilityController.Instance.isBuiltInType(typeId)) { displayName = "\u00A77" + displayName; } @@ -661,6 +663,21 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } Collections.sort(list, (a, b) -> String.CASE_INSENSITIVE_ORDER.compare( a.replaceAll("\u00A7.", ""), b.replaceAll("\u00A7.", ""))); + + // Pin Custom Ability to top of list + String customEntry = null; + for (String name : list) { + String tid = displayNameToTypeId.get(name); + if (tid != null && tid.equals("ability.cnpc.custom")) { + customEntry = name; + break; + } + } + if (customEntry != null) { + list.remove(customEntry); + list.add(0, customEntry); + } + return list; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 46447712b..485d59b87 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -189,7 +189,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } if (ability.isConcurrentCapable()) { sw.addLabel(new GuiNpcLabel(baseId + 3, - StatCollector.translateToLocal("ability.concurrentInfo"), 5, infoY, 0x55FF55)); + StatCollector.translateToLocal("ability.concurrentInfo"), 5, infoY, 0x55FFFF)); } } @@ -268,7 +268,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } if (conditions.size() < MAX_CONDITIONS) { - sw.addButton(new GuiNpcButton(BTN_ADD_COND, L_LABEL_X, y, 50, 20, "gui.add"));; + sw.addButton(new GuiNpcButton(BTN_ADD_COND, L_LABEL_X, y, 50, 20, "gui.add")); y += ROW_H; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java index 7d9c6cc32..46975f289 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityTypeSelect.java @@ -86,7 +86,7 @@ public class SubGuiAbilityTypeSelect extends SubGuiInterface implements ICustomS if (typeId.equals("ability.cnpc.custom")) { displayName = "\u00A7d" + displayName; } else if (AbilityController.Instance.isConcurrentCapableType(typeId)) { - displayName = "\u00A7a" + displayName; + displayName = "\u00A7b" + displayName; } allDisplayNameToTypeId.put(displayName, typeId); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java index 99af06e1c..1fc123d37 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiChainedAbilityConfig.java @@ -402,7 +402,7 @@ public class SubGuiChainedAbilityConfig extends SubGuiInterface implements IText // Concurrent toggle (only shown if resolved ability is concurrent-capable, never for first entry) if (i > 0 && resolvedAbility != null && resolvedAbility.isConcurrentCapable()) { - String cLabel = entry.isConcurrentEnabled() ? "\u00A7aC" : "\u00A77C"; + String cLabel = entry.isConcurrentEnabled() ? "\u00A7bC" : "\u00A77C"; GuiNpcButton cBtn = new GuiNpcButton(ENTRY_BASE + i * ENTRY_STRIDE + 5, L_LABEL_X + 288, y, 20, 20, cLabel); cBtn.setHoverText("ability.hover.concurrent"); sw.addButton(cBtn); diff --git a/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java b/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java index fe3267112..4d03cb2dc 100644 --- a/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java +++ b/src/main/java/noppes/npcs/client/gui/builder/FieldDef.java @@ -309,7 +309,7 @@ public class FieldDef { } public boolean hasRange() { - return min != Float.NEGATIVE_INFINITY || max != Float.POSITIVE_INFINITY; + return min != 0 || max != Float.POSITIVE_INFINITY; } public Class> getEnumClass() { diff --git a/src/main/java/noppes/npcs/controllers/data/HitboxData.java b/src/main/java/noppes/npcs/controllers/data/HitboxData.java index 4d2f0263b..4ccf9f823 100644 --- a/src/main/java/noppes/npcs/controllers/data/HitboxData.java +++ b/src/main/java/noppes/npcs/controllers/data/HitboxData.java @@ -1,9 +1,10 @@ package noppes.npcs.controllers.data; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.entity.data.IHitboxData; import noppes.npcs.config.ConfigMain; -public class HitboxData { +public class HitboxData implements IHitboxData { private float widthScale = 1f; private float heightScale = 1f; diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java index 600926785..21e989644 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -96,6 +96,21 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA */ private static final int CANCEL_WINDOW_TICKS = 10; + /** + * Position tracking for teleport detection during ability execution. + * If the player moves more than {@link #TELEPORT_THRESHOLD_SQ} in a single tick, + * the ability is interrupted as if the player changed dimensions. + */ + private transient double lastAbilityTickX, lastAbilityTickY, lastAbilityTickZ; + private transient boolean trackingAbilityPosition = false; + + /** + * Squared distance threshold for teleport detection (48 blocks). + * Normal sprint speed is ~5.6 blocks/tick; this threshold is well above + * any legitimate per-tick movement including ability-driven dashes. + */ + private static final double TELEPORT_THRESHOLD_SQ = 48.0 * 48.0; + // ═══════════════════════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════════════════════ @@ -257,7 +272,30 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA // Tick active toggles (independent of currentAbility) tickActiveToggles(); - if (chainDelayRemaining > 0 || (currentAbility != null && currentAbility.isExecuting())) { + boolean hasActiveAbility = chainDelayRemaining > 0 || (currentAbility != null && currentAbility.isExecuting()); + + if (hasActiveAbility) { + // Detect large position jump (teleport) while ability is executing. + // Must run BEFORE tickCurrentAbility / applyPositionLock so the raw + // teleported position is visible before any lock snaps it back. + if (trackingAbilityPosition) { + double dx = player.posX - lastAbilityTickX; + double dy = player.posY - lastAbilityTickY; + double dz = player.posZ - lastAbilityTickZ; + if (dx * dx + dy * dy + dz * dz > TELEPORT_THRESHOLD_SQ) { + // Player was teleported — clear ability state (same as dimension change) + trackingAbilityPosition = false; + resetOnTeleport(); + return; + } + } + + // Update tracked position for next tick's comparison + lastAbilityTickX = player.posX; + lastAbilityTickY = player.posY; + lastAbilityTickZ = player.posZ; + trackingAbilityPosition = true; + tickCurrentAbility(); // Apply rotation and position locks after ability tick @@ -267,6 +305,8 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA // Sync lock state to client (only sends when flags change) syncAbilityStateIfNeeded(player); } else { + trackingAbilityPosition = false; + // Safety: release orphaned locks if no ability is executing if (rotationLocked || positionLocked) { releaseRotationControl(); @@ -610,12 +650,10 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA if (!hasExecutingAbility && !isInChainDelay) return false; - // Don't allow cancel during DAZED + // Only allow cancel during WINDUP phase if (hasExecutingAbility) { AbilityPhase phase = currentAbility.getPhase(); - if (phase == AbilityPhase.DAZED) return false; - if (phase != AbilityPhase.WINDUP && phase != AbilityPhase.ACTIVE - && phase != AbilityPhase.BURST_DELAY) return false; + if (phase != AbilityPhase.WINDUP) return false; } long worldTime = getWorldTime(); @@ -698,6 +736,15 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA * Keeps active toggles (dimension change shouldn't reset ongoing effects). */ public void resetOnDimensionChange() { + trackingAbilityPosition = false; + onEntityReconstructed(true, false); + } + + /** + * Reset ability state when a large-distance teleport is detected during execution. + * Same behavior as dimension change — silent cleanup, clear cooldowns, keep toggles. + */ + public void resetOnTeleport() { onEntityReconstructed(true, false); } @@ -719,6 +766,8 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA * The caller must trigger a full ability sync afterwards (e.g. via syncToClient). */ public void resetOnLogin() { + trackingAbilityPosition = false; + // Silently clean up any stale executing ability if (currentAbility != null && currentAbility.isExecuting()) { removeTelegraph(currentAbility); diff --git a/src/main/java/noppes/npcs/controllers/data/TintData.java b/src/main/java/noppes/npcs/controllers/data/TintData.java index 104600daf..1471d967a 100644 --- a/src/main/java/noppes/npcs/controllers/data/TintData.java +++ b/src/main/java/noppes/npcs/controllers/data/TintData.java @@ -1,8 +1,9 @@ package noppes.npcs.controllers.data; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.entity.data.ITintData; -public class TintData { +public class TintData implements ITintData { private boolean tintEnabled = false; private boolean hurtTintEnabled = true; private boolean generalTintEnabled = false; diff --git a/src/main/java/noppes/npcs/items/ItemStaff.java b/src/main/java/noppes/npcs/items/ItemStaff.java index 5dc674b30..39de82e37 100644 --- a/src/main/java/noppes/npcs/items/ItemStaff.java +++ b/src/main/java/noppes/npcs/items/ItemStaff.java @@ -84,13 +84,27 @@ public class ItemStaff extends ItemNpcInterface implements IProjectileCallback { public void onUsingTick(ItemStack stack, EntityPlayer player, int count) { int tick = getMaxItemUseDuration(stack) - count; + int chargeTime = 20 + material.getHarvestLevel() * 8; + if (player.worldObj.isRemote) { spawnParticle(stack, player); + + // Reset charge tick on client to prevent client-side timeout + // (resetChargeTick is not synced via DataWatcher, so the client's + // chargeTick would keep climbing and falsely trigger CHARGE_TIMEOUT_GRACE) + if (tick > chargeTime && stack.stackTagCompound != null) { + Entity existing = player.worldObj + .getEntityByID(stack.stackTagCompound.getInteger("MagicProjectile")); + if (existing instanceof EntityAbilityOrb) { + EntityAbilityOrb orb = (EntityAbilityOrb) existing; + if (orb.isCharging()) { + orb.resetChargeTick(); + } + } + } return; } - int chargeTime = 20 + material.getHarvestLevel() * 8; - // Keep a fully-charged orb alive while the player is still holding if (tick > chargeTime && stack.stackTagCompound != null) { Entity existing = ((WorldServer) player.worldObj) diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java index 1b6a95fb7..cb47a092c 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyOrb.java @@ -49,5 +49,6 @@ public class ScriptEnergyOrb extends ScriptEnergyPro entity.motionY = (dy / len) * entity.getSpeed(); entity.motionZ = (dz / len) * entity.getSpeed(); } + entity.startMoving(null); } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java index c362d72c6..27621f125 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java @@ -1,6 +1,7 @@ package noppes.npcs.scripted.entity; import kamkeel.npcs.entity.EntityEnergyProjectile; +import net.minecraft.entity.EnumCreatureAttribute; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import noppes.npcs.NoppesUtilServer; @@ -11,7 +12,9 @@ import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEnergyProjectile; import noppes.npcs.api.entity.IEntityLivingBase; import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.entity.data.IHitboxData; import noppes.npcs.api.entity.data.IModelData; +import noppes.npcs.api.entity.data.ITintData; import noppes.npcs.api.handler.IActionManager; import noppes.npcs.api.handler.IOverlayHandler; import noppes.npcs.api.handler.data.IAnimationData; @@ -421,6 +424,62 @@ public class ScriptNpc extends ScriptLiving imp return this.npc.advanced.killLines; } + public boolean getOrderedLines() { + return npc.advanced.orderedLines; + } + + public void setOrderedLines(boolean ordered) { + npc.advanced.orderedLines = ordered; + } + + public String getIdleSound() { + return npc.advanced.idleSound; + } + + public void setIdleSound(String sound) { + npc.advanced.idleSound = sound; + } + + public String getAngrySound() { + return npc.advanced.angrySound; + } + + public void setAngrySound(String sound) { + npc.advanced.angrySound = sound; + } + + public String getHurtSound() { + return npc.advanced.hurtSound; + } + + public void setHurtSound(String sound) { + npc.advanced.hurtSound = sound; + } + + public String getDeathSound() { + return npc.advanced.deathSound; + } + + public void setDeathSound(String sound) { + npc.advanced.deathSound = sound; + } + + public String getStepSound() { + return npc.advanced.stepSound; + } + + public void setStepSound(String sound) { + npc.advanced.stepSound = sound; + } + + public boolean getDisablePitch() { + return npc.advanced.disablePitch; + } + + public void setDisablePitch(boolean disablePitch) { + npc.advanced.disablePitch = disablePitch; + } + /** * Kill the npc, doesnt't despawn it */ @@ -848,6 +907,171 @@ public class ScriptNpc extends ScriptLiving imp return npc.ais.tacticalRadius; } + public int getTacticalChance() { + return npc.ais.tacticalChance; + } + + public void setTacticalChance(int chance) { + npc.ais.tacticalChance = chance; + } + + public boolean getCanSwim() { + return npc.ais.canSwim; + } + + public void setCanSwim(boolean canSwim) { + npc.ais.canSwim = canSwim; + } + + public boolean getReactsToFire() { + return npc.ais.reactsToFire; + } + + public void setReactsToFire(boolean reactsToFire) { + npc.ais.reactsToFire = reactsToFire; + } + + public boolean getAvoidsWater() { + return npc.ais.avoidsWater; + } + + public void setAvoidsWater(boolean avoidsWater) { + npc.ais.avoidsWater = avoidsWater; + } + + public boolean getAvoidsSun() { + return npc.ais.avoidsSun; + } + + public void setAvoidsSun(boolean avoidsSun) { + npc.ais.avoidsSun = avoidsSun; + } + + public boolean getDirectLOS() { + return npc.ais.directLOS; + } + + public void setDirectLOS(boolean directLOS) { + npc.ais.directLOS = directLOS; + } + + public int getLeapType() { + return npc.ais.leapType; + } + + public void setLeapType(int leapType) { + npc.ais.leapType = leapType; + } + + public boolean getCanSprint() { + return npc.ais.canSprint; + } + + public void setCanSprint(boolean canSprint) { + npc.ais.canSprint = canSprint; + } + + public boolean getStopAndInteract() { + return npc.ais.stopAndInteract; + } + + public void setStopAndInteract(boolean stopAndInteract) { + npc.ais.stopAndInteract = stopAndInteract; + } + + public int getDoorInteract() { + return npc.ais.doorInteract; + } + + public void setDoorInteract(int doorInteract) { + npc.ais.doorInteract = doorInteract; + } + + public int getWalkingRange() { + return npc.ais.walkingRange; + } + + public void setWalkingRange(int range) { + npc.ais.walkingRange = range; + } + + public boolean getNpcInteracting() { + return npc.ais.npcInteracting; + } + + public void setNpcInteracting(boolean npcInteracting) { + npc.ais.npcInteracting = npcInteracting; + } + + public boolean getMovingPause() { + return npc.ais.movingPause; + } + + public void setMovingPause(boolean movingPause) { + npc.ais.movingPause = movingPause; + } + + public int getMovingPattern() { + return npc.ais.movingPattern; + } + + public void setMovingPattern(int pattern) { + if (pattern < 0) pattern = 0; + if (pattern > 1) pattern = 1; + npc.ais.movingPattern = pattern; + } + + public int getCanFireIndirect() { + return npc.ais.canFireIndirect; + } + + public void setCanFireIndirect(int canFireIndirect) { + npc.ais.canFireIndirect = canFireIndirect; + } + + public int getUseRangeMelee() { + return npc.ais.useRangeMelee; + } + + public void setUseRangeMelee(int useRangeMelee) { + npc.ais.useRangeMelee = useRangeMelee; + } + + public int getDistanceToMelee() { + return npc.ais.distanceToMelee; + } + + public void setDistanceToMelee(int distance) { + npc.ais.distanceToMelee = distance; + } + + public float getBodyOffsetX() { + return npc.ais.bodyOffsetX; + } + + public void setBodyOffsetX(float offsetX) { + npc.ais.bodyOffsetX = offsetX; + npc.script.clientNeedsUpdate = true; + } + + public float getBodyOffsetY() { + return npc.ais.bodyOffsetY; + } + + public void setBodyOffsetY(float offsetY) { + npc.ais.bodyOffsetY = offsetY; + npc.script.clientNeedsUpdate = true; + } + + public float getBodyOffsetZ() { + return npc.ais.bodyOffsetZ; + } + + public void setBodyOffsetZ(float offsetZ) { + npc.ais.bodyOffsetZ = offsetZ; + npc.script.clientNeedsUpdate = true; + } + public void setIgnoreCobweb(boolean ignore) { npc.stats.ignoreCobweb = ignore; } @@ -1152,6 +1376,14 @@ public class ScriptNpc extends ScriptLiving imp return npc instanceof EntityCustomNpc ? ((EntityCustomNpc) npc).modelData : null; } + public IHitboxData getHitboxData() { + return npc.display.hitboxData; + } + + public ITintData getTintData() { + return npc.display.tintData; + } + public void setHeadScale(float x, float y, float z) { if (npc instanceof EntityCustomNpc) { ((EntityCustomNpc) this.npc).modelData.modelScale.head.scaleX = ValueUtil.clamp(x, 0.5f, 1.5f); @@ -1250,6 +1482,66 @@ public class ScriptNpc extends ScriptLiving imp return npc.stats.resistances.knockback; } + public boolean getDamageDisabled() { + return npc.stats.resistances.disableDamage; + } + + public void setDamageDisabled(boolean disabled) { + npc.stats.resistances.disableDamage = disabled; + } + + public boolean getNoFallDamage() { + return npc.stats.noFallDamage; + } + + public void setNoFallDamage(boolean noFallDamage) { + npc.stats.noFallDamage = noFallDamage; + } + + public boolean getImmuneToFire() { + return npc.stats.immuneToFire; + } + + public void setImmuneToFire(boolean immuneToFire) { + npc.stats.immuneToFire = immuneToFire; + npc.setImmuneToFire(immuneToFire); + } + + public boolean getPotionImmune() { + return npc.stats.potionImmune; + } + + public void setPotionImmune(boolean potionImmune) { + npc.stats.potionImmune = potionImmune; + } + + public boolean getBurnInSun() { + return npc.stats.burnInSun; + } + + public void setBurnInSun(boolean burnInSun) { + npc.stats.burnInSun = burnInSun; + } + + public boolean getAttackInvisible() { + return npc.stats.attackInvisible; + } + + public void setAttackInvisible(boolean attackInvisible) { + npc.stats.attackInvisible = attackInvisible; + } + + public int getCreatureType() { + return npc.stats.creatureType.ordinal(); + } + + public void setCreatureType(int type) { + EnumCreatureAttribute[] values = EnumCreatureAttribute.values(); + if (type >= 0 && type < values.length) { + npc.stats.creatureType = values[type]; + } + } + /** * @param type Retaliation type. 0:normal, 1:panic, 2:retreat, 3:nothing */ @@ -1432,6 +1724,15 @@ public class ScriptNpc extends ScriptLiving imp return npc.display.skinOverlayData.overlayList.get(0).getTexture(); } + public String getGlowTexture() { + return npc.display.glowTexture; + } + + public void setGlowTexture(String texture) { + npc.display.glowTexture = texture; + npc.script.clientNeedsUpdate = true; + } + public IOverlayHandler getOverlays() { return npc.display.skinOverlayData; } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index bdaa2abdc..4fc5f8aa7 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1866,6 +1866,8 @@ ability.anchorPoint=Anchor Point ability.anchor.offsetX=Offset X ability.anchor.offsetY=Offset Y ability.anchor.offsetZ=Offset Z +ability.launchFromAnchor=Launch From Anchor +ability.hover.launchFromAnchor=When enabled, the projectile launches from its anchor position instead of snapping to the default eye/look-vector position. ability.vertical=Vertical ability.rotationSpeed=Rotation Speed diff --git a/src/main/resources/assets/customnpcs/textures/gui/ability/orb.png b/src/main/resources/assets/customnpcs/textures/gui/ability/orb.png index c6bf9991d..3b60cff14 100644 Binary files a/src/main/resources/assets/customnpcs/textures/gui/ability/orb.png and b/src/main/resources/assets/customnpcs/textures/gui/ability/orb.png differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/ability/orb_overlay.png b/src/main/resources/assets/customnpcs/textures/gui/ability/orb_overlay.png deleted file mode 100644 index d92a7361f..000000000 Binary files a/src/main/resources/assets/customnpcs/textures/gui/ability/orb_overlay.png and /dev/null differ diff --git a/src/main/resources/assets/customnpcs/textures/gui/icons.png b/src/main/resources/assets/customnpcs/textures/gui/icons.png index 6f2993cd1..261b5a954 100644 Binary files a/src/main/resources/assets/customnpcs/textures/gui/icons.png and b/src/main/resources/assets/customnpcs/textures/gui/icons.png differ