mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 21:54:08 +00:00
Merge branch 'dev' into syntax-highlighter-unified
This commit is contained in:
commit
29f0282fe4
43 files changed
+767
-199
No files matched your search
+1
-1
Submodule src/api updated: b28a7a5c4d...f969b9f540.
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<PlayerData> 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<String, Ability> synced) {
|
||||
public synchronized void setCustomAbilities(Map<String, Ability> 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<PlayerData> 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<String, ChainedAbility> synced) {
|
||||
public synchronized void setChainedAbilities(Map<String, ChainedAbility> synced) {
|
||||
chainedAbilities.clear();
|
||||
chainedAbilitiesById.clear();
|
||||
chainedAbilities.putAll(synced);
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
+2
-2
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+3
-1
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+16
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+19
@@ -787,6 +787,14 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
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<E extends EntityEnergyProjectile>
|
||||
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<E extends EntityEnergyProjectile>
|
||||
).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)
|
||||
|
||||
@@ -51,8 +51,7 @@ public class AbilityOrb extends AbilityEnergyProjectile<EntityAbilityOrb> 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())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 ====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -116,8 +116,6 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
// ==================== HOMING PROPERTIES ====================
|
||||
protected EnergyHomingData homingData = new EnergyHomingData();
|
||||
|
||||
protected Map<Integer, Integer> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<? extends Enum<?>> getEnumClass() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -49,5 +49,6 @@ public class ScriptEnergyOrb<T extends EntityAbilityOrb> extends ScriptEnergyPro
|
||||
entity.motionY = (dy / len) * entity.getSpeed();
|
||||
entity.motionZ = (dz / len) * entity.getSpeed();
|
||||
}
|
||||
entity.startMoving(null);
|
||||
}
|
||||
}
|
||||
@@ -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<T extends EntityNPCInterface> extends ScriptLiving<T> 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<T extends EntityNPCInterface> extends ScriptLiving<T> 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<T extends EntityNPCInterface> extends ScriptLiving<T> 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<T extends EntityNPCInterface> extends ScriptLiving<T> 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<T extends EntityNPCInterface> extends ScriptLiving<T> 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 449 B After Width: | Height: | Size: 446 B |
Binary file not shown.
|
Before Width: | Height: | Size: 614 B |
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 5.8 KiB |
Reference in new issue
Block a user