diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java index 4180783bf..cce9bbe58 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java @@ -2,6 +2,7 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.renderer.OpenGlHelper; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.entity.Render; import net.minecraft.entity.Entity; @@ -26,6 +27,7 @@ public abstract class RenderAbilityProjectile extends Render { /** * Setup GL state for translucent, self-illuminated rendering. + * Forces full brightness so world lighting does not affect projectiles. */ protected void setupRenderState() { GL11.glPushMatrix(); @@ -37,6 +39,9 @@ public abstract class RenderAbilityProjectile extends Render { GL11.glAlphaFunc(GL11.GL_GREATER, 0.003921569F); GL11.glDisable(GL11.GL_LIGHTING); GL11.glDisable(GL11.GL_TEXTURE_2D); + + // Force full brightness - prevents world lighting from darkening projectiles + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0f, 240.0f); } /** diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java index 0a24f1773..10705d762 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java @@ -3,6 +3,7 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.entity.EntityAbilitySweeper; +import net.minecraft.client.renderer.OpenGlHelper; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.entity.Render; import net.minecraft.entity.Entity; @@ -52,6 +53,9 @@ public class RenderAbilitySweeper extends Render { GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); GL11.glDisable(GL11.GL_LIGHTING); GL11.glShadeModel(GL11.GL_SMOOTH); + + // Force full brightness - prevents world lighting from darkening sweeper + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0f, 240.0f); } private void restoreRenderState() { diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java index 5f4e815b7..3b5e49e2d 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java @@ -257,4 +257,83 @@ public class TelegraphRenderer { tessellator.draw(); } + + // ==================== GUI PREVIEW RENDERING ==================== + + /** + * Render a telegraph in GUI space for ability preview. + * This is used by the Manage Abilities GUI to show telegraph shapes. + * + * @param instance The telegraph instance to render + * @param centerX X center position in GUI space + * @param centerY Y center position in GUI space + * @param centerZ Z center position in GUI space + * @param scale Scale factor for the telegraph + * @param partialTicks Partial ticks for interpolation + */ + public void renderTelegraphInGUI(TelegraphInstance instance, + double centerX, double centerY, double centerZ, + float scale, float partialTicks) { + if (instance == null) return; + + Telegraph telegraph = instance.getTelegraph(); + if (telegraph == null || telegraph.getType() == TelegraphType.NONE) { + return; + } + + // Get animated color + int color = instance.getAnimatedColor(partialTicks); + float alpha = ((color >> 24) & 0xFF) / 255.0f; + float red = ((color >> 16) & 0xFF) / 255.0f; + float green = ((color >> 8) & 0xFF) / 255.0f; + float blue = (color & 0xFF) / 255.0f; + + // Setup GL state + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDepthMask(false); + + GL11.glTranslated(centerX, centerY, centerZ); + GL11.glScalef(scale, scale, scale); + + // Rotate for directional shapes + GL11.glRotatef(-instance.getInterpolatedYaw(partialTicks), 0, 1, 0); + + // Render based on type + switch (telegraph.getType()) { + case CIRCLE: + renderCircle(telegraph.getRadius(), red, green, blue, alpha); + break; + case RING: + renderRing(telegraph.getRadius(), telegraph.getInnerRadius(), red, green, blue, alpha); + break; + case LINE: + renderLine(telegraph.getLength(), telegraph.getWidth(), red, green, blue, alpha); + break; + case CONE: + renderCone(telegraph.getLength(), telegraph.getAngle(), red, green, blue, alpha); + break; + case POINT: + renderPoint(red, green, blue, alpha); + break; + default: + break; + } + + // Restore GL state + GL11.glDepthMask(true); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_TEXTURE_2D); + + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } } 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 08eed148b..eb59edf7c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -26,6 +26,8 @@ import noppes.npcs.scripted.event.AbilityEvent; import java.util.ArrayList; import java.util.List; +import net.minecraft.entity.Entity; + public abstract class Ability implements IAbility { // ═══════════════════════════════════════════════════════════════════ @@ -636,6 +638,93 @@ public abstract class Ability implements IAbility { // Override in subclasses to clean up spawned entities, etc. } + // ═══════════════════════════════════════════════════════════════════ + // CLIENT-SIDE PREVIEW METHODS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Get total active phase duration for preview (in ticks). + * Override in subclasses to provide accurate preview duration. + * + * @return Duration of active phase in ticks (default: 40 = 2 seconds) + */ + public int getPreviewActiveDuration() { + return 40; + } + + /** + * Called during preview windup tick (client-side only). + * Override for visual updates during windup preview. + * Do NOT apply damage or world effects here. + * + * @param npc The preview NPC + * @param tick Current tick within windup phase + */ + @SideOnly(Side.CLIENT) + public void onPreviewWindUpTick(EntityNPCInterface npc, int tick) { + // Default: no-op. Override in subclasses for visual effects. + } + + /** + * Called during preview active tick (client-side only). + * Override for visual updates during active preview. + * Do NOT apply damage or world effects here. + * + * @param npc The preview NPC + * @param tick Current tick within active phase + */ + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + // Default: no-op. Override in subclasses for visual effects. + } + + /** + * Create a preview entity for GUI display (client-side only). + * The entity should be in preview mode and not apply damage. + * Override in abilities that spawn entities (Beam, Orb, Disc, etc.) + * + * @param npc The preview NPC + * @return Preview entity, or null if ability doesn't spawn entities + */ + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + return null; // Override in entity-spawning abilities + } + + /** + * Whether to spawn preview entity during WINDUP phase (true) or ACTIVE phase (false). + * Most abilities (Orb, Beam, Disc) spawn during windup for charging effect. + * Laser spawns at active phase since it has no charging state. + */ + public boolean spawnPreviewDuringWindup() { + return true; // Default: spawn during windup for charging + } + + // ── Preview target (set by executor for abilities that need targeting) ── + + protected transient EntityLivingBase previewTarget; + + /** + * Set the fake target entity for preview mode. + * Called by AbilityPreviewExecutor before starting preview. + */ + public void setPreviewTarget(EntityLivingBase target) { + this.previewTarget = target; + } + + public EntityLivingBase getPreviewTarget() { + return previewTarget; + } + + /** + * Called once when transitioning from WINDUP to ACTIVE in preview mode. + * Override in movement-based abilities to initiate movement. + */ + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + // Default: no-op. Override in movement abilities. + } + // ═══════════════════════════════════════════════════════════════════ // CONDITION CHECKING // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 1f6f31250..25941e0a5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -288,6 +288,69 @@ public class AbilityCharge extends Ability implements IAbilityCharge { return instance; } + // ==================== PREVIEW MODE ==================== + + private transient double previewDirX, previewDirZ; + private transient double previewStartX, previewStartZ; + private transient boolean previewCharging = false; + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + previewStartX = npc.posX; + previewStartZ = npc.posZ; + previewCharging = false; + + // Calculate direction toward fake target + if (previewTarget != null) { + double dx = previewTarget.posX - npc.posX; + double dz = previewTarget.posZ - npc.posZ; + double len = Math.sqrt(dx * dx + dz * dz); + if (len > 0) { + previewDirX = dx / len; + previewDirZ = dz / len; + } else { + float yaw = (float) Math.toRadians(npc.rotationYaw); + previewDirX = -Math.sin(yaw); + previewDirZ = Math.cos(yaw); + } + } else { + float yaw = (float) Math.toRadians(npc.rotationYaw); + previewDirX = -Math.sin(yaw); + previewDirZ = Math.cos(yaw); + } + previewCharging = true; + } + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + if (!previewCharging) return; + + // Check distance traveled + double distTraveled = Math.sqrt( + Math.pow(npc.posX - previewStartX, 2) + + Math.pow(npc.posZ - previewStartZ, 2) + ); + + if (distTraveled >= maxRange) { + previewCharging = false; + return; + } + + npc.prevPosX = npc.posX; + npc.prevPosY = npc.posY; + npc.prevPosZ = npc.posZ; + + npc.posX += previewDirX * chargeSpeed; + npc.posZ += previewDirZ * chargeSpeed; + } + + @Override + public int getPreviewActiveDuration() { + return (int) Math.ceil(maxRange / chargeSpeed) + 5; + } + @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("chargeSpeed", chargeSpeed); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 2c03de4c2..054176a7b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -224,6 +224,69 @@ public class AbilityDash extends Ability implements IAbilityDash { return !npc.worldObj.getCollidingBoundingBoxes(npc, npc.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); } + // ==================== PREVIEW MODE ==================== + + private transient double previewDirX, previewDirZ; + private transient double previewStartX, previewStartZ; + private transient boolean previewDashing = false; + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + previewStartX = npc.posX; + previewStartZ = npc.posZ; + previewDashing = false; + + // Pick a random direction based on mode + DashDirection[] directions = dashMode == DashMode.AGGRESSIVE + ? AGGRESSIVE_DIRECTIONS + : DEFENSIVE_DIRECTIONS; + DashDirection dir = directions[RANDOM.nextInt(directions.length)]; + + float baseYaw; + if (previewTarget != null) { + double dx = previewTarget.posX - npc.posX; + double dz = previewTarget.posZ - npc.posZ; + baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + } else { + baseYaw = npc.rotationYaw; + } + + float dashYaw = baseYaw + dir.getAngleOffset(); + float yawRad = (float) Math.toRadians(dashYaw); + previewDirX = -Math.sin(yawRad); + previewDirZ = Math.cos(yawRad); + previewDashing = true; + } + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + if (!previewDashing) return; + + double distTraveled = Math.sqrt( + Math.pow(npc.posX - previewStartX, 2) + + Math.pow(npc.posZ - previewStartZ, 2) + ); + + if (distTraveled >= dashDistance) { + previewDashing = false; + return; + } + + npc.prevPosX = npc.posX; + npc.prevPosY = npc.posY; + npc.prevPosZ = npc.posZ; + + npc.posX += previewDirX * dashSpeed; + npc.posZ += previewDirZ * dashSpeed; + } + + @Override + public int getPreviewActiveDuration() { + return (int) Math.ceil(dashDistance / dashSpeed) + 5; + } + @Override public float getTelegraphRadius() { return 0; // No telegraph for dash diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index e783b3a42..e5c15e010 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -332,4 +332,23 @@ public class AbilityDisc extends Ability implements IAbilityDisc { @Override public void setAnchorPoint(int point) { this.anchorPoint = AnchorPoint.fromId(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + return EntityAbilityDisc.createPreview( + npc.worldObj, npc, + discRadius, discThickness, innerColor, outerColor, + outerColorEnabled, outerColorWidth, rotationSpeed, + lightningEffect, lightningDensity, lightningRadius, + anchorPoint, windUpTicks + ); + } + + @Override + public int getPreviewActiveDuration() { + return maxLifetime > 0 ? Math.min(maxLifetime, 100) : 100; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 7a81da548..c2b452339 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -329,4 +329,23 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { @Override public void setAnchorPoint(int point) { this.anchorPoint = AnchorPoint.fromId(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + return EntityAbilityBeam.createPreview( + npc.worldObj, npc, + beamWidth, headSize, innerColor, outerColor, + outerColorEnabled, outerColorWidth, rotationSpeed, + lightningEffect, lightningDensity, lightningRadius, + anchorPoint, windUpTicks, 1.0f + ); + } + + @Override + public int getPreviewActiveDuration() { + return maxLifetime > 0 ? Math.min(maxLifetime, 100) : 100; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 7ead64c8e..c1a94e657 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -7,6 +7,7 @@ import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityLaser; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -256,4 +257,28 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + return EntityAbilityLaser.createPreview( + npc.worldObj, npc, + laserWidth, innerColor, outerColor, + outerColorEnabled, outerColorWidth, + expansionSpeed, maxDistance, + lightningEffect, lightningDensity, lightningRadius + ); + } + + @Override + public int getPreviewActiveDuration() { + return maxLifetime > 0 ? Math.min(maxLifetime, 60) : 60; + } + + @Override + public boolean spawnPreviewDuringWindup() { + return false; // Laser has no charging, spawns at active phase + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 4ccbdcf12..13c57478a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -487,4 +487,23 @@ public class AbilityOrb extends Ability implements IAbilityOrb { public void setAnchorPoint(int point) { this.anchorPoint = AnchorPoint.fromId(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + return EntityAbilityOrb.createPreview( + npc.worldObj, npc, + orbSize, innerColor, outerColor, + outerColorEnabled, outerColorWidth, rotationSpeed, + lightningEffect, lightningDensity, lightningRadius, lightningFadeTime, + anchorPoint, windUpTicks + ); + } + + @Override + public int getPreviewActiveDuration() { + return maxLifetime > 0 ? Math.min(maxLifetime, 100) : 100; + } } 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 b9da81dc5..a46100c96 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 @@ -361,6 +361,100 @@ public class AbilitySlam extends Ability implements IAbilitySlam { airTicks = 0; } + // ==================== PREVIEW MODE ==================== + + private transient double previewVelX, previewVelY, previewVelZ; + private transient boolean previewLaunched = false; + private transient double previewGroundY; + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + previewLaunched = false; + + double tx, ty, tz; + if (previewTarget != null && targetingMode == TargetingMode.AOE_TARGET) { + tx = previewTarget.posX; + ty = previewTarget.posY; + tz = previewTarget.posZ; + } else { + tx = npc.posX; + ty = npc.posY; + tz = npc.posZ; + } + + previewGroundY = npc.posY; + + double dx = tx - npc.posX; + double dz = tz - npc.posZ; + double horizontalDist = Math.sqrt(dx * dx + dz * dz); + + if (horizontalDist < 0.5) { + previewVelX = 0; + previewVelZ = 0; + previewVelY = 0.8 * leapSpeed; + } else { + if (horizontalDist > maxRange) { + double scale = maxRange / horizontalDist; + dx *= scale; + dz *= scale; + horizontalDist = maxRange; + } + + int flightTicks = (int) Math.max(15, Math.min(horizontalDist * 1.5, 30)); + double drag = 0.91; + double dragPowN = Math.pow(drag, flightTicks); + double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN) * leapSpeed; + + double arcHeight = Math.max(1.0, leapHeight) * leapSpeed; + double peakTicks = flightTicks * 0.4; + double vy = (arcHeight * 2.0 / peakTicks) + (0.08 * peakTicks * 0.5); + vy = Math.max(vy, 0.6 * leapSpeed); + + double dirX = dx / horizontalDist; + double dirZ = dz / horizontalDist; + previewVelX = dirX * vHorizontal; + previewVelZ = dirZ * vHorizontal; + previewVelY = vy; + } + previewLaunched = true; + } + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + if (!previewLaunched) return; + + // Apply velocity + npc.prevPosX = npc.posX; + npc.prevPosY = npc.posY; + npc.prevPosZ = npc.posZ; + + npc.posX += previewVelX; + npc.posY += previewVelY; + npc.posZ += previewVelZ; + + // Gravity and drag + previewVelY -= 0.08; + previewVelY *= 0.98; + previewVelX *= 0.91; + previewVelZ *= 0.91; + + // Ground clamp - stop falling below starting Y + if (npc.posY < previewGroundY && previewVelY < 0) { + npc.posY = previewGroundY; + previewVelY = 0; + previewVelX = 0; + previewVelZ = 0; + previewLaunched = false; + } + } + + @Override + public int getPreviewActiveDuration() { + return 60; + } + @Override public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { // Check if telegraph should be shown diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 2e5926e67..c841ee0f9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -83,8 +83,12 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { /** * Check if beam is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. */ public boolean isCharging() { + if (previewMode) { + return this.charging; + } return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; } @@ -264,6 +268,103 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { return beam; } + /** + * Create a beam in preview mode for GUI display. + * Follows anchor point and animations like in the real game. + * Can be fired when transitioning to active phase. + */ + public static EntityAbilityBeam createPreview(World world, EntityNPCInterface owner, + float beamWidth, float headSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean lightningEffect, float lightningDensity, float lightningRadius, + AnchorPoint anchorPoint, int chargeDuration, float chargeOffsetDistance) { + EntityAbilityBeam beam = new EntityAbilityBeam(world); + beam.setPreviewMode(true); + beam.setPreviewOwner(owner); + + // Set visual properties + beam.beamWidth = beamWidth; + beam.headSize = headSize; + beam.size = headSize; + beam.innerColor = innerColor; + beam.outerColor = outerColor; + beam.outerColorEnabled = outerColorEnabled; + beam.outerColorWidth = outerColorWidth; + beam.rotationSpeed = rotationSpeed; + beam.lightningEffect = lightningEffect; + beam.lightningDensity = lightningDensity; + beam.lightningRadius = lightningRadius; + + // Set charging state + beam.setCharging(true); + beam.chargeDuration = chargeDuration; + beam.chargeTick = 0; + beam.chargeOffsetDistance = chargeOffsetDistance; + beam.anchorPoint = anchorPoint; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint, chargeOffsetDistance); + beam.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + beam.prevPosX = pos.xCoord; + beam.prevPosY = pos.yCoord; + beam.prevPosZ = pos.zCoord; + beam.startX = pos.xCoord; + beam.startY = pos.yCoord; + beam.startZ = pos.zCoord; + + // Initialize head offsets + beam.headOffsetX = 0; + beam.headOffsetY = 0; + beam.headOffsetZ = 0; + beam.prevHeadOffsetX = 0; + beam.prevHeadOffsetY = 0; + beam.prevHeadOffsetZ = 0; + + // Attach to owner for anchor following + beam.attachedToOwner = true; + beam.renderTailOrb = true; + + // Clear motion while charging + beam.motionX = 0; + beam.motionY = 0; + beam.motionZ = 0; + + return beam; + } + + /** + * Start preview firing (simulates firing toward a point in front of NPC). + */ + public void startPreviewFiring() { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Sync prev position to prevent visual jump on first frame + prevPosX = posX; + prevPosY = posY; + prevPosZ = posZ; + + // Fire forward based on owner facing direction + Entity owner = getOwner(); + if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = 0; // Fire horizontally + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } else { + motionX = speed; + motionY = 0; + motionZ = 0; + } + } + /** * Start the beam firing (exit charging mode). * Called by ability when windup ends. @@ -361,6 +462,12 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { ageTrailPoints(); } + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + if (worldObj.isRemote) { // Client: interpolate entity position handleClientInterpolation(); @@ -510,6 +617,37 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } } + /** + * Update movement in preview mode (client-side only, no damage). + */ + private void updatePreviewMovement() { + // Move head offset + headOffsetX += motionX; + headOffsetY += motionY; + headOffsetZ += motionZ; + + // Update origin if attached to owner + if (attachedToOwner) { + Entity owner = getOwner(); + if (owner != null) { + startX = owner.posX; + startY = owner.posY + owner.getEyeHeight() * 0.7; + startZ = owner.posZ; + } + } + + // Calculate world position of head + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + + // Update entity position to head + this.setPosition(headWorldX, headWorldY, headWorldZ); + + // Add trail point + addTrailPoint(); + } + private void updateHoming() { if (!homing) return; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index f63014158..e3896a839 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -61,8 +61,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { /** * Check if disc is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. */ public boolean isCharging() { + if (previewMode) { + return this.charging; + } return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; } @@ -209,6 +213,94 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return disc; } + /** + * Create a disc in preview mode for GUI display. + * Follows anchor point and animations like in the real game. + * Can be fired when transitioning to active phase. + */ + public static EntityAbilityDisc createPreview(World world, EntityNPCInterface owner, + float discRadius, float discThickness, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean lightningEffect, float lightningDensity, float lightningRadius, + AnchorPoint anchorPoint, int chargeDuration) { + EntityAbilityDisc disc = new EntityAbilityDisc(world); + disc.setPreviewMode(true); + disc.setPreviewOwner(owner); + + // Set visual properties + disc.innerColor = innerColor; + disc.outerColor = outerColor; + disc.outerColorEnabled = outerColorEnabled; + disc.outerColorWidth = outerColorWidth; + disc.rotationSpeed = rotationSpeed; + disc.lightningEffect = lightningEffect; + disc.lightningDensity = lightningDensity; + disc.lightningRadius = lightningRadius; + + // Set charging state + disc.setCharging(true); + disc.chargeDuration = chargeDuration; + disc.chargeTick = 0; + disc.anchorPoint = anchorPoint; + + // Store target size and start at 0 for grow effect + disc.targetDiscRadius = discRadius; + disc.targetDiscThickness = discThickness; + disc.discRadius = 0.01f; + disc.discThickness = 0.01f; + disc.size = 0.01f; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); + disc.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + disc.prevPosX = pos.xCoord; + disc.prevPosY = pos.yCoord; + disc.prevPosZ = pos.zCoord; + disc.startX = pos.xCoord; + disc.startY = pos.yCoord; + disc.startZ = pos.zCoord; + + // Clear motion while charging + disc.motionX = 0; + disc.motionY = 0; + disc.motionZ = 0; + + return disc; + } + + /** + * Start preview firing (simulates firing toward a point in front of NPC). + */ + public void startPreviewFiring() { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Sync prev position to prevent visual jump on first frame + prevPosX = posX; + prevPosY = posY; + prevPosZ = posZ; + + // Fire forward based on owner facing direction + Entity owner = getOwner(); + if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = 0; // Fire horizontally + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } else { + motionX = speed; + motionY = 0; + motionZ = 0; + } + } + /** * Start the disc moving (exit charging mode). * Called by ability when windup ends. @@ -261,6 +353,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return; } + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + if (worldObj.isRemote) { handleClientInterpolation(); } else { @@ -332,6 +430,16 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return distTraveled >= maxDistance; } + /** + * Update movement in preview mode (client-side only, no damage). + */ + private void updatePreviewMovement() { + // Simple movement - just apply motion + this.posX += motionX; + this.posY += motionY; + this.posZ += motionZ; + } + private void updateHoming() { if (!homing) return; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 7a1f97d0f..ec208e2cf 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -347,4 +347,56 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { nbt.setDouble("EndY", endY); nbt.setDouble("EndZ", endZ); } + + /** + * Create a laser in preview mode for GUI display. + * Laser doesn't have charging state - spawns at active phase and fires immediately. + */ + public static EntityAbilityLaser createPreview(World world, EntityNPCInterface owner, + float laserWidth, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, + float expansionSpeed, float maxDistance, + boolean lightningEffect, float lightningDensity, float lightningRadius) { + EntityAbilityLaser laser = new EntityAbilityLaser(world); + laser.setPreviewMode(true); + laser.setPreviewOwner(owner); + + // Set visual properties + laser.laserWidth = laserWidth; + laser.size = laserWidth; + laser.innerColor = innerColor; + laser.outerColor = outerColor; + laser.outerColorEnabled = outerColorEnabled; + laser.outerColorWidth = outerColorWidth; + laser.expansionSpeed = expansionSpeed; + laser.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview + laser.lightningEffect = lightningEffect; + laser.lightningDensity = lightningDensity; + laser.lightningRadius = lightningRadius; + + // Position at chest height + double x = owner.posX; + double y = owner.posY + owner.height * 0.7; + double z = owner.posZ; + laser.setPosition(x, y, z); + laser.prevPosX = x; + laser.prevPosY = y; + laser.prevPosZ = z; + laser.startX = x; + laser.startY = y; + laser.startZ = z; + + // Fire in owner's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + laser.dirX = -Math.sin(yaw); + laser.dirY = 0; + laser.dirZ = Math.cos(yaw); + + // Initialize end point (same as start, will expand) + laser.endX = x; + laser.endY = y; + laser.endZ = z; + + return laser; + } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 04e59a240..bad2bc955 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -50,8 +50,12 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { /** * Check if orb is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. */ public boolean isCharging() { + if (previewMode) { + return this.charging; + } return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; } @@ -188,6 +192,95 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { return orb; } + /** + * Create an orb in preview mode for GUI display. + * Follows anchor point and animations like in the real game. + * Can be fired when transitioning to active phase. + */ + public static EntityAbilityOrb createPreview(World world, EntityNPCInterface owner, + float orbSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime, + AnchorPoint anchorPoint, int chargeDuration) { + EntityAbilityOrb orb = new EntityAbilityOrb(world); + orb.setPreviewMode(true); + orb.setPreviewOwner(owner); + + // Set visual properties + orb.innerColor = innerColor; + orb.outerColor = outerColor; + orb.outerColorEnabled = outerColorEnabled; + orb.outerColorWidth = outerColorWidth; + orb.rotationSpeed = rotationSpeed; + orb.lightningEffect = lightningEffect; + orb.lightningDensity = lightningDensity; + orb.lightningRadius = lightningRadius; + orb.lightningFadeTime = lightningFadeTime; + + // Set charging state (like createCharging does) + orb.setCharging(true); + orb.chargeDuration = chargeDuration; + orb.chargeTick = 0; + orb.anchorPoint = anchorPoint; + + // Store target size and start at 0 for grow effect + orb.targetSize = orbSize; + orb.size = 0.01f; + orb.renderCurrentSize = 0.01f; + orb.prevRenderSize = 0.01f; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); + orb.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + orb.prevPosX = pos.xCoord; + orb.prevPosY = pos.yCoord; + orb.prevPosZ = pos.zCoord; + orb.startX = pos.xCoord; + orb.startY = pos.yCoord; + orb.startZ = pos.zCoord; + + // Clear motion while charging + orb.motionX = 0; + orb.motionY = 0; + orb.motionZ = 0; + + return orb; + } + + /** + * Start preview firing (simulates firing toward a point in front of NPC). + */ + public void startPreviewFiring() { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Sync prev position to prevent visual jump on first frame + prevPosX = posX; + prevPosY = posY; + prevPosZ = posZ; + + // Fire forward based on owner facing direction + Entity owner = getOwner(); + if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(0); // Fire horizontally + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } else { + // Default: fire forward (positive X in model space) + motionX = speed; + motionY = 0; + motionZ = 0; + } + } + /** * Start the orb moving (exit charging mode). * Called by ability when windup ends. @@ -232,6 +325,12 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { return; } + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + if (worldObj.isRemote) { handleClientInterpolation(); } else { @@ -254,6 +353,16 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { } } + /** + * Update movement in preview mode (client-side only, no damage). + */ + private void updatePreviewMovement() { + // Simple movement - just apply motion + this.posX += motionX; + this.posY += motionY; + this.posZ += motionZ; + } + private void updateHoming() { if (!homing) return; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 4b0f1e6a5..149ffd329 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -71,6 +71,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA // ==================== STATE ==================== protected boolean hasHit = false; + protected boolean previewMode = false; // Client-side preview mode (no damage/effects) + protected EntityNPCInterface previewOwner = null; // Direct reference for GUI preview (no world lookup) // ==================== ROTATION INTERPOLATION ==================== public float prevRotationValX, prevRotationValY, prevRotationValZ; @@ -202,7 +204,10 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.prevRotationValZ = this.rotationValZ; this.prevRenderSize = this.renderCurrentSize; - super.onUpdate(); + // Skip super.onUpdate() in preview mode to avoid world checks + if (!previewMode) { + super.onUpdate(); + } // Update rotation updateRotation(); @@ -210,26 +215,29 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA // Lerp render size toward actual size this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; - // Set death time on first tick if not already set (handles chunk load/unload) - if (deathWorldTime < 0 && worldObj != null) { - deathWorldTime = worldObj.getTotalWorldTime() + maxLifetime; - } + // Skip lifetime/distance checks in preview mode + if (!previewMode) { + // Set death time on first tick if not already set (handles chunk load/unload) + if (deathWorldTime < 0 && worldObj != null) { + deathWorldTime = worldObj.getTotalWorldTime() + maxLifetime; + } - // Check lifespan using world time (survives chunk unload/reload) - if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { - this.setDead(); - return; - } + // Check lifespan using world time (survives chunk unload/reload) + if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { + this.setDead(); + return; + } - // Check max distance (subclass can override if needed) - if (checkMaxDistance()) { - this.setDead(); - return; - } + // Check max distance (subclass can override if needed) + if (checkMaxDistance()) { + this.setDead(); + return; + } - if (hasHit) { - this.setDead(); - return; + if (hasHit) { + this.setDead(); + return; + } } // Subclass-specific update @@ -310,13 +318,32 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA } } + // ==================== PREVIEW MODE ==================== + + /** + * Set preview mode for GUI display. + * In preview mode, no damage or world effects are applied. + */ + public void setPreviewMode(boolean preview) { + this.previewMode = preview; + } + + /** + * Check if entity is in preview mode. + */ + public boolean isPreviewMode() { + return previewMode; + } + // ==================== DAMAGE & EFFECTS ==================== protected void applyDamage(EntityLivingBase target) { + if (previewMode) return; // Skip damage in preview mode applyDamage(target, this.damage, this.knockback); } protected void applyDamage(EntityLivingBase target, float dmg, float kb) { + if (previewMode) return; // Skip damage in preview mode Entity owner = getOwner(); EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null; @@ -353,6 +380,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA } protected void doExplosion() { + if (previewMode) return; // Skip explosion in preview mode worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f); Entity owner = getOwner(); @@ -384,10 +412,22 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA // ==================== ENTITY HELPERS ==================== protected Entity getOwner() { + // In preview mode, use direct reference (no world lookup) + if (previewMode && previewOwner != null) { + return previewOwner; + } if (ownerEntityId == -1) return null; return worldObj.getEntityByID(ownerEntityId); } + /** + * Set the preview owner for GUI preview mode. + * This allows anchor point calculations without world entity lookup. + */ + public void setPreviewOwner(EntityNPCInterface owner) { + this.previewOwner = owner; + } + protected Entity getTarget() { if (targetEntityId == -1) return null; return worldObj.getEntityByID(targetEntityId); diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index e659fef74..68c021559 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -92,6 +92,10 @@ import kamkeel.npcs.network.packets.request.TraderMarketSavePacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; @@ -501,6 +505,10 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new AbilitiesGetAllPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcGetPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcSavePacket()); + REQUEST_PACKET.registerPacket(new SavedAbilitiesGetPacket()); + REQUEST_PACKET.registerPacket(new SavedAbilityGetPacket()); + REQUEST_PACKET.registerPacket(new SavedAbilityRemovePacket()); + REQUEST_PACKET.registerPacket(new SavedAbilitySavePacket()); // Other Packets REQUEST_PACKET.registerPacket(new IsGuiOpenInform()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index bdd566785..d4968bc5c 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -198,6 +198,10 @@ public enum EnumRequestPacket { NpcAbilitiesGet, NpcAbilitiesSave, AbilitiesGetAll, + SavedAbilitiesGet, + SavedAbilityGet, + SavedAbilityRemove, + SavedAbilitySave, Hammer, RecipeScript, diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java new file mode 100644 index 000000000..0e8b547a3 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java @@ -0,0 +1,51 @@ +package kamkeel.npcs.network.packets.request.ability; + +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.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +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 noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to get list of all saved ability preset names. + */ +public final class SavedAbilitiesGetPacket extends AbstractPacket { + public static String packetName = "Request|SavedAbilitiesGet"; + + public SavedAbilitiesGetPacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.SavedAbilitiesGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java new file mode 100644 index 000000000..7bb2f1826 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java @@ -0,0 +1,68 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +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.nbt.NBTTagCompound; + +import java.io.IOException; + +/** + * Request packet to get a specific saved ability by name. + */ +public final class SavedAbilityGetPacket extends AbstractPacket { + public static String packetName = "Request|SavedAbilityGet"; + + private String abilityName; + + public SavedAbilityGetPacket() { + } + + public SavedAbilityGetPacket(String abilityName) { + this.abilityName = abilityName; + } + + @Override + public Enum getType() { + return EnumRequestPacket.SavedAbilityGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, abilityName); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String name = ByteBufUtils.readString(in); + Ability ability = AbilityController.Instance.getSavedAbility(name); + if (ability != null) { + NBTTagCompound compound = ability.writeNBT(); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java new file mode 100644 index 000000000..3fe4f5a0c --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java @@ -0,0 +1,69 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to remove a saved ability by name. + */ +public final class SavedAbilityRemovePacket extends AbstractPacket { + public static String packetName = "Request|SavedAbilityRemove"; + + private String abilityName; + + public SavedAbilityRemovePacket() { + } + + public SavedAbilityRemovePacket(String abilityName) { + this.abilityName = abilityName; + } + + @Override + public Enum getType() { + return EnumRequestPacket.SavedAbilityRemove; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.GLOBAL_ABILITY; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, abilityName); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String name = ByteBufUtils.readString(in); + AbilityController.Instance.deleteAbility(name); + NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java new file mode 100644 index 000000000..2cf2fa489 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java @@ -0,0 +1,76 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +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.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to save an ability preset. + */ +public final class SavedAbilitySavePacket extends AbstractPacket { + public static String packetName = "Request|SavedAbilitySave"; + + private NBTTagCompound abilityNBT; + + public SavedAbilitySavePacket() { + } + + public SavedAbilitySavePacket(NBTTagCompound abilityNBT) { + this.abilityNBT = abilityNBT; + } + + @Override + public Enum getType() { + return EnumRequestPacket.SavedAbilitySave; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.GLOBAL_ABILITY; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeNBT(out, abilityNBT); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + NBTTagCompound compound = ByteBufUtils.readNBT(in); + Ability ability = AbilityController.Instance.fromNBT(compound); + if (ability != null) { + AbilityController.Instance.saveAbility(ability); + NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, ability.writeNBT()); + } + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 0d758ad41..5e7c2924e 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -66,6 +66,7 @@ public class CustomNpcsPermissions { public static final Permission GLOBAL_NATURALSPAWN = new Permission("customnpcs.global.naturalspawn"); public static final Permission GLOBAL_TAG = new Permission("customnpcs.global.tag"); public static final Permission GLOBAL_ANIMATION = new Permission("customnpcs.global.animation"); + public static final Permission GLOBAL_ABILITY = new Permission("customnpcs.global.ability"); public static final Permission GLOBAL_MAGIC = new Permission("customnpcs.global.magic"); public static final Permission GLOBAL_EFFECT = new Permission("customnpcs.global.effect"); diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 6a33cb2c0..ae97f70b4 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -785,6 +785,18 @@ public class NoppesUtilServer { sendScrollData(player, map, EnumScrollData.ABILITY_TYPES); } + public static void sendSavedAbilitiesData(EntityPlayerMP player) { + HashMap map = new HashMap(); + kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance; + if (controller != null) { + int index = 0; + for (String name : controller.getSavedAbilityNames()) { + map.put(name, index++); + } + } + sendScrollData(player, map, EnumScrollData.ABILITIES); + } + public static DialogOption setNpcDialog(int slot, int dialogId, EntityPlayer player) throws IOException { return setNpcDialog(slot, dialogId, getEditingNpc(player)); } diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 9a0044191..01c226812 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -87,6 +87,7 @@ import noppes.npcs.client.gui.global.GuiNPCManageAnimations; import noppes.npcs.client.gui.global.GuiNPCManageBanks; import noppes.npcs.client.gui.global.GuiNPCManageDialogs; import noppes.npcs.client.gui.global.GuiNPCManageEffects; +import noppes.npcs.client.gui.global.GuiNpcManageAbilities; import noppes.npcs.client.gui.global.GuiNPCManageFactions; import noppes.npcs.client.gui.global.GuiNPCManageLinked; import noppes.npcs.client.gui.global.GuiNPCManageQuest; @@ -497,6 +498,20 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.ManageEffects) return new GuiNPCManageEffects(npc); + else if (gui == EnumGuiType.ManageAbilities) { + EntityCustomNpc abilityNpc; + if (npc != null) { + abilityNpc = new EntityCustomNpc(npc.worldObj); + abilityNpc.copyDataFrom(npc, true); + abilityNpc.display.showName = 1; + abilityNpc.display.showBossBar = 0; + } else { + abilityNpc = new EntityCustomNpc(Minecraft.getMinecraft().theWorld); + abilityNpc.display.texture = "customnpcs:textures/entity/humanmale/AnimationBody.png"; + } + return new GuiNpcManageAbilities(abilityNpc); + } + else if (gui == EnumGuiType.MainMenuGlobal) return new GuiNPCGlobalMainMenu(npc); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index 9abcebbe7..750fe282e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -281,6 +281,13 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void setGuiData(NBTTagCompound compound) { + // Only process NPC ability data, not saved ability preset data + // SubGuiAbilityLoad also implements IGuiData and may receive responses + // that get forwarded here - ignore those by checking for expected keys + if (!compound.hasKey("Abilities")) { + return; + } + abilitiesEnabled = compound.getBoolean("AbilitiesEnabled"); // Support old globalCooldown for backwards compatibility diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java index ee2561b4e..3760666b4 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.advanced; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -8,15 +9,20 @@ import noppes.npcs.client.gui.util.SubGuiInterface; /** * Confirmation dialog for deleting an ability preset. + * Uses packet-based communication for proper client-server architecture. */ public class SubGuiAbilityDeleteConfirm extends SubGuiInterface { private final String abilityName; - private final SubGuiAbilityLoad loadGui; + private final Runnable onDeleted; - public SubGuiAbilityDeleteConfirm(String abilityName, SubGuiAbilityLoad loadGui) { + public SubGuiAbilityDeleteConfirm(String abilityName) { + this(abilityName, null); + } + + public SubGuiAbilityDeleteConfirm(String abilityName, Runnable onDeleted) { this.abilityName = abilityName; - this.loadGui = loadGui; + this.onDeleted = onDeleted; setBackground("menubg.png"); xSize = 200; @@ -44,9 +50,11 @@ public class SubGuiAbilityDeleteConfirm extends SubGuiInterface { int id = guibutton.id; if (id == 0) { - // Delete the ability - AbilityController.Instance.deleteAbility(abilityName); - loadGui.onAbilityDeleted(abilityName); + // Delete the ability via packet + PacketClient.sendClient(new SavedAbilityRemovePacket(abilityName)); + if (onDeleted != null) { + onDeleted.run(); + } close(); } else if (id == 1) { close(); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java index c48683070..1e9133d9d 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -2,28 +2,39 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; import net.minecraft.client.gui.GuiButton; +import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.IScrollData; import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumScrollData; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.Vector; /** * Dialog for loading a saved ability preset. - * Can be opened from SubGuiAbilityConfig (to load into editing ability) - * or from GuiNPCAbilities (to add ability directly to NPC). + * Uses packet-based communication for proper client-server architecture. */ -public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener { +public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener, IScrollData, IGuiData { private final SubGuiAbilityConfig parentConfig; private final GuiNPCAbilities parentAbilities; private String selectedName = null; - private List abilityNames = new ArrayList<>(); + private HashMap abilityData = new HashMap<>(); + private GuiCustomScroll scroll; + + // Track if we're waiting for a load + private boolean waitingForLoad = false; public SubGuiAbilityLoad(SubGuiAbilityConfig parentConfig) { this.parentConfig = parentConfig; @@ -32,6 +43,9 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL setBackground("menubg.png"); xSize = 220; ySize = 180; + + // Request ability list from server + PacketClient.sendClient(new SavedAbilitiesGetPacket()); } public SubGuiAbilityLoad(GuiNPCAbilities parentAbilities) { @@ -41,28 +55,29 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL setBackground("menubg.png"); xSize = 220; ySize = 180; + + // Request ability list from server + PacketClient.sendClient(new SavedAbilitiesGetPacket()); } @Override public void initGui() { super.initGui(); - // Load ability names - Set names = AbilityController.Instance.getSavedAbilityNames(); - abilityNames = new ArrayList<>(names); - int y = guiTop + 5; addLabel(new GuiNpcLabel(0, "ability.load.select", guiLeft + 10, y)); y += 14; // Scroll list of saved abilities - GuiCustomScroll scroll = new GuiCustomScroll(this, 0); - scroll.setSize(200, 110); + if (scroll == null) { + scroll = new GuiCustomScroll(this, 0); + scroll.setSize(200, 110); + } scroll.guiLeft = guiLeft + 10; scroll.guiTop = y; - scroll.setUnsortedList(abilityNames); - if (selectedName != null && abilityNames.contains(selectedName)) { + scroll.setList(getFilteredList()); + if (selectedName != null && abilityData.containsKey(selectedName)) { scroll.setSelected(selectedName); } addScroll(scroll); @@ -70,13 +85,15 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL y += 115; // Buttons - addButton(new GuiNpcButton(0, guiLeft + 10, y, 60, 20, "gui.load")); - addButton(new GuiNpcButton(1, guiLeft + 80, y, 60, 20, "gui.delete")); - addButton(new GuiNpcButton(2, guiLeft + 150, y, 60, 20, "gui.cancel")); + addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.load")); + addButton(new GuiNpcButton(2, guiLeft + 115, y, 95, 20, "gui.cancel")); - // Disable load/delete if nothing selected + // Disable load if nothing selected getButton(0).setEnabled(selectedName != null && !selectedName.isEmpty()); - getButton(1).setEnabled(selectedName != null && !selectedName.isEmpty()); + } + + private List getFilteredList() { + return new ArrayList<>(abilityData.keySet()); } @Override @@ -84,19 +101,9 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL int id = guibutton.id; if (id == 0 && selectedName != null) { - // Load the selected ability - Ability loaded = AbilityController.Instance.getSavedAbility(selectedName); - if (loaded != null) { - if (parentConfig != null) { - parentConfig.loadAbility(loaded); - } else if (parentAbilities != null) { - parentAbilities.loadAbility(loaded); - } - } - close(); - } else if (id == 1 && selectedName != null) { - // Delete confirmation - setSubGui(new SubGuiAbilityDeleteConfirm(selectedName, this)); + // Request ability data from server + waitingForLoad = true; + PacketClient.sendClient(new SavedAbilityGetPacket(selectedName)); } else if (id == 2) { close(); } @@ -113,8 +120,34 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { if (scroll.id == 0 && selection != null && !selection.isEmpty()) { - // Double-click to load immediately - Ability loaded = AbilityController.Instance.getSavedAbility(selection); + // Double-click to load - request from server + selectedName = selection; + waitingForLoad = true; + PacketClient.sendClient(new SavedAbilityGetPacket(selection)); + } + } + + @Override + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.ABILITIES) { + String name = scroll != null ? scroll.getSelected() : null; + this.abilityData = data; + if (scroll != null) { + scroll.setList(getFilteredList()); + if (name != null && abilityData.containsKey(name)) { + scroll.setSelected(name); + } + } + initGui(); + } + } + + @Override + public void setGuiData(NBTTagCompound compound) { + // Received ability data from server + if (waitingForLoad) { + waitingForLoad = false; + Ability loaded = AbilityController.Instance.fromNBT(compound); if (loaded != null) { if (parentConfig != null) { parentConfig.loadAbility(loaded); @@ -126,13 +159,11 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL } } - /** - * Called after an ability is deleted to refresh the list. - */ - public void onAbilityDeleted(String name) { - if (name.equals(selectedName)) { - selectedName = null; + @Override + public void setSelected(String selected) { + this.selectedName = selected; + if (scroll != null) { + scroll.setSelected(selected); } - initGui(); } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java index 3be13f0be..760f3eb9b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java @@ -1,23 +1,30 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.SubGuiInterface; /** * Confirmation dialog for saving an ability preset. + * Uses packet-based communication for proper client-server architecture. */ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { private final Ability ability; - private final boolean exists; + private final IAbilityConfigCallback callback; public SubGuiAbilitySaveConfirm(Ability ability) { + this(ability, null); + } + + public SubGuiAbilitySaveConfirm(Ability ability, IAbilityConfigCallback callback) { this.ability = ability; - this.exists = AbilityController.Instance.hasSavedAbility(ability.getName()); + this.callback = callback; setBackground("menubg.png"); xSize = 200; @@ -30,15 +37,9 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { int y = guiTop + 10; - if (exists) { - addLabel(new GuiNpcLabel(0, "ability.save.overwrite", guiLeft + 10, y)); - y += 12; - addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y)); - } else { - addLabel(new GuiNpcLabel(0, "ability.save.confirm", guiLeft + 10, y)); - y += 12; - addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y)); - } + addLabel(new GuiNpcLabel(0, "ability.save.confirm", guiLeft + 10, y)); + y += 12; + addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y)); y += 30; @@ -51,8 +52,11 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { int id = guibutton.id; if (id == 0) { - // Save the ability - AbilityController.Instance.saveAbility(ability); + // Save the ability via packet + PacketClient.sendClient(new SavedAbilitySavePacket(ability.writeNBT())); + if (callback != null) { + callback.onAbilitySaved(ability); + } close(); } else if (id == 1) { close(); diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index 22d0f3103..cb5220d54 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -1,122 +1,266 @@ package noppes.npcs.client.gui.global; +import kamkeel.npcs.client.renderer.TelegraphRenderer; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.advanced.SubGuiAbilityDeleteConfirm; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.AbilityPreviewExecutor; +import noppes.npcs.client.gui.util.GuiAbilityInterface; import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiNPCInterface2; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.GuiTexturedButton; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.IScrollData; import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumScrollData; +import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.Vector; /** - * Global GUI for managing saved ability presets. - * Allows viewing, editing, and deleting saved abilities. + * Global GUI for managing saved ability presets with 3D preview. + * + * Features: + * - Left side: 3D preview of NPC with ability effects + * - Right side: Ability list with search + * - Play/Pause/Stop buttons to preview abilities */ -public class GuiNpcManageAbilities extends GuiNPCInterface2 implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback { +public class GuiNpcManageAbilities extends GuiAbilityInterface + implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback, + ITextfieldListener, GuiYesNoCallback, IScrollData, IGuiData { + // ==================== DATA ==================== private GuiCustomScroll scroll; - private String selectedName = null; - private List abilityNames = new ArrayList<>(); + private HashMap abilityData = new HashMap<>(); + private String selected = null; private String search = ""; + private Ability selectedAbility = null; + + // ==================== PREVIEW ==================== + private AbilityPreviewExecutor previewExecutor; + private long prevTick = 0; public GuiNpcManageAbilities(EntityNPCInterface npc) { super(npc); - loadAbilityNames(); - } - private void loadAbilityNames() { - Set names = AbilityController.Instance.getSavedAbilityNames(); - abilityNames = new ArrayList<>(names); + // Layout offset + this.xOffset = -148 + 110; + this.yOffset = -170 + 167; + + // Initialize preview executor + previewExecutor = new AbilityPreviewExecutor(); + previewExecutor.setParentGui(this); + + // Request ability data from server + PacketClient.sendClient(new SavedAbilitiesGetPacket()); + + // Enable animation data + AnimationData data = npc.display.animationData; + data.setEnabled(true); } @Override public void initGui() { super.initGui(); - int y = guiTop + 10; + // Edit button (above Remove) + addButton(new GuiNpcButton(100, guiLeft + 368, guiTop + 8, 45, 20, "gui.edit")); + getButton(100).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null); - // Title label - addLabel(new GuiNpcLabel(0, "ability.manager.title", guiLeft + 10, y)); - y += 20; + // Remove button + addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove")); + getButton(1).setEnabled(selected != null && !selected.isEmpty() && abilityData.containsKey(selected)); - // Scroll list of saved abilities + // Scroll list of saved abilities (right side) if (scroll == null) { scroll = new GuiCustomScroll(this, 0); + scroll.setSize(143, 185); } - scroll.setSize(xSize - 90, 160); - scroll.guiLeft = guiLeft + 10; - scroll.guiTop = y; - scroll.setUnsortedList(getFilteredList()); - if (selectedName != null && abilityNames.contains(selectedName)) { - scroll.setSelected(selectedName); - } + scroll.guiLeft = guiLeft + 220; + scroll.guiTop = guiTop + 4; addScroll(scroll); + // Update scroll list + scroll.setList(getSearchList()); + if (selected != null && abilityData.containsKey(selected)) { + scroll.setSelected(selected); + } + // Search bar - addTextField(new GuiNpcTextField(1, this, fontRendererObj, guiLeft + 10, y + 163, xSize - 90, 18, search)); + addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); - // Right side buttons - int btnX = guiLeft + xSize - 70; - addButton(new GuiNpcButton(1, btnX, y, 60, 20, "gui.edit")); - addButton(new GuiNpcButton(2, btnX, y + 25, 60, 20, "gui.delete")); - addButton(new GuiNpcButton(3, btnX, y + 60, 60, 20, "selectServer.refresh")); + // Show ability info only if one is selected + if (selectedAbility == null || selected == null) { + return; + } - // Enable/disable buttons based on selection - getButton(1).setEnabled(selectedName != null && !selectedName.isEmpty()); - getButton(2).setEnabled(selectedName != null && !selectedName.isEmpty()); + // ID/Name label (below search bar) + addLabel(new GuiNpcLabel(10, "ID: " + selectedAbility.getId(), guiLeft + 220, guiTop + 215)); + + // Play/Pause/Stop buttons (below model preview area) + String animTexture = "customnpcs:textures/gui/animation.png"; + int playButtonOffsetX = 60; + + boolean isPlaying = previewExecutor.isPlaying() && !previewExecutor.isPaused(); + boolean isPaused = previewExecutor.isPaused(); + boolean isActive = previewExecutor.isActive(); + + if (!isPlaying || isPaused) { + // Show Play button + String statusKey = isPaused ? "animation.paused" : "animation.stopped"; + addLabel(new GuiNpcLabel(90, statusKey, guiLeft + playButtonOffsetX, guiTop + 198)); + addButton(new GuiTexturedButton(91, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 11, 20, animTexture, 18, 71)); + } else { + // Show Pause button + addLabel(new GuiNpcLabel(90, "animation.playing", guiLeft + playButtonOffsetX, guiTop + 198)); + addButton(new GuiTexturedButton(92, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 14, 20, animTexture, 0, 71)); + } + if (isActive) { + // Show Stop button + addButton(new GuiTexturedButton(93, "", guiLeft + playButtonOffsetX + 90, guiTop + 192, 14, 20, animTexture, 33, 71)); + } } - private List getFilteredList() { + private List getSearchList() { if (search.isEmpty()) { - return new ArrayList<>(abilityNames); + return new ArrayList<>(abilityData.keySet()); } - List filtered = new ArrayList<>(); - for (String name : abilityNames) { + List list = new ArrayList<>(); + for (String name : abilityData.keySet()) { if (name.toLowerCase().contains(search.toLowerCase())) { - filtered.add(name); + list.add(name); } } - return filtered; + return list; } @Override - public void buttonEvent(GuiButton guibutton) { + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + // IMPORTANT: Set NPC rotation BEFORE ticking entities + // This ensures anchor point calculations use the correct facing direction + npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW; + npc.prevRotationYaw = npc.rotationYaw = NPC_FACING_YAW; + npc.prevRotationYawHead = npc.rotationYawHead = NPC_FACING_YAW; + + // Tick preview executor (entities will use the rotation set above) + tickPreview(); + + // Draw base (includes 3D preview via parent class) + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw status text + if (previewExecutor.isActive()) { + String status = previewExecutor.getStatusString(); + fontRendererObj.drawString(status, guiLeft + 12, guiTop + 175, 0xFFFFFF); + } + } + + /** + * Tick the preview executor and animations. + */ + private void tickPreview() { + if (previewExecutor.isPlaying() && !previewExecutor.isPaused()) { + long time = mc.theWorld != null ? mc.theWorld.getTotalWorldTime() : System.currentTimeMillis() / 50; + if (time != prevTick) { + // Advance animation + npc.display.animationData.increaseTime(); + + // Tick preview + previewExecutor.tick(); + + // Sync telegraph to GUI for rendering + setPreviewTelegraph(previewExecutor.getTelegraph()); + + prevTick = time; + + // Check if preview finished + if (!previewExecutor.isActive()) { + initGui(); + } + } + } + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + super.actionPerformed(guibutton); + int id = guibutton.id; - if (id == 1 && selectedName != null) { + if (id == 1 && selected != null && abilityData.containsKey(selected)) { + // Remove - show confirmation + GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1); + displayGuiScreen(guiyesno); + } else if (id == 100 && selectedAbility != null) { // Edit - Ability ability = AbilityController.Instance.getSavedAbility(selectedName); - if (ability != null) { - setSubGui(ability.createConfigGui(this)); + previewExecutor.stop(); + setSubGui(selectedAbility.createConfigGui(this)); + } else if (id == 91) { + // Play button + if (selectedAbility != null) { + if (previewExecutor.isPaused()) { + previewExecutor.play(); + } else { + previewExecutor.startPreview(selectedAbility, npc); + } + initGui(); } - } else if (id == 2 && selectedName != null) { - // Delete - setSubGui(new SubGuiAbilityDeleteConfirm(selectedName, null)); - } else if (id == 3) { - // Refresh - AbilityController.Instance.load(); - loadAbilityNames(); - selectedName = null; + } else if (id == 92) { + // Pause button + previewExecutor.pause(); initGui(); + } else if (id == 93) { + // Stop button + previewExecutor.stop(); + initGui(); + } + } + + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + if (getTextField(55) != null && getTextField(55).isFocused()) { + if (!search.equals(getTextField(55).getText())) { + search = getTextField(55).getText(); + scroll.resetScroll(); + scroll.setList(getSearchList()); + } } } @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { if (scroll.id == 0) { - selectedName = scroll.getSelected(); - initGui(); + String newSelection = scroll.getSelected(); + if (newSelection != null && !newSelection.equals(selected)) { + // Stop any playing preview + previewExecutor.stop(); + + selected = newSelection; + // Request ability data from server + PacketClient.sendClient(new SavedAbilityGetPacket(selected)); + } } } @@ -124,48 +268,90 @@ public class GuiNpcManageAbilities extends GuiNPCInterface2 implements ICustomSc public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { if (scroll.id == 0 && selection != null && !selection.isEmpty()) { // Double-click to edit - Ability ability = AbilityController.Instance.getSavedAbility(selection); - if (ability != null) { - setSubGui(ability.createConfigGui(this)); + if (selectedAbility != null) { + previewExecutor.stop(); + setSubGui(selectedAbility.createConfigGui(this)); } } } @Override - public void keyTyped(char c, int i) { - super.keyTyped(c, i); - if (getTextField(1) != null && getTextField(1).isFocused()) { - if (!search.equals(getTextField(1).getText())) { - search = getTextField(1).getText(); - scroll.setUnsortedList(getFilteredList()); - scroll.resetScroll(); + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.ABILITIES) { + String name = scroll != null ? scroll.getSelected() : null; + this.abilityData = data; + if (scroll != null) { + scroll.setList(getSearchList()); + if (name != null && abilityData.containsKey(name)) { + scroll.setSelected(name); + } } + initGui(); + } + } + + @Override + public void setGuiData(NBTTagCompound compound) { + // Received ability data from server + selectedAbility = AbilityController.Instance.fromNBT(compound); + if (selectedAbility != null) { + selected = selectedAbility.getName(); + if (scroll != null) { + scroll.setSelected(selected); + } + } + previewExecutor.stop(); + initGui(); + } + + @Override + public void setSelected(String selected) { + this.selected = selected; + if (scroll != null) { + scroll.setSelected(selected); } } @Override public void subGuiClosed(SubGuiInterface subgui) { - // Refresh list after any sub gui closes - loadAbilityNames(); - if (subgui instanceof SubGuiAbilityDeleteConfirm) { - // After delete, clear selection if it was deleted - if (selectedName != null && !abilityNames.contains(selectedName)) { - selectedName = null; - } - } initGui(); } + @Override + public void confirmClicked(boolean result, int id) { + NoppesUtil.openGUI(player, this); + if (!result) return; + + if (id == 1 && selected != null && abilityData.containsKey(selected)) { + // Delete ability via packet + PacketClient.sendClient(new SavedAbilityRemovePacket(selected)); + scroll.clear(); + selected = null; + selectedAbility = null; + initGui(); + } + } + @Override public void onAbilitySaved(Ability ability) { - // Save the edited ability - AbilityController.Instance.saveAbility(ability); - loadAbilityNames(); - initGui(); + // Save the edited ability via packet + PacketClient.sendClient(new SavedAbilitySavePacket(ability.writeNBT())); + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + // Search field handling is done in keyTyped } @Override public void save() { - // Nothing to save - abilities are saved individually + // Nothing to save - abilities are saved individually via packets + } + + @Override + public void onGuiClosed() { + super.onGuiClosed(); + // Stop preview when GUI closes + previewExecutor.stop(); } } 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 bca447b1f..b055cbf43 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java @@ -39,6 +39,7 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { private GuiNpcSquareButton effectsButton; private GuiNpcSquareButton magicsButton; private GuiNpcSquareButton abilitiesButton; + private GuiNpcSquareButton auctionsButton; public GuiNPCGlobalMainMenu(EntityNPCInterface npc) { super(npc, 5); @@ -96,7 +97,10 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { magicsButton.setIconPos(24, 24, 48, 56).setIconTexture(specialIcons); registerButton(abilitiesButton = new GuiNpcSquareButton(21, 0, 0, 20, "global.abilities", 0xFF333333)); - abilitiesButton.setIconPos(24, 24, 72, 56).setIconTexture(specialIcons); // Placeholder icon position + abilitiesButton.setIconPos(24, 24, 72, 56).setIconTexture(specialIcons); + + registerButton(auctionsButton = new GuiNpcSquareButton(22, 0, 0, 20, "global.auction", 0xFF333333)); + auctionsButton.setIconPos(24, 24, 96, 56).setIconTexture(specialIcons); // Layout composite buttons optimally. layoutButtons(); @@ -240,7 +244,10 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { NoppesUtil.requestOpenGUI(EnumGuiType.ManageEffects); } if (id == 21) { - NoppesUtil.openGUI(player, new noppes.npcs.client.gui.global.GuiNpcManageAbilities(npc)); + NoppesUtil.requestOpenGUI(EnumGuiType.ManageAbilities); + } + if (id == 22) { + // NoppesUtil.requestOpenGUI(EnumGuiType.ManageAuction); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java new file mode 100644 index 000000000..3dce761d5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java @@ -0,0 +1,368 @@ +package noppes.npcs.client.gui.util; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityProjectile; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.world.World; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.AnimationData; +import noppes.npcs.entity.EntityCustomNpc; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Client-side executor for previewing abilities in the GUI. + * + * Lifecycle: + * 1. WINDUP phase - plays windup animation, entities spawn and charge + * 2. ACTIVE phase - plays active animation, entities fire and move + * 3. EXTENDED phase - continues for +100 ticks after animations to see results + * 4. IDLE - preview complete + * + * Features: + * - Invisible fake target NPC placed in front of the preview NPC for targeting + * - NPC movement tracking for abilities that move the NPC (e.g. slam) + */ +@SideOnly(Side.CLIENT) +public class AbilityPreviewExecutor { + + // ==================== PREVIEW STATE ==================== + private Ability previewAbility; + private EntityNPCInterface previewNpc; + private AbilityPhase phase = AbilityPhase.IDLE; + private int currentTick = 0; + private boolean playing = false; + private boolean paused = false; + + // ==================== TIMING ==================== + private static final int EXTENDED_DURATION = 100; + private int activeDuration = 40; + + // ==================== FAKE TARGET ==================== + /** Invisible fake target NPC for ability targeting (placed 5 blocks in front of preview NPC) */ + private EntityNPCInterface fakeTarget; + private static final double TARGET_DISTANCE = 5.0; + + // ==================== NPC START POSITION ==================== + /** Saved NPC position for restoring after movement abilities */ + private double npcStartX, npcStartY, npcStartZ; + + // ==================== PREVIEW CONTENT ==================== + private TelegraphInstance previewTelegraph; + private List previewEntities = new ArrayList<>(); + + // ==================== CALLBACKS ==================== + private GuiAbilityInterface parentGui; + + public AbilityPreviewExecutor() { + } + + public void setParentGui(GuiAbilityInterface gui) { + this.parentGui = gui; + } + + /** + * Start previewing an ability. + */ + public void startPreview(Ability ability, EntityNPCInterface npc) { + stop(); + + this.previewAbility = ability; + this.previewNpc = npc; + this.phase = AbilityPhase.WINDUP; + this.currentTick = 0; + this.playing = true; + this.paused = false; + this.activeDuration = ability.getPreviewActiveDuration(); + + // Save NPC start position for restoring later + this.npcStartX = npc.posX; + this.npcStartY = npc.posY; + this.npcStartZ = npc.posZ; + + // Create invisible fake target NPC for targeting + createFakeTarget(npc); + + // Set preview target on the ability + ability.setPreviewTarget(fakeTarget); + + // Start tracking NPC movement + if (parentGui != null) { + parentGui.startTrackingMovement(); + } + + // Create telegraph for preview + previewTelegraph = ability.createTelegraph(npc, fakeTarget); + + // Start windup animation + Animation windUpAnim = ability.getWindUpAnimation(); + if (windUpAnim != null) { + AnimationData data = npc.display.animationData; + data.setEnabled(true); + data.setAnimation(windUpAnim); + data.animation.paused = false; + } + + // Spawn preview entity on first tick if ability spawns during windup + if (ability.spawnPreviewDuringWindup()) { + spawnPreviewEntity(); + } + } + + /** + * Create an invisible fake target NPC placed in front of the preview NPC. + * This entity is NOT rendered - it only exists for ability targeting logic. + */ + private void createFakeTarget(EntityNPCInterface npc) { + if (npc.worldObj == null) return; + + try { + fakeTarget = new EntityCustomNpc(npc.worldObj); + // Position target in front of NPC based on its facing direction + float facingYaw = GuiAbilityInterface.NPC_FACING_YAW; + double yawRad = Math.toRadians(facingYaw); + double targetX = npc.posX - Math.sin(yawRad) * TARGET_DISTANCE; + double targetY = npc.posY; + double targetZ = npc.posZ + Math.cos(yawRad) * TARGET_DISTANCE; + fakeTarget.setPosition(targetX, targetY, targetZ); + fakeTarget.prevPosX = targetX; + fakeTarget.prevPosY = targetY; + fakeTarget.prevPosZ = targetZ; + } catch (Exception e) { + fakeTarget = null; + } + } + + /** + * Get the fake target entity for abilities that need a target. + */ + public EntityLivingBase getFakeTarget() { + return fakeTarget; + } + + public void play() { + if (previewAbility == null || phase == AbilityPhase.IDLE) { + return; + } + playing = true; + paused = false; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + if (data.animation != null) { + data.animation.paused = false; + } + } + } + + public void pause() { + paused = true; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + if (data.animation != null) { + data.animation.paused = true; + } + } + } + + public void stop() { + playing = false; + paused = false; + phase = AbilityPhase.IDLE; + currentTick = 0; + + previewTelegraph = null; + + if (parentGui != null) { + for (Entity entity : previewEntities) { + parentGui.removePreviewEntity(entity); + } + parentGui.stopTrackingMovement(); + parentGui.setPreviewTelegraph(null); + } + previewEntities.clear(); + + // Reset NPC position to start position (undo movement from Slam/Charge/Dash) + if (previewNpc != null && npcStartX != 0 && npcStartY != 0 && npcStartZ != 0) { + previewNpc.posX = npcStartX; + previewNpc.posY = npcStartY; + previewNpc.posZ = npcStartZ; + previewNpc.prevPosX = npcStartX; + previewNpc.prevPosY = npcStartY; + previewNpc.prevPosZ = npcStartZ; + } + + // Remove fake target + fakeTarget = null; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + data.setAnimation(new Animation()); + data.animation.paused = false; + } + } + + /** + * Tick the preview forward. + */ + public void tick() { + if (!playing || paused || previewAbility == null || previewNpc == null) { + return; + } + + currentTick++; + + if (previewTelegraph != null) { + previewTelegraph.tick(null); + } + + tickEntities(); + + switch (phase) { + case WINDUP: + tickWindup(); + break; + case ACTIVE: + tickActive(); + break; + case IDLE: + tickExtended(); + break; + default: + break; + } + } + + private void tickWindup() { + previewAbility.onPreviewWindUpTick(previewNpc, currentTick); + + if (currentTick >= previewAbility.getWindUpTicks()) { + transitionToActive(); + } + } + + private void tickActive() { + previewAbility.onPreviewActiveTick(previewNpc, currentTick); + + if (currentTick == 1 && !previewAbility.spawnPreviewDuringWindup()) { + spawnPreviewEntity(); + fireEntities(); + } + + if (currentTick >= activeDuration) { + transitionToExtended(); + } + } + + private void tickExtended() { + if (currentTick >= EXTENDED_DURATION) { + playing = false; + } + } + + private void transitionToActive() { + phase = AbilityPhase.ACTIVE; + currentTick = 0; + + previewTelegraph = null; + + // Notify ability of execution (for movement abilities like Slam/Charge/Dash) + previewAbility.onPreviewExecute(previewNpc); + + fireEntities(); + + Animation activeAnim = previewAbility.getActiveAnimation(); + if (activeAnim != null && previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + data.setAnimation(activeAnim); + data.animation.paused = false; + } + } + + private void transitionToExtended() { + phase = AbilityPhase.IDLE; + currentTick = 0; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + data.setAnimation(new Animation()); + } + } + + private void spawnPreviewEntity() { + Entity entity = previewAbility.createPreviewEntity(previewNpc); + if (entity != null) { + previewEntities.add(entity); + if (parentGui != null) { + parentGui.addPreviewEntity(entity); + } + } + } + + private void fireEntities() { + for (Entity entity : previewEntities) { + if (entity instanceof EntityAbilityOrb) { + ((EntityAbilityOrb) entity).startPreviewFiring(); + } else if (entity instanceof EntityAbilityBeam) { + ((EntityAbilityBeam) entity).startPreviewFiring(); + } else if (entity instanceof EntityAbilityDisc) { + ((EntityAbilityDisc) entity).startPreviewFiring(); + } + } + } + + private void tickEntities() { + Iterator iter = previewEntities.iterator(); + while (iter.hasNext()) { + Entity entity = iter.next(); + if (entity == null || entity.isDead) { + if (parentGui != null) { + parentGui.removePreviewEntity(entity); + } + iter.remove(); + continue; + } + entity.onUpdate(); + } + } + + // ==================== GETTERS ==================== + + public boolean isPlaying() { return playing; } + public boolean isPaused() { return paused; } + public boolean isActive() { return playing || paused; } + public AbilityPhase getPhase() { return phase; } + public int getCurrentTick() { return currentTick; } + public TelegraphInstance getTelegraph() { return previewTelegraph; } + public List getPreviewEntities() { return previewEntities; } + public Ability getPreviewAbility() { return previewAbility; } + public EntityNPCInterface getPreviewNpc() { return previewNpc; } + + public String getStatusString() { + if (!playing && !paused) { + return "Stopped"; + } else if (paused) { + return "Paused"; + } else if (phase == AbilityPhase.WINDUP) { + return "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0); + } else if (phase == AbilityPhase.ACTIVE) { + return "Active: " + currentTick + "/" + activeDuration; + } else if (phase == AbilityPhase.IDLE && playing) { + return "Extended: " + currentTick + "/" + EXTENDED_DURATION; + } + return ""; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java new file mode 100644 index 000000000..c9c44ff93 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java @@ -0,0 +1,391 @@ +package noppes.npcs.client.gui.util; + +import kamkeel.npcs.client.renderer.TelegraphRenderer; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.client.ClientEventHandler; +import noppes.npcs.client.EntityUtil; +import noppes.npcs.entity.EntityCustomNpc; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.entity.data.ModelData; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base GUI class for ability preview with fixed camera. + * + * Key concepts: + * - NPC positioned on left side of preview area + * - NPC faces a fixed direction (NPC_FACING_YAW) + * - Viewing direction can be rotated (rotates entire 3D space) + * - Fixed zoom level + * - Preview entities rendered in same coordinate space as NPC + * - NPC position changes (e.g. slam jump) tracked and rendered + */ +public class GuiAbilityInterface extends GuiNPCInterface2 { + + // ==================== FIXED CAMERA SETTINGS ==================== + /** Fixed camera zoom level (higher = more zoomed in) */ + protected static float CAMERA_ZOOM = 45.0f; + /** NPC facing direction - affects anchor points and visual facing */ + protected static float NPC_FACING_YAW = 160.0f; + /** Camera pitch (slight downward angle) */ + protected static float CAMERA_PITCH = 10.0f; + + // ==================== VIEWING ROTATION ==================== + /** Viewing direction rotation (rotates entire 3D space) */ + protected static float viewRotation = 0.0f; + + private GuiNpcButton btnLeft, btnRight; + + // ==================== NPC STATE ==================== + public ModelData playerdata; + public EntityNPCInterface npc; + + /** NPC starting position (saved when preview starts, used to track movement) */ + protected double npcStartX, npcStartY, npcStartZ; + /** Whether we're tracking NPC movement */ + protected boolean trackingNpcMovement = false; + + /** Entities to render in preview space */ + protected List previewEntities = new ArrayList<>(); + + /** Current telegraph to render in preview */ + protected TelegraphInstance previewTelegraph; + + // ==================== LAYOUT ==================== + /** Preview area bounds */ + protected int previewX, previewY, previewWidth, previewHeight; + /** NPC render position on screen */ + protected int npcScreenX, npcScreenY; + + public int xOffset = 0; + public int yOffset = 0; + + public GuiAbilityInterface(EntityNPCInterface npc) { + super(npc); + this.npc = npc; + this.playerdata = ((EntityCustomNpc) npc).modelData; + this.drawDefaultBackground = false; + + CAMERA_ZOOM = 30f; + NPC_FACING_YAW = 310f; + CAMERA_PITCH = 5f; + } + + @Override + public void initGui() { + super.initGui(); + + // Calculate preview area (left side of GUI) + previewX = guiLeft + 10; + previewY = guiTop + 10; + previewWidth = 200; + previewHeight = 180; + + // NPC position: left side of preview, slightly below center + npcScreenX = previewX + (int)(previewWidth * 0.33f) + xOffset; + npcScreenY = previewY + (int)(previewHeight * 0.85f) + yOffset; + + // Viewing rotation buttons (below preview area) + int btnY = guiTop + 192; + addButton(btnLeft = new GuiNpcButton(680, guiLeft + 10, btnY, 20, 20, "<")); + addButton(btnRight = new GuiNpcButton(681, guiLeft + 32, btnY, 20, 20, ">")); + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + // Handle held rotation buttons + if (Mouse.isButtonDown(0)) { + if (btnLeft != null && btnLeft.mousePressed(mc, mouseX, mouseY)) { + viewRotation += partialTicks * 2; + } else if (btnRight != null && btnRight.mousePressed(mc, mouseX, mouseY)) { + viewRotation -= partialTicks * 2; + } + } + + // Draw background and UI elements + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw the 3D preview + drawPreview(mouseX, mouseY, partialTicks); + } + + /** + * Draw the 3D preview with NPC and entities. + */ + protected void drawPreview(int mouseX, int mouseY, float partialTicks) { + GL11.glColor4f(1, 1, 1, 1); + + // Enable scissor to clip rendering to preview area + GL11.glEnable(GL11.GL_SCISSOR_TEST); + setScissorClip(previewX, previewY, previewWidth, previewHeight); + + // Get the entity to render (could be transformed player model) + EntityLivingBase renderEntity = playerdata.getEntity(npc); + if (renderEntity == null) { + renderEntity = this.npc; + } + + // Sync NPC properties to render entity + EntityUtil.Copy(npc, renderEntity); + + // Set NPC facing direction (affects anchor points and visual facing) + npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW; + npc.prevRotationYaw = npc.rotationYaw = NPC_FACING_YAW; + npc.prevRotationYawHead = npc.rotationYawHead = NPC_FACING_YAW; + + // Also set on render entity + renderEntity.prevRenderYawOffset = renderEntity.renderYawOffset = NPC_FACING_YAW; + renderEntity.prevRotationYaw = renderEntity.rotationYaw = NPC_FACING_YAW; + renderEntity.prevRotationYawHead = renderEntity.rotationYawHead = NPC_FACING_YAW; + renderEntity.rotationPitch = 0; + + // Calculate NPC position delta for movement rendering (e.g. slam jump) + // Interpolate with partial ticks for smooth movement between frames + double npcDeltaX = 0, npcDeltaY = 0, npcDeltaZ = 0; + if (trackingNpcMovement) { + double interpX = npc.prevPosX + (npc.posX - npc.prevPosX) * partialTicks; + double interpY = npc.prevPosY + (npc.posY - npc.prevPosY) * partialTicks; + double interpZ = npc.prevPosZ + (npc.posZ - npc.prevPosZ) * partialTicks; + npcDeltaX = interpX - npcStartX; + npcDeltaY = interpY - npcStartY; + npcDeltaZ = interpZ - npcStartZ; + } + + // ==================== BEGIN 3D RENDERING ==================== + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glPushMatrix(); + + // Move to NPC render position on screen + GL11.glTranslatef(npcScreenX, npcScreenY, 50F); + + // Apply zoom (scale) + GL11.glScalef(-CAMERA_ZOOM, CAMERA_ZOOM, CAMERA_ZOOM); + + // Flip for screen coordinates (Y is inverted) + GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F); + + // Apply slight camera pitch (looking down at scene) + GL11.glRotatef(CAMERA_PITCH, 1.0F, 0.0F, 0.0F); + + // Apply VIEWING rotation (rotates entire space, not just NPC) + GL11.glRotatef(viewRotation, 0.0F, 1.0F, 0.0F); + + // Setup lighting (standard item lighting for consistent colors) + GL11.glRotatef(135F, 0.0F, 1.0F, 0.0F); + RenderHelper.enableStandardItemLighting(); + GL11.glRotatef(-135F, 0.0F, 1.0F, 0.0F); + + // Adjust for entity offset + GL11.glTranslatef(0.0F, renderEntity.yOffset, 0.0F); + + RenderManager.instance.playerViewY = 180F; + ClientEventHandler.renderingEntityInGUI = true; + + // Render the NPC at its position delta (for movement like slam jumps) + try { + RenderManager.instance.renderEntityWithPosYaw(renderEntity, npcDeltaX, npcDeltaY, npcDeltaZ, 0.0F, partialTicks); + } catch (Exception e) { + playerdata.setEntityClass(null); + } + + // Render preview entities at their positions relative to NPC start position + renderPreviewEntities(partialTicks); + + // Render telegraph in preview space + renderPreviewTelegraph(partialTicks); + + ClientEventHandler.renderingEntityInGUI = false; + + GL11.glPopMatrix(); + + // Cleanup GL state + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL12.GL_RESCALE_NORMAL); + OpenGlHelper.setActiveTexture(OpenGlHelper.lightmapTexUnit); + GL11.glDisable(GL11.GL_TEXTURE_2D); + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit); + + // Disable scissor + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + // Clear depth buffer so UI renders on top + GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT); + } + + /** + * Render preview entities in the same coordinate space as the NPC. + * Called WITHIN the 3D matrix transformation. + */ + protected void renderPreviewEntities(float partialTicks) { + // Save current GL state that NPC render may have changed + boolean lightingWasEnabled = GL11.glIsEnabled(GL11.GL_LIGHTING); + + // Use the start position as the reference point for entity offsets + double refX = trackingNpcMovement ? npcStartX : npc.posX; + double refY = trackingNpcMovement ? npcStartY : npc.posY; + double refZ = trackingNpcMovement ? npcStartZ : npc.posZ; + + for (Entity entity : previewEntities) { + if (entity == null || entity.isDead) continue; + + // Calculate entity position relative to reference (NPC start position) + double offsetX = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks - refX; + double offsetY = entity.prevPosY + (entity.posY - entity.prevPosY) * partialTicks - refY; + double offsetZ = entity.prevPosZ + (entity.posZ - entity.prevPosZ) * partialTicks - refZ; + + // The NPC model was translated by npc.yOffset, so we need to subtract it + offsetY -= npc.yOffset; + + // Reset GL state before each entity to ensure colors render correctly + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + GL11.glDisable(GL11.GL_LIGHTING); + + try { + RenderManager.instance.renderEntityWithPosYaw(entity, offsetX, offsetY, offsetZ, entity.rotationYaw, partialTicks); + } catch (Exception e) { + // Ignore rendering errors + } + } + + // Restore lighting state for any subsequent rendering + if (lightingWasEnabled) { + GL11.glEnable(GL11.GL_LIGHTING); + } + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + } + + /** + * Render the telegraph in the 3D preview space. + * Called within the GL matrix transformation. + */ + protected void renderPreviewTelegraph(float partialTicks) { + if (previewTelegraph == null) return; + if (TelegraphRenderer.Instance == null) return; + + // Telegraph position is in world coords - convert to preview space + double refX = trackingNpcMovement ? npcStartX : npc.posX; + double refY = trackingNpcMovement ? npcStartY : npc.posY; + double refZ = trackingNpcMovement ? npcStartZ : npc.posZ; + + double offsetX = previewTelegraph.getInterpolatedX(partialTicks) - refX; + double offsetY = previewTelegraph.getInterpolatedY(partialTicks) - refY; + double offsetZ = previewTelegraph.getInterpolatedZ(partialTicks) - refZ; + + // Adjust for NPC yOffset (same as entities) + offsetY -= npc.yOffset; + + TelegraphRenderer.Instance.renderTelegraphInGUI(previewTelegraph, offsetX, offsetY, offsetZ, 1.0f, partialTicks); + } + + public void setPreviewTelegraph(TelegraphInstance telegraph) { + this.previewTelegraph = telegraph; + } + + /** + * Set scissor clip for preview area. + */ + protected void setScissorClip(int x, int y, int width, int height) { + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scale = sr.getScaleFactor(); + int scaledY = mc.displayHeight - (y + height) * scale; + GL11.glScissor(x * scale, scaledY, width * scale, height * scale); + } + + // ==================== MOVEMENT TRACKING ==================== + + /** + * Start tracking NPC movement from current position. + */ + public void startTrackingMovement() { + npcStartX = npc.posX; + npcStartY = npc.posY; + npcStartZ = npc.posZ; + trackingNpcMovement = true; + } + + /** + * Stop tracking NPC movement. + */ + public void stopTrackingMovement() { + trackingNpcMovement = false; + } + + // ==================== PREVIEW ENTITY MANAGEMENT ==================== + + public void addPreviewEntity(Entity entity) { + if (entity != null && !previewEntities.contains(entity)) { + previewEntities.add(entity); + } + } + + public void removePreviewEntity(Entity entity) { + previewEntities.remove(entity); + } + + public void clearPreviewEntities() { + previewEntities.clear(); + } + + public List getPreviewEntities() { + return previewEntities; + } + + public float getNpcFacingYaw() { + return NPC_FACING_YAW; + } + + // ==================== BACKGROUND ==================== + + @Override + protected void drawBackground() { + super.drawBackground(); + + drawGradientRect(previewX, previewY, previewX + previewWidth, previewY + previewHeight, + 0xc0101010, 0xd0101010); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + } + + // ==================== STANDARD OVERRIDES ==================== + + @Override + public boolean doesGuiPauseGame() { + return false; + } + + @Override + public void keyTyped(char par1, int par2) { + super.keyTyped(par1, par2); + if (par2 == 1) { + close(); + } + } + + public void close() { + this.mc.displayGuiScreen((GuiScreen) null); + this.mc.setIngameFocus(); + } + + @Override + public void save() { + } + + @Override + protected void actionPerformed(GuiButton btn) { + if (btn.id == 670) { + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java b/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java index 8a1d015d0..cef818647 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java @@ -18,11 +18,11 @@ import org.lwjgl.opengl.GL12; public class GuiModelInterface2 extends GuiNPCInterface2 { public ModelData playerdata; - private static float rotation = 0; + protected static float rotation = 0; private GuiNpcButton left, right, zoom, unzoom; - private static float zoomed = 60; + protected static float zoomed = 60; public int xOffset = 0; public int yOffset = 0; diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index 235f2719b..baf994b4e 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -7,6 +7,7 @@ public enum EnumScrollData { DIALOG_GROUP, QUEST_GROUP, ABILITY_TYPES, + ABILITIES, ANIMATIONS, BUILTIN_ANIMATIONS } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 50c150624..fef02e070 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -689,6 +689,8 @@ global.naturalspawn=Spawns global.linked=Linked global.customeffects=Effects global.magic=Magic +global.abilities=Abilities +global.auction=Auction bank.slot=Slot bank.unlock=Unlock @@ -1714,7 +1716,6 @@ ability.delete.confirm=Delete ability preset gui.load=Load # Global Abilities Manager -global.abilities=Abilities ability.manager.title=Ability Manager # Explosive Hover Text diff --git a/src/main/resources/assets/customnpcs/textures/gui/icons.png b/src/main/resources/assets/customnpcs/textures/gui/icons.png index 86bf094c4..6f2993cd1 100644 Binary files a/src/main/resources/assets/customnpcs/textures/gui/icons.png and b/src/main/resources/assets/customnpcs/textures/gui/icons.png differ