Merge branch 'dev' into syntax-highlighter-unified

This commit is contained in:
Kam committed 2026-02-23 17:50:20 -05:00
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;
@@ -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;
}
@@ -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"),
@@ -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));
}
}
@@ -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);
@@ -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);
+1 -1
View File
@@ -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");
+227 -17
View File
@@ -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);
}
}