mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-12 20:07:32 +00:00
Merge branch 'dev' into syntax-highlighter-unified
This commit is contained in:
commit
5cf69f90fb
72 files changed
+4861
-538
No files matched your search
@@ -4,6 +4,8 @@ import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.entity.EntityAbilityDisc;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.util.Vec3;
|
||||
import noppes.npcs.util.MathUtil;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
/**
|
||||
@@ -54,6 +56,16 @@ public class RenderAbilityDisc extends RenderEnergyAbility {
|
||||
GL11.glRotatef(90.0f, 1.0f, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
double motionX = entity.motionX;
|
||||
double motionY = entity.motionY;
|
||||
double motionZ = entity.motionZ;
|
||||
Vec3 motionVec = Vec3.createVectorHelper(motionX, motionY, motionZ);
|
||||
|
||||
float motionYaw = MathUtil.getYaw(motionVec);
|
||||
float motionPitch = MathUtil.getPitch(motionVec);
|
||||
GL11.glRotatef(motionYaw, 0, 1, 0);
|
||||
GL11.glRotatef(motionPitch, 1, 0, 0);
|
||||
|
||||
// Apply Y-axis rotation for spin
|
||||
GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f);
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.entity.EntityAbilityOrb;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.util.Vec3;
|
||||
import noppes.npcs.util.MathUtil;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
/**
|
||||
@@ -41,6 +43,16 @@ public class RenderAbilityOrb extends RenderEnergyAbility {
|
||||
renderAttachedLightning(orb, 0.6f * 0.5f, scale);
|
||||
}
|
||||
|
||||
double motionX = entity.motionX;
|
||||
double motionY = entity.motionY;
|
||||
double motionZ = entity.motionZ;
|
||||
Vec3 motionVec = Vec3.createVectorHelper(motionX, motionY, motionZ);
|
||||
|
||||
float motionYaw = MathUtil.getYaw(motionVec);
|
||||
float motionPitch = MathUtil.getPitch(motionVec);
|
||||
GL11.glRotatef(motionYaw, 0, 1, 0);
|
||||
GL11.glRotatef(motionPitch, 1, 0, 0);
|
||||
|
||||
// Use entity's interpolated rotation for smooth spinning
|
||||
GL11.glRotatef(orb.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f);
|
||||
GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f);
|
||||
|
||||
@@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer;
|
||||
import kamkeel.npcs.entity.EntityEnergyAbility;
|
||||
import kamkeel.npcs.entity.EntityEnergyProjectile;
|
||||
import net.minecraft.client.renderer.OpenGlHelper;
|
||||
import net.minecraft.client.renderer.Tessellator;
|
||||
import net.minecraft.client.renderer.entity.Render;
|
||||
@@ -296,6 +297,18 @@ public abstract class RenderEnergyAbility extends Render {
|
||||
int innerColor = entity.getInnerColor();
|
||||
int fadeTime = entity.getLightningFadeTime();
|
||||
|
||||
// Barrier impact spark: briefly force brighter white crackles at the contact point.
|
||||
if (entity instanceof EntityEnergyProjectile) {
|
||||
int sparkTicks = ((EntityEnergyProjectile) entity).getBarrierSparkTicks();
|
||||
if (sparkTicks > 0) {
|
||||
density = Math.max(density, 4.0f + sparkTicks * 0.6f);
|
||||
radius = Math.max(radius, innerRadius + Math.max(0.35f, baseSize * 0.65f));
|
||||
outerColor = 0xF0F6FF;
|
||||
innerColor = 0xFFFFFF;
|
||||
fadeTime = Math.max(fadeTime, 6);
|
||||
}
|
||||
}
|
||||
|
||||
state.update(density, radius, outerColor, innerColor, fadeTime);
|
||||
state.render();
|
||||
}
|
||||
|
||||
@@ -465,6 +465,11 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
}
|
||||
}
|
||||
|
||||
// Save velocity before damage to cancel vanilla knockback when ability knockback is zero
|
||||
double prevMotionX = hitEntity.motionX;
|
||||
double prevMotionY = hitEntity.motionY;
|
||||
double prevMotionZ = hitEntity.motionZ;
|
||||
|
||||
// Apply damage
|
||||
if (damage > 0) {
|
||||
// Check for ability extenders (e.g., DBC Addon damage routing)
|
||||
@@ -483,9 +488,14 @@ public abstract class Ability implements IAbility, IAbilityAction {
|
||||
}
|
||||
}
|
||||
|
||||
// Apply knockback if any
|
||||
// Apply knockback if any, otherwise restore velocity to cancel vanilla knockback
|
||||
if (knockback > 0 || knockbackUp > 0) {
|
||||
applyKnockback(hitEntity, knockbackDirX, knockbackDirZ, knockback, knockbackUp);
|
||||
} else {
|
||||
hitEntity.motionX = prevMotionX;
|
||||
hitEntity.motionY = prevMotionY;
|
||||
hitEntity.motionZ = prevMotionZ;
|
||||
hitEntity.velocityChanged = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
+26
@@ -37,6 +37,10 @@ public class EnergyBarrierData {
|
||||
public boolean meleeEnabled = false;
|
||||
public float meleeDamageMultiplier = 1.0f;
|
||||
|
||||
// Reflection: rebound projectiles that fail to break the barrier
|
||||
public boolean reflect = false;
|
||||
public float reflectStrengthPct = 25.0f;
|
||||
|
||||
/**
|
||||
* Per-projectile-type damage multipliers.
|
||||
* Key is the ability typeId (e.g., "ability.cnpc.orb", "ability.cnpc.beam").
|
||||
@@ -119,6 +123,22 @@ public class EnergyBarrierData {
|
||||
this.defaultMultiplier = defaultMultiplier;
|
||||
}
|
||||
|
||||
public boolean isReflect() {
|
||||
return reflect;
|
||||
}
|
||||
|
||||
public void setReflect(boolean reflect) {
|
||||
this.reflect = reflect;
|
||||
}
|
||||
|
||||
public float getReflectStrengthPct() {
|
||||
return reflectStrengthPct;
|
||||
}
|
||||
|
||||
public void setReflectStrengthPct(float reflectStrengthPct) {
|
||||
this.reflectStrengthPct = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
|
||||
}
|
||||
|
||||
// ==================== NBT ====================
|
||||
|
||||
public void writeNBT(NBTTagCompound nbt) {
|
||||
@@ -133,6 +153,8 @@ public class EnergyBarrierData {
|
||||
nbt.setBoolean("barrierAbsorbing", absorbing);
|
||||
nbt.setBoolean("barrierMelee", meleeEnabled);
|
||||
nbt.setFloat("barrierMeleeMult", meleeDamageMultiplier);
|
||||
nbt.setBoolean("barrierReflect", reflect);
|
||||
nbt.setFloat("barrierReflectStrPct", reflectStrengthPct);
|
||||
|
||||
NBTTagCompound multNbt = new NBTTagCompound();
|
||||
for (Map.Entry<String, Float> entry : damageMultipliers.entrySet()) {
|
||||
@@ -153,6 +175,8 @@ public class EnergyBarrierData {
|
||||
absorbing = nbt.hasKey("barrierAbsorbing") && nbt.getBoolean("barrierAbsorbing");
|
||||
meleeEnabled = nbt.hasKey("barrierMelee") && nbt.getBoolean("barrierMelee");
|
||||
meleeDamageMultiplier = nbt.hasKey("barrierMeleeMult") ? nbt.getFloat("barrierMeleeMult") : 1.0f;
|
||||
reflect = nbt.hasKey("barrierReflect") && nbt.getBoolean("barrierReflect");
|
||||
setReflectStrengthPct(nbt.hasKey("barrierReflectStrPct") ? nbt.getFloat("barrierReflectStrPct") : 25.0f);
|
||||
|
||||
damageMultipliers.clear();
|
||||
if (nbt.hasKey("barrierMultipliers")) {
|
||||
@@ -174,6 +198,8 @@ public class EnergyBarrierData {
|
||||
copy.absorbing = absorbing;
|
||||
copy.meleeEnabled = meleeEnabled;
|
||||
copy.meleeDamageMultiplier = meleeDamageMultiplier;
|
||||
copy.reflect = reflect;
|
||||
copy.reflectStrengthPct = reflectStrengthPct;
|
||||
copy.damageMultipliers = new HashMap<>(damageMultipliers);
|
||||
return copy;
|
||||
}
|
||||
|
||||
+37
-3
@@ -9,6 +9,10 @@ import noppes.npcs.api.ability.data.IEnergyCombatData;
|
||||
* Used as a parameter object for entity constructors and ability configuration.
|
||||
*/
|
||||
public class EnergyCombatData implements IEnergyCombatData {
|
||||
public static final float MAX_EXPLOSION_RADIUS = 15.0f;
|
||||
public static final int DEFAULT_MAX_HITS = 5;
|
||||
public static final int MAX_HITS = 200;
|
||||
|
||||
public float damage = 7.0f;
|
||||
public float knockback = 1.0f;
|
||||
public float knockbackUp = 0.1f;
|
||||
@@ -17,6 +21,7 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
public float explosionDamageFalloff = 0.5f;
|
||||
public HitType hitType = HitType.SINGLE;
|
||||
public int multiHitDelayTicks = 5;
|
||||
public int maxHits = DEFAULT_MAX_HITS;
|
||||
|
||||
public EnergyCombatData() {
|
||||
}
|
||||
@@ -24,14 +29,21 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
public EnergyCombatData(float damage, float knockback, float knockbackUp,
|
||||
boolean explosive, float explosionRadius, float explosionDamageFalloff,
|
||||
HitType hitType, int multiHitDelay) {
|
||||
this(damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, hitType, multiHitDelay, DEFAULT_MAX_HITS);
|
||||
}
|
||||
|
||||
public EnergyCombatData(float damage, float knockback, float knockbackUp,
|
||||
boolean explosive, float explosionRadius, float explosionDamageFalloff,
|
||||
HitType hitType, int multiHitDelay, int maxHits) {
|
||||
this.damage = damage;
|
||||
this.knockback = knockback;
|
||||
this.knockbackUp = knockbackUp;
|
||||
this.explosive = explosive;
|
||||
this.explosionRadius = explosionRadius;
|
||||
this.explosionRadius = clampExplosionRadius(explosionRadius);
|
||||
this.explosionDamageFalloff = explosionDamageFalloff;
|
||||
this.hitType = hitType;
|
||||
this.multiHitDelayTicks = multiHitDelay;
|
||||
this.maxHits = clampMaxHits(maxHits);
|
||||
}
|
||||
|
||||
public EnergyCombatData(float damage, float knockback, float knockbackUp,
|
||||
@@ -40,7 +52,7 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
this.knockback = knockback;
|
||||
this.knockbackUp = knockbackUp;
|
||||
this.explosive = explosive;
|
||||
this.explosionRadius = explosionRadius;
|
||||
this.explosionRadius = clampExplosionRadius(explosionRadius);
|
||||
this.explosionDamageFalloff = explosionDamageFalloff;
|
||||
}
|
||||
|
||||
@@ -91,7 +103,7 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
|
||||
@Override
|
||||
public void setExplosionRadius(float explosionRadius) {
|
||||
this.explosionRadius = Math.max(0, explosionRadius);
|
||||
this.explosionRadius = clampExplosionRadius(explosionRadius);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -104,6 +116,14 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
this.explosionDamageFalloff = explosionDamageFalloff;
|
||||
}
|
||||
|
||||
public int getMaxHits() {
|
||||
return maxHits;
|
||||
}
|
||||
|
||||
public void setMaxHits(int maxHits) {
|
||||
this.maxHits = clampMaxHits(maxHits);
|
||||
}
|
||||
|
||||
public void writeNBT(NBTTagCompound nbt) {
|
||||
nbt.setFloat("damage", damage);
|
||||
nbt.setFloat("knockback", knockback);
|
||||
@@ -113,6 +133,7 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff);
|
||||
nbt.setInteger("hitType", hitType.ordinal());
|
||||
nbt.setInteger("multiHitDelayTicks", multiHitDelayTicks);
|
||||
nbt.setInteger("maxHits", maxHits);
|
||||
}
|
||||
|
||||
public void readNBT(NBTTagCompound nbt) {
|
||||
@@ -124,12 +145,24 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f;
|
||||
hitType = HitType.fromOrdinal(nbt.hasKey("hitType") ? nbt.getInteger("hitType") : 0);
|
||||
multiHitDelayTicks = nbt.hasKey("multiHitDelayTicks") ? nbt.getInteger("multiHitDelayTicks") : 5;
|
||||
maxHits = nbt.hasKey("maxHits") ? nbt.getInteger("maxHits") : DEFAULT_MAX_HITS;
|
||||
|
||||
// Sanitize
|
||||
if (Float.isNaN(damage) || Float.isInfinite(damage)) damage = 7.0f;
|
||||
if (Float.isNaN(knockback) || Float.isInfinite(knockback) || knockback < 0) knockback = 1.0f;
|
||||
if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius) || explosionRadius < 0) explosionRadius = 3.0f;
|
||||
explosionRadius = clampExplosionRadius(explosionRadius);
|
||||
if (multiHitDelayTicks < 1) multiHitDelayTicks = 1;
|
||||
maxHits = clampMaxHits(maxHits);
|
||||
}
|
||||
|
||||
private static float clampExplosionRadius(float explosionRadius) {
|
||||
if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius)) return 0.0f;
|
||||
return Math.max(0.0f, Math.min(MAX_EXPLOSION_RADIUS, explosionRadius));
|
||||
}
|
||||
|
||||
private static int clampMaxHits(int maxHits) {
|
||||
return Math.max(1, Math.min(MAX_HITS, maxHits));
|
||||
}
|
||||
|
||||
public EnergyCombatData copy() {
|
||||
@@ -137,6 +170,7 @@ public class EnergyCombatData implements IEnergyCombatData {
|
||||
explosive, explosionRadius, explosionDamageFalloff);
|
||||
copy.hitType = hitType;
|
||||
copy.multiHitDelayTicks = multiHitDelayTicks;
|
||||
copy.maxHits = maxHits;
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tick == hitDelayTicks) {
|
||||
if (tick == hitDelayTicks && !caster.worldObj.isRemote) {
|
||||
// Calculate forward and right vectors from caster yaw
|
||||
float yawRad = (float) Math.toRadians(caster.rotationYaw);
|
||||
double forwardX = -Math.sin(yawRad);
|
||||
|
||||
@@ -258,10 +258,12 @@ public abstract class AbilityMovement extends Ability {
|
||||
/**
|
||||
* Apply movement velocity with zero vertical motion.
|
||||
* Used for ground-locked movement like Charge.
|
||||
* motionY is set BEFORE sending velocity packet to prevent client-server desync
|
||||
* (which causes "moved too fast" kicks when used in mid-air).
|
||||
*/
|
||||
protected void applyVelocityFlat(EntityLivingBase caster, float speed) {
|
||||
applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed);
|
||||
caster.motionY = 0;
|
||||
applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,7 +27,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
private float pushRadius = 8.0f;
|
||||
private float pushStrength = 1.5f;
|
||||
private float damage = 8.0f;
|
||||
private int maxTargets = 10;
|
||||
private boolean aoe = true;
|
||||
private int activeDisplayTicks = 10;
|
||||
|
||||
public AbilityShockwave() {
|
||||
@@ -62,7 +62,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
|
||||
@Override
|
||||
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
|
||||
if (!isPreview()) {
|
||||
if (!isPreview() && !caster.worldObj.isRemote) {
|
||||
// Shockwave is instant - apply effect immediately after windup
|
||||
|
||||
// Get all entities in radius
|
||||
@@ -70,47 +70,79 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
int count = 0;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue;
|
||||
if (aoe) {
|
||||
// ALL: push every valid enemy in range
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue;
|
||||
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist > pushRadius) continue;
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist > pushRadius) continue;
|
||||
|
||||
count++;
|
||||
if (count > maxTargets) break;
|
||||
|
||||
// Calculate push direction (away from caster)
|
||||
double dx = entity.posX - caster.posX;
|
||||
double dz = entity.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (len > 0) {
|
||||
dx /= len;
|
||||
dz /= len;
|
||||
} else {
|
||||
// Entity is directly on top of caster, push in random direction
|
||||
double angle = Math.random() * Math.PI * 2;
|
||||
dx = Math.cos(angle);
|
||||
dz = Math.sin(angle);
|
||||
applyShockwavePush(caster, entity, dist);
|
||||
}
|
||||
} else {
|
||||
// SINGULAR: push one target
|
||||
// NPC: push aggro target if valid, otherwise nearest enemy
|
||||
// Player: push nearest enemy in range
|
||||
if (!isPlayerCaster(caster) && target != null && !target.isDead) {
|
||||
double dist = caster.getDistanceToEntity(target);
|
||||
if (dist <= pushRadius && AbilityTargetHelper.shouldAffect(caster, target, TargetFilter.ENEMIES, false)) {
|
||||
applyShockwavePush(caster, target, dist);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Scale knockback by distance (closer = stronger)
|
||||
float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f;
|
||||
float finalPush = pushStrength * distFactor;
|
||||
// Apply damage with custom knockback direction
|
||||
boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz);
|
||||
// Find nearest valid enemy
|
||||
EntityLivingBase nearest = null;
|
||||
double nearestDist = Double.MAX_VALUE;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue;
|
||||
|
||||
// Apply effects if hit connected
|
||||
if (wasHit) {
|
||||
applyEffects(entity);
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist <= pushRadius && dist < nearestDist) {
|
||||
nearest = entity;
|
||||
nearestDist = dist;
|
||||
}
|
||||
}
|
||||
if (nearest != null) {
|
||||
applyShockwavePush(caster, nearest, nearestDist);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applyShockwavePush(EntityLivingBase caster, EntityLivingBase entity, double dist) {
|
||||
// Calculate push direction (away from caster)
|
||||
double dx = entity.posX - caster.posX;
|
||||
double dz = entity.posZ - caster.posZ;
|
||||
double len = Math.sqrt(dx * dx + dz * dz);
|
||||
|
||||
if (len > 0) {
|
||||
dx /= len;
|
||||
dz /= len;
|
||||
} else {
|
||||
// Entity is directly on top of caster, push in random direction
|
||||
double angle = Math.random() * Math.PI * 2;
|
||||
dx = Math.cos(angle);
|
||||
dz = Math.sin(angle);
|
||||
}
|
||||
|
||||
// Scale knockback by distance (closer = stronger)
|
||||
float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f;
|
||||
float finalPush = pushStrength * distFactor;
|
||||
// Apply damage with custom knockback direction
|
||||
boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz);
|
||||
|
||||
// Apply effects if hit connected
|
||||
if (wasHit) {
|
||||
applyEffects(entity);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
|
||||
if (tick >= activeDisplayTicks)
|
||||
@@ -122,7 +154,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
nbt.setFloat("pushRadius", pushRadius);
|
||||
nbt.setFloat("pushStrength", pushStrength);
|
||||
nbt.setFloat("damage", damage);
|
||||
nbt.setInteger("maxTargets", maxTargets);
|
||||
nbt.setBoolean("aoe", aoe);
|
||||
nbt.setInteger("activeDisplayTicks", activeDisplayTicks);
|
||||
}
|
||||
|
||||
@@ -131,7 +163,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
this.pushRadius = nbt.getFloat("pushRadius");
|
||||
this.pushStrength = nbt.getFloat("pushStrength");
|
||||
this.damage = nbt.getFloat("damage");
|
||||
this.maxTargets = nbt.getInteger("maxTargets");
|
||||
this.aoe = !nbt.hasKey("aoe") || nbt.getBoolean("aoe");
|
||||
this.activeDisplayTicks = nbt.hasKey("activeDisplayTicks") ? nbt.getInteger("activeDisplayTicks") : 10;
|
||||
}
|
||||
|
||||
@@ -160,12 +192,12 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
this.damage = damage;
|
||||
}
|
||||
|
||||
public int getMaxTargets() {
|
||||
return maxTargets;
|
||||
public boolean isAoe() {
|
||||
return aoe;
|
||||
}
|
||||
|
||||
public void setMaxTargets(int maxTargets) {
|
||||
this.maxTargets = maxTargets;
|
||||
public void setAoe(boolean aoe) {
|
||||
this.aoe = aoe;
|
||||
}
|
||||
|
||||
public int getActiveDisplayTicks() {
|
||||
@@ -186,7 +218,9 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave {
|
||||
FieldDef.floatField("gui.radius", this::getPushRadius, this::setPushRadius),
|
||||
FieldDef.floatField("gui.strength", this::getPushStrength, this::setPushStrength)
|
||||
),
|
||||
FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets),
|
||||
FieldDef.section("ability.section.aoe"),
|
||||
FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe)
|
||||
.hover("ability.hover.aoe"),
|
||||
FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200),
|
||||
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
|
||||
));
|
||||
|
||||
@@ -13,7 +13,9 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.network.play.server.S12PacketEntityVelocity;
|
||||
import net.minecraft.util.DamageSource;
|
||||
import net.minecraft.world.World;
|
||||
import noppes.npcs.api.ability.type.IAbilitySlam;
|
||||
@@ -61,6 +63,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
private transient boolean hasRisen = false; // Whether entity has risen above launch position
|
||||
private transient int airTicks = 0;
|
||||
private transient int maxAirTicks = 60; // Timeout to prevent stuck in air
|
||||
private transient boolean airSlam = false; // True when slam initiated while already in the air
|
||||
private transient double airSlamStartY = 0; // Start height for air-slam damage scaling
|
||||
private transient boolean wasFlying = false; // Restore player flight after air slam
|
||||
|
||||
public AbilitySlam() {
|
||||
this.typeId = "ability.cnpc.slam";
|
||||
@@ -165,22 +170,70 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
}
|
||||
|
||||
/**
|
||||
* Player slam: Launch in look direction using same ballistic arc as NPC slam.
|
||||
* AOE damage triggers wherever the player lands.
|
||||
* Player slam: behavior depends on whether the player is on the ground or in the air.
|
||||
* <p>
|
||||
* On ground: Launch in look direction using ballistic arc, full damage on landing.
|
||||
* In air: Slam straight down from current height, damage scales with fall distance.
|
||||
* Works regardless of flying state — position is forced downward each tick.
|
||||
*/
|
||||
private void executePlayerSlam(EntityLivingBase caster) {
|
||||
// Calculate target position along player's look direction (horizontal only)
|
||||
float yawRad = (float) Math.toRadians(caster.rotationYaw);
|
||||
double dirX = -Math.sin(yawRad);
|
||||
double dirZ = Math.cos(yawRad);
|
||||
if (!caster.onGround) {
|
||||
// Air slam: player is already airborne (flying, falling, jumping)
|
||||
airSlam = true;
|
||||
airSlamStartY = caster.posY;
|
||||
targetX = caster.posX;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ;
|
||||
|
||||
double launchDist = Math.max(4.0, maxRange * 0.5);
|
||||
// If the player is flying, temporarily disable it so gravity/velocity apply normally.
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
EntityPlayerMP mp = (EntityPlayerMP) caster;
|
||||
wasFlying = mp.capabilities.isFlying;
|
||||
if (wasFlying) {
|
||||
mp.capabilities.isFlying = false;
|
||||
mp.sendPlayerAbilities();
|
||||
}
|
||||
}
|
||||
|
||||
targetX = caster.posX + dirX * launchDist;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ + dirZ * launchDist;
|
||||
hasLaunched = true;
|
||||
hasRisen = true; // Skip rise detection — we're already above ground
|
||||
|
||||
launchTowardTarget(caster);
|
||||
// Start descent smoothly; physics/collision handles exact landing surface.
|
||||
// Only damp horizontal drift when movement is locked during ACTIVE.
|
||||
if (isMovementLockedDuringActive()) {
|
||||
caster.motionX *= 0.25;
|
||||
caster.motionZ *= 0.25;
|
||||
}
|
||||
caster.motionY = Math.min(caster.motionY, -0.2);
|
||||
|
||||
if (!isPreview()) {
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
|
||||
new S12PacketEntityVelocity(caster));
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
|
||||
}
|
||||
} else {
|
||||
// Ground slam: normal ballistic arc
|
||||
airSlam = false;
|
||||
airSlamStartY = 0;
|
||||
wasFlying = false;
|
||||
|
||||
// Calculate target position along player's look direction (horizontal only)
|
||||
float yawRad = (float) Math.toRadians(caster.rotationYaw);
|
||||
double dirX = -Math.sin(yawRad);
|
||||
double dirZ = Math.cos(yawRad);
|
||||
|
||||
double launchDist = Math.max(4.0, maxRange * 0.5);
|
||||
|
||||
targetX = caster.posX + dirX * launchDist;
|
||||
targetY = caster.posY;
|
||||
targetZ = caster.posZ + dirZ * launchDist;
|
||||
|
||||
launchTowardTarget(caster);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,7 +297,12 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(true);
|
||||
}
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
|
||||
new S12PacketEntityVelocity(caster));
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
|
||||
}
|
||||
return;
|
||||
@@ -292,7 +350,14 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(true);
|
||||
}
|
||||
if (!isPreview()) {
|
||||
caster.velocityChanged = true;
|
||||
// Use direct S12 packet for players to ensure velocity reaches client
|
||||
// before flight code can override it
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
|
||||
new S12PacketEntityVelocity(caster));
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Face the target
|
||||
@@ -306,6 +371,10 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
if (!hasLaunched) return;
|
||||
if (hasLanded) return;
|
||||
|
||||
boolean playerCaster = isPlayerCaster(caster);
|
||||
boolean activeMovementLocked = isMovementLockedDuringActive();
|
||||
boolean forceHorizontalControl = !playerCaster || activeMovementLocked;
|
||||
|
||||
airTicks++;
|
||||
|
||||
// Continuously reset fall distance during slam to prevent fall damage
|
||||
@@ -313,6 +382,25 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
caster.fallDistance = 0;
|
||||
}
|
||||
|
||||
// Air slam: accelerate descent smoothly via velocity; landing uses normal collision.
|
||||
if (airSlam && !isPreview() && !hasLanded) {
|
||||
if (forceHorizontalControl) {
|
||||
caster.motionX *= 0.2;
|
||||
caster.motionZ *= 0.2;
|
||||
}
|
||||
double nextMotionY = caster.motionY - 0.18;
|
||||
caster.motionY = Math.max(-1.6, nextMotionY);
|
||||
if (caster instanceof EntityPlayerMP) {
|
||||
// Only push authoritative velocity every tick when ACTIVE movement is locked.
|
||||
if (forceHorizontalControl) {
|
||||
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
|
||||
new S12PacketEntityVelocity(caster));
|
||||
}
|
||||
} else {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent NPC navigator from interfering with ballistic arc.
|
||||
// lockMovement=WINDUP means applyMovementControl() doesn't clear
|
||||
// the navigator during ACTIVE, so AI pathfinding could add motion.
|
||||
@@ -351,16 +439,19 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
return;
|
||||
}
|
||||
|
||||
// Face toward target destination while in air (both NPC and Player)
|
||||
// Face toward destination while in air unless player ACTIVE movement is unlocked.
|
||||
// Unlocked players keep free horizontal drift/look control.
|
||||
if (!isPreview()) {
|
||||
double dx = targetX - caster.posX;
|
||||
double dz = targetZ - caster.posZ;
|
||||
if (dx * dx + dz * dz > 0.25) {
|
||||
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
|
||||
caster.rotationYaw = targetYaw;
|
||||
caster.rotationYawHead = targetYaw;
|
||||
if (isPlayerCaster(caster)) {
|
||||
caster.velocityChanged = true;
|
||||
if (forceHorizontalControl) {
|
||||
double dx = targetX - caster.posX;
|
||||
double dz = targetZ - caster.posZ;
|
||||
if (dx * dx + dz * dz > 0.25) {
|
||||
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
|
||||
caster.rotationYaw = targetYaw;
|
||||
caster.rotationYawHead = targetYaw;
|
||||
if (playerCaster) {
|
||||
caster.velocityChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,13 +465,16 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
restoreFlightIfNeeded(caster);
|
||||
|
||||
// Signal that the ability has completed its active phase
|
||||
signalCompletion();
|
||||
|
||||
// Stop horizontal momentum
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
// Stop horizontal momentum only when movement is controlled by the ability.
|
||||
if (!isPlayerCaster(caster) || isMovementLockedDuringActive()) {
|
||||
caster.motionX = 0;
|
||||
caster.motionZ = 0;
|
||||
}
|
||||
|
||||
if (!isPreview()) {
|
||||
// Reset fall distance to prevent fall damage on landing
|
||||
@@ -394,6 +488,17 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
// Play slam impact sound on landing
|
||||
world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f);
|
||||
|
||||
// Scale damage based on fall distance for air slams
|
||||
float effectiveDamage = damage;
|
||||
if (airSlam) {
|
||||
double fallStartY = airSlamStartY > 0 ? airSlamStartY : startY;
|
||||
double fallDistance = fallStartY - caster.posY;
|
||||
if (fallDistance < 0) fallDistance = 0;
|
||||
// Scale linearly: full damage at leapHeight distance, minimum 25% for tiny falls
|
||||
float heightFactor = (float) Math.min(1.0, fallDistance / Math.max(1.0, leapHeight));
|
||||
effectiveDamage = damage * Math.max(0.25f, heightFactor);
|
||||
}
|
||||
|
||||
// Find all entities in radius
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Entity> entities = world.getEntitiesWithinAABBExcludingEntity(
|
||||
@@ -409,7 +514,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
double dz = livingTarget.posZ - caster.posZ;
|
||||
if (dx * dx + dz * dz <= radius * radius) {
|
||||
// Apply damage with scripted event support
|
||||
boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockbackStrength);
|
||||
boolean wasHit = applyAbilityDamage(caster, livingTarget, effectiveDamage, knockbackStrength);
|
||||
if (wasHit) {
|
||||
applyEffects(livingTarget);
|
||||
}
|
||||
@@ -462,10 +567,14 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
restoreFlightIfNeeded(caster);
|
||||
hasLaunched = false;
|
||||
hasLanded = false;
|
||||
hasRisen = false;
|
||||
airTicks = 0;
|
||||
airSlam = false;
|
||||
airSlamStartY = 0;
|
||||
wasFlying = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -473,18 +582,26 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
if (!isPreview() && caster instanceof EntityNPCInterface) {
|
||||
((EntityNPCInterface) caster).setNpcJumpingState(false);
|
||||
}
|
||||
restoreFlightIfNeeded(caster);
|
||||
hasLaunched = false;
|
||||
hasLanded = false;
|
||||
hasRisen = false;
|
||||
airTicks = 0;
|
||||
airSlam = false;
|
||||
airSlamStartY = 0;
|
||||
wasFlying = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
// Caster may be unavailable here; flight restoration is handled in onComplete/onInterrupt.
|
||||
hasLaunched = false;
|
||||
hasLanded = false;
|
||||
hasRisen = false;
|
||||
airTicks = 0;
|
||||
airSlam = false;
|
||||
airSlamStartY = 0;
|
||||
wasFlying = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -494,6 +611,22 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
|
||||
hasLanded = false;
|
||||
hasRisen = false;
|
||||
airTicks = 0;
|
||||
airSlam = false;
|
||||
airSlamStartY = 0;
|
||||
wasFlying = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore player flight if it was disabled for an air slam.
|
||||
*/
|
||||
private void restoreFlightIfNeeded(EntityLivingBase caster) {
|
||||
if (airSlam && wasFlying && caster instanceof EntityPlayerMP) {
|
||||
EntityPlayerMP mp = (EntityPlayerMP) caster;
|
||||
if (!mp.capabilities.isFlying) {
|
||||
mp.capabilities.isFlying = true;
|
||||
mp.sendPlayerAbilities();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -32,8 +32,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
private float pullStrength = 0.8f;
|
||||
private float damage = 0.0f;
|
||||
private float knockback = 0.0f;
|
||||
private boolean aoe = false;
|
||||
private int maxTargets = 5;
|
||||
private boolean aoe = true;
|
||||
private boolean damageOnPull = false;
|
||||
private float pullDamage = 0.0f;
|
||||
|
||||
@@ -83,13 +82,13 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
pullComplete = false;
|
||||
ticksSincePullDamage = 0;
|
||||
|
||||
if (!isPreview()) {
|
||||
if (aoe) {
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
if (!isPreview() && !caster.worldObj.isRemote) {
|
||||
AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box);
|
||||
|
||||
int count = 0;
|
||||
if (aoe) {
|
||||
// ALL: pull every valid enemy in range
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
@@ -98,19 +97,37 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(entity.getUniqueID());
|
||||
count++;
|
||||
if (count >= maxTargets) break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// NPC single-target mode: pull the aggro target
|
||||
// Player: single-target mode has no effect (no target to pull — use AOE mode instead)
|
||||
// SINGULAR: pull one target
|
||||
// NPC: pull aggro target if valid, otherwise nearest enemy
|
||||
// Player: pull nearest enemy in range
|
||||
if (!isPlayerCaster(caster) && target != null && !target.isDead) {
|
||||
double dist = caster.getDistanceToEntity(target);
|
||||
if (dist <= pullRadius) {
|
||||
getPulledEntities().add(target.getUniqueID());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find nearest valid enemy
|
||||
EntityLivingBase nearest = null;
|
||||
double nearestDist = Double.MAX_VALUE;
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (entity == caster) continue;
|
||||
if (entity.isDead) continue;
|
||||
if (!AbilityTargetHelper.shouldAffect(caster, entity, TargetFilter.ENEMIES, false)) continue;
|
||||
|
||||
double dist = caster.getDistanceToEntity(entity);
|
||||
if (dist <= pullRadius && dist < nearestDist) {
|
||||
nearest = entity;
|
||||
nearestDist = dist;
|
||||
}
|
||||
}
|
||||
if (nearest != null) {
|
||||
getPulledEntities().add(nearest.getUniqueID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,6 +145,8 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
return;
|
||||
}
|
||||
|
||||
if (caster.worldObj.isRemote) return;
|
||||
|
||||
double destX = caster.posX;
|
||||
double destY = caster.posY;
|
||||
double destZ = caster.posZ;
|
||||
@@ -229,7 +248,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
nbt.setFloat("damage", damage);
|
||||
nbt.setFloat("knockback", knockback);
|
||||
nbt.setBoolean("aoe", aoe);
|
||||
nbt.setInteger("maxTargets", maxTargets);
|
||||
nbt.setBoolean("damageOnPull", damageOnPull);
|
||||
nbt.setFloat("pullDamage", pullDamage);
|
||||
}
|
||||
@@ -240,8 +258,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
this.pullStrength = nbt.getFloat("pullStrength");
|
||||
this.damage = nbt.getFloat("damage");
|
||||
this.knockback = nbt.getFloat("knockback");
|
||||
this.aoe = nbt.getBoolean("aoe");
|
||||
this.maxTargets = nbt.getInteger("maxTargets");
|
||||
this.aoe = !nbt.hasKey("aoe") || nbt.getBoolean("aoe");
|
||||
this.damageOnPull = nbt.getBoolean("damageOnPull");
|
||||
this.pullDamage = nbt.getFloat("pullDamage");
|
||||
}
|
||||
@@ -287,14 +304,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
this.aoe = aoe;
|
||||
}
|
||||
|
||||
public int getMaxTargets() {
|
||||
return maxTargets;
|
||||
}
|
||||
|
||||
public void setMaxTargets(int maxTargets) {
|
||||
this.maxTargets = maxTargets;
|
||||
}
|
||||
|
||||
public boolean isDamageOnPull() {
|
||||
return damageOnPull;
|
||||
}
|
||||
@@ -327,8 +336,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex {
|
||||
FieldDef.section("ability.section.aoe"),
|
||||
FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe)
|
||||
.hover("ability.hover.aoe"),
|
||||
FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets)
|
||||
.visibleWhen(this::isAoe),
|
||||
FieldDef.section("ability.section.pullDamage"),
|
||||
FieldDef.boolField("gui.enabled", this::isDamageOnPull, this::setDamageOnPull)
|
||||
.hover("ability.hover.dmgOnPull"),
|
||||
|
||||
+29
-4
@@ -254,6 +254,23 @@ public abstract class AbilityBarrier extends AbilityEnergy {
|
||||
barrierData.absorbing = absorbing;
|
||||
}
|
||||
|
||||
// Reflection data
|
||||
public boolean isReflect() {
|
||||
return barrierData.reflect;
|
||||
}
|
||||
|
||||
public void setReflect(boolean reflect) {
|
||||
barrierData.reflect = reflect;
|
||||
}
|
||||
|
||||
public float getReflectStrengthPct() {
|
||||
return barrierData.reflectStrengthPct;
|
||||
}
|
||||
|
||||
public void setReflectStrengthPct(float strengthPct) {
|
||||
barrierData.setReflectStrengthPct(strengthPct);
|
||||
}
|
||||
|
||||
// Melee data
|
||||
public boolean isMeleeEnabled() {
|
||||
return barrierData.meleeEnabled;
|
||||
@@ -291,12 +308,20 @@ public abstract class AbilityBarrier extends AbilityEnergy {
|
||||
|
||||
// Properties section
|
||||
defs.add(FieldDef.section("ability.section.properties"));
|
||||
defs.add(FieldDef.boolField("ability.solid", this::isSolid, this::setSolid));
|
||||
defs.add(FieldDef.boolField("ability.knockbackEnabled", this::isKnockbackEnabled, this::setKnockbackEnabled));
|
||||
defs.add(FieldDef.boolField("ability.solid", this::isSolid, this::setSolid)
|
||||
.hover("ability.hover.solid"));
|
||||
defs.add(FieldDef.boolField("ability.knockbackEnabled", this::isKnockbackEnabled, this::setKnockbackEnabled)
|
||||
.hover("ability.hover.knockbackEnabled"));
|
||||
defs.add(FieldDef.floatField("ability.knockbackStrength", this::getKnockbackStrength, this::setKnockbackStrength)
|
||||
.range(0, 10).visibleWhen(this::isKnockbackEnabled));
|
||||
defs.add(FieldDef.boolField("ability.absorbing", this::isAbsorbing, this::setAbsorbing));
|
||||
defs.add(FieldDef.boolField("ability.meleeEnabled", this::isMeleeEnabled, this::setMeleeEnabled));
|
||||
defs.add(FieldDef.boolField("ability.absorbing", this::isAbsorbing, this::setAbsorbing)
|
||||
.hover("ability.hover.absorbing"));
|
||||
defs.add(FieldDef.boolField("ability.reflect", this::isReflect, this::setReflect)
|
||||
.hover("ability.hover.reflect"));
|
||||
defs.add(FieldDef.floatField("ability.reflectStrength", this::getReflectStrengthPct, this::setReflectStrengthPct)
|
||||
.range(0, 100).visibleWhen(this::isReflect));
|
||||
defs.add(FieldDef.boolField("ability.meleeEnabled", this::isMeleeEnabled, this::setMeleeEnabled)
|
||||
.hover("ability.hover.meleeEnabled"));
|
||||
defs.add(FieldDef.floatField("ability.meleeDamageMultiplier", this::getMeleeDamageMultiplier, this::setMeleeDamageMultiplier)
|
||||
.range(0, 10).visibleWhen(this::isMeleeEnabled));
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ public class AbilityBeam extends AbilityEnergyProjectile<EntityAbilityBeam> impl
|
||||
|
||||
public AbilityBeam() {
|
||||
super(
|
||||
new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f),
|
||||
new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f, HitType.MULTI, 5),
|
||||
new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 10.0f),
|
||||
new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f, HitType.SINGLE, 5),
|
||||
new EnergyHomingData(0.4f, true, 0.1f, 15.0f),
|
||||
new EnergyLifespanData(150.0f, 200)
|
||||
);
|
||||
@@ -200,7 +200,9 @@ public class AbilityBeam extends AbilityEnergyProjectile<EntityAbilityBeam> impl
|
||||
));
|
||||
defs.add(FieldDef.section("ability.section.explosive"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
|
||||
.visibleWhen(this::isExplosive));
|
||||
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public class AbilityDisc extends AbilityEnergyProjectile<EntityAbilityDisc> impl
|
||||
|
||||
public AbilityDisc() {
|
||||
super(
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f),
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 10.0f),
|
||||
new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f, HitType.PIERCE, 5),
|
||||
new EnergyHomingData(0.6f, true, 0.12f, 18.0f),
|
||||
new EnergyLifespanData(150.0f, 200)
|
||||
@@ -226,7 +226,9 @@ public class AbilityDisc extends AbilityEnergyProjectile<EntityAbilityDisc> impl
|
||||
defs.add(FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay).visibleWhen(this::isBoomerang));
|
||||
defs.add(FieldDef.section("ability.section.explosive"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
|
||||
.visibleWhen(this::isExplosive));
|
||||
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
}
|
||||
}
|
||||
+17
-4
@@ -254,10 +254,11 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
|
||||
/**
|
||||
* Hook for launch target selection.
|
||||
* Default returns null so projectiles launch along owner look vector.
|
||||
* Players launch along their look vector (crosshair-aligned), so target is ignored.
|
||||
* NPCs use their resolved target so launch math can track the target immediately.
|
||||
*/
|
||||
protected EntityLivingBase getLaunchTarget(EntityLivingBase caster, EntityLivingBase target) {
|
||||
return null;
|
||||
return isPlayerCaster(caster) ? null : target;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -650,12 +651,12 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
|
||||
@Override
|
||||
public float getExplosionRadius() {
|
||||
return combatData.explosionRadius;
|
||||
return combatData.getExplosionRadius();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setExplosionRadius(float explosionRadius) {
|
||||
combatData.explosionRadius = explosionRadius;
|
||||
combatData.setExplosionRadius(explosionRadius);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -684,6 +685,14 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
this.combatData.multiHitDelayTicks = Math.max(1, delay);
|
||||
}
|
||||
|
||||
public int getMaxHits() {
|
||||
return combatData.getMaxHits();
|
||||
}
|
||||
|
||||
public void setMaxHits(int maxHits) {
|
||||
combatData.setMaxHits(maxHits);
|
||||
}
|
||||
|
||||
// Lifespan data
|
||||
@Override
|
||||
public float getMaxDistance() {
|
||||
@@ -831,6 +840,10 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
|
||||
.range(1, 200)
|
||||
.visibleWhen(() -> getHitType() == HitType.MULTI.ordinal())
|
||||
.hover("ability.hover.multiHitDelay"));
|
||||
defs.add(FieldDef.intField("ability.maxHits", this::getMaxHits, this::setMaxHits)
|
||||
.range(1, EnergyCombatData.MAX_HITS)
|
||||
.visibleWhen(() -> getHitType() != HitType.SINGLE.ordinal())
|
||||
.hover("ability.hover.maxHits"));
|
||||
|
||||
// Type-specific fields first
|
||||
addTypeDefinitions(defs);
|
||||
|
||||
+3
-1
@@ -213,7 +213,9 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
|
||||
defs.add(FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f));
|
||||
defs.add(FieldDef.section("ability.section.explosive"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
|
||||
.visibleWhen(this::isExplosive));
|
||||
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,7 @@ public class AbilityOrb extends AbilityEnergyProjectile<EntityAbilityOrb> implem
|
||||
|
||||
public AbilityOrb() {
|
||||
super(
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f),
|
||||
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 10.0f),
|
||||
new EnergyCombatData(),
|
||||
new EnergyHomingData(),
|
||||
new EnergyLifespanData()
|
||||
@@ -187,7 +187,9 @@ public class AbilityOrb extends AbilityEnergyProjectile<EntityAbilityOrb> implem
|
||||
defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming));
|
||||
defs.add(FieldDef.section("ability.section.explosive"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
|
||||
.visibleWhen(this::isExplosive));
|
||||
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
}
|
||||
}
|
||||
@@ -180,7 +180,9 @@ public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
|
||||
));
|
||||
defs.add(FieldDef.section("ability.section.explosive"));
|
||||
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive));
|
||||
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
|
||||
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
|
||||
.visibleWhen(this::isExplosive));
|
||||
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,55 @@ import java.util.List;
|
||||
*/
|
||||
public abstract class EntityAbilityBarrier extends EntityEnergyAbility {
|
||||
|
||||
/**
|
||||
* Outcome of a projectile vs barrier interaction.
|
||||
*/
|
||||
public enum ProjectileHitResult {
|
||||
PASS,
|
||||
BLOCKED,
|
||||
BROKEN;
|
||||
|
||||
public boolean isAbsorbed() {
|
||||
return this == BLOCKED || this == BROKEN;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Full result of a projectile vs barrier hit after multiplier/event processing.
|
||||
*/
|
||||
public static class ProjectileHitOutcome {
|
||||
public final ProjectileHitResult result;
|
||||
public final float barrierHealthBefore;
|
||||
public final float barrierHealthAfter;
|
||||
public final float appliedBarrierDamage;
|
||||
public final float remainingProjectileDamage;
|
||||
public final boolean reflectEnabled;
|
||||
public final float reflectStrengthPct;
|
||||
public final boolean useHealth;
|
||||
|
||||
public ProjectileHitOutcome(ProjectileHitResult result,
|
||||
float barrierHealthBefore,
|
||||
float barrierHealthAfter,
|
||||
float appliedBarrierDamage,
|
||||
float remainingProjectileDamage,
|
||||
boolean reflectEnabled,
|
||||
float reflectStrengthPct,
|
||||
boolean useHealth) {
|
||||
this.result = result;
|
||||
this.barrierHealthBefore = barrierHealthBefore;
|
||||
this.barrierHealthAfter = barrierHealthAfter;
|
||||
this.appliedBarrierDamage = appliedBarrierDamage;
|
||||
this.remainingProjectileDamage = remainingProjectileDamage;
|
||||
this.reflectEnabled = reflectEnabled;
|
||||
this.reflectStrengthPct = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
|
||||
this.useHealth = useHealth;
|
||||
}
|
||||
|
||||
public boolean shouldReflect() {
|
||||
return reflectEnabled && useHealth && result == ProjectileHitResult.BLOCKED;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== ACTIVE BARRIER REGISTRY ====================
|
||||
private static final List<WeakReference<EntityAbilityBarrier>> activeBarriers = new ArrayList<>();
|
||||
private boolean tracked = false;
|
||||
@@ -164,6 +213,20 @@ public abstract class EntityAbilityBarrier extends EntityEnergyAbility {
|
||||
* Returns true if the barrier absorbed the hit (projectile should be destroyed).
|
||||
*/
|
||||
public boolean onProjectileHit(EntityEnergyProjectile projectile, float baseDamage) {
|
||||
return onProjectileHitDetailed(projectile, baseDamage).isAbsorbed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply damage to this barrier from a projectile and return detailed outcome.
|
||||
*/
|
||||
public ProjectileHitResult onProjectileHitDetailed(EntityEnergyProjectile projectile, float baseDamage) {
|
||||
return onProjectileHitResolved(projectile, baseDamage).result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply damage to this barrier from a projectile and return full hit context.
|
||||
*/
|
||||
public ProjectileHitOutcome onProjectileHitResolved(EntityEnergyProjectile projectile, float baseDamage) {
|
||||
// Get damage multiplier for this projectile type
|
||||
String typeId = "";
|
||||
if (projectile.getSourceAbility() != null) {
|
||||
@@ -175,25 +238,67 @@ public abstract class EntityAbilityBarrier extends EntityEnergyAbility {
|
||||
// Fire hit event (may cancel or modify damage)
|
||||
if (!worldObj.isRemote) {
|
||||
float eventDamage = EventHooks.onEnergyBarrierHit(this, ownerEntityId, projectile, damage);
|
||||
if (eventDamage < 0) return false; // Event cancelled — don't block
|
||||
if (eventDamage < 0) {
|
||||
return new ProjectileHitOutcome(
|
||||
ProjectileHitResult.PASS,
|
||||
currentHealth,
|
||||
currentHealth,
|
||||
0.0f,
|
||||
0.0f,
|
||||
barrierData.reflect,
|
||||
barrierData.reflectStrengthPct,
|
||||
barrierData.useHealth
|
||||
);
|
||||
}
|
||||
damage = eventDamage;
|
||||
}
|
||||
|
||||
triggerHitFlash();
|
||||
|
||||
float healthBefore = currentHealth;
|
||||
|
||||
if (!barrierData.useHealth) {
|
||||
return true; // Duration-only mode: block but don't take damage
|
||||
return new ProjectileHitOutcome(
|
||||
ProjectileHitResult.BLOCKED,
|
||||
healthBefore,
|
||||
healthBefore,
|
||||
0.0f,
|
||||
0.0f,
|
||||
false,
|
||||
barrierData.reflectStrengthPct,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
currentHealth -= damage;
|
||||
syncHealthPercent();
|
||||
|
||||
if (currentHealth <= 0) {
|
||||
float remaining = Math.max(0.0f, damage - healthBefore);
|
||||
onBarrierDestroyed();
|
||||
this.setDead();
|
||||
return new ProjectileHitOutcome(
|
||||
ProjectileHitResult.BROKEN,
|
||||
healthBefore,
|
||||
0.0f,
|
||||
damage,
|
||||
remaining,
|
||||
barrierData.reflect,
|
||||
barrierData.reflectStrengthPct,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
return new ProjectileHitOutcome(
|
||||
ProjectileHitResult.BLOCKED,
|
||||
healthBefore,
|
||||
currentHealth,
|
||||
damage,
|
||||
0.0f,
|
||||
barrierData.reflect,
|
||||
barrierData.reflectStrengthPct,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== MELEE DAMAGE ====================
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package kamkeel.npcs.entity;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
|
||||
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
|
||||
@@ -67,6 +69,16 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
super(world);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public boolean isInRangeToRenderDist(double distance) {
|
||||
// Beam can extend far from entity position (head); use a generous render distance
|
||||
// that accounts for the full beam length (origin to head)
|
||||
double headDist = Math.sqrt(headOffsetX * headOffsetX + headOffsetY * headOffsetY + headOffsetZ * headOffsetZ);
|
||||
double range = Math.max(128.0D, headDist * 2.0D + 64.0D);
|
||||
return distance < range * range;
|
||||
}
|
||||
|
||||
private static BeamMode modeFromAnchored(boolean anchored) {
|
||||
return anchored ? BeamMode.ANCHORED : BeamMode.FREE_TRAIL;
|
||||
}
|
||||
@@ -503,22 +515,22 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
|
||||
}
|
||||
|
||||
private void checkEntityCollision(double headX, double headY, double headZ) {
|
||||
double hitSize = headSize * 0.5;
|
||||
// Swept head hitbox prevents misses when beam head moves quickly between ticks.
|
||||
double prevHeadWorldX = startX + prevHeadOffsetX;
|
||||
double prevHeadWorldY = startY + prevHeadOffsetY;
|
||||
double prevHeadWorldZ = startZ + prevHeadOffsetZ;
|
||||
double hitSize = Math.max(0.05, headSize * 0.5);
|
||||
AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox(
|
||||
headX - hitSize, headY - hitSize, headZ - hitSize,
|
||||
headX + hitSize, headY + hitSize, headZ + hitSize
|
||||
Math.min(prevHeadWorldX, headX) - hitSize,
|
||||
Math.min(prevHeadWorldY, headY) - hitSize,
|
||||
Math.min(prevHeadWorldZ, headZ) - hitSize,
|
||||
Math.max(prevHeadWorldX, headX) + hitSize,
|
||||
Math.max(prevHeadWorldY, headY) + hitSize,
|
||||
Math.max(prevHeadWorldZ, headZ) + hitSize
|
||||
);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox);
|
||||
|
||||
for (EntityLivingBase entity : entities) {
|
||||
if (processEntityHit(entity, headX, headY, headZ)) {
|
||||
if (DEBUG_LOGGING) {
|
||||
LogWriter.info("[Beam] DEAD: Entity collision with " + entity.getClass().getSimpleName() + " at tick " + ticksExisted);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (processEntitiesInHitBox(hitBox, headX, headY, headZ) && DEBUG_LOGGING) {
|
||||
LogWriter.info("[Beam] DEAD: Entity collision at tick " + ticksExisted);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -265,15 +265,43 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
|
||||
}
|
||||
|
||||
private void checkEntityCollision() {
|
||||
// Disc hitbox: wider but thinner
|
||||
double halfWidth = discRadius * 0.5;
|
||||
double halfHeight = discThickness * 0.5;
|
||||
// Swept disc hitbox to avoid miss-through at high speed.
|
||||
double nextX = posX + motionX;
|
||||
double nextY = posY + motionY;
|
||||
double nextZ = posZ + motionZ;
|
||||
|
||||
double halfRadius = Math.max(0.05, discRadius * 0.5);
|
||||
double halfThickness = Math.max(0.03, discThickness * 0.5);
|
||||
double halfX;
|
||||
double halfY;
|
||||
double halfZ;
|
||||
|
||||
if (vertical) {
|
||||
// Vertical disc: thin axis follows horizontal travel direction, tall on Y.
|
||||
double horizLen = Math.sqrt(motionX * motionX + motionZ * motionZ);
|
||||
if (horizLen > 1.0e-5) {
|
||||
double nx = motionX / horizLen;
|
||||
double nz = motionZ / horizLen;
|
||||
halfX = Math.abs(nx) * halfThickness + Math.abs(nz) * halfRadius;
|
||||
halfZ = Math.abs(nz) * halfThickness + Math.abs(nx) * halfRadius;
|
||||
} else {
|
||||
halfX = halfRadius;
|
||||
halfZ = halfRadius;
|
||||
}
|
||||
halfY = halfRadius;
|
||||
} else {
|
||||
// Horizontal disc: wide on XZ, thin on Y.
|
||||
halfX = halfRadius;
|
||||
halfY = halfThickness;
|
||||
halfZ = halfRadius;
|
||||
}
|
||||
|
||||
AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox(
|
||||
posX - halfWidth, posY - halfHeight, posZ - halfWidth,
|
||||
posX + halfWidth, posY + halfHeight, posZ + halfWidth
|
||||
Math.min(posX, nextX) - halfX, Math.min(posY, nextY) - halfY, Math.min(posZ, nextZ) - halfZ,
|
||||
Math.max(posX, nextX) + halfX, Math.max(posY, nextY) + halfY, Math.max(posZ, nextZ) + halfZ
|
||||
);
|
||||
|
||||
processEntitiesInHitBox(hitBox, posX, posY, posZ);
|
||||
processEntitiesInHitBox(hitBox, nextX, nextY, nextZ);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -264,11 +264,13 @@ public class EntityAbilityDome extends EntityAbilityBarrier {
|
||||
List<EntityLivingBase> entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox);
|
||||
for (EntityLivingBase ent : entities) {
|
||||
if (ent.getEntityId() == ownerEntityId) continue;
|
||||
if (isAllyOfOwner(ent)) continue;
|
||||
|
||||
// Client-side: only process local player for solid prediction
|
||||
// Must be checked before isAllyOfOwner which uses server-only PlayerData
|
||||
if (worldObj.isRemote && (localPlayer == null || ent != localPlayer)) continue;
|
||||
|
||||
if (isAllyOfOwner(ent)) continue;
|
||||
|
||||
// Current position relative to dome center
|
||||
double dx = ent.posX - posX;
|
||||
double dy = (ent.posY + ent.height * 0.5) - posY;
|
||||
|
||||
@@ -41,6 +41,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
|
||||
// Lock vertical direction after firing (only update yaw, keep pitch fixed)
|
||||
private boolean lockVerticalDirection = false;
|
||||
private int reflectedLockTicks = 0;
|
||||
|
||||
// End point for rendering
|
||||
private double endX, endY, endZ;
|
||||
@@ -81,12 +82,19 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
this.dirZ = dz / len;
|
||||
}
|
||||
} else {
|
||||
// Fire in NPC's facing direction
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
this.dirX = -Math.sin(yaw) * Math.cos(pitch);
|
||||
this.dirY = -Math.sin(pitch);
|
||||
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
|
||||
// Fall back to owner look direction so launch matches visual aim.
|
||||
Vec3 look = owner == null ? null : owner.getLookVec();
|
||||
if (look != null) {
|
||||
this.dirX = look.xCoord;
|
||||
this.dirY = look.yCoord;
|
||||
this.dirZ = look.zCoord;
|
||||
} else {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
this.dirX = -Math.sin(yaw) * Math.cos(pitch);
|
||||
this.dirY = -Math.sin(pitch);
|
||||
this.dirZ = Math.cos(yaw) * Math.cos(pitch);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize end point
|
||||
@@ -113,9 +121,14 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
return;
|
||||
}
|
||||
|
||||
// Track owner's anchor position and rotation each tick
|
||||
// so the laser follows the NPC's facing direction
|
||||
updateLaserOriginAndDirection();
|
||||
// After reflection, hold trajectory briefly so the rebound is visible.
|
||||
if (reflectedLockTicks > 0) {
|
||||
reflectedLockTicks--;
|
||||
} else {
|
||||
// Track owner's anchor position and rotation each tick
|
||||
// so the laser follows the NPC's facing direction
|
||||
updateLaserOriginAndDirection();
|
||||
}
|
||||
|
||||
if (!fullyExtended) {
|
||||
// Expand the laser
|
||||
@@ -134,6 +147,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
// Check for block collision along the new segment
|
||||
if (!worldObj.isRemote) {
|
||||
checkBlockCollision();
|
||||
// Re-evaluate barriers against the current beam segment before entity hits.
|
||||
// This prevents first-tick pass-through where the pre-update barrier check
|
||||
// still sees a zero-length beam.
|
||||
if (checkBarrierCollisionForCurrentSegment()) {
|
||||
return;
|
||||
}
|
||||
checkEntityCollisionAlongLine();
|
||||
}
|
||||
} else {
|
||||
@@ -163,19 +182,33 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
if (owner == null || !(owner instanceof EntityLivingBase)) return;
|
||||
EntityLivingBase livingOwner = (EntityLivingBase) owner;
|
||||
|
||||
// Update direction from owner's current rotation
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
if (lockVerticalDirection) {
|
||||
// Only update horizontal direction (yaw), keep vertical (pitch) fixed
|
||||
double horizontalLen = Math.sqrt(dirX * dirX + dirZ * dirZ);
|
||||
if (horizontalLen < 0.001) horizontalLen = 1.0;
|
||||
dirX = -Math.sin(yaw) * horizontalLen;
|
||||
dirZ = Math.cos(yaw) * horizontalLen;
|
||||
} else {
|
||||
// Use owner look vector (head/eye aim) for direction updates so hit logic matches visuals.
|
||||
Vec3 look = livingOwner.getLookVec();
|
||||
if (look == null) {
|
||||
float yaw = (float) Math.toRadians(owner.rotationYaw);
|
||||
float pitch = (float) Math.toRadians(owner.rotationPitch);
|
||||
dirX = -Math.sin(yaw) * Math.cos(pitch);
|
||||
dirY = -Math.sin(pitch);
|
||||
dirZ = Math.cos(yaw) * Math.cos(pitch);
|
||||
look = Vec3.createVectorHelper(
|
||||
-Math.sin(yaw) * Math.cos(pitch),
|
||||
-Math.sin(pitch),
|
||||
Math.cos(yaw) * Math.cos(pitch)
|
||||
);
|
||||
}
|
||||
|
||||
if (lockVerticalDirection) {
|
||||
// Keep current vertical component, but align horizontal direction to current look.
|
||||
double lookHorizontalLen = Math.sqrt(look.xCoord * look.xCoord + look.zCoord * look.zCoord);
|
||||
if (lookHorizontalLen > 0.0001) {
|
||||
double currentHorizontalLen = Math.sqrt(dirX * dirX + dirZ * dirZ);
|
||||
if (currentHorizontalLen < 0.0001) {
|
||||
currentHorizontalLen = Math.sqrt(Math.max(0.0001, 1.0 - dirY * dirY));
|
||||
}
|
||||
dirX = (look.xCoord / lookHorizontalLen) * currentHorizontalLen;
|
||||
dirZ = (look.zCoord / lookHorizontalLen) * currentHorizontalLen;
|
||||
}
|
||||
} else {
|
||||
dirX = look.xCoord;
|
||||
dirY = look.yCoord;
|
||||
dirZ = look.zCoord;
|
||||
}
|
||||
|
||||
// Keep direction normalized for consistent expansion and collision math.
|
||||
@@ -188,10 +221,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
direction.zCoord / dirLen
|
||||
);
|
||||
} else {
|
||||
Vec3 look = livingOwner.getLookVec();
|
||||
if (look != null) {
|
||||
direction = look;
|
||||
}
|
||||
direction = look != null ? look : Vec3.createVectorHelper(1.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
dirX = direction.xCoord;
|
||||
@@ -320,49 +350,24 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
|
||||
/**
|
||||
* Check if an entity's bounding box intersects with the laser line.
|
||||
* Uses separate XZ (horizontal) and Y (vertical) checks against
|
||||
* the entity's full bounding box rather than just its center point.
|
||||
* Uses a swept segment vs expanded AABB intercept test for reliable hits.
|
||||
*/
|
||||
private boolean isEntityOnLine(EntityLivingBase entity) {
|
||||
double ex = entity.posX;
|
||||
double ez = entity.posZ;
|
||||
AxisAlignedBB bb = entity.boundingBox;
|
||||
if (bb == null) return false;
|
||||
|
||||
// Project entity XZ position onto the laser line direction
|
||||
double vx = ex - startX;
|
||||
double vz = ez - startZ;
|
||||
// Project using horizontal direction components only
|
||||
double dirLenXZ = Math.sqrt(dirX * dirX + dirZ * dirZ);
|
||||
double dot;
|
||||
if (dirLenXZ > 0.001) {
|
||||
dot = (vx * dirX + vz * dirZ + (entity.posY + entity.height * 0.5 - startY) * dirY);
|
||||
} else {
|
||||
// Laser fires nearly straight up/down - use full 3D projection
|
||||
dot = vx * dirX + (entity.posY + entity.height * 0.5 - startY) * dirY + vz * dirZ;
|
||||
}
|
||||
dot = Math.max(0, Math.min(dot, currentLength));
|
||||
// Minimum effective width keeps thin lasers from visually clipping targets without hitting.
|
||||
double effectiveWidth = Math.max(laserWidth, 0.65);
|
||||
double expand = effectiveWidth * 0.5;
|
||||
AxisAlignedBB expanded = bb.expand(expand, expand, expand);
|
||||
|
||||
// Closest point on laser line at this parameter
|
||||
double closestX = startX + dirX * dot;
|
||||
double closestY = startY + dirY * dot;
|
||||
double closestZ = startZ + dirZ * dot;
|
||||
Vec3 segmentStart = Vec3.createVectorHelper(startX, startY, startZ);
|
||||
Vec3 segmentEnd = Vec3.createVectorHelper(endX, endY, endZ);
|
||||
MovingObjectPosition intercept = expanded.calculateIntercept(segmentStart, segmentEnd);
|
||||
if (intercept != null) return true;
|
||||
|
||||
// XZ distance check: entity center vs laser line point
|
||||
// Use minimum collision width so visually thin lasers still hit reliably
|
||||
double xzDistSq = (ex - closestX) * (ex - closestX) + (ez - closestZ) * (ez - closestZ);
|
||||
double effectiveWidth = Math.max(laserWidth, 0.5);
|
||||
double hitRadiusXZ = effectiveWidth * 0.5 + entity.width * 0.5;
|
||||
if (xzDistSq > hitRadiusXZ * hitRadiusXZ) return false;
|
||||
|
||||
// Y overlap check: laser point vs entity's full height range
|
||||
double entityMinY = entity.boundingBox.minY;
|
||||
double entityMaxY = entity.boundingBox.maxY;
|
||||
double laserHalfWidth = effectiveWidth * 0.5;
|
||||
|
||||
// Check if laser Y is within entity's height range (expanded by laser half-width)
|
||||
if (closestY < entityMinY - laserHalfWidth) return false;
|
||||
if (closestY > entityMaxY + laserHalfWidth) return false;
|
||||
|
||||
return true;
|
||||
// Handle edge case where the segment starts or ends already inside the target volume.
|
||||
return expanded.isVecInside(segmentStart) || expanded.isVecInside(segmentEnd);
|
||||
}
|
||||
|
||||
// ==================== BARRIER COLLISION ====================
|
||||
@@ -375,7 +380,17 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
protected boolean checkBarrierCollision() {
|
||||
if (currentLength <= 0 || fullyExtended) return false;
|
||||
return checkBarrierCollisionInternal(false);
|
||||
}
|
||||
|
||||
private boolean checkBarrierCollisionForCurrentSegment() {
|
||||
return checkBarrierCollisionInternal(true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private boolean checkBarrierCollisionInternal(boolean allowWhenFullyExtended) {
|
||||
if (currentLength <= 0) return false;
|
||||
if (fullyExtended && !allowWhenFullyExtended) return false;
|
||||
|
||||
List<EntityAbilityBarrier> barriers = EntityAbilityBarrier.getActiveBarriers(worldObj);
|
||||
for (EntityAbilityBarrier barrier : barriers) {
|
||||
@@ -395,32 +410,147 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
|
||||
EntityAbilityDome dome = (EntityAbilityDome) barrier;
|
||||
float intersectDist = getLineSphereIntersection(dome);
|
||||
if (intersectDist >= 0) {
|
||||
// Truncate laser at dome surface
|
||||
currentLength = intersectDist;
|
||||
endX = startX + dirX * currentLength;
|
||||
endY = startY + dirY * currentLength;
|
||||
endZ = startZ + dirZ * currentLength;
|
||||
fullyExtended = true;
|
||||
float damage = getModifiedDamage();
|
||||
EntityAbilityBarrier.ProjectileHitOutcome hitOutcome = dome.onProjectileHitResolved(this, damage);
|
||||
if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float damage = getModifiedDamage();
|
||||
dome.onProjectileHit(this, damage);
|
||||
return false; // Don't kill laser — it's truncated and will linger
|
||||
}
|
||||
} else {
|
||||
// Non-dome barriers: fall through to standard check
|
||||
if (barrier.isIncomingProjectile(this)) {
|
||||
float damage = getModifiedDamage();
|
||||
if (barrier.onProjectileHit(this, damage)) {
|
||||
hasHit = true;
|
||||
this.setDead();
|
||||
if (hitOutcome.result != EntityAbilityBarrier.ProjectileHitResult.BROKEN) {
|
||||
currentLength = Math.max(0.0f, intersectDist);
|
||||
endX = startX + dirX * currentLength;
|
||||
endY = startY + dirY * currentLength;
|
||||
endZ = startZ + dirZ * currentLength;
|
||||
}
|
||||
|
||||
if (handleBarrierHitOutcome(dome, hitOutcome)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// For line-based lasers, use a swept segment against generic barrier checks.
|
||||
// Panel/Dome logic expects previous+current positions and motion; map those
|
||||
// to [start -> end] each tick.
|
||||
double segMotionX = endX - startX;
|
||||
double segMotionY = endY - startY;
|
||||
double segMotionZ = endZ - startZ;
|
||||
boolean incoming = barrier.isIncomingGenericProjectile(
|
||||
endX, endY, endZ,
|
||||
segMotionX, segMotionY, segMotionZ,
|
||||
startX, startY, startZ,
|
||||
this.ownerEntityId
|
||||
);
|
||||
|
||||
if (incoming) {
|
||||
float damage = getModifiedDamage();
|
||||
EntityAbilityBarrier.ProjectileHitOutcome hitOutcome = barrier.onProjectileHitResolved(this, damage);
|
||||
if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hitOutcome.result != EntityAbilityBarrier.ProjectileHitResult.BROKEN) {
|
||||
// Keep current segment length at the exact collision tick position.
|
||||
endX = startX + dirX * currentLength;
|
||||
endY = startY + dirY * currentLength;
|
||||
endZ = startZ + dirZ * currentLength;
|
||||
}
|
||||
|
||||
if (handleBarrierHitOutcome(barrier, hitOutcome)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean reflectFromBarrier(EntityAbilityBarrier barrier, float reflectStrengthPct) {
|
||||
if (barrier == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double vx = dirX;
|
||||
double vy = dirY;
|
||||
double vz = dirZ;
|
||||
double vLenSq = vx * vx + vy * vy + vz * vz;
|
||||
if (vLenSq < 1.0e-8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double[] normal = getBarrierImpactNormal(barrier, vx, vy, vz);
|
||||
if (normal == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double dot = vx * normal[0] + vy * normal[1] + vz * normal[2];
|
||||
if (dot > 0.0) {
|
||||
normal[0] = -normal[0];
|
||||
normal[1] = -normal[1];
|
||||
normal[2] = -normal[2];
|
||||
dot = vx * normal[0] + vy * normal[1] + vz * normal[2];
|
||||
}
|
||||
|
||||
double rx = vx - 2.0 * dot * normal[0];
|
||||
double ry = vy - 2.0 * dot * normal[1];
|
||||
double rz = vz - 2.0 * dot * normal[2];
|
||||
if (ry < -0.06) {
|
||||
ry = Math.max(0.08, Math.abs(ry) * 0.35);
|
||||
}
|
||||
double rLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
||||
if (rLen < 1.0e-8) {
|
||||
rx = -vx;
|
||||
ry = -vy;
|
||||
rz = -vz;
|
||||
rLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
||||
if (rLen < 1.0e-8) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
dirX = rx / rLen;
|
||||
dirY = ry / rLen;
|
||||
dirZ = rz / rLen;
|
||||
|
||||
// Start a fresh segment at the impact edge to make the rebound visible.
|
||||
startX = endX;
|
||||
startY = endY;
|
||||
startZ = endZ;
|
||||
setPosition(startX, startY, startZ);
|
||||
syncPositionStateToCurrent(true);
|
||||
|
||||
currentLength = 0.0f;
|
||||
fullyExtended = false;
|
||||
ticksSinceFullExtension = 0;
|
||||
endX = startX;
|
||||
endY = startY;
|
||||
endZ = startZ;
|
||||
|
||||
reflectedLockTicks = Math.max(reflectedLockTicks, 10);
|
||||
barrierImpactPauseTicks = 0;
|
||||
barrierImpactDestroyOnResume = false;
|
||||
|
||||
Entity barrierOwner = barrier.getOwnerEntity();
|
||||
if (barrierOwner != null) {
|
||||
setOwnerEntityId(barrierOwner.getEntityId());
|
||||
trackProjectile(this);
|
||||
}
|
||||
setTargetEntityId(-1);
|
||||
|
||||
setInnerColor(barrier.getInnerColor());
|
||||
setOuterColor(barrier.getOuterColor());
|
||||
|
||||
float clampedStrength = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
|
||||
float reducedDamage = getDamage() * (1.0f - clampedStrength / 100.0f);
|
||||
setCombatDamage(Math.max(0.0f, reducedDamage));
|
||||
|
||||
hitOnceEntities.clear();
|
||||
lastHitTickByEntity.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Line-sphere intersection between the laser line segment and a dome.
|
||||
* Returns the distance along the laser direction to the first entry point,
|
||||
|
||||
@@ -72,7 +72,7 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
|
||||
* Called by ability when windup ends.
|
||||
*/
|
||||
public void startMoving(EntityLivingBase target) {
|
||||
startMovingAlongLookVectorDefault();
|
||||
startMovingTowardTargetDefault(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -421,11 +421,13 @@ public class EntityAbilityPanel extends EntityAbilityBarrier {
|
||||
|
||||
for (EntityLivingBase ent : entities) {
|
||||
if (ent.getEntityId() == ownerEntityId) continue;
|
||||
if (isAllyOfOwner(ent)) continue;
|
||||
|
||||
// Client-side: only process local player for solid prediction
|
||||
// Must be checked before isAllyOfOwner which uses server-only PlayerData
|
||||
if (worldObj.isRemote && (localPlayer == null || ent != localPlayer)) continue;
|
||||
|
||||
if (isAllyOfOwner(ent)) continue;
|
||||
|
||||
// Current position relative to panel center
|
||||
double dx = ent.posX - posX;
|
||||
double dy = (ent.posY + ent.height * 0.5) - posY;
|
||||
|
||||
@@ -19,7 +19,6 @@ import net.minecraft.block.Block;
|
||||
import net.minecraft.block.material.Material;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.EntityLivingBase;
|
||||
import net.minecraft.entity.EnumCreatureType;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.init.Blocks;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
@@ -125,8 +124,17 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
|
||||
// ==================== STATE ====================
|
||||
protected boolean hasHit = false;
|
||||
protected int hitCount = 0;
|
||||
protected final Set<Integer> hitOnceEntities = new HashSet<Integer>();
|
||||
protected final Map<Integer, Integer> lastHitTickByEntity = new HashMap<Integer, Integer>();
|
||||
protected static final int BARRIER_IMPACT_PAUSE_TICKS = 10;
|
||||
protected static final int BARRIER_BREAK_SPARK_TICKS = 10;
|
||||
protected static final int DW_BARRIER_SPARK_TICKS = 21;
|
||||
protected static final int DW_SYNC_INNER_COLOR = 22;
|
||||
protected static final int DW_SYNC_OUTER_COLOR = 23;
|
||||
protected int barrierImpactPauseTicks = 0;
|
||||
protected boolean barrierImpactDestroyOnResume = false;
|
||||
protected double pausedMotionX, pausedMotionY, pausedMotionZ;
|
||||
|
||||
// ==================== CHARGING STATE (projectile-specific) ====================
|
||||
protected float targetSize = 1.0f;
|
||||
@@ -151,6 +159,15 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
this.noClip = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void entityInit() {
|
||||
super.entityInit(); // DW_CHARGING = 20
|
||||
this.dataWatcher.addObject(DW_BARRIER_SPARK_TICKS, 0);
|
||||
// displayData is not guaranteed to be initialized yet during Entity construction.
|
||||
this.dataWatcher.addObject(DW_SYNC_INNER_COLOR, 0xFFFFFF);
|
||||
this.dataWatcher.addObject(DW_SYNC_OUTER_COLOR, 0xFFFFFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize common properties using data classes. Call from subclass constructors.
|
||||
*/
|
||||
@@ -175,6 +192,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
this.lifespanData = lifespan; // deathWorldTime will be set on first tick when world is available
|
||||
this.lightningData = lightning;
|
||||
this.trajectoryData = trajectory;
|
||||
syncProjectileColorWatchers();
|
||||
|
||||
// Initialize render size
|
||||
this.renderCurrentSize = size;
|
||||
@@ -210,12 +228,21 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
}
|
||||
}
|
||||
|
||||
if (!previewMode) {
|
||||
syncDisplayColorsFromWatchers();
|
||||
}
|
||||
|
||||
// Update rotation
|
||||
updateRotation();
|
||||
|
||||
// Lerp render size toward actual size
|
||||
this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f;
|
||||
|
||||
// Server-synced visual timer used for brief barrier impact lightning.
|
||||
if (!previewMode && !worldObj.isRemote) {
|
||||
tickBarrierSparkTimer();
|
||||
}
|
||||
|
||||
// Skip lifetime/distance checks in preview mode
|
||||
if (!previewMode) {
|
||||
// Only self-destruct if the owner is confirmed dead/killed.
|
||||
@@ -277,10 +304,16 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
}
|
||||
}
|
||||
|
||||
if (!previewMode && !worldObj.isRemote && !isCharging()) {
|
||||
if (tickBarrierImpactPause()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check barrier collisions (server-side, non-preview, non-charging)
|
||||
if (!previewMode && !worldObj.isRemote && !isCharging()) {
|
||||
if (checkBarrierCollision()) {
|
||||
return; // Projectile was absorbed by a barrier
|
||||
return; // Barrier interaction handled (pause + destroy/continue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,7 +364,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
* Check for collision with energy barrier entities.
|
||||
* Barriers only block incoming projectiles from enemies.
|
||||
*
|
||||
* @return true if projectile was absorbed (caller should stop processing)
|
||||
* @return true if barrier interaction was handled (caller should stop processing this tick)
|
||||
*/
|
||||
protected boolean checkBarrierCollision() {
|
||||
List<EntityAbilityBarrier> barriers = EntityAbilityBarrier.getActiveBarriers(worldObj);
|
||||
@@ -348,17 +381,356 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
|
||||
if (barrier.isIncomingProjectile(this)) {
|
||||
float damage = getModifiedDamage();
|
||||
if (barrier.onProjectileHit(this, damage)) {
|
||||
hasHit = true;
|
||||
this.setDead();
|
||||
EntityAbilityBarrier.ProjectileHitOutcome hitOutcome = barrier.onProjectileHitResolved(this, damage);
|
||||
if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) {
|
||||
continue;
|
||||
}
|
||||
if (handleBarrierHitOutcome(barrier, hitOutcome)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the result of a projectile/barrier interaction.
|
||||
*
|
||||
* @return true if caller should stop processing this tick.
|
||||
*/
|
||||
protected boolean handleBarrierHitOutcome(EntityAbilityBarrier barrier, EntityAbilityBarrier.ProjectileHitOutcome hitOutcome) {
|
||||
if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.BROKEN) {
|
||||
if (hitOutcome.remainingProjectileDamage <= 0.0f) {
|
||||
setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS));
|
||||
hasHit = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
setCombatDamage(hitOutcome.remainingProjectileDamage);
|
||||
setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hitOutcome.shouldReflect()) {
|
||||
if (!reflectFromBarrier(barrier, hitOutcome.reflectStrengthPct)) {
|
||||
beginBarrierImpactPause(true, barrier);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
beginBarrierImpactPause(true, barrier);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean reflectFromBarrier(EntityAbilityBarrier barrier, float reflectStrengthPct) {
|
||||
if (barrier == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double vx = motionX;
|
||||
double vy = motionY;
|
||||
double vz = motionZ;
|
||||
double vLenSq = vx * vx + vy * vy + vz * vz;
|
||||
if (vLenSq < 1.0e-8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double[] normal = getBarrierImpactNormal(barrier, vx, vy, vz);
|
||||
if (normal == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
double dot = vx * normal[0] + vy * normal[1] + vz * normal[2];
|
||||
if (dot > 0.0) {
|
||||
normal[0] = -normal[0];
|
||||
normal[1] = -normal[1];
|
||||
normal[2] = -normal[2];
|
||||
dot = vx * normal[0] + vy * normal[1] + vz * normal[2];
|
||||
}
|
||||
|
||||
double rx = vx - 2.0 * dot * normal[0];
|
||||
double ry = vy - 2.0 * dot * normal[1];
|
||||
double rz = vz - 2.0 * dot * normal[2];
|
||||
double rLenSq = rx * rx + ry * ry + rz * rz;
|
||||
if (rLenSq < 1.0e-8) {
|
||||
rx = -vx;
|
||||
ry = -vy;
|
||||
rz = -vz;
|
||||
rLenSq = rx * rx + ry * ry + rz * rz;
|
||||
}
|
||||
|
||||
// Prefer rebounding out and away from ground instead of immediately diving downward.
|
||||
if (ry < -0.06) {
|
||||
double speed = Math.sqrt(Math.max(rLenSq, 1.0e-8));
|
||||
ry = Math.max(0.08, Math.abs(ry) * 0.35);
|
||||
double newLen = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
||||
if (newLen > 1.0e-8) {
|
||||
double scale = speed / newLen;
|
||||
rx *= scale;
|
||||
ry *= scale;
|
||||
rz *= scale;
|
||||
}
|
||||
}
|
||||
|
||||
// Push slightly outside the collision boundary so the reflected motion
|
||||
// starts cleanly from the barrier surface instead of re-colliding in place.
|
||||
snapOutsideBarrierForPause(barrier);
|
||||
|
||||
motionX = rx;
|
||||
motionY = ry;
|
||||
motionZ = rz;
|
||||
pausedMotionX = rx;
|
||||
pausedMotionY = ry;
|
||||
pausedMotionZ = rz;
|
||||
barrierImpactPauseTicks = 0;
|
||||
barrierImpactDestroyOnResume = false;
|
||||
|
||||
Entity barrierOwner = barrier.getOwnerEntity();
|
||||
if (barrierOwner != null) {
|
||||
setOwnerEntityId(barrierOwner.getEntityId());
|
||||
// Ensure ownership-sensitive systems can query this projectile under the new owner.
|
||||
trackProjectile(this);
|
||||
}
|
||||
// Old homing target may now be the new owner or ally; clear it after reflection.
|
||||
setTargetEntityId(-1);
|
||||
|
||||
setInnerColor(barrier.getInnerColor());
|
||||
setOuterColor(barrier.getOuterColor());
|
||||
|
||||
float clampedStrength = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
|
||||
float reducedDamage = getDamage() * (1.0f - clampedStrength / 100.0f);
|
||||
setCombatDamage(Math.max(0.0f, reducedDamage));
|
||||
|
||||
hitOnceEntities.clear();
|
||||
lastHitTickByEntity.clear();
|
||||
hitCount = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
protected double[] getBarrierImpactNormal(EntityAbilityBarrier barrier, double velocityX, double velocityY, double velocityZ) {
|
||||
if (barrier instanceof EntityAbilityDome) {
|
||||
EntityAbilityDome dome = (EntityAbilityDome) barrier;
|
||||
return normalizeVector(posX - dome.posX, posY - dome.posY, posZ - dome.posZ, velocityX, velocityY, velocityZ);
|
||||
}
|
||||
|
||||
if (barrier instanceof EntityAbilityPanel) {
|
||||
EntityAbilityPanel panel = (EntityAbilityPanel) 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};
|
||||
}
|
||||
|
||||
protected void beginBarrierImpactPause(boolean destroyOnResume, EntityAbilityBarrier barrier) {
|
||||
if (barrierImpactPauseTicks <= 0) {
|
||||
pausedMotionX = motionX;
|
||||
pausedMotionY = motionY;
|
||||
pausedMotionZ = motionZ;
|
||||
}
|
||||
|
||||
barrierImpactDestroyOnResume = destroyOnResume;
|
||||
barrierImpactPauseTicks = BARRIER_IMPACT_PAUSE_TICKS;
|
||||
motionX = 0;
|
||||
motionY = 0;
|
||||
motionZ = 0;
|
||||
|
||||
int sparkTicks = getBarrierSparkTicks();
|
||||
if (sparkTicks < BARRIER_IMPACT_PAUSE_TICKS) {
|
||||
setBarrierSparkTicks(BARRIER_IMPACT_PAUSE_TICKS);
|
||||
}
|
||||
}
|
||||
|
||||
protected float getBarrierPauseOutsideDistance() {
|
||||
// Keep the paused projectile just outside the surface without a visible "fling".
|
||||
return Math.max(0.06f, Math.min(0.22f, size * 0.18f + 0.03f));
|
||||
}
|
||||
|
||||
protected void snapOutsideBarrierForPause(EntityAbilityBarrier barrier) {
|
||||
if (barrier == null) return;
|
||||
|
||||
float bias = getBarrierPauseOutsideDistance();
|
||||
if (barrier instanceof EntityAbilityDome) {
|
||||
EntityAbilityDome dome = (EntityAbilityDome) 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 EntityAbilityPanel) {
|
||||
EntityAbilityPanel panel = (EntityAbilityPanel) 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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean tickBarrierImpactPause() {
|
||||
if (barrierImpactPauseTicks <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
motionX = 0;
|
||||
motionY = 0;
|
||||
motionZ = 0;
|
||||
barrierImpactPauseTicks--;
|
||||
|
||||
if (barrierImpactPauseTicks <= 0) {
|
||||
if (barrierImpactDestroyOnResume) {
|
||||
hasHit = true;
|
||||
this.setDead();
|
||||
} else {
|
||||
motionX = pausedMotionX;
|
||||
motionY = pausedMotionY;
|
||||
motionZ = pausedMotionZ;
|
||||
}
|
||||
barrierImpactDestroyOnResume = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void tickBarrierSparkTimer() {
|
||||
int sparkTicks = getBarrierSparkTicks();
|
||||
if (sparkTicks > 0) {
|
||||
setBarrierSparkTicks(sparkTicks - 1);
|
||||
}
|
||||
}
|
||||
|
||||
protected void setBarrierSparkTicks(int ticks) {
|
||||
if (!worldObj.isRemote) {
|
||||
this.dataWatcher.updateObject(DW_BARRIER_SPARK_TICKS, Math.max(0, ticks));
|
||||
}
|
||||
}
|
||||
|
||||
public int getBarrierSparkTicks() {
|
||||
if (this.dataWatcher == null) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, this.dataWatcher.getWatchableObjectInt(DW_BARRIER_SPARK_TICKS));
|
||||
}
|
||||
|
||||
protected void syncProjectileColorWatchers() {
|
||||
if (this.dataWatcher == null) {
|
||||
return;
|
||||
}
|
||||
this.dataWatcher.updateObject(DW_SYNC_INNER_COLOR, this.displayData.getInnerColor());
|
||||
this.dataWatcher.updateObject(DW_SYNC_OUTER_COLOR, this.displayData.getOuterColor());
|
||||
}
|
||||
|
||||
protected void syncDisplayColorsFromWatchers() {
|
||||
if (this.dataWatcher == null) {
|
||||
return;
|
||||
}
|
||||
this.displayData.setInnerColor(this.dataWatcher.getWatchableObjectInt(DW_SYNC_INNER_COLOR));
|
||||
this.displayData.setOuterColor(this.dataWatcher.getWatchableObjectInt(DW_SYNC_OUTER_COLOR));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setInnerColor(int color) {
|
||||
super.setInnerColor(color);
|
||||
if (!previewMode) {
|
||||
syncProjectileColorWatchers();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOuterColor(int color) {
|
||||
super.setOuterColor(color);
|
||||
if (!previewMode) {
|
||||
syncProjectileColorWatchers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the projectile damage modified by extenders (e.g. DBC damage scaling).
|
||||
* Falls back to base getDamage() when no sourceAbility or no extenders modify it.
|
||||
@@ -432,9 +804,23 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
*/
|
||||
protected void handleClientInterpolation() {
|
||||
if (this.interpSteps > 0) {
|
||||
double newX = this.posX + (this.interpTargetX - this.posX) / this.interpSteps;
|
||||
double newY = this.posY + (this.interpTargetY - this.posY) / this.interpSteps;
|
||||
double newZ = this.posZ + (this.interpTargetZ - this.posZ) / this.interpSteps;
|
||||
// For very slow / stationary projectiles, snap directly to avoid jitter
|
||||
double dx = this.interpTargetX - this.posX;
|
||||
double dy = this.interpTargetY - this.posY;
|
||||
double dz = this.interpTargetZ - this.posZ;
|
||||
double distSq = dx * dx + dy * dy + dz * dz;
|
||||
if (distSq < 0.04) {
|
||||
this.setPosition(this.interpTargetX, this.interpTargetY, this.interpTargetZ);
|
||||
this.motionX = this.interpTargetMotionX;
|
||||
this.motionY = this.interpTargetMotionY;
|
||||
this.motionZ = this.interpTargetMotionZ;
|
||||
this.interpSteps = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
double newX = this.posX + dx / this.interpSteps;
|
||||
double newY = this.posY + dy / this.interpSteps;
|
||||
double newZ = this.posZ + dz / this.interpSteps;
|
||||
|
||||
this.motionX = this.motionX + (this.interpTargetMotionX - this.motionX) / this.interpSteps;
|
||||
this.motionY = this.motionY + (this.interpTargetMotionY - this.motionY) / this.interpSteps;
|
||||
@@ -443,6 +829,11 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
this.setPosition(newX, newY, newZ);
|
||||
this.interpSteps--;
|
||||
} else {
|
||||
// Skip motion application for nearly-stationary projectiles to prevent drift
|
||||
double speedSq = this.motionX * this.motionX + this.motionY * this.motionY + this.motionZ * this.motionZ;
|
||||
if (speedSq < 0.0001) {
|
||||
return;
|
||||
}
|
||||
this.posX += this.motionX;
|
||||
this.posY += this.motionY;
|
||||
this.posZ += this.motionZ;
|
||||
@@ -529,8 +920,13 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
|
||||
protected void doExplosion() {
|
||||
if (previewMode) return; // Skip explosion in preview mode
|
||||
if (worldObj.isRemote) return; // Explosions are server-side only
|
||||
float explosionRad = getExplosionRadius();
|
||||
if (Float.isNaN(explosionRad) || explosionRad <= 0) return;
|
||||
final double explosionRadSq = explosionRad * explosionRad;
|
||||
final float baseDamage = getDamage();
|
||||
final float baseKnockback = getKnockback();
|
||||
final float damageFalloff = getExplosionDamageFalloff();
|
||||
spawnExplosionRenderEntity(explosionRad);
|
||||
spawnExplosionVisuals(explosionRad);
|
||||
worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f);
|
||||
@@ -549,16 +945,26 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
if (target == owner) continue;
|
||||
if (shouldIgnoreExplosionTarget(target)) continue;
|
||||
|
||||
double dist = Math.sqrt(
|
||||
Math.pow(target.posX - posX, 2) +
|
||||
Math.pow(target.posY - posY, 2) +
|
||||
Math.pow(target.posZ - posZ, 2)
|
||||
);
|
||||
// Measure distance to nearest point on entity's bounding box, not feet position.
|
||||
// Using feet position (target.posY) causes explosions at impact height to miss
|
||||
// entities standing below — e.g. a projectile hitting at eye height (~1.62 above feet)
|
||||
// would measure dist=1.62, failing a radius<=1.6 check despite a direct hit.
|
||||
if (target.boundingBox == null) continue;
|
||||
double closestX = Math.max(target.boundingBox.minX, Math.min(posX, target.boundingBox.maxX));
|
||||
double closestY = Math.max(target.boundingBox.minY, Math.min(posY, target.boundingBox.maxY));
|
||||
double closestZ = Math.max(target.boundingBox.minZ, Math.min(posZ, target.boundingBox.maxZ));
|
||||
double dx = closestX - posX;
|
||||
double dy = closestY - posY;
|
||||
double dz = closestZ - posZ;
|
||||
double distSq = dx * dx + dy * dy + dz * dz;
|
||||
if (distSq > explosionRadSq) continue;
|
||||
|
||||
if (dist <= explosionRad) {
|
||||
float falloff = 1.0f - (float) (dist / explosionRad) * getExplosionDamageFalloff();
|
||||
applyDamage(target, getDamage() * falloff, getKnockback() * falloff);
|
||||
float falloff = 1.0f;
|
||||
if (damageFalloff != 0.0f && distSq > 0.0D) {
|
||||
float dist = (float) Math.sqrt(distSq);
|
||||
falloff = 1.0f - (dist / explosionRad) * damageFalloff;
|
||||
}
|
||||
applyDamage(target, baseDamage * falloff, baseKnockback * falloff);
|
||||
}
|
||||
|
||||
tryDestroyTerrain(explosionRad);
|
||||
@@ -566,25 +972,22 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
|
||||
/**
|
||||
* Explosion safety filter.
|
||||
* Reuses standard projectile-friendly checks and blocks passive non-player entities.
|
||||
* Reuses standard projectile-friendly checks (owner, faction, party).
|
||||
*/
|
||||
protected boolean shouldIgnoreExplosionTarget(EntityLivingBase target) {
|
||||
if (target == null) return true;
|
||||
if (shouldIgnoreEntity(target)) return true;
|
||||
return isPassiveNonPlayerEntity(target);
|
||||
return shouldIgnoreEntity(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Passive mobs (animals/ambient/water creatures) are never damaged by energy explosions.
|
||||
* Players and hostile mobs remain valid unless other friendly-fire checks reject them.
|
||||
* Clamp a point to the nearest point inside the provided AABB.
|
||||
*/
|
||||
protected boolean isPassiveNonPlayerEntity(EntityLivingBase target) {
|
||||
if (target instanceof EntityPlayer) return false;
|
||||
if (target instanceof EntityNPCInterface) return false;
|
||||
if (target.isCreatureType(EnumCreatureType.monster, false)) return false;
|
||||
return target.isCreatureType(EnumCreatureType.creature, false)
|
||||
|| target.isCreatureType(EnumCreatureType.ambient, false)
|
||||
|| target.isCreatureType(EnumCreatureType.waterCreature, false);
|
||||
protected Vec3 closestPointOnBoundingBox(AxisAlignedBB box, double x, double y, double z) {
|
||||
if (box == null) return null;
|
||||
double closestX = Math.max(box.minX, Math.min(x, box.maxX));
|
||||
double closestY = Math.max(box.minY, Math.min(y, box.maxY));
|
||||
double closestZ = Math.max(box.minZ, Math.min(z, box.maxZ));
|
||||
return Vec3.createVectorHelper(closestX, closestY, closestZ);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -593,7 +996,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
*/
|
||||
protected void spawnExplosionRenderEntity(float explosionRad) {
|
||||
if (worldObj == null || worldObj.isRemote) return;
|
||||
float renderRad = Math.max(0.75f, Math.min(explosionRad, 12.0f));
|
||||
float renderRad = Math.max(0.75f, Math.min(explosionRad, EnergyCombatData.MAX_EXPLOSION_RADIUS));
|
||||
EntityEnergyExplosion fx = new EntityEnergyExplosion(worldObj, this, renderRad);
|
||||
EnergyExplosionSpawnPacket.sendToTracking(getExplosionVisualInstanceId(), fx, this);
|
||||
}
|
||||
@@ -608,7 +1011,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
protected void spawnExplosionVisuals(float explosionRad) {
|
||||
if (worldObj == null) return;
|
||||
|
||||
float visualRad = Math.max(0.75f, Math.min(explosionRad, 12.0f));
|
||||
float visualRad = Math.max(0.75f, Math.min(explosionRad, EnergyCombatData.MAX_EXPLOSION_RADIUS));
|
||||
int coreBursts = Math.max(1, Math.min(4, Math.round(visualRad * 0.35f)));
|
||||
int shellCount = Math.max(1, Math.min(3, Math.round(visualRad / 3.5f) + 1));
|
||||
|
||||
@@ -759,18 +1162,23 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
if (!ConfigEnergy.EnableEnergyExplosionBlockDamage) return;
|
||||
if (!worldObj.getGameRules().getGameRuleBooleanValue("mobGriefing")) return;
|
||||
|
||||
float terrainRad = Math.max(0.5f, Math.min(explosionRad, 12.0f));
|
||||
float terrainRad = Math.max(0.5f, Math.min(explosionRad, EnergyCombatData.MAX_EXPLOSION_RADIUS));
|
||||
int minX = MathHelper.floor_double(posX - terrainRad);
|
||||
int maxX = MathHelper.floor_double(posX + terrainRad);
|
||||
int minY = MathHelper.floor_double(posY - terrainRad);
|
||||
int maxY = MathHelper.floor_double(posY + terrainRad);
|
||||
int minZ = MathHelper.floor_double(posZ - terrainRad);
|
||||
int maxZ = MathHelper.floor_double(posZ + terrainRad);
|
||||
int worldMaxY = Math.max(0, worldObj.getActualHeight() - 1);
|
||||
if (maxY < 0 || minY > worldMaxY) return;
|
||||
float resistanceCutoff = Math.max(4.0f, terrainRad * 6.0f);
|
||||
|
||||
Explosion context = new Explosion(worldObj, this, posX, posY, posZ, terrainRad);
|
||||
context.isFlaming = false;
|
||||
context.isSmoking = true;
|
||||
int lastChunkX = Integer.MIN_VALUE;
|
||||
int lastChunkZ = Integer.MIN_VALUE;
|
||||
boolean lastChunkLoaded = false;
|
||||
|
||||
for (int x = minX; x <= maxX; x++) {
|
||||
for (int y = minY; y <= maxY; y++) {
|
||||
@@ -786,6 +1194,16 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
// Soften cube corners a bit for a less artificial crater edge.
|
||||
double normalized = chebyshev / terrainRad;
|
||||
if (normalized > 0.72D && rand.nextDouble() < (normalized - 0.72D) * 1.4D) continue;
|
||||
if (y < 0 || y > worldMaxY) continue;
|
||||
|
||||
int chunkX = x >> 4;
|
||||
int chunkZ = z >> 4;
|
||||
if (chunkX != lastChunkX || chunkZ != lastChunkZ) {
|
||||
lastChunkX = chunkX;
|
||||
lastChunkZ = chunkZ;
|
||||
lastChunkLoaded = worldObj.getChunkProvider().chunkExists(chunkX, chunkZ);
|
||||
}
|
||||
if (!lastChunkLoaded) continue;
|
||||
|
||||
Block block = worldObj.getBlock(x, y, z);
|
||||
if (block == null || block == Blocks.air || block == Blocks.bedrock) continue;
|
||||
@@ -825,7 +1243,11 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
*/
|
||||
protected boolean beginLookVectorLaunch(boolean resetInterpolationState) {
|
||||
setCharging(false);
|
||||
boolean positioned = setLookVectorLaunchPosition(true);
|
||||
Entity owner = getOwnerEntity();
|
||||
boolean positioned = false;
|
||||
if (owner instanceof EntityLivingBase && shouldSnapLaunchToLookVector((EntityLivingBase) owner)) {
|
||||
positioned = setLookVectorLaunchPosition((EntityLivingBase) owner, getOwnerLookVector(), true);
|
||||
}
|
||||
syncStartPositionToCurrent();
|
||||
if (resetInterpolationState) {
|
||||
syncPositionStateToCurrent(true);
|
||||
@@ -838,7 +1260,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
*/
|
||||
protected boolean beginLookVectorLaunch(EntityLivingBase owner, Vec3 look, boolean resetInterpolationState) {
|
||||
setCharging(false);
|
||||
boolean positioned = setLookVectorLaunchPosition(owner, look, true);
|
||||
boolean positioned = shouldSnapLaunchToLookVector(owner) && setLookVectorLaunchPosition(owner, look, true);
|
||||
syncStartPositionToCurrent();
|
||||
if (resetInterpolationState) {
|
||||
syncPositionStateToCurrent(true);
|
||||
@@ -846,6 +1268,14 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
return positioned;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
protected boolean shouldSnapLaunchToLookVector(EntityLivingBase owner) {
|
||||
return owner instanceof EntityPlayer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default preview launch: snap to look-vector origin and fire along look.
|
||||
*/
|
||||
@@ -1435,7 +1865,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
}
|
||||
|
||||
public float getExplosionRadius() {
|
||||
return combatData.explosionRadius;
|
||||
return combatData.getExplosionRadius();
|
||||
}
|
||||
|
||||
public void setExplosionRadius(float radius) {
|
||||
@@ -1466,8 +1896,17 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
combatData.multiHitDelayTicks = Math.max(1, delayTicks);
|
||||
}
|
||||
|
||||
public int getMaxHits() {
|
||||
return combatData.getMaxHits();
|
||||
}
|
||||
|
||||
public void setMaxHits(int maxHits) {
|
||||
combatData.setMaxHits(maxHits);
|
||||
}
|
||||
|
||||
protected boolean canHitEntityNow(EntityLivingBase entity) {
|
||||
if (entity == null) return false;
|
||||
if (combatData.hitType != HitType.SINGLE && hasReachedMaxHits()) return false;
|
||||
|
||||
int entityId = entity.getEntityId();
|
||||
switch (combatData.hitType) {
|
||||
@@ -1489,10 +1928,17 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
int entityId = entity.getEntityId();
|
||||
hitOnceEntities.add(entityId);
|
||||
lastHitTickByEntity.put(entityId, ticksExisted);
|
||||
hitCount++;
|
||||
}
|
||||
|
||||
protected boolean hasReachedMaxHits() {
|
||||
if (combatData.hitType == HitType.SINGLE) return false;
|
||||
return hitCount >= combatData.getMaxHits();
|
||||
}
|
||||
|
||||
protected boolean shouldTerminateAfterHit() {
|
||||
return combatData.hitType == HitType.SINGLE;
|
||||
if (combatData.hitType == HitType.SINGLE) return true;
|
||||
return hasReachedMaxHits();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1536,12 +1982,13 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
|
||||
recordEntityHit(entity);
|
||||
if (isExplosive()) {
|
||||
Vec3 impactPoint = resolveEntityImpactPoint(entity, impactX, impactY, impactZ);
|
||||
double oldX = posX;
|
||||
double oldY = posY;
|
||||
double oldZ = posZ;
|
||||
posX = impactX;
|
||||
posY = impactY;
|
||||
posZ = impactZ;
|
||||
posX = impactPoint.xCoord;
|
||||
posY = impactPoint.yCoord;
|
||||
posZ = impactPoint.zCoord;
|
||||
doExplosion();
|
||||
posX = oldX;
|
||||
posY = oldY;
|
||||
@@ -1559,6 +2006,26 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a stable impact point on the target so explosive collisions use actual contact.
|
||||
*/
|
||||
protected Vec3 resolveEntityImpactPoint(EntityLivingBase entity, double impactX, double impactY, double impactZ) {
|
||||
if (entity == null || entity.boundingBox == null) {
|
||||
return Vec3.createVectorHelper(impactX, impactY, impactZ);
|
||||
}
|
||||
|
||||
Vec3 segmentStart = Vec3.createVectorHelper(posX, posY, posZ);
|
||||
Vec3 segmentEnd = Vec3.createVectorHelper(impactX, impactY, impactZ);
|
||||
AxisAlignedBB expandedBox = entity.boundingBox.expand(1.0e-4, 1.0e-4, 1.0e-4);
|
||||
MovingObjectPosition intercept = expandedBox.calculateIntercept(segmentStart, segmentEnd);
|
||||
if (intercept != null && intercept.hitVec != null) {
|
||||
return intercept.hitVec;
|
||||
}
|
||||
|
||||
Vec3 closest = closestPointOnBoundingBox(entity.boundingBox, impactX, impactY, impactZ);
|
||||
return closest != null ? closest : Vec3.createVectorHelper(impactX, impactY, impactZ);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan entities in a hitbox and process the first/each valid hit based on hit mode.
|
||||
*/
|
||||
@@ -1662,6 +2129,11 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
return hasHit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasLightningEffect() {
|
||||
return super.hasLightningEffect() || getBarrierSparkTicks() > 0;
|
||||
}
|
||||
|
||||
// ==================== NBT ====================
|
||||
|
||||
@Override
|
||||
@@ -1742,6 +2214,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
|
||||
homingData.readNBT(nbt);
|
||||
lifespanData.readNBT(nbt);
|
||||
trajectoryData.readNBT(nbt);
|
||||
syncProjectileColorWatchers();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -99,6 +99,7 @@ import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket;
|
||||
import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket;
|
||||
import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.auction.ManageAuctionPacket;
|
||||
import kamkeel.npcs.network.packets.request.bank.BankGetPacket;
|
||||
import kamkeel.npcs.network.packets.request.bank.BankRemovePacket;
|
||||
import kamkeel.npcs.network.packets.request.bank.BankSavePacket;
|
||||
@@ -470,6 +471,7 @@ public class PacketHandler {
|
||||
|
||||
// Trader Packets
|
||||
REQUEST_PACKET.registerPacket(new TraderMarketSavePacket());
|
||||
REQUEST_PACKET.registerPacket(new ManageAuctionPacket());
|
||||
|
||||
// Party Packets moved to player channel
|
||||
|
||||
@@ -569,7 +571,6 @@ public class PacketHandler {
|
||||
DATA_PACKET.registerPacket(new DialogPacket());
|
||||
DATA_PACKET.registerPacket(new RolePacket());
|
||||
DATA_PACKET.registerPacket(new WeaponNpcPacket());
|
||||
// DATA_PACKET.registerPacket(new RequestProperSpawnData());
|
||||
|
||||
// Data | Script Packets
|
||||
DATA_PACKET.registerPacket(new ScriptedParticlePacket());
|
||||
@@ -628,6 +629,7 @@ public class PacketHandler {
|
||||
PLAYER_PACKET.registerPacket(new SpecialKeyStatePacket());
|
||||
|
||||
PLAYER_PACKET.registerPacket(new SyncRevisionInfoPacket());
|
||||
PLAYER_PACKET.registerPacket(new RequestProperSpawnData());
|
||||
|
||||
// CustomGUI Packets
|
||||
PLAYER_PACKET.registerPacket(new CustomGuiButtonPacket());
|
||||
|
||||
@@ -23,7 +23,6 @@ public enum EnumDataPacket {
|
||||
DELETE_NPC,
|
||||
CLONE_NPC,
|
||||
WEAPON_NPC,
|
||||
// REQUEST_SPAWN_DATA,
|
||||
|
||||
// Data
|
||||
SCROLL_LIST,
|
||||
|
||||
@@ -42,4 +42,7 @@ public enum EnumPlayerPacket {
|
||||
AbilityHotbarSave,
|
||||
AbilityHotbarSelect,
|
||||
AbilityToggle,
|
||||
|
||||
// NPC Spawn Data Fix
|
||||
RequestSpawnData,
|
||||
}
|
||||
@@ -213,4 +213,5 @@ public enum EnumRequestPacket {
|
||||
Hammer,
|
||||
|
||||
RecipeScript,
|
||||
ManageAuctionAction,
|
||||
}
|
||||
@@ -1,125 +1,125 @@
|
||||
//package kamkeel.npcs.network.packets.data;
|
||||
//
|
||||
//import cpw.mods.fml.relauncher.Side;
|
||||
//import cpw.mods.fml.relauncher.SideOnly;
|
||||
//import io.netty.buffer.ByteBuf;
|
||||
//import kamkeel.npcs.network.AbstractPacket;
|
||||
//import kamkeel.npcs.network.PacketChannel;
|
||||
//import kamkeel.npcs.network.PacketClient;
|
||||
//import kamkeel.npcs.network.PacketHandler;
|
||||
//import kamkeel.npcs.network.enums.EnumDataPacket;
|
||||
//import kamkeel.npcs.network.packets.data.npc.UpdateNpcPacket;
|
||||
//import net.minecraft.client.Minecraft;
|
||||
//import net.minecraft.entity.Entity;
|
||||
//import net.minecraft.entity.player.EntityPlayer;
|
||||
//import net.minecraft.entity.player.EntityPlayerMP;
|
||||
//import net.minecraft.nbt.NBTTagCompound;
|
||||
//import noppes.npcs.entity.EntityNPCInterface;
|
||||
//
|
||||
//import java.io.IOException;
|
||||
//import java.util.HashSet;
|
||||
//import java.util.Iterator;
|
||||
//import java.util.Set;
|
||||
//
|
||||
//public class RequestProperSpawnData extends AbstractPacket {
|
||||
// public static final String packetName = "Data|SpawnData";
|
||||
// private static final int BATCH_LIMIT = 5;
|
||||
//
|
||||
// @SideOnly(Side.CLIENT)
|
||||
// private static final Set<Integer> entitiesToFix = new HashSet<>();
|
||||
//
|
||||
// @SideOnly(Side.CLIENT)
|
||||
// private static long lastBatchAttemptMillis = 0L;
|
||||
//
|
||||
// private int entityId;
|
||||
//
|
||||
// public RequestProperSpawnData() {}
|
||||
//
|
||||
// @SideOnly(Side.CLIENT)
|
||||
// public RequestProperSpawnData(int entityId) {
|
||||
// this.entityId = entityId;
|
||||
// }
|
||||
//
|
||||
// @SideOnly(Side.CLIENT)
|
||||
// public static void clear() {
|
||||
// entitiesToFix.clear();
|
||||
// lastBatchAttemptMillis = 0L;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public Enum getType() {
|
||||
// return EnumDataPacket.REQUEST_SPAWN_DATA;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public PacketChannel getChannel() {
|
||||
// return PacketHandler.DATA_PACKET;
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// @SideOnly(Side.CLIENT)
|
||||
// public void sendData(ByteBuf out) throws IOException {
|
||||
// out.writeInt(this.entityId);
|
||||
// }
|
||||
//
|
||||
// @Override
|
||||
// public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
|
||||
// if (!(player instanceof EntityPlayerMP) || player.worldObj == null || player.worldObj.isRemote)
|
||||
// return;
|
||||
//
|
||||
// int entityId = in.readInt();
|
||||
// Entity entity = player.worldObj.getEntityByID(entityId);
|
||||
// if (entity instanceof EntityNPCInterface) {
|
||||
// EntityNPCInterface npc = (EntityNPCInterface) entity;
|
||||
// NBTTagCompound compound = npc.writeSpawnData();
|
||||
// compound.setInteger("EntityId", npc.getEntityId());
|
||||
// PacketHandler.Instance.sendToPlayer(new UpdateNpcPacket(compound), (EntityPlayerMP) player);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @SideOnly(Side.CLIENT)
|
||||
// public static void reportMissingData(EntityNPCInterface npc) {
|
||||
// if (npc == null || npc.worldObj == null || !npc.worldObj.isRemote)
|
||||
// return;
|
||||
//
|
||||
// int entityID = npc.getEntityId();
|
||||
// if (entityID <= 0)
|
||||
// return;
|
||||
//
|
||||
// if (npc.immediateSpawnDataFixAttempts++ < 3) {
|
||||
// PacketClient.sendClient(new RequestProperSpawnData(entityID));
|
||||
// } else {
|
||||
// entitiesToFix.add(entityID);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// @SideOnly(Side.CLIENT)
|
||||
// public static boolean canDoBatchUpdate() {
|
||||
// return System.currentTimeMillis() - lastBatchAttemptMillis >= 5000;
|
||||
// }
|
||||
//
|
||||
// @SideOnly(Side.CLIENT)
|
||||
// public static void handleBacklog() {
|
||||
// Minecraft mc = Minecraft.getMinecraft();
|
||||
// if (mc == null || mc.theWorld == null || mc.thePlayer == null)
|
||||
// return;
|
||||
//
|
||||
// if (entitiesToFix.isEmpty())
|
||||
// return;
|
||||
// // Remove entity checks if they're no longer loaded
|
||||
// entitiesToFix.removeIf(value -> !(mc.theWorld.getEntityByID(value) instanceof EntityNPCInterface));
|
||||
//
|
||||
// Iterator<Integer> it = entitiesToFix.iterator();
|
||||
// int count = 0;
|
||||
//
|
||||
// while (it.hasNext() && count < BATCH_LIMIT) {
|
||||
// PacketClient.sendClient(new RequestProperSpawnData(it.next()));
|
||||
// it.remove();
|
||||
// count++;
|
||||
// }
|
||||
//
|
||||
// if (count != 0) {
|
||||
// lastBatchAttemptMillis = System.currentTimeMillis();
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
package kamkeel.npcs.network.packets.data;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.network.AbstractPacket;
|
||||
import kamkeel.npcs.network.PacketChannel;
|
||||
import kamkeel.npcs.network.PacketClient;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.enums.EnumPlayerPacket;
|
||||
import kamkeel.npcs.network.packets.data.npc.UpdateNpcPacket;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
public class RequestProperSpawnData extends AbstractPacket {
|
||||
public static final String packetName = "Player|SpawnData";
|
||||
private static final int BATCH_LIMIT = 5;
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
private static final Set<Integer> entitiesToFix = new HashSet<>();
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
private static long lastBatchAttemptMillis = 0L;
|
||||
|
||||
private int entityId;
|
||||
|
||||
public RequestProperSpawnData() {}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public RequestProperSpawnData(int entityId) {
|
||||
this.entityId = entityId;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static void clear() {
|
||||
entitiesToFix.clear();
|
||||
lastBatchAttemptMillis = 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enum getType() {
|
||||
return EnumPlayerPacket.RequestSpawnData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketChannel getChannel() {
|
||||
return PacketHandler.PLAYER_PACKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
out.writeInt(this.entityId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
|
||||
if (!(player instanceof EntityPlayerMP) || player.worldObj == null || player.worldObj.isRemote)
|
||||
return;
|
||||
|
||||
int entityId = in.readInt();
|
||||
Entity entity = player.worldObj.getEntityByID(entityId);
|
||||
if (entity instanceof EntityNPCInterface) {
|
||||
EntityNPCInterface npc = (EntityNPCInterface) entity;
|
||||
NBTTagCompound compound = npc.writeSpawnData();
|
||||
compound.setInteger("EntityId", npc.getEntityId());
|
||||
PacketHandler.Instance.sendToPlayer(new UpdateNpcPacket(compound), (EntityPlayerMP) player);
|
||||
}
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static void reportMissingData(EntityNPCInterface npc) {
|
||||
if (npc == null || npc.worldObj == null || !npc.worldObj.isRemote)
|
||||
return;
|
||||
|
||||
int entityID = npc.getEntityId();
|
||||
if (entityID <= 0)
|
||||
return;
|
||||
|
||||
if (npc.clientFixAttempts++ < 3) {
|
||||
PacketClient.sendClient(new RequestProperSpawnData(entityID));
|
||||
} else {
|
||||
entitiesToFix.add(entityID);
|
||||
}
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static boolean canDoBatchUpdate() {
|
||||
return System.currentTimeMillis() - lastBatchAttemptMillis >= 5000;
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static void handleBacklog() {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
if (mc == null || mc.theWorld == null || mc.thePlayer == null)
|
||||
return;
|
||||
|
||||
if (entitiesToFix.isEmpty())
|
||||
return;
|
||||
// Remove entity checks if they're no longer loaded
|
||||
entitiesToFix.removeIf(value -> !(mc.theWorld.getEntityByID(value) instanceof EntityNPCInterface));
|
||||
|
||||
Iterator<Integer> it = entitiesToFix.iterator();
|
||||
int count = 0;
|
||||
|
||||
while (it.hasNext() && count < BATCH_LIMIT) {
|
||||
PacketClient.sendClient(new RequestProperSpawnData(it.next()));
|
||||
it.remove();
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count != 0) {
|
||||
lastBatchAttemptMillis = System.currentTimeMillis();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import kamkeel.npcs.network.enums.EnumItemPacketType;
|
||||
import kamkeel.npcs.network.enums.EnumRequestPacket;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.util.ChatComponentText;
|
||||
import net.minecraft.util.EnumChatFormatting;
|
||||
import noppes.npcs.CustomNpcsPermissions;
|
||||
import noppes.npcs.NoppesUtilServer;
|
||||
import noppes.npcs.constants.EnumGuiType;
|
||||
|
||||
|
||||
@@ -0,0 +1,522 @@
|
||||
package kamkeel.npcs.network.packets.request.auction;
|
||||
|
||||
import cpw.mods.fml.relauncher.Side;
|
||||
import cpw.mods.fml.relauncher.SideOnly;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import kamkeel.npcs.network.AbstractPacket;
|
||||
import kamkeel.npcs.network.PacketChannel;
|
||||
import kamkeel.npcs.network.PacketClient;
|
||||
import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.enums.EnumRequestPacket;
|
||||
import kamkeel.npcs.network.packets.data.large.GuiDataPacket;
|
||||
import kamkeel.npcs.util.ByteBufUtils;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.entity.player.EntityPlayerMP;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import net.minecraft.nbt.NBTTagList;
|
||||
import noppes.npcs.CustomNpcsPermissions;
|
||||
import noppes.npcs.NoppesUtilServer;
|
||||
import noppes.npcs.config.ConfigMarket;
|
||||
import noppes.npcs.constants.EnumGuiType;
|
||||
import noppes.npcs.constants.EnumClaimType;
|
||||
import noppes.npcs.controllers.AuctionController;
|
||||
import noppes.npcs.controllers.data.AuctionClaim;
|
||||
import noppes.npcs.controllers.data.AuctionFilter;
|
||||
import noppes.npcs.controllers.data.AuctionListing;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Admin-only global auction management packet.
|
||||
*/
|
||||
public class ManageAuctionPacket extends AbstractPacket {
|
||||
private static final int PAGE_SIZE = 45;
|
||||
|
||||
private Action action;
|
||||
private int page;
|
||||
private boolean flag;
|
||||
private String text;
|
||||
private long amount;
|
||||
private long amount2;
|
||||
private int amountInt;
|
||||
private NBTTagCompound data;
|
||||
|
||||
public ManageAuctionPacket() {
|
||||
}
|
||||
|
||||
private ManageAuctionPacket(Action action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enum getType() {
|
||||
return EnumRequestPacket.ManageAuctionAction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PacketChannel getChannel() {
|
||||
return PacketHandler.REQUEST_PACKET;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CustomNpcsPermissions.Permission getPermission() {
|
||||
return CustomNpcsPermissions.GLOBAL_AUCTION;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean needsNPC() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// Client Send Helpers
|
||||
// =========================================
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static void requestListings(AuctionFilter filter, int page) {
|
||||
ManageAuctionPacket packet = new ManageAuctionPacket(Action.REQUEST_LISTINGS);
|
||||
packet.page = page;
|
||||
packet.data = filter != null ? filter.writeToNBT(new NBTTagCompound()) : new NBTTagCompound();
|
||||
PacketClient.sendClient(packet);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static void requestGlobalClaims(int page) {
|
||||
ManageAuctionPacket packet = new ManageAuctionPacket(Action.REQUEST_CLAIMS);
|
||||
packet.page = page;
|
||||
PacketClient.sendClient(packet);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static void createFakeListing(ItemStack item, String sellerName, long startingPrice, long buyoutPrice, int durationHours) {
|
||||
if (item == null) return;
|
||||
|
||||
ManageAuctionPacket packet = new ManageAuctionPacket(Action.CREATE_FAKE);
|
||||
packet.text = sellerName != null ? sellerName : "";
|
||||
packet.amount = startingPrice;
|
||||
packet.amount2 = buyoutPrice;
|
||||
packet.amountInt = durationHours;
|
||||
packet.data = new NBTTagCompound();
|
||||
item.writeToNBT(packet.data);
|
||||
PacketClient.sendClient(packet);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static void manageListing(String listingId, boolean cancelCompletely) {
|
||||
ManageAuctionPacket packet = new ManageAuctionPacket(Action.MANAGE_LISTING);
|
||||
packet.text = listingId;
|
||||
packet.flag = cancelCompletely;
|
||||
PacketClient.sendClient(packet);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static void claimGlobalItem(String claimId) {
|
||||
ManageAuctionPacket packet = new ManageAuctionPacket(Action.CLAIM_ITEM);
|
||||
packet.text = claimId;
|
||||
PacketClient.sendClient(packet);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static void claimGlobalCurrency(String claimId) {
|
||||
ManageAuctionPacket packet = new ManageAuctionPacket(Action.CLAIM_CURRENCY);
|
||||
packet.text = claimId;
|
||||
PacketClient.sendClient(packet);
|
||||
}
|
||||
|
||||
@SideOnly(Side.CLIENT)
|
||||
public static void openBidding(String listingId) {
|
||||
ManageAuctionPacket packet = new ManageAuctionPacket(Action.OPEN_BIDDING);
|
||||
packet.text = listingId;
|
||||
PacketClient.sendClient(packet);
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// Serialization
|
||||
// =========================================
|
||||
|
||||
@Override
|
||||
@SideOnly(Side.CLIENT)
|
||||
public void sendData(ByteBuf out) throws IOException {
|
||||
out.writeInt(action.ordinal());
|
||||
|
||||
switch (action) {
|
||||
case REQUEST_LISTINGS:
|
||||
out.writeInt(page);
|
||||
ByteBufUtils.writeNBT(out, data);
|
||||
break;
|
||||
case REQUEST_CLAIMS:
|
||||
out.writeInt(page);
|
||||
break;
|
||||
case CREATE_FAKE:
|
||||
ByteBufUtils.writeString(out, text);
|
||||
out.writeLong(amount);
|
||||
out.writeLong(amount2);
|
||||
out.writeInt(amountInt);
|
||||
ByteBufUtils.writeNBT(out, data);
|
||||
break;
|
||||
case MANAGE_LISTING:
|
||||
ByteBufUtils.writeString(out, text);
|
||||
out.writeBoolean(flag);
|
||||
break;
|
||||
case CLAIM_ITEM:
|
||||
case CLAIM_CURRENCY:
|
||||
case OPEN_BIDDING:
|
||||
ByteBufUtils.writeString(out, text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
|
||||
if (!(player instanceof EntityPlayerMP)) return;
|
||||
EntityPlayerMP playerMP = (EntityPlayerMP) player;
|
||||
|
||||
if (!CustomNpcsPermissions.hasCustomPermission(playerMP, "customnpcs.global.auction")) {
|
||||
sendError(playerMP, "Missing permission: customnpcs.global.auction");
|
||||
return;
|
||||
}
|
||||
|
||||
int actionOrdinal = in.readInt();
|
||||
if (actionOrdinal < 0 || actionOrdinal >= Action.values().length) return;
|
||||
Action requestedAction = Action.values()[actionOrdinal];
|
||||
|
||||
AuctionController controller = AuctionController.getInstance();
|
||||
if (controller == null) {
|
||||
sendError(playerMP, "Auction system is not available.");
|
||||
return;
|
||||
}
|
||||
|
||||
String result = null;
|
||||
switch (requestedAction) {
|
||||
case REQUEST_LISTINGS:
|
||||
handleRequestListings(in, playerMP, controller);
|
||||
break;
|
||||
case REQUEST_CLAIMS:
|
||||
handleRequestClaims(in, playerMP, controller);
|
||||
break;
|
||||
case CREATE_FAKE:
|
||||
result = handleCreateFake(in, playerMP, controller);
|
||||
break;
|
||||
case MANAGE_LISTING:
|
||||
result = handleManageListing(in, playerMP, controller);
|
||||
break;
|
||||
case CLAIM_ITEM:
|
||||
result = handleClaimItem(in, playerMP, controller);
|
||||
break;
|
||||
case CLAIM_CURRENCY:
|
||||
result = handleClaimCurrency(in, playerMP, controller);
|
||||
break;
|
||||
case OPEN_BIDDING:
|
||||
handleOpenBidding(in, playerMP, controller);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
sendError(playerMP, result);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// Handlers
|
||||
// =========================================
|
||||
|
||||
private void handleRequestListings(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException {
|
||||
int requestPage = Math.max(0, in.readInt());
|
||||
NBTTagCompound filterNbt = ByteBufUtils.readNBT(in);
|
||||
|
||||
AuctionFilter filter = new AuctionFilter();
|
||||
if (filterNbt != null) {
|
||||
filter.readFromNBT(filterNbt);
|
||||
}
|
||||
|
||||
int totalListings = controller.getTotalActiveListings(filter);
|
||||
int totalPages = Math.max(1, (int) Math.ceil((double) totalListings / PAGE_SIZE));
|
||||
int safePage = Math.min(requestPage, totalPages - 1);
|
||||
List<AuctionListing> listings = controller.getActiveListings(filter, safePage, PAGE_SIZE);
|
||||
|
||||
NBTTagCompound response = new NBTTagCompound();
|
||||
response.setBoolean("ManageAuctionListingsData", true);
|
||||
response.setInteger("Page", safePage);
|
||||
response.setInteger("TotalPages", totalPages);
|
||||
response.setInteger("TotalListings", totalListings);
|
||||
|
||||
NBTTagList list = new NBTTagList();
|
||||
for (AuctionListing listing : listings) {
|
||||
list.appendTag(listing.writeToNBT(new NBTTagCompound()));
|
||||
}
|
||||
response.setTag("Listings", list);
|
||||
GuiDataPacket.sendGuiData(player, response);
|
||||
}
|
||||
|
||||
private void handleRequestClaims(ByteBuf in, EntityPlayerMP player, AuctionController controller) {
|
||||
int requestPage = Math.max(0, in.readInt());
|
||||
|
||||
// Global "trades" page contains global claims + active global listings,
|
||||
// ordered like My Trades: sold claims, outbid claims, won claims, active listings, expired claims.
|
||||
List<AuctionClaim> allClaims = controller.getAllGlobalClaims();
|
||||
List<AuctionListing> allListings = controller.getActiveGlobalListings();
|
||||
int totalClaims = allClaims.size();
|
||||
int totalListings = allListings.size();
|
||||
|
||||
List<AuctionClaim> soldClaims = new ArrayList<AuctionClaim>();
|
||||
List<AuctionClaim> outbidClaims = new ArrayList<AuctionClaim>();
|
||||
List<AuctionClaim> wonClaims = new ArrayList<AuctionClaim>();
|
||||
List<AuctionClaim> expiredClaims = new ArrayList<AuctionClaim>();
|
||||
for (AuctionClaim claim : allClaims) {
|
||||
if (claim == null) continue;
|
||||
if (claim.type == EnumClaimType.CURRENCY) {
|
||||
soldClaims.add(claim);
|
||||
} else if (claim.type == EnumClaimType.REFUND) {
|
||||
outbidClaims.add(claim);
|
||||
} else if (claim.type == EnumClaimType.ITEM) {
|
||||
if (claim.isReturned) {
|
||||
expiredClaims.add(claim);
|
||||
} else {
|
||||
wonClaims.add(claim);
|
||||
}
|
||||
} else {
|
||||
wonClaims.add(claim);
|
||||
}
|
||||
}
|
||||
|
||||
List<Entry> ordered = new ArrayList<Entry>();
|
||||
appendClaims(ordered, soldClaims);
|
||||
appendClaims(ordered, outbidClaims);
|
||||
appendClaims(ordered, wonClaims);
|
||||
for (AuctionListing listing : allListings) {
|
||||
ordered.add(Entry.forListing(listing));
|
||||
}
|
||||
appendClaims(ordered, expiredClaims);
|
||||
|
||||
int totalEntries = ordered.size();
|
||||
|
||||
int totalPages = Math.max(1, (int) Math.ceil((double) totalEntries / PAGE_SIZE));
|
||||
int safePage = Math.min(requestPage, totalPages - 1);
|
||||
|
||||
int start = safePage * PAGE_SIZE;
|
||||
int end = Math.min(totalEntries, start + PAGE_SIZE);
|
||||
List<AuctionClaim> pageClaims = new ArrayList<AuctionClaim>();
|
||||
List<AuctionListing> pageListings = new ArrayList<AuctionListing>();
|
||||
|
||||
for (int i = start; i < end; i++) {
|
||||
Entry entry = ordered.get(i);
|
||||
if (entry.claim != null) {
|
||||
pageClaims.add(entry.claim);
|
||||
} else if (entry.listing != null) {
|
||||
pageListings.add(entry.listing);
|
||||
}
|
||||
}
|
||||
|
||||
NBTTagCompound response = new NBTTagCompound();
|
||||
response.setBoolean("ManageAuctionClaimsData", true);
|
||||
response.setInteger("Page", safePage);
|
||||
response.setInteger("TotalPages", totalPages);
|
||||
response.setInteger("TotalClaims", totalClaims);
|
||||
response.setInteger("TotalListings", totalListings);
|
||||
response.setInteger("TotalEntries", totalEntries);
|
||||
|
||||
NBTTagList claimList = new NBTTagList();
|
||||
for (AuctionClaim claim : pageClaims) {
|
||||
claimList.appendTag(claim.writeToNBT(new NBTTagCompound()));
|
||||
}
|
||||
response.setTag("Claims", claimList);
|
||||
|
||||
NBTTagList listingList = new NBTTagList();
|
||||
for (AuctionListing listing : pageListings) {
|
||||
listingList.appendTag(listing.writeToNBT(new NBTTagCompound()));
|
||||
}
|
||||
response.setTag("Listings", listingList);
|
||||
GuiDataPacket.sendGuiData(player, response);
|
||||
}
|
||||
|
||||
private static void appendClaims(List<Entry> out, List<AuctionClaim> claims) {
|
||||
if (claims == null || claims.isEmpty()) return;
|
||||
for (AuctionClaim claim : claims) {
|
||||
out.add(Entry.forClaim(claim));
|
||||
}
|
||||
}
|
||||
|
||||
private String handleCreateFake(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException {
|
||||
if (!ConfigMarket.AuctionEnabled) {
|
||||
return "Auction is disabled.";
|
||||
}
|
||||
|
||||
String sellerName = ByteBufUtils.readString(in);
|
||||
long startingPrice = in.readLong();
|
||||
long buyoutPrice = in.readLong();
|
||||
int durationHours = in.readInt();
|
||||
NBTTagCompound itemNbt = ByteBufUtils.readNBT(in);
|
||||
|
||||
if (itemNbt == null) {
|
||||
return "No item data received.";
|
||||
}
|
||||
ItemStack item = ItemStack.loadItemStackFromNBT(itemNbt);
|
||||
if (item == null) {
|
||||
return "Invalid item data.";
|
||||
}
|
||||
|
||||
// Consume staged item from admin inventory like normal auction sell flow.
|
||||
if (!removeItemFromInventory(player, item)) {
|
||||
return "Item not found in inventory.";
|
||||
}
|
||||
|
||||
String result = controller.createGlobalListing(player, sellerName, item, startingPrice, buyoutPrice, durationHours);
|
||||
if (result != null) {
|
||||
// Failed create, return item (or drop to avoid loss).
|
||||
if (!player.inventory.addItemStackToInventory(item.copy())) {
|
||||
player.entityDropItem(item.copy(), 0.5f);
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
sendRefresh(player, true);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String handleManageListing(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException {
|
||||
String listingId = ByteBufUtils.readString(in);
|
||||
boolean cancelCompletely = in.readBoolean();
|
||||
String result = controller.adminStopListingToGlobal(listingId, player, cancelCompletely);
|
||||
if (result == null) {
|
||||
sendRefresh(player, false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String handleClaimItem(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException {
|
||||
String claimId = ByteBufUtils.readString(in);
|
||||
String result = controller.claimGlobalItem(claimId, player);
|
||||
if (result == null) {
|
||||
sendRefresh(player, false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String handleClaimCurrency(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException {
|
||||
String claimId = ByteBufUtils.readString(in);
|
||||
String result = controller.claimGlobalCurrency(claimId, player);
|
||||
if (result == null) {
|
||||
sendRefresh(player, false);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void handleOpenBidding(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException {
|
||||
String listingId = ByteBufUtils.readString(in);
|
||||
AuctionListing listing = controller.getListing(listingId);
|
||||
if (listing == null || !listing.isActive()) {
|
||||
sendError(player, "Listing not found or has ended.");
|
||||
return;
|
||||
}
|
||||
|
||||
NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuctionBidding, npc);
|
||||
|
||||
NBTTagCompound response = new NBTTagCompound();
|
||||
response.setBoolean("BiddingData", true);
|
||||
response.setTag("Listing", listing.writeToNBT(new NBTTagCompound()));
|
||||
response.setLong("Balance", controller.getPlayerBalance(player));
|
||||
response.setBoolean("IsOwnListing", listing.isSeller(player.getUniqueID()));
|
||||
GuiDataPacket.sendGuiData(player, response);
|
||||
}
|
||||
|
||||
private void sendError(EntityPlayerMP player, String message) {
|
||||
player.addChatMessage(new net.minecraft.util.ChatComponentText(
|
||||
net.minecraft.util.EnumChatFormatting.RED + "[Global Auction] " + message));
|
||||
}
|
||||
|
||||
private void sendRefresh(EntityPlayerMP player, boolean created) {
|
||||
NBTTagCompound response = new NBTTagCompound();
|
||||
response.setBoolean("ManageAuctionRefresh", true);
|
||||
response.setBoolean("ManageAuctionCreateSuccess", created);
|
||||
GuiDataPacket.sendGuiData(player, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two ItemStacks are the same type (item, damage, NBT) ignoring stack size.
|
||||
*/
|
||||
private boolean areItemsSameType(ItemStack item1, ItemStack item2) {
|
||||
if (item1 == null || item2 == null)
|
||||
return item1 == item2;
|
||||
if (item1.getItem() != item2.getItem())
|
||||
return false;
|
||||
if (item1.getItemDamage() != item2.getItemDamage())
|
||||
return false;
|
||||
return ItemStack.areItemStackTagsEqual(item1, item2);
|
||||
}
|
||||
|
||||
private int countItemsInInventory(EntityPlayerMP player, ItemStack target) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < player.inventory.mainInventory.length; i++) {
|
||||
ItemStack stack = player.inventory.mainInventory[i];
|
||||
if (stack != null && areItemsSameType(stack, target)) {
|
||||
count += stack.stackSize;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes items from inventory, consuming from multiple stacks if needed.
|
||||
* Returns true if successful, false if not enough items.
|
||||
*/
|
||||
private boolean removeItemFromInventory(EntityPlayerMP player, ItemStack target) {
|
||||
if (target == null) return false;
|
||||
|
||||
int needed = target.stackSize;
|
||||
int available = countItemsInInventory(player, target);
|
||||
if (available < needed) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < player.inventory.mainInventory.length && needed > 0; i++) {
|
||||
ItemStack stack = player.inventory.mainInventory[i];
|
||||
if (stack == null || !areItemsSameType(stack, target))
|
||||
continue;
|
||||
|
||||
if (needed >= stack.stackSize) {
|
||||
needed -= stack.stackSize;
|
||||
player.inventory.mainInventory[i] = null;
|
||||
} else {
|
||||
stack.splitStack(needed);
|
||||
if (stack.stackSize <= 0) {
|
||||
player.inventory.setInventorySlotContents(i, null);
|
||||
}
|
||||
needed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
player.inventory.markDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static class Entry {
|
||||
final AuctionClaim claim;
|
||||
final AuctionListing listing;
|
||||
|
||||
private Entry(AuctionClaim claim, AuctionListing listing) {
|
||||
this.claim = claim;
|
||||
this.listing = listing;
|
||||
}
|
||||
|
||||
static Entry forClaim(AuctionClaim claim) {
|
||||
return new Entry(claim, null);
|
||||
}
|
||||
|
||||
static Entry forListing(AuctionListing listing) {
|
||||
return new Entry(null, listing);
|
||||
}
|
||||
}
|
||||
|
||||
private enum Action {
|
||||
REQUEST_LISTINGS,
|
||||
REQUEST_CLAIMS,
|
||||
CREATE_FAKE,
|
||||
MANAGE_LISTING,
|
||||
CLAIM_ITEM,
|
||||
CLAIM_CURRENCY,
|
||||
OPEN_BIDDING
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import noppes.npcs.containers.ContainerCrate;
|
||||
import noppes.npcs.containers.ContainerCustomGui;
|
||||
import noppes.npcs.containers.ContainerMail;
|
||||
import noppes.npcs.containers.ContainerManageBanks;
|
||||
import noppes.npcs.containers.ContainerManageAuction;
|
||||
import noppes.npcs.containers.ContainerManageRecipes;
|
||||
import noppes.npcs.containers.ContainerMerchantAdd;
|
||||
import noppes.npcs.containers.ContainerNPCBankLarge;
|
||||
@@ -136,6 +137,9 @@ public class CommonProxy implements IGuiHandler {
|
||||
if (gui == EnumGuiType.ManageBanks)
|
||||
return new ContainerManageBanks(player);
|
||||
|
||||
if (gui == EnumGuiType.ManageAuction)
|
||||
return new ContainerManageAuction(player);
|
||||
|
||||
if (gui == EnumGuiType.MerchantAdd)
|
||||
return new ContainerMerchantAdd(player, ServerEventsHandler.Merchant, player.worldObj);
|
||||
|
||||
|
||||
@@ -295,7 +295,7 @@ public class CustomNpcs {
|
||||
registerNewEntity(EntityAbilityOrb.class, "abilityorb", 64, 1, true);
|
||||
registerNewEntity(EntityAbilityDisc.class, "abilitydisc", 64, 1, true);
|
||||
registerNewEntity(EntityAbilityLaser.class, "abilitylaser", 64, 1, true);
|
||||
registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 64, 1, true);
|
||||
registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 160, 1, true);
|
||||
registerNewEntity(EntityAbilitySweeper.class, "abilitysweeper", 64, 3, true);
|
||||
registerNewEntity(EntityAbilityZone.class, "abilityzone", 64, 3, true);
|
||||
registerNewEntity(EntityAbilityDome.class, "energydome", 64, 3, true);
|
||||
|
||||
@@ -69,6 +69,7 @@ public class CustomNpcsPermissions {
|
||||
public static final Permission GLOBAL_MAGIC = new Permission("customnpcs.global.magic");
|
||||
public static final Permission GLOBAL_ABILITY = new Permission("customnpcs.global.ability");
|
||||
public static final Permission GLOBAL_EFFECT = new Permission("customnpcs.global.effect");
|
||||
public static final Permission GLOBAL_AUCTION = new Permission("customnpcs.global.auction");
|
||||
|
||||
public static final Permission SPAWNER_MOB = new Permission("customnpcs.spawner.mob");
|
||||
public static final Permission SPAWNER_CREATE = new Permission("customnpcs.spawner.create");
|
||||
|
||||
@@ -28,28 +28,45 @@ import noppes.npcs.scripted.event.CustomNPCsEvent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
public class NPCSpawning {
|
||||
private static Set<ChunkCoordIntPair> eligibleChunksForSpawning = Sets.newHashSet();
|
||||
private static final byte CHUNK_SPAWN_RADIUS = 7;
|
||||
private static final int SPAWN_ATTEMPTS_PER_CHUNK = 3;
|
||||
private static final int NATURAL_CAP_PER_256_CHUNKS = 70;
|
||||
private static final long RUNTIME_SPAWN_TICK_INTERVAL = 20L;
|
||||
private static final int RUNTIME_SPAWN_CYCLE_TICKS = 400;
|
||||
private static final String NATURAL_SPAWN_ID_TAG = "CNPCNaturalSpawnId";
|
||||
private static final String NATURAL_SPAWN_NAME_TAG = "CNPCNaturalSpawnName";
|
||||
private static final String NATURAL_SPAWN_TIME_TAG = "CNPCNaturalSpawnTime";
|
||||
private static final Map<Long, Long> SPAWN_COOLDOWNS = new HashMap<Long, Long>();
|
||||
private static final Map<Integer, RuntimeChunkState> RUNTIME_CHUNK_STATES = new HashMap<Integer, RuntimeChunkState>();
|
||||
|
||||
private static boolean animalSpawn;
|
||||
private static boolean monsterSpawn;
|
||||
private static boolean airSpawn;
|
||||
private static boolean liquidSpawn;
|
||||
|
||||
private static class RuntimeChunkState {
|
||||
public ArrayList<ChunkCoordIntPair> chunks = new ArrayList<ChunkCoordIntPair>();
|
||||
public int index = 0;
|
||||
public int signature = 0;
|
||||
}
|
||||
|
||||
public static void findChunksForSpawning(WorldServer world) {
|
||||
if (SpawnController.Instance.data.isEmpty() || world.getWorldInfo().getWorldTotalTime() % 400L != 0L)
|
||||
if (SpawnController.Instance.data.isEmpty() || world.getWorldInfo().getWorldTotalTime() % RUNTIME_SPAWN_TICK_INTERVAL != 0L)
|
||||
return;
|
||||
eligibleChunksForSpawning.clear();
|
||||
for (int i = 0; i < world.playerEntities.size(); ++i) {
|
||||
EntityPlayer entityplayer = (EntityPlayer) world.playerEntities.get(i);
|
||||
int j = MathHelper.floor_double(entityplayer.posX / 16.0D);
|
||||
int k = MathHelper.floor_double(entityplayer.posZ / 16.0D);
|
||||
byte size = 7;
|
||||
byte size = CHUNK_SPAWN_RADIUS;
|
||||
|
||||
for (int x = -size; x <= size; ++x) {
|
||||
for (int z = -size; z <= size; ++z) {
|
||||
@@ -60,22 +77,29 @@ public class NPCSpawning {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (countNPCs(world) > eligibleChunksForSpawning.size()) {
|
||||
int cap = getScaledNaturalCap(eligibleChunksForSpawning.size());
|
||||
int npcCount = countNaturalSpawnedNPCs(world);
|
||||
if (npcCount >= cap) {
|
||||
return;
|
||||
}
|
||||
ArrayList<ChunkCoordIntPair> tmp = new ArrayList(eligibleChunksForSpawning);
|
||||
Collections.shuffle(tmp);
|
||||
Iterator<ChunkCoordIntPair> iterator = tmp.iterator();
|
||||
HashMap<Integer, Integer> spawnEntryCounts = getNaturalSpawnEntryCounts(world);
|
||||
int chunkBudget = getRuntimeChunkBudget(eligibleChunksForSpawning.size());
|
||||
List<ChunkCoordIntPair> chunksToProcess = getRuntimeChunkBatch(world, chunkBudget);
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
ChunkCoordIntPair chunkcoordintpair1 = iterator.next();
|
||||
for (ChunkCoordIntPair chunkcoordintpair1 : chunksToProcess) {
|
||||
if (npcCount >= cap) {
|
||||
return;
|
||||
}
|
||||
|
||||
ChunkPosition chunkposition = getChunk(world, chunkcoordintpair1.chunkXPos, chunkcoordintpair1.chunkZPos);
|
||||
int j1 = chunkposition.chunkPosX;
|
||||
int y = chunkposition.chunkPosY;
|
||||
int l1 = chunkposition.chunkPosZ;
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
for (int i = 0; i < SPAWN_ATTEMPTS_PER_CHUNK; i++) {
|
||||
if (npcCount >= cap) {
|
||||
return;
|
||||
}
|
||||
int x = j1;
|
||||
int z = l1;
|
||||
byte b1 = 6;
|
||||
@@ -85,25 +109,71 @@ public class NPCSpawning {
|
||||
|
||||
String name = world.getBiomeGenForCoords(x, z).biomeName;
|
||||
SpawnData data = SpawnController.Instance.getRandomSpawnData(name, world.provider.dimensionId);
|
||||
if (data == null || !canCreatureTypeSpawnAtLocation(data, world, x, y, z) || world.getClosestPlayer(x, y, z, 24.0D) != null)
|
||||
if (data == null)
|
||||
continue;
|
||||
|
||||
spawnData(data, world, x, y, z);
|
||||
if (trySpawnWithEntryConfig(data, world, x, y, z, world.rand, spawnEntryCounts)) {
|
||||
npcCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int countNPCs(World world) {
|
||||
private static int getRuntimeChunkBudget(int eligibleChunkCount) {
|
||||
if (eligibleChunkCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
int runsPerCycle = Math.max(1, (int) (RUNTIME_SPAWN_CYCLE_TICKS / RUNTIME_SPAWN_TICK_INTERVAL));
|
||||
return Math.max(1, (int) Math.ceil((double) eligibleChunkCount / runsPerCycle));
|
||||
}
|
||||
|
||||
private static List<ChunkCoordIntPair> getRuntimeChunkBatch(World world, int chunkBudget) {
|
||||
ArrayList<ChunkCoordIntPair> result = new ArrayList<ChunkCoordIntPair>();
|
||||
if (chunkBudget <= 0 || eligibleChunksForSpawning.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
int dimensionId = world.provider.dimensionId;
|
||||
RuntimeChunkState state = RUNTIME_CHUNK_STATES.get(dimensionId);
|
||||
if (state == null) {
|
||||
state = new RuntimeChunkState();
|
||||
RUNTIME_CHUNK_STATES.put(dimensionId, state);
|
||||
}
|
||||
|
||||
int signature = eligibleChunksForSpawning.hashCode();
|
||||
if (state.chunks.isEmpty() || state.signature != signature || state.index >= state.chunks.size()) {
|
||||
state.chunks = new ArrayList<ChunkCoordIntPair>(eligibleChunksForSpawning);
|
||||
Collections.shuffle(state.chunks);
|
||||
state.index = 0;
|
||||
state.signature = signature;
|
||||
}
|
||||
|
||||
int remaining = state.chunks.size() - state.index;
|
||||
int count = Math.min(chunkBudget, Math.max(0, remaining));
|
||||
for (int i = 0; i < count; i++) {
|
||||
result.add(state.chunks.get(state.index++));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int countNaturalSpawnedNPCs(World world) {
|
||||
int count = 0;
|
||||
List<Entity> list = world.loadedEntityList;
|
||||
for (Entity entity : list) {
|
||||
if (entity instanceof EntityNPCInterface) {
|
||||
if (entity instanceof EntityNPCInterface && !entity.isDead && entity.getEntityData().hasKey(NATURAL_SPAWN_ID_TAG)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
private static int getScaledNaturalCap(int eligibleChunkCount) {
|
||||
if (eligibleChunkCount <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(1, NATURAL_CAP_PER_256_CHUNKS * eligibleChunkCount / 256);
|
||||
}
|
||||
|
||||
protected static ChunkPosition getChunk(World world, int x, int z) {
|
||||
Chunk chunk = world.getChunkFromChunkCoords(x, z);
|
||||
int k = x * 16 + world.rand.nextInt(16);
|
||||
@@ -112,9 +182,134 @@ public class NPCSpawning {
|
||||
return new ChunkPosition(k, i1, l);
|
||||
}
|
||||
|
||||
private static int getCap(World world) {
|
||||
Set<ChunkCoordIntPair> chunkSet = Sets.newHashSet();
|
||||
for (int i = 0; i < world.playerEntities.size(); ++i) {
|
||||
EntityPlayer entityplayer = (EntityPlayer) world.playerEntities.get(i);
|
||||
int j = MathHelper.floor_double(entityplayer.posX / 16.0D);
|
||||
int k = MathHelper.floor_double(entityplayer.posZ / 16.0D);
|
||||
byte size = CHUNK_SPAWN_RADIUS;
|
||||
|
||||
for (int x = -size; x <= size; ++x) {
|
||||
for (int z = -size; z <= size; ++z) {
|
||||
chunkSet.add(new ChunkCoordIntPair(x + j, z + k));
|
||||
}
|
||||
}
|
||||
}
|
||||
return getScaledNaturalCap(chunkSet.size());
|
||||
}
|
||||
|
||||
private static HashMap<Integer, Integer> getNaturalSpawnEntryCounts(World world) {
|
||||
HashMap<Integer, Integer> counts = new HashMap<Integer, Integer>();
|
||||
List<Entity> list = world.loadedEntityList;
|
||||
for (Entity entity : list) {
|
||||
if (entity == null || entity.isDead || !(entity instanceof EntityNPCInterface)) {
|
||||
continue;
|
||||
}
|
||||
NBTTagCompound entityData = entity.getEntityData();
|
||||
if (!entityData.hasKey(NATURAL_SPAWN_ID_TAG)) {
|
||||
continue;
|
||||
}
|
||||
int spawnId = entityData.getInteger(NATURAL_SPAWN_ID_TAG);
|
||||
Integer count = counts.get(spawnId);
|
||||
counts.put(spawnId, count == null ? 1 : count + 1);
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
private static long getSpawnCooldownKey(World world, int spawnId) {
|
||||
return ((long) world.provider.dimensionId << 32) ^ (spawnId & 0xffffffffL);
|
||||
}
|
||||
|
||||
private static boolean isSpawnOnCooldown(World world, SpawnData data) {
|
||||
if (data.cooldownTicks <= 0 || data.id < 0) {
|
||||
return false;
|
||||
}
|
||||
long key = getSpawnCooldownKey(world, data.id);
|
||||
Long lastSpawnTick = SPAWN_COOLDOWNS.get(key);
|
||||
if (lastSpawnTick == null) {
|
||||
return false;
|
||||
}
|
||||
long tickDelta = world.getWorldInfo().getWorldTotalTime() - lastSpawnTick;
|
||||
if (tickDelta < 0) {
|
||||
SPAWN_COOLDOWNS.remove(key);
|
||||
return false;
|
||||
}
|
||||
return tickDelta < data.cooldownTicks;
|
||||
}
|
||||
|
||||
private static void markSpawnCooldown(World world, SpawnData data) {
|
||||
if (data.cooldownTicks <= 0 || data.id < 0) {
|
||||
return;
|
||||
}
|
||||
long key = getSpawnCooldownKey(world, data.id);
|
||||
SPAWN_COOLDOWNS.put(key, world.getWorldInfo().getWorldTotalTime());
|
||||
}
|
||||
|
||||
private static boolean isSpawnEntryAtCapacity(SpawnData data, Map<Integer, Integer> spawnEntryCounts) {
|
||||
if (data.maxAlive <= 0 || data.id < 0) {
|
||||
return false;
|
||||
}
|
||||
Integer currentCount = spawnEntryCounts.get(data.id);
|
||||
return currentCount != null && currentCount >= data.maxAlive;
|
||||
}
|
||||
|
||||
private static void incrementSpawnEntryCount(SpawnData data, Map<Integer, Integer> spawnEntryCounts) {
|
||||
if (data.id < 0) {
|
||||
return;
|
||||
}
|
||||
Integer currentCount = spawnEntryCounts.get(data.id);
|
||||
spawnEntryCounts.put(data.id, currentCount == null ? 1 : currentCount + 1);
|
||||
}
|
||||
|
||||
private static boolean hasNearbyPlayer(SpawnData data, World world, int x, int y, int z) {
|
||||
if (data.playerMinDistance <= 0) {
|
||||
return false;
|
||||
}
|
||||
return world.getClosestPlayer(x, y, z, data.playerMinDistance) != null;
|
||||
}
|
||||
|
||||
private static boolean trySpawnWithEntryConfig(SpawnData data, World world, int x, int y, int z, Random rand, Map<Integer, Integer> spawnEntryCounts) {
|
||||
if (y < data.spawnHeightMin || y > data.spawnHeightMax) {
|
||||
return false;
|
||||
}
|
||||
if (isSpawnEntryAtCapacity(data, spawnEntryCounts) || isSpawnOnCooldown(world, data)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int attempts = Math.max(1, data.attemptsPerCycle);
|
||||
for (int attempt = 0; attempt < attempts; attempt++) {
|
||||
int attemptX = x;
|
||||
int attemptZ = z;
|
||||
if (attempt > 0) {
|
||||
attemptX += rand.nextInt(5) - rand.nextInt(5);
|
||||
attemptZ += rand.nextInt(5) - rand.nextInt(5);
|
||||
}
|
||||
|
||||
if (!canCreatureTypeSpawnAtLocation(data, world, attemptX, y, attemptZ) || hasNearbyPlayer(data, world, attemptX, y, attemptZ)) {
|
||||
continue;
|
||||
}
|
||||
if (spawnData(data, world, attemptX, y, attemptZ)) {
|
||||
incrementSpawnEntryCount(data, spawnEntryCounts);
|
||||
markSpawnCooldown(world, data);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void performWorldGenSpawning(World world, int x, int z, Random rand) {
|
||||
int cap = getCap(world);
|
||||
int npcCount = countNaturalSpawnedNPCs(world);
|
||||
if (npcCount >= cap) {
|
||||
return;
|
||||
}
|
||||
HashMap<Integer, Integer> spawnEntryCounts = getNaturalSpawnEntryCounts(world);
|
||||
BiomeGenBase biome = world.getBiomeGenForCoords(x + 8, z + 8);
|
||||
while (rand.nextFloat() < biome.getSpawningChance()) {
|
||||
if (npcCount >= cap) {
|
||||
return;
|
||||
}
|
||||
SpawnData data = SpawnController.Instance.getRandomSpawnData(biome.biomeName, world.provider.dimensionId);
|
||||
if (data == null)
|
||||
continue;
|
||||
@@ -127,6 +322,9 @@ public class NPCSpawning {
|
||||
int i2 = k1;
|
||||
|
||||
for (int k2 = 0; k2 < 4; ++k2) {
|
||||
if (npcCount >= cap) {
|
||||
return;
|
||||
}
|
||||
int l2 = world.getTopSolidOrLiquidBlock(j1, k1);
|
||||
if (l2 > data.spawnHeightMax || l2 < data.spawnHeightMin) {
|
||||
continue;
|
||||
@@ -134,14 +332,16 @@ public class NPCSpawning {
|
||||
l2 = l2 + (int) ((data.spawnHeightMax - l2) * Math.random());
|
||||
}
|
||||
|
||||
if (!canCreatureTypeSpawnAtLocation(data, world, j1, l2, k1)) {
|
||||
if (!trySpawnWithEntryConfig(data, world, j1, l2, k1, rand, spawnEntryCounts)) {
|
||||
j1 += rand.nextInt(5) - rand.nextInt(5);
|
||||
|
||||
for (k1 += rand.nextInt(5) - rand.nextInt(5); j1 < x || j1 >= x + size || k1 < z || k1 >= z + size; k1 = i2 + rand.nextInt(5) - rand.nextInt(5)) {
|
||||
j1 = l1 + rand.nextInt(5) - rand.nextInt(5);
|
||||
}
|
||||
} else if (spawnData(data, world, j1, l2, k1))
|
||||
} else {
|
||||
npcCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -184,12 +384,23 @@ public class NPCSpawning {
|
||||
if (spawnEntity instanceof EntityCustomNpc) {
|
||||
EntityCustomNpc npc = (EntityCustomNpc) spawnEntity;
|
||||
npc.stats.spawnCycle = 3;
|
||||
if (data.despawnMode == SpawnData.DESPAWN_FORCE_PERSISTENT) {
|
||||
npc.stats.canDespawn = false;
|
||||
npc.stats.playerSetCanDespawn = false;
|
||||
} else if (data.despawnMode == SpawnData.DESPAWN_FORCE_NATURAL) {
|
||||
npc.stats.canDespawn = true;
|
||||
npc.stats.playerSetCanDespawn = true;
|
||||
}
|
||||
npc.syncDespawnPersistence();
|
||||
npc.ais.returnToStart = false;
|
||||
npc.ais.startPos = new int[]{x, y, z};
|
||||
npc.updateAI = true;
|
||||
npc.updateClient = true;
|
||||
npc.getNavigator().clearPathEntity();
|
||||
}
|
||||
entityliving.getEntityData().setInteger(NATURAL_SPAWN_ID_TAG, data.id);
|
||||
entityliving.getEntityData().setString(NATURAL_SPAWN_NAME_TAG, data.name);
|
||||
entityliving.getEntityData().setLong(NATURAL_SPAWN_TIME_TAG, world.getWorldInfo().getWorldTotalTime());
|
||||
spawnEntity.setLocationAndAngles(x + 0.5, y, z + 0.5, world.rand.nextFloat() * 360.0F, 0.0F);
|
||||
} catch (Exception exception) {
|
||||
exception.printStackTrace();
|
||||
@@ -239,4 +450,3 @@ public class NPCSpawning {
|
||||
return animalSpawn || monsterSpawn || caveSpawn || liquidSpawn;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import kamkeel.npcs.controllers.AbilityController;
|
||||
import kamkeel.npcs.controllers.AttributeController;
|
||||
import kamkeel.npcs.controllers.SyncController;
|
||||
import kamkeel.npcs.controllers.data.ability.Ability;
|
||||
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
|
||||
import kamkeel.npcs.util.AttributeAttackUtil;
|
||||
import net.minecraft.entity.Entity;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
@@ -105,7 +106,8 @@ public class ScriptPlayerEventHandler {
|
||||
// Skip zeroing if the ability has its own movement (Charge/Dash/Slam)
|
||||
if (playerData.abilityData.isMovementLocked()) {
|
||||
Ability current = playerData.abilityData.getCurrentAbility();
|
||||
if (current == null || !current.hasAbilityMovement()) {
|
||||
if (current == null || !current.hasAbilityMovement()
|
||||
|| current.getPhase() != AbilityPhase.ACTIVE) {
|
||||
boolean sFlying = playerData.abilityData.wasFlyingAtLock()
|
||||
|| AbilityController.Instance.isPlayerFlying(player);
|
||||
|
||||
@@ -120,6 +122,17 @@ public class ScriptPlayerEventHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// Zero WASD input during ability-controlled movement to prevent
|
||||
// moveEntityWithHeading from interfering with ability velocity
|
||||
{
|
||||
Ability current = playerData.abilityData.getCurrentAbility();
|
||||
if (current != null && current.hasAbilityMovement()
|
||||
&& current.getPhase() == AbilityPhase.ACTIVE) {
|
||||
player.moveForward = 0;
|
||||
player.moveStrafing = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (playerData.updateClient) {
|
||||
SyncController.syncPlayerData((EntityPlayerMP) player, true);
|
||||
playerData.updateClient = false;
|
||||
|
||||
@@ -530,7 +530,7 @@ public class ServerEventsHandler {
|
||||
|
||||
@SubscribeEvent
|
||||
public void populateChunk(PopulateChunkEvent.Post event) {
|
||||
NPCSpawning.performWorldGenSpawning(event.world, event.chunkX, event.chunkZ, event.rand);
|
||||
NPCSpawning.performWorldGenSpawning(event.world, event.chunkX << 4, event.chunkZ << 4, event.rand);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
|
||||
@@ -25,6 +25,9 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class FlyPathFinder extends PathFinder {
|
||||
private static final float STRAIGHT_COST = 1.0F;
|
||||
private static final float DIAGONAL_2D_COST = 1.4142135F;
|
||||
private static final float DIAGONAL_3D_COST = 1.7320508F;
|
||||
/**
|
||||
* Used to find obstacles
|
||||
*/
|
||||
@@ -141,7 +144,7 @@ public class FlyPathFinder extends PathFinder {
|
||||
|
||||
private NPCPath addToPath(Entity p_75861_1_, NPCPathPoint p_75861_2_, NPCPathPoint p_75861_3_, NPCPathPoint p_75861_4_, float p_75861_5_) {
|
||||
p_75861_2_.totalPathDistance = 0.0F;
|
||||
p_75861_2_.distanceToNext = p_75861_2_.distanceToSquared(p_75861_3_);
|
||||
p_75861_2_.distanceToNext = this.estimateCostToTarget(p_75861_2_, p_75861_3_);
|
||||
p_75861_2_.distanceToTarget = p_75861_2_.distanceToNext;
|
||||
this.path.clearPath();
|
||||
this.path.addPoint(p_75861_2_);
|
||||
@@ -161,7 +164,7 @@ public class FlyPathFinder extends PathFinder {
|
||||
return this.createEntityPath(p_75861_2_, p_75861_3_);
|
||||
}
|
||||
|
||||
if (pathpoint4.distanceToSquared(p_75861_3_) < pathpoint3.distanceToSquared(p_75861_3_)) {
|
||||
if (this.estimateCostToTarget(pathpoint4, p_75861_3_) < this.estimateCostToTarget(pathpoint3, p_75861_3_)) {
|
||||
pathpoint3 = pathpoint4;
|
||||
}
|
||||
|
||||
@@ -170,12 +173,13 @@ public class FlyPathFinder extends PathFinder {
|
||||
|
||||
for (int j = 0; j < i; ++j) {
|
||||
NPCPathPoint pathpoint5 = this.pathOptions[j];
|
||||
float f1 = pathpoint4.totalPathDistance + pathpoint4.distanceToSquared(pathpoint5);
|
||||
float f1 = pathpoint4.totalPathDistance + this.getTraversalCost(pathpoint4, pathpoint5)
|
||||
+ this.getDirectionalPenalty(pathpoint4, pathpoint5, p_75861_3_);
|
||||
|
||||
if (!pathpoint5.isAssigned() || f1 < pathpoint5.totalPathDistance) {
|
||||
pathpoint5.previous = pathpoint4;
|
||||
pathpoint5.totalPathDistance = f1;
|
||||
pathpoint5.distanceToNext = pathpoint5.distanceToSquared(p_75861_3_);
|
||||
pathpoint5.distanceToNext = this.estimateCostToTarget(pathpoint5, p_75861_3_);
|
||||
|
||||
if (pathpoint5.isAssigned()) {
|
||||
this.path.changeDistance(pathpoint5, pathpoint5.totalPathDistance + pathpoint5.distanceToNext);
|
||||
@@ -194,6 +198,50 @@ public class FlyPathFinder extends PathFinder {
|
||||
}
|
||||
}
|
||||
|
||||
private float getTraversalCost(NPCPathPoint from, NPCPathPoint to) {
|
||||
int dx = Math.abs(to.xCoord - from.xCoord);
|
||||
int dy = Math.abs(to.yCoord - from.yCoord);
|
||||
int dz = Math.abs(to.zCoord - from.zCoord);
|
||||
int changedAxes = dx + dy + dz;
|
||||
if (changedAxes <= 1) {
|
||||
return STRAIGHT_COST;
|
||||
}
|
||||
if (changedAxes == 2) {
|
||||
return DIAGONAL_2D_COST;
|
||||
}
|
||||
return DIAGONAL_3D_COST;
|
||||
}
|
||||
|
||||
private float getDirectionalPenalty(NPCPathPoint from, NPCPathPoint to, NPCPathPoint goal) {
|
||||
int stepX = to.xCoord - from.xCoord;
|
||||
int stepY = to.yCoord - from.yCoord;
|
||||
int stepZ = to.zCoord - from.zCoord;
|
||||
int goalX = goal.xCoord - from.xCoord;
|
||||
int goalY = goal.yCoord - from.yCoord;
|
||||
int goalZ = goal.zCoord - from.zCoord;
|
||||
int stepLen = Math.abs(stepX) + Math.abs(stepY) + Math.abs(stepZ);
|
||||
int goalLen = Math.abs(goalX) + Math.abs(goalY) + Math.abs(goalZ);
|
||||
|
||||
if (stepLen == 0 || goalLen == 0) {
|
||||
return 0.0F;
|
||||
}
|
||||
|
||||
float alignment = (float) (stepX * goalX + stepY * goalY + stepZ * goalZ) / (float) (stepLen * goalLen);
|
||||
return (1.0F - alignment) * 0.04F;
|
||||
}
|
||||
|
||||
private float estimateCostToTarget(NPCPathPoint from, NPCPathPoint to) {
|
||||
float dx = Math.abs(to.xCoord - from.xCoord);
|
||||
float dy = Math.abs(to.yCoord - from.yCoord);
|
||||
float dz = Math.abs(to.zCoord - from.zCoord);
|
||||
|
||||
float max = Math.max(dx, Math.max(dy, dz));
|
||||
float min = Math.min(dx, Math.min(dy, dz));
|
||||
float mid = dx + dy + dz - max - min;
|
||||
|
||||
return DIAGONAL_3D_COST * min + DIAGONAL_2D_COST * (mid - min) + STRAIGHT_COST * (max - mid);
|
||||
}
|
||||
|
||||
/**
|
||||
* populates pathOptions with available points and returns the number of options found (args: unused1, currentPoint,
|
||||
* unused2, targetPoint, maxDistance)
|
||||
|
||||
@@ -7,6 +7,8 @@ import noppes.npcs.entity.EntityNPCFlying;
|
||||
import noppes.npcs.entity.EntityNPCInterface;
|
||||
|
||||
public class FlyingMoveHelper extends EntityMoveHelper {
|
||||
private static final double CLOSE_ENOUGH_SQ = 0.01D;
|
||||
private static final float MAX_TURN_PER_TICK = 35.0F;
|
||||
private final EntityNPCInterface entity;
|
||||
|
||||
private double posX;
|
||||
@@ -38,22 +40,24 @@ public class FlyingMoveHelper extends EntityMoveHelper {
|
||||
double d5 = MathHelper.sqrt_double(d4);
|
||||
speed = Math.min(d5 / 5.0D, speed);
|
||||
|
||||
// its here louis!
|
||||
// This is the hurt time that stuns the NPC!!!!!!
|
||||
// if (this.entity.hurtTime == 0 && d4 > 0.5D) {
|
||||
if (d4 >= 0.5D) {
|
||||
this.entity.motionX += (speed * (d0 / d5) - this.entity.motionX) * speed;
|
||||
this.entity.motionZ += (speed * (d2 / d5) - this.entity.motionZ) * speed;
|
||||
if (d4 > CLOSE_ENOUGH_SQ && d5 > 1.0E-4D) {
|
||||
double targetMotionX = speed * (d0 / d5);
|
||||
double targetMotionZ = speed * (d2 / d5);
|
||||
double horizontalLerp = Math.min(1.0D, Math.max(0.12D, speed * 0.6D));
|
||||
this.entity.motionX += (targetMotionX - this.entity.motionX) * horizontalLerp;
|
||||
this.entity.motionZ += (targetMotionZ - this.entity.motionZ) * horizontalLerp;
|
||||
|
||||
if (((EntityNPCFlying) this.entity).flyLimitAllow || !this.entity.ais.hasFlyLimit) {
|
||||
this.entity.motionY = verticalSpeed * (d1 / d5);
|
||||
if (this.entity.motionY > 0) {
|
||||
this.entity.motionY += 0.1D;
|
||||
double targetMotionY = verticalSpeed * (d1 / d5);
|
||||
if (targetMotionY > 0.0D) {
|
||||
targetMotionY += 0.05D;
|
||||
}
|
||||
double verticalLerp = Math.min(1.0D, Math.max(0.15D, verticalSpeed * 0.85D));
|
||||
this.entity.motionY += (targetMotionY - this.entity.motionY) * verticalLerp;
|
||||
}
|
||||
|
||||
this.entity.velocityChanged = true;
|
||||
this.entity.rotationYaw = this.limitAngle(this.entity.rotationYaw, (float) ((Math.atan2(-d0, -d2) + Math.PI) * -(180F / Math.PI)), 20.0F);
|
||||
this.entity.rotationYaw = this.limitAngle(this.entity.rotationYaw, (float) ((Math.atan2(-d0, -d2) + Math.PI) * -(180F / Math.PI)), MAX_TURN_PER_TICK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ import noppes.npcs.entity.EntityNPCInterface;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class PathNavigateFlying extends PathNavigate {
|
||||
private static final long MIN_REPATH_INTERVAL_TICKS = 8L;
|
||||
private static final long MAX_REPATH_INTERVAL_TICKS = 30L;
|
||||
private static final int PATH_REUSE_TARGET_DELTA_SQ = 2;
|
||||
private static final int FINAL_NODE_REUSE_DELTA_SQ = 3;
|
||||
|
||||
private EntityNPCInterface theEntity;
|
||||
private World worldObj;
|
||||
@@ -83,16 +87,21 @@ public class PathNavigateFlying extends PathNavigate {
|
||||
}
|
||||
|
||||
public void updatePath() {
|
||||
if (this.worldObj.getTotalWorldTime() - this.lastTimeUpdated > 20L) {
|
||||
if (this.targetPos != null) {
|
||||
this.currentPath = null;
|
||||
this.currentPath = this.getPathToXYZ(this.targetPos.getX(), this.targetPos.getY(), this.targetPos.getZ());
|
||||
this.lastTimeUpdated = this.worldObj.getTotalWorldTime();
|
||||
this.tryUpdatePath = false;
|
||||
}
|
||||
} else {
|
||||
this.tryUpdatePath = true;
|
||||
if (this.targetPos == null) {
|
||||
this.tryUpdatePath = false;
|
||||
return;
|
||||
}
|
||||
|
||||
long now = this.worldObj.getTotalWorldTime();
|
||||
if (now - this.lastTimeUpdated < this.getAdaptiveRepathInterval()) {
|
||||
this.tryUpdatePath = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentPath = null;
|
||||
this.currentPath = this.getPathToXYZ(this.targetPos.getX(), this.targetPos.getY(), this.targetPos.getZ());
|
||||
this.lastTimeUpdated = now;
|
||||
this.tryUpdatePath = false;
|
||||
}
|
||||
|
||||
public void setAvoidsWater(boolean p_75491_1_) {
|
||||
@@ -156,7 +165,8 @@ public class PathNavigateFlying extends PathNavigate {
|
||||
BlockPos pos = new BlockPos(x, y, z);
|
||||
if (!this.canNavigate()) {
|
||||
return null;
|
||||
} else if (this.currentPath != null && !this.currentPath.isFinished() && pos.equals(this.targetPos)) {
|
||||
} else if (this.canReuseCurrentPath(pos)) {
|
||||
this.targetPos = pos;
|
||||
return this.currentPath;
|
||||
} else {
|
||||
this.targetPos = pos;
|
||||
@@ -180,7 +190,8 @@ public class PathNavigateFlying extends PathNavigate {
|
||||
return null;
|
||||
} else {
|
||||
BlockPos blockpos = new BlockPos(entity);
|
||||
if (this.currentPath != null && !this.currentPath.isFinished() && blockpos.equals(this.targetPos)) {
|
||||
if (this.canReuseCurrentPath(blockpos)) {
|
||||
this.targetPos = blockpos;
|
||||
return this.currentPath;
|
||||
} else {
|
||||
this.targetPos = blockpos;
|
||||
@@ -215,6 +226,8 @@ public class PathNavigateFlying extends PathNavigate {
|
||||
this.removeSunnyPath();
|
||||
}
|
||||
|
||||
this.smoothCurrentPath();
|
||||
|
||||
if (this.currentPath.getCurrentPathLength() == 0) {
|
||||
return false;
|
||||
} else {
|
||||
@@ -242,6 +255,72 @@ public class PathNavigateFlying extends PathNavigate {
|
||||
return !this.isInLiquid() || (this.canSwim && this.isInLiquid());
|
||||
}
|
||||
|
||||
private long getAdaptiveRepathInterval() {
|
||||
if (this.targetPos == null) {
|
||||
return MAX_REPATH_INTERVAL_TICKS;
|
||||
}
|
||||
double distance = this.theEntity.getDistance(this.targetPos.getX() + 0.5D, this.targetPos.getY(), this.targetPos.getZ() + 0.5D);
|
||||
return (long) MathHelper.clamp_double(distance * 0.6D, MIN_REPATH_INTERVAL_TICKS, MAX_REPATH_INTERVAL_TICKS);
|
||||
}
|
||||
|
||||
private boolean canReuseCurrentPath(BlockPos newTarget) {
|
||||
if (this.currentPath == null || this.currentPath.isFinished() || this.targetPos == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.getBlockDistanceSq(this.targetPos, newTarget) <= PATH_REUSE_TARGET_DELTA_SQ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
NPCPathPoint finalPoint = this.currentPath.getFinalPathPoint();
|
||||
return finalPoint != null && this.getPointDistanceSq(finalPoint, newTarget) <= FINAL_NODE_REUSE_DELTA_SQ;
|
||||
}
|
||||
|
||||
private int getBlockDistanceSq(BlockPos a, BlockPos b) {
|
||||
int dx = a.getX() - b.getX();
|
||||
int dy = a.getY() - b.getY();
|
||||
int dz = a.getZ() - b.getZ();
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private int getPointDistanceSq(NPCPathPoint point, BlockPos blockPos) {
|
||||
int dx = point.xCoord - blockPos.getX();
|
||||
int dy = point.yCoord - blockPos.getY();
|
||||
int dz = point.zCoord - blockPos.getZ();
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
|
||||
private void smoothCurrentPath() {
|
||||
if (this.currentPath == null) {
|
||||
return;
|
||||
}
|
||||
int pathLength = this.currentPath.getCurrentPathLength();
|
||||
if (pathLength < 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
int width = MathHelper.ceiling_double_int(this.theEntity.width);
|
||||
int height = MathHelper.ceiling_double_int(this.theEntity.height + 1.0F);
|
||||
NPCPathPoint[] points = this.currentPath.points;
|
||||
int writeIndex = 1;
|
||||
int anchorIndex = 0;
|
||||
|
||||
for (int readIndex = 2; readIndex < pathLength; readIndex++) {
|
||||
Vec3 anchor = this.currentPath.getVectorFromIndex(this.theEntity, anchorIndex);
|
||||
Vec3 probe = this.currentPath.getVectorFromIndex(this.theEntity, readIndex);
|
||||
if (!this.isDirectPathBetweenPoints(anchor, probe, width, height)) {
|
||||
points[writeIndex++] = points[readIndex - 1];
|
||||
anchorIndex = readIndex - 1;
|
||||
}
|
||||
}
|
||||
|
||||
points[writeIndex++] = points[pathLength - 1];
|
||||
this.currentPath.setCurrentPathLength(writeIndex);
|
||||
if (this.currentPath.getCurrentPathIndex() >= writeIndex) {
|
||||
this.currentPath.setCurrentPathIndex(Math.max(0, writeIndex - 1));
|
||||
}
|
||||
}
|
||||
|
||||
public void onUpdateNavigation() {
|
||||
|
||||
++this.totalTicks;
|
||||
|
||||
@@ -102,7 +102,17 @@ public class ClientEventHandler {
|
||||
// Ability Hotbar: Hold HUD key + scroll wheel to cycle
|
||||
if (event.dwheel != 0 && ClientProxy.AbilityHudKey != null) {
|
||||
int hudKeyCode = ClientProxy.AbilityHudKey.getKeyCode();
|
||||
if (hudKeyCode != 0 && org.lwjgl.input.Keyboard.isKeyDown(hudKeyCode)) {
|
||||
boolean hudKeyHeld = false;
|
||||
if (hudKeyCode > 0) {
|
||||
hudKeyHeld = hudKeyCode < org.lwjgl.input.Keyboard.getKeyCount()
|
||||
&& org.lwjgl.input.Keyboard.isKeyDown(hudKeyCode);
|
||||
} else if (hudKeyCode < 0) {
|
||||
int mouseButton = hudKeyCode + 100;
|
||||
hudKeyHeld = mouseButton >= 0
|
||||
&& mouseButton < org.lwjgl.input.Mouse.getButtonCount()
|
||||
&& org.lwjgl.input.Mouse.isButtonDown(mouseButton);
|
||||
}
|
||||
if (hudKeyHeld) {
|
||||
AbilityHotbarComponent comp = (AbilityHotbarComponent) ClientHudManager.getInstance()
|
||||
.getHudComponents().get(EnumHudComponent.AbilityHotbar);
|
||||
if (comp != null && comp.hasAnyAbilities()) {
|
||||
|
||||
@@ -117,6 +117,7 @@ import noppes.npcs.client.gui.global.GuiNPCManageQuest;
|
||||
import noppes.npcs.client.gui.global.GuiNPCManageTags;
|
||||
import noppes.npcs.client.gui.global.GuiNPCManageTransporters;
|
||||
import noppes.npcs.client.gui.global.GuiNpcManageAbilities;
|
||||
import noppes.npcs.client.gui.global.GuiNpcManageAuction;
|
||||
import noppes.npcs.client.gui.global.GuiNpcManageMagic;
|
||||
import noppes.npcs.client.gui.global.GuiNpcManageRecipes;
|
||||
import noppes.npcs.client.gui.global.GuiNpcQuestReward;
|
||||
@@ -221,6 +222,7 @@ import noppes.npcs.containers.ContainerCrate;
|
||||
import noppes.npcs.containers.ContainerCustomGui;
|
||||
import noppes.npcs.containers.ContainerMail;
|
||||
import noppes.npcs.containers.ContainerManageBanks;
|
||||
import noppes.npcs.containers.ContainerManageAuction;
|
||||
import noppes.npcs.containers.ContainerManageRecipes;
|
||||
import noppes.npcs.containers.ContainerNPCBankInterface;
|
||||
import noppes.npcs.containers.ContainerNPCCompanion;
|
||||
@@ -512,6 +514,9 @@ public class ClientProxy extends CommonProxy {
|
||||
else if (gui == EnumGuiType.ManageRecipes)
|
||||
return new GuiNpcManageRecipes(npc, (ContainerManageRecipes) container);
|
||||
|
||||
else if (gui == EnumGuiType.ManageAuction)
|
||||
return new GuiNpcManageAuction(npc, (ContainerManageAuction) container);
|
||||
|
||||
else if (gui == EnumGuiType.ManageDialogs)
|
||||
return new GuiNPCManageDialogs(npc);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import kamkeel.npcs.client.renderer.lightning.LightningBolt;
|
||||
import kamkeel.npcs.controllers.data.energycharge.EnergyChargePreviewManager;
|
||||
import kamkeel.npcs.controllers.data.telegraph.TelegraphManager;
|
||||
import kamkeel.npcs.network.PacketClient;
|
||||
import kamkeel.npcs.network.packets.data.RequestProperSpawnData;
|
||||
import kamkeel.npcs.network.packets.player.CheckPlayerValue;
|
||||
import kamkeel.npcs.network.packets.player.InputDevicePacket;
|
||||
import kamkeel.npcs.network.packets.player.ScreenSizePacket;
|
||||
@@ -61,7 +62,7 @@ public class ClientTickHandler {
|
||||
if (mc.theWorld == null) {
|
||||
ClientCacheHandler.clearCache();
|
||||
ClientAbilityState.reset();
|
||||
// RequestProperSpawnData.clear();
|
||||
RequestProperSpawnData.clear();
|
||||
if (EnergyChargePreviewManager.ClientInstance != null) {
|
||||
EnergyChargePreviewManager.ClientInstance.clear();
|
||||
}
|
||||
@@ -216,8 +217,8 @@ public class ClientTickHandler {
|
||||
return;
|
||||
|
||||
if (mc.theWorld.getTotalWorldTime() % 20 == 0) { // Update every second
|
||||
// if (RequestProperSpawnData.canDoBatchUpdate())
|
||||
// RequestProperSpawnData.handleBacklog();
|
||||
if (RequestProperSpawnData.canDoBatchUpdate())
|
||||
RequestProperSpawnData.handleBacklog();
|
||||
updateCompassMarks();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import noppes.npcs.client.gui.util.GuiNpcButton;
|
||||
import noppes.npcs.client.gui.util.GuiNpcButtonYesNo;
|
||||
import noppes.npcs.client.gui.util.GuiNpcLabel;
|
||||
import noppes.npcs.client.gui.util.GuiNpcTextField;
|
||||
import noppes.npcs.client.gui.util.GuiScrollWindow;
|
||||
import noppes.npcs.client.gui.util.ITextfieldListener;
|
||||
import noppes.npcs.client.gui.util.SubGuiInterface;
|
||||
import noppes.npcs.controllers.data.SpawnData;
|
||||
@@ -15,37 +16,97 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield
|
||||
public SubGuiSpawningOptions(SpawnData data) {
|
||||
this.data = data;
|
||||
setBackground("menubg.png");
|
||||
xSize = 216;
|
||||
xSize = 256;
|
||||
ySize = 216;
|
||||
closeOnEsc = true;
|
||||
}
|
||||
|
||||
public void initGui() {
|
||||
super.initGui();
|
||||
int y = guiTop - 15;
|
||||
int scrollX = guiLeft + 4;
|
||||
int scrollY = guiTop + 4;
|
||||
int scrollW = xSize - 8;
|
||||
int scrollH = ySize - 34;
|
||||
|
||||
addButton(new GuiNpcButtonYesNo(10, guiLeft + 120, y += 22, data.animalSpawning));
|
||||
addLabel(new GuiNpcLabel(10, "spawning.animalSpawning", guiLeft + 4, y + 5));
|
||||
GuiScrollWindow sw = new GuiScrollWindow(this, scrollX, scrollY, scrollW, scrollH, 0);
|
||||
sw.drawDefaultBackground = false;
|
||||
addScrollableGui(0, sw);
|
||||
|
||||
addButton(new GuiNpcButtonYesNo(11, guiLeft + 120, y += 22, data.monsterSpawning));
|
||||
addLabel(new GuiNpcLabel(11, "spawning.monsterSpawning", guiLeft + 4, y + 5));
|
||||
int labelX = 4;
|
||||
int controlX = 132;
|
||||
int y = 6;
|
||||
|
||||
addButton(new GuiNpcButtonYesNo(12, guiLeft + 120, y += 22, data.liquidSpawning));
|
||||
addLabel(new GuiNpcLabel(12, "spawning.liquidSpawning", guiLeft + 4, y + 5));
|
||||
sw.addButton(new GuiNpcButtonYesNo(10, controlX, y, data.animalSpawning));
|
||||
sw.addLabel(new GuiNpcLabel(10, "spawning.animalSpawning", labelX, y + 5));
|
||||
sw.getButton(10).setHoverText("tooltip.naturalspawns.options.animal");
|
||||
y += 22;
|
||||
|
||||
addButton(new GuiNpcButtonYesNo(13, guiLeft + 120, y += 22, data.airSpawning));
|
||||
addLabel(new GuiNpcLabel(13, "spawning.airSpawning", guiLeft + 4, y + 5));
|
||||
sw.addButton(new GuiNpcButtonYesNo(11, controlX, y, data.monsterSpawning));
|
||||
sw.addLabel(new GuiNpcLabel(11, "spawning.monsterSpawning", labelX, y + 5));
|
||||
sw.getButton(11).setHoverText("tooltip.naturalspawns.options.monster");
|
||||
y += 22;
|
||||
|
||||
addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMin));
|
||||
addLabel(new GuiNpcLabel(14, "spawning.minHeight", guiLeft + 4, y + 5));
|
||||
addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMax));
|
||||
addLabel(new GuiNpcLabel(15, "spawning.maxHeight", guiLeft + 4, y + 5));
|
||||
getTextField(14).integersOnly = true;
|
||||
getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0);
|
||||
getTextField(15).integersOnly = true;
|
||||
getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0);
|
||||
sw.addButton(new GuiNpcButtonYesNo(12, controlX, y, data.liquidSpawning));
|
||||
sw.addLabel(new GuiNpcLabel(12, "spawning.liquidSpawning", labelX, y + 5));
|
||||
sw.getButton(12).setHoverText("tooltip.naturalspawns.options.liquid");
|
||||
y += 22;
|
||||
|
||||
addButton(new GuiNpcButton(100, guiLeft + 154, guiTop + 192, 60, 20, "gui.done"));
|
||||
sw.addButton(new GuiNpcButtonYesNo(13, controlX, y, data.airSpawning));
|
||||
sw.addLabel(new GuiNpcLabel(13, "spawning.airSpawning", labelX, y + 5));
|
||||
sw.getButton(13).setHoverText("tooltip.naturalspawns.options.air");
|
||||
y += 22;
|
||||
|
||||
sw.addTextField(new GuiNpcTextField(14, this, controlX, y, 60, 20, "" + data.spawnHeightMin));
|
||||
sw.addLabel(new GuiNpcLabel(14, "spawning.minHeight", labelX, y + 5));
|
||||
sw.getTextField(14).integersOnly = true;
|
||||
sw.getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0);
|
||||
sw.getTextField(14).setHoverText("tooltip.naturalspawns.options.minHeight");
|
||||
y += 22;
|
||||
|
||||
sw.addTextField(new GuiNpcTextField(15, this, controlX, y, 60, 20, "" + data.spawnHeightMax));
|
||||
sw.addLabel(new GuiNpcLabel(15, "spawning.maxHeight", labelX, y + 5));
|
||||
sw.getTextField(15).integersOnly = true;
|
||||
sw.getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0);
|
||||
sw.getTextField(15).setHoverText("tooltip.naturalspawns.options.maxHeight");
|
||||
y += 22;
|
||||
|
||||
sw.addTextField(new GuiNpcTextField(16, this, controlX, y, 60, 20, "" + data.maxAlive));
|
||||
sw.addLabel(new GuiNpcLabel(16, "spawning.maxAlive", labelX, y + 5));
|
||||
sw.getTextField(16).integersOnly = true;
|
||||
sw.getTextField(16).setMinMaxDefault(0, Integer.MAX_VALUE, 0);
|
||||
sw.getTextField(16).setHoverText("tooltip.naturalspawns.options.maxAlive");
|
||||
y += 22;
|
||||
|
||||
sw.addTextField(new GuiNpcTextField(17, this, controlX, y, 60, 20, "" + data.cooldownTicks));
|
||||
sw.addLabel(new GuiNpcLabel(17, "spawning.cooldownTicks", labelX, y + 5));
|
||||
sw.getTextField(17).integersOnly = true;
|
||||
sw.getTextField(17).setMinMaxDefault(0, Integer.MAX_VALUE, 0);
|
||||
sw.getTextField(17).setHoverText("tooltip.naturalspawns.options.cooldownTicks");
|
||||
y += 22;
|
||||
|
||||
sw.addTextField(new GuiNpcTextField(18, this, controlX, y, 60, 20, "" + data.attemptsPerCycle));
|
||||
sw.addLabel(new GuiNpcLabel(18, "spawning.attemptsPerCycle", labelX, y + 5));
|
||||
sw.getTextField(18).integersOnly = true;
|
||||
sw.getTextField(18).setMinMaxDefault(1, Integer.MAX_VALUE, 1);
|
||||
sw.getTextField(18).setHoverText("tooltip.naturalspawns.options.attemptsPerCycle");
|
||||
y += 22;
|
||||
|
||||
sw.addTextField(new GuiNpcTextField(19, this, controlX, y, 60, 20, "" + data.playerMinDistance));
|
||||
sw.addLabel(new GuiNpcLabel(19, "spawning.playerMinDistance", labelX, y + 5));
|
||||
sw.getTextField(19).integersOnly = true;
|
||||
sw.getTextField(19).setMinMaxDefault(0, Integer.MAX_VALUE, 24);
|
||||
sw.getTextField(19).setHoverText("tooltip.naturalspawns.options.playerMinDistance");
|
||||
y += 22;
|
||||
|
||||
sw.addButton(new GuiNpcButton(20, controlX, y, 100, 20,
|
||||
new String[]{"spawning.despawn.forceNatural", "spawning.despawn.preserve", "spawning.despawn.forcePersistent"}, data.despawnMode));
|
||||
sw.addLabel(new GuiNpcLabel(20, "spawning.despawnMode", labelX, y + 5));
|
||||
sw.getButton(20).setHoverText("tooltip.naturalspawns.options.despawnMode");
|
||||
y += 22;
|
||||
|
||||
sw.maxScrollY = Math.max(0, y - sw.clipHeight + 8);
|
||||
|
||||
addButton(new GuiNpcButton(100, guiLeft + xSize - 64, guiTop + ySize - 24, 60, 20, "gui.done"));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,6 +125,9 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield
|
||||
if (button.id == 13) {
|
||||
data.airSpawning = button.getValue() == 1;
|
||||
}
|
||||
if (button.id == 20) {
|
||||
data.setDespawnMode(button.getValue());
|
||||
}
|
||||
if (button.id == 100) {
|
||||
close();
|
||||
}
|
||||
@@ -77,5 +141,17 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield
|
||||
if (textfield.id == 15) {
|
||||
data.spawnHeightMax = textfield.getInteger();
|
||||
}
|
||||
if (textfield.id == 16) {
|
||||
data.setMaxAlive(textfield.getInteger());
|
||||
}
|
||||
if (textfield.id == 17) {
|
||||
data.setCooldownTicks(textfield.getInteger());
|
||||
}
|
||||
if (textfield.id == 18) {
|
||||
data.setAttemptsPerCycle(textfield.getInteger());
|
||||
}
|
||||
if (textfield.id == 19) {
|
||||
data.setPlayerMinDistance(textfield.getInteger());
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -62,6 +62,12 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I
|
||||
|
||||
this.addButton(new GuiNpcButton(1, guiLeft + 358, guiTop + 38, 58, 20, "gui.add"));
|
||||
this.addButton(new GuiNpcButton(2, guiLeft + 358, guiTop + 61, 58, 20, "gui.remove"));
|
||||
getButton(1).setHoverText("tooltip.naturalspawns.main.addProfile");
|
||||
getButton(2).setHoverText("tooltip.naturalspawns.main.removeProfile");
|
||||
|
||||
if (getTextField(55) != null) {
|
||||
getTextField(55).setHoverText("tooltip.naturalspawns.main.search");
|
||||
}
|
||||
|
||||
if (this.spawn.id >= 0)
|
||||
showSpawn();
|
||||
@@ -74,15 +80,18 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I
|
||||
int y = guiTop + 30;
|
||||
addLabel(new GuiNpcLabel(3, "spawning.biomes", guiLeft + 4, y));
|
||||
addButton(new GuiNpcButton(3, guiLeft + 40, y - 5, 50, 20, "selectServer.edit"));
|
||||
getButton(3).setHoverText("tooltip.naturalspawns.main.biomes");
|
||||
|
||||
addLabel(new GuiNpcLabel(11, StatCollector.translateToLocal("Dimensions"), guiLeft + 95, y));
|
||||
addButton(new GuiNpcButton(11, guiLeft + 150, y - 5, 50, 20, "selectServer.edit"));
|
||||
getButton(11).setHoverText("tooltip.naturalspawns.main.dimensions");
|
||||
|
||||
addLabel(new GuiNpcLabel(4, "spawning.options", guiLeft + 4, y += 22));
|
||||
addButton(new GuiNpcButton(4, guiLeft + 90, y - 5, 50, 20, "selectServer.edit"));
|
||||
getButton(4).setHoverText("tooltip.naturalspawns.main.options");
|
||||
|
||||
addSlider(new GuiNpcSlider(this, 5, guiLeft + 4, y += 17, 180, 20, (float) this.spawn.itemWeight / 100));
|
||||
addExtra(new GuiHoverText(1, "spawning.naturalInfo", guiLeft + 188, y + 5));
|
||||
addExtra(new GuiHoverText(1, "spawning.naturalInfoPerformance", guiLeft + 188, y + 5));
|
||||
|
||||
this.spawnEntryScroll.guiLeft = guiLeft + 4;
|
||||
this.spawnEntryScroll.guiTop = y + 40;
|
||||
@@ -96,13 +105,17 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I
|
||||
addScroll(this.spawnEntryScroll);
|
||||
|
||||
addButton(new GuiNpcButton(21, guiLeft + 6, y += 20, 20, 20, "+"));
|
||||
getButton(21).setHoverText("tooltip.naturalspawns.main.addEntry");
|
||||
if (this.spawnEntryScroll.hasSelected()) {
|
||||
int selected = Integer.parseInt(this.spawnEntryScroll.getSelected());
|
||||
addButton(new GuiNpcButton(22, guiLeft + 32, y, 20, 20, "-"));
|
||||
getButton(22).setHoverText("tooltip.naturalspawns.main.removeEntry");
|
||||
GuiNpcTextField num = new GuiNpcTextField(25, this, guiLeft + 60, y += 10, 30, 20, "" + selected);
|
||||
num.integersOnly = true;
|
||||
addTextField(num);
|
||||
getTextField(25).setHoverText("tooltip.naturalspawns.main.slot");
|
||||
addButton(new GuiNpcButton(26, guiLeft + 92, y, 100, 20, this.getTitle(this.spawn.spawnCompounds.get(selected))));
|
||||
getButton(26).setHoverText("tooltip.naturalspawns.main.selectEntry");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ public class AbilityHotbarComponent extends HudComponent {
|
||||
|
||||
private static final int BASE_SLOT_SIZE = 24;
|
||||
private static final int BASE_SPACING = 28;
|
||||
private static final int EDIT_HANDLE_SIZE = 10;
|
||||
|
||||
private AbilityHotbarSlotRenderer[] slots = new AbilityHotbarSlotRenderer[TOTAL_SLOTS];
|
||||
|
||||
@@ -430,25 +431,18 @@ public class AbilityHotbarComponent extends HudComponent {
|
||||
int modeLabelW = fr.getStringWidth(modeLabel);
|
||||
fr.drawStringWithShadow(modeLabel, overlayWidth / 2 - modeLabelW / 2, overlayHeight + 2, 0xAA888888);
|
||||
|
||||
GL11.glDisable(GL11.GL_TEXTURE_2D);
|
||||
GL11.glEnable(GL11.GL_BLEND);
|
||||
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
GL11.glColor4f(0.2f, 0.8f, 0.2f, 0.8f);
|
||||
GL11.glLineWidth(1.0f);
|
||||
GL11.glBegin(GL11.GL_LINE_LOOP);
|
||||
GL11.glVertex2f(0, 0);
|
||||
GL11.glVertex2f(overlayWidth, 0);
|
||||
GL11.glVertex2f(overlayWidth, overlayHeight);
|
||||
GL11.glVertex2f(0, overlayHeight);
|
||||
GL11.glEnd();
|
||||
net.minecraft.client.gui.Gui.drawRect(0, 0, overlayWidth, overlayHeight, 0x22000000);
|
||||
drawRectOutline(0, 0, overlayWidth, overlayHeight, 0xCC66CC66);
|
||||
|
||||
GL11.glColor4f(0.8f, 0.8f, 0.8f, 0.9f);
|
||||
GL11.glBegin(GL11.GL_QUADS);
|
||||
GL11.glVertex2f(overlayWidth - 10, overlayHeight - 10);
|
||||
GL11.glVertex2f(overlayWidth, overlayHeight - 10);
|
||||
GL11.glVertex2f(overlayWidth, overlayHeight);
|
||||
GL11.glVertex2f(overlayWidth - 10, overlayHeight);
|
||||
GL11.glEnd();
|
||||
int handleLeft = overlayWidth - EDIT_HANDLE_SIZE;
|
||||
int handleTop = overlayHeight - EDIT_HANDLE_SIZE;
|
||||
net.minecraft.client.gui.Gui.drawRect(handleLeft, handleTop, overlayWidth, overlayHeight, 0xFFE0E0E0);
|
||||
drawRectOutline(handleLeft, handleTop, overlayWidth, overlayHeight, 0xFF5A5A5A);
|
||||
net.minecraft.client.gui.Gui.drawRect(handleLeft + 2, handleTop + 7, handleLeft + 8, handleTop + 8, 0xAA666666);
|
||||
net.minecraft.client.gui.Gui.drawRect(handleLeft + 4, handleTop + 5, handleLeft + 8, handleTop + 6, 0xAA666666);
|
||||
net.minecraft.client.gui.Gui.drawRect(handleLeft + 6, handleTop + 3, handleLeft + 8, handleTop + 4, 0xAA666666);
|
||||
|
||||
GL11.glDisable(GL11.GL_BLEND);
|
||||
GL11.glEnable(GL11.GL_TEXTURE_2D);
|
||||
@@ -610,7 +604,20 @@ public class AbilityHotbarComponent extends HudComponent {
|
||||
private boolean isHudKeyHeld() {
|
||||
if (ClientProxy.AbilityHudKey == null) return false;
|
||||
int keyCode = ClientProxy.AbilityHudKey.getKeyCode();
|
||||
return keyCode != 0 && org.lwjgl.input.Keyboard.isKeyDown(keyCode);
|
||||
if (keyCode == 0 || keyCode == -1) return false;
|
||||
|
||||
// Mouse bindings are encoded as negative key codes (offset by 100).
|
||||
if (keyCode < -1) {
|
||||
int mouseButton = keyCode + 100;
|
||||
return mouseButton >= 0
|
||||
&& mouseButton < org.lwjgl.input.Mouse.getButtonCount()
|
||||
&& org.lwjgl.input.Mouse.isButtonDown(mouseButton);
|
||||
}
|
||||
|
||||
// Guard against invalid/out-of-range keyboard codes to avoid Buffer.checkIndex crashes.
|
||||
return keyCode > 0
|
||||
&& keyCode < org.lwjgl.input.Keyboard.getKeyCount()
|
||||
&& org.lwjgl.input.Keyboard.isKeyDown(keyCode);
|
||||
}
|
||||
|
||||
public boolean hasAnyAbilities() {
|
||||
@@ -694,4 +701,11 @@ public class AbilityHotbarComponent extends HudComponent {
|
||||
updateAbilities();
|
||||
lastUpdateTime = Minecraft.getSystemTime();
|
||||
}
|
||||
|
||||
private void drawRectOutline(int left, int top, int right, int bottom, int color) {
|
||||
net.minecraft.client.gui.Gui.drawRect(left, top, right, top + 1, color);
|
||||
net.minecraft.client.gui.Gui.drawRect(left, bottom - 1, right, bottom, color);
|
||||
net.minecraft.client.gui.Gui.drawRect(left, top, left + 1, bottom, color);
|
||||
net.minecraft.client.gui.Gui.drawRect(right - 1, top, right, bottom, color);
|
||||
}
|
||||
}
|
||||
@@ -247,7 +247,7 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 {
|
||||
NoppesUtil.requestOpenGUI(EnumGuiType.ManageAbilities);
|
||||
}
|
||||
if (id == 22) {
|
||||
// NoppesUtil.requestOpenGUI(EnumGuiType.ManageAuction);
|
||||
NoppesUtil.requestOpenGUI(EnumGuiType.ManageAuction);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ import net.minecraft.util.EnumChatFormatting;
|
||||
import net.minecraft.util.StatCollector;
|
||||
import net.minecraftforge.event.entity.player.ItemTooltipEvent;
|
||||
import noppes.npcs.client.AuctionClientConfig;
|
||||
import noppes.npcs.client.gui.global.GuiNpcManageAuction;
|
||||
import noppes.npcs.constants.EnumClaimType;
|
||||
import noppes.npcs.containers.ContainerAuctionListing;
|
||||
import noppes.npcs.containers.ContainerAuctionSell;
|
||||
import noppes.npcs.containers.ContainerAuctionTrades;
|
||||
import noppes.npcs.containers.ContainerManageAuction;
|
||||
import noppes.npcs.containers.SlotAuctionDisplay;
|
||||
import noppes.npcs.controllers.data.AuctionClaim;
|
||||
import noppes.npcs.controllers.data.AuctionListing;
|
||||
@@ -49,6 +51,8 @@ public class AuctionTooltipHandler {
|
||||
addTradesTooltip((GuiAuctionTrades) screen, event.itemStack, event.toolTip);
|
||||
} else if (screen instanceof GuiAuctionSell) {
|
||||
addSellTooltip((GuiAuctionSell) screen, event.itemStack, event.toolTip);
|
||||
} else if (screen instanceof GuiNpcManageAuction) {
|
||||
addManageAuctionTooltip((GuiNpcManageAuction) screen, event.itemStack, event.toolTip);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -255,4 +259,125 @@ public class AuctionTooltipHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Global Manage Auction ==========
|
||||
|
||||
private static void addManageAuctionTooltip(GuiNpcManageAuction gui, ItemStack stack, List<String> tooltip) {
|
||||
Slot slot = findHoveredSlot(gui, stack);
|
||||
if (slot == null) return;
|
||||
|
||||
ContainerManageAuction container = gui.getManageContainer();
|
||||
if (container == null) return;
|
||||
|
||||
GuiNpcManageAuction.Tab tab = gui.getActiveTab();
|
||||
|
||||
if (tab == GuiNpcManageAuction.Tab.LISTINGS && container.isDisplaySlot(slot.slotNumber)) {
|
||||
int displayIndex = container.toDisplayIndex(slot.slotNumber);
|
||||
AuctionListing listing = container.getListingAtDisplay(displayIndex);
|
||||
if (listing == null) return;
|
||||
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.seller")
|
||||
.replace("%s", EnumChatFormatting.WHITE + listing.sellerName));
|
||||
|
||||
if (listing.hasBids()) {
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid")
|
||||
.replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.currentBid)));
|
||||
tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount));
|
||||
} else {
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.startingPrice")
|
||||
.replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.startingPrice)));
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids"));
|
||||
}
|
||||
|
||||
if (listing.hasBuyout()) {
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.buyout")
|
||||
.replace("%s", EnumChatFormatting.GREEN + AuctionFormatUtil.formatCurrencyWithName(listing.buyoutPrice)));
|
||||
}
|
||||
|
||||
long timeRemaining = listing.getTimeRemaining();
|
||||
String timeText = StatCollector.translateToLocal("auction.timeLeft")
|
||||
.replace("%s", AuctionFormatUtil.formatTimeRemaining(timeRemaining));
|
||||
tooltip.add((AuctionFormatUtil.isTimeUrgent(timeRemaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText);
|
||||
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.AQUA + "Left-click: View details");
|
||||
tooltip.add(EnumChatFormatting.GOLD + "Right-click: Stop player");
|
||||
tooltip.add(EnumChatFormatting.LIGHT_PURPLE + "Shift+Right-click: Cancel to global");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab == GuiNpcManageAuction.Tab.CLAIMS && container.isDisplaySlot(slot.slotNumber)) {
|
||||
int displayIndex = container.toDisplayIndex(slot.slotNumber);
|
||||
AuctionClaim claim = container.getClaimAtDisplay(displayIndex);
|
||||
if (claim != null) {
|
||||
addGlobalClaimTooltip(claim, tooltip);
|
||||
return;
|
||||
}
|
||||
|
||||
AuctionListing listing = container.getListingAtDisplay(displayIndex);
|
||||
if (listing != null) {
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.seller")
|
||||
.replace("%s", EnumChatFormatting.WHITE + listing.sellerName));
|
||||
if (listing.hasBids()) {
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid")
|
||||
.replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.currentBid)));
|
||||
} else {
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.startingPrice")
|
||||
.replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(listing.startingPrice)));
|
||||
}
|
||||
if (listing.hasBuyout()) {
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.buyout")
|
||||
.replace("%s", EnumChatFormatting.GREEN + AuctionFormatUtil.formatCurrencyWithName(listing.buyoutPrice)));
|
||||
}
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GOLD + "Right-click: Stop player");
|
||||
tooltip.add(EnumChatFormatting.LIGHT_PURPLE + "Shift+Right-click: Cancel to global");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab == GuiNpcManageAuction.Tab.CREATE) {
|
||||
if (container.isCreateSlot(slot.slotNumber)) {
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.leftClear"));
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.rightRemove"));
|
||||
} else if (container.isPlayerSlot(slot.slotNumber)) {
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd"));
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addGlobalClaimTooltip(AuctionClaim claim, List<String> tooltip) {
|
||||
tooltip.add("");
|
||||
if (claim.type == EnumClaimType.ITEM) {
|
||||
tooltip.add(EnumChatFormatting.AQUA + "Global Item Claim");
|
||||
} else if (claim.type == EnumClaimType.CURRENCY) {
|
||||
tooltip.add(EnumChatFormatting.GOLD + "Global Currency Claim");
|
||||
if (claim.itemName != null && !claim.itemName.isEmpty()) {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.soldItem")
|
||||
.replace("%s", EnumChatFormatting.WHITE + claim.itemName));
|
||||
}
|
||||
if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.buyer")
|
||||
.replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName));
|
||||
}
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount")
|
||||
.replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(claim.currency)));
|
||||
tooltip.add(EnumChatFormatting.GRAY + "Claiming this does not change your balance.");
|
||||
} else if (claim.type == EnumClaimType.REFUND) {
|
||||
tooltip.add(EnumChatFormatting.RED + "Global Refund Claim");
|
||||
if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) {
|
||||
tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.outbidBy")
|
||||
.replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName));
|
||||
}
|
||||
tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount")
|
||||
.replace("%s", EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(claim.currency)));
|
||||
}
|
||||
tooltip.add("");
|
||||
tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -416,6 +416,12 @@ public abstract class GuiNPCInterface extends GuiScreen {
|
||||
tf.drawHover(i, j, subGui);
|
||||
}
|
||||
|
||||
for(GuiScrollWindow guiScrollableComponent : scrollWindows.values()) {
|
||||
if (guiScrollableComponent.isMouseOver(mouseX, mouseY)) {
|
||||
guiScrollableComponent.drawHoverTexts(mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
for (GuiScreen gui : extra.values())
|
||||
gui.drawScreen(i, j, f);
|
||||
|
||||
|
||||
@@ -82,7 +82,8 @@ public class GuiNpcButton extends GuiButton {
|
||||
}
|
||||
|
||||
public void setHoverText(String text) {
|
||||
this.hoverableText = StatCollector.translateToLocal(text);
|
||||
String translated = StatCollector.translateToLocal(text);
|
||||
this.hoverableText = translated == null ? "" : translated.replace("\\n", "\n");
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
@@ -201,13 +202,30 @@ public class GuiNpcButton extends GuiButton {
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
String displayString = StatCollector.translateToLocal(hoverableText);
|
||||
GL11.glColor4f(1, 1, 1, 1);
|
||||
List<String> lines = TextSplitter.splitText(displayString, 30);
|
||||
List<String> lines = splitHoverText(displayString, 30);
|
||||
drawHoveringText(lines, i, j, mc);
|
||||
GL11.glDisable(GL11.GL_LIGHTING);
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> splitHoverText(String text, int maxLineLength) {
|
||||
List<String> lines = new ArrayList<String>();
|
||||
if (text == null || text.isEmpty())
|
||||
return lines;
|
||||
|
||||
String normalized = text.replace("\\n", "\n");
|
||||
String[] explicitLines = normalized.split("\\r?\\n", -1);
|
||||
for (String line : explicitLines) {
|
||||
if (line.isEmpty()) {
|
||||
lines.add("");
|
||||
continue;
|
||||
}
|
||||
lines.addAll(TextSplitter.splitText(line, maxLineLength));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
protected void drawHoveringText(List textLines, int x, int y, Minecraft mc) {
|
||||
if (mc.fontRenderer == null || textLines.isEmpty())
|
||||
return;
|
||||
|
||||
@@ -12,6 +12,7 @@ import net.minecraft.util.StatCollector;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GuiNpcTextField extends GuiTextField {
|
||||
@@ -304,7 +305,8 @@ public class GuiNpcTextField extends GuiTextField {
|
||||
}
|
||||
|
||||
public void setHoverText(String text) {
|
||||
this.hoverableText = StatCollector.translateToLocal(text);
|
||||
String translated = StatCollector.translateToLocal(text);
|
||||
this.hoverableText = translated == null ? "" : translated.replace("\\n", "\n");
|
||||
}
|
||||
|
||||
public boolean hasHoverText() {
|
||||
@@ -317,10 +319,14 @@ public class GuiNpcTextField extends GuiTextField {
|
||||
}
|
||||
|
||||
public void drawHover(int mouseX, int mouseY, boolean hasSubGui) {
|
||||
drawHover(mouseX, mouseY, mouseX, mouseY, hasSubGui);
|
||||
}
|
||||
|
||||
public void drawHover(int hitMouseX, int hitMouseY, int tooltipMouseX, int tooltipMouseY, boolean hasSubGui) {
|
||||
if (hasSubGui || !enabled || hoverableText.isEmpty())
|
||||
return;
|
||||
|
||||
boolean isHovered = isMouseOver(mouseX, mouseY);
|
||||
boolean isHovered = isMouseOver(hitMouseX, hitMouseY);
|
||||
if (!isHovered) {
|
||||
wasHovered = false;
|
||||
hoverCount = 0;
|
||||
@@ -338,13 +344,30 @@ public class GuiNpcTextField extends GuiTextField {
|
||||
GL11.glPushMatrix();
|
||||
Minecraft mc = Minecraft.getMinecraft();
|
||||
GL11.glColor4f(1, 1, 1, 1);
|
||||
List<String> lines = TextSplitter.splitText(hoverableText, 30);
|
||||
drawHoveringText(lines, mouseX, mouseY, mc);
|
||||
List<String> lines = splitHoverText(hoverableText, 30);
|
||||
drawHoveringText(lines, tooltipMouseX, tooltipMouseY, mc);
|
||||
GL11.glDisable(GL11.GL_LIGHTING);
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> splitHoverText(String text, int maxLineLength) {
|
||||
List<String> lines = new ArrayList<String>();
|
||||
if (text == null || text.isEmpty())
|
||||
return lines;
|
||||
|
||||
String normalized = text.replace("\\n", "\n");
|
||||
String[] explicitLines = normalized.split("\\r?\\n", -1);
|
||||
for (String line : explicitLines) {
|
||||
if (line.isEmpty()) {
|
||||
lines.add("");
|
||||
continue;
|
||||
}
|
||||
lines.addAll(TextSplitter.splitText(line, maxLineLength));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
protected void drawHoveringText(List<String> textLines, int x, int y, Minecraft mc) {
|
||||
if (mc.fontRenderer == null || textLines.isEmpty())
|
||||
return;
|
||||
|
||||
@@ -251,12 +251,6 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC
|
||||
}
|
||||
|
||||
GL11.glPopMatrix();
|
||||
|
||||
// Draw hover texts in screen space (after scissor/translation are undone)
|
||||
// so tooltips are not clipped and are positioned correctly
|
||||
if (isMouseOver(mouseX, mouseY)) {
|
||||
drawHoverTexts(mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
public void drawScreen(int mouseX, int mouseY, float partialTicks, int mouseScroll) {
|
||||
@@ -293,16 +287,21 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC
|
||||
* Draws hover texts for buttons in screen space (called after GL scissor/translate are undone).
|
||||
* Hover state was already computed during drawComponents with scroll-local coords.
|
||||
*/
|
||||
private void drawHoverTexts(int mouseX, int mouseY) {
|
||||
public void drawHoverTexts(int mouseX, int mouseY) {
|
||||
boolean subGui = parent.hasSubGui();
|
||||
for (GuiNpcButton button : buttons.values()) {
|
||||
if (!button.hoverableText.isEmpty()) {
|
||||
button.drawHover(mouseX, mouseY, subGui);
|
||||
}
|
||||
}
|
||||
// Label hover: convert screen mouse to scroll-local coords for hit testing
|
||||
int localMouseX = mouseX - xPos;
|
||||
int localMouseY = (int) (mouseY - yPos + scrollY);
|
||||
for (GuiNpcTextField textField : textfields.values()) {
|
||||
if (textField.hasHoverText()) {
|
||||
textField.drawHover(localMouseX, localMouseY, mouseX, mouseY, subGui);
|
||||
}
|
||||
}
|
||||
// Label hover: convert screen mouse to scroll-local coords for hit testing
|
||||
for (GuiNpcLabel label : labels.values()) {
|
||||
if (!label.hoverableText.isEmpty()) {
|
||||
label.drawHover(localMouseX, localMouseY, mouseX, mouseY, subGui, fontRendererObj);
|
||||
|
||||
@@ -0,0 +1,384 @@
|
||||
package noppes.npcs.containers;
|
||||
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.inventory.IInventory;
|
||||
import net.minecraft.inventory.InventoryBasic;
|
||||
import net.minecraft.inventory.Slot;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import noppes.npcs.NoppesUtilPlayer;
|
||||
import noppes.npcs.constants.EnumClaimType;
|
||||
import noppes.npcs.controllers.data.AuctionClaim;
|
||||
import noppes.npcs.controllers.data.AuctionListing;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Global auction management container.
|
||||
* Uses a 9x5 display grid plus staged/detail slots on the right panel and player inventory below the grid.
|
||||
*/
|
||||
public class ContainerManageAuction extends ContainerNpcInterface {
|
||||
public enum DisplayMode {
|
||||
LISTINGS,
|
||||
CLAIMS
|
||||
}
|
||||
|
||||
public static final int DISPLAY_COLS = 9;
|
||||
public static final int DISPLAY_ROWS = 5;
|
||||
public static final int DISPLAY_SLOT_COUNT = DISPLAY_COLS * DISPLAY_ROWS;
|
||||
|
||||
public static final int DISPLAY_X = 8;
|
||||
public static final int DISPLAY_Y = 16;
|
||||
|
||||
public static final int PLAYER_INV_X = 8;
|
||||
public static final int PLAYER_INV_Y = 113;
|
||||
public static final int HOTBAR_Y = 171;
|
||||
public static final int PLAYER_SLOT_COUNT = 36;
|
||||
|
||||
public static final int CREATE_SLOT_X = 188;
|
||||
public static final int CREATE_SLOT_Y = 156;
|
||||
public static final int DETAIL_SLOT_X = 194;
|
||||
public static final int DETAIL_SLOT_Y = 56;
|
||||
private static final int MAX_STACK = 64;
|
||||
|
||||
public static final int DISPLAY_SLOT_START = 0;
|
||||
public static final int PLAYER_SLOT_START = DISPLAY_SLOT_START + DISPLAY_SLOT_COUNT;
|
||||
public static final int DETAIL_SLOT_INDEX = PLAYER_SLOT_START + PLAYER_SLOT_COUNT;
|
||||
public static final int CREATE_SLOT_INDEX = DETAIL_SLOT_INDEX + 1;
|
||||
|
||||
private static final int HIDDEN_SLOT_X = -1000;
|
||||
private static final int HIDDEN_SLOT_Y = -1000;
|
||||
|
||||
public final InventoryAuctionDisplay displayInventory;
|
||||
private final IInventory detailInventory;
|
||||
private final IInventory createInventory;
|
||||
private DisplayMode displayMode = DisplayMode.LISTINGS;
|
||||
private Slot detailSlot;
|
||||
private Slot createSlot;
|
||||
private int hiddenDisplaySlot = -1;
|
||||
|
||||
public ContainerManageAuction(EntityPlayer player) {
|
||||
super(player);
|
||||
this.player = player;
|
||||
|
||||
displayInventory = new InventoryAuctionDisplay(DISPLAY_SLOT_COUNT);
|
||||
detailInventory = new InventoryBasic("GlobalAuctionDetail", false, 1);
|
||||
createInventory = new InventoryBasic("GlobalAuctionCreate", false, 1);
|
||||
|
||||
// 9x5 display grid (listings / global claims)
|
||||
for (int row = 0; row < DISPLAY_ROWS; row++) {
|
||||
for (int col = 0; col < DISPLAY_COLS; col++) {
|
||||
int slotIndex = col + row * DISPLAY_COLS;
|
||||
int x = DISPLAY_X + col * 18;
|
||||
int y = DISPLAY_Y + row * 18;
|
||||
addSlotToContainer(new SlotAuctionDisplay(displayInventory, slotIndex, x, y));
|
||||
}
|
||||
}
|
||||
|
||||
// Player inventory below display grid
|
||||
for (int row = 0; row < 3; row++) {
|
||||
for (int col = 0; col < 9; col++) {
|
||||
int playerIndex = col + row * 9 + 9;
|
||||
int x = PLAYER_INV_X + col * 18;
|
||||
int y = PLAYER_INV_Y + row * 18;
|
||||
addSlotToContainer(new Slot(player.inventory, playerIndex, x, y));
|
||||
}
|
||||
}
|
||||
for (int col = 0; col < 9; col++) {
|
||||
int x = PLAYER_INV_X + col * 18;
|
||||
addSlotToContainer(new Slot(player.inventory, col, x, HOTBAR_Y));
|
||||
}
|
||||
|
||||
// Listing details slot (right panel)
|
||||
detailSlot = new SlotAuctionDisplay(detailInventory, 0, DETAIL_SLOT_X, DETAIL_SLOT_Y);
|
||||
addSlotToContainer(detailSlot);
|
||||
|
||||
// Staged fake listing slot
|
||||
createSlot = new Slot(createInventory, 0, CREATE_SLOT_X, CREATE_SLOT_Y) {
|
||||
@Override
|
||||
public boolean isItemValid(ItemStack stack) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canTakeStack(EntityPlayer player) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
addSlotToContainer(createSlot);
|
||||
|
||||
setDetailSlotVisible(false);
|
||||
setCreateSlotVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack transferStackInSlot(EntityPlayer player, int slotIndex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemStack slotClick(int slotIndex, int mouseButton, int mode, EntityPlayer player) {
|
||||
// Block native container interactions. GUI handles all click behaviors.
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setDisplayMode(DisplayMode mode) {
|
||||
this.displayMode = mode != null ? mode : DisplayMode.LISTINGS;
|
||||
}
|
||||
|
||||
public DisplayMode getDisplayMode() {
|
||||
return displayMode;
|
||||
}
|
||||
|
||||
public void clearDisplay() {
|
||||
displayInventory.clear();
|
||||
}
|
||||
|
||||
public void setListingsPage(List<AuctionListing> listings) {
|
||||
displayMode = DisplayMode.LISTINGS;
|
||||
clearHiddenDisplaySlot();
|
||||
displayInventory.clear();
|
||||
if (listings == null) return;
|
||||
|
||||
for (int i = 0; i < listings.size() && i < DISPLAY_SLOT_COUNT; i++) {
|
||||
displayInventory.setListing(i, listings.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
public void setClaimsPage(List<AuctionClaim> claims) {
|
||||
displayMode = DisplayMode.CLAIMS;
|
||||
clearHiddenDisplaySlot();
|
||||
displayInventory.clear();
|
||||
if (claims == null) return;
|
||||
|
||||
for (int i = 0; i < claims.size() && i < DISPLAY_SLOT_COUNT; i++) {
|
||||
displayInventory.setClaimItem(i, claims.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Claims page can contain both global claims and active global listings.
|
||||
* Order mirrors My Trades:
|
||||
* sold claims, outbid claims, won claims, active listings, expired claims.
|
||||
*/
|
||||
public void setClaimsAndListingsPage(List<AuctionClaim> claims, List<AuctionListing> listings) {
|
||||
displayMode = DisplayMode.CLAIMS;
|
||||
clearHiddenDisplaySlot();
|
||||
displayInventory.clear();
|
||||
|
||||
List<AuctionClaim> soldClaims = new ArrayList<AuctionClaim>();
|
||||
List<AuctionClaim> outbidClaims = new ArrayList<AuctionClaim>();
|
||||
List<AuctionClaim> wonClaims = new ArrayList<AuctionClaim>();
|
||||
List<AuctionClaim> expiredClaims = new ArrayList<AuctionClaim>();
|
||||
if (claims != null) {
|
||||
for (AuctionClaim claim : claims) {
|
||||
if (claim == null) continue;
|
||||
if (claim.type == EnumClaimType.CURRENCY) {
|
||||
soldClaims.add(claim);
|
||||
} else if (claim.type == EnumClaimType.REFUND) {
|
||||
outbidClaims.add(claim);
|
||||
} else if (claim.type == EnumClaimType.ITEM) {
|
||||
if (claim.isReturned) {
|
||||
expiredClaims.add(claim);
|
||||
} else {
|
||||
wonClaims.add(claim);
|
||||
}
|
||||
} else {
|
||||
wonClaims.add(claim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int slot = 0;
|
||||
slot = fillClaims(slot, soldClaims);
|
||||
slot = fillClaims(slot, outbidClaims);
|
||||
slot = fillClaims(slot, wonClaims);
|
||||
|
||||
if (listings != null) {
|
||||
for (int i = 0; i < listings.size() && slot < DISPLAY_SLOT_COUNT; i++) {
|
||||
displayInventory.setListing(slot, listings.get(i));
|
||||
slot++;
|
||||
}
|
||||
}
|
||||
|
||||
fillClaims(slot, expiredClaims);
|
||||
}
|
||||
|
||||
private int fillClaims(int slot, List<AuctionClaim> claims) {
|
||||
if (claims == null) return slot;
|
||||
for (int i = 0; i < claims.size() && slot < DISPLAY_SLOT_COUNT; i++) {
|
||||
displayInventory.setClaimItem(slot, claims.get(i));
|
||||
slot++;
|
||||
}
|
||||
return slot;
|
||||
}
|
||||
|
||||
public boolean isDisplaySlot(int containerSlotIndex) {
|
||||
return containerSlotIndex >= DISPLAY_SLOT_START && containerSlotIndex < DISPLAY_SLOT_START + DISPLAY_SLOT_COUNT;
|
||||
}
|
||||
|
||||
public int toDisplayIndex(int containerSlotIndex) {
|
||||
if (!isDisplaySlot(containerSlotIndex)) return -1;
|
||||
return containerSlotIndex - DISPLAY_SLOT_START;
|
||||
}
|
||||
|
||||
public AuctionListing getListingAtDisplay(int displayIndex) {
|
||||
if (displayIndex < 0 || displayIndex >= DISPLAY_SLOT_COUNT) return null;
|
||||
return displayInventory.getListing(displayIndex);
|
||||
}
|
||||
|
||||
public AuctionClaim getClaimAtDisplay(int displayIndex) {
|
||||
if (displayIndex < 0 || displayIndex >= DISPLAY_SLOT_COUNT) return null;
|
||||
return displayInventory.getClaim(displayIndex);
|
||||
}
|
||||
|
||||
public boolean isPlayerSlot(int containerSlotIndex) {
|
||||
return containerSlotIndex >= PLAYER_SLOT_START && containerSlotIndex < PLAYER_SLOT_START + PLAYER_SLOT_COUNT;
|
||||
}
|
||||
|
||||
public boolean isCreateSlot(int containerSlotIndex) {
|
||||
return containerSlotIndex == CREATE_SLOT_INDEX;
|
||||
}
|
||||
|
||||
public boolean isDetailSlot(int containerSlotIndex) {
|
||||
return containerSlotIndex == DETAIL_SLOT_INDEX;
|
||||
}
|
||||
|
||||
public void setDetailItem(ItemStack stack) {
|
||||
detailInventory.setInventorySlotContents(0, stack != null ? stack.copy() : null);
|
||||
}
|
||||
|
||||
public void clearDetailItem() {
|
||||
detailInventory.setInventorySlotContents(0, null);
|
||||
}
|
||||
|
||||
public void setDetailSlotVisible(boolean visible) {
|
||||
if (detailSlot == null) return;
|
||||
detailSlot.xDisplayPosition = visible ? DETAIL_SLOT_X : HIDDEN_SLOT_X;
|
||||
detailSlot.yDisplayPosition = visible ? DETAIL_SLOT_Y : HIDDEN_SLOT_Y;
|
||||
}
|
||||
|
||||
public void setCreateSlotVisible(boolean visible) {
|
||||
if (createSlot == null) return;
|
||||
createSlot.xDisplayPosition = visible ? CREATE_SLOT_X : HIDDEN_SLOT_X;
|
||||
createSlot.yDisplayPosition = visible ? CREATE_SLOT_Y : HIDDEN_SLOT_Y;
|
||||
}
|
||||
|
||||
public void setHiddenDisplaySlot(int displayIndex) {
|
||||
if (displayIndex < 0 || displayIndex >= DISPLAY_SLOT_COUNT) {
|
||||
clearHiddenDisplaySlot();
|
||||
return;
|
||||
}
|
||||
if (hiddenDisplaySlot == displayIndex) return;
|
||||
|
||||
clearHiddenDisplaySlot();
|
||||
hiddenDisplaySlot = displayIndex;
|
||||
displayInventory.setInventorySlotContents(displayIndex, null);
|
||||
}
|
||||
|
||||
public void clearHiddenDisplaySlot() {
|
||||
if (hiddenDisplaySlot < 0 || hiddenDisplaySlot >= DISPLAY_SLOT_COUNT) {
|
||||
hiddenDisplaySlot = -1;
|
||||
return;
|
||||
}
|
||||
restoreDisplaySlot(hiddenDisplaySlot);
|
||||
hiddenDisplaySlot = -1;
|
||||
}
|
||||
|
||||
public int getHiddenDisplaySlot() {
|
||||
return hiddenDisplaySlot;
|
||||
}
|
||||
|
||||
private void restoreDisplaySlot(int displayIndex) {
|
||||
if (displayIndex < 0 || displayIndex >= DISPLAY_SLOT_COUNT) return;
|
||||
|
||||
AuctionClaim claim = displayInventory.getClaim(displayIndex);
|
||||
if (claim != null) {
|
||||
displayInventory.setClaimItem(displayIndex, claim);
|
||||
return;
|
||||
}
|
||||
|
||||
AuctionListing listing = displayInventory.getListing(displayIndex);
|
||||
if (listing != null) {
|
||||
displayInventory.setListing(displayIndex, listing);
|
||||
return;
|
||||
}
|
||||
|
||||
displayInventory.setInventorySlotContents(displayIndex, null);
|
||||
}
|
||||
|
||||
public ItemStack getCreateItem() {
|
||||
return createInventory.getStackInSlot(0);
|
||||
}
|
||||
|
||||
public void clearCreateSlot() {
|
||||
createInventory.setInventorySlotContents(0, null);
|
||||
}
|
||||
|
||||
public void addToCreateSlot(int containerPlayerSlot, boolean fullStack) {
|
||||
if (!isPlayerSlot(containerPlayerSlot)) return;
|
||||
|
||||
Slot slot = (Slot) inventorySlots.get(containerPlayerSlot);
|
||||
if (slot == null) return;
|
||||
ItemStack source = slot.getStack();
|
||||
if (source == null) return;
|
||||
|
||||
ItemStack staged = createInventory.getStackInSlot(0);
|
||||
int toAdd = fullStack ? source.stackSize : 1;
|
||||
|
||||
if (staged == null) {
|
||||
ItemStack copy = source.copy();
|
||||
copy.stackSize = Math.min(MAX_STACK, toAdd);
|
||||
createInventory.setInventorySlotContents(0, copy);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!itemsMatch(staged, source)) {
|
||||
ItemStack copy = source.copy();
|
||||
copy.stackSize = Math.min(MAX_STACK, toAdd);
|
||||
createInventory.setInventorySlotContents(0, copy);
|
||||
return;
|
||||
}
|
||||
|
||||
int space = MAX_STACK - staged.stackSize;
|
||||
int add = Math.min(space, toAdd);
|
||||
if (add > 0) {
|
||||
staged.stackSize += add;
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFromCreateSlot(boolean removeAll) {
|
||||
ItemStack staged = createInventory.getStackInSlot(0);
|
||||
if (staged == null) return;
|
||||
|
||||
if (removeAll || staged.stackSize <= 1) {
|
||||
createInventory.setInventorySlotContents(0, null);
|
||||
} else {
|
||||
staged.stackSize--;
|
||||
}
|
||||
}
|
||||
|
||||
public int countItemInPlayerInventory(ItemStack target) {
|
||||
if (target == null || player == null || player.inventory == null) return 0;
|
||||
int total = 0;
|
||||
for (int i = 0; i < player.inventory.mainInventory.length; i++) {
|
||||
ItemStack stack = player.inventory.mainInventory[i];
|
||||
if (stack != null && itemsMatch(target, stack)) {
|
||||
total += stack.stackSize;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private boolean itemsMatch(ItemStack a, ItemStack b) {
|
||||
return NoppesUtilPlayer.compareItems(a, b, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContainerClosed(EntityPlayer player) {
|
||||
super.onContainerClosed(player);
|
||||
clearHiddenDisplaySlot();
|
||||
clearDetailItem();
|
||||
clearCreateSlot();
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
@@ -63,7 +64,8 @@ public class AuctionController implements IAuctionHandler {
|
||||
|
||||
// Main data stores - listings only, claims are stored in PlayerData
|
||||
private final Map<String, AuctionListing> listings = new ConcurrentHashMap<>();
|
||||
// Claims are now stored in PlayerData.tradeData, not here
|
||||
// Shared global claims for admin-managed auction actions
|
||||
private final List<AuctionClaim> globalClaims = new CopyOnWriteArrayList<>();
|
||||
|
||||
// =========================================
|
||||
// Player Indices for O(1) Lookups
|
||||
@@ -163,9 +165,12 @@ public class AuctionController implements IAuctionHandler {
|
||||
if (listing.status != EnumAuctionStatus.ACTIVE) return;
|
||||
|
||||
listing.status = EnumAuctionStatus.ENDED;
|
||||
boolean isGlobalListing = listing.isGlobalListing;
|
||||
|
||||
// Remove from seller's active listings
|
||||
removeFromPlayerListings(listing.sellerUUID, listing.id);
|
||||
if (!isGlobalListing && listing.sellerUUID != null) {
|
||||
removeFromPlayerListings(listing.sellerUUID, listing.id);
|
||||
}
|
||||
|
||||
// Remove from bidder's active bids if any
|
||||
if (listing.highBidderUUID != null) {
|
||||
@@ -188,26 +193,38 @@ public class AuctionController implements IAuctionHandler {
|
||||
AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim(
|
||||
listing.sellerUUID, listing.sellerName, listing.id, sellerReceives,
|
||||
itemDisplayName, listing.highBidderName);
|
||||
addClaimToPlayer(listing.sellerUUID, currencyClaim);
|
||||
if (isGlobalListing) {
|
||||
addGlobalClaim(currencyClaim);
|
||||
} else {
|
||||
addClaimToPlayer(listing.sellerUUID, currencyClaim);
|
||||
}
|
||||
|
||||
// Notifications
|
||||
sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id,
|
||||
"You won the auction for " + itemDisplayName + "!");
|
||||
sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id,
|
||||
"Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!");
|
||||
if (!isGlobalListing && listing.sellerUUID != null) {
|
||||
sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id,
|
||||
"Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!");
|
||||
}
|
||||
|
||||
logAuction(EnumAuctionLogAction.SOLD, listing.sellerName, itemDisplayName, saleAmount,
|
||||
"Winner: " + listing.highBidderName + ", Tax: " + tax);
|
||||
"Winner: " + listing.highBidderName + ", Tax: " + tax + (isGlobalListing ? ", Global listing" : ""));
|
||||
} else {
|
||||
// No bids - return item to seller
|
||||
// No bids - return item to seller/global claims
|
||||
AuctionClaim returnClaim = AuctionClaim.createItemReturnedClaim(
|
||||
listing.sellerUUID, listing.sellerName, listing.id, listing.item);
|
||||
addClaimToPlayer(listing.sellerUUID, returnClaim);
|
||||
if (isGlobalListing) {
|
||||
addGlobalClaim(returnClaim);
|
||||
} else {
|
||||
addClaimToPlayer(listing.sellerUUID, returnClaim);
|
||||
if (listing.sellerUUID != null) {
|
||||
sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id,
|
||||
"Your auction for " + itemDisplayName + " expired with no bids.");
|
||||
}
|
||||
}
|
||||
|
||||
sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id,
|
||||
"Your auction for " + itemDisplayName + " expired with no bids.");
|
||||
|
||||
logAuction(EnumAuctionLogAction.EXPIRED, listing.sellerName, itemDisplayName, listing.startingPrice, "No bids");
|
||||
logAuction(EnumAuctionLogAction.EXPIRED, listing.sellerName, itemDisplayName, listing.startingPrice,
|
||||
"No bids" + (isGlobalListing ? ", Global listing" : ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,10 +234,12 @@ public class AuctionController implements IAuctionHandler {
|
||||
// =========================================
|
||||
|
||||
private void addToPlayerListings(UUID playerUUID, String listingId) {
|
||||
if (playerUUID == null || listingId == null || listingId.isEmpty()) return;
|
||||
playerListingIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId);
|
||||
}
|
||||
|
||||
private void removeFromPlayerListings(UUID playerUUID, String listingId) {
|
||||
if (playerUUID == null || listingId == null || listingId.isEmpty()) return;
|
||||
Set<String> ids = playerListingIds.get(playerUUID);
|
||||
if (ids != null) {
|
||||
ids.remove(listingId);
|
||||
@@ -231,10 +250,12 @@ public class AuctionController implements IAuctionHandler {
|
||||
}
|
||||
|
||||
private void addToPlayerBids(UUID playerUUID, String listingId) {
|
||||
if (playerUUID == null || listingId == null || listingId.isEmpty()) return;
|
||||
playerBidIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId);
|
||||
}
|
||||
|
||||
private void removeFromPlayerBids(UUID playerUUID, String listingId) {
|
||||
if (playerUUID == null || listingId == null || listingId.isEmpty()) return;
|
||||
Set<String> ids = playerBidIds.get(playerUUID);
|
||||
if (ids != null) {
|
||||
ids.remove(listingId);
|
||||
@@ -259,6 +280,70 @@ public class AuctionController implements IAuctionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a claim to the global admin claims pool.
|
||||
*/
|
||||
private void addGlobalClaim(AuctionClaim claim) {
|
||||
if (claim == null || claim.claimed) return;
|
||||
|
||||
// Global claims are not tied to a single player.
|
||||
claim.playerUUID = null;
|
||||
claim.playerName = "Global";
|
||||
globalClaims.add(claim);
|
||||
markDirty();
|
||||
}
|
||||
|
||||
private List<AuctionClaim> getGlobalClaimsSnapshot() {
|
||||
List<AuctionClaim> result = new ArrayList<>();
|
||||
for (AuctionClaim claim : globalClaims) {
|
||||
if (claim != null && !claim.claimed) {
|
||||
result.add(claim);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private AuctionClaim getGlobalClaimInternal(String claimId) {
|
||||
if (claimId == null || claimId.isEmpty()) return null;
|
||||
for (AuctionClaim claim : globalClaims) {
|
||||
if (claim != null && !claim.claimed && claimId.equals(claim.id)) {
|
||||
return claim;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean claimAndRemoveGlobal(String claimId) {
|
||||
AuctionClaim claim = getGlobalClaimInternal(claimId);
|
||||
if (claim == null) return false;
|
||||
claim.claimed = true;
|
||||
return globalClaims.remove(claim);
|
||||
}
|
||||
|
||||
/**
|
||||
* Full list of unclaimed global claims sorted newest-first.
|
||||
*/
|
||||
public List<AuctionClaim> getAllGlobalClaims() {
|
||||
List<AuctionClaim> snapshot = getGlobalClaimsSnapshot();
|
||||
snapshot.sort((a, b) -> Long.compare(b.createdTime, a.createdTime));
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Full list of active global listings sorted newest-first.
|
||||
*/
|
||||
public List<AuctionListing> getActiveGlobalListings() {
|
||||
List<AuctionListing> result = new ArrayList<>();
|
||||
for (AuctionListing listing : listings.values()) {
|
||||
if (listing == null) continue;
|
||||
if (!listing.isGlobalListing) continue;
|
||||
if (!listing.isActive()) continue;
|
||||
result.add(listing);
|
||||
}
|
||||
result.sort((a, b) -> Long.compare(b.createdTime, a.createdTime));
|
||||
return result;
|
||||
}
|
||||
|
||||
// =========================================
|
||||
// Listing Management
|
||||
// =========================================
|
||||
@@ -335,12 +420,57 @@ public class AuctionController implements IAuctionHandler {
|
||||
return null; // Success
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fake/global listing managed through the global auction admin GUI.
|
||||
* Global listings do not consume player currency and route proceeds to global claims.
|
||||
*/
|
||||
public String createGlobalListing(EntityPlayer admin, String sellerName, ItemStack item, long startingPrice, long buyoutPrice, int durationHours) {
|
||||
if (!ConfigMarket.AuctionEnabled) {
|
||||
return "Auction is disabled.";
|
||||
}
|
||||
if (admin == null) {
|
||||
return "Admin is missing.";
|
||||
}
|
||||
if (item == null) {
|
||||
return "No item provided.";
|
||||
}
|
||||
|
||||
if (startingPrice < ConfigMarket.MinimumListingPrice) {
|
||||
return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + ".";
|
||||
}
|
||||
if (buyoutPrice > 0 && buyoutPrice < startingPrice) {
|
||||
return "Buyout price must be higher than starting price.";
|
||||
}
|
||||
|
||||
String cleanSeller = sellerName != null ? sellerName.trim() : "";
|
||||
if (cleanSeller.isEmpty()) {
|
||||
cleanSeller = "Server";
|
||||
}
|
||||
|
||||
int hours = durationHours > 0 ? durationHours : ConfigMarket.AuctionDurationHours;
|
||||
long durationMs = hours * 60L * 60L * 1000L;
|
||||
|
||||
// Global listings do not belong to a player UUID, only to the global pool.
|
||||
AuctionListing listing = new AuctionListing(null, cleanSeller, item, startingPrice, buyoutPrice, durationMs);
|
||||
listing.isGlobalListing = true;
|
||||
listings.put(listing.id, listing);
|
||||
markDirty();
|
||||
|
||||
logAuction(EnumAuctionLogAction.CREATED, admin.getCommandSenderName(), item.getDisplayName(), startingPrice,
|
||||
"Global listing, Seller: " + cleanSeller + ", Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", DurationHours: " + hours);
|
||||
return null;
|
||||
}
|
||||
|
||||
public String cancelListing(String listingId, EntityPlayer player, boolean isAdmin) {
|
||||
AuctionListing listing = listings.get(listingId);
|
||||
if (listing == null) {
|
||||
return "Listing not found.";
|
||||
}
|
||||
|
||||
if (isAdmin && listing.isGlobalListing) {
|
||||
return adminStopListingToGlobal(listingId, player, true);
|
||||
}
|
||||
|
||||
synchronized (listing) {
|
||||
if (!listing.status.canCancel()) {
|
||||
return "This auction cannot be cancelled.";
|
||||
@@ -616,14 +746,22 @@ public class AuctionController implements IAuctionHandler {
|
||||
AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim(
|
||||
listing.sellerUUID, listing.sellerName, listing.id, sellerReceives,
|
||||
listing.item.getDisplayName(), playerName);
|
||||
addClaimToPlayer(listing.sellerUUID, currencyClaim);
|
||||
if (listing.isGlobalListing) {
|
||||
addGlobalClaim(currencyClaim);
|
||||
} else {
|
||||
addClaimToPlayer(listing.sellerUUID, currencyClaim);
|
||||
}
|
||||
|
||||
// Notifications
|
||||
sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id,
|
||||
"Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!");
|
||||
if (!listing.isGlobalListing && listing.sellerUUID != null) {
|
||||
sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id,
|
||||
"Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!");
|
||||
}
|
||||
|
||||
// Remove from seller's active listings
|
||||
removeFromPlayerListings(listing.sellerUUID, listing.id);
|
||||
if (!listing.isGlobalListing && listing.sellerUUID != null) {
|
||||
removeFromPlayerListings(listing.sellerUUID, listing.id);
|
||||
}
|
||||
|
||||
listing.status = EnumAuctionStatus.ENDED;
|
||||
listing.highBidderUUID = playerUUID;
|
||||
@@ -633,7 +771,7 @@ public class AuctionController implements IAuctionHandler {
|
||||
markDirty();
|
||||
|
||||
logAuction(EnumAuctionLogAction.BUYOUT, playerName, listing.item.getDisplayName(), listing.buyoutPrice,
|
||||
"Seller: " + listing.sellerName + ", Tax: " + tax);
|
||||
"Seller: " + listing.sellerName + ", Tax: " + tax + (listing.isGlobalListing ? ", Global listing" : ""));
|
||||
}
|
||||
|
||||
return null; // Success
|
||||
@@ -665,6 +803,77 @@ public class AuctionController implements IAuctionHandler {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated global claims used by the global auction admin GUI.
|
||||
*/
|
||||
public List<AuctionClaim> getGlobalClaims(int page, int pageSize) {
|
||||
List<AuctionClaim> snapshot = getGlobalClaimsSnapshot();
|
||||
snapshot.sort((a, b) -> Long.compare(b.createdTime, a.createdTime));
|
||||
|
||||
int safePage = Math.max(0, page);
|
||||
int safePageSize = Math.max(1, pageSize);
|
||||
int start = safePage * safePageSize;
|
||||
if (start >= snapshot.size()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
int end = Math.min(start + safePageSize, snapshot.size());
|
||||
return new ArrayList<>(snapshot.subList(start, end));
|
||||
}
|
||||
|
||||
public int getTotalGlobalClaims() {
|
||||
return getGlobalClaimsSnapshot().size();
|
||||
}
|
||||
|
||||
public boolean hasGlobalClaims() {
|
||||
return getTotalGlobalClaims() > 0;
|
||||
}
|
||||
|
||||
public String claimGlobalItem(String claimId, EntityPlayer player) {
|
||||
AuctionClaim claim = getGlobalClaimInternal(claimId);
|
||||
if (claim == null) {
|
||||
return "Global claim not found.";
|
||||
}
|
||||
if (!claim.type.isItem()) {
|
||||
return "This global claim is not an item claim.";
|
||||
}
|
||||
if (claim.item == null) {
|
||||
return "Item data is missing.";
|
||||
}
|
||||
|
||||
if (!player.inventory.addItemStackToInventory(claim.item.copy())) {
|
||||
return "Your inventory is full.";
|
||||
}
|
||||
|
||||
claimAndRemoveGlobal(claim.id);
|
||||
cleanupListingIfComplete(claim.listingId);
|
||||
markDirty();
|
||||
|
||||
logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), claim.item.getDisplayName(), 0,
|
||||
"Global item claim");
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Global currency claims are acknowledged/cleared and do not alter player balances.
|
||||
*/
|
||||
public String claimGlobalCurrency(String claimId, EntityPlayer player) {
|
||||
AuctionClaim claim = getGlobalClaimInternal(claimId);
|
||||
if (claim == null) {
|
||||
return "Global claim not found.";
|
||||
}
|
||||
if (!claim.type.isCurrency()) {
|
||||
return "This global claim is not a currency claim.";
|
||||
}
|
||||
|
||||
claimAndRemoveGlobal(claim.id);
|
||||
cleanupListingIfComplete(claim.listingId);
|
||||
markDirty();
|
||||
|
||||
logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency,
|
||||
"Global currency claim acknowledged (no balance change)");
|
||||
return null;
|
||||
}
|
||||
|
||||
public String claimItem(String claimId, EntityPlayer player) {
|
||||
PlayerData playerData = PlayerData.get(player);
|
||||
if (playerData == null) {
|
||||
@@ -1119,6 +1328,30 @@ public class AuctionController implements IAuctionHandler {
|
||||
|
||||
player.addChatMessage(new ChatComponentText(msg.toString()));
|
||||
}
|
||||
|
||||
// Notify admins with permission about pending global claims.
|
||||
if (CustomNpcsPermissions.hasCustomPermission(player, "customnpcs.global.auction")) {
|
||||
List<AuctionClaim> global = getGlobalClaimsSnapshot();
|
||||
if (!global.isEmpty()) {
|
||||
int itemClaims = 0;
|
||||
int currencyClaims = 0;
|
||||
for (AuctionClaim claim : global) {
|
||||
if (claim.type.isItem()) {
|
||||
itemClaims++;
|
||||
} else {
|
||||
currencyClaims++;
|
||||
}
|
||||
}
|
||||
|
||||
StringBuilder msg = new StringBuilder();
|
||||
msg.append(EnumChatFormatting.GOLD).append("[Auction] ");
|
||||
msg.append(EnumChatFormatting.AQUA).append("Global claims available: ");
|
||||
msg.append(EnumChatFormatting.GREEN).append(itemClaims).append(" item claim(s)");
|
||||
msg.append(EnumChatFormatting.GRAY).append(", ");
|
||||
msg.append(EnumChatFormatting.YELLOW).append(currencyClaims).append(" currency claim(s)");
|
||||
player.addChatMessage(new ChatComponentText(msg.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onPlayerLogout(UUID playerUUID) {
|
||||
@@ -1228,6 +1461,7 @@ public class AuctionController implements IAuctionHandler {
|
||||
|
||||
public void load() {
|
||||
listings.clear();
|
||||
globalClaims.clear();
|
||||
playerListingIds.clear();
|
||||
playerBidIds.clear();
|
||||
|
||||
@@ -1270,7 +1504,9 @@ public class AuctionController implements IAuctionHandler {
|
||||
// Index listings
|
||||
for (AuctionListing listing : listings.values()) {
|
||||
if (listing.status == EnumAuctionStatus.ACTIVE) {
|
||||
addToPlayerListings(listing.sellerUUID, listing.id);
|
||||
if (!listing.isGlobalListing && listing.sellerUUID != null) {
|
||||
addToPlayerListings(listing.sellerUUID, listing.id);
|
||||
}
|
||||
if (listing.highBidderUUID != null) {
|
||||
addToPlayerBids(listing.highBidderUUID, listing.id);
|
||||
}
|
||||
@@ -1291,22 +1527,42 @@ public class AuctionController implements IAuctionHandler {
|
||||
}
|
||||
|
||||
public NBTTagCompound writeToNBT(NBTTagCompound compound) {
|
||||
// Listings only - claims are stored in PlayerData
|
||||
// Listings + global claims (player claims are stored in PlayerData)
|
||||
NBTTagList listingsList = new NBTTagList();
|
||||
for (AuctionListing listing : listings.values()) {
|
||||
listingsList.appendTag(listing.writeToNBT(new NBTTagCompound()));
|
||||
}
|
||||
compound.setTag("Listings", listingsList);
|
||||
|
||||
NBTTagList globalClaimsList = new NBTTagList();
|
||||
for (AuctionClaim claim : globalClaims) {
|
||||
if (claim != null && !claim.claimed) {
|
||||
globalClaimsList.appendTag(claim.writeToNBT(new NBTTagCompound()));
|
||||
}
|
||||
}
|
||||
compound.setTag("GlobalClaims", globalClaimsList);
|
||||
|
||||
return compound;
|
||||
}
|
||||
|
||||
public void readFromNBT(NBTTagCompound compound) {
|
||||
globalClaims.clear();
|
||||
|
||||
NBTTagList listingsList = compound.getTagList("Listings", 10);
|
||||
for (int i = 0; i < listingsList.tagCount(); i++) {
|
||||
AuctionListing listing = AuctionListing.fromNBT(listingsList.getCompoundTagAt(i));
|
||||
listings.put(listing.id, listing);
|
||||
}
|
||||
|
||||
if (compound.hasKey("GlobalClaims")) {
|
||||
NBTTagList claimsList = compound.getTagList("GlobalClaims", 10);
|
||||
for (int i = 0; i < claimsList.tagCount(); i++) {
|
||||
AuctionClaim claim = AuctionClaim.fromNBT(claimsList.getCompoundTagAt(i));
|
||||
if (!claim.claimed) {
|
||||
globalClaims.add(claim);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================
|
||||
@@ -1473,10 +1729,77 @@ public class AuctionController implements IAuctionHandler {
|
||||
return new ArrayList<>(listings.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* Admin management flow for listings.
|
||||
* cancelCompletely=false: stop listing and return assets to the original owners' claim pools.
|
||||
* cancelCompletely=true: cancel listing and route assets to global claims.
|
||||
*/
|
||||
public String adminStopListingToGlobal(String listingId, EntityPlayer admin, boolean cancelCompletely) {
|
||||
AuctionListing listing = listings.get(listingId);
|
||||
if (listing == null) {
|
||||
return "Listing not found.";
|
||||
}
|
||||
if (admin == null) {
|
||||
return "Admin player is missing.";
|
||||
}
|
||||
|
||||
synchronized (listing) {
|
||||
if (!listing.status.canCancel()) {
|
||||
return "This auction cannot be managed.";
|
||||
}
|
||||
|
||||
String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item";
|
||||
|
||||
// Return active highest bid.
|
||||
if (listing.hasBids() && listing.highBidderUUID != null) {
|
||||
AuctionClaim refundClaim;
|
||||
if (cancelCompletely) {
|
||||
refundClaim = AuctionClaim.createRefundClaim(
|
||||
null, "Global", listing.id, listing.currentBid, itemName,
|
||||
listing.highBidderName, null);
|
||||
addGlobalClaim(refundClaim);
|
||||
} else {
|
||||
refundClaim = AuctionClaim.createRefundClaim(
|
||||
listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, itemName,
|
||||
listing.sellerName, null);
|
||||
addClaimToPlayer(listing.highBidderUUID, refundClaim);
|
||||
}
|
||||
removeFromPlayerBids(listing.highBidderUUID, listing.id);
|
||||
}
|
||||
|
||||
// Return the listed item.
|
||||
if (listing.item != null) {
|
||||
AuctionClaim itemClaim;
|
||||
if (cancelCompletely || listing.sellerUUID == null) {
|
||||
itemClaim = AuctionClaim.createItemReturnedClaim(
|
||||
null, "Global", listing.id, listing.item);
|
||||
addGlobalClaim(itemClaim);
|
||||
} else {
|
||||
itemClaim = AuctionClaim.createItemReturnedClaim(
|
||||
listing.sellerUUID, listing.sellerName, listing.id, listing.item);
|
||||
addClaimToPlayer(listing.sellerUUID, itemClaim);
|
||||
}
|
||||
}
|
||||
|
||||
if (!listing.isGlobalListing && listing.sellerUUID != null) {
|
||||
removeFromPlayerListings(listing.sellerUUID, listing.id);
|
||||
}
|
||||
|
||||
listing.status = cancelCompletely ? EnumAuctionStatus.CANCELLED : EnumAuctionStatus.ENDED;
|
||||
markDirty();
|
||||
|
||||
logAuction(EnumAuctionLogAction.CANCELLED, admin.getCommandSenderName(), itemName, listing.currentBid,
|
||||
(cancelCompletely ? "Admin cancelled to global claims" : "Admin stopped to owner claims") +
|
||||
", OriginalSeller: " + listing.sellerName);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void adminCancelListing(String listingId, EntityPlayer admin, String reason) {
|
||||
AuctionListing listing = listings.get(listingId);
|
||||
String itemName = listing != null && listing.item != null ? listing.item.getDisplayName() : "Unknown";
|
||||
cancelListing(listingId, admin, true);
|
||||
adminStopListingToGlobal(listingId, admin, true);
|
||||
if (reason != null && !reason.isEmpty()) {
|
||||
logAuction(EnumAuctionLogAction.CANCELLED, admin.getCommandSenderName(), itemName, 0, "Admin cancel - Reason: " + reason);
|
||||
}
|
||||
|
||||
@@ -223,11 +223,14 @@ public class SpawnController implements INaturalSpawnsHandler {
|
||||
}
|
||||
|
||||
public INaturalSpawn[] getSpawns(String biome) {
|
||||
return getSpawnList(biome).toArray(new INaturalSpawn[]{});
|
||||
List<SpawnData> biomeSpawns = getSpawnList(biome);
|
||||
if (biomeSpawns == null)
|
||||
return new INaturalSpawn[]{};
|
||||
return biomeSpawns.toArray(new INaturalSpawn[]{});
|
||||
}
|
||||
|
||||
public void addSpawn(INaturalSpawn spawn) {
|
||||
((SpawnData) spawn).id = 0;
|
||||
((SpawnData) spawn).id = -1;
|
||||
this.saveSpawnData((SpawnData) spawn);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ public class AuctionListing implements IAuctionListing {
|
||||
public long createdTime;
|
||||
public long endTime;
|
||||
public EnumAuctionStatus status;
|
||||
public boolean isGlobalListing;
|
||||
|
||||
public AuctionListing() {
|
||||
this.id = UUID.randomUUID().toString();
|
||||
@@ -31,6 +32,7 @@ public class AuctionListing implements IAuctionListing {
|
||||
this.bidCount = 0;
|
||||
this.currentBid = 0;
|
||||
this.createdTime = System.currentTimeMillis();
|
||||
this.isGlobalListing = false;
|
||||
}
|
||||
|
||||
public AuctionListing(UUID sellerUUID, String sellerName, ItemStack item, long startingPrice, long buyoutPrice, long durationMs) {
|
||||
@@ -228,6 +230,7 @@ public class AuctionListing implements IAuctionListing {
|
||||
compound.setLong("CreatedTime", createdTime);
|
||||
compound.setLong("EndTime", endTime);
|
||||
compound.setInteger("Status", status.ordinal());
|
||||
compound.setBoolean("IsGlobalListing", isGlobalListing);
|
||||
|
||||
return compound;
|
||||
}
|
||||
@@ -255,6 +258,7 @@ public class AuctionListing implements IAuctionListing {
|
||||
createdTime = compound.getLong("CreatedTime");
|
||||
endTime = compound.getLong("EndTime");
|
||||
status = EnumAuctionStatus.fromOrdinal(compound.getInteger("Status"));
|
||||
isGlobalListing = compound.getBoolean("IsGlobalListing");
|
||||
}
|
||||
|
||||
public static AuctionListing fromNBT(NBTTagCompound compound) {
|
||||
|
||||
@@ -21,6 +21,10 @@ import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class SpawnData extends WeightedRandom.Item implements INaturalSpawn {
|
||||
public static final int DESPAWN_FORCE_NATURAL = INaturalSpawn.DESPAWN_FORCE_NATURAL;
|
||||
public static final int DESPAWN_PRESERVE_TEMPLATE = INaturalSpawn.DESPAWN_PRESERVE_TEMPLATE;
|
||||
public static final int DESPAWN_FORCE_PERSISTENT = INaturalSpawn.DESPAWN_FORCE_PERSISTENT;
|
||||
|
||||
public List<String> biomes = new ArrayList<>();
|
||||
public HashSet<Integer> dimensions = new HashSet<>();
|
||||
public int id = -1;
|
||||
@@ -35,6 +39,11 @@ public class SpawnData extends WeightedRandom.Item implements INaturalSpawn {
|
||||
|
||||
public int spawnHeightMin;
|
||||
public int spawnHeightMax = 100;
|
||||
public int maxAlive = 0;
|
||||
public int cooldownTicks = 0;
|
||||
public int attemptsPerCycle = 1;
|
||||
public int playerMinDistance = 24;
|
||||
public int despawnMode = DESPAWN_FORCE_NATURAL;
|
||||
|
||||
public SpawnData() {
|
||||
super(10);
|
||||
@@ -74,6 +83,19 @@ public class SpawnData extends WeightedRandom.Item implements INaturalSpawn {
|
||||
} else {
|
||||
spawnHeightMax = compound.getInteger("HeightMax");
|
||||
}
|
||||
|
||||
maxAlive = compound.hasKey("MaxAlive") ? Math.max(0, compound.getInteger("MaxAlive")) : 0;
|
||||
cooldownTicks = compound.hasKey("CooldownTicks") ? Math.max(0, compound.getInteger("CooldownTicks")) : 0;
|
||||
attemptsPerCycle = compound.hasKey("AttemptsPerCycle") ? Math.max(1, compound.getInteger("AttemptsPerCycle")) : 1;
|
||||
playerMinDistance = compound.hasKey("PlayerMinDistance") ? Math.max(0, compound.getInteger("PlayerMinDistance")) : 24;
|
||||
if (compound.hasKey("DespawnMode")) {
|
||||
despawnMode = compound.getInteger("DespawnMode");
|
||||
} else {
|
||||
despawnMode = DESPAWN_FORCE_NATURAL;
|
||||
}
|
||||
if (despawnMode < DESPAWN_FORCE_NATURAL || despawnMode > DESPAWN_FORCE_PERSISTENT) {
|
||||
despawnMode = DESPAWN_FORCE_NATURAL;
|
||||
}
|
||||
}
|
||||
|
||||
public NBTTagCompound writeNBT(NBTTagCompound compound) {
|
||||
@@ -98,6 +120,11 @@ public class SpawnData extends WeightedRandom.Item implements INaturalSpawn {
|
||||
|
||||
compound.setInteger("HeightMin", spawnHeightMin);
|
||||
compound.setInteger("HeightMax", spawnHeightMax);
|
||||
compound.setInteger("MaxAlive", maxAlive);
|
||||
compound.setInteger("CooldownTicks", cooldownTicks);
|
||||
compound.setInteger("AttemptsPerCycle", attemptsPerCycle);
|
||||
compound.setInteger("PlayerMinDistance", playerMinDistance);
|
||||
compound.setInteger("DespawnMode", despawnMode);
|
||||
return compound;
|
||||
}
|
||||
|
||||
@@ -204,4 +231,46 @@ public class SpawnData extends WeightedRandom.Item implements INaturalSpawn {
|
||||
public void setBiomes(String[] biomes) {
|
||||
this.biomes = new ArrayList<>(Arrays.asList(biomes));
|
||||
}
|
||||
|
||||
public void setMaxAlive(int maxAlive) {
|
||||
this.maxAlive = Math.max(0, maxAlive);
|
||||
}
|
||||
|
||||
public int getMaxAlive() {
|
||||
return this.maxAlive;
|
||||
}
|
||||
|
||||
public void setCooldownTicks(int ticks) {
|
||||
this.cooldownTicks = Math.max(0, ticks);
|
||||
}
|
||||
|
||||
public int getCooldownTicks() {
|
||||
return this.cooldownTicks;
|
||||
}
|
||||
|
||||
public void setAttemptsPerCycle(int attempts) {
|
||||
this.attemptsPerCycle = Math.max(1, attempts);
|
||||
}
|
||||
|
||||
public int getAttemptsPerCycle() {
|
||||
return this.attemptsPerCycle;
|
||||
}
|
||||
|
||||
public void setPlayerMinDistance(int distance) {
|
||||
this.playerMinDistance = Math.max(0, distance);
|
||||
}
|
||||
|
||||
public int getPlayerMinDistance() {
|
||||
return this.playerMinDistance;
|
||||
}
|
||||
|
||||
public void setDespawnMode(int mode) {
|
||||
if (mode < DESPAWN_FORCE_NATURAL || mode > DESPAWN_FORCE_PERSISTENT)
|
||||
mode = DESPAWN_FORCE_NATURAL;
|
||||
this.despawnMode = mode;
|
||||
}
|
||||
|
||||
public int getDespawnMode() {
|
||||
return this.despawnMode;
|
||||
}
|
||||
}
|
||||
@@ -76,13 +76,13 @@ public abstract class EntityNPCFlying extends EntityNPCInterface {
|
||||
this.flyLimitAllow = true;
|
||||
setNpcFlyingState(true);
|
||||
|
||||
double d3 = this.motionY;
|
||||
super.moveEntityWithHeading(p_70612_1_, p_70612_2_);
|
||||
this.motionY = d3;
|
||||
|
||||
double desiredMotionY = this.motionY;
|
||||
if (this.getNavigator().noPath()) {
|
||||
this.motionY = -Math.abs(this.ais.flyGravity);
|
||||
desiredMotionY = -Math.abs(this.ais.flyGravity);
|
||||
}
|
||||
this.motionY = desiredMotionY;
|
||||
super.moveEntityWithHeading(p_70612_1_, p_70612_2_);
|
||||
this.motionY = desiredMotionY;
|
||||
|
||||
this.updateLimbSwing();
|
||||
}
|
||||
@@ -91,11 +91,13 @@ public abstract class EntityNPCFlying extends EntityNPCInterface {
|
||||
this.prevLimbSwingAmount = this.limbSwingAmount;
|
||||
double distanceX = this.posX - this.prevPosX;
|
||||
double distanceZ = this.posZ - this.prevPosZ;
|
||||
float distance = MathHelper.sqrt_double(distanceX * distanceX + distanceZ * distanceZ) * 4.0F;
|
||||
float distance = MathHelper.sqrt_double(distanceX * distanceX + distanceZ * distanceZ);
|
||||
float swingScale = this.getNavigator().noPath() ? 0.35F : 0.9F;
|
||||
distance *= swingScale;
|
||||
if (distance > 1.0F) {
|
||||
distance = 1.0F;
|
||||
}
|
||||
this.limbSwingAmount += (distance - this.limbSwingAmount) * 0.4F;
|
||||
this.limbSwingAmount += (distance - this.limbSwingAmount) * 0.18F;
|
||||
this.limbSwing += this.limbSwingAmount;
|
||||
}
|
||||
|
||||
@@ -140,13 +142,13 @@ public abstract class EntityNPCFlying extends EntityNPCInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
double d3 = this.motionY;
|
||||
super.performMountedMovement(strafe, forward, moveSpeed);
|
||||
this.motionY = d3;
|
||||
|
||||
double desiredMotionY = this.motionY;
|
||||
if (this.getNavigator().noPath()) {
|
||||
this.motionY = -Math.abs(this.ais.flyGravity);
|
||||
desiredMotionY = -Math.abs(this.ais.flyGravity);
|
||||
}
|
||||
this.motionY = desiredMotionY;
|
||||
super.performMountedMovement(strafe, forward, moveSpeed);
|
||||
this.motionY = desiredMotionY;
|
||||
|
||||
this.updateLimbSwing();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import kamkeel.npcs.network.PacketHandler;
|
||||
import kamkeel.npcs.network.enums.EnumSoundOperation;
|
||||
import kamkeel.npcs.network.packets.data.ChatBubblePacket;
|
||||
import kamkeel.npcs.network.packets.data.QuestCompletionPacket;
|
||||
import kamkeel.npcs.network.packets.data.RequestProperSpawnData;
|
||||
import kamkeel.npcs.network.packets.data.SoundManagementPacket;
|
||||
import kamkeel.npcs.network.packets.data.npc.UpdateNpcPacket;
|
||||
import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket;
|
||||
@@ -230,8 +231,8 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
public FlyingMoveHelper flyMoveHelper = new FlyingMoveHelper(this);
|
||||
public PathNavigate flyNavigator = new PathNavigateFlying(this, worldObj);
|
||||
|
||||
// @SideOnly(Side.CLIENT)
|
||||
// public int immediateSpawnDataFixAttempts = 0;
|
||||
// For Client-Side Use Only
|
||||
public int clientFixAttempts = 0;
|
||||
|
||||
public EntityNPCInterface(World world) {
|
||||
super(world);
|
||||
@@ -254,7 +255,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
setFaction(faction.id);
|
||||
setSize(1, 1);
|
||||
this.updateTasks();
|
||||
this.func_110163_bv();
|
||||
this.syncDespawnPersistence();
|
||||
|
||||
if (!this.isRemote() && this.wrappedNPC == null) {
|
||||
this.wrappedNPC = new ScriptNpc<>(this);
|
||||
@@ -1202,7 +1203,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
this.getEntityAttribute(SharedMonsterAttributes.followRange).setBaseValue(ConfigMain.NpcNavRange);
|
||||
|
||||
this.updateTasks();
|
||||
this.func_110163_bv();
|
||||
this.syncDespawnPersistence();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1403,6 +1404,26 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
field_20062_v += d1 * 0.25D;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void despawnEntity() {
|
||||
syncDespawnPersistence();
|
||||
super.despawnEntity();
|
||||
}
|
||||
|
||||
public void syncDespawnPersistence() {
|
||||
if (stats == null) {
|
||||
if (!this.persistenceRequired) {
|
||||
this.func_110163_bv();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (stats.canDespawn) {
|
||||
this.persistenceRequired = false;
|
||||
} else if (!this.persistenceRequired) {
|
||||
this.func_110163_bv();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean canDespawn() {
|
||||
return stats.canDespawn;
|
||||
@@ -1805,6 +1826,11 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
compound.setBoolean("DeadBody", stats.hideKilledBody);
|
||||
compound.setInteger("StandingState", ais.standingType.ordinal());
|
||||
compound.setInteger("MovingState", ais.movingType.ordinal());
|
||||
compound.setInteger("MovingType", ais.movementType);
|
||||
compound.setDouble("FlySpeed", ais.flySpeed);
|
||||
compound.setDouble("FlyGravity", ais.flyGravity);
|
||||
compound.setBoolean("HasFlyLimit", ais.hasFlyLimit);
|
||||
compound.setInteger("FlyHeightLimit", ais.flyHeightLimit);
|
||||
compound.setInteger("Orientation", ais.orientation);
|
||||
compound.setFloat("OffsetY", ais.bodyOffsetY);
|
||||
compound.setInteger("Role", advanced.role.ordinal());
|
||||
@@ -1836,25 +1862,38 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
try {
|
||||
readSpawnData(ByteBufUtils.readNBT(buf));
|
||||
} catch (IOException e) {
|
||||
// if (this.worldObj != null && this.worldObj.isRemote) {
|
||||
// RequestProperSpawnData.reportMissingData(this);
|
||||
// }
|
||||
if (this.worldObj != null && this.worldObj.isRemote) {
|
||||
RequestProperSpawnData.reportMissingData(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void readSpawnData(NBTTagCompound compound) {
|
||||
// if (this.worldObj != null && this.worldObj.isRemote &&
|
||||
// (compound.hasNoTags() || !compound.hasKey("MaxHealth", Constants.NBT.TAG_DOUBLE))) {
|
||||
// RequestProperSpawnData.reportMissingData(this);
|
||||
// return;
|
||||
// }
|
||||
//
|
||||
// immediateSpawnDataFixAttempts = 0;
|
||||
if (this.worldObj != null && this.worldObj.isRemote) {
|
||||
if(requestClientFix(compound))
|
||||
return;
|
||||
}
|
||||
|
||||
stats.maxHealth = compound.getDouble("MaxHealth");
|
||||
ais.setWalkingSpeed(compound.getInteger("Speed"));
|
||||
stats.hideKilledBody = compound.getBoolean("DeadBody");
|
||||
ais.standingType = EnumStandingType.values()[compound.getInteger("StandingState") % EnumStandingType.values().length];
|
||||
ais.movingType = EnumMovingType.values()[compound.getInteger("MovingState") % EnumMovingType.values().length];
|
||||
if (compound.hasKey("MovingType", Constants.NBT.TAG_INT)) {
|
||||
ais.movementType = compound.getInteger("MovingType");
|
||||
}
|
||||
if (compound.hasKey("FlySpeed", Constants.NBT.TAG_DOUBLE)) {
|
||||
ais.flySpeed = compound.getDouble("FlySpeed");
|
||||
}
|
||||
if (compound.hasKey("FlyGravity", Constants.NBT.TAG_DOUBLE)) {
|
||||
ais.flyGravity = compound.getDouble("FlyGravity");
|
||||
}
|
||||
if (compound.hasKey("HasFlyLimit", Constants.NBT.TAG_BYTE)) {
|
||||
ais.hasFlyLimit = compound.getBoolean("HasFlyLimit");
|
||||
}
|
||||
if (compound.hasKey("FlyHeightLimit", Constants.NBT.TAG_INT)) {
|
||||
ais.flyHeightLimit = compound.getInteger("FlyHeightLimit");
|
||||
}
|
||||
ais.orientation = compound.getInteger("Orientation");
|
||||
ais.bodyOffsetY = compound.getFloat("OffsetY");
|
||||
|
||||
@@ -1927,6 +1966,15 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
|
||||
}
|
||||
}
|
||||
|
||||
public boolean requestClientFix(NBTTagCompound compound){
|
||||
if((compound.hasNoTags() || !compound.hasKey("MaxHealth", Constants.NBT.TAG_DOUBLE))){
|
||||
RequestProperSpawnData.reportMissingData(this);
|
||||
return true;
|
||||
}
|
||||
clientFixAttempts = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canBreatheUnderwater() {
|
||||
return stats.drowningType != 1;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.entity.player.EntityPlayer;
|
||||
import net.minecraft.item.ItemStack;
|
||||
import net.minecraft.nbt.NBTTagCompound;
|
||||
import noppes.npcs.client.gui.global.GuiNpcManageAuction;
|
||||
import noppes.npcs.client.gui.player.GuiAuctionInterface;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
@@ -31,7 +32,7 @@ public abstract class MixinItemStack {
|
||||
@Inject(method = "getTooltip", at = @At("TAIL"), cancellable = true)
|
||||
public void getAttributeTooltip(EntityPlayer player, boolean advanced, CallbackInfoReturnable<List<String>> cir) {
|
||||
boolean override = false;
|
||||
if (Minecraft.getMinecraft().currentScreen instanceof GuiAuctionInterface) {
|
||||
if (Minecraft.getMinecraft().currentScreen instanceof GuiAuctionInterface || Minecraft.getMinecraft().currentScreen instanceof GuiNpcManageAuction) {
|
||||
override = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ public abstract class MixinModelRenderer {
|
||||
float prevAngleX = this.rotateAngleX;
|
||||
float prevAngleY = this.rotateAngleY;
|
||||
float prevAngleZ = this.rotateAngleZ;
|
||||
float prevOffsetX = this.offsetX;
|
||||
float prevOffsetY = this.offsetY;
|
||||
float prevOffsetZ = this.offsetZ;
|
||||
|
||||
boolean changedAngles = false;
|
||||
try {
|
||||
@@ -149,6 +152,11 @@ public abstract class MixinModelRenderer {
|
||||
GL11.glPopMatrix();
|
||||
}
|
||||
|
||||
// Mirror vanilla ModelRenderer behavior: always undo offset translation.
|
||||
// Without this, models that animate via offsetX/Y/Z (e.g. witch nose while drinking)
|
||||
// leak transforms into subsequent renders.
|
||||
GL11.glTranslatef(-this.offsetX, -this.offsetY, -this.offsetZ);
|
||||
|
||||
if (changedAngles) {
|
||||
this.rotationPointX = prevPointX;
|
||||
this.rotationPointY = prevPointY;
|
||||
@@ -156,6 +164,9 @@ public abstract class MixinModelRenderer {
|
||||
this.rotateAngleX = prevAngleX;
|
||||
this.rotateAngleY = prevAngleY;
|
||||
this.rotateAngleZ = prevAngleZ;
|
||||
this.offsetX = prevOffsetX;
|
||||
this.offsetY = prevOffsetY;
|
||||
this.offsetZ = prevOffsetZ;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,22 @@ public class ScriptEnergyBarrier<T extends EntityAbilityBarrier> extends ScriptE
|
||||
entity.getBarrierData().absorbing = absorbing;
|
||||
}
|
||||
|
||||
public boolean isReflect() {
|
||||
return entity.getBarrierData().reflect;
|
||||
}
|
||||
|
||||
public void setReflect(boolean reflect) {
|
||||
entity.getBarrierData().reflect = reflect;
|
||||
}
|
||||
|
||||
public float getReflectStrengthPct() {
|
||||
return entity.getBarrierData().reflectStrengthPct;
|
||||
}
|
||||
|
||||
public void setReflectStrengthPct(float strengthPct) {
|
||||
entity.getBarrierData().setReflectStrengthPct(strengthPct);
|
||||
}
|
||||
|
||||
public int getBarrierType() {
|
||||
return entity instanceof EntityAbilityDome ? 0 : 1;
|
||||
}
|
||||
|
||||
@@ -177,6 +177,14 @@ public class ScriptEnergyProjectile<T extends EntityEnergyProjectile> extends Sc
|
||||
entity.setMultiHitDelayTicks(delayTicks);
|
||||
}
|
||||
|
||||
public int getMaxHits() {
|
||||
return entity.getMaxHits();
|
||||
}
|
||||
|
||||
public void setMaxHits(int maxHits) {
|
||||
entity.setMaxHits(maxHits);
|
||||
}
|
||||
|
||||
// ==================== MOVEMENT ====================
|
||||
|
||||
public float getSpeed() {
|
||||
|
||||
@@ -1109,6 +1109,7 @@ public class ScriptNpc<T extends EntityNPCInterface> extends ScriptLiving<T> imp
|
||||
public void setNaturallyDespawns(boolean canDespawn) {
|
||||
npc.stats.canDespawn = canDespawn;
|
||||
npc.stats.playerSetCanDespawn = canDespawn;
|
||||
npc.syncDespawnPersistence();
|
||||
}
|
||||
|
||||
public boolean spawnedFromSoulStone() {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package noppes.npcs.util;
|
||||
|
||||
import net.minecraft.util.Vec3;
|
||||
|
||||
public class MathUtil {
|
||||
|
||||
public static float getYaw(Vec3 vector) {
|
||||
double x = vector.xCoord;
|
||||
double z = vector.zCoord;
|
||||
double yawRad = Math.atan2(z, x);
|
||||
|
||||
return 90F - (float) Math.toDegrees(yawRad);
|
||||
}
|
||||
|
||||
public static float getPitch(Vec3 vector) {
|
||||
double x = vector.xCoord;
|
||||
double y = vector.yCoord;
|
||||
double z = vector.zCoord;
|
||||
double horizontalMag = Math.sqrt(x * x + z * z);
|
||||
double pitchRad = Math.atan2(y, horizontalMag);
|
||||
|
||||
return (float) -Math.toDegrees(pitchRad);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user