diff --git a/src/api b/src/api index 658da549e..788cd0826 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 658da549eec622ed7bcfb331f655938e8841520a +Subproject commit 788cd08264f226953571b0aa737b98a4e2d6d19c diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java index cfa52a55a..30e59cf89 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -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); diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java index c49ca5f15..1720737c1 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java @@ -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); diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java index ac88c47e8..e87e24d51 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderEnergyAbility.java @@ -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(); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 4a8558218..8c8f16efe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java index 130b7d2ba..1217c85d5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyBarrierData.java @@ -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 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; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java index 0825fcfef..88103197f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/energy/EnergyCombatData.java @@ -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; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 03299a67f..510193d54 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -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); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java index 07810bfe3..266a0d6a4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityMovement.java @@ -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); } /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index b04dd2766..c77994789 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -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 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) )); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 84afb11b6..21177a650 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -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. + *

+ * 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 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 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index c20871dd6..0608d6926 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -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 entities = caster.worldObj.getEntitiesWithinAABB(EntityLivingBase.class, box); + if (!isPreview() && !caster.worldObj.isRemote) { + AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + @SuppressWarnings("unchecked") + List 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"), diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java index 980a1dfeb..d1054e858 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBarrier.java @@ -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)); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java index 9fd5ad973..6e1305ed8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityBeam.java @@ -36,8 +36,8 @@ public class AbilityBeam extends AbilityEnergyProjectile 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 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)); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java index 0d2d2fc99..f264f2802 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityDisc.java @@ -38,7 +38,7 @@ public class AbilityDisc extends AbilityEnergyProjectile 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 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)); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java index cfe9457a6..d65c6e7e6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityEnergyProjectile.java @@ -254,10 +254,11 @@ public abstract class AbilityEnergyProjectile /** * 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 @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 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 .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); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java index 522890d47..49efb5ad1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilityLaserShot.java @@ -213,7 +213,9 @@ public class AbilityLaserShot extends AbilityEnergyProjectile 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 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)); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java index ca9cbc7eb..c38131e9f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/energy/AbilitySlicer.java @@ -180,7 +180,9 @@ public class AbilitySlicer extends AbilityEnergyProjectile { )); 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)); } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java index f1309bb97..6c5fcc76c 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBarrier.java @@ -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> 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 ==================== diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index ab374bf41..1822e0c99 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -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 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); } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index e8b8acc58..503d64f3d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -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); } /** diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java index 8c0fc8fae..cf20be91e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDome.java @@ -264,11 +264,13 @@ public class EntityAbilityDome extends EntityAbilityBarrier { List 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; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 56048d973..a632b16ee 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -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 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, diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 407012efd..3e8b9af39 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -72,7 +72,7 @@ public class EntityAbilityOrb extends EntityEnergyProjectile { * Called by ability when windup ends. */ public void startMoving(EntityLivingBase target) { - startMovingAlongLookVectorDefault(); + startMovingTowardTargetDefault(target); } @Override diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java index 653d336a2..a0092a2d7 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityPanel.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java index 278fb8001..a1d858cf4 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityEnergyProjectile.java @@ -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 hitOnceEntities = new HashSet(); protected final Map lastHitTickByEntity = new HashMap(); + 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 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(); } /** diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index e6a525952..787c6e204 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -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()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index 181956757..10ae639e9 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -23,7 +23,6 @@ public enum EnumDataPacket { DELETE_NPC, CLONE_NPC, WEAPON_NPC, - // REQUEST_SPAWN_DATA, // Data SCROLL_LIST, diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java index e1587467b..3453b6eb4 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumPlayerPacket.java @@ -42,4 +42,7 @@ public enum EnumPlayerPacket { AbilityHotbarSave, AbilityHotbarSelect, AbilityToggle, + + // NPC Spawn Data Fix + RequestSpawnData, } diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index e0f3451a1..8592163be 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -213,4 +213,5 @@ public enum EnumRequestPacket { Hammer, RecipeScript, + ManageAuctionAction, } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/RequestProperSpawnData.java b/src/main/java/kamkeel/npcs/network/packets/data/RequestProperSpawnData.java index d2ad045d1..386a9000b 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/RequestProperSpawnData.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/RequestProperSpawnData.java @@ -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 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 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 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 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(); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/GuiRequestPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/GuiRequestPacket.java index fbfae0a6e..b880d141a 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/GuiRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/GuiRequestPacket.java @@ -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; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/auction/ManageAuctionPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/auction/ManageAuctionPacket.java new file mode 100644 index 000000000..10b4504b9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/auction/ManageAuctionPacket.java @@ -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 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 allClaims = controller.getAllGlobalClaims(); + List allListings = controller.getActiveGlobalListings(); + int totalClaims = allClaims.size(); + int totalListings = allListings.size(); + + List soldClaims = new ArrayList(); + List outbidClaims = new ArrayList(); + List wonClaims = new ArrayList(); + List expiredClaims = new ArrayList(); + 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 ordered = new ArrayList(); + 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 pageClaims = new ArrayList(); + List pageListings = new ArrayList(); + + 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 out, List 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 + } +} diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index e059e9104..6229b0f4e 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -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); diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 718793ddd..93ec65bb6 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -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); diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index af9b2b9af..d27a509e5 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -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"); diff --git a/src/main/java/noppes/npcs/NPCSpawning.java b/src/main/java/noppes/npcs/NPCSpawning.java index ef143024c..ed633c8f7 100644 --- a/src/main/java/noppes/npcs/NPCSpawning.java +++ b/src/main/java/noppes/npcs/NPCSpawning.java @@ -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 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 SPAWN_COOLDOWNS = new HashMap(); + private static final Map RUNTIME_CHUNK_STATES = new HashMap(); private static boolean animalSpawn; private static boolean monsterSpawn; private static boolean airSpawn; private static boolean liquidSpawn; + private static class RuntimeChunkState { + public ArrayList chunks = new ArrayList(); + 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 tmp = new ArrayList(eligibleChunksForSpawning); - Collections.shuffle(tmp); - Iterator iterator = tmp.iterator(); + HashMap spawnEntryCounts = getNaturalSpawnEntryCounts(world); + int chunkBudget = getRuntimeChunkBudget(eligibleChunksForSpawning.size()); + List 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 getRuntimeChunkBatch(World world, int chunkBudget) { + ArrayList result = new ArrayList(); + 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(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 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 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 getNaturalSpawnEntryCounts(World world) { + HashMap counts = new HashMap(); + List 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 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 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 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 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; } } - diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index 5b5efccbe..31a15551a 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -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; diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index 0f9fa1d3e..43d78e6eb 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -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 diff --git a/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java b/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java index ebe00c28e..af80c4a28 100644 --- a/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java +++ b/src/main/java/noppes/npcs/ai/pathfinder/FlyPathFinder.java @@ -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) diff --git a/src/main/java/noppes/npcs/ai/pathfinder/FlyingMoveHelper.java b/src/main/java/noppes/npcs/ai/pathfinder/FlyingMoveHelper.java index dd59a22de..18129de11 100644 --- a/src/main/java/noppes/npcs/ai/pathfinder/FlyingMoveHelper.java +++ b/src/main/java/noppes/npcs/ai/pathfinder/FlyingMoveHelper.java @@ -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); } } } diff --git a/src/main/java/noppes/npcs/ai/pathfinder/PathNavigateFlying.java b/src/main/java/noppes/npcs/ai/pathfinder/PathNavigateFlying.java index df2e4c868..90856dc79 100644 --- a/src/main/java/noppes/npcs/ai/pathfinder/PathNavigateFlying.java +++ b/src/main/java/noppes/npcs/ai/pathfinder/PathNavigateFlying.java @@ -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; diff --git a/src/main/java/noppes/npcs/client/ClientEventHandler.java b/src/main/java/noppes/npcs/client/ClientEventHandler.java index 6eebfd5d5..1e8d7a56d 100644 --- a/src/main/java/noppes/npcs/client/ClientEventHandler.java +++ b/src/main/java/noppes/npcs/client/ClientEventHandler.java @@ -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()) { diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 46eb58efa..635fc1e66 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -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); diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index bfd3b0cae..3636f48fa 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -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(); } } diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java index 29c33352b..ddce230c3 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java @@ -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()); + } } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAuction.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAuction.java new file mode 100644 index 000000000..84e16d24e --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAuction.java @@ -0,0 +1,1096 @@ +package noppes.npcs.client.gui.global; + +import kamkeel.npcs.network.packets.request.auction.ManageAuctionPacket; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.CustomNpcResourceListener; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.GuiContainerNPCInterface2; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.constants.EnumAuctionSort; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.containers.ContainerManageAuction; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.AuctionFormatUtil; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +public class GuiNpcManageAuction extends GuiContainerNPCInterface2 implements IGuiData, ITextfieldListener { + private static final ResourceLocation SLOT_DEFAULT = new ResourceLocation("customnpcs", "textures/gui/slot.png"); + private static final ResourceLocation SLOT_AUCTION = new ResourceLocation("customnpcs", "textures/gui/auction/auction_slot.png"); + private static final ResourceLocation ICON_X = new ResourceLocation("customnpcs", "textures/gui/auction/x_icon.png"); + private static final ResourceLocation ICON_X_CANCEL = new ResourceLocation("customnpcs", "textures/gui/auction/x_icon_blue.png"); + private static final ResourceLocation ICON_CHECK = new ResourceLocation("customnpcs", "textures/gui/auction/check_icon.png"); + private static final ResourceLocation COIN_ICON = new ResourceLocation("customnpcs", "textures/items/npcCoinGold.png"); + + // Slot tints (ARGB) aligned with My Trades style. + private static final int TINT_BLUE = 0x605080FF; + private static final int TINT_GREEN = 0x6050FF50; + private static final int TINT_RED = 0x60FF5050; + + public enum Tab { + LISTINGS, + CREATE, + CLAIMS + } + + private enum PendingAction { + NONE, + STOP, + CANCEL, + CLAIM_ITEM, + CLAIM_CURRENCY + } + + private static final int BTN_TAB_LISTINGS = 100; + private static final int BTN_TAB_CREATE = 101; + private static final int BTN_TAB_CLAIMS = 102; + private static final int BTN_PAGE_PREV = 110; + private static final int BTN_PAGE_NEXT = 111; + private static final int BTN_SORT = 112; + private static final int BTN_CREATE = 120; + + private static final int TXT_SEARCH = 1; + private static final int TXT_FAKE_SELLER = 2; + private static final int TXT_FAKE_START = 3; + private static final int TXT_FAKE_BUYOUT = 4; + private static final int TXT_FAKE_DURATION = 5; + + private static final int RIGHT_X = 188; + private static final int RIGHT_WIDTH = 220; + private static final int TAB_Y = 10; + + private static final int LISTINGS_SORT_Y = 40; + private static final int LISTINGS_SEARCH_LABEL_Y = 66; + private static final int LISTINGS_SEARCH_FIELD_Y = 76; + private static final int LISTINGS_HINT_Y = 98; + private static final int LISTINGS_HINT2_Y = 110; + private static final int LISTINGS_HINT3_Y = 122; + private static final int LISTINGS_HINT4_Y = 134; + private static final int LISTINGS_DETAILS_TITLE_Y = 42; + private static final int DETAILS_PANEL_X = RIGHT_X; + private static final int DETAILS_PANEL_Y = 40; + private static final int DETAILS_PANEL_W = RIGHT_WIDTH - 8; + private static final int DETAILS_PANEL_H = 138; + + private static final int CREATE_FIELD_X = RIGHT_X + 82; + private static final int CREATE_FIELD_WIDTH = 126; + private static final int CREATE_SELLER_Y = 52; + private static final int CREATE_START_Y = 78; + private static final int CREATE_BUYOUT_Y = 104; + private static final int CREATE_DURATION_Y = 130; + private static final int CREATE_BUTTON_Y = 178; + + private final ContainerManageAuction manageContainer; + private final AuctionFilter filter = new AuctionFilter(); + + private Tab activeTab = Tab.LISTINGS; + private String searchText = ""; + + private String fakeSeller = "Server"; + private String fakeStartPrice = "1"; + private String fakeBuyoutPrice = ""; + private String fakeDurationHours = Integer.toString(Math.max(1, AuctionClientConfig.getAuctionDurationHours())); + + private int listingsPage = 0; + private int listingsTotalPages = 1; + private int listingsTotalCount = 0; + + private int claimsPage = 0; + private int claimsTotalPages = 1; + private int claimsTotalCount = 0; + private int claimsListingCount = 0; + private int claimsClaimCount = 0; + + private int pendingDisplaySlot = -1; + private PendingAction pendingAction = PendingAction.NONE; + + private String selectedListingId = null; + private AuctionListing selectedListing = null; + private int selectedDisplaySlot = -1; + private boolean listingsDetailsView = false; + + private String errorMessage = null; + private boolean createRequestPending = false; + + public GuiNpcManageAuction(EntityNPCInterface npc, ContainerManageAuction container) { + super(npc, container); + this.manageContainer = container; + this.drawDefaultBackground = false; + this.ySize = 200; + this.title = ""; + } + + @Override + public void initGui() { + super.initGui(); + + addButton(new GuiNpcButton(BTN_TAB_LISTINGS, guiLeft + RIGHT_X, guiTop + TAB_Y, 70, 20, "global.auction.manage.listings")); + addButton(new GuiNpcButton(BTN_TAB_CREATE, guiLeft + RIGHT_X + 74, guiTop + TAB_Y, 70, 20, "global.auction.manage.create")); + addButton(new GuiNpcButton(BTN_TAB_CLAIMS, guiLeft + RIGHT_X + 148, guiTop + TAB_Y, 70, 20, "global.auction.manage.claims")); + updateTabButtonState(); + + int pageButtonY = guiTop + ySize - 24; + addButton(new GuiNpcButton(BTN_PAGE_PREV, guiLeft + RIGHT_X, pageButtonY, 20, 20, "<")); + addButton(new GuiNpcButton(BTN_PAGE_NEXT, guiLeft + RIGHT_X + 24, pageButtonY, 20, 20, ">")); + + if (activeTab == Tab.LISTINGS && !listingsDetailsView) { + String sortName = filter.getSortBy().getDisplayName(); + addButton(new GuiNpcButton(BTN_SORT, guiLeft + RIGHT_X, guiTop + LISTINGS_SORT_Y, RIGHT_WIDTH - 8, 20, + StatCollector.translateToLocal("global.auction.manage.sort") + ": " + sortName)); + + addTextField(new GuiNpcTextField(TXT_SEARCH, this, fontRendererObj, + guiLeft + RIGHT_X, guiTop + LISTINGS_SEARCH_FIELD_Y, RIGHT_WIDTH - 8, 18, searchText)); + requestListings(); + } else if (activeTab == Tab.LISTINGS) { + manageContainer.setDisplayMode(ContainerManageAuction.DisplayMode.LISTINGS); + } else if (activeTab == Tab.CREATE) { + addTextField(new GuiNpcTextField(TXT_FAKE_SELLER, this, fontRendererObj, + guiLeft + CREATE_FIELD_X, guiTop + CREATE_SELLER_Y, CREATE_FIELD_WIDTH, 18, fakeSeller)); + + GuiNpcTextField startField = new GuiNpcTextField(TXT_FAKE_START, this, fontRendererObj, + guiLeft + CREATE_FIELD_X, guiTop + CREATE_START_Y, CREATE_FIELD_WIDTH, 18, fakeStartPrice); + startField.setIntegersOnly(); + addTextField(startField); + + GuiNpcTextField buyoutField = new GuiNpcTextField(TXT_FAKE_BUYOUT, this, fontRendererObj, + guiLeft + CREATE_FIELD_X, guiTop + CREATE_BUYOUT_Y, CREATE_FIELD_WIDTH, 18, fakeBuyoutPrice); + buyoutField.setIntegersOnly(); + addTextField(buyoutField); + + GuiNpcTextField durationField = new GuiNpcTextField(TXT_FAKE_DURATION, this, fontRendererObj, + guiLeft + CREATE_FIELD_X, guiTop + CREATE_DURATION_Y, CREATE_FIELD_WIDTH, 18, fakeDurationHours); + durationField.setIntegersOnly(); + addTextField(durationField); + + addButton(new GuiNpcButton(BTN_CREATE, guiLeft + RIGHT_X, guiTop + CREATE_BUTTON_Y, RIGHT_WIDTH - 8, 20, + "global.auction.manage.create.submit")); + } else if (activeTab == Tab.CLAIMS) { + requestClaims(); + } + + syncContainerSlots(); + updatePageButtons(); + } + + private void switchTab(Tab tab) { + if (activeTab == tab) return; + activeTab = tab; + listingsDetailsView = false; + if (tab != Tab.LISTINGS) { + selectedListing = null; + selectedListingId = null; + selectedDisplaySlot = -1; + } + clearPending(); + errorMessage = null; + initGui(); + } + + private void updateTabButtonState() { + GuiNpcButton listingsBtn = getButton(BTN_TAB_LISTINGS); + GuiNpcButton createBtn = getButton(BTN_TAB_CREATE); + GuiNpcButton claimsBtn = getButton(BTN_TAB_CLAIMS); + if (listingsBtn != null) { + listingsBtn.enabled = activeTab != Tab.LISTINGS || listingsDetailsView; + } + if (createBtn != null) { + createBtn.enabled = activeTab != Tab.CREATE; + } + if (claimsBtn != null) { + claimsBtn.enabled = activeTab != Tab.CLAIMS; + } + } + + private void syncContainerSlots() { + boolean showCreate = activeTab == Tab.CREATE; + manageContainer.setCreateSlotVisible(showCreate); + + boolean showDetail = activeTab == Tab.LISTINGS && listingsDetailsView && selectedListing != null; + manageContainer.setDetailSlotVisible(showDetail); + if (showDetail) { + manageContainer.setDetailItem(selectedListing.item); + } else { + manageContainer.clearDetailItem(); + } + } + + private void updatePageButtons() { + GuiNpcButton prev = getButton(BTN_PAGE_PREV); + GuiNpcButton next = getButton(BTN_PAGE_NEXT); + if (prev == null || next == null) return; + + if (activeTab == Tab.CREATE) { + prev.visible = false; + next.visible = false; + prev.enabled = false; + next.enabled = false; + return; + } + + if (activeTab == Tab.LISTINGS && listingsDetailsView) { + prev.visible = false; + next.visible = false; + prev.enabled = false; + next.enabled = false; + return; + } + + prev.visible = true; + next.visible = true; + if (activeTab == Tab.LISTINGS) { + prev.enabled = listingsPage > 0; + next.enabled = listingsPage < listingsTotalPages - 1; + } else { + prev.enabled = claimsPage > 0; + next.enabled = claimsPage < claimsTotalPages - 1; + } + } + + private void requestListings() { + filter.setSearchText(searchText); + manageContainer.setDisplayMode(ContainerManageAuction.DisplayMode.LISTINGS); + ManageAuctionPacket.requestListings(filter, listingsPage); + } + + private void requestClaims() { + manageContainer.setDisplayMode(ContainerManageAuction.DisplayMode.CLAIMS); + ManageAuctionPacket.requestGlobalClaims(claimsPage); + } + + private void cycleSort() { + EnumAuctionSort[] values = EnumAuctionSort.values(); + int next = (filter.getSortBy().ordinal() + 1) % values.length; + filter.setSortBy(values[next]); + listingsPage = 0; + clearPending(); + requestListings(); + initGui(); + } + + private void createFakeAuction() { + GuiNpcTextField sellerField = getTextField(TXT_FAKE_SELLER); + GuiNpcTextField startField = getTextField(TXT_FAKE_START); + GuiNpcTextField buyoutField = getTextField(TXT_FAKE_BUYOUT); + GuiNpcTextField durationField = getTextField(TXT_FAKE_DURATION); + + if (sellerField != null) fakeSeller = sellerField.getText(); + if (startField != null) fakeStartPrice = startField.getText(); + if (buyoutField != null) fakeBuyoutPrice = buyoutField.getText(); + if (durationField != null) fakeDurationHours = durationField.getText(); + + ItemStack staged = manageContainer.getCreateItem(); + if (staged == null) { + errorMessage = StatCollector.translateToLocal("global.auction.manage.error.noItem"); + return; + } + + long start = parseLong(fakeStartPrice, 0); + long buyout = parseLong(fakeBuyoutPrice, 0); + int duration = (int) parseLong(fakeDurationHours, AuctionClientConfig.getAuctionDurationHours()); + if (duration <= 0) { + duration = Math.max(1, AuctionClientConfig.getAuctionDurationHours()); + } + + if (start <= 0) { + errorMessage = StatCollector.translateToLocal("global.auction.manage.error.invalidStart"); + return; + } + if (buyout > 0 && buyout < start) { + errorMessage = StatCollector.translateToLocal("global.auction.manage.error.invalidBuyout"); + return; + } + + String seller = fakeSeller != null ? fakeSeller.trim() : ""; + if (seller.isEmpty()) { + seller = "Server"; + fakeSeller = seller; + } + + ManageAuctionPacket.createFakeListing(staged, seller, start, buyout, duration); + createRequestPending = true; + errorMessage = null; + } + + private long parseLong(String value, long fallback) { + if (value == null || value.trim().isEmpty()) return fallback; + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException ignored) { + return fallback; + } + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + super.actionPerformed(guibutton); + if (!(guibutton instanceof GuiNpcButton)) return; + + if (guibutton.id == BTN_TAB_LISTINGS) { + if (activeTab == Tab.LISTINGS && listingsDetailsView) { + listingsDetailsView = false; + selectedListing = null; + selectedListingId = null; + selectedDisplaySlot = -1; + clearPending(); + initGui(); + } else { + switchTab(Tab.LISTINGS); + } + return; + } + if (guibutton.id == BTN_TAB_CREATE) { + switchTab(Tab.CREATE); + return; + } + if (guibutton.id == BTN_TAB_CLAIMS) { + switchTab(Tab.CLAIMS); + return; + } + + if (guibutton.id == BTN_PAGE_PREV) { + if (activeTab == Tab.LISTINGS && listingsPage > 0) { + listingsPage--; + clearPending(); + requestListings(); + NoppesUtil.clickSound(); + } else if (activeTab == Tab.CLAIMS && claimsPage > 0) { + claimsPage--; + clearPending(); + requestClaims(); + NoppesUtil.clickSound(); + } + updatePageButtons(); + return; + } + + if (guibutton.id == BTN_PAGE_NEXT) { + if (activeTab == Tab.LISTINGS && listingsPage < listingsTotalPages - 1) { + listingsPage++; + clearPending(); + requestListings(); + NoppesUtil.clickSound(); + } else if (activeTab == Tab.CLAIMS && claimsPage < claimsTotalPages - 1) { + claimsPage++; + clearPending(); + requestClaims(); + NoppesUtil.clickSound(); + } + updatePageButtons(); + return; + } + + if (guibutton.id == BTN_SORT) { + cycleSort(); + NoppesUtil.clickSound(); + return; + } + + if (guibutton.id == BTN_CREATE) { + createFakeAuction(); + NoppesUtil.clickSound(); + } + } + + @Override + protected void handleMouseClick(Slot slot, int slotIndex, int mouseButton, int clickType) { + if (slot == null) { + clearPending(); + return; + } + + if (manageContainer.isDisplaySlot(slotIndex)) { + int displayIndex = manageContainer.toDisplayIndex(slotIndex); + if (activeTab == Tab.LISTINGS) { + handleListingsSlotClick(displayIndex, mouseButton); + return; + } + if (activeTab == Tab.CLAIMS) { + handleClaimsSlotClick(displayIndex, mouseButton); + return; + } + } + + if (activeTab == Tab.CREATE) { + if (manageContainer.isCreateSlot(slotIndex)) { + if (manageContainer.getCreateItem() != null) { + boolean removeAll = mouseButton == 0; + manageContainer.removeFromCreateSlot(removeAll); + NoppesUtil.clickSound(); + } + return; + } + + if (manageContainer.isPlayerSlot(slotIndex)) { + ItemStack stack = slot.getStack(); + if (stack != null) { + boolean fullStack = mouseButton == 0; + manageContainer.addToCreateSlot(slotIndex, fullStack); + NoppesUtil.clickSound(); + } + return; + } + } + + clearPending(); + } + + private void handleListingsSlotClick(int displayIndex, int mouseButton) { + AuctionListing listing = manageContainer.getListingAtDisplay(displayIndex); + if (listing == null) { + clearPending(); + return; + } + + // Any non-matching input while pending cancels the operation. + if (pendingAction != PendingAction.NONE) { + if (displayIndex == pendingDisplaySlot && isConfirmInput(pendingAction, mouseButton)) { + ManageAuctionPacket.manageListing(listing.id, pendingAction == PendingAction.CANCEL); + clearPending(); + playConfirmSound(); + } else { + clearPending(); + } + return; + } + + if (mouseButton == 0) { + selectListing(displayIndex, listing); + clearPending(); + listingsDetailsView = true; + initGui(); + NoppesUtil.clickSound(); + return; + } + + if (isStopStartInput(mouseButton) || isCancelStartInput(mouseButton)) { + PendingAction action = isShiftKeyDown() ? PendingAction.CANCEL : PendingAction.STOP; + setPending(displayIndex, action); + NoppesUtil.clickSound(); + return; + } + + clearPending(); + } + + private void handleClaimsSlotClick(int displayIndex, int mouseButton) { + AuctionListing listing = manageContainer.getListingAtDisplay(displayIndex); + if (listing != null) { + if (pendingAction != PendingAction.NONE) { + if (displayIndex == pendingDisplaySlot && isConfirmInput(pendingAction, mouseButton)) { + ManageAuctionPacket.manageListing(listing.id, pendingAction == PendingAction.CANCEL); + clearPending(); + playConfirmSound(); + } else { + clearPending(); + } + return; + } + + if (isStopStartInput(mouseButton) || isCancelStartInput(mouseButton)) { + PendingAction action = isShiftKeyDown() ? PendingAction.CANCEL : PendingAction.STOP; + setPending(displayIndex, action); + NoppesUtil.clickSound(); + } else { + clearPending(); + } + return; + } + + AuctionClaim claim = manageContainer.getClaimAtDisplay(displayIndex); + if (claim == null) { + clearPending(); + return; + } + + PendingAction action = claim.type.isItem() ? PendingAction.CLAIM_ITEM : PendingAction.CLAIM_CURRENCY; + if (pendingAction != PendingAction.NONE) { + if (displayIndex == pendingDisplaySlot && pendingAction == action && isConfirmInput(action, mouseButton)) { + if (action == PendingAction.CLAIM_ITEM) { + ManageAuctionPacket.claimGlobalItem(claim.id); + } else { + ManageAuctionPacket.claimGlobalCurrency(claim.id); + } + clearPending(); + playConfirmSound(); + } else { + clearPending(); + } + return; + } + + if (mouseButton == 0) { + setPending(displayIndex, action); + NoppesUtil.clickSound(); + return; + } + + clearPending(); + } + + private boolean isStopStartInput(int mouseButton) { + return mouseButton == 1 && !isShiftKeyDown(); + } + + private boolean isCancelStartInput(int mouseButton) { + return mouseButton == 1 && isShiftKeyDown(); + } + + private boolean isConfirmInput(PendingAction action, int mouseButton) { + if (action == PendingAction.CLAIM_ITEM || action == PendingAction.CLAIM_CURRENCY) { + return mouseButton == 0; + } + if (action == PendingAction.STOP) { + return mouseButton == 1 && !isShiftKeyDown(); + } + if (action == PendingAction.CANCEL) { + return mouseButton == 1 && isShiftKeyDown(); + } + return false; + } + + private void selectListing(int displayIndex, AuctionListing listing) { + selectedDisplaySlot = displayIndex; + selectedListingId = listing != null ? listing.id : null; + selectedListing = listing; + syncContainerSlots(); + updateTabButtonState(); + } + + private void refreshSelectedListing(List pageListings) { + selectedListing = null; + selectedDisplaySlot = -1; + + if (selectedListingId == null || pageListings == null) { + syncContainerSlots(); + return; + } + + for (int i = 0; i < pageListings.size(); i++) { + AuctionListing listing = pageListings.get(i); + if (listing != null && selectedListingId.equals(listing.id)) { + selectedListing = listing; + selectedDisplaySlot = i; + break; + } + } + + if (selectedListing == null) { + selectedListingId = null; + } + + if (selectedListing == null) { + listingsDetailsView = false; + } + + syncContainerSlots(); + updateTabButtonState(); + } + + private void setPending(int displaySlot, PendingAction action) { + this.pendingDisplaySlot = displaySlot; + this.pendingAction = action; + manageContainer.setHiddenDisplaySlot(displaySlot); + } + + private void clearPending() { + manageContainer.clearHiddenDisplaySlot(); + pendingDisplaySlot = -1; + pendingAction = PendingAction.NONE; + } + + private void playConfirmSound() { + mc.getSoundHandler().playSound(PositionedSoundRecord.func_147674_a( + new ResourceLocation("random.orb"), 1.0F)); + } + + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + + if (activeTab == Tab.LISTINGS) { + GuiNpcTextField search = getTextField(TXT_SEARCH); + if (search != null && search.isFocused()) { + String text = search.getText(); + if (!text.equals(searchText)) { + searchText = text; + listingsPage = 0; + clearPending(); + requestListings(); + } + } + } + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + if (textfield == null) return; + + if (textfield.id == TXT_SEARCH) { + searchText = textfield.getText(); + } else if (textfield.id == TXT_FAKE_SELLER) { + fakeSeller = textfield.getText(); + } else if (textfield.id == TXT_FAKE_START) { + fakeStartPrice = textfield.getText(); + } else if (textfield.id == TXT_FAKE_BUYOUT) { + fakeBuyoutPrice = textfield.getText(); + } else if (textfield.id == TXT_FAKE_DURATION) { + fakeDurationHours = textfield.getText(); + } + } + + @Override + public void setGuiData(NBTTagCompound compound) { + if (compound.hasKey("ManageAuctionListingsData")) { + listingsPage = compound.getInteger("Page"); + listingsTotalPages = Math.max(1, compound.getInteger("TotalPages")); + listingsTotalCount = compound.getInteger("TotalListings"); + + NBTTagList list = compound.getTagList("Listings", 10); + List listings = new ArrayList(); + for (int i = 0; i < list.tagCount(); i++) { + listings.add(AuctionListing.fromNBT(list.getCompoundTagAt(i))); + } + manageContainer.setListingsPage(listings); + refreshSelectedListing(listings); + updatePageButtons(); + } + + if (compound.hasKey("ManageAuctionClaimsData")) { + claimsPage = compound.getInteger("Page"); + claimsTotalPages = Math.max(1, compound.getInteger("TotalPages")); + claimsTotalCount = compound.hasKey("TotalEntries") ? compound.getInteger("TotalEntries") : compound.getInteger("TotalClaims"); + claimsClaimCount = compound.getInteger("TotalClaims"); + claimsListingCount = compound.hasKey("TotalListings") ? compound.getInteger("TotalListings") : 0; + + NBTTagList claimList = compound.getTagList("Claims", 10); + List claims = new ArrayList(); + for (int i = 0; i < claimList.tagCount(); i++) { + claims.add(AuctionClaim.fromNBT(claimList.getCompoundTagAt(i))); + } + + NBTTagList listingList = compound.getTagList("Listings", 10); + List listings = new ArrayList(); + for (int i = 0; i < listingList.tagCount(); i++) { + listings.add(AuctionListing.fromNBT(listingList.getCompoundTagAt(i))); + } + + manageContainer.setClaimsAndListingsPage(claims, listings); + updatePageButtons(); + } + + if (compound.getBoolean("ManageAuctionRefresh")) { + boolean createSuccess = compound.getBoolean("ManageAuctionCreateSuccess"); + createRequestPending = false; + if (createSuccess) { + manageContainer.clearCreateSlot(); + listingsDetailsView = false; + selectedListing = null; + selectedListingId = null; + selectedDisplaySlot = -1; + activeTab = Tab.LISTINGS; + clearPending(); + initGui(); + return; + } + + clearPending(); + if (activeTab == Tab.LISTINGS) { + requestListings(); + } else if (activeTab == Tab.CLAIMS) { + requestClaims(); + } + } + } + + @Override + protected void drawGuiContainerBackgroundLayer(float f, int mouseX, int mouseY) { + super.drawGuiContainerBackgroundLayer(f, mouseX, mouseY); + + drawDisplaySlotBackgrounds(); + drawPlayerInventorySlotBackgrounds(); + drawRightPanelSlotBackgrounds(); + drawClaimsTintOverlay(); + drawSelectedListingOverlay(); + drawClaimIndicators(); + drawPendingOverlay(); + } + + private void drawDisplaySlotBackgrounds() { + for (int row = 0; row < ContainerManageAuction.DISPLAY_ROWS; row++) { + for (int col = 0; col < ContainerManageAuction.DISPLAY_COLS; col++) { + int x = guiLeft + ContainerManageAuction.DISPLAY_X + col * 18; + int y = guiTop + ContainerManageAuction.DISPLAY_Y + row * 18; + drawAuctionSlot(x, y); + } + } + } + + private void drawPlayerInventorySlotBackgrounds() { + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + int x = guiLeft + ContainerManageAuction.PLAYER_INV_X + col * 18; + int y = guiTop + ContainerManageAuction.PLAYER_INV_Y + row * 18; + drawDefaultSlot(x, y); + } + } + for (int col = 0; col < 9; col++) { + int x = guiLeft + ContainerManageAuction.PLAYER_INV_X + col * 18; + int y = guiTop + ContainerManageAuction.HOTBAR_Y; + drawDefaultSlot(x, y); + } + } + + private void drawRightPanelSlotBackgrounds() { + if (activeTab == Tab.LISTINGS && listingsDetailsView) { + drawDetailsPanelBackground(); + } + if (activeTab == Tab.LISTINGS && listingsDetailsView && selectedListing != null) { + drawAuctionSlot(guiLeft + ContainerManageAuction.DETAIL_SLOT_X, guiTop + ContainerManageAuction.DETAIL_SLOT_Y); + } else if (activeTab == Tab.CREATE) { + drawAuctionSlot(guiLeft + ContainerManageAuction.CREATE_SLOT_X, guiTop + ContainerManageAuction.CREATE_SLOT_Y); + } + } + + private void drawDetailsPanelBackground() { + int x = guiLeft + DETAILS_PANEL_X; + int y = guiTop + DETAILS_PANEL_Y; + int x2 = x + DETAILS_PANEL_W; + int y2 = y + DETAILS_PANEL_H; + + Gui.drawRect(x, y, x2, y2, 0xCC0F0F0F); + Gui.drawRect(x + 1, y + 1, x2 - 1, y2 - 1, 0x88202020); + Gui.drawRect(x, y, x2, y + 1, 0xFF3A3A3A); + Gui.drawRect(x, y2 - 1, x2, y2, 0xFF3A3A3A); + Gui.drawRect(x, y, x + 1, y2, 0xFF3A3A3A); + Gui.drawRect(x2 - 1, y, x2, y2, 0xFF3A3A3A); + } + + private void drawSelectedListingOverlay() { + if (activeTab != Tab.LISTINGS || selectedDisplaySlot < 0 || selectedDisplaySlot >= ContainerManageAuction.DISPLAY_SLOT_COUNT) { + return; + } + + int x = guiLeft + ContainerManageAuction.DISPLAY_X + (selectedDisplaySlot % ContainerManageAuction.DISPLAY_COLS) * 18; + int y = guiTop + ContainerManageAuction.DISPLAY_Y + (selectedDisplaySlot / ContainerManageAuction.DISPLAY_COLS) * 18; + drawColoredOverlay(x, y, 0x503080FF); + Gui.drawRect(x, y, x + 18, y + 1, 0xFF8AC4FF); + Gui.drawRect(x, y + 17, x + 18, y + 18, 0xFF8AC4FF); + Gui.drawRect(x, y, x + 1, y + 18, 0xFF8AC4FF); + Gui.drawRect(x + 17, y, x + 18, y + 18, 0xFF8AC4FF); + } + + private void drawClaimIndicators() { + if (activeTab != Tab.CLAIMS) return; + + for (int i = 0; i < ContainerManageAuction.DISPLAY_SLOT_COUNT; i++) { + AuctionClaim claim = manageContainer.getClaimAtDisplay(i); + if (claim == null) continue; + + boolean drawCoin = claim.type == EnumClaimType.CURRENCY + || (claim.type == EnumClaimType.REFUND && claim.item == null); + if (!drawCoin) continue; + + int x = guiLeft + ContainerManageAuction.DISPLAY_X + (i % ContainerManageAuction.DISPLAY_COLS) * 18; + int y = guiTop + ContainerManageAuction.DISPLAY_Y + (i / ContainerManageAuction.DISPLAY_COLS) * 18; + drawIconOverlay(x, y, COIN_ICON); + } + } + + private void drawClaimsTintOverlay() { + if (activeTab != Tab.CLAIMS) return; + for (int i = 0; i < ContainerManageAuction.DISPLAY_SLOT_COUNT; i++) { + int tint = getClaimsSlotTint(i); + if (tint == 0) continue; + + int x = guiLeft + ContainerManageAuction.DISPLAY_X + (i % ContainerManageAuction.DISPLAY_COLS) * 18; + int y = guiTop + ContainerManageAuction.DISPLAY_Y + (i / ContainerManageAuction.DISPLAY_COLS) * 18; + drawColoredOverlay(x, y, tint); + } + } + + private int getClaimsSlotTint(int displayIndex) { + AuctionListing listing = manageContainer.getListingAtDisplay(displayIndex); + if (listing != null) { + return TINT_BLUE; + } + + AuctionClaim claim = manageContainer.getClaimAtDisplay(displayIndex); + if (claim == null) return 0; + + if (claim.type == EnumClaimType.CURRENCY) { + return TINT_GREEN; + } + if (claim.type == EnumClaimType.REFUND) { + return TINT_RED; + } + if (claim.type == EnumClaimType.ITEM) { + return claim.isReturned ? TINT_RED : TINT_GREEN; + } + return TINT_GREEN; + } + + private void drawPendingOverlay() { + if (pendingDisplaySlot < 0 || pendingAction == PendingAction.NONE) return; + + int x = guiLeft + ContainerManageAuction.DISPLAY_X + (pendingDisplaySlot % ContainerManageAuction.DISPLAY_COLS) * 18; + int y = guiTop + ContainerManageAuction.DISPLAY_Y + (pendingDisplaySlot / ContainerManageAuction.DISPLAY_COLS) * 18; + + int color = 0x80FFFF00; + ResourceLocation icon = ICON_X; + float iconR = 1f; + float iconG = 1f; + float iconB = 1f; + if (pendingAction == PendingAction.STOP) { + color = 0x805080FF; + icon = ICON_X; + } else if (pendingAction == PendingAction.CANCEL) { + color = 0x80A040D0; + icon = ICON_X_CANCEL; + } else if (pendingAction == PendingAction.CLAIM_ITEM || pendingAction == PendingAction.CLAIM_CURRENCY) { + color = 0x8055FF55; + icon = ICON_CHECK; + } + + drawColoredOverlay(x, y, color); + drawIconOverlay(x, y, icon, iconR, iconG, iconB, 1f); + } + + private void drawAuctionSlot(int x, int y) { + GL11.glColor4f(1f, 1f, 1f, 1f); + mc.renderEngine.bindTexture(SLOT_AUCTION); + Gui.func_146110_a(x, y, 0, 0, 18, 18, 18, 18); + } + + private void drawDefaultSlot(int x, int y) { + GL11.glColor4f(1f, 1f, 1f, 1f); + mc.renderEngine.bindTexture(SLOT_DEFAULT); + drawTexturedModalRect(x, y, 0, 0, 18, 18); + } + + private void drawColoredOverlay(int x, int y, int color) { + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + Gui.drawRect(x + 1, y + 1, x + 17, y + 17, color); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_DEPTH_TEST); + GL11.glColor4f(1f, 1f, 1f, 1f); + } + + private void drawIconOverlay(int x, int y, ResourceLocation icon) { + drawIconOverlay(x, y, icon, 1f, 1f, 1f, 1f); + } + + private void drawIconOverlay(int x, int y, ResourceLocation icon, float r, float g, float b, float a) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(r, g, b, a); + mc.renderEngine.bindTexture(icon); + Gui.func_146110_a(x + 1, y + 1, 0, 0, 16, 16, 16, 16); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + super.drawGuiContainerForegroundLayer(mouseX, mouseY); + int textColor = CustomNpcResourceListener.DefaultTextColor; + + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.title"), 8, 6, textColor); + + if (activeTab == Tab.LISTINGS) { + if (listingsDetailsView) { + drawListingsDetails(textColor); + } else { + drawPageText(listingsPage, listingsTotalPages, listingsTotalCount, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.search"), RIGHT_X, LISTINGS_SEARCH_LABEL_Y, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.listings"), RIGHT_X, LISTINGS_HINT_Y, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.stop"), RIGHT_X, LISTINGS_HINT2_Y, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.cancel"), RIGHT_X, LISTINGS_HINT3_Y, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.selectListing"), RIGHT_X, LISTINGS_HINT4_Y, textColor); + } + } else if (activeTab == Tab.CLAIMS) { + drawPageText(claimsPage, claimsTotalPages, claimsTotalCount, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.claims"), RIGHT_X, 58, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.stop"), RIGHT_X, 70, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.hint.cancel"), RIGHT_X, 82, textColor); + String totals = claimsClaimCount + " claims | " + claimsListingCount + " listings"; + fontRendererObj.drawString(totals, RIGHT_X, 94, textColor); + } else { + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.seller"), RIGHT_X, CREATE_SELLER_Y + 5, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.startPrice"), RIGHT_X, CREATE_START_Y + 5, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.buyoutPrice"), RIGHT_X, CREATE_BUYOUT_Y + 5, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.durationHours"), RIGHT_X, CREATE_DURATION_Y + 5, textColor); + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.stagedItem"), RIGHT_X + 22, ContainerManageAuction.CREATE_SLOT_Y + 5, textColor); + + ItemStack staged = manageContainer.getCreateItem(); + if (staged != null) { + int available = manageContainer.countItemInPlayerInventory(staged); + String countText = staged.stackSize + "/" + available; + int countColor = staged.stackSize <= available ? 0x55FF55 : 0xFF5555; + fontRendererObj.drawStringWithShadow(countText, RIGHT_X + 22, ContainerManageAuction.CREATE_SLOT_Y + 16, countColor); + } + } + + if (errorMessage != null && !errorMessage.isEmpty()) { + fontRendererObj.drawString(EnumChatFormatting.RED + errorMessage, RIGHT_X, ySize - 12, 0xFFFFFF); + } + + drawDynamicTooltip(mouseX, mouseY); + } + + private void drawPageText(int page, int totalPages, int totalCount, int textColor) { + String pageText = (page + 1) + "/" + totalPages + " (" + totalCount + ")"; + fontRendererObj.drawString(StatCollector.translateToLocal("global.auction.manage.page") + ": " + pageText, + RIGHT_X + 48, ySize - 18, textColor); + } + + private void drawListingsDetails(int textColor) { + int panelLeft = DETAILS_PANEL_X + 6; + int panelRight = DETAILS_PANEL_X + DETAILS_PANEL_W - 6; + int y = LISTINGS_DETAILS_TITLE_Y; + + fontRendererObj.drawString(EnumChatFormatting.AQUA + StatCollector.translateToLocal("global.auction.manage.details"), panelLeft, y, 0xFFFFFF); + y += 12; + + if (selectedListing == null) { + fontRendererObj.drawString(EnumChatFormatting.GRAY + StatCollector.translateToLocal("global.auction.manage.selectListing"), panelLeft, y, 0xFFFFFF); + return; + } + + y = ContainerManageAuction.DETAIL_SLOT_Y + 1; + drawDetailsLine(panelLeft + 24, panelRight, y, EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.seller").replace("%s", ""), + EnumChatFormatting.WHITE + selectedListing.sellerName); + + y += 11; + long currentValue = selectedListing.hasBids() ? selectedListing.currentBid : selectedListing.startingPrice; + String currentLabel = selectedListing.hasBids() + ? StatCollector.translateToLocal("auction.info.currentBid") + : StatCollector.translateToLocal("auction.info.startingPrice"); + drawDetailsLine(panelLeft + 24, panelRight, y, EnumChatFormatting.YELLOW + currentLabel, + EnumChatFormatting.GOLD + AuctionFormatUtil.formatCurrencyWithName(currentValue)); + + y += 11; + if (selectedListing.hasBuyout()) { + drawDetailsLine(panelLeft + 24, panelRight, y, EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.info.buyoutPrice"), + EnumChatFormatting.GREEN + AuctionFormatUtil.formatCurrencyWithName(selectedListing.buyoutPrice)); + y += 11; + } + + drawDetailsLine(panelLeft + 24, panelRight, y, EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.info.bidCount"), + EnumChatFormatting.WHITE + Integer.toString(selectedListing.bidCount)); + y += 11; + + long remaining = selectedListing.getTimeRemaining(); + EnumChatFormatting timeColor = AuctionFormatUtil.isTimeUrgent(remaining) ? EnumChatFormatting.RED : EnumChatFormatting.WHITE; + drawDetailsLine(panelLeft + 24, panelRight, y, EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.info.timeLeft"), + timeColor + AuctionFormatUtil.formatTimeRemaining(remaining)); + } + + private void drawDetailsLine(int leftX, int rightX, int y, String label, String value) { + fontRendererObj.drawString(label, leftX, y, 0xFFFFFF); + int width = fontRendererObj.getStringWidth(value); + fontRendererObj.drawString(value, rightX - width, y, 0xFFFFFF); + } + + private int getHoveredDisplaySlot(int mouseX, int mouseY) { + int relX = mouseX - ContainerManageAuction.DISPLAY_X; + int relY = mouseY - ContainerManageAuction.DISPLAY_Y; + if (relX < 0 || relY < 0) return -1; + + int col = relX / 18; + int row = relY / 18; + if (col < 0 || col >= ContainerManageAuction.DISPLAY_COLS || row < 0 || row >= ContainerManageAuction.DISPLAY_ROWS) return -1; + return col + row * ContainerManageAuction.DISPLAY_COLS; + } + + private void drawDynamicTooltip(int mouseX, int mouseY) { + if (hasSubGui()) return; + + // GuiContainer foreground receives absolute mouse coords; convert to GUI-local like auction GUIs. + int guiMouseX = mouseX - guiLeft; + int guiMouseY = mouseY - guiTop; + int hovered = getHoveredDisplaySlot(guiMouseX, guiMouseY); + + List tooltip = new ArrayList(); + + if (hovered >= 0) { + if (hovered == pendingDisplaySlot && pendingAction != PendingAction.NONE) { + if (pendingAction == PendingAction.STOP) { + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("global.auction.manage.confirm.stop")); + } else if (pendingAction == PendingAction.CANCEL) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("global.auction.manage.confirm.cancel")); + } else { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("global.auction.manage.confirm.claim")); + } + tooltip.add(EnumChatFormatting.DARK_GRAY + StatCollector.translateToLocal("auction.trades.otherClickCancel")); + } else if (activeTab == Tab.CLAIMS) { + AuctionClaim claim = manageContainer.getClaimAtDisplay(hovered); + if (claim != null && claim.item == null) { + if (claim.type == EnumClaimType.CURRENCY) { + tooltip.add(EnumChatFormatting.GREEN + "Sold Auction 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))); + } else if (claim.type == EnumClaimType.REFUND) { + tooltip.add(EnumChatFormatting.RED + "Outbid 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))); + } else if (claim.type.isCurrency()) { + tooltip.add(EnumChatFormatting.GOLD + String.format("%,d", claim.currency) + " " + AuctionClientConfig.getCurrencyName()); + } + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim")); + } + } + } + + if (!tooltip.isEmpty()) { + drawHoveringText(tooltip, guiMouseX, guiMouseY, fontRendererObj); + } + } + + public ContainerManageAuction getManageContainer() { + return manageContainer; + } + + public Tab getActiveTab() { + return activeTab; + } + + @Override + public void save() { + } +} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java index ecdec2990..9a0fa727d 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java @@ -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"); } } diff --git a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java index eaadbbc7b..fda6547c5 100644 --- a/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java +++ b/src/main/java/noppes/npcs/client/gui/hud/ability/AbilityHotbarComponent.java @@ -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); + } } diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java index b055cbf43..36370eb92 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java @@ -247,7 +247,7 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { NoppesUtil.requestOpenGUI(EnumGuiType.ManageAbilities); } if (id == 22) { - // NoppesUtil.requestOpenGUI(EnumGuiType.ManageAuction); + NoppesUtil.requestOpenGUI(EnumGuiType.ManageAuction); } } diff --git a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java index f07cb5ac6..b61c1e1b7 100644 --- a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -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 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 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")); + } + } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 658cfc022..7d8b7f072 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -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); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java index 4ebafd54f..2c0d51a18 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java @@ -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 lines = TextSplitter.splitText(displayString, 30); + List lines = splitHoverText(displayString, 30); drawHoveringText(lines, i, j, mc); GL11.glDisable(GL11.GL_LIGHTING); GL11.glPopMatrix(); } } + private List splitHoverText(String text, int maxLineLength) { + List lines = new ArrayList(); + 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; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java index 1d82deb61..71b5ed611 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java @@ -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 lines = TextSplitter.splitText(hoverableText, 30); - drawHoveringText(lines, mouseX, mouseY, mc); + List lines = splitHoverText(hoverableText, 30); + drawHoveringText(lines, tooltipMouseX, tooltipMouseY, mc); GL11.glDisable(GL11.GL_LIGHTING); GL11.glPopMatrix(); } } + private List splitHoverText(String text, int maxLineLength) { + List lines = new ArrayList(); + 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; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java index ab6e612d4..8a20a635c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java @@ -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); diff --git a/src/main/java/noppes/npcs/containers/ContainerManageAuction.java b/src/main/java/noppes/npcs/containers/ContainerManageAuction.java new file mode 100644 index 000000000..1b6b05684 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerManageAuction.java @@ -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 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 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 claims, List listings) { + displayMode = DisplayMode.CLAIMS; + clearHiddenDisplaySlot(); + displayInventory.clear(); + + List soldClaims = new ArrayList(); + List outbidClaims = new ArrayList(); + List wonClaims = new ArrayList(); + List expiredClaims = new ArrayList(); + 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 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(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 7ea79baeb..dfa64b4ac 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -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 listings = new ConcurrentHashMap<>(); - // Claims are now stored in PlayerData.tradeData, not here + // Shared global claims for admin-managed auction actions + private final List 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 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 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 getGlobalClaimsSnapshot() { + List 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 getAllGlobalClaims() { + List 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 getActiveGlobalListings() { + List 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 getGlobalClaims(int page, int pageSize) { + List 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 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); } diff --git a/src/main/java/noppes/npcs/controllers/SpawnController.java b/src/main/java/noppes/npcs/controllers/SpawnController.java index 7a04d788d..ce9d784e4 100644 --- a/src/main/java/noppes/npcs/controllers/SpawnController.java +++ b/src/main/java/noppes/npcs/controllers/SpawnController.java @@ -223,11 +223,14 @@ public class SpawnController implements INaturalSpawnsHandler { } public INaturalSpawn[] getSpawns(String biome) { - return getSpawnList(biome).toArray(new INaturalSpawn[]{}); + List 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); } diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java index 6d57942c1..93fc70674 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -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) { diff --git a/src/main/java/noppes/npcs/controllers/data/SpawnData.java b/src/main/java/noppes/npcs/controllers/data/SpawnData.java index f82d9b225..164f451da 100644 --- a/src/main/java/noppes/npcs/controllers/data/SpawnData.java +++ b/src/main/java/noppes/npcs/controllers/data/SpawnData.java @@ -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 biomes = new ArrayList<>(); public HashSet 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; + } } diff --git a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java index 8c062fdda..6e5923e7d 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCFlying.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCFlying.java @@ -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(); } diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 24579e4ab..a3dcfb84c 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -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; } diff --git a/src/main/java/noppes/npcs/mixin/MixinItemStack.java b/src/main/java/noppes/npcs/mixin/MixinItemStack.java index 4e82147f1..051bc4b46 100644 --- a/src/main/java/noppes/npcs/mixin/MixinItemStack.java +++ b/src/main/java/noppes/npcs/mixin/MixinItemStack.java @@ -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> cir) { boolean override = false; - if (Minecraft.getMinecraft().currentScreen instanceof GuiAuctionInterface) { + if (Minecraft.getMinecraft().currentScreen instanceof GuiAuctionInterface || Minecraft.getMinecraft().currentScreen instanceof GuiNpcManageAuction) { override = true; } diff --git a/src/main/java/noppes/npcs/mixin/MixinModelRenderer.java b/src/main/java/noppes/npcs/mixin/MixinModelRenderer.java index e38805870..603b856b6 100644 --- a/src/main/java/noppes/npcs/mixin/MixinModelRenderer.java +++ b/src/main/java/noppes/npcs/mixin/MixinModelRenderer.java @@ -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; } } } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java index a015a06ad..fe02608e1 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyBarrier.java @@ -121,6 +121,22 @@ public class ScriptEnergyBarrier 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; } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java index de9c8036e..db810848e 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptEnergyProjectile.java @@ -177,6 +177,14 @@ public class ScriptEnergyProjectile extends Sc entity.setMultiHitDelayTicks(delayTicks); } + public int getMaxHits() { + return entity.getMaxHits(); + } + + public void setMaxHits(int maxHits) { + entity.setMaxHits(maxHits); + } + // ==================== MOVEMENT ==================== public float getSpeed() { diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java index 25734b7e4..c362d72c6 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java @@ -1109,6 +1109,7 @@ public class ScriptNpc extends ScriptLiving imp public void setNaturallyDespawns(boolean canDespawn) { npc.stats.canDespawn = canDespawn; npc.stats.playerSetCanDespawn = canDespawn; + npc.syncDespawnPersistence(); } public boolean spawnedFromSoulStone() { diff --git a/src/main/java/noppes/npcs/util/MathUtil.java b/src/main/java/noppes/npcs/util/MathUtil.java new file mode 100644 index 000000000..cfaec4e2a --- /dev/null +++ b/src/main/java/noppes/npcs/util/MathUtil.java @@ -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); + } +} diff --git a/src/main/resources/META-INF/customnpcs_at.cfg b/src/main/resources/META-INF/customnpcs_at.cfg index dbcb46481..a41eb23c9 100644 --- a/src/main/resources/META-INF/customnpcs_at.cfg +++ b/src/main/resources/META-INF/customnpcs_at.cfg @@ -18,6 +18,8 @@ public net.minecraft.client.renderer.entity.RenderPlayer func_77043_a(Lnet/minec #EntityLivingBase public net.minecraft.entity.EntityLivingBase func_82166_i()I # getArmSwingAnimationEnd public net.minecraft.entity.EntityLivingBase field_70703_bu # isJumping +#EntityLiving +public net.minecraft.entity.EntityLiving field_82179_bU # persistenceRequired # Minecraft public net.minecraft.client.Minecraft field_147126_aw # mcMusicTicker # MusicTicker diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 90d877011..ff1899b19 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -425,7 +425,37 @@ spawning.liquidSpawning=Spawns in Liquid spawning.airSpawning=Spawns in Air spawning.minHeight=Minimum Height spawning.maxHeight=Maximum Height +spawning.maxAlive=Max Alive (0 = Unlimited) +spawning.cooldownTicks=Cooldown (Ticks) +spawning.attemptsPerCycle=Attempts Per Cycle +spawning.playerMinDistance=Min Player Distance +spawning.despawnMode=Despawn Mode +spawning.despawn.forceNatural=Force Natural +spawning.despawn.preserve=Preserve Template +spawning.despawn.forcePersistent=Force Persistent spawning.naturalInfo=Add at least one spawn entry and configure spawn options for the NPC to spawn naturally +spawning.naturalInfoPerformance=Natural Spawn Setup\nBest performance baseline:\n- maxAlive: 20-60\n- cooldownTicks: 200-600\n- attemptsPerCycle: 1-2\n- playerMinDistance: 32-48\n- despawnMode: Force Natural +tooltip.naturalspawns.main.addProfile=Create a new natural spawn profile.\nBest performance: Keep profile count focused by biome/theme. +tooltip.naturalspawns.main.removeProfile=Delete the selected natural spawn profile.\nBest performance: Remove unused profiles to reduce selection overhead. +tooltip.naturalspawns.main.search=Filter spawn profiles by name.\nTip: Use biome/type prefixes for faster maintenance. +tooltip.naturalspawns.main.biomes=Choose biomes this profile can spawn in.\nBest performance: Use only needed biomes. +tooltip.naturalspawns.main.dimensions=Choose dimensions this profile can spawn in.\nBest performance: Limit to required dimensions. +tooltip.naturalspawns.main.options=Open advanced spawn controls.\nBest performance: Configure cap, cooldown, attempts, and despawn mode. +tooltip.naturalspawns.main.addEntry=Add an entry slot to this profile.\nBest performance: Keep only active entries. +tooltip.naturalspawns.main.removeEntry=Remove selected entry slot.\nBest performance: Remove empty/duplicate entries. +tooltip.naturalspawns.main.slot=Entry slot index.\nTip: Keep numbering organized for easier debugging. +tooltip.naturalspawns.main.selectEntry=Select which clone/NPC this slot spawns.\nBest performance: Avoid very heavy scripted clones in high-frequency profiles. +tooltip.naturalspawns.options.animal=Spawn Rule: Animal\nUses animal-style spawn checks (surface + creature-valid blocks).\nUse for passive/overworld mobs.\nPerformance: Disable if not needed. +tooltip.naturalspawns.options.monster=Spawn Rule: Monster\nUses monster-style spawn checks (surface + monster-valid blocks).\nUse for hostile/night/cave mobs.\nPerformance: Disable if not needed. +tooltip.naturalspawns.options.liquid=Spawn Rule: Liquid\nAllows spawn validation in liquid/water spaces.\nUse only for aquatic entries.\nPerformance: Keep OFF unless required. +tooltip.naturalspawns.options.air=Spawn Rule: Air\nAllows spawn validation in open-air columns.\nUse only for flying/airborne entries.\nPerformance: Keep OFF unless required. +tooltip.naturalspawns.options.minHeight=Minimum Spawn Y\nLower vertical bound for this profile.\nUse with Max Height to narrow valid layers.\nPerformance: Tighter ranges = fewer failed checks. +tooltip.naturalspawns.options.maxHeight=Maximum Spawn Y\nUpper vertical bound for this profile.\nUse with Min Height to target a specific layer.\nPerformance: Tighter ranges = fewer failed checks. +tooltip.naturalspawns.options.maxAlive=Max Alive (Per Profile)\nCounts loaded entities tagged to this natural spawn profile.\n0 = Unlimited (not recommended).\nPerformance baseline: 20-60. +tooltip.naturalspawns.options.cooldownTicks=Cooldown (Ticks)\nWait time after a successful spawn before this profile can spawn again.\n20 ticks = 1 second.\nPerformance baseline: 200-600 (10-30 seconds). +tooltip.naturalspawns.options.attemptsPerCycle=Attempts Per Cycle\nHow many nearby positions this profile tests each cycle.\nHigher values increase spawn pressure and CPU checks.\nPerformance baseline: 1-2. +tooltip.naturalspawns.options.playerMinDistance=Min Player Distance\nBlocks from nearest player where spawning is blocked.\nHigher values reduce pop-in near players.\nPerformance baseline: 32-48. +tooltip.naturalspawns.options.despawnMode=Despawn Mode (CNPC entries)\nForce Natural: sets canDespawn=true on spawn. Recommended for natural ecosystems.\nPreserve Template: keeps clone/template despawn setting.\nForce Persistent: sets canDespawn=false (NPC stays loaded longer).\nPerformance recommendation: Force Natural. role.name=Role role.none=No Role @@ -1789,6 +1819,7 @@ ability.hitType.single=Single ability.hitType.pierce=Pierce ability.hitType.multi=Multi ability.multiHitDelay=Multi-Hit Delay +ability.maxHits=Max Hits ability.distance=Distance ability.maxDistance=Max Distance ability.lifetime=Lifetime @@ -1997,8 +2028,9 @@ ability.hover.windUpColor=Color of telegraph during charge-up phase ability.hover.activeColor=Color of telegraph when ability is active ability.hover.homing=Projectile tracks toward target ability.hover.homingStr=How aggressively the projectile tracks (higher = tighter turns) -ability.hover.hitType=Single: stops on first hit. Pierce: can hit each entity once. Multi: can re-hit entities using the delay below. +ability.hover.hitType=Single: stops on first hit. Pierce: can hit each entity once. Multi: can re-hit entities using the delay below. Pierce/Multi both respect Max Hits. ability.hover.multiHitDelay=Ticks between repeat hits on the same entity when Hit Type is Multi (20 ticks = 1 second) +ability.hover.maxHits=Maximum total hits before projectile ends in Pierce/Multi modes. Lower values reduce repeated hit spam and improve performance. ability.hover.explosive=Creates explosion on impact ability.hover.explosionRad=Size of explosion radius ability.hover.piercing=Hits multiple targets instead of stopping on first hit @@ -2053,11 +2085,18 @@ ability.solid=Solid ability.knockbackEnabled=Knockback ability.knockbackStrength=Knockback Strength ability.absorbing=Absorbing +ability.reflect=Reflect +ability.reflectStrength=Strength (%) ability.meleeEnabled=Allows Melee ability.meleeDamageMultiplier=Melee Multiplier ability.followCaster=Follow Caster ability.hover.followCaster=Dome stays centered on the caster as they move +ability.hover.solid=Entities cannot pass through the barrier boundary +ability.hover.knockbackEnabled=Repulsion field pushing entities away from barrier surface +ability.hover.absorbing=Damage dealt to the caster is redirected to the barrier's health +ability.hover.reflect=Rebounds non-breaking projectiles and reduces their damage by Strength +ability.hover.meleeEnabled=Barrier can be hit and damaged by melee attacks ability.variant.small=Small @@ -2298,3 +2337,33 @@ auction.bid.yourBalance=Your Balance: %s # Auction Buyout SubGui auction.buyout.title=Buy Now auction.buyout.confirm=Purchase this item for: + +# Global Auction Management GUI +global.auction.manage.title=Global Auction Management +global.auction.manage.listings=Listings +global.auction.manage.create=Create +global.auction.manage.claims=Claims +global.auction.manage.sort=Sort +global.auction.manage.search=Search +global.auction.manage.page=Page +global.auction.manage.hint.listings=Left-click: open listing details +global.auction.manage.hint.listings2=Right-click: stop player | Shift-right: cancel to global +global.auction.manage.hint.stop=Right-click: stop player +global.auction.manage.hint.cancel=Shift-right-click: cancel to global +global.auction.manage.hint.claims=Left-click twice to claim global entries +global.auction.manage.details=Listing Details +global.auction.manage.selectListing=Left-click a listing to view details +global.auction.manage.seller=Seller Name +global.auction.manage.startPrice=Start Price +global.auction.manage.buyoutPrice=Buyout Price +global.auction.manage.durationHours=Duration Hours +global.auction.manage.stagedItem=Staged Item +global.auction.manage.create.submit=Create Fake Auction +global.auction.manage.hint.create=Use right-panel inventory to stage items +global.auction.manage.confirm=Click again to confirm +global.auction.manage.confirm.stop=Right-click again to confirm stop player +global.auction.manage.confirm.cancel=Shift-right-click again to confirm cancel to global +global.auction.manage.confirm.claim=Left-click again to confirm claim +global.auction.manage.error.noItem=Stage an item first +global.auction.manage.error.invalidStart=Enter a valid starting price +global.auction.manage.error.invalidBuyout=Buyout must be zero or >= start diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon_blue.png b/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon_blue.png new file mode 100644 index 000000000..9f4c340c1 Binary files /dev/null and b/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon_blue.png differ